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

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:
Angus McLeod 2020-11-09 14:32:36 +11:00
Ursprung 37c18ff324
Commit 066eef4ef8
33 geänderte Dateien mit 777 neuen und 396 gelöschten Zeilen

Datei anzeigen

@ -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;
}
}
}
});

Datei anzeigen

@ -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"));
}
}
}
});

Datei anzeigen

@ -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')

Datei anzeigen

@ -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);
});
}
}
}
});

Datei anzeigen

@ -1,3 +0,0 @@
import Controller from "@ember/controller";
export default Controller.extend();

Datei anzeigen

@ -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 });
});
}
};

Datei anzeigen

@ -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;

Datei anzeigen

@ -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 = {}) {

Datei anzeigen

@ -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>

Datei anzeigen

@ -1,2 +0,0 @@
{{wizard-export wizards=wizards}}
{{wizard-import}}

Datei anzeigen

@ -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">

Datei anzeigen

@ -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}}

Datei anzeigen

@ -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}}

Datei anzeigen

@ -1,8 +1,21 @@
<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>
{{#if showDocumentation}}
<div class="message-block">
{{d-icon 'question-circle'}}
@ -10,3 +23,4 @@
{{documentation}}
</a>
</div>
{{/if}}

Datei anzeigen

@ -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 {

Datei anzeigen

@ -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;
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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:

Datei anzeigen

@ -303,7 +303,7 @@ pt_br:
log:
nav_label: "Logs"
transfer:
manager:
nav_label: "Transferir"
export:
label: "Exportar"

Datei anzeigen

@ -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:

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -1,5 +0,0 @@
{
"result": {
"covered_percent": 88.16
}
}

Datei anzeigen

@ -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

Datei anzeigen

@ -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)

Datei anzeigen

@ -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

Datei anzeigen

@ -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

Datei anzeigen

@ -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(

Datei anzeigen

@ -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

Datei anzeigen

@ -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