mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-01-28 18:30:05 -05:00
Merge pull request #3482 from riking/patch-3
Import/Export site customizations
This commit is contained in:
commit
76bfd723f6
13 changed files with 284 additions and 1 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
});
|
|
@ -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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -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>
|
|
@ -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>
|
|
@ -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')
|
||||
});
|
|
@ -545,6 +545,9 @@ section.details {
|
|||
.preview-link {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.export {
|
||||
float: right;
|
||||
}
|
||||
padding-left: 10px;
|
||||
width: 70%;
|
||||
.style-name {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
7
app/serializers/site_customization_serializer.rb
Normal file
7
app/serializers/site_customization_serializer.rb
Normal file
|
@ -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
|
|
@ -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."
|
||||
|
|
Loading…
Reference in a new issue