Wizard: Step 1
This commit is contained in:
parent
0471ad393c
commit
3a4615c205
|
@ -98,7 +98,9 @@ function checkExtras(origScope, sep, extras) {
|
||||||
for (var i=0; i<extras.length; i++) {
|
for (var i=0; i<extras.length; i++) {
|
||||||
var messages = extras[i];
|
var messages = extras[i];
|
||||||
scope = origScope.split(sep);
|
scope = origScope.split(sep);
|
||||||
scope.shift();
|
if (scope[0] === 'js') {
|
||||||
|
scope.shift();
|
||||||
|
}
|
||||||
|
|
||||||
while (messages && scope.length > 0) {
|
while (messages && scope.length > 0) {
|
||||||
currentScope = scope.shift();
|
currentScope = scope.shift();
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
|
//= require_tree ./ember-addons/utils
|
||||||
|
//= require ./ember-addons/decorator-alias
|
||||||
|
//= require ./ember-addons/macro-alias
|
||||||
|
//= require ./ember-addons/ember-computed-decorators
|
||||||
|
//= require discourse/lib/raw-handlebars
|
||||||
|
//= require discourse/lib/helpers
|
||||||
//= require wizard/resolver
|
//= require wizard/resolver
|
||||||
//= require wizard/router
|
//= require wizard/router
|
||||||
//= require wizard/wizard
|
//= require wizard/wizard
|
||||||
//= require_tree ./wizard/templates
|
//= require_tree ./wizard/templates
|
||||||
|
//= require_tree ./wizard/components
|
||||||
|
//= require_tree ./wizard/models
|
||||||
//= require_tree ./wizard/routes
|
//= require_tree ./wizard/routes
|
||||||
//= require_tree ./wizard/controllers
|
//= require_tree ./wizard/controllers
|
||||||
|
//= require_tree ./wizard/lib
|
||||||
|
//= require_tree ./wizard/mixins
|
||||||
|
//= require_tree ./wizard/helpers
|
||||||
|
//= require_tree ./wizard/initializers
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNameBindings: [':wizard-field', ':text-field', 'field.invalid'],
|
||||||
|
|
||||||
|
@computed('field.id')
|
||||||
|
inputClassName: id => `field-${Ember.String.dasherize(id)}`
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNameBindings: [':wizard-step-form', 'customStepClass'],
|
||||||
|
|
||||||
|
@computed('step.id')
|
||||||
|
customStepClass: stepId => `wizard-step-${stepId}`,
|
||||||
|
});
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNames: ['wizard-step'],
|
||||||
|
saving: null,
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
this.autoFocus();
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('step.displayIndex', 'wizard.totalSteps')
|
||||||
|
showNextButton: (current, total) => current < total,
|
||||||
|
|
||||||
|
@computed('step.index')
|
||||||
|
showBackButton: index => index > 0,
|
||||||
|
|
||||||
|
@observes('step.id')
|
||||||
|
_stepChanged() {
|
||||||
|
this.set('saving', false);
|
||||||
|
this.autoFocus();
|
||||||
|
},
|
||||||
|
|
||||||
|
keyPress(key) {
|
||||||
|
if (key.keyCode === 13) {
|
||||||
|
this.send('nextStep');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('step.displayIndex', 'wizard.totalSteps')
|
||||||
|
barStyle(displayIndex, totalSteps) {
|
||||||
|
const ratio = parseFloat(displayIndex) / parseFloat(totalSteps) * 100;
|
||||||
|
return Ember.String.htmlSafe(`width: ${ratio}%`);
|
||||||
|
},
|
||||||
|
|
||||||
|
autoFocus() {
|
||||||
|
Ember.run.scheduleOnce('afterRender', () => {
|
||||||
|
const $invalid = $('.wizard-field.invalid:eq(0) input');
|
||||||
|
|
||||||
|
if ($invalid.length) {
|
||||||
|
return $invalid.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('input:eq(0)').focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
saveStep() {
|
||||||
|
const step = this.get('step');
|
||||||
|
step.save()
|
||||||
|
.then(() => this.sendAction('goNext'))
|
||||||
|
.catch(response => {
|
||||||
|
const errors = response.responseJSON.errors;
|
||||||
|
if (errors && errors.length) {
|
||||||
|
errors.forEach(err => {
|
||||||
|
step.fieldError(err.field, err.description);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
backStep() {
|
||||||
|
if (this.get('saving')) { return; }
|
||||||
|
this.sendAction('goBack');
|
||||||
|
},
|
||||||
|
|
||||||
|
nextStep() {
|
||||||
|
if (this.get('saving')) { return; }
|
||||||
|
|
||||||
|
const step = this.get('step');
|
||||||
|
step.checkFields();
|
||||||
|
|
||||||
|
if (step.get('valid')) {
|
||||||
|
this.set('saving', true);
|
||||||
|
step.save()
|
||||||
|
.then(() => this.sendAction('goNext'))
|
||||||
|
.catch(() => null) // we can swallow because the form is already marked as invalid
|
||||||
|
.finally(() => this.set('saving', false));
|
||||||
|
} else {
|
||||||
|
this.autoFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,3 +1,13 @@
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
|
wizard: null,
|
||||||
step: null,
|
step: null,
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
goNext() {
|
||||||
|
this.transitionToRoute('step', this.get('step.next'));
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.transitionToRoute('step', this.get('step.previous'));
|
||||||
|
},
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { registerUnbound } from 'discourse/lib/helpers';
|
||||||
|
|
||||||
|
registerUnbound('i18n', (key, params) => I18n.t(key, params));
|
|
@ -0,0 +1,11 @@
|
||||||
|
export default {
|
||||||
|
name: 'load-helpers',
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
Object.keys(requirejs.entries).forEach(entry => {
|
||||||
|
if ((/\/helpers\//).test(entry)) {
|
||||||
|
require(entry, null, null, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
let token;
|
||||||
|
|
||||||
|
export function ajax(args) {
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
token = $('meta[name="csrf-token"]').attr('content');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
|
args.headers = {
|
||||||
|
'X-CSRF-Token': token
|
||||||
|
};
|
||||||
|
args.success = data => Ember.run(null, resolve, data);
|
||||||
|
args.error = xhr => Ember.run(null, reject, xhr);
|
||||||
|
Ember.$.ajax(args);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export const States = {
|
||||||
|
UNCHECKED: 0,
|
||||||
|
INVALID: 1,
|
||||||
|
VALID: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
_validState: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super();
|
||||||
|
this.set('_validState', States.UNCHECKED);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('_validState')
|
||||||
|
valid: state => state === States.VALID,
|
||||||
|
|
||||||
|
@computed('_validState')
|
||||||
|
invalid: state => state === States.INVALID,
|
||||||
|
|
||||||
|
@computed('_validState')
|
||||||
|
unchecked: state => state === States.UNCHECKED,
|
||||||
|
|
||||||
|
setValid(valid) {
|
||||||
|
this.set('_validState', valid ? States.VALID : States.INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,41 @@
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
import ValidState from 'wizard/mixins/valid-state';
|
||||||
|
import { ajax } from 'wizard/lib/ajax';
|
||||||
|
|
||||||
|
export default Ember.Object.extend(ValidState, {
|
||||||
|
id: null,
|
||||||
|
|
||||||
|
@computed('index')
|
||||||
|
displayIndex: index => index + 1,
|
||||||
|
|
||||||
|
checkFields() {
|
||||||
|
let allValid = true;
|
||||||
|
this.get('fields').forEach(field => {
|
||||||
|
field.check();
|
||||||
|
allValid = allValid && field.get('valid');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setValid(allValid);
|
||||||
|
},
|
||||||
|
|
||||||
|
fieldError(id, description) {
|
||||||
|
const field = this.get('fields').findProperty('id', id);
|
||||||
|
if (field) {
|
||||||
|
field.setValid(false, description);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const fields = {};
|
||||||
|
this.get('fields').forEach(f => fields[f.id] = f.value);
|
||||||
|
|
||||||
|
return ajax({
|
||||||
|
url: `/wizard/steps/${this.get('id')}`,
|
||||||
|
type: 'PUT',
|
||||||
|
data: { fields }
|
||||||
|
}).catch(response => {
|
||||||
|
response.responseJSON.errors.forEach(err => this.fieldError(err.field, err.description));
|
||||||
|
throw response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
import ValidState from 'wizard/mixins/valid-state';
|
||||||
|
|
||||||
|
export default Ember.Object.extend(ValidState, {
|
||||||
|
id: null,
|
||||||
|
type: null,
|
||||||
|
value: null,
|
||||||
|
required: null,
|
||||||
|
|
||||||
|
check() {
|
||||||
|
if (!this.get('required')) {
|
||||||
|
return this.setValid(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const val = this.get('value');
|
||||||
|
this.setValid(val && val.length > 0);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,23 @@
|
||||||
|
import Step from 'wizard/models/step';
|
||||||
|
import WizardField from 'wizard/models/wizard-field';
|
||||||
|
import { ajax } from 'wizard/lib/ajax';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
const Wizard = Ember.Object.extend({
|
||||||
|
@computed('steps.length')
|
||||||
|
totalSteps: length => length
|
||||||
|
});
|
||||||
|
|
||||||
|
export function findWizard() {
|
||||||
|
return ajax({ url: '/wizard.json' }).then(response => {
|
||||||
|
const wizard = response.wizard;
|
||||||
|
wizard.steps = wizard.steps.map(step => {
|
||||||
|
const stepObj = Step.create(step);
|
||||||
|
stepObj.fields = stepObj.fields.map(f => WizardField.create(f));
|
||||||
|
return stepObj;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Wizard.create(wizard);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -8,15 +8,15 @@ function resolveType(parsedName) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function customResolve(parsedName) {
|
||||||
|
return resolveType(parsedName) || this._super(parsedName);
|
||||||
|
}
|
||||||
|
|
||||||
export default Ember.DefaultResolver.extend({
|
export default Ember.DefaultResolver.extend({
|
||||||
|
|
||||||
resolveRoute(parsedName) {
|
resolveRoute: customResolve,
|
||||||
return resolveType(parsedName) || this._super(parsedName);
|
resolveController: customResolve,
|
||||||
},
|
resolveComponent: customResolve,
|
||||||
|
|
||||||
resolveController(parsedName) {
|
|
||||||
return resolveType(parsedName) || this._super(parsedName);
|
|
||||||
},
|
|
||||||
|
|
||||||
resolveTemplate(parsedName) {
|
resolveTemplate(parsedName) {
|
||||||
const templates = Ember.TEMPLATES;
|
const templates = Ember.TEMPLATES;
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { findWizard } from 'wizard/models/wizard';
|
||||||
|
|
||||||
|
export default Ember.Route.extend({
|
||||||
|
model() {
|
||||||
|
return findWizard();
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,5 +1,6 @@
|
||||||
export default Ember.Route.extend({
|
export default Ember.Route.extend({
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
this.replaceWith('step', 'welcome');
|
const appModel = this.modelFor('application');
|
||||||
|
this.replaceWith('step', appModel.start);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
export default Ember.Route.extend({
|
export default Ember.Route.extend({
|
||||||
model(params) {
|
model(params) {
|
||||||
return {
|
const allSteps = this.modelFor('application').steps;
|
||||||
id: params.step_id,
|
return allSteps.findProperty('id', params.step_id);
|
||||||
title: "You're a wizard harry!"
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, step) {
|
||||||
controller.set('step', model);
|
controller.setProperties({
|
||||||
|
step, wizard: this.modelFor('application')
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<div class='wizard-column'>
|
<div class='wizard-column'>
|
||||||
<div class='wizard-column-contents'>
|
<div class='wizard-column-contents'>
|
||||||
Discourse!
|
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class='wizard-footer'>
|
||||||
|
<img src="/images/wizard/discourse.png" class="logo">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<label>
|
||||||
|
<span class='label-value'>{{field.label}}</span>
|
||||||
|
|
||||||
|
<div class='input-area'>
|
||||||
|
{{input value=field.value class=inputClassName placeholder=field.placeholder}}
|
||||||
|
</div>
|
||||||
|
</label>
|
|
@ -0,0 +1,37 @@
|
||||||
|
{{#if step.title}}
|
||||||
|
<h1 class='wizard-step-title'>{{step.title}}</h1>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if step.description}}
|
||||||
|
<p class='wizard-step-description'>{{step.description}}</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#wizard-step-form step=step}}
|
||||||
|
{{#each step.fields as |field|}}
|
||||||
|
{{wizard-field field=field}}
|
||||||
|
{{/each}}
|
||||||
|
{{/wizard-step-form}}
|
||||||
|
|
||||||
|
<div class='wizard-step-footer'>
|
||||||
|
<div class='wizard-progress'>
|
||||||
|
<div class='text'>{{i18n "wizard.step" current=step.displayIndex total=wizard.totalSteps}}</div>
|
||||||
|
<div class='bar-container'>
|
||||||
|
<div class='bar-contents' style={{barStyle}}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if showBackButton}}
|
||||||
|
<button class='wizard-btn back' {{action "backStep"}} disabled={{saving}}>
|
||||||
|
<i class='fa fa-chevron-left'></i>
|
||||||
|
{{i18n "wizard.back"}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if showNextButton}}
|
||||||
|
<button class='wizard-btn next' {{action "nextStep"}} disabled={{saving}}>
|
||||||
|
{{i18n "wizard.next"}}
|
||||||
|
<i class='fa fa-chevron-right'></i>
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,3 +1 @@
|
||||||
<div class='wizard-step'>
|
{{wizard-step step=step wizard=wizard goNext="goNext" goBack="goBack"}}
|
||||||
{{step.title}}
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,10 +1,54 @@
|
||||||
|
|
||||||
module("Acceptance: wizard");
|
module("Acceptance: wizard");
|
||||||
|
|
||||||
test("Wizard loads", assert => {
|
test("Wizard starts", assert => {
|
||||||
visit("/");
|
visit("/");
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
assert.ok(exists('.wizard-column-contents'));
|
assert.ok(exists('.wizard-column-contents'));
|
||||||
assert.equal(currentPath(), 'steps');
|
assert.equal(currentPath(), 'step');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Forum Name Step", assert => {
|
||||||
|
visit("/step/hello-world");
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok(exists('.wizard-step'));
|
||||||
|
assert.ok(exists('.wizard-step-hello-world'), 'it adds a class for the step id');
|
||||||
|
|
||||||
|
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-btn.back'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// invalid data
|
||||||
|
click('.wizard-btn.next');
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok(exists('.invalid .field-full-name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// server validation fail
|
||||||
|
fillIn('input.field-full-name', "Server Fail");
|
||||||
|
click('.wizard-btn.next');
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok(exists('.invalid .field-full-name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// server validation ok
|
||||||
|
fillIn('input.field-full-name', "Evil Trout");
|
||||||
|
click('.wizard-btn.next');
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok(!exists('.wizard-step-title'));
|
||||||
|
assert.ok(!exists('.wizard-step-description'));
|
||||||
|
assert.ok(exists('input.field-email'), "went to the next step");
|
||||||
|
assert.ok(!exists('.wizard-btn.next'));
|
||||||
|
assert.ok(exists('.wizard-btn.back'), 'shows the back button');
|
||||||
|
});
|
||||||
|
|
||||||
|
click('.wizard-btn.back');
|
||||||
|
andThen(() => {
|
||||||
|
assert.ok(exists('.wizard-step-title'));
|
||||||
|
assert.ok(exists('.wizard-btn.next'));
|
||||||
|
assert.ok(!exists('.wizard-prev'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import WizardField from 'wizard/models/wizard-field';
|
||||||
|
|
||||||
|
module("model:wizard-field");
|
||||||
|
|
||||||
|
test('basic state', 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', 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', assert => {
|
||||||
|
const w = WizardField.create({ type: 'text' });
|
||||||
|
assert.ok(w.get('unchecked'));
|
||||||
|
|
||||||
|
w.check();
|
||||||
|
assert.ok(w.get('valid'));
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
/*global document, sinon, QUnit, Logster */
|
/*global document, sinon, Logster, QUnit */
|
||||||
|
|
||||||
//= require env
|
//= require env
|
||||||
//= require jquery.debug
|
//= require jquery.debug
|
||||||
|
@ -8,9 +8,16 @@
|
||||||
//= require ember.debug
|
//= require ember.debug
|
||||||
//= require ember-template-compiler
|
//= require ember-template-compiler
|
||||||
//= require ember-qunit
|
//= require ember-qunit
|
||||||
|
//= require ember-shim
|
||||||
//= require wizard-application
|
//= require wizard-application
|
||||||
//= require helpers/assertions
|
//= require helpers/assertions
|
||||||
//= require_tree ./acceptance
|
//= require_tree ./acceptance
|
||||||
|
//= require_tree ./models
|
||||||
|
//= require locales/en
|
||||||
|
//= require fake_xml_http_request
|
||||||
|
//= require route-recognizer
|
||||||
|
//= require pretender
|
||||||
|
//= require ./wizard-pretender
|
||||||
|
|
||||||
// Trick JSHint into allow document.write
|
// Trick JSHint into allow document.write
|
||||||
var d = document;
|
var d = document;
|
||||||
|
@ -23,15 +30,23 @@ if (window.Logster) {
|
||||||
window.Logster = { enabled: false };
|
window.Logster = { enabled: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var createPretendServer = require('wizard/test/wizard-pretender', null, null, false).default;
|
||||||
|
|
||||||
|
var server;
|
||||||
|
QUnit.testStart(function() {
|
||||||
|
server = createPretendServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.testDone(function() {
|
||||||
|
server.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
var wizard = require('wizard/wizard').default.create({
|
var wizard = require('wizard/wizard').default.create({
|
||||||
rootElement: '#ember-testing'
|
rootElement: '#ember-testing'
|
||||||
});
|
});
|
||||||
wizard.setupForTesting();
|
wizard.setupForTesting();
|
||||||
wizard.injectTestHelpers();
|
wizard.injectTestHelpers();
|
||||||
|
wizard.start();
|
||||||
QUnit.testDone(function() {
|
|
||||||
wizard.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(requirejs.entries).forEach(function(entry) {
|
Object.keys(requirejs.entries).forEach(function(entry) {
|
||||||
if ((/\-test/).test(entry)) {
|
if ((/\-test/).test(entry)) {
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
// 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',
|
||||||
|
steps: [{
|
||||||
|
id: 'hello-world',
|
||||||
|
title: 'hello there',
|
||||||
|
index: 0,
|
||||||
|
description: 'hello!',
|
||||||
|
fields: [{ id: 'full_name', type: 'text', required: true }],
|
||||||
|
next: 'second-step'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'second-step',
|
||||||
|
index: 1,
|
||||||
|
fields: [{ id: 'email', type: 'text', required: true }],
|
||||||
|
previous: 'hello-world'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -4,5 +4,15 @@ import Router from 'wizard/router';
|
||||||
export default Ember.Application.extend({
|
export default Ember.Application.extend({
|
||||||
rootElement: '#wizard-main',
|
rootElement: '#wizard-main',
|
||||||
Resolver,
|
Resolver,
|
||||||
Router
|
Router,
|
||||||
|
|
||||||
|
start() {
|
||||||
|
Object.keys(requirejs._eak_seen).forEach(key => {
|
||||||
|
if (/\/initializers\//.test(key)) {
|
||||||
|
const module = require(key, null, null, true);
|
||||||
|
if (!module) { throw new Error(key + ' must export an initializer.'); }
|
||||||
|
this.instanceInitializer(module.default);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,18 +2,21 @@
|
||||||
@import "vendor/font_awesome/font-awesome";
|
@import "vendor/font_awesome/font-awesome";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: rgb(231,238,247);
|
background-color: #fff;
|
||||||
background-image: url('/images/wizard/bubbles.png');
|
background-image: url('/images/wizard/bubbles.png');
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
background-position: left top;
|
background-position: left top;
|
||||||
|
|
||||||
|
color: #444;
|
||||||
|
|
||||||
|
line-height: 1.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wizard-column {
|
.wizard-column {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 2px;
|
|
||||||
box-shadow: 0 5px 10px rgba(0,0,0,0.2);
|
box-shadow: 0 5px 10px rgba(0,0,0,0.2);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0.75rem auto;
|
margin: 1.5em auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
|
@ -21,11 +24,144 @@ body {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
|
|
||||||
.wizard-column-contents {
|
.wizard-column-contents {
|
||||||
padding: 1em;
|
padding: 1.2em;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0 0 1em 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wizard-step-description {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-footer {
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 0.5em;
|
||||||
|
img.logo {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-progress {
|
||||||
|
.text {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.bar-container {
|
||||||
|
border: 1px solid #ff6600;
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 0.5em;
|
||||||
|
width: 15em;
|
||||||
|
|
||||||
|
.bar-contents {
|
||||||
|
background-color: #ff6600;
|
||||||
|
width: 0;
|
||||||
|
height: 0.5em;
|
||||||
|
transition: width .3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.wizard-step-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
button.wizard-btn {
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, .6);
|
||||||
|
font-size: 1.0em;
|
||||||
|
background-color: #6699ff;
|
||||||
|
color: white;
|
||||||
|
border: 0px;
|
||||||
|
float: right;
|
||||||
|
padding: 0.5em;
|
||||||
|
outline: 0;
|
||||||
|
transition: background-color .3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #80B3FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: #4D80E6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: #000167;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.fa-chevron-right {
|
||||||
|
margin-left: 0.25em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
i.fa-chevron-left {
|
||||||
|
margin-right: 0.25em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.wizard-btn.next {
|
||||||
|
min-width: 70px;
|
||||||
|
|
||||||
|
i.fa-chevron-right {
|
||||||
|
margin-left: 0.25em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.wizard-btn.back {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, .4);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-field {
|
||||||
|
label .label-value {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-area {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-field {
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
transition: border-color .5s;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.invalid {
|
||||||
|
input {
|
||||||
|
padding: 3px;
|
||||||
|
border: 4px solid red;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
require_dependency 'wizard'
|
||||||
|
require_dependency 'wizard/step_updater'
|
||||||
|
|
||||||
|
class StepsController < ApplicationController
|
||||||
|
|
||||||
|
before_filter :ensure_logged_in
|
||||||
|
before_filter :ensure_staff
|
||||||
|
|
||||||
|
def update
|
||||||
|
updater = Wizard::StepUpdater.new(current_user, params[:id])
|
||||||
|
updater.update(params[:fields])
|
||||||
|
render nothing: true
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1,16 +0,0 @@
|
||||||
class Wizard::WizardController < ApplicationController
|
|
||||||
|
|
||||||
before_filter :ensure_logged_in
|
|
||||||
before_filter :ensure_staff
|
|
||||||
|
|
||||||
skip_before_filter :check_xhr, :preload_json
|
|
||||||
|
|
||||||
layout false
|
|
||||||
|
|
||||||
def index
|
|
||||||
end
|
|
||||||
|
|
||||||
def qunit
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
require_dependency 'wizard'
|
||||||
|
|
||||||
|
class WizardController < ApplicationController
|
||||||
|
|
||||||
|
before_filter :ensure_logged_in
|
||||||
|
before_filter :ensure_staff
|
||||||
|
|
||||||
|
skip_before_filter :check_xhr, :preload_json
|
||||||
|
|
||||||
|
layout false
|
||||||
|
|
||||||
|
def index
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
wizard = Wizard.build
|
||||||
|
render_serialized(wizard, WizardSerializer)
|
||||||
|
end
|
||||||
|
format.html {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def qunit
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,44 @@
|
||||||
|
class WizardFieldSerializer < ApplicationSerializer
|
||||||
|
|
||||||
|
attributes :id, :type, :required, :value, :label, :placeholder
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
object.type
|
||||||
|
end
|
||||||
|
|
||||||
|
def required
|
||||||
|
object.required
|
||||||
|
end
|
||||||
|
|
||||||
|
def value
|
||||||
|
object.value
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_value?
|
||||||
|
object.value.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def i18n_key
|
||||||
|
@i18n_key ||= "wizard.step.#{object.step.id}.fields.#{object.id}".underscore
|
||||||
|
end
|
||||||
|
|
||||||
|
def label
|
||||||
|
I18n.t("#{i18n_key}.label", default: '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_label?
|
||||||
|
label.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def placeholder
|
||||||
|
I18n.t("#{i18n_key}.placeholder", default: '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_placeholder?
|
||||||
|
placeholder.present?
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
class WizardSerializer < ApplicationSerializer
|
||||||
|
attributes :start
|
||||||
|
|
||||||
|
has_many :steps, serializer: WizardStepSerializer, embed: :objects
|
||||||
|
|
||||||
|
def start
|
||||||
|
object.start.id
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,50 @@
|
||||||
|
class WizardStepSerializer < ApplicationSerializer
|
||||||
|
|
||||||
|
attributes :id, :next, :previous, :description, :title, :index
|
||||||
|
has_many :fields, serializer: WizardFieldSerializer, embed: :objects
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
object.index
|
||||||
|
end
|
||||||
|
|
||||||
|
def next
|
||||||
|
object.next.id if object.next.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_next?
|
||||||
|
object.next.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def previous
|
||||||
|
object.previous.id if object.previous.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_previous?
|
||||||
|
object.previous.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def i18n_key
|
||||||
|
@i18n_key ||= "wizard.step.#{object.id}".underscore
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
I18n.t("#{i18n_key}.description", default: '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_description?
|
||||||
|
description.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def title
|
||||||
|
I18n.t("#{i18n_key}.title", default: '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_title?
|
||||||
|
title.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,25 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<%= stylesheet_link_tag 'wizard' %>
|
||||||
|
<%= script 'wizard-vendor' %>
|
||||||
|
<%= script 'ember_jquery' %>
|
||||||
|
<%= script 'wizard-application' %>
|
||||||
|
<%= script "locales/#{I18n.locale}" %>
|
||||||
|
<%= render partial: "common/special_font_face" %>
|
||||||
|
<script src="/extra-locales/wizard"></script>
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
|
||||||
|
<title><%= t 'wizard.title' %></title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id='wizard-main'></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var wizard = require('wizard/wizard').default.create();
|
||||||
|
wizard.start();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -7,6 +7,7 @@
|
||||||
<%= javascript_include_tag "qunit" %>
|
<%= javascript_include_tag "qunit" %>
|
||||||
<%= javascript_include_tag "wizard/test/test_helper" %>
|
<%= javascript_include_tag "wizard/test/test_helper" %>
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
|
<script src="/extra-locales/wizard"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="qunit"></div>
|
<div id="qunit"></div>
|
|
@ -1,21 +0,0 @@
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<%= stylesheet_link_tag 'wizard' %>
|
|
||||||
<%= javascript_include_tag 'wizard-vendor' %>
|
|
||||||
<%= javascript_include_tag 'ember_jquery' %>
|
|
||||||
<%= javascript_include_tag 'wizard-application' %>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id='wizard-main'>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
require('wizard/wizard').default.create();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -3225,5 +3225,9 @@ en:
|
||||||
add: "Add"
|
add: "Add"
|
||||||
filter: "Search (URL or External URL)"
|
filter: "Search (URL or External URL)"
|
||||||
|
|
||||||
# WARNING! Keys added here will be in the admin_js section.
|
wizard_js:
|
||||||
# Keys that don't belong in admin should be placed earlier in the file.
|
wizard:
|
||||||
|
back: "Back"
|
||||||
|
next: "Next"
|
||||||
|
step: "Step %{current} of %{total}"
|
||||||
|
|
||||||
|
|
|
@ -3212,3 +3212,25 @@ en:
|
||||||
staff_tag_disallowed: "The tag \"%{tag}\" may only be applied by staff."
|
staff_tag_disallowed: "The tag \"%{tag}\" may only be applied by staff."
|
||||||
staff_tag_remove_disallowed: "The tag \"%{tag}\" may only be removed by staff."
|
staff_tag_remove_disallowed: "The tag \"%{tag}\" may only be removed by staff."
|
||||||
rss_by_tag: "Topics tagged %{tag}"
|
rss_by_tag: "Topics tagged %{tag}"
|
||||||
|
|
||||||
|
wizard:
|
||||||
|
title: "Discourse Setup Wizard"
|
||||||
|
step:
|
||||||
|
forum_title:
|
||||||
|
title: "Welcome to your Discourse forum!"
|
||||||
|
description: "There are a few things you'll need to configure before inviting more people to the party. Don't worry, you can come back and change these settings at any time, so don't overthink it."
|
||||||
|
|
||||||
|
fields:
|
||||||
|
title:
|
||||||
|
label: "Enter a title for your forum"
|
||||||
|
placeholder: "Jane's Hangout"
|
||||||
|
site_description:
|
||||||
|
label: "Describe your forum in one sentence"
|
||||||
|
placeholder: "A place for Jane and her friends to discuss cool stuff"
|
||||||
|
contact:
|
||||||
|
title: "Don't be a Stranger"
|
||||||
|
fields:
|
||||||
|
contact_email:
|
||||||
|
label: "Contact E-mail"
|
||||||
|
placeholder: "name@example.com"
|
||||||
|
|
||||||
|
|
|
@ -53,10 +53,10 @@ Discourse::Application.routes.draw do
|
||||||
resources :forums
|
resources :forums
|
||||||
get "srv/status" => "forums#status"
|
get "srv/status" => "forums#status"
|
||||||
|
|
||||||
namespace :wizard, constraints: StaffConstraint.new do
|
get "wizard" => "wizard#index"
|
||||||
get "" => "wizard#index"
|
get "wizard/qunit" => "wizard#qunit"
|
||||||
get "qunit" => "wizard#qunit"
|
get 'wizard/steps' => 'steps#index'
|
||||||
end
|
put 'wizard/steps/:id' => "steps#update"
|
||||||
|
|
||||||
namespace :admin, constraints: StaffConstraint.new do
|
namespace :admin, constraints: StaffConstraint.new do
|
||||||
get "" => "admin#index"
|
get "" => "admin#index"
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
require_dependency 'wizard/step'
|
||||||
|
require_dependency 'wizard/field'
|
||||||
|
|
||||||
|
class Wizard
|
||||||
|
attr_reader :start
|
||||||
|
attr_reader :steps
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@steps = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_step(args)
|
||||||
|
Step.new(args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_step(step)
|
||||||
|
last_step = @steps.last
|
||||||
|
|
||||||
|
@steps << step
|
||||||
|
|
||||||
|
# If it's the first step
|
||||||
|
if @steps.size == 1
|
||||||
|
@start = step
|
||||||
|
step.index = 0
|
||||||
|
elsif last_step.present?
|
||||||
|
last_step.next = step
|
||||||
|
step.previous = last_step
|
||||||
|
step.index = last_step.index + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.build
|
||||||
|
wizard = Wizard.new
|
||||||
|
title = wizard.create_step('forum-title')
|
||||||
|
title.add_field(id: 'title', type: 'text', required: true, value: SiteSetting.title)
|
||||||
|
title.add_field(id: 'site_description', type: 'text', required: true, value: SiteSetting.site_description)
|
||||||
|
wizard.append_step(title)
|
||||||
|
|
||||||
|
contact = wizard.create_step('contact')
|
||||||
|
contact.add_field(id: 'contact_email', type: 'text', required: true)
|
||||||
|
wizard.append_step(contact)
|
||||||
|
|
||||||
|
wizard
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,15 @@
|
||||||
|
class Wizard
|
||||||
|
class Field
|
||||||
|
attr_reader :id, :type, :required, :value
|
||||||
|
attr_accessor :step
|
||||||
|
|
||||||
|
def initialize(attrs)
|
||||||
|
attrs = attrs || {}
|
||||||
|
|
||||||
|
@id = attrs[:id]
|
||||||
|
@type = attrs[:type]
|
||||||
|
@required = !!attrs[:required]
|
||||||
|
@value = attrs[:value]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
class Wizard
|
||||||
|
class Step
|
||||||
|
attr_reader :id
|
||||||
|
attr_accessor :index, :fields, :next, :previous
|
||||||
|
|
||||||
|
def initialize(id)
|
||||||
|
@id = id
|
||||||
|
@fields = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_field(attrs)
|
||||||
|
field = Field.new(attrs)
|
||||||
|
field.step = self
|
||||||
|
@fields << field
|
||||||
|
field
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,42 @@
|
||||||
|
class Wizard
|
||||||
|
class StepUpdater
|
||||||
|
|
||||||
|
attr_accessor :errors
|
||||||
|
|
||||||
|
def initialize(current_user, id)
|
||||||
|
@current_user = current_user
|
||||||
|
@id = id
|
||||||
|
@errors = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(fields)
|
||||||
|
updater_method = "update_#{@id.underscore}".to_sym
|
||||||
|
|
||||||
|
if respond_to?(updater_method)
|
||||||
|
send(updater_method, fields.symbolize_keys)
|
||||||
|
else
|
||||||
|
raise Discourse::InvalidAccess.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_forum_title(fields)
|
||||||
|
update_setting(:title, fields, :title)
|
||||||
|
update_setting(:site_description, fields, :site_description)
|
||||||
|
end
|
||||||
|
|
||||||
|
def success?
|
||||||
|
@errors.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def update_setting(id, fields, field_id)
|
||||||
|
value = fields[field_id]
|
||||||
|
value.strip! if value.is_a?(String)
|
||||||
|
SiteSetting.set_and_log(id, value, @current_user)
|
||||||
|
rescue Discourse::InvalidParameters => e
|
||||||
|
@errors << {field: field_id, description: e.message }
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -3,7 +3,6 @@ require 'cache'
|
||||||
|
|
||||||
describe Gaps do
|
describe Gaps do
|
||||||
|
|
||||||
|
|
||||||
it 'returns no gaps for empty data' do
|
it 'returns no gaps for empty data' do
|
||||||
expect(Gaps.new(nil, nil)).to be_blank
|
expect(Gaps.new(nil, nil)).to be_blank
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
require_dependency 'wizard/step_updater'
|
||||||
|
|
||||||
|
describe Wizard::StepUpdater do
|
||||||
|
let(:user) { Fabricate(:admin) }
|
||||||
|
|
||||||
|
it "can update the forum title" do
|
||||||
|
updater = Wizard::StepUpdater.new(user, 'forum_title')
|
||||||
|
updater.update(title: 'new forum title')
|
||||||
|
|
||||||
|
expect(updater.success?).to eq(true)
|
||||||
|
expect(SiteSetting.title).to eq("new forum title")
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,48 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
require 'wizard'
|
||||||
|
|
||||||
|
describe Wizard do
|
||||||
|
|
||||||
|
let(:wizard) { Wizard.new }
|
||||||
|
|
||||||
|
it "has default values" do
|
||||||
|
expect(wizard.start).to be_blank
|
||||||
|
expect(wizard.steps).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "append_step" do
|
||||||
|
|
||||||
|
let(:step1) { wizard.create_step('first-step') }
|
||||||
|
let(:step2) { wizard.create_step('second-step') }
|
||||||
|
|
||||||
|
it "adds the step correctly" do
|
||||||
|
|
||||||
|
expect(step1.index).to be_blank
|
||||||
|
|
||||||
|
wizard.append_step(step1)
|
||||||
|
expect(wizard.steps.size).to eq(1)
|
||||||
|
expect(wizard.start).to eq(step1)
|
||||||
|
expect(step1.next).to be_blank
|
||||||
|
expect(step1.previous).to be_blank
|
||||||
|
expect(step1.index).to eq(0)
|
||||||
|
|
||||||
|
expect(step1.fields).to be_empty
|
||||||
|
field = step1.add_field(id: 'test', type: 'text')
|
||||||
|
expect(step1.fields).to eq([field])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sequences multiple steps" do
|
||||||
|
wizard.append_step(step1)
|
||||||
|
wizard.append_step(step2)
|
||||||
|
|
||||||
|
expect(wizard.steps.size).to eq(2)
|
||||||
|
expect(wizard.start).to eq(step1)
|
||||||
|
expect(step1.next).to eq(step2)
|
||||||
|
expect(step1.previous).to be_blank
|
||||||
|
expect(step2.previous).to eq(step1)
|
||||||
|
expect(step1.index).to eq(0)
|
||||||
|
expect(step2.index).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,35 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe StepsController do
|
||||||
|
|
||||||
|
it 'needs you to be logged in' do
|
||||||
|
expect {
|
||||||
|
xhr :put, :update, id: 'made-up-id', fields: { forum_title: "updated title" }
|
||||||
|
}.to raise_error(Discourse::NotLoggedIn)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error if you aren't an admin" do
|
||||||
|
log_in
|
||||||
|
xhr :put, :update, id: 'made-up-id', fields: { forum_title: "updated title" }
|
||||||
|
expect(response).to be_forbidden
|
||||||
|
end
|
||||||
|
|
||||||
|
context "as an admin" do
|
||||||
|
before do
|
||||||
|
log_in(:admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error with an invalid id" do
|
||||||
|
xhr :put, :update, id: 'made-up-id', fields: { forum_title: "updated title" }
|
||||||
|
expect(response).to_not be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates properly if you are staff" do
|
||||||
|
xhr :put, :update, id: 'forum-title', fields: { title: "updated title" }
|
||||||
|
expect(response).to be_success
|
||||||
|
expect(SiteSetting.title).to eq("updated title")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe Wizard::WizardController do
|
describe WizardController do
|
||||||
|
|
||||||
context 'index' do
|
context 'index' do
|
||||||
render_views
|
render_views
|
||||||
|
@ -20,6 +20,14 @@ describe Wizard::WizardController do
|
||||||
xhr :get, :index
|
xhr :get, :index
|
||||||
expect(response).to be_success
|
expect(response).to be_success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "returns JSON when the mime type is appropriate" do
|
||||||
|
log_in(:admin)
|
||||||
|
xhr :get, :index, format: 'json'
|
||||||
|
expect(response).to be_success
|
||||||
|
expect(::JSON.parse(response.body).has_key?('wizard')).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
|
@ -29,7 +29,6 @@ describe "i18n integrity checks" do
|
||||||
client = YAML.load_file("#{Rails.root}/config/locales/client.#{locale}.yml")
|
client = YAML.load_file("#{Rails.root}/config/locales/client.#{locale}.yml")
|
||||||
expect(client.count).to eq(1)
|
expect(client.count).to eq(1)
|
||||||
expect(client[locale]).not_to eq(nil)
|
expect(client[locale]).not_to eq(nil)
|
||||||
expect(client[locale].count).to eq(2)
|
|
||||||
expect(client[locale]["js"]).not_to eq(nil)
|
expect(client[locale]["js"]).not_to eq(nil)
|
||||||
expect(client[locale]["admin_js"]).not_to eq(nil)
|
expect(client[locale]["admin_js"]).not_to eq(nil)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue