mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 13:54:35 +03:00
Merge pull request #210 from kevinansfield/theme-uploads
Theme management
This commit is contained in:
commit
d561691231
@ -32,6 +32,7 @@ export default Component.extend({
|
|||||||
uploadPercentage: 0,
|
uploadPercentage: 0,
|
||||||
|
|
||||||
ajax: injectService(),
|
ajax: injectService(),
|
||||||
|
eventBus: injectService(),
|
||||||
notifications: injectService(),
|
notifications: injectService(),
|
||||||
|
|
||||||
formData: computed('file', function () {
|
formData: computed('file', function () {
|
||||||
@ -57,6 +58,32 @@ export default Component.extend({
|
|||||||
return htmlSafe(`width: ${width}`);
|
return htmlSafe(`width: ${width}`);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// we can optionally listen to a named event bus channel so that the upload
|
||||||
|
// process can be triggered externally
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
let listenTo = this.get('listenTo');
|
||||||
|
|
||||||
|
if (listenTo) {
|
||||||
|
this.get('eventBus').subscribe(`${listenTo}:upload`, this, function (file) {
|
||||||
|
if (file) {
|
||||||
|
this.set('file', file);
|
||||||
|
}
|
||||||
|
this.send('upload');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
let listenTo = this.get('listenTo');
|
||||||
|
|
||||||
|
this._super(...arguments);
|
||||||
|
|
||||||
|
if (listenTo) {
|
||||||
|
this.get('eventBus').unsubscribe(`${listenTo}:upload`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
dragOver(event) {
|
dragOver(event) {
|
||||||
if (!event.dataTransfer) {
|
if (!event.dataTransfer) {
|
||||||
return;
|
return;
|
||||||
@ -142,7 +169,7 @@ export default Component.extend({
|
|||||||
} else if (isRequestEntityTooLargeError(error)) {
|
} else if (isRequestEntityTooLargeError(error)) {
|
||||||
message = 'The file you uploaded was larger than the maximum file size your server allows.';
|
message = 'The file you uploaded was larger than the maximum file size your server allows.';
|
||||||
} else if (error.errors && !isBlank(error.errors[0].message)) {
|
} else if (error.errors && !isBlank(error.errors[0].message)) {
|
||||||
message = error.errors[0].message;
|
message = htmlSafe(error.errors[0].message);
|
||||||
} else {
|
} else {
|
||||||
message = 'Something went wrong :(';
|
message = 'Something went wrong :(';
|
||||||
}
|
}
|
||||||
@ -179,6 +206,7 @@ export default Component.extend({
|
|||||||
let validationResult = this._validate(file);
|
let validationResult = this._validate(file);
|
||||||
|
|
||||||
this.set('file', file);
|
this.set('file', file);
|
||||||
|
invokeAction(this, 'fileSelected', file);
|
||||||
|
|
||||||
if (validationResult === true) {
|
if (validationResult === true) {
|
||||||
run.schedule('actions', this, function () {
|
run.schedule('actions', this, function () {
|
||||||
@ -189,6 +217,12 @@ export default Component.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
upload() {
|
||||||
|
if (this.get('file')) {
|
||||||
|
this.generateRequest();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.set('file', null);
|
this.set('file', null);
|
||||||
this.set('uploadPercentage', 0);
|
this.set('uploadPercentage', 0);
|
||||||
|
@ -231,6 +231,7 @@ export default Component.extend({
|
|||||||
let validationResult = this._validate(file);
|
let validationResult = this._validate(file);
|
||||||
|
|
||||||
this.set('file', file);
|
this.set('file', file);
|
||||||
|
invokeAction(this, 'fileSelected', file);
|
||||||
|
|
||||||
if (validationResult === true) {
|
if (validationResult === true) {
|
||||||
run.schedule('actions', this, function () {
|
run.schedule('actions', this, function () {
|
||||||
|
24
ghost/admin/app/components/gh-theme-table.js
Normal file
24
ghost/admin/app/components/gh-theme-table.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import Component from 'ember-component';
|
||||||
|
import computed from 'ember-computed';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
|
||||||
|
availableThemes: null,
|
||||||
|
activeTheme: null,
|
||||||
|
|
||||||
|
themes: computed('availableThemes', 'activeTheme', function () {
|
||||||
|
return this.get('availableThemes').map((t) => {
|
||||||
|
let theme = {};
|
||||||
|
|
||||||
|
theme.name = t.name;
|
||||||
|
theme.label = t.package ? `${t.package.name} - ${t.package.version}` : t.name;
|
||||||
|
theme.package = t.package;
|
||||||
|
theme.active = !!t.active;
|
||||||
|
theme.isDefault = t.name === 'casper';
|
||||||
|
theme.isDeletable = !theme.active && !theme.isDefault;
|
||||||
|
|
||||||
|
return theme;
|
||||||
|
}).sortBy('label');
|
||||||
|
}).readOnly()
|
||||||
|
|
||||||
|
});
|
21
ghost/admin/app/components/modals/delete-theme.js
Normal file
21
ghost/admin/app/components/modals/delete-theme.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import ModalComponent from 'ghost-admin/components/modals/base';
|
||||||
|
import {alias} from 'ember-computed';
|
||||||
|
import {invokeAction} from 'ember-invoke-action';
|
||||||
|
|
||||||
|
export default ModalComponent.extend({
|
||||||
|
|
||||||
|
submitting: false,
|
||||||
|
|
||||||
|
theme: alias('model.theme'),
|
||||||
|
download: alias('model.download'),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
confirm() {
|
||||||
|
this.set('submitting', true);
|
||||||
|
|
||||||
|
invokeAction(this, 'confirm').finally(() => {
|
||||||
|
this.send('closeModal');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
109
ghost/admin/app/components/modals/upload-theme.js
Normal file
109
ghost/admin/app/components/modals/upload-theme.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import ModalComponent from 'ghost-admin/components/modals/base';
|
||||||
|
import computed, {mapBy, or} from 'ember-computed';
|
||||||
|
import {invokeAction} from 'ember-invoke-action';
|
||||||
|
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
||||||
|
import {UnsupportedMediaTypeError} from 'ghost-admin/services/ajax';
|
||||||
|
import {isBlank} from 'ember-utils';
|
||||||
|
import run from 'ember-runloop';
|
||||||
|
import injectService from 'ember-service/inject';
|
||||||
|
|
||||||
|
export default ModalComponent.extend({
|
||||||
|
|
||||||
|
accept: 'application/zip',
|
||||||
|
availableThemes: null,
|
||||||
|
closeDisabled: false,
|
||||||
|
file: null,
|
||||||
|
theme: false,
|
||||||
|
displayOverwriteWarning: false,
|
||||||
|
|
||||||
|
eventBus: injectService(),
|
||||||
|
|
||||||
|
hideUploader: or('theme', 'displayOverwriteWarning'),
|
||||||
|
|
||||||
|
uploadUrl: computed(function () {
|
||||||
|
return `${ghostPaths().apiRoot}/themes/upload/`;
|
||||||
|
}),
|
||||||
|
|
||||||
|
themeName: computed('theme.{name,package.name}', function () {
|
||||||
|
let t = this.get('theme');
|
||||||
|
|
||||||
|
return t.package ? `${t.package.name} - ${t.package.version}` : t.name;
|
||||||
|
}),
|
||||||
|
|
||||||
|
availableThemeNames: mapBy('model.availableThemes', 'name'),
|
||||||
|
|
||||||
|
fileThemeName: computed('file', function () {
|
||||||
|
let file = this.get('file');
|
||||||
|
return file.name.replace(/\.zip$/, '');
|
||||||
|
}),
|
||||||
|
|
||||||
|
canActivateTheme: computed('theme', function () {
|
||||||
|
let theme = this.get('theme');
|
||||||
|
return theme && !theme.active;
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
validateTheme(file) {
|
||||||
|
let accept = this.get('accept');
|
||||||
|
let themeName = file.name.replace(/\.zip$/, '');
|
||||||
|
let availableThemeNames = this.get('availableThemeNames');
|
||||||
|
|
||||||
|
this.set('file', file);
|
||||||
|
|
||||||
|
if (!isBlank(accept) && file && accept.indexOf(file.type) === -1) {
|
||||||
|
return new UnsupportedMediaTypeError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.name.match(/^casper\.zip$/i)) {
|
||||||
|
return {errors: [{message: 'Sorry, the default Casper theme cannot be overwritten.<br>Please rename your zip file and try again.'}]};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._allowOverwrite && availableThemeNames.includes(themeName)) {
|
||||||
|
this.set('displayOverwriteWarning', true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmOverwrite() {
|
||||||
|
this._allowOverwrite = true;
|
||||||
|
this.set('displayOverwriteWarning', false);
|
||||||
|
|
||||||
|
// we need to schedule afterRender so that the upload component is
|
||||||
|
// displayed again in order to subscribe/respond to the event bus
|
||||||
|
run.schedule('afterRender', this, function () {
|
||||||
|
this.get('eventBus').publish('themeUploader:upload', this.get('file'));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadStarted() {
|
||||||
|
this.set('closeDisabled', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadFinished() {
|
||||||
|
this.set('closeDisabled', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadSuccess(response) {
|
||||||
|
this.set('theme', response.themes[0]);
|
||||||
|
// invoke the passed in confirm action
|
||||||
|
invokeAction(this, 'model.uploadSuccess', this.get('theme'));
|
||||||
|
},
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
// noop - we don't want the enter key doing anything
|
||||||
|
},
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
invokeAction(this, 'model.activate', this.get('theme'));
|
||||||
|
invokeAction(this, 'closeModal');
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
if (!this.get('closeDisabled')) {
|
||||||
|
this._super(...arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -1,37 +1,29 @@
|
|||||||
import Controller from 'ember-controller';
|
import Controller from 'ember-controller';
|
||||||
import computed from 'ember-computed';
|
import computed, {notEmpty} from 'ember-computed';
|
||||||
import injectService from 'ember-service/inject';
|
import injectService from 'ember-service/inject';
|
||||||
import observer from 'ember-metal/observer';
|
import observer from 'ember-metal/observer';
|
||||||
import run from 'ember-runloop';
|
import run from 'ember-runloop';
|
||||||
import SettingsSaveMixin from 'ghost-admin/mixins/settings-save';
|
import SettingsSaveMixin from 'ghost-admin/mixins/settings-save';
|
||||||
import randomPassword from 'ghost-admin/utils/random-password';
|
import randomPassword from 'ghost-admin/utils/random-password';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
export default Controller.extend(SettingsSaveMixin, {
|
export default Controller.extend(SettingsSaveMixin, {
|
||||||
|
|
||||||
|
availableTimezones: null,
|
||||||
|
themeToDelete: null,
|
||||||
|
|
||||||
showUploadLogoModal: false,
|
showUploadLogoModal: false,
|
||||||
showUploadCoverModal: false,
|
showUploadCoverModal: false,
|
||||||
|
showDeleteThemeModal: notEmpty('themeToDelete'),
|
||||||
|
|
||||||
availableTimezones: null,
|
ajax: injectService(),
|
||||||
|
|
||||||
notifications: injectService(),
|
|
||||||
config: injectService(),
|
config: injectService(),
|
||||||
|
ghostPaths: injectService(),
|
||||||
|
notifications: injectService(),
|
||||||
|
session: injectService(),
|
||||||
_scratchFacebook: null,
|
_scratchFacebook: null,
|
||||||
_scratchTwitter: null,
|
_scratchTwitter: null,
|
||||||
|
|
||||||
selectedTheme: computed('model.activeTheme', 'themes', function () {
|
|
||||||
let activeTheme = this.get('model.activeTheme');
|
|
||||||
let themes = this.get('themes');
|
|
||||||
let selectedTheme;
|
|
||||||
|
|
||||||
themes.forEach((theme) => {
|
|
||||||
if (theme.name === activeTheme) {
|
|
||||||
selectedTheme = theme;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return selectedTheme;
|
|
||||||
}),
|
|
||||||
|
|
||||||
logoImageSource: computed('model.logo', function () {
|
logoImageSource: computed('model.logo', function () {
|
||||||
return this.get('model.logo') || '';
|
return this.get('model.logo') || '';
|
||||||
}),
|
}),
|
||||||
@ -55,21 +47,6 @@ export default Controller.extend(SettingsSaveMixin, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
themes: computed(function () {
|
|
||||||
return this.get('model.availableThemes').reduce(function (themes, t) {
|
|
||||||
let theme = {};
|
|
||||||
|
|
||||||
theme.name = t.name;
|
|
||||||
theme.label = t.package ? `${t.package.name} - ${t.package.version}` : t.name;
|
|
||||||
theme.package = t.package;
|
|
||||||
theme.active = !!t.active;
|
|
||||||
|
|
||||||
themes.push(theme);
|
|
||||||
|
|
||||||
return themes;
|
|
||||||
}, []);
|
|
||||||
}).readOnly(),
|
|
||||||
|
|
||||||
generatePassword: observer('model.isPrivate', function () {
|
generatePassword: observer('model.isPrivate', function () {
|
||||||
this.get('model.errors').remove('password');
|
this.get('model.errors').remove('password');
|
||||||
if (this.get('model.isPrivate') && this.get('model.hasDirtyAttributes')) {
|
if (this.get('model.isPrivate') && this.get('model.hasDirtyAttributes')) {
|
||||||
@ -77,6 +54,21 @@ export default Controller.extend(SettingsSaveMixin, {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
_deleteTheme() {
|
||||||
|
let theme = this.get('themeToDelete');
|
||||||
|
let themeURL = `${this.get('ghostPaths.apiRoot')}/themes/${theme.name}/`;
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.get('ajax').del(themeURL).then(() => {
|
||||||
|
this.send('reloadSettings');
|
||||||
|
}).catch((error) => {
|
||||||
|
this.get('notifications').showAPIError(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
let notifications = this.get('notifications');
|
let notifications = this.get('notifications');
|
||||||
let config = this.get('config');
|
let config = this.get('config');
|
||||||
@ -107,10 +99,38 @@ export default Controller.extend(SettingsSaveMixin, {
|
|||||||
|
|
||||||
setTheme(theme) {
|
setTheme(theme) {
|
||||||
this.set('model.activeTheme', theme.name);
|
this.set('model.activeTheme', theme.name);
|
||||||
|
this.send('save');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
downloadTheme(theme) {
|
||||||
|
let themeURL = `${this.get('ghostPaths.apiRoot')}/themes/${theme.name}`;
|
||||||
|
let accessToken = this.get('session.data.authenticated.access_token');
|
||||||
|
let downloadURL = `${themeURL}/download/?access_token=${accessToken}`;
|
||||||
|
let iframe = $('#iframeDownload');
|
||||||
|
|
||||||
|
if (iframe.length === 0) {
|
||||||
|
iframe = $('<iframe>', {id: 'iframeDownload'}).hide().appendTo('body');
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe.attr('src', downloadURL);
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteTheme(theme) {
|
||||||
|
if (theme) {
|
||||||
|
return this.set('themeToDelete', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._deleteTheme();
|
||||||
|
},
|
||||||
|
|
||||||
|
hideDeleteThemeModal() {
|
||||||
|
this.set('themeToDelete', null);
|
||||||
|
},
|
||||||
|
|
||||||
setTimezone(timezone) {
|
setTimezone(timezone) {
|
||||||
this.set('model.activeTimezone', timezone.name);
|
this.set('model.activeTimezone', timezone.name);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleUploadCoverModal() {
|
toggleUploadCoverModal() {
|
||||||
this.toggleProperty('showUploadCoverModal');
|
this.toggleProperty('showUploadCoverModal');
|
||||||
},
|
},
|
||||||
|
@ -5,6 +5,7 @@ import mockSettings from './config/settings';
|
|||||||
import mockSlugs from './config/slugs';
|
import mockSlugs from './config/slugs';
|
||||||
import mockSubscribers from './config/subscribers';
|
import mockSubscribers from './config/subscribers';
|
||||||
import mockTags from './config/tags';
|
import mockTags from './config/tags';
|
||||||
|
import mockThemes from './config/themes';
|
||||||
import mockUsers from './config/users';
|
import mockUsers from './config/users';
|
||||||
|
|
||||||
// import {versionMismatchResponse} from 'utils';
|
// import {versionMismatchResponse} from 'utils';
|
||||||
@ -17,6 +18,9 @@ export default function () {
|
|||||||
// Mock endpoints here to override real API requests during development
|
// Mock endpoints here to override real API requests during development
|
||||||
// this.put('/posts/:id/', versionMismatchResponse);
|
// this.put('/posts/:id/', versionMismatchResponse);
|
||||||
// mockSubscribers(this);
|
// mockSubscribers(this);
|
||||||
|
this.loadFixtures('settings');
|
||||||
|
mockSettings(this);
|
||||||
|
mockThemes(this);
|
||||||
|
|
||||||
// keep this line, it allows all other API requests to hit the real server
|
// keep this line, it allows all other API requests to hit the real server
|
||||||
this.passthrough();
|
this.passthrough();
|
||||||
@ -40,6 +44,7 @@ export function testConfig() {
|
|||||||
mockSlugs(this);
|
mockSlugs(this);
|
||||||
mockSubscribers(this);
|
mockSubscribers(this);
|
||||||
mockTags(this);
|
mockTags(this);
|
||||||
|
mockThemes(this);
|
||||||
mockUsers(this);
|
mockUsers(this);
|
||||||
|
|
||||||
/* Notifications -------------------------------------------------------- */
|
/* Notifications -------------------------------------------------------- */
|
||||||
|
@ -18,11 +18,28 @@ export default function mockSettings(server) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.put('/settings/', function (db, request) {
|
server.put('/settings/', function (db, request) {
|
||||||
|
console.log('/settings/', request.requestBody);
|
||||||
let newSettings = JSON.parse(request.requestBody).settings;
|
let newSettings = JSON.parse(request.requestBody).settings;
|
||||||
|
|
||||||
db.settings.remove();
|
db.settings.remove();
|
||||||
db.settings.insert(newSettings);
|
db.settings.insert(newSettings);
|
||||||
|
|
||||||
|
let [activeTheme] = db.settings.where({key: 'activeTheme'});
|
||||||
|
let [availableThemes] = db.settings.where({key: 'availableThemes'});
|
||||||
|
|
||||||
|
console.log('activeTheme', activeTheme);
|
||||||
|
console.log('availableThemes', availableThemes);
|
||||||
|
|
||||||
|
availableThemes.value.forEach((theme) => {
|
||||||
|
if (theme.name === activeTheme.value) {
|
||||||
|
theme.active = true;
|
||||||
|
} else {
|
||||||
|
theme.active = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.settings.update(availableThemes.id, availableThemes);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
meta: {},
|
meta: {},
|
||||||
settings: db.settings
|
settings: db.settings
|
||||||
|
27
ghost/admin/app/mirage/config/themes.js
Normal file
27
ghost/admin/app/mirage/config/themes.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
let themeCount = 1;
|
||||||
|
|
||||||
|
export default function mockThemes(server) {
|
||||||
|
server.post('/themes/upload/', function (db/*, request*/) {
|
||||||
|
let [availableThemes] = db.settings.where({key: 'availableThemes'});
|
||||||
|
// pretender/mirage doesn't currently process FormData so we can't use
|
||||||
|
// any info passed in through the request
|
||||||
|
let theme = {
|
||||||
|
name: `test-${themeCount}`,
|
||||||
|
package: {
|
||||||
|
name: `Test ${themeCount}`,
|
||||||
|
version: '0.1'
|
||||||
|
},
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
themeCount++;
|
||||||
|
|
||||||
|
availableThemes.value.pushObject(theme);
|
||||||
|
db.settings.remove({key: 'availableThemes'});
|
||||||
|
db.settings.insert(availableThemes);
|
||||||
|
|
||||||
|
return {
|
||||||
|
themes: [theme]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
@ -214,6 +214,7 @@ export default [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'availableThemes',
|
key: 'availableThemes',
|
||||||
|
id: 18,
|
||||||
value: [
|
value: [
|
||||||
{
|
{
|
||||||
name: 'casper',
|
name: 'casper',
|
||||||
@ -222,6 +223,16 @@ export default [
|
|||||||
version: '1.0'
|
version: '1.0'
|
||||||
},
|
},
|
||||||
active: true
|
active: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
package: {
|
||||||
|
name: 'Foo',
|
||||||
|
version: '0.1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bar'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
type: 'theme'
|
type: 'theme'
|
||||||
|
@ -7,7 +7,7 @@ export default Mixin.create({
|
|||||||
save() {
|
save() {
|
||||||
this.set('submitting', true);
|
this.set('submitting', true);
|
||||||
|
|
||||||
this.save().then(() => {
|
this.save().finally(() => {
|
||||||
this.set('submitting', false);
|
this.set('submitting', false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,9 @@ GhostRouter.map(function () {
|
|||||||
this.route('user', {path: ':user_slug'});
|
this.route('user', {path: ':user_slug'});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('settings.general', {path: '/settings/general'});
|
this.route('settings.general', {path: '/settings/general'}, function () {
|
||||||
|
this.route('uploadtheme');
|
||||||
|
});
|
||||||
this.route('settings.tags', {path: '/settings/tags'}, function () {
|
this.route('settings.tags', {path: '/settings/tags'}, function () {
|
||||||
this.route('tag', {path: ':tag_slug'});
|
this.route('tag', {path: ':tag_slug'});
|
||||||
this.route('new');
|
this.route('new');
|
||||||
|
@ -11,6 +11,11 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
|||||||
|
|
||||||
config: injectService(),
|
config: injectService(),
|
||||||
|
|
||||||
|
// TODO: replace with a synchronous settings service
|
||||||
|
querySettings() {
|
||||||
|
return this.store.queryRecord('setting', {type: 'blog,theme,private'});
|
||||||
|
},
|
||||||
|
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
return this.get('session.user')
|
return this.get('session.user')
|
||||||
@ -20,7 +25,7 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
|||||||
|
|
||||||
model() {
|
model() {
|
||||||
return RSVP.hash({
|
return RSVP.hash({
|
||||||
settings: this.store.queryRecord('setting', {type: 'blog,theme,private'}),
|
settings: this.querySettings(),
|
||||||
availableTimezones: this.get('config.availableTimezones')
|
availableTimezones: this.get('config.availableTimezones')
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -32,7 +37,17 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
save() {
|
save() {
|
||||||
this.get('controller').send('save');
|
return this.get('controller').send('save');
|
||||||
|
},
|
||||||
|
|
||||||
|
reloadSettings() {
|
||||||
|
return this.querySettings((settings) => {
|
||||||
|
this.set('controller.model', settings);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
activateTheme(theme) {
|
||||||
|
return this.get('controller').send('setTheme', theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
14
ghost/admin/app/routes/settings/general/uploadtheme.js
Normal file
14
ghost/admin/app/routes/settings/general/uploadtheme.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||||
|
|
||||||
|
export default AuthenticatedRoute.extend({
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return this.modelFor('settings.general').settings.get('availableThemes');
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
cancel() {
|
||||||
|
this.transitionTo('settings.general');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
14
ghost/admin/app/services/event-bus.js
Normal file
14
ghost/admin/app/services/event-bus.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import Service from 'ember-service';
|
||||||
|
import Evented from 'ember-evented';
|
||||||
|
|
||||||
|
export default Service.extend(Evented, {
|
||||||
|
publish() {
|
||||||
|
return this.trigger(...arguments);
|
||||||
|
},
|
||||||
|
subscribe() {
|
||||||
|
return this.on(...arguments);
|
||||||
|
},
|
||||||
|
unsubscribe() {
|
||||||
|
return this.off(...arguments);
|
||||||
|
}
|
||||||
|
});
|
@ -10,7 +10,6 @@
|
|||||||
margin: 1.6em 0;
|
margin: 1.6em 0;
|
||||||
min-height: 130px;
|
min-height: 130px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 130px;
|
|
||||||
background: #f6f7f8;
|
background: #f6f7f8;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #808284;
|
color: #808284;
|
||||||
|
@ -161,3 +161,93 @@
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Themes
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
.settings-themes h3 {
|
||||||
|
margin-bottom: 1.6em;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 13px 15px;
|
||||||
|
border-top: 1px solid #dfe1e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-item--active {
|
||||||
|
background: color(#dfe1e3 lightness(+10%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-item-body .name {
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--darkgrey);
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 400;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-item:last-of-type {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border-bottom: 1px solid #dfe1e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-item-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-items: stretch;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-action:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-action {
|
||||||
|
float: left;
|
||||||
|
margin-right: 20px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.theme-list-action {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* account for length difference between Active and Activate */
|
||||||
|
.theme-list-action-activate {
|
||||||
|
min-width: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-item--active .theme-list-action-activate {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
.theme-list-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-item-body .name {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-item-aside {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-item-body {
|
||||||
|
margin-bottom: 0.35em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list-action:last-child {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
32
ghost/admin/app/templates/components/gh-theme-table.hbs
Normal file
32
ghost/admin/app/templates/components/gh-theme-table.hbs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{{#if themes}}
|
||||||
|
<div class="theme-list">
|
||||||
|
{{#each themes as |theme|}}
|
||||||
|
<div class="theme-list-item {{if theme.active "theme-list-item--active"}}">
|
||||||
|
<div class="theme-list-item-body">
|
||||||
|
<span class="name">{{theme.label}} {{#if theme.isDefault}}(default){{/if}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="theme-list-item-aside">
|
||||||
|
{{#if theme.isDeletable}}
|
||||||
|
<a href="#" {{action deleteTheme theme}} disabled={{theme.active}} class="theme-list-action">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<a href="#" {{action downloadTheme theme}} class="theme-list-action">
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{{#if theme.active}}
|
||||||
|
<span class="theme-list-action theme-list-action-activate">Active</span>
|
||||||
|
{{else}}
|
||||||
|
<a href="#" {{action activateTheme theme}} class="theme-list-action theme-list-action-activate">
|
||||||
|
Activate
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
No theme found!
|
||||||
|
{{/if}}
|
20
ghost/admin/app/templates/components/modals/delete-theme.hbs
Normal file
20
ghost/admin/app/templates/components/modals/delete-theme.hbs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<header class="modal-header">
|
||||||
|
<h1>Are you sure you want to delete this theme?</h1>
|
||||||
|
</header>
|
||||||
|
<a class="close icon-x" href="" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<strong>WARNING:</strong>
|
||||||
|
You're about to delete "<strong>{{theme.label}}</strong>".
|
||||||
|
This is permanent!
|
||||||
|
No backups, no restores, no magic undo button. We warned you, ok?
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<strong>RECOMMENDED:</strong>
|
||||||
|
<a href="#" {{action download}}>Download your theme before continuing</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button {{action "closeModal"}} class="btn btn-default btn-minor">Cancel</button>
|
||||||
|
{{#gh-spin-button action="confirm" class="btn btn-red" submitting=submitting}}Delete{{/gh-spin-button}}
|
||||||
|
</div>
|
50
ghost/admin/app/templates/components/modals/upload-theme.hbs
Normal file
50
ghost/admin/app/templates/components/modals/upload-theme.hbs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<header class="modal-header">
|
||||||
|
<h1>
|
||||||
|
{{#if theme}}
|
||||||
|
Upload successful!
|
||||||
|
{{else}}
|
||||||
|
Upload a theme
|
||||||
|
{{/if}}
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<a class="close icon-x" href="#" title="Close" {{action "closeModal"}}><span class="hidden">Close</span></a>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
{{#if theme}}
|
||||||
|
<p>
|
||||||
|
"{{themeName}}" uploaded successfully.
|
||||||
|
{{#if canActivateTheme}}Do you want to activate it now?{{/if}}
|
||||||
|
</p>
|
||||||
|
{{else if displayOverwriteWarning}}
|
||||||
|
<p>
|
||||||
|
"{{fileThemeName}}" will overwrite an existing theme of the same name. Are you sure?
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
{{gh-file-uploader
|
||||||
|
url=uploadUrl
|
||||||
|
paramName="theme"
|
||||||
|
accept=accept
|
||||||
|
labelText="Click to select or drag-and-drop your theme zip file here."
|
||||||
|
validate=(action "validateTheme")
|
||||||
|
uploadStarted=(action "uploadStarted")
|
||||||
|
uploadFinished=(action "uploadFinished")
|
||||||
|
uploadSuccess=(action "uploadSuccess")
|
||||||
|
listenTo="themeUploader"}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button {{action "closeModal"}} disabled={{closeDisabled}} class="btn btn-default btn-minor">
|
||||||
|
{{#if theme}}Close{{else}}Cancel{{/if}}
|
||||||
|
</button>
|
||||||
|
{{#if displayOverwriteWarning}}
|
||||||
|
<button {{action "confirmOverwrite"}} class="btn btn-red">
|
||||||
|
Overwrite
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
{{#if canActivateTheme}}
|
||||||
|
<button {{action "activate"}} class="btn btn-green">
|
||||||
|
Activate Now
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
@ -39,9 +39,9 @@
|
|||||||
|
|
||||||
{{#if showUploadLogoModal}}
|
{{#if showUploadLogoModal}}
|
||||||
{{gh-fullscreen-modal "upload-image"
|
{{gh-fullscreen-modal "upload-image"
|
||||||
model=(hash model=model imageProperty="logo")
|
model=(hash model=model imageProperty="logo")
|
||||||
close=(action "toggleUploadLogoModal")
|
close=(action "toggleUploadLogoModal")
|
||||||
modifier="action wide"}}
|
modifier="action wide"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -56,9 +56,9 @@
|
|||||||
|
|
||||||
{{#if showUploadCoverModal}}
|
{{#if showUploadCoverModal}}
|
||||||
{{gh-fullscreen-modal "upload-image"
|
{{gh-fullscreen-modal "upload-image"
|
||||||
model=(hash model=model imageProperty="cover")
|
model=(hash model=model imageProperty="cover")
|
||||||
close=(action "toggleUploadCoverModal")
|
close=(action "toggleUploadCoverModal")
|
||||||
modifier="action wide"}}
|
modifier="action wide"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -80,22 +80,6 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group for-select">
|
|
||||||
<label for="activeTheme">Theme</label>
|
|
||||||
<span class="gh-select" data-select-text="{{selectedTheme.label}}" tabindex="0">
|
|
||||||
{{gh-select-native
|
|
||||||
id="activeTheme"
|
|
||||||
name="general[activeTheme]"
|
|
||||||
content=themes
|
|
||||||
optionValuePath="name"
|
|
||||||
optionLabelPath="label"
|
|
||||||
selection=selectedTheme
|
|
||||||
action="setTheme"
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<p>Select a theme for your blog</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="facebook"}}
|
{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="facebook"}}
|
||||||
<label for="facebook">Facebook Page</label>
|
<label for="facebook">Facebook Page</label>
|
||||||
@ -136,6 +120,35 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="settings-themes">
|
||||||
|
<h3>Themes</h3>
|
||||||
|
|
||||||
|
{{gh-theme-table
|
||||||
|
availableThemes=model.availableThemes
|
||||||
|
activeTheme=model.activeTheme
|
||||||
|
activateTheme=(action "setTheme")
|
||||||
|
downloadTheme=(action "downloadTheme")
|
||||||
|
deleteTheme=(action "deleteTheme")}}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
{{#link-to "settings.general.uploadtheme" class="btn btn-green"}}
|
||||||
|
Upload a theme
|
||||||
|
{{/link-to}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if showDeleteThemeModal}}
|
||||||
|
{{gh-fullscreen-modal "delete-theme"
|
||||||
|
model=(hash
|
||||||
|
theme=themeToDelete
|
||||||
|
download=(action "downloadTheme" themeToDelete)
|
||||||
|
)
|
||||||
|
close=(action "hideDeleteThemeModal")
|
||||||
|
confirm=(action "deleteTheme")
|
||||||
|
modifier="action wide"}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{outlet}}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
{{gh-fullscreen-modal "upload-theme"
|
||||||
|
model=(hash
|
||||||
|
availableThemes=model
|
||||||
|
uploadSuccess=(route-action 'reloadSettings')
|
||||||
|
activate=(route-action 'activateTheme')
|
||||||
|
)
|
||||||
|
close=(route-action "cancel")
|
||||||
|
modifier="action wide"}}
|
@ -11,6 +11,8 @@ import run from 'ember-runloop';
|
|||||||
import startApp from '../../helpers/start-app';
|
import startApp from '../../helpers/start-app';
|
||||||
import destroyApp from '../../helpers/destroy-app';
|
import destroyApp from '../../helpers/destroy-app';
|
||||||
import { invalidateSession, authenticateSession } from 'ghost-admin/tests/helpers/ember-simple-auth';
|
import { invalidateSession, authenticateSession } from 'ghost-admin/tests/helpers/ember-simple-auth';
|
||||||
|
import Mirage from 'ember-cli-mirage';
|
||||||
|
import mockThemes from 'ghost-admin/mirage/config/themes';
|
||||||
|
|
||||||
describe('Acceptance: Settings - General', function () {
|
describe('Acceptance: Settings - General', function () {
|
||||||
let application;
|
let application;
|
||||||
@ -125,12 +127,6 @@ describe('Acceptance: Settings - General', function () {
|
|||||||
andThen(() => {
|
andThen(() => {
|
||||||
expect(find('.fullscreen-modal').length).to.equal(0);
|
expect(find('.fullscreen-modal').length).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// renders theme selector correctly
|
|
||||||
andThen(() => {
|
|
||||||
expect(find('#activeTheme select option').length, 'available themes').to.equal(1);
|
|
||||||
expect(find('#activeTheme select option').text().trim()).to.equal('Blog - 1.0');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders timezone selector correctly', function () {
|
it('renders timezone selector correctly', function () {
|
||||||
@ -343,5 +339,161 @@ describe('Acceptance: Settings - General', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows management of themes', function () {
|
||||||
|
// lists available themes + active theme is highlighted
|
||||||
|
|
||||||
|
// theme upload
|
||||||
|
// - displays modal
|
||||||
|
// - validates mime type
|
||||||
|
// - validates casper.zip
|
||||||
|
// - handles validation errors
|
||||||
|
// - handles upload and close
|
||||||
|
// - handles upload and activate
|
||||||
|
// - displays overwrite warning if theme already exists
|
||||||
|
|
||||||
|
// theme activation
|
||||||
|
// - switches theme
|
||||||
|
|
||||||
|
// theme deletion
|
||||||
|
// - displays modal
|
||||||
|
// - deletes theme and refreshes list
|
||||||
|
|
||||||
|
visit('/settings/general');
|
||||||
|
|
||||||
|
// lists available themes (themes are specified in mirage/fixtures/settings)
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.theme-list-item').length,
|
||||||
|
'shows correct number of themes'
|
||||||
|
).to.equal(3);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-list-item:contains("Blog")').hasClass('theme-list-item--active'),
|
||||||
|
'Blog theme marked as active'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// theme upload displays modal
|
||||||
|
click('a:contains("Upload a theme")');
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal .modal-content:contains("Upload a theme")').length,
|
||||||
|
'theme upload modal displayed after button click'
|
||||||
|
).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// cancelling theme upload closes modal
|
||||||
|
click('.fullscreen-modal button:contains("Cancel")');
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal').length === 0,
|
||||||
|
'modal is closed when cancelling'
|
||||||
|
).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// theme upload validates mime type
|
||||||
|
click('a:contains("Upload a theme")');
|
||||||
|
fileUpload('.fullscreen-modal input[type="file"]', ['test'], {type: 'text/csv'});
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal .failed').text(),
|
||||||
|
'validation error is shown for invalid mime type'
|
||||||
|
).to.match(/is not supported/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// theme upload validates casper.zip
|
||||||
|
click('button:contains("Try Again")');
|
||||||
|
fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'casper.zip', type: 'application/zip'});
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal .failed').text(),
|
||||||
|
'validation error is shown when uploading casper.zip'
|
||||||
|
).to.match(/default Casper theme cannot be overwritten/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// theme upload handles validation errors
|
||||||
|
andThen(() => {
|
||||||
|
server.post('/themes/upload/', function () {
|
||||||
|
return new Mirage.Response(422, {}, {
|
||||||
|
errors: [{
|
||||||
|
message: 'Invalid theme'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
click('button:contains("Try Again")');
|
||||||
|
fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'error.zip', type: 'application/zip'});
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal .failed').text().trim(),
|
||||||
|
'validation error is passed through from server'
|
||||||
|
).to.equal('Invalid theme');
|
||||||
|
|
||||||
|
// reset to default mirage handlers
|
||||||
|
mockThemes(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
// theme upload handles success then close
|
||||||
|
click('button:contains("Try Again")');
|
||||||
|
fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'theme-1.zip', type: 'application/zip'});
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal h1').text().trim(),
|
||||||
|
'modal header after successful upload'
|
||||||
|
).to.equal('Upload successful!');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.modal-body').text(),
|
||||||
|
'modal displays theme name after successful upload'
|
||||||
|
).to.match(/"Test 1 - 0\.1" uploaded successfully/);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-list-item').length,
|
||||||
|
'number of themes in list grows after upload'
|
||||||
|
).to.equal(4);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-list-item:contains("Test 1 - 0.1")').hasClass('theme-list-item--active'),
|
||||||
|
'newly uploaded theme is active'
|
||||||
|
).to.be.false;
|
||||||
|
});
|
||||||
|
click('.fullscreen-modal button:contains("Close")');
|
||||||
|
|
||||||
|
// theme upload handles success then activate
|
||||||
|
click('a:contains("Upload a theme")');
|
||||||
|
fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'theme-2.zip', type: 'application/zip'});
|
||||||
|
click('button:contains("Activate Now")');
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.theme-list-item').length,
|
||||||
|
'number of themes in list grows after upload and activate'
|
||||||
|
).to.equal(5);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-list-item:contains("Test 2 - 0.1")').hasClass('theme-list-item--active'),
|
||||||
|
'newly uploaded+activated theme is active'
|
||||||
|
).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// theme activation switches active theme
|
||||||
|
click('.theme-list-item:contains("Blog") a:contains("Activate")');
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.theme-list-item:contains("Test 2 - 0.1")').hasClass('theme-list-item--active'),
|
||||||
|
'previously active theme is not active'
|
||||||
|
).to.be.false;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-list-item:contains("Blog")').hasClass('theme-list-item--active'),
|
||||||
|
'activated theme is active'
|
||||||
|
).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// theme deletion displays modal
|
||||||
|
|
||||||
|
// cancelling theme deletion closes modal
|
||||||
|
|
||||||
|
// confirming theme deletion closes modal and refreshes list
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -131,6 +131,22 @@ describeComponent(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fires fileSelected action on file selection', function (done) {
|
||||||
|
let fileSelected = sinon.spy();
|
||||||
|
this.set('fileSelected', fileSelected);
|
||||||
|
|
||||||
|
stubSuccessfulUpload(server);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-file-uploader url=uploadUrl fileSelected=(action fileSelected)}}`);
|
||||||
|
fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(fileSelected.calledOnce).to.be.true;
|
||||||
|
expect(fileSelected.args[0]).to.not.be.blank;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('fires uploadStarted action on upload start', function (done) {
|
it('fires uploadStarted action on upload start', function (done) {
|
||||||
let uploadStarted = sinon.spy();
|
let uploadStarted = sinon.spy();
|
||||||
this.set('uploadStarted', uploadStarted);
|
this.set('uploadStarted', uploadStarted);
|
||||||
|
@ -201,6 +201,22 @@ describeComponent(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fires fileSelected action on file selection', function (done) {
|
||||||
|
let fileSelected = sinon.spy();
|
||||||
|
this.set('fileSelected', fileSelected);
|
||||||
|
|
||||||
|
stubSuccessfulUpload(server);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-image-uploader url=image fileSelected=(action fileSelected) update=(action update)}}`);
|
||||||
|
fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(fileSelected.calledOnce).to.be.true;
|
||||||
|
expect(fileSelected.args[0]).to.not.be.blank;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('fires uploadStarted action on upload start', function (done) {
|
it('fires uploadStarted action on upload start', function (done) {
|
||||||
let uploadStarted = sinon.spy();
|
let uploadStarted = sinon.spy();
|
||||||
this.set('uploadStarted', uploadStarted);
|
this.set('uploadStarted', uploadStarted);
|
||||||
|
174
ghost/admin/tests/integration/components/gh-theme-table-test.js
Normal file
174
ghost/admin/tests/integration/components/gh-theme-table-test.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/* jshint expr:true */
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import {
|
||||||
|
describeComponent,
|
||||||
|
it
|
||||||
|
} from 'ember-mocha';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import $ from 'jquery';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import run from 'ember-runloop';
|
||||||
|
|
||||||
|
describeComponent(
|
||||||
|
'gh-theme-table',
|
||||||
|
'Integration: Component: gh-theme-table',
|
||||||
|
{
|
||||||
|
integration: true
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
it('renders', function() {
|
||||||
|
this.set('availableThemes', [
|
||||||
|
{name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
|
||||||
|
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
|
||||||
|
{name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}},
|
||||||
|
{name: 'foo'}
|
||||||
|
]);
|
||||||
|
this.set('activeTheme', 'Daring');
|
||||||
|
this.set('actionHandler', sinon.spy());
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-theme-table
|
||||||
|
availableThemes=availableThemes
|
||||||
|
activeTheme=activeTheme
|
||||||
|
activateTheme=(action actionHandler)
|
||||||
|
downloadTheme=(action actionHandler)
|
||||||
|
deleteTheme=(action actionHandler)
|
||||||
|
}}`);
|
||||||
|
|
||||||
|
expect(this.$('.theme-list').length, '.theme-list is present').to.equal(1);
|
||||||
|
expect(this.$('.theme-list-item').length, 'number of rows').to.equal(4);
|
||||||
|
|
||||||
|
let packageNames = this.$('.theme-list-item-body .name').map((i, name) => {
|
||||||
|
return $(name).text().trim();
|
||||||
|
}).toArray();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
packageNames,
|
||||||
|
'themes are ordered by label, casper has "default", package versions are shown'
|
||||||
|
).to.deep.equal([
|
||||||
|
'Casper - 1.3.1 (default)',
|
||||||
|
'Daring - 0.1.4',
|
||||||
|
'foo',
|
||||||
|
'Lanyon - 1.1.0'
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
this.$('.theme-list-item:contains("Daring")').hasClass('theme-list-item--active'),
|
||||||
|
'active theme is highlighted'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
this.$('.theme-list-item:not(:contains("Daring"))').find('a:contains("Activate")').length === 3,
|
||||||
|
'non-active themes have an activate link'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
this.$('.theme-list-item:contains("Daring")').find('a:contains("Activate")').length === 0,
|
||||||
|
'active theme doesn\'t have an activate link'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
this.$('a:contains("Download")').length,
|
||||||
|
'all themes have a download link'
|
||||||
|
).to.equal(4);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
this.$('.theme-list-item:contains("foo")').find('a:contains("Delete")').length === 1,
|
||||||
|
'non-active, non-casper theme has delete link'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
this.$('.theme-list-item:contains("Casper")').find('a:contains("Delete")').length === 0,
|
||||||
|
'casper doesn\'t have delete link'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
this.$('.theme-list-item--active').find('a:contains("Delete")').length === 0,
|
||||||
|
'active theme doesn\'t have delete link'
|
||||||
|
).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delete link triggers passed in action', function () {
|
||||||
|
let deleteAction = sinon.spy();
|
||||||
|
let actionHandler = sinon.spy();
|
||||||
|
|
||||||
|
this.set('availableThemes', [
|
||||||
|
{name: 'Foo', active: true},
|
||||||
|
{name: 'Bar'}
|
||||||
|
]);
|
||||||
|
this.set('activeTheme', 'Foo');
|
||||||
|
this.set('deleteAction', deleteAction);
|
||||||
|
this.set('actionHandler', actionHandler);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-theme-table
|
||||||
|
availableThemes=availableThemes
|
||||||
|
activeTheme=activeTheme
|
||||||
|
activateTheme=(action actionHandler)
|
||||||
|
downloadTheme=(action actionHandler)
|
||||||
|
deleteTheme=(action deleteAction)
|
||||||
|
}}`);
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
this.$('.theme-list-item:contains("Bar") a:contains("Delete")').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteAction.calledOnce).to.be.true;
|
||||||
|
expect(deleteAction.firstCall.args[0].name).to.equal('Bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('download link triggers passed in action', function () {
|
||||||
|
let downloadAction = sinon.spy();
|
||||||
|
let actionHandler = sinon.spy();
|
||||||
|
|
||||||
|
this.set('availableThemes', [
|
||||||
|
{name: 'Foo', active: true},
|
||||||
|
{name: 'Bar'}
|
||||||
|
]);
|
||||||
|
this.set('activeTheme', 'Foo');
|
||||||
|
this.set('downloadAction', downloadAction);
|
||||||
|
this.set('actionHandler', actionHandler);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-theme-table
|
||||||
|
availableThemes=availableThemes
|
||||||
|
activeTheme=activeTheme
|
||||||
|
activateTheme=(action actionHandler)
|
||||||
|
downloadTheme=(action downloadAction)
|
||||||
|
deleteTheme=(action actionHandler)
|
||||||
|
}}`);
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
this.$('.theme-list-item:contains("Foo") a:contains("Download")').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(downloadAction.calledOnce).to.be.true;
|
||||||
|
expect(downloadAction.firstCall.args[0].name).to.equal('Foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('activate link triggers passed in action', function () {
|
||||||
|
let activateAction = sinon.spy();
|
||||||
|
let actionHandler = sinon.spy();
|
||||||
|
|
||||||
|
this.set('availableThemes', [
|
||||||
|
{name: 'Foo', active: true},
|
||||||
|
{name: 'Bar'}
|
||||||
|
]);
|
||||||
|
this.set('activeTheme', 'Foo');
|
||||||
|
this.set('activateAction', activateAction);
|
||||||
|
this.set('actionHandler', actionHandler);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-theme-table
|
||||||
|
availableThemes=availableThemes
|
||||||
|
activeTheme=activeTheme
|
||||||
|
activateTheme=(action activateAction)
|
||||||
|
downloadTheme=(action actionHandler)
|
||||||
|
deleteTheme=(action actionHandler)
|
||||||
|
}}`);
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
this.$('.theme-list-item:contains("Bar") a:contains("Activate")').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activateAction.calledOnce).to.be.true;
|
||||||
|
expect(activateAction.firstCall.args[0].name).to.equal('Bar');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,30 @@
|
|||||||
|
/* jshint expr:true */
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import {
|
||||||
|
describeComponent,
|
||||||
|
it
|
||||||
|
} from 'ember-mocha';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
|
||||||
|
describeComponent(
|
||||||
|
'modals/upload-theme',
|
||||||
|
'Integration: Component: modals/upload-theme',
|
||||||
|
{
|
||||||
|
integration: true
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
it('renders', function() {
|
||||||
|
// Set any properties with this.set('myProperty', 'value');
|
||||||
|
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||||
|
// Template block usage:
|
||||||
|
// this.render(hbs`
|
||||||
|
// {{#modals/upload-theme}}
|
||||||
|
// template content
|
||||||
|
// {{/modals/upload-theme}}
|
||||||
|
// `);
|
||||||
|
|
||||||
|
this.render(hbs`{{modals/upload-theme}}`);
|
||||||
|
expect(this.$()).to.have.length(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -54,43 +54,5 @@ describeModule(
|
|||||||
expect(controller.get('model.permalinks')).to.equal('/:year/:month/:day/:slug/');
|
expect(controller.get('model.permalinks')).to.equal('/:year/:month/:day/:slug/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('themes should be correct', function () {
|
|
||||||
let themes = [];
|
|
||||||
let controller;
|
|
||||||
|
|
||||||
themes.push({
|
|
||||||
name: 'casper',
|
|
||||||
active: true,
|
|
||||||
package: {
|
|
||||||
name: 'Casper',
|
|
||||||
version: '1.1.5'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
themes.push({
|
|
||||||
name: 'rasper',
|
|
||||||
package: {
|
|
||||||
name: 'Rasper',
|
|
||||||
version: '1.0.0'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
controller = this.subject({
|
|
||||||
model: EmberObject.create({
|
|
||||||
availableThemes: themes
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
themes = controller.get('themes');
|
|
||||||
expect(themes).to.be.an.Array;
|
|
||||||
expect(themes.length).to.equal(2);
|
|
||||||
expect(themes.objectAt(0).name).to.equal('casper');
|
|
||||||
expect(themes.objectAt(0).active).to.be.ok;
|
|
||||||
expect(themes.objectAt(0).label).to.equal('Casper - 1.1.5');
|
|
||||||
expect(themes.objectAt(1).name).to.equal('rasper');
|
|
||||||
expect(themes.objectAt(1).active).to.not.be.ok;
|
|
||||||
expect(themes.objectAt(1).label).to.equal('Rasper - 1.0.0');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
37
ghost/admin/tests/unit/services/event-bus-test.js
Normal file
37
ghost/admin/tests/unit/services/event-bus-test.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/* jshint expr:true */
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import {
|
||||||
|
describeModule,
|
||||||
|
it
|
||||||
|
} from 'ember-mocha';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
describeModule(
|
||||||
|
'service:event-bus',
|
||||||
|
'Unit: Service: event-bus',
|
||||||
|
{},
|
||||||
|
function() {
|
||||||
|
it('works', function () {
|
||||||
|
let service = this.subject();
|
||||||
|
let eventHandler = sinon.spy();
|
||||||
|
|
||||||
|
service.subscribe('test-event', eventHandler);
|
||||||
|
|
||||||
|
service.publish('test-event', 'test');
|
||||||
|
|
||||||
|
service.unsubscribe('test-event', eventHandler);
|
||||||
|
|
||||||
|
service.publish('test-event', 'test two');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
eventHandler.calledOnce,
|
||||||
|
'event handler only triggered once'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
eventHandler.calledWith('test'),
|
||||||
|
'event handler was passed correct arguments'
|
||||||
|
).to.be.true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user