mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
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 { 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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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 }) {
|
||||||
|
@ -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",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -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");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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}` },
|
||||||
|
@ -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}}
|
@ -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);
|
||||||
|
@ -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}}
|
||||||
|
@ -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();
|
||||||
|
@ -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}`);
|
||||||
|
@ -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 { 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user