From 9ff0bf510648f6a51df93c99afd72abcc3a32134 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Tue, 8 Sep 2026 10:04:56 +0200 Subject: [PATCH] FIX: Stop the admin wizard editor crashing on multiple mapper inputs WizardMapper renders the connector between its inputs without passing connectors, as that connector is a label rather than a combo box. The GJS migration replaced the null safe gt("connectors.length", 1) macro in WizardMapperConnector with this.connectors.length > 1, so rendering a mapper with more than one input threw: TypeError: Cannot read properties of undefined (reading 'length') That is an unrecoverable Ember render error, so it took the whole admin wizard editor down with it: only the first condition on a step rendered, the add buttons for conditions, required data and permitted params did nothing, the form went blank after saving, and switching steps no longer updated the fields shown. Meta: https://meta.discourse.org/t/custom-wizard-plugin/73345/991 --- .../components/wizard-mapper-connector.gjs | 2 +- plugin.rb | 2 +- .../acceptance/admin-wizards-mapper-test.js | 79 ++++++++++++++++++- .../components/wizard-mapper-test.gjs | 76 ++++++++++++++++++ 4 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 test/javascripts/integration/components/wizard-mapper-test.gjs diff --git a/assets/javascripts/discourse/components/wizard-mapper-connector.gjs b/assets/javascripts/discourse/components/wizard-mapper-connector.gjs index 876162277..dd7c22405 100644 --- a/assets/javascripts/discourse/components/wizard-mapper-connector.gjs +++ b/assets/javascripts/discourse/components/wizard-mapper-connector.gjs @@ -11,7 +11,7 @@ import { defaultConnector } from "../lib/wizard-mapper"; export default class WizardMapperConnector extends Component { @computed("connectors.length") get hasMultiple() { - return this.connectors.length > 1; + return this.connectors?.length > 1; } @computed("connector", "inputTypes") diff --git a/plugin.rb b/plugin.rb index 6520d9d6c..f82b24fa9 100644 --- a/plugin.rb +++ b/plugin.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # name: discourse-custom-wizard # about: Forms for Discourse. Better onboarding, structured posting, data enrichment, automated actions and much more. -# version: 2.18.3 +# version: 2.18.4 # authors: Angus McLeod, Faizaan Gagan, Robert Barrow, Keegan George, Kaitlin Maddever, Marcos Gutierrez # url: https://github.com/paviliondev/discourse-custom-wizard # contact_emails: development@pavilion.tech diff --git a/test/javascripts/acceptance/admin-wizards-mapper-test.js b/test/javascripts/acceptance/admin-wizards-mapper-test.js index 25672598f..447bd3fac 100644 --- a/test/javascripts/acceptance/admin-wizards-mapper-test.js +++ b/test/javascripts/acceptance/admin-wizards-mapper-test.js @@ -1,4 +1,4 @@ -import { click, visit } from "@ember/test-helpers"; +import { click, fillIn, findAll, visit } from "@ember/test-helpers"; import { test } from "qunit"; import { acceptance } from "discourse/tests/helpers/qunit-helpers"; import selectKit from "discourse/tests/helpers/select-kit-helper"; @@ -10,8 +10,27 @@ import { const VISIBLE_ACTION = ".wizard-custom-action.visible"; const USERNAMES_SETTING = `${VISIBLE_ACTION} .field-mapper-setting:last-child`; +const STEP_SETTINGS = ".wizard-custom-step > .field-mapper-setting"; +const CONDITION = 0; +const REQUIRED_DATA = 1; const [firstTag, secondTag] = tagsJson.tags; +function validation(value) { + return { + type: "validation", + pairs: [ + { + index: 0, + key: "user_field_1", + key_type: "user_field", + value, + value_type: "text", + connector: "equal", + }, + ], + }; +} + function assignment(output, outputType) { return [ { @@ -32,6 +51,7 @@ const mappedWizard = { { id: "step_1", title: "step 1", + condition: [validation("one"), { ...validation("two"), connector: "or" }], fields: [{ id: "step_1_field_1", label: "label field", type: "text" }], }, ], @@ -121,6 +141,10 @@ acceptance("Admin | Custom Wizard | Mapped settings", function (needs) { await click(`.wizard-links.action .link-list [data-id="${id}"] button`); } + function stepSetting(index) { + return findAll(STEP_SETTINGS)[index]; + } + test("keeps mapped settings when a wizard is saved without changes", async function (assert) { await visit("/admin/wizards/wizard/mapped_wizard"); await click(".admin-wizard-buttons button"); @@ -223,4 +247,57 @@ acceptance("Admin | Custom Wizard | Mapped settings", function (needs) { .dom(`${USERNAMES_SETTING} .mapper-input`) .exists({ count: 1 }, "the added input renders"); }); + + test("keeps every input of a step mapper when a wizard is saved", async function (assert) { + await visit("/admin/wizards/wizard/mapped_wizard"); + + assert + .dom(".mapper-input", stepSetting(CONDITION)) + .exists({ count: 2 }, "both saved step conditions render"); + + await click(".admin-wizard-buttons button"); + + assert.deepEqual( + savedWizard.steps[0].condition, + mappedWizard.steps[0].condition, + "the step conditions are unchanged" + ); + }); + + test("saves an input added to a step mapper", async function (assert) { + await visit("/admin/wizards/wizard/mapped_wizard"); + + await click( + stepSetting(REQUIRED_DATA).querySelector(".add-mapper-input button") + ); + await fillIn( + stepSetting(REQUIRED_DATA).querySelector(".key .input input"), + "submission_key" + ); + await fillIn( + stepSetting(REQUIRED_DATA).querySelector(".value .input input"), + "field_key" + ); + await click(".admin-wizard-buttons button"); + + assert.deepEqual( + savedWizard.steps[0].required_data, + [ + { + type: "validation", + pairs: [ + { + index: 0, + key: "submission_key", + key_type: "text", + value: "field_key", + value_type: "text", + connector: "equal", + }, + ], + }, + ], + "the added required data is saved" + ); + }); }); diff --git a/test/javascripts/integration/components/wizard-mapper-test.gjs b/test/javascripts/integration/components/wizard-mapper-test.gjs new file mode 100644 index 000000000..517b4db9e --- /dev/null +++ b/test/javascripts/integration/components/wizard-mapper-test.gjs @@ -0,0 +1,76 @@ +import { click, render } from "@ember/test-helpers"; +import { module, test } from "qunit"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import WizardMapper from "discourse/plugins/discourse-custom-wizard/discourse/components/wizard-mapper"; +import CustomWizardAdmin from "discourse/plugins/discourse-custom-wizard/discourse/models/custom-wizard-admin"; + +const options = { + inputTypes: "validation", + inputConnector: "or", + textSelection: true, +}; + +function condition(value, props = {}) { + return { + type: "validation", + pairs: [ + { + index: 0, + key: "Saved key", + key_type: "text", + value, + value_type: "text", + connector: "equal", + }, + ], + ...props, + }; +} + +function stepCondition(...conditions) { + const wizard = CustomWizardAdmin.create({ + id: "test_wizard", + steps: [{ id: "step_1", condition: conditions }], + }); + + return wizard.steps[0].condition; +} + +async function renderMapper(inputs) { + await render( + + ); +} + +module("Integration | Component | wizard-mapper", function (hooks) { + setupRenderingTest(hooks); + + test("renders every saved input and the connector between them", async function (assert) { + await renderMapper( + stepCondition(condition("one"), condition("two", { connector: "or" })) + ); + + assert + .dom(".mapper-input") + .exists({ count: 2 }, "both saved inputs render"); + assert + .dom(".wizard-mapper > .mapper-connector") + .hasText("or", "the connector between the inputs renders"); + }); + + test("connects an added input to the existing ones", async function (assert) { + const inputs = stepCondition(condition("one")); + + await renderMapper(inputs); + await click(".add-mapper-input button"); + + assert.dom(".mapper-input").exists({ count: 2 }, "the added input renders"); + assert.strictEqual( + inputs[1].connector, + "or", + "the added input is connected to the first" + ); + }); +});