diff --git a/apps/admin-x-design-system/src/global/TabView.tsx b/apps/admin-x-design-system/src/global/TabView.tsx index 49b9448e91..23e2ea17b9 100644 --- a/apps/admin-x-design-system/src/global/TabView.tsx +++ b/apps/admin-x-design-system/src/global/TabView.tsx @@ -5,6 +5,8 @@ export type Tab = { id: ID; title: string; counter?: number | null; + tabWrapperClassName?: string; + containerClassName?: string; /** * Optional, so you can just use the tabs to other views @@ -102,6 +104,7 @@ export interface TabViewProps { border?: boolean; buttonBorder?: boolean; width?: TabWidth; + containerClassName?: string; } function TabView({ @@ -110,7 +113,8 @@ function TabView({ selectedTab, border = true, buttonBorder = border, - width = 'normal' + width = 'normal', + containerClassName }: TabViewProps) { if (tabs.length !== 0 && selectedTab === undefined) { selectedTab = tabs[0].id; @@ -126,7 +130,7 @@ function TabView({ }; return ( -
+
({ return ( <> {tab.contents && -
-
{tab.contents}
+
+
{tab.contents}
} diff --git a/apps/admin-x-settings/src/components/settings/advanced/CodeInjection.tsx b/apps/admin-x-settings/src/components/settings/advanced/CodeInjection.tsx index 335a7c8e59..aba04ba3c0 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/CodeInjection.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/CodeInjection.tsx @@ -1,93 +1,26 @@ import CodeModal from './code/CodeModal'; import NiceModal from '@ebay/nice-modal-react'; -import React, {useMemo, useRef, useState} from 'react'; +import React from 'react'; import TopLevelGroup from '../../TopLevelGroup'; -import useSettingGroup from '../../../hooks/useSettingGroup'; -import {Button, CodeEditor, TabView, withErrorBoundary} from '@tryghost/admin-x-design-system'; -import {ReactCodeMirrorRef} from '@uiw/react-codemirror'; -import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; +import {Button, SettingGroupHeader, withErrorBoundary} from '@tryghost/admin-x-design-system'; const CodeInjection: React.FC<{ keywords: string[] }> = ({keywords}) => { - const { - localSettings, - isEditing, - saveState, - handleSave, - handleCancel, - updateSetting, - handleEditingChange - } = useSettingGroup(); - - const [headerContent, footerContent] = getSettingValues(localSettings, ['codeinjection_head', 'codeinjection_foot']); - - const [selectedTab, setSelectedTab] = useState<'header' | 'footer'>('header'); - - const headerEditorRef = useRef(null); - const footerEditorRef = useRef(null); - - const html = useMemo(() => import('@codemirror/lang-html').then(module => module.html()), []); - - const headerProps = { - extensions: [html], - hint: 'Code here will be injected into the {{ghost_head}} tag on every page of the site', - value: headerContent || '', - onChange: (value: string) => updateSetting('codeinjection_head', value) - }; - - const footerProps = { - extensions: [html], - hint: 'Code here will be injected into the {{ghost_foot}} tag on every page of the site', - value: footerContent || '', - onChange: (value: string) => updateSetting('codeinjection_foot', value) - }; - - const tabs = [ - { - id: 'header', - title: 'Site header', - contents: () - }, - { - id: 'footer', - title: 'Site footer', - contents: () - } - ] as const; - return ( + +
+ } description="Add custom code to your publication" - isEditing={isEditing} keywords={keywords} navid='code-injection' - saveState={saveState} testId='code-injection' title="Code injection" - onCancel={handleCancel} - onEditingChange={handleEditingChange} - onSave={handleSave} - > - {isEditing && ( -
- selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} /> -
- )} - + /> ); }; diff --git a/apps/admin-x-settings/src/components/settings/advanced/code/CodeModal.tsx b/apps/admin-x-settings/src/components/settings/advanced/code/CodeModal.tsx index d4a274127c..d8094d9575 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/code/CodeModal.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/code/CodeModal.tsx @@ -1,6 +1,10 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react'; -import React, {useMemo} from 'react'; -import {CodeEditor, Modal} from '@tryghost/admin-x-design-system'; +import React, {useMemo, useRef, useState} from 'react'; +import useSettingGroup from '../../../../hooks/useSettingGroup'; +import {ButtonGroup, CodeEditor, Heading, Modal, TabView} from '@tryghost/admin-x-design-system'; +import {ReactCodeMirrorRef} from '@uiw/react-codemirror'; +import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; +import {useSaveButton} from '../../../../hooks/useSaveButton'; interface CodeModalProps { hint?: React.ReactNode; @@ -9,18 +13,91 @@ interface CodeModalProps { afterClose?: () => void } -const CodeModal: React.FC = ({hint, value, onChange, afterClose}) => { +const CodeModal: React.FC = ({afterClose}) => { + const { + localSettings, + handleSave, + updateSetting + } = useSettingGroup(); const modal = useModal(); + const [headerContent, footerContent] = getSettingValues(localSettings, ['codeinjection_head', 'codeinjection_foot']); + + const [selectedTab, setSelectedTab] = useState<'header' | 'footer'>('header'); + + const headerEditorRef = useRef(null); + const footerEditorRef = useRef(null); + const html = useMemo(() => import('@codemirror/lang-html').then(module => module.html()), []); - const onOk = () => { - modal.remove(); - afterClose?.(); + const headerProps = { + extensions: [html], + hint: 'Code here will be injected into the {{ghost_head}} tag on every page of the site', + value: headerContent || '', + onChange: (value: string) => updateSetting('codeinjection_head', value) }; - return - + const footerProps = { + extensions: [html], + hint: 'Code here will be injected into the {{ghost_foot}} tag on every page of the site', + value: footerContent || '', + onChange: (value: string) => updateSetting('codeinjection_foot', value) + }; + + const tabs = [ + { + id: 'header', + title: 'Site header', + contents: (), + tabWrapperClassName: 'flex-auto', + containerClassName: 'h-full' + }, + { + id: 'footer', + title: 'Site footer', + contents: (), + tabWrapperClassName: 'flex-auto', + containerClassName: 'h-full' + } + ] as const; + + const {savingTitle, isSaving, onSaveClick} = useSaveButton(handleSave, true); + + return } + height='full' + size='full' + testId='modal-code-injection' + > +
+
+ Code injection + { + modal.remove(); + afterClose?.(); + } + }, + { + disabled: isSaving, + label: savingTitle, + color: savingTitle === 'Saved' ? 'green' : 'black', + onClick: onSaveClick + } + ]} /> +
+ + containerClassName='flex-auto flex flex-col mb-16' + selectedTab={selectedTab} + tabs={tabs} + onTabChange={setSelectedTab} + /> +
; }; diff --git a/apps/admin-x-settings/src/hooks/useSaveButton.ts b/apps/admin-x-settings/src/hooks/useSaveButton.ts new file mode 100644 index 0000000000..ac2bce7110 --- /dev/null +++ b/apps/admin-x-settings/src/hooks/useSaveButton.ts @@ -0,0 +1,32 @@ +import {SaveHandler} from '@tryghost/admin-x-framework/hooks'; +import {useState} from 'react'; + +export const useSaveButton = (handleSave: SaveHandler, fakeWhenUnchanged?: boolean) => { + const [savingTitle, setSavingTitle] = useState('Save'); + const [isSaving, setIsSaving] = useState(false); + + const onSaveClick = async () => { + setIsSaving(true); + setSavingTitle('Saving'); + + // Execute the save operation + await handleSave({fakeWhenUnchanged}); + + // After a second, change the label to 'Saved' + setTimeout(() => { + setSavingTitle('Saved'); + + // After yet another second, reset to 'Save' + setTimeout(() => { + setSavingTitle('Save'); + setIsSaving(false); + }, 1000); + }, 1000); + }; + + return { + savingTitle, + isSaving, + onSaveClick + }; +}; diff --git a/apps/admin-x-settings/test/acceptance/advanced/codeInjection.test.ts b/apps/admin-x-settings/test/acceptance/advanced/codeInjection.test.ts index f20f844523..1397fb83a1 100644 --- a/apps/admin-x-settings/test/acceptance/advanced/codeInjection.test.ts +++ b/apps/admin-x-settings/test/acceptance/advanced/codeInjection.test.ts @@ -21,23 +21,24 @@ test.describe('Code injection settings', async () => { const section = page.getByTestId('code-injection'); - await section.getByRole('button', {name: 'Edit'}).click(); + await section.getByRole('button', {name: 'Open'}).click(); + + const modal = page.getByTestId('modal-code-injection'); // Click on the CodeMirror content to make sure it's loaded - await section.getByTestId('header-code').locator('.cm-content').click(); + await modal.getByTestId('header-code').locator('.cm-content').click(); for (const character of (PADDING + 'testhead').split('')) { await page.keyboard.press(character); } - await section.getByRole('tab', {name: 'Site footer'}).click(); - await section.getByTestId('footer-code').locator('.cm-content').click(); + await modal.getByRole('tab', {name: 'Site footer'}).click(); + await modal.getByTestId('footer-code').locator('.cm-content').click(); for (const character of (PADDING + 'testfoot').split('')) { await page.keyboard.press(character); } - await section.getByRole('button', {name: 'Save'}).click(); - await expect(section.getByRole('button', {name: 'Save'})).toBeHidden(); + await modal.getByRole('button', {name: 'Save'}).click(); expect(lastApiRequests.editSettings?.body).toMatchObject({ settings: [ @@ -46,50 +47,4 @@ test.describe('Code injection settings', async () => { ] }); }); - - test('Supports continuing editing in fullscreen', async ({page}) => { - const {lastApiRequests} = await mockApi({page, requests: { - ...globalDataRequests, - editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([ - {key: 'codeinjection_head', value: '<1 /><2 /><3 />'} - ])} - }}); - - await page.goto('/'); - - const section = page.getByTestId('code-injection'); - - await section.getByRole('button', {name: 'Edit'}).click(); - - for (const character of PADDING.split('')) { - await page.keyboard.press(character); - } - - for (const character of '<1>'.split('')) { - await page.keyboard.press(character); - } - - await section.getByRole('button', {name: 'Fullscreen'}).click(); - - await page.keyboard.press('End'); - for (const character of '<2>'.split('')) { - await page.keyboard.press(character); - } - - await page.getByTestId('modal-code').getByRole('button', {name: 'Done'}).click(); - - await page.keyboard.press('End'); - for (const character of '<3>'.split('')) { - await page.keyboard.press(character); - } - - await section.getByRole('button', {name: 'Save'}).click(); - await expect(section.getByRole('button', {name: 'Save'})).toBeHidden(); - - expect(lastApiRequests.editSettings?.body).toMatchObject({ - settings: [ - {key: 'codeinjection_head', value: /<1><2><3>$/} - ] - }); - }); });