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 00000000000..c86784ca2e7
--- /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 00000000000..09158f53752
--- /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("
"));
+ } 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 00000000000..b34f6c4b9f4
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/json-file-uploader.hbs
@@ -0,0 +1,12 @@
+
+
+
+ {{d-button class="fileSelect" action="selectFile" class="" icon="upload" label="upload_selector.select_file"}}
+ {{conditional-loading-spinner condition=loading size="small"}}
+
+
{{i18n "alternation"}}
+
+ {{textarea value=value}}
+
+
{{fa-icon "upload"}}
+
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 00000000000..6d5d53e5bbf
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/modal/upload-customization.hbs
@@ -0,0 +1,8 @@
+
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 00000000000..c6e336157c2
--- /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 ff106787489..61980809a8d 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 7f31ee5d959..16fc2cde982 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 dc104fd743f..30929fe4e09 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 00000000000..1c8ff8f9da7
--- /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 3ee218f287a..6d49c48c08d 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."