diff --git a/interface/app/$libraryId/Explorer/ContextMenu.tsx b/interface/app/$libraryId/Explorer/ContextMenu.tsx
index 9f170f4db..88c800a94 100644
--- a/interface/app/$libraryId/Explorer/ContextMenu.tsx
+++ b/interface/app/$libraryId/Explorer/ContextMenu.tsx
@@ -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');
diff --git a/interface/app/$libraryId/Explorer/File/ContextMenu.tsx b/interface/app/$libraryId/Explorer/File/ContextMenu.tsx
index 04a951b8b..20ed77bd8 100644
--- a/interface/app/$libraryId/Explorer/File/ContextMenu.tsx
+++ b/interface/app/$libraryId/Explorer/File/ContextMenu.tsx
@@ -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;
diff --git a/interface/app/$libraryId/Explorer/index.tsx b/interface/app/$libraryId/Explorer/index.tsx
index bd25b22fd..14c33820c 100644
--- a/interface/app/$libraryId/Explorer/index.tsx
+++ b/interface/app/$libraryId/Explorer/index.tsx
@@ -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) => {
diff --git a/interface/app/$libraryId/Explorer/util.ts b/interface/app/$libraryId/Explorer/util.ts
index 0870ef2eb..bfad83886 100644
--- a/interface/app/$libraryId/Explorer/util.ts
+++ b/interface/app/$libraryId/Explorer/util.ts
@@ -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);
}
diff --git a/interface/app/$libraryId/Layout/Sidebar/index.tsx b/interface/app/$libraryId/Layout/Sidebar/index.tsx
index c525dd4ba..4a430f771 100644
--- a/interface/app/$libraryId/Layout/Sidebar/index.tsx
+++ b/interface/app/$libraryId/Layout/Sidebar/index.tsx
@@ -20,7 +20,7 @@ export default () => {
>
{showControls && }
{(os !== 'browser' || showControls) && (
-
+
)}
diff --git a/interface/app/$libraryId/TopBar/SearchBar.tsx b/interface/app/$libraryId/TopBar/SearchBar.tsx
index ffdaf2e15..de1c309f7 100644
--- a/interface/app/$libraryId/TopBar/SearchBar.tsx
+++ b/interface/app/$libraryId/TopBar/SearchBar.tsx
@@ -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
(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);
}
}}
diff --git a/interface/app/$libraryId/location/$id.tsx b/interface/app/$libraryId/location/$id.tsx
index 971be7d55..1651159c5 100644
--- a/interface/app/$libraryId/location/$id.tsx
+++ b/interface/app/$libraryId/location/$id.tsx
@@ -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();
diff --git a/interface/app/$libraryId/search.tsx b/interface/app/$libraryId/search.tsx
index fa41fce3a..a8d3c9862 100644
--- a/interface/app/$libraryId/search.tsx
+++ b/interface/app/$libraryId/search.tsx
@@ -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;
+export type SearchArgs = z.infer;
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 (
diff --git a/interface/app/$libraryId/settings/library/locations/index.tsx b/interface/app/$libraryId/settings/library/locations/index.tsx
index 97aa3aa56..0590d2062 100644
--- a/interface/app/$libraryId/settings/library/locations/index.tsx
+++ b/interface/app/$libraryId/settings/library/locations/index.tsx
@@ -17,7 +17,7 @@ export const Component = () => {
locations.data?.filter((location) =>
location.name.toLowerCase().includes(search.toLowerCase())
),
- [search]
+ [search, locations.data]
);
return (
diff --git a/interface/hooks/index.ts b/interface/hooks/index.ts
index 350a55d74..765daee99 100644
--- a/interface/hooks/index.ts
+++ b/interface/hooks/index.ts
@@ -13,3 +13,4 @@ export * from './useScrolled';
export * from './useSearchStore';
export * from './useToasts';
export * from './useZodRouteParams';
+export * from './useZodSearchParams';
diff --git a/interface/hooks/useZodRouteParams.tsx b/interface/hooks/useZodRouteParams.ts
similarity index 73%
rename from interface/hooks/useZodRouteParams.tsx
rename to interface/hooks/useZodRouteParams.ts
index cccb505cc..3b4e2253c 100644
--- a/interface/hooks/useZodRouteParams.tsx
+++ b/interface/hooks/useZodRouteParams.ts
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { useParams } from 'react-router';
import { z } from 'zod';
-export function useZodRouteParams(schema: Z): z.infer {
+export function useZodRouteParams>>(schema: Z) {
// eslint-disable-next-line no-restricted-syntax
const params = useParams();
diff --git a/interface/hooks/useZodSearchParams.ts b/interface/hooks/useZodSearchParams.ts
new file mode 100644
index 000000000..2a317f67f
--- /dev/null
+++ b/interface/hooks/useZodSearchParams.ts
@@ -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>>(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 | ((data: z.input) => z.infer),
+ 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;
+}