FEATURE: Wizard Manager
The "Transfer" UI has been upgraded into a full wizard manager, adding additional import/export features and bulk-delete functionality
Dieser Commit ist enthalten in:
Ursprung
37c18ff324
Commit
066eef4ef8
33 geänderte Dateien mit 777 neuen und 396 gelöschten Zeilen
|
@ -1,46 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { A } from "@ember/array";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['container', 'export'],
|
||||
selected: A(),
|
||||
|
||||
actions: {
|
||||
checkChanged(event) {
|
||||
this.set('exportMessage', '');
|
||||
|
||||
let selected = this.get('selected');
|
||||
|
||||
if (event.target.checked) {
|
||||
selected.addObject(event.target.id);
|
||||
} else if (!event.target.checked) {
|
||||
selected.removeObject(event.target.id);
|
||||
}
|
||||
|
||||
this.set('selected', selected);
|
||||
},
|
||||
|
||||
export() {
|
||||
const wizards = this.get('selected');
|
||||
|
||||
if (!wizards.length) {
|
||||
this.set('exportMessage', I18n.t("admin.wizard.transfer.export.none_selected"));
|
||||
} else {
|
||||
this.set('exportMessage', '');
|
||||
|
||||
let url = Discourse.BaseUrl;
|
||||
let route = '/admin/wizards/transfer/export';
|
||||
url += route + '?';
|
||||
|
||||
wizards.forEach((wizard) => {
|
||||
let step = 'wizards[]=' + wizard;
|
||||
step += '&';
|
||||
url += step;
|
||||
});
|
||||
|
||||
location.href = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,84 +0,0 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { default as discourseComputed } from 'discourse-common/utils/decorators';
|
||||
import { notEmpty } from "@ember/object/computed";
|
||||
import Component from "@ember/component";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['container', 'import'],
|
||||
hasLogs: notEmpty('logs'),
|
||||
|
||||
@discourseComputed('successIds', 'failureIds')
|
||||
logs(successIds, failureIds) {
|
||||
let logs = [];
|
||||
|
||||
if (successIds) {
|
||||
logs.push(...successIds.map(id => {
|
||||
return { id, type: 'success' };
|
||||
}));
|
||||
}
|
||||
|
||||
if (failureIds) {
|
||||
logs.push(...failureIds.map(id => {
|
||||
return { id, type: 'failure' };
|
||||
}));
|
||||
}
|
||||
|
||||
return logs;
|
||||
},
|
||||
|
||||
actions: {
|
||||
setFilePath(event) {
|
||||
this.set('importMessage', '');
|
||||
|
||||
// 512 kb is the max file size
|
||||
let maxFileSize = 512 * 1024;
|
||||
|
||||
if (event.target.files[0] === undefined) {
|
||||
this.set('filePath', null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxFileSize < event.target.files[0].size) {
|
||||
this.setProperties({
|
||||
importMessage: I18n.t('admin.wizard.transfer.import.file_size_error'),
|
||||
filePath: null
|
||||
});
|
||||
$('#file-url').val('');
|
||||
} else {
|
||||
this.set('filePath', event.target.files[0]);
|
||||
}
|
||||
},
|
||||
|
||||
import() {
|
||||
const filePath = this.get('filePath');
|
||||
let $formData = new FormData();
|
||||
|
||||
if (filePath) {
|
||||
$formData.append('file', filePath);
|
||||
|
||||
ajax('/admin/wizards/transfer/import', {
|
||||
type: 'POST',
|
||||
data: $formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
}).then(result => {
|
||||
if (result.error) {
|
||||
this.set('importMessage', result.error);
|
||||
} else {
|
||||
this.setProperties({
|
||||
successIds: result.success,
|
||||
failureIds: result.failed,
|
||||
fileName: $('#file-url')[0].files[0].name
|
||||
});
|
||||
}
|
||||
|
||||
this.set('filePath', null);
|
||||
$('#file-url').val('');
|
||||
});
|
||||
} else {
|
||||
this.set('importMessage', I18n.t("admin.wizard.transfer.import.no_file"));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,13 +1,28 @@
|
|||
import { default as discourseComputed } from 'discourse-common/utils/decorators';
|
||||
import { not, notEmpty } from "@ember/object/computed";
|
||||
import Component from "@ember/component";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: 'wizard-message',
|
||||
const icons = {
|
||||
error: 'times-circle',
|
||||
success: 'check-circle',
|
||||
info: 'info-circle'
|
||||
}
|
||||
|
||||
@discourseComputed('key', 'component')
|
||||
message(key, component) {
|
||||
return I18n.t(`admin.wizard.message.${component}.${key}`);
|
||||
export default Component.extend({
|
||||
classNameBindings: [':wizard-message', 'type', 'loading'],
|
||||
showDocumentation: not('loading'),
|
||||
showIcon: not('loading'),
|
||||
hasItems: notEmpty('items'),
|
||||
|
||||
@discourseComputed('type')
|
||||
icon(type) {
|
||||
return icons[type] || 'info-circle';
|
||||
},
|
||||
|
||||
@discourseComputed('key', 'component', 'opts')
|
||||
message(key, component, opts) {
|
||||
return I18n.t(`admin.wizard.message.${component}.${key}`, opts || {});
|
||||
},
|
||||
|
||||
@discourseComputed('component')
|
||||
|
|
229
assets/javascripts/discourse/controllers/admin-wizards-manager.js.es6
Normale Datei
229
assets/javascripts/discourse/controllers/admin-wizards-manager.js.es6
Normale Datei
|
@ -0,0 +1,229 @@
|
|||
import Controller from "@ember/controller";
|
||||
import {
|
||||
default as discourseComputed,
|
||||
observes
|
||||
} from 'discourse-common/utils/decorators';
|
||||
import { empty } from "@ember/object/computed";
|
||||
import CustomWizardManager from '../models/custom-wizard-manager';
|
||||
import { A } from "@ember/array";
|
||||
import I18n from "I18n";
|
||||
import { underscore } from "@ember/string";
|
||||
|
||||
export default Controller.extend({
|
||||
messageUrl: 'https://thepavilion.io/t/3652',
|
||||
messageKey: 'info',
|
||||
messageIcon: 'info-circle',
|
||||
messageClass: 'info',
|
||||
importDisabled: empty('file'),
|
||||
exportWizards: A(),
|
||||
destroyWizards: A(),
|
||||
exportDisabled: empty('exportWizards'),
|
||||
destoryDisabled: empty('destroyWizards'),
|
||||
|
||||
setMessage(type, key, opts={}, items=[]) {
|
||||
this.setProperties({
|
||||
messageKey: key,
|
||||
messageOpts: opts,
|
||||
messageType: type,
|
||||
messageItems: items
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.setProperties({
|
||||
messageKey: 'info',
|
||||
messageOpts: null,
|
||||
messageType: null,
|
||||
messageItems: null
|
||||
})
|
||||
}, 10000);
|
||||
},
|
||||
|
||||
buildWizardLink(wizard) {
|
||||
let html = `<a href='/admin/wizards/wizard/${wizard.id}'>${wizard.name}</a>`;
|
||||
html += `<span class='action'>${I18n.t('admin.wizard.manager.imported')}</span>`;
|
||||
return {
|
||||
icon: 'check-circle',
|
||||
html
|
||||
};
|
||||
},
|
||||
|
||||
buildDestroyedItem(destroyed) {
|
||||
let html = `<span data-wizard-id="${destroyed.id}">${destroyed.name}</span>`;
|
||||
html += `<span class='action'>${I18n.t('admin.wizard.manager.destroyed')}</span>`;
|
||||
return {
|
||||
icon: 'check-circle',
|
||||
html
|
||||
};
|
||||
},
|
||||
|
||||
buildFailureItem(failure) {
|
||||
return {
|
||||
icon: 'times-circle',
|
||||
html: `${failure.id}: ${failure.messages}`
|
||||
};
|
||||
},
|
||||
|
||||
clearFile() {
|
||||
this.setProperties({
|
||||
file: null,
|
||||
filename: null
|
||||
});
|
||||
$('#file-upload').val('');
|
||||
},
|
||||
|
||||
@observes('importing', 'destroying')
|
||||
setLoadingMessages() {
|
||||
if (this.importing) {
|
||||
this.setMessage("loading", "importing");
|
||||
}
|
||||
if (this.destroying) {
|
||||
this.setMessage("loading", "destroying");
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
upload() {
|
||||
$('#file-upload').click();
|
||||
},
|
||||
|
||||
clearFile() {
|
||||
this.clearFile();
|
||||
},
|
||||
|
||||
setFile(event) {
|
||||
let maxFileSize = 512 * 1024;
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (file === undefined) {
|
||||
this.set('file', null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxFileSize < file.size) {
|
||||
this.setMessage("error", "file_size_error");
|
||||
this.set("file", null);
|
||||
$('#file-upload').val('');
|
||||
} else {
|
||||
this.setProperties({
|
||||
file,
|
||||
filename: file.name
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
selectWizard(event) {
|
||||
const type = event.target.classList.contains('export') ? 'export' : 'destroy';
|
||||
const wizards = this.get(`${type}Wizards`);
|
||||
const checked = event.target.checked;
|
||||
|
||||
let wizardId = event.target.closest('tr').getAttribute('data-wizard-id');
|
||||
|
||||
if (wizardId) {
|
||||
wizardId = underscore(wizardId);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
wizards.addObject(wizardId);
|
||||
} else {
|
||||
wizards.removeObject(wizardId);
|
||||
}
|
||||
},
|
||||
|
||||
import() {
|
||||
const file = this.get('file');
|
||||
|
||||
if (!file) {
|
||||
this.setMessage("error", 'no_file');
|
||||
return;
|
||||
}
|
||||
|
||||
let $formData = new FormData();
|
||||
$formData.append('file', file);
|
||||
|
||||
this.set('importing', true);
|
||||
this.setMessage("loading", "importing");
|
||||
|
||||
CustomWizardManager.import($formData).then(result => {
|
||||
if (result.error) {
|
||||
this.setMessage("error", "server_error", {
|
||||
message: result.error
|
||||
});
|
||||
} else {
|
||||
this.setMessage("success", "import_complete", {},
|
||||
result.imported.map(imported => {
|
||||
return this.buildWizardLink(imported);
|
||||
}).concat(
|
||||
result.failures.map(failure => {
|
||||
return this.buildFailureItem(failure);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (result.imported.length) {
|
||||
this.get('wizards').addObjects(result.imported);
|
||||
}
|
||||
}
|
||||
this.clearFile();
|
||||
}).finally(() => {
|
||||
this.set('importing', false);
|
||||
});
|
||||
},
|
||||
|
||||
export() {
|
||||
const exportWizards = this.get('exportWizards');
|
||||
|
||||
if (!exportWizards.length) {
|
||||
this.setMessage("error", 'none_selected');
|
||||
} else {
|
||||
CustomWizardManager.export(exportWizards);
|
||||
exportWizards.clear();
|
||||
$('input.export').prop("checked", false);
|
||||
}
|
||||
},
|
||||
|
||||
destroy() {
|
||||
const destroyWizards = this.get('destroyWizards');
|
||||
|
||||
if (!destroyWizards.length) {
|
||||
this.setMessage("error", 'none_selected');
|
||||
} else {
|
||||
this.set('destroying', true);
|
||||
|
||||
CustomWizardManager.destroy(destroyWizards).then((result) => {
|
||||
if (result.error) {
|
||||
this.setMessage("error", "server_error", {
|
||||
message: result.error
|
||||
});
|
||||
} else {
|
||||
this.setMessage("success", "destroy_complete", {},
|
||||
result.destroyed.map(destroyed => {
|
||||
return this.buildDestroyedItem(destroyed);
|
||||
}).concat(
|
||||
result.failures.map(failure => {
|
||||
return this.buildFailureItem(failure);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (result.destroyed.length) {
|
||||
const destroyedIds = result.destroyed.map(d => d.id);
|
||||
const destroyWizards = this.get('destroyWizards');
|
||||
const wizards = this.get('wizards');
|
||||
|
||||
wizards.removeObjects(
|
||||
wizards.filter(w => {
|
||||
return destroyedIds.includes(w.id);
|
||||
})
|
||||
);
|
||||
|
||||
destroyWizards.removeObjects(destroyedIds);
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
this.set('destroying', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
import Controller from "@ember/controller";
|
||||
|
||||
export default Controller.extend();
|
|
@ -19,7 +19,7 @@ export default {
|
|||
|
||||
this.route('adminWizardsLogs', { path: '/logs', resetNamespace: true });
|
||||
|
||||
this.route('adminWizardsTransfer', { path: '/transfer', resetNamespace: true });
|
||||
this.route('adminWizardsManager', { path: '/manager', resetNamespace: true });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
43
assets/javascripts/discourse/models/custom-wizard-manager.js.es6
Normale Datei
43
assets/javascripts/discourse/models/custom-wizard-manager.js.es6
Normale Datei
|
@ -0,0 +1,43 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import EmberObject from "@ember/object";
|
||||
|
||||
const CustomWizardManager = EmberObject.extend();
|
||||
|
||||
const basePath = "admin/wizards/manager";
|
||||
|
||||
CustomWizardManager.reopenClass({
|
||||
import($formData) {
|
||||
return ajax(`/${basePath}/import`, {
|
||||
type: 'POST',
|
||||
data: $formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
export(wizardIds) {
|
||||
let url = `${Discourse.BaseUrl}/${basePath}/export?`;
|
||||
|
||||
wizardIds.forEach((wizardId, index) => {
|
||||
let step = 'wizard_ids[]=' + wizardId;
|
||||
if (index !== wizardIds[wizardIds.length - 1]) {
|
||||
step += '&';
|
||||
}
|
||||
url += step;
|
||||
});
|
||||
|
||||
location.href = url;
|
||||
},
|
||||
|
||||
destroy(wizardIds) {
|
||||
return ajax(`/${basePath}/destroy`, {
|
||||
type: "DELETE",
|
||||
data: {
|
||||
wizard_ids: wizardIds
|
||||
}
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
});
|
||||
|
||||
export default CustomWizardManager;
|
|
@ -4,6 +4,7 @@ import { buildProperties, present, mapped } from '../lib/wizard-json';
|
|||
import { listProperties, camelCase, snakeCase } from '../lib/wizard';
|
||||
import wizardSchema from '../lib/wizard-schema';
|
||||
import { Promise } from "rsvp";
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
const CustomWizard = EmberObject.extend({
|
||||
save(opts) {
|
||||
|
@ -185,7 +186,7 @@ const CustomWizard = EmberObject.extend({
|
|||
remove() {
|
||||
return ajax(`/admin/wizards/wizard/${this.id}`, {
|
||||
type: 'DELETE'
|
||||
}).then(() => this.destroy());
|
||||
}).then(() => this.destroy()).catch(popupAjaxError);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -195,13 +196,13 @@ CustomWizard.reopenClass({
|
|||
type: 'GET'
|
||||
}).then(result => {
|
||||
return result.wizard_list;
|
||||
});
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
submissions(wizardId) {
|
||||
return ajax(`/admin/wizards/submissions/${wizardId}`, {
|
||||
type: "GET"
|
||||
});
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
create(wizardJson = {}) {
|
||||
|
|
83
assets/javascripts/discourse/templates/admin-wizards-manager.hbs
Normale Datei
83
assets/javascripts/discourse/templates/admin-wizards-manager.hbs
Normale Datei
|
@ -0,0 +1,83 @@
|
|||
<div class="admin-wizard-controls">
|
||||
<h3>{{i18n 'admin.wizard.manager.title'}}</h3>
|
||||
|
||||
<div class="buttons">
|
||||
{{#if filename}}
|
||||
<div class="filename">
|
||||
<a {{action 'clearFile'}}>
|
||||
{{d-icon 'times'}}
|
||||
</a>
|
||||
<span>{{filename}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{input
|
||||
id='file-upload'
|
||||
type="file"
|
||||
accept="application/json"
|
||||
change=(action "setFile")}}
|
||||
{{d-button
|
||||
id="upload-button"
|
||||
label="admin.wizard.manager.upload"
|
||||
action=(action "upload")}}
|
||||
{{d-button
|
||||
id="import-button"
|
||||
label="admin.wizard.manager.import"
|
||||
action=(action "import")
|
||||
disabled=importDisabled}}
|
||||
{{d-button
|
||||
id="export-button"
|
||||
label="admin.wizard.manager.export"
|
||||
action=(action "export")
|
||||
disabled=exportDisabled}}
|
||||
{{d-button
|
||||
id="destroy-button"
|
||||
label="admin.wizard.manager.destroy"
|
||||
action=(action "destroy")
|
||||
disabled=destoryDisabled}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{wizard-message
|
||||
key=messageKey
|
||||
url=messageUrl
|
||||
type=messageType
|
||||
opts=messageOpts
|
||||
items=messageItems
|
||||
loading=loading
|
||||
component='manager'}}
|
||||
|
||||
<div class="admin-wizard-container">
|
||||
<table class="table grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.wizard.label'}}</th>
|
||||
<th class="control-column">{{i18n 'admin.wizard.manager.export'}}</th>
|
||||
<th class="control-column">{{i18n 'admin.wizard.manager.destroy'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each wizards as |wizard|}}
|
||||
<tr data-wizard-id={{dasherize wizard.id}}>
|
||||
<td>
|
||||
{{#link-to "adminWizardsWizardShow" (dasherize wizard.id)}}
|
||||
{{wizard.name}}
|
||||
{{/link-to}}
|
||||
</td>
|
||||
<td class="control-column">
|
||||
{{input
|
||||
type="checkbox"
|
||||
class="export"
|
||||
change=(action 'selectWizard')}}
|
||||
</td>
|
||||
<td class="control-column">
|
||||
{{input
|
||||
type="checkbox"
|
||||
class="destroy"
|
||||
change=(action 'selectWizard')}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -1,2 +0,0 @@
|
|||
{{wizard-export wizards=wizards}}
|
||||
{{wizard-import}}
|
|
@ -6,7 +6,7 @@
|
|||
{{nav-item route='adminWizardsApi' label='admin.wizard.api.nav_label'}}
|
||||
{{/if}}
|
||||
{{nav-item route='adminWizardsLogs' label='admin.wizard.log.nav_label'}}
|
||||
{{nav-item route='adminWizardsTransfer' label='admin.wizard.transfer.nav_label'}}
|
||||
{{nav-item route='adminWizardsManager' label='admin.wizard.manager.nav_label'}}
|
||||
{{/admin-nav}}
|
||||
|
||||
<div class="admin-container">
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
<h2>{{i18n 'admin.wizard.transfer.export.label'}}</h2>
|
||||
|
||||
<ul class="wizard-list-select">
|
||||
{{#each wizards as |w|}}
|
||||
<li>
|
||||
{{input
|
||||
type="checkbox"
|
||||
id=(dasherize w.id)
|
||||
change=(action 'checkChanged')}}
|
||||
|
||||
{{#link-to "adminWizardsWizardShow" (dasherize w.id)}}
|
||||
{{w.name}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
{{d-button id="export-button"
|
||||
class="btn btn-primary side"
|
||||
label="admin.wizard.transfer.export.label"
|
||||
action=(action "export")}}
|
||||
|
||||
{{#if exportMessage}}
|
||||
<div class="export-message">
|
||||
{{exportMessage}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,32 +0,0 @@
|
|||
<h2>{{i18n 'admin.wizard.transfer.import.label'}}</h2>
|
||||
|
||||
<div class="controls">
|
||||
{{input id='file-url' type="file" change=(action "setFilePath")}}
|
||||
|
||||
{{#if importMessage}}
|
||||
<div class="import-message">
|
||||
{{importMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{d-button id="import-button"
|
||||
class="btn btn-primary side"
|
||||
label="admin.wizard.transfer.import.label"
|
||||
action=(action "import")}}
|
||||
</div>
|
||||
|
||||
{{#if hasLogs}}
|
||||
<div class="import-logs">
|
||||
<div class="title">
|
||||
{{i18n 'admin.wizard.transfer.import.logs' fileName=fileName}}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{{#each logs as |l|}}
|
||||
<li class="import-log">
|
||||
{{i18n (concat 'admin.wizard.transfer.import.' l.type) id=l.id}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,12 +1,26 @@
|
|||
<div class="message-block">
|
||||
{{d-icon 'info-circle'}}
|
||||
<span>{{message}}</span>
|
||||
<div class="message-block primary">
|
||||
{{#if showIcon}}
|
||||
{{d-icon icon}}
|
||||
{{/if}}
|
||||
<span class="message-content">{{{message}}}</span>
|
||||
{{#if hasItems}}
|
||||
<ul>
|
||||
{{#each items as |item|}}
|
||||
<li>
|
||||
<span>{{d-icon item.icon}}</span>
|
||||
<span>{{{item.html}}}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="message-block">
|
||||
{{#if showDocumentation}}
|
||||
<div class="message-block">
|
||||
{{d-icon 'question-circle'}}
|
||||
|
||||
<a href={{url}} target="_blank">
|
||||
{{documentation}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,5 +1,5 @@
|
|||
@import 'wizard-mapper';
|
||||
@import 'wizard-transfer';
|
||||
@import 'wizard-manager';
|
||||
@import 'wizard-api';
|
||||
|
||||
.admin-wizard-controls {
|
||||
|
@ -20,22 +20,50 @@
|
|||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.message-block {
|
||||
.d-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.d-icon-check-circle {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.d-icon-times-circle {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
a + a {
|
||||
border-left: 1px solid $primary-medium;
|
||||
padding-left: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.message {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.list-colon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
span.action {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& + div {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.wizard-submissions {
|
||||
|
|
60
assets/stylesheets/common/wizard-manager.scss
Normale Datei
60
assets/stylesheets/common/wizard-manager.scss
Normale Datei
|
@ -0,0 +1,60 @@
|
|||
.admin-wizards-manager .admin-wizard-controls {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
|
||||
> * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#import-button:enabled,
|
||||
#export-button:enabled {
|
||||
background-color: $tertiary;
|
||||
color: $secondary;
|
||||
}
|
||||
|
||||
#destroy-button:enabled {
|
||||
background-color: $danger;
|
||||
color: $secondary;
|
||||
}
|
||||
}
|
||||
|
||||
#file-upload {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filename {
|
||||
padding: 0 10px;
|
||||
border: 1px solid $primary;
|
||||
display: inline-flex;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
|
||||
a {
|
||||
color: $primary;
|
||||
margin-right: 5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wizard-list-select {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.wizard-action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.control-column {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
.admin-wizards-transfer .admin-container .container {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
#file-url {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wizard-list-select {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.wizard-action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.import-message {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.import-logs {
|
||||
margin-top: 20px;
|
||||
|
||||
.title {
|
||||
font-weight: 800;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
|
@ -75,6 +75,18 @@ en:
|
|||
custom_fields:
|
||||
create: "Create or edit a custom field record"
|
||||
documentation: Check out the custom field documentation
|
||||
manager:
|
||||
info: "Export, import or destroy wizards"
|
||||
documentation: Check out the manager documentation
|
||||
none_selected: Please select atleast one wizard
|
||||
no_file: Please choose a file to import
|
||||
file_size_error: The file size must be 512kb or less
|
||||
file_format_error: The file must be a .json file
|
||||
server_error: "Error: {{message}}"
|
||||
importing: Importing wizards...
|
||||
destroying: Destroying wizards...
|
||||
import_complete: Import complete
|
||||
destroy_complete: Destruction complete
|
||||
|
||||
editor:
|
||||
show: "Show"
|
||||
|
@ -373,18 +385,15 @@ en:
|
|||
log:
|
||||
nav_label: "Logs"
|
||||
|
||||
transfer:
|
||||
nav_label: "Transfer"
|
||||
export:
|
||||
label: "Export"
|
||||
none_selected: "Please select atleast one wizard"
|
||||
import:
|
||||
label: "Import"
|
||||
logs: "Import logs for {{fileName}}"
|
||||
success: 'Wizard "{{id}}" saved successfully'
|
||||
failure: 'Wizard "{{id}}" could not be saved'
|
||||
no_file: "Please choose a file to import"
|
||||
file_size_error: "The file must be JSON and 512kb or less"
|
||||
manager:
|
||||
nav_label: Manage
|
||||
title: Manage Wizards
|
||||
export: Export
|
||||
import: Import
|
||||
imported: imported
|
||||
upload: Select wizards.json
|
||||
destroy: Destroy
|
||||
destroyed: destroyed
|
||||
|
||||
wizard_js:
|
||||
group:
|
||||
|
|
|
@ -303,7 +303,7 @@ pt_br:
|
|||
log:
|
||||
nav_label: "Logs"
|
||||
|
||||
transfer:
|
||||
manager:
|
||||
nav_label: "Transferir"
|
||||
export:
|
||||
label: "Exportar"
|
||||
|
|
|
@ -30,18 +30,23 @@ en:
|
|||
no_skip: "Wizard can't be skipped"
|
||||
export:
|
||||
error:
|
||||
select_one: "Please select at least one wizard"
|
||||
select_one: "Please select at least one valid wizard"
|
||||
invalid_wizards: "No valid wizards selected"
|
||||
|
||||
import:
|
||||
error:
|
||||
no_file: "No file selected"
|
||||
file_large: "File too large"
|
||||
invalid_json: "File is not a valid json file"
|
||||
no_valid_wizards: "File doesn't contain any valid wizards"
|
||||
|
||||
destroy:
|
||||
error:
|
||||
no_template: No template found
|
||||
default: Error destroying wizard
|
||||
|
||||
validation:
|
||||
required: "%{property} is required"
|
||||
conflict: "Wizard with %{wizard_id} already exists"
|
||||
conflict: "Wizard with id '%{wizard_id}' already exists"
|
||||
after_time: "After time setting is invalid"
|
||||
|
||||
site_settings:
|
||||
|
|
|
@ -36,8 +36,9 @@ Discourse::Application.routes.append do
|
|||
|
||||
get 'admin/wizards/logs' => 'admin_logs#index'
|
||||
|
||||
get 'admin/wizards/transfer' => 'admin_transfer#index'
|
||||
get 'admin/wizards/transfer/export' => 'admin_transfer#export'
|
||||
post 'admin/wizards/transfer/import' => 'admin_transfer#import'
|
||||
get 'admin/wizards/manager' => 'admin_manager#index'
|
||||
get 'admin/wizards/manager/export' => 'admin_manager#export'
|
||||
post 'admin/wizards/manager/import' => 'admin_manager#import'
|
||||
delete 'admin/wizards/manager/destroy' => 'admin_manager#destroy'
|
||||
end
|
||||
end
|
|
@ -15,4 +15,8 @@ class CustomWizard::AdminController < ::Admin::AdminController
|
|||
def custom_field_list
|
||||
serialize_data(CustomWizard::CustomField.list, CustomWizard::CustomFieldSerializer)
|
||||
end
|
||||
|
||||
def render_error(message)
|
||||
render json: failed_json.merge(error: message)
|
||||
end
|
||||
end
|
123
controllers/custom_wizard/admin/manager.rb
Normale Datei
123
controllers/custom_wizard/admin/manager.rb
Normale Datei
|
@ -0,0 +1,123 @@
|
|||
class CustomWizard::AdminManagerController < CustomWizard::AdminController
|
||||
skip_before_action :check_xhr, only: [:export]
|
||||
before_action :get_wizard_ids, except: [:import]
|
||||
|
||||
def export
|
||||
templates = []
|
||||
|
||||
@wizard_ids.each do |wizard_id|
|
||||
if template = CustomWizard::Template.find(wizard_id)
|
||||
templates.push(template)
|
||||
end
|
||||
end
|
||||
|
||||
if templates.empty?
|
||||
return render_error(I18n.t('wizard.export.error.invalid_wizards'))
|
||||
end
|
||||
|
||||
basename = SiteSetting.title.parameterize || 'discourse'
|
||||
time = Time.now.to_i
|
||||
filename = "#{basename}-wizards-#{time}.json"
|
||||
|
||||
send_data templates.to_json,
|
||||
type: "application/json",
|
||||
disposition: 'attachment',
|
||||
filename: filename
|
||||
end
|
||||
|
||||
def import
|
||||
file = File.read(params['file'].tempfile)
|
||||
|
||||
if file.nil?
|
||||
return render_error(I18n.t('wizard.export.error.no_file'))
|
||||
end
|
||||
|
||||
file_size = file.size
|
||||
max_file_size = 512 * 1024
|
||||
|
||||
if max_file_size < file_size
|
||||
return render_error(I18n.t('wizard.import.error.file_large'))
|
||||
end
|
||||
|
||||
begin
|
||||
template_json = JSON.parse file
|
||||
rescue JSON::ParserError
|
||||
return render_error(I18n.t('wizard.import.error.invalid_json'))
|
||||
end
|
||||
|
||||
imported = []
|
||||
failures = []
|
||||
|
||||
template_json.each do |json|
|
||||
template = CustomWizard::Template.new(json)
|
||||
template.save(skip_jobs: true, create: true)
|
||||
|
||||
if template.errors.any?
|
||||
failures.push(
|
||||
id: json['id'],
|
||||
messages: template.errors.full_messages.join(', ')
|
||||
)
|
||||
else
|
||||
imported.push(
|
||||
id: json['id'],
|
||||
name: json['name']
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
render json: success_json.merge(
|
||||
imported: imported,
|
||||
failures: failures
|
||||
)
|
||||
end
|
||||
|
||||
def destroy
|
||||
destroyed = []
|
||||
failures = []
|
||||
|
||||
@wizard_ids.each do |wizard_id|
|
||||
template = CustomWizard::Template.find(wizard_id)
|
||||
|
||||
if template && CustomWizard::Template.remove(wizard_id)
|
||||
destroyed.push(
|
||||
id: wizard_id,
|
||||
name: template['name']
|
||||
)
|
||||
else
|
||||
failures.push(
|
||||
id: wizard_id,
|
||||
messages: I18n.t("wizard.destroy.error.#{template ? 'default' : 'no_template'}")
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
render json: success_json.merge(
|
||||
destroyed: destroyed,
|
||||
failures: failures
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_wizard_ids
|
||||
if params['wizard_ids'].blank?
|
||||
return render_error(I18n.t('wizard.export.error.select_one'))
|
||||
end
|
||||
|
||||
wizard_ids = []
|
||||
|
||||
params['wizard_ids'].each do |wizard_id|
|
||||
begin
|
||||
wizard_ids.push(wizard_id.underscore)
|
||||
rescue
|
||||
#
|
||||
end
|
||||
end
|
||||
|
||||
if wizard_ids.empty?
|
||||
return render_error(I18n.t('wizard.export.error.invalid_wizards'))
|
||||
end
|
||||
|
||||
@wizard_ids = wizard_ids
|
||||
end
|
||||
end
|
|
@ -1,68 +0,0 @@
|
|||
class CustomWizard::AdminTransferController < CustomWizard::AdminController
|
||||
skip_before_action :check_xhr, :only => [:export]
|
||||
|
||||
def export
|
||||
wizard_ids = params['wizards']
|
||||
templates = []
|
||||
|
||||
if wizard_ids.nil?
|
||||
render json: { error: I18n.t('wizard.export.error.select_one') }
|
||||
return
|
||||
end
|
||||
|
||||
wizard_ids.each do |wizard_id|
|
||||
if template = CustomWizard::Template.find(wizard_id)
|
||||
templates.push(template)
|
||||
end
|
||||
end
|
||||
|
||||
send_data templates.to_json,
|
||||
type: "application/json",
|
||||
disposition: 'attachment',
|
||||
filename: 'wizards.json'
|
||||
end
|
||||
|
||||
def import
|
||||
file = File.read(params['file'].tempfile)
|
||||
|
||||
if file.nil?
|
||||
render json: { error: I18n.t('wizard.import.error.no_file') }
|
||||
return
|
||||
end
|
||||
|
||||
file_size = file.size
|
||||
max_file_size = 512 * 1024
|
||||
|
||||
if max_file_size < file_size
|
||||
render json: { error: I18n.t('wizard.import.error.file_large') }
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
template_json = JSON.parse file
|
||||
rescue JSON::ParserError
|
||||
render json: { error: I18n.t('wizard.import.error.invalid_json') }
|
||||
return
|
||||
end
|
||||
|
||||
success_ids = []
|
||||
failed_ids = []
|
||||
|
||||
template_json.each do |t_json|
|
||||
template = CustomWizard::Template.new(t_json)
|
||||
template.save(skip_jobs: true)
|
||||
|
||||
if template.errors.any?
|
||||
failed_ids.push t_json['id']
|
||||
else
|
||||
success_ids.push t_json['id']
|
||||
end
|
||||
end
|
||||
|
||||
if success_ids.length == 0
|
||||
render json: { error: I18n.t('wizard.import.error.no_valid_wizards') }
|
||||
else
|
||||
render json: { success: success_ids, failed: failed_ids }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"result": {
|
||||
"covered_percent": 88.16
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ module Jobs
|
|||
|
||||
def execute(args)
|
||||
User.human_users.each do |u|
|
||||
if u.custom_fields['redirect_to_wizard'] === args[:wizard_id]
|
||||
if u.custom_fields['redirect_to_wizard'] == args[:wizard_id]
|
||||
u.custom_fields.delete('redirect_to_wizard')
|
||||
u.save_custom_fields(true)
|
||||
end
|
||||
|
|
|
@ -36,6 +36,8 @@ class CustomWizard::Template
|
|||
def self.remove(wizard_id)
|
||||
wizard = CustomWizard::Wizard.create(wizard_id)
|
||||
|
||||
return false if !wizard
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
PluginStore.remove('custom_wizard', wizard.id)
|
||||
|
||||
|
@ -44,6 +46,8 @@ class CustomWizard::Template
|
|||
Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard_id)
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def self.exists?(wizard_id)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class CustomWizard::Validator
|
||||
include HasErrors
|
||||
include ActiveModel::Model
|
||||
|
||||
def initialize(data, opts={})
|
||||
@data = data
|
||||
|
@ -50,14 +51,14 @@ class CustomWizard::Validator
|
|||
def check_required(object, type)
|
||||
CustomWizard::Validator.required[type].each do |property|
|
||||
if object[property].blank?
|
||||
errors.add :validation, I18n.t("wizard.validation.required", property: property)
|
||||
errors.add :base, I18n.t("wizard.validation.required", property: property)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_id(object, type)
|
||||
if type === :wizard && @opts[:create] && CustomWizard::Template.exists?(object[:id])
|
||||
errors.add :validation, I18n.t("wizard.validation.conflict", id: object[:id])
|
||||
errors.add :base, I18n.t("wizard.validation.conflict", wizard_id: object[:id])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -75,7 +76,7 @@ class CustomWizard::Validator
|
|||
end
|
||||
|
||||
if invalid_time || active_time.blank? || active_time < Time.now.utc
|
||||
errors.add :validation, I18n.t("wizard.validation.after_time")
|
||||
errors.add :base, I18n.t("wizard.validation.after_time")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -42,7 +42,7 @@ after_initialize do
|
|||
../controllers/custom_wizard/admin/submissions.rb
|
||||
../controllers/custom_wizard/admin/api.rb
|
||||
../controllers/custom_wizard/admin/logs.rb
|
||||
../controllers/custom_wizard/admin/transfer.rb
|
||||
../controllers/custom_wizard/admin/manager.rb
|
||||
../controllers/custom_wizard/admin/custom_fields.rb
|
||||
../controllers/custom_wizard/wizard.rb
|
||||
../controllers/custom_wizard/steps.rb
|
||||
|
|
|
@ -25,6 +25,15 @@ describe Jobs::ClearAfterTimeWizard do
|
|||
|
||||
CustomWizard::Template.save(after_time_template)
|
||||
|
||||
Jobs::SetAfterTimeWizard.new.execute(wizard_id: 'super_mega_fun_wizard')
|
||||
|
||||
expect(
|
||||
UserCustomField.where(
|
||||
name: 'redirect_to_wizard',
|
||||
value: 'super_mega_fun_wizard'
|
||||
).length
|
||||
).to eq(3)
|
||||
|
||||
described_class.new.execute(wizard_id: 'super_mega_fun_wizard')
|
||||
|
||||
expect(
|
||||
|
|
104
spec/requests/custom_wizard/admin/manager_controller_spec.rb
Normale Datei
104
spec/requests/custom_wizard/admin/manager_controller_spec.rb
Normale Datei
|
@ -0,0 +1,104 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe CustomWizard::AdminManagerController do
|
||||
fab!(:admin_user) { Fabricate(:user, admin: true) }
|
||||
|
||||
let(:template) {
|
||||
JSON.parse(File.open(
|
||||
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json"
|
||||
).read)
|
||||
}
|
||||
|
||||
before do
|
||||
sign_in(admin_user)
|
||||
|
||||
template_2 = template.dup
|
||||
template_2["id"] = 'super_mega_fun_wizard_2'
|
||||
|
||||
template_3 = template.dup
|
||||
template_3["id"] = 'super_mega_fun_wizard_3'
|
||||
template_3["after_signup"] = true
|
||||
|
||||
@template_array = [template, template_2, template_3]
|
||||
|
||||
FileUtils.mkdir_p(file_from_fixtures_tmp_folder) unless Dir.exists?(file_from_fixtures_tmp_folder)
|
||||
@tmp_file_path = File.join(file_from_fixtures_tmp_folder, SecureRandom.hex << 'wizards.json')
|
||||
File.write(@tmp_file_path, @template_array.to_json)
|
||||
end
|
||||
|
||||
it 'exports all the wizard templates' do
|
||||
@template_array.each do |template|
|
||||
CustomWizard::Template.save(template, skip_jobs: true)
|
||||
end
|
||||
|
||||
get '/admin/wizards/manager/export.json', params: {
|
||||
wizard_ids: [
|
||||
'super_mega_fun_wizard',
|
||||
'super_mega_fun_wizard_2',
|
||||
'super_mega_fun_wizard_3'
|
||||
]
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body).to match_array(@template_array)
|
||||
end
|
||||
|
||||
context "import" do
|
||||
it "works" do
|
||||
templates = @template_array.map { |t| t.slice('id', 'name') }
|
||||
|
||||
post '/admin/wizards/manager/import.json', params: {
|
||||
file: fixture_file_upload(File.open(@tmp_file_path))
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body['imported']).to match_array(templates)
|
||||
expect(CustomWizard::Template.list.map {|t| t.slice('id', 'name') }).to match_array(templates)
|
||||
end
|
||||
|
||||
it 'rejects a template with the same id as a saved template' do
|
||||
templates = @template_array.map { |t| t.slice('id', 'name') }
|
||||
|
||||
post '/admin/wizards/manager/import.json', params: {
|
||||
file: fixture_file_upload(File.open(@tmp_file_path))
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body['imported']).to match_array(templates)
|
||||
|
||||
post '/admin/wizards/manager/import.json', params: {
|
||||
file: fixture_file_upload(File.open(@tmp_file_path))
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body['failures']).to match_array(
|
||||
@template_array.map do |t|
|
||||
{
|
||||
id: t['id'],
|
||||
messages: I18n.t("wizard.validation.conflict", wizard_id: t['id'])
|
||||
}.as_json
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'destroys wizard templates' do
|
||||
templates = @template_array.map { |t| t.slice('id', 'name') }
|
||||
|
||||
@template_array.each do |template|
|
||||
CustomWizard::Template.save(template, skip_jobs: true)
|
||||
end
|
||||
|
||||
delete '/admin/wizards/manager/destroy.json', params: {
|
||||
wizard_ids: [
|
||||
'super_mega_fun_wizard',
|
||||
'super_mega_fun_wizard_2',
|
||||
'super_mega_fun_wizard_3'
|
||||
]
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body['destroyed']).to match_array(templates)
|
||||
expect(CustomWizard::Template.list.length).to eq(0)
|
||||
end
|
||||
end
|
|
@ -1,52 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe CustomWizard::AdminTransferController do
|
||||
fab!(:admin_user) { Fabricate(:user, admin: true) }
|
||||
|
||||
let(:template) {
|
||||
JSON.parse(File.open(
|
||||
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json"
|
||||
).read)
|
||||
}
|
||||
|
||||
before do
|
||||
sign_in(admin_user)
|
||||
|
||||
CustomWizard::Template.save(template, skip_jobs: true)
|
||||
|
||||
template_2 = template.dup
|
||||
template_2["id"] = 'super_mega_fun_wizard_2'
|
||||
CustomWizard::Template.save(template_2, skip_jobs: true)
|
||||
|
||||
template_3 = template.dup
|
||||
template_3["id"] = 'super_mega_fun_wizard_3'
|
||||
template_3["after_signup"] = true
|
||||
CustomWizard::Template.save(template_3, skip_jobs: true)
|
||||
|
||||
@template_array = [template, template_2, template_3]
|
||||
|
||||
FileUtils.mkdir_p(file_from_fixtures_tmp_folder) unless Dir.exists?(file_from_fixtures_tmp_folder)
|
||||
@tmp_file_path = File.join(file_from_fixtures_tmp_folder, SecureRandom.hex << 'wizards.json')
|
||||
File.write(@tmp_file_path, @template_array.to_json)
|
||||
end
|
||||
|
||||
it 'exports all the wizard templates' do
|
||||
get '/admin/wizards/transfer/export.json', params: {
|
||||
wizards: [
|
||||
'super_mega_fun_wizard',
|
||||
'super_mega_fun_wizard_2',
|
||||
'super_mega_fun_wizard_3'
|
||||
]
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body).to match_array(@template_array)
|
||||
end
|
||||
|
||||
it 'imports wizard a template' do
|
||||
post '/admin/wizards/transfer/import.json', params: {
|
||||
file: fixture_file_upload(File.open(@tmp_file_path))
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body['success']).to eq(@template_array.map { |t| t['id'] })
|
||||
end
|
||||
end
|
Laden …
In neuem Issue referenzieren