mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-29 13:52:10 +03:00
Refactored theme upload process and linked from design screens
refs https://github.com/TryGhost/Team/issues/1130 - copied theme upload modal to new modals system and refactored for Octane syntax - updated to use `themeManagement` service rather than passed in actions so the modal-based process can be opened from any screen - added default `beforeClose()` for the modal to the modals service so it won't close if an upload is in-progress (defaults were moved directly into the class so it had access to services) - added `themeManagement.upload` action for triggering the upload modal and providing a central place for limit checks - added upload-triggering buttons to change-theme and advanced design screens
This commit is contained in:
parent
3345920f29
commit
13f9cb85fa
140
ghost/admin/app/components/modals/design/upload-theme.hbs
Normal file
140
ghost/admin/app/components/modals/design/upload-theme.hbs
Normal file
@ -0,0 +1,140 @@
|
||||
<div class="modal-content">
|
||||
<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>
|
||||
<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.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={{this.validateTheme}}
|
||||
@uploadStarted={{fn (mut this.themeManagement.isUploading) true}}
|
||||
@uploadFinished={{fn (mut this.themeManagement.isUploading) false}}
|
||||
@uploadSuccess={{this.uploadSuccess}}
|
||||
@uploadFailed={{this.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 type="button" {{on "click" @close}} 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 type="button" {{on "click" this.confirmOverwrite}} class="gh-btn gh-btn-red" data-test-overwrite-button>
|
||||
<span>Overwrite</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
{{#if this.canActivateTheme}}
|
||||
<button type="button" {{on "click" this.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 type="button" {{on "click" this.reset}} class="gh-btn gh-btn-black ml2" data-test-try-again-button>
|
||||
<span>Retry</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
164
ghost/admin/app/components/modals/design/upload-theme.js
Normal file
164
ghost/admin/app/components/modals/design/upload-theme.js
Normal file
@ -0,0 +1,164 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {
|
||||
UnsupportedMediaTypeError,
|
||||
isThemeValidationError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
import {action} from '@ember/object';
|
||||
import {run} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency-decorators';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class UploadThemeModalComponent extends Component {
|
||||
@service eventBus;
|
||||
@service ghostPaths;
|
||||
@service store;
|
||||
@service themeManagement;
|
||||
|
||||
@tracked displayOverwriteWarning = false;
|
||||
@tracked file;
|
||||
@tracked theme;
|
||||
@tracked validationErrors;
|
||||
@tracked validationWarnings;
|
||||
@tracked fatalValidationErrors;
|
||||
|
||||
accept = ['application/zip', 'application/x-zip-compressed'];
|
||||
extensions = ['zip'];
|
||||
|
||||
get themes() {
|
||||
return this.store.peekAll('theme');
|
||||
}
|
||||
|
||||
get currentThemeNames() {
|
||||
return this.themes.map(theme => theme.name);
|
||||
}
|
||||
|
||||
get themeName() {
|
||||
let themePackage = this.theme.package;
|
||||
let name = this.theme.name;
|
||||
|
||||
return themePackage ? `${themePackage.name} - ${themePackage.version}` : name;
|
||||
}
|
||||
|
||||
get fileThemeName() {
|
||||
return this.file?.name.replace(/\.zip$/, '');
|
||||
}
|
||||
|
||||
get canActivateTheme() {
|
||||
return this.theme && !this.theme.active;
|
||||
}
|
||||
|
||||
get uploadUrl() {
|
||||
return `${this.ghostPaths.apiRoot}/themes/upload/`;
|
||||
}
|
||||
|
||||
get hasWarningsOrErrors() {
|
||||
return this.validationWarnings?.length || this.validationErrors?.length;
|
||||
}
|
||||
|
||||
get closeDisabled() {
|
||||
return this.themeManagement.isUploading;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.refreshThemesTask.perform();
|
||||
}
|
||||
|
||||
@task
|
||||
*refreshThemesTask() {
|
||||
yield this.store.findAll('theme');
|
||||
}
|
||||
|
||||
@action
|
||||
validateTheme(file) {
|
||||
const themeName = file.name.replace(/\.zip$/, '').replace(/[^\w@.]/gi, '-').toLowerCase();
|
||||
|
||||
this.file = file;
|
||||
|
||||
const [, extension] = (/(?:\.([^.]+))?$/).exec(file.name);
|
||||
const 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 && this.currentThemeNames.includes(themeName)) {
|
||||
this.displayOverwriteWarning = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@action
|
||||
confirmOverwrite() {
|
||||
this._allowOverwrite = true;
|
||||
this.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);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
uploadSuccess(response) {
|
||||
this.store.pushPayload(response);
|
||||
|
||||
const theme = this.store.peekRecord('theme', response.themes[0].name);
|
||||
|
||||
this.theme = theme;
|
||||
|
||||
if (theme.warnings?.length > 0) {
|
||||
this.validationWarnings = theme.warnings;
|
||||
}
|
||||
|
||||
// Ghost differentiates between errors and fatal errors
|
||||
// You can't activate a theme with fatal errors, but with errors.
|
||||
if (theme.errors?.length > 0) {
|
||||
this.validationErrors = theme.errors;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
uploadFailed(errorResponse) {
|
||||
if (isThemeValidationError(errorResponse)) {
|
||||
const errors = errorResponse.payload.errors[0].details.errors;
|
||||
const fatalErrors = [];
|
||||
const normalErrors = [];
|
||||
|
||||
// to have a proper grouping of fatal errors and none fatal, we need to check
|
||||
// our errors for the fatal property
|
||||
errors.forEach?.((error) => {
|
||||
if (error.fatal) {
|
||||
fatalErrors.push(error);
|
||||
} else {
|
||||
normalErrors.push(errors[i]);
|
||||
}
|
||||
});
|
||||
|
||||
this.fatalValidationErrors = fatalErrors;
|
||||
this.validationErrors = normalErrors;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
activate() {
|
||||
this.themeManagement.activateTask.perform(this.theme);
|
||||
this.args.close();
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.theme = null;
|
||||
this.validationWarnings = [];
|
||||
this.validationErrors = [];
|
||||
this.fatalValidationErrors = [];
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import {inject as service} from '@ember/service';
|
||||
|
||||
export default class AdvancedThemeSettingsController extends Controller {
|
||||
@service store;
|
||||
@service themeManagement;
|
||||
|
||||
get themes() {
|
||||
return this.store.peekAll('theme');
|
||||
|
@ -1,6 +1,9 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class ChangeThemeController extends Controller {
|
||||
@service themeManagement;
|
||||
|
||||
marketplaceThemes = [{
|
||||
name: 'Edition',
|
||||
category: 'Newsletter',
|
||||
|
@ -2,23 +2,32 @@ import EPMModalsService from 'ember-promise-modals/services/modals';
|
||||
import {bind} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export const DEFAULT_MODAL_OPTIONS = {
|
||||
'modals/confirm-unsaved-changes': {
|
||||
className: 'fullscreen-modal-action fullscreen-modal-wide'
|
||||
},
|
||||
'modals/design/confirm-delete-theme': {
|
||||
className: 'fullscreen-modal-action fullscreen-modal-wide'
|
||||
},
|
||||
'modals/design/theme-errors': {
|
||||
className: 'fullscreen-modal-action fullscreen-modal-wide'
|
||||
},
|
||||
'modals/limits/custom-theme': {
|
||||
className: 'fullscreen-modal-action fullscreen-modal-wide'
|
||||
}
|
||||
};
|
||||
|
||||
export default class ModalsService extends EPMModalsService {
|
||||
@service dropdown;
|
||||
@service themeManagement;
|
||||
|
||||
DEFAULT_MODAL_OPTIONS = {
|
||||
'modals/confirm-unsaved-changes': {
|
||||
className: 'fullscreen-modal-action fullscreen-modal-wide'
|
||||
},
|
||||
'modals/design/upload-theme': {
|
||||
className: 'fullscreen-modal-action fullscreen-modal-wide',
|
||||
beforeClose: () => {
|
||||
if (this.themeManagement.isUploading) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
'modals/design/confirm-delete-theme': {
|
||||
className: 'fullscreen-modal-action fullscreen-modal-wide'
|
||||
},
|
||||
'modals/design/theme-errors': {
|
||||
className: 'fullscreen-modal-action fullscreen-modal-wide'
|
||||
},
|
||||
'modals/limits/custom-theme': {
|
||||
className: 'fullscreen-modal-action fullscreen-modal-wide'
|
||||
}
|
||||
}
|
||||
|
||||
// we manually close modals on backdrop clicks and escape rather than letting focus-trap
|
||||
// handle it so we can intercept/abort closing for things like unsaved change confirmations
|
||||
@ -27,7 +36,7 @@ export default class ModalsService extends EPMModalsService {
|
||||
escapeDeactivates = false;
|
||||
|
||||
open(modal, data, options) {
|
||||
const mergedOptions = Object.assign({}, DEFAULT_MODAL_OPTIONS[modal], options);
|
||||
const mergedOptions = Object.assign({}, this.DEFAULT_MODAL_OPTIONS[modal], options);
|
||||
return super.open(modal, data, mergedOptions);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Service from '@ember/service';
|
||||
import config from 'ghost-admin/config/environment';
|
||||
import {action} from '@ember/object';
|
||||
import {isEmpty} from '@ember/utils';
|
||||
import {isThemeValidationError} from 'ghost-admin/services/ajax';
|
||||
import {inject as service} from '@ember/service';
|
||||
@ -14,8 +15,33 @@ export default class ThemeManagementService extends Service {
|
||||
@service modals;
|
||||
@service settings;
|
||||
|
||||
@tracked isUploading;
|
||||
@tracked previewHtml;
|
||||
|
||||
@action
|
||||
async upload(event) {
|
||||
event?.preventDefault();
|
||||
|
||||
if (this.limit.limiter.isLimited('customThemes')) {
|
||||
return this.modals.open('modals/limits/custom-theme');
|
||||
}
|
||||
|
||||
try {
|
||||
// Sending a bad string to make sure it fails (empty string isn't valid)
|
||||
await this.limit.limiter.errorIfWouldGoOverLimit('customThemes', {value: '.'});
|
||||
} catch (error) {
|
||||
if (error.errorType === 'HostLimitError') {
|
||||
return this.modals.open('modals/limit/custom-theme', {
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return this.modals.open('modals/design/upload-theme');
|
||||
}
|
||||
|
||||
@task
|
||||
*activateTask(theme) {
|
||||
let resultModal = null;
|
||||
|
@ -1,6 +1,10 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<span class="gh-canvas-title">Advanced</span>
|
||||
|
||||
<section class="view-actions">
|
||||
<button type="button" class="gh-btn gh-btn-black" {{on "click" this.themeManagement.upload}}><span>Upload theme</span></button>
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<section class="view-container">
|
||||
|
@ -2,9 +2,11 @@
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>All themes</h2>
|
||||
<section class="view-actions">
|
||||
<a href="https://ghost.org/themes/" target="_blank" rel="noopener noreferrer" class="gh-btn gh-btn-grey gh-btn-icon-right">
|
||||
<a href="https://ghost.org/themes/" target="_blank" rel="noopener noreferrer" class="mr4 gh-btn gh-btn-grey gh-btn-icon-right">
|
||||
<span>Premium themes {{svg-jar "external"}}</span>
|
||||
</a>
|
||||
|
||||
<button type="button" class="gh-btn gh-btn-black" {{on "click" this.themeManagement.upload}}><span>Upload theme</span></button>
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user