mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-19 05:45:01 -04:00
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:
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './useOperatingSystem';
|
||||
export * from './useScrolled';
|
||||
export * from './useSearchStore';
|
||||
export * from './useToasts';
|
||||
export * from './useZodRouteParams';
|
||||
|
||||
9
interface/hooks/useZodRouteParams.tsx
Normal file
9
interface/hooks/useZodRouteParams.tsx
Normal 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]);
|
||||
}
|
||||
@@ -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
BIN
pnpm-lock.yaml
generated
Binary file not shown.
Reference in New Issue
Block a user