folder in summary
This commit is contained in:
@@ -4,6 +4,7 @@ import { SidebarExpandIcon } from '../ui/icons';
|
|||||||
import DocumentsPanel from '../documents/panel/DocumentsPanel';
|
import DocumentsPanel from '../documents/panel/DocumentsPanel';
|
||||||
import DocumentViewerPanel from '../preview/DocumentViewerPanel';
|
import DocumentViewerPanel from '../preview/DocumentViewerPanel';
|
||||||
import { usePanelManager } from './PanelManagerContext';
|
import { usePanelManager } from './PanelManagerContext';
|
||||||
|
import { FolderManagerProvider } from '../folders/FolderManagerContext';
|
||||||
|
|
||||||
type Identifier = string | number;
|
type Identifier = string | number;
|
||||||
|
|
||||||
@@ -91,8 +92,15 @@ export const useWorkspaceSurface = ({
|
|||||||
|
|
||||||
const detail = detailPanelOpen && detailPanelProps
|
const detail = detailPanelOpen && detailPanelProps
|
||||||
? (() => {
|
? (() => {
|
||||||
const { onClose, onOpenPreview, tags: tagOptions, ...restDetailProps } = detailPanelProps;
|
const {
|
||||||
return (
|
onClose,
|
||||||
|
onOpenPreview,
|
||||||
|
tags: tagOptions,
|
||||||
|
folderNodes,
|
||||||
|
ensureFolderData,
|
||||||
|
...restDetailProps
|
||||||
|
} = detailPanelProps;
|
||||||
|
const viewer = (
|
||||||
<DocumentViewerPanel
|
<DocumentViewerPanel
|
||||||
variant="sidebar"
|
variant="sidebar"
|
||||||
onCollapsePanel={onClose}
|
onCollapsePanel={onClose}
|
||||||
@@ -101,6 +109,16 @@ export const useWorkspaceSurface = ({
|
|||||||
{...restDetailProps}
|
{...restDetailProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
if (folderNodes && ensureFolderData) {
|
||||||
|
return (
|
||||||
|
<FolderManagerProvider folderNodes={folderNodes} ensureFolderData={ensureFolderData}>
|
||||||
|
{viewer}
|
||||||
|
</FolderManagerProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>{viewer}</>
|
||||||
|
);
|
||||||
})()
|
})()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -140,10 +158,11 @@ export const useWorkspaceSurface = ({
|
|||||||
onUpdateTitle,
|
onUpdateTitle,
|
||||||
onUpdateIssued,
|
onUpdateIssued,
|
||||||
resolveFolderPath,
|
resolveFolderPath,
|
||||||
|
folderNodes,
|
||||||
|
ensureFolderData,
|
||||||
} = detailExtras;
|
} = detailExtras;
|
||||||
|
|
||||||
return {
|
const viewer = (
|
||||||
content: (
|
|
||||||
<DocumentViewerPanel
|
<DocumentViewerPanel
|
||||||
document={previewWorkspaceDocument || null}
|
document={previewWorkspaceDocument || null}
|
||||||
documentLink={documentLink}
|
documentLink={documentLink}
|
||||||
@@ -166,9 +185,17 @@ export const useWorkspaceSurface = ({
|
|||||||
onClosePanel={closeDocumentPreview}
|
onClosePanel={closeDocumentPreview}
|
||||||
resolveFolderPath={resolveFolderPath}
|
resolveFolderPath={resolveFolderPath}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
detail: null,
|
|
||||||
};
|
const content = folderNodes && ensureFolderData
|
||||||
|
? (
|
||||||
|
<FolderManagerProvider folderNodes={folderNodes} ensureFolderData={ensureFolderData}>
|
||||||
|
{viewer}
|
||||||
|
</FolderManagerProvider>
|
||||||
|
)
|
||||||
|
: viewer;
|
||||||
|
|
||||||
|
return { content, detail: null };
|
||||||
}, [
|
}, [
|
||||||
showPreviewWorkspace,
|
showPreviewWorkspace,
|
||||||
previewWorkspaceDocument,
|
previewWorkspaceDocument,
|
||||||
|
|||||||
@@ -25,11 +25,6 @@ interface FolderNode {
|
|||||||
parentId?: Identifier | 'root';
|
parentId?: Identifier | 'root';
|
||||||
}
|
}
|
||||||
|
|
||||||
type DocumentLink = {
|
|
||||||
url?: string;
|
|
||||||
contentType?: string | null;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
interface UseDetailWorkspaceArgs {
|
interface UseDetailWorkspaceArgs {
|
||||||
documents: DocumentLike[];
|
documents: DocumentLike[];
|
||||||
selectionOrder: string[];
|
selectionOrder: string[];
|
||||||
@@ -39,7 +34,6 @@ interface UseDetailWorkspaceArgs {
|
|||||||
ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>;
|
ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>;
|
||||||
detailPanelControlRef: MutableRefObject<{ open?: (args?: { documentIds?: Identifier[] }) => void; close?: () => void } | null>;
|
detailPanelControlRef: MutableRefObject<{ open?: (args?: { documentIds?: Identifier[] }) => void; close?: () => void } | null>;
|
||||||
detailFolderFetchRef: MutableRefObject<Set<Identifier | 'root'>>;
|
detailFolderFetchRef: MutableRefObject<Set<Identifier | 'root'>>;
|
||||||
documentLinks: Map<Identifier, DocumentLink>;
|
|
||||||
previewDocumentId?: Identifier | null;
|
previewDocumentId?: Identifier | null;
|
||||||
activePreviewId?: Identifier | null;
|
activePreviewId?: Identifier | null;
|
||||||
openDocumentPreview?: (args: { documentIds: Identifier[] }) => void;
|
openDocumentPreview?: (args: { documentIds: Identifier[] }) => void;
|
||||||
@@ -67,7 +61,6 @@ interface UseDetailWorkspaceResult {
|
|||||||
inspectDocument: (docId: Identifier | null) => void;
|
inspectDocument: (docId: Identifier | null) => void;
|
||||||
previewActive: boolean;
|
previewActive: boolean;
|
||||||
previewWorkspaceDocument: DocumentLike | null;
|
previewWorkspaceDocument: DocumentLike | null;
|
||||||
documentLink: DocumentLink;
|
|
||||||
resolveThumbnailUrlForDoc: (doc: DocumentLike | null) => string | null;
|
resolveThumbnailUrlForDoc: (doc: DocumentLike | null) => string | null;
|
||||||
resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>;
|
resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>;
|
||||||
}
|
}
|
||||||
@@ -81,7 +74,6 @@ const useDetailWorkspace = ({
|
|||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
detailPanelControlRef,
|
detailPanelControlRef,
|
||||||
detailFolderFetchRef,
|
detailFolderFetchRef,
|
||||||
documentLinks,
|
|
||||||
previewDocumentId,
|
previewDocumentId,
|
||||||
activePreviewId,
|
activePreviewId,
|
||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
@@ -239,11 +231,6 @@ const useDetailWorkspace = ({
|
|||||||
[folderNodes],
|
[folderNodes],
|
||||||
);
|
);
|
||||||
|
|
||||||
const documentLink = useMemo(
|
|
||||||
() => (detailPanelDocument ? documentLinks.get(detailPanelDocument.id) || null : null),
|
|
||||||
[detailPanelDocument, documentLinks],
|
|
||||||
);
|
|
||||||
|
|
||||||
const previewWorkspaceDocument = useMemo(() => {
|
const previewWorkspaceDocument = useMemo(() => {
|
||||||
if (!previewDocumentId) {
|
if (!previewDocumentId) {
|
||||||
return null;
|
return null;
|
||||||
@@ -285,7 +272,6 @@ const useDetailWorkspace = ({
|
|||||||
tagLookupById,
|
tagLookupById,
|
||||||
onTagAdd: handleDocumentTagAdd,
|
onTagAdd: handleDocumentTagAdd,
|
||||||
onTagRemove: handleTagRemove,
|
onTagRemove: handleTagRemove,
|
||||||
documentLink,
|
|
||||||
onOpenPreview: openDocumentPreview,
|
onOpenPreview: openDocumentPreview,
|
||||||
activePreviewId,
|
activePreviewId,
|
||||||
onUpdateTitle: handleDocumentTitleUpdate,
|
onUpdateTitle: handleDocumentTitleUpdate,
|
||||||
@@ -299,6 +285,8 @@ const useDetailWorkspace = ({
|
|||||||
onFolderNavigate: selectFolder,
|
onFolderNavigate: selectFolder,
|
||||||
onClose: handleDetailPanelClose,
|
onClose: handleDetailPanelClose,
|
||||||
resolveFolderPath,
|
resolveFolderPath,
|
||||||
|
folderNodes,
|
||||||
|
ensureFolderData,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
activePreviewId,
|
activePreviewId,
|
||||||
@@ -313,11 +301,12 @@ const useDetailWorkspace = ({
|
|||||||
handleDocumentIssuedUpdate,
|
handleDocumentIssuedUpdate,
|
||||||
handleDocumentTitleUpdate,
|
handleDocumentTitleUpdate,
|
||||||
handleTagRemove,
|
handleTagRemove,
|
||||||
|
folderNodes,
|
||||||
|
ensureFolderData,
|
||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
resolveApiPath,
|
resolveApiPath,
|
||||||
resolveFolderPath,
|
resolveFolderPath,
|
||||||
selectFolder,
|
selectFolder,
|
||||||
documentLink,
|
|
||||||
tags,
|
tags,
|
||||||
tagLookupById,
|
tagLookupById,
|
||||||
],
|
],
|
||||||
@@ -332,7 +321,6 @@ const useDetailWorkspace = ({
|
|||||||
inspectDocument,
|
inspectDocument,
|
||||||
previewActive,
|
previewActive,
|
||||||
previewWorkspaceDocument,
|
previewWorkspaceDocument,
|
||||||
documentLink,
|
|
||||||
resolveThumbnailUrlForDoc,
|
resolveThumbnailUrlForDoc,
|
||||||
resolveFolderPath,
|
resolveFolderPath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { EditIcon, IconX, CheckIcon, PlusIcon } from '../ui/icons';
|
import { EditIcon, IconX, CheckIcon, PlusIcon } from '../ui/icons';
|
||||||
import SelectionAssignmentMenu, {
|
import SelectionAssignmentMenu, {
|
||||||
SelectionAssignmentMenuItem,
|
SelectionAssignmentMenuItem,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
} from '../utils/date';
|
} from '../utils/date';
|
||||||
import { describeDocumentSummary, type DocumentSummaryRow } from './documentSummary';
|
import { describeDocumentSummary, type DocumentSummaryRow } from './documentSummary';
|
||||||
import { isPlainObject } from '../utils/typeGuards';
|
import { isPlainObject } from '../utils/typeGuards';
|
||||||
|
import { useFolderManager } from '../folders/FolderManagerContext';
|
||||||
|
|
||||||
type Identifier = string | number;
|
type Identifier = string | number;
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ interface DocumentLike {
|
|||||||
id?: Identifier;
|
id?: Identifier;
|
||||||
title?: string;
|
title?: string;
|
||||||
issued_at?: string | null;
|
issued_at?: string | null;
|
||||||
|
folder_id?: string | null;
|
||||||
current_version?: { version_number?: number } | null;
|
current_version?: { version_number?: number } | null;
|
||||||
tags?: TagEntry[];
|
tags?: TagEntry[];
|
||||||
correspondents?: CorrespondentEntry[];
|
correspondents?: CorrespondentEntry[];
|
||||||
@@ -71,6 +74,7 @@ export interface DocumentSummarySectionProps {
|
|||||||
onCorrespondentRemove?: (payload: { documentId: Identifier | undefined; correspondentId: Identifier | undefined }) => void;
|
onCorrespondentRemove?: (payload: { documentId: Identifier | undefined; correspondentId: Identifier | undefined }) => void;
|
||||||
onUpdateTitle?: (docId: Identifier | undefined, title: string) => Promise<boolean> | boolean;
|
onUpdateTitle?: (docId: Identifier | undefined, title: string) => Promise<boolean> | boolean;
|
||||||
onUpdateIssued?: (docId: Identifier | undefined, timestamp: number | null) => Promise<boolean> | boolean;
|
onUpdateIssued?: (docId: Identifier | undefined, timestamp: number | null) => Promise<boolean> | boolean;
|
||||||
|
onFolderNavigate?: (folderId: string | null) => void;
|
||||||
layout?: 'default' | 'compact';
|
layout?: 'default' | 'compact';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,8 +460,10 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
|||||||
onCorrespondentRemove,
|
onCorrespondentRemove,
|
||||||
onUpdateTitle,
|
onUpdateTitle,
|
||||||
onUpdateIssued,
|
onUpdateIssued,
|
||||||
|
onFolderNavigate,
|
||||||
layout = 'default',
|
layout = 'default',
|
||||||
}) => {
|
}) => {
|
||||||
|
const folderManager = useFolderManager();
|
||||||
const isCompactLayout = layout === 'compact';
|
const isCompactLayout = layout === 'compact';
|
||||||
const summaryRows = useMemo(() => describeDocumentSummary(document), [document]);
|
const summaryRows = useMemo(() => describeDocumentSummary(document), [document]);
|
||||||
const issuedDateLabel = useMemo(
|
const issuedDateLabel = useMemo(
|
||||||
@@ -499,6 +505,39 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
|||||||
return rows;
|
return rows;
|
||||||
}, [document?.current_version?.version_number]);
|
}, [document?.current_version?.version_number]);
|
||||||
|
|
||||||
|
const resolvedFolderId = document?.folder_id ?? null;
|
||||||
|
|
||||||
|
const [folderName, setFolderName] = useState<string | null>(() => folderManager.getNameSync(resolvedFolderId));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
const cached = folderManager.getNameSync(resolvedFolderId);
|
||||||
|
setFolderName(cached);
|
||||||
|
if (!cached && resolvedFolderId != null) {
|
||||||
|
folderManager.resolveName(resolvedFolderId).then((name) => {
|
||||||
|
if (active) {
|
||||||
|
setFolderName(name);
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [resolvedFolderId, folderManager]);
|
||||||
|
|
||||||
|
const folderHref = resolvedFolderId == null ? '/documents' : `/documents/folder/${resolvedFolderId}`;
|
||||||
|
|
||||||
|
const handleFolderClick = useCallback(
|
||||||
|
(event: React.MouseEvent) => {
|
||||||
|
if (!onFolderNavigate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
onFolderNavigate(resolvedFolderId);
|
||||||
|
},
|
||||||
|
[onFolderNavigate, resolvedFolderId],
|
||||||
|
);
|
||||||
|
|
||||||
const [titleDraft, setTitleDraft] = useState('');
|
const [titleDraft, setTitleDraft] = useState('');
|
||||||
const [titleSaving, setTitleSaving] = useState(false);
|
const [titleSaving, setTitleSaving] = useState(false);
|
||||||
const [titleError, setTitleError] = useState(null);
|
const [titleError, setTitleError] = useState(null);
|
||||||
@@ -741,11 +780,22 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const folderValueContent = (
|
||||||
|
<Link
|
||||||
|
className="document-summary__folder-link"
|
||||||
|
to={folderHref}
|
||||||
|
onClick={handleFolderClick}
|
||||||
|
>
|
||||||
|
{folderName}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
const summaryRowOverrides = {
|
const summaryRowOverrides = {
|
||||||
title: { valueContent: titleMetaDisplay, error: titleError },
|
title: { valueContent: titleMetaDisplay, error: titleError },
|
||||||
issued: { valueContent: issuedDisplay, error: issuedError },
|
issued: { valueContent: issuedDisplay, error: issuedError },
|
||||||
tags: { valueContent: tagsValueContent },
|
tags: { valueContent: tagsValueContent },
|
||||||
correspondents: { valueContent: correspondentsValueContent },
|
correspondents: { valueContent: correspondentsValueContent },
|
||||||
|
folder: { valueContent: folderValueContent },
|
||||||
} as Record<string, { valueContent?: React.ReactNode | null; error?: string | null }>;
|
} as Record<string, { valueContent?: React.ReactNode | null; error?: string | null }>;
|
||||||
|
|
||||||
const baseRows: MetaItem[] = [...summaryRows, ...extraSummaryRows].map((row) => {
|
const baseRows: MetaItem[] = [...summaryRows, ...extraSummaryRows].map((row) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { formatFileSize } from '../utils/format';
|
import { formatFileSize } from '../utils/format';
|
||||||
import { formatDateTime as defaultFormatDateTime } from '../utils/date';
|
import { formatDateTime as defaultFormatDateTime } from '../utils/date';
|
||||||
|
import { DEFAULT_FOLDER_NAME } from '../app/appLayoutUtils';
|
||||||
|
|
||||||
interface DocumentPageMetadata {
|
interface DocumentPageMetadata {
|
||||||
page_count?: number | string | null;
|
page_count?: number | string | null;
|
||||||
@@ -24,11 +25,13 @@ export interface SummaryDocument {
|
|||||||
original_name?: string | null;
|
original_name?: string | null;
|
||||||
filename?: string | null;
|
filename?: string | null;
|
||||||
content_type?: string | null;
|
content_type?: string | null;
|
||||||
|
folder_id?: string | null;
|
||||||
|
folder_name?: string;
|
||||||
current_version?: DocumentVersion | null;
|
current_version?: DocumentVersion | null;
|
||||||
created_at?: string | null;
|
created_at?: string | null;
|
||||||
updated_at?: string | null;
|
updated_at?: string | null;
|
||||||
issued_at?: string | null;
|
issued_at?: string | null;
|
||||||
folder_path?: string | null;
|
folder_path?: string;
|
||||||
tags?: TagEntry[] | null;
|
tags?: TagEntry[] | null;
|
||||||
correspondents?: CorrespondentEntry[] | null;
|
correspondents?: CorrespondentEntry[] | null;
|
||||||
}
|
}
|
||||||
@@ -37,7 +40,7 @@ interface DescribeSummaryOptions {
|
|||||||
formatDateTime?: typeof defaultFormatDateTime;
|
formatDateTime?: typeof defaultFormatDateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentSummaryRowType = 'text' | 'editable-title' | 'editable-issued' | 'tags' | 'correspondents';
|
export type DocumentSummaryRowType = 'text' | 'editable-title' | 'editable-issued' | 'tags' | 'correspondents' | 'folder';
|
||||||
|
|
||||||
export interface DocumentSummaryRow {
|
export interface DocumentSummaryRow {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -86,6 +89,7 @@ export const describeDocumentSummary = (document?: SummaryDocument | null, optio
|
|||||||
const metadata = doc.current_version?.metadata || null;
|
const metadata = doc.current_version?.metadata || null;
|
||||||
const pageCount = coercePageCount(metadata);
|
const pageCount = coercePageCount(metadata);
|
||||||
const pageCountLabel = Number.isFinite(pageCount) ? String(pageCount) : '—';
|
const pageCountLabel = Number.isFinite(pageCount) ? String(pageCount) : '—';
|
||||||
|
const folderLabel = doc.folder_id == null ? DEFAULT_FOLDER_NAME : `Folder ${doc.folder_id}`;
|
||||||
const tags = sanitizeArray<TagEntry>(doc.tags);
|
const tags = sanitizeArray<TagEntry>(doc.tags);
|
||||||
const correspondents = sanitizeArray<CorrespondentEntry>(doc.correspondents);
|
const correspondents = sanitizeArray<CorrespondentEntry>(doc.correspondents);
|
||||||
const tagLabels = tags.map((tag) => tag.label).filter(Boolean) as string[];
|
const tagLabels = tags.map((tag) => tag.label).filter(Boolean) as string[];
|
||||||
@@ -99,6 +103,7 @@ export const describeDocumentSummary = (document?: SummaryDocument | null, optio
|
|||||||
{ key: 'issued', label: 'Issued', value: formatDateLabel(doc.issued_at), kind: 'editable-issued' },
|
{ key: 'issued', label: 'Issued', value: formatDateLabel(doc.issued_at), kind: 'editable-issued' },
|
||||||
{ key: 'created', label: 'Created at', value: formatDateLabel(doc.created_at) },
|
{ key: 'created', label: 'Created at', value: formatDateLabel(doc.created_at) },
|
||||||
{ key: 'updated', label: 'Updated at', value: formatDateLabel(doc.updated_at) },
|
{ key: 'updated', label: 'Updated at', value: formatDateLabel(doc.updated_at) },
|
||||||
|
{ key: 'folder', label: 'Folder', value: folderLabel, kind: 'folder' },
|
||||||
{ key: 'size', label: 'Size', value: sizeLabel },
|
{ key: 'size', label: 'Size', value: sizeLabel },
|
||||||
{ key: 'content-type', label: 'Content type', value: doc.content_type || 'Unknown' },
|
{ key: 'content-type', label: 'Content type', value: doc.content_type || 'Unknown' },
|
||||||
{ key: 'pages', label: 'Pages', value: pageCountLabel },
|
{ key: 'pages', label: 'Pages', value: pageCountLabel },
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
|
||||||
|
import { DEFAULT_FOLDER_NAME } from '../app/appLayoutUtils';
|
||||||
|
|
||||||
|
type FolderId = string | null;
|
||||||
|
|
||||||
|
export interface FolderManager {
|
||||||
|
getNameSync: (folderId: FolderId) => string | null;
|
||||||
|
resolveName: (folderId: FolderId) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultManager: FolderManager = {
|
||||||
|
getNameSync: (folderId) => (folderId == null ? DEFAULT_FOLDER_NAME : `Folder ${folderId}`),
|
||||||
|
resolveName: async (folderId) => (folderId == null ? DEFAULT_FOLDER_NAME : `Folder ${folderId}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
const FolderManagerContext = createContext<FolderManager>(defaultManager);
|
||||||
|
|
||||||
|
interface FolderManagerProviderProps {
|
||||||
|
folderNodes?: Map<string | 'root', { name?: string | null }>;
|
||||||
|
ensureFolderData?: (folderId: string | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FolderManagerProvider: React.FC<FolderManagerProviderProps> = ({
|
||||||
|
folderNodes,
|
||||||
|
ensureFolderData,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const value = useMemo<FolderManager>(() => {
|
||||||
|
if (!folderNodes || !ensureFolderData) {
|
||||||
|
return defaultManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNameSync = (folderId: FolderId) => {
|
||||||
|
if (folderId == null) return DEFAULT_FOLDER_NAME;
|
||||||
|
return folderNodes.get(folderId)?.name ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveName = async (folderId: FolderId) => {
|
||||||
|
const cached = getNameSync(folderId);
|
||||||
|
if (cached) return cached;
|
||||||
|
if (folderId == null) return DEFAULT_FOLDER_NAME;
|
||||||
|
await ensureFolderData(folderId, { includeDocuments: false });
|
||||||
|
return getNameSync(folderId) ?? `Folder ${folderId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { getNameSync, resolveName };
|
||||||
|
}, [folderNodes, ensureFolderData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FolderManagerContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</FolderManagerContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFolderManager = (): FolderManager => useContext(FolderManagerContext);
|
||||||
|
|
||||||
|
export default FolderManagerContext;
|
||||||
@@ -1233,7 +1233,6 @@ const useDocumentsWorkspace = ({
|
|||||||
inspectDocument,
|
inspectDocument,
|
||||||
previewActive,
|
previewActive,
|
||||||
previewWorkspaceDocument,
|
previewWorkspaceDocument,
|
||||||
documentLink,
|
|
||||||
resolveFolderPath,
|
resolveFolderPath,
|
||||||
} = useDetailWorkspace({
|
} = useDetailWorkspace({
|
||||||
documents: viewDocuments,
|
documents: viewDocuments,
|
||||||
@@ -1244,7 +1243,6 @@ const useDocumentsWorkspace = ({
|
|||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
detailPanelControlRef,
|
detailPanelControlRef,
|
||||||
detailFolderFetchRef,
|
detailFolderFetchRef,
|
||||||
documentLinks,
|
|
||||||
previewDocumentId,
|
previewDocumentId,
|
||||||
activePreviewId,
|
activePreviewId,
|
||||||
openDocumentPreview: openDocumentPreviewForDetail,
|
openDocumentPreview: openDocumentPreviewForDetail,
|
||||||
@@ -1599,7 +1597,6 @@ const useDocumentsWorkspace = ({
|
|||||||
revokePasskey,
|
revokePasskey,
|
||||||
previewActive,
|
previewActive,
|
||||||
previewWorkspaceDocument,
|
previewWorkspaceDocument,
|
||||||
documentLink,
|
|
||||||
previewDocumentId,
|
previewDocumentId,
|
||||||
closeDocumentPreview,
|
closeDocumentPreview,
|
||||||
handleThumbnailRegeneration,
|
handleThumbnailRegeneration,
|
||||||
@@ -1650,7 +1647,6 @@ const useDocumentsWorkspace = ({
|
|||||||
revokePasskey,
|
revokePasskey,
|
||||||
previewActive,
|
previewActive,
|
||||||
previewWorkspaceDocument,
|
previewWorkspaceDocument,
|
||||||
documentLink,
|
|
||||||
previewDocumentId,
|
previewDocumentId,
|
||||||
closeDocumentPreview,
|
closeDocumentPreview,
|
||||||
handleThumbnailRegeneration,
|
handleThumbnailRegeneration,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface DocumentLike {
|
|||||||
title?: string;
|
title?: string;
|
||||||
content_type?: string | null;
|
content_type?: string | null;
|
||||||
issued_at?: string | null;
|
issued_at?: string | null;
|
||||||
|
folder_id?: string | null;
|
||||||
correspondents?: Array<{ id?: string | number; name?: string }>;
|
correspondents?: Array<{ id?: string | number; name?: string }>;
|
||||||
current_version?: {
|
current_version?: {
|
||||||
version_number?: number;
|
version_number?: number;
|
||||||
@@ -56,11 +57,6 @@ interface AssetLike {
|
|||||||
|
|
||||||
interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
|
interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
|
||||||
document: DocumentLike | null;
|
document: DocumentLike | null;
|
||||||
documentLink?: {
|
|
||||||
url?: string;
|
|
||||||
contentType?: string | null;
|
|
||||||
filename?: string | null;
|
|
||||||
} | null;
|
|
||||||
ensureAssetUrl?: (docId: string | number, asset: AssetLike, options?: { force?: boolean }) => Promise<unknown>;
|
ensureAssetUrl?: (docId: string | number, asset: AssetLike, options?: { force?: boolean }) => Promise<unknown>;
|
||||||
getDocumentAsset?: (doc: DocumentLike | null, type: string) => AssetLike | null;
|
getDocumentAsset?: (doc: DocumentLike | null, type: string) => AssetLike | null;
|
||||||
ensurePreviewData?: (docId: string | number, options?: { signal?: AbortSignal }) => Promise<DocumentLike | null>;
|
ensurePreviewData?: (docId: string | number, options?: { signal?: AbortSignal }) => Promise<DocumentLike | null>;
|
||||||
@@ -78,7 +74,6 @@ interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
|
|||||||
export const createDocumentViewerHeaderActions = ({
|
export const createDocumentViewerHeaderActions = ({
|
||||||
document,
|
document,
|
||||||
actionState,
|
actionState,
|
||||||
documentLink,
|
|
||||||
onZoom,
|
onZoom,
|
||||||
canZoom = false,
|
canZoom = false,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -86,7 +81,7 @@ export const createDocumentViewerHeaderActions = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadHref = actionState?.downloadHref || documentLink?.url;
|
const downloadHref = actionState?.downloadHref;
|
||||||
if (!downloadHref && !(canZoom && onZoom)) {
|
if (!downloadHref && !(canZoom && onZoom)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -122,7 +117,6 @@ export const createDocumentViewerHeaderActions = ({
|
|||||||
|
|
||||||
const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||||
document,
|
document,
|
||||||
documentLink,
|
|
||||||
tagLookupById,
|
tagLookupById,
|
||||||
tagOptions,
|
tagOptions,
|
||||||
onTagAdd,
|
onTagAdd,
|
||||||
@@ -168,6 +162,16 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
return Boolean(getDocumentAsset(document, 'ocr-text'));
|
return Boolean(getDocumentAsset(document, 'ocr-text'));
|
||||||
}, [document, getDocumentAsset]);
|
}, [document, getDocumentAsset]);
|
||||||
|
|
||||||
|
const navigateToFolder = useCallback(
|
||||||
|
(folderId) => {
|
||||||
|
const target = folderId == null
|
||||||
|
? '/documents'
|
||||||
|
: `/documents/folder/${folderId}`;
|
||||||
|
navigate(target);
|
||||||
|
},
|
||||||
|
[navigate],
|
||||||
|
);
|
||||||
|
|
||||||
const summaryProps = useMemo(
|
const summaryProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
tagLookupById,
|
tagLookupById,
|
||||||
@@ -180,6 +184,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
onCorrespondentRemove,
|
onCorrespondentRemove,
|
||||||
onUpdateTitle,
|
onUpdateTitle,
|
||||||
onUpdateIssued,
|
onUpdateIssued,
|
||||||
|
onFolderNavigate: navigateToFolder,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
tagLookupById,
|
tagLookupById,
|
||||||
@@ -192,6 +197,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
onCorrespondentRemove,
|
onCorrespondentRemove,
|
||||||
onUpdateTitle,
|
onUpdateTitle,
|
||||||
onUpdateIssued,
|
onUpdateIssued,
|
||||||
|
navigateToFolder,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -251,7 +257,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
|
|
||||||
const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false);
|
const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false);
|
||||||
|
|
||||||
const fallbackDocumentLink = useMemo(() => {
|
const resolvedDocumentLink = useMemo(() => {
|
||||||
if (!document) {
|
if (!document) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -272,14 +278,12 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
};
|
};
|
||||||
}, [document, resolveApiPath]);
|
}, [document, resolveApiPath]);
|
||||||
|
|
||||||
const effectiveDocumentLink = documentLink?.url ? documentLink : fallbackDocumentLink;
|
|
||||||
|
|
||||||
const handleZoomOpen = useCallback(() => {
|
const handleZoomOpen = useCallback(() => {
|
||||||
if (!effectiveDocumentLink?.url) {
|
if (!resolvedDocumentLink?.url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setZoomOverlayOpen(true);
|
setZoomOverlayOpen(true);
|
||||||
}, [effectiveDocumentLink?.url]);
|
}, [resolvedDocumentLink?.url]);
|
||||||
|
|
||||||
const handleZoomClose = useCallback(() => {
|
const handleZoomClose = useCallback(() => {
|
||||||
setZoomOverlayOpen(false);
|
setZoomOverlayOpen(false);
|
||||||
@@ -287,7 +291,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setZoomOverlayOpen(false);
|
setZoomOverlayOpen(false);
|
||||||
}, [effectiveDocumentLink?.url, document?.id]);
|
}, [resolvedDocumentLink?.url, document?.id]);
|
||||||
|
|
||||||
const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(null);
|
const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(null);
|
||||||
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
|
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
|
||||||
@@ -344,19 +348,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
];
|
];
|
||||||
}, [document, resolveFolderPath]);
|
}, [document, resolveFolderPath]);
|
||||||
|
|
||||||
const handleBreadcrumbNavigate = useCallback(
|
|
||||||
(crumb) => {
|
|
||||||
if (!crumb?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const target = crumb.id === 'root'
|
|
||||||
? '/documents'
|
|
||||||
: `/documents/folder/${crumb.id}`;
|
|
||||||
navigate(target);
|
|
||||||
},
|
|
||||||
[navigate],
|
|
||||||
);
|
|
||||||
|
|
||||||
const breadcrumbTrailEntries = useMemo(() => {
|
const breadcrumbTrailEntries = useMemo(() => {
|
||||||
if (!breadcrumbs.length) {
|
if (!breadcrumbs.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -365,28 +356,27 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
return breadcrumbs.map((crumb, index) => ({
|
return breadcrumbs.map((crumb, index) => ({
|
||||||
id: crumb.id,
|
id: crumb.id,
|
||||||
label: crumb.name,
|
label: crumb.name,
|
||||||
onClick: index < lastIndex ? () => handleBreadcrumbNavigate(crumb) : null,
|
onClick: index < lastIndex ? () => navigateToFolder(crumb.id) : null,
|
||||||
}));
|
}));
|
||||||
}, [breadcrumbs, handleBreadcrumbNavigate]);
|
}, [breadcrumbs, navigateToFolder]);
|
||||||
|
|
||||||
const zoomDisplay = useMemo(() => {
|
const zoomDisplay = useMemo(() => {
|
||||||
if (!effectiveDocumentLink?.url || !document) {
|
if (!resolvedDocumentLink?.url || !document) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const docContentType = document.content_type;
|
const docContentType = document.content_type;
|
||||||
const versionContentType = document.current_version?.version?.content_type;
|
const versionContentType = document.current_version?.version?.content_type;
|
||||||
const normalizedContentType = effectiveDocumentLink.contentType || docContentType || versionContentType || null;
|
const normalizedContentType = resolvedDocumentLink.contentType || docContentType || versionContentType || null;
|
||||||
return {
|
return {
|
||||||
url: effectiveDocumentLink.url,
|
url: resolvedDocumentLink.url,
|
||||||
alt: document.title,
|
alt: document.title,
|
||||||
contentType: normalizedContentType || undefined,
|
contentType: normalizedContentType || undefined,
|
||||||
};
|
};
|
||||||
}, [effectiveDocumentLink?.url, effectiveDocumentLink?.contentType, document]);
|
}, [resolvedDocumentLink?.url, resolvedDocumentLink?.contentType, document]);
|
||||||
|
|
||||||
const headerActions = createDocumentViewerHeaderActions({
|
const headerActions = createDocumentViewerHeaderActions({
|
||||||
document,
|
document,
|
||||||
actionState,
|
actionState,
|
||||||
documentLink: effectiveDocumentLink,
|
|
||||||
onZoom: zoomDisplay ? handleZoomOpen : null,
|
onZoom: zoomDisplay ? handleZoomOpen : null,
|
||||||
canZoom: Boolean(zoomDisplay),
|
canZoom: Boolean(zoomDisplay),
|
||||||
});
|
});
|
||||||
@@ -486,7 +476,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
<section className={viewerClassName}>
|
<section className={viewerClassName}>
|
||||||
<DocumentViewerLayout
|
<DocumentViewerLayout
|
||||||
document={document}
|
document={document}
|
||||||
documentLink={effectiveDocumentLink}
|
documentLink={resolvedDocumentLink}
|
||||||
summaryProps={summaryProps}
|
summaryProps={summaryProps}
|
||||||
metadataPayload={metadataPayload}
|
metadataPayload={metadataPayload}
|
||||||
contentTabConfig={contentTabConfig}
|
contentTabConfig={contentTabConfig}
|
||||||
|
|||||||
Reference in New Issue
Block a user