[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
This commit is contained in:
Brendan Allan 2023-12-15 03:30:42 +08:00 committed by GitHub
parent 3cabc9c3a9
commit 7aa0452ba3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 165 additions and 90 deletions

View file

@ -1,5 +1,9 @@
import { useCallback, type HTMLAttributes, type PropsWithChildren } from 'react'; 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 { import {
isPath, isPath,
useLibraryContext, useLibraryContext,
@ -24,6 +28,7 @@ export const useViewItemDoubleClick = () => {
const explorer = useExplorerContext(); const explorer = useExplorerContext();
const { library } = useLibraryContext(); const { library } = useLibraryContext();
const { openFilePaths, openEphemeralFiles } = usePlatform(); const { openFilePaths, openEphemeralFiles } = usePlatform();
const [_, setSearchParams] = useRawSearchParams();
const updateAccessTime = useLibraryMutation('files.updateAccessTime'); const updateAccessTime = useLibraryMutation('files.updateAccessTime');
@ -110,11 +115,14 @@ export const useViewItemDoubleClick = () => {
if (items.dirs.length > 0) { if (items.dirs.length > 0) {
const [item] = items.dirs; const [item] = items.dirs;
if (item) { if (item) {
navigate({ setSearchParams((p) => {
pathname: `../location/${item.location_id}`, const newParams = new URLSearchParams();
search: createSearchParams({
path: `${item.materialized_path}${item.name}/` newParams.set('path', `${item.materialized_path}${item.name}/`);
}).toString() const take = p.get('take');
if (take !== null) newParams.set('take', take);
return newParams;
}); });
return; return;
} }

View file

@ -62,7 +62,7 @@ export const AppliedFilters = ({ allowRemove = true }: { allowRemove?: boolean }
<RenderIcon className="h-4 w-4" icon={MagnifyingGlass} /> <RenderIcon className="h-4 w-4" icon={MagnifyingGlass} />
<FilterText>{search.search}</FilterText> <FilterText>{search.search}</FilterText>
</StaticSection> </StaticSection>
{allowRemove && <CloseTab onClick={() => search.setRawSearch('')} />} {allowRemove && <CloseTab onClick={() => search.setSearch('')} />}
</FilterContainer> </FilterContainer>
)} )}
<div <div

View file

@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { Input, ModifierKeys, Shortcut } from '@sd/ui'; import { Input, ModifierKeys, Shortcut } from '@sd/ui';
import { useOperatingSystem } from '~/hooks'; import { useOperatingSystem } from '~/hooks';
import { keybindForOs } from '~/util/keybinds'; import { keybindForOs } from '~/util/keybinds';
@ -46,15 +47,22 @@ export default () => {
}; };
}, [blurHandler, focusHandler]); }, [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) { function updateValue(value: string) {
setValue(value); setValue(value);
search.setSearch(value); updateDebounce(value);
} }
function clearValue() { function clearValue() {
setValue('');
search.setSearch(''); search.setSearch('');
} }
@ -69,10 +77,10 @@ export default () => {
onBlur={() => { onBlur={() => {
if (search.rawSearch === '' && !searchStore.interactingWithSearchOptions) { if (search.rawSearch === '' && !searchStore.interactingWithSearchOptions) {
clearValue(); clearValue();
search.setOpen(false); search.setSearchBarFocused(false);
} }
}} }}
onFocus={() => search.setOpen(true)} onFocus={() => search.setSearchBarFocused(true)}
right={ right={
<div className="pointer-events-none flex h-7 items-center space-x-1 opacity-70 group-focus-within:hidden"> <div className="pointer-events-none flex h-7 items-center space-x-1 opacity-70 group-focus-within:hidden">
{ {

View file

@ -292,14 +292,14 @@ function EscapeButton() {
useKeybind(['Escape'], () => { useKeybind(['Escape'], () => {
search.setSearch(''); search.setSearch('');
search.setOpen(false); search.setSearchBarFocused(false);
}); });
return ( return (
<kbd <kbd
onClick={() => { onClick={() => {
search.setSearch(''); 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" className="ml-2 rounded-lg border border-app-line bg-app-box px-2 py-1 text-[10.5px] tracking-widest shadow"
> >

View file

@ -1,7 +1,6 @@
import { produce } from 'immer'; import { produce } from 'immer';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useDebouncedValue } from 'rooks'; import { useDebouncedValue } from 'rooks';
import { useDebouncedCallback } from 'use-debounce';
import { SearchFilterArgs } from '@sd/client'; import { SearchFilterArgs } from '@sd/client';
import { filterRegistry } from './Filters'; import { filterRegistry } from './Filters';
@ -22,8 +21,7 @@ export interface UseSearchProps {
} }
export function useSearch(props?: UseSearchProps) { export function useSearch(props?: UseSearchProps) {
const [open, setOpen] = useState(false); const [searchBarFocused, setSearchBarFocused] = useState(false);
if (props?.open !== undefined && open !== props.open) setOpen(props.open);
const searchState = useSearchStore(); const searchState = useSearchStore();
@ -123,14 +121,16 @@ export function useSearch(props?: UseSearchProps) {
// Filters generated from the search query // Filters generated from the search query
// rawSearch should only ever be read by the search input // 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); const [searchFromProps, setSearchFromProps] = useState(props?.search);
if (searchFromProps !== props?.search) { if (searchFromProps !== props?.search) {
setSearchFromProps(props?.search); setSearchFromProps(props?.search);
setSearch(props?.search ?? ''); setRawSearch(props?.search ?? '');
} }
const [search] = useDebouncedValue(rawSearch, 300);
const searchFilters = useMemo(() => { const searchFilters = useMemo(() => {
const [name, ext] = search.split('.') ?? []; const [name, ext] = search.split('.') ?? [];
@ -166,14 +166,14 @@ export function useSearch(props?: UseSearchProps) {
}, [allFiltersAsOptions]); }, [allFiltersAsOptions]);
return { return {
open, open: props?.open || searchBarFocused,
setOpen,
fixedFilters, fixedFilters,
fixedFiltersKeys, fixedFiltersKeys,
search, search,
rawSearch: search, rawSearch,
setRawSearch: setSearch, setSearch: setRawSearch,
setSearch: useDebouncedCallback(setSearch, 300), searchBarFocused,
setSearchBarFocused,
dynamicFilters, dynamicFilters,
setDynamicFilters, setDynamicFilters,
updateDynamicFilters, updateDynamicFilters,

View file

@ -1,5 +1,6 @@
import { ArrowClockwise, Info } from '@phosphor-icons/react'; 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 { stringify } from 'uuid';
import { import {
arraysEqual, arraysEqual,
@ -32,7 +33,7 @@ import { ExplorerContextProvider } from '../Explorer/Context';
import { usePathsExplorerQuery } from '../Explorer/queries'; import { usePathsExplorerQuery } from '../Explorer/queries';
import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store'; import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store';
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions'; import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
import { useExplorer, useExplorerSettings } from '../Explorer/useExplorer'; import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer';
import { useExplorerSearchParams } from '../Explorer/util'; import { useExplorerSearchParams } from '../Explorer/util';
import { EmptyNotice } from '../Explorer/View/EmptyNotice'; import { EmptyNotice } from '../Explorer/View/EmptyNotice';
import SearchOptions, { SearchContextProvider, useSearch } from '../Search'; import SearchOptions, { SearchContextProvider, useSearch } from '../Search';
@ -43,6 +44,7 @@ import LocationOptions from './LocationOptions';
export const Component = () => { export const Component = () => {
const { id: locationId } = useZodRouteParams(LocationIdParamsSchema); const { id: locationId } = useZodRouteParams(LocationIdParamsSchema);
const [{ path }] = useExplorerSearchParams();
const result = useLibraryQuery(['locations.get', locationId], { const result = useLibraryQuery(['locations.get', locationId], {
keepPreviousData: true, keepPreviousData: true,
suspense: true suspense: true
@ -50,12 +52,12 @@ export const Component = () => {
useNodes(result.data?.nodes); useNodes(result.data?.nodes);
const location = useCache(result.data?.item); const location = useCache(result.data?.item);
return <LocationExplorer location={location!} />; // 'key' allows search state to be thrown out when entering a folder
return <LocationExplorer key={path} location={location!} />;
}; };
const LocationExplorer = ({ location }: { location: Location; path?: string }) => { const LocationExplorer = ({ location }: { location: Location; path?: string }) => {
const [{ path, take }] = useExplorerSearchParams(); const [{ path, take }] = useExplorerSearchParams();
const rspc = useRspcLibraryContext();
const onlineLocations = useOnlineLocations(); const onlineLocations = useOnlineLocations();
@ -67,71 +69,13 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
return onlineLocations.some((l) => arraysEqual(pub_id, l)); return onlineLocations.some((l) => arraysEqual(pub_id, l));
}, [location.pub_id, onlineLocations]); }, [location.pub_id, onlineLocations]);
const preferences = useLibraryQuery(['preferences.get']); const { explorerSettings, preferences } = useLocationExplorerSettings(location);
const updatePreferences = useLibraryMutation('preferences.update');
const settings = useMemo(() => {
const defaults = createDefaultExplorerSettings<FilePathOrder>({
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<FilePathOrder>,
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 { layoutMode, mediaViewWithDescendants, showHiddenFiles } = const { layoutMode, mediaViewWithDescendants, showHiddenFiles } =
explorerSettings.useSettingsSnapshot(); explorerSettings.useSettingsSnapshot();
const search = useLocationSearch(explorerSettings, location);
const paths = usePathsExplorerQuery({ const paths = usePathsExplorerQuery({
arg: { arg: {
filters: [ filters: [
@ -251,3 +195,117 @@ function getLastSectionOfPath(path: string): string | undefined {
const lastSection = sections[sections.length - 1]; const lastSection = sections[sections.length - 1];
return lastSection; 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<FilePathOrder>({
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<FilePathOrder>,
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<FilePathOrder>,
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;
}

View file

@ -1,4 +1,4 @@
import { useMemo } from 'react'; import { memo, useMemo } from 'react';
import { ObjectKindEnum, ObjectOrder, useCache, useLibraryQuery, useNodes } from '@sd/client'; import { ObjectKindEnum, ObjectOrder, useCache, useLibraryQuery, useNodes } from '@sd/client';
import { LocationIdParamsSchema } from '~/app/route-schemas'; import { LocationIdParamsSchema } from '~/app/route-schemas';
import { Icon } from '~/components'; import { Icon } from '~/components';
@ -82,6 +82,7 @@ export function Component() {
)} )}
</TopBarPortal> </TopBarPortal>
</SearchContextProvider> </SearchContextProvider>
<Explorer <Explorer
emptyNotice={ emptyNotice={
<EmptyNotice <EmptyNotice