From 7ecf12141d7fbe0d5c956f2ba5bce95a7c837ebc Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 5 May 2023 03:16:07 +0800 Subject: [PATCH] fix recursive imports and explorer params (#787) * fix recursive imports and explorer params * fix types --------- Co-authored-by: Utku Bakir <74243531+utkubakir@users.noreply.github.com> --- .../app/$libraryId/Explorer/ContextMenu.tsx | 4 +- .../$libraryId/Explorer/File/ContextMenu.tsx | 5 +- interface/app/$libraryId/Explorer/index.tsx | 21 +--- interface/app/$libraryId/Explorer/util.ts | 19 ++++ interface/app/$libraryId/Layout/index.tsx | 11 +- interface/app/$libraryId/location/$id.tsx | 106 +++++++++++------- .../settings/library/locations/$id.tsx | 15 ++- interface/app/$libraryId/tag/$id.tsx | 13 ++- interface/hooks/index.ts | 1 + interface/hooks/useZodRouteParams.tsx | 9 ++ interface/package.json | 1 + pnpm-lock.yaml | Bin 696868 -> 697210 bytes 12 files changed, 128 insertions(+), 77 deletions(-) create mode 100644 interface/hooks/useZodRouteParams.tsx diff --git a/interface/app/$libraryId/Explorer/ContextMenu.tsx b/interface/app/$libraryId/Explorer/ContextMenu.tsx index 15a1d157a..9f170f4db 100644 --- a/interface/app/$libraryId/Explorer/ContextMenu.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu.tsx @@ -2,10 +2,10 @@ import { Clipboard, FileX, Image, Plus, Repeat, Share, ShieldCheck } from 'phosp import { PropsWithChildren, useMemo } from 'react'; import { useLibraryMutation } from '@sd/client'; import { ContextMenu as CM } from '@sd/ui'; -import { useExplorerParams } from '~/app/$libraryId/location/$id'; import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; import { useOperatingSystem } from '~/hooks/useOperatingSystem'; import { usePlatform } from '~/util/Platform'; +import { useExplorerSearchParams } from './util'; export const OpenInNativeExplorer = () => { const platform = usePlatform(); @@ -38,7 +38,7 @@ export const OpenInNativeExplorer = () => { export default (props: PropsWithChildren) => { const store = useExplorerStore(); - const params = useExplorerParams(); + 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 df68f13fa..04a951b8b 100644 --- a/interface/app/$libraryId/Explorer/File/ContextMenu.tsx +++ b/interface/app/$libraryId/Explorer/File/ContextMenu.tsx @@ -22,14 +22,13 @@ import { useLibraryQuery } from '@sd/client'; import { ContextMenu, dialogManager } from '@sd/ui'; -import { useExplorerParams } from '~/app/$libraryId/location/$id'; import { showAlertDialog } from '~/components/AlertDialog'; import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; import { useOperatingSystem } from '~/hooks/useOperatingSystem'; import { usePlatform } from '~/util/Platform'; import AssignTagMenuItems from '../AssignTagMenuItems'; import { OpenInNativeExplorer } from '../ContextMenu'; -import { getItemFilePath } from '../util'; +import { getItemFilePath, useExplorerSearchParams } from '../util'; import DecryptDialog from './DecryptDialog'; import DeleteDialog from './DeleteDialog'; import EncryptDialog from './EncryptDialog'; @@ -42,7 +41,7 @@ interface Props extends PropsWithChildren { export default ({ data, className, ...props }: Props) => { const store = useExplorerStore(); - const params = useExplorerParams(); + 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 927aae476..bd25b22fd 100644 --- a/interface/app/$libraryId/Explorer/index.tsx +++ b/interface/app/$libraryId/Explorer/index.tsx @@ -1,13 +1,11 @@ import { useEffect } from 'react'; import { useKey } from 'rooks'; import { ExplorerData, useLibrarySubscription } from '@sd/client'; -import { dialogManager } from '~/../packages/ui/src'; import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; -import { Inspector } from '../Explorer/Inspector'; -import { useExplorerParams } from '../location/$id'; import ExplorerContextMenu from './ContextMenu'; -import DeleteDialog from './File/DeleteDialog'; +import { Inspector } from './Inspector'; import View from './View'; +import { useExplorerSearchParams } from './util'; interface Props { // TODO: not using data since context isn't actually used @@ -22,7 +20,7 @@ interface Props { export default function Explorer(props: Props) { const { selectedRowIndex, ...expStore } = useExplorerStore(); - const { location_id, path } = useExplorerParams(); + const { path } = useExplorerSearchParams(); useLibrarySubscription(['jobs.newThumbnail'], { onData: (cas_id) => { @@ -32,7 +30,7 @@ export default function Explorer(props: Props) { useEffect(() => { getExplorerStore().selectedRowIndex = -1; - }, [location_id, path]); + }, [path]); useKey('Space', (e) => { e.preventDefault(); @@ -42,17 +40,6 @@ export default function Explorer(props: Props) { } }); - useKey('Delete', (e) => { - e.preventDefault(); - if (selectedRowIndex !== -1) { - const file = props.items?.[selectedRowIndex]; - if (file && location_id) - dialogManager.create((dp) => ( - - )); - } - }); - return (
diff --git a/interface/app/$libraryId/Explorer/util.ts b/interface/app/$libraryId/Explorer/util.ts index cc67cb623..0870ef2eb 100644 --- a/interface/app/$libraryId/Explorer/util.ts +++ b/interface/app/$libraryId/Explorer/util.ts @@ -1,3 +1,7 @@ +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'; export function getExplorerItemData(data: ExplorerItem) { @@ -20,3 +24,18 @@ export function getItemObject(data: ExplorerItem) { export function getItemFilePath(data: ExplorerItem) { return isObject(data) ? data.item.file_paths[0] : data.item; } + +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; +} diff --git a/interface/app/$libraryId/Layout/index.tsx b/interface/app/$libraryId/Layout/index.tsx index db5075342..ec363a693 100644 --- a/interface/app/$libraryId/Layout/index.tsx +++ b/interface/app/$libraryId/Layout/index.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx'; import { Suspense } from 'react'; -import { Navigate, Outlet, useLocation, useParams } from 'react-router-dom'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; +import { z } from 'zod'; import { ClientContextProvider, LibraryContextProvider, @@ -8,7 +9,7 @@ import { useClientContext, usePlausiblePageViewMonitor } from '@sd/client'; -import { useOperatingSystem } from '~/hooks/useOperatingSystem'; +import { useOperatingSystem, useZodRouteParams } from '~/hooks'; import { usePlatform } from '~/util/Platform'; import { QuickPreview } from '../Explorer/QuickPreview'; import Sidebar from './Sidebar'; @@ -66,8 +67,12 @@ const Layout = () => { ); }; +const PARAMS = z.object({ + libraryId: z.string() +}); + export const Component = () => { - const params = useParams<{ libraryId: string }>(); + const params = useZodRouteParams(PARAMS); return ( diff --git a/interface/app/$libraryId/location/$id.tsx b/interface/app/$libraryId/location/$id.tsx index 34a615465..971be7d55 100644 --- a/interface/app/$libraryId/location/$id.tsx +++ b/interface/app/$libraryId/location/$id.tsx @@ -1,74 +1,58 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { ArrowClockwise, Key, Tag } from 'phosphor-react'; import { useEffect, useMemo } from 'react'; -import { useParams, useSearchParams } from 'react-router-dom'; +import { useKey } from 'rooks'; +import { z } from 'zod'; import { ExplorerData, - LibraryArgs, - LocationExplorerArgs, useLibraryContext, useLibraryMutation, useRspcLibraryContext } from '@sd/client'; +import { dialogManager } from '@sd/ui'; +import { useZodRouteParams } from '~/hooks'; import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; import { useExplorerTopBarOptions } from '~/hooks/useExplorerTopBarOptions'; import Explorer from '../Explorer'; +import DeleteDialog from '../Explorer/File/DeleteDialog'; +import { useExplorerSearchParams } from '../Explorer/util'; import { KeyManager } from '../KeyManager'; import { TOP_BAR_ICON_STYLE, ToolOption } from '../TopBar'; import TopBarChildren from '../TopBar/TopBarChildren'; -export function useExplorerParams() { - const { id } = useParams<{ id?: string }>(); - const location_id = id ? Number(id) : null; - - const [searchParams] = useSearchParams(); - const path = searchParams.get('path') || ''; - const limit = Number(searchParams.get('limit')) || 100; - - return { location_id, path, limit }; -} +const PARAMS = z.object({ + id: z.coerce.number() +}); export const Component = () => { - const { location_id, path } = useExplorerParams(); + 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 const quickRescan = useLibraryMutation('locations.quickRescan'); - const explorerStore = useExplorerStore(); - const explorerState = getExplorerStore(); + const explorerStore = getExplorerStore(); useEffect(() => { - explorerState.locationId = location_id; - if (location_id !== null) quickRescan.mutate({ location_id, sub_path: path }); - }, [explorerState, location_id, path, quickRescan.mutate]); + explorerStore.locationId = location_id; + if (location_id !== null) quickRescan.mutate({ location_id, sub_path: path ?? '' }); + }, [explorerStore, location_id, path, quickRescan]); - if (location_id === null) throw new Error(`location_id is null!`); + const { selectedRowIndex } = useExplorerStore(); - const ctx = useRspcLibraryContext(); - const { library } = useLibraryContext(); + const { query, items } = useItems(); - const query = useInfiniteQuery({ - queryKey: [ - 'locations.getExplorerData', - { - library_id: library.uuid, - arg: { - location_id, - path: explorerStore.layoutMode === 'media' ? null : path, - kind: explorerStore.layoutMode === 'media' ? [5, 7] : null - } - } as LibraryArgs - ] as const, - queryFn: async ({ pageParam: cursor, queryKey }): Promise => { - const arg = queryKey[1]; - arg.arg.cursor = cursor as number[] | undefined; - - return await ctx.client.query(['locations.getExplorerData', arg.arg]); - }, - getNextPageParam: (lastPage) => lastPage.cursor ?? undefined + useKey('Delete', (e) => { + e.preventDefault(); + if (selectedRowIndex !== -1) { + const file = items?.[selectedRowIndex]; + if (file) + dialogManager.create((dp) => ( + + )); + } }); - const items = useMemo(() => query.data?.pages.flatMap((d) => d.items), [query.data]); - return ( <> @@ -121,3 +105,39 @@ const useToolBarOptions = () => { explorerControlOptions ] satisfies ToolOption[][]; }; + +const useItems = () => { + const { id: location_id } = useZodRouteParams(PARAMS); + const { path, limit } = useExplorerSearchParams(); + + const ctx = useRspcLibraryContext(); + const { library } = useLibraryContext(); + + const explorerState = useExplorerStore(); + + const query = useInfiniteQuery({ + queryKey: [ + 'locations.getExplorerData', + { + library_id: library.uuid, + arg: { + location_id, + path: explorerState.layoutMode === 'media' ? null : path, + limit, + kind: explorerState.layoutMode === 'media' ? [5, 7] : null + } + } + ] as const, + queryFn: async ({ pageParam: cursor, queryKey }): Promise => { + const arg = queryKey[1]; + (arg.arg as any).cursor = cursor; + + return await ctx.client.query(['locations.getExplorerData', arg.arg]); + }, + getNextPageParam: (lastPage) => lastPage.cursor ?? undefined + }); + + const items = useMemo(() => query.data?.pages.flatMap((d) => d.items), [query.data]); + + return { query, items }; +}; diff --git a/interface/app/$libraryId/settings/library/locations/$id.tsx b/interface/app/$libraryId/settings/library/locations/$id.tsx index 088d2ab11..8290887dc 100644 --- a/interface/app/$libraryId/settings/library/locations/$id.tsx +++ b/interface/app/$libraryId/settings/library/locations/$id.tsx @@ -2,10 +2,11 @@ import { useQueryClient } from '@tanstack/react-query'; import { Archive, ArrowsClockwise, Info, Trash } from 'phosphor-react'; import { useState } from 'react'; import { Controller } from 'react-hook-form'; -import { useNavigate, useParams } from 'react-router'; +import { useNavigate } from 'react-router'; import { useLibraryMutation, useLibraryQuery } from '@sd/client'; import { Button, Divider, Tooltip, forms, tw } from '@sd/ui'; import { showAlertDialog } from '~/components/AlertDialog'; +import { useZodRouteParams } from '~/hooks'; import ModalLayout from '../../ModalLayout'; import { IndexerRuleEditor } from './IndexerRuleEditor'; @@ -25,6 +26,10 @@ const schema = z.object({ generatePreviewMedia: z.boolean() }); +const PARAMS = z.object({ + id: z.coerce.number().default(0) +}); + export const Component = () => { const form = useZodForm({ schema, @@ -32,7 +37,9 @@ export const Component = () => { indexerRulesIds: [] } }); - const params = useParams<{ id: string }>(); + + const { id: locationId } = useZodRouteParams(PARAMS); + const navigate = useNavigate(); const fullRescan = useLibraryMutation('locations.fullRescan'); const queryClient = useQueryClient(); @@ -51,9 +58,7 @@ export const Component = () => { }); const { isDirty } = form.formState; - // Default to first location if no id is provided - // fallback to 0 (which should always be an invalid location) when parsing fails - const locationId = (params.id ? Number(params.id) : 1) || 0; + useLibraryQuery(['locations.getById', locationId], { onSettled: (data, error) => { if (isFirstLoad) { diff --git a/interface/app/$libraryId/tag/$id.tsx b/interface/app/$libraryId/tag/$id.tsx index a152369c3..b23ab9569 100644 --- a/interface/app/$libraryId/tag/$id.tsx +++ b/interface/app/$libraryId/tag/$id.tsx @@ -1,11 +1,16 @@ -import { useParams } from 'react-router-dom'; +import { z } from 'zod'; import { useLibraryQuery } from '@sd/client'; +import { useZodRouteParams } from '~/hooks'; import Explorer from '../Explorer'; -export const Component = () => { - const { id } = useParams<{ id: string }>(); +const PARAMS = z.object({ + id: z.coerce.number() +}); - const explorerData = useLibraryQuery(['tags.getExplorerData', Number(id)]); +export const Component = () => { + const { id } = useZodRouteParams(PARAMS); + + const explorerData = useLibraryQuery(['tags.getExplorerData', id]); return (
diff --git a/interface/hooks/index.ts b/interface/hooks/index.ts index 034234126..350a55d74 100644 --- a/interface/hooks/index.ts +++ b/interface/hooks/index.ts @@ -12,3 +12,4 @@ export * from './useOperatingSystem'; export * from './useScrolled'; export * from './useSearchStore'; export * from './useToasts'; +export * from './useZodRouteParams'; diff --git a/interface/hooks/useZodRouteParams.tsx b/interface/hooks/useZodRouteParams.tsx new file mode 100644 index 000000000..b61a14b7d --- /dev/null +++ b/interface/hooks/useZodRouteParams.tsx @@ -0,0 +1,9 @@ +import { useMemo } from 'react'; +import { useParams } from 'react-router'; +import { z } from 'zod'; + +export function useZodRouteParams(schema: Z): z.infer { + const params = useParams(); + + return useMemo(() => schema.parse(params), [params, schema]); +} diff --git a/interface/package.json b/interface/package.json index 20cd97597..a292a373e 100644 --- a/interface/package.json +++ b/interface/package.json @@ -55,6 +55,7 @@ "react-responsive": "^9.0.2", "react-router": "6.9.0", "react-router-dom": "6.9.0", + "remix-params-helper": "^0.4.10", "rooks": "^5.14.0", "tailwindcss": "^3.1.8", "use-count-up": "^3.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f8d0b5cb3fa8b5f6074b9a48e27210625777ce1..7c15dc1b9804627778b8294343d55b3b6f86b61b 100644 GIT binary patch delta 315 zcmZ2-MC;cvtqt#NrBZV7XXVSdewSBiMBM>uf-|fo0`0?a(?h@0Fce5K$Zm42yU#7_d#2}gth}pL< z)8z2GB7<(PK9aH1|4VXehZ_1s8Mcxz97 zcubKK=yaerjr9ztum8>_Gx>--q`9c+I`#((< xAO_KFK+L}VpC*UjmG*%D9NPo_a|%SmRcmaY6UB9hXZoo}Y(m>>(z*Uw0|0gHDHH$z