Skip to content

Commit

Permalink
source search query from search params
Browse files Browse the repository at this point in the history
  • Loading branch information
Brendonovich committed Nov 17, 2023
1 parent f69959a commit 91932c9
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 139 deletions.
10 changes: 5 additions & 5 deletions interface/app/$libraryId/Explorer/Search/AppliedFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const FiltersOverflowShade = tw.div`from-app-darkerBox/80 absolute w-10 bg-gradi

export const AppliedOptions = () => {
const searchState = useSearchStore();
const { allFilterArgs } = useSearchContext();
const searchCtx = useSearchContext();

const [scroll, setScroll] = useState(0);

Expand All @@ -47,16 +47,16 @@ export const AppliedOptions = () => {
className="no-scrollbar flex h-full items-center gap-2 overflow-y-auto"
onScroll={handleScroll}
>
{searchState.searchQuery && (
{searchCtx.searchQuery && searchCtx.searchQuery !== '' && (
<FilterContainer>
<StaticSection>
<RenderIcon className="h-4 w-4" icon={MagnifyingGlass} />
<FilterText>{searchState.searchQuery}</FilterText>
<FilterText>{searchCtx.searchQuery}</FilterText>
</StaticSection>
<CloseTab onClick={() => (getSearchStore().searchQuery = null)} />
<CloseTab onClick={() => searchCtx.setSearchQuery('')} />
</FilterContainer>
)}
{allFilterArgs.map(({ arg, removalIndex }, index) => {
{searchCtx.allFilterArgs.map(({ arg, removalIndex }, index) => {
const filter = filterRegistry.find((f) => f.extract(arg));
if (!filter) return;

Expand Down
23 changes: 22 additions & 1 deletion interface/app/$libraryId/Explorer/Search/Context.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { createContext, PropsWithChildren, useContext, useMemo } from 'react';
import { z } from 'zod';
import { SearchFilterArgs } from '@sd/client';
import { useZodSearchParams } from '~/hooks';

import { useTopBarContext } from '../../TopBar/Layout';
import { filterRegistry } from './Filters';
import { argsToOptions, getKey, useSearchStore } from './store';

const Context = createContext<ReturnType<typeof useContextValue> | null>(null);

const SEARCH_PARAMS = z.object({ search: z.string().optional(), filters: z.object({}).optional() });

function useContextValue() {
const [searchParams, setSearchParams] = useZodSearchParams(SEARCH_PARAMS);
const searchState = useSearchStore();

const { fixedArgs, setFixedArgs } = useTopBarContext();
Expand Down Expand Up @@ -69,7 +74,23 @@ function useContextValue() {
return value;
}, [fixedArgs, searchState.filterArgs]);

return { setFixedArgs, fixedArgs, fixedArgsKeys, allFilterArgs };
return {
setFixedArgs,
fixedArgs,
fixedArgsKeys,
allFilterArgs,
searchQuery: searchParams.search,
setSearchQuery(value: string) {
setSearchParams((p) => ({ ...p, search: value }));
},
clearSearchQuery() {
setSearchParams((p) => {
delete p.search;
return { ...p };
});
},
isSearching: searchParams.search !== undefined
};
}

export const SearchContextProvider = ({ children }: PropsWithChildren) => {
Expand Down
5 changes: 3 additions & 2 deletions interface/app/$libraryId/Explorer/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,15 @@ export const Separator = () => <DropdownMenu.Separator className="!border-app-li

const SearchOptions = () => {
const searchState = useSearchStore();
const searchCtx = useSearchContext();

const [newFilterName, setNewFilterName] = useState('');
const [_search, setSearch] = useState('');

const search = useDeferredValue(_search);

useKeybind(['Escape'], () => {
getSearchStore().isSearching = false;
// getSearchStore().isSearching = false;
});

// const savedSearches = useSavedSearches();
Expand Down Expand Up @@ -187,7 +188,7 @@ const SearchOptions = () => {
}

<kbd
onClick={() => (getSearchStore().isSearching = false)}
onClick={() => searchCtx.clearSearchQuery()}
className="ml-2 rounded-lg border border-app-line bg-app-box px-2 py-1 text-[10.5px] tracking-widest shadow"
>
ESC
Expand Down
9 changes: 3 additions & 6 deletions interface/app/$libraryId/Explorer/Search/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ export interface FilterOptionWithType extends FilterOption {
export type AllKeys<T> = T extends any ? keyof T : never;

const searchStore = proxy({
isSearching: false,
interactingWithSearchOptions: false,
searchType: 'paths' as SearchType,
searchQuery: null as string | null,
filterArgs: ref([] as SearchFilterArgs[]),
filterArgsKeys: ref(new Set<string>()),
filterOptions: ref(new Map<string, FilterOptionWithType[]>()),
Expand All @@ -41,7 +39,7 @@ export function useSearchFilters<T extends SearchType>(
_searchType: T,
fixedArgs: SearchFilterArgs[]
) {
const { setFixedArgs, allFilterArgs } = useSearchContext();
const { setFixedArgs, allFilterArgs, searchQuery } = useSearchContext();
const searchState = useSearchStore();

// don't want the search bar to pop in after the top bar has loaded!
Expand All @@ -51,15 +49,15 @@ export function useSearchFilters<T extends SearchType>(
}, [fixedArgs]);

const searchQueryFilters = useMemo(() => {
const [name, ext] = searchState.searchQuery?.split('.') ?? [];
const [name, ext] = searchQuery?.split('.') ?? [];

const filters: SearchFilterArgs[] = [];

if (name) filters.push({ filePath: { name: { contains: name } } });
if (ext) filters.push({ filePath: { extension: { in: [ext] } } });

return filters;
}, [searchState.searchQuery]);
}, [searchQuery]);

return useMemo(
() => [...searchQueryFilters, ...allFilterArgs.map(({ arg }) => arg)],
Expand Down Expand Up @@ -146,7 +144,6 @@ export const useSearchRegisteredFilters = (query: string) => {
};

export const resetSearchStore = () => {
searchStore.searchQuery = null;
searchStore.filterArgs = ref([]);
searchStore.filterArgsKeys = ref(new Set());
};
Expand Down
43 changes: 0 additions & 43 deletions interface/app/$libraryId/Explorer/Search/util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,6 @@ import clsx from 'clsx';
import { InOrNotIn, Range, TextMatch } from '@sd/client';
import { Icon as SDIcon } from '~/components';

function isIn<T>(kind: InOrNotIn<T>): kind is { in: T[] } {
return 'in' in kind;
}

export function inOrNotIn<T>(
kind: InOrNotIn<T> | null | undefined,
value: T,
condition: boolean
): InOrNotIn<T> {
if (condition) {
if (kind && isIn(kind)) {
kind.in.push(value);
return kind;
} else {
return { in: [value] };
}
} else {
if (kind && !isIn(kind)) {
kind.notIn.push(value);
return kind;
} else {
return { notIn: [value] };
}
}
}

export function textMatch(type: 'contains' | 'startsWith' | 'endsWith' | 'equals') {
return (value: string): TextMatch => {
switch (type) {
case 'contains':
return { contains: value };
case 'startsWith':
return { startsWith: value };
case 'endsWith':
return { endsWith: value };
case 'equals':
return { equals: value };
default:
throw new Error('Invalid TextMatch type.');
}
};
}

export const filterTypeCondition = {
inOrNotIn: {
in: 'is',
Expand Down
110 changes: 44 additions & 66 deletions interface/app/$libraryId/TopBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,23 @@
import clsx from 'clsx';
import { useCallback, useEffect, useRef, useState, useTransition } from 'react';
import { useLocation, useResolvedPath } from 'react-router';
import { useCallback, useEffect, useLayoutEffect, useRef, useState, useTransition } from 'react';
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/Search/store';
import { useSearchStore } from '../Explorer/Search/store';

export default () => {
const searchRef = useRef<HTMLInputElement>(null);

const [searchParams, setSearchParams] = useZodSearchParams(SearchParamsSchema);
const location = useLocation();

const searchStore = useSearchStore();

const os = useOperatingSystem(true);
const keybind = keybindForOs(os);

// Wrapping param updates in a transition allows us to track whether
// updating the params triggers a Suspense somewhere else, providing a free
// loading state!
const [_isPending, startTransition] = useTransition();

const searchPath = useResolvedPath('search');

const [value, setValue] = useState(searchParams.search ?? '');

const updateParams = useDebouncedCallback((value: string) => {
getSearchStore().searchQuery = value;
startTransition(() =>
setSearchParams((p) => ({ ...p, search: value }), {
replace: true
})
);
}, 300);

const updateValue = useCallback(
(value: string) => {
setValue(value);
// TODO: idk that looked important but uncommenting it fixed my bug
// if (searchPath.pathname === location.pathname)
updateParams(value);
},
[searchPath.pathname, location.pathname, updateParams]
);

const focusHandler = useCallback(
(event: KeyboardEvent) => {
if (
Expand All @@ -62,6 +32,7 @@ export default () => {
);

const blurHandler = useCallback((event: KeyboardEvent) => {
console.log('blurHandler');
if (event.key === 'Escape' && document.activeElement === searchRef.current) {
// Check if element is in focus, then remove it
event.preventDefault();
Expand All @@ -79,50 +50,57 @@ export default () => {
};
}, [blurHandler, focusHandler]);

const [localValue, setLocalValue] = useState(searchParams.search ?? '');

useLayoutEffect(() => setLocalValue(searchParams.search ?? ''), [searchParams.search]);

const updateValueDebounced = useDebouncedCallback((value: string) => {
setSearchParams((p) => ({ ...p, search: value }), { replace: true });
}, 300);

function updateValue(value: string) {
setLocalValue(value);
updateValueDebounced(value);
}

function clearValue() {
setSearchParams(
(p) => {
delete p.search;
return { ...p };
},
{ replace: true }
);
}

return (
<Input
ref={searchRef}
placeholder="Search"
className="mx-2 w-48 transition-all duration-200 focus-within:w-60"
size="sm"
value={localValue}
onChange={(e) => updateValue(e.target.value)}
onBlur={() => {
if (value === '' && !searchStore.interactingWithSearchOptions) {
getSearchStore().isSearching = false;
// setSearchParams({}, { replace: true });
// navigate(-1);
}
}}
onFocus={() => {
getSearchStore().isSearching = true;
// if (searchPath.pathname !== location.pathname) {
// navigate({
// pathname: 'search',
// search: createSearchParams({ search: value }).toString()
// });
// }
if (localValue === '' && !searchStore.interactingWithSearchOptions) clearValue();
}}
value={value}
onFocus={() => updateValueDebounced(localValue)}
right={
<>
<div
className={clsx(
'pointer-events-none flex h-7 items-center space-x-1 opacity-70 group-focus-within:hidden'
)}
>
{
<Shortcut
chars={keybind([ModifierKeys.Control], ['F'])}
aria-label={`Press ${
os === 'macOS' ? 'Command' : ModifierKeys.Control
}-F to focus search bar`}
className="border-none"
/>
}
</div>
{/* This indicates whether the search is loading, a spinner could be put here */}
{/* {_isPending && <div className="w-8 h-8 bg-red-500" />} */}
</>
<div
className={clsx(
'pointer-events-none flex h-7 items-center space-x-1 opacity-70 group-focus-within:hidden'
)}
>
{
<Shortcut
chars={keybind([ModifierKeys.Control], ['F'])}
aria-label={`Press ${
os === 'macOS' ? 'Command' : ModifierKeys.Control
}-F to focus search bar`}
className="border-none"
/>
}
</div>
}
/>
);
Expand Down
8 changes: 6 additions & 2 deletions interface/app/$libraryId/TopBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useKeyMatcher, useOperatingSystem, useShowControls } from '~/hooks';
import { useTabsContext } from '~/TabsContext';

import SearchOptions from '../Explorer/Search';
import { useSearchContext } from '../Explorer/Search/Context';
import { useSearchStore } from '../Explorer/Search/store';
import { useExplorerStore } from '../Explorer/store';
import { useTopBarContext } from './Layout';
Expand All @@ -21,6 +22,7 @@ const TopBar = () => {

const tabs = useTabsContext();
const ctx = useTopBarContext();
const searchCtx = useSearchContext();
const searchStore = useSearchStore();

useResizeObserver({
Expand All @@ -32,12 +34,14 @@ const TopBar = () => {
}
});

const isSearching = searchCtx.searchQuery !== undefined;

// when the component mounts + crucial state changes, we need to update the height _before_ the browser paints
// in order to avoid jank. resize observer doesn't fire early enought to account for this.
useLayoutEffect(() => {
const height = ref.current!.getBoundingClientRect().height;
ctx.setTopBarHeight.call(undefined, height);
}, [ctx.setTopBarHeight, searchStore.isSearching]);
}, [ctx.setTopBarHeight, searchCtx.isSearching]);

return (
<div
Expand Down Expand Up @@ -71,7 +75,7 @@ const TopBar = () => {

{tabs && <Tabs />}

{searchStore.isSearching && (
{searchCtx.isSearching && (
<>
<hr className="w-full border-t border-sidebar-divider bg-sidebar-divider" />
<SearchOptions />
Expand Down
Loading

0 comments on commit 91932c9

Please sign in to comment.