0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Improved AdminX design settings experience (#18180)

refs https://github.com/TryGhost/Product/issues/3832

---

### <samp>🤖 Generated by Copilot at 60bbe0a</samp>

This pull request improves the performance and readability of the site
design and theme settings UI. It refactors the modal navigation logic,
the color picker field component, and the theme preview component. It
also removes unnecessary dependencies and props from the modal
components, and adds a new `DesignAndThemeModal` component to handle the
modal switching.
This commit is contained in:
Jono M 2023-09-18 12:09:26 +01:00 committed by GitHub
parent a27ba55b50
commit 79cd49c01b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 163 additions and 129 deletions

View file

@ -1,14 +1,15 @@
import ColorIndicator, {SwatchOption} from './ColorIndicator';
import ColorPicker from './ColorPicker';
import clsx from 'clsx';
import {ReactNode, createContext, useContext, useEffect, useId, useState} from 'react';
import {ReactNode, createContext, useContext, useEffect, useId, useMemo, useState} from 'react';
import {ToggleDirections} from './Toggle';
import {debounce} from '../../../utils/debounce';
const ColorPickerContext = createContext<{colorPickers: Array<{ id: string; setExpanded: ((expanded: boolean) => void) }>}>({
colorPickers: []
});
const ColorPickerField = ({testId, title, direction, value, hint, error, eyedropper, clearButtonValue, onChange, swatches = []}: {
const ColorPickerField = ({testId, title, direction, value, hint, error, eyedropper, clearButtonValue, onChange, swatches = [], alwaysOpen = false, debounceMs}: {
testId?: string;
title?: ReactNode;
direction?: ToggleDirections;
@ -19,11 +20,18 @@ const ColorPickerField = ({testId, title, direction, value, hint, error, eyedrop
clearButtonValue?: string | null;
onChange?: (newValue: string | null) => void;
swatches?: SwatchOption[];
alwaysOpen?: boolean;
debounceMs?: number;
}) => {
const [isExpanded, setExpanded] = useState(false);
const [localValue, setLocalValue] = useState(value);
const context = useContext(ColorPickerContext);
const id = useId();
useEffect(() => {
setLocalValue(value);
}, [value]);
useEffect(() => {
context.colorPickers.push({id, setExpanded});
@ -48,16 +56,29 @@ const ColorPickerField = ({testId, title, direction, value, hint, error, eyedrop
}
}, [context, id, isExpanded]);
const debouncedOnChange = useMemo(() => {
if (onChange && debounceMs) {
return debounce(onChange, debounceMs);
} else {
return onChange;
}
}, [debounceMs, onChange]);
const handleChange = (newValue: string | null) => {
setLocalValue(newValue);
debouncedOnChange?.(newValue);
};
let content = (
<ColorIndicator
isExpanded={isExpanded}
swatches={swatches}
value={value}
value={localValue}
onSwatchChange={(newValue) => {
onChange?.(newValue);
handleChange(newValue);
setExpanded(false);
}}
onTogglePicker={() => setExpanded(!isExpanded)}
onTogglePicker={() => !alwaysOpen && setExpanded(!isExpanded)}
/>
);
@ -67,7 +88,7 @@ const ColorPickerField = ({testId, title, direction, value, hint, error, eyedrop
<div className="shrink-0">
{content}
</div>
<div className={clsx('flex-1', direction === 'rtl' ? 'pr-2' : 'pl-2', hint ? 'mt-[-2px]' : 'mt-[1px]')} onClick={() => setExpanded(!isExpanded)}>
<div className={clsx('flex-1', direction === 'rtl' ? 'pr-2' : 'pl-2', hint ? 'mt-[-2px]' : 'mt-[1px]')} onClick={() => !alwaysOpen && setExpanded(!isExpanded)}>
{title}
{hint && <div className={`text-xs ${error ? 'text-red' : 'text-grey-700'}`}>{hint}</div>}
</div>
@ -75,12 +96,12 @@ const ColorPickerField = ({testId, title, direction, value, hint, error, eyedrop
);
}
let selectedSwatch = swatches.find(swatch => swatch.value === value);
let selectedSwatch = swatches.find(swatch => swatch.value === localValue);
return (
<div className="mt-2 flex-col" data-testid={testId} onClick={event => event.stopPropagation()}>
{content}
{isExpanded && <ColorPicker clearButtonValue={clearButtonValue} eyedropper={eyedropper} hexValue={selectedSwatch?.hex || value || undefined} onChange={onChange} />}
{(alwaysOpen || isExpanded) && <ColorPicker clearButtonValue={clearButtonValue} eyedropper={eyedropper} hexValue={selectedSwatch?.hex || localValue || undefined} onChange={handleChange} />}
</div>
);
};

View file

@ -28,59 +28,61 @@ export const RouteContext = createContext<RoutingContextData>({
});
export type RoutingModalProps = {
pathName: string;
params?: Record<string, string>
}
const AddIntegrationModal = () => import('../settings/advanced/integrations/AddIntegrationModal');
const AddNewsletterModal = () => import('../settings/email/newsletters/AddNewsletterModal');
const AddRecommendationModal = () => import('../settings/site/recommendations/AddRecommendationModal');
const AmpModal = () => import('../settings/advanced/integrations/AmpModal');
const ChangeThemeModal = () => import('../settings/site/ThemeModal');
const CustomIntegrationModal = () => import('../settings/advanced/integrations/CustomIntegrationModal');
const DesignModal = () => import('../settings/site/DesignModal');
const EditRecommendationModal = () => import('../settings/site/recommendations/EditRecommendationModal');
const FirstpromoterModal = () => import('../settings/advanced/integrations/FirstPromoterModal');
const HistoryModal = () => import('../settings/advanced/HistoryModal');
const InviteUserModal = () => import('../settings/general/InviteUserModal');
const NavigationModal = () => import('../settings/site/NavigationModal');
const NewsletterDetailModal = () => import('../settings/email/newsletters/NewsletterDetailModal');
const PinturaModal = () => import('../settings/advanced/integrations/PinturaModal');
const PortalModal = () => import('../settings/membership/portal/PortalModal');
const SlackModal = () => import('../settings/advanced/integrations/SlackModal');
const StripeConnectModal = () => import('../settings/membership/stripe/StripeConnectModal');
const TierDetailModal = () => import('../settings/membership/tiers/TierDetailModal');
const UnsplashModal = () => import('../settings/advanced/integrations/UnsplashModal');
const UserDetailModal = () => import('../settings/general/UserDetailModal');
const ZapierModal = () => import('../settings/advanced/integrations/ZapierModal');
const AnnouncementBarModal = () => import('../settings/site/AnnouncementBarModal');
const EmbedSignupFormModal = () => import('../settings/membership/embedSignup/EmbedSignupFormModal');
const modals: {[key: string]: () => Promise<{default: React.FC<NiceModalHocProps & RoutingModalProps>}>} = {
AddIntegrationModal: () => import('../settings/advanced/integrations/AddIntegrationModal'),
AddNewsletterModal: () => import('../settings/email/newsletters/AddNewsletterModal'),
AddRecommendationModal: () => import('../settings/site/recommendations/AddRecommendationModal'),
AmpModal: () => import('../settings/advanced/integrations/AmpModal'),
CustomIntegrationModal: () => import('../settings/advanced/integrations/CustomIntegrationModal'),
DesignAndThemeModal: () => import('../settings/site/DesignAndThemeModal'),
EditRecommendationModal: () => import('../settings/site/recommendations/EditRecommendationModal'),
FirstpromoterModal: () => import('../settings/advanced/integrations/FirstPromoterModal'),
HistoryModal: () => import('../settings/advanced/HistoryModal'),
InviteUserModal: () => import('../settings/general/InviteUserModal'),
NavigationModal: () => import('../settings/site/NavigationModal'),
NewsletterDetailModal: () => import('../settings/email/newsletters/NewsletterDetailModal'),
PinturaModal: () => import('../settings/advanced/integrations/PinturaModal'),
PortalModal: () => import('../settings/membership/portal/PortalModal'),
SlackModal: () => import('../settings/advanced/integrations/SlackModal'),
StripeConnectModal: () => import('../settings/membership/stripe/StripeConnectModal'),
TierDetailModal: () => import('../settings/membership/tiers/TierDetailModal'),
UnsplashModal: () => import('../settings/advanced/integrations/UnsplashModal'),
UserDetailModal: () => import('../settings/general/UserDetailModal'),
ZapierModal: () => import('../settings/advanced/integrations/ZapierModal'),
AnnouncementBarModal: () => import('../settings/site/AnnouncementBarModal'),
EmbedSignupFormModal: () => import('../settings/membership/embedSignup/EmbedSignupFormModal')
};
const modalPaths: {[key: string]: () => Promise<{default: React.FC<NiceModalHocProps & RoutingModalProps>}>} = {
'design/edit/themes': ChangeThemeModal,
'design/edit': DesignModal,
'navigation/edit': NavigationModal,
'users/invite': InviteUserModal,
'users/show/:slug': UserDetailModal,
'portal/edit': PortalModal,
'tiers/add': TierDetailModal,
'tiers/show/:id': TierDetailModal,
'stripe-connect': StripeConnectModal,
'newsletters/add': AddNewsletterModal,
'newsletters/show/:id': NewsletterDetailModal,
'history/view': HistoryModal,
'history/view/:user': HistoryModal,
'integrations/zapier': ZapierModal,
'integrations/slack': SlackModal,
'integrations/amp': AmpModal,
'integrations/unsplash': UnsplashModal,
'integrations/firstpromoter': FirstpromoterModal,
'integrations/pintura': PinturaModal,
'integrations/add': AddIntegrationModal,
'integrations/show/:id': CustomIntegrationModal,
'recommendations/add': AddRecommendationModal,
'recommendations/:id': EditRecommendationModal,
'announcement-bar/edit': AnnouncementBarModal,
'embed-signup-form/show': EmbedSignupFormModal
const modalPaths: {[key: string]: keyof typeof modals} = {
'design/edit/themes': 'DesignAndThemeModal',
'design/edit': 'DesignAndThemeModal',
'navigation/edit': 'NavigationModal',
'users/invite': 'InviteUserModal',
'users/show/:slug': 'UserDetailModal',
'portal/edit': 'PortalModal',
'tiers/add': 'TierDetailModal',
'tiers/show/:id': 'TierDetailModal',
'stripe-connect': 'StripeConnectModal',
'newsletters/add': 'AddNewsletterModal',
'newsletters/show/:id': 'NewsletterDetailModal',
'history/view': 'HistoryModal',
'history/view/:user': 'HistoryModal',
'integrations/zapier': 'ZapierModal',
'integrations/slack': 'SlackModal',
'integrations/amp': 'AmpModal',
'integrations/unsplash': 'UnsplashModal',
'integrations/firstpromoter': 'FirstpromoterModal',
'integrations/pintura': 'PinturaModal',
'integrations/add': 'AddIntegrationModal',
'integrations/show/:id': 'CustomIntegrationModal',
'recommendations/add': 'AddRecommendationModal',
'recommendations/:id': 'EditRecommendationModal',
'announcement-bar/edit': 'AnnouncementBarModal',
'embed-signup-form/show': 'EmbedSignupFormModal'
};
function getHashPath(urlPath: string | undefined) {
@ -97,7 +99,7 @@ function getHashPath(urlPath: string | undefined) {
return null;
}
const handleNavigation = () => {
const handleNavigation = (currentRoute: string | undefined) => {
// Get the hash from the URL
let hash = window.location.hash;
hash = hash.substring(1);
@ -109,13 +111,15 @@ const handleNavigation = () => {
const pathName = getHashPath(url.pathname);
if (pathName) {
const [path, modal] = Object.entries(modalPaths).find(([modalPath]) => matchRoute(pathName, modalPath)) || [];
const [, currentModalName] = Object.entries(modalPaths).find(([modalPath]) => matchRoute(currentRoute || '', modalPath)) || [];
const [path, modalName] = Object.entries(modalPaths).find(([modalPath]) => matchRoute(pathName, modalPath)) || [];
return {
pathName,
modal: (path && modal) ?
modal().then(({default: component}) => {
NiceModal.show(component, {params: matchRoute(pathName, path)});
changingModal: modalName && modalName !== currentModalName,
modal: (path && modalName) ?
modals[modalName]().then(({default: component}) => {
NiceModal.show(component, {pathName, params: matchRoute(pathName, path)});
}) :
undefined
};
@ -143,7 +147,7 @@ const RoutingProvider: React.FC<RouteProviderProps> = ({externalNavigate, childr
useEffect(() => {
// Preload all the modals after initial render to avoid a delay when opening them
setTimeout(() => {
Object.values(modalPaths).forEach(modal => modal());
Object.values(modalPaths).forEach(modal => modals[modal]());
}, 1000);
}, []);
@ -166,13 +170,16 @@ const RoutingProvider: React.FC<RouteProviderProps> = ({externalNavigate, childr
useEffect(() => {
const handleHashChange = () => {
const {pathName, modal} = handleNavigation();
setRoute(pathName);
setRoute((currentRoute) => {
const {pathName, modal, changingModal} = handleNavigation(currentRoute);
if (modal) {
setLoadingModal(true);
modal.then(() => setLoadingModal(false));
}
if (modal && changingModal) {
setLoadingModal(true);
modal.then(() => setLoadingModal(false));
}
return pathName;
});
};
handleHashChange();

View file

@ -0,0 +1,18 @@
import ChangeThemeModal from './ThemeModal';
import DesignModal from './DesignModal';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import {RoutingModalProps} from '../../providers/RoutingProvider';
const DesignAndThemeModal: React.FC<RoutingModalProps> = ({pathName}) => {
const modal = useModal();
if (pathName === 'design/edit') {
return <DesignModal />;
} else if (pathName === 'design/edit/themes') {
return <ChangeThemeModal />;
} else {
modal.remove();
}
};
export default NiceModal.create(DesignAndThemeModal);

View file

@ -2,7 +2,6 @@ import BrandSettings, {BrandSettingValues} from './designAndBranding/BrandSettin
// import Button from '../../../admin-x-ds/global/Button';
// import ChangeThemeModal from './ThemeModal';
import Icon from '../../../admin-x-ds/global/Icon';
import NiceModal, {NiceModalHandler, useModal} from '@ebay/nice-modal-react';
import React, {useEffect, useState} from 'react';
import StickyFooter from '../../../admin-x-ds/global/StickyFooter';
import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
@ -21,7 +20,6 @@ import {useGlobalData} from '../../providers/GlobalDataProvider';
const Sidebar: React.FC<{
brandSettings: BrandSettingValues
themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}>
modal: NiceModalHandler<Record<string, unknown>>;
updateBrandSetting: (key: string, value: SettingValue) => void
updateThemeSetting: (updated: CustomThemeSetting) => void
onTabChange: (id: string) => void
@ -29,7 +27,6 @@ const Sidebar: React.FC<{
}> = ({
brandSettings,
themeSettingSections,
modal,
updateBrandSetting,
updateThemeSetting,
onTabChange,
@ -68,7 +65,6 @@ const Sidebar: React.FC<{
<div className='w-full px-7'>
<button className='group flex w-full items-center justify-between text-sm font-medium opacity-80 transition-all hover:opacity-100' data-testid='change-theme' type='button' onClick={async () => {
await handleSave();
modal.remove();
updateRoute('design/edit/themes');
}}>
<div className='text-left'>
@ -84,8 +80,6 @@ const Sidebar: React.FC<{
};
const DesignModal: React.FC = () => {
const modal = useModal();
const {settings, siteData} = useGlobalData();
const {mutateAsync: editSettings} = useEditSettings();
const {data: {posts: [latestPost]} = {posts: []}} = useBrowsePosts({
@ -110,10 +104,10 @@ const DesignModal: React.FC = () => {
} = useForm({
initialState: {
settings: settings as Array<Setting & { dirty?: boolean }>,
themeSettings: (themeSettings?.custom_theme_settings || []) as Array<CustomThemeSetting & { dirty?: boolean }>
themeSettings: themeSettings ? (themeSettings.custom_theme_settings as Array<CustomThemeSetting & { dirty?: boolean }>) : undefined
},
onSave: async () => {
if (formState.themeSettings.some(setting => setting.dirty)) {
if (formState.themeSettings?.some(setting => setting.dirty)) {
const response = await editThemeSettings(formState.themeSettings);
updateForm(state => ({...state, themeSettings: response.custom_theme_settings}));
}
@ -138,14 +132,14 @@ const DesignModal: React.FC = () => {
};
const updateThemeSetting = (updated: CustomThemeSetting) => {
updateForm(state => ({...state, themeSettings: state.themeSettings.map(setting => (
updateForm(state => ({...state, themeSettings: state.themeSettings?.map(setting => (
setting.key === updated.key ? {...updated, dirty: true} : setting
))}));
};
const [description, accentColor, icon, logo, coverImage] = getSettingValues(formState.settings, ['description', 'accent_color', 'icon', 'logo', 'cover_image']) as string[];
const themeSettingGroups = formState.themeSettings.reduce((groups, setting) => {
const themeSettingGroups = (formState.themeSettings || []).reduce((groups, setting) => {
const group = (setting.group === 'homepage' || setting.group === 'post') ? setting.group : 'site-wide';
return {
@ -208,7 +202,6 @@ const DesignModal: React.FC = () => {
<Sidebar
brandSettings={{description, accentColor, icon, logo, coverImage}}
handleSave={handleSave}
modal={modal}
themeSettingSections={themeSettingSections}
updateBrandSetting={updateBrandSetting}
updateThemeSetting={updateThemeSetting}
@ -240,4 +233,4 @@ const DesignModal: React.FC = () => {
/>;
};
export default NiceModal.create(DesignModal);
export default DesignModal;

View file

@ -37,7 +37,6 @@ interface ThemeModalContentProps {
const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
currentTab,
setCurrentTab,
modal,
themes
}) => {
const {updateRoute} = useRouting();
@ -63,7 +62,6 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
const onClose = () => {
updateRoute('design/edit');
modal.remove();
};
const handleThemeUpload = async ({
@ -216,7 +214,7 @@ const ThemeModalContent: React.FC<ThemeModalContentProps> = ({
return null;
};
const ChangeThemeModal = NiceModal.create(() => {
const ChangeThemeModal = () => {
const [currentTab, setCurrentTab] = useState('official');
const [selectedTheme, setSelectedTheme] = useState<OfficialTheme|null>(null);
const [previewMode, setPreviewMode] = useState('desktop');
@ -291,7 +289,6 @@ const ChangeThemeModal = NiceModal.create(() => {
installedTheme: installedTheme!,
onActivate: () => {
updateRoute('design/edit');
modal.remove();
}
});
};
@ -323,7 +320,6 @@ const ChangeThemeModal = NiceModal.create(() => {
}}
onClose={() => {
updateRoute('design/edit');
modal.remove();
}}
onInstall={onInstall} />
}
@ -348,6 +344,6 @@ const ChangeThemeModal = NiceModal.create(() => {
</div>
</Modal>
);
});
};
export default ChangeThemeModal;

View file

@ -6,10 +6,9 @@ import React, {useRef, useState} from 'react';
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../../admin-x-ds/global/form/TextField';
import usePinturaEditor from '../../../../hooks/usePinturaEditor';
import {SettingValue} from '../../../../api/settings';
import {SettingValue, getSettingValues} from '../../../../api/settings';
import {debounce} from '../../../../utils/debounce';
import {getImageUrl, useUploadImage} from '../../../../api/images';
import {getSettingValues} from '../../../../api/settings';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
export interface BrandSettingValues {
@ -33,7 +32,6 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
updateSetting('description', value);
}, 500)
);
const updateSettingDebounced = debounce(updateSetting, 500);
const pinturaEnabled = Boolean(pintura) && Boolean(pinturaJsUrl) && Boolean(pinturaCssUrl);
@ -64,11 +62,13 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
}}
/>
<ColorPickerField
debounceMs={200}
direction='rtl'
title={<Heading className='mt-[3px]' grey={true} level={6}>Accent color</Heading>}
value={values.accentColor}
alwaysOpen
// we debounce this because the color picker fires a lot of events.
onChange={value => updateSettingDebounced('accent_color', value)}
onChange={value => updateSetting('accent_color', value)}
/>
<div className={`flex justify-between ${values.icon ? 'items-start ' : 'items-end'}`}>
<div>

View file

@ -1,4 +1,5 @@
import React, {useEffect, useRef} from 'react';
import IframeBuffering from '../../../../utils/IframeBuffering';
import React, {useCallback} from 'react';
import {CustomThemeSetting} from '../../../../api/customThemeSettings';
type BrandSettings = {
@ -7,7 +8,7 @@ type BrandSettings = {
icon: string;
logo: string;
coverImage: string;
themeSettings: Array<CustomThemeSetting & { dirty?: boolean }>;
themeSettings?: Array<CustomThemeSetting & { dirty?: boolean }>;
}
interface ThemePreviewProps {
@ -28,8 +29,13 @@ function getPreviewData({
icon: string;
logo: string;
coverImage: string;
themeSettings: Array<CustomThemeSetting & { dirty?: boolean }>,
}): string {
themeSettings?: Array<CustomThemeSetting & { dirty?: boolean }>,
}) {
// Don't render twice while theme settings are loading
if (!themeSettings) {
return;
}
const params = new URLSearchParams();
params.append('c', accentColor);
params.append('d', description);
@ -48,10 +54,10 @@ function getPreviewData({
}
const ThemePreview: React.FC<ThemePreviewProps> = ({settings,url}) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const previewData = getPreviewData({...settings});
useEffect(() => {
if (!url) {
const injectContentIntoIframe = useCallback((iframe: HTMLIFrameElement) => {
if (!url || !previewData) {
return;
}
@ -60,9 +66,7 @@ const ThemePreview: React.FC<ThemePreviewProps> = ({settings,url}) => {
method: 'POST',
headers: {
'Content-Type': 'text/html;charset=utf-8',
'x-ghost-preview': getPreviewData({
...settings
}),
'x-ghost-preview': previewData,
Accept: 'text/plain',
mode: 'cors',
credentials: 'include'
@ -86,27 +90,22 @@ const ThemePreview: React.FC<ThemePreviewProps> = ({settings,url}) => {
// Send the data to the iframe's window using postMessage
// Inject the received content into the iframe
const iframe = iframeRef.current;
if (iframe) {
iframe.contentDocument?.open();
iframe.contentDocument?.write(finalDoc);
iframe.contentDocument?.close();
}
})
.catch(() => {
// handle error in fetching data
iframe.contentDocument?.open();
iframe.contentDocument?.write(finalDoc);
iframe.contentDocument?.close();
});
}, [url, settings]);
}, [previewData, url]);
return (
<>
<iframe
ref={iframeRef}
className='h-[110%] w-[110%] origin-top-left scale-[.90909] max-[1600px]:h-[130%] max-[1600px]:w-[130%] max-[1600px]:scale-[.76923]'
data-testid="theme-preview"
height="100%"
title="Site Preview"
></iframe>
</>
<IframeBuffering
addDelay={false}
className="absolute h-[110%] w-[110%] origin-top-left scale-[.90909] max-[1600px]:h-[130%] max-[1600px]:w-[130%] max-[1600px]:scale-[.76923]"
generateContent={injectContentIntoIframe}
height='100%'
parentClassName="relative h-full w-full"
testId="theme-preview"
width='100%'
/>
);
};

View file

@ -8,7 +8,6 @@ import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupCon
import TextField from '../../../../admin-x-ds/global/form/TextField';
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
import {CustomThemeSetting} from '../../../../api/customThemeSettings';
import {debounce} from '../../../../utils/debounce';
import {getImageUrl, useUploadImage} from '../../../../api/images';
import {humanizeSettingKey} from '../../../../api/settings';
@ -23,8 +22,6 @@ const ThemeSetting: React.FC<{
setSetting(imageUrl);
};
const updateSettingDebounced = debounce(setSetting, 500);
switch (setting.type) {
case 'text':
return (
@ -58,11 +55,12 @@ const ThemeSetting: React.FC<{
case 'color':
return (
<ColorPickerField
debounceMs={200}
direction='rtl'
hint={setting.description}
title={humanizeSettingKey(setting.key)}
value={setting.value || ''}
onChange={value => updateSettingDebounced(value)}
onChange={value => setSetting(value)}
/>
);
case 'image':

View file

@ -49,6 +49,7 @@ const IframeBuffering: React.FC<IframeBufferingProps> = ({generateContent, class
<iframe
ref={iframes[0]}
className={`${className} ${visibleIframeIndex !== 0 ? 'z-10 opacity-0' : 'z-20 opacity-100'}`}
data-visible={visibleIframeIndex === 0}
frameBorder="0"
height={height}
title="Buffered Preview 1"
@ -58,6 +59,7 @@ const IframeBuffering: React.FC<IframeBufferingProps> = ({generateContent, class
<iframe
ref={iframes[1]}
className={`${className} ${visibleIframeIndex !== 1 ? 'z-10 opacity-0' : 'z-20 opacity-100'}`}
data-visible={visibleIframeIndex === 1}
frameBorder="0"
height={height}
title="Buffered Preview 2"

View file

@ -29,11 +29,11 @@ test.describe('Design settings', async () => {
// Homepage and post preview
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('homepage preview')).toHaveCount(1);
await expect(modal.frameLocator('[data-testid="theme-preview"] iframe[data-visible=true]').getByText('homepage preview')).toHaveCount(1);
await modal.getByTestId('design-toolbar').getByRole('tab', {name: 'Post'}).click();
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('post preview')).toHaveCount(1);
await expect(modal.frameLocator('[data-testid="theme-preview"] iframe[data-visible=true]').getByText('post preview')).toHaveCount(1);
// Desktop and mobile preview
@ -49,11 +49,11 @@ test.describe('Design settings', async () => {
await modal.getByTestId('design-setting-tabs').getByRole('tab', {name: 'Homepage'}).click();
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('homepage preview')).toHaveCount(1);
await expect(modal.frameLocator('[data-testid="theme-preview"] iframe[data-visible=true]').getByText('homepage preview')).toHaveCount(1);
await modal.getByTestId('design-setting-tabs').getByRole('tab', {name: 'Post'}).click();
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('post preview')).toHaveCount(1);
await expect(modal.frameLocator('[data-testid="theme-preview"] iframe[data-visible=true]').getByText('post preview')).toHaveCount(1);
});
test('Editing brand settings', async ({page}) => {
@ -77,7 +77,7 @@ test.describe('Design settings', async () => {
const modal = page.getByTestId('design-modal');
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('homepage preview')).toHaveCount(1);
await expect(modal.frameLocator('[data-testid="theme-preview"] iframe[data-visible=true]').getByText('homepage preview')).toHaveCount(1);
await modal.getByLabel('Site description').fill('new description');
// set timeout of 500ms to wait for the debounce