From 5a8753fb8fd1bf5293a9e92c9488ba2ea23b71b0 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 12 Feb 2021 09:19:25 +0000 Subject: [PATCH] Added ability to install free themes directly from GitHub (#1837) refs https://github.com/TryGhost/Ghost/issues/12608 requires https://github.com/TryGhost/Ghost/pull/12635 - adds `/settings/themes/install` route with `source` and `ref` query params that match the API. Shows a confirmation modal when accessed asking to confirm installation and activation - does not allow Casper to be installed - warns if installing the theme will overwrite an existing one - follows similar process to zip uploads for error handling - adds install/preview links for Massively in the free themes showcase Co-authored-by: Sanne de Vries --- ghost/admin/app/components/gh-theme-table.hbs | 6 +- .../app/components/modal-install-theme.hbs | 142 ++++++++++++++++ .../app/components/modal-install-theme.js | 157 ++++++++++++++++++ .../app/components/modal-upload-theme.hbs | 6 +- .../app/controllers/settings/theme/install.js | 18 ++ ghost/admin/app/router.js | 1 + .../app/routes/settings/theme/install.js | 15 ++ ghost/admin/app/styles/layouts/apps.css | 4 +- ghost/admin/app/styles/layouts/settings.css | 136 ++++++--------- ghost/admin/app/styles/patterns/buttons.css | 5 + ghost/admin/app/templates/settings/theme.hbs | 59 +++---- .../app/templates/settings/theme/install.hbs | 10 ++ .../templates/settings/theme/uploadtheme.hbs | 2 +- .../tests/acceptance/settings/theme-test.js | 10 +- 14 files changed, 428 insertions(+), 143 deletions(-) create mode 100644 ghost/admin/app/components/modal-install-theme.hbs create mode 100644 ghost/admin/app/components/modal-install-theme.js create mode 100644 ghost/admin/app/controllers/settings/theme/install.js create mode 100644 ghost/admin/app/routes/settings/theme/install.js create mode 100644 ghost/admin/app/templates/settings/theme/install.hbs diff --git a/ghost/admin/app/components/gh-theme-table.hbs b/ghost/admin/app/components/gh-theme-table.hbs index 9d3a36ede0..32a7173be7 100644 --- a/ghost/admin/app/components/gh-theme-table.hbs +++ b/ghost/admin/app/components/gh-theme-table.hbs @@ -14,15 +14,15 @@
{{!--Delete--}} {{#if theme.isDeletable}} - Delete + Delete {{/if}} {{!--Download--}} - Download + Download {{!--Active Label / Activate Button--}} {{#if theme.active}} Active {{else}} - + Activate {{/if}} diff --git a/ghost/admin/app/components/modal-install-theme.hbs b/ghost/admin/app/components/modal-install-theme.hbs new file mode 100644 index 0000000000..0481ce3565 --- /dev/null +++ b/ghost/admin/app/components/modal-install-theme.hbs @@ -0,0 +1,142 @@ +
+ + + + + + +
\ No newline at end of file diff --git a/ghost/admin/app/components/modal-install-theme.js b/ghost/admin/app/components/modal-install-theme.js new file mode 100644 index 0000000000..0c2a373905 --- /dev/null +++ b/ghost/admin/app/components/modal-install-theme.js @@ -0,0 +1,157 @@ +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'; + +// 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 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; + } + + 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 = []; + } +} diff --git a/ghost/admin/app/components/modal-upload-theme.hbs b/ghost/admin/app/components/modal-upload-theme.hbs index 9652d821ba..049a57385e 100644 --- a/ghost/admin/app/components/modal-upload-theme.hbs +++ b/ghost/admin/app/components/modal-upload-theme.hbs @@ -20,11 +20,9 @@ {{#if this.theme}} {{#if this.hasWarningsOrErrors}}

+ The theme "{{this.themeName}}" was installed successfully but we detected some {{if this.validationErrors "errors" "warnings"}}. {{#if this.canActivateTheme}} - The theme "{{this.themeName}}" was uploaded successfully but we detected some {{#if this.validationErrors}}errors{{else}}warnings{{/if}}. You are still able to activate and use the theme but it is recommended to fix these {{#if this.validationErrors}}errors{{else}}warnings{{/if}} before you do so. - {{else}} - The theme "{{this.themeName}}" was uploaded successfully but we detected some - {{#if this.validationErrors}}errors{{else}}warnings{{/if}}. + 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}}

diff --git a/ghost/admin/app/controllers/settings/theme/install.js b/ghost/admin/app/controllers/settings/theme/install.js new file mode 100644 index 0000000000..a242e4c75f --- /dev/null +++ b/ghost/admin/app/controllers/settings/theme/install.js @@ -0,0 +1,18 @@ +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'); + } +} diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js index 2a2fa95dee..374362ce47 100644 --- a/ghost/admin/app/router.js +++ b/ghost/admin/app/router.js @@ -53,6 +53,7 @@ Router.map(function () { this.route('settings.code-injection', {path: '/settings/code-injection'}); 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'}); diff --git a/ghost/admin/app/routes/settings/theme/install.js b/ghost/admin/app/routes/settings/theme/install.js new file mode 100644 index 0000000000..ae082afd89 --- /dev/null +++ b/ghost/admin/app/routes/settings/theme/install.js @@ -0,0 +1,15 @@ +import Route from '@ember/routing/route'; + +export default class InstallThemeRoute extends Route { + redirect(model, transition) { + const {source, ref} = transition.to.queryParams || {}; + + if (!source || !ref) { + this.transitionTo('settings.theme'); + } + } + + model() { + return this.store.findAll('theme'); + } +} diff --git a/ghost/admin/app/styles/layouts/apps.css b/ghost/admin/app/styles/layouts/apps.css index 815c08b86b..454f004366 100644 --- a/ghost/admin/app/styles/layouts/apps.css +++ b/ghost/admin/app/styles/layouts/apps.css @@ -217,8 +217,8 @@ .apps-configured a { display: inline-block; - padding: 0 4px; - border-radius: 4px; + padding: 2px 6px; + border-radius: 3px; } .apps-configured-action { diff --git a/ghost/admin/app/styles/layouts/settings.css b/ghost/admin/app/styles/layouts/settings.css index 8e39424c85..387369bc12 100644 --- a/ghost/admin/app/styles/layouts/settings.css +++ b/ghost/admin/app/styles/layouts/settings.css @@ -139,7 +139,7 @@ /* Setting headers */ .gh-setting-header { - margin: 4vw 0 5px 1px; + margin: 4vw 0 0 1px; color: var(--black); text-transform: uppercase; font-weight: 500; @@ -349,7 +349,6 @@ /* ---------------------------------------------------------- */ .gh-theme-directory-container { - border-top: var(--lightgrey) 1px solid; padding: 25px 0 0; } @@ -381,10 +380,15 @@ .td-item img { box-shadow: 0 0 1px rgba(0,0,0,.02), 0 9px 25px -12px rgba(0,0,0,0.5); transition: all .8s ease; + border-radius: 3px; +} + +.td-item svg circle { + stroke: var(--midlightgrey); } .td-item:hover { - transform: translateY(-1.5%); + transform: translateY(-1%); transition: all .3s ease; } @@ -396,7 +400,7 @@ .td-item-desc { display: flex; width: 100%; - margin-top: 10px; + margin-top: 16px; text-transform: uppercase; font-weight: 700; } @@ -410,94 +414,56 @@ color: color-mod(var(--midgrey) l(-5%)); } -.td-cta { - display: grid; - justify-content: space-between; - grid-template-columns: 1fr 1fr; - grid-gap: 24px; - margin: 2vw 0 4vw; +.td-item-screenshot { + line-height: 0; + border-radius: 3px; } -.td-cta-box { +.td-item-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.td-item-overlay:hover, +.td-item-overlay:focus { + background-color: var(--white-90); +} + +.td-item-action { + display: none; +} + +.td-item-overlay:hover .td-item-action { + display: block; +} + +.td-item-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; width: 100%; - padding: 14px; - text-decoration: none; - color: var(--darkgrey); - border-radius: 5px; - box-shadow: 0 3px 6px -2px rgba(0,0,0,.1); + height: 100%; + margin-bottom: 39px; + padding: 2rem; background: var(--white); + border-radius: 3px; + box-shadow: 0 0 1px rgba(0,0,0,.02), 0 9px 25px -10px rgba(0,0,0,0.2); transition: all .8s ease; } -.td-cta-box:hover { - transform: translateY(-2%); - box-shadow: 0 0 1px rgba(0,0,0,.02), 0 10px 10px -10px rgba(0,0,0,.12); +.td-item-empty:hover { + box-shadow: 0 0 1px rgba(0,0,0,.02), 0 19px 35px -14px rgba(0,0,0,.2); transition: all .3s ease; } -.td-cta-icon { - flex-shrink: 0; - display: flex; - justify-content: center; - align-items: center; - height: 60px; - width: 60px; - background: var(--midgrey); - border-radius: 6px; -} - -.td-cta-icon svg { - height: 28px; - width: 28px; - fill: #fff; -} - -.td-cta-marketplace .td-cta-icon { - background: var(--purple); -} - -.td-cta-docs .td-cta-icon { - background: var(--blue); -} - -.td-cta-content-wrapper { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; -} - -.td-cta-content { - display: flex; - flex-direction: column; - margin: 0 0 0 14px; -} - -.td-cta-content p { - margin: 0; - color: color-mod(var(--midgrey) l(-5%)); - line-height: 1.3em; -} - -.td-cta-arrow { - flex-shrink: 0; - align-self: center; - display: flex; - align-items: center; - height: 100%; -} - -.td-cta-arrow svg { - margin-left: 20px; - height: 20px; - opacity: 0.5; -} - -.td-cta-arrow svg path { - fill: var(--midgrey); -} - @media (max-width: 1400px) { .theme-directory { grid-template-columns: 1fr 1fr 1fr 1fr; @@ -822,10 +788,6 @@ /* Themes /* ---------------------------------------------------------- */ -.gh-themes-container { - padding: 5px 0; -} - @media (max-width: 500px) { .gh-themes-container .apps-configured { justify-content: flex-end; @@ -835,10 +797,6 @@ } } -.gh-themes-uploadbtn { - margin-top: 25px; -} - /*Errors */ .theme-validation-container { max-height: calc(100vh - 12vw - 110px); diff --git a/ghost/admin/app/styles/patterns/buttons.css b/ghost/admin/app/styles/patterns/buttons.css index 22dfcbae8d..7d67d209a3 100644 --- a/ghost/admin/app/styles/patterns/buttons.css +++ b/ghost/admin/app/styles/patterns/buttons.css @@ -85,6 +85,7 @@ fieldset[disabled] .gh-btn { background: color-mod(var(--black) l(-20%)) !important; } +.gh-btn-primary, .gh-btn-black svg { fill: var(--white); } @@ -216,6 +217,10 @@ fieldset[disabled] .gh-btn { border-color: var(--red); } +.gh-btn-hover-background:hover { + background: var(--lightgrey); +} + /* Special Buttons /* ---------------------------------------------------------- */ diff --git a/ghost/admin/app/templates/settings/theme.hbs b/ghost/admin/app/templates/settings/theme.hbs index 8bd72c34b9..f1fd0733b7 100644 --- a/ghost/admin/app/templates/settings/theme.hbs +++ b/ghost/admin/app/templates/settings/theme.hbs @@ -15,12 +15,15 @@ {{/if}}
-
Theme Directory
Installed Themes
@@ -105,9 +80,15 @@ @downloadTheme={{action "downloadTheme"}} @deleteTheme={{action "deleteTheme"}} /> - - Upload a theme - +
+ + Upload a theme + + + + Theme developer docs + +
{{#if this.showDeleteThemeModal}} @@ -152,7 +133,7 @@ {{outlet}} diff --git a/ghost/admin/app/templates/settings/theme/install.hbs b/ghost/admin/app/templates/settings/theme/install.hbs new file mode 100644 index 0000000000..fbc2b6658a --- /dev/null +++ b/ghost/admin/app/templates/settings/theme/install.hbs @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/ghost/admin/app/templates/settings/theme/uploadtheme.hbs b/ghost/admin/app/templates/settings/theme/uploadtheme.hbs index d39f8a4adc..b1db5c0b28 100644 --- a/ghost/admin/app/templates/settings/theme/uploadtheme.hbs +++ b/ghost/admin/app/templates/settings/theme/uploadtheme.hbs @@ -1,6 +1,6 @@