mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-01 04:53:33 +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 { 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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue