diff --git a/assets/javascripts/discourse/components/wizard-export.js.es6 b/assets/javascripts/discourse/components/wizard-export.js.es6 new file mode 100644 index 00000000..a2505d24 --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-export.js.es6 @@ -0,0 +1,42 @@ +export default Ember.Component.extend({ + classNames: ['container', 'export'], + selected: Ember.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 new file mode 100644 index 00000000..1cf6618d --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-import.js.es6 @@ -0,0 +1,81 @@ +import { ajax } from 'discourse/lib/ajax'; +import { default as computed } from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + classNames: ['container', 'import'], + hasLogs: Ember.computed.notEmpty('logs'), + + @computed('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/controllers/admin-wizards-transfer.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-transfer.js.es6 new file mode 100644 index 00000000..77c79b72 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards-transfer.js.es6 @@ -0,0 +1 @@ +export default Ember.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 f5022153..001569f2 100644 --- a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 +++ b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 @@ -11,6 +11,9 @@ export default { this.route('adminWizardsApis', { path: '/apis', resetNamespace: true }, function() { this.route('adminWizardsApi', { path: '/:name', resetNamespace: true }); }); + + this.route('adminWizardsTransfer', { path: '/transfer', resetNamespace: true }); + }); } }; diff --git a/assets/javascripts/discourse/routes/admin-wizards-transfer.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-transfer.js.es6 new file mode 100644 index 00000000..b86b49cc --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-wizards-transfer.js.es6 @@ -0,0 +1,7 @@ +import CustomWizard from '../models/custom-wizard'; + +export default Discourse.Route.extend({ + model() { + return CustomWizard.all(); + } +}); diff --git a/assets/javascripts/discourse/templates/admin-wizards-transfer.hbs b/assets/javascripts/discourse/templates/admin-wizards-transfer.hbs new file mode 100644 index 00000000..ff36a823 --- /dev/null +++ b/assets/javascripts/discourse/templates/admin-wizards-transfer.hbs @@ -0,0 +1,2 @@ +{{wizard-export wizards=model}} +{{wizard-import}} diff --git a/assets/javascripts/discourse/templates/admin-wizards.hbs b/assets/javascripts/discourse/templates/admin-wizards.hbs index 3c26e24a..4ebb6a36 100644 --- a/assets/javascripts/discourse/templates/admin-wizards.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards.hbs @@ -2,6 +2,7 @@ {{nav-item route='adminWizardsCustom' label='admin.wizard.custom_label'}} {{nav-item route='adminWizardsSubmissions' label='admin.wizard.submissions_label'}} {{nav-item route='adminWizardsApis' label='admin.wizard.api.nav_label'}} + {{nav-item route='adminWizardsTransfer' label='admin.wizard.transfer.nav_label'}} {{/admin-nav}}
diff --git a/assets/javascripts/discourse/templates/components/wizard-export.hbs b/assets/javascripts/discourse/templates/components/wizard-export.hbs new file mode 100644 index 00000000..869b372a --- /dev/null +++ b/assets/javascripts/discourse/templates/components/wizard-export.hbs @@ -0,0 +1,25 @@ +

{{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 new file mode 100644 index 00000000..9f46e32b --- /dev/null +++ b/assets/javascripts/discourse/templates/components/wizard-import.hbs @@ -0,0 +1,32 @@ +

{{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}} +
+ + +
+{{/if}} \ No newline at end of file diff --git a/assets/stylesheets/wizard_custom_admin.scss b/assets/stylesheets/wizard_custom_admin.scss index 3399b148..7ae598ac 100644 --- a/assets/stylesheets/wizard_custom_admin.scss +++ b/assets/stylesheets/wizard_custom_admin.scss @@ -224,11 +224,11 @@ .required-data .setting-value { flex-flow: wrap; - + .custom-inputs { margin-bottom: 20px; } - + .required-data-message .label { margin-bottom: 5px; } @@ -480,3 +480,40 @@ .wizard-step-contents{ height: unset !important; } + +// Tansfer tab + +.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; + } +} + diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0aa235d8..d65c42c0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -215,6 +215,19 @@ en: log: 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" + wizard_js: location: name: diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 8ce4b4bb..d1c42401 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -10,6 +10,15 @@ en: too_short: "%{label} must be at least %{min} characters" none: "We couldn't find a wizard at that address." no_skip: "Wizard can't be skipped" + export: + error: + select_one: "Please select atleast one wizard" + 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" site_settings: wizard_redirect_exclude_paths: "Routes excluded from wizard redirects." diff --git a/controllers/transfer.rb b/controllers/transfer.rb new file mode 100644 index 00000000..4da52bb9 --- /dev/null +++ b/controllers/transfer.rb @@ -0,0 +1,74 @@ +class CustomWizard::TransferController < ::ApplicationController + before_action :ensure_logged_in + before_action :ensure_admin + skip_before_action :check_xhr, :only => [:export] + + def index + end + + def export + wizards = params['wizards'] + wizard_objects = [] + + if wizards.nil? + render json: { error: I18n.t('wizard.export.error.select_one') } + return + end + + wizards.each do |w| + wizard_objects.push(PluginStore.get('custom_wizard', w.tr('-', '_'))) + end + + send_data wizard_objects.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 + + fileSize = file.size + maxFileSize = 512 * 1024 + + if maxFileSize < fileSize + render json: { error: I18n.t('wizard.import.error.file_large') } + return + end + + begin + jsonObject = JSON.parse file + rescue JSON::ParserError + render json: { error: I18n.t('wizard.import.error.invalid_json') } + return + end + + countValid = 0 + success_ids = [] + failed_ids = [] + + jsonObject.each do |o| + if !CustomWizard::Template.new(o) + failed_ids.push o['id'] + next + end + + countValid += 1 + pluginStoreEntry = PluginStore.new 'custom_wizard' + saved = pluginStoreEntry.set(o['id'], o) unless pluginStoreEntry.get(o['id']) + success_ids.push o['id'] if !!saved + failed_ids.push o['id'] if !saved + end + + if countValid == 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/plugin.rb b/plugin.rb index 1709b7ea..1acacf99 100644 --- a/plugin.rb +++ b/plugin.rb @@ -12,6 +12,7 @@ config = Rails.application.config config.assets.paths << Rails.root.join('plugins', 'discourse-custom-wizard', 'assets', 'javascripts') config.assets.paths << Rails.root.join('plugins', 'discourse-custom-wizard', 'assets', 'stylesheets', 'wizard') + if Rails.env.production? config.assets.precompile += %w{ wizard-custom-lib.js @@ -75,6 +76,10 @@ after_initialize do delete 'admin/wizards/apis/logs/:name' => 'api#clearlogs' get 'admin/wizards/apis/:name/redirect' => 'api#redirect' get 'admin/wizards/apis/:name/authorize' => 'api#authorize' + #transfer code + get 'admin/wizards/transfer' => 'transfer#index' + get 'admin/wizards/transfer/export' => 'transfer#export' + post 'admin/wizards/transfer/import' => 'transfer#import' end end @@ -89,6 +94,8 @@ after_initialize do load File.expand_path('../controllers/wizard.rb', __FILE__) load File.expand_path('../controllers/steps.rb', __FILE__) load File.expand_path('../controllers/admin.rb', __FILE__) + #transfer code + load File.expand_path('../controllers/transfer.rb', __FILE__) load File.expand_path('../jobs/refresh_api_access_token.rb', __FILE__) load File.expand_path('../lib/api/api.rb', __FILE__) @@ -145,7 +152,7 @@ after_initialize do @excluded_routes ||= SiteSetting.wizard_redirect_exclude_paths.split('|') + ['/w/'] url = request.referer || request.original_url - if request.format === 'text/html' && !@excluded_routes.any? { |str| /#{str}/ =~ url } && wizard_id + if request.format === 'text/html' && !@excluded_routes.any? {|str| /#{str}/ =~ url} && wizard_id if request.referer !~ /\/w\// && request.referer !~ /\/invites\// CustomWizard::Wizard.set_submission_redirect(current_user, wizard_id, request.referer) end @@ -157,7 +164,7 @@ after_initialize do end end - add_to_serializer(:current_user, :redirect_to_wizard) { object.custom_fields['redirect_to_wizard'] } + add_to_serializer(:current_user, :redirect_to_wizard) {object.custom_fields['redirect_to_wizard']} ## TODO limit this to the first admin SiteSerializer.class_eval do @@ -169,7 +176,7 @@ after_initialize do def complete_custom_wizard if scope.user && requires_completion = CustomWizard::Wizard.prompt_completion(scope.user) - requires_completion.map { |w| { name: w[:name], url: "/w/#{w[:id]}" } } + requires_completion.map {|w| {name: w[:name], url: "/w/#{w[:id]}"}} end end