From e859e3efa2521ccbd1568e0ebfcb8b63a911d8bc Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Fri, 13 Oct 2017 21:02:34 +0800 Subject: [PATCH] various --- app/controllers/admin.rb | 15 +- app/views/layouts/custom_wizard.html.erb | 5 +- .../components/wizard-custom-action.js.es6 | 10 +- .../components/wizard-custom-field.js.es6 | 36 ++- .../components/wizard-custom-step.js.es6 | 22 +- .../discourse/controllers/admin-wizard.js.es6 | 23 +- .../discourse/models/custom-wizard.js.es6 | 171 +++++++------ .../discourse/routes/admin-wizard.js.es6 | 21 +- .../routes/admin-wizards-custom-index.js.es6 | 5 + .../routes/admin-wizards-custom.js.es6 | 5 +- .../routes/admin-wizards-index.js.es6 | 5 + .../discourse/routes/admin-wizards.js.es6 | 1 - .../discourse/templates/admin-wizard.hbs | 30 ++- .../templates/admin-wizards-submissions.hbs | 2 +- .../components/wizard-custom-action.hbs | 6 +- .../components/wizard-custom-field.hbs | 81 +++++-- .../components/wizard-custom-step.hbs | 17 +- assets/javascripts/wizard-custom.js | 1 - .../components/wizard-field-composer.js.es6 | 28 --- .../wizard/controllers/custom-step.js.es6 | 6 +- .../wizard/initializers/custom.js.es6 | 129 +++++----- .../javascripts/wizard/models/custom.js.es6 | 15 +- .../wizard/routes/custom-index.js.es6 | 17 +- .../wizard/routes/custom-step.js.es6 | 24 +- .../javascripts/wizard/routes/custom.js.es6 | 37 +++ .../wizard/templates/application.hbs | 17 +- .../components/wizard-field-composer.hbs | 1 - .../javascripts/wizard/templates/custom.hbs | 16 ++ .../wizard/templates/custom.index.hbs | 3 + .../wizard/templates/custom.step.hbs | 10 +- assets/stylesheets/wizard/wizard_custom.scss | 89 +++++++ ...m_wizard.scss => wizard_custom_admin.scss} | 64 ++++- config/locales/client.en.yml | 46 ++-- config/locales/server.en.yml | 14 +- lib/builder.rb | 228 +++++++++++------- lib/field.rb | 2 +- lib/wizard.rb | 3 +- plugin.rb | 114 +++++++-- public/desktop.css | 12 - 39 files changed, 892 insertions(+), 439 deletions(-) create mode 100644 assets/javascripts/discourse/routes/admin-wizards-custom-index.js.es6 create mode 100644 assets/javascripts/discourse/routes/admin-wizards-index.js.es6 delete mode 100644 assets/javascripts/discourse/routes/admin-wizards.js.es6 delete mode 100644 assets/javascripts/wizard/components/wizard-field-composer.js.es6 create mode 100644 assets/javascripts/wizard/routes/custom.js.es6 delete mode 100644 assets/javascripts/wizard/templates/components/wizard-field-composer.hbs create mode 100644 assets/javascripts/wizard/templates/custom.hbs create mode 100644 assets/javascripts/wizard/templates/custom.index.hbs create mode 100644 assets/stylesheets/wizard/wizard_custom.scss rename assets/stylesheets/{custom_wizard.scss => wizard_custom_admin.scss} (54%) delete mode 100644 public/desktop.css diff --git a/app/controllers/admin.rb b/app/controllers/admin.rb index c6a25bc3..34015ddf 100644 --- a/app/controllers/admin.rb +++ b/app/controllers/admin.rb @@ -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 + PluginStore.set('custom_wizard', wizard["id"], wizard) render json: success_json end diff --git a/app/views/layouts/custom_wizard.html.erb b/app/views/layouts/custom_wizard.html.erb index 6dd50ade..8b6ed735 100644 --- a/app/views/layouts/custom_wizard.html.erb +++ b/app/views/layouts/custom_wizard.html.erb @@ -1,8 +1,7 @@ - <%= 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 @@ <%= render partial: "layouts/head" %> - <%= t 'custom_wizard.title' %> + <%= t 'wizard.custom_title' %> diff --git a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 index 8179def1..513202b0 100644 --- a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 @@ -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')); + } }); diff --git a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 index e308b804..f0e11ca3 100644 --- a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 @@ -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()); }, diff --git a/assets/javascripts/discourse/components/wizard-custom-step.js.es6 b/assets/javascripts/discourse/components/wizard-custom-step.js.es6 index 76a86bd9..b602d20f 100644 --- a/assets/javascripts/discourse/components/wizard-custom-step.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-step.js.es6 @@ -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); }, diff --git a/assets/javascripts/discourse/controllers/admin-wizard.js.es6 b/assets/javascripts/discourse/controllers/admin-wizard.js.es6 index 1130d6c8..ded0244f 100644 --- a/assets/javascripts/discourse/controllers/admin-wizard.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizard.js.es6 @@ -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); diff --git a/assets/javascripts/discourse/models/custom-wizard.js.es6 b/assets/javascripts/discourse/models/custom-wizard.js.es6 index cd709f62..f6262c59 100644 --- a/assets/javascripts/discourse/models/custom-wizard.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard.js.es6 @@ -1,90 +1,90 @@ -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'); + 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'); + if (save_submissions) wizard['save_submissions'] = save_submissions; + + const multiple_submissions = this.get('multiple_submissions'); + if (multiple_submissions) wizard['multiple_submissions'] = multiple_submissions; + + ajax("/admin/wizards/custom/save", { + type: 'PUT', + data: { + wizard: JSON.stringify(wizard) + } + }).then((result) => resolve(result)); + }); + }, + + buildSteps(stepsObj, reject) { let steps = []; - stepsObj.forEach((s) => { + stepsObj.some((s) => { + if (!s.id || !s.id.underscore()) reject('id_required'); - if (!s.title && !s.translation_key) return; - - let step = { - id: (s.title || s.translation_key.split('.').pop()).underscore(), - fields: [], - actions: [] - }; + let step = { id: s.id.underscore() }; if (s.title) step['title'] = s.title; - if (s.translation_key) step['translation_key'] = s.translation_key; + 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'); - fields.forEach((f) => { - const fl = f.get('label'); - const fkey = f.get('translation_key'); + if (fields.length) { + step['fields'] = []; - if (!fl && !fkey) return; + fields.some((f) => { + let id = f.get('id'); - f.set('id', (fl || fkey.split('.').pop()).underscore()); + if (!id || !id.underscore()) reject('id_required'); + f.set('id', id.underscore()); - if (f.get('type') === 'dropdown') { - const choices = f.get('choices'); + 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'); + } + } - choices.forEach((c) => { - const cl = c.get('label'); - const ckey = c.get('translation_key'); + step['fields'].push(f); + }); + } - if (!cl && !ckey) return; + const actions = s.actions; + if (actions.length) { + step['actions'] = []; - c.set('id', (cl || ckey.split('.').pop()).underscore()); - }); - } + actions.some((a) => { + let id = a.get('id'); + if (!id || !id.underscore()) reject('id_required'); - step['fields'].push(f); - }); + a.set('id', id.underscore()); - s.actions.forEach((a) => { - const al = a.get('label'); - if (!al) return; - a.set('id', al.underscore()); - step['actions'].push(a); - }); + step['actions'].push(a); + }); + + } steps.push(step); }); - const id = this.get('id'); - const name = this.get('name'); - const background = this.get('background'); - const save_submissions = this.get('save_submissions'); - let wizard = { id, name, background, save_submissions, steps }; - - const existingId = this.get('existingId'); - if (existingId && existingId !== id) { - wizard['existing_id'] = existingId; - }; - - return ajax("/admin/wizards/custom/save", { - type: 'PUT', - data: { - wizard: JSON.stringify(wizard) - } - }); + return steps; }, remove() { @@ -121,35 +121,49 @@ 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) => { - let fields = Ember.A(); + // clean empty strings + Object.keys(s).forEach((key) => (s[key] === '') && delete s[key]); - s.fields.forEach((f) => { - let field = Ember.Object.create(f); - let choices = Ember.A(); + let fields = Ember.A(); - f.choices.forEach((c) => { - choices.pushObject(Ember.Object.create(c)); + 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) => { + choices.pushObject(Ember.Object.create(c)); + }); + + field.set('choices', choices); + } + + fields.pushObject(field); }); - - field.set('choices', choices); - - fields.pushObject(field); - }); + } let actions = Ember.A(); - s.actions.forEach((a) => { - actions.pushObject(Ember.Object.create(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(); }; diff --git a/assets/javascripts/discourse/routes/admin-wizard.js.es6 b/assets/javascripts/discourse/routes/admin-wizard.js.es6 index 3af0937f..31e84a7e 100644 --- a/assets/javascripts/discourse/routes/admin-wizard.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizard.js.es6 @@ -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; }, diff --git a/assets/javascripts/discourse/routes/admin-wizards-custom-index.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-custom-index.js.es6 new file mode 100644 index 00000000..7231c01d --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-wizards-custom-index.js.es6 @@ -0,0 +1,5 @@ +export default Discourse.Route.extend({ + redirect() { + this.transitionTo('adminWizard', 'first'); + } +}); diff --git a/assets/javascripts/discourse/routes/admin-wizards-custom.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-custom.js.es6 index 53e813ef..86893505 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-custom.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-custom.js.es6 @@ -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); }; }, diff --git a/assets/javascripts/discourse/routes/admin-wizards-index.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-index.js.es6 new file mode 100644 index 00000000..149e51be --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-wizards-index.js.es6 @@ -0,0 +1,5 @@ +export default Discourse.Route.extend({ + redirect() { + this.transitionTo('adminWizardsCustom'); + } +}); diff --git a/assets/javascripts/discourse/routes/admin-wizards.js.es6 b/assets/javascripts/discourse/routes/admin-wizards.js.es6 deleted file mode 100644 index f6eac996..00000000 --- a/assets/javascripts/discourse/routes/admin-wizards.js.es6 +++ /dev/null @@ -1 +0,0 @@ -export default Discourse.Route.extend(); diff --git a/assets/javascripts/discourse/templates/admin-wizard.hbs b/assets/javascripts/discourse/templates/admin-wizard.hbs index 93f24627..7515da7e 100644 --- a/assets/javascripts/discourse/templates/admin-wizard.hbs +++ b/assets/javascripts/discourse/templates/admin-wizard.hbs @@ -6,10 +6,10 @@
-

{{i18n 'admin.wizard.background'}}

+

{{i18n 'admin.wizard.id'}}

- {{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}}
@@ -22,6 +22,15 @@ +
+
+

{{i18n 'admin.wizard.background'}}

+
+
+ {{input name="background" value=model.background placeholderKey="admin.wizard.background_placeholder"}} +
+
+

{{i18n 'admin.wizard.save_submissions'}}

@@ -32,6 +41,16 @@
+
+
+

{{i18n 'admin.wizard.multiple_submissions'}}

+
+
+ {{input type='checkbox' checked=model.multiple_submissions}} + {{i18n 'admin.wizard.multiple_submissions_label'}} +
+
+

{{i18n 'admin.wizard.url'}}

@@ -55,8 +74,11 @@
{{#unless newWizard}} - + {{/unless}} - {{savingStatus}} + {{conditional-loading-spinner condition=saving size='small'}} + {{#if error}} + {{d-icon "times"}}{{error}} + {{/if}}
diff --git a/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs b/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs index 8b19f143..00da5748 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs @@ -1,5 +1,5 @@
-
+
    {{#each model as |s|}}
  • diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs index c7b912bb..685a25a4 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs @@ -1,15 +1,15 @@
    -

    {{i18n "admin.wizard.action.label"}}

    +

    {{i18n "admin.wizard.action.id"}}

    - {{input value=action.label}} + {{input value=action.id placeholderKey='admin.wizard.id_placeholder' disabled=existingId}}
    -

    {{i18n "admin.wizard.action.type"}}

    +

    {{i18n "admin.wizard.type"}}

    {{combo-box value=action.type content=types}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs index e2393722..1321b18e 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs @@ -1,9 +1,18 @@
    -

    {{i18n 'admin.wizard.translation'}}

    +

    {{i18n 'admin.wizard.id'}}

    - {{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}} +
    +
    + +
    +
    +

    {{i18n 'admin.wizard.key'}}

    +
    +
    + {{input name="key" value=field.key placeholderKey="admin.wizard.key_placeholder"}}
    @@ -12,7 +21,7 @@

    {{i18n 'admin.wizard.field.label'}}

    - {{input name="label" value=field.label}} + {{input name="label" value=field.label placeholder=(i18n "admin.wizard.custom_text_placeholder")}}
    @@ -21,13 +30,13 @@

    {{i18n 'admin.wizard.field.description'}}

- {{textarea name="description" value=field.description}} + {{textarea name="description" value=field.description placeholder=(i18n "admin.wizard.custom_text_placeholder")}}
-

{{i18n 'admin.wizard.field.type'}}

+

{{i18n 'admin.wizard.type'}}

{{combo-box value=field.type content=types}} @@ -44,17 +53,57 @@
-{{#if isDropdown}} -
-
- {{i18n 'admin.wizard.field.choices_label'}} +{{#if isInput}} +
+
+

{{i18n 'admin.wizard.field.min_length'}}

+
+
+ {{input type="number" name="min_length" value=field.min_length placeholder=(i18n 'admin.wizard.field.min_length_placeholder')}} +
+
+{{/if}} + +{{#if isDropdown}} +
+
+ {{i18n 'admin.wizard.field.choices_label'}} +
+
+
+ {{i18n 'admin.wizard.field.choices_translation'}} +
+
+ {{input name="key" value=field.choices_key placeholderKey="admin.wizard.key_placeholder"}} +
+
+
+
+ {{i18n 'admin.wizard.field.choices_preset.label'}} +
+ {{combo-box value=field.choices_preset content=presetChoices none='admin.wizard.field.choices_preset.none'}} + + {{#each presetFilters as |f|}} + + {{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')}} + + {{d-button action='removeFilter' actionParam=f icon='times'}} + {{/each}} +
{{d-button action='addFilter' label='admin.wizard.add' icon='plus'}}
+
+
+
+ {{i18n 'admin.wizard.field.choices_custom'}} +
+ {{#each dropdownChoices as |c|}} + + {{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')}} + + {{d-button action='removeChoice' actionParam=c icon='times'}} + {{/each}} +
{{d-button action='addChoice' label='admin.wizard.add' icon='plus'}}
- {{#each dropdownChoices as |c|}} - - {{input type='text' value=c.label}} - - {{d-button action='removeChoice' actionParam=c icon='times'}} - {{/each}} - {{d-button action='addChoice' label='admin.wizard.add' icon='plus'}}
{{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs index 55f5a757..69e18738 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs @@ -1,9 +1,18 @@
-

{{i18n 'admin.wizard.translation'}}

+

{{i18n 'admin.wizard.id'}}

- {{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}} +
+
+ +
+
+

{{i18n 'admin.wizard.key'}}

+
+
+ {{input name="key" value=step.key placeholderKey="admin.wizard.key_placeholder"}}
@@ -12,7 +21,7 @@

{{i18n 'admin.wizard.step.title'}}

- {{input name="title" value=step.title placeholderKey="admin.wizard.step.title_placeholder"}} + {{input name="title" value=step.title placeholderKey="admin.wizard.custom_text_placeholder"}}
@@ -30,7 +39,7 @@

{{i18n 'admin.wizard.step.description'}}

- {{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")}}
diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js index e3ddb25a..651d01e4 100644 --- a/assets/javascripts/wizard-custom.js +++ b/assets/javascripts/wizard-custom.js @@ -1,5 +1,4 @@ //= require ./wizard/custom-wizard -//= require_tree ./wizard/components //= require_tree ./wizard/controllers //= require_tree ./wizard/helpers //= require_tree ./wizard/initializers diff --git a/assets/javascripts/wizard/components/wizard-field-composer.js.es6 b/assets/javascripts/wizard/components/wizard-field-composer.js.es6 deleted file mode 100644 index 01533e59..00000000 --- a/assets/javascripts/wizard/components/wizard-field-composer.js.es6 +++ /dev/null @@ -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); - } -}); diff --git a/assets/javascripts/wizard/controllers/custom-step.js.es6 b/assets/javascripts/wizard/controllers/custom-step.js.es6 index 46d2d279..fec11ef1 100644 --- a/assets/javascripts/wizard/controllers/custom-step.js.es6 +++ b/assets/javascripts/wizard/controllers/custom-step.js.es6 @@ -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); } } }); diff --git a/assets/javascripts/wizard/initializers/custom.js.es6 b/assets/javascripts/wizard/initializers/custom.js.es6 index baa7214c..62b0adbf 100644 --- a/assets/javascripts/wizard/initializers/custom.js.es6 +++ b/assets/javascripts/wizard/initializers/custom.js.es6 @@ -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'); diff --git a/assets/javascripts/wizard/models/custom.js.es6 b/assets/javascripts/wizard/models/custom.js.es6 index af992d50..79f8b1c8 100644 --- a/assets/javascripts/wizard/models/custom.js.es6 +++ b/assets/javascripts/wizard/models/custom.js.es6 @@ -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; - wizard.steps = wizard.steps.map(step => { - const stepObj = Step.create(step); - stepObj.fields = stepObj.fields.map(f => WizardField.create(f)); - return stepObj; - }); + + 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); }); diff --git a/assets/javascripts/wizard/routes/custom-index.js.es6 b/assets/javascripts/wizard/routes/custom-index.js.es6 index 721b9bb1..08aa8eb7 100644 --- a/assets/javascripts/wizard/routes/custom-index.js.es6 +++ b/assets/javascripts/wizard/routes/custom-index.js.es6 @@ -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'); - this.replaceWith('custom.step', appModel.start); + 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); } }); diff --git a/assets/javascripts/wizard/routes/custom-step.js.es6 b/assets/javascripts/wizard/routes/custom-step.js.es6 index dcb6a890..b72f8184 100644 --- a/assets/javascripts/wizard/routes/custom-step.js.es6 +++ b/assets/javascripts/wizard/routes/custom-step.js.es6 @@ -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') + }); + } +}); diff --git a/assets/javascripts/wizard/routes/custom.js.es6 b/assets/javascripts/wizard/routes/custom.js.es6 new file mode 100644 index 00000000..1f627fb3 --- /dev/null +++ b/assets/javascripts/wizard/routes/custom.js.es6 @@ -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)); + } + } +}); diff --git a/assets/javascripts/wizard/templates/application.hbs b/assets/javascripts/wizard/templates/application.hbs index 6b253ede..c24cd689 100644 --- a/assets/javascripts/wizard/templates/application.hbs +++ b/assets/javascripts/wizard/templates/application.hbs @@ -1,16 +1 @@ -{{#if showCanvas}} - {{wizard-canvas}} -{{/if}} - -
-
- {{outlet}} -
- -
+{{outlet}} diff --git a/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs b/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs deleted file mode 100644 index df0ad909..00000000 --- a/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs +++ /dev/null @@ -1 +0,0 @@ -{{textarea elementId=field.id value=field.value placeholder=field.placeholder tabindex="9"}} diff --git a/assets/javascripts/wizard/templates/custom.hbs b/assets/javascripts/wizard/templates/custom.hbs new file mode 100644 index 00000000..509795f1 --- /dev/null +++ b/assets/javascripts/wizard/templates/custom.hbs @@ -0,0 +1,16 @@ +{{#if showCanvas}} + {{wizard-canvas}} +{{/if}} + +
+
+ {{outlet}} +
+ +
diff --git a/assets/javascripts/wizard/templates/custom.index.hbs b/assets/javascripts/wizard/templates/custom.index.hbs new file mode 100644 index 00000000..cfe754ff --- /dev/null +++ b/assets/javascripts/wizard/templates/custom.index.hbs @@ -0,0 +1,3 @@ +{{#if completed}} + {{i18n 'wizard.completed'}} +{{/if}} diff --git a/assets/javascripts/wizard/templates/custom.step.hbs b/assets/javascripts/wizard/templates/custom.step.hbs index 2e00198a..50c73752 100644 --- a/assets/javascripts/wizard/templates/custom.step.hbs +++ b/assets/javascripts/wizard/templates/custom.step.hbs @@ -1 +1,9 @@ -{{wizard-step step=step wizard=wizard goNext="goNext" goBack="goBack"}} +
+ {{stepMessage.text}} +
+{{wizard-step step=step + wizard=wizard + goNext="goNext" + goBack="goBack" + finished="finished" + showMessage="showMessage"}} diff --git a/assets/stylesheets/wizard/wizard_custom.scss b/assets/stylesheets/wizard/wizard_custom.scss new file mode 100644 index 00000000..cc182052 --- /dev/null +++ b/assets/stylesheets/wizard/wizard_custom.scss @@ -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; + } +} diff --git a/assets/stylesheets/custom_wizard.scss b/assets/stylesheets/wizard_custom_admin.scss similarity index 54% rename from assets/stylesheets/custom_wizard.scss rename to assets/stylesheets/wizard_custom_admin.scss index b56dd7ad..3b087e8a 100644 --- a/assets/stylesheets/custom_wizard.scss +++ b/assets/stylesheets/wizard_custom_admin.scss @@ -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; } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 1798fe1c..7d047b02 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 39be4121..64ec3e73 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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" diff --git a/lib/builder.rb b/lib/builder.rb index 0fbb11e0..968de8f4 100644 --- a/lib/builder.rb +++ b/lib/builder.rb @@ -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,99 +28,152 @@ class CustomWizard::Builder end def build - @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'] + 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.key = s['key'] if s['key'] - s['fields'].each do |f| - params = { - id: f['id'], - type: f['type'], - required: f['required'] - } + if s['fields'] && s['fields'].length + s['fields'].each do |f| + params = { + id: f['id'], + type: f['type'], + required: f['required'] + } - 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[:label] = f['label'] if f['label'] + params[:description] = f['description'] if f['description'] + params[:key] = f['key'] if f['key'] - field = step.add_field(params) - - if f['type'] == 'dropdown' - f['choices'].each do |c| - field.add_choice(c['id'], label: c['label']) - 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) - end - - submission['user_id'] = @wizard.user.id - submission['completed'] = updater.step.next.nil? - - input.each do |key, value| - submission[key] = value - end - - submissions.push(submission) - - PluginStore.set('custom_wizard_submissions', store_key, submissions) - end - - if s['actions'] && s['actions'].length - s['actions'].each do |a| - if a['type'] === 'create_topic' - creator = PostCreator.new(user, - title: input[a['title']], - raw: input[a['post']], - category: a['category_id'], - skip_validations: true) - - post = creator.create - if creator.errors.present? - raise StandardError, creator.errors.full_messages.join(" ") - end - - updater.result = { topic_id: post.topic.id } + 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 - if a['type'] === 'send_message' - creator = PostCreator.new(user, - title: input[a['title']], - raw: input[a['post']], - archetype: Archetype.private_message, - target_usernames: a['username']) + field = step.add_field(params) - post = creator.create + if f['type'] === 'dropdown' + if f['choices'] && f['choices'].length > 0 + f['choices'].each do |c| + 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 creator.errors.present? - raise StandardError, creator.errors.full_messages.join(" ") + 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 - - updater.result = { topic_id: post.topic.id } end end end - CustomWizard::Builder.step_handlers.each do |handler| - if handler[:wizard_id] == @wizard.id - handler[:block].call(self) + step.on_update do |updater| + @updater = updater + input = updater.fields + user = @wizard.user + + 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 + + next if updater.errors.any? + + CustomWizard::Builder.step_handlers.each do |handler| + if handler[:wizard_id] == @wizard.id + handler[:block].call(self) + end + end + + next if updater.errors.any? + + if s['actions'] && s['actions'].length + s['actions'].each do |a| + if a['type'] === 'create_topic' + creator = PostCreator.new(user, + title: input[a['title']], + raw: input[a['post']], + category: a['category_id'], + skip_validations: true) + + post = creator.create + if creator.errors.present? + 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, + title: input[a['title']], + raw: input[a['post']], + archetype: Archetype.private_message, + target_usernames: a['username']) + + post = creator.create + + if creator.errors.present? + updater.errors.add(:send_message, creator.errors.full_messages.join(" ")) + else + updater.result = { topic_id: post.topic.id } + end + end + end + end + + 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 diff --git a/lib/field.rb b/lib/field.rb index 4d9bdbbe..1840876e 100644 --- a/lib/field.rb +++ b/lib/field.rb @@ -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 diff --git a/lib/wizard.rb b/lib/wizard.rb index c5339231..e593b923 100644 --- a/lib/wizard.rb +++ b/lib/wizard.rb @@ -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 diff --git a/plugin.rb b/plugin.rb index 6806b10e..76f5a4f2 100644 --- a/plugin.rb +++ b/plugin.rb @@ -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 diff --git a/public/desktop.css b/public/desktop.css deleted file mode 100644 index 1a3469d8..00000000 --- a/public/desktop.css +++ /dev/null @@ -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; -}