diff --git a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 index 09896a6c..b5f6b0ee 100644 --- a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 @@ -107,6 +107,40 @@ export default Component.extend(UndoChanges, { return this.setupTypeOutput(fieldType, options); }, + @discourseComputed("step.index") + fieldConditionOptions(stepIndex) { + const options = { + inputTypes: "validation", + context: "field", + textSelection: "value", + userFieldSelection: true, + groupSelection: true, + }; + + if (stepIndex > 0) { + options.wizardFieldSelection = true; + options.wizardActionSelection = true; + } + + return options; + }, + + @discourseComputed("step.index") + fieldIndexOptions(stepIndex) { + const options = { + context: "field", + userFieldSelection: true, + groupSelection: true, + }; + + if (stepIndex > 0) { + options.wizardFieldSelection = true; + options.wizardActionSelection = true; + } + + return options; + }, + 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 5715bbda..2a07dd65 100644 --- a/assets/javascripts/discourse/components/wizard-custom-step.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-step.js.es6 @@ -1,8 +1,27 @@ import Component from "@ember/component"; +import discourseComputed from "discourse-common/utils/decorators"; export default Component.extend({ classNames: "wizard-custom-step", + @discourseComputed("step.index") + stepConditionOptions(stepIndex) { + const options = { + inputTypes: "validation", + context: "step", + textSelection: "value", + userFieldSelection: true, + groupSelection: true, + }; + + if (stepIndex > 0) { + options["wizardFieldSelection"] = true; + options["wizardActionSelection"] = true; + } + + return options; + }, + actions: { bannerUploadDone(upload) { this.set("step.banner", upload.url); diff --git a/assets/javascripts/discourse/components/wizard-links.js.es6 b/assets/javascripts/discourse/components/wizard-links.js.es6 index 5d82a57d..c32809aa 100644 --- a/assets/javascripts/discourse/components/wizard-links.js.es6 +++ b/assets/javascripts/discourse/components/wizard-links.js.es6 @@ -38,6 +38,7 @@ export default Component.extend({ const items = this.items; const item = items.findBy("id", itemId); items.removeObject(item); + item.set("index", newIndex); items.insertAt(newIndex, item); scheduleOnce("afterRender", this, () => this.applySortable()); }, @@ -90,22 +91,14 @@ export default Component.extend({ params.isNew = true; - let next = 1; - + let index = 0; if (items.length) { - next = - Math.max.apply( - Math, - items.map((i) => { - let parts = i.id.split("_"); - let lastPart = parts[parts.length - 1]; - return isNaN(lastPart) ? 0 : lastPart; - }) - ) + 1; + index = items.length; } - let id = `${itemType}_${next}`; + params.index = index; + let id = `${itemType}_${index + 1}`; if (itemType === "field") { id = `${this.parentId}_${id}`; } diff --git a/assets/javascripts/discourse/lib/wizard-json.js.es6 b/assets/javascripts/discourse/lib/wizard-json.js.es6 index 4b0aaa3d..79da60cb 100644 --- a/assets/javascripts/discourse/lib/wizard-json.js.es6 +++ b/assets/javascripts/discourse/lib/wizard-json.js.es6 @@ -23,54 +23,69 @@ function castCase(property, value) { return property.indexOf("_type") > -1 ? camelCase(value) : value; } -function buildProperty(json, property, type) { - let value = json[property]; +function buildMappedProperty(value) { + let inputs = []; - if (mapped(property, type) && present(value) && value.constructor === Array) { - let inputs = []; + value.forEach((inputJson) => { + let input = {}; - value.forEach((inputJson) => { - let input = {}; + Object.keys(inputJson).forEach((inputKey) => { + if (inputKey === "pairs") { + let pairs = []; + let pairCount = inputJson.pairs.length; - Object.keys(inputJson).forEach((inputKey) => { - if (inputKey === "pairs") { - let pairs = []; - let pairCount = inputJson.pairs.length; + inputJson.pairs.forEach((pairJson) => { + let pair = {}; - inputJson.pairs.forEach((pairJson) => { - let pair = {}; - - Object.keys(pairJson).forEach((pairKey) => { - pair[pairKey] = castCase(pairKey, pairJson[pairKey]); - }); - - pair.pairCount = pairCount; - - pairs.push(EmberObject.create(pair)); + Object.keys(pairJson).forEach((pairKey) => { + pair[pairKey] = castCase(pairKey, pairJson[pairKey]); }); - input.pairs = pairs; - } else { - input[inputKey] = castCase(inputKey, inputJson[inputKey]); - } - }); + pair.pairCount = pairCount; - inputs.push(EmberObject.create(input)); + pairs.push(EmberObject.create(pair)); + }); + + input.pairs = pairs; + } else { + input[inputKey] = castCase(inputKey, inputJson[inputKey]); + } }); - return A(inputs); - } else { - return value; - } + inputs.push(EmberObject.create(input)); + }); + + return A(inputs); } -function buildObject(json, type) { +function buildProperty(json, property, type, objectIndex) { + let value = json[property]; + if ( + property === "index" && + (value === null || value === undefined) && + (objectIndex !== null || objectIndex !== undefined) + ) { + return objectIndex; + } + + if ( + !mapped(property, type) || + !present(value) || + !value.constructor === Array + ) { + return value; + } + + return buildMappedProperty(value); +} + +function buildObject(json, type, objectIndex) { let props = { isNew: false, }; Object.keys(json).forEach((prop) => { - props[prop] = buildProperty(json, prop, type); + props[prop] = buildProperty(json, prop, type, objectIndex); }); return EmberObject.create(props); @@ -80,8 +95,8 @@ function buildObjectArray(json, type) { let array = A(); if (present(json)) { - json.forEach((objJson) => { - let object = buildObject(objJson, type); + json.forEach((objJson, objectIndex) => { + let object = buildObject(objJson, type, objectIndex); if (hasAdvancedProperties(object, type)) { object.set("showAdvanced", true); @@ -94,9 +109,9 @@ function buildObjectArray(json, type) { return array; } -function buildBasicProperties(json, type, props) { +function buildBasicProperties(json, type, props, objectIndex = null) { listProperties(type).forEach((p) => { - props[p] = buildProperty(json, p, type); + props[p] = buildProperty(json, p, type, objectIndex); if (hasAdvancedProperties(json, type)) { props.showAdvanced = true; @@ -142,12 +157,17 @@ function buildProperties(json) { props = buildBasicProperties(json, "wizard", props); if (present(json.steps)) { - json.steps.forEach((stepJson) => { + json.steps.forEach((stepJson, objectIndex) => { let stepProps = { isNew: false, }; - stepProps = buildBasicProperties(stepJson, "step", stepProps); + stepProps = buildBasicProperties( + stepJson, + "step", + stepProps, + objectIndex + ); stepProps.fields = buildObjectArray(stepJson.fields, "field"); props.steps.pushObject(EmberObject.create(stepProps)); diff --git a/assets/javascripts/discourse/lib/wizard-schema.js.es6 b/assets/javascripts/discourse/lib/wizard-schema.js.es6 index effd8a5c..d196f387 100644 --- a/assets/javascripts/discourse/lib/wizard-schema.js.es6 +++ b/assets/javascripts/discourse/lib/wizard-schema.js.es6 @@ -37,6 +37,7 @@ const wizard = { const step = { basic: { id: null, + index: null, title: null, key: null, banner: null, @@ -44,9 +45,11 @@ const step = { required_data: null, required_data_message: null, permitted_params: null, + condition: null, + force_final: false, }, - mapped: ["required_data", "permitted_params"], - advanced: ["required_data", "permitted_params"], + mapped: ["required_data", "permitted_params", "condition", "index"], + advanced: ["required_data", "permitted_params", "condition", "index"], required: ["id"], dependent: {}, objectArrays: { @@ -60,16 +63,18 @@ const step = { const field = { basic: { id: null, + index: null, label: null, image: null, description: null, required: null, key: null, type: null, + condition: null, }, types: {}, - mapped: ["prefill", "content"], - advanced: ["property", "key"], + mapped: ["prefill", "content", "condition", "index"], + advanced: ["property", "key", "condition", "index"], required: ["id", "type"], dependent: {}, objectArrays: {}, diff --git a/assets/javascripts/discourse/models/custom-wizard.js.es6 b/assets/javascripts/discourse/models/custom-wizard.js.es6 index 7d97183f..e6a8408d 100644 --- a/assets/javascripts/discourse/models/custom-wizard.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard.js.es6 @@ -131,11 +131,15 @@ const CustomWizard = EmberObject.extend({ return result; }, - buildMappedJson(inputs) { - if (!inputs || !inputs.length) { + buildMappedJson(value) { + if (typeof value === "string" || Number.isInteger(value)) { + return value; + } + if (!value || !value.length) { return false; } + let inputs = value; let result = []; inputs.forEach((inpt) => { diff --git a/assets/javascripts/discourse/templates/admin-wizards-logs.hbs b/assets/javascripts/discourse/templates/admin-wizards-logs.hbs index f98623aa..18fd3fdb 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-logs.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-logs.hbs @@ -31,4 +31,4 @@ {{/if}} {{conditional-loading-spinner condition=refreshing}} -{{/load-more}} \ No newline at end of file +{{/load-more}} diff --git a/assets/javascripts/discourse/templates/admin-wizards-manager.hbs b/assets/javascripts/discourse/templates/admin-wizards-manager.hbs index cc26d808..9ee2f080 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-manager.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-manager.hbs @@ -80,4 +80,4 @@ {{/each}} - \ No newline at end of file + diff --git a/assets/javascripts/discourse/templates/components/custom-field-input.hbs b/assets/javascripts/discourse/templates/components/custom-field-input.hbs index 7e6598a0..205b1644 100644 --- a/assets/javascripts/discourse/templates/components/custom-field-input.hbs +++ b/assets/javascripts/discourse/templates/components/custom-field-input.hbs @@ -60,4 +60,4 @@ {{d-button action="edit" icon="pencil-alt"}} -{{/if}} \ No newline at end of file +{{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-advanced-toggle.hbs b/assets/javascripts/discourse/templates/components/wizard-advanced-toggle.hbs index 9f8563fc..ec2bcb76 100644 --- a/assets/javascripts/discourse/templates/components/wizard-advanced-toggle.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-advanced-toggle.hbs @@ -1,4 +1,4 @@ {{d-button action="toggleAdvanced" label="admin.wizard.advanced" - class=toggleClass}} \ No newline at end of file + class=toggleClass}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs index de97d44b..2f0b20ba 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs @@ -75,7 +75,7 @@
- +
{{input type="number" @@ -118,7 +118,7 @@
- +
{{input value=field.file_types class="medium"}}
@@ -130,7 +130,7 @@
- +
{{input type="number" value=field.limit class="small"}}
@@ -142,7 +142,7 @@
- +
{{input value=field.format class="medium"}} @@ -155,7 +155,7 @@
- +
{{wizard-mapper inputs=field.prefill @@ -171,7 +171,7 @@
- +
{{wizard-mapper inputs=field.content @@ -187,7 +187,31 @@ {{#if field.showAdvanced}}
- + +
+
+ +
+ +
+ {{wizard-mapper + inputs=field.condition + options=fieldConditionOptions}} +
+
+ +
+
+ +
+ +
+ {{wizard-mapper + inputs=field.index + options=fieldIndexOptions}} +
+
+ {{#if isCategory}}
@@ -205,7 +229,7 @@
{{/if}} - +
@@ -218,7 +242,7 @@ placeholderKey="admin.wizard.translation_placeholder"}}
- + {{#if validations}} {{wizard-realtime-validations field=field validations=validations}} {{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs index e468c9ab..85adfe8a 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs @@ -37,7 +37,28 @@ {{#if step.showAdvanced}}
- + +
+
+ +
+ +
+ {{wizard-mapper + inputs=step.condition + options=stepConditionOptions}} +
+
+ +
+
+
+

{{i18n "admin.wizard.step.force_final.label"}}

+ {{input type="checkbox" checked=step.force_final}} + {{i18n "admin.wizard.step.force_final.description"}} +
+
+
@@ -80,7 +101,7 @@ )}}
- +
@@ -92,7 +113,6 @@ placeholderKey="admin.wizard.translation_placeholder"}}
-
{{/if}} @@ -105,8 +125,9 @@ {{#each step.fields as |field|}} {{wizard-custom-field field=field + step=step currentFieldId=currentField.id fieldTypes=fieldTypes removeField="removeField" wizardFields=wizardFields}} -{{/each}} \ No newline at end of file +{{/each}} diff --git a/assets/javascripts/discourse/templates/components/wizard-mapper-connector.hbs b/assets/javascripts/discourse/templates/components/wizard-mapper-connector.hbs index f0689f9e..7b610e2b 100644 --- a/assets/javascripts/discourse/templates/components/wizard-mapper-connector.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-mapper-connector.hbs @@ -9,4 +9,4 @@ {{connectorLabel}} {{/if}} -{{/if}} \ No newline at end of file +{{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-mapper-pair.hbs b/assets/javascripts/discourse/templates/components/wizard-mapper-pair.hbs index 272b0b58..ffb9eaf2 100644 --- a/assets/javascripts/discourse/templates/components/wizard-mapper-pair.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-mapper-pair.hbs @@ -32,4 +32,4 @@ {{#if showRemove}} {{d-icon "times"}} -{{/if}} \ No newline at end of file +{{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-mapper-selector-type.hbs b/assets/javascripts/discourse/templates/components/wizard-mapper-selector-type.hbs index 2ef7f2a3..32c4c26e 100644 --- a/assets/javascripts/discourse/templates/components/wizard-mapper-selector-type.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-mapper-selector-type.hbs @@ -1 +1 @@ -{{item.label}} \ No newline at end of file +{{item.label}} diff --git a/assets/javascripts/discourse/templates/components/wizard-message.hbs b/assets/javascripts/discourse/templates/components/wizard-message.hbs index 4c48002a..380fc5b3 100644 --- a/assets/javascripts/discourse/templates/components/wizard-message.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-message.hbs @@ -23,4 +23,4 @@ {{documentation}}
-{{/if}} \ No newline at end of file +{{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-text-editor.hbs b/assets/javascripts/discourse/templates/components/wizard-text-editor.hbs index a37f37a3..c657049d 100644 --- a/assets/javascripts/discourse/templates/components/wizard-text-editor.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-text-editor.hbs @@ -38,4 +38,4 @@
{{/if}} {{/if}} -
\ No newline at end of file +
diff --git a/assets/javascripts/wizard/controllers/custom-step.js.es6 b/assets/javascripts/wizard/controllers/custom-step.js.es6 index bf415bc9..b44c0fca 100644 --- a/assets/javascripts/wizard/controllers/custom-step.js.es6 +++ b/assets/javascripts/wizard/controllers/custom-step.js.es6 @@ -4,14 +4,15 @@ import getUrl from "discourse-common/lib/get-url"; export default StepController.extend({ actions: { goNext(response) { - const next = this.get("step.next"); + let nextStepId = response["next_step_id"]; + if (response.redirect_on_next) { window.location.href = response.redirect_on_next; } else if (response.refresh_required) { - const id = this.get("wizard.id"); - window.location.href = getUrl(`/w/${id}/steps/${next}`); + const wizardId = this.get("wizard.id"); + window.location.href = getUrl(`/w/${wizardId}/steps/${nextStepId}`); } else { - this.transitionToRoute("custom.step", next); + this.transitionToRoute("custom.step", nextStepId); } }, diff --git a/assets/javascripts/wizard/initializers/custom-wizard-step.js.es6 b/assets/javascripts/wizard/initializers/custom-wizard-step.js.es6 index 18fa9c70..fbbe7d8b 100644 --- a/assets/javascripts/wizard/initializers/custom-wizard-step.js.es6 +++ b/assets/javascripts/wizard/initializers/custom-wizard-step.js.es6 @@ -8,6 +8,9 @@ export default { const CustomWizard = requirejs( "discourse/plugins/discourse-custom-wizard/wizard/models/custom" ).default; + const updateCachedWizard = requirejs( + "discourse/plugins/discourse-custom-wizard/wizard/models/custom" + ).updateCachedWizard; const StepModel = requirejs("wizard/models/step").default; const StepComponent = requirejs("wizard/components/wizard-step").default; const ajax = requirejs("wizard/lib/ajax").ajax; @@ -18,6 +21,7 @@ export default { "discourse/plugins/discourse-custom-wizard/wizard/lib/text-lite" ).cook; const { schedule } = requirejs("@ember/runloop"); + const { alias, not } = requirejs("@ember/object/computed"); StepModel.reopen({ save() { @@ -155,12 +159,17 @@ export default { this.sendAction("showMessage", message); }.observes("step.message"), + showNextButton: not("step.final"), + showDoneButton: alias("step.final"), + advance() { this.set("saving", true); this.get("step") .save() .then((response) => { - if (this.get("finalStep")) { + updateCachedWizard(CustomWizard.build(response["wizard"])); + + if (response["final"]) { CustomWizard.finished(response); } else { this.sendAction("goNext", response); @@ -178,7 +187,6 @@ export default { }, done() { - this.set("finalStep", true); this.send("nextStep"); }, diff --git a/assets/javascripts/wizard/models/custom.js.es6 b/assets/javascripts/wizard/models/custom.js.es6 index 4e214eed..31a403da 100644 --- a/assets/javascripts/wizard/models/custom.js.es6 +++ b/assets/javascripts/wizard/models/custom.js.es6 @@ -31,63 +31,45 @@ CustomWizard.reopenClass({ } window.location.href = getUrl(url); }, -}); -export function findCustomWizard(wizardId, params = {}) { - let url = `/w/${wizardId}`; - - let paramKeys = Object.keys(params).filter((k) => { - if (k === "wizard_id") { - return false; - } - return !!params[k]; - }); - - if (paramKeys.length) { - url += "?"; - paramKeys.forEach((k, i) => { - if (i > 0) { - url += "&"; - } - url += `${k}=${params[k]}`; - }); - } - - return ajax({ url, cache: false, dataType: "json" }).then((result) => { - const wizard = result; - if (!wizard) { + build(wizardJson) { + if (!wizardJson) { return null; } - if (!wizard.completed) { - wizard.steps = wizard.steps.map((step) => { - const stepObj = Step.create(step); + if (!wizardJson.completed && wizardJson.steps) { + wizardJson.steps = wizardJson.steps + .map((step) => { + const stepObj = Step.create(step); - stepObj.fields.sort((a, b) => { - return parseFloat(a.number) - parseFloat(b.number); + stepObj.fields.sort((a, b) => { + return parseFloat(a.number) - parseFloat(b.number); + }); + + let tabindex = 1; + stepObj.fields.forEach((f) => { + f.tabindex = tabindex; + + if (["date_time"].includes(f.type)) { + tabindex = tabindex + 2; + } else { + tabindex++; + } + }); + + stepObj.fields = stepObj.fields.map((f) => WizardField.create(f)); + + return stepObj; + }) + .sort((a, b) => { + return parseFloat(a.index) - parseFloat(b.index); }); - - let tabindex = 1; - stepObj.fields.forEach((f) => { - f.tabindex = tabindex; - - if (["date_time"].includes(f.type)) { - tabindex = tabindex + 2; - } else { - tabindex++; - } - }); - - stepObj.fields = stepObj.fields.map((f) => WizardField.create(f)); - - return stepObj; - }); } - if (wizard.categories) { + if (wizardJson.categories) { let subcatMap = {}; let categoriesById = {}; - let categories = wizard.categories.map((c) => { + let categories = wizardJson.categories.map((c) => { if (c.parent_category_id) { subcatMap[c.parent_category_id] = subcatMap[c.parent_category_id] || []; @@ -116,12 +98,47 @@ export function findCustomWizard(wizardId, params = {}) { Discourse.Site.currentProp("categoriesById", categoriesById); Discourse.Site.currentProp( "uncategorized_category_id", - wizard.uncategorized_category_id + wizardJson.uncategorized_category_id ); } - return CustomWizard.create(wizard); + return CustomWizard.create(wizardJson); + }, +}); + +export function findCustomWizard(wizardId, params = {}) { + let url = `/w/${wizardId}`; + + let paramKeys = Object.keys(params).filter((k) => { + if (k === "wizard_id") { + return false; + } + return !!params[k]; + }); + + if (paramKeys.length) { + url += "?"; + paramKeys.forEach((k, i) => { + if (i > 0) { + url += "&"; + } + url += `${k}=${params[k]}`; + }); + } + + return ajax({ url, cache: false, dataType: "json" }).then((result) => { + return CustomWizard.build(result); }); } +let _wizard_store; + +export function updateCachedWizard(wizard) { + _wizard_store = wizard; +} + +export function getCachedWizard() { + return _wizard_store; +} + export default CustomWizard; diff --git a/assets/javascripts/wizard/routes/custom-index.js.es6 b/assets/javascripts/wizard/routes/custom-index.js.es6 index 05f2f2bf..a8abc152 100644 --- a/assets/javascripts/wizard/routes/custom-index.js.es6 +++ b/assets/javascripts/wizard/routes/custom-index.js.es6 @@ -1,22 +1,19 @@ +import { getCachedWizard } from "../models/custom"; + export default Ember.Route.extend({ beforeModel() { - const appModel = this.modelFor("custom"); - if ( - appModel && - appModel.permitted && - !appModel.completed && - appModel.start - ) { - this.replaceWith("custom.step", appModel.start); + const wizard = getCachedWizard(); + if (wizard && wizard.permitted && !wizard.completed && wizard.start) { + this.replaceWith("custom.step", wizard.start); } }, model() { - return this.modelFor("custom"); + return getCachedWizard(); }, setupController(controller, model) { - if (model) { + if (model && model.id) { const completed = model.get("completed"); const permitted = model.get("permitted"); const wizardId = model.get("id"); diff --git a/assets/javascripts/wizard/routes/custom-step.js.es6 b/assets/javascripts/wizard/routes/custom-step.js.es6 index 3cb5db6e..8088727a 100644 --- a/assets/javascripts/wizard/routes/custom-step.js.es6 +++ b/assets/javascripts/wizard/routes/custom-step.js.es6 @@ -1,28 +1,33 @@ import WizardI18n from "../lib/wizard-i18n"; +import { getCachedWizard } from "../models/custom"; 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]; - } + beforeModel() { + this.set("wizard", getCachedWizard()); + }, - return appModel; + model(params) { + const wizard = this.wizard; + + if (wizard && wizard.steps) { + const step = wizard.steps.findBy("id", params.step_id); + return step ? step : wizard.steps[0]; + } else { + return wizard; + } }, afterModel(model) { if (model.completed) { return this.transitionTo("index"); } - return model.set("wizardId", this.modelFor("custom").id); + return model.set("wizardId", this.wizard.id); }, setupController(controller, model) { let props = { step: model, - wizard: this.modelFor("custom"), + wizard: this.wizard, }; if (!model.permitted) { diff --git a/assets/javascripts/wizard/routes/custom.js.es6 b/assets/javascripts/wizard/routes/custom.js.es6 index 041ea967..367fa36b 100644 --- a/assets/javascripts/wizard/routes/custom.js.es6 +++ b/assets/javascripts/wizard/routes/custom.js.es6 @@ -1,6 +1,6 @@ /* eslint no-undef: 0*/ -import { findCustomWizard } from "../models/custom"; +import { findCustomWizard, updateCachedWizard } from "../models/custom"; import { ajax } from "wizard/lib/ajax"; export default Ember.Route.extend({ @@ -12,7 +12,9 @@ export default Ember.Route.extend({ return findCustomWizard(params.wizard_id, this.get("queryParams")); }, - afterModel() { + afterModel(model) { + updateCachedWizard(model); + return ajax({ url: `/site/settings`, type: "GET", @@ -25,11 +27,11 @@ export default Ember.Route.extend({ const background = model ? model.get("background") : "AliceBlue"; Ember.run.scheduleOnce("afterRender", this, function () { $("body.custom-wizard").css("background", background); - if (model) { - $("#custom-wizard-main").addClass(model.get("id").dasherize()); + + if (model && model.id) { + $("#custom-wizard-main").addClass(model.id.dasherize()); } }); - controller.setProperties({ customWizard: true, logoUrl: Wizard.SiteSettings.logo_small, diff --git a/assets/javascripts/wizard/templates/components/wizard-field-checkbox.hbs b/assets/javascripts/wizard/templates/components/wizard-field-checkbox.hbs index 8cbc2b3e..053e0218 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-checkbox.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-checkbox.hbs @@ -1 +1 @@ -{{input type="checkbox" id=field.id checked=field.value tabindex=field.tabindex}} \ No newline at end of file +{{input type="checkbox" id=field.id checked=field.value tabindex=field.tabindex}} diff --git a/assets/javascripts/wizard/templates/components/wizard-field-date-time.hbs b/assets/javascripts/wizard/templates/components/wizard-field-date-time.hbs index 0b4f7916..dae4523d 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-date-time.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-date-time.hbs @@ -2,4 +2,4 @@ date=dateTime onChange=(action "onChange") tabindex=field.tabindex -}} \ No newline at end of file +}} diff --git a/assets/javascripts/wizard/templates/components/wizard-field-date.hbs b/assets/javascripts/wizard/templates/components/wizard-field-date.hbs index 7b914807..4ac6571b 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-date.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-date.hbs @@ -2,4 +2,4 @@ date=date onChange=(action "onChange") tabindex=field.tabindex -}} \ No newline at end of file +}} diff --git a/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs b/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs index fbb68721..7ce4c298 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-dropdown.hbs @@ -5,4 +5,4 @@ tabindex=field.tabindex options=(hash none="select_kit.default_header_text" - )}} \ No newline at end of file + )}} diff --git a/assets/javascripts/wizard/templates/components/wizard-field-group.hbs b/assets/javascripts/wizard/templates/components/wizard-field-group.hbs index 0c3b102d..92c08e2b 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-group.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-group.hbs @@ -7,4 +7,4 @@ onChange=(action (mut field.value)) options=(hash none="group.select" - )}} \ No newline at end of file + )}} diff --git a/assets/javascripts/wizard/templates/components/wizard-field-number.hbs b/assets/javascripts/wizard/templates/components/wizard-field-number.hbs index 8704dac0..f5d6543c 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-number.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-number.hbs @@ -1 +1 @@ -{{input type="number" step="0.01" id=field.id value=field.value tabindex=field.tabindex}} \ No newline at end of file +{{input type="number" step="0.01" id=field.id value=field.value tabindex=field.tabindex}} diff --git a/assets/javascripts/wizard/templates/components/wizard-field-time.hbs b/assets/javascripts/wizard/templates/components/wizard-field-time.hbs index dafa9e59..d4cc425a 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-time.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-time.hbs @@ -2,4 +2,4 @@ date=time onChange=(action "onChange") tabindex=field.tabindex -}} \ No newline at end of file +}} diff --git a/assets/javascripts/wizard/templates/components/wizard-field-url.hbs b/assets/javascripts/wizard/templates/components/wizard-field-url.hbs index e9015010..c7e1a508 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-url.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-url.hbs @@ -1 +1 @@ -{{input type="text" id=field.id value=field.value tabindex=field.tabindex}} \ No newline at end of file +{{input type="text" id=field.id value=field.value tabindex=field.tabindex}} diff --git a/assets/javascripts/wizard/templates/components/wizard-similar-topics.hbs b/assets/javascripts/wizard/templates/components/wizard-similar-topics.hbs index e3163517..d1ee1fa1 100644 --- a/assets/javascripts/wizard/templates/components/wizard-similar-topics.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-similar-topics.hbs @@ -8,4 +8,4 @@ {{wizard-i18n "realtime_validations.similar_topics.show"}} -{{/if}} \ No newline at end of file +{{/if}} diff --git a/assets/javascripts/wizard/templates/components/wizard-time-input.hbs b/assets/javascripts/wizard/templates/components/wizard-time-input.hbs index 6207851b..73fa4968 100644 --- a/assets/javascripts/wizard/templates/components/wizard-time-input.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-time-input.hbs @@ -10,4 +10,4 @@ autoInsertNoneItem=false translatedFilterPlaceholder="--:--" ) -}} \ No newline at end of file +}} diff --git a/assets/stylesheets/common/wizard-admin.scss b/assets/stylesheets/common/wizard-admin.scss index d7d00795..3c4b78da 100644 --- a/assets/stylesheets/common/wizard-admin.scss +++ b/assets/stylesheets/common/wizard-admin.scss @@ -303,6 +303,16 @@ max-width: 250px !important; min-width: 250px !important; } + + &.force-final { + padding: 1em; + background-color: var(--primary-very-low); + + label, + span { + font-size: 1em; + } + } } &.full, diff --git a/assets/stylesheets/wizard/custom/base.scss b/assets/stylesheets/wizard/custom/base.scss index 7aa884e9..39ab061a 100644 --- a/assets/stylesheets/wizard/custom/base.scss +++ b/assets/stylesheets/wizard/custom/base.scss @@ -50,6 +50,11 @@ textarea { border-color: var(--danger); box-shadow: shadow("focus-danger"); } + + &[type="checkbox"] { + margin-bottom: 0; + margin-right: 10px; + } } .spinner { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 1ebe3a83..0c364853 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -56,6 +56,8 @@ en: undo: "Undo" clear: "Clear" select_type: "Select a type" + condition: "Condition" + index: "Index" message: wizard: @@ -153,6 +155,9 @@ en: not_permitted_message: "Message shown when required data not present" permitted_params: label: "Params" + force_final: + label: "Conditional Final Step" + description: "Display this step as the final step if conditions on later steps have not passed when the user reaches this step." field: header: "Fields" diff --git a/controllers/custom_wizard/admin/manager.rb b/controllers/custom_wizard/admin/manager.rb index 1596c233..2277de48 100644 --- a/controllers/custom_wizard/admin/manager.rb +++ b/controllers/custom_wizard/admin/manager.rb @@ -41,7 +41,7 @@ class CustomWizard::AdminManagerController < CustomWizard::AdminController end begin - template_json = JSON.parse file + template_json = JSON.parse(file) rescue JSON::ParserError return render_error(I18n.t('wizard.import.error.invalid_json')) end diff --git a/controllers/custom_wizard/admin/wizard.rb b/controllers/custom_wizard/admin/wizard.rb index 658f6682..0af55d95 100644 --- a/controllers/custom_wizard/admin/wizard.rb +++ b/controllers/custom_wizard/admin/wizard.rb @@ -37,7 +37,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController wizard_id = template.save(create: params[:create]) if template.errors.any? - render json: failed_json.merge(errors: result.errors.full_messages) + render json: failed_json.merge(errors: template.errors.full_messages) else render json: success_json.merge(wizard_id: wizard_id) end @@ -83,15 +83,19 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController permitted: mapped_params, steps: [ :id, + :index, :title, :key, :banner, :raw_description, :required_data_message, + :force_final, required_data: mapped_params, permitted_params: mapped_params, + condition: mapped_params, fields: [ :id, + :index, :label, :image, :description, @@ -107,6 +111,8 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController :property, prefill: mapped_params, content: mapped_params, + condition: mapped_params, + index: mapped_params, validations: {}, ] ], diff --git a/controllers/custom_wizard/steps.rb b/controllers/custom_wizard/steps.rb index 9c6eec47..277b94b2 100644 --- a/controllers/custom_wizard/steps.rb +++ b/controllers/custom_wizard/steps.rb @@ -4,31 +4,67 @@ class CustomWizard::StepsController < ::ApplicationController before_action :ensure_can_update def update - params.require(:step_id) - params.require(:wizard_id) - - wizard = @builder.build - step = wizard.steps.select { |s| s.id == update_params[:step_id] }.first - - raise Discourse::InvalidParameters.new(:step_id) if !step - update = update_params.to_h update[:fields] = {} if params[:fields] - field_ids = step.fields.map(&:id) + field_ids = @step_template['fields'].map { |f| f['id'] } params[:fields].each do |k, v| update[:fields][k] = v if field_ids.include? k end end - updater = wizard.create_updater(update[:step_id], update[:fields]) + @builder.build + + updater = @builder.wizard.create_updater(update[:step_id], update[:fields]) updater.update + @result = updater.result if updater.success? - result = success_json - result.merge!(updater.result) if updater.result + wizard_id = update_params[:wizard_id] + builder = CustomWizard::Builder.new(wizard_id, current_user) + @wizard = builder.build + + current_step = @wizard.find_step(update[:step_id]) + current_submission = @wizard.current_submission + result = {} + + if current_step.conditional_final_step && !current_step.last_step + current_step.force_final = true + end + + if current_step.final? + builder.template.actions.each do |action_template| + if action_template['run_after'] === 'wizard_completion' + CustomWizard::Action.new( + action: action_template, + wizard: @wizard, + data: current_submission + ).perform + end + end + + @wizard.save_submission(current_submission) + + if redirect = get_redirect + updater.result[:redirect_on_complete] = redirect + end + + @wizard.final_cleanup! + + result[:final] = true + else + result[:final] = false + result[:next_step_id] = current_step.next.id + end + + result.merge!(updater.result) if updater.result.present? result[:refresh_required] = true if updater.refresh_required? + result[:wizard] = ::CustomWizard::WizardSerializer.new( + @wizard, + scope: Guardian.new(current_user), + root: false + ).as_json render json: result else @@ -43,21 +79,31 @@ class CustomWizard::StepsController < ::ApplicationController private def ensure_can_update - @builder = CustomWizard::Builder.new( - update_params[:wizard_id].underscore, - current_user - ) + @builder = CustomWizard::Builder.new(update_params[:wizard_id], current_user) + raise Discourse::InvalidParameters.new(:wizard_id) if @builder.template.nil? + raise Discourse::InvalidAccess.new if !@builder.wizard || !@builder.wizard.can_access? - if @builder.nil? - raise Discourse::InvalidParameters.new(:wizard_id) - end - - if !@builder.wizard || !@builder.wizard.can_access? - raise Discourse::InvalidAccess.new - end + @step_template = @builder.template.steps.select do |s| + s['id'] == update_params[:step_id] + end.first + raise Discourse::InvalidParameters.new(:step_id) if !@step_template + raise Discourse::InvalidAccess.new if !@builder.check_condition(@step_template) end def update_params - params.permit(:wizard_id, :step_id) + @update_params || begin + params.require(:step_id) + params.require(:wizard_id) + params.permit(:wizard_id, :step_id).transform_values { |v| v.underscore } + end + end + + def get_redirect + return @result[:redirect_on_next] if @result[:redirect_on_next].present? + + current_submission = @wizard.current_submission + return nil unless current_submission.present? + ## route_to set by actions, redirect_on_complete set by actions, redirect_to set at wizard entry + current_submission[:route_to] || current_submission[:redirect_on_complete] || current_submission[:redirect_to] end end diff --git a/controllers/custom_wizard/wizard.rb b/controllers/custom_wizard/wizard.rb index 5cbeb6e3..73c1f592 100644 --- a/controllers/custom_wizard/wizard.rb +++ b/controllers/custom_wizard/wizard.rb @@ -65,10 +65,7 @@ class CustomWizard::WizardController < ::ApplicationController result.merge!(redirect_to: submission['redirect_to']) end - if user.custom_fields['redirect_to_wizard'] === wizard.id - user.custom_fields.delete('redirect_to_wizard') - user.save_custom_fields(true) - end + wizard.final_cleanup! end render json: result diff --git a/coverage/.last_run.json b/coverage/.last_run.json index 42d46659..3e7f27f6 100644 --- a/coverage/.last_run.json +++ b/coverage/.last_run.json @@ -1,5 +1,5 @@ { "result": { - "line": 89.56 + "line": 90.52 } } diff --git a/extensions/wizard_field.rb b/extensions/wizard_field.rb deleted file mode 100644 index 2042872f..00000000 --- a/extensions/wizard_field.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true -module CustomWizardFieldExtension - attr_reader :raw, - :label, - :description, - :image, - :key, - :validations, - :min_length, - :max_length, - :char_counter, - :file_types, - :format, - :limit, - :property, - :content, - :number - - def initialize(attrs) - super - @raw = attrs || {} - @description = attrs[:description] - @image = attrs[:image] - @key = attrs[:key] - @validations = attrs[:validations] - @min_length = attrs[:min_length] - @max_length = attrs[:max_length] - @char_counter = attrs[:char_counter] - @file_types = attrs[:file_types] - @format = attrs[:format] - @limit = attrs[:limit] - @property = attrs[:property] - @content = attrs[:content] - @number = attrs[:number] - end - - def label - @label ||= PrettyText.cook(@raw[:label]) - end -end diff --git a/extensions/wizard_step.rb b/extensions/wizard_step.rb deleted file mode 100644 index 4ae0224c..00000000 --- a/extensions/wizard_step.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true -module CustomWizardStepExtension - attr_accessor :title, :description, :key, :permitted, :permitted_message -end diff --git a/lib/custom_wizard/action.rb b/lib/custom_wizard/action.rb index 8f09d46a..fc436f3b 100644 --- a/lib/custom_wizard/action.rb +++ b/lib/custom_wizard/action.rb @@ -6,12 +6,12 @@ class CustomWizard::Action :guardian, :result - def initialize(params) - @wizard = params[:wizard] - @action = params[:action] - @user = params[:user] + def initialize(opts) + @wizard = opts[:wizard] + @action = opts[:action] + @user = @wizard.user @guardian = Guardian.new(@user) - @data = params[:data] + @data = opts[:data] @log = [] @result = CustomWizard::ActionResult.new end diff --git a/lib/custom_wizard/builder.rb b/lib/custom_wizard/builder.rb index 9a2d1060..daa73476 100644 --- a/lib/custom_wizard/builder.rb +++ b/lib/custom_wizard/builder.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true class CustomWizard::Builder - attr_accessor :wizard, :updater, :submissions + attr_accessor :wizard, :updater, :template def initialize(wizard_id, user = nil) - template = CustomWizard::Template.find(wizard_id) - return nil if template.blank? - - @wizard = CustomWizard::Wizard.new(template, user) - @steps = template['steps'] || [] - @actions = template['actions'] || [] - @submissions = @wizard.submissions + @template = CustomWizard::Template.create(wizard_id) + return nil if @template.nil? + @wizard = CustomWizard::Wizard.new(template.data, user) end def self.sorted_handlers @@ -28,7 +24,7 @@ class CustomWizard::Builder def mapper CustomWizard::Mapper.new( user: @wizard.user, - data: @submissions.last + data: @wizard.current_submission ) end @@ -38,103 +34,38 @@ class CustomWizard::Builder build_opts[:reset] = build_opts[:reset] || @wizard.restart_on_revisit - @steps.each do |step_template| + @template.steps.each do |step_template| + next if !check_condition(step_template) + @wizard.append_step(step_template['id']) do |step| - step.permitted = true + step = check_if_permitted(step, step_template) + next if !step.permitted - if step_template['required_data'] - step = ensure_required_data(step, step_template) - end - - if !step.permitted - if step_template['required_data_message'] - step.permitted_message = step_template['required_data_message'] - end - next - end - - step.title = step_template['title'] if step_template['title'] - step.banner = step_template['banner'] if step_template['banner'] - step.key = step_template['key'] if step_template['key'] - - if step_template['description'] - step.description = mapper.interpolate( - step_template['description'], - user: true, - value: true - ) - end - - if permitted_params = step_template['permitted_params'] - save_permitted_params(permitted_params, params) - end - - if step_template['fields'] && step_template['fields'].length - step_template['fields'].each_with_index do |field_template, index| - append_field(step, step_template, field_template, build_opts, index) - end - end + save_permitted_params(step_template, params) + step = add_step_attributes(step, step_template) + step = append_step_fields(step, step_template, build_opts) step.on_update do |updater| @updater = updater - user = @wizard.user + @submission = (@wizard.current_submission || {}) + .merge(@updater.submission) + .with_indifferent_access - updater.validate + @updater.validate + next if @updater.errors.any? - next if updater.errors.any? + apply_step_handlers + next if @updater.errors.any? - CustomWizard::Builder.step_handlers.each do |handler| - if handler[:wizard_id] == @wizard.id - handler[:block].call(self) - end - end + run_step_actions - next if updater.errors.any? - - submission = updater.submission - - if current_submission = @wizard.current_submission - submission = current_submission.merge(submission) - end - - final_step = updater.step.next.nil? - - 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( - wizard: @wizard, - action: action, - user: user, - data: submission - ).perform - end - end - end - - if updater.errors.empty? - if route_to = submission['route_to'] - submission.delete('route_to') + if @updater.errors.empty? + if route_to = @submission['route_to'] + @submission.delete('route_to') end - if @wizard.save_submissions - save_submissions(submission, final_step) - end - - if final_step - if @wizard.id == @wizard.user.custom_fields['redirect_to_wizard'] - @wizard.user.custom_fields.delete('redirect_to_wizard') - @wizard.user.save_custom_fields(true) - end - - redirect_url = route_to || submission['redirect_on_complete'] || submission["redirect_to"] - updater.result[:redirect_on_complete] = redirect_url - elsif route_to - updater.result[:redirect_on_next] = route_to - end + @wizard.save_submission(@submission) + @updater.result[:redirect_on_next] = route_to if route_to true else @@ -144,25 +75,21 @@ class CustomWizard::Builder end end + @wizard.update_step_order! @wizard end - def append_field(step, step_template, field_template, build_opts, index) + def append_field(step, step_template, field_template, build_opts) params = { id: field_template['id'], type: field_template['type'], - required: field_template['required'], - number: index + 1 + required: field_template['required'] } - params[:label] = field_template['label'] if field_template['label'] - params[:description] = field_template['description'] if field_template['description'] - params[:image] = field_template['image'] if field_template['image'] - params[:key] = field_template['key'] if field_template['key'] - params[:validations] = field_template['validations'] if field_template['validations'] - params[:min_length] = field_template['min_length'] if field_template['min_length'] - params[:max_length] = field_template['max_length'] if field_template['max_length'] - params[:char_counter] = field_template['char_counter'] if field_template['char_counter'] + %w(label description image key validations min_length max_length char_counter).each do |key| + params[key.to_sym] = field_template[key] if field_template[key] + end + params[:value] = prefill_field(field_template, step_template) if !build_opts[:reset] && (submission = @wizard.current_submission) @@ -209,7 +136,7 @@ class CustomWizard::Builder content = CustomWizard::Mapper.new( inputs: content_inputs, user: @wizard.user, - data: @submissions.last, + data: @wizard.current_submission, opts: { with_type: true } @@ -240,6 +167,16 @@ class CustomWizard::Builder end end + if field_template['index'].present? + index = CustomWizard::Mapper.new( + inputs: field_template['index'], + user: @wizard.user, + data: @wizard.current_submission + ).perform + + params[:index] = index.to_i unless index.nil? + end + field = step.add_field(params) end @@ -248,41 +185,91 @@ class CustomWizard::Builder CustomWizard::Mapper.new( inputs: prefill, user: @wizard.user, - data: @submissions.last + data: @wizard.current_submission ).perform end end + def check_condition(template) + if template['condition'].present? + CustomWizard::Mapper.new( + inputs: template['condition'], + user: @wizard.user, + data: @wizard.current_submission + ).perform + else + true + end + end + + def check_if_permitted(step, step_template) + step.permitted = true + + if step_template['required_data'] + step = ensure_required_data(step, step_template) + end + + if !step.permitted + if step_template['required_data_message'] + step.permitted_message = step_template['required_data_message'] + end + end + + step + end + + def add_step_attributes(step, step_template) + %w(index title banner key force_final).each do |attr| + step.send("#{attr}=", step_template[attr]) if step_template[attr] + end + + if step_template['description'] + step.description = mapper.interpolate( + step_template['description'], + user: true, + value: true + ) + end + + step + end + + def append_step_fields(step, step_template, build_opts) + if step_template['fields'] && step_template['fields'].length + step_template['fields'].each do |field_template| + next if !check_condition(field_template) + append_field(step, step_template, field_template, build_opts) + end + end + + step.update_field_order! + step + end + def standardise_boolean(value) ActiveRecord::Type::Boolean.new.cast(value) end - def save_submissions(submission, final_step) - if final_step - submission['submitted_at'] = Time.now.iso8601 - end + def save_permitted_params(step_template, params) + return unless step_template['permitted_params'].present? - if submission.present? - @submissions.pop(1) if @wizard.unfinished? - @submissions.push(submission) - @wizard.set_submissions(@submissions) - end - end - - def save_permitted_params(permitted_params, params) + permitted_params = step_template['permitted_params'] permitted_data = {} + submission_key = nil + params_key = nil + submission = @wizard.current_submission || {} permitted_params.each do |pp| pair = pp['pairs'].first params_key = pair['key'].to_sym submission_key = pair['value'].to_sym - permitted_data[submission_key] = params[params_key] if params[params_key] + + if submission_key && params_key + submission[submission_key] = params[params_key] + end end - if permitted_data.present? - current_data = @submissions.last || {} - save_submissions(current_data.merge(permitted_data), false) - end + @wizard.save_submission(submission) end def ensure_required_data(step, step_template) @@ -291,13 +278,13 @@ class CustomWizard::Builder pair['key'].present? && pair['value'].present? end - if pairs.any? && !@submissions.last + if pairs.any? && !@wizard.current_submission step.permitted = false break end pairs.each do |pair| - pair['key'] = @submissions.last[pair['key']] + pair['key'] = @wizard.current_submission[pair['key']] end if !mapper.validate_pairs(pairs) @@ -308,4 +295,26 @@ class CustomWizard::Builder step end + + def apply_step_handlers + CustomWizard::Builder.step_handlers.each do |handler| + if handler[:wizard_id] == @wizard.id + handler[:block].call(self) + end + end + end + + def run_step_actions + if @template.actions.present? + @template.actions.each do |action_template| + if action_template['run_after'] === updater.step.id + CustomWizard::Action.new( + action: action_template, + wizard: @wizard, + data: @submission + ).perform + end + end + end + end end diff --git a/lib/custom_wizard/field.rb b/lib/custom_wizard/field.rb index 1ddbcd3b..51e3d4de 100644 --- a/lib/custom_wizard/field.rb +++ b/lib/custom_wizard/field.rb @@ -1,5 +1,55 @@ # frozen_string_literal: true + class CustomWizard::Field + include ActiveModel::SerializerSupport + + attr_reader :raw, + :id, + :type, + :required, + :value, + :label, + :description, + :image, + :key, + :validations, + :min_length, + :max_length, + :char_counter, + :file_types, + :format, + :limit, + :property, + :content + + attr_accessor :index, + :step + + def initialize(attrs) + @raw = attrs || {} + @id = attrs[:id] + @index = attrs[:index] + @type = attrs[:type] + @required = !!attrs[:required] + @value = attrs[:value] + @description = attrs[:description] + @image = attrs[:image] + @key = attrs[:key] + @validations = attrs[:validations] + @min_length = attrs[:min_length] + @max_length = attrs[:max_length] + @char_counter = attrs[:char_counter] + @file_types = attrs[:file_types] + @format = attrs[:format] + @limit = attrs[:limit] + @property = attrs[:property] + @content = attrs[:content] + end + + def label + @label ||= PrettyText.cook(@raw[:label]) + end + def self.types @types ||= { text: { diff --git a/lib/custom_wizard/step.rb b/lib/custom_wizard/step.rb new file mode 100644 index 00000000..5ffd8024 --- /dev/null +++ b/lib/custom_wizard/step.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class CustomWizard::Step + include ActiveModel::SerializerSupport + + attr_reader :id, + :updater + + attr_accessor :index, + :title, + :description, + :key, + :permitted, + :permitted_message, + :fields, + :next, + :previous, + :banner, + :disabled, + :description_vars, + :last_step, + :force_final, + :conditional_final_step, + :wizard + + def initialize(id) + @id = id + @fields = [] + end + + def add_field(attrs) + field = ::CustomWizard::Field.new(attrs) + field.index = (@fields.size == 1 ? 0 : @fields.size) if field.index.nil? + field.step = self + @fields << field + field + end + + def has_fields? + @fields.present? + end + + def on_update(&block) + @updater = block + end + + def update_field_order! + @fields.sort_by!(&:index) + end + + def final? + return true if force_final && conditional_final_step + return true if last_step + false + end +end diff --git a/lib/custom_wizard/step_updater.rb b/lib/custom_wizard/step_updater.rb index 3a9d65f1..ab86f3fa 100644 --- a/lib/custom_wizard/step_updater.rb +++ b/lib/custom_wizard/step_updater.rb @@ -2,14 +2,15 @@ class CustomWizard::StepUpdater include ActiveModel::Model - attr_accessor :refresh_required, :submission, :result, :step + attr_accessor :refresh_required, :result + attr_reader :step, :submission def initialize(current_user, wizard, step, submission) @current_user = current_user @wizard = wizard @step = step @refresh_required = false - @submission = submission.to_h.with_indifferent_access + @submission = submission.with_indifferent_access @result = {} end diff --git a/lib/custom_wizard/template.rb b/lib/custom_wizard/template.rb index 91b97f73..b0f786c9 100644 --- a/lib/custom_wizard/template.rb +++ b/lib/custom_wizard/template.rb @@ -4,10 +4,14 @@ class CustomWizard::Template include HasErrors attr_reader :data, - :opts + :opts, + :steps, + :actions def initialize(data) @data = data + @steps = data['steps'] || [] + @actions = data['actions'] || [] end def save(opts = {}) @@ -31,6 +35,14 @@ class CustomWizard::Template new(data).save(opts) end + def self.create(wizard_id) + if data = find(wizard_id) + new(data) + else + nil + end + end + def self.find(wizard_id) PluginStore.get(CustomWizard::PLUGIN_NAME, wizard_id) end @@ -88,6 +100,12 @@ class CustomWizard::Template if step[:raw_description] step[:description] = PrettyText.cook(step[:raw_description]) end + + remove_non_mapped_index(step) + + step[:fields].each do |field| + remove_non_mapped_index(field) + end end end @@ -118,4 +136,10 @@ class CustomWizard::Template end end end + + def remove_non_mapped_index(object) + if !object[:index].is_a?(Array) + object.delete(:index) + end + end end diff --git a/lib/custom_wizard/wizard.rb b/lib/custom_wizard/wizard.rb index 3e12c31d..82693eed 100644 --- a/lib/custom_wizard/wizard.rb +++ b/lib/custom_wizard/wizard.rb @@ -26,9 +26,10 @@ class CustomWizard::Wizard :needs_groups, :steps, :step_ids, + :first_step, + :start, :actions, - :user, - :first_step + :user def initialize(attrs = {}, user = nil) @user = user @@ -68,8 +69,8 @@ class CustomWizard::Wizard val.nil? ? false : ActiveRecord::Type::Boolean.new.cast(val) end - def create_step(step_name) - ::Wizard::Step.new(step_name) + def create_step(step_id) + ::CustomWizard::Step.new(step_id) end def append_step(step) @@ -77,37 +78,58 @@ class CustomWizard::Wizard yield step if block_given? - last_step = steps.last steps << step + step.wizard = self + step.index = (steps.size == 1 ? 0 : steps.size) if step.index.nil? + end - if steps.size == 1 - @first_step = step - step.index = 0 - elsif last_step.present? - last_step.next = step - step.previous = last_step - step.index = last_step.index + 1 + def update_step_order! + steps.sort_by!(&:index) + + steps.each_with_index do |step, index| + if index === 0 + @first_step = step + @start = step.id + else + last_step = steps[index - 1] + last_step.next = step + step.previous = last_step + end + + step.index = index + + if index === (steps.length - 1) + step.conditional_final_step = true + end + + if index === (step_ids.length - 1) + step.last_step = true + end + + if step.previous && step.previous.id === last_completed_step_id + @start = step.id + end end end - def start - return nil if !user - - if unfinished? && last_completed_step = ::UserHistory.where( + def last_completed_step_id + if user && unfinished? && last_completed_step = ::UserHistory.where( acting_user_id: user.id, action: ::UserHistory.actions[:custom_wizard_step], context: id, - subject: steps.map(&:id) + subject: step_ids ).order("created_at").last - step_id = last_completed_step.subject - last_index = steps.index { |s| s.id == step_id } - steps[last_index + 1] + last_completed_step.subject else - @first_step + nil end end + def find_step(step_id) + steps.select { |step| step.id === step_id }.first + end + def create_updater(step_id, submission) step = @steps.find { |s| s.id == step_id } wizard = self @@ -200,12 +222,13 @@ class CustomWizard::Wizard end def submissions - Array.wrap(PluginStore.get("#{id}_submissions", user.id)) + return nil unless user.present? + @submissions ||= Array.wrap(PluginStore.get("#{id}_submissions", user.id)) end def current_submission - if submissions.present? && !submissions.last.key?("submitted_at") - submissions.last + if submissions.present? && submissions.last.present? && !submissions.last.key?("submitted_at") + submissions.last.with_indifferent_access else nil end @@ -213,6 +236,27 @@ class CustomWizard::Wizard def set_submissions(submissions) PluginStore.set("#{id}_submissions", user.id, Array.wrap(submissions)) + @submissions = nil + end + + def save_submission(submission) + return nil unless save_submissions + + submissions.pop(1) if unfinished? + submissions.push(submission) + set_submissions(submissions) + end + + def final_cleanup! + if id == user.custom_fields['redirect_to_wizard'] + user.custom_fields.delete('redirect_to_wizard') + user.save_custom_fields(true) + end + + if submission = current_submission + submission['submitted_at'] = Time.now.iso8601 + save_submission(submission) + end end def self.submissions(wizard_id, user) @@ -276,7 +320,7 @@ class CustomWizard::Wizard end def self.set_submission_redirect(user, wizard_id, url) - PluginStore.set("#{wizard_id.underscore}_submissions", user.id, [{ redirect_to: url }]) + set_submissions(wizard_id, user, [{ redirect_to: url }]) end def self.set_wizard_redirect(wizard_id, user) diff --git a/plugin.rb b/plugin.rb index 56673a13..e3d32129 100644 --- a/plugin.rb +++ b/plugin.rb @@ -66,6 +66,7 @@ after_initialize do ../lib/custom_wizard/mapper.rb ../lib/custom_wizard/log.rb ../lib/custom_wizard/step_updater.rb + ../lib/custom_wizard/step.rb ../lib/custom_wizard/template.rb ../lib/custom_wizard/wizard.rb ../lib/custom_wizard/api/api.rb @@ -89,8 +90,6 @@ after_initialize do ../extensions/extra_locales_controller.rb ../extensions/invites_controller.rb ../extensions/users_controller.rb - ../extensions/wizard_field.rb - ../extensions/wizard_step.rb ../extensions/custom_field/preloader.rb ../extensions/custom_field/serializer.rb ].each do |path| @@ -172,8 +171,6 @@ after_initialize do ::ExtraLocalesController.prepend ExtraLocalesControllerCustomWizard ::InvitesController.prepend InvitesControllerCustomWizard ::UsersController.prepend CustomWizardUsersController - ::Wizard::Field.prepend CustomWizardFieldExtension - ::Wizard::Step.prepend CustomWizardStepExtension full_path = "#{Rails.root}/plugins/discourse-custom-wizard/assets/stylesheets/wizard/wizard_custom.scss" if Stylesheet::Importer.respond_to?(:plugin_assets) diff --git a/serializers/custom_wizard/wizard_field_serializer.rb b/serializers/custom_wizard/wizard_field_serializer.rb index f9c42e6c..19025dff 100644 --- a/serializers/custom_wizard/wizard_field_serializer.rb +++ b/serializers/custom_wizard/wizard_field_serializer.rb @@ -1,8 +1,16 @@ # frozen_string_literal: true -class CustomWizard::FieldSerializer < ::WizardFieldSerializer +class CustomWizard::FieldSerializer < ::ApplicationSerializer - attributes :image, + attributes :id, + :index, + :type, + :required, + :value, + :label, + :placeholder, + :description, + :image, :file_types, :format, :limit, @@ -10,19 +18,54 @@ class CustomWizard::FieldSerializer < ::WizardFieldSerializer :content, :validations, :max_length, - :char_counter, - :number + :char_counter + + def id + object.id + end + + def index + object.index + end + + def type + object.type + end + + def required + object.required + end + + def value + object.value + end + + def include_value? + object.value.present? + end + + def i18n_key + @i18n_key ||= "wizard.step.#{object.step.id}.fields.#{object.id}".underscore + end def label return object.label if object.label.present? I18n.t("#{object.key || i18n_key}.label", default: '') end + def include_label? + label.present? + end + def description return object.description if object.description.present? I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url) end + def include_description? + description.present? + end + def image object.image end @@ -35,6 +78,10 @@ class CustomWizard::FieldSerializer < ::WizardFieldSerializer I18n.t("#{object.key || i18n_key}.placeholder", default: '') end + def include_placeholder? + placeholder.present? + end + def file_types object.file_types end @@ -55,10 +102,6 @@ class CustomWizard::FieldSerializer < ::WizardFieldSerializer object.content end - def include_choices? - object.choices.present? - end - def validations validations = {} object.validations&.each do |type, props| @@ -77,8 +120,4 @@ class CustomWizard::FieldSerializer < ::WizardFieldSerializer def char_counter object.char_counter end - - def number - object.number - end end diff --git a/serializers/custom_wizard/wizard_serializer.rb b/serializers/custom_wizard/wizard_serializer.rb index 9ab7291f..f858c195 100644 --- a/serializers/custom_wizard/wizard_serializer.rb +++ b/serializers/custom_wizard/wizard_serializer.rb @@ -30,7 +30,7 @@ class CustomWizard::WizardSerializer < CustomWizard::BasicWizardSerializer end def start - object.start.id + object.start end def include_start? diff --git a/serializers/custom_wizard/wizard_step_serializer.rb b/serializers/custom_wizard/wizard_step_serializer.rb index b5648e46..85f527bb 100644 --- a/serializers/custom_wizard/wizard_step_serializer.rb +++ b/serializers/custom_wizard/wizard_step_serializer.rb @@ -1,20 +1,74 @@ # frozen_string_literal: true -class CustomWizard::StepSerializer < ::WizardStepSerializer +class CustomWizard::StepSerializer < ::ApplicationSerializer + + attributes :id, + :index, + :next, + :previous, + :description, + :title, + :banner, + :permitted, + :permitted_message, + :final - attributes :permitted, :permitted_message has_many :fields, serializer: ::CustomWizard::FieldSerializer, embed: :objects + def id + object.id + end + + def index + object.index + end + + def next + object.next.id if object.next.present? + end + + def include_next? + object.next.present? + end + + def previous + object.previous.id if object.previous.present? + end + + def include_previous? + object.previous.present? + end + + def i18n_key + @i18n_key ||= "wizard.step.#{object.id}".underscore + end + def title return PrettyText.cook(object.title) if object.title PrettyText.cook(I18n.t("#{object.key || i18n_key}.title", default: '')) end + def include_title? + title.present? + end + def description return object.description if object.description PrettyText.cook(I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url)) end + def include_description? + description.present? + end + + def banner + object.banner + end + + def include_banner? + object.banner.present? + end + def permitted object.permitted end @@ -22,4 +76,8 @@ class CustomWizard::StepSerializer < ::WizardStepSerializer def permitted_message object.permitted_message end + + def final + object.final? + end end diff --git a/spec/components/custom_wizard/action_spec.rb b/spec/components/custom_wizard/action_spec.rb index 44cb6144..28f2cab8 100644 --- a/spec/components/custom_wizard/action_spec.rb +++ b/spec/components/custom_wizard/action_spec.rb @@ -6,19 +6,25 @@ describe CustomWizard::Action do fab!(:category) { Fabricate(:category, name: 'cat1', slug: 'cat-slug') } fab!(:group) { Fabricate(:group) } + let(:wizard_template) { + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read + ) + } + let(:open_composer) { - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/actions/open_composer.json" - ).read) + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/actions/open_composer.json" + ).read + ) } before do Group.refresh_automatic_group!(:trust_level_2) - CustomWizard::Template.save( - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" - ).read), - skip_jobs: true) + CustomWizard::Template.save(wizard_template, skip_jobs: true) @template = CustomWizard::Template.find('super_mega_fun_wizard') end @@ -156,7 +162,6 @@ describe CustomWizard::Action do action = CustomWizard::Action.new( wizard: wizard, action: open_composer, - user: user, data: {} ) action.perform @@ -179,8 +184,7 @@ describe CustomWizard::Action do it 'creates a group' do wizard = CustomWizard::Builder.new(@template[:id], user).build - step_id = wizard.steps[0].id - updater = wizard.create_updater(step_id, step_1_field_1: "Text input").update + wizard.create_updater(wizard.steps[0].id, step_1_field_1: "Text input").update expect(Group.where(name: wizard.current_submission['action_9']).exists?).to eq(true) end @@ -210,6 +214,6 @@ describe CustomWizard::Action do wizard = CustomWizard::Builder.new(@template[:id], user).build updater = wizard.create_updater(wizard.steps.last.id, {}) updater.update - expect(updater.result[:redirect_on_complete]).to eq("https://google.com") + expect(updater.result[:redirect_on_next]).to eq("https://google.com") end end diff --git a/spec/components/custom_wizard/builder_spec.rb b/spec/components/custom_wizard/builder_spec.rb index 54a6ceff..d9d3524e 100644 --- a/spec/components/custom_wizard/builder_spec.rb +++ b/spec/components/custom_wizard/builder_spec.rb @@ -16,21 +16,35 @@ describe CustomWizard::Builder do fab!(:group) { Fabricate(:group) } let(:required_data_json) { - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/required_data.json" - ).read) + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/required_data.json" + ).read + ) } let(:permitted_json) { - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" - ).read) + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" + ).read + ) } let(:permitted_param_json) { - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/permitted_params.json" - ).read) + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/permitted_params.json" + ).read + ) + } + + let(:user_condition_json) { + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/condition/user_condition.json" + ).read + ) } before do @@ -263,6 +277,23 @@ describe CustomWizard::Builder do expect(wizard.current_submission['saved_param']).to eq('param_value') end end + + context "with condition" do + before do + @template[:steps][0][:condition] = user_condition_json['condition'] + CustomWizard::Template.save(@template.as_json) + end + + it "adds step when condition is passed" do + wizard = CustomWizard::Builder.new(@template[:id], trusted_user).build + expect(wizard.steps.first.id).to eq(@template[:steps][0]['id']) + end + + it "does not add step when condition is not passed" do + wizard = CustomWizard::Builder.new(@template[:id], user).build + expect(wizard.steps.first.id).to eq(@template[:steps][1]['id']) + end + end end context 'building field' do @@ -284,6 +315,23 @@ describe CustomWizard::Builder do .fields.length ).to eq(4) end + + context "with condition" do + before do + @template[:steps][0][:fields][0][:condition] = user_condition_json['condition'] + CustomWizard::Template.save(@template.as_json) + end + + it "adds field when condition is passed" do + wizard = CustomWizard::Builder.new(@template[:id], trusted_user).build + expect(wizard.steps.first.fields.first.id).to eq(@template[:steps][0][:fields][0]['id']) + end + + it "does not add field when condition is not passed" do + wizard = CustomWizard::Builder.new(@template[:id], user).build + expect(wizard.steps.first.fields.first.id).to eq(@template[:steps][0][:fields][1]['id']) + end + end end context 'on update' do diff --git a/spec/components/custom_wizard/field_spec.rb b/spec/components/custom_wizard/field_spec.rb index f6c2d68b..871c42cd 100644 --- a/spec/components/custom_wizard/field_spec.rb +++ b/spec/components/custom_wizard/field_spec.rb @@ -2,6 +2,12 @@ require_relative '../../plugin_helper' describe CustomWizard::Field do + let(:field_hash) do + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/field/field.json" + ).read).with_indifferent_access + end + before do CustomWizard::Field.register( 'location', @@ -13,6 +19,19 @@ describe CustomWizard::Field do ) end + it "initialize custom field attributes" do + field = CustomWizard::Field.new(field_hash) + expect(field.id).to eq("field_id") + expect(field.index).to eq(0) + expect(field.label).to eq("

Field Label

") + expect(field.image).to eq("field_image_url.png") + expect(field.description).to eq("Field description") + expect(field.required).to eq(true) + expect(field.key).to eq("field.locale.key") + expect(field.type).to eq("field_type") + expect(field.content).to eq([]) + end + it "registers custom field types" do expect(CustomWizard::Field.types[:location].present?).to eq(true) end diff --git a/spec/components/custom_wizard/step_spec.rb b/spec/components/custom_wizard/step_spec.rb new file mode 100644 index 00000000..bf4613a4 --- /dev/null +++ b/spec/components/custom_wizard/step_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +require_relative '../../plugin_helper' + +describe CustomWizard::Step do + let(:step_hash) do + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/step.json" + ).read + ).with_indifferent_access + end + + let(:field_hash) do + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/field/field.json" + ).read + ).with_indifferent_access + end + + before do + @step = CustomWizard::Step.new(step_hash[:id]) + end + + it "adds fields" do + @step.add_field(field_hash) + expect(@step.fields.size).to eq(1) + expect(@step.fields.first.index).to eq(0) + end + + it "adds fields with custom indexes" do + field_hash[:index] = 2 + @step.add_field(field_hash) + expect(@step.fields.first.index).to eq(2) + end +end diff --git a/spec/components/custom_wizard/wizard_spec.rb b/spec/components/custom_wizard/wizard_spec.rb index 57d15241..aed44fe6 100644 --- a/spec/components/custom_wizard/wizard_spec.rb +++ b/spec/components/custom_wizard/wizard_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative '../../plugin_helper' describe CustomWizard::Wizard do @@ -7,27 +8,33 @@ describe CustomWizard::Wizard do fab!(:admin_user) { Fabricate(:user, admin: true) } let(:template_json) { - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" - ).read) + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read + ) } let(:permitted_json) { - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" - ).read) + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" + ).read + ) } before do Group.refresh_automatic_group!(:trust_level_3) - @permitted_template = template_json.dup @permitted_template["permitted"] = permitted_json["permitted"] - @wizard = CustomWizard::Wizard.new(template_json, user) + end + + def append_steps template_json['steps'].each do |step_template| @wizard.append_step(step_template['id']) end + @wizard.update_step_order! end def progress_step(step_id, acting_user: user, wizard: @wizard) @@ -37,16 +44,48 @@ describe CustomWizard::Wizard do context: wizard.id, subject: step_id ) + @wizard.update_step_order! end - it "appends steps from a template" do + it "appends steps" do + append_steps expect(@wizard.steps.length).to eq(3) end + it "appends steps with indexes" do + append_steps + expect(@wizard.steps.first.index).to eq(0) + expect(@wizard.steps.last.index).to eq(2) + end + + it "appends steps with custom indexes" do + template_json['steps'][0]['index'] = 2 + template_json['steps'][1]['index'] = 1 + template_json['steps'][2]['index'] = 0 + + template_json['steps'].each do |step_template| + @wizard.append_step(step_template['id']) do |step| + step.index = step_template['index'] if step_template['index'] + end + end + + expect(@wizard.steps.first.index).to eq(2) + expect(@wizard.steps.last.index).to eq(0) + + @wizard.update_step_order! + + expect(@wizard.steps.first.id).to eq("step_3") + expect(@wizard.steps.last.id).to eq("step_1") + + expect(@wizard.steps.first.next.id).to eq("step_2") + expect(@wizard.steps.last.next).to eq(nil) + end + it "determines the user's current step" do - expect(@wizard.start.id).to eq('step_1') + append_steps + expect(@wizard.start).to eq('step_1') progress_step('step_1') - expect(@wizard.start.id).to eq('step_2') + expect(@wizard.start).to eq('step_2') end it "creates a step updater" do @@ -57,6 +96,7 @@ describe CustomWizard::Wizard do end it "determines whether a wizard is unfinished" do + append_steps expect(@wizard.unfinished?).to eq(true) progress_step("step_1") expect(@wizard.unfinished?).to eq(true) @@ -67,6 +107,7 @@ describe CustomWizard::Wizard do end it "determines whether a wizard has been completed by a user" do + append_steps expect(@wizard.completed?).to eq(false) progress_step("step_1") progress_step("step_2") @@ -75,6 +116,8 @@ describe CustomWizard::Wizard do end it "is not completed if steps submitted before after time" do + append_steps + progress_step("step_1") progress_step("step_2") progress_step("step_3") @@ -83,7 +126,6 @@ describe CustomWizard::Wizard do template_json['after_time_scheduled'] = Time.now + 3.hours wizard = CustomWizard::Wizard.new(template_json, user) - expect(wizard.completed?).to eq(false) end @@ -125,6 +167,8 @@ describe CustomWizard::Wizard do end it "lets a permitted user access a complete wizard with multiple submissions" do + append_steps + progress_step("step_1", acting_user: trusted_user) progress_step("step_2", acting_user: trusted_user) progress_step("step_3", acting_user: trusted_user) @@ -135,6 +179,8 @@ describe CustomWizard::Wizard do end it "does not let an unpermitted user access a complete wizard without multiple submissions" do + append_steps + progress_step("step_1", acting_user: trusted_user) progress_step("step_2", acting_user: trusted_user) progress_step("step_3", acting_user: trusted_user) diff --git a/spec/extensions/wizard_field_spec.rb b/spec/extensions/wizard_field_spec.rb deleted file mode 100644 index 370c25e7..00000000 --- a/spec/extensions/wizard_field_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true -require_relative '../plugin_helper' - -describe CustomWizardFieldExtension do - let(:field_hash) do - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/field/field.json" - ).read).with_indifferent_access - end - - it "adds custom field attributes" do - field = Wizard::Field.new(field_hash) - expect(field.id).to eq("field_id") - expect(field.label).to eq("

Field Label

") - expect(field.image).to eq("field_image_url.png") - expect(field.description).to eq("Field description") - expect(field.required).to eq(true) - expect(field.key).to eq("field.locale.key") - expect(field.type).to eq("field_type") - expect(field.content).to eq([]) - end -end diff --git a/spec/extensions/wizard_step_spec.rb b/spec/extensions/wizard_step_spec.rb deleted file mode 100644 index 1a9cc0e5..00000000 --- a/spec/extensions/wizard_step_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true -require_relative '../plugin_helper' - -describe CustomWizardStepExtension do - let(:step_hash) do - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/step.json" - ).read).with_indifferent_access - end - - it "adds custom step attributes" do - step = Wizard::Step.new(step_hash[:id]) - [ - :title, - :description, - :key, - :permitted, - :permitted_message - ].each do |attr| - step.send("#{attr.to_s}=", step_hash[attr]) - expect(step.send(attr)).to eq(step_hash[attr]) - end - end -end diff --git a/spec/fixtures/condition/user_condition.json b/spec/fixtures/condition/user_condition.json new file mode 100644 index 00000000..c6347b04 --- /dev/null +++ b/spec/fixtures/condition/user_condition.json @@ -0,0 +1,17 @@ +{ + "condition": [ + { + "type": "validation", + "pairs": [ + { + "index": 0, + "key": "username", + "key_type": "user_field", + "value": "angus", + "value_type": "text", + "connector": "equal" + } + ] + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/condition/wizard_field_condition.json b/spec/fixtures/condition/wizard_field_condition.json new file mode 100644 index 00000000..12d47b38 --- /dev/null +++ b/spec/fixtures/condition/wizard_field_condition.json @@ -0,0 +1,17 @@ +{ + "condition": [ + { + "type": "validation", + "pairs": [ + { + "index": 0, + "key": "step_1_field_1", + "key_type": "wizard_field", + "value": "Condition will pass", + "value_type": "text", + "connector": "equal" + } + ] + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/field/field.json b/spec/fixtures/field/field.json index c2de266d..c46a53cd 100644 --- a/spec/fixtures/field/field.json +++ b/spec/fixtures/field/field.json @@ -1,5 +1,6 @@ { "id": "field_id", + "index": 0, "label": "Field Label", "image": "field_image_url.png", "description": "Field description", diff --git a/spec/fixtures/step/step.json b/spec/fixtures/step/step.json index 5c6c5f58..97cd3318 100644 --- a/spec/fixtures/step/step.json +++ b/spec/fixtures/step/step.json @@ -1,5 +1,6 @@ { "id": "step_1", + "index": 0, "title": "Text", "description": "Step description", "image": "step_image_url.png", diff --git a/spec/fixtures/wizard.json b/spec/fixtures/wizard.json index ccaea390..20ff1441 100644 --- a/spec/fixtures/wizard.json +++ b/spec/fixtures/wizard.json @@ -535,7 +535,7 @@ }, { "id": "action_10", - "run_after": "wizard_completion", + "run_after": "step_3", "type": "route_to", "url": [ { diff --git a/spec/requests/custom_wizard/steps_controller_spec.rb b/spec/requests/custom_wizard/steps_controller_spec.rb index dd3b52a1..c58f13a2 100644 --- a/spec/requests/custom_wizard/steps_controller_spec.rb +++ b/spec/requests/custom_wizard/steps_controller_spec.rb @@ -11,12 +11,49 @@ describe CustomWizard::StepsController do ) } - before do - CustomWizard::Template.save( - JSON.parse(File.open( + fab!(:user2) { + Fabricate( + :user, + username: 'bob', + email: "bob@email.com", + trust_level: TrustLevel[2] + ) + } + + let(:wizard_template) { + JSON.parse( + File.open( "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" - ).read), - skip_jobs: true) + ).read + ) + } + + let(:wizard_field_condition_template) { + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/condition/wizard_field_condition.json" + ).read + ) + } + + let(:user_condition_template) { + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/condition/user_condition.json" + ).read + ) + } + + let(:permitted_json) { + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" + ).read + ) + } + + before do + CustomWizard::Template.save(wizard_template, skip_jobs: true) sign_in(user) end @@ -27,17 +64,189 @@ describe CustomWizard::StepsController do } } expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_2") - wizard = CustomWizard::Builder.new("super_mega_fun_wizard", user).build - expect(wizard.current_submission['step_1_field_1']).to eq("Text input") - expect(wizard.start.id).to eq("step_2") + wizard_id = response.parsed_body['wizard']['id'] + wizard = CustomWizard::Wizard.create(wizard_id, user) + expect(wizard.submissions.last['step_1_field_1']).to eq("Text input") + end + + context "raises an error" do + it "when the wizard doesnt exist" do + put '/w/not-super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(400) + end + + it "when the user cant access the wizard" do + new_template = wizard_template.dup + new_template["permitted"] = permitted_json["permitted"] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(403) + end + + it "when the step doesnt exist" do + put '/w/super-mega-fun-wizard/steps/step_10.json' + expect(response.status).to eq(400) + end + + it "when user cant see the step due to conditions" do + sign_in(user2) + + new_wizard_template = wizard_template.dup + new_wizard_template['steps'][0]['condition'] = user_condition_template['condition'] + CustomWizard::Template.save(new_wizard_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(403) + end end it "works if the step has no fields" do put '/w/super-mega-fun-wizard/steps/step_1.json' expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_2") + end - wizard = CustomWizard::Builder.new("super_mega_fun_wizard", user).build - expect(wizard.start.id).to eq("step_2") + it "returns an updated wizard when condition passes" do + new_template = wizard_template.dup + new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will pass" + } + } + expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_2") + end + + it "returns an updated wizard when condition doesnt pass" do + new_template = wizard_template.dup + new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition wont pass" + } + } + expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_3") + end + + it "runs completion actions if user has completed wizard" do + new_template = wizard_template.dup + + ## route_to action + new_template['actions'].last['run_after'] = 'wizard_completion' + new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] + new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition wont pass" + } + } + expect(response.status).to eq(200) + expect(response.parsed_body['redirect_on_complete']).to eq("https://google.com") + end + + it "saves results of completion actions if user has completed wizard" do + new_template = wizard_template.dup + + ## Create group action + new_template['actions'].first['run_after'] = 'wizard_completion' + new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "My cool group" + } + } + expect(response.status).to eq(200) + + put '/w/super-mega-fun-wizard/steps/step_3.json' + expect(response.status).to eq(200) + + wizard_id = response.parsed_body['wizard']['id'] + wizard = CustomWizard::Wizard.create(wizard_id, user) + group_name = wizard.submissions.last['action_9'] + group = Group.find_by(name: group_name) + expect(group.full_name).to eq("My cool group") + end + + it "returns a final step without conditions" do + put '/w/super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(false) + + put '/w/super-mega-fun-wizard/steps/step_2.json' + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(false) + + put '/w/super-mega-fun-wizard/steps/step_3.json' + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(true) + end + + it "returns the correct final step when the conditional final step and last step are the same" do + new_template = wizard_template.dup + new_template['steps'][0]['condition'] = user_condition_template['condition'] + new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will pass" + } + } + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(false) + + put '/w/super-mega-fun-wizard/steps/step_2.json' + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(false) + + put '/w/super-mega-fun-wizard/steps/step_3.json' + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(true) + end + + it "returns the correct final step when the conditional final step and last step are different" do + new_template = wizard_template.dup + new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will not pass" + } + } + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(false) + + put '/w/super-mega-fun-wizard/steps/step_2.json' + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(true) + end + + it "returns the correct final step when the conditional final step is determined in the same action" do + new_template = wizard_template.dup + new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] + new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will not pass" + } + } + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(true) end end diff --git a/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb b/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb index 944947e6..1fa9671c 100644 --- a/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb +++ b/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb @@ -20,9 +20,11 @@ describe CustomWizard::FieldSerializer do each_serializer: CustomWizard::FieldSerializer, scope: Guardian.new(user) ).as_json - expect(json_array.length).to eq(4) + + expect(json_array.size).to eq(4) expect(json_array[0][:label]).to eq("

Text

") expect(json_array[0][:description]).to eq("Text field description.") + expect(json_array[3][:index]).to eq(3) end it "should return optional field attributes" do @@ -32,7 +34,6 @@ describe CustomWizard::FieldSerializer do scope: Guardian.new(user) ).as_json expect(json_array[0][:format]).to eq("YYYY-MM-DD") - expect(json_array[3][:number]).to eq(4) expect(json_array[6][:file_types]).to eq(".jpg,.jpeg,.png") end end diff --git a/spec/serializers/custom_wizard/wizard_step_serializer.rb b/spec/serializers/custom_wizard/wizard_step_serializer.rb deleted file mode 100644 index c2d82962..00000000 --- a/spec/serializers/custom_wizard/wizard_step_serializer.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../plugin_helper' - -describe CustomWizard::StepSerializer do - fab!(:user) { Fabricate(:user) } - - let(:required_data_json) { - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/required_data.json" - ).read) - } - - before do - CustomWizard::Template.save( - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" - ).read), - skip_jobs: true) - @wizard = CustomWizard::Builder.new("super_mega_fun_wizard", user).build - end - - it 'should return basic step attributes' do - json_array = ActiveModel::ArraySerializer.new( - @wizard.steps, - each_serializer: CustomWizard::StepSerializer, - scope: Guardian.new(user) - ).as_json - expect(json_array[0][:wizard_step][:title]).to eq("Text") - expect(json_array[0][:wizard_step][:description]).to eq("Text inputs!") - end - - it 'should return fields' do - json_array = ActiveModel::ArraySerializer.new( - @wizard.steps, - each_serializer: CustomWizard::StepSerializer, - scope: Guardian.new(user) - ).as_json - expect(json_array[0][:wizard_step][:fields].length).to eq(4) - end - - context 'with required data' do - before do - @template[:steps][0][:required_data] = required_data_json['required_data'] - @template[:steps][0][:required_data_message] = required_data_json['required_data_message'] - CustomWizard::Template.save(@template.as_json) - end - - it 'should return permitted attributes' do - json_array = ActiveModel::ArraySerializer.new( - @wizard.steps, - each_serializer: CustomWizard::StepSerializer, - scope: Guardian.new(user) - ).as_json - expect(json_array[0][:wizard_step][:permitted]).to eq(false) - expect(json_array[0][:wizard_step][:permitted_message]).to eq("Missing required data") - end - end -end diff --git a/spec/serializers/custom_wizard/wizard_step_serializer_spec.rb b/spec/serializers/custom_wizard/wizard_step_serializer_spec.rb new file mode 100644 index 00000000..35ce0fd2 --- /dev/null +++ b/spec/serializers/custom_wizard/wizard_step_serializer_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require_relative '../../plugin_helper' + +describe CustomWizard::StepSerializer do + fab!(:user) { Fabricate(:user) } + + let(:wizard_template) { + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read + ) + } + + let(:required_data_json) { + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/required_data.json" + ).read + ) + } + + before do + CustomWizard::Template.save(wizard_template, skip_jobs: true) + @wizard = CustomWizard::Builder.new("super_mega_fun_wizard", user).build + end + + it 'should return basic step attributes' do + json_array = ActiveModel::ArraySerializer.new( + @wizard.steps, + each_serializer: described_class, + scope: Guardian.new(user) + ).as_json + expect(json_array[0][:title]).to eq("

Text

") + expect(json_array[0][:description]).to eq("

Text inputs!

") + expect(json_array[1][:index]).to eq(1) + end + + it 'should return fields' do + json_array = ActiveModel::ArraySerializer.new( + @wizard.steps, + each_serializer: described_class, + scope: Guardian.new(user) + ).as_json + expect(json_array[0][:fields].length).to eq(4) + end + + context 'with required data' do + before do + wizard_template['steps'][0]['required_data'] = required_data_json['required_data'] + wizard_template['steps'][0]['required_data_message'] = required_data_json['required_data_message'] + CustomWizard::Template.save(wizard_template) + @wizard = CustomWizard::Builder.new("super_mega_fun_wizard", user).build + end + + it 'should return permitted attributes' do + json_array = ActiveModel::ArraySerializer.new( + @wizard.steps, + each_serializer: described_class, + scope: Guardian.new(user) + ).as_json + expect(json_array[0][:permitted]).to eq(false) + expect(json_array[0][:permitted_message]).to eq("Missing required data") + end + end +end