feat: Remove documentLink prop and onNavigatorSnapshot logic, and add openDetailPanel and closeDetailPanel to document shell.

This commit is contained in:
2025-12-04 01:32:28 +01:00
parent 8bb9f9950a
commit 3f7c294948
9 changed files with 129 additions and 183 deletions
+56 -33
View File
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import type { ReactNode } from 'react';
import type { ReactNode, ComponentProps } from 'react';
import { useNavigate } from 'react-router-dom';
import {
DocumentsFilterProvider,
@@ -13,25 +13,24 @@ import { PanelManagerProvider, usePanelManager } from './PanelManagerContext';
import Sidebar from '../sidebar/Sidebar';
import useDocumentsShell from './useDocumentsShell';
const DocumentsRouteContent: React.FC = () => {
const {
sidebarProps,
surfaceConfig,
documentsFilter,
} = useDocumentsShell();
const navigate = useNavigate();
const DocumentsInner: React.FC<{
sidebarProps: ComponentProps<typeof Sidebar> | null;
surfaceConfig: any;
onNavigate: (documentId: string) => void;
}> = ({ sidebarProps, surfaceConfig, onNavigate }) => {
const { openPreview } = usePreviewContext();
const { collapsed: sidebarCollapsed } = useSidebarContext();
const {
sidebarSuppressed,
expandSidebar,
openDetailPanel,
} = usePanelManager();
const { openPreview } = usePreviewContext();
const { openDetailPanel } = surfaceConfig;
const sidebarHidden = sidebarCollapsed || sidebarSuppressed;
const handleHeaderBreadcrumbClick = useCallback((crumb: DocumentsHeaderBreadcrumb) => {
const navigate = useNavigate();
const handleBreadcrumbNavigate = useCallback((crumb: DocumentsHeaderBreadcrumb) => {
if (!crumb || !crumb.id) {
return;
}
@@ -39,10 +38,6 @@ const DocumentsRouteContent: React.FC = () => {
navigate(target);
}, [navigate]);
const handleDocumentNavigate = useCallback((documentId: string) => {
navigate(`/documents/${documentId}`);
}, [navigate]);
const handleOpenSidepanel = useCallback((docId: string) => {
if (openDetailPanel) {
openDetailPanel({ documentIds: [docId] });
@@ -51,9 +46,9 @@ const DocumentsRouteContent: React.FC = () => {
const documentsTablePropsWithNav = useMemo(() => (
surfaceConfig.documentsTableProps
? { ...surfaceConfig.documentsTableProps, onBreadcrumbNavigate: handleHeaderBreadcrumbClick }
? { ...surfaceConfig.documentsTableProps, onBreadcrumbNavigate: handleBreadcrumbNavigate }
: null
), [surfaceConfig.documentsTableProps, handleHeaderBreadcrumbClick]);
), [surfaceConfig.documentsTableProps, handleBreadcrumbNavigate]);
const { surface } = useWorkspaceSurface({
sidebarHidden,
@@ -87,30 +82,58 @@ const DocumentsRouteContent: React.FC = () => {
);
};
const content = renderSurface();
return (
<DocumentOpenProvider
onOpenViewer={onNavigate}
onOpenPreview={openPreview}
onOpenSidepanel={handleOpenSidepanel}
>
{renderSurface()}
</DocumentOpenProvider>
);
};
const DocumentsRouteContent: React.FC = () => {
const {
sidebarProps,
surfaceConfig,
documentsFilter,
} = useDocumentsShell();
const navigate = useNavigate();
const handleDocumentNavigate = useCallback((documentId: string) => {
navigate(`/documents/${documentId}`);
}, [navigate]);
return (
<DocumentsFilterProvider value={documentsFilter}>
<PreviewProvider
onNavigate={handleDocumentNavigate}
>
<DocumentOpenProvider
onOpenViewer={handleDocumentNavigate}
onOpenPreview={openPreview}
onOpenSidepanel={handleOpenSidepanel}
>
{content}
</DocumentOpenProvider>
<DocumentsInner
sidebarProps={sidebarProps}
surfaceConfig={surfaceConfig}
onNavigate={handleDocumentNavigate}
/>
</PreviewProvider>
</DocumentsFilterProvider>
);
};
const DocumentsRoute: React.FC = () => (
<SidebarProvider>
<PanelManagerProvider>
<DocumentsRouteContent />
</PanelManagerProvider>
</SidebarProvider>
);
const DocumentsRoute: React.FC = () => {
const { surfaceConfig } = useDocumentsShell();
const { detailPanelOpen, closeDetailPanel } = surfaceConfig;
return (
<SidebarProvider>
<PanelManagerProvider
isOpen={detailPanelOpen}
onClose={closeDetailPanel}
>
<DocumentsRouteContent />
</PanelManagerProvider>
</SidebarProvider>
);
};
export default DocumentsRoute;
+26 -9
View File
@@ -89,9 +89,11 @@ const applyPanelWidthToRoot = (panel: PanelKey, width: number, active: boolean):
interface PanelManagerProviderProps {
children: ReactNode;
isOpen?: boolean;
onClose?: () => void;
}
export const PanelManagerProvider: React.FC<PanelManagerProviderProps> = ({ children }) => {
export const PanelManagerProvider: React.FC<PanelManagerProviderProps> = ({ children, isOpen, onClose }) => {
const { collapsed, setCollapsed } = useSidebarContext();
const initialSidebarWidth = readStoredWidth('sidebar', DEFAULT_SIDEBAR_WIDTH);
const initialDetailWidth = readStoredWidth('detail', DEFAULT_DETAIL_WIDTH);
@@ -100,7 +102,13 @@ export const PanelManagerProvider: React.FC<PanelManagerProviderProps> = ({ chil
const [detailWidth, setDetailWidthState] = useState(() => clampPanelWidth('detail', initialDetailWidth));
const [resizingPanel, setResizingPanel] = useState(null);
const [sidebarSuppressed, setSidebarSuppressed] = useState(false);
const [detailPanelOpen, setDetailPanelOpen] = useState(false);
const [detailPanelOpen, setDetailPanelOpen] = useState(Boolean(isOpen));
useEffect(() => {
if (isOpen !== undefined) {
setDetailPanelOpen(isOpen);
}
}, [isOpen]);
const detailCloseHandlerRef = useRef(null);
const panelWidthsRef = useRef({ sidebar: sidebarWidth, detail: detailWidth });
@@ -109,7 +117,11 @@ export const PanelManagerProvider: React.FC<PanelManagerProviderProps> = ({ chil
const closeDetailPanel = useCallback(() => {
const handler = detailCloseHandlerRef.current;
handler?.();
}, []);
onClose?.();
if (isOpen === undefined) {
setDetailPanelOpen(false);
}
}, [onClose, isOpen]);
useEffect(() => {
panelWidthsRef.current.sidebar = sidebarWidth;
@@ -228,11 +240,16 @@ export const PanelManagerProvider: React.FC<PanelManagerProviderProps> = ({ chil
}, []);
const setDetailActive = useCallback(
(isOpen) => {
setDetailPanelOpen(Boolean(isOpen));
handlePanelLayoutChange('detail', isOpen ? 'opened' : 'closed');
(active) => {
if (isOpen === undefined) {
setDetailPanelOpen(Boolean(active));
}
if (!active) {
onClose?.();
}
handlePanelLayoutChange('detail', active ? 'opened' : 'closed');
},
[handlePanelLayoutChange],
[handlePanelLayoutChange, isOpen, onClose],
);
const expandSidebar = useCallback(() => {
@@ -394,8 +411,8 @@ export const usePanelResizeBindings = (
const handleProps = enabled
? {
onPointerDown: handlePointerDown,
}
onPointerDown: handlePointerDown,
}
: {};
return {
+6 -2
View File
@@ -6,7 +6,10 @@ import type Sidebar from '../sidebar/Sidebar';
import type { UseWorkspaceSurfaceArgs } from './useWorkspaceSurface';
import type { Identifier } from '../types/identifiers';
type WorkspaceSurfaceConfig = Omit<UseWorkspaceSurfaceArgs, 'sidebarHidden' | 'onExpandSidebar'>;
type WorkspaceSurfaceConfig = Omit<UseWorkspaceSurfaceArgs, 'sidebarHidden' | 'onExpandSidebar'> & {
openDetailPanel?: (args: { documentIds?: Identifier[] }) => void;
closeDetailPanel?: () => void;
};
interface DocumentsShellView {
sidebarProps: ComponentProps<typeof Sidebar> | null;
@@ -23,8 +26,9 @@ const useDocumentsShell = (): DocumentsShellView => {
documentsTableProps: (shell.documentsTableProps ?? null) as WorkspaceSurfaceConfig['documentsTableProps'],
detailPanelProps: (shell.detailPanelProps ?? null) as WorkspaceSurfaceConfig['detailPanelProps'],
detailPanelOpen: Boolean(shell.detailPanelOpen),
openDetailPanel: shell.openDetailPanel as WorkspaceSurfaceConfig['openDetailPanel'],
closeDetailPanel: shell.closeDetailPanel as WorkspaceSurfaceConfig['closeDetailPanel'],
previewWorkspaceDocument: shell.previewWorkspaceDocument,
documentLink: shell.documentLink,
previewDocumentId: (shell.previewDocumentId as Identifier | null | undefined) ?? null,
closeDocumentPreview: shell.closeDocumentPreview as WorkspaceSurfaceConfig['closeDocumentPreview'],
ensurePreviewData: shell.ensurePreviewData as WorkspaceSurfaceConfig['ensurePreviewData'],
-4
View File
@@ -36,7 +36,6 @@ export interface UseWorkspaceSurfaceArgs {
detailPanelProps?: DetailPanelProps;
detailPanelOpen?: boolean;
previewWorkspaceDocument?: unknown;
documentLink?: unknown;
previewDocumentId?: Identifier | null;
ensureAssetUrl?: EnsureAssetUrl;
ensurePreviewData?: EnsurePreviewData;
@@ -56,7 +55,6 @@ export const useWorkspaceSurface = ({
detailPanelProps,
detailPanelOpen = false,
previewWorkspaceDocument,
documentLink,
previewDocumentId,
ensureAssetUrl,
ensurePreviewData,
@@ -181,7 +179,6 @@ export const useWorkspaceSurface = ({
const viewer = (
<DocumentViewerPanel
document={previewWorkspaceDocument || null}
documentLink={documentLink}
hydrateDocument={ensurePreviewData}
tagLookupById={tagLookupById}
tagOptions={tagOptions}
@@ -214,7 +211,6 @@ export const useWorkspaceSurface = ({
}, [
showPreviewWorkspace,
previewWorkspaceDocument,
documentLink,
ensurePreviewData,
ensureAssetUrl,
getDocumentAsset,
+20 -6
View File
@@ -5,6 +5,7 @@ import { getTagColorStyle } from '../utils/colors';
import type { DocumentId } from '../types/identifiers';
import type { Document } from '../types/documents';
import { LayoutCard } from './LayoutSystem';
import { useCardPointer } from './useCardPointer';
const preventAll = (event?: React.SyntheticEvent | Event | null) => {
if (!event) return;
@@ -21,16 +22,18 @@ interface DesktopDocumentCardProps {
docTagTokens?: string;
ensureAssetUrl?: (...args: any[]) => Promise<unknown>;
getDocumentAsset?: (...args: any[]) => unknown;
handleNavigatorSnapshot?: (...args: any[]) => void;
cardPointerHandlers?: React.HTMLAttributes<HTMLDivElement>;
onDocumentActivate?: (id: string, event?: any) => void;
onSelect: (ids: string[], extend?: boolean) => void;
onDeselect: (ids: string[]) => void;
selection: string[];
requestCanvasFocus?: () => void;
onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: Document, tag: any) => void;
onDocTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
layoutCard?: LayoutCard;
layoutCard: LayoutCard;
}
const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
@@ -42,9 +45,11 @@ const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
docTagTokens,
ensureAssetUrl,
getDocumentAsset,
handleNavigatorSnapshot,
cardPointerHandlers,
onDocumentActivate,
onSelect,
onDeselect,
selection,
requestCanvasFocus,
onTagDragEnter,
onTagDragOver,
onTagDragLeave,
@@ -53,6 +58,16 @@ const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
onDocTagDragEnd,
layoutCard,
}) => {
const cardPointerHandlers = useCardPointer(
layoutCard,
!!selected,
selection,
onSelect,
onDeselect,
onDocumentActivate,
requestCanvasFocus
);
const correspondents = useMemo(() => resolveCorrespondents(doc), [doc]);
const tags = Array.isArray(doc?.tags) ? doc.tags : [];
@@ -108,7 +123,6 @@ const DesktopDocumentCard: React.FC<DesktopDocumentCardProps> = ({
title={doc.title}
ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
onNavigatorSnapshot={handleNavigatorSnapshot}
shouldLoad={shouldLoad}
/>
{correspondents.length > 0 && (
+1 -45
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useMemo } from 'react';
import type { JSX } from 'react';
import { resolveDocumentAssetUrl } from '../asset_manager';
import type { Identifier } from '../types/identifiers';
@@ -14,19 +14,11 @@ type EnsureAssetUrl = (
type GetDocumentAsset = (document: Document | null, assetType: string) => Asset | null;
interface NavigatorSnapshot {
url: string | null;
alt?: string;
width: number | null;
height: number | null;
}
interface DesktopPreviewCardProps {
doc: Document | null;
title?: string;
ensureAssetUrl?: EnsureAssetUrl | null;
getDocumentAsset?: GetDocumentAsset;
onNavigatorSnapshot?: (docId: Identifier, snapshot: NavigatorSnapshot | null) => void;
shouldLoad?: boolean;
}
@@ -35,7 +27,6 @@ const DesktopPreviewCard = ({
title,
ensureAssetUrl,
getDocumentAsset,
onNavigatorSnapshot,
shouldLoad = true,
}: DesktopPreviewCardProps): JSX.Element => {
const currentUrl = useMemo(() => {
@@ -46,41 +37,6 @@ const DesktopPreviewCard = ({
});
}, [doc, ensureAssetUrl, getDocumentAsset, shouldLoad]);
const currentMetadata = useMemo(() => {
if (!doc || !getDocumentAsset) return null;
const asset = getDocumentAsset(doc, 'thumbnail');
return asset?.metadata || null;
}, [doc, getDocumentAsset]);
const docId = doc?.id ?? null;
const metadataWidth = Number((currentMetadata as { width?: number } | null)?.width);
const metadataHeight = Number((currentMetadata as { height?: number } | null)?.height);
useEffect(() => {
if (!onNavigatorSnapshot || !docId) {
return undefined;
}
if (!currentUrl) {
onNavigatorSnapshot(docId, null);
return undefined;
}
const snapshot = {
url: currentUrl,
alt: title,
width: Number.isFinite(metadataWidth) && metadataWidth > 0 ? metadataWidth : null,
height: Number.isFinite(metadataHeight) && metadataHeight > 0 ? metadataHeight : null,
};
onNavigatorSnapshot(docId, snapshot);
return () => onNavigatorSnapshot(docId, null);
}, [
docId,
currentUrl,
title,
metadataWidth,
metadataHeight,
onNavigatorSnapshot,
]);
const hasPreview = Boolean(currentUrl);
const cardClasses = ['desk-item__card'];
if (!hasPreview) cardClasses.push('desk-item__card--empty');
+4 -40
View File
@@ -9,7 +9,6 @@ import { LayoutStore, LayoutCard } from './LayoutSystem';
import DesktopDocumentCard from './DesktopDocumentCard';
import usePreviewMetadata from './hooks/usePreviewMetadata';
import useDeskTagInteractions from './tags/useDeskTagInteractions';
import { useCardPointer } from './useCardPointer';
import '../styles/workspace/workspace-layout.css';
import '../styles/workspace/workspace-items.css';
import '../styles/workspace/workspace-cards.css';
@@ -21,17 +20,6 @@ import type { DocumentsListEntry, Document } from '../types/documents';
import { useAppState } from '../app/appState';
import { useDocumentOpen } from '../contexts/DocumentOpenContext';
type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null;
type OverlaySource = { url: string; alt?: string | null; mimeType?: string | null; };
export interface DeskDocument {
id?: Identifier | null;
title?: string;
tags?: TagLike[] | null;
documentLink?: OverlaySource | null;
[key: string]: unknown;
}
interface DocumentSizeInfo {
width: number;
height: number;
@@ -39,7 +27,7 @@ interface DocumentSizeInfo {
}
// Fallback size computation
const computeFallbackCardSize = (_doc: DeskDocument, defaultSize: number = 200): DocumentSizeInfo => {
const computeFallbackCardSize = (_doc: Document, defaultSize: number = 200): DocumentSizeInfo => {
const size = Math.round(defaultSize * (1 / Math.SQRT2));
return { width: size, height: size, source: 'fallback' };
};
@@ -55,29 +43,6 @@ export interface DesktopWorkspaceProps {
defaultCardSize?: number;
}
// Wrapper to handle hooks per card
const DesktopDocumentContainer: React.FC<React.ComponentProps<typeof DesktopDocumentCard> & {
onSelect: (ids: string[], extend?: boolean) => void;
onDeselect: (ids: string[]) => void;
onDocumentActivate?: (id: string, event?: any) => void;
selection: string[];
requestCanvasFocus?: () => void;
}> = React.memo((props) => {
const { layoutCard, selected, onSelect, onDeselect, onDocumentActivate, selection, requestCanvasFocus } = props;
// We assume layoutCard is always present in this context
const cardPointerHandlers = useCardPointer(layoutCard!, !!selected, selection, onSelect, onDeselect, onDocumentActivate, requestCanvasFocus);
return (
<DesktopDocumentCard
{...props}
cardPointerHandlers={cardPointerHandlers}
/>
);
});
DesktopDocumentContainer.displayName = 'DesktopDocumentContainer';
const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
entries,
ensureAssetUrl,
@@ -102,7 +67,7 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
.filter((entry): entry is { type: 'document'; document: Document } & DocumentsListEntry =>
entry.type === 'document' && !!entry.document
)
.map(entry => entry.document as DeskDocument);
.map(entry => entry.document);
}, [entries]);
// Layout System Initialization
@@ -189,7 +154,7 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
const metadataMap = usePreviewMetadata(items, getDocumentAsset, ensureAssetUrl);
const ensureDocumentSize = useCallback((doc: DeskDocument): DocumentSizeInfo => {
const ensureDocumentSize = useCallback((doc: Document): DocumentSizeInfo => {
if (doc.id) {
const meta = metadataMap.get(String(doc.id));
if (meta && meta.width && meta.height)
@@ -404,7 +369,7 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
});
return (
<DesktopDocumentContainer
<DesktopDocumentCard
key={docId}
doc={doc}
style={{
@@ -421,7 +386,6 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
docTagTokens=""
ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
handleNavigatorSnapshot={() => { }}
onDocumentActivate={(_id, event) => {
const isPreview = event && ((event as any).altKey || (event as any).button === 1);
openDocument(doc, isPreview ? 'preview' : 'sidepanel');
+16 -25
View File
@@ -6,12 +6,6 @@ import MediaViewer from './MediaViewer';
import type { Document } from '../types/documents';
interface DocumentLink {
url?: string;
mimeType?: string;
filename?: string;
}
interface ContentTabConfig {
id?: string;
label?: string;
@@ -29,7 +23,6 @@ type LayoutMode = 'split' | 'stacked' | (string & {});
interface DocumentViewerLayoutProps {
document?: Document | null;
documentLink?: DocumentLink | null;
summaryProps?: Record<string, unknown>;
metadataPayload?: unknown;
contentTabConfig?: ContentTabConfig | null;
@@ -43,7 +36,6 @@ interface DocumentViewerLayoutProps {
const DocumentViewerLayout = ({
document,
documentLink,
summaryProps = {},
metadataPayload,
contentTabConfig,
@@ -58,28 +50,27 @@ const DocumentViewerLayout = ({
const viewportRef = useRef<HTMLDivElement>(null);
const previewContent = useMemo(() => {
if (!document || !documentLink?.url) {
if (!document) {
return null;
}
const normalizedMimeType = (documentLink.mimeType
|| document.mime_type
|| '')
.toLowerCase();
const normalizedFilename = documentLink.filename
|| document.filename
|| document.original_name
|| '';
const downloadUrl = document.current_version?.download?.url;
if (!downloadUrl) {
return null;
}
const normalizedMimeType = (document.mime_type || '').toLowerCase();
const normalizedFilename = document.filename;
const isPdf = normalizedMimeType === 'application/pdf'
|| normalizedMimeType === 'application/x-pdf';
if (isPdf) {
const documentTitle = document.title
|| document.filename
|| document.original_name;
const documentTitle = document.title;
return (
<PdfViewer
src={documentLink.url}
src={downloadUrl}
title={documentTitle ? `Preview of ${documentTitle}` : undefined}
viewportRef={viewportRef}
/>
@@ -88,23 +79,23 @@ const DocumentViewerLayout = ({
return (
<MediaViewer
src={documentLink.url}
src={downloadUrl}
mimeType={normalizedMimeType}
filename={normalizedFilename}
alt={`Preview of ${document.title}`}
/>
);
}, [document, documentLink, viewportRef]);
}, [document, viewportRef]);
const renderViewportPane = useCallback(() => (
<div className="document-viewer__viewport" ref={viewportRef}>
{!documentLink?.url ? (
{!document?.current_version?.download?.url ? (
<div className="document-viewer__message">{previewLoadingMessage}</div>
) : (
previewContent
)}
</div>
), [previewContent, documentLink?.url, previewLoadingMessage, viewportRef]);
), [previewContent, document?.current_version?.download?.url, previewLoadingMessage, viewportRef]);
const viewportPane = renderViewportPane();
@@ -230,24 +230,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(null);
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
const resolvedDocumentLink = useMemo(() => {
if (!document) {
return null;
}
const downloadUrl = document.current_version?.download?.url;
const href = downloadUrl;
if (!href) {
return null;
}
const mimeType = document.mime_type;
const filename = document.current_version?.filename || document.filename || document.title || null;
return {
url: href,
mimeType,
filename,
};
}, [document]);
const {
panelStyle: managedDetailPanelStyle,
handleProps: managedResizeHandleProps,
@@ -407,7 +389,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
<section className={viewerClassName}>
<DocumentViewerLayout
document={document}
documentLink={resolvedDocumentLink}
summaryProps={summaryProps}
metadataPayload={metadataPayload}
contentTabConfig={contentTabConfig}