mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-26 20:34:02 +03:00
parent
7b593e5df0
commit
679dc3c0d6
@ -4,9 +4,6 @@ import Controller from '@ember/controller';
|
|||||||
import NavigationItem from 'ghost-admin/models/navigation-item';
|
import NavigationItem from 'ghost-admin/models/navigation-item';
|
||||||
import RSVP from 'rsvp';
|
import RSVP from 'rsvp';
|
||||||
import {computed} from '@ember/object';
|
import {computed} from '@ember/object';
|
||||||
import {isEmpty} from '@ember/utils';
|
|
||||||
import {isThemeValidationError} from 'ghost-admin/services/ajax';
|
|
||||||
import {notEmpty} from '@ember/object/computed';
|
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
import {task} from 'ember-concurrency';
|
import {task} from 'ember-concurrency';
|
||||||
|
|
||||||
@ -20,8 +17,6 @@ export default Controller.extend({
|
|||||||
dirtyAttributes: false,
|
dirtyAttributes: false,
|
||||||
newNavItem: null,
|
newNavItem: null,
|
||||||
newSecondaryNavItem: null,
|
newSecondaryNavItem: null,
|
||||||
themes: null,
|
|
||||||
themeToDelete: null,
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
@ -29,8 +24,6 @@ export default Controller.extend({
|
|||||||
this.set('newSecondaryNavItem', NavigationItem.create({isNew: true, isSecondary: true}));
|
this.set('newSecondaryNavItem', NavigationItem.create({isNew: true, isSecondary: true}));
|
||||||
},
|
},
|
||||||
|
|
||||||
showDeleteThemeModal: notEmpty('themeToDelete'),
|
|
||||||
|
|
||||||
blogUrl: computed('config.blogUrl', function () {
|
blogUrl: computed('config.blogUrl', function () {
|
||||||
let url = this.get('config.blogUrl');
|
let url = this.get('config.blogUrl');
|
||||||
|
|
||||||
@ -128,76 +121,6 @@ export default Controller.extend({
|
|||||||
return transition.retry();
|
return transition.retry();
|
||||||
},
|
},
|
||||||
|
|
||||||
activateTheme(theme) {
|
|
||||||
return theme.activate().then((activatedTheme) => {
|
|
||||||
if (!isEmpty(activatedTheme.get('warnings'))) {
|
|
||||||
this.set('themeWarnings', activatedTheme.get('warnings'));
|
|
||||||
this.set('showThemeWarningsModal', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isEmpty(activatedTheme.get('errors'))) {
|
|
||||||
this.set('themeErrors', activatedTheme.get('errors'));
|
|
||||||
this.set('showThemeWarningsModal', true);
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
|
||||||
if (isThemeValidationError(error)) {
|
|
||||||
let errors = error.payload.errors[0].details.errors;
|
|
||||||
let fatalErrors = [];
|
|
||||||
let normalErrors = [];
|
|
||||||
|
|
||||||
// to have a proper grouping of fatal errors and none fatal, we need to check
|
|
||||||
// our errors for the fatal property
|
|
||||||
if (errors.length > 0) {
|
|
||||||
for (let i = 0; i < errors.length; i += 1) {
|
|
||||||
if (errors[i].fatal) {
|
|
||||||
fatalErrors.push(errors[i]);
|
|
||||||
} else {
|
|
||||||
normalErrors.push(errors[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set('themeErrors', normalErrors);
|
|
||||||
this.set('themeFatalErrors', fatalErrors);
|
|
||||||
this.set('showThemeErrorsModal', true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
downloadTheme(theme) {
|
|
||||||
let downloadURL = `${this.get('ghostPaths.apiRoot')}/themes/${theme.name}/download/`;
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
|
|
||||||
hideThemeWarningsModal() {
|
|
||||||
this.set('themeWarnings', null);
|
|
||||||
this.set('themeErrors', null);
|
|
||||||
this.set('themeFatalErrors', null);
|
|
||||||
this.set('showThemeWarningsModal', false);
|
|
||||||
this.set('showThemeErrorsModal', false);
|
|
||||||
},
|
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.set('newNavItem', NavigationItem.create({isNew: true}));
|
this.set('newNavItem', NavigationItem.create({isNew: true}));
|
||||||
this.set('newSecondaryNavItem', NavigationItem.create({isNew: true, isSecondary: true}));
|
this.set('newSecondaryNavItem', NavigationItem.create({isNew: true, isSecondary: true}));
|
||||||
@ -253,22 +176,5 @@ export default Controller.extend({
|
|||||||
this.set('newNavItem', NavigationItem.create({isNew: true}));
|
this.set('newNavItem', NavigationItem.create({isNew: true}));
|
||||||
$('.gh-blognav-container:first .gh-blognav-line:last input:first').focus();
|
$('.gh-blognav-container:first .gh-blognav-line:last input:first').focus();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
_deleteTheme() {
|
|
||||||
let theme = this.store.peekRecord('theme', this.themeToDelete.name);
|
|
||||||
|
|
||||||
if (!theme) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return theme.destroyRecord().then(() => {
|
|
||||||
// HACK: this is a private method, we need to unload from the store
|
|
||||||
// here so that uploading another theme with the same "id" doesn't
|
|
||||||
// attempt to update the deleted record
|
|
||||||
theme.unloadRecord();
|
|
||||||
}).catch((error) => {
|
|
||||||
this.notifications.showAPIError(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
125
ghost/admin/app/controllers/settings/theme.js
Normal file
125
ghost/admin/app/controllers/settings/theme.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||||
|
import $ from 'jquery';
|
||||||
|
import Controller from '@ember/controller';
|
||||||
|
import {computed} from '@ember/object';
|
||||||
|
import {isEmpty} from '@ember/utils';
|
||||||
|
import {isThemeValidationError} from 'ghost-admin/services/ajax';
|
||||||
|
import {notEmpty} from '@ember/object/computed';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
|
export default Controller.extend({
|
||||||
|
config: service(),
|
||||||
|
ghostPaths: service(),
|
||||||
|
notifications: service(),
|
||||||
|
session: service(),
|
||||||
|
settings: service(),
|
||||||
|
|
||||||
|
dirtyAttributes: false,
|
||||||
|
newNavItem: null,
|
||||||
|
newSecondaryNavItem: null,
|
||||||
|
themes: null,
|
||||||
|
themeToDelete: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
showDeleteThemeModal: notEmpty('themeToDelete'),
|
||||||
|
|
||||||
|
blogUrl: computed('config.blogUrl', function () {
|
||||||
|
let url = this.get('config.blogUrl');
|
||||||
|
|
||||||
|
return url.slice(-1) !== '/' ? `${url}/` : url;
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
activateTheme(theme) {
|
||||||
|
return theme.activate().then((activatedTheme) => {
|
||||||
|
if (!isEmpty(activatedTheme.get('warnings'))) {
|
||||||
|
this.set('themeWarnings', activatedTheme.get('warnings'));
|
||||||
|
this.set('showThemeWarningsModal', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmpty(activatedTheme.get('errors'))) {
|
||||||
|
this.set('themeErrors', activatedTheme.get('errors'));
|
||||||
|
this.set('showThemeWarningsModal', true);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
if (isThemeValidationError(error)) {
|
||||||
|
let errors = error.payload.errors[0].details.errors;
|
||||||
|
let fatalErrors = [];
|
||||||
|
let normalErrors = [];
|
||||||
|
|
||||||
|
// to have a proper grouping of fatal errors and none fatal, we need to check
|
||||||
|
// our errors for the fatal property
|
||||||
|
if (errors.length > 0) {
|
||||||
|
for (let i = 0; i < errors.length; i += 1) {
|
||||||
|
if (errors[i].fatal) {
|
||||||
|
fatalErrors.push(errors[i]);
|
||||||
|
} else {
|
||||||
|
normalErrors.push(errors[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('themeErrors', normalErrors);
|
||||||
|
this.set('themeFatalErrors', fatalErrors);
|
||||||
|
this.set('showThemeErrorsModal', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadTheme(theme) {
|
||||||
|
let downloadURL = `${this.get('ghostPaths.apiRoot')}/themes/${theme.name}/download/`;
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
hideThemeWarningsModal() {
|
||||||
|
this.set('themeWarnings', null);
|
||||||
|
this.set('themeErrors', null);
|
||||||
|
this.set('themeFatalErrors', null);
|
||||||
|
this.set('showThemeWarningsModal', false);
|
||||||
|
this.set('showThemeErrorsModal', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {}
|
||||||
|
},
|
||||||
|
|
||||||
|
_deleteTheme() {
|
||||||
|
let theme = this.store.peekRecord('theme', this.themeToDelete.name);
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return theme.destroyRecord().then(() => {
|
||||||
|
// HACK: this is a private method, we need to unload from the store
|
||||||
|
// here so that uploading another theme with the same "id" doesn't
|
||||||
|
// attempt to update the deleted record
|
||||||
|
theme.unloadRecord();
|
||||||
|
}).catch((error) => {
|
||||||
|
this.notifications.showAPIError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -61,6 +61,9 @@ Router.map(function () {
|
|||||||
this.route('settings.design', {path: '/settings/design'}, function () {
|
this.route('settings.design', {path: '/settings/design'}, function () {
|
||||||
this.route('uploadtheme');
|
this.route('uploadtheme');
|
||||||
});
|
});
|
||||||
|
this.route('settings.theme', {path: '/settings/theme'}, function () {
|
||||||
|
this.route('uploadtheme');
|
||||||
|
});
|
||||||
this.route('settings.integrations', {path: '/settings/integrations'}, function () {
|
this.route('settings.integrations', {path: '/settings/integrations'}, function () {
|
||||||
this.route('new');
|
this.route('new');
|
||||||
});
|
});
|
||||||
|
@ -15,13 +15,11 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
|||||||
|
|
||||||
model() {
|
model() {
|
||||||
return RSVP.hash({
|
return RSVP.hash({
|
||||||
settings: this.settings.reload(),
|
settings: this.settings.reload()
|
||||||
themes: this.store.findAll('theme')
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController(controller) {
|
setupController() {
|
||||||
controller.set('themes', this.store.peekAll('theme'));
|
|
||||||
this.controller.send('reset');
|
this.controller.send('reset');
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -49,10 +47,6 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
|||||||
controller.send('toggleLeaveSettingsModal', transition);
|
controller.send('toggleLeaveSettingsModal', transition);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
activateTheme(theme) {
|
|
||||||
return this.controller.send('activateTheme', theme);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
44
ghost/admin/app/routes/settings/theme.js
Normal file
44
ghost/admin/app/routes/settings/theme.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||||
|
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||||
|
import RSVP from 'rsvp';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
|
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||||
|
settings: service(),
|
||||||
|
|
||||||
|
beforeModel() {
|
||||||
|
this._super(...arguments);
|
||||||
|
return this.get('session.user')
|
||||||
|
.then(this.transitionAuthor());
|
||||||
|
},
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return RSVP.hash({
|
||||||
|
settings: this.settings.reload(),
|
||||||
|
themes: this.store.findAll('theme')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller) {
|
||||||
|
controller.set('themes', this.store.peekAll('theme'));
|
||||||
|
this.controller.send('reset');
|
||||||
|
},
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.controller.set('leaveSettingsTransition', null);
|
||||||
|
this.controller.set('showLeaveSettingsModal', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
activateTheme(theme) {
|
||||||
|
return this.controller.send('activateTheme', theme);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
buildRouteInfoMetadata() {
|
||||||
|
return {
|
||||||
|
titleToken: 'Settings - Theme'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
18
ghost/admin/app/routes/settings/theme/uploadtheme.js
Normal file
18
ghost/admin/app/routes/settings/theme/uploadtheme.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||||
|
|
||||||
|
export default AuthenticatedRoute.extend({
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return this.store.findAll('theme');
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
controller.set('themes', model);
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
cancel() {
|
||||||
|
this.transitionTo('settings.theme');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -11,21 +11,37 @@
|
|||||||
.gh-settings-main-grid .gh-setting-group {
|
.gh-settings-main-grid .gh-setting-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--darkgrey);
|
color: var(--darkgrey);
|
||||||
border-top: 1px solid var(--whitegrey);
|
border-bottom: 1px solid var(--whitegrey);
|
||||||
border-left: 1px solid var(--whitegrey);
|
border-right: 1px solid var(--whitegrey);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-settings-main-grid .gh-setting-group:hover {
|
.gh-settings-main-grid a.gh-setting-group:hover {
|
||||||
background: var(--whitegrey-l2);
|
background: var(--whitegrey-l2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-settings-main-grid .gh-setting-group:nth-child(3n-2) {
|
.gh-settings-main-grid .gh-setting-group:nth-child(3n) {
|
||||||
border-left: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-settings-main-grid .gh-setting-group:nth-child(-n+3) {
|
.gh-settings-main-grid .gh-setting-group:nth-last-child(-n+3) {
|
||||||
border-top: none;
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-settings-main-grid .gh-setting-group:first-child {
|
||||||
|
border-top-left-radius: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-settings-main-grid .gh-setting-group:nth-child(3) {
|
||||||
|
border-top-right-radius: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-settings-main-grid .gh-setting-group:nth-last-child(3) {
|
||||||
|
border-bottom-left-radius: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-settings-main-grid .gh-setting-group:last-child {
|
||||||
|
border-bottom-right-radius: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-settings-main-grid .gh-setting-group svg {
|
.gh-settings-main-grid .gh-setting-group svg {
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
</GhCanvasHeader>
|
</GhCanvasHeader>
|
||||||
|
|
||||||
<section class="view-container">
|
<section class="view-container">
|
||||||
{{!-- <div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2"> --}}
|
|
||||||
<div class="gh-settings-main-grid">
|
<div class="gh-settings-main-grid">
|
||||||
<LinkTo class="gh-setting-group" @route="settings.general" data-test-nav="settings">
|
<LinkTo class="gh-setting-group" @route="settings.general" data-test-nav="settings">
|
||||||
{{svg-jar "page"}}
|
{{svg-jar "page"}}
|
||||||
@ -15,6 +14,13 @@
|
|||||||
<p>Update basic publication details, and generic site metadata</p>
|
<p>Update basic publication details, and generic site metadata</p>
|
||||||
</div>
|
</div>
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
|
<LinkTo class="gh-setting-group" @route="settings.theme" data-test-nav="theme">
|
||||||
|
{{svg-jar "paintbrush"}}
|
||||||
|
<div>
|
||||||
|
<h4>Theme</h4>
|
||||||
|
<p>Install and update site theme</p>
|
||||||
|
</div>
|
||||||
|
</LinkTo>
|
||||||
<LinkTo class="gh-setting-group" @route="settings.design" data-test-nav="design">
|
<LinkTo class="gh-setting-group" @route="settings.design" data-test-nav="design">
|
||||||
{{svg-jar "paintbrush"}}
|
{{svg-jar "paintbrush"}}
|
||||||
<div>
|
<div>
|
||||||
@ -43,13 +49,15 @@
|
|||||||
<p>Add code to the header or footer of your publication</p>
|
<p>Add code to the header or footer of your publication</p>
|
||||||
</div>
|
</div>
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
<LinkTo class="gh-setting-group"o @route="settings.labs" data-test-nav="labs">
|
<LinkTo class="gh-setting-group" @route="settings.labs" data-test-nav="labs">
|
||||||
{{svg-jar "labs"}}
|
{{svg-jar "labs"}}
|
||||||
<div>
|
<div>
|
||||||
<h4>Labs</h4>
|
<h4>Labs</h4>
|
||||||
<p>Testing ground for new or experimental features</p>
|
<p>Testing ground for new or experimental features</p>
|
||||||
</div>
|
</div>
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
|
<div class="gh-setting-group"></div>
|
||||||
|
<div class="gh-setting-group"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
@ -71,145 +71,8 @@
|
|||||||
data-test-navitem="new" />
|
data-test-navitem="new" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gh-setting-header">Theme Directory</div>
|
|
||||||
<div class="gh-theme-directory-container">
|
|
||||||
<div class="theme-directory">
|
|
||||||
<a class="td-item" href="https://github.com/TryGhost/Massively" target="_blank" rel="noopener noreferrer">
|
|
||||||
<div class="td-item-screenshot">
|
|
||||||
<img style="object-fit:contain;" src="assets/img/themes/massively.jpg" alt="Massively Theme" />
|
|
||||||
</div>
|
|
||||||
<div class="td-item-desc">
|
|
||||||
<div>Massively <span>— Free</span></div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="td-item" href="https://themeforest.net/item/nurui-multipurpose-ghost-blog-theme/22243886" target="_blank" rel="noopener noreferrer">
|
|
||||||
<div class="td-item-screenshot">
|
|
||||||
<img style="object-fit:contain;" src="assets/img/themes/nurui.jpg" alt="Nurui Theme" />
|
|
||||||
</div>
|
|
||||||
<div class="td-item-desc">
|
|
||||||
<div>Nurui <span>— Premium</span></div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="td-item" href="https://themeforest.net/item/pacific-big-bold-photographydriven-theme/19774541" target="_blank" rel="noopener noreferrer">
|
|
||||||
<div class="td-item-screenshot">
|
|
||||||
<img style="object-fit:contain;" src="assets/img/themes/pacific.jpg" alt="Pacific Theme" />
|
|
||||||
</div>
|
|
||||||
<div class="td-item-desc">
|
|
||||||
<div>Pacific <span>— Premium</span></div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="td-item" href="https://www.hauntedthemes.com/" target="_blank" rel="noopener noreferrer">
|
|
||||||
<div class="td-item-screenshot">
|
|
||||||
<img style="object-fit:contain;" src="assets/img/themes/farafra.jpg" alt="Farafra Theme" />
|
|
||||||
</div>
|
|
||||||
<div class="td-item-desc">
|
|
||||||
<div>Farafra <span>— Premium</span></div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="td-item" href="https://themeforest.net/item/valkyrie-a-highly-visual-ghost-blog/22576630" target="_blank" rel="noopener noreferrer">
|
|
||||||
<div class="td-item-screenshot">
|
|
||||||
<img style="object-fit:contain;" src="assets/img/themes/valkyrie.jpg" alt="Valkyrie Theme" />
|
|
||||||
</div>
|
|
||||||
<div class="td-item-desc">
|
|
||||||
<div>Valkyrie <span>— Premium</span></div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="td-item" href="https://themeforest.net/item/sente-magazine-ghost-blog-theme/21019644" target="_blank" rel="noopener noreferrer">
|
|
||||||
<div class="td-item-screenshot">
|
|
||||||
<img style="object-fit:contain;" src="assets/img/themes/sente.jpg" alt="Sente Theme" />
|
|
||||||
</div>
|
|
||||||
<div class="td-item-desc">
|
|
||||||
<div>Sente <span>— Premium</span></div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="td-cta">
|
|
||||||
<a class="td-cta-box td-cta-marketplace" href="https://ghost.org/marketplace/" target="_blank" rel="noopener">
|
|
||||||
<div class="td-cta-icon">{{svg-jar "store"}}</div>
|
|
||||||
<div class="td-cta-content-wrapper">
|
|
||||||
<div class="td-cta-content">
|
|
||||||
<h4 class="fw6 f6">Theme Marketplace</h4>
|
|
||||||
<p>Explore a huge range of free and premium themes for Ghost with a range of design and layout options</p>
|
|
||||||
</div>
|
|
||||||
<div class="td-cta-arrow">
|
|
||||||
{{svg-jar "arrow-right"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="td-cta-box td-cta-docs" href="https://ghost.org/docs/themes/" target="_blank" rel="noopener">
|
|
||||||
<div class="td-cta-icon">{{svg-jar "book-open"}}</div>
|
|
||||||
<div class="td-cta-content-wrapper">
|
|
||||||
<div class="td-cta-content">
|
|
||||||
<h4 class="fw6 f6">Developer Docs</h4>
|
|
||||||
<p>Build your own custom Ghost theme from scratch using our comprehensive Handlebars.js SDK</p>
|
|
||||||
</div>
|
|
||||||
<div class="td-cta-arrow">
|
|
||||||
{{svg-jar "arrow-right"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gh-setting-header">Installed Themes</div>
|
|
||||||
<div class="gh-themes-container">
|
|
||||||
|
|
||||||
<GhThemeTable
|
|
||||||
@themes={{this.themes}}
|
|
||||||
@activateTheme={{action "activateTheme"}}
|
|
||||||
@downloadTheme={{action "downloadTheme"}}
|
|
||||||
@deleteTheme={{action "deleteTheme"}} />
|
|
||||||
|
|
||||||
<LinkTo @route="settings.design.uploadtheme" class="gh-btn gh-btn-green gh-themes-uploadbtn" data-test-upload-theme-button={{true}}>
|
|
||||||
<span>Upload a theme</span>
|
|
||||||
</LinkTo>
|
|
||||||
|
|
||||||
|
|
||||||
{{#if this.showDeleteThemeModal}}
|
|
||||||
<GhFullscreenModal @modal="delete-theme"
|
|
||||||
@model={{hash
|
|
||||||
theme=this.themeToDelete
|
|
||||||
download=(action "downloadTheme" this.themeToDelete)
|
|
||||||
}}
|
|
||||||
@close={{action "hideDeleteThemeModal"}}
|
|
||||||
@confirm={{action "deleteTheme"}}
|
|
||||||
@modifier="action wide" />
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.showThemeWarningsModal}}
|
|
||||||
<GhFullscreenModal @modal="theme-warnings"
|
|
||||||
@model={{hash
|
|
||||||
title="Activation successful"
|
|
||||||
warnings=this.themeWarnings
|
|
||||||
errors=this.themeErrors
|
|
||||||
canActivate=true
|
|
||||||
}}
|
|
||||||
@close={{action "hideThemeWarningsModal"}}
|
|
||||||
@modifier="action wide" />
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.showThemeErrorsModal}}
|
|
||||||
<GhFullscreenModal @modal="theme-warnings"
|
|
||||||
@model={{hash
|
|
||||||
title="Activation failed"
|
|
||||||
errors=this.themeErrors
|
|
||||||
fatalErrors=this.themeFatalErrors
|
|
||||||
canActivate=false
|
|
||||||
}}
|
|
||||||
@close={{action "hideThemeWarningsModal"}}
|
|
||||||
@modifier="action wide" />
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
|
|
||||||
<GhTourItem @throbberId="upload-a-theme"
|
|
||||||
@target=".gh-themes-uploadbtn"
|
|
||||||
@throbberAttachment="top middle"
|
|
||||||
@popoverTriangleClass="bottom"
|
|
||||||
/>
|
|
||||||
|
158
ghost/admin/app/templates/settings/theme.hbs
Normal file
158
ghost/admin/app/templates/settings/theme.hbs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<section class="gh-canvas">
|
||||||
|
<GhCanvasHeader class="gh-canvas-header">
|
||||||
|
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||||
|
<LinkTo @route="settings">Settings</LinkTo>
|
||||||
|
<span>{{svg-jar "arrow-right"}}</span>
|
||||||
|
Theme
|
||||||
|
</h2>
|
||||||
|
</GhCanvasHeader>
|
||||||
|
|
||||||
|
{{#if this.showLeaveSettingsModal}}
|
||||||
|
<GhFullscreenModal @modal="leave-settings"
|
||||||
|
@confirm={{action "leaveSettings"}}
|
||||||
|
@close={{action "toggleLeaveSettingsModal"}}
|
||||||
|
@modifier="action wide" />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<section class="view-container">
|
||||||
|
<div class="gh-setting-header gh-first-header">Theme Directory</div>
|
||||||
|
<div class="gh-theme-directory-container">
|
||||||
|
<div class="theme-directory">
|
||||||
|
<a class="td-item" href="https://github.com/TryGhost/Massively" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="td-item-screenshot">
|
||||||
|
<img style="object-fit:contain;" src="assets/img/themes/massively.jpg" alt="Massively Theme" />
|
||||||
|
</div>
|
||||||
|
<div class="td-item-desc">
|
||||||
|
<div>Massively <span>— Free</span></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="td-item" href="https://themeforest.net/item/nurui-multipurpose-ghost-blog-theme/22243886" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="td-item-screenshot">
|
||||||
|
<img style="object-fit:contain;" src="assets/img/themes/nurui.jpg" alt="Nurui Theme" />
|
||||||
|
</div>
|
||||||
|
<div class="td-item-desc">
|
||||||
|
<div>Nurui <span>— Premium</span></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="td-item" href="https://themeforest.net/item/pacific-big-bold-photographydriven-theme/19774541" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="td-item-screenshot">
|
||||||
|
<img style="object-fit:contain;" src="assets/img/themes/pacific.jpg" alt="Pacific Theme" />
|
||||||
|
</div>
|
||||||
|
<div class="td-item-desc">
|
||||||
|
<div>Pacific <span>— Premium</span></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="td-item" href="https://www.hauntedthemes.com/" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="td-item-screenshot">
|
||||||
|
<img style="object-fit:contain;" src="assets/img/themes/farafra.jpg" alt="Farafra Theme" />
|
||||||
|
</div>
|
||||||
|
<div class="td-item-desc">
|
||||||
|
<div>Farafra <span>— Premium</span></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="td-item" href="https://themeforest.net/item/valkyrie-a-highly-visual-ghost-blog/22576630" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="td-item-screenshot">
|
||||||
|
<img style="object-fit:contain;" src="assets/img/themes/valkyrie.jpg" alt="Valkyrie Theme" />
|
||||||
|
</div>
|
||||||
|
<div class="td-item-desc">
|
||||||
|
<div>Valkyrie <span>— Premium</span></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="td-item" href="https://themeforest.net/item/sente-magazine-ghost-blog-theme/21019644" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="td-item-screenshot">
|
||||||
|
<img style="object-fit:contain;" src="assets/img/themes/sente.jpg" alt="Sente Theme" />
|
||||||
|
</div>
|
||||||
|
<div class="td-item-desc">
|
||||||
|
<div>Sente <span>— Premium</span></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="td-cta">
|
||||||
|
<a class="td-cta-box td-cta-marketplace" href="https://ghost.org/marketplace/" target="_blank" rel="noopener">
|
||||||
|
<div class="td-cta-icon">{{svg-jar "store"}}</div>
|
||||||
|
<div class="td-cta-content-wrapper">
|
||||||
|
<div class="td-cta-content">
|
||||||
|
<h4 class="fw6 f6">Theme Marketplace</h4>
|
||||||
|
<p>Explore a huge range of free and premium themes for Ghost with a range of design and layout options</p>
|
||||||
|
</div>
|
||||||
|
<div class="td-cta-arrow">
|
||||||
|
{{svg-jar "arrow-right"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="td-cta-box td-cta-docs" href="https://ghost.org/docs/themes/" target="_blank" rel="noopener">
|
||||||
|
<div class="td-cta-icon">{{svg-jar "book-open"}}</div>
|
||||||
|
<div class="td-cta-content-wrapper">
|
||||||
|
<div class="td-cta-content">
|
||||||
|
<h4 class="fw6 f6">Developer Docs</h4>
|
||||||
|
<p>Build your own custom Ghost theme from scratch using our comprehensive Handlebars.js SDK</p>
|
||||||
|
</div>
|
||||||
|
<div class="td-cta-arrow">
|
||||||
|
{{svg-jar "arrow-right"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gh-setting-header">Installed Themes</div>
|
||||||
|
<div class="gh-themes-container">
|
||||||
|
|
||||||
|
<GhThemeTable
|
||||||
|
@themes={{this.themes}}
|
||||||
|
@activateTheme={{action "activateTheme"}}
|
||||||
|
@downloadTheme={{action "downloadTheme"}}
|
||||||
|
@deleteTheme={{action "deleteTheme"}} />
|
||||||
|
|
||||||
|
<LinkTo @route="settings.theme.uploadtheme" class="gh-btn gh-btn-green gh-themes-uploadbtn" data-test-upload-theme-button={{true}}>
|
||||||
|
<span>Upload a theme</span>
|
||||||
|
</LinkTo>
|
||||||
|
|
||||||
|
|
||||||
|
{{#if this.showDeleteThemeModal}}
|
||||||
|
<GhFullscreenModal @modal="delete-theme"
|
||||||
|
@model={{hash
|
||||||
|
theme=this.themeToDelete
|
||||||
|
download=(action "downloadTheme" this.themeToDelete)
|
||||||
|
}}
|
||||||
|
@close={{action "hideDeleteThemeModal"}}
|
||||||
|
@confirm={{action "deleteTheme"}}
|
||||||
|
@modifier="action wide" />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.showThemeWarningsModal}}
|
||||||
|
<GhFullscreenModal @modal="theme-warnings"
|
||||||
|
@model={{hash
|
||||||
|
title="Activation successful"
|
||||||
|
warnings=this.themeWarnings
|
||||||
|
errors=this.themeErrors
|
||||||
|
canActivate=true
|
||||||
|
}}
|
||||||
|
@close={{action "hideThemeWarningsModal"}}
|
||||||
|
@modifier="action wide" />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.showThemeErrorsModal}}
|
||||||
|
<GhFullscreenModal @modal="theme-warnings"
|
||||||
|
@model={{hash
|
||||||
|
title="Activation failed"
|
||||||
|
errors=this.themeErrors
|
||||||
|
fatalErrors=this.themeFatalErrors
|
||||||
|
canActivate=false
|
||||||
|
}}
|
||||||
|
@close={{action "hideThemeWarningsModal"}}
|
||||||
|
@modifier="action wide" />
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{outlet}}
|
||||||
|
|
||||||
|
<GhTourItem @throbberId="upload-a-theme"
|
||||||
|
@target=".gh-themes-uploadbtn"
|
||||||
|
@throbberAttachment="top middle"
|
||||||
|
@popoverTriangleClass="bottom"
|
||||||
|
/>
|
7
ghost/admin/app/templates/settings/theme/uploadtheme.hbs
Normal file
7
ghost/admin/app/templates/settings/theme/uploadtheme.hbs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<GhFullscreenModal @modal="upload-theme"
|
||||||
|
@model={{hash
|
||||||
|
themes=themes
|
||||||
|
activate=(route-action 'activateTheme')
|
||||||
|
}}
|
||||||
|
@close={{route-action "cancel"}}
|
||||||
|
@modifier="action wide" />
|
@ -108,7 +108,7 @@ describe('Acceptance: Error Handling', function () {
|
|||||||
it('handles ember-ajax HTML response', async function () {
|
it('handles ember-ajax HTML response', async function () {
|
||||||
this.server.del('/themes/foo/', htmlErrorResponse);
|
this.server.del('/themes/foo/', htmlErrorResponse);
|
||||||
|
|
||||||
await visit('/settings/design');
|
await visit('/settings/theme');
|
||||||
await click('[data-test-theme-id="foo"] [data-test-theme-delete-button]');
|
await click('[data-test-theme-id="foo"] [data-test-theme-delete-button]');
|
||||||
await click('.fullscreen-modal [data-test-delete-button]');
|
await click('.fullscreen-modal [data-test-delete-button]');
|
||||||
|
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import Mirage from 'ember-cli-mirage';
|
|
||||||
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
|
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
|
||||||
import mockThemes from 'ghost-admin/mirage/config/themes';
|
|
||||||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||||
import {beforeEach, describe, it} from 'mocha';
|
import {beforeEach, describe, it} from 'mocha';
|
||||||
import {blur, click, currentRouteName, currentURL, fillIn, find, findAll, triggerEvent, typeIn} from '@ember/test-helpers';
|
import {blur, click, currentRouteName, currentURL, fillIn, find, findAll, triggerEvent, typeIn} from '@ember/test-helpers';
|
||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import {fileUpload} from '../../helpers/file-upload';
|
|
||||||
import {setupApplicationTest} from 'ember-mocha';
|
import {setupApplicationTest} from 'ember-mocha';
|
||||||
import {setupMirage} from 'ember-cli-mirage/test-support';
|
import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||||
import {visit} from '../../helpers/visit';
|
import {visit} from '../../helpers/visit';
|
||||||
@ -278,504 +275,5 @@ describe('Acceptance: Settings - Design', function () {
|
|||||||
|
|
||||||
expect(navSetting.value).to.equal('[]');
|
expect(navSetting.value).to.equal('[]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows management of themes', async 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
|
|
||||||
|
|
||||||
this.server.loadFixtures('themes');
|
|
||||||
await visit('/settings/design');
|
|
||||||
|
|
||||||
// lists available themes (themes are specified in mirage/fixtures/settings)
|
|
||||||
expect(
|
|
||||||
findAll('[data-test-theme-id]').length,
|
|
||||||
'shows correct number of themes'
|
|
||||||
).to.equal(3);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('[data-test-theme-active="true"] [data-test-theme-title]').textContent.trim(),
|
|
||||||
'Blog theme marked as active'
|
|
||||||
).to.equal('Blog (default)');
|
|
||||||
|
|
||||||
// theme upload displays modal
|
|
||||||
await click('[data-test-upload-theme-button]');
|
|
||||||
expect(
|
|
||||||
findAll('[data-test-modal="upload-theme"]').length,
|
|
||||||
'theme upload modal displayed after button click'
|
|
||||||
).to.equal(1);
|
|
||||||
|
|
||||||
// cancelling theme upload closes modal
|
|
||||||
await click('.fullscreen-modal [data-test-close-button]');
|
|
||||||
expect(
|
|
||||||
findAll('.fullscreen-modal').length === 0,
|
|
||||||
'upload theme modal is closed when cancelling'
|
|
||||||
).to.be.true;
|
|
||||||
|
|
||||||
// theme upload validates mime type
|
|
||||||
await click('[data-test-upload-theme-button]');
|
|
||||||
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {type: 'text/csv'});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.fullscreen-modal .failed').textContent,
|
|
||||||
'validation error is shown for invalid mime type'
|
|
||||||
).to.match(/is not supported/);
|
|
||||||
|
|
||||||
// theme upload validates casper.zip
|
|
||||||
await click('[data-test-upload-try-again-button]');
|
|
||||||
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'casper.zip', type: 'application/zip'});
|
|
||||||
expect(
|
|
||||||
find('.fullscreen-modal .failed').textContent,
|
|
||||||
'validation error is shown when uploading casper.zip'
|
|
||||||
).to.match(/default Casper theme cannot be overwritten/);
|
|
||||||
|
|
||||||
// theme upload handles upload errors
|
|
||||||
this.server.post('/themes/upload/', function () {
|
|
||||||
return new Mirage.Response(422, {}, {
|
|
||||||
errors: [{
|
|
||||||
message: 'Invalid theme'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await click('[data-test-upload-try-again-button]');
|
|
||||||
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'error.zip', type: 'application/zip'});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.fullscreen-modal .failed').textContent.trim(),
|
|
||||||
'validation error is passed through from server'
|
|
||||||
).to.equal('Invalid theme');
|
|
||||||
|
|
||||||
// reset to default mirage handlers
|
|
||||||
mockThemes(this.server);
|
|
||||||
|
|
||||||
// theme upload handles validation errors
|
|
||||||
this.server.post('/themes/upload/', function () {
|
|
||||||
return new Mirage.Response(422, {}, {
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message: 'Theme is not compatible or contains errors.',
|
|
||||||
type: 'ThemeValidationError',
|
|
||||||
details: {
|
|
||||||
errors: [{
|
|
||||||
level: 'error',
|
|
||||||
rule: 'Assets such as CSS & JS must use the <code>{{asset}}</code> helper',
|
|
||||||
details: '<p>The listed files should be included using the <code>{{asset}}</code> helper.</p>',
|
|
||||||
failures: [
|
|
||||||
{
|
|
||||||
ref: '/assets/javascripts/ui.js'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, {
|
|
||||||
level: 'error',
|
|
||||||
rule: 'Templates must contain valid Handlebars.',
|
|
||||||
failures: [
|
|
||||||
{
|
|
||||||
ref: 'index.hbs',
|
|
||||||
message: 'The partial index_meta could not be found'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ref: 'tag.hbs',
|
|
||||||
message: 'The partial index_meta could not be found'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await click('[data-test-upload-try-again-button]');
|
|
||||||
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'bad-theme.zip', type: 'application/zip'});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.fullscreen-modal h1').textContent.trim(),
|
|
||||||
'modal title after uploading invalid theme'
|
|
||||||
).to.equal('Invalid theme');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
findAll('.theme-validation-rule-text')[1].textContent,
|
|
||||||
'top-level errors are displayed'
|
|
||||||
).to.match(/Templates must contain valid Handlebars/);
|
|
||||||
|
|
||||||
await click('[data-test-toggle-details]');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.theme-validation-details').textContent,
|
|
||||||
'top-level errors do not escape HTML'
|
|
||||||
).to.match(/The listed files should be included using the {{asset}} helper/);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.theme-validation-list ul li').textContent,
|
|
||||||
'individual failures are displayed'
|
|
||||||
).to.match(/\/assets\/javascripts\/ui\.js/);
|
|
||||||
|
|
||||||
// reset to default mirage handlers
|
|
||||||
mockThemes(this.server);
|
|
||||||
|
|
||||||
await click('.fullscreen-modal [data-test-try-again-button]');
|
|
||||||
expect(
|
|
||||||
findAll('.theme-validation-errors').length,
|
|
||||||
'"Try Again" resets form after theme validation error'
|
|
||||||
).to.equal(0);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
findAll('.gh-image-uploader').length,
|
|
||||||
'"Try Again" resets form after theme validation error'
|
|
||||||
).to.equal(1);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.fullscreen-modal h1').textContent.trim(),
|
|
||||||
'"Try Again" resets form after theme validation error'
|
|
||||||
).to.equal('Upload a theme');
|
|
||||||
|
|
||||||
// theme upload handles validation warnings
|
|
||||||
this.server.post('/themes/upload/', function ({themes}) {
|
|
||||||
let theme = {
|
|
||||||
name: 'blackpalm',
|
|
||||||
package: {
|
|
||||||
name: 'BlackPalm',
|
|
||||||
version: '1.0.0'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
themes.create(theme);
|
|
||||||
|
|
||||||
theme.warnings = [{
|
|
||||||
level: 'warning',
|
|
||||||
rule: 'Assets such as CSS & JS must use the <code>{{asset}}</code> helper',
|
|
||||||
details: '<p>The listed files should be included using the <code>{{asset}}</code> helper. For more information, please see the <a href="https://ghost.org/docs/themes/helpers/asset/">asset helper documentation</a>.</p>',
|
|
||||||
failures: [
|
|
||||||
{
|
|
||||||
ref: '/assets/dist/img/apple-touch-icon.png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ref: '/assets/dist/img/favicon.ico'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ref: '/assets/dist/css/blackpalm.min.css'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ref: '/assets/dist/js/blackpalm.min.js'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
code: 'GS030-ASSET-REQ'
|
|
||||||
}];
|
|
||||||
|
|
||||||
return new Mirage.Response(200, {}, {
|
|
||||||
themes: [theme]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'warning-theme.zip', type: 'application/zip'});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.fullscreen-modal h1').textContent.trim(),
|
|
||||||
'modal title after uploading theme with warnings'
|
|
||||||
).to.equal('Upload successful with warnings');
|
|
||||||
|
|
||||||
await click('[data-test-toggle-details]');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.theme-validation-details').textContent,
|
|
||||||
'top-level warnings are displayed'
|
|
||||||
).to.match(/The listed files should be included using the {{asset}} helper/);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.theme-validation-list ul li').textContent,
|
|
||||||
'individual warning failures are displayed'
|
|
||||||
).to.match(/\/assets\/dist\/img\/apple-touch-icon\.png/);
|
|
||||||
|
|
||||||
// reset to default mirage handlers
|
|
||||||
mockThemes(this.server);
|
|
||||||
|
|
||||||
await click('.fullscreen-modal [data-test-close-button]');
|
|
||||||
|
|
||||||
// theme upload handles success then close
|
|
||||||
await click('[data-test-upload-theme-button]');
|
|
||||||
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'theme-1.zip', type: 'application/zip'});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.fullscreen-modal h1').textContent.trim(),
|
|
||||||
'modal header after successful upload'
|
|
||||||
).to.equal('Upload successful!');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.modal-body').textContent,
|
|
||||||
'modal displays theme name after successful upload'
|
|
||||||
).to.match(/"Test 1 - 0\.1" uploaded successfully/);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
findAll('[data-test-theme-id]').length,
|
|
||||||
'number of themes in list grows after upload'
|
|
||||||
).to.equal(5);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('[data-test-theme-active="true"] [data-test-theme-title]').textContent.trim(),
|
|
||||||
'newly uploaded theme is not active'
|
|
||||||
).to.equal('Blog (default)');
|
|
||||||
|
|
||||||
await click('.fullscreen-modal [data-test-close-button]');
|
|
||||||
|
|
||||||
// theme upload handles success then activate
|
|
||||||
await click('[data-test-upload-theme-button]');
|
|
||||||
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'theme-2.zip', type: 'application/zip'});
|
|
||||||
await click('.fullscreen-modal [data-test-activate-now-button]');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
findAll('[data-test-theme-id]').length,
|
|
||||||
'number of themes in list grows after upload and activate'
|
|
||||||
).to.equal(6);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('[data-test-theme-active="true"] [data-test-theme-title]').textContent.trim(),
|
|
||||||
'newly uploaded+activated theme is active'
|
|
||||||
).to.equal('Test 2');
|
|
||||||
|
|
||||||
// theme activation switches active theme
|
|
||||||
await click('[data-test-theme-id="casper"] [data-test-theme-activate-button]');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('[data-test-theme-id="test-2"] .apps-card-app').classList.contains('theme-list-item--active'),
|
|
||||||
'previously active theme is not active'
|
|
||||||
).to.be.false;
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('[data-test-theme-id="casper"] .apps-card-app').classList.contains('theme-list-item--active'),
|
|
||||||
'activated theme is active'
|
|
||||||
).to.be.true;
|
|
||||||
|
|
||||||
// theme activation shows errors
|
|
||||||
this.server.put('themes/:theme/activate', function () {
|
|
||||||
return new Mirage.Response(422, {}, {
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message: 'Theme is not compatible or contains errors.',
|
|
||||||
type: 'ThemeValidationError',
|
|
||||||
details: {
|
|
||||||
checkedVersion: '2.x',
|
|
||||||
name: 'casper',
|
|
||||||
version: '2.9.7',
|
|
||||||
errors: [{
|
|
||||||
level: 'error',
|
|
||||||
rule: 'Assets such as CSS & JS must use the <code>{{asset}}</code> helper',
|
|
||||||
details: '<p>The listed files should be included using the <code>{{asset}}</code> helper.</p>',
|
|
||||||
failures: [
|
|
||||||
{
|
|
||||||
ref: '/assets/javascripts/ui.js'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, {
|
|
||||||
level: 'error',
|
|
||||||
fatal: true,
|
|
||||||
rule: 'Templates must contain valid Handlebars.',
|
|
||||||
failures: [
|
|
||||||
{
|
|
||||||
ref: 'index.hbs',
|
|
||||||
message: 'The partial index_meta could not be found'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ref: 'tag.hbs',
|
|
||||||
message: 'The partial index_meta could not be found'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await click('[data-test-theme-id="test-2"] [data-test-theme-activate-button]');
|
|
||||||
|
|
||||||
expect(find('[data-test-theme-warnings-modal]')).to.exist;
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('[data-test-theme-warnings-title]').textContent.trim(),
|
|
||||||
'modal title after activating invalid theme'
|
|
||||||
).to.equal('Activation failed');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('[data-test-theme-fatal-errors]').textContent,
|
|
||||||
'top-level errors are displayed in activation errors'
|
|
||||||
).to.match(/Templates must contain valid Handlebars/);
|
|
||||||
|
|
||||||
await click('[data-test-theme-errors] [data-test-toggle-details]');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('[data-test-theme-errors] .theme-validation-details').textContent,
|
|
||||||
'top-level errors do not escape HTML in activation errors'
|
|
||||||
).to.match(/The listed files should be included using the {{asset}} helper/);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.theme-validation-list ul li').textContent,
|
|
||||||
'individual failures are displayed in activation errors'
|
|
||||||
).to.match(/\/assets\/javascripts\/ui\.js/);
|
|
||||||
|
|
||||||
// restore default mirage handlers
|
|
||||||
mockThemes(this.server);
|
|
||||||
|
|
||||||
await click('[data-test-modal-close-button]');
|
|
||||||
expect(find('[data-test-theme-warnings-modal]')).to.not.exist;
|
|
||||||
|
|
||||||
// theme activation shows warnings
|
|
||||||
this.server.put('themes/:theme/activate', function ({themes}, {params}) {
|
|
||||||
themes.all().update('active', false);
|
|
||||||
let theme = themes.findBy({name: params.theme}).update({active: true});
|
|
||||||
|
|
||||||
theme.update({warnings: [{
|
|
||||||
level: 'warning',
|
|
||||||
rule: 'Assets such as CSS & JS must use the <code>{{asset}}</code> helper',
|
|
||||||
details: '<p>The listed files should be included using the <code>{{asset}}</code> helper. For more information, please see the <a href="https://ghost.org/docs/themes/helpers/asset/">asset helper documentation</a>.</p>',
|
|
||||||
failures: [
|
|
||||||
{
|
|
||||||
ref: '/assets/dist/img/apple-touch-icon.png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ref: '/assets/dist/img/favicon.ico'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ref: '/assets/dist/css/blackpalm.min.css'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ref: '/assets/dist/js/blackpalm.min.js'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
code: 'GS030-ASSET-REQ'
|
|
||||||
}]});
|
|
||||||
|
|
||||||
return {themes: [theme]};
|
|
||||||
});
|
|
||||||
|
|
||||||
await click('[data-test-theme-id="test-2"] [data-test-theme-activate-button]');
|
|
||||||
|
|
||||||
expect(find('[data-test-theme-warnings-modal]')).to.exist;
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('[data-test-theme-warnings-title]').textContent.trim(),
|
|
||||||
'modal title after activating theme with warnings'
|
|
||||||
).to.equal('Activation successful with warnings');
|
|
||||||
|
|
||||||
await click('[data-test-toggle-details]');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.theme-validation-details').textContent,
|
|
||||||
'top-level warnings are displayed in activation warnings'
|
|
||||||
).to.match(/The listed files should be included using the {{asset}} helper/);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.theme-validation-list ul li').textContent,
|
|
||||||
'individual warning failures are displayed in activation warnings'
|
|
||||||
).to.match(/\/assets\/dist\/img\/apple-touch-icon\.png/);
|
|
||||||
|
|
||||||
// restore default mirage handlers
|
|
||||||
mockThemes(this.server);
|
|
||||||
|
|
||||||
await click('[data-test-modal-close-button]');
|
|
||||||
// reactivate casper to continue tests
|
|
||||||
await click('[data-test-theme-id="casper"] [data-test-theme-activate-button]');
|
|
||||||
|
|
||||||
// theme deletion displays modal
|
|
||||||
await click('[data-test-theme-id="test-1"] [data-test-theme-delete-button]');
|
|
||||||
expect(
|
|
||||||
findAll('[data-test-delete-theme-modal]').length,
|
|
||||||
'theme deletion modal displayed after button click'
|
|
||||||
).to.equal(1);
|
|
||||||
|
|
||||||
// cancelling theme deletion closes modal
|
|
||||||
await click('.fullscreen-modal [data-test-cancel-button]');
|
|
||||||
expect(
|
|
||||||
findAll('.fullscreen-modal').length === 0,
|
|
||||||
'delete theme modal is closed when cancelling'
|
|
||||||
).to.be.true;
|
|
||||||
|
|
||||||
// confirming theme deletion closes modal and refreshes list
|
|
||||||
await click('[data-test-theme-id="test-1"] [data-test-theme-delete-button]');
|
|
||||||
await click('.fullscreen-modal [data-test-delete-button]');
|
|
||||||
expect(
|
|
||||||
findAll('.fullscreen-modal').length === 0,
|
|
||||||
'delete theme modal closes after deletion'
|
|
||||||
).to.be.true;
|
|
||||||
|
|
||||||
expect(
|
|
||||||
findAll('[data-test-theme-id]').length,
|
|
||||||
'number of themes in list shrinks after delete'
|
|
||||||
).to.equal(5);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('[data-test-theme-title]').textContent,
|
|
||||||
'correct theme is removed from theme list after deletion'
|
|
||||||
).to.not.match(/Test 1/);
|
|
||||||
|
|
||||||
// validation errors are handled when deleting a theme
|
|
||||||
this.server.del('/themes/:theme/', function () {
|
|
||||||
return new Mirage.Response(422, {}, {
|
|
||||||
errors: [{
|
|
||||||
message: 'Can\'t delete theme'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await click('[data-test-theme-id="test-2"] [data-test-theme-delete-button]');
|
|
||||||
await click('.fullscreen-modal [data-test-delete-button]');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
findAll('.fullscreen-modal').length === 0,
|
|
||||||
'delete theme modal closes after failed deletion'
|
|
||||||
).to.be.true;
|
|
||||||
|
|
||||||
expect(
|
|
||||||
findAll('.gh-alert').length,
|
|
||||||
'alert is shown when deletion fails'
|
|
||||||
).to.equal(1);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find('.gh-alert').textContent,
|
|
||||||
'failed deletion alert has correct text'
|
|
||||||
).to.match(/Can't delete theme/);
|
|
||||||
|
|
||||||
// restore default mirage handlers
|
|
||||||
mockThemes(this.server);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can delete then re-upload the same theme', async function () {
|
|
||||||
this.server.loadFixtures('themes');
|
|
||||||
|
|
||||||
// mock theme upload to emulate uploading theme with same id
|
|
||||||
this.server.post('/themes/upload/', function ({themes}) {
|
|
||||||
let theme = themes.create({
|
|
||||||
name: 'foo',
|
|
||||||
package: {
|
|
||||||
name: 'Foo',
|
|
||||||
version: '0.1'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {themes: [theme]};
|
|
||||||
});
|
|
||||||
|
|
||||||
await visit('/settings/design');
|
|
||||||
await click('[data-test-theme-id="foo"] [data-test-theme-delete-button]');
|
|
||||||
await click('.fullscreen-modal [data-test-delete-button]');
|
|
||||||
|
|
||||||
await click('[data-test-upload-theme-button]');
|
|
||||||
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'foo.zip', type: 'application/zip'});
|
|
||||||
// this will fail if upload failed because there won't be an activate now button
|
|
||||||
await click('.fullscreen-modal [data-test-activate-now-button]');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
556
ghost/admin/tests/acceptance/settings/theme-test.js
Normal file
556
ghost/admin/tests/acceptance/settings/theme-test.js
Normal file
@ -0,0 +1,556 @@
|
|||||||
|
import Mirage from 'ember-cli-mirage';
|
||||||
|
import mockThemes from 'ghost-admin/mirage/config/themes';
|
||||||
|
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||||
|
import {beforeEach, describe, it} from 'mocha';
|
||||||
|
import {click, currentRouteName, currentURL, find, findAll} from '@ember/test-helpers';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
import {fileUpload} from '../../helpers/file-upload';
|
||||||
|
import {setupApplicationTest} from 'ember-mocha';
|
||||||
|
import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||||
|
import {visit} from '../../helpers/visit';
|
||||||
|
|
||||||
|
describe('Acceptance: Settings - Design', function () {
|
||||||
|
let hooks = setupApplicationTest();
|
||||||
|
setupMirage(hooks);
|
||||||
|
|
||||||
|
it('redirects to signin when not authenticated', async function () {
|
||||||
|
await invalidateSession();
|
||||||
|
await visit('/settings/theme');
|
||||||
|
|
||||||
|
expect(currentURL(), 'currentURL').to.equal('/signin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to staff page when authenticated as contributor', async function () {
|
||||||
|
let role = this.server.create('role', {name: 'Contributor'});
|
||||||
|
this.server.create('user', {roles: [role], slug: 'test-user'});
|
||||||
|
|
||||||
|
await authenticateSession();
|
||||||
|
await visit('/settings/theme');
|
||||||
|
|
||||||
|
expect(currentURL(), 'currentURL').to.equal('/staff/test-user');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to staff page when authenticated as author', async function () {
|
||||||
|
let role = this.server.create('role', {name: 'Author'});
|
||||||
|
this.server.create('user', {roles: [role], slug: 'test-user'});
|
||||||
|
|
||||||
|
await authenticateSession();
|
||||||
|
await visit('/settings/theme');
|
||||||
|
|
||||||
|
expect(currentURL(), 'currentURL').to.equal('/staff/test-user');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when logged in', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
let role = this.server.create('role', {name: 'Administrator'});
|
||||||
|
this.server.create('user', {roles: [role]});
|
||||||
|
|
||||||
|
await authenticateSession();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can visit /settings/theme', async function () {
|
||||||
|
await visit('/settings/theme');
|
||||||
|
|
||||||
|
expect(currentRouteName()).to.equal('settings.theme.index');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows management of themes', async 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
|
||||||
|
|
||||||
|
this.server.loadFixtures('themes');
|
||||||
|
await visit('/settings/theme');
|
||||||
|
|
||||||
|
// lists available themes (themes are specified in mirage/fixtures/settings)
|
||||||
|
expect(
|
||||||
|
findAll('[data-test-theme-id]').length,
|
||||||
|
'shows correct number of themes'
|
||||||
|
).to.equal(3);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-theme-active="true"] [data-test-theme-title]').textContent.trim(),
|
||||||
|
'Blog theme marked as active'
|
||||||
|
).to.equal('Blog (default)');
|
||||||
|
|
||||||
|
// theme upload displays modal
|
||||||
|
await click('[data-test-upload-theme-button]');
|
||||||
|
expect(
|
||||||
|
findAll('[data-test-modal="upload-theme"]').length,
|
||||||
|
'theme upload modal displayed after button click'
|
||||||
|
).to.equal(1);
|
||||||
|
|
||||||
|
// cancelling theme upload closes modal
|
||||||
|
await click('.fullscreen-modal [data-test-close-button]');
|
||||||
|
expect(
|
||||||
|
findAll('.fullscreen-modal').length === 0,
|
||||||
|
'upload theme modal is closed when cancelling'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
// theme upload validates mime type
|
||||||
|
await click('[data-test-upload-theme-button]');
|
||||||
|
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {type: 'text/csv'});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal .failed').textContent,
|
||||||
|
'validation error is shown for invalid mime type'
|
||||||
|
).to.match(/is not supported/);
|
||||||
|
|
||||||
|
// theme upload validates casper.zip
|
||||||
|
await click('[data-test-upload-try-again-button]');
|
||||||
|
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'casper.zip', type: 'application/zip'});
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal .failed').textContent,
|
||||||
|
'validation error is shown when uploading casper.zip'
|
||||||
|
).to.match(/default Casper theme cannot be overwritten/);
|
||||||
|
|
||||||
|
// theme upload handles upload errors
|
||||||
|
this.server.post('/themes/upload/', function () {
|
||||||
|
return new Mirage.Response(422, {}, {
|
||||||
|
errors: [{
|
||||||
|
message: 'Invalid theme'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await click('[data-test-upload-try-again-button]');
|
||||||
|
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'error.zip', type: 'application/zip'});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal .failed').textContent.trim(),
|
||||||
|
'validation error is passed through from server'
|
||||||
|
).to.equal('Invalid theme');
|
||||||
|
|
||||||
|
// reset to default mirage handlers
|
||||||
|
mockThemes(this.server);
|
||||||
|
|
||||||
|
// theme upload handles validation errors
|
||||||
|
this.server.post('/themes/upload/', function () {
|
||||||
|
return new Mirage.Response(422, {}, {
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'Theme is not compatible or contains errors.',
|
||||||
|
type: 'ThemeValidationError',
|
||||||
|
details: {
|
||||||
|
errors: [{
|
||||||
|
level: 'error',
|
||||||
|
rule: 'Assets such as CSS & JS must use the <code>{{asset}}</code> helper',
|
||||||
|
details: '<p>The listed files should be included using the <code>{{asset}}</code> helper.</p>',
|
||||||
|
failures: [
|
||||||
|
{
|
||||||
|
ref: '/assets/javascripts/ui.js'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
level: 'error',
|
||||||
|
rule: 'Templates must contain valid Handlebars.',
|
||||||
|
failures: [
|
||||||
|
{
|
||||||
|
ref: 'index.hbs',
|
||||||
|
message: 'The partial index_meta could not be found'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ref: 'tag.hbs',
|
||||||
|
message: 'The partial index_meta could not be found'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await click('[data-test-upload-try-again-button]');
|
||||||
|
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'bad-theme.zip', type: 'application/zip'});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal h1').textContent.trim(),
|
||||||
|
'modal title after uploading invalid theme'
|
||||||
|
).to.equal('Invalid theme');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
findAll('.theme-validation-rule-text')[1].textContent,
|
||||||
|
'top-level errors are displayed'
|
||||||
|
).to.match(/Templates must contain valid Handlebars/);
|
||||||
|
|
||||||
|
await click('[data-test-toggle-details]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-details').textContent,
|
||||||
|
'top-level errors do not escape HTML'
|
||||||
|
).to.match(/The listed files should be included using the {{asset}} helper/);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-list ul li').textContent,
|
||||||
|
'individual failures are displayed'
|
||||||
|
).to.match(/\/assets\/javascripts\/ui\.js/);
|
||||||
|
|
||||||
|
// reset to default mirage handlers
|
||||||
|
mockThemes(this.server);
|
||||||
|
|
||||||
|
await click('.fullscreen-modal [data-test-try-again-button]');
|
||||||
|
expect(
|
||||||
|
findAll('.theme-validation-errors').length,
|
||||||
|
'"Try Again" resets form after theme validation error'
|
||||||
|
).to.equal(0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
findAll('.gh-image-uploader').length,
|
||||||
|
'"Try Again" resets form after theme validation error'
|
||||||
|
).to.equal(1);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal h1').textContent.trim(),
|
||||||
|
'"Try Again" resets form after theme validation error'
|
||||||
|
).to.equal('Upload a theme');
|
||||||
|
|
||||||
|
// theme upload handles validation warnings
|
||||||
|
this.server.post('/themes/upload/', function ({themes}) {
|
||||||
|
let theme = {
|
||||||
|
name: 'blackpalm',
|
||||||
|
package: {
|
||||||
|
name: 'BlackPalm',
|
||||||
|
version: '1.0.0'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
themes.create(theme);
|
||||||
|
|
||||||
|
theme.warnings = [{
|
||||||
|
level: 'warning',
|
||||||
|
rule: 'Assets such as CSS & JS must use the <code>{{asset}}</code> helper',
|
||||||
|
details: '<p>The listed files should be included using the <code>{{asset}}</code> helper. For more information, please see the <a href="https://ghost.org/docs/themes/helpers/asset/">asset helper documentation</a>.</p>',
|
||||||
|
failures: [
|
||||||
|
{
|
||||||
|
ref: '/assets/dist/img/apple-touch-icon.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ref: '/assets/dist/img/favicon.ico'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ref: '/assets/dist/css/blackpalm.min.css'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ref: '/assets/dist/js/blackpalm.min.js'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
code: 'GS030-ASSET-REQ'
|
||||||
|
}];
|
||||||
|
|
||||||
|
return new Mirage.Response(200, {}, {
|
||||||
|
themes: [theme]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'warning-theme.zip', type: 'application/zip'});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal h1').textContent.trim(),
|
||||||
|
'modal title after uploading theme with warnings'
|
||||||
|
).to.equal('Upload successful with warnings');
|
||||||
|
|
||||||
|
await click('[data-test-toggle-details]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-details').textContent,
|
||||||
|
'top-level warnings are displayed'
|
||||||
|
).to.match(/The listed files should be included using the {{asset}} helper/);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-list ul li').textContent,
|
||||||
|
'individual warning failures are displayed'
|
||||||
|
).to.match(/\/assets\/dist\/img\/apple-touch-icon\.png/);
|
||||||
|
|
||||||
|
// reset to default mirage handlers
|
||||||
|
mockThemes(this.server);
|
||||||
|
|
||||||
|
await click('.fullscreen-modal [data-test-close-button]');
|
||||||
|
|
||||||
|
// theme upload handles success then close
|
||||||
|
await click('[data-test-upload-theme-button]');
|
||||||
|
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'theme-1.zip', type: 'application/zip'});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal h1').textContent.trim(),
|
||||||
|
'modal header after successful upload'
|
||||||
|
).to.equal('Upload successful!');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.modal-body').textContent,
|
||||||
|
'modal displays theme name after successful upload'
|
||||||
|
).to.match(/"Test 1 - 0\.1" uploaded successfully/);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
findAll('[data-test-theme-id]').length,
|
||||||
|
'number of themes in list grows after upload'
|
||||||
|
).to.equal(5);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-theme-active="true"] [data-test-theme-title]').textContent.trim(),
|
||||||
|
'newly uploaded theme is not active'
|
||||||
|
).to.equal('Blog (default)');
|
||||||
|
|
||||||
|
await click('.fullscreen-modal [data-test-close-button]');
|
||||||
|
|
||||||
|
// theme upload handles success then activate
|
||||||
|
await click('[data-test-upload-theme-button]');
|
||||||
|
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'theme-2.zip', type: 'application/zip'});
|
||||||
|
await click('.fullscreen-modal [data-test-activate-now-button]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
findAll('[data-test-theme-id]').length,
|
||||||
|
'number of themes in list grows after upload and activate'
|
||||||
|
).to.equal(6);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-theme-active="true"] [data-test-theme-title]').textContent.trim(),
|
||||||
|
'newly uploaded+activated theme is active'
|
||||||
|
).to.equal('Test 2');
|
||||||
|
|
||||||
|
// theme activation switches active theme
|
||||||
|
await click('[data-test-theme-id="casper"] [data-test-theme-activate-button]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-theme-id="test-2"] .apps-card-app').classList.contains('theme-list-item--active'),
|
||||||
|
'previously active theme is not active'
|
||||||
|
).to.be.false;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-theme-id="casper"] .apps-card-app').classList.contains('theme-list-item--active'),
|
||||||
|
'activated theme is active'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
// theme activation shows errors
|
||||||
|
this.server.put('themes/:theme/activate', function () {
|
||||||
|
return new Mirage.Response(422, {}, {
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'Theme is not compatible or contains errors.',
|
||||||
|
type: 'ThemeValidationError',
|
||||||
|
details: {
|
||||||
|
checkedVersion: '2.x',
|
||||||
|
name: 'casper',
|
||||||
|
version: '2.9.7',
|
||||||
|
errors: [{
|
||||||
|
level: 'error',
|
||||||
|
rule: 'Assets such as CSS & JS must use the <code>{{asset}}</code> helper',
|
||||||
|
details: '<p>The listed files should be included using the <code>{{asset}}</code> helper.</p>',
|
||||||
|
failures: [
|
||||||
|
{
|
||||||
|
ref: '/assets/javascripts/ui.js'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
level: 'error',
|
||||||
|
fatal: true,
|
||||||
|
rule: 'Templates must contain valid Handlebars.',
|
||||||
|
failures: [
|
||||||
|
{
|
||||||
|
ref: 'index.hbs',
|
||||||
|
message: 'The partial index_meta could not be found'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ref: 'tag.hbs',
|
||||||
|
message: 'The partial index_meta could not be found'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await click('[data-test-theme-id="test-2"] [data-test-theme-activate-button]');
|
||||||
|
|
||||||
|
expect(find('[data-test-theme-warnings-modal]')).to.exist;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-theme-warnings-title]').textContent.trim(),
|
||||||
|
'modal title after activating invalid theme'
|
||||||
|
).to.equal('Activation failed');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-theme-fatal-errors]').textContent,
|
||||||
|
'top-level errors are displayed in activation errors'
|
||||||
|
).to.match(/Templates must contain valid Handlebars/);
|
||||||
|
|
||||||
|
await click('[data-test-theme-errors] [data-test-toggle-details]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-theme-errors] .theme-validation-details').textContent,
|
||||||
|
'top-level errors do not escape HTML in activation errors'
|
||||||
|
).to.match(/The listed files should be included using the {{asset}} helper/);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-list ul li').textContent,
|
||||||
|
'individual failures are displayed in activation errors'
|
||||||
|
).to.match(/\/assets\/javascripts\/ui\.js/);
|
||||||
|
|
||||||
|
// restore default mirage handlers
|
||||||
|
mockThemes(this.server);
|
||||||
|
|
||||||
|
await click('[data-test-modal-close-button]');
|
||||||
|
expect(find('[data-test-theme-warnings-modal]')).to.not.exist;
|
||||||
|
|
||||||
|
// theme activation shows warnings
|
||||||
|
this.server.put('themes/:theme/activate', function ({themes}, {params}) {
|
||||||
|
themes.all().update('active', false);
|
||||||
|
let theme = themes.findBy({name: params.theme}).update({active: true});
|
||||||
|
|
||||||
|
theme.update({warnings: [{
|
||||||
|
level: 'warning',
|
||||||
|
rule: 'Assets such as CSS & JS must use the <code>{{asset}}</code> helper',
|
||||||
|
details: '<p>The listed files should be included using the <code>{{asset}}</code> helper. For more information, please see the <a href="https://ghost.org/docs/themes/helpers/asset/">asset helper documentation</a>.</p>',
|
||||||
|
failures: [
|
||||||
|
{
|
||||||
|
ref: '/assets/dist/img/apple-touch-icon.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ref: '/assets/dist/img/favicon.ico'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ref: '/assets/dist/css/blackpalm.min.css'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ref: '/assets/dist/js/blackpalm.min.js'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
code: 'GS030-ASSET-REQ'
|
||||||
|
}]});
|
||||||
|
|
||||||
|
return {themes: [theme]};
|
||||||
|
});
|
||||||
|
|
||||||
|
await click('[data-test-theme-id="test-2"] [data-test-theme-activate-button]');
|
||||||
|
|
||||||
|
expect(find('[data-test-theme-warnings-modal]')).to.exist;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-theme-warnings-title]').textContent.trim(),
|
||||||
|
'modal title after activating theme with warnings'
|
||||||
|
).to.equal('Activation successful with warnings');
|
||||||
|
|
||||||
|
await click('[data-test-toggle-details]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-details').textContent,
|
||||||
|
'top-level warnings are displayed in activation warnings'
|
||||||
|
).to.match(/The listed files should be included using the {{asset}} helper/);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-list ul li').textContent,
|
||||||
|
'individual warning failures are displayed in activation warnings'
|
||||||
|
).to.match(/\/assets\/dist\/img\/apple-touch-icon\.png/);
|
||||||
|
|
||||||
|
// restore default mirage handlers
|
||||||
|
mockThemes(this.server);
|
||||||
|
|
||||||
|
await click('[data-test-modal-close-button]');
|
||||||
|
// reactivate casper to continue tests
|
||||||
|
await click('[data-test-theme-id="casper"] [data-test-theme-activate-button]');
|
||||||
|
|
||||||
|
// theme deletion displays modal
|
||||||
|
await click('[data-test-theme-id="test-1"] [data-test-theme-delete-button]');
|
||||||
|
expect(
|
||||||
|
findAll('[data-test-delete-theme-modal]').length,
|
||||||
|
'theme deletion modal displayed after button click'
|
||||||
|
).to.equal(1);
|
||||||
|
|
||||||
|
// cancelling theme deletion closes modal
|
||||||
|
await click('.fullscreen-modal [data-test-cancel-button]');
|
||||||
|
expect(
|
||||||
|
findAll('.fullscreen-modal').length === 0,
|
||||||
|
'delete theme modal is closed when cancelling'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
// confirming theme deletion closes modal and refreshes list
|
||||||
|
await click('[data-test-theme-id="test-1"] [data-test-theme-delete-button]');
|
||||||
|
await click('.fullscreen-modal [data-test-delete-button]');
|
||||||
|
expect(
|
||||||
|
findAll('.fullscreen-modal').length === 0,
|
||||||
|
'delete theme modal closes after deletion'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
findAll('[data-test-theme-id]').length,
|
||||||
|
'number of themes in list shrinks after delete'
|
||||||
|
).to.equal(5);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-theme-title]').textContent,
|
||||||
|
'correct theme is removed from theme list after deletion'
|
||||||
|
).to.not.match(/Test 1/);
|
||||||
|
|
||||||
|
// validation errors are handled when deleting a theme
|
||||||
|
this.server.del('/themes/:theme/', function () {
|
||||||
|
return new Mirage.Response(422, {}, {
|
||||||
|
errors: [{
|
||||||
|
message: 'Can\'t delete theme'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await click('[data-test-theme-id="test-2"] [data-test-theme-delete-button]');
|
||||||
|
await click('.fullscreen-modal [data-test-delete-button]');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
findAll('.fullscreen-modal').length === 0,
|
||||||
|
'delete theme modal closes after failed deletion'
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
findAll('.gh-alert').length,
|
||||||
|
'alert is shown when deletion fails'
|
||||||
|
).to.equal(1);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.gh-alert').textContent,
|
||||||
|
'failed deletion alert has correct text'
|
||||||
|
).to.match(/Can't delete theme/);
|
||||||
|
|
||||||
|
// restore default mirage handlers
|
||||||
|
mockThemes(this.server);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can delete then re-upload the same theme', async function () {
|
||||||
|
this.server.loadFixtures('themes');
|
||||||
|
|
||||||
|
// mock theme upload to emulate uploading theme with same id
|
||||||
|
this.server.post('/themes/upload/', function ({themes}) {
|
||||||
|
let theme = themes.create({
|
||||||
|
name: 'foo',
|
||||||
|
package: {
|
||||||
|
name: 'Foo',
|
||||||
|
version: '0.1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {themes: [theme]};
|
||||||
|
});
|
||||||
|
|
||||||
|
await visit('/settings/theme');
|
||||||
|
await click('[data-test-theme-id="foo"] [data-test-theme-delete-button]');
|
||||||
|
await click('.fullscreen-modal [data-test-delete-button]');
|
||||||
|
|
||||||
|
await click('[data-test-upload-theme-button]');
|
||||||
|
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'foo.zip', type: 'application/zip'});
|
||||||
|
// this will fail if upload failed because there won't be an activate now button
|
||||||
|
await click('.fullscreen-modal [data-test-activate-now-button]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user