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>
This commit is contained in:
Brendan Allan
2023-05-05 03:16:07 +08:00
committed by GitHub
parent 0849e9d605
commit 7ecf12141d
12 changed files with 128 additions and 77 deletions

View File

@@ -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');

View File

@@ -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;

View File

@@ -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) => (
<DeleteDialog {...dp} location_id={location_id} path_id={file.item.id} />
));
}
});
return (
<div className="flex h-screen w-full flex-col bg-app">
<div className="flex flex-1">

View File

@@ -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;
}

View File

@@ -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 (
<ClientContextProvider currentLibraryId={params.libraryId ?? null}>

View File

@@ -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<LocationExplorerArgs>
] as const,
queryFn: async ({ pageParam: cursor, queryKey }): Promise<ExplorerData> => {
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) => (
<DeleteDialog {...dp} location_id={location_id} path_id={file.item.id} />
));
}
});
const items = useMemo(() => query.data?.pages.flatMap((d) => d.items), [query.data]);
return (
<>
<TopBarChildren toolOptions={useToolBarOptions()} />
@@ -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<ExplorerData> => {
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 };
};

View File

@@ -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) {

View File

@@ -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 (
<div className="w-full">

View File

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

View File

@@ -0,0 +1,9 @@
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> {
const params = useParams();
return useMemo(() => schema.parse(params), [params, schema]);
}

View File

@@ -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",

BIN
pnpm-lock.yaml generated
View File

Binary file not shown.