mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 07:53:49 +08:00
DEV: Make wizard an ember addon (#17027)
Co-authored-by: David Taylor <david@taylorhq.com>
This commit is contained in:
5
.github/workflows/tests.yml
vendored
5
.github/workflows/tests.yml
vendored
@ -158,11 +158,6 @@ jobs:
|
|||||||
run: QUNIT_EMBER_CLI=0 bin/rake qunit:test['1200000']
|
run: QUNIT_EMBER_CLI=0 bin/rake qunit:test['1200000']
|
||||||
timeout-minutes: 30
|
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)
|
- name: Plugin QUnit (Legacy)
|
||||||
if: matrix.build_type == 'frontend-legacy' && matrix.target == 'plugins'
|
if: matrix.build_type == 'frontend-legacy' && matrix.target == 'plugins'
|
||||||
run: QUNIT_EMBER_CLI=0 bin/rake plugin:qunit['*','1200000']
|
run: QUNIT_EMBER_CLI=0 bin/rake plugin:qunit['*','1200000']
|
||||||
|
@ -93,6 +93,7 @@ export function buildResolver(baseName) {
|
|||||||
if (split.length > 1) {
|
if (split.length > 1) {
|
||||||
const appBase = `${baseName}/${split[0]}s/`;
|
const appBase = `${baseName}/${split[0]}s/`;
|
||||||
const adminBase = "admin/" + split[0] + "s/";
|
const adminBase = "admin/" + split[0] + "s/";
|
||||||
|
const wizardBase = "wizard/" + split[0] + "s/";
|
||||||
|
|
||||||
// Allow render 'admin/templates/xyz' too
|
// Allow render 'admin/templates/xyz' too
|
||||||
split[1] = split[1].replace(".templates", "").replace("/templates", "");
|
split[1] = split[1].replace(".templates", "").replace("/templates", "");
|
||||||
@ -101,7 +102,8 @@ export function buildResolver(baseName) {
|
|||||||
let dashed = dasherize(split[1].replace(/\./g, "/"));
|
let dashed = dasherize(split[1].replace(/\./g, "/"));
|
||||||
if (
|
if (
|
||||||
requirejs.entries[appBase + dashed] ||
|
requirejs.entries[appBase + dashed] ||
|
||||||
requirejs.entries[adminBase + dashed]
|
requirejs.entries[adminBase + dashed] ||
|
||||||
|
requirejs.entries[wizardBase + dashed]
|
||||||
) {
|
) {
|
||||||
return split[0] + ":" + dashed;
|
return split[0] + ":" + dashed;
|
||||||
}
|
}
|
||||||
@ -110,7 +112,8 @@ export function buildResolver(baseName) {
|
|||||||
dashed = dasherize(split[1].replace(/\./g, "-"));
|
dashed = dasherize(split[1].replace(/\./g, "-"));
|
||||||
if (
|
if (
|
||||||
requirejs.entries[appBase + dashed] ||
|
requirejs.entries[appBase + dashed] ||
|
||||||
requirejs.entries[adminBase + dashed]
|
requirejs.entries[adminBase + dashed] ||
|
||||||
|
requirejs.entries[wizardBase + dashed]
|
||||||
) {
|
) {
|
||||||
return split[0] + ":" + dashed;
|
return split[0] + ":" + dashed;
|
||||||
}
|
}
|
||||||
@ -253,6 +256,7 @@ export function buildResolver(baseName) {
|
|||||||
templates[decamelized.replace(/\_/, "/")] ||
|
templates[decamelized.replace(/\_/, "/")] ||
|
||||||
templates[`${baseName}/templates/${withoutType}`] ||
|
templates[`${baseName}/templates/${withoutType}`] ||
|
||||||
this.findAdminTemplate(parsedName) ||
|
this.findAdminTemplate(parsedName) ||
|
||||||
|
this.findWizardTemplate(parsedName) ||
|
||||||
this.findUnderscoredTemplate(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/")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,8 @@ const Notice = EmberObject.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
tagName: "",
|
||||||
|
router: service(),
|
||||||
logsNoticeService: service("logsNotice"),
|
logsNoticeService: service("logsNotice"),
|
||||||
logNotice: null,
|
logNotice: null,
|
||||||
|
|
||||||
@ -70,6 +72,10 @@ export default Component.extend({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get visible() {
|
||||||
|
return !this.router.currentRouteName.startsWith("wizard.");
|
||||||
|
},
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"site.isReadOnly",
|
"site.isReadOnly",
|
||||||
"site.wizard_required",
|
"site.wizard_required",
|
||||||
|
@ -24,7 +24,6 @@ const SERVER_SIDE_ONLY = [
|
|||||||
/^\/raw\//,
|
/^\/raw\//,
|
||||||
/^\/posts\/\d+\/raw/,
|
/^\/posts\/\d+\/raw/,
|
||||||
/^\/raw\/\d+/,
|
/^\/raw\/\d+/,
|
||||||
/^\/wizard/,
|
|
||||||
/\.rss$/,
|
/\.rss$/,
|
||||||
/\.json$/,
|
/\.json$/,
|
||||||
/^\/admin\/upgrade$/,
|
/^\/admin\/upgrade$/,
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
{{#each notices as |notice|}}
|
<div class="global-notice">
|
||||||
<div class="row">
|
{{#if this.visible}}
|
||||||
<div id="global-notice-{{notice.id}}" class="alert alert-{{notice.options.level}} {{notice.id}}">
|
{{#each notices as |notice|}}
|
||||||
{{#if notice.options.html}}
|
<div class="row">
|
||||||
{{html-safe notice.options.html}}
|
<div
|
||||||
{{/if}}
|
id="global-notice-{{notice.id}}"
|
||||||
<span class="text">{{html-safe notice.text}}</span>
|
class="alert alert-{{notice.options.level}} {{notice.id}}"
|
||||||
|
>
|
||||||
|
{{#if notice.options.html}}
|
||||||
|
{{html-safe notice.options.html}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if notice.options.dismissable}}
|
<span class="text">{{html-safe notice.text}}</span>
|
||||||
{{d-button
|
|
||||||
class="btn-flat close"
|
{{#if notice.options.dismissable}}
|
||||||
icon="times"
|
{{d-button
|
||||||
action=(action "dismissNotice")
|
class="btn-flat close"
|
||||||
actionParam=notice
|
icon="times"
|
||||||
}}
|
action=(action "dismissNotice")
|
||||||
{{/if}}
|
actionParam=notice
|
||||||
</div>
|
}}
|
||||||
</div>
|
{{/if}}
|
||||||
{{/each}}
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
@ -128,6 +128,9 @@ module.exports = function (defaults) {
|
|||||||
concat(mergeTrees([app.options.adminTree]), {
|
concat(mergeTrees([app.options.adminTree]), {
|
||||||
outputFile: `assets/admin.js`,
|
outputFile: `assets/admin.js`,
|
||||||
}),
|
}),
|
||||||
|
concat(mergeTrees([app.options.wizardTree]), {
|
||||||
|
outputFile: `assets/wizard.js`,
|
||||||
|
}),
|
||||||
prettyTextEngine(vendorJs, "discourse-markdown"),
|
prettyTextEngine(vendorJs, "discourse-markdown"),
|
||||||
concat("public/assets/scripts", {
|
concat("public/assets/scripts", {
|
||||||
outputFile: `assets/start-discourse.js`,
|
outputFile: `assets/start-discourse.js`,
|
||||||
|
@ -85,10 +85,16 @@ function head(buffer, bootstrap, headers, baseURL) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (bootstrap.preloaded.currentUser) {
|
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) {
|
if (staff) {
|
||||||
buffer.push(`<script src="${baseURL}assets/admin.js"></script>`);
|
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) =>
|
bootstrap.plugin_js.forEach((src) =>
|
||||||
|
@ -59,6 +59,7 @@ class TranslationPlugin extends Plugin {
|
|||||||
let extras = {
|
let extras = {
|
||||||
en: {
|
en: {
|
||||||
admin: parsed.en.admin_js.admin,
|
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, parsed);
|
||||||
this.replaceMF(formats, extras);
|
this.replaceMF(formats, extras);
|
||||||
|
|
||||||
formats = Object.keys(formats).map((k) => `"${k}": ${formats[k]}`);
|
formats = Object.entries(formats).map(([k, v]) => `"${k}": ${v}`);
|
||||||
|
|
||||||
let contents = `
|
let contents = `
|
||||||
I18n.locale = 'en';
|
I18n.locale = 'en';
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
"@ember/test-helpers": "^2.2.0",
|
"@ember/test-helpers": "^2.2.0",
|
||||||
"@glimmer/component": "^1.0.4",
|
"@glimmer/component": "^1.0.4",
|
||||||
"@glimmer/tracking": "^1.0.4",
|
"@glimmer/tracking": "^1.0.4",
|
||||||
"tippy.js": "^6.3.7",
|
|
||||||
"@popperjs/core": "2.10.2",
|
"@popperjs/core": "2.10.2",
|
||||||
"@uppy/aws-s3": "^2.0.8",
|
"@uppy/aws-s3": "^2.0.8",
|
||||||
"@uppy/aws-s3-multipart": "^2.2.1",
|
"@uppy/aws-s3-multipart": "^2.2.1",
|
||||||
@ -72,8 +71,10 @@
|
|||||||
"sass": "^1.32.8",
|
"sass": "^1.32.8",
|
||||||
"select-kit": "^1.0.0",
|
"select-kit": "^1.0.0",
|
||||||
"sinon": "^13.0.1",
|
"sinon": "^13.0.1",
|
||||||
|
"tippy.js": "^6.3.7",
|
||||||
"virtual-dom": "^2.1.1",
|
"virtual-dom": "^2.1.1",
|
||||||
"webpack": "^5.67.0"
|
"webpack": "^5.67.0",
|
||||||
|
"wizard": "^1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.* || 14.* || >= 16",
|
"node": "12.* || 14.* || >= 16",
|
||||||
|
@ -6,11 +6,12 @@
|
|||||||
// TODO: Remove this and have resolver find the templates
|
// TODO: Remove this and have resolver find the templates
|
||||||
const prefix = "discourse/templates/";
|
const prefix = "discourse/templates/";
|
||||||
const adminPrefix = "admin/templates/";
|
const adminPrefix = "admin/templates/";
|
||||||
|
const wizardPrefix = "wizard/templates/";
|
||||||
let len = prefix.length;
|
let len = prefix.length;
|
||||||
Object.keys(requirejs.entries).forEach(function (key) {
|
Object.keys(requirejs.entries).forEach(function (key) {
|
||||||
if (key.indexOf(prefix) === 0) {
|
if (key.startsWith(prefix)) {
|
||||||
Ember.TEMPLATES[key.slice(len)] = require(key).default;
|
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;
|
Ember.TEMPLATES[key] = require(key).default;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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"));
|
||||||
|
});
|
||||||
|
});
|
@ -36,7 +36,6 @@
|
|||||||
width: 1000px;
|
width: 1000px;
|
||||||
height: 1000px;
|
height: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script src="{{rootURL}}assets/test-i18n.js"></script>
|
<script src="{{rootURL}}assets/test-i18n.js"></script>
|
||||||
@ -51,6 +50,7 @@
|
|||||||
<script src="{{rootURL}}assets/discourse.js"></script>
|
<script src="{{rootURL}}assets/discourse.js"></script>
|
||||||
<script src="{{rootURL}}assets/discourse-markdown.js"></script>
|
<script src="{{rootURL}}assets/discourse-markdown.js"></script>
|
||||||
<script src="{{rootURL}}assets/admin.js"></script>
|
<script src="{{rootURL}}assets/admin.js"></script>
|
||||||
|
<script src="{{rootURL}}assets/wizard.js"></script>
|
||||||
{{content-for "test-plugin-js"}}
|
{{content-for "test-plugin-js"}}
|
||||||
<script src="{{rootURL}}assets/test-helpers.js"></script>
|
<script src="{{rootURL}}assets/test-helpers.js"></script>
|
||||||
<script src="{{rootURL}}assets/core-tests.js"></script>
|
<script src="{{rootURL}}assets/core-tests.js"></script>
|
||||||
|
@ -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"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -8,6 +8,7 @@
|
|||||||
"discourse-widget-hbs",
|
"discourse-widget-hbs",
|
||||||
"pretty-text",
|
"pretty-text",
|
||||||
"select-kit",
|
"select-kit",
|
||||||
"truth-helpers"
|
"truth-helpers",
|
||||||
|
"wizard"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import MultiSelectComponent from "select-kit/components/multi-select";
|
|||||||
import { computed } from "@ember/object";
|
import { computed } from "@ember/object";
|
||||||
import { isDevelopment } from "discourse-common/config/environment";
|
import { isDevelopment } from "discourse-common/config/environment";
|
||||||
import { makeArray } from "discourse-common/lib/helpers";
|
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({
|
export default MultiSelectComponent.extend({
|
||||||
pluginApiIdentifiers: ["icon-picker"],
|
pluginApiIdentifiers: ["icon-picker"],
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
let ajax;
|
|
||||||
if (window.Discourse) {
|
|
||||||
ajax = requirejs("discourse/lib/ajax").ajax;
|
|
||||||
} else {
|
|
||||||
ajax = requirejs("wizard/lib/ajax").ajax;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ajax };
|
|
@ -1,6 +1,6 @@
|
|||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import Mixin from "@ember/object/mixin";
|
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 getURL from "discourse-common/lib/get-url";
|
||||||
import { isEmpty } from "@ember/utils";
|
import { isEmpty } from "@ember/utils";
|
||||||
import { makeArray } from "discourse-common/lib/helpers";
|
import { makeArray } from "discourse-common/lib/helpers";
|
||||||
|
@ -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
|
|
@ -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;
|
|
||||||
});
|
|
@ -1,5 +0,0 @@
|
|||||||
// discourse-skip-module
|
|
||||||
(function () {
|
|
||||||
const wizard = require("wizard/wizard").default.create();
|
|
||||||
wizard.start();
|
|
||||||
})();
|
|
@ -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
|
|
2
app/assets/javascripts/wizard.js
Normal file
2
app/assets/javascripts/wizard.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
//= require discourse/app/lib/export-result
|
||||||
|
//= require_tree ./wizard/addon
|
1
app/assets/javascripts/wizard/.npmrc
Normal file
1
app/assets/javascripts/wizard/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
engine-strict = true
|
@ -31,6 +31,18 @@ export default createPreviewComponent(659, 320, {
|
|||||||
this.wizard.off("homepageStyleChanged", this.onHomepageStyleChange);
|
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) {
|
mouseDown(e) {
|
||||||
const slider = this.element.querySelector(".previews");
|
const slider = this.element.querySelector(".previews");
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
@ -40,7 +52,8 @@ export default createPreviewComponent(659, 320, {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
mouseLeave() {
|
@bind
|
||||||
|
handleMouseLeave() {
|
||||||
this.set("draggingActive", false);
|
this.set("draggingActive", false);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -48,7 +61,8 @@ export default createPreviewComponent(659, 320, {
|
|||||||
this.set("draggingActive", false);
|
this.set("draggingActive", false);
|
||||||
},
|
},
|
||||||
|
|
||||||
mouseMove(e) {
|
@bind
|
||||||
|
handleMouseMove(e) {
|
||||||
if (!this.draggingActive) {
|
if (!this.draggingActive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
@ -3,8 +3,8 @@ import discourseComputed from "discourse-common/utils/decorators";
|
|||||||
import { action, set } from "@ember/object";
|
import { action, set } from "@ember/object";
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
init(...args) {
|
init() {
|
||||||
this._super(...args);
|
this._super(...arguments);
|
||||||
|
|
||||||
if (this.field.id === "color_scheme") {
|
if (this.field.id === "color_scheme") {
|
||||||
for (let choice of this.field.choices) {
|
for (let choice of this.field.choices) {
|
||||||
@ -17,10 +17,7 @@ export default Component.extend({
|
|||||||
|
|
||||||
@discourseComputed("field.id")
|
@discourseComputed("field.id")
|
||||||
componentName(id) {
|
componentName(id) {
|
||||||
if (id === "color_scheme") {
|
return id === "color_scheme" ? "color-palettes" : "combo-box";
|
||||||
return "color-palettes";
|
|
||||||
}
|
|
||||||
return "combo-box";
|
|
||||||
},
|
},
|
||||||
|
|
||||||
keyPress(e) {
|
keyPress(e) {
|
@ -4,7 +4,6 @@ import I18n from "I18n";
|
|||||||
import { dasherize } from "@ember/string";
|
import { dasherize } from "@ember/string";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { getOwner } from "discourse-common/lib/get-owner";
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
import { getToken } from "wizard/lib/ajax";
|
|
||||||
import getUrl from "discourse-common/lib/get-url";
|
import getUrl from "discourse-common/lib/get-url";
|
||||||
import Uppy from "@uppy/core";
|
import Uppy from "@uppy/core";
|
||||||
import DropTarget from "@uppy/drop-target";
|
import DropTarget from "@uppy/drop-target";
|
||||||
@ -37,7 +36,7 @@ export default Component.extend({
|
|||||||
this._uppyInstance.use(XHRUpload, {
|
this._uppyInstance.use(XHRUpload, {
|
||||||
endpoint: getUrl("/uploads.json"),
|
endpoint: getUrl("/uploads.json"),
|
||||||
headers: {
|
headers: {
|
||||||
"X-CSRF-Token": getToken(),
|
"X-CSRF-Token": this.session.csrfToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -150,8 +150,10 @@ export default Component.extend({
|
|||||||
|
|
||||||
if (result.warnings.length) {
|
if (result.warnings.length) {
|
||||||
const unwarned = result.warnings.filter((w) => !alreadyWarned[w]);
|
const unwarned = result.warnings.filter((w) => !alreadyWarned[w]);
|
||||||
|
|
||||||
if (unwarned.length) {
|
if (unwarned.length) {
|
||||||
unwarned.forEach((w) => (alreadyWarned[w] = true));
|
unwarned.forEach((w) => (alreadyWarned[w] = true));
|
||||||
|
|
||||||
return window.bootbox.confirm(
|
return window.bootbox.confirm(
|
||||||
unwarned.map((w) => I18n.t(`wizard.${w}`)).join("\n"),
|
unwarned.map((w) => I18n.t(`wizard.${w}`)).join("\n"),
|
||||||
I18n.t("no_value"),
|
I18n.t("no_value"),
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
@ -1,13 +1,15 @@
|
|||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import ValidState from "wizard/mixins/valid-state";
|
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";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default EmberObject.extend(ValidState, {
|
export default EmberObject.extend(ValidState, {
|
||||||
id: null,
|
id: null,
|
||||||
|
|
||||||
@discourseComputed("index")
|
@discourseComputed("index")
|
||||||
displayIndex: (index) => index + 1,
|
displayIndex(index) {
|
||||||
|
return index + 1;
|
||||||
|
},
|
||||||
|
|
||||||
@discourseComputed("fields.[]")
|
@discourseComputed("fields.[]")
|
||||||
fieldsById(fields) {
|
fieldsById(fields) {
|
||||||
@ -48,8 +50,8 @@ export default EmberObject.extend(ValidState, {
|
|||||||
url: `/wizard/steps/${this.id}`,
|
url: `/wizard/steps/${this.id}`,
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
data: { fields },
|
data: { fields },
|
||||||
}).catch((response) => {
|
}).catch((error) => {
|
||||||
response.responseJSON.errors.forEach((err) =>
|
error.jqXHR.responseJSON.errors.forEach((err) =>
|
||||||
this.fieldError(err.field, err.description)
|
this.fieldError(err.field, err.description)
|
||||||
);
|
);
|
||||||
});
|
});
|
@ -2,12 +2,11 @@ import EmberObject from "@ember/object";
|
|||||||
import Evented from "@ember/object/evented";
|
import Evented from "@ember/object/evented";
|
||||||
import Step from "wizard/models/step";
|
import Step from "wizard/models/step";
|
||||||
import WizardField from "wizard/models/wizard-field";
|
import WizardField from "wizard/models/wizard-field";
|
||||||
import { ajax } from "wizard/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import { readOnly } from "@ember/object/computed";
|
||||||
|
|
||||||
const Wizard = EmberObject.extend(Evented, {
|
const Wizard = EmberObject.extend(Evented, {
|
||||||
@discourseComputed("steps.length")
|
totalSteps: readOnly("steps.length"),
|
||||||
totalSteps: (length) => length,
|
|
||||||
|
|
||||||
getTitle() {
|
getTitle() {
|
||||||
const titleStep = this.steps.findBy("id", "forum-title");
|
const titleStep = this.steps.findBy("id", "forum-title");
|
||||||
@ -53,8 +52,7 @@ const Wizard = EmberObject.extend(Evented, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function findWizard() {
|
export function findWizard() {
|
||||||
return ajax({ url: "/wizard.json" }).then((response) => {
|
return ajax({ url: "/wizard.json" }).then(({ wizard }) => {
|
||||||
const wizard = response.wizard;
|
|
||||||
wizard.steps = wizard.steps.map((step) => {
|
wizard.steps = wizard.steps.map((step) => {
|
||||||
const stepObj = Step.create(step);
|
const stepObj = Step.create(step);
|
||||||
stepObj.fields = stepObj.fields.map((f) => WizardField.create(f));
|
stepObj.fields = stepObj.fields.map((f) => WizardField.create(f));
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,5 @@
|
|||||||
|
export default function () {
|
||||||
|
this.route("wizard", function () {
|
||||||
|
this.route("step", { path: "/steps/:step_id" });
|
||||||
|
});
|
||||||
|
}
|
17
app/assets/javascripts/wizard/addon/routes/wizard-step.js
Normal file
17
app/assets/javascripts/wizard/addon/routes/wizard-step.js
Normal 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 });
|
||||||
|
},
|
||||||
|
});
|
21
app/assets/javascripts/wizard/addon/routes/wizard.js
Normal file
21
app/assets/javascripts/wizard/addon/routes/wizard.js
Normal 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);
|
||||||
|
},
|
||||||
|
});
|
@ -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}}
|
6
app/assets/javascripts/wizard/addon/templates/step.hbs
Normal file
6
app/assets/javascripts/wizard/addon/templates/step.hbs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{{wizard-step
|
||||||
|
step=step
|
||||||
|
wizard=wizard
|
||||||
|
goNext=(action "goNext")
|
||||||
|
goBack=(action "goBack")
|
||||||
|
}}
|
46
app/assets/javascripts/wizard/addon/templates/wizard.hbs
Normal file
46
app/assets/javascripts/wizard/addon/templates/wizard.hbs
Normal file
File diff suppressed because one or more lines are too long
@ -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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
0
app/assets/javascripts/wizard/app/.gitkeep
Normal file
0
app/assets/javascripts/wizard/app/.gitkeep
Normal 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)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -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)}`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -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"));
|
|
||||||
},
|
|
||||||
});
|
|
9
app/assets/javascripts/wizard/ember-cli-build.js
Normal file
9
app/assets/javascripts/wizard/ember-cli-build.js
Normal 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();
|
||||||
|
};
|
20
app/assets/javascripts/wizard/index.js
Normal file
20
app/assets/javascripts/wizard/index.js
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
@ -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);
|
|
||||||
},
|
|
||||||
};
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
57
app/assets/javascripts/wizard/package.json
Normal file
57
app/assets/javascripts/wizard/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
@ -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();
|
|
||||||
},
|
|
||||||
});
|
|
@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
@ -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
@ -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>
|
|
@ -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}}
|
|
@ -1 +0,0 @@
|
|||||||
{{wizard-step step=step wizard=wizard goNext=(action "goNext") goBack=(action "goBack")}}
|
|
@ -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"), "can’t 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"), "can’t 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"));
|
|
||||||
});
|
|
@ -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"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
@ -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));
|
|
||||||
});
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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"));
|
|
||||||
});
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
@ -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;
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
@ -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">
|
'<svg xmlns="http://www.w3.org/2000/svg" width="415.2" height="414" viewBox="0 0 415.2 414">
|
||||||
<defs>
|
<defs>
|
||||||
<style>
|
<style>
|
||||||
.cls-1 {fill: #adaeb0;}
|
.cls-1 {fill: #{rgba($primary-low-mid, 0.6)};}
|
||||||
.cls-2 {opacity: 0.45;}
|
.cls-2 {opacity: 0.45;}
|
||||||
.cls-3 {opacity: 0.65;}
|
.cls-3 {opacity: 0.65;}
|
||||||
.cls-4 {opacity: 0.35;}
|
.cls-4 {opacity: 0.35;}
|
||||||
@ -720,35 +720,21 @@ $bubbles-mask: svg-uri(
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.wizard {
|
body.wizard {
|
||||||
background-color: var(--secondary);
|
background: var(--secondary) $bubbles-mask;
|
||||||
background-repeat: repeat;
|
|
||||||
background-position: left top;
|
|
||||||
color: var(--primary-very-high);
|
color: var(--primary-very-high);
|
||||||
line-height: $line-height-large;
|
line-height: $line-height-large;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
Oxygen-Sans, Ubuntu, Cantarell, Arial, sans-serif;
|
Oxygen-Sans, Ubuntu, Cantarell, Arial, sans-serif;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
#wizard-main {
|
#wizard-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
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 {
|
.text-field input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: $font-up-1;
|
font-size: $font-up-1;
|
||||||
|
margin: 0;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
border: 1px solid var(--primary-low-mid);
|
border: 1px solid var(--primary-low-mid);
|
||||||
@ -1238,11 +1225,6 @@ body.wizard {
|
|||||||
.radio-field-choice {
|
.radio-field-choice {
|
||||||
margin-bottom: 1.25em;
|
margin-bottom: 1.25em;
|
||||||
|
|
||||||
input {
|
|
||||||
/* TODO: Custom :focus style */
|
|
||||||
/* outline: 0;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-label {
|
.radio-label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
@ -1250,8 +1232,7 @@ body.wizard {
|
|||||||
.radio-description {
|
.radio-description {
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
margin-left: 1.75em;
|
margin-left: 1.75em;
|
||||||
color: #777;
|
color: var(--primary-high);
|
||||||
color: var(--primary-low-mid);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ class BootstrapController < ApplicationController
|
|||||||
add_style(mobile_view? ? :mobile : :desktop)
|
add_style(mobile_view? ? :mobile : :desktop)
|
||||||
end
|
end
|
||||||
add_style(:admin) if staff?
|
add_style(:admin) if staff?
|
||||||
|
add_style(:wizard) if admin?
|
||||||
|
|
||||||
assets_fake_request = ActionDispatch::Request.new(request.env.dup)
|
assets_fake_request = ActionDispatch::Request.new(request.env.dup)
|
||||||
assets_for_url = params[:for_url]
|
assets_for_url = params[:for_url]
|
||||||
@ -51,10 +52,15 @@ class BootstrapController < ApplicationController
|
|||||||
if ExtraLocalesController.client_overrides_exist?
|
if ExtraLocalesController.client_overrides_exist?
|
||||||
extra_locales << ExtraLocalesController.url('overrides')
|
extra_locales << ExtraLocalesController.url('overrides')
|
||||||
end
|
end
|
||||||
|
|
||||||
if staff?
|
if staff?
|
||||||
extra_locales << ExtraLocalesController.url('admin')
|
extra_locales << ExtraLocalesController.url('admin')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if admin?
|
||||||
|
extra_locales << ExtraLocalesController.url('wizard')
|
||||||
|
end
|
||||||
|
|
||||||
plugin_js = Discourse.find_plugin_js_assets(
|
plugin_js = Discourse.find_plugin_js_assets(
|
||||||
include_official: allow_plugins?,
|
include_official: allow_plugins?,
|
||||||
include_unofficial: allow_third_party_plugins?,
|
include_unofficial: allow_third_party_plugins?,
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class WizardController < ApplicationController
|
class WizardController < ApplicationController
|
||||||
requires_login except: [:qunit]
|
requires_login
|
||||||
|
|
||||||
before_action :ensure_admin, except: [:qunit]
|
before_action :ensure_admin
|
||||||
before_action :ensure_wizard_enabled, only: [:index]
|
before_action :ensure_wizard_enabled
|
||||||
skip_before_action :check_xhr, :preload_json
|
|
||||||
|
|
||||||
layout false
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@ -15,12 +12,10 @@ class WizardController < ApplicationController
|
|||||||
wizard = Wizard::Builder.new(current_user).build
|
wizard = Wizard::Builder.new(current_user).build
|
||||||
render_serialized(wizard, WizardSerializer)
|
render_serialized(wizard, WizardSerializer)
|
||||||
end
|
end
|
||||||
format.html {}
|
|
||||||
|
format.html do
|
||||||
|
render body: nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def qunit
|
|
||||||
raise Discourse::InvalidAccess.new if Rails.env.production?
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# base.rb uses this style of require, so maintain usage of it here
|
|
||||||
|
|
||||||
module Jobs
|
module Jobs
|
||||||
class CriticalUserEmail < UserEmail
|
class CriticalUserEmail < UserEmail
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user