mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 05:37:34 +03:00
Themes controllers code extraction (#10818)
refs #10790 - Extracted 'setFromZip' method into themes services - Extracted 'activate' method - Extracted 'destroy' method - Extracted 'download' method - The method name here tries to follow 'setFrom...` convention we've agreed upon. So, in this case, we have get() which returns JSON response and getZip() which returns a file
This commit is contained in:
parent
c2bb34ff9c
commit
4529ab514c
32
core/frontend/services/themes/activate.js
Normal file
32
core/frontend/services/themes/activate.js
Normal file
@ -0,0 +1,32 @@
|
||||
const common = require('../../../server/lib/common');
|
||||
const active = require('./active');
|
||||
|
||||
function activate(loadedTheme, checkedTheme, error) {
|
||||
// no need to check the score, activation should be used in combination with validate.check
|
||||
// Use the two theme objects to set the current active theme
|
||||
try {
|
||||
let previousGhostAPI;
|
||||
|
||||
if (active.get()) {
|
||||
previousGhostAPI = active.get().engine('ghost-api');
|
||||
}
|
||||
|
||||
active.set(loadedTheme, checkedTheme, error);
|
||||
const currentGhostAPI = active.get().engine('ghost-api');
|
||||
|
||||
common.events.emit('services.themes.activated');
|
||||
|
||||
if (previousGhostAPI !== undefined && (previousGhostAPI !== currentGhostAPI)) {
|
||||
common.events.emit('services.themes.api.changed');
|
||||
const siteApp = require('../../../server/web/site/app');
|
||||
siteApp.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
common.logging.error(new common.errors.InternalServerError({
|
||||
message: common.i18n.t('errors.middleware.themehandler.activateFailed', {theme: loadedTheme.name}),
|
||||
err: err
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = activate;
|
@ -3,6 +3,7 @@ const debug = require('ghost-ignition').debug('themes');
|
||||
const common = require('../../../server/lib/common');
|
||||
const themeLoader = require('./loader');
|
||||
const active = require('./active');
|
||||
const activate = require('./activate');
|
||||
const validate = require('./validate');
|
||||
const Storage = require('./Storage');
|
||||
const settingsCache = require('../../../server/services/settings/cache');
|
||||
@ -16,8 +17,7 @@ module.exports = {
|
||||
// Init themes module
|
||||
// TODO: move this once we're clear what needs to happen here
|
||||
init: function initThemes() {
|
||||
var activeThemeName = settingsCache.get('active_theme'),
|
||||
self = this;
|
||||
var activeThemeName = settingsCache.get('active_theme');
|
||||
|
||||
debug('init themes', activeThemeName);
|
||||
|
||||
@ -46,7 +46,7 @@ module.exports = {
|
||||
|
||||
common.logging.error(checkError);
|
||||
|
||||
self.activate(theme, checkedTheme, checkError);
|
||||
activate(theme, checkedTheme, checkError);
|
||||
} else {
|
||||
// CASE: inform that the theme has errors, but not fatal (theme still works)
|
||||
if (checkedTheme.results.error.length) {
|
||||
@ -63,7 +63,7 @@ module.exports = {
|
||||
|
||||
debug('Activating theme (method A on boot)', activeThemeName);
|
||||
|
||||
self.activate(theme, checkedTheme);
|
||||
activate(theme, checkedTheme);
|
||||
}
|
||||
});
|
||||
})
|
||||
@ -97,32 +97,24 @@ module.exports = {
|
||||
return engineDefaults['ghost-api'];
|
||||
}
|
||||
},
|
||||
activate: function activate(loadedTheme, checkedTheme, error) {
|
||||
// no need to check the score, activation should be used in combination with validate.check
|
||||
// Use the two theme objects to set the current active theme
|
||||
try {
|
||||
let previousGhostAPI;
|
||||
activate: function (themeName) {
|
||||
const loadedTheme = this.list.get(themeName);
|
||||
|
||||
if (this.getActive()) {
|
||||
previousGhostAPI = this.getApiVersion();
|
||||
}
|
||||
|
||||
active.set(loadedTheme, checkedTheme, error);
|
||||
const currentGhostAPI = this.getApiVersion();
|
||||
|
||||
common.events.emit('services.themes.activated');
|
||||
|
||||
if (previousGhostAPI !== undefined && (previousGhostAPI !== currentGhostAPI)) {
|
||||
common.events.emit('services.themes.api.changed');
|
||||
const siteApp = require('../../../server/web/site/app');
|
||||
siteApp.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
common.logging.error(new common.errors.InternalServerError({
|
||||
message: common.i18n.t('errors.middleware.themehandler.activateFailed', {theme: loadedTheme.name}),
|
||||
err: err
|
||||
if (!loadedTheme) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('notices.data.validation.index.themeCannotBeActivated', {themeName: themeName}),
|
||||
errorDetails: themeName
|
||||
}));
|
||||
}
|
||||
|
||||
return validate.checkSafe(loadedTheme)
|
||||
.then((checkedTheme) => {
|
||||
debug('Activating theme (method B on API "activate")', themeName);
|
||||
activate(loadedTheme, checkedTheme);
|
||||
|
||||
return checkedTheme;
|
||||
});
|
||||
},
|
||||
settings: require('./settings'),
|
||||
middleware: require('./middleware')
|
||||
};
|
||||
|
123
core/frontend/services/themes/settings.js
Normal file
123
core/frontend/services/themes/settings.js
Normal file
@ -0,0 +1,123 @@
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const activate = require('./activate');
|
||||
const validate = require('./validate');
|
||||
const list = require('./list');
|
||||
const Storage = require('./Storage');
|
||||
const themeLoader = require('./loader');
|
||||
const toJSON = require('./to-json');
|
||||
|
||||
const settingsCache = require('../../../server/services/settings/cache');
|
||||
const common = require('../../../server/lib/common');
|
||||
const debug = require('ghost-ignition').debug('api:themes');
|
||||
|
||||
let themeStorage;
|
||||
|
||||
const getStorage = () => {
|
||||
themeStorage = themeStorage || new Storage();
|
||||
|
||||
return themeStorage;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
get: require('./to-json'),
|
||||
getZip: (themeName) => {
|
||||
const theme = list.get(themeName);
|
||||
|
||||
if (!theme) {
|
||||
return Promise.reject(new common.errors.BadRequestError({
|
||||
message: common.i18n.t('errors.api.themes.invalidThemeName')
|
||||
}));
|
||||
}
|
||||
|
||||
return getStorage().serve({
|
||||
name: themeName
|
||||
});
|
||||
},
|
||||
setFromZip: (zip) => {
|
||||
const shortName = getStorage().getSanitizedFileName(zip.name.split('.zip')[0]);
|
||||
|
||||
// check if zip name is casper.zip
|
||||
if (zip.name === 'casper.zip') {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.themes.overrideCasper')
|
||||
});
|
||||
}
|
||||
|
||||
let checkedTheme;
|
||||
|
||||
return validate.checkSafe(zip, true)
|
||||
.then((_checkedTheme) => {
|
||||
checkedTheme = _checkedTheme;
|
||||
|
||||
return getStorage().exists(shortName);
|
||||
})
|
||||
.then((themeExists) => {
|
||||
// CASE: delete existing theme
|
||||
if (themeExists) {
|
||||
return getStorage().delete(shortName);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// CASE: store extracted theme
|
||||
return getStorage().save({
|
||||
name: shortName,
|
||||
path: checkedTheme.path
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// CASE: loads the theme from the fs & sets the theme on the themeList
|
||||
return themeLoader.loadOneTheme(shortName);
|
||||
})
|
||||
.then((loadedTheme) => {
|
||||
// CASE: if this is the active theme, we are overriding
|
||||
if (shortName === settingsCache.get('active_theme')) {
|
||||
debug('Activating theme (method C, on API "override")', shortName);
|
||||
activate(loadedTheme, checkedTheme);
|
||||
|
||||
// CASE: clear cache
|
||||
this.headers.cacheInvalidate = true;
|
||||
}
|
||||
|
||||
// @TODO: unify the name across gscan and Ghost!
|
||||
return toJSON(shortName, checkedTheme);
|
||||
})
|
||||
.finally(() => {
|
||||
// @TODO: we should probably do this as part of saving the theme
|
||||
// CASE: remove extracted dir from gscan
|
||||
// happens in background
|
||||
if (checkedTheme) {
|
||||
fs.remove(checkedTheme.path)
|
||||
.catch((err) => {
|
||||
common.logging.error(new common.errors.GhostError({err: err}));
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
destroy: function (themeName) {
|
||||
if (themeName === 'casper') {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.themes.destroyCasper')
|
||||
});
|
||||
}
|
||||
|
||||
if (themeName === settingsCache.get('active_theme')) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.themes.destroyActive')
|
||||
});
|
||||
}
|
||||
|
||||
const theme = list.get(themeName);
|
||||
|
||||
if (!theme) {
|
||||
throw new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.themes.themeDoesNotExist')
|
||||
});
|
||||
}
|
||||
|
||||
return getStorage().delete(themeName)
|
||||
.then(() => {
|
||||
list.del(themeName);
|
||||
});
|
||||
}
|
||||
};
|
@ -1,14 +1,9 @@
|
||||
// # Themes API
|
||||
// RESTful API for Themes
|
||||
const debug = require('ghost-ignition').debug('api:themes'),
|
||||
Promise = require('bluebird'),
|
||||
fs = require('fs-extra'),
|
||||
localUtils = require('./utils'),
|
||||
const localUtils = require('./utils'),
|
||||
common = require('../../lib/common'),
|
||||
models = require('../../models'),
|
||||
settingsCache = require('../../services/settings/cache'),
|
||||
themeService = require('../../../frontend/services/themes'),
|
||||
themeList = themeService.list;
|
||||
themeService = require('../../../frontend/services/themes');
|
||||
|
||||
let themes;
|
||||
|
||||
@ -31,7 +26,7 @@ themes = {
|
||||
// Main action
|
||||
.then(() => {
|
||||
// Return JSON result
|
||||
return themeService.toJSON();
|
||||
return themeService.settings.get();
|
||||
});
|
||||
},
|
||||
|
||||
@ -40,40 +35,24 @@ themes = {
|
||||
newSettings = [{
|
||||
key: 'active_theme',
|
||||
value: themeName
|
||||
}],
|
||||
loadedTheme,
|
||||
checkedTheme;
|
||||
}];
|
||||
|
||||
return localUtils
|
||||
// Permissions
|
||||
.handlePermissions('themes', 'activate')(options)
|
||||
// Validation
|
||||
// Validation & activation
|
||||
.then(() => {
|
||||
loadedTheme = themeList.get(themeName);
|
||||
|
||||
if (!loadedTheme) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('notices.data.validation.index.themeCannotBeActivated', {themeName: themeName}),
|
||||
errorDetails: newSettings
|
||||
}));
|
||||
}
|
||||
|
||||
return themeService.validate.checkSafe(loadedTheme);
|
||||
return themeService.activate(themeName);
|
||||
})
|
||||
// Update setting
|
||||
.then((_checkedTheme) => {
|
||||
checkedTheme = _checkedTheme;
|
||||
// We use the model, not the API here, as we don't want to trigger permissions
|
||||
return models.Settings.edit(newSettings, options);
|
||||
.then((checkedTheme) => {
|
||||
// @NOTE: We use the model, not the API here, as we don't want to trigger permissions
|
||||
return models.Settings.edit(newSettings, options)
|
||||
.then(() => checkedTheme);
|
||||
})
|
||||
// Call activate
|
||||
.then(() => {
|
||||
// Activate! (sort of)
|
||||
debug('Activating theme (method B on API "activate")', themeName);
|
||||
themeService.activate(loadedTheme, checkedTheme);
|
||||
|
||||
.then((checkedTheme) => {
|
||||
// Return JSON result
|
||||
return themeService.toJSON(themeName, checkedTheme);
|
||||
return themeService.settings.get(themeName, checkedTheme);
|
||||
});
|
||||
},
|
||||
|
||||
@ -84,91 +63,31 @@ themes = {
|
||||
options.originalname = options.originalname.toLowerCase();
|
||||
|
||||
let zip = {
|
||||
path: options.path,
|
||||
name: options.originalname,
|
||||
shortName: themeService.storage.getSanitizedFileName(options.originalname.split('.zip')[0])
|
||||
},
|
||||
checkedTheme;
|
||||
|
||||
// check if zip name is casper.zip
|
||||
if (zip.name === 'casper.zip') {
|
||||
throw new common.errors.ValidationError({message: common.i18n.t('errors.api.themes.overrideCasper')});
|
||||
}
|
||||
path: options.path,
|
||||
name: options.originalname
|
||||
};
|
||||
|
||||
return localUtils
|
||||
// Permissions
|
||||
.handlePermissions('themes', 'add')(options)
|
||||
// Validation
|
||||
.then(() => {
|
||||
return themeService.validate.checkSafe(zip, true);
|
||||
return themeService.settings.setFromZip(zip);
|
||||
})
|
||||
// More validation (existence check)
|
||||
.then((_checkedTheme) => {
|
||||
checkedTheme = _checkedTheme;
|
||||
|
||||
return themeService.storage.exists(zip.shortName);
|
||||
})
|
||||
// If the theme existed we need to delete it
|
||||
.then((themeExists) => {
|
||||
// delete existing theme
|
||||
if (themeExists) {
|
||||
return themeService.storage.delete(zip.shortName);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// store extracted theme
|
||||
return themeService.storage.save({
|
||||
name: zip.shortName,
|
||||
path: checkedTheme.path
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Loads the theme from the filesystem
|
||||
// Sets the theme on the themeList
|
||||
return themeService.loadOne(zip.shortName);
|
||||
})
|
||||
.then((loadedTheme) => {
|
||||
// If this is the active theme, we are overriding
|
||||
// This is a special case of activation
|
||||
if (zip.shortName === settingsCache.get('active_theme')) {
|
||||
// Activate! (sort of)
|
||||
debug('Activating theme (method C, on API "override")', zip.shortName);
|
||||
themeService.activate(loadedTheme, checkedTheme);
|
||||
}
|
||||
|
||||
.then((theme) => {
|
||||
common.events.emit('theme.uploaded');
|
||||
|
||||
// @TODO: unify the name across gscan and Ghost!
|
||||
return themeService.toJSON(zip.shortName, checkedTheme);
|
||||
})
|
||||
.finally(() => {
|
||||
// @TODO we should probably do this as part of saving the theme
|
||||
// remove extracted dir from gscan
|
||||
// happens in background
|
||||
if (checkedTheme) {
|
||||
fs.remove(checkedTheme.path)
|
||||
.catch((err) => {
|
||||
common.logging.error(new common.errors.GhostError({err: err}));
|
||||
});
|
||||
}
|
||||
return theme;
|
||||
});
|
||||
},
|
||||
|
||||
download(options) {
|
||||
let themeName = options.name,
|
||||
theme = themeList.get(themeName);
|
||||
|
||||
if (!theme) {
|
||||
return Promise.reject(new common.errors.BadRequestError({message: common.i18n.t('errors.api.themes.invalidThemeName')}));
|
||||
}
|
||||
let themeName = options.name;
|
||||
|
||||
return localUtils
|
||||
// Permissions
|
||||
.handlePermissions('themes', 'read')(options)
|
||||
.then(() => {
|
||||
return themeService.storage.serve({
|
||||
name: themeName
|
||||
});
|
||||
return themeService.settings.getZip(themeName);
|
||||
});
|
||||
},
|
||||
|
||||
@ -177,35 +96,14 @@ themes = {
|
||||
* remove theme folder
|
||||
*/
|
||||
destroy(options) {
|
||||
let themeName = options.name,
|
||||
theme;
|
||||
let themeName = options.name;
|
||||
|
||||
return localUtils
|
||||
// Permissions
|
||||
.handlePermissions('themes', 'destroy')(options)
|
||||
// Validation
|
||||
.then(() => {
|
||||
if (themeName === 'casper') {
|
||||
throw new common.errors.ValidationError({message: common.i18n.t('errors.api.themes.destroyCasper')});
|
||||
}
|
||||
|
||||
if (themeName === settingsCache.get('active_theme')) {
|
||||
throw new common.errors.ValidationError({message: common.i18n.t('errors.api.themes.destroyActive')});
|
||||
}
|
||||
|
||||
theme = themeList.get(themeName);
|
||||
|
||||
if (!theme) {
|
||||
throw new common.errors.NotFoundError({message: common.i18n.t('errors.api.themes.themeDoesNotExist')});
|
||||
}
|
||||
|
||||
// Actually do the deletion here
|
||||
return themeService.storage.delete(themeName);
|
||||
})
|
||||
// And some extra stuff to maintain state here
|
||||
.then(() => {
|
||||
themeList.del(themeName);
|
||||
// Delete returns an empty 204 response
|
||||
return themeService.settings.destroy(themeName);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,9 +1,5 @@
|
||||
const Promise = require('bluebird');
|
||||
const fs = require('fs-extra');
|
||||
const debug = require('ghost-ignition').debug('api:themes');
|
||||
const common = require('../../lib/common');
|
||||
const themeService = require('../../../frontend/services/themes');
|
||||
const settingsCache = require('../../services/settings/cache');
|
||||
const models = require('../../models');
|
||||
|
||||
module.exports = {
|
||||
@ -12,7 +8,7 @@ module.exports = {
|
||||
browse: {
|
||||
permissions: true,
|
||||
query() {
|
||||
return themeService.toJSON();
|
||||
return themeService.settings.get();
|
||||
}
|
||||
},
|
||||
|
||||
@ -33,34 +29,19 @@ module.exports = {
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
let themeName = frame.options.name;
|
||||
let checkedTheme;
|
||||
|
||||
const newSettings = [{
|
||||
key: 'active_theme',
|
||||
value: themeName
|
||||
}];
|
||||
|
||||
const loadedTheme = themeService.list.get(themeName);
|
||||
|
||||
if (!loadedTheme) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('notices.data.validation.index.themeCannotBeActivated', {themeName: themeName}),
|
||||
errorDetails: newSettings
|
||||
}));
|
||||
}
|
||||
|
||||
return themeService.validate.checkSafe(loadedTheme)
|
||||
.then((_checkedTheme) => {
|
||||
checkedTheme = _checkedTheme;
|
||||
|
||||
return themeService.activate(themeName)
|
||||
.then((checkedTheme) => {
|
||||
// @NOTE: we use the model, not the API here, as we don't want to trigger permissions
|
||||
return models.Settings.edit(newSettings, frame.options);
|
||||
return models.Settings.edit(newSettings, frame.options)
|
||||
.then(() => checkedTheme);
|
||||
})
|
||||
.then(() => {
|
||||
debug('Activating theme (method B on API "activate")', themeName);
|
||||
themeService.activate(loadedTheme, checkedTheme);
|
||||
|
||||
return themeService.toJSON(themeName, checkedTheme);
|
||||
.then((checkedTheme) => {
|
||||
return themeService.settings.get(themeName, checkedTheme);
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -76,67 +57,13 @@ module.exports = {
|
||||
|
||||
let zip = {
|
||||
path: frame.file.path,
|
||||
name: frame.file.originalname,
|
||||
shortName: themeService.storage.getSanitizedFileName(frame.file.originalname.split('.zip')[0])
|
||||
name: frame.file.originalname
|
||||
};
|
||||
|
||||
let checkedTheme;
|
||||
|
||||
// check if zip name is casper.zip
|
||||
if (zip.name === 'casper.zip') {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.themes.overrideCasper')
|
||||
});
|
||||
}
|
||||
|
||||
return themeService.validate.checkSafe(zip, true)
|
||||
.then((_checkedTheme) => {
|
||||
checkedTheme = _checkedTheme;
|
||||
|
||||
return themeService.storage.exists(zip.shortName);
|
||||
})
|
||||
.then((themeExists) => {
|
||||
// CASE: delete existing theme
|
||||
if (themeExists) {
|
||||
return themeService.storage.delete(zip.shortName);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// CASE: store extracted theme
|
||||
return themeService.storage.save({
|
||||
name: zip.shortName,
|
||||
path: checkedTheme.path
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// CASE: loads the theme from the fs & sets the theme on the themeList
|
||||
return themeService.loadOne(zip.shortName);
|
||||
})
|
||||
.then((loadedTheme) => {
|
||||
// CASE: if this is the active theme, we are overriding
|
||||
if (zip.shortName === settingsCache.get('active_theme')) {
|
||||
debug('Activating theme (method C, on API "override")', zip.shortName);
|
||||
themeService.activate(loadedTheme, checkedTheme);
|
||||
|
||||
// CASE: clear cache
|
||||
this.headers.cacheInvalidate = true;
|
||||
}
|
||||
|
||||
return themeService.settings.setFromZip(zip)
|
||||
.then((theme) => {
|
||||
common.events.emit('theme.uploaded');
|
||||
|
||||
// @TODO: unify the name across gscan and Ghost!
|
||||
return themeService.toJSON(zip.shortName, checkedTheme);
|
||||
})
|
||||
.finally(() => {
|
||||
// @TODO: we should probably do this as part of saving the theme
|
||||
// CASE: remove extracted dir from gscan
|
||||
// happens in background
|
||||
if (checkedTheme) {
|
||||
fs.remove(checkedTheme.path)
|
||||
.catch((err) => {
|
||||
common.logging.error(new common.errors.GhostError({err: err}));
|
||||
});
|
||||
}
|
||||
return theme;
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -157,17 +84,8 @@ module.exports = {
|
||||
},
|
||||
query(frame) {
|
||||
let themeName = frame.options.name;
|
||||
const theme = themeService.list.get(themeName);
|
||||
|
||||
if (!theme) {
|
||||
return Promise.reject(new common.errors.BadRequestError({
|
||||
message: common.i18n.t('errors.api.themes.invalidThemeName')
|
||||
}));
|
||||
}
|
||||
|
||||
return themeService.storage.serve({
|
||||
name: themeName
|
||||
});
|
||||
return themeService.settings.getZip(themeName);
|
||||
}
|
||||
},
|
||||
|
||||
@ -190,30 +108,7 @@ module.exports = {
|
||||
query(frame) {
|
||||
let themeName = frame.options.name;
|
||||
|
||||
if (themeName === 'casper') {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.themes.destroyCasper')
|
||||
});
|
||||
}
|
||||
|
||||
if (themeName === settingsCache.get('active_theme')) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.themes.destroyActive')
|
||||
});
|
||||
}
|
||||
|
||||
const theme = themeService.list.get(themeName);
|
||||
|
||||
if (!theme) {
|
||||
throw new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.themes.themeDoesNotExist')
|
||||
});
|
||||
}
|
||||
|
||||
return themeService.storage.delete(themeName)
|
||||
.then(() => {
|
||||
themeService.list.del(themeName);
|
||||
});
|
||||
return themeService.settings.destroy(themeName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user