mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -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 Heading from '../../../../admin-x-ds/global/Heading';
|
||||||
import Hint from '../../../../admin-x-ds/global/Hint';
|
import Hint from '../../../../admin-x-ds/global/Hint';
|
||||||
import ImageUpload from '../../../../admin-x-ds/global/form/ImageUpload';
|
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 SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import {SettingValue} from '../../../../api/settings';
|
import {SettingValue} from '../../../../api/settings';
|
||||||
|
import {debounce} from '../../../../utils/debounce';
|
||||||
import {getImageUrl, useUploadImage} from '../../../../api/images';
|
import {getImageUrl, useUploadImage} from '../../../../api/images';
|
||||||
|
|
||||||
export interface BrandSettingValues {
|
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 BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key: string, value: SettingValue) => void }> = ({values,updateSetting}) => {
|
||||||
const {mutateAsync: uploadImage} = useUploadImage();
|
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 (
|
return (
|
||||||
<div className='mt-7'>
|
<div className='mt-7'>
|
||||||
|
@ -26,8 +35,13 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
||||||
clearBg={true}
|
clearBg={true}
|
||||||
hint='Used in your theme, meta data and search results'
|
hint='Used in your theme, meta data and search results'
|
||||||
title='Site description'
|
title='Site description'
|
||||||
value={values.description}
|
value={siteDescription}
|
||||||
onChange={event => updateSetting('description', event.target.value)}
|
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'>
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<Heading grey={true} level={6}>Accent color</Heading>
|
<Heading grey={true} level={6}>Accent color</Heading>
|
||||||
|
@ -40,7 +54,8 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
||||||
maxLength={7}
|
maxLength={7}
|
||||||
type='color'
|
type='color'
|
||||||
value={values.accentColor}
|
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>
|
||||||
</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 expect(modal.frameLocator('[data-testid="theme-preview"]').getByText('homepage preview')).toHaveCount(1);
|
||||||
|
|
||||||
await modal.getByLabel('Site description').fill('new description');
|
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();
|
await modal.getByRole('button', {name: 'Save'}).click();
|
||||||
|
|
||||||
expect(lastPreviewRequest.previewHeader).toMatch(/&d=new\+description&/);
|
expect(lastPreviewRequest.previewHeader).toMatch(/&d=new\+description&/);
|
||||||
|
|
Loading…
Add table
Reference in a new issue