From 9f12b571ef794b272bed68b0603636f565bdd2a4 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 31 Aug 2016 13:35:49 -0400 Subject: [PATCH] Wizard: Server Side Validation + Finished Step --- .../components/combo-box.js.es6 | 5 ++- .../helpers/bound-i18n.js.es6 | 3 ++ .../components/category-chooser.js.es6 | 2 +- .../topic-footer-mobile-dropdown.js.es6 | 2 +- app/assets/javascripts/ember_jquery.js | 1 + app/assets/javascripts/main_include.js | 1 - app/assets/javascripts/vendor.js | 2 - app/assets/javascripts/wizard-vendor.js | 2 +- .../wizard/components/wizard-field.js.es6 | 5 ++- .../wizard/components/wizard-step.js.es6 | 3 ++ .../wizard/mixins/valid-state.js.es6 | 9 ++++- .../javascripts/wizard/routes/step.js.es6 | 3 +- .../components/wizard-field-dropdown.hbs | 1 + .../components/wizard-field-text.hbs | 1 + .../templates/components/wizard-field.hbs | 10 ++++- .../templates/components/wizard-step.hbs | 37 +++++++++++------- .../wizard/test/acceptance/wizard-test.js.es6 | 5 +++ .../wizard/test/wizard-pretender.js.es6 | 5 ++- .../stylesheets/common/base/combobox.scss | 5 +++ app/assets/stylesheets/vendor/select2.scss | 5 --- app/assets/stylesheets/wizard.scss | 38 ++++++++++++++++++- app/controllers/steps_controller.rb | 11 +++++- app/serializers/wizard_field_serializer.rb | 21 +++++++++- app/views/wizard/index.html.erb | 4 +- app/views/wizard/qunit.html.erb | 1 + config/locales/client.en.yml | 1 + config/locales/server.en.yml | 23 +++++++++++ lib/wizard.rb | 14 ++++++- lib/wizard/field.rb | 9 ++++- lib/wizard/step_updater.rb | 22 +++++------ spec/components/step_updater_spec.rb | 28 +++++++++++++- spec/components/wizard_spec.rb | 1 - spec/components/wizard_step_spec.rb | 24 ++++++++++++ .../extra_locales_controller_spec.rb | 4 ++ spec/controllers/steps_controller_spec.rb | 14 +++---- 35 files changed, 260 insertions(+), 62 deletions(-) rename app/assets/javascripts/{discourse => discourse-common}/components/combo-box.js.es6 (94%) create mode 100644 app/assets/javascripts/discourse-common/helpers/bound-i18n.js.es6 create mode 100644 app/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs create mode 100644 app/assets/javascripts/wizard/templates/components/wizard-field-text.hbs create mode 100644 spec/components/wizard_step_spec.rb diff --git a/app/assets/javascripts/discourse/components/combo-box.js.es6 b/app/assets/javascripts/discourse-common/components/combo-box.js.es6 similarity index 94% rename from app/assets/javascripts/discourse/components/combo-box.js.es6 rename to app/assets/javascripts/discourse-common/components/combo-box.js.es6 index 5caf1adea9a..ce222e21145 100644 --- a/app/assets/javascripts/discourse/components/combo-box.js.es6 +++ b/app/assets/javascripts/discourse-common/components/combo-box.js.es6 @@ -64,10 +64,11 @@ export default Ember.Component.extend({ } const $elem = this.$(); - const minimumResultsForSearch = this.capabilities.isIOS ? -1 : 5; + const caps = this.capabilities; + const minimumResultsForSearch = (caps && caps.isIOS) ? -1 : 5; $elem.select2({ formatResult: this.comboTemplate, minimumResultsForSearch, - width: 'resolve', + width: this.get('width') || 'resolve', allowClear: true }); diff --git a/app/assets/javascripts/discourse-common/helpers/bound-i18n.js.es6 b/app/assets/javascripts/discourse-common/helpers/bound-i18n.js.es6 new file mode 100644 index 00000000000..d507efd5ec8 --- /dev/null +++ b/app/assets/javascripts/discourse-common/helpers/bound-i18n.js.es6 @@ -0,0 +1,3 @@ +import { htmlHelper } from 'discourse-common/lib/helpers'; + +export default htmlHelper((key, params) => I18n.t(key, params.hash)); diff --git a/app/assets/javascripts/discourse/components/category-chooser.js.es6 b/app/assets/javascripts/discourse/components/category-chooser.js.es6 index ddc8aad2b0b..42fe4412ed3 100644 --- a/app/assets/javascripts/discourse/components/category-chooser.js.es6 +++ b/app/assets/javascripts/discourse/components/category-chooser.js.es6 @@ -1,4 +1,4 @@ -import ComboboxView from 'discourse/components/combo-box'; +import ComboboxView from 'discourse-common/components/combo-box'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import computed from 'ember-addons/ember-computed-decorators'; import { observes, on } from 'ember-addons/ember-computed-decorators'; diff --git a/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 b/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 index a9ea76d56c0..413085aef80 100644 --- a/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 @@ -1,5 +1,5 @@ import { iconHTML } from 'discourse-common/helpers/fa-icon'; -import Combobox from 'discourse/components/combo-box'; +import Combobox from 'discourse-common/components/combo-box'; import { on, observes } from 'ember-addons/ember-computed-decorators'; export default Combobox.extend({ diff --git a/app/assets/javascripts/ember_jquery.js b/app/assets/javascripts/ember_jquery.js index 55986994d89..23e6b286b18 100644 --- a/app/assets/javascripts/ember_jquery.js +++ b/app/assets/javascripts/ember_jquery.js @@ -1,3 +1,4 @@ +//= require env //= require jquery_include //= require ember_include //= require loader diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 81be2e938f6..91a0044d679 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -60,7 +60,6 @@ //= require ./discourse/views/container //= require ./discourse/views/modal-body //= require ./discourse/views/flag -//= require ./discourse/components/combo-box //= require ./discourse/components/edit-category-panel //= require ./discourse/views/button //= require ./discourse/components/dropdown-button diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index 22624ed0089..9b2cd2a5282 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -1,5 +1,4 @@ //= require logster -//= require ./env //= require ./discourse-objects //= require probes.js @@ -38,4 +37,3 @@ //= require virtual-dom //= require virtual-dom-amd //= require highlight.js -//= require_tree ./discourse/ember diff --git a/app/assets/javascripts/wizard-vendor.js b/app/assets/javascripts/wizard-vendor.js index cb06474666c..d25fb76a086 100644 --- a/app/assets/javascripts/wizard-vendor.js +++ b/app/assets/javascripts/wizard-vendor.js @@ -1,2 +1,2 @@ -//= require env //= require template_include.js +//= require select2.js diff --git a/app/assets/javascripts/wizard/components/wizard-field.js.es6 b/app/assets/javascripts/wizard/components/wizard-field.js.es6 index 4d2500322d1..cb4c67de9d0 100644 --- a/app/assets/javascripts/wizard/components/wizard-field.js.es6 +++ b/app/assets/javascripts/wizard/components/wizard-field.js.es6 @@ -4,5 +4,8 @@ export default Ember.Component.extend({ classNameBindings: [':wizard-field', ':text-field', 'field.invalid'], @computed('field.id') - inputClassName: id => `field-${Ember.String.dasherize(id)}` + inputClassName: id => `field-${Ember.String.dasherize(id)}`, + + @computed('field.type') + inputComponentName: type => `wizard-field-${type}` }); diff --git a/app/assets/javascripts/wizard/components/wizard-step.js.es6 b/app/assets/javascripts/wizard/components/wizard-step.js.es6 index a39d50e3b24..026c4b67168 100644 --- a/app/assets/javascripts/wizard/components/wizard-step.js.es6 +++ b/app/assets/javascripts/wizard/components/wizard-step.js.es6 @@ -12,6 +12,9 @@ export default Ember.Component.extend({ @computed('step.displayIndex', 'wizard.totalSteps') showNextButton: (current, total) => current < total, + @computed('step.displayIndex', 'wizard.totalSteps') + showDoneButton: (current, total) => current === total, + @computed('step.index') showBackButton: index => index > 0, diff --git a/app/assets/javascripts/wizard/mixins/valid-state.js.es6 b/app/assets/javascripts/wizard/mixins/valid-state.js.es6 index e26d12f0488..afa027b2527 100644 --- a/app/assets/javascripts/wizard/mixins/valid-state.js.es6 +++ b/app/assets/javascripts/wizard/mixins/valid-state.js.es6 @@ -8,6 +8,7 @@ export const States = { export default { _validState: null, + errorDescription: null, init() { this._super(); @@ -23,8 +24,14 @@ export default { @computed('_validState') unchecked: state => state === States.UNCHECKED, - setValid(valid) { + setValid(valid, description) { this.set('_validState', valid ? States.VALID : States.INVALID); + + if (!valid && description && description.length) { + this.set('errorDescription', description); + } else { + this.set('errorDescription', null); + } } }; diff --git a/app/assets/javascripts/wizard/routes/step.js.es6 b/app/assets/javascripts/wizard/routes/step.js.es6 index 35dc8df7292..3487d61612a 100644 --- a/app/assets/javascripts/wizard/routes/step.js.es6 +++ b/app/assets/javascripts/wizard/routes/step.js.es6 @@ -1,7 +1,8 @@ export default Ember.Route.extend({ model(params) { const allSteps = this.modelFor('application').steps; - return allSteps.findProperty('id', params.step_id); + const step = allSteps.findProperty('id', params.step_id); + return step ? step : allSteps[0]; }, setupController(controller, step) { diff --git a/app/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs b/app/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs new file mode 100644 index 00000000000..8339c52697f --- /dev/null +++ b/app/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs @@ -0,0 +1 @@ +{{combo-box value=field.value content=field.options nameProperty="label" width="400px"}} diff --git a/app/assets/javascripts/wizard/templates/components/wizard-field-text.hbs b/app/assets/javascripts/wizard/templates/components/wizard-field-text.hbs new file mode 100644 index 00000000000..2413f146dc3 --- /dev/null +++ b/app/assets/javascripts/wizard/templates/components/wizard-field-text.hbs @@ -0,0 +1 @@ +{{input value=field.value class=inputClassName placeholder=field.placeholder}} diff --git a/app/assets/javascripts/wizard/templates/components/wizard-field.hbs b/app/assets/javascripts/wizard/templates/components/wizard-field.hbs index b7b1c1a01af..d757791f83d 100644 --- a/app/assets/javascripts/wizard/templates/components/wizard-field.hbs +++ b/app/assets/javascripts/wizard/templates/components/wizard-field.hbs @@ -2,6 +2,14 @@ {{field.label}}
- {{input value=field.value class=inputClassName placeholder=field.placeholder}} + {{component inputComponentName field=field inputClassName=inputClassName}}
+ + {{#if field.errorDescription}} +
{{field.errorDescription}}
+ {{/if}} + + {{#if field.description}} +
{{field.description}}
+ {{/if}} diff --git a/app/assets/javascripts/wizard/templates/components/wizard-step.hbs b/app/assets/javascripts/wizard/templates/components/wizard-step.hbs index e174779bbd5..ff6a901208b 100644 --- a/app/assets/javascripts/wizard/templates/components/wizard-step.hbs +++ b/app/assets/javascripts/wizard/templates/components/wizard-step.hbs @@ -3,7 +3,7 @@ {{/if}} {{#if step.description}} -

{{step.description}}

+

{{{step.description}}}

{{/if}} {{#wizard-step-form step=step}} @@ -14,24 +14,33 @@ diff --git a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 index c3a9d16f32d..345f613563c 100644 --- a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 +++ b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 @@ -18,7 +18,9 @@ test("Forum Name Step", assert => { assert.ok(exists('.wizard-step-title')); assert.ok(exists('.wizard-step-description')); assert.ok(!exists('.invalid .field-full-name'), "don't show it as invalid until the user does something"); + assert.ok(exists('.wizard-field .field-description')); assert.ok(!exists('.wizard-btn.back')); + assert.ok(!exists('.wizard-field .field-error-description')); }); // invalid data @@ -32,16 +34,19 @@ test("Forum Name Step", assert => { click('.wizard-btn.next'); andThen(() => { assert.ok(exists('.invalid .field-full-name')); + assert.ok(exists('.wizard-field .field-error-description')); }); // server validation ok fillIn('input.field-full-name', "Evil Trout"); click('.wizard-btn.next'); andThen(() => { + assert.ok(!exists('.wizard-field .field-error-description')); assert.ok(!exists('.wizard-step-title')); assert.ok(!exists('.wizard-step-description')); assert.ok(exists('input.field-email'), "went to the next step"); assert.ok(!exists('.wizard-btn.next')); + assert.ok(exists('.wizard-btn.done'), 'last step shows a done button'); assert.ok(exists('.wizard-btn.back'), 'shows the back button'); }); diff --git a/app/assets/javascripts/wizard/test/wizard-pretender.js.es6 b/app/assets/javascripts/wizard/test/wizard-pretender.js.es6 index 786c52b7ad7..6e0808fc674 100644 --- a/app/assets/javascripts/wizard/test/wizard-pretender.js.es6 +++ b/app/assets/javascripts/wizard/test/wizard-pretender.js.es6 @@ -40,7 +40,10 @@ export default function() { title: 'hello there', index: 0, description: 'hello!', - fields: [{ id: 'full_name', type: 'text', required: true }], + fields: [{ id: 'full_name', + type: 'text', + required: true, + description: "Your name" }], next: 'second-step' }, { diff --git a/app/assets/stylesheets/common/base/combobox.scss b/app/assets/stylesheets/common/base/combobox.scss index 4e1b81e48d6..f0def437254 100644 --- a/app/assets/stylesheets/common/base/combobox.scss +++ b/app/assets/stylesheets/common/base/combobox.scss @@ -1,3 +1,8 @@ +.select2-results .select2-highlighted { + background: dark-light-diff($highlight, $secondary, 50%, -80%); + color: $primary; +} + .category-combobox, .select2-drop { .badge-category { diff --git a/app/assets/stylesheets/vendor/select2.scss b/app/assets/stylesheets/vendor/select2.scss index 87080ec2f8f..5bd68377de9 100644 --- a/app/assets/stylesheets/vendor/select2.scss +++ b/app/assets/stylesheets/vendor/select2.scss @@ -332,11 +332,6 @@ Version: @@ver@@ Timestamp: @@timestamp@@ .select2-results-dept-6 .select2-result-label { padding-left: 110px } .select2-results-dept-7 .select2-result-label { padding-left: 120px } -.select2-results .select2-highlighted { - background: dark-light-diff($highlight, $secondary, 50%, -80%); - color: $primary; -} - .select2-results li em { background: #feffde; font-style: normal; diff --git a/app/assets/stylesheets/wizard.scss b/app/assets/stylesheets/wizard.scss index 23b30a79b2e..dfffa573b76 100644 --- a/app/assets/stylesheets/wizard.scss +++ b/app/assets/stylesheets/wizard.scss @@ -1,7 +1,8 @@ @import "vendor/normalize"; @import "vendor/font_awesome/font-awesome"; +@import "vendor/select2"; -body { +body.wizard { background-color: #fff; background-image: url('/images/wizard/bubbles.png'); background-repeat: repeat; @@ -12,6 +13,10 @@ body { line-height: 1.4em; } +.select { + width: 400px; +} + .wizard-column { background-color: white; box-shadow: 0 5px 10px rgba(0,0,0,0.2); @@ -77,10 +82,10 @@ body { background-color: #6699ff; color: white; border: 0px; - float: right; padding: 0.5em; outline: 0; transition: background-color .3s; + margin-right: 0.5em; &:hover { background-color: #80B3FF; @@ -131,6 +136,25 @@ body { } } + button.wizard-btn:last-child { + margin-right: 0; + } + + button.wizard-btn.done { + background-color: #33B333; + + &:hover { + background-color: #4DCD4D; + } + + &:active { + background-color: #66E666; + } + + &:disabled { + background-color: #006700; + } + } } .wizard-field { @@ -142,6 +166,16 @@ body { margin-top: 0.5em; } + .field-error-description { + color: red; + font-weight: bold; + } + + .field-description { + color: #999; + margin-top: 0.5em; + } + &.text-field { input { width: 100%; diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index 5dbfa6844e4..f2c62bb52d0 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -9,7 +9,16 @@ class StepsController < ApplicationController def update updater = Wizard::StepUpdater.new(current_user, params[:id]) updater.update(params[:fields]) - render nothing: true + + if updater.success? + render json: success_json + else + errors = [] + updater.errors.messages.each do |field, msg| + errors << {field: field, description: msg.join } + end + render json: { errors: errors }, status: 422 + end end end diff --git a/app/serializers/wizard_field_serializer.rb b/app/serializers/wizard_field_serializer.rb index 838226002bd..07babfed04d 100644 --- a/app/serializers/wizard_field_serializer.rb +++ b/app/serializers/wizard_field_serializer.rb @@ -1,6 +1,6 @@ class WizardFieldSerializer < ApplicationSerializer - attributes :id, :type, :required, :value, :label, :placeholder + attributes :id, :type, :required, :value, :label, :placeholder, :description, :options def id object.id @@ -41,4 +41,23 @@ class WizardFieldSerializer < ApplicationSerializer def include_placeholder? placeholder.present? end + + def description + I18n.t("#{i18n_key}.description", default: '') + end + + def include_description? + description.present? + end + + def options + object.options.map do |o| + {id: o, label: I18n.t("#{i18n_key}.options.#{o}")} + end + end + + def include_options? + object.options.present? + end + end diff --git a/app/views/wizard/index.html.erb b/app/views/wizard/index.html.erb index 1021f6aa148..153026a2fac 100644 --- a/app/views/wizard/index.html.erb +++ b/app/views/wizard/index.html.erb @@ -1,8 +1,8 @@ <%= stylesheet_link_tag 'wizard' %> - <%= script 'wizard-vendor' %> <%= script 'ember_jquery' %> + <%= script 'wizard-vendor' %> <%= script 'wizard-application' %> <%= script "locales/#{I18n.locale}" %> <%= render partial: "common/special_font_face" %> @@ -12,7 +12,7 @@ <%= t 'wizard.title' %> - +