mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 14:03:48 +03:00
✨ theme management UI
refs https://github.com/TryGhost/Ghost/issues/7204, requires https://github.com/TryGhost/Ghost/pull/7209 - replaces theme dropdown with a table - adds theme upload modal - validates theme mime type - prevents upload of `casper.zip` (default Casper theme can't be overwritten) - warns if an upload will overwrite an existing theme - gives option of immediately activating the uploaded theme or closing after successful upload - adds theme activation link/action - adds theme download link/action - adds theme deletion modal - warns about no undo possibility - offers possibility to download theme - modifies mirage config to handle theme changes
This commit is contained in:
parent
3bfc342314
commit
0abe447551
@ -32,6 +32,7 @@ export default Component.extend({
|
||||
uploadPercentage: 0,
|
||||
|
||||
ajax: injectService(),
|
||||
eventBus: injectService(),
|
||||
notifications: injectService(),
|
||||
|
||||
formData: computed('file', function () {
|
||||
@ -57,6 +58,32 @@ export default Component.extend({
|
||||
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) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
@ -142,7 +169,7 @@ export default Component.extend({
|
||||
} else if (isRequestEntityTooLargeError(error)) {
|
||||
message = 'The file you uploaded was larger than the maximum file size your server allows.';
|
||||
} else if (error.errors && !isBlank(error.errors[0].message)) {
|
||||
message = error.errors[0].message;
|
||||
message = htmlSafe(error.errors[0].message);
|
||||
} else {
|
||||
message = 'Something went wrong :(';
|
||||
}
|
||||
@ -190,6 +217,12 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
upload() {
|
||||
if (this.get('file')) {
|
||||
this.generateRequest();
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('file', null);
|
||||
this.set('uploadPercentage', 0);
|
||||
|
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 computed from 'ember-computed';
|
||||
import computed, {notEmpty} from 'ember-computed';
|
||||
import injectService from 'ember-service/inject';
|
||||
import observer from 'ember-metal/observer';
|
||||
import run from 'ember-runloop';
|
||||
import SettingsSaveMixin from 'ghost-admin/mixins/settings-save';
|
||||
import randomPassword from 'ghost-admin/utils/random-password';
|
||||
import $ from 'jquery';
|
||||
|
||||
export default Controller.extend(SettingsSaveMixin, {
|
||||
|
||||
availableTimezones: null,
|
||||
themeToDelete: null,
|
||||
|
||||
showUploadLogoModal: false,
|
||||
showUploadCoverModal: false,
|
||||
showDeleteThemeModal: notEmpty('themeToDelete'),
|
||||
|
||||
availableTimezones: null,
|
||||
|
||||
notifications: injectService(),
|
||||
ajax: injectService(),
|
||||
config: injectService(),
|
||||
ghostPaths: injectService(),
|
||||
notifications: injectService(),
|
||||
session: injectService(),
|
||||
_scratchFacebook: 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 () {
|
||||
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 () {
|
||||
this.get('model.errors').remove('password');
|
||||
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() {
|
||||
let notifications = this.get('notifications');
|
||||
let config = this.get('config');
|
||||
@ -107,10 +99,38 @@ export default Controller.extend(SettingsSaveMixin, {
|
||||
|
||||
setTheme(theme) {
|
||||
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) {
|
||||
this.set('model.activeTimezone', timezone.name);
|
||||
},
|
||||
|
||||
toggleUploadCoverModal() {
|
||||
this.toggleProperty('showUploadCoverModal');
|
||||
},
|
||||
|
@ -5,6 +5,7 @@ import mockSettings from './config/settings';
|
||||
import mockSlugs from './config/slugs';
|
||||
import mockSubscribers from './config/subscribers';
|
||||
import mockTags from './config/tags';
|
||||
import mockThemes from './config/themes';
|
||||
import mockUsers from './config/users';
|
||||
|
||||
// import {versionMismatchResponse} from 'utils';
|
||||
@ -17,6 +18,9 @@ export default function () {
|
||||
// Mock endpoints here to override real API requests during development
|
||||
// this.put('/posts/:id/', versionMismatchResponse);
|
||||
// mockSubscribers(this);
|
||||
this.loadFixtures('settings');
|
||||
mockSettings(this);
|
||||
mockThemes(this);
|
||||
|
||||
// keep this line, it allows all other API requests to hit the real server
|
||||
this.passthrough();
|
||||
@ -40,6 +44,7 @@ export function testConfig() {
|
||||
mockSlugs(this);
|
||||
mockSubscribers(this);
|
||||
mockTags(this);
|
||||
mockThemes(this);
|
||||
mockUsers(this);
|
||||
|
||||
/* Notifications -------------------------------------------------------- */
|
||||
|
@ -18,11 +18,28 @@ export default function mockSettings(server) {
|
||||
});
|
||||
|
||||
server.put('/settings/', function (db, request) {
|
||||
console.log('/settings/', request.requestBody);
|
||||
let newSettings = JSON.parse(request.requestBody).settings;
|
||||
|
||||
db.settings.remove();
|
||||
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 {
|
||||
meta: {},
|
||||
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',
|
||||
id: 18,
|
||||
value: [
|
||||
{
|
||||
name: 'casper',
|
||||
@ -222,6 +223,16 @@ export default [
|
||||
version: '1.0'
|
||||
},
|
||||
active: true
|
||||
},
|
||||
{
|
||||
name: 'foo',
|
||||
package: {
|
||||
name: 'Foo',
|
||||
version: '0.1'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'bar'
|
||||
}
|
||||
],
|
||||
type: 'theme'
|
||||
|
@ -44,7 +44,9 @@ GhostRouter.map(function () {
|
||||
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('tag', {path: ':tag_slug'});
|
||||
this.route('new');
|
||||
|
@ -11,6 +11,11 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
||||
|
||||
config: injectService(),
|
||||
|
||||
// TODO: replace with a synchronous settings service
|
||||
querySettings() {
|
||||
return this.store.queryRecord('setting', {type: 'blog,theme,private'});
|
||||
},
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
return this.get('session.user')
|
||||
@ -20,7 +25,7 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
||||
|
||||
model() {
|
||||
return RSVP.hash({
|
||||
settings: this.store.queryRecord('setting', {type: 'blog,theme,private'}),
|
||||
settings: this.querySettings(),
|
||||
availableTimezones: this.get('config.availableTimezones')
|
||||
});
|
||||
},
|
||||
@ -32,7 +37,17 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
||||
|
||||
actions: {
|
||||
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;
|
||||
min-height: 130px;
|
||||
width: 100%;
|
||||
height: 130px;
|
||||
background: #f6f7f8;
|
||||
border-radius: 4px;
|
||||
color: #808284;
|
||||
|
@ -161,3 +161,93 @@
|
||||
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>
|
@ -80,22 +80,6 @@
|
||||
</label>
|
||||
</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">
|
||||
{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="facebook"}}
|
||||
<label for="facebook">Facebook Page</label>
|
||||
@ -136,6 +120,35 @@
|
||||
{{/if}}
|
||||
</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>
|
||||
</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 destroyApp from '../../helpers/destroy-app';
|
||||
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 () {
|
||||
let application;
|
||||
@ -125,12 +127,6 @@ describe('Acceptance: Settings - General', function () {
|
||||
andThen(() => {
|
||||
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 () {
|
||||
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
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/');
|
||||
});
|
||||
});
|
||||
|
||||
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