From 2228f75645d849a460e9e727c7533211e293ba73 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Thu, 23 Nov 2023 08:35:51 -0800 Subject: [PATCH] DEV: Modernize Wizard model implementation (#23640) + native classes + tracked properties - Ember.Object - Ember.Evented - observers - mixins - computed/discourseComputed Also removes unused wizard infrastructure for warnings. It appears that once upon on time, either the server can generate warnings, or some client code can generate them, which requires an extra confirmation from the user before they can continue to the next step. This code is not tested and appears unused and defunct. Nothing generates such warning and the server does not serialize them. Extracted from https://github.com/discourse/discourse/pull/23678 --- .../tests/unit/models/wizard-field-test.js | 22 +- .../addon/components/homepage-preview.js | 40 ++- .../components/image-preview-logo-small.js | 16 +- .../addon/components/image-preview-logo.js | 16 +- .../addon/components/styling-preview.js | 10 +- .../addon/components/wizard-field-dropdown.js | 4 - .../addon/components/wizard-field-image.js | 2 +- .../wizard/addon/components/wizard-field.hbs | 4 +- .../addon/components/wizard-preview-base.js | 35 +- .../wizard/addon/components/wizard-step.hbs | 4 +- .../wizard/addon/components/wizard-step.js | 29 +- .../wizard/addon/controllers/wizard-step.js | 2 +- .../wizard/addon/mixins/valid-state.js | 36 --- .../javascripts/wizard/addon/models/step.js | 59 ---- .../wizard/addon/models/wizard-field.js | 23 -- .../javascripts/wizard/addon/models/wizard.js | 305 +++++++++++++++--- .../javascripts/wizard/addon/routes/wizard.js | 4 +- 17 files changed, 357 insertions(+), 254 deletions(-) delete mode 100644 app/assets/javascripts/wizard/addon/mixins/valid-state.js delete mode 100644 app/assets/javascripts/wizard/addon/models/step.js delete mode 100644 app/assets/javascripts/wizard/addon/models/wizard-field.js diff --git a/app/assets/javascripts/discourse/tests/unit/models/wizard-field-test.js b/app/assets/javascripts/discourse/tests/unit/models/wizard-field-test.js index b553964ec87..f510d4c39f9 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/wizard-field-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/wizard-field-test.js @@ -1,44 +1,38 @@ -import { getOwner } from "@ember/application"; import { setupTest } from "ember-qunit"; import { module, test } from "qunit"; +import { Field } from "wizard/models/wizard"; module("Unit | Model | Wizard | wizard-field", function (hooks) { setupTest(hooks); test("basic state", function (assert) { - const store = getOwner(this).lookup("service:store"); - const field = store.createRecord("wizard-field", { type: "text" }); + const field = new Field({ type: "text" }); assert.ok(field.unchecked); assert.ok(!field.valid); assert.ok(!field.invalid); }); test("text - required - validation", function (assert) { - const store = getOwner(this).lookup("service:store"); - const field = store.createRecord("wizard-field", { - type: "text", - required: true, - }); + const field = new Field({ type: "text", required: true }); assert.ok(field.unchecked); - field.check(); + field.validate(); assert.ok(!field.unchecked); assert.ok(!field.valid); assert.ok(field.invalid); - field.set("value", "a value"); - field.check(); + field.value = "a value"; + field.validate(); assert.ok(!field.unchecked); assert.ok(field.valid); assert.ok(!field.invalid); }); test("text - optional - validation", function (assert) { - const store = getOwner(this).lookup("service:store"); - const field = store.createRecord("wizard-field", { type: "text" }); + const field = new Field({ type: "text" }); assert.ok(field.unchecked); - field.check(); + field.validate(); assert.ok(field.valid); }); }); diff --git a/app/assets/javascripts/wizard/addon/components/homepage-preview.js b/app/assets/javascripts/wizard/addon/components/homepage-preview.js index d92a59ddba0..a1a4e05793f 100644 --- a/app/assets/javascripts/wizard/addon/components/homepage-preview.js +++ b/app/assets/javascripts/wizard/addon/components/homepage-preview.js @@ -15,7 +15,7 @@ export default WizardPreviewBaseComponent.extend({ images() { return { - logo: this.wizard.getLogoUrl(), + logo: this.wizard.logoUrl, avatar: "/images/wizard/trout.png", }; }, @@ -23,25 +23,25 @@ export default WizardPreviewBaseComponent.extend({ paint({ ctx, colors, font, width, height }) { this.drawFullHeader(colors, font, this.logo); - if (this.get("step.fieldsById.homepage_style.value") === "latest") { + const homepageStyle = this.getHomepageStyle(); + + if (homepageStyle === "latest") { this.drawPills(colors, font, height * 0.15); this.renderLatest(ctx, colors, font, width, height); } else if ( ["categories_only", "categories_with_featured_topics"].includes( - this.get("step.fieldsById.homepage_style.value") + homepageStyle ) ) { this.drawPills(colors, font, height * 0.15, { categories: true }); this.renderCategories(ctx, colors, font, width, height); } else if ( ["categories_boxes", "categories_boxes_with_topics"].includes( - this.get("step.fieldsById.homepage_style.value") + homepageStyle ) ) { this.drawPills(colors, font, height * 0.15, { categories: true }); - const topics = - this.get("step.fieldsById.homepage_style.value") === - "categories_boxes_with_topics"; + const topics = homepageStyle === "categories_boxes_with_topics"; this.renderCategoriesBoxes(ctx, colors, font, width, height, { topics }); } else { this.drawPills(colors, font, height * 0.15, { categories: true }); @@ -146,9 +146,10 @@ export default WizardPreviewBaseComponent.extend({ ctx.font = `${bodyFontSize * 0.9}em '${font}'`; ctx.fillStyle = textColor; ctx.fillText("Category", cols[0], headingY); - if ( - this.get("step.fieldsById.homepage_style.value") === "categories_only" - ) { + + const homepageStyle = this.getHomepageStyle(); + + if (homepageStyle === "categories_only") { ctx.fillText("Topics", cols[4], headingY); } else { ctx.fillText("Topics", cols[1], headingY); @@ -183,10 +184,7 @@ export default WizardPreviewBaseComponent.extend({ ctx.lineTo(margin, y + categoryHeight); ctx.stroke(); - if ( - this.get("step.fieldsById.homepage_style.value") === - "categories_with_featured_topics" - ) { + if (homepageStyle === "categories_with_featured_topics") { ctx.font = `${bodyFontSize}em '${font}'`; ctx.fillText( Math.floor(Math.random() * 90) + 10, @@ -204,10 +202,7 @@ export default WizardPreviewBaseComponent.extend({ }); // Featured Topics - if ( - this.get("step.fieldsById.homepage_style.value") === - "categories_with_featured_topics" - ) { + if (homepageStyle === "categories_with_featured_topics") { const topicHeight = height / 15; y = headingY + bodyFontSize * 22; @@ -249,10 +244,7 @@ export default WizardPreviewBaseComponent.extend({ ctx.fillStyle = textColor; ctx.fillText("Category", cols[0], headingY); ctx.fillText("Topics", cols[1], headingY); - if ( - this.get("step.fieldsById.homepage_style.value") === - "categories_and_latest_topics" - ) { + if (this.getHomepageStyle() === "categories_and_latest_topics") { ctx.fillText("Latest", cols[2], headingY); } else { ctx.fillText("Top", cols[2], headingY); @@ -346,6 +338,10 @@ export default WizardPreviewBaseComponent.extend({ }); }, + getHomepageStyle() { + return this.step.valueFor("homepage_style"); + }, + getTitles() { return LOREM.split(".") .slice(0, 8) diff --git a/app/assets/javascripts/wizard/addon/components/image-preview-logo-small.js b/app/assets/javascripts/wizard/addon/components/image-preview-logo-small.js index 45420ae8ecf..4e506066285 100644 --- a/app/assets/javascripts/wizard/addon/components/image-preview-logo-small.js +++ b/app/assets/javascripts/wizard/addon/components/image-preview-logo-small.js @@ -1,4 +1,4 @@ -import { observes } from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; import { drawHeader, LOREM } from "wizard/lib/preview"; import WizardPreviewBaseComponent from "./wizard-preview-base"; @@ -7,13 +7,23 @@ export default WizardPreviewBaseComponent.extend({ height: 100, image: null, - @observes("field.value") + didInsertElement() { + this._super(...arguments); + this.field.addListener(this.imageChanged); + }, + + willDestroyElement() { + this._super(...arguments); + this.field.removeListener(this.imageChanged); + }, + + @action imageChanged() { this.reload(); }, images() { - return { image: this.get("field.value") }; + return { image: this.field.value }; }, paint(options) { diff --git a/app/assets/javascripts/wizard/addon/components/image-preview-logo.js b/app/assets/javascripts/wizard/addon/components/image-preview-logo.js index e515a13f900..d4b311f3b4a 100644 --- a/app/assets/javascripts/wizard/addon/components/image-preview-logo.js +++ b/app/assets/javascripts/wizard/addon/components/image-preview-logo.js @@ -1,4 +1,4 @@ -import { observes } from "discourse-common/utils/decorators"; +import { action } from "@ember/object"; import { drawHeader } from "wizard/lib/preview"; import WizardPreviewBaseComponent from "./wizard-preview-base"; @@ -7,13 +7,23 @@ export default WizardPreviewBaseComponent.extend({ height: 100, image: null, - @observes("field.value") + didInsertElement() { + this._super(...arguments); + this.field.addListener(this.imageChanged); + }, + + willDestroyElement() { + this._super(...arguments); + this.field.removeListener(this.imageChanged); + }, + + @action imageChanged() { this.reload(); }, images() { - return { image: this.get("field.value") }; + return { image: this.field.value }; }, paint({ ctx, colors, font, width, height }) { diff --git a/app/assets/javascripts/wizard/addon/components/styling-preview.js b/app/assets/javascripts/wizard/addon/components/styling-preview.js index f2adf369aef..e831df580fb 100644 --- a/app/assets/javascripts/wizard/addon/components/styling-preview.js +++ b/app/assets/javascripts/wizard/addon/components/styling-preview.js @@ -22,12 +22,16 @@ export default WizardPreviewBaseComponent.extend({ init() { this._super(...arguments); - this.wizard.on("homepageStyleChanged", this.onHomepageStyleChange); + this.step + .findField("homepage_style") + ?.addListener(this.onHomepageStyleChange); }, willDestroy() { this._super(...arguments); - this.wizard.off("homepageStyleChanged", this.onHomepageStyleChange); + this.step + .findField("homepage_style") + ?.removeListener(this.onHomepageStyleChange); }, didInsertElement() { @@ -104,7 +108,7 @@ export default WizardPreviewBaseComponent.extend({ images() { return { - logo: this.wizard.getLogoUrl(), + logo: this.wizard.logoUrl, avatar: "/images/wizard/trout.png", }; }, diff --git a/app/assets/javascripts/wizard/addon/components/wizard-field-dropdown.js b/app/assets/javascripts/wizard/addon/components/wizard-field-dropdown.js index 40f357c7ccd..997304d612e 100644 --- a/app/assets/javascripts/wizard/addon/components/wizard-field-dropdown.js +++ b/app/assets/javascripts/wizard/addon/components/wizard-field-dropdown.js @@ -27,9 +27,5 @@ export default Component.extend({ @action onChangeValue(value) { this.set("field.value", value); - - if (this.field.id === "homepage_style") { - this.wizard.trigger("homepageStyleChanged"); - } }, }); diff --git a/app/assets/javascripts/wizard/addon/components/wizard-field-image.js b/app/assets/javascripts/wizard/addon/components/wizard-field-image.js index 81c6998ea4d..99d8a9b81bf 100644 --- a/app/assets/javascripts/wizard/addon/components/wizard-field-image.js +++ b/app/assets/javascripts/wizard/addon/components/wizard-field-image.js @@ -30,7 +30,7 @@ export default Component.extend({ }, setupUploads() { - const id = this.get("field.id"); + const id = this.field.id; this._uppyInstance = new Uppy({ id: `wizard-field-image-${id}`, meta: { upload_type: `wizard_${id}` }, diff --git a/app/assets/javascripts/wizard/addon/components/wizard-field.hbs b/app/assets/javascripts/wizard/addon/components/wizard-field.hbs index 044c474a20b..ca5361ad1d2 100644 --- a/app/assets/javascripts/wizard/addon/components/wizard-field.hbs +++ b/app/assets/javascripts/wizard/addon/components/wizard-field.hbs @@ -32,8 +32,8 @@ }} {{/if}} -{{#if this.field.extra_description}} +{{#if this.field.extraDescription}}
{{html-safe - this.field.extra_description + this.field.extraDescription }}
{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/wizard/addon/components/wizard-preview-base.js b/app/assets/javascripts/wizard/addon/components/wizard-preview-base.js index 1b9182900a6..bfdb6109169 100644 --- a/app/assets/javascripts/wizard/addon/components/wizard-preview-base.js +++ b/app/assets/javascripts/wizard/addon/components/wizard-preview-base.js @@ -1,11 +1,11 @@ +/*eslint no-bitwise:0 */ import Component from "@ember/component"; +import { action } from "@ember/object"; import { scheduleOnce } from "@ember/runloop"; import { htmlSafe } from "@ember/template"; import { Promise } from "rsvp"; import PreloadStore from "discourse/lib/preload-store"; -/*eslint no-bitwise:0 */ import getUrl from "discourse-common/lib/get-url"; -import { observes } from "discourse-common/utils/decorators"; import { darkLightDiff, drawHeader } from "wizard/lib/preview"; export const LOREM = ` @@ -61,22 +61,47 @@ export default Component.extend({ const c = this.element.querySelector("canvas"); this.ctx = c.getContext("2d"); this.ctx.scale(scale, scale); + + if (this.step) { + this.step.findField("color_scheme")?.addListener(this.themeChanged); + this.step.findField("homepage_style")?.addListener(this.themeChanged); + this.step.findField("body_font")?.addListener(this.themeBodyFontChanged); + this.step + .findField("heading_font") + ?.addListener(this.themeHeadingFontChanged); + } + this.reload(); }, - @observes("step.fieldsById.{color_scheme,homepage_style}.value") + willDestroyElement() { + this._super(...arguments); + + if (this.step) { + this.step.findField("color_scheme")?.removeListener(this.themeChanged); + this.step.findField("homepage_style")?.removeListener(this.themeChanged); + this.step + .findField("body_font") + ?.removeListener(this.themeBodyFontChanged); + this.step + .findField("heading_font") + ?.removeListener(this.themeHeadingFontChanged); + } + }, + + @action themeChanged() { this.triggerRepaint(); }, - @observes("step.fieldsById.{body_font}.value") + @action themeBodyFontChanged() { if (!this.loadingFontVariants) { this.loadFontVariants(this.wizard.font); } }, - @observes("step.fieldsById.{heading_font}.value") + @action themeHeadingFontChanged() { if (!this.loadingFontVariants) { this.loadFontVariants(this.wizard.headingFont); diff --git a/app/assets/javascripts/wizard/addon/components/wizard-step.hbs b/app/assets/javascripts/wizard/addon/components/wizard-step.hbs index bd8bbea6d00..2e1b5ab3933 100644 --- a/app/assets/javascripts/wizard/addon/components/wizard-step.hbs +++ b/app/assets/javascripts/wizard/addon/components/wizard-step.hbs @@ -33,7 +33,7 @@ {{#if this.includeSidebar}}
{{#each this.step.fields as |field|}} - {{#if field.show_in_sidebar}} + {{#if field.showInSidebar}} {{#each this.step.fields as |field|}} - {{#unless field.show_in_sidebar}} + {{#unless field.showInSidebar}} !alreadyWarned[w]); - - if (unwarned.length) { - unwarned.forEach((w) => (alreadyWarned[w] = true)); - - return this.dialog.confirm({ - message: unwarned.map((w) => I18n.t(`wizard.${w}`)).join("\n"), - didConfirm: () => this.advance(), - }); - } - } - - if (step.get("valid")) { + if (this.step.validate()) { this.advance(); } else { this.autoFocus(); diff --git a/app/assets/javascripts/wizard/addon/controllers/wizard-step.js b/app/assets/javascripts/wizard/addon/controllers/wizard-step.js index 034e20169f6..6ccd04fed59 100644 --- a/app/assets/javascripts/wizard/addon/controllers/wizard-step.js +++ b/app/assets/javascripts/wizard/addon/controllers/wizard-step.js @@ -11,7 +11,7 @@ export default Controller.extend({ @action goNext(response) { - const next = this.get("step.next"); + const next = this.step.next; if (response?.refresh_required) { document.location = getUrl(`/wizard/steps/${next}`); diff --git a/app/assets/javascripts/wizard/addon/mixins/valid-state.js b/app/assets/javascripts/wizard/addon/mixins/valid-state.js deleted file mode 100644 index ca86d7e4ddf..00000000000 --- a/app/assets/javascripts/wizard/addon/mixins/valid-state.js +++ /dev/null @@ -1,36 +0,0 @@ -import discourseComputed from "discourse-common/utils/decorators"; - -export const States = { - UNCHECKED: 0, - INVALID: 1, - VALID: 2, -}; - -export default { - _validState: null, - errorDescription: null, - - init() { - this._super(...arguments); - this.set("_validState", States.UNCHECKED); - }, - - @discourseComputed("_validState") - valid: (state) => state === States.VALID, - - @discourseComputed("_validState") - invalid: (state) => state === States.INVALID, - - @discourseComputed("_validState") - unchecked: (state) => state === States.UNCHECKED, - - setValid(valid, description) { - this.set("_validState", valid ? States.VALID : States.INVALID); - - if (!valid && description && description.length) { - this.set("errorDescription", description); - } else { - this.set("errorDescription", null); - } - }, -}; diff --git a/app/assets/javascripts/wizard/addon/models/step.js b/app/assets/javascripts/wizard/addon/models/step.js deleted file mode 100644 index ec13d9b0b54..00000000000 --- a/app/assets/javascripts/wizard/addon/models/step.js +++ /dev/null @@ -1,59 +0,0 @@ -import EmberObject from "@ember/object"; -import { ajax } from "discourse/lib/ajax"; -import discourseComputed from "discourse-common/utils/decorators"; -import ValidState from "wizard/mixins/valid-state"; - -export default EmberObject.extend(ValidState, { - id: null, - - @discourseComputed("index") - displayIndex(index) { - return index + 1; - }, - - @discourseComputed("fields.[]") - fieldsById(fields) { - const lookup = {}; - fields.forEach((field) => (lookup[field.get("id")] = field)); - return lookup; - }, - - validate() { - let allValid = true; - const result = { warnings: [] }; - - this.fields.forEach((field) => { - allValid = allValid && field.check(); - const warning = field.get("warning"); - if (warning) { - result.warnings.push(warning); - } - }); - - this.setValid(allValid); - - return result; - }, - - fieldError(id, description) { - const field = this.fields.findBy("id", id); - if (field) { - field.setValid(false, description); - } - }, - - save() { - const fields = {}; - this.fields.forEach((f) => (fields[f.id] = f.value)); - - return ajax({ - url: `/wizard/steps/${this.id}`, - type: "PUT", - data: { fields }, - }).catch((error) => { - error.jqXHR.responseJSON.errors.forEach((err) => - this.fieldError(err.field, err.description) - ); - }); - }, -}); diff --git a/app/assets/javascripts/wizard/addon/models/wizard-field.js b/app/assets/javascripts/wizard/addon/models/wizard-field.js deleted file mode 100644 index cbd07ccfebe..00000000000 --- a/app/assets/javascripts/wizard/addon/models/wizard-field.js +++ /dev/null @@ -1,23 +0,0 @@ -import EmberObject from "@ember/object"; -import ValidState from "wizard/mixins/valid-state"; - -export default EmberObject.extend(ValidState, { - id: null, - type: null, - value: null, - required: null, - warning: null, - - check() { - if (!this.required) { - this.setValid(true); - return true; - } - - const val = this.value; - const valid = val && val.length > 0; - - this.setValid(valid); - return valid; - }, -}); diff --git a/app/assets/javascripts/wizard/addon/models/wizard.js b/app/assets/javascripts/wizard/addon/models/wizard.js index 38528905d23..7fdb46977e4 100644 --- a/app/assets/javascripts/wizard/addon/models/wizard.js +++ b/app/assets/javascripts/wizard/addon/models/wizard.js @@ -1,64 +1,271 @@ -import EmberObject from "@ember/object"; -import { readOnly } from "@ember/object/computed"; -import Evented from "@ember/object/evented"; +import { tracked } from "@glimmer/tracking"; import { ajax } from "discourse/lib/ajax"; -import Step from "wizard/models/step"; -import WizardField from "wizard/models/wizard-field"; -const Wizard = EmberObject.extend(Evented, { - totalSteps: readOnly("steps.length"), +export default class Wizard { + static async load() { + return Wizard.parse((await ajax({ url: "/wizard.json" })).wizard); + } - getTitle() { - const titleStep = this.steps.findBy("id", "forum-title"); - if (!titleStep) { - return; - } - return titleStep.get("fieldsById.title.value"); - }, + static parse({ current_color_scheme, steps, ...payload }) { + return new Wizard({ + ...payload, + currentColorScheme: current_color_scheme, + steps: steps.map((step) => Step.parse(step)), + }); + } - getLogoUrl() { - const logoStep = this.steps.findBy("id", "logos"); - if (!logoStep) { - return; - } - return logoStep.get("fieldsById.logo.value"); - }, + constructor(payload) { + safeAssign(this, payload, [ + "start", + "completed", + "steps", + "currentColorScheme", + ]); + } + + get totalSteps() { + return this.steps.length; + } + + get title() { + return this.findStep("forum-tile")?.valueFor("title"); + } + + get logoUrl() { + return this.findStep("logos")?.valueFor("logo"); + } get currentColors() { - const colorStep = this.steps.findBy("id", "styling"); - if (!colorStep) { - return this.current_color_scheme; + const step = this.findStep("styling"); + + if (!step) { + return this.currentColorScheme; } - const themeChoice = colorStep.fieldsById.color_scheme; - if (!themeChoice) { - return; - } + const field = step.findField("color_scheme"); - return themeChoice.choices?.findBy("id", themeChoice.value)?.data.colors; - }, + return field?.chosen?.data.colors; + } get font() { - const fontChoice = this.steps.findBy("id", "styling")?.fieldsById - ?.body_font; - return fontChoice.choices?.findBy("id", fontChoice.value); - }, + return this.findStep("styling")?.findField("body_font").chosen; + } get headingFont() { - const fontChoice = this.steps.findBy("id", "styling")?.fieldsById - ?.heading_font; - return fontChoice.choices?.findBy("id", fontChoice.value); - }, -}); + return this.findStep("styling")?.findField("heading_font").chosen; + } -export function findWizard() { - return ajax({ url: "/wizard.json" }).then(({ wizard }) => { - wizard.steps = wizard.steps.map((step) => { - const stepObj = Step.create(step); - stepObj.fields = stepObj.fields.map((f) => WizardField.create(f)); - return stepObj; - }); - - return Wizard.create(wizard); - }); + findStep(id) { + return this.steps.find((step) => step.id === id); + } +} + +const ValidStates = { + UNCHECKED: 0, + INVALID: 1, + VALID: 2, +}; + +export class Step { + static parse({ fields, ...payload }) { + return new Step({ + ...payload, + fields: fields.map((field) => Field.parse(field)), + }); + } + + @tracked _validState = ValidStates.UNCHECKED; + + constructor(payload) { + safeAssign(this, payload, [ + "id", + "next", + "previous", + "description", + "title", + "index", + "banner", + "emoji", + "fields", + ]); + } + + get valid() { + return this._validState === ValidStates.VALID; + } + + set valid(valid) { + this._validState = valid ? ValidStates.VALID : ValidStates.INVALID; + } + + get invalid() { + return this._validState === ValidStates.INVALID; + } + + get unchecked() { + return this._validState === ValidStates.UNCHECKED; + } + + get displayIndex() { + return this.index + 1; + } + + valueFor(id) { + return this.findField(id)?.value; + } + + findField(id) { + return this.fields.find((field) => field.id === id); + } + + fieldError(id, description) { + let field = this.findField(id); + if (field) { + field.errorDescription = description; + } + } + + validate() { + let valid = this.fields + .map((field) => field.validate()) + .every((result) => result); + + return (this.valid = valid); + } + + serialize() { + let data = {}; + + for (let field of this.fields) { + data[field.id] = field.value; + } + + return data; + } + + async save() { + try { + return await ajax({ + url: `/wizard/steps/${this.id}`, + type: "PUT", + data: { fields: this.serialize() }, + }); + } catch (error) { + for (let err of error.jqXHR.responseJSON.errors) { + this.fieldError(err.field, err.description); + } + } + } +} + +export class Field { + static parse({ extra_description, show_in_sidebar, choices, ...payload }) { + return new Field({ + ...payload, + extraDescription: extra_description, + showInSidebar: show_in_sidebar, + choices: choices?.map((choice) => Choice.parse(choice)), + }); + } + + @tracked _value = null; + @tracked _validState = ValidStates.UNCHECKED; + @tracked _errorDescription = null; + + _listeners = []; + + constructor(payload) { + safeAssign(this, payload, [ + "id", + "type", + "required", + "value", + "label", + "placeholder", + "description", + "extraDescription", + "icon", + "disabled", + "showInSidebar", + "choices", + ]); + } + + get value() { + return this._value; + } + + set value(newValue) { + this._value = newValue; + + for (let listener of this._listeners) { + listener(); + } + } + + get chosen() { + return this.choices?.find((choice) => choice.id === this.value); + } + + get valid() { + return this._validState === ValidStates.VALID; + } + + set valid(valid) { + this._validState = valid ? ValidStates.VALID : ValidStates.INVALID; + this._errorDescription = null; + } + + get invalid() { + return this._validState === ValidStates.INVALID; + } + + get unchecked() { + return this._validState === ValidStates.UNCHECKED; + } + + get errorDescription() { + return this._errorDescription; + } + + set errorDescription(description) { + this._validState = ValidStates.INVALID; + this._errorDescription = description; + } + + validate() { + let valid = true; + + if (this.required) { + valid = !!(this.value?.length > 0); + } + + return (this.valid = valid); + } + + addListener(listener) { + this._listeners.push(listener); + } + + removeListener(listener) { + this._listeners = this._listeners.filter((l) => l === listener); + } +} + +export class Choice { + static parse({ extra_label, ...payload }) { + return new Choice({ ...payload, extraLabel: extra_label }); + } + + constructor({ id, label, extraLabel, description, icon, data }) { + Object.assign(this, { id, label, extraLabel, description, icon, data }); + } +} + +function safeAssign(object, payload, permittedKeys) { + for (const [key, value] of Object.entries(payload)) { + if (permittedKeys.includes(key)) { + object[key] = value; + } + } } diff --git a/app/assets/javascripts/wizard/addon/routes/wizard.js b/app/assets/javascripts/wizard/addon/routes/wizard.js index 2b78f4a2772..49c92605d20 100644 --- a/app/assets/javascripts/wizard/addon/routes/wizard.js +++ b/app/assets/javascripts/wizard/addon/routes/wizard.js @@ -1,10 +1,10 @@ import Route from "@ember/routing/route"; import DisableSidebar from "discourse/mixins/disable-sidebar"; -import { findWizard } from "wizard/models/wizard"; +import Wizard from "wizard/models/wizard"; export default Route.extend(DisableSidebar, { model() { - return findWizard(); + return Wizard.load(); }, activate() {