feat: Refactor sidebar props to simplify data flow

This commit is contained in:
2025-12-06 23:04:49 +01:00
parent 54d72138d2
commit bd1cdc0817
8 changed files with 111 additions and 247 deletions
+3 -3
View File
@@ -14,11 +14,11 @@ import useDocumentsShell from './useDocumentsShell';
import useFolderData from '../documents/data/useFolderData';
const DocumentsInner: React.FC<{
sidebarProps: ComponentProps<typeof Sidebar> | null;
surfaceConfig: any;
folderData: any;
onNavigate: (documentId: string) => void;
}> = ({ sidebarProps, surfaceConfig, folderData, onNavigate }) => {
sidebarProps: ComponentProps<typeof Sidebar> | null;
}> = ({ surfaceConfig, folderData, onNavigate, sidebarProps }) => {
const { openPreview } = usePreviewContext();
const { collapsed: sidebarCollapsed } = useSidebarContext();
const {
@@ -112,10 +112,10 @@ const DocumentsRouteContent: React.FC = () => {
onNavigate={handleDocumentNavigate}
>
<DocumentsInner
sidebarProps={sidebarProps}
surfaceConfig={surfaceConfig}
folderData={folderData}
onNavigate={handleDocumentNavigate}
sidebarProps={sidebarProps}
/>
</PreviewProvider>
</DocumentsFilterProvider>
@@ -157,9 +157,6 @@ const useDocumentsWorkspace = ({
const { client: apiClient } = useApi();
const tenantRecord = (tenant ?? null) as TenantOption | null;
const tenantNameCandidate = tenantRecord?.name ?? tenantRecord?.slug ?? null;
const tenantName = tenantNameCandidate ? String(tenantNameCandidate) : null;
const currentTenantId: Identifier | null = (tenantRecord?.id ?? null) as Identifier | null;
const tenantOptions: TenantOption[] = Array.isArray(tenantOptionsRaw)
@@ -606,7 +603,6 @@ const useDocumentsWorkspace = ({
const {
dropOverlayState,
handleFileDrop,
handleFileSelection,
uploadQueue,
clearUploadQueue,
resetUploadsState,
@@ -1242,7 +1238,6 @@ const useDocumentsWorkspace = ({
);
const sidebarProps = useSidebarProps({
folderNodes,
folderClickHandlers,
handleFolderDelete,
handleFolderRename,
@@ -1252,20 +1247,6 @@ const useDocumentsWorkspace = ({
draggedFolderId,
handlePromptCreateFolder,
creatingFolder,
tags,
handleTagCreate,
correspondents,
handleCorrespondentCreate,
appStatus,
previewActive,
handleLogout,
tenantName,
tenantOptions,
currentTenantId,
handleTenantSelect,
openSettings,
handleFileSelection,
uploadQueue,
});
const contextValue = {
@@ -1320,6 +1301,10 @@ const useDocumentsWorkspace = ({
documentsManager,
foldersManager,
handleBreadcrumbNavigate,
tenant: tenantRecord,
tenants: tenantOptions,
tenantOptions,
handleTenantSelect,
};
// hook callers handle rendering / routing
+52 -51
View File
@@ -1,23 +1,19 @@
import React, { useRef } from 'react';
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
import React, { useRef, useSyncExternalStore, useCallback } from 'react';
import { useAppShell } from '../lib/context/AppShellContext';
import FoldersManager from '../documents/FoldersManager';
import { usePanelResizeBindings } from '../app/PanelManagerContext';
import type { Identifier } from '../types/identifiers';
import SidebarFolderList from './components/SidebarFolderList';
import SidebarTagList, { TagEntry } from './components/SidebarTagList';
import SidebarCorrespondentList, { CorrespondentEntry } from './components/SidebarCorrespondentList';
import { FolderIdentifier, FolderTreeNode } from './components/SidebarFolderNode';
import SidebarTagList from './components/SidebarTagList';
import SidebarCorrespondentList from './components/SidebarCorrespondentList';
import { FolderIdentifier } from './components/SidebarFolderNode';
import { TenantOption } from './components/SidebarMenu';
import SidebarHeader from './components/SidebarHeader';
import SidebarSearch from './components/SidebarSearch';
type UploadHandler = (
files: FileList | File[] | Iterable<File>,
targetFolderId?: FolderIdentifier | null,
) => void;
interface SidebarProps {
folderNodes: Map<FolderIdentifier, FolderTreeNode>;
onToggle: (folderId: FolderIdentifier) => void;
onSelect: (folderId: FolderIdentifier) => void;
onDrop: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
@@ -29,26 +25,11 @@ interface SidebarProps {
onFolderDragStart?: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
onFolderDragEnd?: (event: React.DragEvent<HTMLDivElement>) => void;
draggedFolderId?: FolderIdentifier | null;
onCreateFolder?: (parentId?: FolderIdentifier | null) => void;
onCreateFolder?: (parentId?: Identifier | null) => void;
creatingFolder?: boolean;
tags?: TagEntry[];
untaggedFilterId?: Identifier | null;
correspondents?: CorrespondentEntry[];
onManageTags?: () => void;
onManageCorrespondents?: () => void;
onCreateTag?: (label: string) => Promise<void> | void;
onCreateCorrespondent?: (name: string) => Promise<void> | void;
onLogout?: () => void;
tenantName?: string | null;
tenants?: TenantOption[];
activeTenantId?: Identifier | null;
onSelectTenant?: (tenant: TenantOption | null, options?: { refreshOnly?: boolean }) => void;
onOpenSettings?: () => void;
onUploadFiles?: UploadHandler;
}
const Sidebar: React.FC<SidebarProps> = ({
folderNodes,
onToggle,
onSelect,
onDrop,
@@ -62,24 +43,46 @@ const Sidebar: React.FC<SidebarProps> = ({
draggedFolderId = null,
onCreateFolder,
creatingFolder = false,
tags = [],
untaggedFilterId = null,
correspondents = [],
onManageTags,
onManageCorrespondents,
onCreateTag,
onCreateCorrespondent,
onLogout,
tenantName,
tenants = [],
activeTenantId = null,
onSelectTenant,
onOpenSettings,
onUploadFiles,
}) => {
const {
sidebarSuppressed,
} = usePanelManager();
openTagsModal,
openCorrespondentsModal,
handleLogout,
tags = [],
correspondents = [],
foldersManager,
tenant,
tenants,
tenantOptions,
handleTenantSelect,
openSettings,
handleFileSelection, // Used for upload
} = useAppShell();
// Subscribe to folder tree updates
const subscribeToTree = useCallback((callback: () => void) => {
if (!foldersManager) return () => { };
return (foldersManager as FoldersManager).subscribe(callback);
}, [foldersManager]);
const getTreeSnapshot = useCallback(() => {
if (!foldersManager) return [];
return (foldersManager as FoldersManager).getTreeSnapshot();
}, [foldersManager]);
const roots = useSyncExternalStore(subscribeToTree, getTreeSnapshot);
const onManageTags = openTagsModal;
const onManageCorrespondents = openCorrespondentsModal;
// Derived or context-based handlers/values
const tenantName = (tenant as TenantOption)?.name;
const effectiveTenants = (tenants || tenantOptions || []);
const activeTenantId = (tenant as TenantOption)?.id;
// Upload handler wrapper (if needed, or pass handleFileSelection directly if signature matches)
const onUploadFiles = handleFileSelection;
const sidebarRef = useRef<HTMLDivElement | null>(null);
const {
@@ -109,11 +112,11 @@ const Sidebar: React.FC<SidebarProps> = ({
<SidebarHeader
tenantName={tenantName}
tenants={tenants}
tenants={effectiveTenants}
activeTenantId={activeTenantId}
onSelectTenant={onSelectTenant}
onOpenSettings={onOpenSettings}
onLogout={onLogout}
onSelectTenant={handleTenantSelect}
onOpenSettings={openSettings}
onLogout={handleLogout}
onUploadFiles={onUploadFiles}
selectedFolder={selectedFolder}
/>
@@ -122,7 +125,7 @@ const Sidebar: React.FC<SidebarProps> = ({
<SidebarSearch />
<SidebarFolderList
folderNodes={folderNodes}
roots={roots}
selectedFolder={selectedFolder}
onToggle={onToggle}
onSelect={onSelect}
@@ -140,14 +143,12 @@ const Sidebar: React.FC<SidebarProps> = ({
<SidebarTagList
tags={tags}
untaggedFilterId={untaggedFilterId}
onCreateTag={onCreateTag}
untaggedFilterId={null}
onManageTags={onManageTags}
/>
<SidebarCorrespondentList
correspondents={correspondents}
onCreateCorrespondent={onCreateCorrespondent}
onManageCorrespondents={onManageCorrespondents}
/>
</div>
@@ -3,7 +3,7 @@ import { PlusIcon, SettingsIcon } from '../../components/icons';
import type { Identifier } from '../../types/identifiers';
import { useDocumentsFilter } from '../../documents/context/DocumentsFilterContext';
export interface CorrespondentEntry {
interface CorrespondentEntry {
id: Identifier;
name: string;
}
@@ -2,10 +2,11 @@ import React, { useCallback } from 'react';
import type { ReactNode } from 'react';
import type { Identifier } from '../../types/identifiers';
import { FolderPlusIcon } from '../../components/icons';
import FolderNode, { FolderIdentifier, FolderTreeNode } from './SidebarFolderNode';
import FolderNode, { FolderIdentifier } from './SidebarFolderNode';
import type { FolderTreeNode } from '../../lib/api/apiTypes';
interface SidebarFolderListProps {
folderNodes: Map<FolderIdentifier, FolderTreeNode>;
roots: FolderTreeNode[];
selectedFolder: FolderIdentifier | null;
onToggle: (folderId: FolderIdentifier) => void;
onSelect: (folderId: FolderIdentifier) => void;
@@ -22,7 +23,7 @@ interface SidebarFolderListProps {
}
const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
folderNodes,
roots,
selectedFolder,
onToggle,
onSelect,
@@ -37,20 +38,44 @@ const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
onCreateFolder,
creatingFolder,
}) => {
const rootNode = folderNodes.get('root');
// Local expansion state for the UI
const [expandedIds, setExpandedIds] = React.useState<Set<string>>(new Set(['root']));
// Override validation or external toggle if needed
React.useEffect(() => {
if (selectedFolder) {
// ensure parent paths are expanded?
// Without a parent map, we can't easily auto-expand up.
// Rely on user or initial state for now, or add parent pointers.
}
}, [selectedFolder]);
const handleToggle = useCallback((folderId: FolderIdentifier) => {
setExpandedIds((prev) => {
const next = new Set(prev);
const id = String(folderId);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
onToggle(folderId); // propagate event just in case parent needs it
}, [onToggle]);
const renderNodes = useCallback(
(ids: FolderIdentifier[], depth: number): ReactNode =>
ids.map((id) => {
const node = folderNodes.get(id);
if (!node) return null;
(nodes: FolderTreeNode[], depth: number): ReactNode =>
nodes.map((node) => {
const isExpanded = expandedIds.has(String(node.id));
return (
<FolderNode
key={id}
key={node.id}
node={node}
depth={depth}
isSelected={selectedFolder === id}
onToggle={onToggle}
isSelected={selectedFolder === node.id}
onToggle={handleToggle}
expanded={isExpanded}
onSelect={onSelect}
onDrop={onDrop}
onDragOver={onDragOver}
@@ -66,9 +91,9 @@ const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
);
}),
[
folderNodes,
expandedIds,
selectedFolder,
onToggle,
handleToggle,
onSelect,
onDrop,
onDragOver,
@@ -101,7 +126,7 @@ const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
) : null}
</div>
<ul className="folder-tree">
{rootNode && renderNodes([rootNode.id], 0)}
{renderNodes(roots, 0)}
</ul>
</div>
);
@@ -9,16 +9,9 @@ import {
} from '../../components/icons';
import type { Identifier } from '../../types/identifiers';
export type FolderIdentifier = Identifier | 'root';
import type { FolderTreeNode } from '../../lib/api/apiTypes';
export interface FolderTreeNode {
id: FolderIdentifier;
name?: string;
children: FolderIdentifier[];
hasChildren?: boolean;
loaded?: boolean;
expanded?: boolean;
}
export type FolderIdentifier = Identifier | 'root';
interface FolderNodeProps {
node: FolderTreeNode;
@@ -31,7 +24,8 @@ interface FolderNodeProps {
onDragLeave: (event: React.DragEvent<HTMLDivElement>) => void;
onDelete: (folderId: FolderIdentifier) => void;
onRename?: (folderId: FolderIdentifier, name: string) => void;
renderChildren: (ids: FolderIdentifier[], depth: number) => ReactNode;
renderChildren: (nodes: FolderTreeNode[], depth: number) => ReactNode;
expanded: boolean;
onFolderDragStart?: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
onFolderDragEnd?: (event: React.DragEvent<HTMLDivElement>) => void;
draggingFolderId?: FolderIdentifier | null;
@@ -50,20 +44,21 @@ const FolderNode: React.FC<FolderNodeProps> = ({
onDelete,
onRename,
renderChildren,
expanded,
onFolderDragStart,
onFolderDragEnd,
draggingFolderId,
onCreateFolder,
}) => {
const isRoot = node.id === 'root';
const childIds = Array.isArray(node.children) ? node.children : [];
const hasChildren = Boolean(node.hasChildren || childIds.length);
const canToggle = hasChildren || !node.loaded;
const childNodes = node.children || [];
const hasChildren = childNodes.length > 0;
const canToggle = hasChildren; // Simplified: only toggle if we have children to show
const showChevron = canToggle;
const icon = showChevron ? <ChevronIcon className="toggle-icon" /> : null;
const canDrag = !isRoot;
const isDragging = draggingFolderId === node.id;
const isExpanded = Boolean(node.expanded);
const isExpanded = expanded;
const rowClasses = ['folder-row'];
if (isSelected) {
rowClasses.push('active');
@@ -162,9 +157,9 @@ const FolderNode: React.FC<FolderNodeProps> = ({
)}
</div>
</div>
{isExpanded && childIds.length > 0 && (
{isExpanded && childNodes.length > 0 && (
<ul className={`folder-children${depth === 0 ? ' folder-children--level1' : ''}`}>
{renderChildren(childIds, depth + 1)}
{renderChildren(childNodes, depth + 1)}
</ul>
)}
</li>
@@ -4,7 +4,7 @@ import { getTagColorStyle } from '../../utils/colors';
import type { Identifier } from '../../types/identifiers';
import { useDocumentsFilter } from '../../documents/context/DocumentsFilterContext';
export interface TagEntry {
interface TagEntry {
id: Identifier;
label: string;
color?: string | null;
-142
View File
@@ -1,20 +1,7 @@
import { useMemo } from 'react';
import type { DragEvent } from 'react';
import { TAG_FILTER_UNTAGGED } from '../app/workspaceUtils';
import type { Identifier } from '../types/identifiers';
interface FolderTreeNode {
id: Identifier;
name?: string;
hasChildren?: boolean;
loaded?: boolean;
expanded?: boolean;
children: Identifier[];
[key: string]: unknown;
}
type FolderNodeMap = Map<Identifier, FolderTreeNode>;
type FolderToggleHandler = (folderId: Identifier) => void | Promise<void>;
type FolderDropHandler = (
event: DragEvent<HTMLElement>,
@@ -29,45 +16,8 @@ interface FolderClickHandlers {
onDragLeave: (event: DragEvent<HTMLElement>) => void;
}
interface TagOption {
id?: Identifier;
label?: string;
color?: string | null;
[key: string]: unknown;
}
interface CorrespondentOption {
id?: Identifier;
name?: string;
[key: string]: unknown;
}
interface TenantOption {
id?: Identifier;
name?: string;
[key: string]: unknown;
}
interface UploadQueueItem {
id: string;
name?: string;
size?: number | null;
folderId?: Identifier | 'root';
status?: string;
error?: unknown;
code?: number | null;
document?: unknown;
conflictDocumentId?: Identifier | null;
[key: string]: unknown;
}
type UploadFilesHandler = (
files: FileList | File[] | Iterable<File>,
targetFolderId: Identifier | 'root',
) => void | Promise<void>;
interface UseSidebarPropsArgs {
folderNodes: FolderNodeMap;
folderClickHandlers: FolderClickHandlers;
handleFolderDelete: (folderId: Identifier) => void | Promise<void>;
handleFolderRename: (folderId: Identifier, name: string) => void | Promise<void>;
@@ -77,28 +27,9 @@ interface UseSidebarPropsArgs {
draggedFolderId: Identifier | null;
handlePromptCreateFolder?: (parentId?: Identifier | null) => void;
creatingFolder: boolean;
tags: TagOption[];
handleTagCreate: (payload: { label?: string }) => void | Promise<void>;
correspondents: CorrespondentOption[];
handleCorrespondentCreate: (payload: { name?: string }) =>
| Promise<CorrespondentOption | null | void>
| CorrespondentOption
| null
| void;
appStatus: string;
previewActive: boolean;
handleLogout: () => void | Promise<void>;
tenantName: string | null;
tenantOptions: TenantOption[];
currentTenantId: Identifier | null;
handleTenantSelect: (tenant: TenantOption | null, options?: { refreshOnly?: boolean }) => void | Promise<void>;
openSettings?: () => void;
handleFileSelection?: UploadFilesHandler;
uploadQueue?: UploadQueueItem[];
}
interface SidebarHookResult {
folderNodes: FolderNodeMap;
onToggle: FolderClickHandlers['onToggle'];
onSelect: FolderClickHandlers['onSelect'];
onDrop: FolderClickHandlers['onDrop'];
@@ -112,24 +43,9 @@ interface SidebarHookResult {
draggedFolderId: Identifier | null;
onCreateFolder: UseSidebarPropsArgs['handlePromptCreateFolder'];
creatingFolder: boolean;
tags: TagOption[];
untaggedFilterId: typeof TAG_FILTER_UNTAGGED;
onCreateTag: (label: string) => void;
correspondents: CorrespondentOption[];
onCreateCorrespondent: (name: string) => void;
previewActive: boolean;
onLogout: UseSidebarPropsArgs['handleLogout'];
tenantName: string | null;
tenants: TenantOption[];
activeTenantId: Identifier | null;
onSelectTenant: UseSidebarPropsArgs['handleTenantSelect'];
onOpenSettings: UseSidebarPropsArgs['openSettings'];
onUploadFiles: (files: FileList | File[] | Iterable<File>, targetFolderId?: Identifier | 'root') => void;
uploadQueue?: UploadQueueItem[];
}
const useSidebarProps = ({
folderNodes,
folderClickHandlers,
handleFolderDelete,
handleFolderRename,
@@ -139,24 +55,9 @@ const useSidebarProps = ({
draggedFolderId,
handlePromptCreateFolder,
creatingFolder,
tags,
handleTagCreate,
correspondents,
handleCorrespondentCreate,
appStatus,
previewActive,
handleLogout,
tenantName,
tenantOptions,
currentTenantId,
handleTenantSelect,
openSettings,
handleFileSelection,
uploadQueue,
}: UseSidebarPropsArgs): SidebarHookResult =>
useMemo(
() => ({
folderNodes,
onToggle: folderClickHandlers.onToggle,
onSelect: folderClickHandlers.onSelect,
onDrop: folderClickHandlers.onDrop,
@@ -170,60 +71,17 @@ const useSidebarProps = ({
draggedFolderId,
onCreateFolder: (parentId) => handlePromptCreateFolder?.(parentId),
creatingFolder,
tags,
untaggedFilterId: TAG_FILTER_UNTAGGED,
onCreateTag: (label) => handleTagCreate({ label }),
correspondents,
onCreateCorrespondent: (name) => handleCorrespondentCreate({ name }),
appStatus,
previewActive,
onLogout: handleLogout,
tenantName,
tenants: tenantOptions,
activeTenantId: currentTenantId,
onSelectTenant: handleTenantSelect,
onOpenSettings: openSettings,
onUploadFiles: (files, targetFolderId) => {
if (!handleFileSelection) {
return;
}
if (!files) {
return;
}
const normalizedFiles = Array.isArray(files) ? files : Array.from(files);
if (normalizedFiles.length === 0) {
return;
}
const folderId = targetFolderId ?? selectedFolder ?? 'root';
handleFileSelection(normalizedFiles, folderId);
},
uploadQueue,
}),
[
handleFileSelection,
uploadQueue,
appStatus,
correspondents,
creatingFolder,
currentTenantId,
folderClickHandlers,
folderNodes,
handleCorrespondentCreate,
handleFolderDragEnd,
handleFolderDragStart,
handleFolderDelete,
handleFolderRename,
handleLogout,
handlePromptCreateFolder,
handleTagCreate,
handleTenantSelect,
openSettings,
previewActive,
draggedFolderId,
selectedFolder,
tags,
tenantName,
tenantOptions,
],
);