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 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,
});
-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_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 -1
View File
@@ -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;
+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 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,
+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,
});