0
Fork 0
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:
Ronald Langeveld 2023-08-23 13:59:43 +02:00 committed by GitHub
parent 50147c6a67
commit 429e8ed4d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 45 additions and 4 deletions

View file

@ -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>

View 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);
}
};
}

View file

@ -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&/);