mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 14:03:48 +03:00
Cleaned up customThemeSettings
feature flag
refs https://github.com/TryGhost/Team/issues/1164 - removed flag and labs screen toggle - removed all conditionals - removed all old/unused route/controller/component files - renamed labs components and classes to non-labs naming
This commit is contained in:
parent
a0f5804aa8
commit
40a48c1e99
@ -1,32 +0,0 @@
|
||||
<div class="apps-grid" data-test-themes-list>
|
||||
{{#each this.sortedThemes as |theme index|}}
|
||||
<div class="apps-grid-cell" data-test-theme-id="{{theme.name}}" data-test-theme-active="{{theme.active}}">
|
||||
<div class="apps-card-app {{if theme.active "theme-list-item--active"}}">
|
||||
<div class="apps-card-meta flex-grow-1">
|
||||
<h3 class="apps-card-app-title" data-test-theme-title>
|
||||
{{theme.label}}
|
||||
{{#if theme.active}}<span class="gh-badge gh-badge-green">Active</span>{{/if}}
|
||||
</h3>
|
||||
<p class="apps-card-app-desc" data-test-theme-description><span class="description">Version {{theme.version}}</span></p>
|
||||
</div>
|
||||
{{#unless theme.active}}
|
||||
<button type="button" {{on "click" (fn this.activateTheme theme.model dd)}} class="apps-configured-action darkgrey apps-configured-action-activate green-hover green-bg-hover" data-test-button="activate">Activate</button>
|
||||
{{/unless}}
|
||||
<GhBasicDropdown @verticalPosition="below" @horizontalPosition="right" @buttonPosition="right" as |dd|>
|
||||
<dd.Trigger class="gh-btn gh-btn-icon" data-test-button="actions"><span>{{svg-jar "dotdotdot"}}</span></dd.Trigger>
|
||||
|
||||
<dd.Content class="relative-dropdown-menu">
|
||||
<ul class="dropdown-menu" data-test-actions-for={{theme.name}}>
|
||||
|
||||
<li><button type="button" {{on "click" (fn this.downloadTheme theme.name dd)}} class="darkgrey darkgrey-hover lightgrey-bg-hover" data-test-button="download">Download</button></li>
|
||||
|
||||
{{#if theme.isDeletable}}
|
||||
<li><button type="button" {{on "click" (fn this.deleteTheme theme.model dd)}} disabled={{theme.active}} class="gh-list-delete" data-test-button="delete">Delete</button></li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</dd.Content>
|
||||
</GhBasicDropdown>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
@ -1,99 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {get} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class GhThemeTableComponent extends Component {
|
||||
@service ghostPaths;
|
||||
@service modals;
|
||||
@service themeManagement;
|
||||
@service utils;
|
||||
|
||||
activateTaskInstance = null;
|
||||
confirmDeleteModal = null;
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this.confirmDeleteModal?.close?.();
|
||||
this.activateTaskInstance?.cancel();
|
||||
}
|
||||
|
||||
get sortedThemes() {
|
||||
let themes = this.args.themes.map((t) => {
|
||||
let theme = {};
|
||||
let themePackage = get(t, 'package');
|
||||
|
||||
theme.model = t;
|
||||
theme.name = get(t, 'name');
|
||||
theme.label = themePackage ? `${themePackage.name}` : theme.name;
|
||||
theme.version = themePackage ? `${themePackage.version}` : '1.0';
|
||||
theme.package = themePackage;
|
||||
theme.active = get(t, 'active');
|
||||
theme.isDeletable = !theme.active;
|
||||
|
||||
return theme;
|
||||
});
|
||||
let duplicateThemes = [];
|
||||
|
||||
themes.forEach((theme) => {
|
||||
let duplicateLabels = themes.filterBy('label', theme.label);
|
||||
|
||||
if (duplicateLabels.length > 1) {
|
||||
duplicateThemes.pushObject(theme);
|
||||
}
|
||||
});
|
||||
|
||||
duplicateThemes.forEach((theme) => {
|
||||
if (theme.name !== 'casper') {
|
||||
theme.label = `${theme.label} (${theme.name})`;
|
||||
}
|
||||
});
|
||||
|
||||
// "(default)" needs to be added to casper manually as it's always
|
||||
// displayed and would mess up the duplicate checking if added earlier
|
||||
let casper = themes.findBy('name', 'casper');
|
||||
if (casper) {
|
||||
casper.label = `${casper.label} (default)`;
|
||||
casper.isDefault = true;
|
||||
casper.isDeletable = false;
|
||||
}
|
||||
|
||||
// sorting manually because .sortBy('label') has a different sorting
|
||||
// algorithm to [...strings].sort()
|
||||
return themes.sort((themeA, themeB) => {
|
||||
let a = themeA.label.toLowerCase();
|
||||
let b = themeB.label.toLowerCase();
|
||||
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
downloadTheme(themeName, dropdown) {
|
||||
dropdown?.actions.close();
|
||||
this.utils.downloadFile(`${this.ghostPaths.apiRoot}/themes/${themeName}/download/`);
|
||||
}
|
||||
|
||||
@action
|
||||
activateTheme(theme, dropdown) {
|
||||
dropdown?.actions.close();
|
||||
this.activateTaskInstance = this.themeManagement.activateTask.perform(theme);
|
||||
}
|
||||
|
||||
@action
|
||||
deleteTheme(theme, dropdown) {
|
||||
dropdown?.actions.close();
|
||||
|
||||
this.confirmDeleteModal = this.modals.open('modals/design/confirm-delete-theme', {
|
||||
theme
|
||||
}).finally(() => {
|
||||
this.confirmDeleteModal = null;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,45 +1,32 @@
|
||||
<div class="apps-grid" data-test-themes-list>
|
||||
{{#if this.sortedThemes}}
|
||||
|
||||
{{#each this.sortedThemes as |theme|}}
|
||||
<div class="apps-grid-cell" data-test-theme-id="{{theme.name}}" data-test-theme-active="{{theme.active}}">
|
||||
<div class="apps-card-app {{if theme.active "theme-list-item--active"}}">
|
||||
<div class="apps-card-left">
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title" data-test-theme-title>{{theme.label}}</h3>
|
||||
{{#each this.sortedThemes as |theme index|}}
|
||||
<div class="apps-grid-cell" data-test-theme-id="{{theme.name}}" data-test-theme-active="{{theme.active}}">
|
||||
<div class="apps-card-app {{if theme.active "theme-list-item--active"}}">
|
||||
<div class="apps-card-meta flex-grow-1">
|
||||
<h3 class="apps-card-app-title" data-test-theme-title>
|
||||
{{theme.label}}
|
||||
{{#if theme.active}}<span class="gh-badge gh-badge-green">Active</span>{{/if}}
|
||||
</h3>
|
||||
<p class="apps-card-app-desc" data-test-theme-description><span class="description">Version {{theme.version}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="apps-card-right">
|
||||
<div class="apps-configured">
|
||||
{{!--Delete--}}
|
||||
{{#if theme.isDeletable}}
|
||||
<a href="#" {{action this.deleteTheme theme}} disabled={{theme.active}} class="apps-configured-action darkgrey red-hover red-bg-hover" data-test-theme-delete-button>Delete</a>
|
||||
{{/if}}
|
||||
{{!--Download--}}
|
||||
<a href="#" {{action this.downloadTheme theme}} class="apps-configured-action darkgrey darkgrey-hover lightgrey-bg-hover" data-test-theme-download-button>Download</a>
|
||||
{{!--Active Label / Activate Button--}}
|
||||
{{#if theme.active}}
|
||||
<span class="gh-badge gh-badge-black apps-configured-action" data-test-theme-badge>Active</span>
|
||||
{{else}}
|
||||
<a href="#" {{action this.activateTheme theme.model}} class="apps-configured-action darkgrey apps-configured-action-activate green-hover green-bg-hover" data-test-theme-activate-button>
|
||||
Activate
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{#unless theme.active}}
|
||||
<button type="button" {{on "click" (fn this.activateTheme theme.model dd)}} class="apps-configured-action darkgrey apps-configured-action-activate green-hover green-bg-hover" data-test-button="activate">Activate</button>
|
||||
{{/unless}}
|
||||
<GhBasicDropdown @verticalPosition="below" @horizontalPosition="right" @buttonPosition="right" as |dd|>
|
||||
<dd.Trigger class="gh-btn gh-btn-icon" data-test-button="actions"><span>{{svg-jar "dotdotdot"}}</span></dd.Trigger>
|
||||
|
||||
{{else}}
|
||||
<div class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">No themes found</h3>
|
||||
<p class="apps-card-app-desc"><span class="description">Please upload a theme to continue</span></p>
|
||||
<dd.Content class="relative-dropdown-menu">
|
||||
<ul class="dropdown-menu" data-test-actions-for={{theme.name}}>
|
||||
|
||||
<li><button type="button" {{on "click" (fn this.downloadTheme theme.name dd)}} class="darkgrey darkgrey-hover lightgrey-bg-hover" data-test-button="download">Download</button></li>
|
||||
|
||||
{{#if theme.isDeletable}}
|
||||
<li><button type="button" {{on "click" (fn this.deleteTheme theme.model dd)}} disabled={{theme.active}} class="gh-list-delete" data-test-button="delete">Delete</button></li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</dd.Content>
|
||||
</GhBasicDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
@ -1,13 +1,25 @@
|
||||
import Component from '@ember/component';
|
||||
import {computed} from '@ember/object';
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {get} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
export default class GhThemeTableComponent extends Component {
|
||||
@service ghostPaths;
|
||||
@service modals;
|
||||
@service themeManagement;
|
||||
@service utils;
|
||||
|
||||
themes: null,
|
||||
activateTaskInstance = null;
|
||||
confirmDeleteModal = null;
|
||||
|
||||
sortedThemes: computed('themes.@each.active', function () {
|
||||
let themes = this.themes.map((t) => {
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this.confirmDeleteModal?.close?.();
|
||||
this.activateTaskInstance?.cancel();
|
||||
}
|
||||
|
||||
get sortedThemes() {
|
||||
let themes = this.args.themes.map((t) => {
|
||||
let theme = {};
|
||||
let themePackage = get(t, 'package');
|
||||
|
||||
@ -60,6 +72,28 @@ export default Component.extend({
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}).readOnly()
|
||||
}
|
||||
|
||||
});
|
||||
@action
|
||||
downloadTheme(themeName, dropdown) {
|
||||
dropdown?.actions.close();
|
||||
this.utils.downloadFile(`${this.ghostPaths.apiRoot}/themes/${themeName}/download/`);
|
||||
}
|
||||
|
||||
@action
|
||||
activateTheme(theme, dropdown) {
|
||||
dropdown?.actions.close();
|
||||
this.activateTaskInstance = this.themeManagement.activateTask.perform(theme);
|
||||
}
|
||||
|
||||
@action
|
||||
deleteTheme(theme, dropdown) {
|
||||
dropdown?.actions.close();
|
||||
|
||||
this.confirmDeleteModal = this.modals.open('modals/design/confirm-delete-theme', {
|
||||
theme
|
||||
}).finally(() => {
|
||||
this.confirmDeleteModal = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
<header class="modal-header" data-test-delete-theme-modal>
|
||||
<h1>Are you sure you want to delete this</h1>
|
||||
</header>
|
||||
<a class="close" href="" role="button" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>You're about to delete "<strong>{{this.theme.label}}</strong>". This is permanent! We warned you, k? Maybe <a href="#" {{action this.download}}>download your theme before continuing</a></p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="gh-btn" data-test-cancel-button><span>Cancel</span></button>
|
||||
<GhTaskButton @buttonText="Delete" @successText="Deleted" @task={{this.deleteTheme}} @class="gh-btn gh-btn-red gh-btn-icon" data-test-delete-button="true" />
|
||||
</div>
|
@ -1,25 +0,0 @@
|
||||
import ModalComponent from 'ghost-admin/components/modal-base';
|
||||
import {alias} from '@ember/object/computed';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
// Allowed actions
|
||||
confirm: () => {},
|
||||
|
||||
theme: alias('model.theme'),
|
||||
download: alias('model.download'),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.deleteTheme.perform();
|
||||
}
|
||||
},
|
||||
|
||||
deleteTheme: task(function* () {
|
||||
try {
|
||||
yield this.confirm();
|
||||
} finally {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}).drop()
|
||||
});
|
@ -1,147 +0,0 @@
|
||||
<div class="theme-validation-container" {{did-update this.reset @model}}>
|
||||
<header class="modal-header" data-test-modal="install-theme">
|
||||
<h1>
|
||||
{{#if this.installSuccess}}
|
||||
{{#if this.hasWarningsOrErrors}}
|
||||
Install successful with {{#if this.validationErrors}}errors{{else}}warnings{{/if}}
|
||||
{{else}}
|
||||
Install successful!
|
||||
{{/if}}
|
||||
{{else if this.hasWarningsOrErrors}}
|
||||
Invalid theme
|
||||
{{else}}
|
||||
Install theme
|
||||
{{/if}}
|
||||
</h1>
|
||||
</header>
|
||||
<button type="button" class="close" title="Close" {{on "click" this.close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||
|
||||
<div class="modal-body">
|
||||
{{#if this.isReady}}
|
||||
<p>
|
||||
You're about to install <strong>{{this.themeName}}</strong> from the theme directory.
|
||||
</p>
|
||||
{{#if this.willOverwriteExisting}}
|
||||
<p>
|
||||
This will overwrite your existing version of {{this.themeName}}{{if this.willOverwriteExisting.active " which is your active theme"}}.
|
||||
Any custom changes will be lost.
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.marketplaceTheme.shortImage}}
|
||||
<div class="theme-validation-screenshot relative">
|
||||
<img style="object-fit:contain;" src={{this.marketplaceTheme.shortImage}} alt="Edition Theme" />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.willOverwriteDefault}}
|
||||
<p>
|
||||
Sorry, the default Casper theme cannot be overwritten.<br>
|
||||
If you wish to make changes please download the theme and upload a renamed zip file.
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.installSuccess}}
|
||||
{{#if this.hasWarningsOrErrors}}
|
||||
<p>
|
||||
The theme <strong>"{{this.themeName}}"</strong> was installed successfully but we detected some {{if this.validationErrors "errors" "warnings"}}.
|
||||
{{#unless this.theme.active}}
|
||||
You are still able to activate and use the theme but it is recommended to fix these {{if this.validationErrors "errors" "warnings"}} before you do so.
|
||||
{{/unless}}
|
||||
</p>
|
||||
{{else}}
|
||||
{{!-- Installed with no errors --}}
|
||||
<p>The theme <strong>"{{this.themeName}}"</strong> was installed successfully. {{unless this.theme.active "Do you want to activate it now?"}}</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.installError}}
|
||||
{{!-- Outright failure - not found, not a theme, server error, etc --}}
|
||||
<p>{{this.themeName}} failed to install.</p>
|
||||
<p class="error"><strong class="response">{{this.installError}}</strong></p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.installFailure}}
|
||||
{{!-- Invalid theme --}}
|
||||
<p>This theme is invalid and cannot be activated. Contact the theme developer.</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.fatalValidationErrors}}
|
||||
<div>
|
||||
<h2 class="mb0 mt4 f5 fw6">Fatal Errors</h2>
|
||||
<p class="mb2">Must-fix to activate theme</p>
|
||||
</div>
|
||||
|
||||
<ul class="pa0">
|
||||
{{#each this.fatalValidationErrors as |error|}}
|
||||
<li class="theme-validation-item theme-fatal-error">
|
||||
<GhThemeErrorLi @error={{error}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationErrors}}
|
||||
<div>
|
||||
<h2 class="mb0 mt4 f5 fw6">Errors</h2>
|
||||
<p class="mb2">Highly recommended to fix, functionality <strong>could</strong> be restricted</p>
|
||||
</div>
|
||||
<ul class="pa0">
|
||||
{{#each this.validationErrors as |error|}}
|
||||
<li class="theme-validation-item theme-error">
|
||||
<GhThemeErrorLi @error={{error}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationWarnings}}
|
||||
<div>
|
||||
<h2 class="mb0 mt4 f5 fw6">Warnings</h2>
|
||||
</div>
|
||||
<ul class="pa0">
|
||||
{{#each this.validationWarnings as |error|}}
|
||||
<li class="theme-validation-item theme-warning">
|
||||
<GhThemeErrorLi @error={{error}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="flex items-center justify-between {{if this.hasActionButton "flex-auto"}}">
|
||||
<button type="button" class="gh-btn" {{on "click" this.close}} data-test-button="cancel">
|
||||
<span>{{#if (or this.installSuccess this.installFailure)}}Close{{else}}Cancel{{/if}}</span>
|
||||
</button>
|
||||
|
||||
{{#if this.hasActionButton}}
|
||||
<div class="flex items-center">
|
||||
{{#if this.shouldShowInstall}}
|
||||
<GhTaskButton
|
||||
@task={{this.installTask}}
|
||||
@type="button"
|
||||
@class="gh-btn gh-btn-icon gh-btn-black"
|
||||
@buttonText={{if this.willOverwriteExisting "Overwrite" "Install"}}
|
||||
@runningText="Installing"
|
||||
@successText="Installed"
|
||||
data-test-button="install"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.shouldShowActivate}}
|
||||
<GhTaskButton
|
||||
@task={{this.activateTask}}
|
||||
@type="button"
|
||||
@class="gh-btn gh-btn-icon gh-btn-black"
|
||||
@buttonText="Activate"
|
||||
@runningText="Activating"
|
||||
data-test-button="activate"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,163 +0,0 @@
|
||||
import ModalBase from 'ghost-admin/components/modal-base';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import {action} from '@ember/object';
|
||||
import {isThemeValidationError} from 'ghost-admin/services/ajax';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency-decorators';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
import {MARKETPLACE_THEMES} from 'ghost-admin/controllers/settings/theme';
|
||||
|
||||
// TODO: update modals to work fully with Glimmer components
|
||||
@classic
|
||||
export default class ModalInstallThemeComponent extends ModalBase {
|
||||
@service ajax;
|
||||
@service ghostPaths;
|
||||
@service store;
|
||||
|
||||
@tracked model;
|
||||
@tracked theme;
|
||||
@tracked installError = '';
|
||||
@tracked validationWarnings = [];
|
||||
@tracked validationErrors = [];
|
||||
@tracked fatalValidationErrors = [];
|
||||
|
||||
get themeName() {
|
||||
return this.model.ref.split('/')[1];
|
||||
}
|
||||
|
||||
get marketplaceTheme() {
|
||||
return MARKETPLACE_THEMES.find(theme => theme.name.toLowerCase() === this.themeName.toLowerCase());
|
||||
}
|
||||
|
||||
get currentThemeNames() {
|
||||
return this.model.themes.mapBy('name');
|
||||
}
|
||||
|
||||
get willOverwriteDefault() {
|
||||
return this.themeName.toLowerCase() === 'casper';
|
||||
}
|
||||
|
||||
get willOverwriteExisting() {
|
||||
return this.model.themes.findBy('name', this.themeName.toLowerCase());
|
||||
}
|
||||
|
||||
get installSuccess() {
|
||||
return !!this.theme;
|
||||
}
|
||||
|
||||
get installFailure() {
|
||||
return !this.installSuccess && (this.validationErrors.length || this.fatalValidationErrors.length);
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
return !this.installSuccess && !this.installError && !this.installFailure && !this.willOverwriteDefault;
|
||||
}
|
||||
|
||||
get hasWarningsOrErrors() {
|
||||
return this.validationWarnings.length > 0 || this.validationErrors.length > 0;
|
||||
}
|
||||
|
||||
get shouldShowInstall() {
|
||||
return !this.installSuccess && !this.installFailure && !this.willOverwriteDefault;
|
||||
}
|
||||
|
||||
get shouldShowActivate() {
|
||||
return this.installSuccess && !this.theme.active;
|
||||
}
|
||||
|
||||
get hasActionButton() {
|
||||
return this.shouldShowInstall || this.shouldShowActivate;
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.theme = null;
|
||||
this.resetErrors();
|
||||
}
|
||||
|
||||
actions = {
|
||||
confirm() {
|
||||
// noop - needed to override ModalBase.actions.confirm
|
||||
},
|
||||
|
||||
// needed because ModalBase uses .send() for keyboard events
|
||||
closeModal() {
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*installTask() {
|
||||
try {
|
||||
const url = this.ghostPaths.url.api('themes/install') + `?source=github&ref=${this.model.ref}`;
|
||||
const result = yield this.ajax.post(url);
|
||||
|
||||
this.installError = '';
|
||||
|
||||
if (result.themes) {
|
||||
// show theme in list immediately
|
||||
this.store.pushPayload(result);
|
||||
|
||||
this.theme = this.store.peekRecord('theme', result.themes[0].name);
|
||||
|
||||
this.validationWarnings = this.theme.warnings || [];
|
||||
this.validationErrors = this.theme.errors || [];
|
||||
this.fatalValidationErrors = [];
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
if (isThemeValidationError(error)) {
|
||||
this.resetErrors();
|
||||
|
||||
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 && 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.fatalValidationErrors = fatalErrors;
|
||||
this.validationErrors = normalErrors;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (error.payload?.errors) {
|
||||
this.installError = error.payload.errors[0].message;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.installError = error.message;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*activateTask() {
|
||||
yield this.theme.activate();
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
resetErrors() {
|
||||
this.installError = '';
|
||||
this.validationWarnings = [];
|
||||
this.validationErrors = [];
|
||||
this.fatalValidationErrors = [];
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
<div class="theme-validation-container" data-test-theme-warnings-modal="true">
|
||||
<header class="modal-header">
|
||||
<h1 data-test-theme-warnings-title>
|
||||
{{#unless this.canActivate}}
|
||||
{{this.title}}
|
||||
{{else}}
|
||||
{{this.title}} with {{#if this.errors}}errors{{else}}warnings{{/if}}
|
||||
{{/unless}}
|
||||
</h1>
|
||||
</header>
|
||||
<a class="close" href="#" role="button" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
{{#if this.fatalErrors}}
|
||||
<div>
|
||||
<h2 class="mb0 mt4 f5 fw6 red">Fatal Errors</h2>
|
||||
<p class="mb2 red">Must-fix to activate theme</p>
|
||||
</div>
|
||||
<ul class="pa0" data-test-theme-fatal-errors>
|
||||
{{#each this.fatalErrors as |error|}}
|
||||
<li class="theme-validation-item theme-fatal-error">
|
||||
<GhThemeErrorLi @error={{error}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.errors}}
|
||||
<div>
|
||||
<h2 class="mb0 mt4 f5 fw6">Errors</h2>
|
||||
<p class="mb2">Highly recommended to fix, functionality <span>could</span> be restricted</p>
|
||||
</div>
|
||||
|
||||
<ul class="pa0" data-test-theme-errors>
|
||||
{{#each this.errors as |error|}}
|
||||
<li class="theme-validation-item theme-error">
|
||||
<GhThemeErrorLi @error={{error}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and this.warnings (or this.fatalErrors this.errors))}}
|
||||
<div>
|
||||
<h2 class="mb0 mt4 f5 fw6">Warnings</h2>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.warnings}}
|
||||
<ul class="pa0" data-test-theme-warnings>
|
||||
{{#each this.warnings as |error|}}
|
||||
<li class="theme-validation-item theme-warning">
|
||||
<GhThemeErrorLi @error={{error}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{action "closeModal"}} class="gh-btn" data-test-modal-close-button>
|
||||
<span>Ok</span>
|
||||
</button>
|
||||
</div>
|
@ -1,17 +0,0 @@
|
||||
import ModalComponent from 'ghost-admin/components/modal-base';
|
||||
import {reads} from '@ember/object/computed';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
title: reads('model.title'),
|
||||
message: reads('model.message'),
|
||||
warnings: reads('model.warnings'),
|
||||
errors: reads('model.errors'),
|
||||
fatalErrors: reads('model.fatalErrors'),
|
||||
canActivate: reads('model.canActivate'),
|
||||
|
||||
actions: {
|
||||
confirm() {
|
||||
this.send('closeModal');
|
||||
}
|
||||
}
|
||||
});
|
@ -1,25 +0,0 @@
|
||||
|
||||
<header class="modal-header" data-test-modal="delete-user">
|
||||
<h1>Upgrade to enable custom themes</h1>
|
||||
</header>
|
||||
<button class="close" title="Close" {{on "click" this.closeModal}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
{{#if this.model.limitErrorMessage}}
|
||||
{{html-safe this.model.limitErrorMessage}}
|
||||
{{else}}
|
||||
Your current plan only supports official themes. You can install them from the <a href="https://ghost.org/marketplace/">Ghost theme marketplace</a>.
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{on "click" this.closeModal}} class="gh-btn" data-test-button="cancel-upgrade">
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
|
||||
<button {{on "click" (action "upgrade")}} class="gh-btn gh-btn-green" data-test-button="upgrade-plan">
|
||||
<span>Upgrade</span>
|
||||
</button>
|
||||
</div>
|
@ -1,16 +0,0 @@
|
||||
import ModalComponent from 'ghost-admin/components/modal-base';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
router: service(),
|
||||
|
||||
actions: {
|
||||
upgrade() {
|
||||
this.router.transitionTo('pro');
|
||||
},
|
||||
|
||||
confirm() {
|
||||
this.send('upgrade');
|
||||
}
|
||||
}
|
||||
});
|
@ -1,138 +0,0 @@
|
||||
<div class="theme-validation-container">
|
||||
<header class="modal-header" data-test-modal="upload-theme">
|
||||
<h1>
|
||||
{{#if this.theme}}
|
||||
{{#if this.hasWarningsOrErrors}}
|
||||
Upload successful with {{#if this.validationErrors}}errors{{else}}warnings{{/if}}
|
||||
{{else}}
|
||||
Upload successful!
|
||||
{{/if}}
|
||||
{{else if (or this.validationErrors this.fatalValidationErrors)}}
|
||||
Invalid theme
|
||||
{{else}}
|
||||
Upload a theme
|
||||
{{/if}}
|
||||
</h1>
|
||||
</header>
|
||||
<a class="close" href="#" role="button" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body">
|
||||
{{#if this.theme}}
|
||||
{{#if this.hasWarningsOrErrors}}
|
||||
<p>
|
||||
The theme <strong>"{{this.themeName}}"</strong> was installed successfully but we detected some {{if this.validationErrors "errors" "warnings"}}.
|
||||
{{#if this.canActivateTheme}}
|
||||
You are still able to activate and use the theme but it is recommended to fix these {{if this.validationErrors "errors" "warnings"}} before you do so.
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
{{#if this.validationErrors}}
|
||||
<div>
|
||||
<h2 class="mb0 mt4 f5 fw6">Errors</h2>
|
||||
<p class="mb2">Highly recommended to fix, functionality <strong>could</strong> be restricted</p>
|
||||
</div>
|
||||
<ul class="pa0">
|
||||
{{#each this.validationErrors as |error|}}
|
||||
<li class="theme-validation-item theme-error">
|
||||
<GhThemeErrorLi @error={{error}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationWarnings}}
|
||||
<div>
|
||||
<h2 class="mb0 mt4 f5 fw6">Warnings</h2>
|
||||
</div>
|
||||
<ul class="pa0">
|
||||
{{#each this.validationWarnings as |error|}}
|
||||
<li class="theme-validation-item theme-warning">
|
||||
<GhThemeErrorLi @error={{error}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p>
|
||||
"{{this.themeName}}" uploaded successfully.
|
||||
{{#if this.canActivateTheme}}Do you want to activate it now?{{/if}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{else if this.displayOverwriteWarning}}
|
||||
<p>
|
||||
The theme folder <strong>"{{this.fileThemeName}}"</strong> already exists. Do you want to overwrite it?
|
||||
</p>
|
||||
{{else if (or this.validationErrors this.fatalValidationErrors)}}
|
||||
|
||||
<p>
|
||||
This theme is invalid and cannot be activated. Fix the following errors and re-upload the theme.
|
||||
</p>
|
||||
|
||||
{{#if this.fatalValidationErrors}}
|
||||
<div>
|
||||
<h2 class="mb0 mt4 f5 fw6">Fatal Errors</h2>
|
||||
<p class="mb2">Must-fix to activate theme</p>
|
||||
</div>
|
||||
|
||||
<ul class="pa0">
|
||||
{{#each this.fatalValidationErrors as |error|}}
|
||||
<li class="theme-validation-item theme-fatal-error">
|
||||
<GhThemeErrorLi @error={{error}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationErrors}}
|
||||
<div>
|
||||
<h2 class="mb0 mt4 f5 fw6">Errors</h2>
|
||||
<p class="mb2">Highly recommended to fix, functionality <strong>could</strong> be restricted</p>
|
||||
</div>
|
||||
<ul class="pa0">
|
||||
{{#each this.validationErrors as |error|}}
|
||||
<li class="theme-validation-item theme-error">
|
||||
<GhThemeErrorLi @error={{error}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<GhFileUploader
|
||||
@url={{this.uploadUrl}}
|
||||
@paramName="file"
|
||||
@accept={{this.accept}}
|
||||
@labelText="Click to select or drag-and-drop your theme zip file here."
|
||||
@validate={{action "validateTheme"}}
|
||||
@uploadStarted={{action "uploadStarted"}}
|
||||
@uploadFinished={{action "uploadFinished"}}
|
||||
@uploadSuccess={{action "uploadSuccess"}}
|
||||
@uploadFailed={{action "uploadFailed"}}
|
||||
@listenTo="themeUploader" />
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer {{if (and this.theme this.hasWarningsOrErrors) "top-shadow"}}">
|
||||
<div class="flex items-center justify-between {{if (or this.displayOverwriteWarning this.canActivateTheme this.validationErrors this.fatalValidationErrors) "flex-auto"}}">
|
||||
<button {{action "closeModal"}} disabled={{this.closeDisabled}} class="gh-btn" data-test-close-button>
|
||||
<span>{{#if this.theme}}Close{{else}}Cancel{{/if}}</span>
|
||||
</button>
|
||||
<div class="flex items-center">
|
||||
{{#if this.displayOverwriteWarning}}
|
||||
<button {{action "confirmOverwrite"}} class="gh-btn gh-btn-red" data-test-overwrite-button>
|
||||
<span>Overwrite</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
{{#if this.canActivateTheme}}
|
||||
<button {{action "activate"}} class="gh-btn" data-test-activate-now-button>
|
||||
<span>Activate{{#if this.validationErrors}} with errors{{/if}}</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
{{#if (or this.validationErrors this.fatalValidationErrors)}}
|
||||
<button {{action "reset"}} class="gh-btn gh-btn-black ml2" data-test-try-again-button>
|
||||
<span>Retry</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,176 +0,0 @@
|
||||
import ModalComponent from 'ghost-admin/components/modal-base';
|
||||
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
||||
import {
|
||||
UnsupportedMediaTypeError,
|
||||
isThemeValidationError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
import {computed} from '@ember/object';
|
||||
import {get} from '@ember/object';
|
||||
import {mapBy, or} from '@ember/object/computed';
|
||||
import {run} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const DEFAULTS = {
|
||||
accept: ['application/zip', 'application/x-zip-compressed'],
|
||||
extensions: ['zip']
|
||||
};
|
||||
|
||||
export default ModalComponent.extend({
|
||||
eventBus: service(),
|
||||
store: service(),
|
||||
|
||||
accept: null,
|
||||
extensions: null,
|
||||
themes: null,
|
||||
closeDisabled: false,
|
||||
file: null,
|
||||
theme: false,
|
||||
displayOverwriteWarning: false,
|
||||
|
||||
hideUploader: or('theme', 'displayOverwriteWarning'),
|
||||
currentThemeNames: mapBy('model.themes', 'name'),
|
||||
|
||||
uploadUrl: computed(function () {
|
||||
return `${ghostPaths().apiRoot}/themes/upload/`;
|
||||
}),
|
||||
|
||||
themeName: computed('theme.{name,package.name}', function () {
|
||||
let themePackage = this.get('theme.package');
|
||||
let name = this.get('theme.name');
|
||||
|
||||
return themePackage ? `${themePackage.name} - ${themePackage.version}` : name;
|
||||
}),
|
||||
|
||||
fileThemeName: computed('file', function () {
|
||||
let file = this.file;
|
||||
return file.name.replace(/\.zip$/, '');
|
||||
}),
|
||||
|
||||
canActivateTheme: computed('theme', function () {
|
||||
let theme = this.theme;
|
||||
return theme && !theme.get('active');
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.accept = this.accept || DEFAULTS.accept;
|
||||
this.extensions = this.extensions || DEFAULTS.extensions;
|
||||
},
|
||||
|
||||
actions: {
|
||||
validateTheme(file) {
|
||||
let themeName = file.name.replace(/\.zip$/, '').replace(/[^\w@.]/gi, '-').toLowerCase();
|
||||
|
||||
let currentThemeNames = this.currentThemeNames;
|
||||
|
||||
this.set('file', file);
|
||||
|
||||
let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name);
|
||||
let extensions = this.extensions;
|
||||
|
||||
if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) {
|
||||
return new UnsupportedMediaTypeError();
|
||||
}
|
||||
|
||||
if (file.name.match(/^casper\.zip$/i)) {
|
||||
return {payload: {errors: [{message: 'Sorry, the default Casper theme cannot be overwritten.<br>Please rename your zip file and try again.'}]}};
|
||||
}
|
||||
|
||||
if (!this._allowOverwrite && currentThemeNames.includes(themeName)) {
|
||||
this.set('displayOverwriteWarning', true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
confirmOverwrite() {
|
||||
this._allowOverwrite = true;
|
||||
this.set('displayOverwriteWarning', false);
|
||||
|
||||
// we need to schedule afterRender so that the upload component is
|
||||
// displayed again in order to subscribe/respond to the event bus
|
||||
run.schedule('afterRender', this, function () {
|
||||
this.eventBus.publish('themeUploader:upload', this.file);
|
||||
});
|
||||
},
|
||||
|
||||
uploadStarted() {
|
||||
this.set('closeDisabled', true);
|
||||
},
|
||||
|
||||
uploadFinished() {
|
||||
this.set('closeDisabled', false);
|
||||
},
|
||||
|
||||
uploadSuccess(response) {
|
||||
this.store.pushPayload(response);
|
||||
|
||||
let theme = this.store.peekRecord('theme', response.themes[0].name);
|
||||
|
||||
this.set('theme', theme);
|
||||
|
||||
if (get(theme, 'warnings.length') > 0) {
|
||||
this.set('validationWarnings', get(theme, 'warnings'));
|
||||
}
|
||||
|
||||
// Ghost differentiates between errors and fatal errors
|
||||
// You can't activate a theme with fatal errors, but with errors.
|
||||
if (get(theme, 'errors.length') > 0) {
|
||||
this.set('validationErrors', get(theme, 'errors'));
|
||||
}
|
||||
|
||||
this.set('hasWarningsOrErrors', this.get('validationErrors.length') || this.get('validationWarnings.length'));
|
||||
|
||||
// invoke the passed in confirm action
|
||||
this.get('model.uploadSuccess')(theme);
|
||||
},
|
||||
|
||||
uploadFailed(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 && 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('fatalValidationErrors', fatalErrors);
|
||||
this.set('validationErrors', normalErrors);
|
||||
}
|
||||
},
|
||||
|
||||
confirm() {
|
||||
// noop - we don't want the enter key doing anything
|
||||
},
|
||||
|
||||
activate() {
|
||||
this.get('model.activate')(this.theme);
|
||||
this.closeModal();
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
if (!this.closeDisabled) {
|
||||
this._super(...arguments);
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.set('theme', null);
|
||||
this.set('validationWarnings', []);
|
||||
this.set('validationErrors', []);
|
||||
this.set('fatalValidationErrors', []);
|
||||
this.set('hasWarningsOrErrors', false);
|
||||
}
|
||||
}
|
||||
});
|
@ -1,169 +0,0 @@
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
import Controller from '@ember/controller';
|
||||
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 const MARKETPLACE_THEMES = [{
|
||||
name: 'Edition',
|
||||
category: 'Newsletter',
|
||||
url: 'https://github.com/TryGhost/Edition',
|
||||
previewUrl: 'https://ghost.org/themes/edition',
|
||||
ref: 'TryGhost/Edition',
|
||||
image: 'assets/img/themes/Edition.jpg',
|
||||
shortImage: 'assets/img/themes/Edition-cut.jpg'
|
||||
}, {
|
||||
name: 'Alto',
|
||||
category: 'Blog',
|
||||
url: 'https://github.com/TryGhost/Alto',
|
||||
previewUrl: 'https://ghost.org/themes/alto',
|
||||
ref: 'TryGhost/Alto',
|
||||
image: 'assets/img/themes/Alto.jpg',
|
||||
shortImage: 'assets/img/themes/Alto-cut.jpg'
|
||||
}, {
|
||||
name: 'London',
|
||||
category: 'Photography',
|
||||
url: 'https://github.com/TryGhost/London',
|
||||
previewUrl: 'https://ghost.org/themes/london',
|
||||
ref: 'TryGhost/London',
|
||||
image: 'assets/img/themes/London.jpg',
|
||||
shortImage: 'assets/img/themes/London-cut.jpg'
|
||||
}, {
|
||||
name: 'Ease',
|
||||
category: 'Documentation',
|
||||
url: 'https://github.com/TryGhost/Ease',
|
||||
previewUrl: 'https://ghost.org/themes/ease',
|
||||
ref: 'TryGhost/Ease',
|
||||
image: 'assets/img/themes/Ease.jpg',
|
||||
shortImage: 'assets/img/themes/Ease-cut.jpg'
|
||||
}];
|
||||
|
||||
export default Controller.extend({
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
limit: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
settings: service(),
|
||||
utils: service(),
|
||||
|
||||
dirtyAttributes: false,
|
||||
newNavItem: null,
|
||||
newSecondaryNavItem: null,
|
||||
themes: null,
|
||||
themeToDelete: null,
|
||||
displayUpgradeModal: false,
|
||||
limitErrorMessage: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.marketplaceThemes = MARKETPLACE_THEMES;
|
||||
},
|
||||
|
||||
showDeleteThemeModal: notEmpty('themeToDelete'),
|
||||
|
||||
actions: {
|
||||
async activateTheme(theme) {
|
||||
const isOverLimit = await this.limit.checkWouldGoOverLimit('customThemes', {value: theme.name});
|
||||
if (isOverLimit) {
|
||||
try {
|
||||
await this.limit.limiter.errorIfWouldGoOverLimit('customThemes', {value: theme.name});
|
||||
this.limitErrorMessage = null;
|
||||
} catch (error) {
|
||||
if (error.errorType !== 'HostLimitError') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.limitErrorMessage = error.message;
|
||||
}
|
||||
|
||||
this.set('displayUpgradeModal', true);
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
this.utils.downloadFile(`${this.get('ghostPaths.apiRoot')}/themes/${theme.name}/download/`);
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
hideUpgradeModal() {
|
||||
this.set('displayUpgradeModal', 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);
|
||||
});
|
||||
}
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class InstallThemeController extends Controller {
|
||||
@service router;
|
||||
|
||||
queryParams = ['source', 'ref'];
|
||||
|
||||
@tracked source = '';
|
||||
@tracked ref = '';
|
||||
|
||||
@action
|
||||
close() {
|
||||
this.router.transitionTo('settings.theme');
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class UploadThemeController extends Controller {
|
||||
@service limit;
|
||||
|
||||
get isAllowed() {
|
||||
return !this.limit.limiter.isLimited('customThemes');
|
||||
}
|
||||
}
|
@ -74,12 +74,6 @@ Router.map(function () {
|
||||
this.route('settings.integrations.unsplash', {path: '/settings/integrations/unsplash'});
|
||||
this.route('settings.integrations.zapier', {path: '/settings/integrations/zapier'});
|
||||
|
||||
// TODO: remove in customThemeSettings cleanup
|
||||
// this.route('settings.theme', {path: '/settings/theme'}, function () {
|
||||
// this.route('uploadtheme');
|
||||
// this.route('install');
|
||||
// });
|
||||
|
||||
this.route('settings.navigation', {path: '/settings/navigation'});
|
||||
this.route('settings.labs', {path: '/settings/labs'});
|
||||
|
||||
|
@ -15,10 +15,6 @@ export default class SettingsDesignRoute extends AuthenticatedRoute {
|
||||
if (!this.session.user.isAdmin) {
|
||||
return this.transitionTo('site');
|
||||
}
|
||||
|
||||
if (!this.feature.customThemeSettings) {
|
||||
return this.transitionTo('settings');
|
||||
}
|
||||
}
|
||||
|
||||
model() {
|
||||
|
@ -1,42 +0,0 @@
|
||||
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, {
|
||||
feature: service(),
|
||||
settings: service(),
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
this.transitionAuthor(this.session.user);
|
||||
|
||||
if (this.feature.customThemeSettings) {
|
||||
this.transitionTo('settings.design');
|
||||
}
|
||||
},
|
||||
|
||||
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');
|
||||
},
|
||||
|
||||
actions: {
|
||||
activateTheme(theme) {
|
||||
return this.controller.send('activateTheme', theme);
|
||||
}
|
||||
},
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'Settings - Theme'
|
||||
};
|
||||
}
|
||||
});
|
@ -49,7 +49,6 @@ export default Service.extend({
|
||||
nightShift: feature('nightShift', {user: true, onChange: '_setAdminTheme'}),
|
||||
multipleProducts: feature('multipleProducts'),
|
||||
oauthLogin: feature('oauthLogin'),
|
||||
customThemeSettings: feature('customThemeSettings'),
|
||||
membersActivity: feature('membersActivity'),
|
||||
cardSettingsPanel: feature('cardSettingsPanel'),
|
||||
membersAutoLogin: feature('membersAutoLogin'),
|
||||
|
@ -537,19 +537,15 @@ input:focus,
|
||||
background-color: var(--black-90);
|
||||
}
|
||||
|
||||
.td-cta-box {
|
||||
background: #191b1f;
|
||||
}
|
||||
|
||||
.td-item-empty {
|
||||
background: var(--whitegrey-l1);
|
||||
}
|
||||
|
||||
.gh-themes-container-labs {
|
||||
.gh-themes-container {
|
||||
background: var(--whitegrey-l2);
|
||||
}
|
||||
|
||||
.gh-themes-container-labs .apps-grid {
|
||||
.gh-themes-container .apps-grid {
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
@ -405,19 +405,6 @@
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.gh-theme-directory-container {
|
||||
padding: 25px 0 0;
|
||||
}
|
||||
|
||||
.theme-directory {
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-gap: 25px;
|
||||
max-width: 1320px;
|
||||
margin: 0 0 4vw;
|
||||
}
|
||||
|
||||
.td-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -505,22 +492,7 @@
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.td-cta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.theme-directory {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.theme-directory {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
.td-item:nth-child(4),
|
||||
.td-item:nth-child(5),
|
||||
.td-item:nth-child(6) {
|
||||
@ -528,16 +500,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.theme-directory {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.td-cta {
|
||||
margin: 50px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* General
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
@ -1713,29 +1675,29 @@ p.theme-validation-details {
|
||||
stroke: var(--darkgrey);
|
||||
}
|
||||
|
||||
.gh-themes-container-labs {
|
||||
.gh-themes-container {
|
||||
margin-bottom: 40px;
|
||||
background: var(--main-color-content-greybg);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.gh-themes-container-labs .apps-grid-cell {
|
||||
.gh-themes-container .apps-grid-cell {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.gh-themes-container-labs .apps-grid-cell:hover {
|
||||
.gh-themes-container .apps-grid-cell:hover {
|
||||
background: var(--whitegrey-l1);
|
||||
}
|
||||
|
||||
.gh-themes-container-labs .apps-card-app {
|
||||
.gh-themes-container .apps-card-app {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.gh-themes-container-labs .apps-grid-cell:last-of-type .apps-card-app {
|
||||
.gh-themes-container .apps-grid-cell:last-of-type .apps-card-app {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.gh-themes-container-labs .apps-configured-action {
|
||||
.gh-themes-container .apps-configured-action {
|
||||
display: block;
|
||||
margin-right: 16px;
|
||||
padding: 2px 6px;
|
||||
@ -1743,15 +1705,15 @@ p.theme-validation-details {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.gh-themes-container-labs .gh-btn-icon {
|
||||
.gh-themes-container .gh-btn-icon {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.gh-themes-container-labs .gh-btn-icon:hover {
|
||||
.gh-themes-container .gh-btn-icon:hover {
|
||||
background: var(--whitegrey-d1);
|
||||
}
|
||||
|
||||
.gh-themes-container-labs .gh-btn-icon svg {
|
||||
.gh-themes-container .gh-btn-icon svg {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
@ -1760,19 +1722,19 @@ p.theme-validation-details {
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.gh-themes-container-labs .apps-configured {
|
||||
.gh-themes-container .apps-configured {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.gh-themes-container-labs .apps-card-meta {
|
||||
.gh-themes-container .apps-card-meta {
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.gh-theme-directory-container-labs {
|
||||
.gh-theme-directory-container {
|
||||
padding: 8px 0 0;
|
||||
}
|
||||
|
||||
.theme-directory-labs {
|
||||
.theme-directory {
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
@ -1782,32 +1744,32 @@ p.theme-validation-details {
|
||||
}
|
||||
|
||||
@media (min-width: 1800px) {
|
||||
.theme-directory-labs {
|
||||
.theme-directory {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.theme-directory-labs {
|
||||
.theme-directory {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 800px) and (max-width: 890px) {
|
||||
.theme-directory-labs {
|
||||
.theme-directory {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.theme-directory-labs {
|
||||
.theme-directory {
|
||||
grid-column-gap: 32px;
|
||||
grid-row-gap: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.theme-directory-labs {
|
||||
.theme-directory {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -1862,7 +1824,7 @@ p.theme-validation-details {
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
|
||||
.theme-directory-labs .td-item-desc {
|
||||
.theme-directory .td-item-desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -164,7 +164,7 @@
|
||||
<div class="gh-dashboard-container col-2">
|
||||
<div class="gh-dashboard-box">
|
||||
<div class="content">
|
||||
<h2>Customize your site{{unless this.feature.customThemeSettings " design"}}</h2>
|
||||
<h2>Customize your site</h2>
|
||||
<p>Stand out from the crowd. Ghost lets you customize everything so you can create a publication that doesn’t just look the same as what everyone else has.</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
|
@ -9,13 +9,13 @@
|
||||
|
||||
<section class="view-container">
|
||||
{{#liquid-if this.showAdvanced}}
|
||||
<div class="gh-themes-container-labs">
|
||||
<GhThemeTableLabs @themes={{this.themes}} />
|
||||
<div class="gh-themes-container">
|
||||
<GhThemeTable @themes={{this.themes}} />
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
|
||||
<div class="gh-theme-directory-container-labs">
|
||||
<div class="theme-directory-labs">
|
||||
<div class="gh-theme-directory-container">
|
||||
<div class="theme-directory">
|
||||
{{#each this.themesList as |theme|}}
|
||||
<LinkTo @route="settings.design.change-theme.view" @model={{theme.name}} class="td-item td-item-labs" data-test-theme-link={{theme.name}}>
|
||||
<div class="gh-theme-browser">
|
||||
|
@ -248,19 +248,6 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-expandable-block">
|
||||
<div class="gh-expandable-header">
|
||||
<div>
|
||||
<h4 class="gh-expandable-title">Custom theme settings</h4>
|
||||
<p class="gh-expandable-description">
|
||||
Redesign of "Design" settings in Admin and allow themes to specify custom settings.
|
||||
</p>
|
||||
</div>
|
||||
<div class="for-switch">
|
||||
<GhFeatureFlag @flag="customThemeSettings" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-expandable-block">
|
||||
<div class="gh-expandable-header">
|
||||
<div>
|
||||
|
@ -1,105 +0,0 @@
|
||||
<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>
|
||||
|
||||
<section class="view-container">
|
||||
<div class="gh-setting-header gh-first-header flex justify-between">
|
||||
<span>Ghost theme directory</span>
|
||||
<a href="https://ghost.org/themes/" target="_blank" rel="noopener noreferrer" class="gh-td-marketplace">
|
||||
<span>View more {{svg-jar "arrow-right-small"}}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="gh-theme-directory-container">
|
||||
<div class="theme-directory">
|
||||
{{#each this.marketplaceThemes as |theme|}}
|
||||
<a class="td-item" href={{theme.url}} target="_blank" rel="noopener noreferrer">
|
||||
<div class="td-item-screenshot relative">
|
||||
<img style="object-fit:contain;" src={{theme.image}} alt="{{theme.name}} Theme" />
|
||||
<div class="td-item-overlay">
|
||||
<LinkTo class="td-item-action gh-btn gh-btn-black mb4" @route="settings.theme.install" @query={{hash source="github" ref=theme.ref}}><span>Install</span></LinkTo>
|
||||
<a href={{theme.previewUrl}} class="td-item-action gh-btn" target="_blank" rel="noopener"><span>Preview</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="td-item-desc">
|
||||
<div>{{theme.name}}</div>
|
||||
<span class="td-item-category">• {{theme.category}}</span>
|
||||
</div>
|
||||
</a>
|
||||
{{/each}}
|
||||
</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"}} />
|
||||
|
||||
<div class="flex justify-between mt6">
|
||||
<LinkTo id="upload-theme" @route="settings.theme.uploadtheme" class="gh-btn gh-btn-green" data-test-button="uploadtheme">
|
||||
<span>Upload a theme</span>
|
||||
</LinkTo>
|
||||
|
||||
<a href="https://ghost.org/docs/themes/" target=_"blank" rel="noopener noreferrer" class="gh-btn gh-btn-outline">
|
||||
<span>Theme developer docs</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
{{#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}}
|
||||
|
||||
{{#if this.displayUpgradeModal}}
|
||||
<GhFullscreenModal @modal="upgrade-host-limit-custom-theme"
|
||||
@model={{hash
|
||||
limitErrorMessage=limitErrorMessage
|
||||
}}
|
||||
@close={{action "hideUpgradeModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
@ -1,10 +0,0 @@
|
||||
<GhFullscreenModal
|
||||
@modal="install-theme"
|
||||
@model={{hash
|
||||
source=this.source
|
||||
ref=this.ref
|
||||
themes=@model
|
||||
}}
|
||||
@close={{this.close}}
|
||||
@modifier="action wide"
|
||||
/>
|
@ -1,16 +0,0 @@
|
||||
{{#if this.isAllowed}}
|
||||
<GhFullscreenModal @modal="upload-theme"
|
||||
@model={{hash
|
||||
themes=this.themes
|
||||
activate=(route-action 'activateTheme')
|
||||
}}
|
||||
@close={{route-action "cancel"}}
|
||||
@modifier="action wide" />
|
||||
{{else}}
|
||||
<GhFullscreenModal @modal="upgrade-host-limit-custom-theme"
|
||||
@model={{hash
|
||||
limitErrorMessage=limitErrorMessage
|
||||
}}
|
||||
@close={{route-action "cancel"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
@ -1,5 +1,4 @@
|
||||
import Mirage from 'ember-cli-mirage';
|
||||
import enableLabsFlag from '../helpers/enable-labs-flag';
|
||||
import {authenticateSession} from 'ember-simple-auth/test-support';
|
||||
import {beforeEach, describe, it} from 'mocha';
|
||||
import {click, currentRouteName, fillIn, find, findAll, visit} from '@ember/test-helpers';
|
||||
@ -109,8 +108,6 @@ describe('Acceptance: Error Handling', function () {
|
||||
});
|
||||
|
||||
it('handles ember-ajax HTML response', async function () {
|
||||
enableLabsFlag(this.server, 'customThemeSettings');
|
||||
|
||||
this.server.del('/themes/foo/', htmlErrorResponse);
|
||||
|
||||
await visit('/settings/design/change-theme');
|
||||
|
@ -1,4 +1,3 @@
|
||||
import enableLabsFlag from '../../helpers/enable-labs-flag';
|
||||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||
import {click, currentURL, find, findAll} from '@ember/test-helpers';
|
||||
import {expect} from 'chai';
|
||||
@ -12,8 +11,6 @@ describe('Acceptance: Settings - Design', function () {
|
||||
setupMirage(hooks);
|
||||
|
||||
beforeEach(async function () {
|
||||
enableLabsFlag(this.server, 'customThemeSettings');
|
||||
|
||||
let role = this.server.create('role', {name: 'Administrator'});
|
||||
this.server.create('user', {roles: [role]});
|
||||
|
||||
|
@ -1,557 +0,0 @@
|
||||
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';
|
||||
|
||||
// TODO: remove along with customThemeSettings feature flag removal
|
||||
describe.skip('Acceptance: Settings - Theme', 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('/settings/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('/settings/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-button="uploadtheme"]');
|
||||
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-button="uploadtheme"]');
|
||||
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-button="uploadtheme"]');
|
||||
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-button="uploadtheme"]');
|
||||
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-button="uploadtheme"]');
|
||||
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