0
0
Fork 1
Spiegel von https://github.com/paviliondev/discourse-custom-wizard.git synchronisiert 2024-09-19 23:31:11 +02:00
Dieser Commit ist enthalten in:
Angus McLeod 2020-04-11 16:22:12 +10:00
Ursprung ae87e383d2
Commit d8fd5cb258
24 geänderte Dateien mit 384 neuen und 324 gelöschten Zeilen

Datei anzeigen

@ -21,17 +21,6 @@ export default Component.extend({
publicTopicFields: or('createTopic', 'openComposer'), publicTopicFields: or('createTopic', 'openComposer'),
showSkipRedirect: or('createTopic', 'sendMessage'), showSkipRedirect: or('createTopic', 'sendMessage'),
@observes('action.type')
setupDefaults() {
const defaultProperties = schema.action.types[this.action.type];
Object.keys(defaultProperties).forEach(property => {
if (defaultProperties[property]) {
this.set(`action.${property}`, defaultProperties[property]);
}
});
},
@discourseComputed('wizard.steps') @discourseComputed('wizard.steps')
runAfterContent(steps) { runAfterContent(steps) {
let content = steps.map(function(step) { let content = steps.map(function(step) {

Datei anzeigen

@ -19,17 +19,6 @@ export default Component.extend({
showMinLength: or('isText', 'isTextarea', 'isUrl', 'isComposer'), showMinLength: or('isText', 'isTextarea', 'isUrl', 'isComposer'),
categoryPropertyTypes: selectKitContent(['id', 'slug']), categoryPropertyTypes: selectKitContent(['id', 'slug']),
@observes('field.type')
setupDefaults() {
const defaultProperties = schema.field.types[this.field.type];
Object.keys(defaultProperties).forEach(property => {
if (defaultProperties[property]) {
this.set(`field.${property}`, defaultProperties[property]);
}
});
},
@observes('field.type') @observes('field.type')
clearMapped() { clearMapped() {
schema.field.mapped.forEach(property => { schema.field.mapped.forEach(property => {

Datei anzeigen

@ -18,11 +18,11 @@ export default Component.extend({
}, },
applySortable() { applySortable() {
$(this.element).find("ul").sortable({tolerance: 'pointer'}).on('sortupdate', (e, ui) => { $(this.element).find("ul")
const itemId = ui.item.data('id'); .sortable({ tolerance: 'pointer' })
const index = ui.item.index(); .on('sortupdate', (e, ui) => {
bind(this, this.updateItemOrder(itemId, index)); this.updateItemOrder(ui.item.data('id'), ui.item.index());
}); });
}, },
updateItemOrder(itemId, newIndex) { updateItemOrder(itemId, newIndex) {
@ -51,7 +51,7 @@ export default Component.extend({
label = generateName(item.type); label = generateName(item.type);
} }
link.label = label; link.label = `${label} (${item.id})`;
let classes = 'btn'; let classes = 'btn';
if (current && item.id === current.id) { if (current && item.id === current.id) {
@ -78,21 +78,29 @@ export default Component.extend({
add() { add() {
const items = this.items; const items = this.items;
const itemType = this.itemType; const itemType = this.itemType;
let next = 1;
if (items.length) {
next = Math.max.apply(Math, items.map((i) => (i.id.split('_')[1]))) + 1;
}
let params = { let params = {
id: `${itemType}_${items.length + 1}`, id: `${itemType}_${next}`,
isNew: true isNew: true
}; };
if (schema[itemType].objectArrays) { let objectArrays = schema[itemType].objectArrays;
Object.keys(schema[itemType].objectArrays).forEach(objectType => { if (objectArrays) {
Object.keys(objectArrays).forEach(objectType => {
params[objectArrays[objectType].property] = A(); params[objectArrays[objectType].property] = A();
}); });
}; };
params = this.setDefaults(schema[itemType].basic, params); params = this.setDefaults(schema[itemType].basic, params);
if (schema[itemType].types) {
params = this.setDefaults(schema[itemType].types[params.type], params); let types = schema[itemType].types;
if (types && params.type) {
params = this.setDefaults(types[params.type], params);
} }
const newItem = EmberObject.create(params); const newItem = EmberObject.create(params);
@ -107,8 +115,26 @@ export default Component.extend({
remove(itemId) { remove(itemId) {
const items = this.items; const items = this.items;
items.removeObject(items.findBy('id', itemId)); let item;
this.set('current', items[items.length - 1]); let index;
items.forEach((it, ind) => {
if (it.id === itemId) {
item = it;
index = ind;
}
});
let nextIndex;
if (this.current.id === itemId) {
nextIndex = index < (items.length-2) ? (index+1) : (index-1);
}
items.removeObject(item);
if (nextIndex) {
this.set('current', items[nextIndex]);
}
} }
} }
}); });

Datei anzeigen

@ -1,6 +1,7 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { gt } from '@ember/object/computed'; import { gt } from '@ember/object/computed';
import { computed } from "@ember/object"; import { computed } from "@ember/object";
import { removeMapperClasses } from '../lib/wizard-mapper';
export default Component.extend({ export default Component.extend({
classNameBindings: [':mapper-connector', ':mapper-block', 'hasMultiple::single'], classNameBindings: [':mapper-connector', ':mapper-block', 'hasMultiple::single'],
@ -9,5 +10,11 @@ export default Component.extend({
let key = this.connector; let key = this.connector;
let path = this.inputTypes ? `input.${key}.name` : `connector.${key}`; let path = this.inputTypes ? `input.${key}.name` : `connector.${key}`;
return I18n.t(`admin.wizard.${path}`); return I18n.t(`admin.wizard.${path}`);
}) }),
actions: {
onOpen() {
removeMapperClasses(this);
}
}
}); });

Datei anzeigen

@ -2,13 +2,13 @@ import { alias, or, gt } from "@ember/object/computed";
import { computed } from "@ember/object"; import { computed } from "@ember/object";
import { default as discourseComputed, observes, on } from "discourse-common/utils/decorators"; import { default as discourseComputed, observes, on } from "discourse-common/utils/decorators";
import { getOwner } from 'discourse-common/lib/get-owner'; import { getOwner } from 'discourse-common/lib/get-owner';
import { defaultSelectionType, selectionTypes } from '../lib/wizard-mapper'; import { defaultSelectionType, selectionTypes, removeMapperClasses } from '../lib/wizard-mapper';
import { snakeCase } from '../lib/wizard'; import { snakeCase } from '../lib/wizard';
import Component from "@ember/component"; import Component from "@ember/component";
import { bind } from "@ember/runloop"; import { bind } from "@ember/runloop";
export default Component.extend({ export default Component.extend({
classNames: 'mapper-selector', classNameBindings: [':mapper-selector', 'activeType'],
groups: alias('site.groups'), groups: alias('site.groups'),
categories: alias('site.categories'), categories: alias('site.categories'),
showText: computed('activeType', function() { return this.showInput('text') }), showText: computed('activeType', function() { return this.showInput('text') }),
@ -30,7 +30,6 @@ export default Component.extend({
userEnabled: computed('options.userSelection', 'inputType', function() { return this.optionEnabled('userSelection') }), userEnabled: computed('options.userSelection', 'inputType', function() { return this.optionEnabled('userSelection') }),
listEnabled: computed('options.listSelection', 'inputType', function() { return this.optionEnabled('listSelection') }), listEnabled: computed('options.listSelection', 'inputType', function() { return this.optionEnabled('listSelection') }),
hasTypes: gt('selectorTypes.length', 1), hasTypes: gt('selectorTypes.length', 1),
showTypes: false,
didInsertElement() { didInsertElement() {
$(document).on("click", bind(this, this.documentClick)); $(document).on("click", bind(this, this.documentClick));
@ -41,14 +40,16 @@ export default Component.extend({
}, },
documentClick(e) { documentClick(e) {
let $element = $(this.element); if (this._state == "destroying") return;
let $target = $(e.target); let $target = $(e.target);
if (!$target.hasClass('type-selector-icon') && if (!$target.parents('.wizard-mapper .input').length) {
$target.closest($element).length < 1 && this.send('disableActive');
this._state !== "destroying") { }
this.set("showTypes", false); if (!$target.parents('.type-selector').length) {
this.send('hideTypes');
} }
}, },
@ -147,14 +148,34 @@ export default Component.extend({
return this.activeType === type && this[`${type}Enabled`]; return this.activeType === type && this[`${type}Enabled`];
}, },
removeClasses() {
removeMapperClasses(this);
},
actions: { actions: {
toggleType(type) { toggleType(type) {
this.set('activeType', type); this.set('activeType', type);
this.set('showTypes', false); this.send('hideTypes');
}, },
toggleTypes() { // jquery is used here to ensure other selectors and types disable properly
this.toggleProperty('showTypes')
showTypes() {
this.removeClasses();
$(this.element).find('.selector-types').addClass('show');
},
hideTypes() {
$(this.element).find('.selector-types').removeClass('show');
},
enableActive() {
this.removeClasses();
$(this.element).addClass('active');
},
disableActive() {
$(this.element).removeClass('active');
} }
} }
}) })

Datei anzeigen

@ -7,6 +7,7 @@ import { dasherize } from "@ember/string";
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
import { scheduleOnce, later } from "@ember/runloop"; import { scheduleOnce, later } from "@ember/runloop";
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import copyText from "discourse/lib/copy-text";
export default Controller.extend({ export default Controller.extend({
hasName: notEmpty('model.name'), hasName: notEmpty('model.name'),
@ -58,7 +59,7 @@ export default Controller.extend({
let stepFields = s.fields.map((f) => { let stepFields = s.fields.map((f) => {
return EmberObject.create({ return EmberObject.create({
id: f.id, id: f.id,
label: `${f.label} (${s.id})`, label: `${f.label} (${s.id}, ${f.id})`,
type: f.type type: f.type
}); });
}); });
@ -95,13 +96,15 @@ export default Controller.extend({
}).catch((result) => { }).catch((result) => {
this.set('saving', false); this.set('saving', false);
let error = true; let errorType = 'failed';
let errorParams = {};
if (result.error) { if (result.error) {
let type = result.error.type; errorType = result.error.type;
let params = result.error.params || {}; errorParams = result.error.params;
error = I18n.t(`admin.wizard.error.${type}`, params);
} }
this.set('error', error);
this.set('error', I18n.t(`admin.wizard.error.${errorType}`, errorParams));
later(() => this.set('error', null), 10000); later(() => this.set('error', null), 10000);
}); });
@ -127,6 +130,20 @@ export default Controller.extend({
toggleAdvanced() { toggleAdvanced() {
this.toggleProperty('model.showAdvanced'); this.toggleProperty('model.showAdvanced');
},
copyUrl() {
const $copyRange = $('<p id="copy-range"></p>');
$copyRange.html(this.wizardUrl);
$(document.body).append($copyRange);
if (copyText(this.wizardUrl, $copyRange[0])) {
this.set("copiedUrl", true);
later(() => this.set("copiedUrl", false), 2000);
}
$copyRange.remove();
} }
} }
}); });

Datei anzeigen

@ -134,7 +134,8 @@ function actionPatch(json) {
function buildProperties(json) { function buildProperties(json) {
let props = { let props = {
steps: A() steps: A(),
actions: A()
}; };
if (present(json)) { if (present(json)) {

Datei anzeigen

@ -22,6 +22,12 @@ function inputTypesContent(options = {}) {
mapInputTypes(selectableInputTypes); mapInputTypes(selectableInputTypes);
} }
function removeMapperClasses(ctx) {
const $mapper = $(ctx.element).parents('.wizard-mapper');
$mapper.find('.selector-types').removeClass('show');
$mapper.find('.mapper-selector').removeClass('active');
}
// Connectors // Connectors
const connectors = { const connectors = {
@ -154,6 +160,7 @@ export {
defaultInputType, defaultInputType,
defaultSelectionType, defaultSelectionType,
defaultConnector, defaultConnector,
removeMapperClasses,
connectorContent, connectorContent,
inputTypesContent, inputTypesContent,
selectionTypes, selectionTypes,

Datei anzeigen

@ -126,7 +126,7 @@ const fieldProperties = {
description: null, description: null,
required: null, required: null,
key: null, key: null,
type: 'text' type: null
}, },
types: { types: {
text: { text: {
@ -138,15 +138,15 @@ const fieldProperties = {
composer: { composer: {
min_length: null min_length: null
}, },
text_only: {
},
number: { number: {
}, },
checkbox: {
},
url: { url: {
min_length: null min_length: null
}, },
'text-only': {
},
'user-selector': {
},
upload: { upload: {
file_types: '.jpg,.png' file_types: '.jpg,.png'
}, },
@ -168,6 +168,8 @@ const fieldProperties = {
group: { group: {
prefill: null, prefill: null,
content: null content: null
},
user_selector: {
} }
}, },
mapped: [ mapped: [
@ -193,7 +195,7 @@ const actionProperties = {
basic: { basic: {
id: null, id: null,
run_after: 'wizard_completion', run_after: 'wizard_completion',
type: 'create_topic' type: null
}, },
types: { types: {
create_topic: { create_topic: {

Datei anzeigen

@ -44,7 +44,7 @@ const CustomWizard = EmberObject.extend({
for (let property of listProperties(type, objectType)) { for (let property of listProperties(type, objectType)) {
let value = object.get(property); let value = object.get(property);
result = this.validateValue(property, value, type, result); result = this.validateValue(property, value, object, type, result);
if (result.error) { if (result.error) {
break; break;
@ -90,7 +90,7 @@ const CustomWizard = EmberObject.extend({
return result; return result;
}, },
validateValue(property, value, type, result) { validateValue(property, value, object, type, result) {
if (schema[type].required.indexOf(property) > -1 && !value) { if (schema[type].required.indexOf(property) > -1 && !value) {
result.error = { result.error = {
type: 'required', type: 'required',
@ -130,6 +130,10 @@ const CustomWizard = EmberObject.extend({
type: inpt.type, type: inpt.type,
}; };
if (inpt.connector) {
input.connector = inpt.connector;
}
if (present(inpt.output)) { if (present(inpt.output)) {
input.output = inpt.output; input.output = inpt.output;
input.output_type = snakeCase(inpt.output_type); input.output_type = snakeCase(inpt.output_type);

Datei anzeigen

@ -8,6 +8,11 @@
<div class="wizard-url"> <div class="wizard-url">
{{#if model.name}} {{#if model.name}}
<a href="{{wizardUrl}}" target="_blank">{{wizardUrl}}</a> <a href="{{wizardUrl}}" target="_blank">{{wizardUrl}}</a>
{{#if copiedUrl}}
{{d-button class="btn-hover pull-right" icon="copy" label="ip_lookup.copied"}}
{{else}}
{{d-button action=(action "copyUrl") class="pull-right no-text" icon="copy"}}
{{/if}}
{{/if}} {{/if}}
</div> </div>
</div> </div>

Datei anzeigen

@ -1,12 +1,14 @@
<div class="wizard-header medium">{{{i18n header}}}</div> <div class="wizard-header medium">{{{i18n header}}}</div>
{{#if anyLinks}}
<ul> <div class="link-list">
{{#if anyLinks}}
{{#each links as |l|}} {{#each links as |l|}}
<li data-id='{{l.id}}'> <div data-id='{{l.id}}'>
{{d-button action="change" actionParam=l.id translatedLabel=l.label class=l.classes}} {{d-button action="change" actionParam=l.id translatedLabel=l.label class=l.classes}}
{{d-button action='remove' actionParam=l.id icon='times' class='remove'}} {{d-button action='remove' actionParam=l.id icon='times' class='remove'}}
</li> </div>
{{/each}} {{/each}}
</ul> {{/if}}
{{/if}} {{d-button action='add' label='admin.wizard.add' icon='plus'}}
{{d-button action='add' label='admin.wizard.add' icon='plus'}} </div>

Datei anzeigen

@ -2,7 +2,8 @@
{{combo-box {{combo-box
value=connector value=connector
content=connectors content=connectors
onChange=(action (mut connector))}} onChange=(action (mut connector))
onOpen=(action "onOpen")}}
{{else}} {{else}}
<span class="connector-single"> <span class="connector-single">
{{connectorLabel}} {{connectorLabel}}

Datei anzeigen

@ -1,19 +1,17 @@
<div class="type-selector"> <div class="type-selector">
{{#if hasTypes}} {{#if hasTypes}}
<a {{action "toggleTypes"}} class="active"> <a {{action "showTypes"}} class="active">
{{activeTypeLabel}} {{activeTypeLabel}}
</a> </a>
{{#if showTypes}} <div class="selector-types">
<div class="selector-types"> {{#each selectorTypes as |item|}}
{{#each selectorTypes as |item|}} {{wizard-mapper-selector-type
{{wizard-mapper-selector-type activeType=activeType
activeType=activeType item=item
item=item toggle=(action 'toggleType')}}
toggle=(action 'toggleType')}} {{/each}}
{{/each}} </div>
</div>
{{/if}}
{{else}} {{else}}
<span>{{activeTypeLabel}}</span> <span>{{activeTypeLabel}}</span>
{{/if}} {{/if}}
@ -24,6 +22,7 @@
{{input {{input
type="text" type="text"
value=value value=value
click=(action 'enableActive')
placeholder=(i18n placeholderKey)}} placeholder=(i18n placeholderKey)}}
{{/if}} {{/if}}
@ -32,6 +31,8 @@
value=value value=value
content=comboBoxContent content=comboBoxContent
onChange=(action (mut value)) onChange=(action (mut value))
onOpen=(action "enableActive")
onClick=(action 'enableActive')
options=(hash options=(hash
none=placeholderKey none=placeholderKey
)}} )}}
@ -42,6 +43,8 @@
content=multiSelectContent content=multiSelectContent
value=value value=value
onChange=(action (mut value)) onChange=(action (mut value))
onOpen=(action "enableActive")
onClose=(action "disableActive")
options=multiSelectOptions}} options=multiSelectOptions}}
{{/if}} {{/if}}
@ -55,6 +58,8 @@
{{tag-chooser {{tag-chooser
tags=value tags=value
filterable=true filterable=true
onOpen=(action "enableActive")
onClose=(action "disableActive")
options=(hash options=(hash
none=placeholderKey none=placeholderKey
)}} )}}
@ -65,6 +70,7 @@
includeMessageableGroups='true' includeMessageableGroups='true'
placeholderKey=placeholderKey placeholderKey=placeholderKey
usernames=value usernames=value
autocomplete="discourse"}} autocomplete="discourse"
click=(action "enableActive")}}
{{/if}} {{/if}}
</div> </div>

Datei anzeigen

@ -161,7 +161,7 @@ export default {
const fields = {}; const fields = {};
this.get('fields').forEach(f => { this.get('fields').forEach(f => {
if (f.type !== 'text-only') { if (f.type !== 'text_only') {
fields[f.id] = f.value; fields[f.id] = f.value;
} }
}); });
@ -218,8 +218,8 @@ export default {
inputComponentName: function() { inputComponentName: function() {
const type = this.get('field.type'); const type = this.get('field.type');
const id = this.get('field.id'); const id = this.get('field.id');
if (['text-only'].includes(type)) return false; if (['text_only'].includes(type)) return false;
return (type === 'component') ? dasherize(id) : `wizard-field-${type}`; return dasherize((type === 'component') ? id : `wizard-field-${type}`);
}.property('field.type', 'field.id') }.property('field.type', 'field.id')
}); });
@ -230,8 +230,8 @@ export default {
'dropdown', 'dropdown',
'tag', 'tag',
'image', 'image',
'user-selector', 'user_selector',
'text-only', 'text_only',
'composer', 'composer',
'category', 'category',
'group' 'group'

Datei anzeigen

@ -2,18 +2,21 @@
@import 'wizard-transfer'; @import 'wizard-transfer';
@import 'wizard-api'; @import 'wizard-api';
$setting-background: dark-light-diff($primary, $secondary, 96%, -65%);
body.admin-wizard { body.admin-wizard {
.admin-container > .row {
display: flex;
}
.boxed.white { .boxed.white {
background-color: initial; background-color: initial;
} }
} }
.wizard-list { .wizard-list {
float: left;
width: 250px; width: 250px;
min-width: 250px;
margin-top: 10px; margin-top: 10px;
float: none;
} }
.wizard-settings-parent { .wizard-settings-parent {
@ -46,7 +49,7 @@ body.admin-wizard {
.wizard-custom-field { .wizard-custom-field {
position: relative; position: relative;
background: transparent; background: transparent;
background-color: $setting-background; background-color: dark-light-diff($primary, $secondary, 96%, -65%);
padding: 20px; padding: 20px;
} }
@ -93,9 +96,11 @@ body.admin-wizard {
font-size: 1.5em; font-size: 1.5em;
min-height: 31px; min-height: 31px;
margin-bottom: 30px; margin-bottom: 30px;
display: flex;
input { input {
margin-bottom: 0; margin-bottom: 0;
width: 350px;
} }
} }
@ -113,11 +118,18 @@ body.admin-wizard {
} }
.wizard-url { .wizard-url {
display: inline-block; display: inline-flex;
font-size: 1rem;
margin-left: 20px; margin-left: 20px;
background-color: $setting-background;
padding: 2px 6px; a {
padding: 6px 12px;
font-size: 1rem;
background-color: $primary-low;
}
button {
font-size: 1rem;
}
} }
} }
@ -125,10 +137,6 @@ body.admin-wizard {
margin-top: 20px; margin-top: 20px;
} }
.content-list + .content {
overflow: hidden;
}
.admin-wizard.settings { .admin-wizard.settings {
margin-top: 10px; margin-top: 10px;
margin-left: 30px; margin-left: 30px;
@ -358,16 +366,19 @@ body.admin-wizard {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
ul { .link-list {
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
display: inline-block; display: inline-flex;
align-items: flex-start;
flex-flow: wrap;
li { > div {
display: inline-block; display: flex;
margin-bottom: 7px; align-items: center;
margin-right: 7px; margin-bottom: 10px;
margin-right: 10px;
} }
} }

Datei anzeigen

@ -100,13 +100,44 @@
} }
} }
.mapper-input.assignment,
.mapper-input.validation,
.mapper-input.association {
.mapper-selector {
max-width: 250px;
min-width: 250px;
> input, .select-kit, .ac-wrap, .autocomplete.ac-user {
width: 250px !important;
}
}
}
.mapper-input.conditional {
.mapper-selector {
max-width: 170px;
min-width: 170px;
&:not(.text).active .input {
width: 250px;
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.2);
position: absolute;
z-index: 300;
.select-kit, .ac-wrap, .autocomplete.ac-user, .select-kit-wrapper {
width: 250px !important;
}
}
}
}
.mapper-selector { .mapper-selector {
width: 100%; width: 100%;
max-width: 150px; position: relative;
min-width: 150px;
input, .select-kit, .ac-wrap, .autocomplete.ac-user { .input {
width: 150px !important; width: 100%;
transition: all 0.1s;
} }
.type-selector { .type-selector {
@ -139,12 +170,16 @@
.selector-types { .selector-types {
box-shadow: shadow('dropdown'); box-shadow: shadow('dropdown');
position: absolute; position: absolute;
display: none;
background: $secondary; background: $secondary;
z-index: 200; z-index: 200;
padding: 5px 7px; padding: 5px 7px;
display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid $primary-low; border: 1px solid $primary-low;
&.show {
display: flex;
}
} }
.value-list .remove-value-btn { .value-list .remove-value-btn {

Datei anzeigen

@ -95,6 +95,7 @@ en:
list: "Enter item" list: "Enter item"
error: error:
failed: "failed to save wizard"
required: "{{type}} requires {{property}}" required: "{{type}} requires {{property}}"
invalid: "{{property}} is invalid" invalid: "{{property}} is invalid"
dependent: "{{property}} is dependent on {{dependent}}" dependent: "{{property}} is dependent on {{dependent}}"

Datei anzeigen

@ -18,16 +18,17 @@ class CustomWizard::AdminController < ::ApplicationController
else else
wizard = result[:wizard] wizard = result[:wizard]
existing_wizard = result[:existing_wizard] existing_wizard = result[:existing_wizard]
after_time = result[:after_time]
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
PluginStore.set('custom_wizard', wizard["id"], wizard) PluginStore.set('custom_wizard', wizard["id"], wizard)
if wizard['after_time'] && result[:new_after_time] if after_time[:enabled]
Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id']) 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']) Jobs.enqueue_at(after_time[:scheduled], :set_after_time_wizard, wizard_id: wizard['id'])
end end
if existing_wizard && existing_wizard['after_time'] && !wizard['after_time'] if existing_wizard && existing_wizard['after_time'] && !after_time[:enabled]
Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id']) Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard['id'])
Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard['id']) Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard['id'])
end end
@ -181,19 +182,24 @@ class CustomWizard::AdminController < ::ApplicationController
def validate_after_time(wizard, existing_wizard) def validate_after_time(wizard, existing_wizard)
new = false new = false
error = nil error = nil
enabled = false
scheduled = nil
if wizard["after_time"] if wizard["after_time"]
enabled = true
if !wizard["after_time_scheduled"] && !existing_wizard["after_time_scheduled"] if !wizard["after_time_scheduled"] && !existing_wizard["after_time_scheduled"]
error = 'after_time_need_time' error = 'after_time_need_time'
else else
after_time_scheduled = Time.parse(wizard["after_time_scheduled"]).utc scheduled = Time.parse(wizard["after_time_scheduled"]).utc
new = false
new = existing_wizard['after_time_scheduled'] ? if existing_wizard['after_time_scheduled']
after_time_scheduled != Time.parse(existing_wizard['after_time_scheduled']).utc : new = scheduled != Time.parse(existing_wizard['after_time_scheduled']).utc
true end
begin begin
error = 'after_time_invalid' if new && after_time_scheduled < Time.now.utc error = 'after_time_invalid' if new && scheduled < Time.now.utc
rescue ArgumentError rescue ArgumentError
error = 'after_time_invalid' error = 'after_time_invalid'
end end
@ -203,7 +209,11 @@ class CustomWizard::AdminController < ::ApplicationController
if error if error
{ error: { type: error } } { error: { type: error } }
else else
{ new: new } {
new: new,
scheduled: scheduled,
enabled: enabled
}
end end
end end
@ -214,8 +224,8 @@ class CustomWizard::AdminController < ::ApplicationController
validation = validate_wizard(wizard) validation = validate_wizard(wizard)
return validation if validation[:error] return validation if validation[:error]
after_time_validation = validate_after_time(wizard, existing_wizard) after_time = validate_after_time(wizard, existing_wizard)
return after_time_validation if after_time_validation[:error] return after_time if after_time[:error]
wizard['steps'].each do |step| wizard['steps'].each do |step|
if step['raw_description'] if step['raw_description']
@ -223,10 +233,15 @@ class CustomWizard::AdminController < ::ApplicationController
end end
end end
{ result = {
wizard: wizard, wizard: wizard,
existing_wizard: existing_wizard, existing_wizard: existing_wizard
new_after_time: after_time_validation[:new]
} }
if after_time[:enabled]
result[:after_time] = after_time
end
result
end end
end end

Datei anzeigen

@ -124,7 +124,7 @@ class CustomWizard::Builder
if step_template['fields'] && step_template['fields'].length if step_template['fields'] && step_template['fields'].length
step_template['fields'].each do |field| step_template['fields'].each do |field|
validate_field(field, updater, step_template) if field['type'] != 'text-only' validate_field(field, updater, step_template) if field['type'] != 'text_only'
end end
end end

Datei anzeigen

@ -1,6 +1,6 @@
class CustomWizard::Field class CustomWizard::Field
def self.types def self.types
@types ||= ['checkbox', 'composer', 'dropdown', 'tag', 'category', 'group', 'text', 'textarea', 'text-only', 'number', 'upload', 'user-selector', 'url'] @types ||= ['text', 'textarea', 'composer', 'text_only', 'number', 'checkbox', 'url', 'upload', 'dropdown', 'tag', 'category', 'group', 'user_selector']
end end
def self.require_assets def self.require_assets

Datei anzeigen

@ -0,0 +1,92 @@
describe CustomWizard::Action do
let(:create_topic_action) {{"id":"create_topic","type":"create_topic","title":"text","post":"textarea"}}
let(:send_message_action) {{"id":"send_message","type":"send_message","title":"text","post":"textarea","username":"angus"}}
let(:route_to_action) {{"id":"route_to","type":"route_to","url":"https://google.com"}}
let(:open_composer_action) {{"id":"open_composer","type":"open_composer","title":"text","post":"textarea"}}
let(:add_to_group_action) {{"id":"add_to_group","type":"add_to_group","group_id":"dropdown_groups"}}
it 'creates a topic' do
template['steps'][0]['fields'] = [text_field, textarea_field]
template['steps'][0]["actions"] = [create_topic_action]
updater = run_update(template, nil,
text: "Topic Title",
textarea: "topic body"
)
topic = Topic.where(title: "Topic Title")
expect(topic.exists?).to eq(true)
expect(Post.where(
topic_id: topic.pluck(:id),
raw: "topic body"
).exists?).to eq(true)
end
it 'sends a message' do
fields = [text_field, textarea_field]
if extra_field
fields.push(extra_field)
end
template['steps'][0]['fields'] = fields
template['steps'][0]["actions"] = [send_message_action.merge(extra_action_opts)]
run_update(template, nil,
text: "Message Title",
textarea: "message body"
)
topic = Topic.where(
archetype: Archetype.private_message,
title: "Message Title"
)
expect(topic.exists?).to eq(true)
expect(
topic.first.topic_allowed_users.first.user.username
).to eq('angus')
expect(Post.where(
topic_id: topic.pluck(:id),
raw: "message body"
).exists?).to eq(true)
end
it 'updates a profile' do
run_update(template, template['steps'][1]['id'], name: "Sally")
expect(user.name).to eq('Sally')
end
it 'opens a composer' do
template['steps'][0]['fields'] = [text_field, textarea_field]
template['steps'][0]["actions"] = [open_composer_action]
updater = run_update(template, nil,
text: "Topic Title",
textarea: "topic body"
)
expect(updater.result.blank?).to eq(true)
updater = run_update(template, template['steps'][1]['id'])
expect(updater.result[:redirect_on_complete]).to eq(
"/new-topic?title=Topic%20Title&body=topic%20body"
)
end
it 'adds a user to a group' do
template['steps'][0]['fields'] = [dropdown_groups_field]
template['steps'][0]["actions"] = [add_to_group_action]
updater = run_update(template, nil, dropdown_groups: group.id)
expect(group.users.first.username).to eq('angus')
end
it 're-routes a user' do
template['steps'][0]["actions"] = [route_to_action]
updater = run_update(template, nil, {})
expect(updater.result[:redirect_on_next]).to eq(
"https://google.com"
)
end
end

Datei anzeigen

@ -15,27 +15,6 @@ describe CustomWizard::Builder do
).read) ).read)
end end
let(:permitted_params) {[{"key":"param_key","value":"submission_param_key"}]}
let(:required_data) {[{"key":"nickname","connector":"equals","value":"name"}]}
let(:required_data_message) {"Nickname is required to match your name"}
let(:checkbox_field) {{"id":"checkbox","type":"checkbox","label":"Checkbox"}}
let(:composer_field) {{"id": "composer","label":"Composer","type":"composer"}}
let(:tag_field) {{"id": "tag","type": "tag","label": "Tag","limit": "2"}}
let(:category_field) {{"id": "category","type": "category","limit": "1","label": "Category"}}
let(:image_field) {{"id": "image","type": "image","label": "Image"}}
let(:text_field) {{"id": "text","type": "text","label": "Text"}}
let(:textarea_field) {{"id": "textarea","type": "textarea","label": "Textarea"}}
let(:text_only_field) {{"id": "text_only","type": "text-only","label": "Text only"}}
let(:upload_field) {{"id": "upload","type": "upload","file_types": ".jpg,.png,.pdf","label": "Upload"}}
let(:user_selector_field) {{"id": "user_selector","type": "user-selector","label": "User selector"}}
let(:create_topic_action) {{"id":"create_topic","type":"create_topic","title":"text","post":"textarea"}}
let(:send_message_action) {{"id":"send_message","type":"send_message","title":"text","post":"textarea","username":"angus"}}
let(:route_to_action) {{"id":"route_to","type":"route_to","url":"https://google.com"}}
let(:open_composer_action) {{"id":"open_composer","type":"open_composer","title":"text","post":"textarea"}}
let(:add_to_group_action) {{"id":"add_to_group","type":"add_to_group","group_id":"dropdown_groups"}}
def build_wizard(t = template, u = user, build_opts = {}, params = {}) def build_wizard(t = template, u = user, build_opts = {}, params = {})
CustomWizard::Wizard.add_wizard(t) CustomWizard::Wizard.add_wizard(t)
CustomWizard::Builder.new(u, 'welcome').build(build_opts, params) CustomWizard::Builder.new(u, 'welcome').build(build_opts, params)
@ -59,22 +38,6 @@ describe CustomWizard::Builder do
updater updater
end end
def send_message(extra_field = nil, extra_action_opts = {})
fields = [text_field, textarea_field]
if extra_field
fields.push(extra_field)
end
template['steps'][0]['fields'] = fields
template['steps'][0]["actions"] = [send_message_action.merge(extra_action_opts)]
run_update(template, nil,
text: "Message Title",
textarea: "message body"
)
end
context 'disabled' do context 'disabled' do
before do before do
SiteSetting.custom_wizard_enabled = false SiteSetting.custom_wizard_enabled = false
@ -101,7 +64,7 @@ describe CustomWizard::Builder do
expect(build_wizard.steps.length).to eq(2) expect(build_wizard.steps.length).to eq(2)
end end
it 'returns no steps if the multiple submissions are disabled and user has completed it' do it 'returns no steps if multiple submissions are disabled and user has completed' do
history_params = { history_params = {
action: UserHistory.actions[:custom_wizard_step], action: UserHistory.actions[:custom_wizard_step],
acting_user_id: user.id, acting_user_id: user.id,
@ -114,12 +77,12 @@ describe CustomWizard::Builder do
expect(build_wizard(template).steps.length).to eq(0) expect(build_wizard(template).steps.length).to eq(0)
end end
it 'returns no steps if has min trust and user does not meet it' do it 'returns no steps if user is not permitted' do
template["min_trust"] = 3 template["min_trust"] = 3
expect(build_wizard(template).steps.length).to eq(0) expect(build_wizard(template).steps.length).to eq(0)
end end
it 'returns steps if it has min trust and user meets it' do it 'returns steps if user is permitted' do
template["min_trust"] = 3 template["min_trust"] = 3
expect(build_wizard(template, trusted_user).steps.length).to eq(2) expect(build_wizard(template, trusted_user).steps.length).to eq(2)
end end
@ -156,12 +119,6 @@ describe CustomWizard::Builder do
expect(build_wizard(template, user).steps[0].permitted).to eq(false) expect(build_wizard(template, user).steps[0].permitted).to eq(false)
end end
it "is not permitted if required data is not present" do
template['steps'][0]['required_data'] = required_data
add_submission_data(nickname: "John")
expect(build_wizard(template, user).steps[0].permitted).to eq(false)
end
it 'it shows required data message if required data has message' do it 'it shows required data message if required data has message' do
template['steps'][0]['required_data'] = required_data template['steps'][0]['required_data'] = required_data
template['steps'][0]['required_data_message'] = required_data_message template['steps'][0]['required_data_message'] = required_data_message
@ -217,151 +174,9 @@ describe CustomWizard::Builder do
end end
end end
context 'actions' do it 'runs actions attached to a step' do
it 'runs actions attached to a step' do run_update(template, template['steps'][1]['id'], name: "Gus")
run_update(template, template['steps'][1]['id'], name: "Gus") expect(user.name).to eq('Gus')
expect(user.name).to eq('Gus')
end
it 'interpolates user data correctly' do
user.name = "Angus"
user.save!
expect(
CustomWizard::Builder.fill_placeholders(
"My name is u{name}",
user,
{}
)
).to eq('My name is Angus')
end
it 'creates a topic' do
template['steps'][0]['fields'] = [text_field, textarea_field]
template['steps'][0]["actions"] = [create_topic_action]
updater = run_update(template, nil,
text: "Topic Title",
textarea: "topic body"
)
topic = Topic.where(title: "Topic Title")
expect(topic.exists?).to eq(true)
expect(Post.where(
topic_id: topic.pluck(:id),
raw: "topic body"
).exists?).to eq(true)
end
it 'creates a topic with a custom title' do
user.name = "Angus"
user.save!
template['steps'][0]['fields'] = [text_field, textarea_field]
create_topic_action['custom_title_enabled'] = true
create_topic_action['custom_title'] = "u{name}' Topic Title"
template['steps'][0]["actions"] = [create_topic_action]
run_update(template, nil, textarea: "topic body")
topic = Topic.where(title: "Angus' Topic Title")
expect(topic.exists?).to eq(true)
expect(Post.where(
topic_id: topic.pluck(:id),
raw: "topic body"
).exists?).to eq(true)
end
it 'creates a topic with a custom post' do
user.name = "Angus"
user.save!
template['steps'][0]['fields'] = [text_field, textarea_field]
create_topic_action['post_builder'] = true
create_topic_action['post_template'] = "u{name}' w{textarea}"
template['steps'][0]["actions"] = [create_topic_action]
run_update(template, nil,
text: "Topic Title",
textarea: "topic body"
)
topic = Topic.where(title: "Topic Title")
expect(topic.exists?).to eq(true)
expect(Post.where(
topic_id: topic.pluck(:id),
raw: "Angus' topic body"
).exists?).to eq(true)
end
it 'sends a message' do
send_message
topic = Topic.where(
archetype: Archetype.private_message,
title: "Message Title"
)
expect(topic.exists?).to eq(true)
expect(
topic.first.topic_allowed_users.first.user.username
).to eq('angus')
expect(Post.where(
topic_id: topic.pluck(:id),
raw: "message body"
).exists?).to eq(true)
end
it 'doesnt sent a message if the required data is not present' do
send_message(user_selector_field, required: "user_selector")
topic = Topic.where(
archetype: Archetype.private_message,
title: "Message Title"
)
expect(topic.exists?).to eq(false)
end
it 'updates a profile' do
run_update(template, template['steps'][1]['id'], name: "Sally")
expect(user.name).to eq('Sally')
end
it 'opens a composer' do
template['steps'][0]['fields'] = [text_field, textarea_field]
template['steps'][0]["actions"] = [open_composer_action]
updater = run_update(template, nil,
text: "Topic Title",
textarea: "topic body"
)
expect(updater.result.blank?).to eq(true)
updater = run_update(template, template['steps'][1]['id'])
expect(updater.result[:redirect_on_complete]).to eq(
"/new-topic?title=Topic%20Title&body=topic%20body"
)
end
it 'adds a user to a group' do
template['steps'][0]['fields'] = [dropdown_groups_field]
template['steps'][0]["actions"] = [add_to_group_action]
updater = run_update(template, nil, dropdown_groups: group.id)
expect(group.users.first.username).to eq('angus')
end
it 're-routes a user' do
template['steps'][0]["actions"] = [route_to_action]
updater = run_update(template, nil, {})
expect(updater.result[:redirect_on_next]).to eq(
"https://google.com"
)
end
end end
end end
end end

Datei anzeigen

@ -0,0 +1,14 @@
describe CustomWizard::Mapper do
it 'interpolates user data' do
user.name = "Angus"
user.save!
expect(
CustomWizard::Builder.fill_placeholders(
"My name is u{name}",
user,
{}
)
).to eq('My name is Angus')
end