diff --git a/ghost/admin/app/components/announcement-settings/background.js b/ghost/admin/app/components/announcement-settings/background.js index 622a7d9d99..b1051671ef 100644 --- a/ghost/admin/app/components/announcement-settings/background.js +++ b/ghost/admin/app/components/announcement-settings/background.js @@ -20,7 +20,6 @@ export default class AnnouncementSettingsBackgroundComponent extends Component { @action setBackground(value) { this.settings.announcementBackground = value; - this.settings.save(); this.args.onChange?.(); } } diff --git a/ghost/admin/app/components/announcement-settings/content.js b/ghost/admin/app/components/announcement-settings/content.js index 2cd7acecbc..8bd70253f7 100644 --- a/ghost/admin/app/components/announcement-settings/content.js +++ b/ghost/admin/app/components/announcement-settings/content.js @@ -13,6 +13,5 @@ export default class AnnouncementSettingsContentComponent extends Component { @action setContent(html) { this.settings.announcementContent = html; - this.settings.save(); } } diff --git a/ghost/admin/app/components/announcement-settings/visibility.js b/ghost/admin/app/components/announcement-settings/visibility.js index 3b5a851205..26589558eb 100644 --- a/ghost/admin/app/components/announcement-settings/visibility.js +++ b/ghost/admin/app/components/announcement-settings/visibility.js @@ -48,7 +48,9 @@ export default class AnnouncementSettingsVisibilityComponent extends Component { } this.settings.announcementVisibility = updatedVisibilityOptions; - this.settings.save(); - this.args.onChange?.(); + // update preview if there are no visibility options or just one to avoid update flickering on every check + if (!updatedVisibilityOptions.length || updatedVisibilityOptions.length === 1) { + this.args.onChange?.(); + } } } diff --git a/ghost/admin/app/controllers/settings/announcement-bar/index.js b/ghost/admin/app/controllers/settings/announcement-bar/index.js index 32f8fecb74..2d775b16bc 100644 --- a/ghost/admin/app/controllers/settings/announcement-bar/index.js +++ b/ghost/admin/app/controllers/settings/announcement-bar/index.js @@ -42,8 +42,7 @@ export default class SettingsAnnouncementBarIndexController extends Controller { } yield Promise.all([ - this.settings.save(), - this.customThemeSettings.save() + this.settings.save() ]); // ensure task button switches to success state diff --git a/ghost/admin/app/routes/settings/announcement-bar.js b/ghost/admin/app/routes/settings/announcement-bar.js index 69b6ffe4cd..c8d97230b2 100644 --- a/ghost/admin/app/routes/settings/announcement-bar.js +++ b/ghost/admin/app/routes/settings/announcement-bar.js @@ -1,7 +1,9 @@ import AdminRoute from 'ghost-admin/routes/authenticated'; +import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; +import {action} from '@ember/object'; import {inject as service} from '@ember/service'; -export default class SettingsDesignRoute extends AdminRoute { +export default class AnnouncementBarRoute extends AdminRoute { @service customThemeSettings; @service feature; @service modals; @@ -41,6 +43,8 @@ export default class SettingsDesignRoute extends AdminRoute { deactivate() { this.ui.contextualNavMenu = null; + this.confirmModal = null; + this.hasConfirmed = false; } buildRouteInfoMetadata() { @@ -49,4 +53,41 @@ export default class SettingsDesignRoute extends AdminRoute { mainClasses: ['gh-main-fullwidth'] }; } + + @action + willTransition(transition) { + if (this.hasConfirmed) { + return true; + } + + // always abort when not confirmed because Ember's router doesn't automatically wait on promises + transition.abort(); + + this.confirmUnsavedChanges().then((shouldLeave) => { + if (shouldLeave === true) { + this.hasConfirmed = true; + return transition.retry(); + } + }); + } + + confirmUnsavedChanges() { + if (!this.settings.hasDirtyAttributes) { + return Promise.resolve(true); + } + + if (!this.confirmModal) { + this.confirmModal = this.modals.open(ConfirmUnsavedChangesModal) + .then((discardChanges) => { + if (discardChanges === true) { + this.settings.rollbackAttributes(); + } + return discardChanges; + }).finally(() => { + this.confirmModal = null; + }); + } + + return this.confirmModal; + } } diff --git a/ghost/admin/app/services/theme-management.js b/ghost/admin/app/services/theme-management.js index e81936711f..3d5dfd8897 100644 --- a/ghost/admin/app/services/theme-management.js +++ b/ghost/admin/app/services/theme-management.js @@ -222,6 +222,14 @@ export default class ThemeManagementService extends Service { params.append('logo', this.settings.logo); params.append('cover', this.settings.coverImage); + if (this.settings.announcementContent) { + params.append('announcement', this.settings.announcementContent); + } + params.append('announcement_bg', this.settings.announcementBackground); + if (this.settings.announcementVisibility.length) { + params.append('announcement_vis', this.settings.announcementVisibility); + } + params.append('custom', JSON.stringify(this.customThemeSettings.keyValueObject)); return params.toString(); diff --git a/ghost/admin/app/styles/layouts/settings.css b/ghost/admin/app/styles/layouts/settings.css index 3cdf5a595c..4c7e029de7 100644 --- a/ghost/admin/app/styles/layouts/settings.css +++ b/ghost/admin/app/styles/layouts/settings.css @@ -3708,7 +3708,8 @@ p.theme-validation-details { /* Announcement bar */ .gh-announcement-editor { - height: 120px; + min-height: 120px; + overflow: auto; padding: 6px 12px 6px 12px; border: 1px solid var(--whitegrey-d1); background: var(--white); diff --git a/ghost/announcement-bar/src/App.js b/ghost/announcement-bar/src/App.js index 37caac4f4f..a88ced6237 100644 --- a/ghost/announcement-bar/src/App.js +++ b/ghost/announcement-bar/src/App.js @@ -1,30 +1,12 @@ import React from 'react'; -import {AnnouncementBar} from './components/AnnouncementBar'; -import setupGhostApi from './utils/api'; - -export function App({apiUrl}) { - const api = React.useRef(setupGhostApi({apiUrl})); - const [siteSettings, setSiteSettings] = React.useState(); - - React.useEffect(() => { - if (siteSettings) { - return; - } - const getSiteSettings = async () => { - const announcement = await api.current.init(); - - setSiteSettings(announcement); - }; - - getSiteSettings(); - // We only do this for init - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); +import {Preview} from './components/Preview'; +import {Main} from './components/Main'; +export function App({apiUrl, previewData}) { + if (previewData) { + return ; + } return ( - +
); } diff --git a/ghost/announcement-bar/src/components/Main.js b/ghost/announcement-bar/src/components/Main.js new file mode 100644 index 0000000000..27e798d7e5 --- /dev/null +++ b/ghost/announcement-bar/src/components/Main.js @@ -0,0 +1,27 @@ +import React from 'react'; +import {AnnouncementBar} from './AnnouncementBar'; +import setupGhostApi from '../utils/api'; + +export function Main({apiUrl}) { + const api = React.useRef(setupGhostApi({apiUrl})); + const [siteSettings, setSiteSettings] = React.useState(); + + React.useEffect(() => { + if (siteSettings) { + return; + } + const getSiteSettings = async () => { + const announcement = await api.current.init(); + + setSiteSettings(announcement); + }; + + getSiteSettings(); + // We only do this for init + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); +} diff --git a/ghost/announcement-bar/src/components/Preview.js b/ghost/announcement-bar/src/components/Preview.js new file mode 100644 index 0000000000..7f89e8de13 --- /dev/null +++ b/ghost/announcement-bar/src/components/Preview.js @@ -0,0 +1,8 @@ +import React from 'react'; +import {AnnouncementBar} from './AnnouncementBar'; + +export function Preview({previewData}) { + return ( + + ); +} diff --git a/ghost/announcement-bar/src/index.js b/ghost/announcement-bar/src/index.js index 878bbad9aa..88de427ad7 100644 --- a/ghost/announcement-bar/src/index.js +++ b/ghost/announcement-bar/src/index.js @@ -22,22 +22,34 @@ function getSiteData() { const scriptTag = document.querySelector('script[data-announcement-bar]'); if (scriptTag) { const apiUrl = scriptTag.dataset.apiUrl; - return {apiUrl}; + return {apiUrl, previewData: getPreviewData(scriptTag)}; } return {}; } +function getPreviewData(scriptTag) { + if (scriptTag.dataset.preview) { + const announcement = scriptTag.dataset.announcement; + const announcementBackground = scriptTag.dataset.announcementBackground; + + return {announcement, announcement_background: announcementBackground}; + } + + return null; +} + function setup() { addRootDiv(); } function init() { - const {apiUrl} = getSiteData(); + const {apiUrl, previewData} = getSiteData(); setup(); ReactDOM.render( , document.getElementById(ROOT_DIV_ID) diff --git a/ghost/core/core/frontend/helpers/ghost_head.js b/ghost/core/core/frontend/helpers/ghost_head.js index 413f48297c..52d453f2af 100644 --- a/ghost/core/core/frontend/helpers/ghost_head.js +++ b/ghost/core/core/frontend/helpers/ghost_head.js @@ -87,8 +87,10 @@ function getSearchHelper(frontendKey) { return helper; } -function getAnnouncementBarHelper() { - if (!settingsCache.get('announcement_content')) { +function getAnnouncementBarHelper(data) { + const preview = data?.site?._preview; + + if (!settingsCache.get('announcement_content') && !preview) { return ''; } @@ -100,6 +102,20 @@ function getAnnouncementBarHelper() { 'api-url': announcementUrl }; + if (preview) { + const searchParam = new URLSearchParams(preview); + const announcement = searchParam.get('announcement'); + const announcementBackground = searchParam.has('announcement_bg') ? searchParam.get('announcement_bg') : ''; + const announcementVisibility = searchParam.has('announcement_vis'); + + if (!announcement || !announcementVisibility) { + return; + } + attrs.announcement = escapeExpression(announcement); + attrs['announcement-background'] = escapeExpression(announcementBackground); + attrs.preview = true; + } + const dataAttrs = getDataAttributes(attrs); let helper = ``; @@ -249,7 +265,7 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam if (!_.includes(context, 'amp')) { head.push(getMembersHelper(options.data, frontendKey)); head.push(getSearchHelper(frontendKey)); - head.push(getAnnouncementBarHelper()); + head.push(getAnnouncementBarHelper(options.data)); try { head.push(getWebmentionDiscoveryLink()); } catch (err) {