mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 10:03:28 +00:00
move to Explorer/Search
This commit is contained in:
parent
fa43998afa
commit
054af931f2
|
@ -1,8 +1,9 @@
|
|||
import { MagnifyingGlass, X } from '@phosphor-icons/react';
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
import { SearchFilterArgs } from '@sd/client';
|
||||
import { tw } from '@sd/ui';
|
||||
import { useTopBarContext } from '~/app/$libraryId/TopBar/Layout';
|
||||
|
||||
import { useSearchContext } from './Context';
|
||||
import { filterRegistry } from './Filters';
|
||||
import { getSearchStore, updateFilterArgs, useSearchStore } from './store';
|
||||
import { RenderIcon } from './util';
|
||||
|
@ -30,13 +31,13 @@ export const CloseTab = forwardRef<HTMLDivElement, { onClick: () => void }>(({ o
|
|||
export const AppliedOptions = () => {
|
||||
const searchState = useSearchStore();
|
||||
|
||||
const { fixedArgs } = useTopBarContext();
|
||||
const { fixedArgs } = useSearchContext();
|
||||
|
||||
const allArgs = useMemo(() => {
|
||||
if (!fixedArgs) return [];
|
||||
|
||||
const value: { arg: SearchFilterArgs; removalIndex: number | null }[] = fixedArgs.map(
|
||||
(arg, i) => ({
|
||||
(arg) => ({
|
||||
arg,
|
||||
removalIndex: null
|
||||
})
|
||||
|
@ -51,8 +52,8 @@ export const AppliedOptions = () => {
|
|||
);
|
||||
if (fixedEquivalentIndex !== -1) {
|
||||
const merged = filter.merge(
|
||||
filter.extract(fixedArgs[fixedEquivalentIndex]!)!,
|
||||
filter.extract(arg)
|
||||
filter.extract(fixedArgs[fixedEquivalentIndex]!)! as any,
|
||||
filter.extract(arg)! as any
|
||||
);
|
||||
|
||||
value[fixedEquivalentIndex] = {
|
45
interface/app/$libraryId/Explorer/Search/Context.tsx
Normal file
45
interface/app/$libraryId/Explorer/Search/Context.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { createContext, PropsWithChildren, useContext, useMemo } from 'react';
|
||||
|
||||
import { useTopBarContext } from '../../TopBar/Layout';
|
||||
import { argsToOptions, getKey, useSearchStore } from './store';
|
||||
|
||||
const Context = createContext<ReturnType<typeof useContextValue> | null>(null);
|
||||
|
||||
function useContextValue() {
|
||||
const searchState = useSearchStore();
|
||||
|
||||
const { fixedArgs, setFixedArgs } = useTopBarContext();
|
||||
|
||||
const fixedArgsAsOptions = useMemo(() => {
|
||||
return fixedArgs ? argsToOptions(fixedArgs, searchState.filterOptions) : null;
|
||||
}, [fixedArgs, searchState.filterOptions]);
|
||||
|
||||
const fixedArgsKeys = useMemo(() => {
|
||||
const keys = fixedArgsAsOptions
|
||||
? new Set(
|
||||
fixedArgsAsOptions?.map(({ arg, filter }) => {
|
||||
return getKey({
|
||||
type: filter.name,
|
||||
name: arg.name,
|
||||
value: arg.value
|
||||
});
|
||||
})
|
||||
)
|
||||
: null;
|
||||
return keys;
|
||||
}, [fixedArgsAsOptions]);
|
||||
|
||||
return { setFixedArgs, fixedArgs, fixedArgsKeys };
|
||||
}
|
||||
|
||||
export const SearchContextProvider = ({ children }: PropsWithChildren) => {
|
||||
return <Context.Provider value={useContextValue()}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
export function useSearchContext() {
|
||||
const ctx = useContext(Context);
|
||||
|
||||
if (!ctx) throw new Error('SearchContextProvider not found!');
|
||||
|
||||
return ctx;
|
||||
}
|
|
@ -2,9 +2,9 @@ import { CircleDashed, Cube, Folder, Icon, SelectionSlash, Textbox } from '@phos
|
|||
import { useState } from 'react';
|
||||
import { InOrNotIn, ObjectKind, SearchFilterArgs, TextMatch, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Input } from '@sd/ui';
|
||||
import { useTopBarContext } from '~/app/$libraryId/TopBar/Layout';
|
||||
|
||||
import { SearchOptionItem, SearchOptionSubMenu } from '.';
|
||||
import { useSearchContext } from './Context';
|
||||
import { AllKeys, FilterOption, getKey, updateFilterArgs, useSearchStore } from './store';
|
||||
import { FilterTypeCondition, filterTypeCondition } from './util';
|
||||
|
||||
|
@ -53,7 +53,7 @@ const FilterOptionList = ({
|
|||
options: FilterOption[];
|
||||
}) => {
|
||||
const store = useSearchStore();
|
||||
const { fixedArgsKeys } = useTopBarContext();
|
||||
const { fixedArgsKeys } = useSearchContext();
|
||||
|
||||
return (
|
||||
<SearchOptionSubMenu name={filter.name} icon={filter.icon}>
|
||||
|
@ -88,7 +88,7 @@ const FilterOptionList = ({
|
|||
if (rawArg) filter.applyAdd(arg, option);
|
||||
} else {
|
||||
if (!filter.applyRemove(arg, option))
|
||||
args.splice(rawArgIndex);
|
||||
args.splice(rawArgIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,7 +109,7 @@ const FilterOptionList = ({
|
|||
const FilterOptionText = ({ filter }: { filter: SearchFilterCRUD }) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const { fixedArgsKeys } = useTopBarContext();
|
||||
const { fixedArgsKeys } = useSearchContext();
|
||||
|
||||
return (
|
||||
<SearchOptionSubMenu name={filter.name} icon={filter.icon}>
|
||||
|
@ -140,9 +140,9 @@ const FilterOptionText = ({ filter }: { filter: SearchFilterCRUD }) => {
|
|||
};
|
||||
|
||||
const FilterOptionBoolean = ({ filter }: { filter: SearchFilterCRUD }) => {
|
||||
const { filterArgs, filterArgsKeys } = useSearchStore();
|
||||
const { filterArgsKeys } = useSearchStore();
|
||||
|
||||
const { fixedArgsKeys } = useTopBarContext();
|
||||
const { fixedArgsKeys } = useSearchContext();
|
||||
|
||||
const key = getKey({
|
||||
type: filter.name,
|
||||
|
@ -161,12 +161,10 @@ const FilterOptionBoolean = ({ filter }: { filter: SearchFilterCRUD }) => {
|
|||
const index = args.findIndex((f) => filter.extract(f) !== undefined);
|
||||
|
||||
if (index !== -1) {
|
||||
args.splice(index);
|
||||
args.splice(index, 1);
|
||||
} else {
|
||||
const arg = filter.create(true);
|
||||
args.push(arg);
|
||||
|
||||
filter.applyAdd(arg, { name: filter.name, value: true });
|
||||
}
|
||||
|
||||
return args;
|
||||
|
@ -196,6 +194,7 @@ function createInOrNotInFilter<T extends string | number>(
|
|||
| 'applyAdd'
|
||||
| 'applyRemove'
|
||||
| 'create'
|
||||
| 'merge'
|
||||
> & {
|
||||
create(value: InOrNotIn<T>): SearchFilterArgs;
|
||||
argsToOptions(values: T[], options: Map<string, FilterOption[]>): FilterOption[];
|
||||
|
@ -206,7 +205,7 @@ function createInOrNotInFilter<T extends string | number>(
|
|||
create: (data) => {
|
||||
if (typeof data === 'number' || typeof data === 'string')
|
||||
return filter.create({
|
||||
in: [data]
|
||||
in: [data as any]
|
||||
});
|
||||
else if (data) return filter.create(data);
|
||||
else return filter.create({ in: [] });
|
||||
|
@ -288,6 +287,7 @@ function createTextMatchFilter(
|
|||
| 'applyAdd'
|
||||
| 'applyRemove'
|
||||
| 'create'
|
||||
| 'merge'
|
||||
> & {
|
||||
create(value: TextMatch): SearchFilterArgs;
|
||||
}
|
||||
|
@ -361,9 +361,7 @@ function createTextMatchFilter(
|
|||
else if ('equals' in data) return { equals: value };
|
||||
},
|
||||
applyRemove: () => undefined,
|
||||
merge: (left, right) => {
|
||||
return left;
|
||||
}
|
||||
merge: (left) => left
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -379,6 +377,7 @@ function createBooleanFilter(
|
|||
| 'applyAdd'
|
||||
| 'applyRemove'
|
||||
| 'create'
|
||||
| 'merge'
|
||||
> & {
|
||||
create(value: boolean): SearchFilterArgs;
|
||||
}
|
||||
|
@ -416,9 +415,7 @@ function createBooleanFilter(
|
|||
return value;
|
||||
},
|
||||
applyRemove: () => undefined,
|
||||
merge: (left, right) => {
|
||||
return left;
|
||||
}
|
||||
merge: (left) => left
|
||||
};
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ import { IconTypes } from '@sd/assets/util';
|
|||
import clsx from 'clsx';
|
||||
import { memo, PropsWithChildren, useDeferredValue, useState } from 'react';
|
||||
import { Button, ContextMenuDivItem, DropdownMenu, Input, RadixCheckbox, tw } from '@sd/ui';
|
||||
import { useTopBarContext } from '~/app/$libraryId/TopBar/Layout';
|
||||
import { useKeybind } from '~/hooks';
|
||||
|
||||
import { AppliedOptions } from './AppliedFilters';
|
||||
import { useSearchContext } from './Context';
|
||||
import { filterRegistry } from './Filters';
|
||||
import { useSavedSearches } from './SavedSearches';
|
||||
import {
|
||||
|
@ -195,7 +195,7 @@ const SearchOptions = () => {
|
|||
export default SearchOptions;
|
||||
|
||||
const SearchResults = memo(({ search }: { search: string }) => {
|
||||
const { fixedArgsKeys } = useTopBarContext();
|
||||
const { fixedArgsKeys } = useSearchContext();
|
||||
const searchState = useSearchStore();
|
||||
const searchResults = useSearchRegisteredFilters(search);
|
||||
|
||||
|
@ -215,26 +215,23 @@ const SearchResults = memo(({ search }: { search: string }) => {
|
|||
updateFilterArgs((args) => {
|
||||
if (fixedArgsKeys?.has(option.key)) return args;
|
||||
|
||||
let rawArg = args.find((arg) => filter.extract(arg));
|
||||
const rawArg = args.find((arg) => filter.extract(arg));
|
||||
|
||||
if (!rawArg) {
|
||||
rawArg = filter.create(option.value);
|
||||
args.push(rawArg);
|
||||
}
|
||||
const arg = filter.create(option.value);
|
||||
args.push(arg);
|
||||
} else {
|
||||
const rawArgIndex = args.findIndex((arg) =>
|
||||
filter.extract(arg)
|
||||
)!;
|
||||
|
||||
const rawArgIndex = args.findIndex((arg) => filter.extract(arg))!;
|
||||
const arg = filter.extract(rawArg)! as any;
|
||||
|
||||
const arg = filter.extract(rawArg)! as any;
|
||||
|
||||
if (!filter.getCondition?.(arg))
|
||||
filter.setCondition(
|
||||
arg,
|
||||
Object.keys(filter.conditions)[0] as any as never
|
||||
);
|
||||
|
||||
if (value) filter.applyAdd(arg, option);
|
||||
else {
|
||||
if (!filter.applyRemove(arg, option)) args.splice(rawArgIndex);
|
||||
if (value) filter.applyAdd(arg, option);
|
||||
else {
|
||||
if (!filter.applyRemove(arg, option))
|
||||
args.splice(rawArgIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
|
@ -7,6 +7,7 @@ import { proxyMap } from 'valtio/utils';
|
|||
import { SearchFilterArgs } from '@sd/client';
|
||||
import { useTopBarContext } from '~/app/$libraryId/TopBar/Layout';
|
||||
|
||||
import { useSearchContext } from './Context';
|
||||
import { filterRegistry, FilterType, RenderSearchFilter } from './Filters';
|
||||
import { FilterTypeCondition } from './util';
|
||||
|
||||
|
@ -43,7 +44,7 @@ const searchStore = proxy({
|
|||
searchQuery: null as string | null,
|
||||
filterArgs: ref([] as SearchFilterArgs[]),
|
||||
filterArgsKeys: ref(new Set<string>()),
|
||||
filterOptions: ref(new Map<string, FilterOption[]>()),
|
||||
filterOptions: ref(new Map<string, FilterOptionWithType[]>()),
|
||||
// we register filters so we can search them
|
||||
registeredFilters: proxyMap() as Map<string, FilterOptionWithType>
|
||||
});
|
||||
|
@ -53,7 +54,7 @@ export function useSearchFilters<T extends SearchType>(
|
|||
fixedArgs: SearchFilterArgs[]
|
||||
) {
|
||||
const { filterArgs } = useSearchStore();
|
||||
const topBar = useTopBarContext();
|
||||
const topBar = useSearchContext();
|
||||
|
||||
// don't want the search bar to pop in after the top bar has loaded!
|
||||
useLayoutEffect(() => {
|
|
@ -1,6 +1,5 @@
|
|||
import { FolderNotchOpen } from '@phosphor-icons/react';
|
||||
import { CSSProperties, type PropsWithChildren, type ReactNode } from 'react';
|
||||
import { SlideDown } from 'react-slidedown';
|
||||
import { useKeys } from 'rooks';
|
||||
import { getExplorerLayoutStore, useExplorerLayoutStore, useLibrarySubscription } from '@sd/client';
|
||||
import { useKeysMatcher, useOperatingSystem } from '~/hooks';
|
||||
|
@ -11,15 +10,15 @@ import ContextMenu from './ContextMenu';
|
|||
import DismissibleNotice from './DismissibleNotice';
|
||||
import { Inspector, INSPECTOR_WIDTH } from './Inspector';
|
||||
import ExplorerContextMenu from './ParentContextMenu';
|
||||
import SearchOptions from './Search';
|
||||
import { useExplorerStore } from './store';
|
||||
import { useKeyRevealFinder } from './useKeyRevealFinder';
|
||||
import View, { EmptyNotice, ExplorerViewProps } from './View';
|
||||
import { ExplorerPath, PATH_BAR_HEIGHT } from './View/ExplorerPath';
|
||||
import SearchOptions from './View/SearchOptions';
|
||||
|
||||
import 'react-slidedown/lib/slidedown.css';
|
||||
|
||||
import { useSearchStore } from './View/SearchOptions/store';
|
||||
import { useSearchStore } from './Search/store';
|
||||
|
||||
interface Props {
|
||||
emptyNotice?: ExplorerViewProps['emptyNotice'];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ArrowsOutSimple, EjectSimple, X } from '@phosphor-icons/react';
|
||||
import { EjectSimple, X } from '@phosphor-icons/react';
|
||||
import { Laptop } from '@sd/assets/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
@ -15,7 +15,7 @@ import { Button, Tooltip } from '@sd/ui';
|
|||
import { AddLocationButton } from '~/app/$libraryId/settings/library/locations/AddLocationButton';
|
||||
import { Folder, SubtleButton } from '~/components';
|
||||
|
||||
import { useSavedSearches } from '../../Explorer/View/SearchOptions/SavedSearches';
|
||||
import { useSavedSearches } from '../../Explorer/Search/SavedSearches';
|
||||
import SidebarLink from './Link';
|
||||
import LocationsContextMenu from './LocationsContextMenu';
|
||||
import Section from './Section';
|
||||
|
|
|
@ -1,37 +1,15 @@
|
|||
import { createContext, useContext, useMemo, useState } from 'react';
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
import { Outlet } from 'react-router';
|
||||
import { SearchFilterArgs } from '@sd/client';
|
||||
|
||||
import TopBar from '.';
|
||||
import { argsToOptions, getKey, useSearchStore } from '../Explorer/View/SearchOptions/store';
|
||||
|
||||
const TopBarContext = createContext<ReturnType<typeof useContextValue> | null>(null);
|
||||
|
||||
function useContextValue(props: { left: HTMLDivElement | null; right: HTMLDivElement | null }) {
|
||||
const [fixedArgs, setFixedArgs] = useState<SearchFilterArgs[] | null>(null);
|
||||
|
||||
const searchState = useSearchStore();
|
||||
|
||||
const fixedArgsAsOptions = useMemo(() => {
|
||||
return fixedArgs ? argsToOptions(fixedArgs, searchState.filterOptions) : null;
|
||||
}, [fixedArgs, searchState.filterOptions]);
|
||||
|
||||
const fixedArgsKeys = useMemo(() => {
|
||||
const keys = fixedArgsAsOptions
|
||||
? new Set(
|
||||
fixedArgsAsOptions?.map(({ arg, filter }) => {
|
||||
return getKey({
|
||||
type: filter.name,
|
||||
name: arg.name,
|
||||
value: arg.value
|
||||
});
|
||||
})
|
||||
)
|
||||
: null;
|
||||
return keys;
|
||||
}, [fixedArgsAsOptions]);
|
||||
|
||||
return { ...props, setFixedArgs, fixedArgs, fixedArgsKeys };
|
||||
return { ...props, fixedArgs, setFixedArgs };
|
||||
}
|
||||
|
||||
export const Component = () => {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import clsx from 'clsx';
|
||||
import { useCallback, useEffect, useRef, useState, useTransition } from 'react';
|
||||
import { useLocation, useNavigate, useResolvedPath } from 'react-router';
|
||||
import { useLocation, useResolvedPath } from 'react-router';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { Input, ModifierKeys, Shortcut } from '@sd/ui';
|
||||
import { SearchParamsSchema } from '~/app/route-schemas';
|
||||
import { useOperatingSystem, useZodSearchParams } from '~/hooks';
|
||||
import { keybindForOs } from '~/util/keybinds';
|
||||
|
||||
import { getSearchStore, useSearchStore } from '../Explorer/View/SearchOptions/store';
|
||||
import { getSearchStore, useSearchStore } from '../Explorer/Search/store';
|
||||
|
||||
export default () => {
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
|
|
@ -24,13 +24,13 @@ import { useKeyDeleteFile, useZodRouteParams } from '~/hooks';
|
|||
import Explorer from '../Explorer';
|
||||
import { ExplorerContextProvider } from '../Explorer/Context';
|
||||
import { usePathsInfiniteQuery } from '../Explorer/queries';
|
||||
import { SearchContextProvider } from '../Explorer/Search/Context';
|
||||
import { useSearchFilters } from '../Explorer/Search/store';
|
||||
import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store';
|
||||
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
|
||||
import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { useExplorerSearchParams } from '../Explorer/util';
|
||||
import { EmptyNotice } from '../Explorer/View';
|
||||
import { FilterType } from '../Explorer/View/SearchOptions/Filters';
|
||||
import { useSearchFilters } from '../Explorer/View/SearchOptions/store';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
import LocationOptions from './LocationOptions';
|
||||
|
||||
|
@ -43,9 +43,11 @@ export const Component = () => {
|
|||
});
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<LocationExplorer path={path} location={location.data!} />)
|
||||
</Suspense>
|
||||
<SearchContextProvider>
|
||||
<Suspense>
|
||||
<LocationExplorer path={path} location={location.data!} />)
|
||||
</Suspense>
|
||||
</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
import { getIcon, iconNames } from '@sd/assets/util';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
ObjectFilterArgs,
|
||||
ObjectKindEnum,
|
||||
ObjectOrder,
|
||||
Tag,
|
||||
useLibraryContext,
|
||||
useLibraryQuery
|
||||
} from '@sd/client';
|
||||
import { ObjectKindEnum, ObjectOrder, Tag, useLibraryContext, useLibraryQuery } from '@sd/client';
|
||||
import { LocationIdParamsSchema } from '~/app/route-schemas';
|
||||
import { useZodRouteParams } from '~/hooks';
|
||||
|
||||
import Explorer from '../Explorer';
|
||||
import { ExplorerContextProvider } from '../Explorer/Context';
|
||||
import { useObjectsInfiniteQuery } from '../Explorer/queries';
|
||||
import { SearchContextProvider } from '../Explorer/Search/Context';
|
||||
import { useSearchFilters } from '../Explorer/Search/store';
|
||||
import { createDefaultExplorerSettings, objectOrderingKeysSchema } from '../Explorer/store';
|
||||
import { DefaultTopBarOptions } from '../Explorer/TopBarOptions';
|
||||
import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer';
|
||||
import { EmptyNotice } from '../Explorer/View';
|
||||
import { useSearchFilters } from '../Explorer/View/SearchOptions/store';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
|
||||
export const Component = () => {
|
||||
return (
|
||||
<SearchContextProvider>
|
||||
<Inner />
|
||||
</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
function Inner() {
|
||||
const { id: tagId } = useZodRouteParams(LocationIdParamsSchema);
|
||||
const tag = useLibraryQuery(['tags.get', tagId], { suspense: true });
|
||||
|
||||
|
@ -78,7 +80,7 @@ export const Component = () => {
|
|||
/>
|
||||
</ExplorerContextProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function useItems({ tag, settings }: { tag: Tag; settings: UseExplorerSettings<ObjectOrder> }) {
|
||||
const { library } = useLibraryContext();
|
||||
|
|
|
@ -2,7 +2,7 @@ import { MouseEvent } from 'react';
|
|||
import { useNavigate } from 'react-router';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
|
||||
import { useSearchStore } from '../app/$libraryId/Explorer/View/SearchOptions/store';
|
||||
import { useSearchStore } from '../app/$libraryId/Explorer/Search/store';
|
||||
|
||||
export const useMouseNavigate = () => {
|
||||
const idx = history.state.idx as number;
|
||||
|
|
Loading…
Reference in a new issue