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
This commit is contained in:
Godfrey Chan 2023-11-23 08:35:51 -08:00 committed by GitHub
parent 7c9cf666da
commit 2228f75645
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 357 additions and 254 deletions

View File

@ -1,44 +1,38 @@
import { getOwner } from "@ember/application";
import { setupTest } from "ember-qunit"; import { setupTest } from "ember-qunit";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { Field } from "wizard/models/wizard";
module("Unit | Model | Wizard | wizard-field", function (hooks) { module("Unit | Model | Wizard | wizard-field", function (hooks) {
setupTest(hooks); setupTest(hooks);
test("basic state", function (assert) { test("basic state", function (assert) {
const store = getOwner(this).lookup("service:store"); const field = new Field({ type: "text" });
const field = store.createRecord("wizard-field", { type: "text" });
assert.ok(field.unchecked); assert.ok(field.unchecked);
assert.ok(!field.valid); assert.ok(!field.valid);
assert.ok(!field.invalid); assert.ok(!field.invalid);
}); });
test("text - required - validation", function (assert) { test("text - required - validation", function (assert) {
const store = getOwner(this).lookup("service:store"); const field = new Field({ type: "text", required: true });
const field = store.createRecord("wizard-field", {
type: "text",
required: true,
});
assert.ok(field.unchecked); assert.ok(field.unchecked);
field.check(); field.validate();
assert.ok(!field.unchecked); assert.ok(!field.unchecked);
assert.ok(!field.valid); assert.ok(!field.valid);
assert.ok(field.invalid); assert.ok(field.invalid);
field.set("value", "a value"); field.value = "a value";
field.check(); field.validate();
assert.ok(!field.unchecked); assert.ok(!field.unchecked);
assert.ok(field.valid); assert.ok(field.valid);
assert.ok(!field.invalid); assert.ok(!field.invalid);
}); });
test("text - optional - validation", function (assert) { test("text - optional - validation", function (assert) {
const store = getOwner(this).lookup("service:store"); const field = new Field({ type: "text" });
const field = store.createRecord("wizard-field", { type: "text" });
assert.ok(field.unchecked); assert.ok(field.unchecked);
field.check(); field.validate();
assert.ok(field.valid); assert.ok(field.valid);
}); });
}); });

View File

@ -15,7 +15,7 @@ export default WizardPreviewBaseComponent.extend({
images() { images() {
return { return {
logo: this.wizard.getLogoUrl(), logo: this.wizard.logoUrl,
avatar: "/images/wizard/trout.png", avatar: "/images/wizard/trout.png",
}; };
}, },
@ -23,25 +23,25 @@ export default WizardPreviewBaseComponent.extend({
paint({ ctx, colors, font, width, height }) { paint({ ctx, colors, font, width, height }) {
this.drawFullHeader(colors, font, this.logo); 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.drawPills(colors, font, height * 0.15);
this.renderLatest(ctx, colors, font, width, height); this.renderLatest(ctx, colors, font, width, height);
} else if ( } else if (
["categories_only", "categories_with_featured_topics"].includes( ["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.drawPills(colors, font, height * 0.15, { categories: true });
this.renderCategories(ctx, colors, font, width, height); this.renderCategories(ctx, colors, font, width, height);
} else if ( } else if (
["categories_boxes", "categories_boxes_with_topics"].includes( ["categories_boxes", "categories_boxes_with_topics"].includes(
this.get("step.fieldsById.homepage_style.value") homepageStyle
) )
) { ) {
this.drawPills(colors, font, height * 0.15, { categories: true }); this.drawPills(colors, font, height * 0.15, { categories: true });
const topics = const topics = homepageStyle === "categories_boxes_with_topics";
this.get("step.fieldsById.homepage_style.value") ===
"categories_boxes_with_topics";
this.renderCategoriesBoxes(ctx, colors, font, width, height, { topics }); this.renderCategoriesBoxes(ctx, colors, font, width, height, { topics });
} else { } else {
this.drawPills(colors, font, height * 0.15, { categories: true }); 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.font = `${bodyFontSize * 0.9}em '${font}'`;
ctx.fillStyle = textColor; ctx.fillStyle = textColor;
ctx.fillText("Category", cols[0], headingY); 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); ctx.fillText("Topics", cols[4], headingY);
} else { } else {
ctx.fillText("Topics", cols[1], headingY); ctx.fillText("Topics", cols[1], headingY);
@ -183,10 +184,7 @@ export default WizardPreviewBaseComponent.extend({
ctx.lineTo(margin, y + categoryHeight); ctx.lineTo(margin, y + categoryHeight);
ctx.stroke(); ctx.stroke();
if ( if (homepageStyle === "categories_with_featured_topics") {
this.get("step.fieldsById.homepage_style.value") ===
"categories_with_featured_topics"
) {
ctx.font = `${bodyFontSize}em '${font}'`; ctx.font = `${bodyFontSize}em '${font}'`;
ctx.fillText( ctx.fillText(
Math.floor(Math.random() * 90) + 10, Math.floor(Math.random() * 90) + 10,
@ -204,10 +202,7 @@ export default WizardPreviewBaseComponent.extend({
}); });
// Featured Topics // Featured Topics
if ( if (homepageStyle === "categories_with_featured_topics") {
this.get("step.fieldsById.homepage_style.value") ===
"categories_with_featured_topics"
) {
const topicHeight = height / 15; const topicHeight = height / 15;
y = headingY + bodyFontSize * 22; y = headingY + bodyFontSize * 22;
@ -249,10 +244,7 @@ export default WizardPreviewBaseComponent.extend({
ctx.fillStyle = textColor; ctx.fillStyle = textColor;
ctx.fillText("Category", cols[0], headingY); ctx.fillText("Category", cols[0], headingY);
ctx.fillText("Topics", cols[1], headingY); ctx.fillText("Topics", cols[1], headingY);
if ( if (this.getHomepageStyle() === "categories_and_latest_topics") {
this.get("step.fieldsById.homepage_style.value") ===
"categories_and_latest_topics"
) {
ctx.fillText("Latest", cols[2], headingY); ctx.fillText("Latest", cols[2], headingY);
} else { } else {
ctx.fillText("Top", cols[2], headingY); ctx.fillText("Top", cols[2], headingY);
@ -346,6 +338,10 @@ export default WizardPreviewBaseComponent.extend({
}); });
}, },
getHomepageStyle() {
return this.step.valueFor("homepage_style");
},
getTitles() { getTitles() {
return LOREM.split(".") return LOREM.split(".")
.slice(0, 8) .slice(0, 8)

View File

@ -1,4 +1,4 @@
import { observes } from "discourse-common/utils/decorators"; import { action } from "@ember/object";
import { drawHeader, LOREM } from "wizard/lib/preview"; import { drawHeader, LOREM } from "wizard/lib/preview";
import WizardPreviewBaseComponent from "./wizard-preview-base"; import WizardPreviewBaseComponent from "./wizard-preview-base";
@ -7,13 +7,23 @@ export default WizardPreviewBaseComponent.extend({
height: 100, height: 100,
image: null, 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() { imageChanged() {
this.reload(); this.reload();
}, },
images() { images() {
return { image: this.get("field.value") }; return { image: this.field.value };
}, },
paint(options) { paint(options) {

View File

@ -1,4 +1,4 @@
import { observes } from "discourse-common/utils/decorators"; import { action } from "@ember/object";
import { drawHeader } from "wizard/lib/preview"; import { drawHeader } from "wizard/lib/preview";
import WizardPreviewBaseComponent from "./wizard-preview-base"; import WizardPreviewBaseComponent from "./wizard-preview-base";
@ -7,13 +7,23 @@ export default WizardPreviewBaseComponent.extend({
height: 100, height: 100,
image: null, 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() { imageChanged() {
this.reload(); this.reload();
}, },
images() { images() {
return { image: this.get("field.value") }; return { image: this.field.value };
}, },
paint({ ctx, colors, font, width, height }) { paint({ ctx, colors, font, width, height }) {

View File

@ -22,12 +22,16 @@ export default WizardPreviewBaseComponent.extend({
init() { init() {
this._super(...arguments); this._super(...arguments);
this.wizard.on("homepageStyleChanged", this.onHomepageStyleChange); this.step
.findField("homepage_style")
?.addListener(this.onHomepageStyleChange);
}, },
willDestroy() { willDestroy() {
this._super(...arguments); this._super(...arguments);
this.wizard.off("homepageStyleChanged", this.onHomepageStyleChange); this.step
.findField("homepage_style")
?.removeListener(this.onHomepageStyleChange);
}, },
didInsertElement() { didInsertElement() {
@ -104,7 +108,7 @@ export default WizardPreviewBaseComponent.extend({
images() { images() {
return { return {
logo: this.wizard.getLogoUrl(), logo: this.wizard.logoUrl,
avatar: "/images/wizard/trout.png", avatar: "/images/wizard/trout.png",
}; };
}, },

View File

@ -27,9 +27,5 @@ export default Component.extend({
@action @action
onChangeValue(value) { onChangeValue(value) {
this.set("field.value", value); this.set("field.value", value);
if (this.field.id === "homepage_style") {
this.wizard.trigger("homepageStyleChanged");
}
}, },
}); });

View File

@ -30,7 +30,7 @@ export default Component.extend({
}, },
setupUploads() { setupUploads() {
const id = this.get("field.id"); const id = this.field.id;
this._uppyInstance = new Uppy({ this._uppyInstance = new Uppy({
id: `wizard-field-image-${id}`, id: `wizard-field-image-${id}`,
meta: { upload_type: `wizard_${id}` }, meta: { upload_type: `wizard_${id}` },

View File

@ -32,8 +32,8 @@
}}</div> }}</div>
{{/if}} {{/if}}
{{#if this.field.extra_description}} {{#if this.field.extraDescription}}
<div class="wizard-container__description extra">{{html-safe <div class="wizard-container__description extra">{{html-safe
this.field.extra_description this.field.extraDescription
}}</div> }}</div>
{{/if}} {{/if}}

View File

@ -1,11 +1,11 @@
/*eslint no-bitwise:0 */
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object";
import { scheduleOnce } from "@ember/runloop"; import { scheduleOnce } from "@ember/runloop";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import { Promise } from "rsvp"; import { Promise } from "rsvp";
import PreloadStore from "discourse/lib/preload-store"; import PreloadStore from "discourse/lib/preload-store";
/*eslint no-bitwise:0 */
import getUrl from "discourse-common/lib/get-url"; import getUrl from "discourse-common/lib/get-url";
import { observes } from "discourse-common/utils/decorators";
import { darkLightDiff, drawHeader } from "wizard/lib/preview"; import { darkLightDiff, drawHeader } from "wizard/lib/preview";
export const LOREM = ` export const LOREM = `
@ -61,22 +61,47 @@ export default Component.extend({
const c = this.element.querySelector("canvas"); const c = this.element.querySelector("canvas");
this.ctx = c.getContext("2d"); this.ctx = c.getContext("2d");
this.ctx.scale(scale, scale); 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(); 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() { themeChanged() {
this.triggerRepaint(); this.triggerRepaint();
}, },
@observes("step.fieldsById.{body_font}.value") @action
themeBodyFontChanged() { themeBodyFontChanged() {
if (!this.loadingFontVariants) { if (!this.loadingFontVariants) {
this.loadFontVariants(this.wizard.font); this.loadFontVariants(this.wizard.font);
} }
}, },
@observes("step.fieldsById.{heading_font}.value") @action
themeHeadingFontChanged() { themeHeadingFontChanged() {
if (!this.loadingFontVariants) { if (!this.loadingFontVariants) {
this.loadFontVariants(this.wizard.headingFont); this.loadFontVariants(this.wizard.headingFont);

View File

@ -33,7 +33,7 @@
{{#if this.includeSidebar}} {{#if this.includeSidebar}}
<div class="wizard-container__sidebar"> <div class="wizard-container__sidebar">
{{#each this.step.fields as |field|}} {{#each this.step.fields as |field|}}
{{#if field.show_in_sidebar}} {{#if field.showInSidebar}}
<WizardField <WizardField
@field={{field}} @field={{field}}
@step={{this.step}} @step={{this.step}}
@ -45,7 +45,7 @@
{{/if}} {{/if}}
<div class="wizard-container__fields"> <div class="wizard-container__fields">
{{#each this.step.fields as |field|}} {{#each this.step.fields as |field|}}
{{#unless field.show_in_sidebar}} {{#unless field.showInSidebar}}
<WizardField <WizardField
@field={{field}} @field={{field}}
@step={{this.step}} @step={{this.step}}

View File

@ -4,13 +4,9 @@ import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import $ from "jquery"; import $ from "jquery";
import discourseComputed, { observes } from "discourse-common/utils/decorators"; import discourseComputed, { observes } from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
const alreadyWarned = {};
export default Component.extend({ export default Component.extend({
router: service(), router: service(),
dialog: service(),
classNameBindings: [":wizard-container__step", "stepClass"], classNameBindings: [":wizard-container__step", "stepClass"],
saving: null, saving: null,
@ -72,7 +68,7 @@ export default Component.extend({
return step; return step;
}, },
@observes("step.id") @observes("step")
_stepChanged() { _stepChanged() {
this.set("saving", false); this.set("saving", false);
this.autoFocus(); this.autoFocus();
@ -90,7 +86,7 @@ export default Component.extend({
@discourseComputed("step.fields") @discourseComputed("step.fields")
includeSidebar(fields) { includeSidebar(fields) {
return !!fields.findBy("show_in_sidebar"); return !!fields.findBy("showInSidebar");
}, },
autoFocus() { autoFocus() {
@ -125,9 +121,8 @@ export default Component.extend({
exitEarly(event) { exitEarly(event) {
event?.preventDefault(); event?.preventDefault();
const step = this.step; const step = this.step;
step.validate();
if (step.get("valid")) { if (step.validate()) {
this.set("saving", true); this.set("saving", true);
step step
@ -158,23 +153,7 @@ export default Component.extend({
return; return;
} }
const step = this.step; if (this.step.validate()) {
const result = step.validate();
if (result.warnings.length) {
const unwarned = result.warnings.filter((w) => !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")) {
this.advance(); this.advance();
} else { } else {
this.autoFocus(); this.autoFocus();

View File

@ -11,7 +11,7 @@ export default Controller.extend({
@action @action
goNext(response) { goNext(response) {
const next = this.get("step.next"); const next = this.step.next;
if (response?.refresh_required) { if (response?.refresh_required) {
document.location = getUrl(`/wizard/steps/${next}`); document.location = getUrl(`/wizard/steps/${next}`);

View File

@ -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);
}
},
};

View File

@ -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)
);
});
},
});

View File

@ -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;
},
});

View File

@ -1,64 +1,271 @@
import EmberObject from "@ember/object"; import { tracked } from "@glimmer/tracking";
import { readOnly } from "@ember/object/computed";
import Evented from "@ember/object/evented";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import Step from "wizard/models/step";
import WizardField from "wizard/models/wizard-field";
const Wizard = EmberObject.extend(Evented, { export default class Wizard {
totalSteps: readOnly("steps.length"), static async load() {
return Wizard.parse((await ajax({ url: "/wizard.json" })).wizard);
}
getTitle() { static parse({ current_color_scheme, steps, ...payload }) {
const titleStep = this.steps.findBy("id", "forum-title"); return new Wizard({
if (!titleStep) { ...payload,
return; currentColorScheme: current_color_scheme,
} steps: steps.map((step) => Step.parse(step)),
return titleStep.get("fieldsById.title.value"); });
}, }
getLogoUrl() { constructor(payload) {
const logoStep = this.steps.findBy("id", "logos"); safeAssign(this, payload, [
if (!logoStep) { "start",
return; "completed",
} "steps",
return logoStep.get("fieldsById.logo.value"); "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() { get currentColors() {
const colorStep = this.steps.findBy("id", "styling"); const step = this.findStep("styling");
if (!colorStep) {
return this.current_color_scheme; if (!step) {
return this.currentColorScheme;
} }
const themeChoice = colorStep.fieldsById.color_scheme; const field = step.findField("color_scheme");
if (!themeChoice) {
return;
}
return themeChoice.choices?.findBy("id", themeChoice.value)?.data.colors; return field?.chosen?.data.colors;
}, }
get font() { get font() {
const fontChoice = this.steps.findBy("id", "styling")?.fieldsById return this.findStep("styling")?.findField("body_font").chosen;
?.body_font; }
return fontChoice.choices?.findBy("id", fontChoice.value);
},
get headingFont() { get headingFont() {
const fontChoice = this.steps.findBy("id", "styling")?.fieldsById return this.findStep("styling")?.findField("heading_font").chosen;
?.heading_font; }
return fontChoice.choices?.findBy("id", fontChoice.value);
},
});
export function findWizard() { findStep(id) {
return ajax({ url: "/wizard.json" }).then(({ wizard }) => { return this.steps.find((step) => step.id === id);
wizard.steps = wizard.steps.map((step) => { }
const stepObj = Step.create(step); }
stepObj.fields = stepObj.fields.map((f) => WizardField.create(f));
return stepObj; const ValidStates = {
}); UNCHECKED: 0,
INVALID: 1,
return Wizard.create(wizard); 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;
}
}
} }

View File

@ -1,10 +1,10 @@
import Route from "@ember/routing/route"; import Route from "@ember/routing/route";
import DisableSidebar from "discourse/mixins/disable-sidebar"; import DisableSidebar from "discourse/mixins/disable-sidebar";
import { findWizard } from "wizard/models/wizard"; import Wizard from "wizard/models/wizard";
export default Route.extend(DisableSidebar, { export default Route.extend(DisableSidebar, {
model() { model() {
return findWizard(); return Wizard.load();
}, },
activate() { activate() {