1
0
Fork 0

Step and field conditionality (#87)

* Re structure builder logic to allow for step conditionality

Concerns
- Performance. Look at whether the additional build in the steps controller can be reduced
- Does not work if applied to the last step.
- Certain conditions will not work with the first step(?)
- How should this be scoped to known functionality?

* Add indexes and conditions to steps and fields

* Complete and add spec

* Complete backend

* Complete step conditionality and field indexing

* Fix failing spec

* Update coverage

* Apply rubocop

* Apply prettier

* Apply prettier to wizard js

* Fix schema issues created in merge

* Remove setting label for force_final

* Improve client wizard cache naming

* Improve steps controller and spec conditionality

* Improve final step attribute naming

* Fix failing spec

* Linting

* Add one more final step test

* Linting

* Fix eslint issues

* Apply prettier

* Linting, syntax, merge and copy cleanups

* Update wizard-admin.scss

* Fix template linting

* Rubocop fixes
Dieser Commit ist enthalten in:
Angus McLeod 2021-04-21 03:58:19 +10:00 committet von GitHub
Ursprung ec21c8e274
Commit ceef3f4bc9
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
70 geänderte Dateien mit 1387 neuen und 573 gelöschten Zeilen

Datei anzeigen

@ -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);

Datei anzeigen

@ -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);

Datei anzeigen

@ -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}`;
}

Datei anzeigen

@ -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));

Datei anzeigen

@ -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: {},

Datei anzeigen

@ -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) => {

Datei anzeigen

@ -188,6 +188,30 @@
{{#if field.showAdvanced}}
<div class="advanced-settings">
<div class="setting full field-mapper-setting">
<div class="setting-label">
<label>{{i18n "admin.wizard.condition"}}</label>
</div>
<div class="setting-value">
{{wizard-mapper
inputs=field.condition
options=fieldConditionOptions}}
</div>
</div>
<div class="setting full field-mapper-setting">
<div class="setting-label">
<label>{{i18n "admin.wizard.index"}}</label>
</div>
<div class="setting-value">
{{wizard-mapper
inputs=field.index
options=fieldIndexOptions}}
</div>
</div>
{{#if isCategory}}
<div class="setting">
<div class="setting-label">

Datei anzeigen

@ -38,6 +38,27 @@
{{#if step.showAdvanced}}
<div class="advanced-settings">
<div class="setting full field-mapper-setting">
<div class="setting-label">
<label>{{i18n "admin.wizard.condition"}}</label>
</div>
<div class="setting-value">
{{wizard-mapper
inputs=step.condition
options=stepConditionOptions}}
</div>
</div>
<div class="setting full">
<div class="setting-label"></div>
<div class="setting-value force-final">
<h4>{{i18n "admin.wizard.step.force_final.label"}}</h4>
{{input type="checkbox" checked=step.force_final}}
<span>{{i18n "admin.wizard.step.force_final.description"}}</span>
</div>
</div>
<div class="setting full field-mapper-setting">
<div class="setting-label">
<label>{{i18n "admin.wizard.step.required_data.label"}}</label>
@ -92,7 +113,6 @@
placeholderKey="admin.wizard.translation_placeholder"}}
</div>
</div>
</div>
{{/if}}
@ -105,6 +125,7 @@
{{#each step.fields as |field|}}
{{wizard-custom-field
field=field
step=step
currentFieldId=currentField.id
fieldTypes=fieldTypes
removeField="removeField"

Datei anzeigen

@ -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);
}
},

Datei anzeigen

@ -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");
},

Datei anzeigen

@ -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;

Datei anzeigen

@ -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");

Datei anzeigen

@ -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) {

Datei anzeigen

@ -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,

Datei anzeigen

@ -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,

Datei anzeigen

@ -50,6 +50,11 @@ textarea {
border-color: var(--danger);
box-shadow: shadow("focus-danger");
}
&[type="checkbox"] {
margin-bottom: 0;
margin-right: 10px;
}
}
.spinner {

Datei anzeigen

@ -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"

Datei anzeigen

@ -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

Datei anzeigen

@ -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: {},
]
],

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -1,5 +1,5 @@
{
"result": {
"line": 89.56
"line": 90.52
}
}

Datei anzeigen

@ -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

Datei anzeigen

@ -1,4 +0,0 @@
# frozen_string_literal: true
module CustomWizardStepExtension
attr_accessor :title, :description, :key, :permitted, :permitted_message
end

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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: {

56
lib/custom_wizard/step.rb Normale Datei
Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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)

Datei anzeigen

@ -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)

Datei anzeigen

@ -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

Datei anzeigen

@ -30,7 +30,7 @@ class CustomWizard::WizardSerializer < CustomWizard::BasicWizardSerializer
end
def start
object.start.id
object.start
end
def include_start?

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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("<p>Field Label</p>")
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

Datei anzeigen

@ -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

Datei anzeigen

@ -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)

Datei anzeigen

@ -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("<p>Field Label</p>")
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

Datei anzeigen

@ -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

17
spec/fixtures/condition/user_condition.json gevendort Normale Datei
Datei anzeigen

@ -0,0 +1,17 @@
{
"condition": [
{
"type": "validation",
"pairs": [
{
"index": 0,
"key": "username",
"key_type": "user_field",
"value": "angus",
"value_type": "text",
"connector": "equal"
}
]
}
]
}

Datei anzeigen

@ -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"
}
]
}
]
}

Datei anzeigen

@ -1,5 +1,6 @@
{
"id": "field_id",
"index": 0,
"label": "Field Label",
"image": "field_image_url.png",
"description": "Field description",

Datei anzeigen

@ -1,5 +1,6 @@
{
"id": "step_1",
"index": 0,
"title": "Text",
"description": "Step description",
"image": "step_image_url.png",

Datei anzeigen

@ -535,7 +535,7 @@
},
{
"id": "action_10",
"run_after": "wizard_completion",
"run_after": "step_3",
"type": "route_to",
"url": [
{

Datei anzeigen

@ -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

Datei anzeigen

@ -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("<p>Text</p>")
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

Datei anzeigen

@ -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

Datei anzeigen

@ -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("<p>Text</p>")
expect(json_array[0][:description]).to eq("<p>Text inputs!</p>")
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