diff --git a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 index 5d3f3cec..181c37ca 100644 --- a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 @@ -1,66 +1,43 @@ -import { default as discourseComputed, observes, on } from 'discourse-common/utils/decorators'; -import { equal, not, empty, or } from "@ember/object/computed"; -import { actionTypes, generateName, selectKitContent, profileFields } from '../lib/wizard'; +import { default as discourseComputed, observes } from 'discourse-common/utils/decorators'; +import { equal, empty, or } from "@ember/object/computed"; +import { actionTypes, generateName, selectKitContent } from '../lib/wizard'; import Component from "@ember/component"; export default Component.extend({ classNames: 'wizard-custom-action', - types: actionTypes.map(t => ({ id: t, name: generateName(t) })), + actionTypes: actionTypes.map(t => ({ id: t, name: generateName(t) })), createTopic: equal('action.type', 'create_topic'), updateProfile: equal('action.type', 'update_profile'), sendMessage: equal('action.type', 'send_message'), + openComposer: equal('action.type', 'open_composer'), sendToApi: equal('action.type', 'send_to_api'), - apiEmpty: empty('action.api'), addToGroup: equal('action.type', 'add_to_group'), routeTo: equal('action.type', 'route_to'), - disableId: not('action.isNew'), + apiEmpty: empty('action.api'), groupPropertyTypes: selectKitContent(['id', 'name']), hasAdvanced: or('hasCustomFields', 'routeTo'), hasCustomFields: or('basicTopicFields', 'updateProfile'), + basicTopicFields: or('createTopic', 'sendMessage', 'openComposer'), + publicTopicFields: or('createTopic', 'openComposer'), + showSkipRedirect: or('createTopic', 'sendMessage'), - @on('didInsertElement') @observes('action.type') - setLabel() { + setupDefaults() { if (this.action.type) { this.set('action.label', generateName(this.action.type)); }; }, - - @discourseComputed('action.type') - basicTopicFields(actionType) { - return ['create_topic', 'send_message', 'open_composer'].indexOf(actionType) > -1; - }, - - @discourseComputed('action.type') - publicTopicFields(actionType) { - return ['create_topic', 'open_composer'].indexOf(actionType) > -1; - }, - - @discourseComputed('action.type') - newTopicFields(actionType) { - return ['create_topic', 'send_message'].indexOf(actionType) > -1; - }, - @discourseComputed('wizardFields') - categoryFields(fields) { - return fields.filter(f => f.type == 'category'); - }, - - @discourseComputed('wizardFields') - tagFields(fields) { - return fields.filter(f => f.type == 'tag'); - }, - - @observes('action.custom_category_wizard_field') - toggleCustomCategoryUserField() { - if (this.action.custom_category_wizard_field) - this.set('action.custom_category_user_field', false); - }, - - @observes('action.custom_category_user_field') - toggleCustomCategoryWizardField() { - if (this.action.custom_category_user_field) - this.set('action.custom_category_wizard_field', false); + @discourseComputed('wizard.steps') + runAfterContent(steps) { + let content = steps.map(s => ({ id: s.id, name: s.label })); + + content.unshift({ + id: 'wizard_completion', + name: I18n.t('admin.wizard.action.run_after.wizard_completion') + }); + + return content; }, @discourseComputed('wizard.apis') diff --git a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 index 60755bd4..3f8b4396 100644 --- a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 @@ -1,5 +1,5 @@ -import { default as discourseComputed, observes, on } from 'discourse-common/utils/decorators'; -import { equal, not, or } from "@ember/object/computed"; +import { default as discourseComputed, observes } from 'discourse-common/utils/decorators'; +import { equal, or } from "@ember/object/computed"; import { selectKitContent } from '../lib/wizard'; import Component from "@ember/component"; @@ -10,23 +10,30 @@ export default Component.extend({ isCategory: equal('field.type', 'category'), isGroup: equal('field.type', 'group'), isTag: equal('field.type', 'tag'), - disableId: not('field.isNew'), + isText: equal('field.type', 'text'), + isTextarea: equal('field.type', 'textarea'), + isUrl: equal('field.type', 'url'), + showPrefill: or('isCategory', 'isTag', 'isGroup', 'isDropdown'), + showContent: or('isCategory', 'isTag', 'isGroup', 'isDropdown'), + showLimit: or('isCategory', 'isTag'), + showMinLength: or('isText', 'isTextarea', 'isUrl'), categoryPropertyTypes: selectKitContent(['id', 'slug']), - prefillEnabled: or('isCategory', 'isTag', 'isGroup', 'isDropdown'), - contentEnabled: or('isCategory', 'isTag', 'isGroup', 'isDropdown'), - - @discourseComputed('field.type') - isInput: (type) => type === 'text' || type === 'textarea' || type === 'url', - - @discourseComputed('field.type') - isCategoryOrTag: (type) => type === 'tag' || type === 'category', - - @on('didInsertElement') - @observes('isUpload') - setupFileType() { + + @observes('isUpload', 'isCategory') + setupDefaults() { if (this.isUpload && !this.field.file_types) { this.set('field.file_types', '.jpg,.png'); } + + if (this.isCategory && !this.field.property) { + this.set('field.property', 'id'); + } + }, + + @observes('field.type') + clearMappedProperties() { + this.set('field.content', null); + this.set('field.prefill', null); }, setupTypeOutput(fieldType, options) { @@ -80,19 +87,6 @@ export default Component.extend({ return this.setupTypeOutput(fieldType, options); }, - @observes('field.type') - clearInputs() { - this.set('field.content', null); - this.set('field.prefill', null); - }, - - @observes('isCategory') - setupCategoryType() { - if (this.isCategory && !this.field.property) { - this.set('field.property', 'id'); - } - }, - actions: { imageUploadDone(upload) { this.set("field.image", upload.url); diff --git a/assets/javascripts/discourse/components/wizard-custom-step.js.es6 b/assets/javascripts/discourse/components/wizard-custom-step.js.es6 index 6b4dc4a9..ff3d4ee6 100644 --- a/assets/javascripts/discourse/components/wizard-custom-step.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-step.js.es6 @@ -1,34 +1,7 @@ -import { observes, on, default as discourseComputed } from 'discourse-common/utils/decorators'; -import { not } from "@ember/object/computed"; -import EmberObject from "@ember/object"; import Component from "@ember/component"; export default Component.extend({ classNames: 'wizard-custom-step', - disableId: not('step.isNew'), - - @discourseComputed('wizardFields', 'wizard.steps') - requiredContent(wizardFields, steps) { - let content = wizardFields; - let actions = []; - - steps.forEach(s => { - actions.push(...s.actions); - }); - - actions.forEach(a => { - if (a.type === 'route_to' && a.code) { - content.push( - EmberObject.create({ - id: a.code, - label: "code (Route To)" - }) - ); - } - }); - - return content; - }, actions: { bannerUploadDone(upload) { diff --git a/assets/javascripts/discourse/controllers/admin-wizard.js.es6 b/assets/javascripts/discourse/controllers/admin-wizard.js.es6 index e07ff8a1..2fde2bf8 100644 --- a/assets/javascripts/discourse/controllers/admin-wizard.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizard.js.es6 @@ -99,7 +99,7 @@ export default Controller.extend({ } }).catch((result) => { this.set('saving', false); - this.set('error', I18n.t(`admin.wizard.error.${result.error}`)); + this.set('error', I18n.t(`admin.wizard.error.${result.error}`, result.errorParams || {})); later(() => this.set('error', null), 10000); }); }, diff --git a/assets/javascripts/discourse/lib/wizard-json.js.es6 b/assets/javascripts/discourse/lib/wizard-json.js.es6 index d7aa8a09..1b9c2f80 100644 --- a/assets/javascripts/discourse/lib/wizard-json.js.es6 +++ b/assets/javascripts/discourse/lib/wizard-json.js.es6 @@ -19,132 +19,6 @@ function mapped(property, type) { mappedProperties[type].indexOf(property) > -1; } -function buildJson(object, type) { - let result = {}; - - properties[type].forEach((p) => { - let value = object.get(p); - - if (mapped(p, type)) { - value = buildMappedJson(value); - } - - if (value) { - result[p] = value; - } - }); - - return result; -} - -function buildMappedJson(inputs) { - if (!inputs || !inputs.length) return false; - - let result = []; - - inputs.forEach(inpt => { - let input = { - type: inpt.type, - }; - - if (present(inpt.output)) { - input.output = inpt.output; - input.output_type = snakeCase(inpt.output_type); - input.output_connector = inpt.output_connector; - } - - if (present(inpt.pairs)) { - input.pairs = []; - - inpt.pairs.forEach(pr => { - if (present(pr.key) && present(pr.value)) { - - let pairParams = { - index: pr.index, - key: pr.key, - key_type: snakeCase(pr.key_type), - value: pr.value, - value_type: snakeCase(pr.value_type), - connector: pr.connector - } - - input.pairs.push(pairParams); - } - }); - } - - if ((input.type === 'assignment' && present(input.output)) || - present(input.pairs)) { - - result.push(input); - } - }); - - if (!result.length) { - result = false; - } - - return result; -} - -function buildStepJson(object) { - let steps = []; - let error = null; - - object.some((s) => { - let step = buildJson(s, 'step'); - let fields = s.fields; - - if (fields.length) { - step.fields = []; - - fields.some((f) => { - if (!f.type) { - error = 'type_required'; - return; - } - - step.fields.push( - buildJson(f, 'field') - ); - }); - - if (error) return; - } - - let actions = s.actions; - - if (actions.length) { - step.actions = []; - - actions.some((a) => { - if (a.api_body) { - try { - JSON.parse(a.api_body); - } catch (e) { - error = 'invalid_api_body'; - return; - } - } - - step.actions.push( - buildJson(a, 'action') - ); - }); - - if (error) return; - } - - steps.push(step); - }); - - if (error) { - return { error }; - } else { - return { steps }; - }; -} - function castCase(property, value) { return property.indexOf('_type') > -1 ? camelCase(value) : value; } @@ -223,9 +97,9 @@ function objectHasAdvanced(params, type) { } function buildProperties(json) { - let steps = A(); let props = { - steps + steps: A(); + action: A(); }; if (present(json)) { @@ -268,25 +142,23 @@ function buildProperties(json) { }); } - stepParams.actions = A(); - - if (present(stepJson.actions)) { - stepJson.actions.forEach((a) => { - let params = buildObject(a, 'action'); - - if (objectHasAdvanced(params, 'action')) { - params.showAdvanced = true; - } - - stepParams.actions.pushObject(params); - }); - } - steps.pushObject( EmberObject.create(stepParams) ); }); }; + + if (present(json.actions)) { + json.actions.forEach((a) => { + let params = buildObject(a, 'action'); + + if (objectHasAdvanced(params, 'action')) { + params.showAdvanced = true; + } + + props.actions.pushObject(params); + }); + } } else { props.id = ''; props.name = ''; @@ -299,7 +171,6 @@ function buildProperties(json) { props.prompt_completion = false; props.restart_on_revisit = false; props.permitted = null; - props.steps = A(); } return props; diff --git a/assets/javascripts/discourse/lib/wizard.js.es6 b/assets/javascripts/discourse/lib/wizard.js.es6 index d488c56b..2b8a1ee8 100644 --- a/assets/javascripts/discourse/lib/wizard.js.es6 +++ b/assets/javascripts/discourse/lib/wizard.js.es6 @@ -58,7 +58,9 @@ const wizardProperties = [ 'prompt_completion', 'restart_on_revisit', 'theme_id', - 'permitted' + 'permitted', + 'steps', + 'actions' ]; const stepProperties = [ @@ -69,7 +71,8 @@ const stepProperties = [ 'raw_description', 'required_data', 'required_data_message', - 'permitted_params' + 'permitted_params', + 'fields' ] const fieldProperties = [ @@ -91,6 +94,7 @@ const fieldProperties = [ const actionProperties = [ 'id', 'type', + 'run_after', 'title', 'post', 'post_builder', @@ -117,6 +121,12 @@ const properties = { action: actionProperties } +const objectArrays = [ + 'steps', + 'fields', + 'actions' +]; + const mappedProperties = { wizard: [ 'permitted' @@ -202,6 +212,7 @@ export { camelCase, snakeCase, properties, + objectArrays, wizardProperties, mappedProperties, profileFields, diff --git a/assets/javascripts/discourse/models/custom-wizard.js.es6 b/assets/javascripts/discourse/models/custom-wizard.js.es6 index 9fcc0cf3..58a89e76 100644 --- a/assets/javascripts/discourse/models/custom-wizard.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard.js.es6 @@ -1,32 +1,20 @@ import { ajax } from 'discourse/lib/ajax'; import EmberObject from "@ember/object"; -import { buildStepJson, buildJson, buildProperties } from '../lib/wizard-json'; +import { buildJson, buildProperties, present } from '../lib/wizard-json'; +import { properties, arrays, camelCase, snakeCase } from '../lib/wizard'; import { Promise } from "rsvp"; +const jsonStrings = ['api_body']; +const required = ['id', 'steps', 'type']; +const dependent = { after_time: 'after_time_scheduled' } + const CustomWizard = EmberObject.extend({ save() { return new Promise((resolve, reject) => { - let wizardJson = buildJson(this, 'wizard'); + let json = this.buildJson(this, 'wizard'); - if (wizardJson.after_time && !wizardJson.after_time_scheduled) { - reject({ - error: 'after_time_need_time' - }); - }; - - if (this.steps.length > 0) { - let stepsResult = buildStepJson(this.steps); - - if (stepsResult.error || - !stepsResult.steps || - stepsResult.steps.length < 1) { - - reject({ - error: stepsResult.error || 'steps_required' - }); - } else { - wizardJson.steps = stepsResult.steps; - } + if (json.error) { + reject({ eror: json.error }); } ajax("/admin/wizards/custom/save", { @@ -44,6 +32,111 @@ const CustomWizard = EmberObject.extend({ }); }, + buildJson(object, type, result = {}) { + for (let property of properties[type]) { + let value = object.get(property); + + if (objectArrays[type]) { + result[property] = []; + + for (let obj of value) { + let obj = this.buildJson(value, property, result); + + if (obj.error) { + result.error = r.error; + break; + } else { + result[property].push(obj); + } + } + } + + if (required[property] && !value) { + result.error = 'required' + result.errorParams = { type, property }; + } + + if (dependent[property] && !properties[type][dependent[property]]) { + result.error = 'dependent'; + result.errorParams = { + dependentProperty: properties[type][dependent[property]], + property + } + } + + if (jsonStrings[property]) { + try { + value = JSON.parse(value); + } catch (e) { + result.error = 'invalid'; + result.errorParams = { property }; + } + } + + if (mapped(property, type)) { + value = this.buildMappedJson(value); + } + + if (result.error) { + break; + } else if (value) { + result[property] = value; + } + }); + + return result; + } + + buildMappedJson(inputs) { + if (!inputs || !inputs.length) return false; + + let result = []; + + inputs.forEach(inpt => { + let input = { + type: inpt.type, + }; + + if (present(inpt.output)) { + input.output = inpt.output; + input.output_type = snakeCase(inpt.output_type); + input.output_connector = inpt.output_connector; + } + + if (present(inpt.pairs)) { + input.pairs = []; + + inpt.pairs.forEach(pr => { + if (present(pr.key) && present(pr.value)) { + + let pairParams = { + index: pr.index, + key: pr.key, + key_type: snakeCase(pr.key_type), + value: pr.value, + value_type: snakeCase(pr.value_type), + connector: pr.connector + } + + input.pairs.push(pairParams); + } + }); + } + + if ((input.type === 'assignment' && present(input.output)) || + present(input.pairs)) { + + result.push(input); + } + }); + + if (!result.length) { + result = false; + } + + return result; + }, + remove() { return ajax("/admin/wizards/custom/remove", { type: 'DELETE', diff --git a/assets/javascripts/discourse/templates/admin-wizard.hbs b/assets/javascripts/discourse/templates/admin-wizard.hbs index 0be2bcb3..6fba246e 100644 --- a/assets/javascripts/discourse/templates/admin-wizard.hbs +++ b/assets/javascripts/discourse/templates/admin-wizard.hbs @@ -157,7 +157,16 @@ step=currentStep wizard=model currentField=currentField - currentAction=currentAction + wizardFields=wizardFields}} + {{/if}} + + {{wizard-links type="action" current=currentAction items=model.actions}} + + {{#if currentAction}} + {{wizard-custom-action + action=currentAction + wizard=model + removeAction="removeAction" wizardFields=wizardFields}} {{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs index 6c8c424b..f05c75ff 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs @@ -6,7 +6,7 @@
{{combo-box value=action.type - content=types + content=actionTypes onChange=(action (mut action.type)) options=(hash none="admin.wizard.field.type" @@ -14,6 +14,19 @@
+
+
+ +
+ +
+ {{combo-box + value=action.run_after + content=runAfterContent + onChange=(action (mut action.run_after))}} +
+
+ {{#if basicTopicFields}}
@@ -99,6 +112,7 @@ inputs=action.tags options=(hash tagSelection='output' + listSelection='output' wizardFieldSelection=true userFieldSelection='key,value' context='action' @@ -280,7 +294,7 @@
{{/if}} - {{#if newTopicFields}} + {{#if showSkipRedirect}}
diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs index bc3c2942..b2ba27e9 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs @@ -49,7 +49,7 @@
{{combo-box value=field.type - content=types + content=fieldTypes onChange=(action (mut field.type)) options=(hash none="admin.wizard.field.type" @@ -57,7 +57,7 @@
-{{#if isInput}} +{{#if showMinLength}}
@@ -81,7 +81,7 @@
{{/if}} -{{#if isCategoryOrTag}} +{{#if showLimit}}
@@ -116,7 +116,7 @@
{{/if}} - {{#if prefillEnabled}} + {{#if showPrefill}}
@@ -130,7 +130,7 @@
{{/if}} - {{#if contentEnabled}} + {{#if showContent}}
diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs index edc79bcd..4881b674 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs @@ -103,17 +103,7 @@ {{#if currentField}} {{wizard-custom-field field=currentField - types=wizard.fieldTypes + fieldTypes=wizard.fieldTypes removeField="removeField" wizardFields=wizardFields}} -{{/if}} - -{{wizard-links type="action" current=currentAction items=step.actions}} - -{{#if currentAction}} - {{wizard-custom-action - action=currentAction - wizard=wizard - removeAction="removeAction" - wizardFields=wizardFields}} {{/if}} \ No newline at end of file diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 5cd0148e..a32d0272 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -95,13 +95,9 @@ en: list: "Enter item" error: - name_required: "Wizards must have a name." - steps_required: "Wizards must have at least one step." - id_required: "All wizards, steps, fields and actions need an id." - invalid_api_body: "Request body JSON needs to be a valid JSON." - type_required: "All fields need a type." - after_time_need_time: "After time is enabled but no time is set." - after_time_invalid: "After time is invalid." + required: "{{type}} requires {{property}}" + invalid: "{{property}} is invalid" + dependent: "{{dependentProperty}} is dependent on {{property}}" step: header: "Steps" @@ -154,6 +150,10 @@ en: topic_attr: "Topic Attribute" interpolate_fields: "Insert wizard fields using the field_id in w{}. Insert user fields using field key in u{}." + run_after: + label: "Run After" + wizard_completion: "Wizard completes" + custom_fields: label: "Custom" key: "field" diff --git a/controllers/custom_wizard/admin.rb b/controllers/custom_wizard/admin.rb index c66474ec..0ea04be6 100644 --- a/controllers/custom_wizard/admin.rb +++ b/controllers/custom_wizard/admin.rb @@ -10,93 +10,31 @@ class CustomWizard::AdminController < ::ApplicationController render json: { types: CustomWizard::Field.types } end - def save - params.require(:wizard) + def save + result = build_wizard - wizard = ::JSON.parse(params[:wizard]) - existing = PluginStore.get('custom_wizard', wizard['id']) || {} - new_time = false - error = nil + if result[:error] + render json: { error: result[:error] } + else + wizard = result[:wizard] + existing_wizard = result[:existing_wizard] + + ActiveRecord::Base.transaction do + PluginStore.set('custom_wizard', wizard["id"], wizard) + + if wizard['after_time'] && result[:new_after_time] + Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id']) + Jobs.enqueue_at(after_time_scheduled, :set_after_time_wizard, wizard_id: wizard['id']) + end - if wizard["id"].blank? - error = 'id_required' - elsif wizard["name"].blank? - error = 'name_required' - elsif wizard["steps"].blank? - error = 'steps_required' - elsif wizard["after_time"] - if !wizard["after_time_scheduled"] && !existing["after_time_scheduled"] - error = 'after_time_need_time' - else - after_time_scheduled = Time.parse(wizard["after_time_scheduled"]).utc - - new_time = existing['after_time_scheduled'] ? - after_time_scheduled != Time.parse(existing['after_time_scheduled']).utc : - true - - begin - if new_time && after_time_scheduled < Time.now.utc - error = 'after_time_invalid' - end - rescue ArgumentError - error = 'after_time_invalid' + if existing_wizard && existing_wizard['after_time'] && !wizard['after_time'] + Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id']) + Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard['id']) end end + + render json: success_json.merge(wizard: wizard) end - - return render json: { error: error } if error - - wizard["steps"].each do |s| - if s["id"].blank? - error = 'id_required' - break - end - - if s["fields"] && s["fields"].present? - s["fields"].each do |f| - if f["id"].blank? - error = 'id_required' - break - end - - if f["type"].blank? - error = 'type_required' - break - end - end - end - - if s["actions"] && s["actions"].present? - s["actions"].each do |a| - if a["id"].blank? - error = 'id_required' - break - end - end - end - end - - return render json: { error: error } if error - - ## end of error checks - - wizard['steps'].each do |s| - s['description'] = PrettyText.cook(s['raw_description']) if s['raw_description'] - end - - if wizard['after_time'] && new_time - Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id']) - Jobs.enqueue_at(after_time_scheduled, :set_after_time_wizard, wizard_id: wizard['id']) - end - - if existing['after_time'] && !wizard['after_time'] - Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id']) - Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard['id']) - end - - PluginStore.set('custom_wizard', wizard["id"], wizard) - - render json: success_json.merge(wizard: wizard) end def remove @@ -149,4 +87,139 @@ class CustomWizard::AdminController < ::ApplicationController render json: success_json.merge(submissions: all_submissions) end + + private + + def wizard_params + params.require(:wizard) + params[:wizard] + end + + def required_properties + { + wizard: ['id', 'name', 'steps'], + step: ['id'], + field: ['id', 'type'], + action: ['id', 'type'] + } + end + + def dependent_properties + { + after_time: 'after_time_scheduled' + } + end + + def check_required(object, type, error) + object.each do |property, value| + required = required_properties[type].include?(property) + + if required && property.blank? + error = { + type: 'required', + params: { property: property } + } + end + end + + error + end + + def check_depdendent(object, error) + object.each do |property, value| + dependent = dependent_properties[property] + + if dependent && object[dependent].blank? + error = { + type: 'dependent', + params: { dependent: dependent, property: property } + } + end + end + + error + end + + def validate_wizard(wizard) + error = nil + + error = check_required(wizard, :wizard, error) + error = check_depdendent(wizard, error) + + wizard['steps'].each do |step| + error = check_required(step, :step, error) + error = check_depdendent(step, error) + break if error.present? + + step['fields'].each do |field| + error = check_required(field, :field, error) + error = check_depdendent(field, error) + break if error.present? + end + end + + wizard['actions'].each do |action| + error = check_required(action, :action, error) + error = check_depdendent(action, error) + break if error.present? + end + + if error + { error: error } + else + { success: true } + end + end + + def validate_after_time(wizard, existing_wizard) + new = false + error = nil + + if wizard["after_time"] + if !wizard["after_time_scheduled"] && !existing_wizard["after_time_scheduled"] + error = 'after_time_need_time' + else + after_time_scheduled = Time.parse(wizard["after_time_scheduled"]).utc + + new = existing_wizard['after_time_scheduled'] ? + after_time_scheduled != Time.parse(existing_wizard['after_time_scheduled']).utc : + true + + begin + error = 'after_time_invalid' if new && after_time_scheduled < Time.now.utc + rescue ArgumentError + error = 'after_time_invalid' + end + end + end + + if error + { error: { type: error } } + else + { new: new } + end + end + + def build_wizard + wizard = ::JSON.parse(wizard_params) + existing_wizard = PluginStore.get('custom_wizard', wizard['id']) || {} + + validation = validate_wizard(wizard) + return validation[:error] if validation[:error] + + after_time_validation = validate_after_time(wizard, existing_wizard) + return after_time_validation[:error] if after_time_validation[:error] + + wizard['steps'].each do |step| + if s['raw_description'] + step['description'] = PrettyText.cook(s['raw_description']) + end + end + + result = { + wizard: wizard, + existing_wizard: existing_wizard, + new_after_time: after_time_validation[:new] + } + end end diff --git a/lib/custom_wizard/builder.rb b/lib/custom_wizard/builder.rb index 01d77fd1..c65b46ac 100644 --- a/lib/custom_wizard/builder.rb +++ b/lib/custom_wizard/builder.rb @@ -2,11 +2,12 @@ class CustomWizard::Builder attr_accessor :wizard, :updater, :submissions def initialize(user=nil, wizard_id) - data = PluginStore.get('custom_wizard', wizard_id) - return if data.blank? + template = PluginStore.get('custom_wizard', wizard_id) + return if template.blank? - @steps = data['steps'] - @wizard = CustomWizard::Wizard.new(user, data) + @steps = template['steps'] + @actions = template['actions'] + @wizard = CustomWizard::Wizard.new(user, template) @submissions = Array.wrap(PluginStore.get("#{wizard_id}_submissions", user.id)) if user end @@ -144,19 +145,24 @@ class CustomWizard::Builder submission = @submissions.last data = submission.merge(data) end + + final_step = updater.step.next.nil? - if step_template['actions'] && step_template['actions'].length && data - step_template['actions'].each do |action| - CustomWizard::Action.new( - action: action, - user: user, - data: data, - updater: updater - ).perform + if @actions.present? + @actions.each do |action| + + if (action.run_after === updater.step.id) || + (final_step && (!action.run_after || (action.run_after === 'wizard_completion'))) + + CustomWizard::Action.new( + action: action, + user: user, + data: data, + updater: updater + ).perform + end end end - - final_step = updater.step.next.nil? if route_to = data['route_to'] data.delete('route_to')