From 6d55e61cee8299fe8d03f294e529deb64aeca3df Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Thu, 13 Nov 2025 02:37:16 +0100 Subject: [PATCH] typescript --- frontend/src/app/DocumentsRoute.tsx | 97 +++++++--- frontend/src/app/LoginRoute.tsx | 20 ++- frontend/src/app/useManagementModals.tsx | 44 ++++- frontend/src/app/useWorkspaceSurface.tsx | 2 - frontend/src/assets/logo.svg | 74 +------- frontend/src/assets/logo_lines.svg | 75 ++++++++ frontend/src/desktop/DesktopWorkspace.tsx | 15 +- frontend/src/desktop/createDesktopSurface.tsx | 2 - frontend/src/desktop/events.ts | 7 +- frontend/src/desktop/pointer/pointerUtils.ts | 14 +- frontend/src/desktop/useDocumentDrag.ts | 9 +- frontend/src/detail/useDetailWorkspace.ts | 5 +- frontend/src/documents/DocumentInfoPanel.tsx | 4 +- .../src/documents/DocumentSummarySection.tsx | 35 +++- .../src/documents/DocumentThumbnailImage.tsx | 36 ++-- frontend/src/documents/DocumentsGrid.tsx | 12 +- frontend/src/documents/DocumentsList.tsx | 12 +- .../documents/SelectionFloatingActions.tsx | 134 +++++++------- frontend/src/documents/documentActions.ts | 20 +-- .../documents/hooks/useDocumentsPanelProps.ts | 2 +- .../src/documents/panel/DocumentsPanel.tsx | 8 +- .../panel/createDocumentsSurface.tsx | 2 - frontend/src/documents/tagTransfer.ts | 28 ++- .../useDocumentCorrespondentActions.ts | 25 ++- .../documents/useDocumentDragHandlers.ts | 49 +++-- .../hooks/documents/useDocumentMutations.ts | 12 +- .../src/hooks/documents/useDocumentTagging.ts | 14 +- .../src/hooks/documents/useDocumentUploads.ts | 36 ++-- .../hooks/documents/useDocumentsWorkspace.ts | 115 ++++++++++-- frontend/src/hooks/documents/useFileDrop.ts | 6 +- frontend/src/hooks/documents/useFolderTree.ts | 169 +++++++++++++----- .../src/hooks/documents/useTenantManager.ts | 2 +- frontend/src/index.tsx | 2 +- frontend/src/preview/DocumentViewerPanel.tsx | 2 +- frontend/src/preview/useViewerLayoutMode.ts | 2 +- frontend/src/routes/DocumentViewerRoute.tsx | 14 +- frontend/src/settings/useCapabilitySets.ts | 61 +++++-- frontend/src/settings/usePasskeys.ts | 8 +- frontend/src/styles/documents/listing.css | 90 +++++++++- frontend/src/styles/sidebar/sidebar.css | 1 + frontend/src/ui/BreadcrumbTrail.tsx | 16 +- frontend/src/utils/ocr.ts | 31 ++-- frontend/src/utils/webauthn.ts | 17 +- 43 files changed, 923 insertions(+), 406 deletions(-) create mode 100644 frontend/src/assets/logo_lines.svg diff --git a/frontend/src/app/DocumentsRoute.tsx b/frontend/src/app/DocumentsRoute.tsx index 0a5dbda..fc0f1e1 100644 --- a/frontend/src/app/DocumentsRoute.tsx +++ b/frontend/src/app/DocumentsRoute.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo } from 'react'; +import type { ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAppShell } from '../appShellContext'; import DocumentsLayout from './DocumentsLayout'; @@ -8,7 +9,45 @@ import BreadcrumbTrail from '../ui/BreadcrumbTrail'; import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext'; import { PanelManagerProvider, usePanelManager } from './PanelManagerContext'; -type Breadcrumb = { id?: string | number; name?: string; label?: string; title?: string }; +type Identifier = string | number; + +type Breadcrumb = { id?: Identifier; name?: string; label?: string; title?: string }; + +type EnsureAssetUrl = ( + docId: Identifier, + asset: unknown, + options?: Record, +) => Promise | void; + +type EnsurePreviewData = (docId: Identifier, options?: Record) => Promise; +type GetDocumentAsset = (document: unknown, assetType: string) => unknown; +type ResolveApiPath = (path: string) => string; +type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; + +interface DocumentsTableProps { + breadcrumbs?: Breadcrumb[] | null; + [key: string]: unknown; +} + +interface DocumentsRouteAppShell { + sidebarProps?: Record | null; + documentsTableProps?: DocumentsTableProps | null; + detailPanelProps?: Record | null; + detailPanelOpen?: boolean; + documentsViewMode?: string; + deskWorkspaceProps?: Record | null; + openTagsModal?: () => void; + openCorrespondentsModal?: () => void; + previewWorkspaceDocument?: unknown; + previewWorkspaceEntry?: unknown; + previewDocumentId?: Identifier | null; + closeDocumentPreview?: () => void; + ensurePreviewData?: EnsurePreviewData; + resolveApiPath?: ResolveApiPath; + ensureAssetUrl?: EnsureAssetUrl; + getDocumentAsset?: GetDocumentAsset; + notifyApiError?: NotifyApiError; +} const DocumentsRouteContent: React.FC = () => { const { @@ -29,7 +68,7 @@ const DocumentsRouteContent: React.FC = () => { ensureAssetUrl, getDocumentAsset, notifyApiError, - } = useAppShell(); + } = useAppShell() as DocumentsRouteAppShell; const navigate = useNavigate(); const { collapsed: sidebarCollapsed } = useSidebarContext(); const { @@ -37,18 +76,25 @@ const DocumentsRouteContent: React.FC = () => { expandSidebar, } = usePanelManager(); + const safeSidebarProps = useMemo>( + () => (sidebarProps && typeof sidebarProps === 'object' ? sidebarProps : {}), + [sidebarProps], + ); + const sidebarPropsWithActions = useMemo( () => ({ - ...sidebarProps, + ...safeSidebarProps, onManageTags: openTagsModal, onManageCorrespondents: openCorrespondentsModal, }), - [sidebarProps, openTagsModal, openCorrespondentsModal], + [safeSidebarProps, openTagsModal, openCorrespondentsModal], ); const sidebarHidden = sidebarCollapsed || sidebarSuppressed; - const breadcrumbs = documentsTableProps?.breadcrumbs || null; + const breadcrumbs = Array.isArray(documentsTableProps?.breadcrumbs) + ? documentsTableProps?.breadcrumbs + : null; const parentBreadcrumb = useMemo(() => { if (!Array.isArray(breadcrumbs) || breadcrumbs.length <= 1) { return null; @@ -114,11 +160,14 @@ const DocumentsRouteContent: React.FC = () => { const variant = surface.variant || 'documents'; + const surfaceDetail = (surface as { detail?: ReactNode }).detail || null; + const hasDetail = Boolean(surfaceDetail); + const mainContentClass = `main-content main-content--${variant}${ - surface.detail ? ' main-content--has-detail' : '' + hasDetail ? ' main-content--has-detail' : '' }`; const bodyClass = `main-content__body main-content__body--${variant}${ - surface.detail ? ' main-content__body--has-detail' : '' + hasDetail ? ' main-content__body--has-detail' : '' }`; const header = surface.header || null; @@ -153,26 +202,30 @@ const DocumentsRouteContent: React.FC = () => {
{header ? ( -
+ <> +
+ +
{(header.selectionLabel || header.floatingActions) ? ( -
- {header.selectionLabel ? ( - {header.selectionLabel} - ) : null} - {header.floatingActions || null} +
+
+ {header.selectionLabel ? ( + {header.selectionLabel} + ) : null} + {header.floatingActions || null} +
) : null} - -
+ ) : null}
{surface.content}
- {surface.detail || null} + {surfaceDetail}
); diff --git a/frontend/src/app/LoginRoute.tsx b/frontend/src/app/LoginRoute.tsx index 68d6c42..df75310 100644 --- a/frontend/src/app/LoginRoute.tsx +++ b/frontend/src/app/LoginRoute.tsx @@ -221,13 +221,18 @@ const LoginRoute: React.FC = () => { const publicKey = preparePublicKeyRequestOptions({ publicKey: publicKeyOptions }); setStatusMessage('Confirm the passkey prompt to continue.', 'info'); - const assertion = await navigator.credentials.get({ publicKey }); + const assertion = await navigator.credentials.get({ publicKey }) as PublicKeyCredential | null; if (!assertion) { setStatusMessage('Passkey login cancelled.', 'info'); return; } + if (!(assertion instanceof PublicKeyCredential)) { + setStatusMessage('Unexpected credential response.', 'error'); + return; + } + const serialized = serializeAuthenticationCredential(assertion); const finishPayload = { challengeId, @@ -303,7 +308,11 @@ const LoginRoute: React.FC = () => { setStatusMessage('Signing you in…', 'info'); try { - const payload = { + const payload: { + magic_token: string; + username?: string; + preferred_tenant_id?: string | number; + } = { magic_token: magicToken, }; if (magicUsername) { @@ -419,13 +428,18 @@ const LoginRoute: React.FC = () => { const publicKey = preparePublicKeyCreationOptions({ publicKey: publicKeyOptions }); setStatusMessage('Confirm the passkey prompt to finish creating your account.', 'info'); - const credential = await navigator.credentials.create({ publicKey }); + const credential = await navigator.credentials.create({ publicKey }) as PublicKeyCredential | null; if (!credential) { setStatusMessage('Signup cancelled.', 'info'); return; } + if (!(credential instanceof PublicKeyCredential)) { + setStatusMessage('Unexpected credential response.', 'error'); + return; + } + const serialized = serializeRegistrationCredential(credential); const finishPayload = { signup_token: signupToken, diff --git a/frontend/src/app/useManagementModals.tsx b/frontend/src/app/useManagementModals.tsx index 319c635..3eb8207 100644 --- a/frontend/src/app/useManagementModals.tsx +++ b/frontend/src/app/useManagementModals.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ReactNode } from 'react'; import TagsPanel from '../tags/TagsPanel'; -import CorrespondentsPanel from '../correspondents/CorrespondentsPanel'; +import CorrespondentsPanel, { CorrespondentsPanelProps } from '../correspondents/CorrespondentsPanel'; import PanelHeader from '../ui/PanelHeader'; const TAGS_MODAL = 'tags'; @@ -135,6 +135,36 @@ export const useManagementModals = ({ tags, ]); + const handleCorrespondentCreateSafe = useCallback( + async (payload) => { + if (typeof onCorrespondentCreate !== 'function') { + return undefined; + } + return onCorrespondentCreate(payload) ?? undefined; + }, + [onCorrespondentCreate], + ); + + const handleCorrespondentUpdateSafe = useCallback( + async (id, payload) => { + if (typeof onCorrespondentUpdate !== 'function') { + return; + } + await onCorrespondentUpdate(id, payload); + }, + [onCorrespondentUpdate], + ); + + const handleCorrespondentDeleteSafe = useCallback( + async (id) => { + if (typeof onCorrespondentDelete !== 'function') { + return; + } + await onCorrespondentDelete(id); + }, + [onCorrespondentDelete], + ); + const correspondentsModal = useMemo(() => { if (activeModal !== CORRESPONDENTS_MODAL) { return null; @@ -167,9 +197,9 @@ export const useManagementModals = ({
@@ -180,9 +210,9 @@ export const useManagementModals = ({ activeModal, closeActiveModal, correspondents, - onCorrespondentCreate, - onCorrespondentDelete, - onCorrespondentUpdate, + handleCorrespondentCreateSafe, + handleCorrespondentDeleteSafe, + handleCorrespondentUpdateSafe, refreshCorrespondents, setStatusMessage, ]); diff --git a/frontend/src/app/useWorkspaceSurface.tsx b/frontend/src/app/useWorkspaceSurface.tsx index aba8ed6..1f11933 100644 --- a/frontend/src/app/useWorkspaceSurface.tsx +++ b/frontend/src/app/useWorkspaceSurface.tsx @@ -140,7 +140,6 @@ export const useWorkspaceSurface = ({ onUpdateTitle, onUpdateIssued, resolveFolderPath, - onFolderNavigate, } = detailExtras; return createDocumentViewerSurface({ document: previewWorkspaceDocument, @@ -162,7 +161,6 @@ export const useWorkspaceSurface = ({ onUpdateTitle, onUpdateIssued, resolveFolderPath, - onFolderNavigate, }); }, [ showPreviewWorkspace, diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg index 41b9561..8746aec 100644 --- a/frontend/src/assets/logo.svg +++ b/frontend/src/assets/logo.svg @@ -1,72 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/frontend/src/assets/logo_lines.svg b/frontend/src/assets/logo_lines.svg new file mode 100644 index 0000000..d1919dd --- /dev/null +++ b/frontend/src/assets/logo_lines.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx index fe29760..0405fd5 100644 --- a/frontend/src/desktop/DesktopWorkspace.tsx +++ b/frontend/src/desktop/DesktopWorkspace.tsx @@ -51,6 +51,13 @@ interface NavigatorSnapshot { height?: number | null; } +type OverlayOriginHint = { + rotation?: number; + scale?: number; + width?: number; + height?: number; +}; + interface OverlayOriginTransform { rotation: number; scaleX: number; @@ -180,7 +187,7 @@ interface DesktopWorkspaceViewProps { bringToFront: (docId: Identifier | null | undefined) => void; setDraggingId: (value: string | null) => void; canvasSize: { width: number; height: number }; - openOverlayForDoc: (docId: Identifier | null | undefined, originInfo?: OverlayOriginTransform | null) => void; + openOverlayForDoc: (docId: Identifier | null | undefined, originInfo?: OverlayOriginHint | null) => void; recalcVisibleDocIds: () => void; dragSettings: DragSettings; onInspectDocument?: DesktopWorkspaceProps['onInspectDocument']; @@ -340,7 +347,7 @@ const DesktopWorkspace: React.FC = ({ if (existing && existing.width === normalized.width && existing.height === normalized.height) { return; } - const next = new Map(docSizeMapRef.current); + const next = new Map(docSizeMapRef.current); next.set(docKey, { ...normalized, source: 'snapshot' }); docSizeMapRef.current = next; setDocSizeVersion((value) => value + 1); @@ -505,7 +512,7 @@ const DesktopWorkspace: React.FC = ({ useEffect(() => { const current = docSizeMapRef.current; - const next = new Map(current); + const next = new Map(current); const itemKeys = new Set(items.filter((doc) => doc?.id != null).map((doc) => String(doc.id))); let changed = false; @@ -618,7 +625,7 @@ const DesktopWorkspace: React.FC = ({ }, [draggingId, items, setDraggingId]); const openOverlayForDoc = useCallback( - (docId: Identifier | null | undefined, originInfo: OverlayOriginTransform | null = null) => { + (docId: Identifier | null | undefined, originInfo: OverlayOriginHint | null = null) => { if (!docId) { return; } diff --git a/frontend/src/desktop/createDesktopSurface.tsx b/frontend/src/desktop/createDesktopSurface.tsx index a16fb9e..73554a9 100644 --- a/frontend/src/desktop/createDesktopSurface.tsx +++ b/frontend/src/desktop/createDesktopSurface.tsx @@ -126,8 +126,6 @@ const createDesktopSurface = ({ title, subtitle, sidebarToggle, - parentBreadcrumb, - onNavigateParent, actions, breadcrumbs: workspaceProps?.breadcrumbs || null, selectionLabel: null, diff --git a/frontend/src/desktop/events.ts b/frontend/src/desktop/events.ts index c1ece50..25419cb 100644 --- a/frontend/src/desktop/events.ts +++ b/frontend/src/desktop/events.ts @@ -21,8 +21,11 @@ export const preventAll = (event?: PreventableEvent | null): void => { type AnyFn = (...args: unknown[]) => unknown; -export const safeInvoke = (fn: Fn | null | undefined, ...args: Parameters): ReturnType | undefined => - (fn ? fn(...args) : undefined); +export const safeInvoke = ( + fn: Fn | null | undefined, + ...args: Parameters +): ReturnType | undefined => + (fn ? (fn(...args) as ReturnType) : undefined); export const getPointerPosition = ( event?: PointerLikeEvent | null, diff --git a/frontend/src/desktop/pointer/pointerUtils.ts b/frontend/src/desktop/pointer/pointerUtils.ts index 4068a1f..ef4e7de 100644 --- a/frontend/src/desktop/pointer/pointerUtils.ts +++ b/frontend/src/desktop/pointer/pointerUtils.ts @@ -65,8 +65,8 @@ export const createPointerIntent = ({ const alreadySelected = selectedDocumentIds.includes(doc.id); const selectionCount = Array.isArray(selectedDocumentIds) ? selectedDocumentIds.length : 0; - let clickAction = CLICK_ACTIONS.none; - let dragAction = DRAG_ACTIONS.none; + let clickAction: ClickAction = CLICK_ACTIONS.none; + let dragAction: DragAction = DRAG_ACTIONS.none; if (metaKey) { clickAction = CLICK_ACTIONS.addStack; @@ -79,8 +79,8 @@ export const createPointerIntent = ({ dragAction = DRAG_ACTIONS.dragSelectSingle; } - const stackList = Array.isArray(stackHits) && stackHits.length > 0 - ? stackHits.slice() + const stackList: string[] = Array.isArray(stackHits) && stackHits.length > 0 + ? stackHits.map((value) => String(value)) : [String(doc.id)]; const stackDocIdsForDrag = dragAction === DRAG_ACTIONS.dragSelectStack ? stackList : null; @@ -160,9 +160,9 @@ export const applyLongPressSelection = ({ intent, stackDocIds, syntheticEvent, o return; } - const stackCopy = Array.isArray(stackDocIds) && stackDocIds.length > 0 - ? stackDocIds.slice() - : [intent.docId]; + const stackCopy: string[] = Array.isArray(stackDocIds) && stackDocIds.length > 0 + ? stackDocIds.map((value) => String(value)) + : [String(intent.docId)]; safeInvoke(onDocumentStackSelect, stackCopy, syntheticEvent, { replace: true }); diff --git a/frontend/src/desktop/useDocumentDrag.ts b/frontend/src/desktop/useDocumentDrag.ts index 9da034f..11dbe6b 100644 --- a/frontend/src/desktop/useDocumentDrag.ts +++ b/frontend/src/desktop/useDocumentDrag.ts @@ -56,6 +56,7 @@ interface DragGroupItemInternal extends EngineGroupItem { offsetX?: number; offsetY?: number; targetRotation?: number; + initialRotation?: number; } type EnsureDocumentSizeFn = (doc: DocumentLike | null | undefined) => DocumentSizeInfo | null; @@ -466,9 +467,9 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { baseOffsetY, offsetX: baseOffsetX, offsetY: baseOffsetY, - initialRotation, + targetRotation: initialRotation, displayRotation: initialRotation, - targetRotation, + } satisfies DragGroupItemInternal; }); @@ -897,6 +898,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { if (state.moved) { commitActiveDragTransforms([state.docKey]); const inertiaState: EngineInertiaState = { + docId: state.docKey, restRotation: state.restRotation, dynamicRotation: state.dynamicRotation, angularVelocity: state.angularVelocity, @@ -904,6 +906,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { width: state.width, height: state.height, dragScale: state.dragScale || 1, + lastTimestamp: state.lastTimestamp, }; const docId = state.docKey; finishDrag(event.pointerId); @@ -956,6 +959,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { commitActiveDragTransforms([state.docKey]); const inertiaState: EngineInertiaState = { + docId: state.docKey, restRotation: state.restRotation, dynamicRotation: state.dynamicRotation, angularVelocity: state.angularVelocity, @@ -963,6 +967,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { width: state.width, height: state.height, dragScale: state.dragScale || 1, + lastTimestamp: state.lastTimestamp, }; const docId = state.docKey; finishDrag(event.pointerId); diff --git a/frontend/src/detail/useDetailWorkspace.ts b/frontend/src/detail/useDetailWorkspace.ts index df6354e..d4fb063 100644 --- a/frontend/src/detail/useDetailWorkspace.ts +++ b/frontend/src/detail/useDetailWorkspace.ts @@ -8,6 +8,7 @@ import { isDocumentRowKey, } from '../app/appLayoutUtils'; import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel'; +import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr'; type Identifier = string | number; @@ -51,8 +52,8 @@ interface UseDetailWorkspaceArgs { handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise | boolean; handleDocumentTagAdd?: (doc: DocumentLike, value: string, context?: { option?: unknown }) => void; handleTagRemove?: (...args: unknown[]) => void; - ensureAssetUrl?: (...args: unknown[]) => unknown; - getDocumentAsset?: (...args: unknown[]) => unknown; + ensureAssetUrl?: EnsureAssetUrl; + getDocumentAsset?: GetDocumentAsset; ensurePreviewData?: (docId: Identifier, options?: Record) => Promise; correspondents?: unknown[]; handleCorrespondentAdd?: (...args: unknown[]) => void; diff --git a/frontend/src/documents/DocumentInfoPanel.tsx b/frontend/src/documents/DocumentInfoPanel.tsx index 8cd4416..f359f3d 100644 --- a/frontend/src/documents/DocumentInfoPanel.tsx +++ b/frontend/src/documents/DocumentInfoPanel.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from 'react'; import DocumentSummarySection, { DocumentSummarySectionProps } from './DocumentSummarySection'; import { buildDocumentMetadataItems, extractDocumentMetadataPayload } from './documentMetadata'; -type PanelTab = { id: string; label: string; render: () => ReactNode }; +type PanelTab = { id: string; label: string; render: (context?: Record) => ReactNode }; type ContentState = | { status: 'idle'; data: null; error: null } @@ -13,7 +13,7 @@ type ContentState = | { status: 'unavailable'; data: null; error: null } | { status: 'error'; data: null; error: unknown }; -interface DocumentInfoPanelProps { +export interface DocumentInfoPanelProps { document: DocumentSummarySectionProps['document']; summaryProps?: Omit; metadataItems?: Array<{ label: string; value?: string }>; diff --git a/frontend/src/documents/DocumentSummarySection.tsx b/frontend/src/documents/DocumentSummarySection.tsx index dd4ff37..2dd8230 100644 --- a/frontend/src/documents/DocumentSummarySection.tsx +++ b/frontend/src/documents/DocumentSummarySection.tsx @@ -110,6 +110,31 @@ interface QuickAddEntry { original: QuickAddOption | string; } +const resolveOptionName = (source: unknown): string => { + if (!source) { + return ''; + } + if (typeof source === 'string') { + return source.trim(); + } + if (typeof source === 'object') { + const candidate = source as { name?: string; label?: string; trim?: () => string }; + if (typeof candidate.name === 'string' && candidate.name.trim()) { + return candidate.name.trim(); + } + if (typeof candidate.label === 'string' && candidate.label.trim()) { + return candidate.label.trim(); + } + if (typeof candidate.trim === 'function') { + const viaTrim = candidate.trim(); + if (typeof viaTrim === 'string' && viaTrim.trim()) { + return viaTrim.trim(); + } + } + } + return ''; +}; + const normalizeQuickAddOption = (option: QuickAddOption | string | null | undefined): QuickAddEntry | null => { if (option == null) { return null; @@ -348,16 +373,12 @@ export const CorrespondentSection: React.FC = ({ return; } const source = item.payload ?? item; - const resolvedName = - source?.name?.trim?.() - || source?.label?.trim?.() - || source?.trim?.() - || ''; + const resolvedName = resolveOptionName(source); if (!resolvedName) { return; } - const payload = typeof source === 'object' - ? { ...source, name: resolvedName } + const payload = (source && typeof source === 'object') + ? { ...(source as Record), name: resolvedName } : { id: null, name: resolvedName }; onAdd({ name: resolvedName, option: payload, input: null }); }, diff --git a/frontend/src/documents/DocumentThumbnailImage.tsx b/frontend/src/documents/DocumentThumbnailImage.tsx index 93db57b..9ed0217 100644 --- a/frontend/src/documents/DocumentThumbnailImage.tsx +++ b/frontend/src/documents/DocumentThumbnailImage.tsx @@ -1,6 +1,16 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import type { CSSProperties, JSX, MutableRefObject } from 'react'; -import { getAssetFromVersion, resolveDocumentAssetUrl, createAssetView } from '../asset_manager'; +import { + getAssetFromVersion, + resolveDocumentAssetUrl, + createAssetView, +} from '../asset_manager'; +import type { + DocumentLike as AssetManagerDocumentLike, + AssetLike as AssetManagerAssetLike, + EnsureAssetUrl as AssetManagerEnsureAssetUrl, + GetAsset as AssetManagerGetAsset, +} from '../asset_manager'; const DEFAULT_THUMBNAIL_SIZE = 48; @@ -74,26 +84,10 @@ interface DocumentVersionLike { [key: string]: unknown; } -interface DocumentLike { - id?: Identifier; - current_version?: DocumentVersionLike; - [key: string]: unknown; -} - -interface AssetLike { - id?: Identifier; - url?: string | null; - metadata?: Record | null; - [key: string]: unknown; -} - -type EnsureAssetUrl = ( - documentId: Identifier, - asset: AssetLike, - options?: { start?: number; limit?: number; [key: string]: unknown }, -) => Promise | void; - -type GetDocumentAsset = (document: DocumentLike | null | undefined, assetType: string) => AssetLike | null | undefined; +type DocumentLike = AssetManagerDocumentLike; +type AssetLike = AssetManagerAssetLike; +type EnsureAssetUrl = AssetManagerEnsureAssetUrl; +type GetDocumentAsset = AssetManagerGetAsset; interface DocumentThumbnailImageProps { document?: DocumentLike | null; diff --git a/frontend/src/documents/DocumentsGrid.tsx b/frontend/src/documents/DocumentsGrid.tsx index c7f6af5..3f06fc3 100644 --- a/frontend/src/documents/DocumentsGrid.tsx +++ b/frontend/src/documents/DocumentsGrid.tsx @@ -127,9 +127,9 @@ const DocumentsGrid: React.FC = ({ submitEditing: submitDocumentEditing, savingId: savingDocumentId, attachInputRef: attachDocumentInputRef, - } = useInlineRename(onDocumentRename, { - getCurrentValue: (doc) => doc?.title ?? '', - getEntityId: (doc) => doc?.id ?? null, + } = useInlineRename(onDocumentRename, { + getCurrentValue: (doc: DocumentLike) => doc?.title ?? '', + getEntityId: (doc: DocumentLike) => doc?.id ?? null, }); const { @@ -141,9 +141,9 @@ const DocumentsGrid: React.FC = ({ submitEditing: submitFolderEditing, savingId: savingFolderId, attachInputRef: attachFolderInputRef, - } = useInlineRename(onFolderRename, { - getCurrentValue: (folder) => folder?.name ?? '', - getEntityId: (folder) => folder?.id ?? null, + } = useInlineRename(onFolderRename, { + getCurrentValue: (folder: FolderLike) => folder?.name ?? '', + getEntityId: (folder: FolderLike) => folder?.id ?? null, }); const documentSelectionCount = selectedDocumentIdsSet?.size ?? 0; diff --git a/frontend/src/documents/DocumentsList.tsx b/frontend/src/documents/DocumentsList.tsx index 10d3238..d725e1e 100644 --- a/frontend/src/documents/DocumentsList.tsx +++ b/frontend/src/documents/DocumentsList.tsx @@ -132,9 +132,9 @@ const DocumentsList: React.FC = ({ submitEditing: submitDocumentEditing, savingId: savingDocumentId, attachInputRef: attachDocumentInputRef, - } = useInlineRename(onDocumentRename, { - getCurrentValue: (doc) => doc?.title ?? '', - getEntityId: (doc) => doc?.id ?? null, + } = useInlineRename(onDocumentRename, { + getCurrentValue: (doc: DocumentLike) => doc?.title ?? '', + getEntityId: (doc: DocumentLike) => doc?.id ?? null, }); const { @@ -146,9 +146,9 @@ const DocumentsList: React.FC = ({ submitEditing: submitFolderEditing, savingId: savingFolderId, attachInputRef: attachFolderInputRef, - } = useInlineRename(onFolderRename, { - getCurrentValue: (folder) => folder?.name ?? '', - getEntityId: (folder) => folder?.id ?? null, + } = useInlineRename(onFolderRename, { + getCurrentValue: (folder: FolderLike) => folder?.name ?? '', + getEntityId: (folder: FolderLike) => folder?.id ?? null, }); diff --git a/frontend/src/documents/SelectionFloatingActions.tsx b/frontend/src/documents/SelectionFloatingActions.tsx index 9d97e5e..bb4f326 100644 --- a/frontend/src/documents/SelectionFloatingActions.tsx +++ b/frontend/src/documents/SelectionFloatingActions.tsx @@ -503,38 +503,82 @@ const SelectionFloatingActions: React.FC = ({ /> ) : null; + const showPrimaryButtons = Boolean(onBulkReanalyze || onDeleteSelection || onClearSelection); + + const moveMenu = typeof onMoveDocumentsToFolder === 'function' ? ( + + {loadingFolders ? ( + + ) : ( + + )} + {' '} + Move + + )} + items={moveAssignments} + placeholder="Search folders…" + emptyMessage={loadingFolders ? 'Loading folders…' : 'No folders'} + onToggle={(item) => handleMoveSelectionToFolder(item?.payload || item)} + disabled={!documentCount || (loadingFolders && !moveAssignments.length)} + createLabel={null} + showStateIndicators={false} + showCounts={false} + onOpenMenu={handleMoveMenuOpen} + renderItemLabel={renderFolderLabel} + /> + ) : null; + + const primaryButtons = showPrimaryButtons ? ( +
+ {typeof onBulkReanalyze === 'function' ? ( + + ) : null} + {typeof onDeleteSelection === 'function' ? ( + + ) : null} + {typeof onClearSelection === 'function' ? ( + + ) : null} +
+ ) : null; + return ( <> {summaryNode ? ( {summaryNode} ) : null} -
- {typeof onMoveDocumentsToFolder === 'function' ? ( - - {loadingFolders ? ( -