1
0
Fork 0
Dieser Commit ist enthalten in:
Angus McLeod 2017-10-13 21:02:34 +08:00
Ursprung dd26ac63af
Commit e859e3efa2
39 geänderte Dateien mit 892 neuen und 439 gelöschten Zeilen

Datei anzeigen

@ -15,20 +15,7 @@ class CustomWizard::AdminController < ::ApplicationController
wizard = ::JSON.parse(params[:wizard])
saved = false
if wizard["existing_id"] && rows = PluginStoreRow.where(plugin_name: 'custom_wizard').order(:id)
rows.each do |r, i|
wizard = CustomWizard::Wizard.new(r.value)
if wizard.id = wizard["existing_id"]
r.update_all(key: wizard['id'], value: wizard)
saved = true
end
end
end
unless saved
PluginStore.set('custom_wizard', wizard["id"], wizard)
end
render json: success_json
end

Datei anzeigen

@ -1,8 +1,7 @@
<html>
<head>
<link href="<%= Discourse.base_uri %>/plugins/discourse-custom-wizard/desktop.css" media="all" rel="stylesheet" data-target="desktop" type="text/css" />
<%= discourse_stylesheet_link_tag :wizard, theme_key: nil %>
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %>
<%= stylesheet_link_tag "wizard_custom", media: "all", "data-turbolinks-track" => "reload" %>
<%= preload_script "ember_jquery" %>
<%= preload_script "wizard-vendor" %>
<%= preload_script "wizard-application" %>
@ -18,7 +17,7 @@
<meta name="discourse-base-uri" content="<%= Discourse.base_uri %>">
<%= render partial: "layouts/head" %>
<title><%= t 'custom_wizard.title' %></title>
<title><%= t 'wizard.custom_title' %></title>
</head>
<body class='custom-wizard'>

Datei anzeigen

@ -1,8 +1,16 @@
import { on, observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
classNames: 'wizard-custom-action',
types: ['create_topic', 'update_profile', 'send_message'],
profileFields: ['name', 'username', 'email'],
createTopic: Ember.computed.equal('action.type', 'create_topic'),
updateProfile: Ember.computed.equal('action.type', 'update_profile'),
sendMessage: Ember.computed.equal('action.type', 'send_message')
sendMessage: Ember.computed.equal('action.type', 'send_message'),
@on('init')
@observes('action')
setup() {
this.set('existingId', this.get('action.id'));
}
});

Datei anzeigen

@ -5,19 +5,43 @@ export default Ember.Component.extend({
isDropdown: Ember.computed.equal('field.type', 'dropdown'),
@on('init')
@observes('field.id')
init() {
this._super(...arguments);
if (!this.get('field.choices')) {
this.set('field.choices', Ember.A());
}
@observes('field')
setup() {
this.set('existingId', this.get('field.id'));
},
@computed('field.type')
isInput: (type) => type === 'text' || type === 'textarea',
@computed('field.choices.[]')
dropdownChoices: choices => choices,
@computed('field.choices_filters.[]')
presetFilters: filters => filters,
@computed()
presetChoices() {
return [
{ id: 'categories', name: I18n.t('admin.wizard.field.choices_preset.categories') }
];
},
actions: {
addFilter() {
if (!this.get('field.choices_filters')) {
this.set('field.choices_filters', Ember.A());
}
this.get('field.choices_filters').pushObject(Ember.Object.create());
},
removeFilter(f) {
this.get('field.choices_filters').removeObject(f);
},
addChoice() {
if (!this.get('field.choices')) {
this.set('field.choices', Ember.A());
}
this.get('field.choices').pushObject(Ember.Object.create());
},

Datei anzeigen

@ -7,15 +7,15 @@ export default Ember.Component.extend({
@on('init')
@observes('step')
setup() {
this._super(...arguments);
setCurrent() {
this.set('existingId', this.get('step.id'));
const fields = this.get('step.fields') || [];
const actions = this.get('step.actions') || [];
this.set('currentField', fields[0]);
this.set('currentAction', actions[0]);
},
@computed('step.fields.[]', 'currentField')
@computed('step.fields.@each.id', 'currentField')
fieldLinks(fields, current) {
if (!fields) return;
@ -24,7 +24,7 @@ export default Ember.Component.extend({
const id = f.get('id');
const label = f.get('label');
let link = { id, label: label || id };
let link = { id, label: label || id || 'new' };
let classes = 'btn';
if (current && f.get('id') === current.get('id')) {
@ -38,7 +38,7 @@ export default Ember.Component.extend({
});
},
@computed('step.actions.[]', 'currentAction')
@computed('step.actions.@each.id', 'currentAction')
actionLinks(actions, current) {
if (!actions) return;
@ -47,7 +47,7 @@ export default Ember.Component.extend({
const id = a.get('id');
const label = a.get('label');
let link = { id, label: label || id };
let link = { id, label: label || id || 'new' };
let classes = 'btn';
if (current && a.get('id') === current.get('id')) {
@ -64,20 +64,14 @@ export default Ember.Component.extend({
actions: {
addField() {
const fields = this.get('step.fields');
const newNum = fields.length + 1;
const field = Ember.Object.create({
id: `field-${newNum}`
});
const field = Ember.Object.create();
fields.pushObject(field);
this.set('currentField', field);
},
addAction() {
const actions = this.get('step.actions');
const newNum = actions.length + 1;
const action = Ember.Object.create({
id: `action-${newNum}`
});
const action = Ember.Object.create();
actions.pushObject(action);
this.set('currentAction', action);
},

Datei anzeigen

@ -1,15 +1,14 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
@computed('model.steps.[]', 'currentStep')
@computed('model.steps.@each.id', 'currentStep')
stepLinks(steps, currentStep) {
return steps.map((s) => {
if (s) {
const id = s.get('id');
const title = s.get('title');
let link = { id, title: title || id };
let link = { id, title: title || id || 'new' };
let classes = 'btn';
if (currentStep && id === currentStep.get('id')) {
@ -25,17 +24,27 @@ export default Ember.Controller.extend({
@computed('model.id', 'model.name')
wizardUrl(wizardId) {
return window.location.origin + '/wizard/custom/' + Ember.String.dasherize(wizardId);
return window.location.origin + '/w/' + Ember.String.dasherize(wizardId);
},
actions: {
save() {
this.get('model').save().then(() => {
this.setProperties({
saving: true,
error: null
});
const wizard = this.get('model');
wizard.save().then(() => {
this.set('saving', false);
if (this.get('newWizard')) {
this.send("refreshAllWizards");
} else {
this.send("refreshWizard");
}
}).catch((error) => {
this.set('saving', false);
this.set('error', I18n.t(`admin.wizard.error.${error}`));
Ember.run.later(() => this.set('error', null), 10000);
});
},
@ -47,11 +56,9 @@ export default Ember.Controller.extend({
addStep() {
const steps = this.get('model.steps');
const newNum = steps.length + 1;
const step = Ember.Object.create({
fields: Ember.A(),
actions: Ember.A(),
id: `step-${newNum}`
actions: Ember.A()
});
steps.pushObject(step);
this.set('currentStep', step);

Datei anzeigen

@ -1,92 +1,92 @@
import { observes, on } from 'ember-addons/ember-computed-decorators';
import { ajax } from 'discourse/lib/ajax';
const CustomWizard = Discourse.Model.extend({
@on('init')
setup() {
const id = this.get('id');
if (id) this.set('existingId', id);
},
@observes('name')
updateId() {
const name = this.get('name');
this.set('id', name.underscore());
},
save() {
const stepsObj = this.get('steps');
let steps = [];
stepsObj.forEach((s) => {
if (!s.title && !s.translation_key) return;
let step = {
id: (s.title || s.translation_key.split('.').pop()).underscore(),
fields: [],
actions: []
};
if (s.title) step['title'] = s.title;
if (s.translation_key) step['translation_key'] = s.translation_key;
if (s.banner) step['banner'] = s.banner;
if (s.description) step['description'] = s.description;
const fields = s.get('fields');
fields.forEach((f) => {
const fl = f.get('label');
const fkey = f.get('translation_key');
if (!fl && !fkey) return;
f.set('id', (fl || fkey.split('.').pop()).underscore());
if (f.get('type') === 'dropdown') {
const choices = f.get('choices');
choices.forEach((c) => {
const cl = c.get('label');
const ckey = c.get('translation_key');
if (!cl && !ckey) return;
c.set('id', (cl || ckey.split('.').pop()).underscore());
});
}
step['fields'].push(f);
});
s.actions.forEach((a) => {
const al = a.get('label');
if (!al) return;
a.set('id', al.underscore());
step['actions'].push(a);
});
steps.push(step);
});
return new Ember.RSVP.Promise((resolve, reject) => {
const id = this.get('id');
if (!id || !id.underscore()) reject('id_required');
let wizard = { id: id.underscore() };
const steps = this.get('steps');
if (steps.length) wizard['steps'] = this.buildSteps(steps, reject);
const name = this.get('name');
if (name) wizard['name'] = name;
const background = this.get('background');
if (background) wizard['background'] = background;
const save_submissions = this.get('save_submissions');
let wizard = { id, name, background, save_submissions, steps };
if (save_submissions) wizard['save_submissions'] = save_submissions;
const existingId = this.get('existingId');
if (existingId && existingId !== id) {
wizard['existing_id'] = existingId;
};
const multiple_submissions = this.get('multiple_submissions');
if (multiple_submissions) wizard['multiple_submissions'] = multiple_submissions;
return ajax("/admin/wizards/custom/save", {
ajax("/admin/wizards/custom/save", {
type: 'PUT',
data: {
wizard: JSON.stringify(wizard)
}
}).then((result) => resolve(result));
});
},
buildSteps(stepsObj, reject) {
let steps = [];
stepsObj.some((s) => {
if (!s.id || !s.id.underscore()) reject('id_required');
let step = { id: s.id.underscore() };
if (s.title) step['title'] = s.title;
if (s.key) step['key'] = s.key;
if (s.banner) step['banner'] = s.banner;
if (s.description) step['description'] = s.description;
const fields = s.get('fields');
if (fields.length) {
step['fields'] = [];
fields.some((f) => {
let id = f.get('id');
if (!id || !id.underscore()) reject('id_required');
f.set('id', id.underscore());
if (f.get('type') === 'dropdown') {
const choices = f.get('choices');
if (choices && choices.length < 1 && !f.get('choices_key') && !f.get('choices_categories')) {
reject('field.need_choices');
}
}
step['fields'].push(f);
});
}
const actions = s.actions;
if (actions.length) {
step['actions'] = [];
actions.some((a) => {
let id = a.get('id');
if (!id || !id.underscore()) reject('id_required');
a.set('id', id.underscore());
step['actions'].push(a);
});
}
steps.push(step);
});
return steps;
},
remove() {
return ajax("/admin/wizards/custom/remove", {
type: 'DELETE',
@ -121,16 +121,26 @@ CustomWizard.reopenClass({
if (w) {
props['id'] = w.id;
props['existingId'] = true;
props['name'] = w.name;
props['background'] = w.background;
props['save_submissions'] = w.save_submissions;
props['multiple_submissions'] = w.multiple_submissions;
if (w.steps) {
if (w.steps && w.steps.length) {
w.steps.forEach((s) => {
// clean empty strings
Object.keys(s).forEach((key) => (s[key] === '') && delete s[key]);
let fields = Ember.A();
if (s.fields && s.fields.length) {
s.fields.forEach((f) => {
Object.keys(f).forEach((key) => (f[key] === '') && delete f[key]);
let field = Ember.Object.create(f);
if (f.choices) {
let choices = Ember.A();
f.choices.forEach((c) => {
@ -138,18 +148,22 @@ CustomWizard.reopenClass({
});
field.set('choices', choices);
}
fields.pushObject(field);
});
}
let actions = Ember.A();
if (s.actions && s.actions.length) {
s.actions.forEach((a) => {
actions.pushObject(Ember.Object.create(a));
});
}
steps.pushObject(Ember.Object.create({
id: s.id,
translation_key: s.translation_key,
key: s.key,
title: s.title,
description: s.description,
banner: s.banner,
@ -163,6 +177,7 @@ CustomWizard.reopenClass({
props['name'] = '';
props['background'] = '';
props['save_submissions'] = true;
props['multiple_submissions'] = false;
props['steps'] = Ember.A();
};

Datei anzeigen

@ -2,15 +2,30 @@ import CustomWizard from '../models/custom-wizard';
import { ajax } from 'discourse/lib/ajax';
export default Discourse.Route.extend({
beforeModel() {
const param = this.paramsFor('adminWizard').wizard_id;
const wizards = this.modelFor('admin-wizards-custom');
if (wizards.length && (param === 'first' || param === 'last')) {
const wizard = wizards.get(`${param}Object`);
if (wizard) {
this.transitionTo('adminWizard', wizard.id.dasherize());
}
}
},
model(params) {
if (params.wizard_id === 'new') {
const wizardId = params.wizard_id;
if (wizardId === 'new') {
this.set('newWizard', true);
return CustomWizard.create();
};
this.set('newWizard', false);
const wizard = this.modelFor('admin-wizards-custom').findBy('id', params.wizard_id.underscore());
if (!wizard) return this.transitionTo('adminWizardsCustom.index');
const wizard = this.modelFor('admin-wizards-custom').findBy('id', wizardId.underscore());
if (!wizard) return this.transitionTo('adminWizard', 'new');
return wizard;
},

Datei anzeigen

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
redirect() {
this.transitionTo('adminWizard', 'first');
}
});

Datei anzeigen

@ -7,8 +7,9 @@ export default Discourse.Route.extend({
afterModel(model) {
const transitionToWizard = this.get('transitionToWizard');
if (transitionToWizard === 'last' && model.length) {
this.transitionTo('adminWizard', model[model.length - 1].id);
if (transitionToWizard && model.length) {
this.set('transitionToWizard', null);
this.transitionTo('adminWizard', transitionToWizard);
};
},

Datei anzeigen

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
redirect() {
this.transitionTo('adminWizardsCustom');
}
});

Datei anzeigen

@ -1 +0,0 @@
export default Discourse.Route.extend();

Datei anzeigen

@ -6,10 +6,10 @@
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.background'}}</h3>
<h3>{{i18n 'admin.wizard.id'}}</h3>
</div>
<div class="setting-value">
{{input name="background" value=model.background placeholderKey="admin.wizard.background_placeholder"}}
{{input name="name" value=model.id placeholderKey="admin.wizard.id_placeholder" disabled=model.existingId}}
</div>
</div>
@ -22,6 +22,15 @@
</div>
</div>
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.background'}}</h3>
</div>
<div class="setting-value">
{{input name="background" value=model.background placeholderKey="admin.wizard.background_placeholder"}}
</div>
</div>
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.save_submissions'}}</h3>
@ -32,6 +41,16 @@
</div>
</div>
<div class="setting full">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.multiple_submissions'}}</h3>
</div>
<div class="setting-value">
{{input type='checkbox' checked=model.multiple_submissions}}
<span for="save">{{i18n 'admin.wizard.multiple_submissions_label'}}</span>
</div>
</div>
<div class="setting full">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.url'}}</h3>
@ -55,8 +74,11 @@
<div class='buttons'>
<button {{action "save"}} disabled={{disableSave}} class='btn btn-primary'>{{i18n 'admin.wizard.save'}}</button>
{{#unless newWizard}}
<button {{action "remove"}} class='btn btn-danger'>{{d-icon "trash-o"}}{{i18n 'admin.wizard.remove'}}</button>
<button {{action "remove"}} class='btn btn-danger remove'>{{d-icon "trash-o"}}{{i18n 'admin.wizard.remove'}}</button>
{{/unless}}
<span class="saving {{unless savingStatus 'hidden'}}">{{savingStatus}}</span>
{{conditional-loading-spinner condition=saving size='small'}}
{{#if error}}
<span class="error">{{d-icon "times"}}{{error}}</span>
{{/if}}
</div>
</div>

Datei anzeigen

@ -1,5 +1,5 @@
<div class='row'>
<div class='wizard-list'>
<div class='content-list wizard-list'>
<ul>
{{#each model as |s|}}
<li>

Datei anzeigen

@ -1,15 +1,15 @@
<div class="setting">
<div class="setting-label">
<h3>{{i18n "admin.wizard.action.label"}}</h3>
<h3>{{i18n "admin.wizard.action.id"}}</h3>
</div>
<div class="setting-value">
{{input value=action.label}}
{{input value=action.id placeholderKey='admin.wizard.id_placeholder' disabled=existingId}}
</div>
</div>
<div class="setting">
<div class="setting-label">
<h3>{{i18n "admin.wizard.action.type"}}</h3>
<h3>{{i18n "admin.wizard.type"}}</h3>
</div>
<div class="setting-value">
{{combo-box value=action.type content=types}}

Datei anzeigen

@ -1,9 +1,18 @@
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.translation'}}</h3>
<h3>{{i18n 'admin.wizard.id'}}</h3>
</div>
<div class="setting-value">
{{input name="translation_key" value=field.translation_key placeholderKey="admin.wizard.field.translation_placeholder"}}
{{input name="id" value=field.id placeholderKey="admin.wizard.id_placeholder" disabled=existingId}}
</div>
</div>
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.key'}}</h3>
</div>
<div class="setting-value">
{{input name="key" value=field.key placeholderKey="admin.wizard.key_placeholder"}}
</div>
</div>
@ -12,7 +21,7 @@
<h3>{{i18n 'admin.wizard.field.label'}}</h3>
</div>
<div class="setting-value">
{{input name="label" value=field.label}}
{{input name="label" value=field.label placeholder=(i18n "admin.wizard.custom_text_placeholder")}}
</div>
</div>
@ -21,13 +30,13 @@
<h3>{{i18n 'admin.wizard.field.description'}}</h3>
</div>
<div class="setting-value">
{{textarea name="description" value=field.description}}
{{textarea name="description" value=field.description placeholder=(i18n "admin.wizard.custom_text_placeholder")}}
</div>
</div>
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.field.type'}}</h3>
<h3>{{i18n 'admin.wizard.type'}}</h3>
</div>
<div class="setting-value">
{{combo-box value=field.type content=types}}
@ -44,17 +53,57 @@
</div>
</div>
{{#if isInput}}
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.field.min_length'}}</h3>
</div>
<div class="setting-value">
{{input type="number" name="min_length" value=field.min_length placeholder=(i18n 'admin.wizard.field.min_length_placeholder')}}
</div>
</div>
{{/if}}
{{#if isDropdown}}
<div class="wizard-dropdown-choices">
<div class="wizard-header small">
<div class="wizard-header medium">
{{i18n 'admin.wizard.field.choices_label'}}
</div>
<div class="setting">
<div class="wizard-header small">
{{i18n 'admin.wizard.field.choices_translation'}}
</div>
<div class="setting-value">
{{input name="key" value=field.choices_key placeholderKey="admin.wizard.key_placeholder"}}
</div>
</div>
<div class="setting full">
<div class="wizard-header small">
{{i18n 'admin.wizard.field.choices_preset.label'}}
</div>
{{combo-box value=field.choices_preset content=presetChoices none='admin.wizard.field.choices_preset.none'}}
<label>{{i18n 'admin.wizard.field.choices_preset.filter'}}</label>
{{#each presetFilters as |f|}}
<span class='custom-input'>
{{input type="text" value=f.key placeholder=(i18n 'admin.wizard.field.choices_preset.key')}}
{{input type="text" value=f.value placeholder=(i18n 'admin.wizard.field.choices_preset.value')}}
</span>
{{d-button action='removeFilter' actionParam=f icon='times'}}
{{/each}}
<div>{{d-button action='addFilter' label='admin.wizard.add' icon='plus'}}</div>
</div>
<div class="setting full">
<div class="wizard-header small">
{{i18n 'admin.wizard.field.choices_custom'}}
</div>
{{#each dropdownChoices as |c|}}
<span class='wizard-dropdown-choice'>
{{input type='text' value=c.label}}
<span class='custom-input'>
{{input type='text' value=c.value placeholder=(i18n 'admin.wizard.field.choice.value')}}
{{input type='text' value=c.label placeholder=(i18n 'admin.wizard.field.choice.label')}}
</span>
{{d-button action='removeChoice' actionParam=c icon='times'}}
{{/each}}
{{d-button action='addChoice' label='admin.wizard.add' icon='plus'}}
<div>{{d-button action='addChoice' label='admin.wizard.add' icon='plus'}}</div>
</div>
</div>
{{/if}}

Datei anzeigen

@ -1,9 +1,18 @@
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.translation'}}</h3>
<h3>{{i18n 'admin.wizard.id'}}</h3>
</div>
<div class="setting-value">
{{input name="translation_key" value=step.translation_key placeholderKey="admin.wizard.step.translation_placeholder"}}
{{input name="id" value=step.id placeholderKey="admin.wizard.id_placeholder" disabled=existingId}}
</div>
</div>
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.key'}}</h3>
</div>
<div class="setting-value">
{{input name="key" value=step.key placeholderKey="admin.wizard.key_placeholder"}}
</div>
</div>
@ -12,7 +21,7 @@
<h3>{{i18n 'admin.wizard.step.title'}}</h3>
</div>
<div class="setting-value">
{{input name="title" value=step.title placeholderKey="admin.wizard.step.title_placeholder"}}
{{input name="title" value=step.title placeholderKey="admin.wizard.custom_text_placeholder"}}
</div>
</div>
@ -30,7 +39,7 @@
<h3>{{i18n 'admin.wizard.step.description'}}</h3>
</div>
<div class="setting-value">
{{textarea name="description" value=step.description placeholder=(i18n "admin.wizard.step.description_placeholder")}}
{{textarea name="description" value=step.description placeholder=(i18n "admin.wizard.custom_text_placeholder")}}
</div>
</div>

Datei anzeigen

@ -1,5 +1,4 @@
//= require ./wizard/custom-wizard
//= require_tree ./wizard/components
//= require_tree ./wizard/controllers
//= require_tree ./wizard/helpers
//= require_tree ./wizard/initializers

Datei anzeigen

@ -1,28 +0,0 @@
import { observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
classNames: 'wizard-field-composer',
keyPress(e) {
e.stopPropagation();
},
@observes('field.value')
validate() {
const minLength = Wizard.SiteSettings.min_post_length;
const post = this.get('field.value');
const field = this.get('field');
field.set('customValidation', true);
if (!post) {
return field.setValid(false);
}
if (minLength && post.length < minLength) {
return field.setValid(false, I18n.t('wizard.validation.too_short', { min: minLength }));
}
field.setValid(true);
}
});

Datei anzeigen

@ -7,7 +7,7 @@ export default StepController.extend({
const next = this.get('step.next');
if (response.refresh_required) {
const id = this.get('wizard.id');
document.location = getUrl(`/wizard/custom/${id}/steps/${next}`);
document.location = getUrl(`/w/${id}/steps/${next}`);
} else {
this.transitionToRoute('custom.step', next);
}
@ -15,6 +15,10 @@ export default StepController.extend({
goBack() {
this.transitionToRoute('custom.step', this.get('step.previous'));
},
showMessage(message) {
this.set('stepMessage', message);
}
}
});

Datei anzeigen

@ -4,80 +4,27 @@ export default {
initialize(app) {
if (app.constructor.name !== 'Class' || app.get('rootElement') !== '#custom-wizard-main') return;
const WizardApplicationRoute = requirejs('wizard/routes/application').default;
const findCustomWizard = requirejs('discourse/plugins/discourse-custom-wizard/wizard/models/custom').findCustomWizard;
const Router = requirejs('wizard/router').default;
const ApplicationRoute = requirejs('wizard/routes/application').default;
const ajax = requirejs('wizard/lib/ajax').ajax;
const StepRoute = requirejs('wizard/routes/step').default;
const StepModel = requirejs('wizard/models/step').default;
const WizardStep = requirejs('wizard/components/wizard-step').default;
const getUrl = requirejs('discourse-common/lib/get-url').default;
const FieldModel = requirejs('wizard/models/wizard-field').default;
Router.reopen({
rootURL: getUrl('/w/')
});
Router.map(function() {
this.route('custom', { path: '/custom/:id' }, function() {
this.route('custom', { path: '/:wizard_id' }, function() {
this.route('step', { path: '/steps/:step_id' });
});
});
WizardApplicationRoute.reopen({
model() {
const customParams = this.paramsFor('custom');
return findCustomWizard(customParams.id);
},
afterModel(model) {
return Ember.RSVP.hash({
info: ajax({
url: `/site/basic-info`,
type: 'GET',
}).then((result) => {
return model.set('siteInfo', result);
}),
settings: ajax({
url: `/site/settings`,
type: 'GET',
}).then((result) => {
Object.assign(Wizard.SiteSettings, result);
})
});
},
setupController(controller, model) {
Ember.run.scheduleOnce('afterRender', this, function(){
$('body.custom-wizard').css('background', model.get('background'));
});
controller.setProperties({
customWizard: true,
siteInfo: model.get('siteInfo')
});
}
});
StepModel.reopen({
save() {
const fields = {};
this.get('fields').forEach(f => fields[f.id] = f.value);
return ajax({
url: `/wizard/custom/${this.get('wizardId')}/steps/${this.get('id')}`,
type: 'PUT',
data: { fields }
}).catch(response => {
response.responseJSON.errors.forEach(err => this.fieldError(err.field, err.description));
throw response;
});
}
});
StepRoute.reopen({
afterModel(model) {
if (!model) {
return document.location = getUrl("/");
}
const wizard = this.modelFor('application');
return model.set("wizardId", wizard.id);
ApplicationRoute.reopen({
redirect() {
this.transitionTo('custom');
}
});
@ -93,12 +40,17 @@ export default {
};
}.property('step.banner'),
handleMessage: function() {
const message = this.get('step.message');
this.sendAction('showMessage', message);
}.observes('step.message'),
advance() {
this.set('saving', true);
this.get('step').save()
.then(response => {
if (this.get('finalStep')) {
document.location = getUrl("/");
this.sendAction('finished', response);
} else {
this.sendAction('goNext', response);
}
@ -111,10 +63,61 @@ export default {
quit() {
this.set('finalStep', true);
this.send('nextStep');
},
showMessage(message) {
this.sendAction('showMessage', message);
}
}
});
StepModel.reopen({
save() {
const wizardId = this.get('wizardId');
const fields = {};
this.get('fields').forEach(f => fields[f.id] = f.value);
return ajax({
url: `/w/${wizardId}/steps/${this.get('id')}`,
type: 'PUT',
data: { fields }
}).catch(response => {
if (response && response.responseJSON && response.responseJSON.errors) {
let wizardErrors = [];
response.responseJSON.errors.forEach(err => {
if (err.field === wizardId) {
wizardErrors.push(err.description);
} else if (err.field) {
this.fieldError(err.field, err.description);
} else if (err) {
wizardErrors.push(err);
}
});
if (wizardErrors.length) {
this.handleWizardError(wizardErrors.join('\n'));
}
throw response;
}
if (response && response.responseText) {
const responseText = response.responseText;
const start = responseText.indexOf('>') + 1;
const end = responseText.indexOf('plugins');
const message = responseText.substring(start, end);
this.handleWizardError(message);
throw message;
}
});
},
handleWizardError(message) {
this.set('message', {
state: 'error',
text: message
});
Ember.run.later(() => this.set('message', null), 6000);
}
});
FieldModel.reopen({
check() {
let valid = this.get('valid');

Datei anzeigen

@ -9,13 +9,16 @@ const CustomWizard = Ember.Object.extend({
});
export function findCustomWizard(wizardId) {
return ajax({ url: `/wizard/custom/${wizardId}` }).then(result => {
return ajax({ url: `/w/${wizardId}` }).then(result => {
const wizard = result.wizard;
if (!wizard.completed) {
wizard.steps = wizard.steps.map(step => {
const stepObj = Step.create(step);
stepObj.fields = stepObj.fields.map(f => WizardField.create(f));
return stepObj;
});
}
return CustomWizard.create(wizard);
});

Datei anzeigen

@ -1,8 +1,15 @@
import IndexRoute from 'wizard/routes/index';
export default IndexRoute.extend({
export default Ember.Route.extend({
beforeModel() {
const appModel = this.modelFor('application');
const appModel = this.modelFor('custom');
if (appModel.completed) {
this.set('completed', true);
} else if (appModel.start) {
this.replaceWith('custom.step', appModel.start);
}
},
setupController(controller) {
const completed = this.get('completed');
controller.set('completed', completed);
}
});

Datei anzeigen

@ -1,3 +1,23 @@
import StepRoute from 'wizard/routes/step';
export default Ember.Route.extend({
model(params) {
const appModel = this.modelFor('custom');
const allSteps = appModel.steps;
if (allSteps) {
const step = allSteps.findBy('id', params.step_id);
return step ? step : allSteps[0];
};
export default StepRoute.extend();
return appModel;
},
afterModel(model) {
if (model.completed) return this.transitionTo('index');
return model.set("wizardId", this.modelFor('custom').id);
},
setupController(controller, step) {
controller.setProperties({
step, wizard: this.modelFor('custom')
});
}
});

Datei anzeigen

@ -0,0 +1,37 @@
import { findCustomWizard } from '../models/custom';
import { ajax } from 'wizard/lib/ajax';
import { getUrl } from 'discourse-common/lib/get-url';
export default Ember.Route.extend({
model(params) {
return findCustomWizard(params.wizard_id);
},
afterModel() {
return ajax({
url: `/site/settings`,
type: 'GET',
}).then((result) => {
Object.assign(Wizard.SiteSettings, result);
});
},
setupController(controller, model) {
Ember.run.scheduleOnce('afterRender', this, function(){
$('body.custom-wizard').css('background', model.get('background'));
});
controller.setProperties({
customWizard: true,
logoUrl: Wizard.SiteSettings.logo_small_url
});
},
actions: {
finished(result) {
let url = "/";
if (result.topic_id) url += `t/${result.topic_id}`;
document.location.replace(getUrl(url));
}
}
});

Datei anzeigen

@ -1,16 +1 @@
{{#if showCanvas}}
{{wizard-canvas}}
{{/if}}
<div class='wizard-column'>
<div class='wizard-column-contents'>
{{outlet}}
</div>
<div class='wizard-footer'>
{{#if customWizard}}
<img src="{{siteInfo.logo_small_url}}" style="background-image: initial; width: 33px; height: 33px;"/>
{{else}}
<div class='discourse-logo'></div>
{{/if}}
</div>
</div>
{{outlet}}

Datei anzeigen

@ -1 +0,0 @@
{{textarea elementId=field.id value=field.value placeholder=field.placeholder tabindex="9"}}

Datei anzeigen

@ -0,0 +1,16 @@
{{#if showCanvas}}
{{wizard-canvas}}
{{/if}}
<div class='wizard-column'>
<div class='wizard-column-contents'>
{{outlet}}
</div>
<div class='wizard-footer'>
{{#if customWizard}}
<img src="{{logoUrl}}" style="background-image: initial; width: 33px; height: 33px;"/>
{{else}}
<div class='discourse-logo'></div>
{{/if}}
</div>
</div>

Datei anzeigen

@ -0,0 +1,3 @@
{{#if completed}}
{{i18n 'wizard.completed'}}
{{/if}}

Datei anzeigen

@ -1 +1,9 @@
{{wizard-step step=step wizard=wizard goNext="goNext" goBack="goBack"}}
<div class="step-message {{stepMessage.state}}">
{{stepMessage.text}}
</div>
{{wizard-step step=step
wizard=wizard
goNext="goNext"
goBack="goBack"
finished="finished"
showMessage="showMessage"}}

Datei anzeigen

@ -0,0 +1,89 @@
.custom-wizard {
background-color: initial;
.wizard-step-description {
line-height: 1.7;
}
.wizard-column .wizard-step-banner {
width: initial;
max-width: 660px;
}
.control-group {
display: inline-block;
vertical-align: top;
margin-right: 20px;
.controls {
margin: 5px 0;
}
input {
width: 200px;
line-height: 24px;
}
}
.wizard-step-form .wizard-btn {
display: block;
margin: 10px 0;
}
.wizard-column .wizard-field .input-area {
margin: 0.5em 0;
}
}
.p-list-box {
max-width: 550px;
position: relative;
margin: 10px 0;
.spinner {
position: absolute;
right: 50%;
top: 50%;
}
.p-text {
margin-bottom: 5px;
}
ul {
border: 1px solid #e9e9e9;
padding: 0;
margin: 0;
list-style: none;
height: 95px;
overflow: scroll;
}
li {
padding: 6px 12px;
cursor: pointer;
background-color: #fff;
&:hover, &.selected {
background-color: #eee;
}
i {
margin-right: 5px;
}
}
.no-results {
padding: 15px;
}
.default {
margin: 0 auto;
top: 50%;
transform: translateY(-50%);
position: absolute;
width: 100%;
text-align: center;
color: #919191;
}
}

Datei anzeigen

@ -13,15 +13,17 @@
}
.wizard-header {
font-size: 1.3em;
font-size: 1.4em;
margin-bottom: 15px;
&.medium {
font-size: 1.1em;
font-size: 1.2em;
}
&.small {
font-size: 0.97em;
font-size: 1em;
text-decoration: underline;
margin-bottom: 5px;
}
}
@ -36,7 +38,7 @@
.setting {
display: inline-block;
vertical-align: top;
min-width: 49%;
width: 49%;
.setting-label {
width: 90px;
@ -49,6 +51,22 @@
&.full {
width: 100%;
}
label {
margin: 5px 0;
}
}
.buttons .error {
color: $danger;
.fa {
margin-right: 5px;
}
}
.buttons .remove {
float: right;
}
}
@ -60,19 +78,49 @@
}
}
.wizard-column-contents {
position: relative;
}
.wizard-custom-step {
display: inline-block;
width: 100%;
margin-bottom: 20px;
padding: 15px;
background-color: dark-light-diff($primary, $secondary, 96%, -65%);
}
.wizard-dropdown-choices {
margin-bottom: 25px;
.step-message {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 0;
line-height: 0;
text-align: center;
transition: all .2s;
&.success {
height: 60px;
line-height: 60px;
background-color: $success;
color: $secondary;
}
&.error {
height: 60px;
line-height: 60px;
background-color: $danger;
color: $secondary;
}
}
.wizard-dropdown-choice {
.wizard-dropdown-choices {
padding: 15px 15px 0 15px;
margin-bottom: 20px;
background-color: $secondary;
}
.setting .custom-input {
display: inline-block;
}

Datei anzeigen

@ -12,38 +12,55 @@ en:
background_placeholder: "Background css property"
save_submissions: "Save"
save_submissions_label: "Save wizard submissions"
multiple_submissions: "Multiple"
multiple_submissions_label: "Allow multiple submissions by the same user"
save: "Save Changes"
remove: "Delete Wizard"
header: "Wizard"
add: "Add"
url: "Url"
translation: "Translation"
key: "Key"
id: "Id"
id_placeholder: "Underscored. Cannot be changed."
key_placeholder: "Translation key"
custom_text_placeholder: "Overrides translation"
type: "Type"
error:
name_required: "Wizards must have a name."
steps_required: "Wizards must have at least one step."
id_required: "All Step, Fields and Actions need an Id"
field:
need_choices: "All dropdowns need a translated choices, custom choies or preset choices."
choices_label_empty: "Custom choice labels cannot be empty."
step:
header: "Steps"
title: "Title"
title_placeholder: "Overrides title translation"
banner: "Banner"
banner_placeholder: "Image url"
description: "Description"
description_placeholder: "Overrides description translation"
translation_placeholder: "Translation key for step"
field:
header: "Fields"
label: "Label"
description: "Description"
type: "Type"
choices_label: "Dropdown Choices"
add_choice: "Add"
choices_label: "Dropdown Choices (use one type)"
choices_translation: "Translation"
choices_custom: "Custom"
choices_preset:
label: "Preset"
none: "Select a data type"
categories: "Categories"
filter: "Filter"
key: "Key"
value: "Value"
choice:
value: "Value"
label: "Label"
required: "Required"
required_label: "Field is Required"
translation_placeholder: "Translation key for field"
min_length: "Min Length"
min_length_placeholder: "Minimum length in characters"
action:
header: "Actions"
label: "Label"
type: "Type"
send_message:
label: "Send Message"
title: "Title"
@ -62,5 +79,4 @@ en:
wizard_js:
wizard:
validation:
too_short: "Post must be at least {{min}} characters"
completed: "You have completed this wizard."

Datei anzeigen

@ -1,11 +1,5 @@
en:
custom_wizard:
title: "Wizard"
new_wizard:
step_1:
title: "Translated title"
description: "Translated description"
field_1:
label: "Translated field title"
description: "Translated field description"
wizard:
custom_title: "Wizard"
field:
too_short: "%{label} must be at least %{min} characters"

Datei anzeigen

@ -1,12 +1,17 @@
class CustomWizard::Builder
attr_accessor :wizard, :updater, :submission
def initialize(user, wizard_id)
data = PluginStore.get('custom_wizard', wizard_id)
@custom_wizard = CustomWizard::Wizard.new(data)
@wizard = Wizard.new(user)
@wizard.id = wizard_id
@wizard.save_submissions = data['save_submissions']
@wizard.background = data["background"]
@wizard = Wizard.new(user,
id: wizard_id,
save_submissions: data['save_submissions'],
multiple_submissions: data['multiple_submissions'],
background: data["background"],
custom: true
)
end
def self.sorted_handlers
@ -23,13 +28,16 @@ class CustomWizard::Builder
end
def build
unless (@wizard.completed? && !@custom_wizard.respond_to?(:multiple_submissions)) ||
!@custom_wizard.steps
@custom_wizard.steps.each do |s|
@wizard.append_step(s['id']) do |step|
step.title = s['title'] if s['title']
step.description = s['description'] if s['description']
step.banner = s['banner'] if s['banner']
step.translation_key = s['translation_key'] if s['translation_key']
step.key = s['key'] if s['key']
if s['fields'] && s['fields'].length
s['fields'].each do |f|
params = {
id: f['id'],
@ -39,44 +47,76 @@ class CustomWizard::Builder
params[:label] = f['label'] if f['label']
params[:description] = f['description'] if f['description']
params[:translation_key] = f['translation_key'] if f['translation_key']
params[:key] = f['key'] if f['key']
submissions = Array.wrap(PluginStore.get("custom_wizard_submissions", @wizard.id))
if submissions.last && submissions.last['completed'] === false
@submission = submissions.last
params[:value] = @submission[f['id']] if @submission[f['id']]
end
field = step.add_field(params)
if f['type'] == 'dropdown'
if f['type'] === 'dropdown'
if f['choices'] && f['choices'].length > 0
f['choices'].each do |c|
field.add_choice(c['id'], label: c['label'])
field.add_choice(c['value'], label: c['label'])
end
elsif f['choices_key'] && f['choices_key'].length > 0
choices = I18n.t(f['choices_key'])
if choices.is_a?(Hash)
choices.each do |k, v|
field.add_choice(k, label: v)
end
end
elsif f['choices_preset'] && f['choices_preset'].length > 0
objects = []
if f['choices_preset'] === 'categories'
objects = Site.new(Guardian.new(@wizard.user)).categories
end
if f['choices_filters'] && f['choices_filters'].length > 0
f['choices_filters'].each do |f|
objects.reject! { |o| o[f['key']] != f['value'] }
end
end
if objects.length > 0
objects.each do |o|
field.add_choice(o.id, label: o.name)
end
end
end
end
end
end
step.on_update do |updater|
@updater = updater
input = updater.fields
user = @wizard.user
if @wizard.save_submissions && input
store_key = @wizard.id
submissions = Array.wrap(PluginStore.get("custom_wizard_submissions", store_key))
submission = {}
if submissions.last && submissions.last['completed'] === false
submission = submissions.last
submissions.pop(1)
if s['fields'] && s['fields'].length
s['fields'].each do |f|
value = input[f['id']]
min_length = f['min_length']
if min_length && value.is_a?(String) && value.length < min_length.to_i
label = f['label'] || I18n.t("#{f['key']}.label")
updater.errors.add(f['id'].to_s, I18n.t('wizard.field.too_short', label: label, min: min_length.to_i))
end
end
end
submission['user_id'] = @wizard.user.id
submission['completed'] = updater.step.next.nil?
next if updater.errors.any?
input.each do |key, value|
submission[key] = value
CustomWizard::Builder.step_handlers.each do |handler|
if handler[:wizard_id] == @wizard.id
handler[:block].call(self)
end
end
submissions.push(submission)
PluginStore.set('custom_wizard_submissions', store_key, submissions)
end
next if updater.errors.any?
if s['actions'] && s['actions'].length
s['actions'].each do |a|
@ -89,11 +129,11 @@ class CustomWizard::Builder
post = creator.create
if creator.errors.present?
raise StandardError, creator.errors.full_messages.join(" ")
end
updater.errors.add(:create_topic, creator.errors.full_messages.join(" "))
else
updater.result = { topic_id: post.topic.id }
end
end
if a['type'] === 'send_message'
creator = PostCreator.new(user,
@ -105,17 +145,35 @@ class CustomWizard::Builder
post = creator.create
if creator.errors.present?
raise StandardError, creator.errors.full_messages.join(" ")
end
updater.errors.add(:send_message, creator.errors.full_messages.join(" "))
else
updater.result = { topic_id: post.topic.id }
end
end
end
end
CustomWizard::Builder.step_handlers.each do |handler|
if handler[:wizard_id] == @wizard.id
handler[:block].call(self)
if @wizard.save_submissions && updater.errors.empty?
store_key = @wizard.id
submissions = Array.wrap(PluginStore.get("custom_wizard_submissions", store_key))
submission = {}
if submissions.last && submissions.last['completed'] === false
submission = submissions.last
submissions.pop(1)
end
submission['user_id'] = @wizard.user.id
submission['completed'] = updater.step.next.nil?
if input
input.each do |key, value|
submission[key] = value
end
end
submissions.push(submission)
PluginStore.set('custom_wizard_submissions', store_key, submissions)
end
end
end

Datei anzeigen

@ -1,6 +1,6 @@
class CustomWizard::Field
def self.types
@types ||= ['dropdown', 'image', 'radio', 'text', 'textarea', 'composer']
@types ||= ['dropdown', 'image', 'radio', 'text', 'textarea']
end
def self.require_assets

Datei anzeigen

@ -1,6 +1,6 @@
class CustomWizard::Wizard
attr_reader :id, :name, :steps, :background, :save_submissions, :custom
attr_reader :id, :name, :steps, :background, :save_submissions, :multiple_submissions, :custom
def initialize(data)
data = data.is_a?(String) ? ::JSON.parse(data) : data
@ -8,6 +8,7 @@ class CustomWizard::Wizard
@name = data['name']
@background = data['background']
@save_submissions = data['save_submissions']
@multiple_submissions = data['multiple_submissions']
@steps = data['steps']
@custom = true
end

114
plugin.rb
Datei anzeigen

@ -3,10 +3,11 @@
# version: 0.1
# authors: Angus McLeod
register_asset 'stylesheets/custom_wizard.scss'
register_asset 'stylesheets/wizard_custom_admin.scss'
config = Rails.application.config
config.assets.paths << Rails.root.join("plugins", "discourse-custom-wizard", "assets", "javascripts")
config.assets.paths << Rails.root.join("plugins", "discourse-custom-wizard", "assets", "stylesheets", "wizard")
after_initialize do
require_dependency "application_controller"
@ -33,9 +34,7 @@ after_initialize do
require_dependency 'admin_constraint'
Discourse::Application.routes.append do
namespace :wizard do
mount ::CustomWizard::Engine, at: 'custom'
end
mount ::CustomWizard::Engine, at: 'w'
scope module: 'custom_wizard', constraints: AdminConstraint.new do
get 'admin/wizards' => 'admin#index'
@ -52,21 +51,61 @@ after_initialize do
end
end
class ::Wizard
attr_accessor :id, :background, :save_submissions
end
class ::Wizard::Step
attr_accessor :title, :description, :translation_key
end
class ::Wizard::StepUpdater
attr_accessor :result, :step
end
require_dependency 'wizard'
require_dependency 'wizard/step'
require_dependency 'wizard/step_updater'
require_dependency 'wizard/field'
Wizard::Field.class_eval do
attr_reader :label, :description, :translation_key
::Wizard.class_eval do
attr_accessor :id, :background, :save_submissions, :multiple_submissions
def initialize(user, attrs = {})
@steps = []
@user = user
@first_step = nil
@max_topics_to_require_completion = 15
@id = attrs[:id] if attrs[:id]
@save_submissions = attrs[:save_submissions] if attrs[:save_submissions]
@multiple_submissions = attrs[:multiple_submissions] if attrs[:multiple_submissions]
@background = attrs[:background] if attrs[:background]
@custom = attrs[:custom] if attrs[:custom]
end
def completed?
completed_steps?(@steps.map(&:id))
end
def completed_steps?(steps)
steps = [steps].flatten.uniq
completed = UserHistory.where(
acting_user_id: @user.id,
action: UserHistory.actions[:wizard_step]
).where(context: steps)
.distinct.order(:context).pluck(:context)
steps.sort == completed
end
def start
completed = UserHistory.where(
acting_user_id: @user.id,
action: UserHistory.actions[:wizard_step]
).where(context: @steps.map(&:id))
.uniq.pluck(:context)
# First uncompleted step
steps = @custom ? @steps : steps_with_fields
steps.each do |s|
return s unless completed.include?(s.id)
end
@first_step
end
end
::Wizard::Field.class_eval do
attr_reader :label, :description, :key, :min_length
def initialize(attrs)
attrs = attrs || {}
@ -76,14 +115,23 @@ after_initialize do
@required = !!attrs[:required]
@label = attrs[:label]
@description = attrs[:description]
@translation_key = attrs[:translation_key]
@key = attrs[:key]
@min_length = attrs[:min_length]
@value = attrs[:value]
@choices = []
end
end
class ::Wizard::Step
attr_accessor :title, :description, :key
end
class ::Wizard::StepUpdater
attr_accessor :result, :step
end
::WizardSerializer.class_eval do
attributes :id, :background
attributes :id, :background, :completed
def id
object.id
@ -93,32 +141,48 @@ after_initialize do
object.background
end
def completed
object.completed?
end
def include_completed?
object.completed? && !object.multiple_submissions && !scope.current_user.admin?
end
def include_start?
object.start
object.start && include_steps?
end
def include_steps?
!include_completed?
end
end
::WizardStepSerializer.class_eval do
def title
return object.title if object.title
I18n.t("#{object.translation_key || i18n_key}.title", default: '')
I18n.t("#{object.key || i18n_key}.title", default: '')
end
def description
return object.description if object.description
I18n.t("#{object.translation_key || i18n_key}.description", default: '')
I18n.t("#{object.key || i18n_key}.description", default: '')
end
end
::WizardFieldSerializer.class_eval do
def label
return object.label if object.label
I18n.t("#{object.translation_key || i18n_key}.label", default: '')
I18n.t("#{object.key || i18n_key}.label", default: '')
end
def description
return object.description if object.description
I18n.t("#{object.translation_key || i18n_key}.description", default: '')
I18n.t("#{object.key || i18n_key}.description", default: '')
end
def placeholder
I18n.t("#{object.key || i18n_key}.placeholder", default: '')
end
end
end

Datei anzeigen

@ -1,12 +0,0 @@
.custom-wizard {
background-color: initial;
}
.custom-wizard .wizard-step-description {
line-height: 1.7;
}
.custom-wizard .wizard-column .wizard-step-banner {
width: initial;
max-width: 660px;
}