mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
Added debounce to design modal on Admin X (#17793)
refs https://github.com/TryGhost/Product/issues/3349 - When updating certain states, eg the branding colour using or a typing in a text box, we want it display on the preview almost immediately. However this comes with a drawback of sending a ton of requests to the server. - This fix adds debouncing which essentially adds a small delay of 500ms, to wait for the user to finish typing / selecting colour before making a request. --- <!-- Leave the line below if you'd like GitHub Copilot to generate a summary from your commit --> <!-- copilot:summary --> ### <samp>🤖 Generated by Copilot at 4e623ff</samp> Improved the performance and user experience of the site description and accent color settings by debouncing the backend updates. Added a `debounce` utility function in `debounce.ts`.
This commit is contained in:
parent
50147c6a67
commit
429e8ed4d9
3 changed files with 45 additions and 4 deletions
|
@ -1,10 +1,11 @@
|
|||
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||
import Hint from '../../../../admin-x-ds/global/Hint';
|
||||
import ImageUpload from '../../../../admin-x-ds/global/form/ImageUpload';
|
||||
import React from 'react';
|
||||
import React, {useRef, useState} from 'react';
|
||||
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import {SettingValue} from '../../../../api/settings';
|
||||
import {debounce} from '../../../../utils/debounce';
|
||||
import {getImageUrl, useUploadImage} from '../../../../api/images';
|
||||
|
||||
export interface BrandSettingValues {
|
||||
|
@ -17,6 +18,14 @@ export interface BrandSettingValues {
|
|||
|
||||
const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key: string, value: SettingValue) => void }> = ({values,updateSetting}) => {
|
||||
const {mutateAsync: uploadImage} = useUploadImage();
|
||||
const [siteDescription, setSiteDescription] = useState(values.description);
|
||||
|
||||
const updateDescriptionDebouncedRef = useRef(
|
||||
debounce((value: string) => {
|
||||
updateSetting('description', value);
|
||||
}, 500)
|
||||
);
|
||||
const updateSettingDebounced = debounce(updateSetting, 500);
|
||||
|
||||
return (
|
||||
<div className='mt-7'>
|
||||
|
@ -26,8 +35,13 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
|||
clearBg={true}
|
||||
hint='Used in your theme, meta data and search results'
|
||||
title='Site description'
|
||||
value={values.description}
|
||||
onChange={event => updateSetting('description', event.target.value)}
|
||||
value={siteDescription}
|
||||
onChange={(event) => {
|
||||
// Immediately update the local state
|
||||
setSiteDescription(event.target.value);
|
||||
// Debounce the updateSetting call
|
||||
updateDescriptionDebouncedRef.current(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<Heading grey={true} level={6}>Accent color</Heading>
|
||||
|
@ -40,7 +54,8 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
|||
maxLength={7}
|
||||
type='color'
|
||||
value={values.accentColor}
|
||||
onChange={event => updateSetting('accent_color', event.target.value)}
|
||||
// we debounce this because the color picker fires a lot of events.
|
||||
onChange={event => updateSettingDebounced('accent_color', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
24
apps/admin-x-settings/src/utils/debounce.ts
Normal file
24
apps/admin-x-settings/src/utils/debounce.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
export function debounce<T extends unknown[]>(func: (...args: T) => void, wait: number, immediate: boolean = false): (...args: T) => void {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null;
|
||||
|
||||
return function (this: unknown, ...args: T): void {
|
||||
const later = () => {
|
||||
timeoutId = null;
|
||||
if (!immediate) {
|
||||
func.apply(this, args);
|
||||
}
|
||||
};
|
||||
|
||||
const callNow = immediate && !timeoutId;
|
||||
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(later, wait);
|
||||
|
||||
if (callNow) {
|
||||
func.apply(this, args);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -80,6 +80,8 @@ test.describe('Design settings', async () => {
|
|||
await expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('homepage preview')).toHaveCount(1);
|
||||
|
||||
await modal.getByLabel('Site description').fill('new description');
|
||||
// set timeout of 500ms to wait for the debounce
|
||||
await page.waitForTimeout(500);
|
||||
await modal.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
expect(lastPreviewRequest.previewHeader).toMatch(/&d=new\+description&/);
|
||||
|
|
Loading…
Reference in a new issue