diff --git a/frontend/src/app/DocumentsRoute.tsx b/frontend/src/app/DocumentsRoute.tsx index 3acdb42..26cd8b7 100644 --- a/frontend/src/app/DocumentsRoute.tsx +++ b/frontend/src/app/DocumentsRoute.tsx @@ -14,11 +14,11 @@ import useDocumentsShell from './useDocumentsShell'; import useFolderData from '../documents/data/useFolderData'; const DocumentsInner: React.FC<{ - sidebarProps: ComponentProps | null; surfaceConfig: any; folderData: any; onNavigate: (documentId: string) => void; -}> = ({ sidebarProps, surfaceConfig, folderData, onNavigate }) => { + sidebarProps: ComponentProps | null; +}> = ({ surfaceConfig, folderData, onNavigate, sidebarProps }) => { const { openPreview } = usePreviewContext(); const { collapsed: sidebarCollapsed } = useSidebarContext(); const { @@ -112,10 +112,10 @@ const DocumentsRouteContent: React.FC = () => { onNavigate={handleDocumentNavigate} > diff --git a/frontend/src/documents/data/useDocumentsWorkspace.ts b/frontend/src/documents/data/useDocumentsWorkspace.ts index 22a6898..f5ddb4f 100644 --- a/frontend/src/documents/data/useDocumentsWorkspace.ts +++ b/frontend/src/documents/data/useDocumentsWorkspace.ts @@ -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 diff --git a/frontend/src/sidebar/Sidebar.tsx b/frontend/src/sidebar/Sidebar.tsx index 161ab03..3165ede 100644 --- a/frontend/src/sidebar/Sidebar.tsx +++ b/frontend/src/sidebar/Sidebar.tsx @@ -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, - targetFolderId?: FolderIdentifier | null, -) => void; + interface SidebarProps { - folderNodes: Map; onToggle: (folderId: FolderIdentifier) => void; onSelect: (folderId: FolderIdentifier) => void; onDrop: (event: React.DragEvent, folderId: FolderIdentifier) => void; @@ -29,26 +25,11 @@ interface SidebarProps { onFolderDragStart?: (event: React.DragEvent, folderId: FolderIdentifier) => void; onFolderDragEnd?: (event: React.DragEvent) => 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; - onCreateCorrespondent?: (name: string) => Promise | 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 = ({ - folderNodes, onToggle, onSelect, onDrop, @@ -62,24 +43,46 @@ const Sidebar: React.FC = ({ 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(null); const { @@ -109,11 +112,11 @@ const Sidebar: React.FC = ({ @@ -122,7 +125,7 @@ const Sidebar: React.FC = ({ = ({ diff --git a/frontend/src/sidebar/components/SidebarCorrespondentList.tsx b/frontend/src/sidebar/components/SidebarCorrespondentList.tsx index 091fc56..d165c6a 100644 --- a/frontend/src/sidebar/components/SidebarCorrespondentList.tsx +++ b/frontend/src/sidebar/components/SidebarCorrespondentList.tsx @@ -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; } diff --git a/frontend/src/sidebar/components/SidebarFolderList.tsx b/frontend/src/sidebar/components/SidebarFolderList.tsx index cf3046f..2b85390 100644 --- a/frontend/src/sidebar/components/SidebarFolderList.tsx +++ b/frontend/src/sidebar/components/SidebarFolderList.tsx @@ -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; + roots: FolderTreeNode[]; selectedFolder: FolderIdentifier | null; onToggle: (folderId: FolderIdentifier) => void; onSelect: (folderId: FolderIdentifier) => void; @@ -22,7 +23,7 @@ interface SidebarFolderListProps { } const SidebarFolderList: React.FC = ({ - folderNodes, + roots, selectedFolder, onToggle, onSelect, @@ -37,20 +38,44 @@ const SidebarFolderList: React.FC = ({ onCreateFolder, creatingFolder, }) => { - const rootNode = folderNodes.get('root'); + // Local expansion state for the UI + const [expandedIds, setExpandedIds] = React.useState>(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 ( = ({ ); }), [ - folderNodes, + expandedIds, selectedFolder, - onToggle, + handleToggle, onSelect, onDrop, onDragOver, @@ -101,7 +126,7 @@ const SidebarFolderList: React.FC = ({ ) : null}
    - {rootNode && renderNodes([rootNode.id], 0)} + {renderNodes(roots, 0)}
); diff --git a/frontend/src/sidebar/components/SidebarFolderNode.tsx b/frontend/src/sidebar/components/SidebarFolderNode.tsx index 2e78030..0951501 100644 --- a/frontend/src/sidebar/components/SidebarFolderNode.tsx +++ b/frontend/src/sidebar/components/SidebarFolderNode.tsx @@ -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) => 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, folderId: FolderIdentifier) => void; onFolderDragEnd?: (event: React.DragEvent) => void; draggingFolderId?: FolderIdentifier | null; @@ -50,20 +44,21 @@ const FolderNode: React.FC = ({ 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 ? : 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 = ({ )} - {isExpanded && childIds.length > 0 && ( + {isExpanded && childNodes.length > 0 && (
    - {renderChildren(childIds, depth + 1)} + {renderChildren(childNodes, depth + 1)}
)} diff --git a/frontend/src/sidebar/components/SidebarTagList.tsx b/frontend/src/sidebar/components/SidebarTagList.tsx index 11e294f..776bace 100644 --- a/frontend/src/sidebar/components/SidebarTagList.tsx +++ b/frontend/src/sidebar/components/SidebarTagList.tsx @@ -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; diff --git a/frontend/src/sidebar/useSidebarProps.ts b/frontend/src/sidebar/useSidebarProps.ts index 887ca29..512b9aa 100644 --- a/frontend/src/sidebar/useSidebarProps.ts +++ b/frontend/src/sidebar/useSidebarProps.ts @@ -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; - type FolderToggleHandler = (folderId: Identifier) => void | Promise; type FolderDropHandler = ( event: DragEvent, @@ -29,45 +16,8 @@ interface FolderClickHandlers { onDragLeave: (event: DragEvent) => 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, - targetFolderId: Identifier | 'root', -) => void | Promise; interface UseSidebarPropsArgs { - folderNodes: FolderNodeMap; folderClickHandlers: FolderClickHandlers; handleFolderDelete: (folderId: Identifier) => void | Promise; handleFolderRename: (folderId: Identifier, name: string) => void | Promise; @@ -77,28 +27,9 @@ interface UseSidebarPropsArgs { draggedFolderId: Identifier | null; handlePromptCreateFolder?: (parentId?: Identifier | null) => void; creatingFolder: boolean; - tags: TagOption[]; - handleTagCreate: (payload: { label?: string }) => void | Promise; - correspondents: CorrespondentOption[]; - handleCorrespondentCreate: (payload: { name?: string }) => - | Promise - | CorrespondentOption - | null - | void; - appStatus: string; - previewActive: boolean; - handleLogout: () => void | Promise; - tenantName: string | null; - tenantOptions: TenantOption[]; - currentTenantId: Identifier | null; - handleTenantSelect: (tenant: TenantOption | null, options?: { refreshOnly?: boolean }) => void | Promise; - 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, 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, ], );