[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 { 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;
}

View file

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

View file

@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { Input, ModifierKeys, Shortcut } from '@sd/ui';
import { useOperatingSystem } from '~/hooks';
import { keybindForOs } from '~/util/keybinds';
@ -46,15 +47,22 @@ export default () => {
};
}, [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={
<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'], () => {
search.setSearch('');
search.setOpen(false);
search.setSearchBarFocused(false);
});
return (
<kbd
onClick={() => {
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"
>

View file

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

View file

@ -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 <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 [{ 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<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 { 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<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 { LocationIdParamsSchema } from '~/app/route-schemas';
import { Icon } from '~/components';
@ -82,6 +82,7 @@ export function Component() {
)}
</TopBarPortal>
</SearchContextProvider>
<Explorer
emptyNotice={
<EmptyNotice