DEV: Make wizard an ember addon (#17027)

Co-authored-by: David Taylor <david@taylorhq.com>
This commit is contained in:
Jarek Radosz 2022-06-17 14:50:21 +02:00 committed by GitHub
parent fddd6fd5e0
commit fcb4e5a1a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 688 additions and 928 deletions

View File

@ -158,11 +158,6 @@ jobs:
run: QUNIT_EMBER_CLI=0 bin/rake qunit:test['1200000']
timeout-minutes: 30
- name: Wizard QUnit (Legacy)
if: matrix.build_type == 'frontend-legacy' && matrix.target == 'core'
run: QUNIT_EMBER_CLI=0 bin/rake qunit:test['600000','/wizard/qunit']
timeout-minutes: 10
- name: Plugin QUnit (Legacy)
if: matrix.build_type == 'frontend-legacy' && matrix.target == 'plugins'
run: QUNIT_EMBER_CLI=0 bin/rake plugin:qunit['*','1200000']

View File

@ -93,6 +93,7 @@ export function buildResolver(baseName) {
if (split.length > 1) {
const appBase = `${baseName}/${split[0]}s/`;
const adminBase = "admin/" + split[0] + "s/";
const wizardBase = "wizard/" + split[0] + "s/";
// Allow render 'admin/templates/xyz' too
split[1] = split[1].replace(".templates", "").replace("/templates", "");
@ -101,7 +102,8 @@ export function buildResolver(baseName) {
let dashed = dasherize(split[1].replace(/\./g, "/"));
if (
requirejs.entries[appBase + dashed] ||
requirejs.entries[adminBase + dashed]
requirejs.entries[adminBase + dashed] ||
requirejs.entries[wizardBase + dashed]
) {
return split[0] + ":" + dashed;
}
@ -110,7 +112,8 @@ export function buildResolver(baseName) {
dashed = dasherize(split[1].replace(/\./g, "-"));
if (
requirejs.entries[appBase + dashed] ||
requirejs.entries[adminBase + dashed]
requirejs.entries[adminBase + dashed] ||
requirejs.entries[wizardBase + dashed]
) {
return split[0] + ":" + dashed;
}
@ -253,6 +256,7 @@ export function buildResolver(baseName) {
templates[decamelized.replace(/\_/, "/")] ||
templates[`${baseName}/templates/${withoutType}`] ||
this.findAdminTemplate(parsedName) ||
this.findWizardTemplate(parsedName) ||
this.findUnderscoredTemplate(parsedName)
);
},
@ -296,5 +300,37 @@ export function buildResolver(baseName) {
);
}
},
findWizardTemplate(parsedName) {
let decamelized = decamelize(parsedName.fullNameWithoutType);
if (decamelized.startsWith("components")) {
let comPath = `wizard/templates/${decamelized}`;
const compTemplate =
Ember.TEMPLATES[`javascripts/${comPath}`] || Ember.TEMPLATES[comPath];
if (compTemplate) {
return compTemplate;
}
}
if (decamelized === "javascripts/wizard") {
return Ember.TEMPLATES["wizard/templates/wizard"];
}
if (
decamelized.startsWith("wizard") ||
decamelized.startsWith("javascripts/wizard")
) {
decamelized = decamelized.replace(/^wizard\_/, "wizard/templates/");
decamelized = decamelized.replace(/^wizard\./, "wizard/templates/");
decamelized = decamelized.replace(/\./g, "_");
const dashed = decamelized.replace(/_/g, "-");
return (
Ember.TEMPLATES[decamelized] ||
Ember.TEMPLATES[dashed] ||
Ember.TEMPLATES[dashed.replace("wizard-", "wizard/")]
);
}
},
});
}

View File

@ -50,6 +50,8 @@ const Notice = EmberObject.extend({
});
export default Component.extend({
tagName: "",
router: service(),
logsNoticeService: service("logsNotice"),
logNotice: null,
@ -70,6 +72,10 @@ export default Component.extend({
);
},
get visible() {
return !this.router.currentRouteName.startsWith("wizard.");
},
@discourseComputed(
"site.isReadOnly",
"site.wizard_required",

View File

@ -24,7 +24,6 @@ const SERVER_SIDE_ONLY = [
/^\/raw\//,
/^\/posts\/\d+\/raw/,
/^\/raw\/\d+/,
/^\/wizard/,
/\.rss$/,
/\.json$/,
/^\/admin\/upgrade$/,

View File

@ -1,19 +1,27 @@
{{#each notices as |notice|}}
<div class="row">
<div id="global-notice-{{notice.id}}" class="alert alert-{{notice.options.level}} {{notice.id}}">
{{#if notice.options.html}}
{{html-safe notice.options.html}}
{{/if}}
<span class="text">{{html-safe notice.text}}</span>
<div class="global-notice">
{{#if this.visible}}
{{#each notices as |notice|}}
<div class="row">
<div
id="global-notice-{{notice.id}}"
class="alert alert-{{notice.options.level}} {{notice.id}}"
>
{{#if notice.options.html}}
{{html-safe notice.options.html}}
{{/if}}
{{#if notice.options.dismissable}}
{{d-button
class="btn-flat close"
icon="times"
action=(action "dismissNotice")
actionParam=notice
}}
{{/if}}
</div>
</div>
{{/each}}
<span class="text">{{html-safe notice.text}}</span>
{{#if notice.options.dismissable}}
{{d-button
class="btn-flat close"
icon="times"
action=(action "dismissNotice")
actionParam=notice
}}
{{/if}}
</div>
</div>
{{/each}}
{{/if}}
</div>

View File

@ -128,6 +128,9 @@ module.exports = function (defaults) {
concat(mergeTrees([app.options.adminTree]), {
outputFile: `assets/admin.js`,
}),
concat(mergeTrees([app.options.wizardTree]), {
outputFile: `assets/wizard.js`,
}),
prettyTextEngine(vendorJs, "discourse-markdown"),
concat("public/assets/scripts", {
outputFile: `assets/start-discourse.js`,

View File

@ -85,10 +85,16 @@ function head(buffer, bootstrap, headers, baseURL) {
});
if (bootstrap.preloaded.currentUser) {
let staff = JSON.parse(bootstrap.preloaded.currentUser).staff;
const user = JSON.parse(bootstrap.preloaded.currentUser);
let { admin, staff } = user;
if (staff) {
buffer.push(`<script src="${baseURL}assets/admin.js"></script>`);
}
if (admin) {
buffer.push(`<script src="${baseURL}assets/wizard.js"></script>`);
}
}
bootstrap.plugin_js.forEach((src) =>

View File

@ -59,6 +59,7 @@ class TranslationPlugin extends Plugin {
let extras = {
en: {
admin: parsed.en.admin_js.admin,
wizard: parsed.en.wizard_js.wizard,
},
};
@ -70,7 +71,7 @@ class TranslationPlugin extends Plugin {
this.replaceMF(formats, parsed);
this.replaceMF(formats, extras);
formats = Object.keys(formats).map((k) => `"${k}": ${formats[k]}`);
formats = Object.entries(formats).map(([k, v]) => `"${k}": ${v}`);
let contents = `
I18n.locale = 'en';

View File

@ -23,7 +23,6 @@
"@ember/test-helpers": "^2.2.0",
"@glimmer/component": "^1.0.4",
"@glimmer/tracking": "^1.0.4",
"tippy.js": "^6.3.7",
"@popperjs/core": "2.10.2",
"@uppy/aws-s3": "^2.0.8",
"@uppy/aws-s3-multipart": "^2.2.1",
@ -72,8 +71,10 @@
"sass": "^1.32.8",
"select-kit": "^1.0.0",
"sinon": "^13.0.1",
"tippy.js": "^6.3.7",
"virtual-dom": "^2.1.1",
"webpack": "^5.67.0"
"webpack": "^5.67.0",
"wizard": "^1.0.0"
},
"engines": {
"node": "12.* || 14.* || >= 16",

View File

@ -6,11 +6,12 @@
// TODO: Remove this and have resolver find the templates
const prefix = "discourse/templates/";
const adminPrefix = "admin/templates/";
const wizardPrefix = "wizard/templates/";
let len = prefix.length;
Object.keys(requirejs.entries).forEach(function (key) {
if (key.indexOf(prefix) === 0) {
if (key.startsWith(prefix)) {
Ember.TEMPLATES[key.slice(len)] = require(key).default;
} else if (key.indexOf(adminPrefix) === 0) {
} else if (key.startsWith(adminPrefix) || key.startsWith(wizardPrefix)) {
Ember.TEMPLATES[key] = require(key).default;
}
});

View File

@ -0,0 +1,66 @@
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
import { click, currentRouteName, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
acceptance("Wizard", function (needs) {
needs.user();
test("Wizard starts", async function (assert) {
await visit("/wizard");
assert.ok(exists(".wizard-column-contents"));
assert.strictEqual(currentRouteName(), "wizard.step");
});
test("Going back and forth in steps", async function (assert) {
await visit("/wizard/steps/hello-world");
assert.ok(exists(".wizard-step"));
assert.ok(
exists(".wizard-step-hello-world"),
"it adds a class for the step id"
);
assert.ok(!exists(".wizard-btn.finish"), "cannot finish on first step");
assert.ok(exists(".wizard-progress"));
assert.ok(exists(".wizard-step-title"));
assert.ok(exists(".wizard-step-description"));
assert.ok(
!exists(".invalid .field-full-name"),
"don't show it as invalid until the user does something"
);
assert.ok(exists(".wizard-field .field-description"));
assert.ok(!exists(".wizard-btn.back"));
assert.ok(!exists(".wizard-field .field-error-description"));
// invalid data
await click(".wizard-btn.next");
assert.ok(exists(".invalid .field-full-name"));
// server validation fail
await fillIn("input.field-full-name", "Server Fail");
await click(".wizard-btn.next");
assert.ok(exists(".invalid .field-full-name"));
assert.ok(exists(".wizard-field .field-error-description"));
// server validation ok
await fillIn("input.field-full-name", "Evil Trout");
await click(".wizard-btn.next");
assert.ok(!exists(".wizard-field .field-error-description"));
assert.ok(!exists(".wizard-step-description"));
assert.ok(
exists(".wizard-btn.finish"),
"shows finish on an intermediate step"
);
await click(".wizard-btn.next");
assert.ok(exists(".select-kit.field-snack"), "went to the next step");
assert.ok(exists(".preview-area"), "renders the component field");
assert.ok(exists(".wizard-btn.done"), "last step shows a done button");
assert.ok(exists(".action-link.back"), "shows the back button");
assert.ok(!exists(".wizard-step-title"));
assert.ok(!exists(".wizard-btn.finish"), "cannot finish on last step");
await click(".action-link.back");
assert.ok(exists(".wizard-step-title"));
assert.ok(exists(".wizard-btn.next"));
assert.ok(!exists(".wizard-prev"));
});
});

View File

@ -36,7 +36,6 @@
width: 1000px;
height: 1000px;
}
</style>
<script src="{{rootURL}}assets/test-i18n.js"></script>
@ -51,6 +50,7 @@
<script src="{{rootURL}}assets/discourse.js"></script>
<script src="{{rootURL}}assets/discourse-markdown.js"></script>
<script src="{{rootURL}}assets/admin.js"></script>
<script src="{{rootURL}}assets/wizard.js"></script>
{{content-for "test-plugin-js"}}
<script src="{{rootURL}}assets/test-helpers.js"></script>
<script src="{{rootURL}}assets/core-tests.js"></script>

View File

@ -0,0 +1,100 @@
import {
count,
discourseModule,
exists,
} from "discourse/tests/helpers/qunit-helpers";
import componentTest, {
setupRenderingTest,
} from "discourse/tests/helpers/component-test";
import { click, fillIn } from "@ember/test-helpers";
import hbs from "htmlbars-inline-precompile";
discourseModule(
"Integration | Component | Wizard | invite-list",
function (hooks) {
setupRenderingTest(hooks);
componentTest("can add users", {
template: hbs`{{invite-list field=field}}`,
beforeEach() {
this.set("field", {});
},
async test(assert) {
assert.ok(
!exists(".users-list .invite-list-user"),
"no users at first"
);
assert.ok(!exists(".new-user .invalid"), "not invalid at first");
const firstVal = JSON.parse(this.field.value);
assert.strictEqual(firstVal.length, 0, "empty JSON at first");
assert.ok(
this.field.warning,
"it has a warning since no users were added"
);
await click(".add-user");
assert.ok(
!exists(".users-list .invite-list-user"),
"doesn't add a blank user"
);
assert.strictEqual(count(".new-user .invalid"), 1);
await fillIn(".invite-email", "eviltrout@example.com");
await click(".add-user");
assert.strictEqual(
count(".users-list .invite-list-user"),
1,
"adds the user"
);
assert.ok(!exists(".new-user .invalid"));
const val = JSON.parse(this.field.value);
assert.strictEqual(val.length, 1);
assert.strictEqual(
val[0].email,
"eviltrout@example.com",
"adds the email to the JSON"
);
assert.ok(val[0].role.length, "adds the role to the JSON");
assert.ok(
!this.get("field.warning"),
"no warning once the user is added"
);
await fillIn(".invite-email", "eviltrout@example.com");
await click(".add-user");
assert.strictEqual(
count(".users-list .invite-list-user"),
1,
"can't add the same user twice"
);
assert.strictEqual(count(".new-user .invalid"), 1);
await fillIn(".invite-email", "not-an-email");
await click(".add-user");
assert.strictEqual(
count(".users-list .invite-list-user"),
1,
"won't add an invalid email"
);
assert.strictEqual(count(".new-user .invalid"), 1);
await click(
".invite-list .invite-list-user:nth-of-type(1) .remove-user"
);
assert.ok(
!exists(".users-list .invite-list-user"),
0,
"removed the user"
);
},
});
}
);

View File

@ -0,0 +1,35 @@
import { module, test } from "qunit";
import WizardField from "wizard/models/wizard-field";
module("Unit | Model | Wizard | wizard-field", function () {
test("basic state", function (assert) {
const w = WizardField.create({ type: "text" });
assert.ok(w.unchecked);
assert.ok(!w.valid);
assert.ok(!w.invalid);
});
test("text - required - validation", function (assert) {
const w = WizardField.create({ type: "text", required: true });
assert.ok(w.unchecked);
w.check();
assert.ok(!w.unchecked);
assert.ok(!w.valid);
assert.ok(w.invalid);
w.set("value", "a value");
w.check();
assert.ok(!w.unchecked);
assert.ok(w.valid);
assert.ok(!w.invalid);
});
test("text - optional - validation", function (assert) {
const f = WizardField.create({ type: "text" });
assert.ok(f.unchecked);
f.check();
assert.ok(f.valid);
});
});

View File

@ -8,6 +8,7 @@
"discourse-widget-hbs",
"pretty-text",
"select-kit",
"truth-helpers"
"truth-helpers",
"wizard"
]
}

View File

@ -7,7 +7,7 @@ import MultiSelectComponent from "select-kit/components/multi-select";
import { computed } from "@ember/object";
import { isDevelopment } from "discourse-common/config/environment";
import { makeArray } from "discourse-common/lib/helpers";
import { ajax } from "select-kit/lib/ajax-helper";
import { ajax } from "discourse/lib/ajax";
export default MultiSelectComponent.extend({
pluginApiIdentifiers: ["icon-picker"],

View File

@ -1,8 +0,0 @@
let ajax;
if (window.Discourse) {
ajax = requirejs("discourse/lib/ajax").ajax;
} else {
ajax = requirejs("wizard/lib/ajax").ajax;
}
export { ajax };

View File

@ -1,6 +1,6 @@
import I18n from "I18n";
import Mixin from "@ember/object/mixin";
import { ajax } from "select-kit/lib/ajax-helper";
import { ajax } from "discourse/lib/ajax";
import getURL from "discourse-common/lib/get-url";
import { isEmpty } from "@ember/utils";
import { makeArray } from "discourse-common/lib/helpers";

View File

@ -1,13 +0,0 @@
//= require_tree ./truth-helpers/addon
//= require_tree ./discourse-common/addon
//= require_tree ./select-kit/addon
//= require wizard/router
//= require wizard/wizard
//= require_tree ./wizard/templates
//= require_tree ./wizard/components
//= require_tree ./wizard/models
//= require_tree ./wizard/routes
//= require_tree ./wizard/controllers
//= require_tree ./wizard/lib
//= require_tree ./wizard/mixins
//= require_tree ./wizard/initializers

View File

@ -1,47 +0,0 @@
define("@popperjs/core", ["exports"], function (__exports__) {
__exports__.default = window.Popper;
__exports__.createPopper = window.Popper.createPopper;
__exports__.defaultModifiers = window.Popper.defaultModifiers;
__exports__.popperGenerator = window.Popper.popperGenerator;
});
define("tippy.js", ["exports"], function (__exports__) {
__exports__.default = window.tippy;
});
define("@uppy/core", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.Core;
__exports__.BasePlugin = window.Uppy.Core.BasePlugin;
});
define("@uppy/aws-s3", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.AwsS3;
});
define("@uppy/aws-s3-multipart", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.AwsS3Multipart;
});
define("@uppy/xhr-upload", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.XHRUpload;
});
define("@uppy/drop-target", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.DropTarget;
});
define("@uppy/utils/lib/delay", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.Utils.delay;
});
define("@uppy/utils/lib/EventTracker", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.Utils.EventTracker;
});
define("@uppy/utils/lib/AbortController", ["exports"], function (__exports__) {
__exports__.AbortController =
window.Uppy.Utils.AbortControllerLib.AbortController;
__exports__.AbortSignal = window.Uppy.Utils.AbortControllerLib.AbortSignal;
__exports__.createAbortError =
window.Uppy.Utils.AbortControllerLib.createAbortError;
});

View File

@ -1,5 +0,0 @@
// discourse-skip-module
(function () {
const wizard = require("wizard/wizard").default.create();
wizard.start();
})();

View File

@ -1,10 +0,0 @@
//= require ember_jquery
//= require template_include.js
//= require uppy.js
//= require bootstrap-modal.js
//= require bootbox.js
//= require virtual-dom
//= require virtual-dom-amd
//= require popper.js
//= require tippy.umd.js
//= require wizard-shims

View File

@ -0,0 +1,2 @@
//= require discourse/app/lib/export-result
//= require_tree ./wizard/addon

View File

@ -0,0 +1 @@
engine-strict = true

View File

@ -31,6 +31,18 @@ export default createPreviewComponent(659, 320, {
this.wizard.off("homepageStyleChanged", this.onHomepageStyleChange);
},
didInsertElement() {
this._super(...arguments);
this.element.addEventListener("mouseleave", this.handleMouseLeave);
this.element.addEventListener("mousemove", this.handleMouseMove);
},
willDestroyElement() {
this._super(...arguments);
this.element.removeEventListener("mouseleave", this.handleMouseLeave);
this.element.removeEventListener("mousemove", this.handleMouseMove);
},
mouseDown(e) {
const slider = this.element.querySelector(".previews");
this.setProperties({
@ -40,7 +52,8 @@ export default createPreviewComponent(659, 320, {
});
},
mouseLeave() {
@bind
handleMouseLeave() {
this.set("draggingActive", false);
},
@ -48,7 +61,8 @@ export default createPreviewComponent(659, 320, {
this.set("draggingActive", false);
},
mouseMove(e) {
@bind
handleMouseMove(e) {
if (!this.draggingActive) {
return;
}

View File

@ -3,8 +3,8 @@ import discourseComputed from "discourse-common/utils/decorators";
import { action, set } from "@ember/object";
export default Component.extend({
init(...args) {
this._super(...args);
init() {
this._super(...arguments);
if (this.field.id === "color_scheme") {
for (let choice of this.field.choices) {
@ -17,10 +17,7 @@ export default Component.extend({
@discourseComputed("field.id")
componentName(id) {
if (id === "color_scheme") {
return "color-palettes";
}
return "combo-box";
return id === "color_scheme" ? "color-palettes" : "combo-box";
},
keyPress(e) {

View File

@ -4,7 +4,6 @@ import I18n from "I18n";
import { dasherize } from "@ember/string";
import discourseComputed from "discourse-common/utils/decorators";
import { getOwner } from "discourse-common/lib/get-owner";
import { getToken } from "wizard/lib/ajax";
import getUrl from "discourse-common/lib/get-url";
import Uppy from "@uppy/core";
import DropTarget from "@uppy/drop-target";
@ -37,7 +36,7 @@ export default Component.extend({
this._uppyInstance.use(XHRUpload, {
endpoint: getUrl("/uploads.json"),
headers: {
"X-CSRF-Token": getToken(),
"X-CSRF-Token": this.session.csrfToken,
},
});

View File

@ -150,8 +150,10 @@ export default Component.extend({
if (result.warnings.length) {
const unwarned = result.warnings.filter((w) => !alreadyWarned[w]);
if (unwarned.length) {
unwarned.forEach((w) => (alreadyWarned[w] = true));
return window.bootbox.confirm(
unwarned.map((w) => I18n.t(`wizard.${w}`)).join("\n"),
I18n.t("no_value"),

View File

@ -0,0 +1,24 @@
import getUrl from "discourse-common/lib/get-url";
import Controller from "@ember/controller";
import { action } from "@ember/object";
export default Controller.extend({
wizard: null,
step: null,
@action
goNext(response) {
const next = this.get("step.next");
if (response?.refresh_required) {
document.location = getUrl(`/wizard/steps/${next}`);
} else if (response?.success) {
this.transitionToRoute("wizard.step", next);
}
},
@action
goBack() {
this.transitionToRoute("wizard.step", this.step.previous);
},
});

View File

@ -1,13 +1,15 @@
import EmberObject from "@ember/object";
import ValidState from "wizard/mixins/valid-state";
import { ajax } from "wizard/lib/ajax";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
export default EmberObject.extend(ValidState, {
id: null,
@discourseComputed("index")
displayIndex: (index) => index + 1,
displayIndex(index) {
return index + 1;
},
@discourseComputed("fields.[]")
fieldsById(fields) {
@ -48,8 +50,8 @@ export default EmberObject.extend(ValidState, {
url: `/wizard/steps/${this.id}`,
type: "PUT",
data: { fields },
}).catch((response) => {
response.responseJSON.errors.forEach((err) =>
}).catch((error) => {
error.jqXHR.responseJSON.errors.forEach((err) =>
this.fieldError(err.field, err.description)
);
});

View File

@ -2,12 +2,11 @@ import EmberObject from "@ember/object";
import Evented from "@ember/object/evented";
import Step from "wizard/models/step";
import WizardField from "wizard/models/wizard-field";
import { ajax } from "wizard/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import { readOnly } from "@ember/object/computed";
const Wizard = EmberObject.extend(Evented, {
@discourseComputed("steps.length")
totalSteps: (length) => length,
totalSteps: readOnly("steps.length"),
getTitle() {
const titleStep = this.steps.findBy("id", "forum-title");
@ -53,8 +52,7 @@ const Wizard = EmberObject.extend(Evented, {
});
export function findWizard() {
return ajax({ url: "/wizard.json" }).then((response) => {
const wizard = response.wizard;
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));

View File

@ -0,0 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
beforeModel() {
const appModel = this.modelFor("wizard");
this.replaceWith("wizard.step", appModel.start);
},
});

View File

@ -0,0 +1,5 @@
export default function () {
this.route("wizard", function () {
this.route("step", { path: "/steps/:step_id" });
});
}

View File

@ -0,0 +1,17 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
model(params) {
const allSteps = this.modelFor("wizard").steps;
const step = allSteps.findBy("id", params.step_id);
return step || allSteps[0];
},
setupController(controller, step) {
const wizard = this.modelFor("wizard");
this.controllerFor("wizard").set("currentStepId", step.id);
controller.setProperties({ step, wizard });
},
});

View File

@ -0,0 +1,21 @@
import Route from "@ember/routing/route";
import { findWizard } from "wizard/models/wizard";
export default Route.extend({
model() {
return findWizard();
},
activate() {
document.body.classList.add("wizard");
this.controllerFor("application").setProperties({
showTop: false,
showFooter: false,
});
},
deactivate() {
document.body.classList.remove("wizard");
this.controllerFor("application").set("showTop", true);
},
});

View File

@ -0,0 +1,30 @@
{{#each field.choices as |choice|}}
<div class="radio-field-choice {{fieldClass}}">
<div class="radio-area">
{{radio-button
selection=field.value
value=choice.id
name=choice.label
onChange=(action "changed")
}}
<span class="radio-label">
{{#if choice.icon}}
{{d-icon choice.icon}}
{{/if}}
{{choice.label}}
</span>
{{#if choice.extraLabel}}
<span class="extra-label">
{{html-safe choice.extraLabel}}
</span>
{{/if}}
</div>
<div class="radio-description">
{{choice.description}}
</div>
</div>
{{/each}}

View File

@ -0,0 +1,6 @@
{{wizard-step
step=step
wizard=wizard
goNext=(action "goNext")
goBack=(action "goBack")
}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,59 @@
export default function (helpers) {
const { parsePostData, response } = helpers;
this.get("/wizard.json", () => {
return response({
wizard: {
start: "hello-world",
completed: true,
steps: [
{
id: "hello-world",
title: "hello there",
index: 0,
description: "hello!",
fields: [
{
id: "full_name",
type: "text",
required: true,
description: "Your name",
},
],
next: "second-step",
},
{
id: "second-step",
title: "Second step",
index: 1,
fields: [{ id: "some-title", type: "text" }],
previous: "hello-world",
next: "last-step",
},
{
id: "last-step",
index: 2,
fields: [
{ id: "snack", type: "dropdown", required: true },
{ id: "theme-preview", type: "component" },
{ id: "an-image", type: "image" },
],
previous: "second-step",
},
],
},
});
});
this.put("/wizard/steps/:id", (request) => {
const body = parsePostData(request.requestBody);
if (body.fields.full_name === "Server Fail") {
return response(422, {
errors: [{ field: "full_name", description: "Invalid name" }],
});
} else {
return response(200, { success: true });
}
});
}

View File

@ -1,21 +0,0 @@
import { observes, on } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { next } from "@ember/runloop";
export default Component.extend({
tagName: "label",
click(e) {
e.preventDefault();
this.onChange(this.radioValue);
},
@observes("value")
@on("init")
updateVal() {
const checked = this.value === this.radioValue;
next(
() => (this.element.querySelector("input[type=radio]").checked = checked)
);
},
});

View File

@ -1,25 +0,0 @@
import Controller from "@ember/controller";
import { dasherize } from "@ember/string";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
currentStepId: null,
@discourseComputed("currentStepId")
showCanvas(currentStepId) {
return currentStepId === "finished";
},
@discourseComputed("model")
fontClasses(model) {
const fontsStep = model.steps.findBy("id", "styling");
if (!fontsStep) {
return [];
}
const fontField = fontsStep.get("fieldsById.body_font");
return fontField.choices.map(
(choice) => `body-font-${dasherize(choice.id)}`
);
},
});

View File

@ -1,29 +0,0 @@
import getUrl from "discourse-common/lib/get-url";
import Controller from "@ember/controller";
import { action } from "@ember/object";
export default Controller.extend({
wizard: null,
step: null,
@action
goNext(response) {
const next = this.get("step.next");
if (response && response.refresh_required) {
if (this.get("step.id") === "locale") {
document.location = getUrl(`/wizard/steps/${next}`);
return;
} else {
this.send("refreshRoute");
}
}
if (response && response.success) {
this.transitionToRoute("step", next);
}
},
@action
goBack() {
this.transitionToRoute("step", this.get("step.previous"));
},
});

View File

@ -0,0 +1,9 @@
"use strict";
const EmberAddon = require("ember-cli/lib/broccoli/ember-addon");
module.exports = function (defaults) {
let app = new EmberAddon(defaults, {});
return app.toTree();
};

View File

@ -0,0 +1,20 @@
"use strict";
const calculateCacheKeyForTree = require("calculate-cache-key-for-tree");
module.exports = {
name: require("./package").name,
treeForAddon(tree) {
let app = this._findHost();
app.options.wizardTree = this._super.treeForAddon.call(this, tree);
return;
},
cacheKeyForTree(tree) {
return calculateCacheKeyForTree(tree, this);
},
isDevelopingAddon() {
return true;
},
};

View File

@ -1,14 +0,0 @@
import { registerHelpers } from "discourse-common/lib/helpers";
export default {
name: "load-helpers",
initialize(application) {
Object.keys(requirejs.entries).forEach((entry) => {
if (/\/helpers\//.test(entry)) {
requirejs(entry, null, null, true);
}
});
registerHelpers(application);
},
};

View File

@ -1,33 +0,0 @@
import { Promise } from "rsvp";
import getUrl from "discourse-common/lib/get-url";
import jQuery from "jquery";
import { run } from "@ember/runloop";
let token;
export function getToken() {
if (!token) {
token = document.querySelector('meta[name="csrf-token"]')?.content;
}
return token;
}
export function ajax(args) {
let url;
if (arguments.length === 2) {
url = arguments[0];
args = arguments[1];
} else {
url = args.url;
}
return new Promise((resolve, reject) => {
args.headers = { "X-CSRF-Token": getToken() };
args.success = (data) => run(null, resolve, data);
args.error = (xhr) => run(null, reject, xhr);
args.url = getUrl(url);
jQuery.ajax(args);
});
}

View File

@ -0,0 +1,57 @@
{
"name": "wizard",
"version": "1.0.0",
"description": "Discourse's setup wizard",
"author": "Discourse",
"license": "MIT",
"keywords": [
"ember-addon"
],
"repository": "",
"scripts": {
"build": "ember build",
"lint:hbs": "ember-template-lint .",
"lint:js": "eslint .",
"start": "ember serve"
},
"dependencies": {
"ember-auto-import": "^2.2.4",
"ember-cli-babel": "^7.13.0",
"ember-cli-htmlbars": "^4.2.0",
"xss": "^1.0.8",
"webpack": "^5.67.0"
},
"devDependencies": {
"@ember/optional-features": "^1.1.0",
"@glimmer/component": "^1.0.0",
"babel-eslint": "^10.0.3",
"broccoli-asset-rev": "^3.0.0",
"ember-cli": "~3.25.3",
"ember-cli-dependency-checker": "^3.2.0",
"ember-cli-eslint": "^5.1.0",
"ember-cli-inject-live-reload": "^2.0.1",
"ember-cli-sri": "^2.1.1",
"ember-cli-template-lint": "^1.0.0-beta.3",
"ember-cli-uglify": "^3.0.0",
"ember-disable-prototype-extensions": "^1.1.3",
"ember-export-application-global": "^2.0.1",
"ember-load-initializers": "^2.1.1",
"ember-maybe-import-regenerator": "^0.1.6",
"ember-resolver": "^7.0.0",
"ember-source": "~3.15.0",
"ember-source-channel-url": "^2.0.1",
"ember-try": "^2.0.0",
"eslint": "^7.27.0",
"eslint-plugin-ember": "^7.7.1",
"eslint-plugin-node": "^10.0.0",
"loader.js": "^4.7.0"
},
"engines": {
"node": "12.* || 14.* || >= 16",
"npm": "please-use-yarn",
"yarn": ">= 1.21.1"
},
"ember": {
"edition": "default"
}
}

View File

@ -1,14 +0,0 @@
import EmberRouter from "@ember/routing/router";
import getUrl from "discourse-common/lib/get-url";
import { isTesting } from "discourse-common/config/environment";
const Router = EmberRouter.extend({
rootURL: getUrl("/wizard/"),
location: isTesting() ? "none" : "history",
});
Router.map(function () {
this.route("step", { path: "/steps/:step_id" });
});
export default Router;

View File

@ -1,14 +0,0 @@
import Route from "@ember/routing/route";
import { findWizard } from "wizard/models/wizard";
import { action } from "@ember/object";
export default Route.extend({
model() {
return findWizard();
},
@action
refreshRoute() {
this.refresh();
},
});

View File

@ -1,7 +0,0 @@
import Route from "@ember/routing/route";
export default Route.extend({
beforeModel() {
const appModel = this.modelFor("application");
this.replaceWith("step", appModel.start);
},
});

View File

@ -1,17 +0,0 @@
import Route from "@ember/routing/route";
export default Route.extend({
model(params) {
const allSteps = this.modelFor("application").steps;
const step = allSteps.findBy("id", params.step_id);
return step ? step : allSteps[0];
},
setupController(controller, step) {
this.controllerFor("application").set("currentStepId", step.get("id"));
controller.setProperties({
step,
wizard: this.modelFor("application"),
});
},
});

File diff suppressed because one or more lines are too long

View File

@ -1,17 +0,0 @@
<div class="radio-area">
<input type="radio" name={{label}}>
<span class="radio-label">
{{#if icon}}
{{d-icon icon}}
{{/if}}
{{label}}
</span>
{{#if extraLabel}}
<span class="extra-label">
{{html-safe extraLabel}}
</span>
{{/if}}
</div>
<div class="radio-description">
{{description}}
</div>

View File

@ -1,11 +0,0 @@
{{#each field.choices as |c|}}
<div class="radio-field-choice {{fieldClass}}">
{{radio-button value=field.value
radioValue=c.id
label=c.label
extraLabel=c.extra_label
icon=c.icon
description=c.description
onChange=(action "changed")}}
</div>
{{/each}}

View File

@ -1 +0,0 @@
{{wizard-step step=step wizard=wizard goNext=(action "goNext") goBack=(action "goBack")}}

View File

@ -1,78 +0,0 @@
import { click, currentRouteName, fillIn, visit } from "@ember/test-helpers";
import { module, test } from "qunit";
import { run } from "@ember/runloop";
import startApp from "wizard/test/helpers/start-app";
let wizard;
module("Acceptance: wizard", {
beforeEach() {
wizard = startApp();
},
afterEach() {
run(wizard, "destroy");
},
});
function exists(selector) {
return document.querySelector(selector) !== null;
}
test("Wizard starts", async function (assert) {
await visit("/");
assert.ok(exists(".wizard-column-contents"));
assert.strictEqual(currentRouteName(), "step");
});
test("Going back and forth in steps", async function (assert) {
await visit("/steps/hello-world");
assert.ok(exists(".wizard-step"));
assert.ok(
exists(".wizard-step-hello-world"),
"it adds a class for the step id"
);
assert.ok(!exists(".wizard-btn.finish"), "cant finish on first step");
assert.ok(exists(".wizard-progress"));
assert.ok(exists(".wizard-step-title"));
assert.ok(exists(".wizard-step-description"));
assert.ok(
!exists(".invalid .field-full-name"),
"don't show it as invalid until the user does something"
);
assert.ok(exists(".wizard-field .field-description"));
assert.ok(!exists(".wizard-btn.back"));
assert.ok(!exists(".wizard-field .field-error-description"));
// invalid data
await click(".wizard-btn.next");
assert.ok(exists(".invalid .field-full-name"));
// server validation fail
await fillIn("input.field-full-name", "Server Fail");
await click(".wizard-btn.next");
assert.ok(exists(".invalid .field-full-name"));
assert.ok(exists(".wizard-field .field-error-description"));
// server validation ok
await fillIn("input.field-full-name", "Evil Trout");
await click(".wizard-btn.next");
assert.ok(!exists(".wizard-field .field-error-description"));
assert.ok(!exists(".wizard-step-description"));
assert.ok(
exists(".wizard-btn.finish"),
"shows finish on an intermediate step"
);
await click(".wizard-btn.next");
assert.ok(exists(".select-kit.field-snack"), "went to the next step");
assert.ok(exists(".preview-area"), "renders the component field");
assert.ok(exists(".wizard-btn.done"), "last step shows a done button");
assert.ok(exists(".action-link.back"), "shows the back button");
assert.ok(!exists(".wizard-step-title"));
assert.ok(!exists(".wizard-btn.finish"), "cant finish on last step");
await click(".action-link.back");
assert.ok(exists(".wizard-step-title"));
assert.ok(exists(".wizard-btn.next"));
assert.ok(!exists(".wizard-prev"));
});

View File

@ -1,82 +0,0 @@
import { componentTest } from "wizard/test/helpers/component-test";
import { moduleForComponent } from "ember-qunit";
import { click, fillIn } from "@ember/test-helpers";
moduleForComponent("invite-list", { integration: true });
componentTest("can add users", {
template: `{{invite-list field=field}}`,
beforeEach() {
this.set("field", {});
},
async test(assert) {
assert.ok(
document.querySelectorAll(".users-list .invite-list-user").length === 0,
"no users at first"
);
assert.ok(
document.querySelectorAll(".new-user .invalid").length === 0,
"not invalid at first"
);
const firstVal = JSON.parse(this.get("field.value"));
assert.strictEqual(firstVal.length, 0, "empty JSON at first");
assert.ok(
this.get("field.warning"),
"it has a warning since no users were added"
);
await click(".add-user");
assert.ok(
document.querySelectorAll(".users-list .invite-list-user").length === 0,
"doesn't add a blank user"
);
assert.ok(document.querySelectorAll(".new-user .invalid").length === 1);
await fillIn(".invite-email", "eviltrout@example.com");
await click(".add-user");
assert.ok(
document.querySelectorAll(".users-list .invite-list-user").length === 1,
"adds the user"
);
assert.ok(document.querySelectorAll(".new-user .invalid").length === 0);
const val = JSON.parse(this.get("field.value"));
assert.strictEqual(val.length, 1);
assert.strictEqual(
val[0].email,
"eviltrout@example.com",
"adds the email to the JSON"
);
assert.ok(val[0].role.length, "adds the role to the JSON");
assert.ok(!this.get("field.warning"), "no warning once the user is added");
await fillIn(".invite-email", "eviltrout@example.com");
await click(".add-user");
assert.ok(
document.querySelectorAll(".users-list .invite-list-user").length === 1,
"can't add the same user twice"
);
assert.ok(document.querySelectorAll(".new-user .invalid").length === 1);
await fillIn(".invite-email", "not-an-email");
await click(".add-user");
assert.ok(
document.querySelectorAll(".users-list .invite-list-user").length === 1,
"won't add an invalid email"
);
assert.ok(document.querySelectorAll(".new-user .invalid").length === 1);
await click(".invite-list .invite-list-user:nth-of-type(1) .remove-user");
assert.ok(
document.querySelectorAll(".users-list .invite-list-user").length === 0,
"removed the user"
);
},
});

View File

@ -1,18 +0,0 @@
/* eslint-disable no-undef */
import initializer from "wizard/initializers/load-helpers";
import { test } from "qunit";
export function componentTest(name, opts) {
opts = opts || {};
test(name, function (assert) {
initializer.initialize(this.registry);
if (opts.beforeEach) {
opts.beforeEach.call(this);
}
andThen(() => this.render(opts.template));
andThen(() => opts.test.call(this, assert));
});
}

View File

@ -1,19 +0,0 @@
import Wizard from "wizard/wizard";
import initializer from "wizard/initializers/load-helpers";
import { run } from "@ember/runloop";
let app;
let started = false;
export default function () {
run(() => (app = Wizard.create({ rootElement: "#ember-testing" })));
if (!started) {
initializer.initialize(app);
app.start();
started = true;
}
app.setupForTesting();
app.injectTestHelpers();
return app;
}

View File

@ -1,36 +0,0 @@
import WizardField from "wizard/models/wizard-field";
import { moduleFor } from "ember-qunit";
import { test } from "qunit";
moduleFor("model:wizard-field");
test("basic state", function (assert) {
const w = WizardField.create({ type: "text" });
assert.ok(w.get("unchecked"));
assert.ok(!w.get("valid"));
assert.ok(!w.get("invalid"));
});
test("text - required - validation", function (assert) {
const w = WizardField.create({ type: "text", required: true });
assert.ok(w.get("unchecked"));
w.check();
assert.ok(!w.get("unchecked"));
assert.ok(!w.get("valid"));
assert.ok(w.get("invalid"));
w.set("value", "a value");
w.check();
assert.ok(!w.get("unchecked"));
assert.ok(w.get("valid"));
assert.ok(!w.get("invalid"));
});
test("text - optional - validation", function (assert) {
const f = WizardField.create({ type: "text" });
assert.ok(f.get("unchecked"));
f.check();
assert.ok(f.get("valid"));
});

View File

@ -1,76 +0,0 @@
// discourse-skip-module
/*global document, Logster, QUnit */
//= require env
//= require jquery.debug
//= require ember.debug
//= require locales/i18n
//= require locales/en
//= require route-recognizer
//= require fake_xml_http_request
//= require pretender
//= require qunit
//= require ember-qunit
//= require discourse-loader
//= require jquery.debug
//= require handlebars
//= require ember-template-compiler
//= require wizard-application
//= require wizard-vendor
//= require_tree ./helpers
//= require_tree ./acceptance
//= require_tree ./models
//= require_tree ./components
//= require ./wizard-pretender
//= require test-shims
document.addEventListener("DOMContentLoaded", function () {
document.body.insertAdjacentHTML(
"afterbegin",
`
<div id="ember-testing-container"><div id="ember-testing"></div></div>
<style>#ember-testing-container { position: absolute; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; } #ember-testing { zoom: 50%; }</style>
`
);
});
if (window.Logster) {
Logster.enabled = false;
} else {
window.Logster = { enabled: false };
}
// eslint-disable-next-line no-undef
Ember.Test.adapter = window.QUnitAdapter.create();
let createPretendServer = requirejs(
"wizard/test/wizard-pretender",
null,
null,
false
).default;
let server;
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.get("qunit_disable_auto_start") === "1") {
QUnit.config.autostart = false;
}
QUnit.testStart(function () {
server = createPretendServer();
});
QUnit.testDone(function () {
server.shutdown();
});
let _testApp = requirejs("wizard/test/helpers/start-app").default();
let _buildResolver = requirejs("discourse-common/resolver").buildResolver;
window.setResolver(_buildResolver("wizard").create({ namespace: _testApp }));
Object.keys(requirejs.entries).forEach(function (entry) {
if (/\-test/.test(entry)) {
requirejs(entry, null, null, true);
}
});

View File

@ -1,106 +0,0 @@
import Pretender from "pretender";
// TODO: This file has some copied and pasted functions from `create-pretender` - would be good
// to centralize that code at some point.
function parsePostData(query) {
const result = {};
query.split("&").forEach(function (part) {
const item = part.split("=");
const firstSeg = decodeURIComponent(item[0]);
const m = /^([^\[]+)\[([^\]]+)\]/.exec(firstSeg);
const val = decodeURIComponent(item[1]).replace(/\+/g, " ");
if (m) {
result[m[1]] = result[m[1]] || {};
result[m[1]][m[2]] = val;
} else {
result[firstSeg] = val;
}
});
return result;
}
function response(code, obj) {
if (typeof code === "object") {
obj = code;
code = 200;
}
return [code, { "Content-Type": "application/json" }, obj];
}
export default function () {
const server = new Pretender(function () {
this.get("/wizard.json", () => {
return response(200, {
wizard: {
start: "hello-world",
completed: true,
steps: [
{
id: "hello-world",
title: "hello there",
index: 0,
description: "hello!",
fields: [
{
id: "full_name",
type: "text",
required: true,
description: "Your name",
},
],
next: "second-step",
},
{
id: "second-step",
title: "Second step",
index: 1,
fields: [{ id: "some-title", type: "text" }],
previous: "hello-world",
next: "last-step",
},
{
id: "last-step",
index: 2,
fields: [
{ id: "snack", type: "dropdown", required: true },
{ id: "theme-preview", type: "component" },
{ id: "an-image", type: "image" },
],
previous: "second-step",
},
],
},
});
});
this.put("/wizard/steps/:id", (request) => {
const body = parsePostData(request.requestBody);
if (body.fields.full_name === "Server Fail") {
return response(422, {
errors: [{ field: "full_name", description: "Invalid name" }],
});
} else {
return response(200, { success: true });
}
});
});
server.prepareBody = function (body) {
if (body && typeof body === "object") {
return JSON.stringify(body);
}
return body;
};
server.unhandledRequest = function (verb, path) {
const error =
"Unhandled request in test environment: " + path + " (" + verb + ")";
window.console.error(error);
throw error;
};
return server;
}

View File

@ -1,29 +0,0 @@
import Application from "@ember/application";
import { buildResolver } from "discourse-common/resolver";
export default Application.extend({
rootElement: "#wizard-main",
Resolver: buildResolver("wizard"),
start() {
// required for select kit to work without Ember CLI
// eslint-disable-next-line no-undef
Object.keys(Ember.TEMPLATES).forEach((k) => {
if (k.indexOf("select-kit") === 0) {
// eslint-disable-next-line no-undef
let template = Ember.TEMPLATES[k];
define(k, () => template);
}
});
Object.keys(requirejs._eak_seen).forEach((key) => {
if (/\/initializers\//.test(key)) {
const module = requirejs(key, null, null, true);
if (!module) {
throw new Error(key + " must export an initializer.");
}
this.initializer(module.default);
}
});
},
});

View File

@ -10,7 +10,7 @@ $bubbles-mask: svg-uri(
'<svg xmlns="http://www.w3.org/2000/svg" width="415.2" height="414" viewBox="0 0 415.2 414">
<defs>
<style>
.cls-1 {fill: #adaeb0;}
.cls-1 {fill: #{rgba($primary-low-mid, 0.6)};}
.cls-2 {opacity: 0.45;}
.cls-3 {opacity: 0.65;}
.cls-4 {opacity: 0.35;}
@ -720,35 +720,21 @@ $bubbles-mask: svg-uri(
}
body.wizard {
background-color: var(--secondary);
background-repeat: repeat;
background-position: left top;
background: var(--secondary) $bubbles-mask;
color: var(--primary-very-high);
line-height: $line-height-large;
font-size: 15px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen-Sans, Ubuntu, Cantarell, Arial, sans-serif;
label {
display: block;
}
#wizard-main {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 100%;
}
#wizard-main:before {
mask: $bubbles-mask;
-webkit-mask: $bubbles-mask;
mask-size: 30%;
-webkit-mask-size: 30%;
background-color: var(--primary-low-mid);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: "";
opacity: 0.6;
}
}
@ -1221,6 +1207,7 @@ body.wizard {
.text-field input {
width: 100%;
font-size: $font-up-1;
margin: 0;
padding: 6px;
background-color: var(--secondary);
border: 1px solid var(--primary-low-mid);
@ -1238,11 +1225,6 @@ body.wizard {
.radio-field-choice {
margin-bottom: 1.25em;
input {
/* TODO: Custom :focus style */
/* outline: 0;*/
}
.radio-label {
font-weight: bold;
margin-left: 0.5em;
@ -1250,8 +1232,7 @@ body.wizard {
.radio-description {
margin-top: 0.25em;
margin-left: 1.75em;
color: #777;
color: var(--primary-low-mid);
color: var(--primary-high);
}
}

View File

@ -27,6 +27,7 @@ class BootstrapController < ApplicationController
add_style(mobile_view? ? :mobile : :desktop)
end
add_style(:admin) if staff?
add_style(:wizard) if admin?
assets_fake_request = ActionDispatch::Request.new(request.env.dup)
assets_for_url = params[:for_url]
@ -51,10 +52,15 @@ class BootstrapController < ApplicationController
if ExtraLocalesController.client_overrides_exist?
extra_locales << ExtraLocalesController.url('overrides')
end
if staff?
extra_locales << ExtraLocalesController.url('admin')
end
if admin?
extra_locales << ExtraLocalesController.url('wizard')
end
plugin_js = Discourse.find_plugin_js_assets(
include_official: allow_plugins?,
include_unofficial: allow_third_party_plugins?,

View File

@ -1,13 +1,10 @@
# frozen_string_literal: true
class WizardController < ApplicationController
requires_login except: [:qunit]
requires_login
before_action :ensure_admin, except: [:qunit]
before_action :ensure_wizard_enabled, only: [:index]
skip_before_action :check_xhr, :preload_json
layout false
before_action :ensure_admin
before_action :ensure_wizard_enabled
def index
respond_to do |format|
@ -15,12 +12,10 @@ class WizardController < ApplicationController
wizard = Wizard::Builder.new(current_user).build
render_serialized(wizard, WizardSerializer)
end
format.html {}
format.html do
render body: nil
end
end
end
def qunit
raise Discourse::InvalidAccess.new if Rails.env.production?
end
end

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
# base.rb uses this style of require, so maintain usage of it here
module Jobs
class CriticalUserEmail < UserEmail

Some files were not shown because too many files have changed in this diff Show More