0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-24 23:48:13 -05:00

Added install+activate process to view theme screen

closes https://github.com/TryGhost/Team/issues/1130

- migrated install theme process to new modal system and changed to install+activate
- added "Use theme" button that opens the new install+activate modal
- when the view theme screen opens the install modal, an `onSuccess` callback is passed which sets a property that will skip closing the install modal when the view theme modal is closed and transitions to the `settings.design` route leaving the "success" modal state on screen
- added a `skipErrors` option to `themeManage.activateThemeTask` so that it can be used from processes that already handle theme errors without opening extra modals on top
This commit is contained in:
Kevin Ansfield 2021-10-12 16:47:33 +01:00
parent 342d9a242c
commit 569e4576d5
6 changed files with 312 additions and 32 deletions

View file

@ -0,0 +1,115 @@
<div class="modal-content">
<div class="theme-validation-container">
<header class="modal-header" data-test-modal="upload-theme">
<h1>{{if this.installSuccess "Theme installed and activated" "Use this theme"}}</h1>
</header>
<button type="button" class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
<div class="modal-body">
{{#if this.isConfirming}}
<p>
You're about to use the <strong>{{this.themeName}}</strong> theme for your site.
{{#unless this.willOverwriteExisting}}The look of your site will instantly be updated.{{/unless}}
</p>
{{#if this.willOverwriteExisting}}
<p>
You already have a version of <strong>{{this.themeName}}</strong> installed.
This will overwrite your existing version, any custom changes to theme files will be lost.
</p>
{{/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"}}.
</p>
{{else}}
{{!-- Installed with no errors --}}
<p>The theme <strong>"{{this.themeName}}"</strong> was installed successfully.</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 installed. 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 install 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">
<button {{on "click" (fn @close false)}} class="gh-btn" data-test-button="cancel">
<span>{{if (or this.installSuccess this.installFailure) "Close" "Cancel"}}</span>
</button>
{{#if this.shouldShowInstall}}
<GhTaskButton
@disabled={{this.refreshThemesTask.isRunning}}
@buttonText={{if this.willOverwriteExisting "Overwrite" "Use theme"}}
@runningText="Installing"
@successText="Installed"
@task={{this.installThemeTask}}
@unlinkedTask={{true}} {{!-- button will be removed on success so avoid self-cancel warning --}}
@class="gh-btn gh-btn-primary gh-btn-icon"
/>
{{/if}}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,124 @@
import Component from '@glimmer/component';
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';
export default class InstallThemeModalComponent extends Component {
@service ajax;
@service ghostPaths;
@service store;
@service themeManagement;
@tracked installedTheme = null;
@tracked installError = '';
@tracked validationWarnings = [];
@tracked validationErrors = [];
@tracked fatalValidationErrors = [];
themes = this.store.peekAll('theme');
constructor() {
super(...arguments);
this.refreshThemesTask.perform();
}
get themeName() {
return this.args.data.theme.name;
}
get isConfirming() {
return !this.installSuccess && !this.installError && !this.installFailure && !this.willOverwriteDefault;
}
get installSuccess() {
return !!this.installedTheme;
}
get installFailure() {
return !this.installSuccess && (this.validationErrors.length || this.fatalValidationErrors.length);
}
get willOverwriteDefault() {
return this.themeName.toLowerCase() === 'casper';
}
get willOverwriteExisting() {
return this.themes.findBy('name', this.themeName.toLowerCase());
}
get hasWarningsOrErrors() {
return this.validationWarnings.length > 0 || this.validationErrors.length > 0;
}
get shouldShowInstall() {
return !this.installSuccess && !this.installFailure && !this.willOverwriteDefault;
}
@task
*refreshThemesTask() {
yield this.store.findAll('theme', {reload: true});
}
@task
*installThemeTask() {
try {
const url = this.ghostPaths.url.api('themes/install') + `?source=github&ref=${this.args.data.theme.ref}`;
const result = yield this.ajax.post(url);
this.installError = '';
if (result.themes) {
// show theme in list immediately
this.store.pushPayload(result);
this.installedTheme = this.store.peekRecord('theme', result.themes[0].name);
this.validationWarnings = this.installedTheme.warnings || [];
this.validationErrors = this.installedTheme.errors || [];
this.fatalValidationErrors = [];
// activate but prevent additional error modal from showing
yield this.themeManagement.activateTask.perform(this.installedTheme, {skipErrors: true});
// let modal opener do any other background stuff
this.args.data.onSuccess?.();
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;
}
}
}

View file

@ -5,10 +5,14 @@
<span>{{svg-jar "arrow-right"}}</span>
{{@data.theme.name}}
</h2>
<section class="view-actions">
<button type="button" class="gh-btn gh-btn-primary" {{on "click" this.installTheme}}><span>Use {{@data.theme.name}}</span></button>
</section>
</GhCanvasHeader>
<section class="view-container">
<GhBrowserPreview class="gh-branding-settings-previewcontainer">
<GhBrowserPreview class="gh-branding-settings-previewcontainer" @title={{@data.theme.name}}>
<iframe src={{@data.theme.previewUrl}} class="site-frame gh-branding-settings-preview"></iframe>
</GhBrowserPreview>
</section>

View file

@ -0,0 +1,30 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
export default class ViewThemeModalComponent extends Component {
@service modals;
@service router;
willDestroy() {
super.willDestroy(...arguments);
// leave install modal visiible if it's in the success state because
// we're switching over to the design customisation screen in the bg
// and don't want to auto-close when this modal closes
if (this.installModal && !this.showingSuccessModal) {
this.installModal.close();
}
}
@action
installTheme() {
this.installModal = this.modals.open('modals/design/install-theme', {
theme: this.args.data.theme,
onSuccess: () => {
this.showingSuccessModal = true;
this.router.transitionTo('settings.design');
}
});
}
}

View file

@ -13,6 +13,9 @@ export default class ModalsService extends EPMModalsService {
'modals/design/confirm-delete-theme': {
className: 'fullscreen-modal-action fullscreen-modal-wide'
},
'modals/design/install-theme': {
className: 'fullscreen-modal-action fullscreen-modal-wide'
},
'modals/design/theme-errors': {
className: 'fullscreen-modal-action fullscreen-modal-wide'
},

View file

@ -39,7 +39,7 @@ export default class ThemeManagementService extends Service {
}
@task
*activateTask(theme) {
*activateTask(theme, options) {
let resultModal = null;
try {
@ -68,44 +68,48 @@ export default class ThemeManagementService extends Service {
this.updatePreviewHtmlTask.perform();
this.customThemeSettings.load();
const {warnings, errors} = activatedTheme;
if (!options.skipErrors) {
const {warnings, errors} = activatedTheme;
if (!isEmpty(warnings) || !isEmpty(errors)) {
resultModal = this.modals.open('modals/design/theme-errors', {
title: 'Activation successful',
canActivate: true,
warnings,
errors
});
if (!isEmpty(warnings) || !isEmpty(errors)) {
resultModal = this.modals.open('modals/design/theme-errors', {
title: 'Activation successful',
canActivate: true,
warnings,
errors
});
yield resultModal;
yield resultModal;
}
}
} catch (error) {
if (isThemeValidationError(error)) {
let errors = error.payload.errors[0].details.errors;
let fatalErrors = [];
let normalErrors = [];
if (!options.skipErrors) {
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]);
// 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]);
}
}
}
resultModal = this.modals.open('modals/design/theme-errors', {
title: 'Activation failed',
canActivate: false,
errors: normalErrors,
fatalErrors
});
yield resultModal;
}
resultModal = this.modals.open('modals/design/theme-errors', {
title: 'Activation failed',
canActivate: false,
errors: normalErrors,
fatalErrors
});
yield resultModal;
}
throw error;