mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 21:36:56 -04:00
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:
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -17,7 +17,7 @@ export const Component = () => {
|
||||
locations.data?.filter((location) =>
|
||||
location.name.toLowerCase().includes(search.toLowerCase())
|
||||
),
|
||||
[search]
|
||||
[search, locations.data]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,3 +13,4 @@ export * from './useScrolled';
|
||||
export * from './useSearchStore';
|
||||
export * from './useToasts';
|
||||
export * from './useZodRouteParams';
|
||||
export * from './useZodSearchParams';
|
||||
|
||||
@@ -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();
|
||||
|
||||
39
interface/hooks/useZodSearchParams.ts
Normal file
39
interface/hooks/useZodSearchParams.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user