mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-05 09:13:28 +00:00
[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:
parent
3cabc9c3a9
commit
7aa0452ba3
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
{
|
{
|
||||||
|
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue