1
0
Fork 0
Dieser Commit ist enthalten in:
Angus McLeod 2020-04-08 12:52:07 +10:00
Ursprung 5a6afef005
Commit 98f9215d65
14 geänderte Dateien mit 398 neuen und 387 gelöschten Zeilen

Datei anzeigen

@ -1,66 +1,43 @@
import { default as discourseComputed, observes, on } from 'discourse-common/utils/decorators';
import { equal, not, empty, or } from "@ember/object/computed";
import { actionTypes, generateName, selectKitContent, profileFields } from '../lib/wizard';
import { default as discourseComputed, observes } from 'discourse-common/utils/decorators';
import { equal, empty, or } from "@ember/object/computed";
import { actionTypes, generateName, selectKitContent } from '../lib/wizard';
import Component from "@ember/component";
export default Component.extend({
classNames: 'wizard-custom-action',
types: actionTypes.map(t => ({ id: t, name: generateName(t) })),
actionTypes: actionTypes.map(t => ({ id: t, name: generateName(t) })),
createTopic: equal('action.type', 'create_topic'),
updateProfile: equal('action.type', 'update_profile'),
sendMessage: equal('action.type', 'send_message'),
openComposer: equal('action.type', 'open_composer'),
sendToApi: equal('action.type', 'send_to_api'),
apiEmpty: empty('action.api'),
addToGroup: equal('action.type', 'add_to_group'),
routeTo: equal('action.type', 'route_to'),
disableId: not('action.isNew'),
apiEmpty: empty('action.api'),
groupPropertyTypes: selectKitContent(['id', 'name']),
hasAdvanced: or('hasCustomFields', 'routeTo'),
hasCustomFields: or('basicTopicFields', 'updateProfile'),
basicTopicFields: or('createTopic', 'sendMessage', 'openComposer'),
publicTopicFields: or('createTopic', 'openComposer'),
showSkipRedirect: or('createTopic', 'sendMessage'),
@on('didInsertElement')
@observes('action.type')
setLabel() {
setupDefaults() {
if (this.action.type) {
this.set('action.label', generateName(this.action.type));
};
},
@discourseComputed('action.type')
basicTopicFields(actionType) {
return ['create_topic', 'send_message', 'open_composer'].indexOf(actionType) > -1;
},
@discourseComputed('wizard.steps')
runAfterContent(steps) {
let content = steps.map(s => ({ id: s.id, name: s.label }));
@discourseComputed('action.type')
publicTopicFields(actionType) {
return ['create_topic', 'open_composer'].indexOf(actionType) > -1;
},
content.unshift({
id: 'wizard_completion',
name: I18n.t('admin.wizard.action.run_after.wizard_completion')
});
@discourseComputed('action.type')
newTopicFields(actionType) {
return ['create_topic', 'send_message'].indexOf(actionType) > -1;
},
@discourseComputed('wizardFields')
categoryFields(fields) {
return fields.filter(f => f.type == 'category');
},
@discourseComputed('wizardFields')
tagFields(fields) {
return fields.filter(f => f.type == 'tag');
},
@observes('action.custom_category_wizard_field')
toggleCustomCategoryUserField() {
if (this.action.custom_category_wizard_field)
this.set('action.custom_category_user_field', false);
},
@observes('action.custom_category_user_field')
toggleCustomCategoryWizardField() {
if (this.action.custom_category_user_field)
this.set('action.custom_category_wizard_field', false);
return content;
},
@discourseComputed('wizard.apis')

Datei anzeigen

@ -1,5 +1,5 @@
import { default as discourseComputed, observes, on } from 'discourse-common/utils/decorators';
import { equal, not, or } from "@ember/object/computed";
import { default as discourseComputed, observes } from 'discourse-common/utils/decorators';
import { equal, or } from "@ember/object/computed";
import { selectKitContent } from '../lib/wizard';
import Component from "@ember/component";
@ -10,23 +10,30 @@ export default Component.extend({
isCategory: equal('field.type', 'category'),
isGroup: equal('field.type', 'group'),
isTag: equal('field.type', 'tag'),
disableId: not('field.isNew'),
isText: equal('field.type', 'text'),
isTextarea: equal('field.type', 'textarea'),
isUrl: equal('field.type', 'url'),
showPrefill: or('isCategory', 'isTag', 'isGroup', 'isDropdown'),
showContent: or('isCategory', 'isTag', 'isGroup', 'isDropdown'),
showLimit: or('isCategory', 'isTag'),
showMinLength: or('isText', 'isTextarea', 'isUrl'),
categoryPropertyTypes: selectKitContent(['id', 'slug']),
prefillEnabled: or('isCategory', 'isTag', 'isGroup', 'isDropdown'),
contentEnabled: or('isCategory', 'isTag', 'isGroup', 'isDropdown'),
@discourseComputed('field.type')
isInput: (type) => type === 'text' || type === 'textarea' || type === 'url',
@discourseComputed('field.type')
isCategoryOrTag: (type) => type === 'tag' || type === 'category',
@on('didInsertElement')
@observes('isUpload')
setupFileType() {
@observes('isUpload', 'isCategory')
setupDefaults() {
if (this.isUpload && !this.field.file_types) {
this.set('field.file_types', '.jpg,.png');
}
if (this.isCategory && !this.field.property) {
this.set('field.property', 'id');
}
},
@observes('field.type')
clearMappedProperties() {
this.set('field.content', null);
this.set('field.prefill', null);
},
setupTypeOutput(fieldType, options) {
@ -80,19 +87,6 @@ export default Component.extend({
return this.setupTypeOutput(fieldType, options);
},
@observes('field.type')
clearInputs() {
this.set('field.content', null);
this.set('field.prefill', null);
},
@observes('isCategory')
setupCategoryType() {
if (this.isCategory && !this.field.property) {
this.set('field.property', 'id');
}
},
actions: {
imageUploadDone(upload) {
this.set("field.image", upload.url);

Datei anzeigen

@ -1,34 +1,7 @@
import { observes, on, default as discourseComputed } from 'discourse-common/utils/decorators';
import { not } from "@ember/object/computed";
import EmberObject from "@ember/object";
import Component from "@ember/component";
export default Component.extend({
classNames: 'wizard-custom-step',
disableId: not('step.isNew'),
@discourseComputed('wizardFields', 'wizard.steps')
requiredContent(wizardFields, steps) {
let content = wizardFields;
let actions = [];
steps.forEach(s => {
actions.push(...s.actions);
});
actions.forEach(a => {
if (a.type === 'route_to' && a.code) {
content.push(
EmberObject.create({
id: a.code,
label: "code (Route To)"
})
);
}
});
return content;
},
actions: {
bannerUploadDone(upload) {

Datei anzeigen

@ -99,7 +99,7 @@ export default Controller.extend({
}
}).catch((result) => {
this.set('saving', false);
this.set('error', I18n.t(`admin.wizard.error.${result.error}`));
this.set('error', I18n.t(`admin.wizard.error.${result.error}`, result.errorParams || {}));
later(() => this.set('error', null), 10000);
});
},

Datei anzeigen

@ -19,132 +19,6 @@ function mapped(property, type) {
mappedProperties[type].indexOf(property) > -1;
}
function buildJson(object, type) {
let result = {};
properties[type].forEach((p) => {
let value = object.get(p);
if (mapped(p, type)) {
value = buildMappedJson(value);
}
if (value) {
result[p] = value;
}
});
return result;
}
function buildMappedJson(inputs) {
if (!inputs || !inputs.length) return false;
let result = [];
inputs.forEach(inpt => {
let input = {
type: inpt.type,
};
if (present(inpt.output)) {
input.output = inpt.output;
input.output_type = snakeCase(inpt.output_type);
input.output_connector = inpt.output_connector;
}
if (present(inpt.pairs)) {
input.pairs = [];
inpt.pairs.forEach(pr => {
if (present(pr.key) && present(pr.value)) {
let pairParams = {
index: pr.index,
key: pr.key,
key_type: snakeCase(pr.key_type),
value: pr.value,
value_type: snakeCase(pr.value_type),
connector: pr.connector
}
input.pairs.push(pairParams);
}
});
}
if ((input.type === 'assignment' && present(input.output)) ||
present(input.pairs)) {
result.push(input);
}
});
if (!result.length) {
result = false;
}
return result;
}
function buildStepJson(object) {
let steps = [];
let error = null;
object.some((s) => {
let step = buildJson(s, 'step');
let fields = s.fields;
if (fields.length) {
step.fields = [];
fields.some((f) => {
if (!f.type) {
error = 'type_required';
return;
}
step.fields.push(
buildJson(f, 'field')
);
});
if (error) return;
}
let actions = s.actions;
if (actions.length) {
step.actions = [];
actions.some((a) => {
if (a.api_body) {
try {
JSON.parse(a.api_body);
} catch (e) {
error = 'invalid_api_body';
return;
}
}
step.actions.push(
buildJson(a, 'action')
);
});
if (error) return;
}
steps.push(step);
});
if (error) {
return { error };
} else {
return { steps };
};
}
function castCase(property, value) {
return property.indexOf('_type') > -1 ? camelCase(value) : value;
}
@ -223,9 +97,9 @@ function objectHasAdvanced(params, type) {
}
function buildProperties(json) {
let steps = A();
let props = {
steps
steps: A();
action: A();
};
if (present(json)) {
@ -268,25 +142,23 @@ function buildProperties(json) {
});
}
stepParams.actions = A();
if (present(stepJson.actions)) {
stepJson.actions.forEach((a) => {
let params = buildObject(a, 'action');
if (objectHasAdvanced(params, 'action')) {
params.showAdvanced = true;
}
stepParams.actions.pushObject(params);
});
}
steps.pushObject(
EmberObject.create(stepParams)
);
});
};
if (present(json.actions)) {
json.actions.forEach((a) => {
let params = buildObject(a, 'action');
if (objectHasAdvanced(params, 'action')) {
params.showAdvanced = true;
}
props.actions.pushObject(params);
});
}
} else {
props.id = '';
props.name = '';
@ -299,7 +171,6 @@ function buildProperties(json) {
props.prompt_completion = false;
props.restart_on_revisit = false;
props.permitted = null;
props.steps = A();
}
return props;

Datei anzeigen

@ -58,7 +58,9 @@ const wizardProperties = [
'prompt_completion',
'restart_on_revisit',
'theme_id',
'permitted'
'permitted',
'steps',
'actions'
];
const stepProperties = [
@ -69,7 +71,8 @@ const stepProperties = [
'raw_description',
'required_data',
'required_data_message',
'permitted_params'
'permitted_params',
'fields'
]
const fieldProperties = [
@ -91,6 +94,7 @@ const fieldProperties = [
const actionProperties = [
'id',
'type',
'run_after',
'title',
'post',
'post_builder',
@ -117,6 +121,12 @@ const properties = {
action: actionProperties
}
const objectArrays = [
'steps',
'fields',
'actions'
];
const mappedProperties = {
wizard: [
'permitted'
@ -202,6 +212,7 @@ export {
camelCase,
snakeCase,
properties,
objectArrays,
wizardProperties,
mappedProperties,
profileFields,

Datei anzeigen

@ -1,32 +1,20 @@
import { ajax } from 'discourse/lib/ajax';
import EmberObject from "@ember/object";
import { buildStepJson, buildJson, buildProperties } from '../lib/wizard-json';
import { buildJson, buildProperties, present } from '../lib/wizard-json';
import { properties, arrays, camelCase, snakeCase } from '../lib/wizard';
import { Promise } from "rsvp";
const jsonStrings = ['api_body'];
const required = ['id', 'steps', 'type'];
const dependent = { after_time: 'after_time_scheduled' }
const CustomWizard = EmberObject.extend({
save() {
return new Promise((resolve, reject) => {
let wizardJson = buildJson(this, 'wizard');
let json = this.buildJson(this, 'wizard');
if (wizardJson.after_time && !wizardJson.after_time_scheduled) {
reject({
error: 'after_time_need_time'
});
};
if (this.steps.length > 0) {
let stepsResult = buildStepJson(this.steps);
if (stepsResult.error ||
!stepsResult.steps ||
stepsResult.steps.length < 1) {
reject({
error: stepsResult.error || 'steps_required'
});
} else {
wizardJson.steps = stepsResult.steps;
}
if (json.error) {
reject({ eror: json.error });
}
ajax("/admin/wizards/custom/save", {
@ -44,6 +32,111 @@ const CustomWizard = EmberObject.extend({
});
},
buildJson(object, type, result = {}) {
for (let property of properties[type]) {
let value = object.get(property);
if (objectArrays[type]) {
result[property] = [];
for (let obj of value) {
let obj = this.buildJson(value, property, result);
if (obj.error) {
result.error = r.error;
break;
} else {
result[property].push(obj);
}
}
}
if (required[property] && !value) {
result.error = 'required'
result.errorParams = { type, property };
}
if (dependent[property] && !properties[type][dependent[property]]) {
result.error = 'dependent';
result.errorParams = {
dependentProperty: properties[type][dependent[property]],
property
}
}
if (jsonStrings[property]) {
try {
value = JSON.parse(value);
} catch (e) {
result.error = 'invalid';
result.errorParams = { property };
}
}
if (mapped(property, type)) {
value = this.buildMappedJson(value);
}
if (result.error) {
break;
} else if (value) {
result[property] = value;
}
});
return result;
}
buildMappedJson(inputs) {
if (!inputs || !inputs.length) return false;
let result = [];
inputs.forEach(inpt => {
let input = {
type: inpt.type,
};
if (present(inpt.output)) {
input.output = inpt.output;
input.output_type = snakeCase(inpt.output_type);
input.output_connector = inpt.output_connector;
}
if (present(inpt.pairs)) {
input.pairs = [];
inpt.pairs.forEach(pr => {
if (present(pr.key) && present(pr.value)) {
let pairParams = {
index: pr.index,
key: pr.key,
key_type: snakeCase(pr.key_type),
value: pr.value,
value_type: snakeCase(pr.value_type),
connector: pr.connector
}
input.pairs.push(pairParams);
}
});
}
if ((input.type === 'assignment' && present(input.output)) ||
present(input.pairs)) {
result.push(input);
}
});
if (!result.length) {
result = false;
}
return result;
},
remove() {
return ajax("/admin/wizards/custom/remove", {
type: 'DELETE',

Datei anzeigen

@ -157,7 +157,16 @@
step=currentStep
wizard=model
currentField=currentField
currentAction=currentAction
wizardFields=wizardFields}}
{{/if}}
{{wizard-links type="action" current=currentAction items=model.actions}}
{{#if currentAction}}
{{wizard-custom-action
action=currentAction
wizard=model
removeAction="removeAction"
wizardFields=wizardFields}}
{{/if}}

Datei anzeigen

@ -6,7 +6,7 @@
<div class="setting-value">
{{combo-box
value=action.type
content=types
content=actionTypes
onChange=(action (mut action.type))
options=(hash
none="admin.wizard.field.type"
@ -14,6 +14,19 @@
</div>
</div>
<div class="setting">
<div class="setting-label">
<label>{{i18n "admin.wizard.run_after"}}</label>
</div>
<div class="setting-value">
{{combo-box
value=action.run_after
content=runAfterContent
onChange=(action (mut action.run_after))}}
</div>
</div>
{{#if basicTopicFields}}
<div class="setting full field-mapper-setting">
<div class="setting-label">
@ -99,6 +112,7 @@
inputs=action.tags
options=(hash
tagSelection='output'
listSelection='output'
wizardFieldSelection=true
userFieldSelection='key,value'
context='action'
@ -280,7 +294,7 @@
</div>
{{/if}}
{{#if newTopicFields}}
{{#if showSkipRedirect}}
<div class="setting full">
<div class="setting-label">
<label>{{i18n "admin.wizard.action.skip_redirect.label"}}</label>

Datei anzeigen

@ -49,7 +49,7 @@
<div class="setting-value">
{{combo-box
value=field.type
content=types
content=fieldTypes
onChange=(action (mut field.type))
options=(hash
none="admin.wizard.field.type"
@ -57,7 +57,7 @@
</div>
</div>
{{#if isInput}}
{{#if showMinLength}}
<div class="setting">
<div class="setting-label">
<label>{{i18n 'admin.wizard.field.min_length'}}</label>
@ -81,7 +81,7 @@
</div>
{{/if}}
{{#if isCategoryOrTag}}
{{#if showLimit}}
<div class="setting">
<div class="setting-label">
<label>{{i18n 'admin.wizard.field.limit'}}</label>
@ -116,7 +116,7 @@
</div>
{{/if}}
{{#if prefillEnabled}}
{{#if showPrefill}}
<div class="setting full field-mapper-setting">
<div class="setting-label">
<label>{{i18n 'admin.wizard.field.prefill'}}</label>
@ -130,7 +130,7 @@
</div>
{{/if}}
{{#if contentEnabled}}
{{#if showContent}}
<div class="setting full field-mapper-setting">
<div class="setting-label">
<label>{{i18n 'admin.wizard.field.content'}}</label>

Datei anzeigen

@ -103,17 +103,7 @@
{{#if currentField}}
{{wizard-custom-field
field=currentField
types=wizard.fieldTypes
fieldTypes=wizard.fieldTypes
removeField="removeField"
wizardFields=wizardFields}}
{{/if}}
{{wizard-links type="action" current=currentAction items=step.actions}}
{{#if currentAction}}
{{wizard-custom-action
action=currentAction
wizard=wizard
removeAction="removeAction"
wizardFields=wizardFields}}
{{/if}}

Datei anzeigen

@ -95,13 +95,9 @@ en:
list: "Enter item"
error:
name_required: "Wizards must have a name."
steps_required: "Wizards must have at least one step."
id_required: "All wizards, steps, fields and actions need an id."
invalid_api_body: "Request body JSON needs to be a valid JSON."
type_required: "All fields need a type."
after_time_need_time: "After time is enabled but no time is set."
after_time_invalid: "After time is invalid."
required: "{{type}} requires {{property}}"
invalid: "{{property}} is invalid"
dependent: "{{dependentProperty}} is dependent on {{property}}"
step:
header: "Steps"
@ -154,6 +150,10 @@ en:
topic_attr: "Topic Attribute"
interpolate_fields: "Insert wizard fields using the field_id in w{}. Insert user fields using field key in u{}."
run_after:
label: "Run After"
wizard_completion: "Wizard completes"
custom_fields:
label: "Custom"
key: "field"

Datei anzeigen

@ -11,92 +11,30 @@ class CustomWizard::AdminController < ::ApplicationController
end
def save
params.require(:wizard)
result = build_wizard
wizard = ::JSON.parse(params[:wizard])
existing = PluginStore.get('custom_wizard', wizard['id']) || {}
new_time = false
error = nil
if result[:error]
render json: { error: result[:error] }
else
wizard = result[:wizard]
existing_wizard = result[:existing_wizard]
if wizard["id"].blank?
error = 'id_required'
elsif wizard["name"].blank?
error = 'name_required'
elsif wizard["steps"].blank?
error = 'steps_required'
elsif wizard["after_time"]
if !wizard["after_time_scheduled"] && !existing["after_time_scheduled"]
error = 'after_time_need_time'
else
after_time_scheduled = Time.parse(wizard["after_time_scheduled"]).utc
ActiveRecord::Base.transaction do
PluginStore.set('custom_wizard', wizard["id"], wizard)
new_time = existing['after_time_scheduled'] ?
after_time_scheduled != Time.parse(existing['after_time_scheduled']).utc :
true
begin
if new_time && after_time_scheduled < Time.now.utc
error = 'after_time_invalid'
end
rescue ArgumentError
error = 'after_time_invalid'
if wizard['after_time'] && result[:new_after_time]
Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id'])
Jobs.enqueue_at(after_time_scheduled, :set_after_time_wizard, wizard_id: wizard['id'])
end
end
end
return render json: { error: error } if error
wizard["steps"].each do |s|
if s["id"].blank?
error = 'id_required'
break
end
if s["fields"] && s["fields"].present?
s["fields"].each do |f|
if f["id"].blank?
error = 'id_required'
break
end
if f["type"].blank?
error = 'type_required'
break
end
if existing_wizard && existing_wizard['after_time'] && !wizard['after_time']
Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id'])
Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard['id'])
end
end
if s["actions"] && s["actions"].present?
s["actions"].each do |a|
if a["id"].blank?
error = 'id_required'
break
end
end
end
render json: success_json.merge(wizard: wizard)
end
return render json: { error: error } if error
## end of error checks
wizard['steps'].each do |s|
s['description'] = PrettyText.cook(s['raw_description']) if s['raw_description']
end
if wizard['after_time'] && new_time
Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id'])
Jobs.enqueue_at(after_time_scheduled, :set_after_time_wizard, wizard_id: wizard['id'])
end
if existing['after_time'] && !wizard['after_time']
Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id'])
Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard['id'])
end
PluginStore.set('custom_wizard', wizard["id"], wizard)
render json: success_json.merge(wizard: wizard)
end
def remove
@ -149,4 +87,139 @@ class CustomWizard::AdminController < ::ApplicationController
render json: success_json.merge(submissions: all_submissions)
end
private
def wizard_params
params.require(:wizard)
params[:wizard]
end
def required_properties
{
wizard: ['id', 'name', 'steps'],
step: ['id'],
field: ['id', 'type'],
action: ['id', 'type']
}
end
def dependent_properties
{
after_time: 'after_time_scheduled'
}
end
def check_required(object, type, error)
object.each do |property, value|
required = required_properties[type].include?(property)
if required && property.blank?
error = {
type: 'required',
params: { property: property }
}
end
end
error
end
def check_depdendent(object, error)
object.each do |property, value|
dependent = dependent_properties[property]
if dependent && object[dependent].blank?
error = {
type: 'dependent',
params: { dependent: dependent, property: property }
}
end
end
error
end
def validate_wizard(wizard)
error = nil
error = check_required(wizard, :wizard, error)
error = check_depdendent(wizard, error)
wizard['steps'].each do |step|
error = check_required(step, :step, error)
error = check_depdendent(step, error)
break if error.present?
step['fields'].each do |field|
error = check_required(field, :field, error)
error = check_depdendent(field, error)
break if error.present?
end
end
wizard['actions'].each do |action|
error = check_required(action, :action, error)
error = check_depdendent(action, error)
break if error.present?
end
if error
{ error: error }
else
{ success: true }
end
end
def validate_after_time(wizard, existing_wizard)
new = false
error = nil
if wizard["after_time"]
if !wizard["after_time_scheduled"] && !existing_wizard["after_time_scheduled"]
error = 'after_time_need_time'
else
after_time_scheduled = Time.parse(wizard["after_time_scheduled"]).utc
new = existing_wizard['after_time_scheduled'] ?
after_time_scheduled != Time.parse(existing_wizard['after_time_scheduled']).utc :
true
begin
error = 'after_time_invalid' if new && after_time_scheduled < Time.now.utc
rescue ArgumentError
error = 'after_time_invalid'
end
end
end
if error
{ error: { type: error } }
else
{ new: new }
end
end
def build_wizard
wizard = ::JSON.parse(wizard_params)
existing_wizard = PluginStore.get('custom_wizard', wizard['id']) || {}
validation = validate_wizard(wizard)
return validation[:error] if validation[:error]
after_time_validation = validate_after_time(wizard, existing_wizard)
return after_time_validation[:error] if after_time_validation[:error]
wizard['steps'].each do |step|
if s['raw_description']
step['description'] = PrettyText.cook(s['raw_description'])
end
end
result = {
wizard: wizard,
existing_wizard: existing_wizard,
new_after_time: after_time_validation[:new]
}
end
end

Datei anzeigen

@ -2,11 +2,12 @@ class CustomWizard::Builder
attr_accessor :wizard, :updater, :submissions
def initialize(user=nil, wizard_id)
data = PluginStore.get('custom_wizard', wizard_id)
return if data.blank?
template = PluginStore.get('custom_wizard', wizard_id)
return if template.blank?
@steps = data['steps']
@wizard = CustomWizard::Wizard.new(user, data)
@steps = template['steps']
@actions = template['actions']
@wizard = CustomWizard::Wizard.new(user, template)
@submissions = Array.wrap(PluginStore.get("#{wizard_id}_submissions", user.id)) if user
end
@ -145,19 +146,24 @@ class CustomWizard::Builder
data = submission.merge(data)
end
if step_template['actions'] && step_template['actions'].length && data
step_template['actions'].each do |action|
CustomWizard::Action.new(
action: action,
user: user,
data: data,
updater: updater
).perform
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(
action: action,
user: user,
data: data,
updater: updater
).perform
end
end
end
final_step = updater.step.next.nil?
if route_to = data['route_to']
data.delete('route_to')
end