From 3c8dc540c860779ba701f93c40cae0792b9c5df1 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Fri, 10 Apr 2020 17:57:49 +1000 Subject: [PATCH] wip --- .../components/wizard-custom-action.js.es6 | 15 +- .../components/wizard-custom-field.js.es6 | 26 +- .../discourse/components/wizard-links.js.es6 | 43 +- .../discourse/components/wizard-mapper.js.es6 | 12 +- .../components/wizard-text-editor.js.es6 | 4 +- .../discourse/controllers/admin-wizard.js.es6 | 11 +- .../discourse/lib/wizard-json.js.es6 | 133 ++---- .../javascripts/discourse/lib/wizard.js.es6 | 438 +++++++++--------- .../discourse/models/custom-wizard.js.es6 | 131 +++--- .../discourse/routes/admin-wizard.js.es6 | 4 +- .../discourse/templates/admin-wizard.hbs | 4 +- .../components/wizard-custom-action.hbs | 3 +- .../components/wizard-custom-step.hbs | 3 +- assets/stylesheets/common/wizard-admin.scss | 7 +- config/locales/client.en.yml | 2 +- controllers/custom_wizard/admin.rb | 75 +-- lib/custom_wizard/field.rb | 2 +- spec/components/custom_wizard/builder_spec.rb | 2 + 18 files changed, 461 insertions(+), 454 deletions(-) diff --git a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 index 63d5d902..37b76f83 100644 --- a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 @@ -1,11 +1,11 @@ import { default as discourseComputed, observes, on } from 'discourse-common/utils/decorators'; import { equal, empty, or } from "@ember/object/computed"; -import { actionTypes, generateName, selectKitContent } from '../lib/wizard'; +import { generateName, selectKitContent, schema } from '../lib/wizard'; import Component from "@ember/component"; export default Component.extend({ classNames: 'wizard-custom-action', - actionTypes: actionTypes.map(t => ({ id: t, name: generateName(t) })), + actionTypes: Object.keys(schema.action.types).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'), @@ -21,6 +21,17 @@ export default Component.extend({ publicTopicFields: or('createTopic', 'openComposer'), showSkipRedirect: or('createTopic', 'sendMessage'), + @observes('action.type') + setupDefaults() { + const defaultProperties = schema.action.types[this.action.type]; + + Object.keys(defaultProperties).forEach(property => { + if (defaultProperties[property]) { + this.set(`action.${property}`, defaultProperties[property]); + } + }); + }, + @discourseComputed('wizard.steps') runAfterContent(steps) { let content = steps.map(function(step) { diff --git a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 index 3f8b4396..b0903ae0 100644 --- a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 @@ -1,6 +1,6 @@ import { default as discourseComputed, observes } from 'discourse-common/utils/decorators'; import { equal, or } from "@ember/object/computed"; -import { selectKitContent } from '../lib/wizard'; +import { selectKitContent, schema } from '../lib/wizard'; import Component from "@ember/component"; export default Component.extend({ @@ -16,24 +16,25 @@ export default Component.extend({ showPrefill: or('isCategory', 'isTag', 'isGroup', 'isDropdown'), showContent: or('isCategory', 'isTag', 'isGroup', 'isDropdown'), showLimit: or('isCategory', 'isTag'), - showMinLength: or('isText', 'isTextarea', 'isUrl'), + showMinLength: or('isText', 'isTextarea', 'isUrl', 'isComposer'), categoryPropertyTypes: selectKitContent(['id', 'slug']), - @observes('isUpload', 'isCategory') + @observes('field.type') setupDefaults() { - if (this.isUpload && !this.field.file_types) { - this.set('field.file_types', '.jpg,.png'); - } + const defaultProperties = schema.field.types[this.field.type]; - if (this.isCategory && !this.field.property) { - this.set('field.property', 'id'); - } + Object.keys(defaultProperties).forEach(property => { + if (defaultProperties[property]) { + this.set(`field.${property}`, defaultProperties[property]); + } + }); }, @observes('field.type') - clearMappedProperties() { - this.set('field.content', null); - this.set('field.prefill', null); + clearMapped() { + schema.field.mapped.forEach(property => { + this.set(`field.${property}`, null); + }); }, setupTypeOutput(fieldType, options) { @@ -66,7 +67,6 @@ export default Component.extend({ options.wizardFieldSelection = 'key,value'; options.listSelection += ',assignment'; options.inputTypes = 'association,assignment'; - options.singular = true; options.pairConnector = 'association'; options.keyPlaceholder = 'admin.wizard.key'; options.valuePlaceholder = 'admin.wizard.value'; diff --git a/assets/javascripts/discourse/components/wizard-links.js.es6 b/assets/javascripts/discourse/components/wizard-links.js.es6 index e973e648..fedbc273 100644 --- a/assets/javascripts/discourse/components/wizard-links.js.es6 +++ b/assets/javascripts/discourse/components/wizard-links.js.es6 @@ -1,5 +1,5 @@ import { default as discourseComputed, on, observes } from 'discourse-common/utils/decorators'; -import { generateName, defaultProperties } from '../lib/wizard'; +import { generateName, schema } from '../lib/wizard'; import { notEmpty } from "@ember/object/computed"; import { scheduleOnce, bind } from "@ember/runloop"; import EmberObject from "@ember/object"; @@ -7,7 +7,7 @@ import Component from "@ember/component"; import { A } from "@ember/array"; export default Component.extend({ - classNameBindings: [':wizard-links', 'type'], + classNameBindings: [':wizard-links', 'itemType'], items: A(), anyLinks: notEmpty('links'), @@ -33,10 +33,10 @@ export default Component.extend({ scheduleOnce('afterRender', this, () => this.applySortable()); }, - @discourseComputed('type') - header: (type) => `admin.wizard.${type}.header`, + @discourseComputed('itemType') + header: (itemType) => `admin.wizard.${itemType}.header`, - @discourseComputed('current', 'items.@each.id', 'items.@each.type') + @discourseComputed('current', 'items.@each.id', 'items.@each.type', 'items.@each.label', 'items.@each.title') links(current, items) { if (!items) return; @@ -50,7 +50,7 @@ export default Component.extend({ if (this.generateLabels && item.type) { label = generateName(item.type); } - + link.label = label; let classes = 'btn'; @@ -64,28 +64,37 @@ export default Component.extend({ } }); }, + + setDefaults(object, params) { + Object.keys(object).forEach(property => { + if (object[property]) { + params[property] = object[property]; + } + }); + return params; + }, actions: { add() { const items = this.items; - const type = this.type; - const newId = `${type}_${items.length + 1}`; + const itemType = this.itemType; let params = { - id: newId, + id: `${itemType}_${items.length + 1}`, isNew: true }; - - if (type === 'step') { - params.fields = A(); + + if (schema[itemType].objectArrays) { + Object.keys(schema[itemType].objectArrays).forEach(objectType => { + params[objectArrays[objectType].property] = A(); + }); }; - if (defaultProperties[type]) { - Object.keys(defaultProperties[type]).forEach(key => { - params[key] = defaultProperties[type][key]; - }); + params = this.setDefaults(schema[itemType].basic, params); + if (schema[itemType].types) { + params = this.setDefaults(schema[itemType].types[params.type], params); } - + const newItem = EmberObject.create(params); items.pushObject(newItem); diff --git a/assets/javascripts/discourse/components/wizard-mapper.js.es6 b/assets/javascripts/discourse/components/wizard-mapper.js.es6 index e08fa649..2d1875bc 100644 --- a/assets/javascripts/discourse/components/wizard-mapper.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper.js.es6 @@ -1,23 +1,23 @@ import { getOwner } from 'discourse-common/lib/get-owner'; import { newInput, selectionTypes } from '../lib/wizard-mapper'; import { default as discourseComputed, observes, on } from 'discourse-common/utils/decorators'; -import { gt } from "@ember/object/computed"; import Component from "@ember/component"; import { A } from "@ember/array"; export default Component.extend({ classNames: 'wizard-mapper', - hasInput: gt('inputs.length', 0), - @discourseComputed('options.singular', 'hasInput') - canAdd(singular, hasInput) { - return !singular || !hasInput; + @discourseComputed('inputs.@each.type') + canAdd(inputs) { + return !inputs || inputs.every(i => { + return ['assignment','association'].indexOf(i.type) === -1; + }); }, @discourseComputed('options.@each.inputType') inputOptions(options) { let result = { - inputTypes: options.inputTypes || 'conditional,assignment', + inputTypes: options.inputTypes || 'assignment,conditional', inputConnector: options.inputConnector || 'or', pairConnector: options.pairConnector || null, outputConnector: options.outputConnector || null, diff --git a/assets/javascripts/discourse/components/wizard-text-editor.js.es6 b/assets/javascripts/discourse/components/wizard-text-editor.js.es6 index 08966890..5c95f179 100644 --- a/assets/javascripts/discourse/components/wizard-text-editor.js.es6 +++ b/assets/javascripts/discourse/components/wizard-text-editor.js.es6 @@ -1,5 +1,5 @@ import { default as discourseComputed, on } from 'discourse-common/utils/decorators'; -import { profileFields } from '../lib/wizard'; +import { userProperties } from '../lib/wizard'; import { scheduleOnce } from "@ember/runloop"; import Component from "@ember/component"; @@ -34,7 +34,7 @@ export default Component.extend({ @discourseComputed() userFieldList() { - return profileFields.map((f) => ` u{${f}}`); + return userProperties.map((f) => ` u{${f}}`); }, @discourseComputed('wizardFields') diff --git a/assets/javascripts/discourse/controllers/admin-wizard.js.es6 b/assets/javascripts/discourse/controllers/admin-wizard.js.es6 index d4b21192..b5d4b3b6 100644 --- a/assets/javascripts/discourse/controllers/admin-wizard.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizard.js.es6 @@ -93,9 +93,16 @@ export default Controller.extend({ this.send("refreshWizard"); } }).catch((result) => { - console.log('catch result: ', result) this.set('saving', false); - this.set('error', I18n.t(`admin.wizard.error.${result.error}`, result.errorParams || {})); + + let error = true; + if (result.error) { + let type = result.error.type; + let params = result.error.params || {}; + error = I18n.t(`admin.wizard.error.${type}`, params); + } + this.set('error', error); + 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 3dccdd85..070fdc6e 100644 --- a/assets/javascripts/discourse/lib/wizard-json.js.es6 +++ b/assets/javascripts/discourse/lib/wizard-json.js.es6 @@ -1,4 +1,4 @@ -import { properties, mappedProperties, advancedProperties, camelCase, snakeCase } from '../lib/wizard'; +import { schema, listProperties, camelCase, snakeCase } from '../lib/wizard'; import EmberObject from '@ember/object'; import { A } from "@ember/array"; @@ -15,8 +15,7 @@ function present(val) { } function mapped(property, type) { - return mappedProperties[type] && - mappedProperties[type].indexOf(property) > -1; + return schema[type].mapped.indexOf(property) > -1; } function castCase(property, value) { @@ -67,36 +66,54 @@ function buildProperty(json, property, type) { } function buildObject(json, type) { - let params = { + let props = { isNew: false } Object.keys(json).forEach(prop => { - params[prop] = buildProperty(json, prop, type) + props[prop] = buildProperty(json, prop, type) }); - return EmberObject.create(params); + return EmberObject.create(props); } -function wizardHasAdvanced(property, value) { - if (property === 'save_submissions' && value == false) return true; - if (property === 'restart_on_revisit' && value == true) return true; - return false; +function buildObjectArray(json, type) { + let array = A(); + + if (present(json)) { + json.forEach((objJson) => { + let object = buildObject(objJson, type); + + if (hasAdvancedProperties(object, type)) { + object.set('showAdvanced', true); + } + + array.pushObject(object); + }); + } + + return array; } -function stepHasAdvanced(property, value) { - return advancedProperties.steps[property] && present(value); +function buildBasicProperties(json, type, props) { + listProperties(type).forEach((p) => { + props[p] = buildProperty(json, p, type); + + if (hasAdvancedProperties(json, type)) { + result.showAdvanced = true; + } + }); + + return props; } -function objectHasAdvanced(params, type) { - return Object.keys(params).some(p => { - let value = params[p]; - let advanced = advancedProperties[type][params.type]; - return advanced && advanced.indexOf(p) > -1 && present(value); +function hasAdvancedProperties(object, type) { + return Object.keys(object).some(p => { + return schema[type].advanced.indexOf(p) > -1 && present(object[p]); }); } -/// to be removed +/// to be removed: necessary due to action array being moved from step to wizard function actionPatch(json) { let actions = json.actions || []; @@ -117,86 +134,32 @@ function actionPatch(json) { function buildProperties(json) { let props = { - steps: A(), - actions: A() + steps: A() }; if (present(json)) { - props.id = json.id; props.existingId = true; - - // to fix - properties.wizard - .filter(p => ['steps', 'actions'].indexOf(p) === -1) - .forEach((p) => { - props[p] = buildProperty(json, p, 'wizard'); - - if (wizardHasAdvanced(p, json[p])) { - props.showAdvanced = true; - } - }); + props = buildBasicProperties(json, 'wizard', props); if (present(json.steps)) { json.steps.forEach((stepJson) => { - let stepParams = { + let stepProps = { isNew: false }; + + stepProps = buildBasicProperties(stepJson, 'step', stepProps); + stepProps.fields = buildObjectArray(stepJson.fields, 'field'); - properties.steps.forEach((p) => { - stepParams[p] = buildProperty(stepJson, p, 'wizard'); - - if (stepHasAdvanced(p, stepJson[p])) { - stepParams.showAdvanced = true; - } - }); - - stepParams.fields = A(); - - if (present(stepJson.fields)) { - stepJson.fields.forEach((f) => { - let params = buildObject(f, 'fields'); - - if (objectHasAdvanced(params, 'fields')) { - params.showAdvanced = true; - } - - stepParams.fields.pushObject(params); - }); - } - - props.steps.pushObject( - EmberObject.create(stepParams) - ); + props.steps.pushObject(EmberObject.create(stepProps)); }); }; - - // to be removed - json = actionPatch(json); - // to be removed - - if (present(json.actions)) { - json.actions.forEach((a) => { - let params = buildObject(a, 'actions'); - - if (objectHasAdvanced(params, 'actions')) { - params.showAdvanced = true; - } - - props.actions.pushObject(params); - }); - } + + json = actionPatch(json); // to be removed - see above + props.actions = buildObjectArray(json.actions, 'action'); } else { - props.id = ''; - props.name = ''; - props.background = ''; - props.save_submissions = true; - props.multiple_submissions = false; - props.after_signup = false; - props.after_time = false; - props.required = false; - props.prompt_completion = false; - props.restart_on_revisit = false; - props.permitted = null; + listProperties('wizard').forEach(prop => { + props[prop] = schema.wizard.basic[prop]; + }); } return props; diff --git a/assets/javascripts/discourse/lib/wizard.js.es6 b/assets/javascripts/discourse/lib/wizard.js.es6 index 5a33235c..7905e4c7 100644 --- a/assets/javascripts/discourse/lib/wizard.js.es6 +++ b/assets/javascripts/discourse/lib/wizard.js.es6 @@ -30,7 +30,7 @@ function camelCase(string) { }); } -const profileFields = [ +const userProperties = [ 'name', 'email', 'avatar', @@ -45,164 +45,198 @@ const profileFields = [ 'trust_level' ]; -const wizardProperties = [ - 'id', - 'name', - 'background', - 'save_submissions', - 'multiple_submissions', - 'after_signup', - 'after_time', - 'after_time_scheduled', - 'required', - 'prompt_completion', - 'restart_on_revisit', - 'theme_id', - 'permitted', - 'steps', - 'actions' -]; - -const stepProperties = [ - 'id', - 'title', - 'key', - 'banner', - 'raw_description', - 'required_data', - 'required_data_message', - 'permitted_params', - 'fields' -] - -const fieldProperties = [ - 'id', - 'label', - 'key', - 'image', - 'description', - 'type', - 'required', - 'min_length', - 'file_types', - 'property', - 'limit', - 'prefill', - 'content' -] - -const actionProperties = [ - 'id', - 'type', - 'run_after', - 'title', - 'post', - 'post_builder', - 'post_template', - 'category', - 'tags', - 'skip_redirect', - 'custom_fields', - 'required', - 'recipient', - 'profile_updates', - 'group', - 'url', - 'code', - 'api', - 'api_endpoint', - 'api_body' -] - -const properties = { - wizard: wizardProperties, - steps: stepProperties, - fields: fieldProperties, - actions: actionProperties -} - -const actionTypeProperties = { - create_topic: [ - 'id', - 'type', - 'run_after', - 'title', - 'post', - 'post_builder', - 'post_template', - 'category', - 'tags', - 'skip_redirect', - 'custom_fields' - ], - send_message: [ - 'id', - 'type', - 'run_after', - 'title', - 'post', - 'post_builder', - 'post_template', - 'skip_redirect', - 'custom_fields', - 'required', - 'recipient' - ], - open_composer: [ - 'id', - 'type', - 'run_after', - 'title', - 'post', - 'post_builder', - 'post_template', - 'category', - 'tags', - 'custom_fields' - ], - update_profile: [ - 'id', - 'type', - 'run_after', - 'profile_updates', - 'custom_fields' - ], - add_to_group: [ - 'id', - 'type', - 'run_after', - 'group' - ], - route_to: [ - 'id', - 'type', - 'run_after', - 'url', - 'code' - ], - send_to_api: [ - 'id', - 'type', - 'run_after', - 'api', - 'api_endpoint', - 'api_body' - ] -} - -const mappedProperties = { - wizard: [ +const wizardProperties = { + basic: { + id: null, + name: null, + background: null, + save_submissions: true, + multiple_submissions: null, + after_signup: null, + after_time: null, + after_time_scheduled: null, + required: null, + prompt_completion: null, + restart_on_revisit: null, + theme_id: null, + permitted: null + }, + mapped: [ 'permitted' ], - steps: [ + advanced: [ + 'restart_on_revisit', + ], + required: [ + 'id', + ], + dependent: { + after_time: 'after_time_scheduled' + }, + objectArrays: { + step: { + property: 'steps', + required: false + }, + action: { + property: 'actions', + required: false + } + } +}; + +const stepProperties = { + basic: { + id: null, + title: null, + key: null, + banner: null, + raw_description: null, + required_data: null, + required_data_message: null, + permitted_params: null + }, + mapped: [ 'required_data', 'permitted_params' ], - fields: [ + advanced: [ + 'required_data', + 'permitted_params' + ], + required: [ + 'id' + ], + dependent: { + }, + objectArrays: { + field: { + property: 'fields', + required: false + } + } +} + +const fieldProperties = { + basic: { + id: null, + label: null, + image: null, + description: null, + required: null, + key: null, + type: 'text' + }, + types: { + text: { + min_length: null + }, + textarea: { + min_length: null + }, + composer: { + min_length: null + }, + number: { + }, + url: { + min_length: null + }, + 'text-only': { + }, + 'user-selector': { + }, + upload: { + file_types: '.jpg,.png' + }, + dropdown: { + prefill: null, + content: null + }, + tag: { + limit: null, + prefill: null, + content: null + }, + category: { + limit: 1, + property: 'id', + prefill: null, + content: null + }, + group: { + prefill: null, + content: null + } + }, + mapped: [ 'prefill', 'content' ], - actions: [ + advanced: [ + 'prefill', + 'content', + 'property' + ], + required: [ + 'id', + 'type' + ], + dependent: { + }, + objectArrays: { + } +} + +const actionProperties = { + basic: { + id: null, + run_after: 'wizard_completion', + type: 'create_topic' + }, + types: { + create_topic: { + title: null, + post: null, + post_builder: null, + post_template: null, + category: null, + tags: null, + custom_fields: null, + skip_redirect: null + }, + send_message: { + title: null, + post: null, + post_builder: null, + post_template: null, + skip_redirect: null, + custom_fields: null, + required: null, + recipient: null + }, + open_composer: { + title: null, + post: null, + post_builder: null, + post_template: null, + category: null, + tags: null, + custom_fields: null + }, + update_profile: { + profile_updates: null, + custom_fields: null + }, + add_to_group: { + group: null + }, + route_to: { + url: null, + code: null + } + }, + mapped: [ 'title', 'category', 'tags', @@ -211,67 +245,46 @@ const mappedProperties = { 'recipient', 'profile_updates', 'group' - ] -} - -const defaultProperties = { - action: { - run_after: 'wizard_completion' + ], + advanced: [ + 'code', + 'custom_fields', + 'skip_redirect', + 'required' + ], + required: [ + 'id', + 'type' + ], + dependent: { + }, + objectArrays: { } } -const advancedFieldTypes = [ - 'category', - 'tag', - 'group', - 'dropdown' -] +if (Discourse.SiteSettings.wizard_api_features) { + actionProperties.types.send_to_api = { + api: null, + api_endpoint: null, + api_body: null + } +} -const advancedFieldProperties = [ - 'prefill', - 'content' -] +const schema = { + wizard: wizardProperties, + step: stepProperties, + field: fieldProperties, + action: actionProperties +} -const actionTypes = [ - 'create_topic', - 'update_profile', - 'send_message', - 'send_to_api', - 'add_to_group', - 'route_to', - 'open_composer' -].filter(function(type) { - return Discourse.SiteSettings.wizard_api_features || type !== 'send_to_api'; -}); - -const advancedProperties = { - steps: [ - 'required_data', - 'permitted_params' - ], - fields: advancedFieldTypes.reduce( - function(map, type) { - map[type] = advancedFieldProperties; - if (type === 'category') { - map[type].push('property'); - } - return map; - }, {} - ), - actions: actionTypes.reduce( - function(map, type) { - if (type === 'route_to') { - map[type] = ['code']; - } else if (['create_topic', 'send_message', 'open_composer', 'update_profile'].indexOf(type) > -1) { - map[type] = ['custom_fields']; - } else if (['create_topic', 'send_message'].indexOf(type) > -1) { - map[type].push('skip_redirect'); - } else if (type === 'send_message') { - map[type].push('required'); - } - return map; - }, {} - ) +function listProperties(type, objectType = null) { + let properties = Object.keys(schema[type].basic); + + if (schema[type].types && objectType) { + properties = properties.concat(Object.keys(schema[type].types[objectType])); + } + + return properties; } export { @@ -280,12 +293,7 @@ export { generateId, camelCase, snakeCase, - properties, - wizardProperties, - mappedProperties, - profileFields, - advancedProperties, - actionTypes, - actionTypeProperties, - defaultProperties + schema, + userProperties, + listProperties }; \ No newline at end of file diff --git a/assets/javascripts/discourse/models/custom-wizard.js.es6 b/assets/javascripts/discourse/models/custom-wizard.js.es6 index b76682b2..3b789676 100644 --- a/assets/javascripts/discourse/models/custom-wizard.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard.js.es6 @@ -1,13 +1,9 @@ import { ajax } from 'discourse/lib/ajax'; import EmberObject from "@ember/object"; import { buildProperties, present, mapped } from '../lib/wizard-json'; -import { properties, actionTypeProperties, camelCase, snakeCase } from '../lib/wizard'; +import { schema, listProperties, 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) => { @@ -33,87 +29,96 @@ const CustomWizard = EmberObject.extend({ }, buildJson(object, type, result = {}) { - let allowedProperties; + let objectType = object.type || null; - if (type === 'actions') { - if (!object.type) { + if (schema[type].types) { + if (!objectType) { result.error = { type: 'required', - params: { - type, - property: 'type' - } + params: { type, property: 'type' } } return result; } - - allowedProperties = actionTypeProperties[object.type]; - } else { - allowedProperties = properties[type]; } - for (let property of allowedProperties) { + for (let property of listProperties(type, objectType)) { let value = object.get(property); - if (required[property] && !value) { - result.error = { - type: 'required', - params: { type, property } - } - } - - let dependentOn = dependent[property]; - if (dependentOn && value && !object[dependentOn]) { - result.error = { - type: 'dependent', - params: { - property, - dependentOn - } - } - } - - if (jsonStrings[property]) { - try { - value = JSON.parse(value); - } catch (e) { - result.error = { - type: 'invalid', - params: { type, property } - } - } - } + result = this.validateValue(property, value, type, result); if (result.error) { break; } - - if (properties[property]) { - result[property] = []; + + if (mapped(property, type)) { + value = this.buildMappedJson(value); + } + + if (value !== undefined && value !== null) { + result[property] = value; + } + }; + + if (!result.error) { + for (let arrayObjectType of Object.keys(schema[type].objectArrays)) { + let arraySchema = schema[type].objectArrays[arrayObjectType]; + let objectArray = object.get(arraySchema.property); - for (let item of value) { - let itemParams = this.buildJson(item, property); + if (arraySchema.required && !present(objectArray)) { + result.error = { + type: 'required', + params: { type, property: arraySchema.property } + } + break; + } + + result[arraySchema.property] = []; + + for (let item of objectArray) { + let itemProps = this.buildJson(item, arrayObjectType); - if (itemParams.error) { - result.error = r.error; + if (itemProps.error) { + result.error = itemProps.error; break; } else { - result[property].push(itemParams); + result[arraySchema.property].push(itemProps); } } - } else { - if (mapped(property, type)) { - value = this.buildMappedJson(value); - } - - if (value !== undefined && value !== null) { - result[property] = value; - } - } - }; + }; + } return result; }, + + validateValue(property, value, type, result) { + if (schema[type].required.indexOf(property) > -1 && !value) { + result.error = { + type: 'required', + params: { type, property } + } + } + + let dependent = schema[type].dependent[property]; + if (dependent && value && !object[dependent]) { + result.error = { + type: 'dependent', + params: { property, dependent } + } + } + + if (property === 'api_body') { + try { + value = JSON.parse(value); + } catch (e) { + result.error = { + type: 'invalid', + params: { type, property } + } + } + } + + return result; + }, buildMappedJson(inputs) { if (!inputs || !inputs.length) return false; diff --git a/assets/javascripts/discourse/routes/admin-wizard.js.es6 b/assets/javascripts/discourse/routes/admin-wizard.js.es6 index 607efaa6..826fbf7b 100644 --- a/assets/javascripts/discourse/routes/admin-wizard.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizard.js.es6 @@ -1,6 +1,6 @@ import CustomWizard from '../models/custom-wizard'; import { ajax } from 'discourse/lib/ajax'; -import { selectKitContent, profileFields, generateName } from '../lib/wizard'; +import { selectKitContent, userProperties, generateName } from '../lib/wizard'; import DiscourseRoute from "discourse/routes/discourse"; import { all } from "rsvp"; @@ -79,7 +79,7 @@ export default DiscourseRoute.extend({ id: `user_field_${f.id}`, name: f.name })).concat( - profileFields.map((f) => ({ + userProperties.map((f) => ({ id: f, name: generateName(f) })) diff --git a/assets/javascripts/discourse/templates/admin-wizard.hbs b/assets/javascripts/discourse/templates/admin-wizard.hbs index f24f1650..7029b9e0 100644 --- a/assets/javascripts/discourse/templates/admin-wizard.hbs +++ b/assets/javascripts/discourse/templates/admin-wizard.hbs @@ -151,7 +151,7 @@ {{wizard-links - type="step" + itemType="step" current=currentStep items=model.steps}} @@ -164,7 +164,7 @@ {{/if}} {{wizard-links - type="action" + itemType="action" current=currentAction items=model.actions generateLabels=true}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs index c53d2614..32083f03 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs @@ -112,6 +112,7 @@ inputs=action.tags options=(hash tagSelection='output' + outputDefaultSelection='tag' listSelection='output' wizardFieldSelection=true userFieldSelection='key,value' @@ -152,7 +153,6 @@ {{wizard-mapper inputs=action.profile_updates options=(hash - singular=true inputTypes='association' textSelection='value' userFieldSelection='key' @@ -263,7 +263,6 @@ {{wizard-mapper inputs=action.custom_fields options=(hash - singular=true inputTypes='association' wizardFieldSelection='value' userFieldSelection='value' diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs index 4881b674..6752f6af 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs @@ -49,6 +49,7 @@ inputs=step.required_data options=(hash inputTypes='validation' + inputConnector='and' wizardFieldSelection='value' userFieldSelection='value' keyPlaceholder="admin.wizard.submission_key" @@ -98,7 +99,7 @@ {{/if}} {{/if}} -{{wizard-links type="field" current=currentField items=step.fields}} +{{wizard-links itemType="field" current=currentField items=step.fields}} {{#if currentField}} {{wizard-custom-field diff --git a/assets/stylesheets/common/wizard-admin.scss b/assets/stylesheets/common/wizard-admin.scss index 5038546a..6353f790 100644 --- a/assets/stylesheets/common/wizard-admin.scss +++ b/assets/stylesheets/common/wizard-admin.scss @@ -31,20 +31,19 @@ body.admin-wizard { } .wizard-settings, -.wizard-custom-step { +.wizard-custom-step, +.wizard-custom-action { @extend .wizard-settings-parent; @extend .wizard-settings-group; } .wizard-basic-details, .wizard-custom-field, -.wizard-custom-action, .advanced-settings { @extend .wizard-settings-group; } -.wizard-custom-field, -.wizard-custom-action { +.wizard-custom-field { position: relative; background: transparent; background-color: $setting-background; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 415f3979..0e139be0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -97,7 +97,7 @@ en: error: required: "{{type}} requires {{property}}" invalid: "{{property}} is invalid" - dependent: "{{property}} is dependent on {{dependentOn}}" + dependent: "{{property}} is dependent on {{dependent}}" step: header: "Steps" diff --git a/controllers/custom_wizard/admin.rb b/controllers/custom_wizard/admin.rb index 53ba8058..d9ba5ba5 100644 --- a/controllers/custom_wizard/admin.rb +++ b/controllers/custom_wizard/admin.rb @@ -106,18 +106,21 @@ class CustomWizard::AdminController < ::ApplicationController def dependent_properties { - after_time: 'after_time_scheduled' + wizard: { + after_time: 'after_time_scheduled' + }, + step: {}, + field: {}, + action: {} } end - def check_required(object, type, error) - object.each do |property, value| - required = required_properties[type].include?(property) - - if required && property.blank? + def check_required(object, type, error) + required_properties[type].each do |property| + if object[property].blank? error = { type: 'required', - params: { property: property } + params: { type: type, property: property } } end end @@ -125,18 +128,16 @@ class CustomWizard::AdminController < ::ApplicationController error end - def check_depdendent(object, error) - object.each do |property, value| - dependent = dependent_properties[property] - - if dependent && object[dependent].blank? + def check_depdendent(object, type, error) + dependent_properties[type].each do |property, dependent| + if object[property] && object[dependent].blank? error = { type: 'dependent', - params: { dependent: dependent, property: property } + params: { property: property, dependent: dependent } } end end - + error end @@ -144,27 +145,29 @@ class CustomWizard::AdminController < ::ApplicationController 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? - - if step['fields'].present? - step['fields'].each do |field| - error = check_required(field, :field, error) - error = check_depdendent(field, error) - break if error.present? + error = check_depdendent(wizard, :wizard, error) + + if !error + wizard['steps'].each do |step| + error = check_required(step, :step, error) + error = check_depdendent(step, :step, error) + break if error.present? + + if step['fields'].present? + step['fields'].each do |field| + error = check_required(field, :field, error) + error = check_depdendent(field, :field, error) + break if error.present? + end end end - end - - if wizard['actions'].present? - wizard['actions'].each do |action| - error = check_required(action, :action, error) - error = check_depdendent(action, error) - break if error.present? + + if wizard['actions'].present? + wizard['actions'].each do |action| + error = check_required(action, :action, error) + error = check_depdendent(action, :action, error) + break if error.present? + end end end @@ -209,10 +212,10 @@ class CustomWizard::AdminController < ::ApplicationController existing_wizard = PluginStore.get('custom_wizard', wizard['id']) || {} validation = validate_wizard(wizard) - return validation[:error] if validation[:error] + return validation if validation[:error] after_time_validation = validate_after_time(wizard, existing_wizard) - return after_time_validation[:error] if after_time_validation[:error] + return after_time_validation if after_time_validation[:error] wizard['steps'].each do |step| if step['raw_description'] @@ -220,7 +223,7 @@ class CustomWizard::AdminController < ::ApplicationController end end - result = { + { wizard: wizard, existing_wizard: existing_wizard, new_after_time: after_time_validation[:new] diff --git a/lib/custom_wizard/field.rb b/lib/custom_wizard/field.rb index c7725f1b..7fe1f25d 100644 --- a/lib/custom_wizard/field.rb +++ b/lib/custom_wizard/field.rb @@ -1,6 +1,6 @@ class CustomWizard::Field def self.types - @types ||= ['checkbox', 'composer', 'dropdown', 'tag', 'category', 'group', 'image', 'text', 'textarea', 'text-only', 'number', 'upload', 'user-selector', 'url'] + @types ||= ['checkbox', 'composer', 'dropdown', 'tag', 'category', 'group', 'text', 'textarea', 'text-only', 'number', 'upload', 'user-selector', 'url'] end def self.require_assets diff --git a/spec/components/custom_wizard/builder_spec.rb b/spec/components/custom_wizard/builder_spec.rb index c53b8430..4bd31127 100644 --- a/spec/components/custom_wizard/builder_spec.rb +++ b/spec/components/custom_wizard/builder_spec.rb @@ -18,6 +18,7 @@ describe CustomWizard::Builder do let(:permitted_params) {[{"key":"param_key","value":"submission_param_key"}]} let(:required_data) {[{"key":"nickname","connector":"equals","value":"name"}]} let(:required_data_message) {"Nickname is required to match your name"} + let(:checkbox_field) {{"id":"checkbox","type":"checkbox","label":"Checkbox"}} let(:composer_field) {{"id": "composer","label":"Composer","type":"composer"}} let(:tag_field) {{"id": "tag","type": "tag","label": "Tag","limit": "2"}} @@ -28,6 +29,7 @@ describe CustomWizard::Builder do let(:text_only_field) {{"id": "text_only","type": "text-only","label": "Text only"}} let(:upload_field) {{"id": "upload","type": "upload","file_types": ".jpg,.png,.pdf","label": "Upload"}} let(:user_selector_field) {{"id": "user_selector","type": "user-selector","label": "User selector"}} + let(:create_topic_action) {{"id":"create_topic","type":"create_topic","title":"text","post":"textarea"}} let(:send_message_action) {{"id":"send_message","type":"send_message","title":"text","post":"textarea","username":"angus"}} let(:route_to_action) {{"id":"route_to","type":"route_to","url":"https://google.com"}}