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:
parent
7c9cf666da
commit
2228f75645
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
|
@ -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",
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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}` },
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.field.extra_description}}
|
||||
{{#if this.field.extraDescription}}
|
||||
<div class="wizard-container__description extra">{{html-safe
|
||||
this.field.extra_description
|
||||
this.field.extraDescription
|
||||
}}</div>
|
||||
{{/if}}
|
|
@ -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);
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
{{#if this.includeSidebar}}
|
||||
<div class="wizard-container__sidebar">
|
||||
{{#each this.step.fields as |field|}}
|
||||
{{#if field.show_in_sidebar}}
|
||||
{{#if field.showInSidebar}}
|
||||
<WizardField
|
||||
@field={{field}}
|
||||
@step={{this.step}}
|
||||
|
@ -45,7 +45,7 @@
|
|||
{{/if}}
|
||||
<div class="wizard-container__fields">
|
||||
{{#each this.step.fields as |field|}}
|
||||
{{#unless field.show_in_sidebar}}
|
||||
{{#unless field.showInSidebar}}
|
||||
<WizardField
|
||||
@field={{field}}
|
||||
@step={{this.step}}
|
||||
|
|
|
@ -4,13 +4,9 @@ import { schedule } from "@ember/runloop";
|
|||
import { inject as service } from "@ember/service";
|
||||
import $ from "jquery";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
const alreadyWarned = {};
|
||||
|
||||
export default Component.extend({
|
||||
router: service(),
|
||||
dialog: service(),
|
||||
classNameBindings: [":wizard-container__step", "stepClass"],
|
||||
saving: null,
|
||||
|
||||
|
@ -72,7 +68,7 @@ export default Component.extend({
|
|||
return step;
|
||||
},
|
||||
|
||||
@observes("step.id")
|
||||
@observes("step")
|
||||
_stepChanged() {
|
||||
this.set("saving", false);
|
||||
this.autoFocus();
|
||||
|
@ -90,7 +86,7 @@ export default Component.extend({
|
|||
|
||||
@discourseComputed("step.fields")
|
||||
includeSidebar(fields) {
|
||||
return !!fields.findBy("show_in_sidebar");
|
||||
return !!fields.findBy("showInSidebar");
|
||||
},
|
||||
|
||||
autoFocus() {
|
||||
|
@ -125,9 +121,8 @@ export default Component.extend({
|
|||
exitEarly(event) {
|
||||
event?.preventDefault();
|
||||
const step = this.step;
|
||||
step.validate();
|
||||
|
||||
if (step.get("valid")) {
|
||||
if (step.validate()) {
|
||||
this.set("saving", true);
|
||||
|
||||
step
|
||||
|
@ -158,23 +153,7 @@ export default Component.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
const step = this.step;
|
||||
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")) {
|
||||
if (this.step.validate()) {
|
||||
this.advance();
|
||||
} else {
|
||||
this.autoFocus();
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue