mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 11:55:03 +03:00
parent
7b593e5df0
commit
679dc3c0d6
@ -4,9 +4,6 @@ import Controller from '@ember/controller';
|
||||
import NavigationItem from 'ghost-admin/models/navigation-item';
|
||||
import RSVP from 'rsvp';
|
||||
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 {task} from 'ember-concurrency';
|
||||
|
||||
@ -20,17 +17,13 @@ export default Controller.extend({
|
||||
dirtyAttributes: false,
|
||||
newNavItem: null,
|
||||
newSecondaryNavItem: null,
|
||||
themes: null,
|
||||
themeToDelete: null,
|
||||
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('newNavItem', NavigationItem.create({isNew: true}));
|
||||
this.set('newSecondaryNavItem', NavigationItem.create({isNew: true, isSecondary: true}));
|
||||
},
|
||||
|
||||
showDeleteThemeModal: notEmpty('themeToDelete'),
|
||||
|
||||
blogUrl: computed('config.blogUrl', function () {
|
||||
let url = this.get('config.blogUrl');
|
||||
|
||||
@ -128,76 +121,6 @@ export default Controller.extend({
|
||||
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() {
|
||||
this.set('newNavItem', NavigationItem.create({isNew: 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}));
|
||||
$('.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('uploadtheme');
|
||||
});
|
||||
this.route('settings.theme', {path: '/settings/theme'}, function () {
|
||||
this.route('uploadtheme');
|
||||
});
|
||||
this.route('settings.integrations', {path: '/settings/integrations'}, function () {
|
||||
this.route('new');
|
||||
});
|
||||
|
@ -15,13 +15,11 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
|
||||
model() {
|
||||
return RSVP.hash({
|
||||
settings: this.settings.reload(),
|
||||
themes: this.store.findAll('theme')
|
||||
settings: this.settings.reload()
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller) {
|
||||
controller.set('themes', this.store.peekAll('theme'));
|
||||
setupController() {
|
||||
this.controller.send('reset');
|
||||
},
|
||||
|
||||
@ -49,10 +47,6 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
controller.send('toggleLeaveSettingsModal', transition);
|
||||
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 {
|
||||
display: flex;
|
||||
color: var(--darkgrey);
|
||||
border-top: 1px solid var(--whitegrey);
|
||||
border-left: 1px solid var(--whitegrey);
|
||||
border-bottom: 1px solid var(--whitegrey);
|
||||
border-right: 1px solid var(--whitegrey);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.gh-settings-main-grid .gh-setting-group:hover {
|
||||
.gh-settings-main-grid a.gh-setting-group:hover {
|
||||
background: var(--whitegrey-l2);
|
||||
}
|
||||
|
||||
.gh-settings-main-grid .gh-setting-group:nth-child(3n-2) {
|
||||
border-left: none;
|
||||
.gh-settings-main-grid .gh-setting-group:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.gh-settings-main-grid .gh-setting-group:nth-child(-n+3) {
|
||||
border-top: none;
|
||||
.gh-settings-main-grid .gh-setting-group:nth-last-child(-n+3) {
|
||||
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 {
|
||||
|
@ -6,7 +6,6 @@
|
||||
</GhCanvasHeader>
|
||||
|
||||
<section class="view-container">
|
||||
{{!-- <div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2"> --}}
|
||||
<div class="gh-settings-main-grid">
|
||||
<LinkTo class="gh-setting-group" @route="settings.general" data-test-nav="settings">
|
||||
{{svg-jar "page"}}
|
||||
@ -15,6 +14,13 @@
|
||||
<p>Update basic publication details, and generic site metadata</p>
|
||||
</div>
|
||||
</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">
|
||||
{{svg-jar "paintbrush"}}
|
||||
<div>
|
||||
@ -43,13 +49,15 @@
|
||||
<p>Add code to the header or footer of your publication</p>
|
||||
</div>
|
||||
</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"}}
|
||||
<div>
|
||||
<h4>Labs</h4>
|
||||
<p>Testing ground for new or experimental features</p>
|
||||
</div>
|
||||
</LinkTo>
|
||||
<div class="gh-setting-group"></div>
|
||||
<div class="gh-setting-group"></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
@ -71,145 +71,8 @@
|
||||
data-test-navitem="new" />
|
||||
</form>
|
||||
</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>
|
||||
|
||||
{{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 () {
|
||||
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('.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 mockThemes from 'ghost-admin/mirage/config/themes';
|
||||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||
import {beforeEach, describe, it} from 'mocha';
|
||||
import {blur, click, currentRouteName, currentURL, fillIn, find, findAll, triggerEvent, typeIn} 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';
|
||||
@ -278,504 +275,5 @@ describe('Acceptance: Settings - Design', function () {
|
||||
|
||||
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