refactor: Consolidate workspace utilities into new workspaceUtils.ts with typed entry keys, replacing appLayoutUtils.ts, and update related components.

This commit is contained in:
2025-11-24 22:38:01 +01:00
parent deec2fe69d
commit 096c377897
19 changed files with 202 additions and 283 deletions
-4
View File
@@ -22,7 +22,6 @@ type EnsureAssetUrl = (
type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>; type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
type GetDocumentAsset = (document: unknown, assetType: string) => unknown; type GetDocumentAsset = (document: unknown, assetType: string) => unknown;
type ResolveApiPath = (path: string) => string;
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
interface DocumentsTableProps { interface DocumentsTableProps {
@@ -43,7 +42,6 @@ interface DocumentsRouteAppShell {
previewDocumentId?: Identifier | null; previewDocumentId?: Identifier | null;
closeDocumentPreview?: () => void; closeDocumentPreview?: () => void;
ensurePreviewData?: EnsurePreviewData; ensurePreviewData?: EnsurePreviewData;
resolveApiPath?: ResolveApiPath;
ensureAssetUrl?: EnsureAssetUrl; ensureAssetUrl?: EnsureAssetUrl;
getDocumentAsset?: GetDocumentAsset; getDocumentAsset?: GetDocumentAsset;
notifyApiError?: NotifyApiError; notifyApiError?: NotifyApiError;
@@ -63,7 +61,6 @@ const DocumentsRouteContent: React.FC = () => {
previewDocumentId, previewDocumentId,
closeDocumentPreview, closeDocumentPreview,
ensurePreviewData, ensurePreviewData,
resolveApiPath,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
notifyApiError, notifyApiError,
@@ -118,7 +115,6 @@ const DocumentsRouteContent: React.FC = () => {
ensureAssetUrl, ensureAssetUrl,
ensurePreviewData, ensurePreviewData,
getDocumentAsset, getDocumentAsset,
resolveApiPath,
notifyApiError, notifyApiError,
closeDocumentPreview, closeDocumentPreview,
}); });
-115
View File
@@ -1,115 +0,0 @@
import { resolveAssetExpiresAt, resolveAssetUrl } from '../asset_manager';
export const ASSET_PRESIGN_TTL_MS = 240 * 1000; // backend issues 5 min tokens; refresh slightly early
export const DEFAULT_FOLDER_NAME = 'Documents';
export const DEFAULT_SORT_FIELD = 'title';
export const DEFAULT_SORT_DIRECTION = 'asc';
export const SORT_FIELD_VALUES = ['title', 'issued_at', 'created_at', 'updated_at'];
export const TAG_FILTER_UNTAGGED = '__UNTAGGED__';
const ROW_KEY_SEPARATOR = ':';
const DOCUMENT_ROW_PREFIX = 'document';
const FOLDER_ROW_PREFIX = 'folder';
export const resolveApiPath = (path = '') => path;
const makeRowKey = (type, id) =>
id ? `${type}${ROW_KEY_SEPARATOR}${id}` : `${type}${ROW_KEY_SEPARATOR}`;
const normalizeRowKey = (key: string | number | null) => String(key ?? '');
const getRowType = (key) => normalizeRowKey(key).split(ROW_KEY_SEPARATOR, 1)[0] ?? '';
export const getRowId = (key) => {
const normalized = normalizeRowKey(key);
const separatorIndex = normalized.indexOf(ROW_KEY_SEPARATOR);
if (separatorIndex === -1) return normalized;
return normalized.slice(separatorIndex + 1);
};
export const isDocumentRowKey = (key) => getRowType(key) === DOCUMENT_ROW_PREFIX;
export const isFolderRowKey = (key) => getRowType(key) === FOLDER_ROW_PREFIX;
export const resolveDocumentRowKey = (documentId) =>
documentId ? makeRowKey(DOCUMENT_ROW_PREFIX, documentId) : null;
export const resolveFolderRowKey = (folderId) =>
folderId ? makeRowKey(FOLDER_ROW_PREFIX, folderId) : null;
export const hasFiles = (event) =>
Array.from(event.dataTransfer?.types || []).includes('Files');
const isAssetEquivalent = (lhs, rhs) => {
if (!lhs || !rhs) return false;
const lhsPrimaryMetadata = lhs?.metadata;
const rhsPrimaryMetadata = rhs?.metadata;
const lhsExpiresAt = resolveAssetExpiresAt(lhs);
const rhsExpiresAt = resolveAssetExpiresAt(rhs);
return (
lhs.id === rhs.id
&& resolveAssetUrl(lhs) === resolveAssetUrl(rhs)
&& lhsExpiresAt === rhsExpiresAt
&& lhsPrimaryMetadata?.width === rhsPrimaryMetadata?.width
&& lhsPrimaryMetadata?.height === rhsPrimaryMetadata?.height
&& lhs.mime_type === rhs.mime_type
&& lhs.asset_type === rhs.asset_type
&& lhs.updated_at === rhs.updated_at
);
};
const mergeAssetIntoGroup = (group, assetData) => {
if (!assetData || !assetData.asset_type) {
if (Array.isArray(group)) {
return group;
}
return group || {};
}
if (Array.isArray(group) || !group) {
const list = Array.isArray(group) ? group : [];
const index = list.findIndex((item) => item?.id === assetData.id);
if (index >= 0) {
const existing = list[index];
if (isAssetEquivalent(existing, assetData)) {
return list;
}
const next = list.slice();
next[index] = { ...existing, ...assetData };
return next;
}
return list.concat({ ...assetData });
}
const key = assetData.asset_type;
const previous = group?.[key];
if (previous && isAssetEquivalent(previous, assetData)) {
return group;
}
const next = { ...(group || {}) };
next[key] = { ...(previous || {}), ...assetData };
return next;
};
export const mergeAssetIntoDocument = (doc, assetData) => {
if (!doc) return doc;
const existingGroup = doc.current_version?.assets || null;
const nextGroup = mergeAssetIntoGroup(existingGroup, assetData);
if (nextGroup === existingGroup) {
return doc;
}
const updatedCurrentVersion = doc.current_version
? { ...doc.current_version, assets: nextGroup }
: { assets: nextGroup };
return { ...doc, current_version: updatedCurrentVersion };
};
export const createRootNode = () => ({
id: 'root',
name: DEFAULT_FOLDER_NAME,
parentId: null,
children: [],
expanded: true,
loaded: false,
hasChildren: false,
});
+1 -1
View File
@@ -3,7 +3,7 @@ import {
DEFAULT_SORT_DIRECTION, DEFAULT_SORT_DIRECTION,
DEFAULT_SORT_FIELD, DEFAULT_SORT_FIELD,
SORT_FIELD_VALUES, SORT_FIELD_VALUES,
} from './appLayoutUtils'; } from './workspaceUtils';
const VIEW_MODE_STORAGE_KEY = 'papercrate_view_mode'; const VIEW_MODE_STORAGE_KEY = 'papercrate_view_mode';
const SORT_FIELD_STORAGE_KEY = 'papercrate_sort_field'; const SORT_FIELD_STORAGE_KEY = 'papercrate_sort_field';
+1 -1
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import { TAG_FILTER_UNTAGGED } from './appLayoutUtils'; import { TAG_FILTER_UNTAGGED } from './workspaceUtils';
import { listDocuments } from '../lib/apiClient'; import { listDocuments } from '../lib/apiClient';
type Identifier = string | number; type Identifier = string | number;
+30 -35
View File
@@ -11,7 +11,6 @@ type Identifier = string | number;
type EnsureAssetUrl = (docId: Identifier, asset: unknown, options?: Record<string, unknown>) => Promise<unknown> | void; type EnsureAssetUrl = (docId: Identifier, asset: unknown, options?: Record<string, unknown>) => Promise<unknown> | void;
type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>; type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
type GetDocumentAsset = (document: unknown, assetType: string) => unknown; type GetDocumentAsset = (document: unknown, assetType: string) => unknown;
type ResolveApiPath = (path: string) => string;
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
type WorkspaceSurface = { content: ReactNode; detail?: ReactNode | null } | null; type WorkspaceSurface = { content: ReactNode; detail?: ReactNode | null } | null;
@@ -28,7 +27,6 @@ interface UseWorkspaceSurfaceArgs {
ensureAssetUrl?: EnsureAssetUrl; ensureAssetUrl?: EnsureAssetUrl;
ensurePreviewData?: EnsurePreviewData; ensurePreviewData?: EnsurePreviewData;
getDocumentAsset?: GetDocumentAsset; getDocumentAsset?: GetDocumentAsset;
resolveApiPath?: ResolveApiPath;
notifyApiError?: NotifyApiError; notifyApiError?: NotifyApiError;
closeDocumentPreview?: () => void; closeDocumentPreview?: () => void;
} }
@@ -49,7 +47,6 @@ export const useWorkspaceSurface = ({
ensureAssetUrl, ensureAssetUrl,
ensurePreviewData, ensurePreviewData,
getDocumentAsset, getDocumentAsset,
resolveApiPath,
notifyApiError, notifyApiError,
closeDocumentPreview, closeDocumentPreview,
}: UseWorkspaceSurfaceArgs): UseWorkspaceSurfaceResult => { }: UseWorkspaceSurfaceArgs): UseWorkspaceSurfaceResult => {
@@ -92,34 +89,34 @@ export const useWorkspaceSurface = ({
const detail = detailPanelOpen && detailPanelProps const detail = detailPanelOpen && detailPanelProps
? (() => { ? (() => {
const { const {
onClose, onClose,
onOpenPreview, onOpenPreview,
tags: tagOptions, tags: tagOptions,
folderNodes, folderNodes,
ensureFolderData, ensureFolderData,
...restDetailProps ...restDetailProps
} = detailPanelProps; } = detailPanelProps;
const viewer = ( const viewer = (
<DocumentViewerPanel <DocumentViewerPanel
variant="sidebar" variant="sidebar"
onCollapsePanel={onClose} onCollapsePanel={onClose}
onMaximizePanel={onOpenPreview} onMaximizePanel={onOpenPreview}
tagOptions={tagOptions} tagOptions={tagOptions}
{...restDetailProps} {...restDetailProps}
/> />
); );
if (folderNodes && ensureFolderData) { if (folderNodes && ensureFolderData) {
return (
<FolderManagerProvider folderNodes={folderNodes} ensureFolderData={ensureFolderData}>
{viewer}
</FolderManagerProvider>
);
}
return ( return (
<>{viewer}</> <FolderManagerProvider folderNodes={folderNodes} ensureFolderData={ensureFolderData}>
{viewer}
</FolderManagerProvider>
); );
})() }
return (
<>{viewer}</>
);
})()
: null; : null;
return { return {
@@ -179,7 +176,6 @@ export const useWorkspaceSurface = ({
ensureAssetUrl={ensureAssetUrl} ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset} getDocumentAsset={getDocumentAsset}
ensurePreviewData={ensurePreviewData} ensurePreviewData={ensurePreviewData}
resolveApiPath={resolveApiPath}
notifyApiError={notifyApiError} notifyApiError={notifyApiError}
sidebarToggle={sidebarToggle} sidebarToggle={sidebarToggle}
onClosePanel={closeDocumentPreview} onClosePanel={closeDocumentPreview}
@@ -189,10 +185,10 @@ export const useWorkspaceSurface = ({
const content = folderNodes && ensureFolderData const content = folderNodes && ensureFolderData
? ( ? (
<FolderManagerProvider folderNodes={folderNodes} ensureFolderData={ensureFolderData}> <FolderManagerProvider folderNodes={folderNodes} ensureFolderData={ensureFolderData}>
{viewer} {viewer}
</FolderManagerProvider> </FolderManagerProvider>
) )
: viewer; : viewer;
return { content, detail: null }; return { content, detail: null };
@@ -203,7 +199,6 @@ export const useWorkspaceSurface = ({
ensurePreviewData, ensurePreviewData,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
resolveApiPath,
notifyApiError, notifyApiError,
renderSidebarToggle, renderSidebarToggle,
closeDocumentPreview, closeDocumentPreview,
+65
View File
@@ -0,0 +1,65 @@
export const DEFAULT_FOLDER_NAME = 'Documents';
export const DEFAULT_SORT_FIELD = 'title';
export const DEFAULT_SORT_DIRECTION = 'asc';
export const SORT_FIELD_VALUES = ['title', 'issued_at', 'created_at', 'updated_at'];
export const TAG_FILTER_UNTAGGED = '__UNTAGGED__';
const ROW_KEY_SEPARATOR = ':';
const DOCUMENT_ROW_PREFIX = 'document';
const FOLDER_ROW_PREFIX = 'folder';
const makeRowKey = (type, id) =>
id ? `${type}${ROW_KEY_SEPARATOR}${id}` : `${type}${ROW_KEY_SEPARATOR}`;
const getRowType = (key: string | null) => (key ?? '').split(ROW_KEY_SEPARATOR, 1)[0] ?? '';
export const getRowId = (key: string) => {
const parts = key.split(ROW_KEY_SEPARATOR);
return parts.slice(1).join(ROW_KEY_SEPARATOR);
};
export const isDocumentRowKey = (key) => getRowType(key) === DOCUMENT_ROW_PREFIX;
export const isFolderRowKey = (key) => getRowType(key) === FOLDER_ROW_PREFIX;
export const resolveDocumentRowKey = (documentId) =>
documentId ? makeRowKey(DOCUMENT_ROW_PREFIX, documentId) : null;
export const resolveFolderRowKey = (folderId) =>
folderId ? makeRowKey(FOLDER_ROW_PREFIX, folderId) : null;
export const hasFiles = (event) =>
Array.from(event.dataTransfer?.types || []).includes('Files');
const mergeAssetIntoGroup = (group, assetData) => {
if (!assetData || !assetData.asset_type) {
return group || [];
}
const list = Array.isArray(group) ? group : [];
const index = list.findIndex((item) => item?.asset_type === assetData.asset_type);
if (index >= 0) {
const next = list.slice();
next[index] = assetData;
return next;
}
return list.concat(assetData);
};
export const mergeAssetIntoDocument = (doc, assetData) => {
if (!doc) return doc;
const nextGroup = mergeAssetIntoGroup(doc.current_version?.assets, assetData);
return {
...doc,
current_version: { ...(doc.current_version || {}), assets: nextGroup },
};
};
export const createRootNode = () => ({
id: 'root',
name: DEFAULT_FOLDER_NAME,
parentId: null,
children: [],
expanded: true,
loaded: false,
hasChildren: false,
});
+6 -7
View File
@@ -42,7 +42,7 @@ export const resolveAssetUrl = (asset?: { download?: { url: string } | null } |
export type EnsureAssetUrl = ( export type EnsureAssetUrl = (
documentId: Identifier, documentId: Identifier,
asset: AssetLike, asset: AssetLike,
options?: { force?: boolean; [key: string]: unknown }, options?: { force?: boolean;[key: string]: unknown },
) => Promise<unknown>; ) => Promise<unknown>;
export type GetAsset = (document: DocumentLike, assetType: string) => Nullable<AssetLike>; export type GetAsset = (document: DocumentLike, assetType: string) => Nullable<AssetLike>;
@@ -171,7 +171,7 @@ export const resolveDocumentAssetUrl = (
}: { }: {
ensureAssetUrl?: EnsureAssetUrl; ensureAssetUrl?: EnsureAssetUrl;
getAsset?: GetAsset; getAsset?: GetAsset;
ensureOptions?: { force?: boolean; [key: string]: unknown }; ensureOptions?: { force?: boolean;[key: string]: unknown };
} = {}, } = {},
): Nullable<string> => { ): Nullable<string> => {
if (!doc || !type) { if (!doc || !type) {
@@ -191,24 +191,23 @@ export const resolveDocumentAssetUrl = (
} }
if (doc.id && asset.id && ensureAssetUrl) { if (doc.id && asset.id && ensureAssetUrl) {
const force = Boolean(url && expiresAt && expiresAt <= now); const force = Boolean(url && expiresAt && expiresAt <= now);
const options: { force: boolean; [key: string]: unknown } = { const options: { force: boolean;[key: string]: unknown } = {
force, force,
...(ensureOptions || {}), ...(ensureOptions || {}),
}; };
ensureAssetUrl(doc.id, asset, options).catch(() => {}); ensureAssetUrl(doc.id, asset, options).catch(() => { });
} }
return null; return null;
}; };
class AssetManager { class AssetManager {
fetchAsset: ((id: Identifier) => Promise<AssetLike | null>) | null; fetchAsset: ((id: Identifier) => Promise<AssetLike | null>) | null;
assetPresignTtlMs: number;
assetCache: Map<Identifier, AssetLike>; assetCache: Map<Identifier, AssetLike>;
assetInflight: Map<string, Promise<AssetLike | null>>; assetInflight: Map<string, Promise<AssetLike | null>>;
constructor({ fetchAsset, assetPresignTtlMs }: { fetchAsset: ((id: Identifier) => Promise<AssetLike | null>) | null; assetPresignTtlMs: number }) { constructor({ fetchAsset }: { fetchAsset: ((id: Identifier) => Promise<AssetLike | null>) | null }) {
this.fetchAsset = fetchAsset; this.fetchAsset = fetchAsset;
this.assetPresignTtlMs = assetPresignTtlMs;
this.assetCache = new Map(); this.assetCache = new Map();
this.assetInflight = new Map(); this.assetInflight = new Map();
} }
+1 -5
View File
@@ -6,7 +6,7 @@ import {
DEFAULT_FOLDER_NAME, DEFAULT_FOLDER_NAME,
getRowId, getRowId,
isDocumentRowKey, isDocumentRowKey,
} from '../app/appLayoutUtils'; } from '../app/workspaceUtils';
import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel'; import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel';
import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr'; import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr';
@@ -46,7 +46,6 @@ interface UseDetailWorkspaceArgs {
correspondents?: unknown[]; correspondents?: unknown[];
handleCorrespondentAdd?: (...args: unknown[]) => void; handleCorrespondentAdd?: (...args: unknown[]) => void;
handleCorrespondentRemove?: (...args: unknown[]) => void; handleCorrespondentRemove?: (...args: unknown[]) => void;
resolveApiPath?: (path: string) => string;
selectFolder?: (folderId?: Identifier | 'root') => void; selectFolder?: (folderId?: Identifier | 'root') => void;
tags?: unknown[]; tags?: unknown[];
tagLookupById?: Map<Identifier, unknown> | null; tagLookupById?: Map<Identifier, unknown> | null;
@@ -86,7 +85,6 @@ const useDetailWorkspace = ({
correspondents, correspondents,
handleCorrespondentAdd, handleCorrespondentAdd,
handleCorrespondentRemove, handleCorrespondentRemove,
resolveApiPath,
selectFolder, selectFolder,
tags, tags,
tagLookupById, tagLookupById,
@@ -281,7 +279,6 @@ const useDetailWorkspace = ({
correspondents, correspondents,
onCorrespondentAdd: handleCorrespondentAdd, onCorrespondentAdd: handleCorrespondentAdd,
onCorrespondentRemove: handleCorrespondentRemove, onCorrespondentRemove: handleCorrespondentRemove,
resolveApiPath,
onFolderNavigate: selectFolder, onFolderNavigate: selectFolder,
onClose: handleDetailPanelClose, onClose: handleDetailPanelClose,
resolveFolderPath, resolveFolderPath,
@@ -304,7 +301,6 @@ const useDetailWorkspace = ({
folderNodes, folderNodes,
ensureFolderData, ensureFolderData,
openDocumentPreview, openDocumentPreview,
resolveApiPath,
resolveFolderPath, resolveFolderPath,
selectFolder, selectFolder,
tags, tags,
+17 -21
View File
@@ -6,13 +6,11 @@ import type {
DocumentLike as OcrDocumentLike, DocumentLike as OcrDocumentLike,
} from '../utils/ocr'; } from '../utils/ocr';
type ResolveApiPath = (path: string) => string;
export type DocumentLike = OcrDocumentLike; export type DocumentLike = OcrDocumentLike;
const asyncFalse = async () => false; const asyncFalse = async () => false;
const resolveDocumentDownloadHref = (document?: DocumentLike | null, resolveApiPath?: ResolveApiPath | null): string | null => { const resolveDocumentDownloadHref = (document?: DocumentLike | null): string | null => {
if (!document) { if (!document) {
return null; return null;
} }
@@ -20,7 +18,7 @@ const resolveDocumentDownloadHref = (document?: DocumentLike | null, resolveApiP
if (!downloadUrl) { if (!downloadUrl) {
return null; return null;
} }
return resolveApiPath ? resolveApiPath(downloadUrl) : downloadUrl; return downloadUrl;
}; };
const hasDocumentOcrAsset = (document?: DocumentLike | null, getDocumentAsset?: GetDocumentAsset | null): boolean => { const hasDocumentOcrAsset = (document?: DocumentLike | null, getDocumentAsset?: GetDocumentAsset | null): boolean => {
@@ -32,7 +30,6 @@ const hasDocumentOcrAsset = (document?: DocumentLike | null, getDocumentAsset?:
interface CreateDocumentActionStateArgs { interface CreateDocumentActionStateArgs {
document: DocumentLike | null; document: DocumentLike | null;
resolveApiPath?: ResolveApiPath | null;
ensurePreviewData: EnsurePreviewData; ensurePreviewData: EnsurePreviewData;
ensureAssetUrl: EnsureAssetUrl; ensureAssetUrl: EnsureAssetUrl;
getDocumentAsset?: GetDocumentAsset | null; getDocumentAsset?: GetDocumentAsset | null;
@@ -42,7 +39,6 @@ interface CreateDocumentActionStateArgs {
export const createDocumentActionState = ({ export const createDocumentActionState = ({
document, document,
resolveApiPath,
ensurePreviewData, ensurePreviewData,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
@@ -57,27 +53,27 @@ export const createDocumentActionState = ({
}; };
} }
const downloadHref = resolveDocumentDownloadHref(document, resolveApiPath); const downloadHref = resolveDocumentDownloadHref(document);
const hasOcr = hasDocumentOcrAsset(document, getDocumentAsset); const hasOcr = hasDocumentOcrAsset(document, getDocumentAsset);
const openOcr = hasOcr const openOcr = hasOcr
? async () => { ? async () => {
try { try {
const success = await openOcrTextInNewTab({ const success = await openOcrTextInNewTab({
document, document,
ensurePreviewData, ensurePreviewData,
getDocumentAsset, getDocumentAsset,
ensureAssetUrl, ensureAssetUrl,
}); });
if (!success) { if (!success) {
notifyApiError?.(new Error('OCR text URL unavailable.'), ocrErrorMessage); notifyApiError?.(new Error('OCR text URL unavailable.'), ocrErrorMessage);
}
return success;
} catch (error) {
notifyApiError?.(error, ocrErrorMessage);
throw error;
} }
return success;
} catch (error) {
notifyApiError?.(error, ocrErrorMessage);
throw error;
} }
}
: asyncFalse; : asyncFalse;
return { return {
+1 -1
View File
@@ -1,6 +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'; import { DEFAULT_FOLDER_NAME } from '../app/workspaceUtils';
interface DocumentPageMetadata { interface DocumentPageMetadata {
page_count?: number | string | null; page_count?: number | string | null;
@@ -1,5 +1,5 @@
import React, { createContext, useContext, useMemo, type ReactNode } from 'react'; import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
import { DEFAULT_FOLDER_NAME } from '../app/appLayoutUtils'; import { DEFAULT_FOLDER_NAME } from '../app/workspaceUtils';
type FolderId = string | null; type FolderId = string | null;
@@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import { DEFAULT_FOLDER_NAME, getRowId, isDocumentRowKey } from '../../app/appLayoutUtils'; import { DEFAULT_FOLDER_NAME, getRowId, isDocumentRowKey } from '../../app/workspaceUtils';
import { import {
addDocumentTags, addDocumentTags,
createTag, createTag,
@@ -1,7 +1,7 @@
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import useFileDrop from './useFileDrop'; import useFileDrop from './useFileDrop';
import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/appLayoutUtils'; import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/workspaceUtils';
import { fetchDocument } from '../../lib/apiClient'; import { fetchDocument } from '../../lib/apiClient';
type Identifier = string | number; type Identifier = string | number;
@@ -87,9 +87,9 @@ const mapFilesToEntries = (filesInput?: FileList | File[] | null): FileEntry[] =
const relativePath = (file as File & { webkitRelativePath?: string })?.webkitRelativePath ?? ''; const relativePath = (file as File & { webkitRelativePath?: string })?.webkitRelativePath ?? '';
const segments = relativePath const segments = relativePath
? relativePath ? relativePath
.split('/') .split('/')
.slice(0, -1) .slice(0, -1)
.filter(Boolean) .filter(Boolean)
: []; : [];
return { file, segments }; return { file, segments };
}); });
@@ -325,9 +325,9 @@ const useDocumentUploads = ({
const relativePath = (fileFromItem as File & { webkitRelativePath?: string })?.webkitRelativePath ?? ''; const relativePath = (fileFromItem as File & { webkitRelativePath?: string })?.webkitRelativePath ?? '';
const segments = relativePath const segments = relativePath
? relativePath ? relativePath
.split('/') .split('/')
.slice(0, -1) .slice(0, -1)
.filter(Boolean) .filter(Boolean)
: []; : [];
pushFile(fileFromItem, segments); pushFile(fileFromItem, segments);
} }
@@ -355,9 +355,9 @@ const useDocumentUploads = ({
const relativePath = (file as File & { webkitRelativePath?: string })?.webkitRelativePath ?? ''; const relativePath = (file as File & { webkitRelativePath?: string })?.webkitRelativePath ?? '';
const segments = relativePath const segments = relativePath
? relativePath ? relativePath
.split('/') .split('/')
.slice(0, -1) .slice(0, -1)
.filter(Boolean) .filter(Boolean)
: []; : [];
pushFile(file, segments); pushFile(file, segments);
}); });
@@ -29,7 +29,7 @@ import useDocumentsPanelProps from '../../documents/hooks/useDocumentsPanelProps
import useDocumentPreview from '../../app/useDocumentPreview'; import useDocumentPreview from '../../app/useDocumentPreview';
import useSidebarProps from '../../sidebar/useSidebarProps'; import useSidebarProps from '../../sidebar/useSidebarProps';
import { import {
ASSET_PRESIGN_TTL_MS,
DEFAULT_SORT_DIRECTION, DEFAULT_SORT_DIRECTION,
DEFAULT_SORT_FIELD, DEFAULT_SORT_FIELD,
createRootNode, createRootNode,
@@ -37,10 +37,9 @@ import {
isDocumentRowKey, isDocumentRowKey,
isFolderRowKey, isFolderRowKey,
mergeAssetIntoDocument, mergeAssetIntoDocument,
resolveApiPath,
resolveDocumentRowKey, resolveDocumentRowKey,
resolveFolderRowKey, resolveFolderRowKey,
} from '../../app/appLayoutUtils'; } from '../../app/workspaceUtils';
import useDocumentsSearch from '../../app/useDocumentsSearch'; import useDocumentsSearch from '../../app/useDocumentsSearch';
import useDocumentsStore from './store/useDocumentsStore'; import useDocumentsStore from './store/useDocumentsStore';
import useAuthManager from './useAuthManager'; import useAuthManager from './useAuthManager';
@@ -64,7 +63,7 @@ const EntryType = Object.freeze({
folder: 'folder', folder: 'folder',
}); });
const noop = () => {}; const noop = () => { };
type Identifier = string | number; type Identifier = string | number;
type DocumentId = Identifier; type DocumentId = Identifier;
@@ -79,7 +78,7 @@ interface DocumentLike {
interface FolderContentsEntry { interface FolderContentsEntry {
folder?: { id?: FolderId; name?: string | null } | null; folder?: { id?: FolderId; name?: string | null } | null;
documents?: DocumentLike[]; documents?: DocumentLike[];
subfolders?: Array<{ id?: FolderId; name?: string | null; [key: string]: unknown }>; subfolders?: Array<{ id?: FolderId; name?: string | null;[key: string]: unknown }>;
__includesDocuments?: boolean; __includesDocuments?: boolean;
__sortField?: string | null; __sortField?: string | null;
__sortDirection?: string | null; __sortDirection?: string | null;
@@ -189,7 +188,7 @@ const useDocumentsWorkspace = ({
const breadcrumbFetchRef = useRef(new Set()); const breadcrumbFetchRef = useRef(new Set());
const tagRemovalCursorActiveRef = useRef(false); const tagRemovalCursorActiveRef = useRef(false);
const tenantIdRef = useRef(currentTenantId); const tenantIdRef = useRef(currentTenantId);
const detailPanelControlRef = useRef({ open: () => {}, close: () => {} }); const detailPanelControlRef = useRef({ open: () => { }, close: () => { } });
const setTagRemovalCursor = useCallback((active) => { const setTagRemovalCursor = useCallback((active) => {
if (tagRemovalCursorActiveRef.current === active) { if (tagRemovalCursorActiveRef.current === active) {
return; return;
@@ -222,7 +221,7 @@ const useDocumentsWorkspace = ({
const asset = await fetchAsset(id); const asset = await fetchAsset(id);
return (asset as unknown) as any; return (asset as unknown) as any;
}; };
assetManagerRef.current = new AssetManager({ fetchAsset: fetcher, assetPresignTtlMs: ASSET_PRESIGN_TTL_MS }); assetManagerRef.current = new AssetManager({ fetchAsset: fetcher });
} }
const assetManager = assetManagerRef.current; const assetManager = assetManagerRef.current;
@@ -425,8 +424,8 @@ const useDocumentsWorkspace = ({
showingSearchResults showingSearchResults
? [] ? []
: currentSubfolders : currentSubfolders
.map((folder) => resolveFolderRowKey(folder.id)) .map((folder) => resolveFolderRowKey(folder.id))
.filter(Boolean), .filter(Boolean),
[showingSearchResults, currentSubfolders], [showingSearchResults, currentSubfolders],
); );
@@ -846,7 +845,7 @@ const useDocumentsWorkspace = ({
const initializeAfterLogin = useCallback(async () => { const initializeAfterLogin = useCallback(async () => {
await Promise.all([refreshTags(), refreshCorrespondents()]); await Promise.all([refreshTags(), refreshCorrespondents()]);
const initialFolder = routeFolderId && routeFolderId !== 'root' ? routeFolderId : 'root'; const initialFolder = routeFolderId && routeFolderId !== 'root' ? routeFolderId : 'root';
await loadFolder(initialFolder, {} ); await loadFolder(initialFolder, {});
}, [refreshTags, refreshCorrespondents, routeFolderId, loadFolder]); }, [refreshTags, refreshCorrespondents, routeFolderId, loadFolder]);
useEffect(() => { useEffect(() => {
@@ -960,12 +959,6 @@ const useDocumentsWorkspace = ({
[assetManager, updateDocumentCaches, notifyApiError], [assetManager, updateDocumentCaches, notifyApiError],
); );
const handleDocumentTagDrop = useCallback( const handleDocumentTagDrop = useCallback(
async (documentId, tag) => { async (documentId, tag) => {
if (!documentId || !tag?.id) { if (!documentId || !tag?.id) {
@@ -1176,7 +1169,6 @@ const useDocumentsWorkspace = ({
correspondents, correspondents,
handleCorrespondentAdd, handleCorrespondentAdd,
handleCorrespondentRemove, handleCorrespondentRemove,
resolveApiPath,
selectFolder, selectFolder,
tags, tags,
tagLookupById, tagLookupById,
@@ -1462,5 +1454,4 @@ const useDocumentsWorkspace = ({
}; };
}; };
export default useDocumentsWorkspace; export default useDocumentsWorkspace;
@@ -8,7 +8,7 @@ import {
isFolderRowKey, isFolderRowKey,
resolveDocumentRowKey, resolveDocumentRowKey,
resolveFolderRowKey, resolveFolderRowKey,
} from '../../app/appLayoutUtils'; } from '../../app/workspaceUtils';
type Identifier = string | number; type Identifier = string | number;
type FolderId = Identifier | 'root'; type FolderId = Identifier | 'root';
@@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { DragEvent } from 'react'; import type { DragEvent } from 'react';
import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/appLayoutUtils'; import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/workspaceUtils';
import { import {
createFolder, createFolder,
deleteFolder, deleteFolder,
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { DEFAULT_FOLDER_NAME } from '../../app/appLayoutUtils'; import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils';
type Identifier = string | number; type Identifier = string | number;
type FolderId = Identifier | 'root'; type FolderId = Identifier | 'root';
+54 -58
View File
@@ -62,7 +62,6 @@ interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
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>;
resolveApiPath?: (path: string) => string;
notifyApiError?: (error: unknown, fallbackMessage?: string) => void; notifyApiError?: (error: unknown, fallbackMessage?: string) => void;
sidebarToggle?: ReactNode; sidebarToggle?: ReactNode;
onClosePanel?: () => void; onClosePanel?: () => void;
@@ -131,7 +130,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
ensurePreviewData, ensurePreviewData,
resolveApiPath,
notifyApiError, notifyApiError,
sidebarToggle = null, sidebarToggle = null,
onClosePanel, onClosePanel,
@@ -264,7 +262,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
return null; return null;
} }
const downloadUrl = document.current_version?.download?.url; const downloadUrl = document.current_version?.download?.url;
const href = resolveApiPath ? resolveApiPath(downloadUrl) : downloadUrl; const href = downloadUrl;
if (!href) { if (!href) {
return null; return null;
} }
@@ -275,7 +273,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
mimeType, mimeType,
filename, filename,
}; };
}, [document, resolveApiPath]); }, [document]);
const handleZoomOpen = useCallback(() => { const handleZoomOpen = useCallback(() => {
if (!resolvedDocumentLink?.url) { if (!resolvedDocumentLink?.url) {
@@ -311,18 +309,16 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
() => () =>
document document
? createDocumentActionState({ ? createDocumentActionState({
document, document,
resolveApiPath, ensurePreviewData,
ensurePreviewData, ensureAssetUrl,
ensureAssetUrl, getDocumentAsset,
getDocumentAsset, notifyApiError,
notifyApiError, ocrErrorMessage: 'Unable to open OCR text.',
ocrErrorMessage: 'Unable to open OCR text.', })
})
: null, : null,
[ [
document, document,
resolveApiPath,
ensurePreviewData, ensurePreviewData,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
@@ -337,8 +333,8 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
const folderSegments = resolveFolderPath(document.folder_id); const folderSegments = resolveFolderPath(document.folder_id);
const normalizedSegments = Array.isArray(folderSegments) const normalizedSegments = Array.isArray(folderSegments)
? folderSegments ? folderSegments
.filter((segment) => segment && segment.id && segment.name) .filter((segment) => segment && segment.id && segment.name)
.map((segment) => ({ id: segment.id, name: segment.name })) .map((segment) => ({ id: segment.id, name: segment.name }))
: []; : [];
return [ return [
@@ -380,62 +376,62 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
const collapseButton = isSidebarVariant && onCollapsePanel const collapseButton = isSidebarVariant && onCollapsePanel
? ( ? (
<button <button
type="button" type="button"
className="icon-button" className="icon-button"
onClick={() => onCollapsePanel?.()} onClick={() => onCollapsePanel?.()}
aria-label="Close detail panel" aria-label="Close detail panel"
title="Close detail panel" title="Close detail panel"
> >
<IconX /> <IconX />
</button> </button>
) )
: null; : null;
const maximizeButton = isSidebarVariant && onMaximizePanel const maximizeButton = isSidebarVariant && onMaximizePanel
? ( ? (
<button <button
type="button" type="button"
className="icon-button" className="icon-button"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
const targetId = document?.id; const targetId = document?.id;
if (targetId == null) { if (targetId == null) {
return; return;
} }
onMaximizePanel?.({ documentIds: [targetId] }); onMaximizePanel?.({ documentIds: [targetId] });
}} }}
aria-label="Maximize" aria-label="Maximize"
title="Maximize" title="Maximize"
> >
<WindowMaximizeIcon className="icon--flip-y" /> <WindowMaximizeIcon className="icon--flip-y" />
</button> </button>
) )
: null; : null;
const closeButton = !isSidebarVariant && onClosePanel const closeButton = !isSidebarVariant && onClosePanel
? ( ? (
<button <button
type="button" type="button"
className="icon-button" className="icon-button"
onClick={() => onClosePanel?.()} onClick={() => onClosePanel?.()}
aria-label="Close preview" aria-label="Close preview"
title="Close preview" title="Close preview"
> >
<CloseIcon /> <CloseIcon />
</button> </button>
) )
: null; : null;
const headerLeadingButtons = isSidebarVariant const headerLeadingButtons = isSidebarVariant
? [ ? [
collapseButton ? <React.Fragment key="collapse-button">{collapseButton}</React.Fragment> : null, collapseButton ? <React.Fragment key="collapse-button">{collapseButton}</React.Fragment> : null,
maximizeButton ? <React.Fragment key="maximize-button">{maximizeButton}</React.Fragment> : null, maximizeButton ? <React.Fragment key="maximize-button">{maximizeButton}</React.Fragment> : null,
].filter(Boolean) ].filter(Boolean)
: [ : [
sidebarToggle ? <React.Fragment key="sidebar-toggle">{sidebarToggle}</React.Fragment> : null, sidebarToggle ? <React.Fragment key="sidebar-toggle">{sidebarToggle}</React.Fragment> : null,
closeButton ? <React.Fragment key="close-button">{closeButton}</React.Fragment> : null, closeButton ? <React.Fragment key="close-button">{closeButton}</React.Fragment> : null,
].filter(Boolean); ].filter(Boolean);
const headerLeadingContent = headerLeadingButtons.length ? headerLeadingButtons : null; const headerLeadingContent = headerLeadingButtons.length ? headerLeadingButtons : null;
const resizeHandle = isSidebarVariant ? ( const resizeHandle = isSidebarVariant ? (
+1 -1
View File
@@ -1,6 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { DragEvent } from 'react'; import type { DragEvent } from 'react';
import { TAG_FILTER_UNTAGGED } from '../app/appLayoutUtils'; import { TAG_FILTER_UNTAGGED } from '../app/workspaceUtils';
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore'; import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
type Identifier = string | number; type Identifier = string | number;