zod-powered search params (#790)

* zod-powered search params

* fix ci

* fix context menu

* fix the *other* context menu

---------

Co-authored-by: Utku Bakir <74243531+utkubakir@users.noreply.github.com>
This commit is contained in:
Brendan Allan
2023-05-05 13:39:52 +08:00
committed by GitHub
parent 51d1cb0618
commit 835de32fd6
12 changed files with 77 additions and 41 deletions

View File

@@ -38,7 +38,7 @@ export const OpenInNativeExplorer = () => {
export default (props: PropsWithChildren) => {
const store = useExplorerStore();
const params = useExplorerSearchParams();
const [params] = useExplorerSearchParams();
const generateThumbsForLocation = useLibraryMutation('jobs.generateThumbsForLocation');
const objectValidator = useLibraryMutation('jobs.objectValidator');

View File

@@ -41,7 +41,7 @@ interface Props extends PropsWithChildren {
export default ({ data, className, ...props }: Props) => {
const store = useExplorerStore();
const params = useExplorerSearchParams();
const [params] = useExplorerSearchParams();
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
const keyManagerUnlocked = useLibraryQuery(['keys.isUnlocked']).data ?? false;

View File

@@ -20,7 +20,7 @@ interface Props {
export default function Explorer(props: Props) {
const { selectedRowIndex, ...expStore } = useExplorerStore();
const { path } = useExplorerSearchParams();
const [{ path }] = useExplorerSearchParams();
useLibrarySubscription(['jobs.newThumbnail'], {
onData: (cas_id) => {

View File

@@ -1,8 +1,6 @@
import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { getParams } from 'remix-params-helper';
import { z } from 'zod';
import { ExplorerItem, ObjectKind, ObjectKindKey, isObject, isPath } from '@sd/client';
import { useZodSearchParams } from '~/hooks';
export function getExplorerItemData(data: ExplorerItem) {
const objectData = getItemObject(data);
@@ -25,17 +23,11 @@ export function getItemFilePath(data: ExplorerItem) {
return isObject(data) ? data.item.file_paths[0] : data.item;
}
const SEARCH_PARAMS = z.object({
export const SEARCH_PARAMS = z.object({
path: z.string().default(''),
limit: z.coerce.number().default(100)
});
export function useExplorerSearchParams() {
const [searchParams] = useSearchParams();
const result = useMemo(() => getParams(searchParams, SEARCH_PARAMS), [searchParams]);
if (!result.success) throw result.errors;
return result.data;
return useZodSearchParams(SEARCH_PARAMS);
}

View File

@@ -20,7 +20,7 @@ export default () => {
>
{showControls && <MacTrafficLights className="absolute left-[13px] top-[13px] z-50" />}
{(os !== 'browser' || showControls) && (
<div className="flex justify-end -mt-[4px]">
<div className="-mt-[4px] flex justify-end">
<NavigationButtons />
</div>
)}

View File

@@ -1,19 +1,25 @@
import clsx from 'clsx';
import { useEffect, useRef, useState, useTransition } from 'react';
import { useCallback, useRef, useState, useTransition } from 'react';
import { useLocation, useNavigate, useResolvedPath } from 'react-router';
import { createSearchParams, useSearchParams } from 'react-router-dom';
import { createSearchParams } from 'react-router-dom';
import { useKey, useKeys } from 'rooks';
import { useDebouncedCallback } from 'use-debounce';
import { z } from 'zod';
import { Input, Shortcut } from '@sd/ui';
import { useZodSearchParams } from '~/hooks';
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
import { getSearchStore } from '~/hooks/useSearchStore';
export const SEARCH_PARAM_KEY = 'search';
export const SEARCH_PARAMS = z.object({
search: z.string().default('')
});
export default () => {
const searchRef = useRef<HTMLInputElement>(null);
const [searchParams, setSearchParams] = useSearchParams();
const [searchParams, setSearchParams] = useZodSearchParams(SEARCH_PARAMS);
const navigate = useNavigate();
const location = useLocation();
@@ -27,20 +33,23 @@ export default () => {
const searchPath = useResolvedPath('search');
const [value, setValue] = useState(searchParams.get(SEARCH_PARAM_KEY) || '');
const [value, setValue] = useState(searchParams.search);
const updateParams = useDebouncedCallback((value: string) => {
startTransition(() =>
setSearchParams((p) => (p.set(SEARCH_PARAM_KEY, value), p), {
setSearchParams((p) => ({ ...p, search: value }), {
replace: true
})
);
}, 300);
useEffect(() => {
if (searchPath.pathname === location.pathname) updateParams(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
const updateValue = useCallback(
(value: string) => {
setValue(value);
if (searchPath.pathname === location.pathname) updateParams(value);
},
[searchPath.pathname, location.pathname, updateParams]
);
useKeys([os === 'macOS' ? 'Meta' : 'Ctrl', 'f'], () => searchRef.current?.focus());
useKey('Escape', () => searchRef.current?.blur());
@@ -51,11 +60,11 @@ export default () => {
placeholder="Search"
className="w-52 transition-all duration-200 focus-within:w-60"
size="sm"
onChange={(e) => setValue(e.target.value)}
onChange={(e) => updateValue(e.target.value)}
onBlur={() => {
getSearchStore().isFocused = false;
if (value === '') {
setSearchParams((p) => (p.delete(SEARCH_PARAM_KEY), p), { replace: true });
setSearchParams({}, { replace: true });
navigate(-1);
}
}}

View File

@@ -25,7 +25,7 @@ const PARAMS = z.object({
});
export const Component = () => {
const { path } = useExplorerSearchParams();
const [{ path }] = useExplorerSearchParams();
const { id: location_id } = useZodRouteParams(PARAMS);
// // we destructure this since `mutate` is a stable reference but the object it's in is not
@@ -108,7 +108,7 @@ const useToolBarOptions = () => {
const useItems = () => {
const { id: location_id } = useZodRouteParams(PARAMS);
const { path, limit } = useExplorerSearchParams();
const [{ path, limit }] = useExplorerSearchParams();
const ctx = useRspcLibraryContext();
const { library } = useLibraryContext();

View File

@@ -1,21 +1,21 @@
import { MagnifyingGlass } from 'phosphor-react';
import { Suspense, memo, useDeferredValue, useEffect, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { z } from 'zod';
import { useLibraryQuery } from '@sd/client';
import { useZodSearchParams } from '~/hooks';
import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
import { useExplorerTopBarOptions } from '~/hooks/useExplorerTopBarOptions';
import Explorer from './Explorer';
import { getExplorerItemData } from './Explorer/util';
import TopBarChildren from './TopBar/TopBarChildren';
const schema = z.object({
const SEARCH_PARAMS = z.object({
search: z.string().optional(),
take: z.number().optional(),
take: z.coerce.number().optional(),
order: z.union([z.object({ name: z.boolean() }), z.object({ name: z.boolean() })]).optional()
});
export type SearchArgs = z.infer<typeof schema>;
export type SearchArgs = z.infer<typeof SEARCH_PARAMS>;
const ExplorerStuff = memo((props: { args: SearchArgs }) => {
const explorerStore = useExplorerStore();
@@ -65,14 +65,9 @@ const ExplorerStuff = memo((props: { args: SearchArgs }) => {
});
export const Component = () => {
const [searchParams] = useSearchParams();
const [searchParams] = useZodSearchParams(SEARCH_PARAMS);
const searchObj = useMemo(
() => schema.parse(Object.fromEntries([...searchParams])),
[searchParams]
);
const search = useDeferredValue(searchObj);
const search = useDeferredValue(searchParams);
return (
<Suspense fallback="LOADING FIRST RENDER">

View File

@@ -17,7 +17,7 @@ export const Component = () => {
locations.data?.filter((location) =>
location.name.toLowerCase().includes(search.toLowerCase())
),
[search]
[search, locations.data]
);
return (

View File

@@ -13,3 +13,4 @@ export * from './useScrolled';
export * from './useSearchStore';
export * from './useToasts';
export * from './useZodRouteParams';
export * from './useZodSearchParams';

View File

@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { useParams } from 'react-router';
import { z } from 'zod';
export function useZodRouteParams<Z extends z.ZodType>(schema: Z): z.infer<Z> {
export function useZodRouteParams<Z extends z.ZodType<Record<string, any>>>(schema: Z) {
// eslint-disable-next-line no-restricted-syntax
const params = useParams();

View File

@@ -0,0 +1,39 @@
import { useCallback, useMemo } from 'react';
import { NavigateOptions, useSearchParams } from 'react-router-dom';
import { getParams } from 'remix-params-helper';
import { z } from 'zod';
export function useZodSearchParams<Z extends z.ZodType<Record<string, any>>>(schema: Z) {
// eslint-disable-next-line no-restricted-syntax
const [searchParams, setSearchParams] = useSearchParams();
const typedSearchParams = useMemo(
() => getParams(searchParams, schema),
[searchParams, schema]
);
if (!typedSearchParams.success) throw typedSearchParams.errors;
return [
typedSearchParams.data,
useCallback(
(
data: z.input<Z> | ((data: z.input<Z>) => z.infer<Z>),
navigateOpts?: NavigateOptions
) => {
if (typeof data === 'function') {
setSearchParams((params) => {
const typedPrevParams = getParams(params, schema);
if (!typedPrevParams.success) throw typedPrevParams.errors;
return data(typedPrevParams.data);
}, navigateOpts);
} else {
setSearchParams(data as any, navigateOpts);
}
},
[setSearchParams, schema]
)
] as const;
}