refactor: Consolidate workspace utilities into new workspaceUtils.ts with typed entry keys, replacing appLayoutUtils.ts, and update related components.
This commit is contained in:
@@ -22,7 +22,6 @@ type EnsureAssetUrl = (
|
||||
|
||||
type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
|
||||
type GetDocumentAsset = (document: unknown, assetType: string) => unknown;
|
||||
type ResolveApiPath = (path: string) => string;
|
||||
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
|
||||
|
||||
interface DocumentsTableProps {
|
||||
@@ -43,7 +42,6 @@ interface DocumentsRouteAppShell {
|
||||
previewDocumentId?: Identifier | null;
|
||||
closeDocumentPreview?: () => void;
|
||||
ensurePreviewData?: EnsurePreviewData;
|
||||
resolveApiPath?: ResolveApiPath;
|
||||
ensureAssetUrl?: EnsureAssetUrl;
|
||||
getDocumentAsset?: GetDocumentAsset;
|
||||
notifyApiError?: NotifyApiError;
|
||||
@@ -63,7 +61,6 @@ const DocumentsRouteContent: React.FC = () => {
|
||||
previewDocumentId,
|
||||
closeDocumentPreview,
|
||||
ensurePreviewData,
|
||||
resolveApiPath,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
notifyApiError,
|
||||
@@ -118,7 +115,6 @@ const DocumentsRouteContent: React.FC = () => {
|
||||
ensureAssetUrl,
|
||||
ensurePreviewData,
|
||||
getDocumentAsset,
|
||||
resolveApiPath,
|
||||
notifyApiError,
|
||||
closeDocumentPreview,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
DEFAULT_SORT_DIRECTION,
|
||||
DEFAULT_SORT_FIELD,
|
||||
SORT_FIELD_VALUES,
|
||||
} from './appLayoutUtils';
|
||||
} from './workspaceUtils';
|
||||
|
||||
const VIEW_MODE_STORAGE_KEY = 'papercrate_view_mode';
|
||||
const SORT_FIELD_STORAGE_KEY = 'papercrate_sort_field';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } 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';
|
||||
|
||||
type Identifier = string | number;
|
||||
|
||||
@@ -11,7 +11,6 @@ type Identifier = string | number;
|
||||
type EnsureAssetUrl = (docId: Identifier, asset: unknown, options?: Record<string, unknown>) => Promise<unknown> | void;
|
||||
type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
|
||||
type GetDocumentAsset = (document: unknown, assetType: string) => unknown;
|
||||
type ResolveApiPath = (path: string) => string;
|
||||
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
|
||||
|
||||
type WorkspaceSurface = { content: ReactNode; detail?: ReactNode | null } | null;
|
||||
@@ -28,7 +27,6 @@ interface UseWorkspaceSurfaceArgs {
|
||||
ensureAssetUrl?: EnsureAssetUrl;
|
||||
ensurePreviewData?: EnsurePreviewData;
|
||||
getDocumentAsset?: GetDocumentAsset;
|
||||
resolveApiPath?: ResolveApiPath;
|
||||
notifyApiError?: NotifyApiError;
|
||||
closeDocumentPreview?: () => void;
|
||||
}
|
||||
@@ -49,7 +47,6 @@ export const useWorkspaceSurface = ({
|
||||
ensureAssetUrl,
|
||||
ensurePreviewData,
|
||||
getDocumentAsset,
|
||||
resolveApiPath,
|
||||
notifyApiError,
|
||||
closeDocumentPreview,
|
||||
}: UseWorkspaceSurfaceArgs): UseWorkspaceSurfaceResult => {
|
||||
@@ -92,34 +89,34 @@ export const useWorkspaceSurface = ({
|
||||
|
||||
const detail = detailPanelOpen && detailPanelProps
|
||||
? (() => {
|
||||
const {
|
||||
onClose,
|
||||
onOpenPreview,
|
||||
tags: tagOptions,
|
||||
folderNodes,
|
||||
ensureFolderData,
|
||||
...restDetailProps
|
||||
} = detailPanelProps;
|
||||
const viewer = (
|
||||
<DocumentViewerPanel
|
||||
variant="sidebar"
|
||||
onCollapsePanel={onClose}
|
||||
onMaximizePanel={onOpenPreview}
|
||||
tagOptions={tagOptions}
|
||||
{...restDetailProps}
|
||||
/>
|
||||
);
|
||||
if (folderNodes && ensureFolderData) {
|
||||
return (
|
||||
<FolderManagerProvider folderNodes={folderNodes} ensureFolderData={ensureFolderData}>
|
||||
{viewer}
|
||||
</FolderManagerProvider>
|
||||
);
|
||||
}
|
||||
const {
|
||||
onClose,
|
||||
onOpenPreview,
|
||||
tags: tagOptions,
|
||||
folderNodes,
|
||||
ensureFolderData,
|
||||
...restDetailProps
|
||||
} = detailPanelProps;
|
||||
const viewer = (
|
||||
<DocumentViewerPanel
|
||||
variant="sidebar"
|
||||
onCollapsePanel={onClose}
|
||||
onMaximizePanel={onOpenPreview}
|
||||
tagOptions={tagOptions}
|
||||
{...restDetailProps}
|
||||
/>
|
||||
);
|
||||
if (folderNodes && ensureFolderData) {
|
||||
return (
|
||||
<>{viewer}</>
|
||||
<FolderManagerProvider folderNodes={folderNodes} ensureFolderData={ensureFolderData}>
|
||||
{viewer}
|
||||
</FolderManagerProvider>
|
||||
);
|
||||
})()
|
||||
}
|
||||
return (
|
||||
<>{viewer}</>
|
||||
);
|
||||
})()
|
||||
: null;
|
||||
|
||||
return {
|
||||
@@ -179,7 +176,6 @@ export const useWorkspaceSurface = ({
|
||||
ensureAssetUrl={ensureAssetUrl}
|
||||
getDocumentAsset={getDocumentAsset}
|
||||
ensurePreviewData={ensurePreviewData}
|
||||
resolveApiPath={resolveApiPath}
|
||||
notifyApiError={notifyApiError}
|
||||
sidebarToggle={sidebarToggle}
|
||||
onClosePanel={closeDocumentPreview}
|
||||
@@ -189,10 +185,10 @@ export const useWorkspaceSurface = ({
|
||||
|
||||
const content = folderNodes && ensureFolderData
|
||||
? (
|
||||
<FolderManagerProvider folderNodes={folderNodes} ensureFolderData={ensureFolderData}>
|
||||
{viewer}
|
||||
</FolderManagerProvider>
|
||||
)
|
||||
<FolderManagerProvider folderNodes={folderNodes} ensureFolderData={ensureFolderData}>
|
||||
{viewer}
|
||||
</FolderManagerProvider>
|
||||
)
|
||||
: viewer;
|
||||
|
||||
return { content, detail: null };
|
||||
@@ -203,7 +199,6 @@ export const useWorkspaceSurface = ({
|
||||
ensurePreviewData,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
resolveApiPath,
|
||||
notifyApiError,
|
||||
renderSidebarToggle,
|
||||
closeDocumentPreview,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -42,7 +42,7 @@ export const resolveAssetUrl = (asset?: { download?: { url: string } | null } |
|
||||
export type EnsureAssetUrl = (
|
||||
documentId: Identifier,
|
||||
asset: AssetLike,
|
||||
options?: { force?: boolean; [key: string]: unknown },
|
||||
options?: { force?: boolean;[key: string]: unknown },
|
||||
) => Promise<unknown>;
|
||||
|
||||
export type GetAsset = (document: DocumentLike, assetType: string) => Nullable<AssetLike>;
|
||||
@@ -171,7 +171,7 @@ export const resolveDocumentAssetUrl = (
|
||||
}: {
|
||||
ensureAssetUrl?: EnsureAssetUrl;
|
||||
getAsset?: GetAsset;
|
||||
ensureOptions?: { force?: boolean; [key: string]: unknown };
|
||||
ensureOptions?: { force?: boolean;[key: string]: unknown };
|
||||
} = {},
|
||||
): Nullable<string> => {
|
||||
if (!doc || !type) {
|
||||
@@ -191,24 +191,23 @@ export const resolveDocumentAssetUrl = (
|
||||
}
|
||||
if (doc.id && asset.id && ensureAssetUrl) {
|
||||
const force = Boolean(url && expiresAt && expiresAt <= now);
|
||||
const options: { force: boolean; [key: string]: unknown } = {
|
||||
const options: { force: boolean;[key: string]: unknown } = {
|
||||
force,
|
||||
...(ensureOptions || {}),
|
||||
};
|
||||
ensureAssetUrl(doc.id, asset, options).catch(() => {});
|
||||
ensureAssetUrl(doc.id, asset, options).catch(() => { });
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
class AssetManager {
|
||||
fetchAsset: ((id: Identifier) => Promise<AssetLike | null>) | null;
|
||||
assetPresignTtlMs: number;
|
||||
|
||||
assetCache: Map<Identifier, AssetLike>;
|
||||
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.assetPresignTtlMs = assetPresignTtlMs;
|
||||
this.assetCache = new Map();
|
||||
this.assetInflight = new Map();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DEFAULT_FOLDER_NAME,
|
||||
getRowId,
|
||||
isDocumentRowKey,
|
||||
} from '../app/appLayoutUtils';
|
||||
} from '../app/workspaceUtils';
|
||||
import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel';
|
||||
import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr';
|
||||
|
||||
@@ -46,7 +46,6 @@ interface UseDetailWorkspaceArgs {
|
||||
correspondents?: unknown[];
|
||||
handleCorrespondentAdd?: (...args: unknown[]) => void;
|
||||
handleCorrespondentRemove?: (...args: unknown[]) => void;
|
||||
resolveApiPath?: (path: string) => string;
|
||||
selectFolder?: (folderId?: Identifier | 'root') => void;
|
||||
tags?: unknown[];
|
||||
tagLookupById?: Map<Identifier, unknown> | null;
|
||||
@@ -86,7 +85,6 @@ const useDetailWorkspace = ({
|
||||
correspondents,
|
||||
handleCorrespondentAdd,
|
||||
handleCorrespondentRemove,
|
||||
resolveApiPath,
|
||||
selectFolder,
|
||||
tags,
|
||||
tagLookupById,
|
||||
@@ -281,7 +279,6 @@ const useDetailWorkspace = ({
|
||||
correspondents,
|
||||
onCorrespondentAdd: handleCorrespondentAdd,
|
||||
onCorrespondentRemove: handleCorrespondentRemove,
|
||||
resolveApiPath,
|
||||
onFolderNavigate: selectFolder,
|
||||
onClose: handleDetailPanelClose,
|
||||
resolveFolderPath,
|
||||
@@ -304,7 +301,6 @@ const useDetailWorkspace = ({
|
||||
folderNodes,
|
||||
ensureFolderData,
|
||||
openDocumentPreview,
|
||||
resolveApiPath,
|
||||
resolveFolderPath,
|
||||
selectFolder,
|
||||
tags,
|
||||
|
||||
@@ -6,13 +6,11 @@ import type {
|
||||
DocumentLike as OcrDocumentLike,
|
||||
} from '../utils/ocr';
|
||||
|
||||
type ResolveApiPath = (path: string) => string;
|
||||
|
||||
export type DocumentLike = OcrDocumentLike;
|
||||
|
||||
const asyncFalse = async () => false;
|
||||
|
||||
const resolveDocumentDownloadHref = (document?: DocumentLike | null, resolveApiPath?: ResolveApiPath | null): string | null => {
|
||||
const resolveDocumentDownloadHref = (document?: DocumentLike | null): string | null => {
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
@@ -20,7 +18,7 @@ const resolveDocumentDownloadHref = (document?: DocumentLike | null, resolveApiP
|
||||
if (!downloadUrl) {
|
||||
return null;
|
||||
}
|
||||
return resolveApiPath ? resolveApiPath(downloadUrl) : downloadUrl;
|
||||
return downloadUrl;
|
||||
};
|
||||
|
||||
const hasDocumentOcrAsset = (document?: DocumentLike | null, getDocumentAsset?: GetDocumentAsset | null): boolean => {
|
||||
@@ -32,7 +30,6 @@ const hasDocumentOcrAsset = (document?: DocumentLike | null, getDocumentAsset?:
|
||||
|
||||
interface CreateDocumentActionStateArgs {
|
||||
document: DocumentLike | null;
|
||||
resolveApiPath?: ResolveApiPath | null;
|
||||
ensurePreviewData: EnsurePreviewData;
|
||||
ensureAssetUrl: EnsureAssetUrl;
|
||||
getDocumentAsset?: GetDocumentAsset | null;
|
||||
@@ -42,7 +39,6 @@ interface CreateDocumentActionStateArgs {
|
||||
|
||||
export const createDocumentActionState = ({
|
||||
document,
|
||||
resolveApiPath,
|
||||
ensurePreviewData,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
@@ -57,27 +53,27 @@ export const createDocumentActionState = ({
|
||||
};
|
||||
}
|
||||
|
||||
const downloadHref = resolveDocumentDownloadHref(document, resolveApiPath);
|
||||
const downloadHref = resolveDocumentDownloadHref(document);
|
||||
const hasOcr = hasDocumentOcrAsset(document, getDocumentAsset);
|
||||
|
||||
const openOcr = hasOcr
|
||||
? async () => {
|
||||
try {
|
||||
const success = await openOcrTextInNewTab({
|
||||
document,
|
||||
ensurePreviewData,
|
||||
getDocumentAsset,
|
||||
ensureAssetUrl,
|
||||
});
|
||||
if (!success) {
|
||||
notifyApiError?.(new Error('OCR text URL unavailable.'), ocrErrorMessage);
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
notifyApiError?.(error, ocrErrorMessage);
|
||||
throw error;
|
||||
try {
|
||||
const success = await openOcrTextInNewTab({
|
||||
document,
|
||||
ensurePreviewData,
|
||||
getDocumentAsset,
|
||||
ensureAssetUrl,
|
||||
});
|
||||
if (!success) {
|
||||
notifyApiError?.(new Error('OCR text URL unavailable.'), ocrErrorMessage);
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
notifyApiError?.(error, ocrErrorMessage);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
: asyncFalse;
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { formatFileSize } from '../utils/format';
|
||||
import { formatDateTime as defaultFormatDateTime } from '../utils/date';
|
||||
import { DEFAULT_FOLDER_NAME } from '../app/appLayoutUtils';
|
||||
import { DEFAULT_FOLDER_NAME } from '../app/workspaceUtils';
|
||||
|
||||
interface DocumentPageMetadata {
|
||||
page_count?: number | string | null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } 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 {
|
||||
addDocumentTags,
|
||||
createTag,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
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';
|
||||
|
||||
type Identifier = string | number;
|
||||
@@ -87,9 +87,9 @@ const mapFilesToEntries = (filesInput?: FileList | File[] | null): FileEntry[] =
|
||||
const relativePath = (file as File & { webkitRelativePath?: string })?.webkitRelativePath ?? '';
|
||||
const segments = relativePath
|
||||
? relativePath
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.filter(Boolean)
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
return { file, segments };
|
||||
});
|
||||
@@ -325,9 +325,9 @@ const useDocumentUploads = ({
|
||||
const relativePath = (fileFromItem as File & { webkitRelativePath?: string })?.webkitRelativePath ?? '';
|
||||
const segments = relativePath
|
||||
? relativePath
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.filter(Boolean)
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
pushFile(fileFromItem, segments);
|
||||
}
|
||||
@@ -355,9 +355,9 @@ const useDocumentUploads = ({
|
||||
const relativePath = (file as File & { webkitRelativePath?: string })?.webkitRelativePath ?? '';
|
||||
const segments = relativePath
|
||||
? relativePath
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.filter(Boolean)
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
pushFile(file, segments);
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ import useDocumentsPanelProps from '../../documents/hooks/useDocumentsPanelProps
|
||||
import useDocumentPreview from '../../app/useDocumentPreview';
|
||||
import useSidebarProps from '../../sidebar/useSidebarProps';
|
||||
import {
|
||||
ASSET_PRESIGN_TTL_MS,
|
||||
|
||||
DEFAULT_SORT_DIRECTION,
|
||||
DEFAULT_SORT_FIELD,
|
||||
createRootNode,
|
||||
@@ -37,10 +37,9 @@ import {
|
||||
isDocumentRowKey,
|
||||
isFolderRowKey,
|
||||
mergeAssetIntoDocument,
|
||||
resolveApiPath,
|
||||
resolveDocumentRowKey,
|
||||
resolveFolderRowKey,
|
||||
} from '../../app/appLayoutUtils';
|
||||
} from '../../app/workspaceUtils';
|
||||
import useDocumentsSearch from '../../app/useDocumentsSearch';
|
||||
import useDocumentsStore from './store/useDocumentsStore';
|
||||
import useAuthManager from './useAuthManager';
|
||||
@@ -64,7 +63,7 @@ const EntryType = Object.freeze({
|
||||
folder: 'folder',
|
||||
});
|
||||
|
||||
const noop = () => {};
|
||||
const noop = () => { };
|
||||
|
||||
type Identifier = string | number;
|
||||
type DocumentId = Identifier;
|
||||
@@ -79,7 +78,7 @@ interface DocumentLike {
|
||||
interface FolderContentsEntry {
|
||||
folder?: { id?: FolderId; name?: string | null } | null;
|
||||
documents?: DocumentLike[];
|
||||
subfolders?: Array<{ id?: FolderId; name?: string | null; [key: string]: unknown }>;
|
||||
subfolders?: Array<{ id?: FolderId; name?: string | null;[key: string]: unknown }>;
|
||||
__includesDocuments?: boolean;
|
||||
__sortField?: string | null;
|
||||
__sortDirection?: string | null;
|
||||
@@ -189,7 +188,7 @@ const useDocumentsWorkspace = ({
|
||||
const breadcrumbFetchRef = useRef(new Set());
|
||||
const tagRemovalCursorActiveRef = useRef(false);
|
||||
const tenantIdRef = useRef(currentTenantId);
|
||||
const detailPanelControlRef = useRef({ open: () => {}, close: () => {} });
|
||||
const detailPanelControlRef = useRef({ open: () => { }, close: () => { } });
|
||||
const setTagRemovalCursor = useCallback((active) => {
|
||||
if (tagRemovalCursorActiveRef.current === active) {
|
||||
return;
|
||||
@@ -222,7 +221,7 @@ const useDocumentsWorkspace = ({
|
||||
const asset = await fetchAsset(id);
|
||||
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;
|
||||
|
||||
@@ -328,7 +327,7 @@ const useDocumentsWorkspace = ({
|
||||
() => documentsManager.getSnapshot(),
|
||||
() => documentsManager.getSnapshot(),
|
||||
);
|
||||
|
||||
|
||||
const {
|
||||
folderNodes,
|
||||
setFolderNodes,
|
||||
@@ -425,8 +424,8 @@ const useDocumentsWorkspace = ({
|
||||
showingSearchResults
|
||||
? []
|
||||
: currentSubfolders
|
||||
.map((folder) => resolveFolderRowKey(folder.id))
|
||||
.filter(Boolean),
|
||||
.map((folder) => resolveFolderRowKey(folder.id))
|
||||
.filter(Boolean),
|
||||
[showingSearchResults, currentSubfolders],
|
||||
);
|
||||
|
||||
@@ -846,7 +845,7 @@ const useDocumentsWorkspace = ({
|
||||
const initializeAfterLogin = useCallback(async () => {
|
||||
await Promise.all([refreshTags(), refreshCorrespondents()]);
|
||||
const initialFolder = routeFolderId && routeFolderId !== 'root' ? routeFolderId : 'root';
|
||||
await loadFolder(initialFolder, {} );
|
||||
await loadFolder(initialFolder, {});
|
||||
}, [refreshTags, refreshCorrespondents, routeFolderId, loadFolder]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -960,12 +959,6 @@ const useDocumentsWorkspace = ({
|
||||
[assetManager, updateDocumentCaches, notifyApiError],
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleDocumentTagDrop = useCallback(
|
||||
async (documentId, tag) => {
|
||||
if (!documentId || !tag?.id) {
|
||||
@@ -1176,7 +1169,6 @@ const useDocumentsWorkspace = ({
|
||||
correspondents,
|
||||
handleCorrespondentAdd,
|
||||
handleCorrespondentRemove,
|
||||
resolveApiPath,
|
||||
selectFolder,
|
||||
tags,
|
||||
tagLookupById,
|
||||
@@ -1462,5 +1454,4 @@ const useDocumentsWorkspace = ({
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export default useDocumentsWorkspace;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
isFolderRowKey,
|
||||
resolveDocumentRowKey,
|
||||
resolveFolderRowKey,
|
||||
} from '../../app/appLayoutUtils';
|
||||
} from '../../app/workspaceUtils';
|
||||
|
||||
type Identifier = string | number;
|
||||
type FolderId = Identifier | 'root';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { DragEvent } from 'react';
|
||||
import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/appLayoutUtils';
|
||||
import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/workspaceUtils';
|
||||
import {
|
||||
createFolder,
|
||||
deleteFolder,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 FolderId = Identifier | 'root';
|
||||
|
||||
@@ -62,7 +62,6 @@ interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
|
||||
ensureAssetUrl?: (docId: string | number, asset: AssetLike, options?: { force?: boolean }) => Promise<unknown>;
|
||||
getDocumentAsset?: (doc: DocumentLike | null, type: string) => AssetLike | null;
|
||||
ensurePreviewData?: (docId: string | number, options?: { signal?: AbortSignal }) => Promise<DocumentLike | null>;
|
||||
resolveApiPath?: (path: string) => string;
|
||||
notifyApiError?: (error: unknown, fallbackMessage?: string) => void;
|
||||
sidebarToggle?: ReactNode;
|
||||
onClosePanel?: () => void;
|
||||
@@ -131,7 +130,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
ensurePreviewData,
|
||||
resolveApiPath,
|
||||
notifyApiError,
|
||||
sidebarToggle = null,
|
||||
onClosePanel,
|
||||
@@ -264,7 +262,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
return null;
|
||||
}
|
||||
const downloadUrl = document.current_version?.download?.url;
|
||||
const href = resolveApiPath ? resolveApiPath(downloadUrl) : downloadUrl;
|
||||
const href = downloadUrl;
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
@@ -275,7 +273,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
mimeType,
|
||||
filename,
|
||||
};
|
||||
}, [document, resolveApiPath]);
|
||||
}, [document]);
|
||||
|
||||
const handleZoomOpen = useCallback(() => {
|
||||
if (!resolvedDocumentLink?.url) {
|
||||
@@ -311,18 +309,16 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
() =>
|
||||
document
|
||||
? createDocumentActionState({
|
||||
document,
|
||||
resolveApiPath,
|
||||
ensurePreviewData,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
notifyApiError,
|
||||
ocrErrorMessage: 'Unable to open OCR text.',
|
||||
})
|
||||
document,
|
||||
ensurePreviewData,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
notifyApiError,
|
||||
ocrErrorMessage: 'Unable to open OCR text.',
|
||||
})
|
||||
: null,
|
||||
[
|
||||
document,
|
||||
resolveApiPath,
|
||||
ensurePreviewData,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
@@ -337,8 +333,8 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
const folderSegments = resolveFolderPath(document.folder_id);
|
||||
const normalizedSegments = Array.isArray(folderSegments)
|
||||
? folderSegments
|
||||
.filter((segment) => segment && segment.id && segment.name)
|
||||
.map((segment) => ({ id: segment.id, name: segment.name }))
|
||||
.filter((segment) => segment && segment.id && segment.name)
|
||||
.map((segment) => ({ id: segment.id, name: segment.name }))
|
||||
: [];
|
||||
|
||||
return [
|
||||
@@ -380,62 +376,62 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
||||
|
||||
const collapseButton = isSidebarVariant && onCollapsePanel
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={() => onCollapsePanel?.()}
|
||||
aria-label="Close detail panel"
|
||||
title="Close detail panel"
|
||||
>
|
||||
<IconX />
|
||||
</button>
|
||||
)
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={() => onCollapsePanel?.()}
|
||||
aria-label="Close detail panel"
|
||||
title="Close detail panel"
|
||||
>
|
||||
<IconX />
|
||||
</button>
|
||||
)
|
||||
: null;
|
||||
|
||||
const maximizeButton = isSidebarVariant && onMaximizePanel
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const targetId = document?.id;
|
||||
if (targetId == null) {
|
||||
return;
|
||||
}
|
||||
onMaximizePanel?.({ documentIds: [targetId] });
|
||||
}}
|
||||
aria-label="Maximize"
|
||||
title="Maximize"
|
||||
>
|
||||
<WindowMaximizeIcon className="icon--flip-y" />
|
||||
</button>
|
||||
)
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const targetId = document?.id;
|
||||
if (targetId == null) {
|
||||
return;
|
||||
}
|
||||
onMaximizePanel?.({ documentIds: [targetId] });
|
||||
}}
|
||||
aria-label="Maximize"
|
||||
title="Maximize"
|
||||
>
|
||||
<WindowMaximizeIcon className="icon--flip-y" />
|
||||
</button>
|
||||
)
|
||||
: null;
|
||||
|
||||
const closeButton = !isSidebarVariant && onClosePanel
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={() => onClosePanel?.()}
|
||||
aria-label="Close preview"
|
||||
title="Close preview"
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
)
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={() => onClosePanel?.()}
|
||||
aria-label="Close preview"
|
||||
title="Close preview"
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
)
|
||||
: null;
|
||||
|
||||
const headerLeadingButtons = isSidebarVariant
|
||||
? [
|
||||
collapseButton ? <React.Fragment key="collapse-button">{collapseButton}</React.Fragment> : null,
|
||||
maximizeButton ? <React.Fragment key="maximize-button">{maximizeButton}</React.Fragment> : null,
|
||||
].filter(Boolean)
|
||||
collapseButton ? <React.Fragment key="collapse-button">{collapseButton}</React.Fragment> : null,
|
||||
maximizeButton ? <React.Fragment key="maximize-button">{maximizeButton}</React.Fragment> : null,
|
||||
].filter(Boolean)
|
||||
: [
|
||||
sidebarToggle ? <React.Fragment key="sidebar-toggle">{sidebarToggle}</React.Fragment> : null,
|
||||
closeButton ? <React.Fragment key="close-button">{closeButton}</React.Fragment> : null,
|
||||
].filter(Boolean);
|
||||
sidebarToggle ? <React.Fragment key="sidebar-toggle">{sidebarToggle}</React.Fragment> : null,
|
||||
closeButton ? <React.Fragment key="close-button">{closeButton}</React.Fragment> : null,
|
||||
].filter(Boolean);
|
||||
const headerLeadingContent = headerLeadingButtons.length ? headerLeadingButtons : null;
|
||||
|
||||
const resizeHandle = isSidebarVariant ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } 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';
|
||||
|
||||
type Identifier = string | number;
|
||||
|
||||
Reference in New Issue
Block a user