0
0
Fork 1
Spiegel von https://github.com/paviliondev/discourse-custom-wizard.git synchronisiert 2024-11-22 09:20:29 +01:00

Improve change handling structure

Dieser Commit ist enthalten in:
Angus McLeod 2020-04-20 19:41:13 +10:00
Ursprung d194a8313a
Commit 32aa7cc897
21 geänderte Dateien mit 288 neuen und 70 gelöschten Zeilen

Datei anzeigen

@ -1,11 +1,15 @@
import { default as discourseComputed, observes, on } from 'discourse-common/utils/decorators'; import { default as discourseComputed } from 'discourse-common/utils/decorators';
import { equal, empty, or } from "@ember/object/computed"; import { equal, empty, or } from "@ember/object/computed";
import { generateName, selectKitContent } from '../lib/wizard'; import { generateName, selectKitContent } from '../lib/wizard';
import { computed } from "@ember/object";
import wizardSchema from '../lib/wizard-schema'; import wizardSchema from '../lib/wizard-schema';
import UndoChanges from '../mixins/undo-changes';
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({ export default Component.extend(UndoChanges, {
classNames: 'wizard-custom-action', componentType: 'action',
classNameBindings: [':wizard-custom-action', 'visible'],
visible: computed('currentActionId', function() { return this.action.id === this.currentActionId }),
actionTypes: Object.keys(wizardSchema.action.types).map(t => ({ id: t, name: generateName(t) })), actionTypes: Object.keys(wizardSchema.action.types).map(t => ({ id: t, name: generateName(t) })),
createTopic: equal('action.type', 'create_topic'), createTopic: equal('action.type', 'create_topic'),
updateProfile: equal('action.type', 'update_profile'), updateProfile: equal('action.type', 'update_profile'),

Datei anzeigen

@ -1,11 +1,14 @@
import { default as discourseComputed, observes } from 'discourse-common/utils/decorators'; import { default as discourseComputed } from 'discourse-common/utils/decorators';
import { equal, or } from "@ember/object/computed"; import { equal, or } from "@ember/object/computed";
import { computed } from "@ember/object";
import { selectKitContent } from '../lib/wizard'; import { selectKitContent } from '../lib/wizard';
import { default as wizardSchema, setSchemaDefaults } from '../lib/wizard-schema'; import UndoChanges from '../mixins/undo-changes';
import Component from "@ember/component"; import Component from "@ember/component";
export default Component.extend({ export default Component.extend(UndoChanges, {
classNames: 'wizard-custom-field', componentType: 'field',
classNameBindings: [':wizard-custom-field', 'visible'],
visible: computed('currentFieldId', function() { return this.field.id === this.currentFieldId }),
isDropdown: equal('field.type', 'dropdown'), isDropdown: equal('field.type', 'dropdown'),
isUpload: equal('field.type', 'upload'), isUpload: equal('field.type', 'upload'),
isCategory: equal('field.type', 'category'), isCategory: equal('field.type', 'category'),
@ -20,20 +23,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']),
// setTypeDefaults only set defaults if the field type of a specific field
// changes, and not when switching between fields. Switching between fields also
// changes the field.type property in this component
@observes('field.id', 'field.type')
setTypeDefaults(ctx, changed) {
if (this.field.id === this.bufferedFieldId) {
setSchemaDefaults(this.field, 'field');
}
if (changed === 'field.type') {
this.set('bufferedFieldId', this.field.id);
}
},
setupTypeOutput(fieldType, options) { setupTypeOutput(fieldType, options) {
const selectionType = { const selectionType = {
category: 'category', category: 'category',
@ -84,7 +73,7 @@ export default Component.extend({
return this.setupTypeOutput(fieldType, options); return this.setupTypeOutput(fieldType, options);
}, },
actions: { actions: {
imageUploadDone(upload) { imageUploadDone(upload) {
this.set("field.image", upload.url); this.set("field.image", upload.url);
}, },

Datei anzeigen

@ -1,6 +1,6 @@
import { default as discourseComputed, on, observes } from 'discourse-common/utils/decorators'; import { default as discourseComputed, on, observes } from 'discourse-common/utils/decorators';
import { generateName } from '../lib/wizard'; import { generateName } from '../lib/wizard';
import { default as wizardSchema, setSchemaDefaults } from '../lib/wizard-schema'; import { default as wizardSchema, setWizardDefaults } from '../lib/wizard-schema';
import { notEmpty } from "@ember/object/computed"; import { notEmpty } from "@ember/object/computed";
import { scheduleOnce, bind } from "@ember/runloop"; import { scheduleOnce, bind } from "@ember/runloop";
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
@ -70,6 +70,10 @@ export default Component.extend({
add() { add() {
const items = this.get('items'); const items = this.get('items');
const itemType = this.itemType; const itemType = this.itemType;
let params = setWizardDefaults({}, itemType);
params.isNew = true;
let next = 1; let next = 1;
if (items.length) { if (items.length) {
@ -84,11 +88,8 @@ export default Component.extend({
if (itemType === 'field') { if (itemType === 'field') {
id = `${this.parentId}_${id}`; id = `${this.parentId}_${id}`;
} }
let params = { params.id = id;
id,
isNew: true
};
let objectArrays = wizardSchema[itemType].objectArrays; let objectArrays = wizardSchema[itemType].objectArrays;
if (objectArrays) { if (objectArrays) {
@ -96,9 +97,7 @@ export default Component.extend({
params[objectArrays[objectType].property] = A(); params[objectArrays[objectType].property] = A();
}); });
}; };
setSchemaDefaults(params, itemType);
const newItem = EmberObject.create(params); const newItem = EmberObject.create(params);
items.pushObject(newItem); items.pushObject(newItem);

Datei anzeigen

@ -3,6 +3,7 @@ import { gt } from '@ember/object/computed';
import { computed } from "@ember/object"; import { computed } from "@ember/object";
import { defaultConnector } from '../lib/wizard-mapper'; import { defaultConnector } from '../lib/wizard-mapper';
import { later } from "@ember/runloop"; import { later } from "@ember/runloop";
import { observes } from "discourse-common/utils/decorators";
export default Component.extend({ export default Component.extend({
classNameBindings: [':mapper-connector', ':mapper-block', 'hasMultiple::single'], classNameBindings: [':mapper-connector', ':mapper-block', 'hasMultiple::single'],
@ -22,5 +23,10 @@ export default Component.extend({
); );
}); });
} }
},
@observes('connector')
updated() {
this.onUpdate('connector');
} }
}); });

Datei anzeigen

@ -167,6 +167,11 @@ export default Component.extend({
return this.activeType === type && this[`${type}Enabled`]; return this.activeType === type && this[`${type}Enabled`];
}, },
@observes('activeType', 'value')
updated() {
this.onUpdate('selector');
},
actions: { actions: {
toggleType(type) { toggleType(type) {
this.set('activeType', type); this.set('activeType', type);

Datei anzeigen

@ -59,6 +59,8 @@ export default Component.extend({
this.get('inputs').pushObject( this.get('inputs').pushObject(
newInput(this.inputOptions, this.inputs.length) newInput(this.inputOptions, this.inputs.length)
); );
this.onUpdate(this.property, 'input');
}, },
remove(input) { remove(input) {
@ -68,6 +70,12 @@ export default Component.extend({
if (inputs.length) { if (inputs.length) {
inputs[0].set('connector', null); inputs[0].set('connector', null);
} }
this.onUpdate(this.property, 'input');
},
inputUpdated(type) {
this.onUpdate(this.property, type);
} }
} }
}); });

Datei anzeigen

@ -79,7 +79,7 @@ function buildObject(json, type) {
Object.keys(json).forEach(prop => { Object.keys(json).forEach(prop => {
props[prop] = buildProperty(json, prop, type) props[prop] = buildProperty(json, prop, type)
}); });
return EmberObject.create(props); return EmberObject.create(props);
} }
@ -162,6 +162,7 @@ function buildProperties(json) {
}; };
json = actionPatch(json); // to be removed - see above json = actionPatch(json); // to be removed - see above
props.actions = buildObjectArray(json.actions, 'action'); props.actions = buildObjectArray(json.actions, 'action');
} else { } else {
listProperties('wizard').forEach(prop => { listProperties('wizard').forEach(prop => {

Datei anzeigen

@ -1,4 +1,4 @@
import { set } from "@ember/object"; import { set, get } from "@ember/object";
const wizard = { const wizard = {
basic: { basic: {
@ -194,25 +194,27 @@ if (Discourse.SiteSettings.wizard_apis_enabled) {
} }
} }
export function setSchemaDefaults(obj, objType) { export function setWizardDefaults(obj, itemType, opts={}) {
let objSchema = wizardSchema[objType]; const objSchema = wizardSchema[itemType];
let basicDefaults = objSchema.basic; const basicDefaults = objSchema.basic;
const typeDefaults = objSchema.types[obj.type];
Object.keys(basicDefaults).forEach(property => {
if (basicDefaults[property]) { Object.keys(basicDefaults).forEach(property => {
set(obj, property, basicDefaults[property]); let defaultValue = get(basicDefaults, property);
if (defaultValue) {
set(obj, property, defaultValue);
} }
}); });
if (objSchema.types && obj.type) { if (typeDefaults) {
let typeDefaults = objSchema.types[obj.type];
Object.keys(typeDefaults).forEach(property => { Object.keys(typeDefaults).forEach(property => {
if (typeDefaults.hasOwnProperty(property)) { if (typeDefaults.hasOwnProperty(property)) {
set(obj, property, typeDefaults[property]); set(obj, property, get(typeDefaults, property));
} }
}); });
} }
return obj;
} }
export default wizardSchema; export default wizardSchema;

Datei anzeigen

@ -49,11 +49,23 @@ const userProperties = [
'trust_level' 'trust_level'
]; ];
function listProperties(type, objectType = null) { function listProperties(type, opts={}) {
let properties = Object.keys(wizardSchema[type].basic); let properties = Object.keys(wizardSchema[type].basic);
if (wizardSchema[type].types && objectType) { const types = wizardSchema[type].types;
properties = properties.concat(Object.keys(wizardSchema[type].types[objectType]));
if (types) {
let typeProperties = [];
if (opts.allTypes) {
Object.keys(types).forEach(type => {
typeProperties = typeProperties.concat(Object.keys(types[type]));
});
} else if (opts.objectType) {
typeProperties = Object.keys(types[opts.objectType]);
}
properties = properties.concat(typeProperties);
} }
return properties; return properties;

Datei anzeigen

@ -0,0 +1,124 @@
import { listProperties } from '../lib/wizard';
import { default as wizardSchema } from '../lib/wizard-schema';
import { set, get } from "@ember/object";
import Mixin from "@ember/object/mixin";
import { observes } from 'discourse-common/utils/decorators';
export default Mixin.create({
didInsertElement() {
this._super(...arguments);
this.setupObservers();
const obj = this.get(this.componentType);
this.setProperties({
originalObject: JSON.parse(JSON.stringify(obj)),
undoIcon: obj.isNew ? 'times' : 'undo',
undoKey: `admin.wizard.${obj.isNew ? 'clear' : 'undo'}`
})
},
willDestroyElement() {
this._super(...arguments);
this.removeObservers();
},
removeObservers(objType=null) {
const componentType = this.componentType;
const obj = this.get(componentType);
let opts = {
objectType: objType || obj.type
}
listProperties(componentType, opts).forEach(property => {
obj.removeObserver(property, this, this.toggleUndo);
});
},
setupObservers(objType=null) {
const componentType = this.componentType;
const obj = this.get(componentType);
let opts = {
objectType: objType || obj.type
}
listProperties(componentType, opts).forEach(property => {
obj.addObserver(property, this, this.toggleUndo);
});
},
revertToOriginal(revertBasic=false) {
const original = JSON.parse(JSON.stringify(this.originalObject));
const componentType = this.componentType;
const obj = this.get(componentType);
const objSchema = wizardSchema[componentType];
const basicDefaults = objSchema.basic;
if (revertBasic) {
Object.keys(basicDefaults).forEach(property => {
let value;
if (original.hasOwnProperty(property)) {
value = get(original, property);
} else if (basicDefaults.hasOwnProperty(property)) {
value = get(basicDefaults, property);
}
set(obj, property, value);
});
}
if (objSchema.types && obj.type) {
let typeDefaults = objSchema.types[obj.type];
Object.keys(typeDefaults).forEach(property => {
let value;
if (original.type === obj.type && original.hasOwnProperty(property)) {
value = get(original, property);
} else if (typeDefaults.hasOwnProperty(property)) {
value = get(typeDefaults, property);
}
set(obj, property, value);
});
}
},
toggleUndo() {
const current = this.get(this.componentType);
const original = this.originalObject;
this.set('showUndo', !_.isEqual(current, original));
},
actions: {
undoChanges() {
const componentType = this.componentType;
const original = this.get('originalObject');
const obj = this.get(componentType);
this.removeObservers(obj.type);
this.revertToOriginal(true);
this.set('showUndo', false);
this.setupObservers(this.get(componentType).type);
},
changeType(type) {
const componentType = this.componentType;
const original = this.get('originalObject');
const obj = this.get(componentType);
this.removeObservers(obj.type);
obj.set('type', type);
this.revertToOriginal();
this.set('showUndo', type !== original.type);
this.setupObservers(type);
},
mappedFieldUpdated(property, type) {
this.get(this.componentType).notifyPropertyChange(property);
}
}
})

Datei anzeigen

@ -49,7 +49,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, object, type, result); result = this.validateValue(property, value, object, type, result);

Datei anzeigen

@ -22,7 +22,7 @@ export default DiscourseRoute.extend({
const parentModel = this.modelFor('adminWizardsWizard'); const parentModel = this.modelFor('adminWizardsWizard');
const wizard = CustomWizard.create((!model || model.create) ? {} : model); const wizard = CustomWizard.create((!model || model.create) ? {} : model);
controller.setProperties({ let props = {
wizardList: parentModel.wizard_list, wizardList: parentModel.wizard_list,
fieldTypes: selectKitContent(Object.keys(parentModel.field_types)), fieldTypes: selectKitContent(Object.keys(parentModel.field_types)),
userFields: parentModel.userFields, userFields: parentModel.userFields,
@ -32,6 +32,8 @@ export default DiscourseRoute.extend({
currentStep: wizard.steps[0], currentStep: wizard.steps[0],
currentAction: wizard.actions[0], currentAction: wizard.actions[0],
creating: model.create creating: model.create
}); };
controller.setProperties(props);
} }
}); });

Datei anzeigen

@ -175,13 +175,14 @@
items=wizard.actions items=wizard.actions
generateLabels=true}} generateLabels=true}}
{{#if currentAction}} {{#each wizard.actions as |action|}}
{{wizard-custom-action {{wizard-custom-action
action=currentAction action=action
currentActionId=currentAction.id
wizard=wizard wizard=wizard
removeAction="removeAction" removeAction="removeAction"
wizardFields=wizardFields}} wizardFields=wizardFields}}
{{/if}} {{/each}}
<div class='admin-wizard-buttons'> <div class='admin-wizard-buttons'>
<button {{action "save"}} disabled={{disableSave}} class='btn btn-primary'> <button {{action "save"}} disabled={{disableSave}} class='btn btn-primary'>

Datei anzeigen

@ -1,3 +1,11 @@
{{#if showUndo}}
{{d-button
action="undoChanges"
icon=undoIcon
label=undoKey
class="undo-changes"}}
{{/if}}
<div class="setting"> <div class="setting">
<div class="setting-label"> <div class="setting-label">
<label>{{i18n "admin.wizard.type"}}</label> <label>{{i18n "admin.wizard.type"}}</label>
@ -7,7 +15,7 @@
{{combo-box {{combo-box
value=action.type value=action.type
content=actionTypes content=actionTypes
onChange=(action (mut action.type)) onChange=(action "changeType")
options=(hash options=(hash
none="admin.wizard.field.type" none="admin.wizard.field.type"
)}} )}}
@ -36,6 +44,8 @@
<div class="setting-value"> <div class="setting-value">
{{wizard-mapper {{wizard-mapper
inputs=action.title inputs=action.title
property='title'
onUpdate=(action 'mappedFieldUpdated')
options=(hash options=(hash
wizardFieldSelection=true wizardFieldSelection=true
userFieldSelection='key,value' userFieldSelection='key,value'
@ -91,6 +101,8 @@
<div class="setting-value"> <div class="setting-value">
{{wizard-mapper {{wizard-mapper
inputs=action.category inputs=action.category
property='category'
onUpdate=(action 'mappedFieldUpdated')
options=(hash options=(hash
textSelection='key,value' textSelection='key,value'
wizardFieldSelection=true wizardFieldSelection=true
@ -110,6 +122,8 @@
<div class="setting-value"> <div class="setting-value">
{{wizard-mapper {{wizard-mapper
inputs=action.tags inputs=action.tags
property='tags'
onUpdate=(action 'mappedFieldUpdated')
options=(hash options=(hash
tagSelection='output' tagSelection='output'
outputDefaultSelection='tag' outputDefaultSelection='tag'
@ -131,6 +145,8 @@
<div class="setting-value"> <div class="setting-value">
{{wizard-mapper {{wizard-mapper
inputs=action.recipient inputs=action.recipient
property='recipient'
onUpdate=(action 'mappedFieldUpdated')
options=(hash options=(hash
textSelection='value,output' textSelection='value,output'
wizardFieldSelection=true wizardFieldSelection=true
@ -152,6 +168,8 @@
{{wizard-mapper {{wizard-mapper
inputs=action.profile_updates inputs=action.profile_updates
property='profile_updates'
onUpdate=(action 'mappedFieldUpdated')
options=(hash options=(hash
inputTypes='association' inputTypes='association'
textSelection='value' textSelection='value'
@ -223,6 +241,8 @@
<div class="setting-value"> <div class="setting-value">
{{wizard-mapper {{wizard-mapper
inputs=action.group inputs=action.group
property='group'
onUpdate=(action 'mappedFieldUpdated')
options=(hash options=(hash
textSelection='value,output' textSelection='value,output'
wizardFieldSelection='key,value,assignment' wizardFieldSelection='key,value,assignment'
@ -262,6 +282,8 @@
<div class="setting-value"> <div class="setting-value">
{{wizard-mapper {{wizard-mapper
inputs=action.custom_fields inputs=action.custom_fields
property='custom_fields'
onUpdate=(action 'mappedFieldUpdated')
options=(hash options=(hash
inputTypes='association' inputTypes='association'
wizardFieldSelection='value' wizardFieldSelection='value'
@ -282,6 +304,8 @@
<div class="setting-value"> <div class="setting-value">
{{wizard-mapper {{wizard-mapper
inputs=action.required inputs=action.required
property='required'
onUpdate=(action 'mappedFieldUpdated')
options=(hash options=(hash
textSelection='value' textSelection='value'
wizardFieldSelection=true wizardFieldSelection=true

Datei anzeigen

@ -1,3 +1,11 @@
{{#if showUndo}}
{{d-button
action="undoChanges"
icon=undoIcon
label=undoKey
class="undo-changes"}}
{{/if}}
<div class="setting"> <div class="setting">
<div class="setting-label"> <div class="setting-label">
<label>{{i18n 'admin.wizard.field.label'}}</label> <label>{{i18n 'admin.wizard.field.label'}}</label>
@ -50,7 +58,7 @@
{{combo-box {{combo-box
value=field.type value=field.type
content=fieldTypes content=fieldTypes
onChange=(action (mut field.type)) onChange=(action "changeType")
options=(hash options=(hash
none="admin.wizard.field.type" none="admin.wizard.field.type"
)}} )}}
@ -106,6 +114,8 @@
<div class="setting-value"> <div class="setting-value">
{{wizard-mapper {{wizard-mapper
inputs=field.prefill inputs=field.prefill
property='prefill'
onUpdate=(action 'mappedFieldUpdated')
options=prefillOptions}} options=prefillOptions}}
</div> </div>
</div> </div>
@ -120,6 +130,8 @@
<div class="setting-value"> <div class="setting-value">
{{wizard-mapper {{wizard-mapper
inputs=field.content inputs=field.content
property='content'
onUpdate=(action 'mappedFieldUpdated')
options=contentOptions}} options=contentOptions}}
</div> </div>
</div> </div>

Datei anzeigen

@ -102,10 +102,11 @@
items=step.fields items=step.fields
parentId=step.id}} parentId=step.id}}
{{#if currentField}} {{#each step.fields as |field|}}
{{wizard-custom-field {{wizard-custom-field
field=currentField field=field
currentFieldId=currentField.id
fieldTypes=fieldTypes fieldTypes=fieldTypes
removeField="removeField" removeField="removeField"
wizardFields=wizardFields}} wizardFields=wizardFields}}
{{/if}} {{/each}}

Datei anzeigen

@ -4,7 +4,8 @@
inputTypes=true inputTypes=true
inputType=inputType inputType=inputType
connectorType="type" connectorType="type"
options=options}} options=options
onUpdate=onUpdate}}
{{#if hasPairs}} {{#if hasPairs}}
<div class="mapper-pairs mapper-block"> <div class="mapper-pairs mapper-block">
@ -14,7 +15,8 @@
last=pair.last last=pair.last
inputType=inputType inputType=inputType
options=options options=options
removePair=(action 'removePair')}} removePair=(action 'removePair')
onUpdate=onUpdate}}
{{/each}} {{/each}}
{{#if canAddPair}} {{#if canAddPair}}
@ -32,7 +34,8 @@
connectors=connectors connectors=connectors
connectorType="output" connectorType="output"
inputType=inputType inputType=inputType
options=options}} options=options
onUpdate=onUpdate}}
{{/if}} {{/if}}
<div class="output mapper-block"> <div class="output mapper-block">
@ -41,7 +44,8 @@
inputType=input.type inputType=input.type
value=input.output value=input.output
activeType=input.output_type activeType=input.output_type
options=options}} options=options
onUpdate=onUpdate}}
</div> </div>
{{/if}} {{/if}}

Datei anzeigen

@ -4,7 +4,8 @@
inputType=inputType inputType=inputType
value=pair.key value=pair.key
activeType=pair.key_type activeType=pair.key_type
options=options}} options=options
onUpdate=onUpdate}}
</div> </div>
{{wizard-mapper-connector {{wizard-mapper-connector
@ -12,7 +13,8 @@
connectors=connectors connectors=connectors
connectorType="pair" connectorType="pair"
inputType=inputType inputType=inputType
options=options}} options=options
onUpdate=onUpdate}}
<div class="value mapper-block"> <div class="value mapper-block">
{{wizard-mapper-selector {{wizard-mapper-selector
@ -20,7 +22,8 @@
inputType=inputType inputType=inputType
value=pair.value value=pair.value
activeType=pair.value_type activeType=pair.value_type
options=options}} options=options
onUpdate=onUpdate}}
</div> </div>
{{#if showJoin}} {{#if showJoin}}

Datei anzeigen

@ -1,12 +1,16 @@
{{#each inputs as |input|}} {{#each inputs as |input|}}
{{#if input.connector}} {{#if input.connector}}
{{wizard-mapper-connector connector=input.connector connectorType="input"}} {{wizard-mapper-connector
connector=input.connector
connectorType="input"
onUpdate=(action 'inputUpdated')}}
{{/if}} {{/if}}
{{wizard-mapper-input {{wizard-mapper-input
input=input input=input
options=inputOptions options=inputOptions
remove=(action 'remove')}} remove=(action 'remove')
onUpdate=(action 'inputUpdated')}}
{{/each}} {{/each}}
{{#if canAdd}} {{#if canAdd}}

Datei anzeigen

@ -82,12 +82,27 @@
} }
.wizard-custom-field { .wizard-custom-field {
position: relative;
background: transparent; background: transparent;
background-color: dark-light-diff($primary, $secondary, 96%, -65%); background-color: dark-light-diff($primary, $secondary, 96%, -65%);
padding: 20px; padding: 20px;
} }
.wizard-custom-field,
.wizard-custom-action {
position: relative;
display: none;
&.visible {
display: flex;
}
.undo-changes {
position: absolute;
top: 0;
right: 0;
}
}
.admin-wizard-container.settings .wizard-basic-details { .admin-wizard-container.settings .wizard-basic-details {
justify-content: initial; justify-content: initial;

Datei anzeigen

@ -53,6 +53,8 @@ en:
group: "Group" group: "Group"
permitted: "Permitted" permitted: "Permitted"
advanced: "Advanced" advanced: "Advanced"
undo: "Undo"
clear: "Clear"
message: message:
select: "Select a wizard, or create a new one" select: "Select a wizard, or create a new one"