various
Dieser Commit ist enthalten in:
Ursprung
dd26ac63af
Commit
e859e3efa2
39 geänderte Dateien mit 892 neuen und 439 gelöschten Zeilen
|
@ -15,20 +15,7 @@ class CustomWizard::AdminController < ::ApplicationController
|
|||
|
||||
wizard = ::JSON.parse(params[:wizard])
|
||||
|
||||
saved = false
|
||||
if wizard["existing_id"] && rows = PluginStoreRow.where(plugin_name: 'custom_wizard').order(:id)
|
||||
rows.each do |r, i|
|
||||
wizard = CustomWizard::Wizard.new(r.value)
|
||||
if wizard.id = wizard["existing_id"]
|
||||
r.update_all(key: wizard['id'], value: wizard)
|
||||
saved = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless saved
|
||||
PluginStore.set('custom_wizard', wizard["id"], wizard)
|
||||
end
|
||||
PluginStore.set('custom_wizard', wizard["id"], wizard)
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<link href="<%= Discourse.base_uri %>/plugins/discourse-custom-wizard/desktop.css" media="all" rel="stylesheet" data-target="desktop" type="text/css" />
|
||||
<%= discourse_stylesheet_link_tag :wizard, theme_key: nil %>
|
||||
<%= discourse_stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %>
|
||||
<%= stylesheet_link_tag "wizard_custom", media: "all", "data-turbolinks-track" => "reload" %>
|
||||
<%= preload_script "ember_jquery" %>
|
||||
<%= preload_script "wizard-vendor" %>
|
||||
<%= preload_script "wizard-application" %>
|
||||
|
@ -18,7 +17,7 @@
|
|||
<meta name="discourse-base-uri" content="<%= Discourse.base_uri %>">
|
||||
|
||||
<%= render partial: "layouts/head" %>
|
||||
<title><%= t 'custom_wizard.title' %></title>
|
||||
<title><%= t 'wizard.custom_title' %></title>
|
||||
</head>
|
||||
|
||||
<body class='custom-wizard'>
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
import { on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: 'wizard-custom-action',
|
||||
types: ['create_topic', 'update_profile', 'send_message'],
|
||||
profileFields: ['name', 'username', 'email'],
|
||||
createTopic: Ember.computed.equal('action.type', 'create_topic'),
|
||||
updateProfile: Ember.computed.equal('action.type', 'update_profile'),
|
||||
sendMessage: Ember.computed.equal('action.type', 'send_message')
|
||||
sendMessage: Ember.computed.equal('action.type', 'send_message'),
|
||||
|
||||
@on('init')
|
||||
@observes('action')
|
||||
setup() {
|
||||
this.set('existingId', this.get('action.id'));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -5,19 +5,43 @@ export default Ember.Component.extend({
|
|||
isDropdown: Ember.computed.equal('field.type', 'dropdown'),
|
||||
|
||||
@on('init')
|
||||
@observes('field.id')
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
if (!this.get('field.choices')) {
|
||||
this.set('field.choices', Ember.A());
|
||||
}
|
||||
@observes('field')
|
||||
setup() {
|
||||
this.set('existingId', this.get('field.id'));
|
||||
},
|
||||
|
||||
@computed('field.type')
|
||||
isInput: (type) => type === 'text' || type === 'textarea',
|
||||
|
||||
@computed('field.choices.[]')
|
||||
dropdownChoices: choices => choices,
|
||||
|
||||
@computed('field.choices_filters.[]')
|
||||
presetFilters: filters => filters,
|
||||
|
||||
@computed()
|
||||
presetChoices() {
|
||||
return [
|
||||
{ id: 'categories', name: I18n.t('admin.wizard.field.choices_preset.categories') }
|
||||
];
|
||||
},
|
||||
|
||||
actions: {
|
||||
addFilter() {
|
||||
if (!this.get('field.choices_filters')) {
|
||||
this.set('field.choices_filters', Ember.A());
|
||||
}
|
||||
this.get('field.choices_filters').pushObject(Ember.Object.create());
|
||||
},
|
||||
|
||||
removeFilter(f) {
|
||||
this.get('field.choices_filters').removeObject(f);
|
||||
},
|
||||
|
||||
addChoice() {
|
||||
if (!this.get('field.choices')) {
|
||||
this.set('field.choices', Ember.A());
|
||||
}
|
||||
this.get('field.choices').pushObject(Ember.Object.create());
|
||||
},
|
||||
|
||||
|
|
|
@ -7,15 +7,15 @@ export default Ember.Component.extend({
|
|||
|
||||
@on('init')
|
||||
@observes('step')
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
setCurrent() {
|
||||
this.set('existingId', this.get('step.id'));
|
||||
const fields = this.get('step.fields') || [];
|
||||
const actions = this.get('step.actions') || [];
|
||||
this.set('currentField', fields[0]);
|
||||
this.set('currentAction', actions[0]);
|
||||
},
|
||||
|
||||
@computed('step.fields.[]', 'currentField')
|
||||
@computed('step.fields.@each.id', 'currentField')
|
||||
fieldLinks(fields, current) {
|
||||
if (!fields) return;
|
||||
|
||||
|
@ -24,7 +24,7 @@ export default Ember.Component.extend({
|
|||
const id = f.get('id');
|
||||
const label = f.get('label');
|
||||
|
||||
let link = { id, label: label || id };
|
||||
let link = { id, label: label || id || 'new' };
|
||||
|
||||
let classes = 'btn';
|
||||
if (current && f.get('id') === current.get('id')) {
|
||||
|
@ -38,7 +38,7 @@ export default Ember.Component.extend({
|
|||
});
|
||||
},
|
||||
|
||||
@computed('step.actions.[]', 'currentAction')
|
||||
@computed('step.actions.@each.id', 'currentAction')
|
||||
actionLinks(actions, current) {
|
||||
if (!actions) return;
|
||||
|
||||
|
@ -47,7 +47,7 @@ export default Ember.Component.extend({
|
|||
const id = a.get('id');
|
||||
const label = a.get('label');
|
||||
|
||||
let link = { id, label: label || id };
|
||||
let link = { id, label: label || id || 'new' };
|
||||
|
||||
let classes = 'btn';
|
||||
if (current && a.get('id') === current.get('id')) {
|
||||
|
@ -64,20 +64,14 @@ export default Ember.Component.extend({
|
|||
actions: {
|
||||
addField() {
|
||||
const fields = this.get('step.fields');
|
||||
const newNum = fields.length + 1;
|
||||
const field = Ember.Object.create({
|
||||
id: `field-${newNum}`
|
||||
});
|
||||
const field = Ember.Object.create();
|
||||
fields.pushObject(field);
|
||||
this.set('currentField', field);
|
||||
},
|
||||
|
||||
addAction() {
|
||||
const actions = this.get('step.actions');
|
||||
const newNum = actions.length + 1;
|
||||
const action = Ember.Object.create({
|
||||
id: `action-${newNum}`
|
||||
});
|
||||
const action = Ember.Object.create();
|
||||
actions.pushObject(action);
|
||||
this.set('currentAction', action);
|
||||
},
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
@computed('model.steps.[]', 'currentStep')
|
||||
@computed('model.steps.@each.id', 'currentStep')
|
||||
stepLinks(steps, currentStep) {
|
||||
return steps.map((s) => {
|
||||
if (s) {
|
||||
const id = s.get('id');
|
||||
const title = s.get('title');
|
||||
|
||||
let link = { id, title: title || id };
|
||||
let link = { id, title: title || id || 'new' };
|
||||
|
||||
let classes = 'btn';
|
||||
if (currentStep && id === currentStep.get('id')) {
|
||||
|
@ -25,17 +24,27 @@ export default Ember.Controller.extend({
|
|||
|
||||
@computed('model.id', 'model.name')
|
||||
wizardUrl(wizardId) {
|
||||
return window.location.origin + '/wizard/custom/' + Ember.String.dasherize(wizardId);
|
||||
return window.location.origin + '/w/' + Ember.String.dasherize(wizardId);
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.get('model').save().then(() => {
|
||||
this.setProperties({
|
||||
saving: true,
|
||||
error: null
|
||||
});
|
||||
const wizard = this.get('model');
|
||||
wizard.save().then(() => {
|
||||
this.set('saving', false);
|
||||
if (this.get('newWizard')) {
|
||||
this.send("refreshAllWizards");
|
||||
} else {
|
||||
this.send("refreshWizard");
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.set('saving', false);
|
||||
this.set('error', I18n.t(`admin.wizard.error.${error}`));
|
||||
Ember.run.later(() => this.set('error', null), 10000);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -47,11 +56,9 @@ export default Ember.Controller.extend({
|
|||
|
||||
addStep() {
|
||||
const steps = this.get('model.steps');
|
||||
const newNum = steps.length + 1;
|
||||
const step = Ember.Object.create({
|
||||
fields: Ember.A(),
|
||||
actions: Ember.A(),
|
||||
id: `step-${newNum}`
|
||||
actions: Ember.A()
|
||||
});
|
||||
steps.pushObject(step);
|
||||
this.set('currentStep', step);
|
||||
|
|
|
@ -1,90 +1,90 @@
|
|||
import { observes, on } from 'ember-addons/ember-computed-decorators';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
const CustomWizard = Discourse.Model.extend({
|
||||
@on('init')
|
||||
setup() {
|
||||
const id = this.get('id');
|
||||
if (id) this.set('existingId', id);
|
||||
},
|
||||
|
||||
@observes('name')
|
||||
updateId() {
|
||||
const name = this.get('name');
|
||||
this.set('id', name.underscore());
|
||||
},
|
||||
|
||||
save() {
|
||||
const stepsObj = this.get('steps');
|
||||
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||
const id = this.get('id');
|
||||
if (!id || !id.underscore()) reject('id_required');
|
||||
|
||||
let wizard = { id: id.underscore() };
|
||||
|
||||
const steps = this.get('steps');
|
||||
if (steps.length) wizard['steps'] = this.buildSteps(steps, reject);
|
||||
|
||||
const name = this.get('name');
|
||||
if (name) wizard['name'] = name;
|
||||
|
||||
const background = this.get('background');
|
||||
if (background) wizard['background'] = background;
|
||||
|
||||
const save_submissions = this.get('save_submissions');
|
||||
if (save_submissions) wizard['save_submissions'] = save_submissions;
|
||||
|
||||
const multiple_submissions = this.get('multiple_submissions');
|
||||
if (multiple_submissions) wizard['multiple_submissions'] = multiple_submissions;
|
||||
|
||||
ajax("/admin/wizards/custom/save", {
|
||||
type: 'PUT',
|
||||
data: {
|
||||
wizard: JSON.stringify(wizard)
|
||||
}
|
||||
}).then((result) => resolve(result));
|
||||
});
|
||||
},
|
||||
|
||||
buildSteps(stepsObj, reject) {
|
||||
let steps = [];
|
||||
|
||||
stepsObj.forEach((s) => {
|
||||
stepsObj.some((s) => {
|
||||
if (!s.id || !s.id.underscore()) reject('id_required');
|
||||
|
||||
if (!s.title && !s.translation_key) return;
|
||||
|
||||
let step = {
|
||||
id: (s.title || s.translation_key.split('.').pop()).underscore(),
|
||||
fields: [],
|
||||
actions: []
|
||||
};
|
||||
let step = { id: s.id.underscore() };
|
||||
|
||||
if (s.title) step['title'] = s.title;
|
||||
if (s.translation_key) step['translation_key'] = s.translation_key;
|
||||
if (s.key) step['key'] = s.key;
|
||||
if (s.banner) step['banner'] = s.banner;
|
||||
if (s.description) step['description'] = s.description;
|
||||
|
||||
const fields = s.get('fields');
|
||||
fields.forEach((f) => {
|
||||
const fl = f.get('label');
|
||||
const fkey = f.get('translation_key');
|
||||
if (fields.length) {
|
||||
step['fields'] = [];
|
||||
|
||||
if (!fl && !fkey) return;
|
||||
fields.some((f) => {
|
||||
let id = f.get('id');
|
||||
|
||||
f.set('id', (fl || fkey.split('.').pop()).underscore());
|
||||
if (!id || !id.underscore()) reject('id_required');
|
||||
f.set('id', id.underscore());
|
||||
|
||||
if (f.get('type') === 'dropdown') {
|
||||
const choices = f.get('choices');
|
||||
if (f.get('type') === 'dropdown') {
|
||||
const choices = f.get('choices');
|
||||
if (choices && choices.length < 1 && !f.get('choices_key') && !f.get('choices_categories')) {
|
||||
reject('field.need_choices');
|
||||
}
|
||||
}
|
||||
|
||||
choices.forEach((c) => {
|
||||
const cl = c.get('label');
|
||||
const ckey = c.get('translation_key');
|
||||
step['fields'].push(f);
|
||||
});
|
||||
}
|
||||
|
||||
if (!cl && !ckey) return;
|
||||
const actions = s.actions;
|
||||
if (actions.length) {
|
||||
step['actions'] = [];
|
||||
|
||||
c.set('id', (cl || ckey.split('.').pop()).underscore());
|
||||
});
|
||||
}
|
||||
actions.some((a) => {
|
||||
let id = a.get('id');
|
||||
if (!id || !id.underscore()) reject('id_required');
|
||||
|
||||
step['fields'].push(f);
|
||||
});
|
||||
a.set('id', id.underscore());
|
||||
|
||||
s.actions.forEach((a) => {
|
||||
const al = a.get('label');
|
||||
if (!al) return;
|
||||
a.set('id', al.underscore());
|
||||
step['actions'].push(a);
|
||||
});
|
||||
step['actions'].push(a);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
steps.push(step);
|
||||
});
|
||||
|
||||
const id = this.get('id');
|
||||
const name = this.get('name');
|
||||
const background = this.get('background');
|
||||
const save_submissions = this.get('save_submissions');
|
||||
let wizard = { id, name, background, save_submissions, steps };
|
||||
|
||||
const existingId = this.get('existingId');
|
||||
if (existingId && existingId !== id) {
|
||||
wizard['existing_id'] = existingId;
|
||||
};
|
||||
|
||||
return ajax("/admin/wizards/custom/save", {
|
||||
type: 'PUT',
|
||||
data: {
|
||||
wizard: JSON.stringify(wizard)
|
||||
}
|
||||
});
|
||||
return steps;
|
||||
},
|
||||
|
||||
remove() {
|
||||
|
@ -121,35 +121,49 @@ CustomWizard.reopenClass({
|
|||
|
||||
if (w) {
|
||||
props['id'] = w.id;
|
||||
props['existingId'] = true;
|
||||
props['name'] = w.name;
|
||||
props['background'] = w.background;
|
||||
props['save_submissions'] = w.save_submissions;
|
||||
props['multiple_submissions'] = w.multiple_submissions;
|
||||
|
||||
if (w.steps) {
|
||||
if (w.steps && w.steps.length) {
|
||||
w.steps.forEach((s) => {
|
||||
let fields = Ember.A();
|
||||
// clean empty strings
|
||||
Object.keys(s).forEach((key) => (s[key] === '') && delete s[key]);
|
||||
|
||||
s.fields.forEach((f) => {
|
||||
let field = Ember.Object.create(f);
|
||||
let choices = Ember.A();
|
||||
let fields = Ember.A();
|
||||
|
||||
f.choices.forEach((c) => {
|
||||
choices.pushObject(Ember.Object.create(c));
|
||||
if (s.fields && s.fields.length) {
|
||||
s.fields.forEach((f) => {
|
||||
Object.keys(f).forEach((key) => (f[key] === '') && delete f[key]);
|
||||
|
||||
let field = Ember.Object.create(f);
|
||||
|
||||
if (f.choices) {
|
||||
let choices = Ember.A();
|
||||
|
||||
f.choices.forEach((c) => {
|
||||
choices.pushObject(Ember.Object.create(c));
|
||||
});
|
||||
|
||||
field.set('choices', choices);
|
||||
}
|
||||
|
||||
fields.pushObject(field);
|
||||
});
|
||||
|
||||
field.set('choices', choices);
|
||||
|
||||
fields.pushObject(field);
|
||||
});
|
||||
}
|
||||
|
||||
let actions = Ember.A();
|
||||
s.actions.forEach((a) => {
|
||||
actions.pushObject(Ember.Object.create(a));
|
||||
});
|
||||
if (s.actions && s.actions.length) {
|
||||
s.actions.forEach((a) => {
|
||||
actions.pushObject(Ember.Object.create(a));
|
||||
});
|
||||
}
|
||||
|
||||
steps.pushObject(Ember.Object.create({
|
||||
id: s.id,
|
||||
translation_key: s.translation_key,
|
||||
key: s.key,
|
||||
title: s.title,
|
||||
description: s.description,
|
||||
banner: s.banner,
|
||||
|
@ -163,6 +177,7 @@ CustomWizard.reopenClass({
|
|||
props['name'] = '';
|
||||
props['background'] = '';
|
||||
props['save_submissions'] = true;
|
||||
props['multiple_submissions'] = false;
|
||||
props['steps'] = Ember.A();
|
||||
};
|
||||
|
||||
|
|
|
@ -2,15 +2,30 @@ import CustomWizard from '../models/custom-wizard';
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
beforeModel() {
|
||||
const param = this.paramsFor('adminWizard').wizard_id;
|
||||
const wizards = this.modelFor('admin-wizards-custom');
|
||||
|
||||
if (wizards.length && (param === 'first' || param === 'last')) {
|
||||
const wizard = wizards.get(`${param}Object`);
|
||||
if (wizard) {
|
||||
this.transitionTo('adminWizard', wizard.id.dasherize());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
model(params) {
|
||||
if (params.wizard_id === 'new') {
|
||||
const wizardId = params.wizard_id;
|
||||
|
||||
if (wizardId === 'new') {
|
||||
this.set('newWizard', true);
|
||||
return CustomWizard.create();
|
||||
};
|
||||
this.set('newWizard', false);
|
||||
|
||||
const wizard = this.modelFor('admin-wizards-custom').findBy('id', params.wizard_id.underscore());
|
||||
if (!wizard) return this.transitionTo('adminWizardsCustom.index');
|
||||
const wizard = this.modelFor('admin-wizards-custom').findBy('id', wizardId.underscore());
|
||||
|
||||
if (!wizard) return this.transitionTo('adminWizard', 'new');
|
||||
|
||||
return wizard;
|
||||
},
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export default Discourse.Route.extend({
|
||||
redirect() {
|
||||
this.transitionTo('adminWizard', 'first');
|
||||
}
|
||||
});
|
|
@ -7,8 +7,9 @@ export default Discourse.Route.extend({
|
|||
|
||||
afterModel(model) {
|
||||
const transitionToWizard = this.get('transitionToWizard');
|
||||
if (transitionToWizard === 'last' && model.length) {
|
||||
this.transitionTo('adminWizard', model[model.length - 1].id);
|
||||
if (transitionToWizard && model.length) {
|
||||
this.set('transitionToWizard', null);
|
||||
this.transitionTo('adminWizard', transitionToWizard);
|
||||
};
|
||||
},
|
||||
|
||||
|
|
5
assets/javascripts/discourse/routes/admin-wizards-index.js.es6
Normale Datei
5
assets/javascripts/discourse/routes/admin-wizards-index.js.es6
Normale Datei
|
@ -0,0 +1,5 @@
|
|||
export default Discourse.Route.extend({
|
||||
redirect() {
|
||||
this.transitionTo('adminWizardsCustom');
|
||||
}
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
export default Discourse.Route.extend();
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.background'}}</h3>
|
||||
<h3>{{i18n 'admin.wizard.id'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input name="background" value=model.background placeholderKey="admin.wizard.background_placeholder"}}
|
||||
{{input name="name" value=model.id placeholderKey="admin.wizard.id_placeholder" disabled=model.existingId}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -22,6 +22,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.background'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input name="background" value=model.background placeholderKey="admin.wizard.background_placeholder"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.save_submissions'}}</h3>
|
||||
|
@ -32,6 +41,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting full">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.multiple_submissions'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input type='checkbox' checked=model.multiple_submissions}}
|
||||
<span for="save">{{i18n 'admin.wizard.multiple_submissions_label'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting full">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.url'}}</h3>
|
||||
|
@ -55,8 +74,11 @@
|
|||
<div class='buttons'>
|
||||
<button {{action "save"}} disabled={{disableSave}} class='btn btn-primary'>{{i18n 'admin.wizard.save'}}</button>
|
||||
{{#unless newWizard}}
|
||||
<button {{action "remove"}} class='btn btn-danger'>{{d-icon "trash-o"}}{{i18n 'admin.wizard.remove'}}</button>
|
||||
<button {{action "remove"}} class='btn btn-danger remove'>{{d-icon "trash-o"}}{{i18n 'admin.wizard.remove'}}</button>
|
||||
{{/unless}}
|
||||
<span class="saving {{unless savingStatus 'hidden'}}">{{savingStatus}}</span>
|
||||
{{conditional-loading-spinner condition=saving size='small'}}
|
||||
{{#if error}}
|
||||
<span class="error">{{d-icon "times"}}{{error}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class='row'>
|
||||
<div class='wizard-list'>
|
||||
<div class='content-list wizard-list'>
|
||||
<ul>
|
||||
{{#each model as |s|}}
|
||||
<li>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n "admin.wizard.action.label"}}</h3>
|
||||
<h3>{{i18n "admin.wizard.action.id"}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input value=action.label}}
|
||||
{{input value=action.id placeholderKey='admin.wizard.id_placeholder' disabled=existingId}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n "admin.wizard.action.type"}}</h3>
|
||||
<h3>{{i18n "admin.wizard.type"}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{combo-box value=action.type content=types}}
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.translation'}}</h3>
|
||||
<h3>{{i18n 'admin.wizard.id'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input name="translation_key" value=field.translation_key placeholderKey="admin.wizard.field.translation_placeholder"}}
|
||||
{{input name="id" value=field.id placeholderKey="admin.wizard.id_placeholder" disabled=existingId}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.key'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input name="key" value=field.key placeholderKey="admin.wizard.key_placeholder"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -12,7 +21,7 @@
|
|||
<h3>{{i18n 'admin.wizard.field.label'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input name="label" value=field.label}}
|
||||
{{input name="label" value=field.label placeholder=(i18n "admin.wizard.custom_text_placeholder")}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -21,13 +30,13 @@
|
|||
<h3>{{i18n 'admin.wizard.field.description'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{textarea name="description" value=field.description}}
|
||||
{{textarea name="description" value=field.description placeholder=(i18n "admin.wizard.custom_text_placeholder")}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.field.type'}}</h3>
|
||||
<h3>{{i18n 'admin.wizard.type'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{combo-box value=field.type content=types}}
|
||||
|
@ -44,17 +53,57 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{#if isDropdown}}
|
||||
<div class="wizard-dropdown-choices">
|
||||
<div class="wizard-header small">
|
||||
{{i18n 'admin.wizard.field.choices_label'}}
|
||||
{{#if isInput}}
|
||||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.field.min_length'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input type="number" name="min_length" value=field.min_length placeholder=(i18n 'admin.wizard.field.min_length_placeholder')}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if isDropdown}}
|
||||
<div class="wizard-dropdown-choices">
|
||||
<div class="wizard-header medium">
|
||||
{{i18n 'admin.wizard.field.choices_label'}}
|
||||
</div>
|
||||
<div class="setting">
|
||||
<div class="wizard-header small">
|
||||
{{i18n 'admin.wizard.field.choices_translation'}}
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input name="key" value=field.choices_key placeholderKey="admin.wizard.key_placeholder"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting full">
|
||||
<div class="wizard-header small">
|
||||
{{i18n 'admin.wizard.field.choices_preset.label'}}
|
||||
</div>
|
||||
{{combo-box value=field.choices_preset content=presetChoices none='admin.wizard.field.choices_preset.none'}}
|
||||
<label>{{i18n 'admin.wizard.field.choices_preset.filter'}}</label>
|
||||
{{#each presetFilters as |f|}}
|
||||
<span class='custom-input'>
|
||||
{{input type="text" value=f.key placeholder=(i18n 'admin.wizard.field.choices_preset.key')}}
|
||||
{{input type="text" value=f.value placeholder=(i18n 'admin.wizard.field.choices_preset.value')}}
|
||||
</span>
|
||||
{{d-button action='removeFilter' actionParam=f icon='times'}}
|
||||
{{/each}}
|
||||
<div>{{d-button action='addFilter' label='admin.wizard.add' icon='plus'}}</div>
|
||||
</div>
|
||||
<div class="setting full">
|
||||
<div class="wizard-header small">
|
||||
{{i18n 'admin.wizard.field.choices_custom'}}
|
||||
</div>
|
||||
{{#each dropdownChoices as |c|}}
|
||||
<span class='custom-input'>
|
||||
{{input type='text' value=c.value placeholder=(i18n 'admin.wizard.field.choice.value')}}
|
||||
{{input type='text' value=c.label placeholder=(i18n 'admin.wizard.field.choice.label')}}
|
||||
</span>
|
||||
{{d-button action='removeChoice' actionParam=c icon='times'}}
|
||||
{{/each}}
|
||||
<div>{{d-button action='addChoice' label='admin.wizard.add' icon='plus'}}</div>
|
||||
</div>
|
||||
{{#each dropdownChoices as |c|}}
|
||||
<span class='wizard-dropdown-choice'>
|
||||
{{input type='text' value=c.label}}
|
||||
</span>
|
||||
{{d-button action='removeChoice' actionParam=c icon='times'}}
|
||||
{{/each}}
|
||||
{{d-button action='addChoice' label='admin.wizard.add' icon='plus'}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.translation'}}</h3>
|
||||
<h3>{{i18n 'admin.wizard.id'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input name="translation_key" value=step.translation_key placeholderKey="admin.wizard.step.translation_placeholder"}}
|
||||
{{input name="id" value=step.id placeholderKey="admin.wizard.id_placeholder" disabled=existingId}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting">
|
||||
<div class="setting-label">
|
||||
<h3>{{i18n 'admin.wizard.key'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input name="key" value=step.key placeholderKey="admin.wizard.key_placeholder"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -12,7 +21,7 @@
|
|||
<h3>{{i18n 'admin.wizard.step.title'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{input name="title" value=step.title placeholderKey="admin.wizard.step.title_placeholder"}}
|
||||
{{input name="title" value=step.title placeholderKey="admin.wizard.custom_text_placeholder"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -30,7 +39,7 @@
|
|||
<h3>{{i18n 'admin.wizard.step.description'}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{textarea name="description" value=step.description placeholder=(i18n "admin.wizard.step.description_placeholder")}}
|
||||
{{textarea name="description" value=step.description placeholder=(i18n "admin.wizard.custom_text_placeholder")}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//= require ./wizard/custom-wizard
|
||||
//= require_tree ./wizard/components
|
||||
//= require_tree ./wizard/controllers
|
||||
//= require_tree ./wizard/helpers
|
||||
//= require_tree ./wizard/initializers
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: 'wizard-field-composer',
|
||||
|
||||
keyPress(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
@observes('field.value')
|
||||
validate() {
|
||||
const minLength = Wizard.SiteSettings.min_post_length;
|
||||
const post = this.get('field.value');
|
||||
const field = this.get('field');
|
||||
|
||||
field.set('customValidation', true);
|
||||
|
||||
if (!post) {
|
||||
return field.setValid(false);
|
||||
}
|
||||
|
||||
if (minLength && post.length < minLength) {
|
||||
return field.setValid(false, I18n.t('wizard.validation.too_short', { min: minLength }));
|
||||
}
|
||||
|
||||
field.setValid(true);
|
||||
}
|
||||
});
|
|
@ -7,7 +7,7 @@ export default StepController.extend({
|
|||
const next = this.get('step.next');
|
||||
if (response.refresh_required) {
|
||||
const id = this.get('wizard.id');
|
||||
document.location = getUrl(`/wizard/custom/${id}/steps/${next}`);
|
||||
document.location = getUrl(`/w/${id}/steps/${next}`);
|
||||
} else {
|
||||
this.transitionToRoute('custom.step', next);
|
||||
}
|
||||
|
@ -15,6 +15,10 @@ export default StepController.extend({
|
|||
|
||||
goBack() {
|
||||
this.transitionToRoute('custom.step', this.get('step.previous'));
|
||||
},
|
||||
|
||||
showMessage(message) {
|
||||
this.set('stepMessage', message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,80 +4,27 @@ export default {
|
|||
initialize(app) {
|
||||
if (app.constructor.name !== 'Class' || app.get('rootElement') !== '#custom-wizard-main') return;
|
||||
|
||||
const WizardApplicationRoute = requirejs('wizard/routes/application').default;
|
||||
const findCustomWizard = requirejs('discourse/plugins/discourse-custom-wizard/wizard/models/custom').findCustomWizard;
|
||||
const Router = requirejs('wizard/router').default;
|
||||
const ApplicationRoute = requirejs('wizard/routes/application').default;
|
||||
const ajax = requirejs('wizard/lib/ajax').ajax;
|
||||
const StepRoute = requirejs('wizard/routes/step').default;
|
||||
const StepModel = requirejs('wizard/models/step').default;
|
||||
const WizardStep = requirejs('wizard/components/wizard-step').default;
|
||||
const getUrl = requirejs('discourse-common/lib/get-url').default;
|
||||
const FieldModel = requirejs('wizard/models/wizard-field').default;
|
||||
|
||||
Router.reopen({
|
||||
rootURL: getUrl('/w/')
|
||||
});
|
||||
|
||||
Router.map(function() {
|
||||
this.route('custom', { path: '/custom/:id' }, function() {
|
||||
this.route('custom', { path: '/:wizard_id' }, function() {
|
||||
this.route('step', { path: '/steps/:step_id' });
|
||||
});
|
||||
});
|
||||
|
||||
WizardApplicationRoute.reopen({
|
||||
model() {
|
||||
const customParams = this.paramsFor('custom');
|
||||
return findCustomWizard(customParams.id);
|
||||
},
|
||||
|
||||
afterModel(model) {
|
||||
return Ember.RSVP.hash({
|
||||
info: ajax({
|
||||
url: `/site/basic-info`,
|
||||
type: 'GET',
|
||||
}).then((result) => {
|
||||
return model.set('siteInfo', result);
|
||||
}),
|
||||
settings: ajax({
|
||||
url: `/site/settings`,
|
||||
type: 'GET',
|
||||
}).then((result) => {
|
||||
Object.assign(Wizard.SiteSettings, result);
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
Ember.run.scheduleOnce('afterRender', this, function(){
|
||||
$('body.custom-wizard').css('background', model.get('background'));
|
||||
});
|
||||
|
||||
controller.setProperties({
|
||||
customWizard: true,
|
||||
siteInfo: model.get('siteInfo')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
StepModel.reopen({
|
||||
save() {
|
||||
const fields = {};
|
||||
this.get('fields').forEach(f => fields[f.id] = f.value);
|
||||
return ajax({
|
||||
url: `/wizard/custom/${this.get('wizardId')}/steps/${this.get('id')}`,
|
||||
type: 'PUT',
|
||||
data: { fields }
|
||||
}).catch(response => {
|
||||
response.responseJSON.errors.forEach(err => this.fieldError(err.field, err.description));
|
||||
throw response;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
StepRoute.reopen({
|
||||
afterModel(model) {
|
||||
if (!model) {
|
||||
return document.location = getUrl("/");
|
||||
}
|
||||
|
||||
const wizard = this.modelFor('application');
|
||||
return model.set("wizardId", wizard.id);
|
||||
ApplicationRoute.reopen({
|
||||
redirect() {
|
||||
this.transitionTo('custom');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -93,12 +40,17 @@ export default {
|
|||
};
|
||||
}.property('step.banner'),
|
||||
|
||||
handleMessage: function() {
|
||||
const message = this.get('step.message');
|
||||
this.sendAction('showMessage', message);
|
||||
}.observes('step.message'),
|
||||
|
||||
advance() {
|
||||
this.set('saving', true);
|
||||
this.get('step').save()
|
||||
.then(response => {
|
||||
if (this.get('finalStep')) {
|
||||
document.location = getUrl("/");
|
||||
this.sendAction('finished', response);
|
||||
} else {
|
||||
this.sendAction('goNext', response);
|
||||
}
|
||||
|
@ -111,10 +63,61 @@ export default {
|
|||
quit() {
|
||||
this.set('finalStep', true);
|
||||
this.send('nextStep');
|
||||
},
|
||||
|
||||
showMessage(message) {
|
||||
this.sendAction('showMessage', message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
StepModel.reopen({
|
||||
save() {
|
||||
const wizardId = this.get('wizardId');
|
||||
const fields = {};
|
||||
this.get('fields').forEach(f => fields[f.id] = f.value);
|
||||
return ajax({
|
||||
url: `/w/${wizardId}/steps/${this.get('id')}`,
|
||||
type: 'PUT',
|
||||
data: { fields }
|
||||
}).catch(response => {
|
||||
if (response && response.responseJSON && response.responseJSON.errors) {
|
||||
let wizardErrors = [];
|
||||
response.responseJSON.errors.forEach(err => {
|
||||
if (err.field === wizardId) {
|
||||
wizardErrors.push(err.description);
|
||||
} else if (err.field) {
|
||||
this.fieldError(err.field, err.description);
|
||||
} else if (err) {
|
||||
wizardErrors.push(err);
|
||||
}
|
||||
});
|
||||
if (wizardErrors.length) {
|
||||
this.handleWizardError(wizardErrors.join('\n'));
|
||||
}
|
||||
throw response;
|
||||
}
|
||||
|
||||
if (response && response.responseText) {
|
||||
const responseText = response.responseText;
|
||||
const start = responseText.indexOf('>') + 1;
|
||||
const end = responseText.indexOf('plugins');
|
||||
const message = responseText.substring(start, end);
|
||||
this.handleWizardError(message);
|
||||
throw message;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleWizardError(message) {
|
||||
this.set('message', {
|
||||
state: 'error',
|
||||
text: message
|
||||
});
|
||||
Ember.run.later(() => this.set('message', null), 6000);
|
||||
}
|
||||
});
|
||||
|
||||
FieldModel.reopen({
|
||||
check() {
|
||||
let valid = this.get('valid');
|
||||
|
|
|
@ -9,13 +9,16 @@ const CustomWizard = Ember.Object.extend({
|
|||
});
|
||||
|
||||
export function findCustomWizard(wizardId) {
|
||||
return ajax({ url: `/wizard/custom/${wizardId}` }).then(result => {
|
||||
return ajax({ url: `/w/${wizardId}` }).then(result => {
|
||||
const wizard = result.wizard;
|
||||
wizard.steps = wizard.steps.map(step => {
|
||||
const stepObj = Step.create(step);
|
||||
stepObj.fields = stepObj.fields.map(f => WizardField.create(f));
|
||||
return stepObj;
|
||||
});
|
||||
|
||||
if (!wizard.completed) {
|
||||
wizard.steps = wizard.steps.map(step => {
|
||||
const stepObj = Step.create(step);
|
||||
stepObj.fields = stepObj.fields.map(f => WizardField.create(f));
|
||||
return stepObj;
|
||||
});
|
||||
}
|
||||
|
||||
return CustomWizard.create(wizard);
|
||||
});
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import IndexRoute from 'wizard/routes/index';
|
||||
|
||||
export default IndexRoute.extend({
|
||||
export default Ember.Route.extend({
|
||||
beforeModel() {
|
||||
const appModel = this.modelFor('application');
|
||||
this.replaceWith('custom.step', appModel.start);
|
||||
const appModel = this.modelFor('custom');
|
||||
if (appModel.completed) {
|
||||
this.set('completed', true);
|
||||
} else if (appModel.start) {
|
||||
this.replaceWith('custom.step', appModel.start);
|
||||
}
|
||||
},
|
||||
|
||||
setupController(controller) {
|
||||
const completed = this.get('completed');
|
||||
controller.set('completed', completed);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,3 +1,23 @@
|
|||
import StepRoute from 'wizard/routes/step';
|
||||
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];
|
||||
};
|
||||
|
||||
export default StepRoute.extend();
|
||||
return appModel;
|
||||
},
|
||||
|
||||
afterModel(model) {
|
||||
if (model.completed) return this.transitionTo('index');
|
||||
return model.set("wizardId", this.modelFor('custom').id);
|
||||
},
|
||||
|
||||
setupController(controller, step) {
|
||||
controller.setProperties({
|
||||
step, wizard: this.modelFor('custom')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
37
assets/javascripts/wizard/routes/custom.js.es6
Normale Datei
37
assets/javascripts/wizard/routes/custom.js.es6
Normale Datei
|
@ -0,0 +1,37 @@
|
|||
import { findCustomWizard } from '../models/custom';
|
||||
import { ajax } from 'wizard/lib/ajax';
|
||||
import { getUrl } from 'discourse-common/lib/get-url';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model(params) {
|
||||
return findCustomWizard(params.wizard_id);
|
||||
},
|
||||
|
||||
afterModel() {
|
||||
return ajax({
|
||||
url: `/site/settings`,
|
||||
type: 'GET',
|
||||
}).then((result) => {
|
||||
Object.assign(Wizard.SiteSettings, result);
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
Ember.run.scheduleOnce('afterRender', this, function(){
|
||||
$('body.custom-wizard').css('background', model.get('background'));
|
||||
});
|
||||
|
||||
controller.setProperties({
|
||||
customWizard: true,
|
||||
logoUrl: Wizard.SiteSettings.logo_small_url
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
finished(result) {
|
||||
let url = "/";
|
||||
if (result.topic_id) url += `t/${result.topic_id}`;
|
||||
document.location.replace(getUrl(url));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,16 +1 @@
|
|||
{{#if showCanvas}}
|
||||
{{wizard-canvas}}
|
||||
{{/if}}
|
||||
|
||||
<div class='wizard-column'>
|
||||
<div class='wizard-column-contents'>
|
||||
{{outlet}}
|
||||
</div>
|
||||
<div class='wizard-footer'>
|
||||
{{#if customWizard}}
|
||||
<img src="{{siteInfo.logo_small_url}}" style="background-image: initial; width: 33px; height: 33px;"/>
|
||||
{{else}}
|
||||
<div class='discourse-logo'></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{outlet}}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
{{textarea elementId=field.id value=field.value placeholder=field.placeholder tabindex="9"}}
|
16
assets/javascripts/wizard/templates/custom.hbs
Normale Datei
16
assets/javascripts/wizard/templates/custom.hbs
Normale Datei
|
@ -0,0 +1,16 @@
|
|||
{{#if showCanvas}}
|
||||
{{wizard-canvas}}
|
||||
{{/if}}
|
||||
|
||||
<div class='wizard-column'>
|
||||
<div class='wizard-column-contents'>
|
||||
{{outlet}}
|
||||
</div>
|
||||
<div class='wizard-footer'>
|
||||
{{#if customWizard}}
|
||||
<img src="{{logoUrl}}" style="background-image: initial; width: 33px; height: 33px;"/>
|
||||
{{else}}
|
||||
<div class='discourse-logo'></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
3
assets/javascripts/wizard/templates/custom.index.hbs
Normale Datei
3
assets/javascripts/wizard/templates/custom.index.hbs
Normale Datei
|
@ -0,0 +1,3 @@
|
|||
{{#if completed}}
|
||||
{{i18n 'wizard.completed'}}
|
||||
{{/if}}
|
|
@ -1 +1,9 @@
|
|||
{{wizard-step step=step wizard=wizard goNext="goNext" goBack="goBack"}}
|
||||
<div class="step-message {{stepMessage.state}}">
|
||||
{{stepMessage.text}}
|
||||
</div>
|
||||
{{wizard-step step=step
|
||||
wizard=wizard
|
||||
goNext="goNext"
|
||||
goBack="goBack"
|
||||
finished="finished"
|
||||
showMessage="showMessage"}}
|
||||
|
|
89
assets/stylesheets/wizard/wizard_custom.scss
Normale Datei
89
assets/stylesheets/wizard/wizard_custom.scss
Normale Datei
|
@ -0,0 +1,89 @@
|
|||
.custom-wizard {
|
||||
background-color: initial;
|
||||
|
||||
.wizard-step-description {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.wizard-column .wizard-step-banner {
|
||||
width: initial;
|
||||
max-width: 660px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 20px;
|
||||
|
||||
.controls {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 200px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.wizard-step-form .wizard-btn {
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.wizard-column .wizard-field .input-area {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.p-list-box {
|
||||
max-width: 550px;
|
||||
position: relative;
|
||||
margin: 10px 0;
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
right: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.p-text {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
ul {
|
||||
border: 1px solid #e9e9e9;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
height: 95px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
|
||||
&:hover, &.selected {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.default {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #919191;
|
||||
}
|
||||
}
|
|
@ -13,15 +13,17 @@
|
|||
}
|
||||
|
||||
.wizard-header {
|
||||
font-size: 1.3em;
|
||||
font-size: 1.4em;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&.medium {
|
||||
font-size: 1.1em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
&.small {
|
||||
font-size: 0.97em;
|
||||
font-size: 1em;
|
||||
text-decoration: underline;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,7 +38,7 @@
|
|||
.setting {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
min-width: 49%;
|
||||
width: 49%;
|
||||
|
||||
.setting-label {
|
||||
width: 90px;
|
||||
|
@ -49,6 +51,22 @@
|
|||
&.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons .error {
|
||||
color: $danger;
|
||||
|
||||
.fa {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons .remove {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,19 +78,49 @@
|
|||
}
|
||||
}
|
||||
|
||||
.wizard-column-contents {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wizard-custom-step {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: dark-light-diff($primary, $secondary, 96%, -65%);
|
||||
}
|
||||
|
||||
.wizard-dropdown-choices {
|
||||
margin-bottom: 25px;
|
||||
.step-message {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
transition: all .2s;
|
||||
|
||||
&.success {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
background-color: $success;
|
||||
color: $secondary;
|
||||
}
|
||||
|
||||
&.error {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
background-color: $danger;
|
||||
color: $secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.wizard-dropdown-choice {
|
||||
.wizard-dropdown-choices {
|
||||
padding: 15px 15px 0 15px;
|
||||
margin-bottom: 20px;
|
||||
background-color: $secondary;
|
||||
}
|
||||
|
||||
.setting .custom-input {
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
@ -12,38 +12,55 @@ en:
|
|||
background_placeholder: "Background css property"
|
||||
save_submissions: "Save"
|
||||
save_submissions_label: "Save wizard submissions"
|
||||
multiple_submissions: "Multiple"
|
||||
multiple_submissions_label: "Allow multiple submissions by the same user"
|
||||
save: "Save Changes"
|
||||
remove: "Delete Wizard"
|
||||
header: "Wizard"
|
||||
add: "Add"
|
||||
url: "Url"
|
||||
translation: "Translation"
|
||||
|
||||
key: "Key"
|
||||
id: "Id"
|
||||
id_placeholder: "Underscored. Cannot be changed."
|
||||
key_placeholder: "Translation key"
|
||||
custom_text_placeholder: "Overrides translation"
|
||||
type: "Type"
|
||||
error:
|
||||
name_required: "Wizards must have a name."
|
||||
steps_required: "Wizards must have at least one step."
|
||||
id_required: "All Step, Fields and Actions need an Id"
|
||||
field:
|
||||
need_choices: "All dropdowns need a translated choices, custom choies or preset choices."
|
||||
choices_label_empty: "Custom choice labels cannot be empty."
|
||||
step:
|
||||
header: "Steps"
|
||||
title: "Title"
|
||||
title_placeholder: "Overrides title translation"
|
||||
banner: "Banner"
|
||||
banner_placeholder: "Image url"
|
||||
description: "Description"
|
||||
description_placeholder: "Overrides description translation"
|
||||
translation_placeholder: "Translation key for step"
|
||||
|
||||
field:
|
||||
header: "Fields"
|
||||
label: "Label"
|
||||
description: "Description"
|
||||
type: "Type"
|
||||
choices_label: "Dropdown Choices"
|
||||
add_choice: "Add"
|
||||
choices_label: "Dropdown Choices (use one type)"
|
||||
choices_translation: "Translation"
|
||||
choices_custom: "Custom"
|
||||
choices_preset:
|
||||
label: "Preset"
|
||||
none: "Select a data type"
|
||||
categories: "Categories"
|
||||
filter: "Filter"
|
||||
key: "Key"
|
||||
value: "Value"
|
||||
choice:
|
||||
value: "Value"
|
||||
label: "Label"
|
||||
required: "Required"
|
||||
required_label: "Field is Required"
|
||||
translation_placeholder: "Translation key for field"
|
||||
|
||||
min_length: "Min Length"
|
||||
min_length_placeholder: "Minimum length in characters"
|
||||
action:
|
||||
header: "Actions"
|
||||
label: "Label"
|
||||
type: "Type"
|
||||
send_message:
|
||||
label: "Send Message"
|
||||
title: "Title"
|
||||
|
@ -62,5 +79,4 @@ en:
|
|||
|
||||
wizard_js:
|
||||
wizard:
|
||||
validation:
|
||||
too_short: "Post must be at least {{min}} characters"
|
||||
completed: "You have completed this wizard."
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
en:
|
||||
custom_wizard:
|
||||
title: "Wizard"
|
||||
|
||||
new_wizard:
|
||||
step_1:
|
||||
title: "Translated title"
|
||||
description: "Translated description"
|
||||
field_1:
|
||||
label: "Translated field title"
|
||||
description: "Translated field description"
|
||||
wizard:
|
||||
custom_title: "Wizard"
|
||||
field:
|
||||
too_short: "%{label} must be at least %{min} characters"
|
||||
|
|
228
lib/builder.rb
228
lib/builder.rb
|
@ -1,12 +1,17 @@
|
|||
class CustomWizard::Builder
|
||||
|
||||
attr_accessor :wizard, :updater, :submission
|
||||
|
||||
def initialize(user, wizard_id)
|
||||
data = PluginStore.get('custom_wizard', wizard_id)
|
||||
@custom_wizard = CustomWizard::Wizard.new(data)
|
||||
@wizard = Wizard.new(user)
|
||||
@wizard.id = wizard_id
|
||||
@wizard.save_submissions = data['save_submissions']
|
||||
@wizard.background = data["background"]
|
||||
@wizard = Wizard.new(user,
|
||||
id: wizard_id,
|
||||
save_submissions: data['save_submissions'],
|
||||
multiple_submissions: data['multiple_submissions'],
|
||||
background: data["background"],
|
||||
custom: true
|
||||
)
|
||||
end
|
||||
|
||||
def self.sorted_handlers
|
||||
|
@ -23,99 +28,152 @@ class CustomWizard::Builder
|
|||
end
|
||||
|
||||
def build
|
||||
@custom_wizard.steps.each do |s|
|
||||
@wizard.append_step(s['id']) do |step|
|
||||
step.title = s['title'] if s['title']
|
||||
step.description = s['description'] if s['description']
|
||||
step.banner = s['banner'] if s['banner']
|
||||
step.translation_key = s['translation_key'] if s['translation_key']
|
||||
unless (@wizard.completed? && !@custom_wizard.respond_to?(:multiple_submissions)) ||
|
||||
!@custom_wizard.steps
|
||||
@custom_wizard.steps.each do |s|
|
||||
@wizard.append_step(s['id']) do |step|
|
||||
step.title = s['title'] if s['title']
|
||||
step.description = s['description'] if s['description']
|
||||
step.banner = s['banner'] if s['banner']
|
||||
step.key = s['key'] if s['key']
|
||||
|
||||
s['fields'].each do |f|
|
||||
params = {
|
||||
id: f['id'],
|
||||
type: f['type'],
|
||||
required: f['required']
|
||||
}
|
||||
if s['fields'] && s['fields'].length
|
||||
s['fields'].each do |f|
|
||||
params = {
|
||||
id: f['id'],
|
||||
type: f['type'],
|
||||
required: f['required']
|
||||
}
|
||||
|
||||
params[:label] = f['label'] if f['label']
|
||||
params[:description] = f['description'] if f['description']
|
||||
params[:translation_key] = f['translation_key'] if f['translation_key']
|
||||
params[:label] = f['label'] if f['label']
|
||||
params[:description] = f['description'] if f['description']
|
||||
params[:key] = f['key'] if f['key']
|
||||
|
||||
field = step.add_field(params)
|
||||
|
||||
if f['type'] == 'dropdown'
|
||||
f['choices'].each do |c|
|
||||
field.add_choice(c['id'], label: c['label'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
step.on_update do |updater|
|
||||
|
||||
@updater = updater
|
||||
input = updater.fields
|
||||
user = @wizard.user
|
||||
|
||||
if @wizard.save_submissions && input
|
||||
store_key = @wizard.id
|
||||
submissions = Array.wrap(PluginStore.get("custom_wizard_submissions", store_key))
|
||||
submission = {}
|
||||
|
||||
if submissions.last && submissions.last['completed'] === false
|
||||
submission = submissions.last
|
||||
submissions.pop(1)
|
||||
end
|
||||
|
||||
submission['user_id'] = @wizard.user.id
|
||||
submission['completed'] = updater.step.next.nil?
|
||||
|
||||
input.each do |key, value|
|
||||
submission[key] = value
|
||||
end
|
||||
|
||||
submissions.push(submission)
|
||||
|
||||
PluginStore.set('custom_wizard_submissions', store_key, submissions)
|
||||
end
|
||||
|
||||
if s['actions'] && s['actions'].length
|
||||
s['actions'].each do |a|
|
||||
if a['type'] === 'create_topic'
|
||||
creator = PostCreator.new(user,
|
||||
title: input[a['title']],
|
||||
raw: input[a['post']],
|
||||
category: a['category_id'],
|
||||
skip_validations: true)
|
||||
|
||||
post = creator.create
|
||||
if creator.errors.present?
|
||||
raise StandardError, creator.errors.full_messages.join(" ")
|
||||
end
|
||||
|
||||
updater.result = { topic_id: post.topic.id }
|
||||
submissions = Array.wrap(PluginStore.get("custom_wizard_submissions", @wizard.id))
|
||||
if submissions.last && submissions.last['completed'] === false
|
||||
@submission = submissions.last
|
||||
params[:value] = @submission[f['id']] if @submission[f['id']]
|
||||
end
|
||||
|
||||
if a['type'] === 'send_message'
|
||||
creator = PostCreator.new(user,
|
||||
title: input[a['title']],
|
||||
raw: input[a['post']],
|
||||
archetype: Archetype.private_message,
|
||||
target_usernames: a['username'])
|
||||
field = step.add_field(params)
|
||||
|
||||
post = creator.create
|
||||
if f['type'] === 'dropdown'
|
||||
if f['choices'] && f['choices'].length > 0
|
||||
f['choices'].each do |c|
|
||||
field.add_choice(c['value'], label: c['label'])
|
||||
end
|
||||
elsif f['choices_key'] && f['choices_key'].length > 0
|
||||
choices = I18n.t(f['choices_key'])
|
||||
if choices.is_a?(Hash)
|
||||
choices.each do |k, v|
|
||||
field.add_choice(k, label: v)
|
||||
end
|
||||
end
|
||||
elsif f['choices_preset'] && f['choices_preset'].length > 0
|
||||
objects = []
|
||||
|
||||
if creator.errors.present?
|
||||
raise StandardError, creator.errors.full_messages.join(" ")
|
||||
if f['choices_preset'] === 'categories'
|
||||
objects = Site.new(Guardian.new(@wizard.user)).categories
|
||||
end
|
||||
|
||||
if f['choices_filters'] && f['choices_filters'].length > 0
|
||||
f['choices_filters'].each do |f|
|
||||
objects.reject! { |o| o[f['key']] != f['value'] }
|
||||
end
|
||||
end
|
||||
|
||||
if objects.length > 0
|
||||
objects.each do |o|
|
||||
field.add_choice(o.id, label: o.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
updater.result = { topic_id: post.topic.id }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
CustomWizard::Builder.step_handlers.each do |handler|
|
||||
if handler[:wizard_id] == @wizard.id
|
||||
handler[:block].call(self)
|
||||
step.on_update do |updater|
|
||||
@updater = updater
|
||||
input = updater.fields
|
||||
user = @wizard.user
|
||||
|
||||
if s['fields'] && s['fields'].length
|
||||
s['fields'].each do |f|
|
||||
value = input[f['id']]
|
||||
min_length = f['min_length']
|
||||
if min_length && value.is_a?(String) && value.length < min_length.to_i
|
||||
label = f['label'] || I18n.t("#{f['key']}.label")
|
||||
updater.errors.add(f['id'].to_s, I18n.t('wizard.field.too_short', label: label, min: min_length.to_i))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
next if updater.errors.any?
|
||||
|
||||
CustomWizard::Builder.step_handlers.each do |handler|
|
||||
if handler[:wizard_id] == @wizard.id
|
||||
handler[:block].call(self)
|
||||
end
|
||||
end
|
||||
|
||||
next if updater.errors.any?
|
||||
|
||||
if s['actions'] && s['actions'].length
|
||||
s['actions'].each do |a|
|
||||
if a['type'] === 'create_topic'
|
||||
creator = PostCreator.new(user,
|
||||
title: input[a['title']],
|
||||
raw: input[a['post']],
|
||||
category: a['category_id'],
|
||||
skip_validations: true)
|
||||
|
||||
post = creator.create
|
||||
if creator.errors.present?
|
||||
updater.errors.add(:create_topic, creator.errors.full_messages.join(" "))
|
||||
else
|
||||
updater.result = { topic_id: post.topic.id }
|
||||
end
|
||||
end
|
||||
|
||||
if a['type'] === 'send_message'
|
||||
creator = PostCreator.new(user,
|
||||
title: input[a['title']],
|
||||
raw: input[a['post']],
|
||||
archetype: Archetype.private_message,
|
||||
target_usernames: a['username'])
|
||||
|
||||
post = creator.create
|
||||
|
||||
if creator.errors.present?
|
||||
updater.errors.add(:send_message, creator.errors.full_messages.join(" "))
|
||||
else
|
||||
updater.result = { topic_id: post.topic.id }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if @wizard.save_submissions && updater.errors.empty?
|
||||
store_key = @wizard.id
|
||||
submissions = Array.wrap(PluginStore.get("custom_wizard_submissions", store_key))
|
||||
submission = {}
|
||||
|
||||
if submissions.last && submissions.last['completed'] === false
|
||||
submission = submissions.last
|
||||
submissions.pop(1)
|
||||
end
|
||||
|
||||
submission['user_id'] = @wizard.user.id
|
||||
submission['completed'] = updater.step.next.nil?
|
||||
|
||||
if input
|
||||
input.each do |key, value|
|
||||
submission[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
submissions.push(submission)
|
||||
PluginStore.set('custom_wizard_submissions', store_key, submissions)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
class CustomWizard::Field
|
||||
def self.types
|
||||
@types ||= ['dropdown', 'image', 'radio', 'text', 'textarea', 'composer']
|
||||
@types ||= ['dropdown', 'image', 'radio', 'text', 'textarea']
|
||||
end
|
||||
|
||||
def self.require_assets
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
class CustomWizard::Wizard
|
||||
|
||||
attr_reader :id, :name, :steps, :background, :save_submissions, :custom
|
||||
attr_reader :id, :name, :steps, :background, :save_submissions, :multiple_submissions, :custom
|
||||
|
||||
def initialize(data)
|
||||
data = data.is_a?(String) ? ::JSON.parse(data) : data
|
||||
|
@ -8,6 +8,7 @@ class CustomWizard::Wizard
|
|||
@name = data['name']
|
||||
@background = data['background']
|
||||
@save_submissions = data['save_submissions']
|
||||
@multiple_submissions = data['multiple_submissions']
|
||||
@steps = data['steps']
|
||||
@custom = true
|
||||
end
|
||||
|
|
114
plugin.rb
114
plugin.rb
|
@ -3,10 +3,11 @@
|
|||
# version: 0.1
|
||||
# authors: Angus McLeod
|
||||
|
||||
register_asset 'stylesheets/custom_wizard.scss'
|
||||
register_asset 'stylesheets/wizard_custom_admin.scss'
|
||||
|
||||
config = Rails.application.config
|
||||
config.assets.paths << Rails.root.join("plugins", "discourse-custom-wizard", "assets", "javascripts")
|
||||
config.assets.paths << Rails.root.join("plugins", "discourse-custom-wizard", "assets", "stylesheets", "wizard")
|
||||
|
||||
after_initialize do
|
||||
require_dependency "application_controller"
|
||||
|
@ -33,9 +34,7 @@ after_initialize do
|
|||
|
||||
require_dependency 'admin_constraint'
|
||||
Discourse::Application.routes.append do
|
||||
namespace :wizard do
|
||||
mount ::CustomWizard::Engine, at: 'custom'
|
||||
end
|
||||
mount ::CustomWizard::Engine, at: 'w'
|
||||
|
||||
scope module: 'custom_wizard', constraints: AdminConstraint.new do
|
||||
get 'admin/wizards' => 'admin#index'
|
||||
|
@ -52,21 +51,61 @@ after_initialize do
|
|||
end
|
||||
end
|
||||
|
||||
class ::Wizard
|
||||
attr_accessor :id, :background, :save_submissions
|
||||
end
|
||||
|
||||
class ::Wizard::Step
|
||||
attr_accessor :title, :description, :translation_key
|
||||
end
|
||||
|
||||
class ::Wizard::StepUpdater
|
||||
attr_accessor :result, :step
|
||||
end
|
||||
|
||||
require_dependency 'wizard'
|
||||
require_dependency 'wizard/step'
|
||||
require_dependency 'wizard/step_updater'
|
||||
require_dependency 'wizard/field'
|
||||
Wizard::Field.class_eval do
|
||||
attr_reader :label, :description, :translation_key
|
||||
|
||||
::Wizard.class_eval do
|
||||
attr_accessor :id, :background, :save_submissions, :multiple_submissions
|
||||
|
||||
def initialize(user, attrs = {})
|
||||
@steps = []
|
||||
@user = user
|
||||
@first_step = nil
|
||||
@max_topics_to_require_completion = 15
|
||||
@id = attrs[:id] if attrs[:id]
|
||||
@save_submissions = attrs[:save_submissions] if attrs[:save_submissions]
|
||||
@multiple_submissions = attrs[:multiple_submissions] if attrs[:multiple_submissions]
|
||||
@background = attrs[:background] if attrs[:background]
|
||||
@custom = attrs[:custom] if attrs[:custom]
|
||||
end
|
||||
|
||||
def completed?
|
||||
completed_steps?(@steps.map(&:id))
|
||||
end
|
||||
|
||||
def completed_steps?(steps)
|
||||
steps = [steps].flatten.uniq
|
||||
|
||||
completed = UserHistory.where(
|
||||
acting_user_id: @user.id,
|
||||
action: UserHistory.actions[:wizard_step]
|
||||
).where(context: steps)
|
||||
.distinct.order(:context).pluck(:context)
|
||||
|
||||
steps.sort == completed
|
||||
end
|
||||
|
||||
def start
|
||||
completed = UserHistory.where(
|
||||
acting_user_id: @user.id,
|
||||
action: UserHistory.actions[:wizard_step]
|
||||
).where(context: @steps.map(&:id))
|
||||
.uniq.pluck(:context)
|
||||
|
||||
# First uncompleted step
|
||||
steps = @custom ? @steps : steps_with_fields
|
||||
steps.each do |s|
|
||||
return s unless completed.include?(s.id)
|
||||
end
|
||||
|
||||
@first_step
|
||||
end
|
||||
end
|
||||
|
||||
::Wizard::Field.class_eval do
|
||||
attr_reader :label, :description, :key, :min_length
|
||||
|
||||
def initialize(attrs)
|
||||
attrs = attrs || {}
|
||||
|
@ -76,14 +115,23 @@ after_initialize do
|
|||
@required = !!attrs[:required]
|
||||
@label = attrs[:label]
|
||||
@description = attrs[:description]
|
||||
@translation_key = attrs[:translation_key]
|
||||
@key = attrs[:key]
|
||||
@min_length = attrs[:min_length]
|
||||
@value = attrs[:value]
|
||||
@choices = []
|
||||
end
|
||||
end
|
||||
|
||||
class ::Wizard::Step
|
||||
attr_accessor :title, :description, :key
|
||||
end
|
||||
|
||||
class ::Wizard::StepUpdater
|
||||
attr_accessor :result, :step
|
||||
end
|
||||
|
||||
::WizardSerializer.class_eval do
|
||||
attributes :id, :background
|
||||
attributes :id, :background, :completed
|
||||
|
||||
def id
|
||||
object.id
|
||||
|
@ -93,32 +141,48 @@ after_initialize do
|
|||
object.background
|
||||
end
|
||||
|
||||
def completed
|
||||
object.completed?
|
||||
end
|
||||
|
||||
def include_completed?
|
||||
object.completed? && !object.multiple_submissions && !scope.current_user.admin?
|
||||
end
|
||||
|
||||
def include_start?
|
||||
object.start
|
||||
object.start && include_steps?
|
||||
end
|
||||
|
||||
def include_steps?
|
||||
!include_completed?
|
||||
end
|
||||
end
|
||||
|
||||
::WizardStepSerializer.class_eval do
|
||||
def title
|
||||
return object.title if object.title
|
||||
I18n.t("#{object.translation_key || i18n_key}.title", default: '')
|
||||
I18n.t("#{object.key || i18n_key}.title", default: '')
|
||||
end
|
||||
|
||||
def description
|
||||
return object.description if object.description
|
||||
I18n.t("#{object.translation_key || i18n_key}.description", default: '')
|
||||
I18n.t("#{object.key || i18n_key}.description", default: '')
|
||||
end
|
||||
end
|
||||
|
||||
::WizardFieldSerializer.class_eval do
|
||||
def label
|
||||
return object.label if object.label
|
||||
I18n.t("#{object.translation_key || i18n_key}.label", default: '')
|
||||
I18n.t("#{object.key || i18n_key}.label", default: '')
|
||||
end
|
||||
|
||||
def description
|
||||
return object.description if object.description
|
||||
I18n.t("#{object.translation_key || i18n_key}.description", default: '')
|
||||
I18n.t("#{object.key || i18n_key}.description", default: '')
|
||||
end
|
||||
|
||||
def placeholder
|
||||
I18n.t("#{object.key || i18n_key}.placeholder", default: '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
.custom-wizard {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
.custom-wizard .wizard-step-description {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.custom-wizard .wizard-column .wizard-step-banner {
|
||||
width: initial;
|
||||
max-width: 660px;
|
||||
}
|
Laden …
In neuem Issue referenzieren