From 066eef4ef841c809f4458be4896fa5f76738c3e3 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 9 Nov 2020 14:32:36 +1100 Subject: [PATCH] FEATURE: Wizard Manager The "Transfer" UI has been upgraded into a full wizard manager, adding additional import/export features and bulk-delete functionality --- .../discourse/components/wizard-export.js.es6 | 46 ---- .../discourse/components/wizard-import.js.es6 | 84 ------- .../components/wizard-message.js.es6 | 23 +- .../controllers/admin-wizards-manager.js.es6 | 229 ++++++++++++++++++ .../controllers/admin-wizards-transfer.js.es6 | 3 - .../custom-wizard-admin-route-map.js.es6 | 2 +- .../models/custom-wizard-manager.js.es6 | 43 ++++ .../discourse/models/custom-wizard.js.es6 | 7 +- ...er.js.es6 => admin-wizards-manager.js.es6} | 0 .../templates/admin-wizards-manager.hbs | 83 +++++++ .../templates/admin-wizards-transfer.hbs | 2 - .../discourse/templates/admin-wizards.hbs | 2 +- .../templates/components/wizard-export.hbs | 27 --- .../templates/components/wizard-import.hbs | 32 --- .../templates/components/wizard-message.hbs | 34 ++- assets/stylesheets/common/wizard-admin.scss | 30 ++- assets/stylesheets/common/wizard-manager.scss | 60 +++++ .../stylesheets/common/wizard-transfer.scss | 33 --- config/locales/client.en.yml | 33 ++- config/locales/client.pt_br.yml | 2 +- config/locales/server.en.yml | 11 +- config/routes.rb | 7 +- controllers/custom_wizard/admin/admin.rb | 4 + controllers/custom_wizard/admin/manager.rb | 123 ++++++++++ controllers/custom_wizard/admin/transfer.rb | 68 ------ coverage/.last_run.json | 5 - jobs/clear_after_time_wizard.rb | 2 +- lib/custom_wizard/template.rb | 4 + lib/custom_wizard/validator.rb | 7 +- plugin.rb | 2 +- spec/jobs/clear_after_time_wizard_spec.rb | 9 + .../admin/manager_controller_spec.rb | 104 ++++++++ .../admin/transfer_controller_spec.rb | 52 ---- 33 files changed, 777 insertions(+), 396 deletions(-) delete mode 100644 assets/javascripts/discourse/components/wizard-export.js.es6 delete mode 100644 assets/javascripts/discourse/components/wizard-import.js.es6 create mode 100644 assets/javascripts/discourse/controllers/admin-wizards-manager.js.es6 delete mode 100644 assets/javascripts/discourse/controllers/admin-wizards-transfer.js.es6 create mode 100644 assets/javascripts/discourse/models/custom-wizard-manager.js.es6 rename assets/javascripts/discourse/routes/{admin-wizards-transfer.js.es6 => admin-wizards-manager.js.es6} (100%) create mode 100644 assets/javascripts/discourse/templates/admin-wizards-manager.hbs delete mode 100644 assets/javascripts/discourse/templates/admin-wizards-transfer.hbs delete mode 100644 assets/javascripts/discourse/templates/components/wizard-export.hbs delete mode 100644 assets/javascripts/discourse/templates/components/wizard-import.hbs create mode 100644 assets/stylesheets/common/wizard-manager.scss delete mode 100644 assets/stylesheets/common/wizard-transfer.scss create mode 100644 controllers/custom_wizard/admin/manager.rb delete mode 100644 controllers/custom_wizard/admin/transfer.rb delete mode 100644 coverage/.last_run.json create mode 100644 spec/requests/custom_wizard/admin/manager_controller_spec.rb delete mode 100644 spec/requests/custom_wizard/admin/transfer_controller_spec.rb diff --git a/assets/javascripts/discourse/components/wizard-export.js.es6 b/assets/javascripts/discourse/components/wizard-export.js.es6 deleted file mode 100644 index c75b745e..00000000 --- a/assets/javascripts/discourse/components/wizard-export.js.es6 +++ /dev/null @@ -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; - } - } - } -}); \ No newline at end of file diff --git a/assets/javascripts/discourse/components/wizard-import.js.es6 b/assets/javascripts/discourse/components/wizard-import.js.es6 deleted file mode 100644 index 446d2b00..00000000 --- a/assets/javascripts/discourse/components/wizard-import.js.es6 +++ /dev/null @@ -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")); - } - } - } -}); \ No newline at end of file diff --git a/assets/javascripts/discourse/components/wizard-message.js.es6 b/assets/javascripts/discourse/components/wizard-message.js.es6 index 0592e9ca..afcde014 100644 --- a/assets/javascripts/discourse/components/wizard-message.js.es6 +++ b/assets/javascripts/discourse/components/wizard-message.js.es6 @@ -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"; +const icons = { + error: 'times-circle', + success: 'check-circle', + info: 'info-circle' +} + export default Component.extend({ - classNames: 'wizard-message', + classNameBindings: [':wizard-message', 'type', 'loading'], + showDocumentation: not('loading'), + showIcon: not('loading'), + hasItems: notEmpty('items'), - @discourseComputed('key', 'component') - message(key, component) { - return I18n.t(`admin.wizard.message.${component}.${key}`); + @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') diff --git a/assets/javascripts/discourse/controllers/admin-wizards-manager.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-manager.js.es6 new file mode 100644 index 00000000..56aa4446 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards-manager.js.es6 @@ -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 = `${wizard.name}`; + html += `${I18n.t('admin.wizard.manager.imported')}`; + return { + icon: 'check-circle', + html + }; + }, + + buildDestroyedItem(destroyed) { + let html = `${destroyed.name}`; + html += `${I18n.t('admin.wizard.manager.destroyed')}`; + 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); + }); + } + } + } +}); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-transfer.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-transfer.js.es6 deleted file mode 100644 index 7ae8f5a1..00000000 --- a/assets/javascripts/discourse/controllers/admin-wizards-transfer.js.es6 +++ /dev/null @@ -1,3 +0,0 @@ -import Controller from "@ember/controller"; - -export default Controller.extend(); diff --git a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 index 97157964..8274ef12 100644 --- a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 +++ b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 @@ -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 }); }); } }; diff --git a/assets/javascripts/discourse/models/custom-wizard-manager.js.es6 b/assets/javascripts/discourse/models/custom-wizard-manager.js.es6 new file mode 100644 index 00000000..68d8d567 --- /dev/null +++ b/assets/javascripts/discourse/models/custom-wizard-manager.js.es6 @@ -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; \ No newline at end of file diff --git a/assets/javascripts/discourse/models/custom-wizard.js.es6 b/assets/javascripts/discourse/models/custom-wizard.js.es6 index 7a842987..ea555f41 100644 --- a/assets/javascripts/discourse/models/custom-wizard.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard.js.es6 @@ -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 = {}) { diff --git a/assets/javascripts/discourse/routes/admin-wizards-transfer.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-manager.js.es6 similarity index 100% rename from assets/javascripts/discourse/routes/admin-wizards-transfer.js.es6 rename to assets/javascripts/discourse/routes/admin-wizards-manager.js.es6 diff --git a/assets/javascripts/discourse/templates/admin-wizards-manager.hbs b/assets/javascripts/discourse/templates/admin-wizards-manager.hbs new file mode 100644 index 00000000..3eda71e2 --- /dev/null +++ b/assets/javascripts/discourse/templates/admin-wizards-manager.hbs @@ -0,0 +1,83 @@ +
+

{{i18n 'admin.wizard.manager.title'}}

+ +
+ {{#if filename}} +
+ + {{d-icon 'times'}} + + {{filename}} +
+ {{/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}} +
+
+ +{{wizard-message + key=messageKey + url=messageUrl + type=messageType + opts=messageOpts + items=messageItems + loading=loading + component='manager'}} + +
+ + + + + + + + + + {{#each wizards as |wizard|}} + + + + + + {{/each}} + +
{{i18n 'admin.wizard.label'}}{{i18n 'admin.wizard.manager.export'}}{{i18n 'admin.wizard.manager.destroy'}}
+ {{#link-to "adminWizardsWizardShow" (dasherize wizard.id)}} + {{wizard.name}} + {{/link-to}} + + {{input + type="checkbox" + class="export" + change=(action 'selectWizard')}} + + {{input + type="checkbox" + class="destroy" + change=(action 'selectWizard')}} +
+
\ No newline at end of file diff --git a/assets/javascripts/discourse/templates/admin-wizards-transfer.hbs b/assets/javascripts/discourse/templates/admin-wizards-transfer.hbs deleted file mode 100644 index 5cfa3f56..00000000 --- a/assets/javascripts/discourse/templates/admin-wizards-transfer.hbs +++ /dev/null @@ -1,2 +0,0 @@ -{{wizard-export wizards=wizards}} -{{wizard-import}} diff --git a/assets/javascripts/discourse/templates/admin-wizards.hbs b/assets/javascripts/discourse/templates/admin-wizards.hbs index 53ec86a6..c616e668 100644 --- a/assets/javascripts/discourse/templates/admin-wizards.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards.hbs @@ -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}}
diff --git a/assets/javascripts/discourse/templates/components/wizard-export.hbs b/assets/javascripts/discourse/templates/components/wizard-export.hbs deleted file mode 100644 index 9bbfabcf..00000000 --- a/assets/javascripts/discourse/templates/components/wizard-export.hbs +++ /dev/null @@ -1,27 +0,0 @@ -

{{i18n 'admin.wizard.transfer.export.label'}}

- - - -{{d-button id="export-button" - class="btn btn-primary side" - label="admin.wizard.transfer.export.label" - action=(action "export")}} - -{{#if exportMessage}} -
- {{exportMessage}} -
-{{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/templates/components/wizard-import.hbs b/assets/javascripts/discourse/templates/components/wizard-import.hbs deleted file mode 100644 index 9f46e32b..00000000 --- a/assets/javascripts/discourse/templates/components/wizard-import.hbs +++ /dev/null @@ -1,32 +0,0 @@ -

{{i18n 'admin.wizard.transfer.import.label'}}

- -
- {{input id='file-url' type="file" change=(action "setFilePath")}} - - {{#if importMessage}} -
- {{importMessage}} -
- {{/if}} - - {{d-button id="import-button" - class="btn btn-primary side" - label="admin.wizard.transfer.import.label" - action=(action "import")}} -
- -{{#if hasLogs}} -
-
- {{i18n 'admin.wizard.transfer.import.logs' fileName=fileName}} -
- -
    - {{#each logs as |l|}} -
  • - {{i18n (concat 'admin.wizard.transfer.import.' l.type) id=l.id}} -
  • - {{/each}} -
-
-{{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/templates/components/wizard-message.hbs b/assets/javascripts/discourse/templates/components/wizard-message.hbs index 9f66fc77..78c7558b 100644 --- a/assets/javascripts/discourse/templates/components/wizard-message.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-message.hbs @@ -1,12 +1,26 @@ -
- {{d-icon 'info-circle'}} - {{message}} +
+ {{#if showIcon}} + {{d-icon icon}} + {{/if}} + {{{message}}} + {{#if hasItems}} +
    + {{#each items as |item|}} +
  • + {{d-icon item.icon}} + {{{item.html}}} +
  • + {{/each}} +
+ {{/if}}
-
- {{d-icon 'question-circle'}} - - - {{documentation}} - -
\ No newline at end of file +{{#if showDocumentation}} +
+ {{d-icon 'question-circle'}} + + + {{documentation}} + +
+{{/if}} \ No newline at end of file diff --git a/assets/stylesheets/common/wizard-admin.scss b/assets/stylesheets/common/wizard-admin.scss index b92e6e33..79cfb033 100644 --- a/assets/stylesheets/common/wizard-admin.scss +++ b/assets/stylesheets/common/wizard-admin.scss @@ -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 { diff --git a/assets/stylesheets/common/wizard-manager.scss b/assets/stylesheets/common/wizard-manager.scss new file mode 100644 index 00000000..5437134f --- /dev/null +++ b/assets/stylesheets/common/wizard-manager.scss @@ -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; +} diff --git a/assets/stylesheets/common/wizard-transfer.scss b/assets/stylesheets/common/wizard-transfer.scss deleted file mode 100644 index e31168ba..00000000 --- a/assets/stylesheets/common/wizard-transfer.scss +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 906d6f77..8d57523a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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: diff --git a/config/locales/client.pt_br.yml b/config/locales/client.pt_br.yml index 23ab4b9a..b79830f3 100644 --- a/config/locales/client.pt_br.yml +++ b/config/locales/client.pt_br.yml @@ -303,7 +303,7 @@ pt_br: log: nav_label: "Logs" - transfer: + manager: nav_label: "Transferir" export: label: "Exportar" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b06be5f8..6aa27a7b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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: diff --git a/config/routes.rb b/config/routes.rb index a765a809..311de5e6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 \ No newline at end of file diff --git a/controllers/custom_wizard/admin/admin.rb b/controllers/custom_wizard/admin/admin.rb index 5da337a4..a2cfb977 100644 --- a/controllers/custom_wizard/admin/admin.rb +++ b/controllers/custom_wizard/admin/admin.rb @@ -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 \ No newline at end of file diff --git a/controllers/custom_wizard/admin/manager.rb b/controllers/custom_wizard/admin/manager.rb new file mode 100644 index 00000000..d7fe5ac6 --- /dev/null +++ b/controllers/custom_wizard/admin/manager.rb @@ -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 diff --git a/controllers/custom_wizard/admin/transfer.rb b/controllers/custom_wizard/admin/transfer.rb deleted file mode 100644 index b55fe872..00000000 --- a/controllers/custom_wizard/admin/transfer.rb +++ /dev/null @@ -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 diff --git a/coverage/.last_run.json b/coverage/.last_run.json deleted file mode 100644 index 8e48d1a2..00000000 --- a/coverage/.last_run.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "result": { - "covered_percent": 88.16 - } -} diff --git a/jobs/clear_after_time_wizard.rb b/jobs/clear_after_time_wizard.rb index ffaa6fa9..50443691 100644 --- a/jobs/clear_after_time_wizard.rb +++ b/jobs/clear_after_time_wizard.rb @@ -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 diff --git a/lib/custom_wizard/template.rb b/lib/custom_wizard/template.rb index fc79e91d..584df568 100644 --- a/lib/custom_wizard/template.rb +++ b/lib/custom_wizard/template.rb @@ -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) diff --git a/lib/custom_wizard/validator.rb b/lib/custom_wizard/validator.rb index e98023d0..241e3b8e 100644 --- a/lib/custom_wizard/validator.rb +++ b/lib/custom_wizard/validator.rb @@ -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 \ No newline at end of file diff --git a/plugin.rb b/plugin.rb index 95608e6e..a2e4ffee 100644 --- a/plugin.rb +++ b/plugin.rb @@ -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 diff --git a/spec/jobs/clear_after_time_wizard_spec.rb b/spec/jobs/clear_after_time_wizard_spec.rb index b6d675fe..ad7e0982 100644 --- a/spec/jobs/clear_after_time_wizard_spec.rb +++ b/spec/jobs/clear_after_time_wizard_spec.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( diff --git a/spec/requests/custom_wizard/admin/manager_controller_spec.rb b/spec/requests/custom_wizard/admin/manager_controller_spec.rb new file mode 100644 index 00000000..3912bf35 --- /dev/null +++ b/spec/requests/custom_wizard/admin/manager_controller_spec.rb @@ -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 \ No newline at end of file diff --git a/spec/requests/custom_wizard/admin/transfer_controller_spec.rb b/spec/requests/custom_wizard/admin/transfer_controller_spec.rb deleted file mode 100644 index 217311c2..00000000 --- a/spec/requests/custom_wizard/admin/transfer_controller_spec.rb +++ /dev/null @@ -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 \ No newline at end of file