diff --git a/app/assets/javascripts/admin/controllers/admin-customize-css-html.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-css-html.js.es6 index 266f06c3e..be6834333 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-css-html.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-css-html.js.es6 @@ -1,3 +1,5 @@ +import showModal from 'discourse/lib/show-modal'; + /** This controller supports interface for creating custom CSS skins in Discourse. @@ -21,6 +23,10 @@ export default Ember.ArrayController.extend({ this.set('selectedItem', item); }, + importModal: function() { + showModal('upload-customization'); + }, + /** Select a given style diff --git a/app/assets/javascripts/admin/models/site_customization.js b/app/assets/javascripts/admin/models/site_customization.js index 27b6ddbdd..f7b6346a9 100644 --- a/app/assets/javascripts/admin/models/site_customization.js +++ b/app/assets/javascripts/admin/models/site_customization.js @@ -78,13 +78,18 @@ Discourse.SiteCustomization = Discourse.Model.extend({ siteCustomization.set('savingStatus', I18n.t('saved')); siteCustomization.set('saving',false); siteCustomization.startTrackingChanges(); + return siteCustomization; }); }, destroy: function() { if (!this.id) return; return Discourse.ajax("/admin/site_customizations/" + this.id, { type: 'DELETE' }); - } + }, + + download_url: function() { + return Discourse.getURL('/admin/site_customizations/' + this.id); + }.property('id') }); var SiteCustomizations = Ember.ArrayProxy.extend({ diff --git a/app/assets/javascripts/admin/templates/customize_css_html.hbs b/app/assets/javascripts/admin/templates/customize_css_html.hbs index 004318e1e..e2cc7951a 100644 --- a/app/assets/javascripts/admin/templates/customize_css_html.hbs +++ b/app/assets/javascripts/admin/templates/customize_css_html.hbs @@ -8,12 +8,14 @@ <button {{action "newCustomization"}} class='btn'> {{fa-icon "plus"}}{{i18n 'admin.customize.new'}} </button> + {{d-button action="importModal" icon="upload" label="admin.customize.import"}} </div> {{#if selectedItem}} <div {{bind-attr class=":current-style view.maximized:maximized"}}> <div class='wrapper'> {{text-field class="style-name" value=selectedItem.name}} + <a class="btn export" download target="_blank" href={{selectedItem.download_url}}>{{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a> <div class='admin-controls'> <ul class="nav nav-pills"> diff --git a/app/assets/javascripts/discourse/components/json-file-uploader.js.es6 b/app/assets/javascripts/discourse/components/json-file-uploader.js.es6 new file mode 100644 index 000000000..c86784ca2 --- /dev/null +++ b/app/assets/javascripts/discourse/components/json-file-uploader.js.es6 @@ -0,0 +1,105 @@ + +export default Em.Component.extend({ + fileInput: null, + loading: false, + expectedRootObjectName: null, + hover: 0, + + classNames: ['json-uploader'], + + _initialize: function() { + const $this = this.$(); + const self = this; + + const $fileInput = $this.find('#js-file-input'); + this.set('fileInput', $fileInput[0]); + + $fileInput.on('change', function() { + self.fileSelected(this.files); + }); + + const $dragContainer = $this.find('.jsfu-shade-container'); + + $this.on('dragover', function(e) { + if (e.preventDefault) e.preventDefault(); + return false; + }); + $this.on('dragenter', function(e) { + if (e.preventDefault) e.preventDefault(); + self.set('hover', self.get('hover') + 1); + return false; + }); + $this.on('dragleave', function(e) { + if (e.preventDefault) e.preventDefault(); + self.set('hover', self.get('hover') - 1); + return false; + }); + $this.on('drop', function(e) { + if (e.preventDefault) e.preventDefault(); + + self.set('hover', 0); + self.fileSelected(e.dataTransfer.files); + return false; + }); + + }.on('didInsertElement'), + + accept: function() { + return ".json,application/json,application/x-javascript,text/json" + (this.get('extension') ? "," + this.get('extension') : ""); + }.property('extension'), + + setReady: function() { + let parsed; + try { + parsed = JSON.parse(this.get('value')); + } catch (e) { + this.set('ready', false); + return; + } + + const rootObject = parsed[this.get('expectedRootObjectName')]; + + if (rootObject !== null && rootObject !== undefined) { + this.set('ready', true); + } else { + this.set('ready', false); + } + }.observes('destination', 'expectedRootObjectName'), + + actions: { + selectFile: function() { + const $fileInput = $(this.get('fileInput')); + $fileInput.click(); + } + }, + + fileSelected(fileList) { + const self = this; + let files = []; + for (let i = 0; i < fileList.length; i++) { + files[i] = fileList[i]; + } + const fileNameRegex = /\.(json|txt)$/; + files = files.filter(function(file) { + if (fileNameRegex.test(file.name)) { + return true; + } + if (file.type === "text/plain") { + return true; + } + return false; + }); + const firstFile = fileList[0]; + + this.set('loading', true); + + let reader = new FileReader(); + reader.onload = function(evt) { + self.set('value', evt.target.result); + self.set('loading', false); + }; + + reader.readAsText(firstFile); + } + +}); diff --git a/app/assets/javascripts/discourse/controllers/upload-customization.js.es6 b/app/assets/javascripts/discourse/controllers/upload-customization.js.es6 new file mode 100644 index 000000000..09158f537 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/upload-customization.js.es6 @@ -0,0 +1,52 @@ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; + +export default Ember.Controller.extend(ModalFunctionality, { + notReady: Em.computed.not('ready'), + + needs: ['admin-customize-css-html'], + + title: "hi", + + ready: function() { + let parsed; + try { + parsed = JSON.parse(this.get('customizationFile')); + } catch (e) { + return false; + } + + return !!parsed["site_customization"]; + }.property('customizationFile'), + + actions: { + createCustomization: function() { + const self = this; + const object = JSON.parse(this.get('customizationFile')).site_customization; + + // Slight fixup before creating object + object.enabled = false; + delete object.id; + delete object.key; + + const customization = Discourse.SiteCustomization.create(object); + + this.set('loading', true); + customization.save().then(function(customization) { + self.send('closeModal'); + self.set('loading', false); + + const parentController = self.get('controllers.admin-customize-css-html'); + parentController.pushObject(customization); + parentController.set('selectedItem', customization); + }).catch(function(xhr) { + self.set('loading', false); + if (xhr.responseJSON) { + bootbox.alert(xhr.responseJSON.errors.join("<br>")); + } else { + bootbox.alert(I18n.t('generic_error')); + } + }); + } + } + +}); diff --git a/app/assets/javascripts/discourse/templates/components/json-file-uploader.hbs b/app/assets/javascripts/discourse/templates/components/json-file-uploader.hbs new file mode 100644 index 000000000..b34f6c4b9 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/json-file-uploader.hbs @@ -0,0 +1,12 @@ +<div class="jsfu-shade-container"> + <div class="jsfu-file"> + <input id="js-file-input" type="file" style="display:none;" accept={{accept}}> + {{d-button class="fileSelect" action="selectFile" class="" icon="upload" label="upload_selector.select_file"}} + {{conditional-loading-spinner condition=loading size="small"}} + </div> + <div class="jsfu-separator">{{i18n "alternation"}}</div> + <div class="jsfu-paste"> + {{textarea value=value}} + </div> + <div class="jsfu-shade {{if hover '' 'hidden'}}"><span class="text">{{fa-icon "upload"}}</span></div> +</div> diff --git a/app/assets/javascripts/discourse/templates/modal/upload-customization.hbs b/app/assets/javascripts/discourse/templates/modal/upload-customization.hbs new file mode 100644 index 000000000..6d5d53e5b --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/upload-customization.hbs @@ -0,0 +1,8 @@ +<form {{action "dummy" on="submit"}}> + <div class='modal-body'> + {{json-file-uploader value=customizationFile extension=".dcstyle.json"}} + </div> + <div class="modal-footer"> + {{d-button class='btn-primary' action='createCustomization' type='submit' disabled=notReady icon="plus" label='admin.customize.import'}} + </div> +</form> diff --git a/app/assets/javascripts/discourse/views/upload-customization.js.es6 b/app/assets/javascripts/discourse/views/upload-customization.js.es6 new file mode 100644 index 000000000..c6e336157 --- /dev/null +++ b/app/assets/javascripts/discourse/views/upload-customization.js.es6 @@ -0,0 +1,6 @@ +import ModalBodyView from "discourse/views/modal-body"; + +export default ModalBodyView.extend({ + templateName: 'modal/upload-customization', + title: I18n.t('admin.customize.import_title') +}); diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index ff1067874..61980809a 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -545,6 +545,9 @@ section.details { .preview-link { margin-left: 15px; } + .export { + float: right; + } padding-left: 10px; width: 70%; .style-name { diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 7f31ee5d9..16fc2cde9 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -138,6 +138,55 @@ .raw-email-textarea { height: 300px; } + .json-uploader { + .jsfu-shade-container { + display: table-row; + width: 100%; + height: 100%; + position: relative; + } + .jsfu-shade { + z-index: 1; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + .text { + color: rgb(255,255,255); + position: absolute; + top: 40%; + font-size: 36px; + text-align: center; + line-height: 38px; + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + } + } + .jsfu-file { + display: table-cell; + vertical-align: middle; + min-width: 120px; + } + .jsfu-separator { + vertical-align: middle; + display: table-cell; + font-size: 36px; + padding-left: 10px; + padding-right: 10px; + } + .jsfu-paste { + display: table-cell; + width: 100%; + textarea { + margin-bottom: 0; + margin-top: 4px; + } + } + } } .password-confirmation { display: none; diff --git a/app/controllers/admin/site_customizations_controller.rb b/app/controllers/admin/site_customizations_controller.rb index dc104fd74..30929fe4e 100644 --- a/app/controllers/admin/site_customizations_controller.rb +++ b/app/controllers/admin/site_customizations_controller.rb @@ -2,6 +2,8 @@ class Admin::SiteCustomizationsController < Admin::AdminController before_filter :enable_customization + skip_before_filter :check_xhr, only: [:show] + def index @site_customizations = SiteCustomization.order(:name) @@ -48,6 +50,26 @@ class Admin::SiteCustomizationsController < Admin::AdminController end end + def show + @site_customization = SiteCustomization.find(params[:id]) + + respond_to do |format| + format.json do + check_xhr + render json: SiteCustomizationSerializer.new(@site_customization) + end + + format.any(:html, :text) do + raise RenderEmpty.new if request.xhr? + + response.headers['Content-Disposition'] = "attachment; filename=#{@site_customization.name.parameterize}.dcstyle.json" + response.sending_file = true + render json: SiteCustomizationSerializer.new(@site_customization) + end + end + + end + private def site_customization_params diff --git a/app/serializers/site_customization_serializer.rb b/app/serializers/site_customization_serializer.rb new file mode 100644 index 000000000..1c8ff8f9d --- /dev/null +++ b/app/serializers/site_customization_serializer.rb @@ -0,0 +1,7 @@ +class SiteCustomizationSerializer < ApplicationSerializer + + attributes :id, :name, :key, :enabled, :created_at, :updated_at, + :stylesheet, :header, :footer, :top, + :mobile_stylesheet, :mobile_header, :mobile_footer, :mobile_top, + :head_tag, :body_tag +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3ee218f28..6d49c48c0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -153,6 +153,7 @@ en: every_two_weeks: "every two weeks" every_three_days: "every three days" max_of_count: "max of {{count}}" + alternation: "or" character_count: one: "{{count}} character" other: "{{count}} characters" @@ -863,6 +864,7 @@ en: hint: "(you can also drag & drop into the editor to upload them)" hint_for_supported_browsers: "(you can also drag and drop or paste images into the editor to upload them)" uploading: "Uploading" + select_file: "Select File" image_link: "link your image will point to" search: @@ -1880,6 +1882,8 @@ en: screened_email: "Export full screened email list in CSV format." screened_ip: "Export full screened IP list in CSV format." screened_url: "Export full screened URL list in CSV format." + export_json: + button_text: "Export" invite: button_text: "Send Invites" @@ -1909,6 +1913,8 @@ en: save: "Save" new: "New" new_style: "New Style" + import: "Import" + import_title: "Select a file or paste text" delete: "Delete" delete_confirm: "Delete this customization?" about: "Modify CSS stylesheets and HTML headers on the site. Add a customization to start."