mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): race condition with gallery search
It was possible to clear the search term while a debounced setSearchTerm is still pending. This resulted in the gallery getting out of sync w/ the search term. To fix this, we need to lift the state up a bit and cancel any pending debounced setSearchTerm calls when closing the search or clearing the search term box.
This commit is contained in:
parent
c296ae8cfe
commit
9870f5a96f
@ -12,7 +12,8 @@ import {
|
|||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { galleryViewChanged, searchTermChanged } from 'features/gallery/store/gallerySlice';
|
import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useGallerySearchTerm';
|
||||||
|
import { galleryViewChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -41,8 +42,9 @@ export const Gallery = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const galleryView = useAppSelector((s) => s.gallery.galleryView);
|
const galleryView = useAppSelector((s) => s.gallery.galleryView);
|
||||||
const searchTerm = useAppSelector((s) => s.gallery.searchTerm);
|
const initialSearchTerm = useAppSelector((s) => s.gallery.searchTerm);
|
||||||
const searchDisclosure = useDisclosure({ defaultIsOpen: !!searchTerm.length });
|
const searchDisclosure = useDisclosure({ defaultIsOpen: initialSearchTerm.length > 0 });
|
||||||
|
const [searchTerm, onChangeSearchTerm, onResetSearchTerm] = useGallerySearchTerm();
|
||||||
|
|
||||||
const handleClickImages = useCallback(() => {
|
const handleClickImages = useCallback(() => {
|
||||||
dispatch(galleryViewChanged('images'));
|
dispatch(galleryViewChanged('images'));
|
||||||
@ -53,13 +55,9 @@ export const Gallery = () => {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleClickSearch = useCallback(() => {
|
const handleClickSearch = useCallback(() => {
|
||||||
if (searchTerm.length) {
|
|
||||||
dispatch(searchTermChanged(''));
|
|
||||||
searchDisclosure.onToggle();
|
searchDisclosure.onToggle();
|
||||||
} else {
|
onResetSearchTerm();
|
||||||
searchDisclosure.onToggle();
|
}, [onResetSearchTerm, searchDisclosure]);
|
||||||
}
|
|
||||||
}, [searchTerm, dispatch, searchDisclosure]);
|
|
||||||
|
|
||||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||||
const boardName = useBoardName(selectedBoardId);
|
const boardName = useBoardName(selectedBoardId);
|
||||||
@ -92,7 +90,11 @@ export const Gallery = () => {
|
|||||||
<Box w="full">
|
<Box w="full">
|
||||||
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||||
<Box w="full" pt={2}>
|
<Box w="full" pt={2}>
|
||||||
<GallerySearch />
|
<GallerySearch
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
onChangeSearchTerm={onChangeSearchTerm}
|
||||||
|
onResetSearchTerm={onResetSearchTerm}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,51 +1,37 @@
|
|||||||
import { IconButton, Input, InputGroup, InputRightElement, Spinner } from '@invoke-ai/ui-library';
|
import { IconButton, Input, InputGroup, InputRightElement, Spinner } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import { searchTermChanged } from 'features/gallery/store/gallerySlice';
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiXBold } from 'react-icons/pi';
|
import { PiXBold } from 'react-icons/pi';
|
||||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
export const GallerySearch = () => {
|
type Props = {
|
||||||
const dispatch = useAppDispatch();
|
searchTerm: string;
|
||||||
const searchTerm = useAppSelector((s) => s.gallery.searchTerm);
|
onChangeSearchTerm: (value: string) => void;
|
||||||
|
onResetSearchTerm: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GallerySearch = ({ searchTerm, onChangeSearchTerm, onResetSearchTerm }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchTermInput, setSearchTermInput] = useState(searchTerm);
|
|
||||||
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
||||||
const { isPending } = useListImagesQuery(queryArgs, {
|
const { isPending } = useListImagesQuery(queryArgs, {
|
||||||
selectFromResult: ({ isLoading, isFetching }) => ({ isPending: isLoading || isFetching }),
|
selectFromResult: ({ isLoading, isFetching }) => ({ isPending: isLoading || isFetching }),
|
||||||
});
|
});
|
||||||
const debouncedSetSearchTerm = useMemo(() => {
|
|
||||||
return debounce((value: string) => {
|
|
||||||
dispatch(searchTermChanged(value));
|
|
||||||
}, 1000);
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const handleChangeInput = useCallback(
|
const handleChangeInput = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearchTermInput(e.target.value);
|
onChangeSearchTerm(e.target.value);
|
||||||
debouncedSetSearchTerm(e.target.value);
|
|
||||||
},
|
},
|
||||||
[debouncedSetSearchTerm]
|
[onChangeSearchTerm]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClearInput = useCallback(() => {
|
|
||||||
setSearchTermInput('');
|
|
||||||
dispatch(searchTermChanged(''));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchTermInput(searchTerm);
|
|
||||||
}, [searchTerm]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('gallery.searchImages')}
|
placeholder={t('gallery.searchImages')}
|
||||||
value={searchTermInput}
|
value={searchTerm}
|
||||||
onChange={handleChangeInput}
|
onChange={handleChangeInput}
|
||||||
data-testid="image-search-input"
|
data-testid="image-search-input"
|
||||||
/>
|
/>
|
||||||
@ -54,10 +40,10 @@ export const GallerySearch = () => {
|
|||||||
<Spinner size="sm" opacity={0.5} />
|
<Spinner size="sm" opacity={0.5} />
|
||||||
</InputRightElement>
|
</InputRightElement>
|
||||||
)}
|
)}
|
||||||
{!isPending && searchTermInput.length && (
|
{!isPending && searchTerm.length && (
|
||||||
<InputRightElement h="full" pe={2}>
|
<InputRightElement h="full" pe={2}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleClearInput}
|
onClick={onResetSearchTerm}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="link"
|
variant="link"
|
||||||
aria-label={t('boards.clearSearch')}
|
aria-label={t('boards.clearSearch')}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||||
|
import { searchTermChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
export const useGallerySearchTerm = () => {
|
||||||
|
// Highlander!
|
||||||
|
useAssertSingleton('gallery-search-state');
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const searchTerm = useAppSelector((s) => s.gallery.searchTerm);
|
||||||
|
|
||||||
|
const [localSearchTerm, setLocalSearchTerm] = useState(searchTerm);
|
||||||
|
|
||||||
|
const debouncedSetSearchTerm = useMemo(() => {
|
||||||
|
return debounce((val: string) => {
|
||||||
|
dispatch(searchTermChanged(val));
|
||||||
|
}, 1000);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(val: string) => {
|
||||||
|
setLocalSearchTerm(val);
|
||||||
|
debouncedSetSearchTerm(val);
|
||||||
|
},
|
||||||
|
[debouncedSetSearchTerm]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onReset = useCallback(() => {
|
||||||
|
debouncedSetSearchTerm.cancel();
|
||||||
|
setLocalSearchTerm('');
|
||||||
|
dispatch(searchTermChanged(''));
|
||||||
|
}, [debouncedSetSearchTerm, dispatch]);
|
||||||
|
|
||||||
|
return [localSearchTerm, onChange, onReset] as const;
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user