diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx index feb5a9c0e1..8776fa888b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx @@ -12,7 +12,8 @@ import { useDisclosure, } from '@invoke-ai/ui-library'; 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 { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -41,8 +42,9 @@ export const Gallery = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const galleryView = useAppSelector((s) => s.gallery.galleryView); - const searchTerm = useAppSelector((s) => s.gallery.searchTerm); - const searchDisclosure = useDisclosure({ defaultIsOpen: !!searchTerm.length }); + const initialSearchTerm = useAppSelector((s) => s.gallery.searchTerm); + const searchDisclosure = useDisclosure({ defaultIsOpen: initialSearchTerm.length > 0 }); + const [searchTerm, onChangeSearchTerm, onResetSearchTerm] = useGallerySearchTerm(); const handleClickImages = useCallback(() => { dispatch(galleryViewChanged('images')); @@ -53,13 +55,9 @@ export const Gallery = () => { }, [dispatch]); const handleClickSearch = useCallback(() => { - if (searchTerm.length) { - dispatch(searchTermChanged('')); - searchDisclosure.onToggle(); - } else { - searchDisclosure.onToggle(); - } - }, [searchTerm, dispatch, searchDisclosure]); + searchDisclosure.onToggle(); + onResetSearchTerm(); + }, [onResetSearchTerm, searchDisclosure]); const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId); const boardName = useBoardName(selectedBoardId); @@ -92,7 +90,11 @@ export const Gallery = () => { - + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySearch.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySearch.tsx index d4f546e1ed..3990495106 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySearch.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySearch.tsx @@ -1,51 +1,37 @@ 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 { searchTermChanged } from 'features/gallery/store/gallerySlice'; -import { debounce } from 'lodash-es'; import type { ChangeEvent } from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiXBold } from 'react-icons/pi'; import { useListImagesQuery } from 'services/api/endpoints/images'; -export const GallerySearch = () => { - const dispatch = useAppDispatch(); - const searchTerm = useAppSelector((s) => s.gallery.searchTerm); +type Props = { + searchTerm: string; + onChangeSearchTerm: (value: string) => void; + onResetSearchTerm: () => void; +}; + +export const GallerySearch = ({ searchTerm, onChangeSearchTerm, onResetSearchTerm }: Props) => { const { t } = useTranslation(); - const [searchTermInput, setSearchTermInput] = useState(searchTerm); const queryArgs = useAppSelector(selectListImagesQueryArgs); const { isPending } = useListImagesQuery(queryArgs, { selectFromResult: ({ isLoading, isFetching }) => ({ isPending: isLoading || isFetching }), }); - const debouncedSetSearchTerm = useMemo(() => { - return debounce((value: string) => { - dispatch(searchTermChanged(value)); - }, 1000); - }, [dispatch]); const handleChangeInput = useCallback( (e: ChangeEvent) => { - setSearchTermInput(e.target.value); - debouncedSetSearchTerm(e.target.value); + onChangeSearchTerm(e.target.value); }, - [debouncedSetSearchTerm] + [onChangeSearchTerm] ); - const handleClearInput = useCallback(() => { - setSearchTermInput(''); - dispatch(searchTermChanged('')); - }, [dispatch]); - - useEffect(() => { - setSearchTermInput(searchTerm); - }, [searchTerm]); - return ( @@ -54,10 +40,10 @@ export const GallerySearch = () => { )} - {!isPending && searchTermInput.length && ( + {!isPending && searchTerm.length && ( { + // 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; +};