FEATURE: revamped wizard (#17477)

* FEATURE: revamped wizard

* UX: Wizard redesign (#17381)

* UX: Step 1-2

* swap out images

* UX: Finalize all steps

* UX: mobile

* UX: Fix test

* more test

* DEV: remove unneeded wizard components

* DEV: fix wizard tests

* DEV: update rails tests for new wizard

* Remove empty hbs files that were created because of rebase

* Fixes for rebase

* Fix wizard image link

* More rebase fixes

* Fix rails tests

* FIX: Update preview for new color schemes: (#17481)

* UX: make layout more responsive, update images

* fix typo

* DEV: move discourse logo svg to template only component

* DEV: formatting improvements

* Remove unneeded files

* Add tests for privacy step

* Fix banner image height for step "ready"

Co-authored-by: Jordan Vidrine <30537603+jordanvidrine@users.noreply.github.com>
Co-authored-by: awesomerobot <kris.aubuchon@discourse.org>
This commit is contained in:
Arpit Jalan 2022-07-27 06:53:01 +05:30 committed by GitHub
parent 021200167c
commit 10f200a5d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1088 additions and 2402 deletions

View File

@ -7,60 +7,74 @@ acceptance("Wizard", function (needs) {
test("Wizard starts", async function (assert) {
await visit("/wizard");
assert.ok(exists(".wizard-column-contents"));
assert.ok(exists(".wizard-container"));
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-container__step"));
assert.ok(
exists(".wizard-step-hello-world"),
exists(".wizard-container__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"),
!exists(".wizard-container__button.finish"),
"cannot finish on first step"
);
assert.ok(exists(".wizard-container__step-progress"));
assert.ok(exists(".wizard-container__step-title"));
assert.ok(exists(".wizard-container__step-description"));
assert.ok(
!exists(".invalid #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"));
assert.ok(!exists(".wizard-container__button.back"));
assert.ok(!exists(".wizard-container__field .error"));
// invalid data
await click(".wizard-btn.next");
assert.ok(exists(".invalid .field-full-name"));
await click(".wizard-container__button.next");
assert.ok(exists(".invalid #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"));
await fillIn("input#full_name", "Server Fail");
await click(".wizard-container__button.next");
assert.ok(exists(".invalid #full_name"));
assert.ok(exists(".wizard-container__field .error"));
// 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"));
await fillIn("input#full_name", "Evil Trout");
await click(".wizard-container__button.next");
assert.ok(!exists(".wizard-container__field .error"));
assert.ok(!exists(".wizard-container__step-description"));
assert.ok(
exists(".wizard-btn.finish"),
exists(".wizard-container__button.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(".wizard-container__button.next");
assert.ok(
exists(".dropdown-field.dropdown-snack"),
"went to the next step"
);
assert.ok(
exists(".wizard-container__preview"),
"renders the component field"
);
assert.ok(
exists(".wizard-container__button.jump-in"),
"last step shows a jump in button"
);
assert.ok(exists(".wizard-container__link.back"), "shows the back button");
assert.ok(!exists(".wizard-container__step-title"));
assert.ok(
!exists(".wizard-container__button.finish"),
"cannot finish on last step"
);
await click(".action-link.back");
assert.ok(exists(".wizard-step-title"));
assert.ok(exists(".wizard-btn.next"));
await click(".wizard-container__link.back");
assert.ok(exists(".wizard-container__step-title"));
assert.ok(exists(".wizard-container__button.next"));
assert.ok(!exists(".wizard-prev"));
});
});

View File

@ -1,73 +0,0 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { click, fillIn, render } from "@ember/test-helpers";
import { count, exists } from "discourse/tests/helpers/qunit-helpers";
import { hbs } from "ember-cli-htmlbars";
module("Integration | Component | Wizard | invite-list", function (hooks) {
setupRenderingTest(hooks);
test("can add users", async function (assert) {
this.set("field", {});
await render(hbs`<InviteList @field={{this.field}} />`);
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

@ -4,7 +4,7 @@ import {
darkLightDiff,
} from "wizard/lib/preview";
export default createPreviewComponent(659, 320, {
export default createPreviewComponent(342, 322, {
logo: null,
avatar: null,
@ -22,9 +22,7 @@ export default createPreviewComponent(659, 320, {
},
paint({ ctx, colors, font, width, height }) {
if (this.logo) {
this.drawFullHeader(colors, font, this.logo);
}
this.drawFullHeader(colors, font, this.logo);
if (this.get("step.fieldsById.homepage_style.value") === "latest") {
this.drawPills(colors, font, height * 0.15);
@ -361,7 +359,10 @@ export default createPreviewComponent(659, 320, {
renderLatest(ctx, colors, font, width, height) {
const rowHeight = height / 6.6;
const textColor = darkLightDiff(colors.primary, colors.secondary, 50, 50);
// accounts for hard-set color variables in solarized themes
const textColor =
colors.primary_medium ||
darkLightDiff(colors.primary, colors.secondary, 50, 50);
const bodyFontSize = height / 440.0;
ctx.font = `${bodyFontSize}em '${font}'`;
@ -370,12 +371,10 @@ export default createPreviewComponent(659, 320, {
const drawLine = (y) => {
ctx.beginPath();
ctx.strokeStyle = darkLightDiff(
colors.primary,
colors.secondary,
90,
-75
);
// accounts for hard-set color variables in solarized themes
ctx.strokeStyle =
colors.primary_low ||
darkLightDiff(colors.primary, colors.secondary, 90, -75);
ctx.moveTo(margin, y);
ctx.lineTo(width - margin, y);
ctx.stroke();

View File

@ -1,11 +0,0 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["invite-list-user"],
@discourseComputed("user.role")
roleName(role) {
return this.roles.findBy("id", role).label;
},
});

View File

@ -1,78 +0,0 @@
import Component from "@ember/component";
import I18n from "I18n";
import { schedule } from "@ember/runloop";
import { action } from "@ember/object";
export default Component.extend({
classNames: ["invite-list"],
users: null,
inviteEmail: "",
inviteRole: "",
invalid: false,
init() {
this._super(...arguments);
this.set("users", []);
this.set("roles", [
{ id: "moderator", label: I18n.t("wizard.invites.roles.moderator") },
{ id: "regular", label: I18n.t("wizard.invites.roles.regular") },
]);
this.set("inviteRole", this.get("roles.0.id"));
this.updateField();
},
keyPress(e) {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
this.send("addUser");
}
},
updateField() {
const users = this.users;
this.set("field.value", JSON.stringify(users));
const staffCount = this.get("step.fieldsById.staff_count.value") || 1;
const showWarning = staffCount < 3 && users.length === 0;
this.set("field.warning", showWarning ? "invites.none_added" : null);
},
@action
addUser() {
const user = {
email: this.inviteEmail || "",
role: this.inviteRole,
};
if (!/(.+)@(.+){2,}\.(.+){2,}/.test(user.email)) {
return this.set("invalid", true);
}
const users = this.users;
if (users.findBy("email", user.email)) {
return this.set("invalid", true);
}
this.set("invalid", false);
users.pushObject(user);
this.updateField();
this.set("inviteEmail", "");
schedule("afterRender", () =>
this.element.querySelector(".invite-email").focus()
);
},
@action
removeUser(user) {
this.users.removeObject(user);
this.updateField();
},
});

View File

@ -1,7 +0,0 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
@discourseComputed("field.value")
showStaffCount: (staffCount) => staffCount > 1,
});

View File

@ -13,7 +13,7 @@ Nullam eget sem non elit tincidunt rhoncus. Fusce
velit nisl, porttitor sed nisl ac, consectetur interdum
metus. Fusce in consequat augue, vel facilisis felis.`;
export default createPreviewComponent(659, 320, {
export default createPreviewComponent(642, 322, {
logo: null,
avatar: null,
previewTopic: true,
@ -113,9 +113,7 @@ export default createPreviewComponent(659, 320, {
paint({ ctx, colors, font, headingFont, width, height }) {
const headerHeight = height * 0.3;
if (this.logo) {
this.drawFullHeader(colors, headingFont, this.logo);
}
this.drawFullHeader(colors, headingFont, this.logo);
const margin = 20;
const avatarSize = height * 0.15;
@ -152,8 +150,10 @@ export default createPreviewComponent(659, 320, {
ctx.beginPath();
ctx.rect(margin, line + lineHeight, shareButtonWidth, height * 0.1);
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 90, 65);
ctx.fill();
// accounts for hard-set color variables in solarized themes
ctx.fillStyle =
colors.primary_low ||
darkLightDiff(colors.primary, colors.secondary, 90, 65);
ctx.fillStyle = chooseDarker(colors.primary, colors.secondary);
ctx.font = `${bodyFontSize}em '${font}'`;
ctx.fillText(

View File

@ -1,3 +1,5 @@
import Component from "@ember/component";
export default Component.extend({});
export default Component.extend({
tagName: "",
});

View File

@ -10,7 +10,7 @@ import DropTarget from "@uppy/drop-target";
import XHRUpload from "@uppy/xhr-upload";
export default Component.extend({
classNames: ["wizard-image-row"],
classNames: ["wizard-container__image-upload"],
uploading: false,
@discourseComputed("field.id")

View File

@ -1,9 +0,0 @@
import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
@action
changed(value) {
this.set("field.value", value);
},
});

View File

@ -1,6 +0,0 @@
import Component from "@ember/component";
export default Component.extend({
keyPress(e) {
e.stopPropagation();
},
});

View File

@ -3,7 +3,11 @@ import { dasherize } from "@ember/string";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
classNameBindings: [":wizard-field", "typeClasses", "field.invalid"],
classNameBindings: [
":wizard-container__field",
"typeClasses",
"field.invalid",
],
@discourseComputed("field.type", "field.id")
typeClasses: (type, id) =>

View File

@ -1,9 +1,5 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
classNameBindings: [":wizard-step-form", "customStepClass"],
@discourseComputed("step.id")
customStepClass: (stepId) => `wizard-step-${stepId}`,
classNameBindings: [":wizard-container__step-form"],
});

View File

@ -9,7 +9,7 @@ import { action } from "@ember/object";
const alreadyWarned = {};
export default Component.extend({
classNames: ["wizard-step"],
classNameBindings: [":wizard-container__step", "stepClass"],
saving: null,
didInsertElement() {
@ -17,27 +17,40 @@ export default Component.extend({
this.autoFocus();
},
@discourseComputed("step.index")
showQuitButton: (index) => index === 0,
@discourseComputed("step.displayIndex", "wizard.totalSteps")
showNextButton: (current, total) => current < total,
showNextButton(current, total) {
return current < total;
},
@discourseComputed("step.displayIndex", "wizard.totalSteps")
showDoneButton: (current, total) => current === total,
@discourseComputed("step.id", "step.displayIndex", "wizard.totalSteps")
showDoneButton(step, current, total) {
return step === "ready" || current === total;
},
@discourseComputed(
"step.index",
"step.displayIndex",
"wizard.totalSteps",
"wizard.completed"
)
showFinishButton: (index, displayIndex, total, completed) => {
return index !== 0 && displayIndex !== total && completed;
@discourseComputed("step.id")
showFinishButton(step) {
return step === "styling" || step === "branding";
},
@discourseComputed("step.index")
showBackButton: (index) => index > 0,
showBackButton(index) {
return index > 0;
},
@discourseComputed("step.id")
nextButtonLabel(step) {
return `wizard.${step === "ready" ? "configure_more" : "next"}`;
},
@discourseComputed("step.id")
nextButtonClass(step) {
return step === "ready" ? "configure-more" : "next";
},
@discourseComputed("step.id")
stepClass(step) {
return step;
},
@discourseComputed("step.banner")
bannerImage(src) {
@ -47,9 +60,9 @@ export default Component.extend({
return getUrl(`/images/wizard/${src}`);
},
@discourseComputed("step.id")
bannerAndDescriptionClass(id) {
return `wizard-banner-and-description wizard-banner-and-description-${id}`;
@discourseComputed()
bannerAndDescriptionClass() {
return `wizard-container__step-banner`;
},
@observes("step.id")
@ -89,7 +102,7 @@ export default Component.extend({
autoFocus() {
schedule("afterRender", () => {
const $invalid = $(
".wizard-field.invalid:nth-of-type(1) .wizard-focusable"
".wizard-container__input.invalid:nth-of-type(1) .wizard-focusable"
);
if ($invalid.length) {

View File

@ -191,7 +191,10 @@ export function createPreviewComponent(width, height, obj) {
avatarSize,
avatarSize
);
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 45, 55);
// accounts for hard-set color variables in solarized themes
ctx.fillStyle =
colors.primary_low_mid ||
darkLightDiff(colors.primary, colors.secondary, 45, 55);
const pathScale = headerHeight / 1200;
// search icon SVG path

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -1,6 +0,0 @@
<span class="email">{{this.user.email}}</span>
<span class="role">{{this.roleName}}</span>
<button type="button" class="wizard-btn small danger remove-user" {{action this.removeUser this.user}}>
{{d-icon "times"}}
</button>

View File

@ -1,20 +0,0 @@
{{#if this.users}}
<div class="users-list">
{{#each this.users as |user|}}
<InviteListUser @user={{user}} @roles={{this.roles}} @removeUser={{action "removeUser"}} />
{{/each}}
</div>
{{/if}}
<div class="new-user">
<div class="text-field {{if this.invalid "invalid"}}">
<Input class="invite-email wizard-focusable" @value={{this.inviteEmail}} placeholder="user@example.com" tabindex="9" />
</div>
<ComboBox @value={{this.inviteRole}} @content={{this.roles}} @nameProperty="label" @onChange={{action (mut this.inviteRole)}} />
<button type="button" class="wizard-btn small add-user" {{action "addUser"}}>
{{d-icon "plus"}}{{i18n "wizard.invites.add_user"}}
</button>
</div>

View File

@ -1,5 +0,0 @@
{{#if this.showStaffCount}}
<div class="staff-count">
{{i18n "wizard.staff_count" count=this.field.value}}
</div>
{{/if}}

View File

@ -1,9 +1,9 @@
<div class="previews {{if this.draggingActive "dragging"}}">
<div class="preview-area topic-preview">
<div class="wizard-container__preview topic-preview">
<canvas width={{this.elementWidth}} height={{this.elementHeight}} style={{this.canvasStyle}}>
</canvas>
</div>
<div class="preview-area homepage-preview">
<div class="wizard-container__preview homepage-preview">
<HomepagePreview @wizard={{this.wizard}} @step={{this.step}} />
</div>
</div>

View File

@ -1,4 +1,4 @@
<div class="preview-area">
<div class="wizard-container__preview">
<canvas
width={{this.elementWidth}}
height={{this.elementHeight}}

View File

@ -1,4 +1,10 @@
<label>
<Input @type="checkbox" class="wizard-checkbox" @checked={{this.field.value}} />
{{this.field.placeholder}}
<label class="wizard-container__label">
<Input @type="checkbox" class="wizard-container__checkbox" @checked={{this.field.value}} />
<span class="wizard-container__checkbox-slider"></span>
{{#if this.field.icon}}
{{d-icon this.field.icon}}
{{/if}}
<span class="wizard-container__checkbox-label">
{{this.field.placeholder}}
</span>
</label>

View File

@ -1,7 +1,7 @@
{{#each this.field.choices as |c|}}
<div class="checkbox-field-choice {{this.fieldClass}}">
<label id={{c.id}} value={{c.label}}>
<Input @type="checkbox" class="wizard-checkbox" @checked={{c.checked}} {{on "click" (action "changed")}} />
<Input @type="checkbox" class="wizard-container__checkbox" @checked={{c.checked}} {{on "click" (action "changed")}} />
{{#if c.icon}}
{{d-icon c.icon}}
{{/if}}

View File

@ -1,6 +1,6 @@
{{component
this.componentName
class=this.fieldClass
class="wizard-container__dropdown"
value=this.field.value
content=this.field.choices
nameProperty="label"

View File

@ -2,7 +2,7 @@
{{component this.previewComponent field=this.field fieldClass=this.fieldClass wizard=this.wizard}}
{{/if}}
<label class="wizard-btn wizard-btn-upload {{if this.uploading "disabled"}}">
<label class="wizard-container__button wizard-container__button-upload {{if this.uploading "disabled"}}">
{{#if this.uploading}}
{{i18n "wizard.uploading"}}
{{else}}

View File

@ -1,25 +0,0 @@
{{#each this.field.choices as |choice|}}
<div class="radio-field-choice {{this.fieldClass}}">
<div class="radio-area">
<RadioButton @selection={{this.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

@ -1 +1 @@
<Input id={{this.field.id}} @value={{this.field.value}} class={{this.fieldClass}} placeholder={{this.field.placeholder}} tabindex="9" />
<Input id={{this.field.id}} @value={{this.field.value}} class="wizard-container__text-input" placeholder={{this.field.placeholder}} tabindex="9" />

View File

@ -1 +0,0 @@
<Textarea id={{this.field.id}} @value={{this.field.value}} class={{this.fieldClass}} placeholder={{this.field.placeholder}} tabindex="9" />

View File

@ -1,18 +1,20 @@
<label for={{this.field.id}}>
<span class="label-value">
{{this.field.label}}
{{#if this.field.label}}
<label for={{this.field.id}}>
<span class="wizard-container__label">
{{this.field.label}}
</span>
{{#if this.field.required}}
<span class="field-required">*</span>
<span class="wizard-container__label required">*</span>
{{/if}}
</span>
{{#if this.field.description}}
<div class="field-description">{{html-safe this.field.description}}</div>
{{/if}}
</label>
{{#if this.field.description}}
<div class="wizard-container__description">{{html-safe this.field.description}}</div>
{{/if}}
</label>
{{/if}}
<div class="input-area">
<div class="wizard-container__input">
{{component
this.inputComponentName
field=this.field
@ -23,9 +25,9 @@
</div>
{{#if this.field.errorDescription}}
<div class="field-error-description">{{html-safe this.field.errorDescription}}</div>
<div class="wizard-container__description error">{{html-safe this.field.errorDescription}}</div>
{{/if}}
{{#if this.field.extra_description}}
<div class="field-extra-description">{{html-safe this.field.extra_description}}</div>
<div class="wizard-container__description extra">{{html-safe this.field.extra_description}}</div>
{{/if}}

View File

@ -1,74 +1,75 @@
<div class="wizard-step-contents">
<div class="wizard-container__step-contents">
{{#if this.step.title}}
<h1 class="wizard-step-title">{{this.step.title}}</h1>
<h1 class="wizard-container__step-title">{{this.step.title}}</h1>
{{/if}}
<div class={{this.bannerAndDescriptionClass}}>
{{#if this.bannerImage}}
<img src={{this.bannerImage}} class="wizard-step-banner">
<div class="wizard-container__step-container">
{{#if this.step.fields}}
<WizardStepForm @step={{this.step}}>
{{#if this.includeSidebar}}
<div class="wizard-container__sidebar">
{{#each this.step.fields as |field|}}
{{#if field.show_in_sidebar}}
<WizardField @field={{field}} @step={{this.step}} @wizard={{this.wizard}} />
{{/if}}
{{/each}}
</div>
{{/if}}
<div class="wizard-container__fields">
{{#each this.step.fields as |field|}}
{{#unless field.show_in_sidebar}}
<WizardField @field={{field}} @step={{this.step}} @wizard={{this.wizard}} />
{{/unless}}
{{/each}}
</div>
</WizardStepForm>
{{/if}}
{{#if (or this.bannerImage this.step.description)}}
<div class={{this.bannerAndDescriptionClass}}>
{{#if this.step.description}}
<p class="wizard-container__step-description">{{html-safe this.step.description}}</p>
{{/if}}
{{#if this.step.description}}
<p class="wizard-step-description">{{html-safe this.step.description}}</p>
{{/if}}
</div>
<WizardStepForm @step={{this.step}}>
{{#if this.includeSidebar}}
<div class="wizard-fields-sidebar">
{{#each this.step.fields as |field|}}
{{#if field.show_in_sidebar}}
<WizardField @field={{field}} @step={{this.step}} @wizard={{this.wizard}} />
{{/if}}
{{/each}}
{{#if this.bannerImage}}
<img src={{this.bannerImage}} class="wizard-container__step-banner-image">
{{/if}}
</div>
{{/if}}
<div class="wizard-fields-main">
{{#each this.step.fields as |field|}}
{{#unless field.show_in_sidebar}}
<WizardField @field={{field}} @step={{this.step}} @wizard={{this.wizard}} />
{{/unless}}
{{/each}}
</div>
</WizardStepForm>
</div>
</div>
<div class="wizard-step-footer">
<div class="wizard-container__step-footer">
<div class="wizard-progress">
<div class="white"></div>
<div style={{this.barStyle}} class="black"></div>
<div class="screen"></div>
<span>{{bound-i18n "wizard.step" current=this.step.displayIndex total=this.wizard.totalSteps}}</span>
</div>
<div class="wizard-container__buttons">
<div class="wizard-buttons">
{{#if this.showQuitButton}}
<a href {{action "quit"}} tabindex="11" class="action-link quit">{{i18n "wizard.quit"}}</a>
{{#if this.showDoneButton}}
<button {{action "quit"}} disabled={{this.saving}} type="button" class="wizard-container__button jump-in">
{{i18n "wizard.done"}}
</button>
{{/if}}
{{#if this.showNextButton}}
<button {{action "nextStep"}} disabled={{this.saving}} type="button" class="wizard-container__button primary {{this.nextButtonClass}}">
{{i18n this.nextButtonLabel}}
</button>
{{/if}}
{{#if this.showFinishButton}}
<button {{action "exitEarly"}} disabled={{this.saving}} tabindex="10" type="button" class="wizard-btn finish">
<button {{action "exitEarly"}} disabled={{this.saving}} type="button" class="wizard-container__button finish">
{{i18n "wizard.finish"}}
</button>
{{/if}}
{{#if this.showBackButton}}
<a href {{action "backStep"}} tabindex="11" class="action-link back">{{i18n "wizard.back"}}</a>
{{/if}}
</div>
{{#if this.showNextButton}}
<button {{action "nextStep"}} disabled={{this.saving}} tabindex="10" type="button" class="wizard-btn next primary">
{{i18n "wizard.next"}}
{{d-icon "chevron-right"}}
</button>
{{/if}}
<div class="wizard-container__step-progress">
<a href {{action "backStep"}} class="wizard-container__link back {{unless this.showBackButton "inactive"}}">{{d-icon "chevron-left"}}</a>
<span class="wizard-container__step-text">{{bound-i18n "wizard.step-text"}}</span>
<span class="wizard-container__step-count">{{bound-i18n "wizard.step" current=this.step.displayIndex total=this.wizard.totalSteps}}</span>
<a href {{action "nextStep"}} class="wizard-container__link {{unless this.showNextButton "inactive"}}">{{d-icon "chevron-right"}}</a>
{{#if this.showDoneButton}}
<button {{action "quit"}} disabled={{this.saving}} tabindex="10" type="button" class="wizard-btn done">
{{i18n "wizard.done"}}
</button>
{{/if}}
</div>
</div>

File diff suppressed because one or more lines are too long

View File

@ -20,10 +20,10 @@ export default function (helpers) {
description: "Your name",
},
],
next: "second-step",
next: "styling",
},
{
id: "second-step",
id: "styling",
title: "Second step",
index: 1,
fields: [{ id: "some-title", type: "text" }],
@ -38,7 +38,7 @@ export default function (helpers) {
{ id: "theme-preview", type: "component" },
{ id: "an-image", type: "image" },
],
previous: "second-step",
previous: "styling",
},
],
},

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class WizardFieldSerializer < ApplicationSerializer
attributes :id, :type, :required, :value, :label, :placeholder, :description, :extra_description, :show_in_sidebar
attributes :id, :type, :required, :value, :label, :placeholder, :description, :extra_description, :icon, :show_in_sidebar
has_many :choices, serializer: WizardFieldChoiceSerializer, embed: :objects
def id
@ -67,6 +67,14 @@ class WizardFieldSerializer < ApplicationSerializer
extra_description.present?
end
def icon
object.icon
end
def include_icon?
object.icon.present?
end
def show_in_sidebar
object.show_in_sidebar
end

View File

@ -3,7 +3,7 @@
<%= raw(t 'finish_installation.confirm_email.message', email: @email) %>
<div class='row'>
<%= button_to(finish_installation_resend_email_path, method: :put, class: 'wizard-btn primary') do %>
<%= button_to(finish_installation_resend_email_path, method: :put, class: 'wizard-container__button primary') do %>
<%= t 'finish_installation.resend_email.title' %>
<% end %>
</div>

View File

@ -9,7 +9,7 @@
</div>
<div class='row'>
<%= link_to(finish_installation_register_path, class: 'wizard-btn primary') do %>
<%= link_to(finish_installation_register_path, class: 'wizard-container__button primary') do %>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 448 512">
<path fill="currentColor" d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"></path>
</svg>

View File

@ -3,7 +3,7 @@
<%- if @allowed_emails.present? %>
<%= form_tag(finish_installation_register_path) do %>
<div class='wizard-field text-field'>
<div class='wizard-container__input text-field'>
<label for="email">
<span class="label-value"><%= t 'js.user.email.title' %></span>
</label>
@ -15,12 +15,12 @@
</div>
</div>
<div class='wizard-field text-field <% if @user.errors[:username].present? %>invalid<% end %>'>
<div class='wizard-container__input text-field <% if @user.errors[:username].present? %>invalid<% end %>'>
<label for="username">
<span class="label-value"><%= t 'js.user.username.title' %></span>
</label>
<div class='field-description'><%= t 'js.user.username.instructions' %></div>
<div class='wizard-container__description'><%= t 'js.user.username.instructions' %></div>
<div class='input-area'>
<%= text_field_tag(:username, params[:username]) %>
@ -30,12 +30,12 @@
<%- end %>
</div>
<div class='wizard-field text-field <% if @user.errors[:username].present? %>invalid<% end %>'>
<div class='wizard-container__input text-field <% if @user.errors[:username].present? %>invalid<% end %>'>
<label for="password">
<span class="label-value"><%= t 'js.user.password.title' %></span>
</label>
<div class='field-description'><%= t 'js.user.password.instructions', count: SiteSetting.min_admin_password_length %></div>
<div class='wizard-container__description'><%= t 'js.user.password.instructions', count: SiteSetting.min_admin_password_length %></div>
<div class='input-area'>
<%= password_field_tag(:password, params[:password]) %>
@ -45,7 +45,7 @@
<% end %>
</div>
<%= submit_tag(t('finish_installation.register.button'), class: 'wizard-btn primary') %>
<%= submit_tag(t('finish_installation.register.button'), class: 'wizard-container__button primary') %>
<%- end %>
<%- else -%>

View File

@ -10,8 +10,8 @@
<body class='wizard'>
<div id='wizard-main'>
<div class='wizard-column'>
<div class='wizard-column-contents finish-installation'>
<div class='wizard-container'>
<div class='wizard-container-contents finish-installation'>
<%= yield %>
</div>
<div class='wizard-footer'>

View File

@ -5709,15 +5709,16 @@ en:
wizard_js:
wizard:
done: "Done"
finish: "Finish"
done: "Jump in!"
finish: "Exit setup"
back: "Back"
next: "Next"
configure_more: "Configure More..."
step-text: "Step"
step: "%{current} of %{total}"
upload: "Upload"
uploading: "Uploading..."
upload_error: "Sorry, there was an error uploading that file. Please try again."
quit: "Maybe Later"
staff_count:
one: "Your community has %{count} staff (you)."

View File

@ -4891,105 +4891,43 @@ en:
wizard:
title: "Discourse Setup"
step:
locale:
title: "Welcome to your Discourse!"
fields:
default_locale:
description: "Whats the default language for your community?"
forum_title:
title: "Name"
description: "Your name is a sign visible in the distance, the <i>first</i> thing potential visitors will notice about your community. What does your name and title say about your community?"
introduction:
title: "Tell us about your community"
fields:
title:
label: "Your communitys name"
label: "Community name"
placeholder: "Janes Hangout"
site_description:
label: "Describe your community in one short sentence (used in search results and social media)"
label: "Describe your community in a sentence"
placeholder: "A place for Jane and her friends to discuss cool stuff"
short_site_description:
label: "Describe your community in few words (used for the homepage title)"
placeholder: "Best community ever"
introduction:
title: "Introduction"
disabled:
"<p>We couldnt find any topic with the title “%{topic_title}”.</p>
<ul>
<li>If you have changed the title, edit that topic to modify your sites introductory text.</li>
<li>If you have deleted this topic, create another topic with “%{topic_title}” as the title. The content of the first post is your sites introductory text.</li>
</ul>"
fields:
welcome:
label: "Welcome Topic"
description:
"<p>How would you describe your community to a stranger on an elevator in about 1 minute?</p>
<ul>
<li>Who are these discussions for?</li>
<li>What can I find here?</li>
<li>Why should I visit?</li>
</ul>
<p>Your welcome topic is the first thing new arrivals will read. Think of it as your <b>one paragraph</b> 'elevator pitch' or 'mission statement'. </p>"
one_paragraph: "Please restrict your welcome message to one paragraph."
extra_description: "If you are not sure, you can skip this step and edit your welcome topic later."
contact_email:
label: "Point of contact"
placeholder: "example@user.com"
description: "Person or group responsible for this community. Used for critical updates, and listed on <a href='%{base_path}/about' target='_blank'>your about page</a> for urgent contact."
default_locale:
label: "Language"
privacy:
title: "Access"
description: "<p>Is your community open to everyone, or is it restricted by membership, invitation, or approval? If you prefer, you can set things up privately, then switch over to public later.</p>"
title: "Member Experience"
fields:
privacy:
choices:
open:
label: "Public"
description: "Anyone can access this community"
restricted:
label: "Private"
description: "Only logged in users can access this community"
privacy_options:
description: "How do new users sign up for an account?"
choices:
open:
label: "Users can sign up on their own."
invite_only:
label: "Users must be invited by trusted users or staff before they can sign up."
must_approve:
label: "Users can sign up on their own, but must be approved by staff."
login_required:
placeholder: "Private"
extra_description: "Only logged in users can access this community"
invite_only:
placeholder: "Invite Only"
extra_description: "Users must be invited by trusted users or staff, otherwise users can sign up on their own"
must_approve_users:
placeholder: "Require Approval"
extra_description: "Users must be approved by staff"
contact:
title: "Contact"
fields:
contact_email:
label: "Mail"
placeholder: "name@example.com"
description: "Email address for the person or group responsible for this community. Used for critical notifications such as unhandled flags, security updates, and on <a href='%{base_path}/about' target='_blank'>your about page</a> for urgent community contact."
contact_url:
label: "Web Page"
placeholder: "https://www.example.com/contact-us"
description: "General contact web page for you or your organization. Will be displayed on <a href='%{base_path}/about' target='_blank'>your about page</a>."
site_contact:
label: "Automated Messages"
description: "All automated Discourse personal messages will be sent from this user, such as flag warnings and backup completion notices."
corporate:
title: "Organization"
description: "This information will be entered in your <a href='%{base_path}/tos' target='blank'>Terms of Service</a>, which is a topic you can edit in the Staff category. If you dont have a company, feel free to skip this step for now."
fields:
company_name:
label: "Company Name"
placeholder: "Example Organization"
governing_law:
label: "Governing Law"
placeholder: "California law"
city_for_disputes:
label: "City for Disputes"
placeholder: "San Francisco, California"
ready:
title: "Your Discourse is Ready!"
description: "That's it! You've done the basics to setup your community. Now you can jump in and have a look around, write a welcome topic, and send invites!<br><br>Have fun!"
styling:
title: "Styling"
title: "Look & Feel"
fields:
color_scheme:
label: "Color scheme"
@ -5021,25 +4959,45 @@ en:
subcategories_with_featured_topics:
label: "Subcategories with Featured Topics"
logos:
title: "Logos"
branding:
title: "Community Branding"
fields:
logo:
label: "Primary Logo"
description: "The logo image at the top left of your site. Use a wide rectangular image with a height of 120 and an aspect ratio greater than 3:1"
description: "The logo at the top left of your site. Use a wide rectangular image with a height of 120 and an aspect ratio greater than 3:1"
logo_small:
label: "Square Logo"
description: "A square version of your logo. Shown at the top left of your site when scrolling down, in the browser, and when sharing on social platforms. Ideally larger than 512x512."
icons:
title: "Icons"
fields:
description: "A square version of your logo. Shown at the top left when scrolling down, and when sharing on social platforms. Ideally at least 512x512."
favicon:
label: "Browser Icon"
description: "Icon image used to represent your site in web browsers that looks good at small sizes. Recommended image extensions are PNG or JPG. We'll use the square logo by default."
description: "Icon used to represent your site in web browsers that looks good at small sizes. PNG or JPG is recommended. Square logo is used by default."
large_icon:
label: "Large Icon"
description: "Icon image used to represent your site on modern devices that looks good at larger sizes. Ideally larger than 512 × 512. We'll use the square logo by default."
description: "Icon used to represent your site on mobile devices that looks good at larger sizes. Ideally greater than 512x512. Square logo used by default."
corporate:
title: "Contact & Org"
fields:
company_name:
label: "Company Name"
placeholder: "Example Organization"
description: "Entered in your Terms of Service page. Feel free to skip if no company exists."
governing_law:
label: "Governing Law"
placeholder: "California law"
description: "Entered in your Terms of Service page. Feel free to skip if no company exists."
contact_url:
label: "Web Page"
placeholder: "https://www.example.com/contact-us"
description: "General contact web page for you or your organization. Will be displayed on <a href='%{base_path}/about' target='_blank'>your about page</a>."
city_for_disputes:
label: "City for Disputes"
placeholder: "San Francisco, California"
description: "Entered in your Terms of Service page. Feel free to skip if no company exists."
site_contact:
label: "Automated Messages"
description: "All automated Discourse personal messages will be sent from this user, such as flag warnings and backup completion notices."
invites:
title: "Invite Staff"

View File

@ -1,71 +0,0 @@
# frozen_string_literal: true
class IntroductionUpdater
def initialize(user)
@user = user
end
def get_summary
summary_from_post(find_welcome_post)
end
def update_summary(new_value)
post = find_welcome_post
return unless post
previous_value = summary_from_post(post).strip
if previous_value != new_value
revisor = PostRevisor.new(post)
if post.raw.chomp == I18n.t('discourse_welcome_topic.body', base_path: Discourse.base_path).chomp
revisor.revise!(@user, raw: new_value)
else
remaining = post.raw[previous_value.length..-1]
revisor.revise!(@user, raw: "#{new_value}#{remaining}")
end
end
end
protected
def summary_from_post(post)
post ? post.raw.split("\n").first : nil
end
def find_welcome_post
topic_id = SiteSetting.welcome_topic_id
if topic_id <= 0
title = I18n.t("discourse_welcome_topic.title")
topic_id = find_topic_id(title)
end
if topic_id.blank?
title = I18n.t("discourse_welcome_topic.title", locale: :en)
topic_id = find_topic_id(title)
end
if topic_id.blank?
topic_id = Topic.listable_topics
.where(pinned_globally: true)
.order(:created_at)
.limit(1)
.pluck(:id)
end
welcome_topic = Topic.where(id: topic_id).first
return nil if welcome_topic.blank?
welcome_topic.first_post
end
def find_topic_id(topic_title)
slug = Slug.for(topic_title, nil)
return nil if slug.blank?
Topic.listable_topics
.where(slug: slug)
.pluck(:id)
end
end

View File

@ -10,8 +10,12 @@ class Wizard
def build
return @wizard unless SiteSetting.wizard_enabled? && @wizard.user.try(:staff?)
@wizard.append_step('locale') do |step|
step.banner = "welcome.png"
@wizard.append_step('introduction') do |step|
step.banner = "welcome-illustration.svg"
step.add_field(id: 'title', type: 'text', required: true, value: SiteSetting.title)
step.add_field(id: 'site_description', type: 'text', required: false, value: SiteSetting.site_description)
step.add_field(id: 'contact_email', type: 'text', required: true, value: SiteSetting.contact_email)
languages = step.add_field(id: 'default_locale',
type: 'dropdown',
@ -23,6 +27,12 @@ class Wizard
end
step.on_update do |updater|
updater.ensure_changed(:title)
if updater.errors.blank?
updater.apply_settings(:title, :site_description, :contact_email)
end
old_locale = SiteSetting.default_locale
updater.apply_setting(:default_locale)
@ -37,108 +47,39 @@ class Wizard
end
end
@wizard.append_step('forum-title') do |step|
step.add_field(id: 'title', type: 'text', required: true, value: SiteSetting.title)
step.add_field(id: 'site_description', type: 'text', required: false, value: SiteSetting.site_description)
step.add_field(id: 'short_site_description', type: 'text', required: false, value: SiteSetting.short_site_description)
step.on_update do |updater|
updater.ensure_changed(:title)
if updater.errors.blank?
updater.apply_settings(:title, :site_description, :short_site_description)
end
end
end
@wizard.append_step('introduction') do |step|
introduction = IntroductionUpdater.new(@wizard.user)
if @wizard.completed_steps?('introduction') && !introduction.get_summary
step.disabled = true
step.description_vars = { topic_title: I18n.t("discourse_welcome_topic.title") }
else
step.add_field(id: 'welcome', type: 'textarea', required: false, value: introduction.get_summary)
step.on_update do |updater|
value = updater.fields[:welcome].strip
if value.index("\n")
updater.errors.add(:welcome, I18n.t("wizard.step.introduction.fields.welcome.one_paragraph"))
else
introduction.update_summary(value)
end
end
end
end
@wizard.append_step('privacy') do |step|
privacy = step.add_field(id: 'privacy',
type: 'radio',
required: true,
value: SiteSetting.login_required? ? 'restricted' : 'open')
privacy.add_choice('open', icon: 'unlock')
privacy.add_choice('restricted', icon: 'lock')
step.banner = "members-illustration.svg"
step.add_field(
id: 'login_required',
type: 'checkbox',
icon: 'unlock',
value: SiteSetting.login_required
)
unless SiteSetting.invite_only? && SiteSetting.must_approve_users?
privacy_option_value = "open"
privacy_option_value = "invite_only" if SiteSetting.invite_only?
privacy_option_value = "must_approve" if SiteSetting.must_approve_users?
privacy_options = step.add_field(id: 'privacy_options',
type: 'radio',
required: true,
value: privacy_option_value)
privacy_options.add_choice('open')
privacy_options.add_choice('invite_only')
privacy_options.add_choice('must_approve')
end
step.add_field(
id: 'invite_only',
type: 'checkbox',
icon: 'user-plus',
value: SiteSetting.invite_only
)
step.add_field(
id: 'must_approve_users',
type: 'checkbox',
icon: 'user-shield',
value: SiteSetting.must_approve_users
)
step.on_update do |updater|
updater.update_setting(:login_required, updater.fields[:privacy] == 'restricted')
unless SiteSetting.invite_only? && SiteSetting.must_approve_users?
updater.update_setting(:invite_only, updater.fields[:privacy_options] == "invite_only")
updater.update_setting(:must_approve_users, updater.fields[:privacy_options] == "must_approve")
end
updater.update_setting(:login_required, updater.fields[:login_required])
updater.update_setting(:invite_only, updater.fields[:invite_only])
updater.update_setting(:must_approve_users, updater.fields[:must_approve_users])
end
end
@wizard.append_step('contact') do |step|
step.add_field(id: 'contact_email', type: 'text', required: true, value: SiteSetting.contact_email)
step.add_field(id: 'contact_url', type: 'text', value: SiteSetting.contact_url)
username = SiteSetting.site_contact_username
username = Discourse.system_user.username if username.blank?
contact = step.add_field(id: 'site_contact', type: 'dropdown', value: username)
User.human_users.where(admin: true).pluck(:username).each do |c|
contact.add_choice(c) unless reserved_usernames.include?(c.downcase)
end
contact.add_choice(Discourse.system_user.username)
step.on_update do |updater|
update_tos do |raw|
replace_setting_value(updater, raw, 'contact_email')
end
updater.apply_settings(:contact_email, :contact_url)
updater.update_setting(:site_contact_username, updater.fields[:site_contact])
end
end
@wizard.append_step('corporate') do |step|
step.description_vars = { base_path: Discourse.base_path }
step.add_field(id: 'company_name', type: 'text', value: SiteSetting.company_name)
step.add_field(id: 'governing_law', type: 'text', value: SiteSetting.governing_law)
step.add_field(id: 'city_for_disputes', type: 'text', value: SiteSetting.city_for_disputes)
step.on_update do |updater|
update_tos do |raw|
replace_setting_value(updater, raw, 'company_name')
replace_setting_value(updater, raw, 'governing_law')
replace_setting_value(updater, raw, 'city_for_disputes')
end
updater.apply_settings(:company_name, :governing_law, :city_for_disputes)
end
@wizard.append_step('ready') do |step|
# no form on this page, just info.
step.banner = "finished-illustration.svg"
end
@wizard.append_step('styling') do |step|
@ -242,7 +183,7 @@ class Wizard
end
end
@wizard.append_step('logos') do |step|
@wizard.append_step('branding') do |step|
step.add_field(id: 'logo', type: 'image', value: SiteSetting.site_logo_url)
step.add_field(id: 'logo_small', type: 'image', value: SiteSetting.site_logo_small_url)
@ -255,47 +196,35 @@ class Wizard
end
end
@wizard.append_step('icons') do |step|
step.add_field(id: 'favicon', type: 'image', value: SiteSetting.site_favicon_url)
step.add_field(id: 'large_icon', type: 'image', value: SiteSetting.site_large_icon_url)
@wizard.append_step('corporate') do |step|
step.description_vars = { base_path: Discourse.base_path }
step.add_field(id: 'company_name', type: 'text', value: SiteSetting.company_name)
step.add_field(id: 'governing_law', type: 'text', value: SiteSetting.governing_law)
step.add_field(id: 'contact_url', type: 'text', value: SiteSetting.contact_url)
step.add_field(id: 'city_for_disputes', type: 'text', value: SiteSetting.city_for_disputes)
username = SiteSetting.site_contact_username
username = Discourse.system_user.username if username.blank?
contact = step.add_field(id: 'site_contact', type: 'dropdown', value: username)
User.human_users.where(admin: true).pluck(:username).each do |c|
contact.add_choice(c) unless reserved_usernames.include?(c.downcase)
end
contact.add_choice(Discourse.system_user.username)
step.on_update do |updater|
updater.apply_settings(:favicon) if SiteSetting.site_favicon_url != updater.fields[:favicon]
updater.apply_settings(:large_icon) if SiteSetting.site_large_icon_url != updater.fields[:large_icon]
end
end
@wizard.append_step('invites') do |step|
if SiteSetting.enable_local_logins
staff_count = User.staff.human_users.where('username_lower not in (?)', reserved_usernames).count
step.add_field(id: 'staff_count', type: 'component', value: staff_count)
step.add_field(id: 'invite_list', type: 'component')
step.on_update do |updater|
users = JSON.parse(updater.fields[:invite_list])
users.each do |u|
args = { email: u['email'] }
args[:moderator] = true if u['role'] == 'moderator'
begin
Invite.generate(@wizard.user, args)
rescue => e
updater.errors.add(:invite_list, e.message.concat("<br>"))
end
end
update_tos do |raw|
replace_setting_value(updater, raw, 'company_name')
replace_setting_value(updater, raw, 'governing_law')
replace_setting_value(updater, raw, 'city_for_disputes')
end
else
step.disabled = true
updater.apply_settings(:company_name, :governing_law, :city_for_disputes, :contact_url)
updater.update_setting(:site_contact_username, updater.fields[:site_contact])
end
end
DiscourseEvent.trigger(:build_wizard, @wizard)
@wizard.append_step('finished') do |step|
step.banner = "finished.png"
step.description_vars = { base_path: Discourse.base_path }
end
@wizard
end

View File

@ -16,7 +16,7 @@ class Wizard
end
class Field
attr_reader :id, :type, :required, :value, :choices, :show_in_sidebar
attr_reader :id, :type, :required, :value, :icon, :choices, :show_in_sidebar
attr_accessor :step
def initialize(attrs)
@ -26,6 +26,7 @@ class Wizard
@type = attrs[:type]
@required = !!attrs[:required]
@value = attrs[:value]
@icon = attrs[:icon]
@choices = []
@show_in_sidebar = attrs[:show_in_sidebar]
end

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

View File

@ -1,95 +0,0 @@
# frozen_string_literal: true
require 'introduction_updater'
describe IntroductionUpdater do
describe "#get_summary" do
subject { IntroductionUpdater.new(Fabricate(:admin)) }
let(:welcome_post_raw) { "lorem ipsum" }
let(:welcome_topic) do
topic = Fabricate(:topic)
Fabricate(:post, topic: topic, raw: welcome_post_raw, post_number: 1)
topic
end
it "finds the welcome topic by site setting" do
SiteSetting.welcome_topic_id = welcome_topic.id
expect(subject.get_summary).to eq(welcome_post_raw)
end
context "without custom field" do
it "finds the welcome topic by slug using the default locale" do
I18n.locale = :de
welcome_topic.title = I18n.t("discourse_welcome_topic.title")
welcome_topic.save!
expect(subject.get_summary).to eq(welcome_post_raw)
end
it "finds the welcome topic by slug using the English locale" do
welcome_topic.title = I18n.t("discourse_welcome_topic.title", locale: :en)
welcome_topic.save!
I18n.locale = :de
expect(subject.get_summary).to eq(welcome_post_raw)
end
it "doesn't find the topic when slug_generation_method is set to 'none'" do
SiteSetting.slug_generation_method = :none
welcome_topic.title = I18n.t("discourse_welcome_topic.title")
welcome_topic.save!
expect(subject.get_summary).to be_blank
end
it "finds the oldest globally pinned topic" do
welcome_topic.update_columns(pinned_at: Time.zone.now, pinned_globally: true)
expect(subject.get_summary).to eq(welcome_post_raw)
end
it "doesn't find the topic when there is no globally pinned topic or a topic with the correct slug" do
expect(subject.get_summary).to be_blank
end
end
end
describe "update_summary" do
let(:welcome_topic) do
topic = Fabricate(:topic, title: I18n.t("discourse_welcome_topic.title"))
Fabricate(
:post,
topic: topic,
raw: I18n.t("discourse_welcome_topic.body", base_path: Discourse.base_path),
post_number: 1
)
topic
end
let(:first_post) { welcome_topic.posts.first }
let(:new_summary) { "Welcome to my new site. It's gonna be good." }
subject { IntroductionUpdater.new(Fabricate(:admin)).update_summary(new_summary) }
before do
SiteSetting.welcome_topic_id = welcome_topic.id
end
it "completely replaces post if it has default value" do
subject
expect {
expect(first_post.reload.raw).to eq(new_summary)
}.to_not change { welcome_topic.reload.category_id }
end
it "only replaces first paragraph if it has custom content" do
paragraph1 = "This is the summary of my community"
paragraph2 = "And this is something I added later"
first_post.update!(raw: [paragraph1, paragraph2].join("\n\n"))
subject
expect(first_post.reload.raw).to eq([new_summary, paragraph2].join("\n\n"))
end
end
end

View File

@ -8,164 +8,63 @@ describe Wizard::StepUpdater do
fab!(:user) { Fabricate(:admin) }
let(:wizard) { Wizard::Builder.new(user).build }
context "locale" do
it "does not require refresh when the language stays the same" do
context "introduction" do
it "updates the introduction step" do
locale = SiteSettings::DefaultsProvider::DEFAULT_LOCALE
updater = wizard.create_updater('locale', default_locale: locale)
updater = wizard.create_updater('introduction',
title: 'new forum title',
site_description: 'neat place',
default_locale: locale,
contact_email: 'eviltrout@example.com')
updater.update
expect(updater.success?).to eq(true)
expect(SiteSetting.title).to eq("new forum title")
expect(SiteSetting.site_description).to eq("neat place")
expect(SiteSetting.contact_email).to eq("eviltrout@example.com")
expect(updater.refresh_required?).to eq(false)
expect(wizard.completed_steps?('locale')).to eq(true)
expect(wizard.completed_steps?('introduction')).to eq(true)
end
it "updates the locale and requires refresh when it does change" do
updater = wizard.create_updater('locale', default_locale: 'ru')
updater = wizard.create_updater('introduction', default_locale: 'ru')
updater.update
expect(SiteSetting.default_locale).to eq('ru')
expect(updater.refresh_required?).to eq(true)
expect(wizard.completed_steps?('locale')).to eq(true)
expect(wizard.completed_steps?('introduction')).to eq(true)
end
it "won't allow updates to the default value, when required" do
updater = wizard.create_updater('introduction', title: SiteSetting.title, site_description: 'neat place')
updater.update
expect(updater.success?).to eq(false)
end
end
it "updates the forum title step" do
updater = wizard.create_updater('forum_title', title: 'new forum title', site_description: 'neat place', short_site_description: 'best community')
updater.update
expect(updater.success?).to eq(true)
expect(SiteSetting.title).to eq("new forum title")
expect(SiteSetting.site_description).to eq("neat place")
expect(SiteSetting.short_site_description).to eq("best community")
expect(wizard.completed_steps?('forum-title')).to eq(true)
end
it "updates the introduction step" do
topic = Fabricate(:topic, title: "Welcome to Discourse")
welcome_post = Fabricate(:post, topic: topic, raw: "this will be the welcome topic post\n\ncool!")
updater = wizard.create_updater('introduction', welcome: "Welcome to my new awesome forum!")
updater.update
expect(updater.success?).to eq(true)
welcome_post.reload
expect(welcome_post.raw).to eq("Welcome to my new awesome forum!\n\ncool!")
expect(wizard.completed_steps?('introduction')).to eq(true)
end
it "won't allow updates to the default value, when required" do
updater = wizard.create_updater('forum_title', title: SiteSetting.title, site_description: 'neat place')
updater.update
expect(updater.success?).to eq(false)
end
context "privacy settings" do
context "privacy" do
it "updates to open correctly" do
updater = wizard.create_updater('privacy', privacy: 'open', privacy_options: 'open')
updater = wizard.create_updater('privacy', login_required: false, invite_only: false, must_approve_users: false)
updater.update
expect(updater.success?).to eq(true)
expect(SiteSetting.login_required?).to eq(false)
expect(SiteSetting.invite_only?).to eq(false)
expect(SiteSetting.must_approve_users?).to eq(false)
expect(wizard.completed_steps?('privacy')).to eq(true)
end
it "updates to private correctly" do
updater = wizard.create_updater('privacy', privacy: 'restricted', privacy_options: 'invite_only')
updater = wizard.create_updater('privacy', login_required: true, invite_only: true, must_approve_users: true)
updater.update
expect(updater.success?).to eq(true)
expect(SiteSetting.login_required?).to eq(true)
expect(SiteSetting.invite_only?).to eq(true)
expect(SiteSetting.must_approve_users?).to eq(true)
expect(wizard.completed_steps?('privacy')).to eq(true)
end
end
context "contact step" do
it "updates the fields correctly" do
p = Fabricate(:post, raw: '<contact_email> template')
SiteSetting.tos_topic_id = p.topic_id
updater = wizard.create_updater('contact',
contact_email: 'eviltrout@example.com',
contact_url: 'http://example.com/custom-contact-url',
site_contact: user.username)
updater.update
expect(updater).to be_success
expect(SiteSetting.contact_email).to eq("eviltrout@example.com")
expect(SiteSetting.contact_url).to eq("http://example.com/custom-contact-url")
expect(SiteSetting.site_contact_username).to eq(user.username)
# Should update the TOS topic
raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck_first(:raw)
expect(raw).to eq("<eviltrout@example.com> template")
# Can update the TOS topic again
updater = wizard.create_updater('contact', contact_email: 'alice@example.com')
updater.update
raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck_first(:raw)
expect(raw).to eq("<alice@example.com> template")
# Can update the TOS to nothing
updater = wizard.create_updater('contact', {})
updater.update
raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck_first(:raw)
expect(raw).to eq("<contact_email> template")
expect(wizard.completed_steps?('contact')).to eq(true)
end
it "doesn't update when there are errors" do
updater = wizard.create_updater('contact',
contact_email: 'not-an-email',
site_contact_username: 'not-a-username')
updater.update
expect(updater).to_not be_success
expect(updater.errors).to be_present
expect(wizard.completed_steps?('contact')).to eq(false)
end
end
context "corporate step" do
it "updates the fields properly" do
p = Fabricate(:post, raw: 'company_name - governing_law - city_for_disputes template')
SiteSetting.tos_topic_id = p.topic_id
updater = wizard.create_updater('corporate',
company_name: 'ACME, Inc.',
governing_law: 'New Jersey law',
city_for_disputes: 'Fairfield, New Jersey')
updater.update
expect(updater).to be_success
expect(SiteSetting.company_name).to eq("ACME, Inc.")
expect(SiteSetting.governing_law).to eq("New Jersey law")
expect(SiteSetting.city_for_disputes).to eq("Fairfield, New Jersey")
# Should update the TOS topic
raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck_first(:raw)
expect(raw).to eq("ACME, Inc. - New Jersey law - Fairfield, New Jersey template")
# Can update the TOS topic again
updater = wizard.create_updater('corporate',
company_name: 'Pied Piper Inc',
governing_law: 'California law',
city_for_disputes: 'San Francisco, California')
updater.update
raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck_first(:raw)
expect(raw).to eq("Pied Piper Inc - California law - San Francisco, California template")
# Can update the TOS to nothing
updater = wizard.create_updater('corporate', {})
updater.update
raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck_first(:raw)
expect(raw).to eq("company_name - governing_law - city_for_disputes template")
expect(wizard.completed_steps?('corporate')).to eq(true)
end
end
context "styling step" do
context "styling" do
it "updates fonts" do
updater = wizard.create_updater('styling',
body_font: 'open_sans',
@ -340,16 +239,15 @@ describe Wizard::StepUpdater do
expect(SiteSetting.top_menu).to eq('latest|new|unread|top|categories')
end
end
end
context "logos step" do
context "branding" do
it "updates the fields correctly" do
upload = Fabricate(:upload)
upload2 = Fabricate(:upload)
updater = wizard.create_updater(
'logos',
'branding',
logo: upload.url,
logo_small: upload2.url
)
@ -357,52 +255,49 @@ describe Wizard::StepUpdater do
updater.update
expect(updater).to be_success
expect(wizard.completed_steps?('logos')).to eq(true)
expect(wizard.completed_steps?('branding')).to eq(true)
expect(SiteSetting.logo).to eq(upload)
expect(SiteSetting.logo_small).to eq(upload2)
end
end
context "icons step" do
it "updates the fields correctly" do
upload = Fabricate(:upload)
upload2 = Fabricate(:upload)
updater = wizard.create_updater('icons',
favicon: upload.url,
large_icon: upload2.url
)
context "corporate" do
it "updates the fields properly" do
p = Fabricate(:post, raw: 'company_name - governing_law - city_for_disputes template')
SiteSetting.tos_topic_id = p.topic_id
updater = wizard.create_updater('corporate',
company_name: 'ACME, Inc.',
governing_law: 'New Jersey law',
contact_url: 'http://example.com/custom-contact-url',
city_for_disputes: 'Fairfield, New Jersey')
updater.update
expect(updater).to be_success
expect(wizard.completed_steps?('icons')).to eq(true)
expect(SiteSetting.favicon).to eq(upload)
expect(SiteSetting.large_icon).to eq(upload2)
expect(SiteSetting.company_name).to eq("ACME, Inc.")
expect(SiteSetting.governing_law).to eq("New Jersey law")
expect(SiteSetting.contact_url).to eq("http://example.com/custom-contact-url")
expect(SiteSetting.city_for_disputes).to eq("Fairfield, New Jersey")
# Should update the TOS topic
raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck_first(:raw)
expect(raw).to eq("ACME, Inc. - New Jersey law - Fairfield, New Jersey template")
# Can update the TOS topic again
updater = wizard.create_updater('corporate',
company_name: 'Pied Piper Inc',
governing_law: 'California law',
city_for_disputes: 'San Francisco, California')
updater.update
raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck_first(:raw)
expect(raw).to eq("Pied Piper Inc - California law - San Francisco, California template")
# Can update the TOS to nothing
updater = wizard.create_updater('corporate', {})
updater.update
raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck_first(:raw)
expect(raw).to eq("company_name - governing_law - city_for_disputes template")
expect(wizard.completed_steps?('corporate')).to eq(true)
end
end
context "invites step" do
let(:invites) {
return [{ email: 'regular@example.com', role: 'regular' },
{ email: 'moderator@example.com', role: 'moderator' }]
}
it "updates the fields correctly" do
updater = wizard.create_updater('invites', invite_list: invites.to_json)
updater.update
expect(updater).to be_success
expect(wizard.completed_steps?('invites')).to eq(true)
reg_invite = Invite.where(email: 'regular@example.com').first
expect(reg_invite).to be_present
expect(reg_invite.moderator?).to eq(false)
mod_invite = Invite.where(email: 'moderator@example.com').first
expect(mod_invite).to be_present
expect(mod_invite.moderator?).to eq(true)
end
end
end

View File

@ -32,15 +32,30 @@ describe Wizard::Builder do
expect(wizard.steps).to be_blank
end
it "returns wizard with disabled invites step when local_logins are off" do
SiteSetting.enable_local_logins = false
context 'privacy step' do
let(:privacy_step) { wizard.steps.find { |s| s.id == 'privacy' } }
invites_step = wizard.steps.find { |s| s.id == "invites" }
expect(invites_step.fields).to be_blank
expect(invites_step.disabled).to be_truthy
it 'should set the right default value for the fields' do
SiteSetting.login_required = true
SiteSetting.invite_only = false
SiteSetting.must_approve_users = true
fields = privacy_step.fields
login_required_field = fields.first
invite_only_field = fields.second
must_approve_users_field = fields.last
expect(fields.length).to eq(3)
expect(login_required_field.id).to eq('login_required')
expect(login_required_field.value).to eq(true)
expect(invite_only_field.id).to eq('invite_only')
expect(invite_only_field.value).to eq(false)
expect(must_approve_users_field.id).to eq('must_approve_users')
expect(must_approve_users_field.value).to eq(true)
end
end
context 'styling step' do
context 'styling' do
let(:styling_step) { wizard.steps.find { |s| s.id == 'styling' } }
let(:font_field) { styling_step.fields[1] }
fab!(:theme) { Fabricate(:theme) }
@ -101,8 +116,8 @@ describe Wizard::Builder do
end
end
context 'logos step' do
let(:logos_step) { wizard.steps.find { |s| s.id == 'logos' } }
context 'branding' do
let(:branding_step) { wizard.steps.find { |s| s.id == 'branding' } }
it 'should set the right default value for the fields' do
upload = Fabricate(:upload)
@ -111,7 +126,7 @@ describe Wizard::Builder do
SiteSetting.logo = upload
SiteSetting.logo_small = upload2
fields = logos_step.fields
fields = branding_step.fields
logo_field = fields.first
logo_small_field = fields.last
@ -121,89 +136,4 @@ describe Wizard::Builder do
expect(logo_small_field.value).to eq(GlobalPathInstance.full_cdn_url(upload2.url))
end
end
context 'icons step' do
let(:icons_step) { wizard.steps.find { |s| s.id == 'icons' } }
it 'should set the right default value for the fields' do
upload = Fabricate(:upload)
upload2 = Fabricate(:upload)
SiteSetting.favicon = upload
SiteSetting.large_icon = upload2
fields = icons_step.fields
favicon_field = fields.first
large_icon_field = fields.last
expect(favicon_field.id).to eq('favicon')
expect(favicon_field.value).to eq(GlobalPathInstance.full_cdn_url(upload.url))
expect(large_icon_field.id).to eq('large_icon')
expect(large_icon_field.value).to eq(GlobalPathInstance.full_cdn_url(upload2.url))
end
end
context 'introduction step' do
let(:wizard) { Wizard::Builder.new(moderator).build }
let(:introduction_step) { wizard.steps.find { |s| s.id == 'introduction' } }
context 'step has not been completed' do
it 'enables the step' do
expect(introduction_step.disabled).to be_nil
end
end
context 'step has been completed' do
before do
wizard = Wizard::Builder.new(moderator).build
introduction_step = wizard.steps.find { |s| s.id == 'introduction' }
# manually sets the step as completed
logger = StaffActionLogger.new(moderator)
logger.log_wizard_step(introduction_step)
end
it 'disables step if no welcome topic' do
expect(introduction_step.disabled).to eq(true)
end
it 'enables step if welcome topic is present' do
topic = Fabricate(:topic, title: 'Welcome to Discourse')
welcome_post = Fabricate(:post, topic: topic, raw: "this will be the welcome topic post\n\ncool!")
expect(introduction_step.disabled).to be_nil
end
end
end
context 'privacy step' do
let(:privacy_step) { wizard.steps.find { |s| s.id == 'privacy' } }
it 'should set the right default value for the fields' do
SiteSetting.login_required = true
SiteSetting.invite_only = true
fields = privacy_step.fields
login_required_field = fields.first
privacy_options_field = fields.last
expect(fields.length).to eq(2)
expect(login_required_field.id).to eq('privacy')
expect(login_required_field.value).to eq("restricted")
expect(privacy_options_field.id).to eq('privacy_options')
expect(privacy_options_field.value).to eq("invite_only")
end
it 'should not show privacy_options field on special case' do
SiteSetting.invite_only = true
SiteSetting.must_approve_users = true
fields = privacy_step.fields
login_required_field = fields.first
expect(fields.length).to eq(1)
expect(login_required_field.id).to eq('privacy')
end
end
end

View File

@ -29,15 +29,15 @@ describe StepsController do
it "raises an error if the wizard is disabled" do
SiteSetting.wizard_enabled = false
put "/wizard/steps/contact.json", params: {
put "/wizard/steps/introduction.json", params: {
fields: { contact_email: "eviltrout@example.com" }
}
expect(response).to be_forbidden
end
it "updates properly if you are staff" do
put "/wizard/steps/contact.json", params: {
fields: { contact_email: "eviltrout@example.com" }
put "/wizard/steps/introduction.json", params: {
fields: { title: "FooBar", default_locale: SiteSetting.default_locale, contact_email: "eviltrout@example.com" }
}
expect(response.status).to eq(200)
@ -45,7 +45,7 @@ describe StepsController do
end
it "returns errors if the field has them" do
put "/wizard/steps/contact.json", params: {
put "/wizard/steps/introduction.json", params: {
fields: { contact_email: "not-an-email" }
}

View File

@ -40,17 +40,27 @@ describe WizardSerializer do
let(:serializer) { WizardSerializer.new(wizard, scope: Guardian.new(admin)) }
it "has expected steps" do
SiteSetting.login_required = true
SiteSetting.invite_only = true
SiteSetting.must_approve_users = true
json = MultiJson.load(MultiJson.dump(serializer.as_json))
steps = json['wizard']['steps']
expect(steps.first['id']).to eq('locale')
expect(steps.last['id']).to eq('finished')
expect(steps.first['id']).to eq('introduction')
expect(steps.last['id']).to eq('corporate')
privacy_step = steps.find { |s| s['id'] == 'privacy' }
expect(privacy_step).to_not be_nil
privacy_field = privacy_step['fields'].find { |f| f['id'] == 'privacy' }
expect(privacy_field['choices'].find { |c| c['id'] == 'open' }).to_not be_nil
login_required_field = privacy_step['fields'].find { |f| f['id'] == 'login_required' }
expect(login_required_field['value']).to eq(true)
invite_only_field = privacy_step['fields'].find { |f| f['id'] == 'invite_only' }
expect(invite_only_field['value']).to eq(true)
must_approve_users_field = privacy_step['fields'].find { |f| f['id'] == 'must_approve_users' }
expect(must_approve_users_field['value']).to eq(true)
end
end
end