From 7aa0452ba36f7cdc182eb991e04c03addf57b4f0 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 15 Dec 2023 03:30:42 +0800 Subject: [PATCH] [ENG-1503] Close search bar when navigating into folder (#1890) * track search state differently and key location explorer to path * Update ViewItem.tsx * remove useless memoisation --- .../app/$libraryId/Explorer/View/ViewItem.tsx | 20 +- .../app/$libraryId/Search/AppliedFilters.tsx | 2 +- interface/app/$libraryId/Search/SearchBar.tsx | 18 +- interface/app/$libraryId/Search/index.tsx | 4 +- interface/app/$libraryId/Search/useSearch.ts | 20 +- interface/app/$libraryId/location/$id.tsx | 188 ++++++++++++------ interface/app/$libraryId/tag/$id.tsx | 3 +- 7 files changed, 165 insertions(+), 90 deletions(-) diff --git a/interface/app/$libraryId/Explorer/View/ViewItem.tsx b/interface/app/$libraryId/Explorer/View/ViewItem.tsx index 64b171040..7ae493c8a 100644 --- a/interface/app/$libraryId/Explorer/View/ViewItem.tsx +++ b/interface/app/$libraryId/Explorer/View/ViewItem.tsx @@ -1,5 +1,9 @@ import { useCallback, type HTMLAttributes, type PropsWithChildren } from 'react'; -import { createSearchParams, useNavigate } from 'react-router-dom'; +import { + createSearchParams, + useNavigate, + useSearchParams as useRawSearchParams +} from 'react-router-dom'; import { isPath, useLibraryContext, @@ -24,6 +28,7 @@ export const useViewItemDoubleClick = () => { const explorer = useExplorerContext(); const { library } = useLibraryContext(); const { openFilePaths, openEphemeralFiles } = usePlatform(); + const [_, setSearchParams] = useRawSearchParams(); const updateAccessTime = useLibraryMutation('files.updateAccessTime'); @@ -110,11 +115,14 @@ export const useViewItemDoubleClick = () => { if (items.dirs.length > 0) { const [item] = items.dirs; if (item) { - navigate({ - pathname: `../location/${item.location_id}`, - search: createSearchParams({ - path: `${item.materialized_path}${item.name}/` - }).toString() + setSearchParams((p) => { + const newParams = new URLSearchParams(); + + newParams.set('path', `${item.materialized_path}${item.name}/`); + const take = p.get('take'); + if (take !== null) newParams.set('take', take); + + return newParams; }); return; } diff --git a/interface/app/$libraryId/Search/AppliedFilters.tsx b/interface/app/$libraryId/Search/AppliedFilters.tsx index 941fd0d8c..260e6d8e7 100644 --- a/interface/app/$libraryId/Search/AppliedFilters.tsx +++ b/interface/app/$libraryId/Search/AppliedFilters.tsx @@ -62,7 +62,7 @@ export const AppliedFilters = ({ allowRemove = true }: { allowRemove?: boolean } {search.search} - {allowRemove && search.setRawSearch('')} />} + {allowRemove && search.setSearch('')} />} )}
{ }; }, [blurHandler, focusHandler]); - const [value, setValue] = useState(search.search); + const [value, setValue] = useState(''); + + useEffect(() => { + setValue(search.rawSearch); + }, [search.rawSearch]); + + const updateDebounce = useDebouncedCallback((value: string) => { + search.setSearch(value); + }, 300); function updateValue(value: string) { setValue(value); - search.setSearch(value); + updateDebounce(value); } function clearValue() { - setValue(''); search.setSearch(''); } @@ -69,10 +77,10 @@ export default () => { onBlur={() => { if (search.rawSearch === '' && !searchStore.interactingWithSearchOptions) { clearValue(); - search.setOpen(false); + search.setSearchBarFocused(false); } }} - onFocus={() => search.setOpen(true)} + onFocus={() => search.setSearchBarFocused(true)} right={
{ diff --git a/interface/app/$libraryId/Search/index.tsx b/interface/app/$libraryId/Search/index.tsx index 5aa9c6d78..8153c43ef 100644 --- a/interface/app/$libraryId/Search/index.tsx +++ b/interface/app/$libraryId/Search/index.tsx @@ -292,14 +292,14 @@ function EscapeButton() { useKeybind(['Escape'], () => { search.setSearch(''); - search.setOpen(false); + search.setSearchBarFocused(false); }); return ( { search.setSearch(''); - search.setOpen(false); + search.setSearchBarFocused(false); }} className="ml-2 rounded-lg border border-app-line bg-app-box px-2 py-1 text-[10.5px] tracking-widest shadow" > diff --git a/interface/app/$libraryId/Search/useSearch.ts b/interface/app/$libraryId/Search/useSearch.ts index 6e4a633b2..f81207ecb 100644 --- a/interface/app/$libraryId/Search/useSearch.ts +++ b/interface/app/$libraryId/Search/useSearch.ts @@ -1,7 +1,6 @@ import { produce } from 'immer'; import { useCallback, useMemo, useState } from 'react'; import { useDebouncedValue } from 'rooks'; -import { useDebouncedCallback } from 'use-debounce'; import { SearchFilterArgs } from '@sd/client'; import { filterRegistry } from './Filters'; @@ -22,8 +21,7 @@ export interface UseSearchProps { } export function useSearch(props?: UseSearchProps) { - const [open, setOpen] = useState(false); - if (props?.open !== undefined && open !== props.open) setOpen(props.open); + const [searchBarFocused, setSearchBarFocused] = useState(false); const searchState = useSearchStore(); @@ -123,14 +121,16 @@ export function useSearch(props?: UseSearchProps) { // Filters generated from the search query // rawSearch should only ever be read by the search input - const [search, setSearch] = useState(props?.search ?? ''); + const [rawSearch, setRawSearch] = useState(props?.search ?? ''); const [searchFromProps, setSearchFromProps] = useState(props?.search); if (searchFromProps !== props?.search) { setSearchFromProps(props?.search); - setSearch(props?.search ?? ''); + setRawSearch(props?.search ?? ''); } + const [search] = useDebouncedValue(rawSearch, 300); + const searchFilters = useMemo(() => { const [name, ext] = search.split('.') ?? []; @@ -166,14 +166,14 @@ export function useSearch(props?: UseSearchProps) { }, [allFiltersAsOptions]); return { - open, - setOpen, + open: props?.open || searchBarFocused, fixedFilters, fixedFiltersKeys, search, - rawSearch: search, - setRawSearch: setSearch, - setSearch: useDebouncedCallback(setSearch, 300), + rawSearch, + setSearch: setRawSearch, + searchBarFocused, + setSearchBarFocused, dynamicFilters, setDynamicFilters, updateDynamicFilters, diff --git a/interface/app/$libraryId/location/$id.tsx b/interface/app/$libraryId/location/$id.tsx index f2a293795..1c9dca81d 100644 --- a/interface/app/$libraryId/location/$id.tsx +++ b/interface/app/$libraryId/location/$id.tsx @@ -1,5 +1,6 @@ import { ArrowClockwise, Info } from '@phosphor-icons/react'; -import { useEffect, useMemo } from 'react'; +import { memo, useEffect, useMemo } from 'react'; +import { useSearchParams as useRawSearchParams } from 'react-router-dom'; import { stringify } from 'uuid'; import { arraysEqual, @@ -32,7 +33,7 @@ import { ExplorerContextProvider } from '../Explorer/Context'; import { usePathsExplorerQuery } from '../Explorer/queries'; import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store'; import { DefaultTopBarOptions } from '../Explorer/TopBarOptions'; -import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer'; +import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer'; import { useExplorerSearchParams } from '../Explorer/util'; import { EmptyNotice } from '../Explorer/View/EmptyNotice'; import SearchOptions, { SearchContextProvider, useSearch } from '../Search'; @@ -43,6 +44,7 @@ import LocationOptions from './LocationOptions'; export const Component = () => { const { id: locationId } = useZodRouteParams(LocationIdParamsSchema); + const [{ path }] = useExplorerSearchParams(); const result = useLibraryQuery(['locations.get', locationId], { keepPreviousData: true, suspense: true @@ -50,12 +52,12 @@ export const Component = () => { useNodes(result.data?.nodes); const location = useCache(result.data?.item); - return ; + // 'key' allows search state to be thrown out when entering a folder + return ; }; const LocationExplorer = ({ location }: { location: Location; path?: string }) => { const [{ path, take }] = useExplorerSearchParams(); - const rspc = useRspcLibraryContext(); const onlineLocations = useOnlineLocations(); @@ -67,71 +69,13 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) = return onlineLocations.some((l) => arraysEqual(pub_id, l)); }, [location.pub_id, onlineLocations]); - const preferences = useLibraryQuery(['preferences.get']); - const updatePreferences = useLibraryMutation('preferences.update'); - - const settings = useMemo(() => { - const defaults = createDefaultExplorerSettings({ - order: { field: 'name', value: 'Asc' } - }); - - if (!location) return defaults; - - const pubId = stringify(location.pub_id); - - const settings = preferences.data?.location?.[pubId]?.explorer; - - if (!settings) return defaults; - - for (const [key, value] of Object.entries(settings)) { - if (value !== null) Object.assign(defaults, { [key]: value }); - } - - return defaults; - }, [location, preferences.data?.location]); - - const onSettingsChanged = async ( - settings: ExplorerSettings, - changedLocation: Location - ) => { - if (changedLocation.id === location.id && preferences.isLoading) return; - - const pubId = stringify(changedLocation.pub_id); - - try { - await updatePreferences.mutateAsync({ - location: { [pubId]: { explorer: settings } } - }); - rspc.queryClient.invalidateQueries(['preferences.get']); - } catch (e) { - alert('An error has occurred while updating your preferences.'); - } - }; - - const explorerSettings = useExplorerSettings({ - settings, - onSettingsChanged, - orderingKeys: filePathOrderingKeysSchema, - location - }); - - const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot(); - - const fixedFilters = useMemo( - () => [ - { filePath: { locations: { in: [location.id] } } }, - ...(explorerSettingsSnapshot.layoutMode === 'media' - ? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }] - : []) - ], - [location.id, explorerSettingsSnapshot.layoutMode] - ); - - const search = useSearch({ fixedFilters }); + const { explorerSettings, preferences } = useLocationExplorerSettings(location); const { layoutMode, mediaViewWithDescendants, showHiddenFiles } = explorerSettings.useSettingsSnapshot(); + const search = useLocationSearch(explorerSettings, location); + const paths = usePathsExplorerQuery({ arg: { filters: [ @@ -251,3 +195,117 @@ function getLastSectionOfPath(path: string): string | undefined { const lastSection = sections[sections.length - 1]; return lastSection; } + +function useLocationExplorerSettings(location: Location) { + const rspc = useRspcLibraryContext(); + + const preferences = useLibraryQuery(['preferences.get']); + const updatePreferences = useLibraryMutation('preferences.update'); + + const settings = useMemo(() => { + const defaults = createDefaultExplorerSettings({ + order: { field: 'name', value: 'Asc' } + }); + + if (!location) return defaults; + + const pubId = stringify(location.pub_id); + + const settings = preferences.data?.location?.[pubId]?.explorer; + + if (!settings) return defaults; + + for (const [key, value] of Object.entries(settings)) { + if (value !== null) Object.assign(defaults, { [key]: value }); + } + + return defaults; + }, [location, preferences.data?.location]); + + const onSettingsChanged = async ( + settings: ExplorerSettings, + changedLocation: Location + ) => { + if (changedLocation.id === location.id && preferences.isLoading) return; + + const pubId = stringify(changedLocation.pub_id); + + try { + await updatePreferences.mutateAsync({ + location: { [pubId]: { explorer: settings } } + }); + rspc.queryClient.invalidateQueries(['preferences.get']); + } catch (e) { + alert('An error has occurred while updating your preferences.'); + } + }; + + return { + explorerSettings: useExplorerSettings({ + settings, + onSettingsChanged, + orderingKeys: filePathOrderingKeysSchema, + location + }), + preferences + }; +} + +function useLocationSearch( + explorerSettings: UseExplorerSettings, + location: Location +) { + const [searchParams, setSearchParams] = useRawSearchParams(); + const explorerSettingsSnapshot = explorerSettings.useSettingsSnapshot(); + + const fixedFilters = useMemo( + () => [ + { filePath: { locations: { in: [location.id] } } }, + ...(explorerSettingsSnapshot.layoutMode === 'media' + ? [{ object: { kind: { in: [ObjectKindEnum.Image, ObjectKindEnum.Video] } } }] + : []) + ], + [location.id, explorerSettingsSnapshot.layoutMode] + ); + + const filtersParam = searchParams.get('filters'); + const dynamicFilters = useMemo(() => JSON.parse(filtersParam ?? '[]'), [filtersParam]); + + const searchQueryParam = searchParams.get('search'); + + const search = useSearch({ + open: !!searchQueryParam || dynamicFilters.length > 0 || undefined, + search: searchParams.get('search') ?? undefined, + fixedFilters, + dynamicFilters + }); + + useEffect(() => { + setSearchParams( + (p) => { + if (search.dynamicFilters.length > 0) + p.set('filters', JSON.stringify(search.dynamicFilters)); + else p.delete('filters'); + + return p; + }, + { replace: true } + ); + }, [search.dynamicFilters, setSearchParams]); + + const searchQuery = search.search; + + useEffect(() => { + setSearchParams( + (p) => { + if (searchQuery !== '') p.set('search', searchQuery); + else p.delete('search'); + + return p; + }, + { replace: true } + ); + }, [searchQuery, setSearchParams]); + + return search; +} diff --git a/interface/app/$libraryId/tag/$id.tsx b/interface/app/$libraryId/tag/$id.tsx index 7fc370e4b..0f8f6ce89 100644 --- a/interface/app/$libraryId/tag/$id.tsx +++ b/interface/app/$libraryId/tag/$id.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { ObjectKindEnum, ObjectOrder, useCache, useLibraryQuery, useNodes } from '@sd/client'; import { LocationIdParamsSchema } from '~/app/route-schemas'; import { Icon } from '~/components'; @@ -82,6 +82,7 @@ export function Component() { )} +