mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-14 04:14:04 +00:00
[ENG-623] - List view obstruction fix (#839)
* Obstruction fix * add location_id as type * PR tweaks * Update core.ts
This commit is contained in:
parent
36b7bf686a
commit
40beeb2a20
|
@ -72,7 +72,11 @@ const ListViewItem = memo((props: ListViewItemProps) => {
|
|||
);
|
||||
});
|
||||
|
||||
export default () => {
|
||||
interface Props {
|
||||
listViewHeadersClassName?: string;
|
||||
}
|
||||
|
||||
export default (props: Props) => {
|
||||
const explorerStore = useExplorerStore();
|
||||
const dismissibleNoticeStore = useDismissibleNoticeStore();
|
||||
const { data, scrollRef, onLoadMore, hasNextPage, isFetchingNextPage } =
|
||||
|
@ -271,7 +275,6 @@ export default () => {
|
|||
// Resize view on item selection/deselection
|
||||
useEffect(() => {
|
||||
const { selectedRowIndex } = explorerStore;
|
||||
|
||||
if (
|
||||
explorerStore.showInspector &&
|
||||
typeof lastSelectedIndex.current !== typeof selectedRowIndex
|
||||
|
@ -340,7 +343,8 @@ export default () => {
|
|||
onClick={(e) => e.stopPropagation()}
|
||||
className={clsx(
|
||||
'sticky top-0 z-20 table-header-group',
|
||||
isScrolled && 'top-bar-blur !bg-app/90'
|
||||
isScrolled && 'top-bar-blur !bg-app/90',
|
||||
props.listViewHeadersClassName
|
||||
)}
|
||||
>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
|
|
|
@ -84,6 +84,7 @@ interface Props {
|
|||
hasNextPage?: boolean;
|
||||
isFetchingNextPage?: boolean;
|
||||
viewClassName?: string;
|
||||
listViewHeadersClassName?: string;
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
|
@ -118,7 +119,9 @@ export default memo((props: Props) => {
|
|||
}}
|
||||
>
|
||||
{layoutMode === 'grid' && <GridView />}
|
||||
{layoutMode === 'rows' && <ListView />}
|
||||
{layoutMode === 'rows' && (
|
||||
<ListView listViewHeadersClassName={props.listViewHeadersClassName} />
|
||||
)}
|
||||
{layoutMode === 'media' && <MediaView />}
|
||||
</ViewContext.Provider>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ReactNode, useEffect, useMemo } from 'react';
|
|||
import { useKey } from 'rooks';
|
||||
import { ExplorerItem, useLibrarySubscription } from '@sd/client';
|
||||
import { getExplorerStore, useExplorerStore } from '~/hooks';
|
||||
import useKeyDeleteFile from '~/hooks/useKeyDeleteFile';
|
||||
import ExplorerContextMenu from './ContextMenu';
|
||||
import { Inspector } from './Inspector';
|
||||
import View from './View';
|
||||
|
@ -20,12 +21,20 @@ interface Props {
|
|||
children?: ReactNode;
|
||||
inspectorClassName?: string;
|
||||
explorerClassName?: string;
|
||||
listViewHeadersClassName?: string;
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export default function Explorer(props: Props) {
|
||||
const { selectedRowIndex, ...expStore } = useExplorerStore();
|
||||
const [{ path }] = useExplorerSearchParams();
|
||||
const selectedItem = useMemo(() => {
|
||||
if (selectedRowIndex === null) return null;
|
||||
|
||||
return props.items?.[selectedRowIndex] ?? null;
|
||||
}, [selectedRowIndex, props.items]);
|
||||
|
||||
useKeyDeleteFile(selectedItem, expStore.locationId);
|
||||
|
||||
useLibrarySubscription(['jobs.newThumbnail'], {
|
||||
onStarted: () => {
|
||||
|
@ -44,12 +53,6 @@ export default function Explorer(props: Props) {
|
|||
getExplorerStore().selectedRowIndex = null;
|
||||
}, [path]);
|
||||
|
||||
const selectedItem = useMemo(() => {
|
||||
if (selectedRowIndex === null) return null;
|
||||
|
||||
return props.items?.[selectedRowIndex] ?? null;
|
||||
}, [selectedRowIndex, props.items]);
|
||||
|
||||
useKey('Space', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -68,6 +71,7 @@ export default function Explorer(props: Props) {
|
|||
data={props.items}
|
||||
onLoadMore={props.onLoadMore}
|
||||
hasNextPage={props.hasNextPage}
|
||||
listViewHeadersClassName={props.listViewHeadersClassName}
|
||||
isFetchingNextPage={props.isFetchingNextPage}
|
||||
viewClassName={props.viewClassName}
|
||||
/>
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useKey } from 'rooks';
|
||||
import { z } from 'zod';
|
||||
import { useLibraryContext, useLibraryMutation, useLibraryQuery, useRspcLibraryContext } from '@sd/client';
|
||||
import { Folder, dialogManager } from '@sd/ui';
|
||||
import {
|
||||
ExplorerItem,
|
||||
useLibraryContext,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useRspcLibraryContext
|
||||
} from '@sd/client';
|
||||
import { Folder } from '@sd/ui';
|
||||
import {
|
||||
getExplorerStore,
|
||||
useExplorerStore,
|
||||
|
@ -11,10 +16,10 @@ import {
|
|||
useZodRouteParams
|
||||
} from '~/hooks';
|
||||
import Explorer from '../Explorer';
|
||||
import DeleteDialog from '../Explorer/File/DeleteDialog';
|
||||
import { useExplorerOrder, useExplorerSearchParams } from '../Explorer/util';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
import TopBarOptions from '../TopBar/TopBarOptions';
|
||||
import useKeyDeleteFile from '~/hooks/useKeyDeleteFile';
|
||||
|
||||
const PARAMS = z.object({
|
||||
id: z.coerce.number()
|
||||
|
@ -32,36 +37,22 @@ export const Component = () => {
|
|||
const { mutate: quickRescan } = useLibraryMutation('locations.quickRescan');
|
||||
|
||||
const explorerStore = getExplorerStore();
|
||||
|
||||
useEffect(() => {
|
||||
explorerStore.locationId = location_id;
|
||||
if (location_id !== null) quickRescan({ location_id, sub_path: path ?? '' });
|
||||
}, [explorerStore, location_id, path, quickRescan]);
|
||||
|
||||
const { query, items } = useItems();
|
||||
const file = explorerStore.selectedRowIndex !== null && items?.[explorerStore.selectedRowIndex];
|
||||
useKeyDeleteFile(file as ExplorerItem, location_id)
|
||||
|
||||
useKey('Delete', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const explorerStore = getExplorerStore();
|
||||
|
||||
if (explorerStore.selectedRowIndex === null) return;
|
||||
|
||||
const file = items?.[explorerStore.selectedRowIndex];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
dialogManager.create((dp) => (
|
||||
<DeleteDialog {...dp} location_id={location_id} path_id={file.item.id} />
|
||||
));
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopBarPortal
|
||||
left={
|
||||
<>
|
||||
<Folder size={22} className="ml-3 mr-2 -mt-[1px] inline-block" />
|
||||
<Folder size={22} className="-mt-[1px] ml-3 mr-2 inline-block" />
|
||||
<span className="text-sm font-medium">
|
||||
{path ? getLastSectionOfPath(path) : location?.name}
|
||||
</span>
|
||||
|
@ -73,7 +64,7 @@ export const Component = () => {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
<div className="relative flex w-full flex-col">
|
||||
<div className="relative flex flex-col w-full">
|
||||
<Explorer
|
||||
items={items}
|
||||
onLoadMore={query.fetchNextPage}
|
||||
|
@ -125,7 +116,6 @@ const useItems = () => {
|
|||
return { query, items };
|
||||
};
|
||||
|
||||
|
||||
function getLastSectionOfPath(path: string): string | undefined {
|
||||
if (path.endsWith('/')) {
|
||||
path = path.slice(0, -1);
|
||||
|
|
|
@ -135,17 +135,18 @@ export const Component = () => {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
<Statistics />
|
||||
<Explorer
|
||||
inspectorClassName="!pt-0 !fixed !top-[50px] !right-[10px] !w-[260px]"
|
||||
explorerClassName="!overflow-visible" // required to ensure categories are sticky, remove with caution
|
||||
viewClassName="!pl-0 !pt-0 !h-auto"
|
||||
inspectorClassName="!pt-0 !fixed !top-[50px] !right-[10px] !w-[260px]"
|
||||
viewClassName="!pl-0 !pt-[0] !h-auto !overflow-visible"
|
||||
explorerClassName="!overflow-visible" //required to keep categories sticky, remove with caution
|
||||
listViewHeadersClassName="!top-[65px] z-30"
|
||||
items={items}
|
||||
onLoadMore={query.fetchNextPage}
|
||||
hasNextPage={query.hasNextPage}
|
||||
isFetchingNextPage={query.isFetchingNextPage}
|
||||
scrollRef={page?.ref}
|
||||
>
|
||||
<Statistics />
|
||||
<div className="no-scrollbar sticky top-0 z-10 mt-2 flex space-x-[1px] overflow-x-scroll bg-app/90 px-5 py-1.5 backdrop-blur">
|
||||
{categories.data?.map((category) => {
|
||||
const iconString = CategoryToIcon[category.name] || 'Document';
|
||||
|
@ -156,7 +157,9 @@ export const Component = () => {
|
|||
icon={getIcon(iconString, isDark)}
|
||||
items={category.count}
|
||||
selected={selectedCategory === category.name}
|
||||
onClick={() => setSelectedCategory(category.name)}
|
||||
onClick={() => {
|
||||
setSelectedCategory(category.name);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
18
interface/hooks/useKeyDeleteFile.tsx
Normal file
18
interface/hooks/useKeyDeleteFile.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { useKey } from 'rooks';
|
||||
import { ExplorerItem } from '@sd/client';
|
||||
import { dialogManager } from '@sd/ui';
|
||||
import DeleteDialog from '../app/$libraryId/Explorer/File/DeleteDialog';
|
||||
|
||||
const useKeyDeleteFile = (selectedItem: ExplorerItem | null, location_id: number | null) => {
|
||||
return useKey('Delete', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!selectedItem || !location_id) return;
|
||||
|
||||
dialogManager.create((dp) => (
|
||||
<DeleteDialog {...dp} location_id={location_id} path_id={selectedItem.item.id} />
|
||||
));
|
||||
});
|
||||
};
|
||||
|
||||
export default useKeyDeleteFile;
|
|
@ -103,6 +103,8 @@ export type NodeConfig = { id: string; name: string; p2p_port: number | null; p2
|
|||
|
||||
export type CategoryItem = { name: string; count: number }
|
||||
|
||||
export type Location = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string }
|
||||
|
||||
/**
|
||||
* This denotes the `StoredKey` version.
|
||||
*/
|
||||
|
@ -117,12 +119,12 @@ export type EncryptedKey = number[]
|
|||
|
||||
export type PeerId = string
|
||||
|
||||
export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null }
|
||||
|
||||
export type GenerateThumbsForLocationArgs = { id: number; path: string }
|
||||
|
||||
export type LibraryConfigWrapped = { uuid: string; config: LibraryConfig }
|
||||
|
||||
export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string }
|
||||
|
||||
/**
|
||||
* These parameters define the password-hashing level.
|
||||
*
|
||||
|
@ -199,17 +201,13 @@ export type ObjectSearchArgs = { take?: number | null; tagId?: number | null; cu
|
|||
|
||||
export type SetNoteArgs = { id: number; note: string | null }
|
||||
|
||||
export type Statistics = { id: number; date_captured: string; total_object_count: number; library_db_size: string; total_bytes_used: string; total_bytes_capacity: string; total_unique_bytes: string; total_bytes_free: string; preview_media_bytes: string }
|
||||
|
||||
export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string }
|
||||
|
||||
export type FilePathSearchOrdering = { name: boolean } | { sizeInBytes: boolean } | { dateCreated: boolean } | { dateModified: boolean } | { dateIndexed: boolean } | { object: ObjectSearchOrdering }
|
||||
|
||||
export type FileCopierJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string; target_file_name_suffix: string | null }
|
||||
|
||||
export type Location = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string }
|
||||
export type Statistics = { id: number; date_captured: string; total_object_count: number; library_db_size: string; total_bytes_used: string; total_bytes_capacity: string; total_unique_bytes: string; total_bytes_free: string; preview_media_bytes: string }
|
||||
|
||||
export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null }
|
||||
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string }
|
||||
|
||||
export type BuildInfo = { version: string; commit: string }
|
||||
|
||||
|
@ -222,12 +220,14 @@ export type Algorithm = "XChaCha20Poly1305" | "Aes256Gcm"
|
|||
|
||||
export type ObjectSearchOrdering = { dateAccessed: boolean }
|
||||
|
||||
export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string }
|
||||
|
||||
export type OwnedOperationItem = { id: any; data: OwnedOperationData }
|
||||
|
||||
export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null }
|
||||
|
||||
export type CRDTOperationType = SharedOperation | RelationOperation | OwnedOperation
|
||||
|
||||
export type IndexerRule = { id: number; kind: number; name: string; default: boolean; parameters: number[]; date_created: string; date_modified: string }
|
||||
|
||||
/**
|
||||
* TODO: P2P event for the frontend
|
||||
*/
|
||||
|
@ -289,6 +289,8 @@ export type OwnedOperationData = { Create: { [key: string]: any } } | { CreateMa
|
|||
|
||||
export type SharedOperationData = SharedOperationCreateData | { field: string; value: any } | null
|
||||
|
||||
export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string }
|
||||
|
||||
export type SearchData<T> = { cursor: number[] | null; items: T[] }
|
||||
|
||||
export type OptionalRange<T> = { from: T | null; to: T | null }
|
||||
|
@ -301,6 +303,8 @@ export type TagAssignArgs = { object_id: number; tag_id: number; unassign: boole
|
|||
|
||||
export type ChangeNodeNameArgs = { name: string }
|
||||
|
||||
export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null }
|
||||
|
||||
/**
|
||||
* This defines all available password hashing algorithms.
|
||||
*/
|
||||
|
@ -321,14 +325,10 @@ export type AutomountUpdateArgs = { uuid: string; status: boolean }
|
|||
|
||||
export type Protected<T> = T
|
||||
|
||||
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string }
|
||||
|
||||
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused" | "CompletedWithErrors"
|
||||
|
||||
export type RestoreBackupArgs = { password: Protected<string>; secret_key: Protected<string>; path: string }
|
||||
|
||||
export type IndexerRule = { id: number; kind: number; name: string; default: boolean; parameters: number[]; date_created: string; date_modified: string }
|
||||
|
||||
export type RelationOperation = { relation_item: string; relation_group: string; relation: string; data: RelationOperationData }
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue