mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-30 06:12:03 +03:00
85d7932e45
refs https://github.com/TryGhost/Team/issues/559
refs 054a5f15f5
- with the update of `ember-promise-modals` we started to get deprecation warnings when using `modals.open('modal-component-name')`
- upcoming Ember build updates will introduce tree shaking but using run-time lookup of modal components by name works against that because it's not statically analysable
- switched to importing components and passing the component class directly, eg. `modals.open(ModalComponent)`
- standardized modal component class names with a `MyModal` style to get better behaviour in code editors when it auto generates imports
- dropped the modal defaults from the modals service because we can now use a static `modalOptions` property on the modal components themselves when we want to override the defaults
174 lines
4.7 KiB
JavaScript
174 lines
4.7 KiB
JavaScript
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';
|
|
import {tracked} from '@glimmer/tracking';
|
|
|
|
export default class UploadThemeModal extends Component {
|
|
@service eventBus;
|
|
@service ghostPaths;
|
|
@service store;
|
|
@service themeManagement;
|
|
|
|
static modalOptions = {
|
|
beforeClose: () => {
|
|
if (this.themeManagement.isUploading) {
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
@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(error);
|
|
}
|
|
});
|
|
|
|
this.fatalValidationErrors = fatalErrors;
|
|
this.validationErrors = normalErrors;
|
|
}
|
|
}
|
|
|
|
@action
|
|
activate() {
|
|
this.themeManagement.activateTask.perform(this.theme);
|
|
this.args.data.onActivationSuccess?.();
|
|
this.args.close();
|
|
}
|
|
|
|
@action
|
|
reset() {
|
|
this.theme = null;
|
|
this.validationWarnings = [];
|
|
this.validationErrors = [];
|
|
this.fatalValidationErrors = [];
|
|
}
|
|
}
|