From 70e7bda87e6516283be72505bfbd897625059c12 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Mon, 24 Nov 2025 23:44:35 +0100 Subject: [PATCH] refactor: introduce dedicated identifier types for improved clarity and type safety --- frontend/src/app/DocumentsRoute.tsx | 3 +- frontend/src/app/LoginRoute.tsx | 20 +- frontend/src/app/UploadQueueOverlay.tsx | 8 +- frontend/src/app/entryKey.ts | 6 +- frontend/src/app/useDetailPanel.ts | 10 +- frontend/src/app/useDocumentPreview.ts | 4 +- frontend/src/app/useDocumentSelection.ts | 13 +- frontend/src/app/useDocumentsSearch.ts | 3 +- frontend/src/app/useManagementModals.tsx | 4 +- frontend/src/app/useWorkspaceSelection.ts | 8 +- frontend/src/app/useWorkspaceSurface.tsx | 3 +- frontend/src/asset_manager.ts | 2 +- .../correspondents/CorrespondentsPanel.tsx | 10 +- frontend/src/desktop/DesktopDocumentCard.tsx | 17 +- frontend/src/desktop/DesktopPreviewCard.tsx | 5 +- frontend/src/desktop/DesktopWorkspace.tsx | 15 +- frontend/src/desktop/db.ts | 36 +- .../src/desktop/hooks/usePreviewMetadata.ts | 9 +- frontend/src/desktop/pointer/pointerUtils.ts | 7 +- frontend/src/desktop/useDocumentDrag.ts | 357 +++++----- frontend/src/desktop/workspaceEngine.ts | 65 +- frontend/src/detail/PreviewZoomOverlay.tsx | 66 +- frontend/src/detail/useDetailWorkspace.ts | 3 +- frontend/src/documents/CorrespondentLinks.tsx | 8 +- frontend/src/documents/DocumentInfoPanel.tsx | 12 +- .../src/documents/DocumentSummarySection.tsx | 13 +- .../src/documents/DocumentThumbnailImage.tsx | 2 +- frontend/src/documents/DocumentsGrid.tsx | 639 +++++++++-------- frontend/src/documents/DocumentsList.tsx | 673 +++++++++--------- frontend/src/documents/DocumentsManager.ts | 3 +- .../src/documents/SelectionAssignmentMenu.tsx | 12 +- .../documents/SelectionFloatingActions.tsx | 8 +- .../context/DocumentsFilterContext.tsx | 5 +- frontend/src/documents/correspondents.ts | 8 +- .../documents/hooks/useBulkDocumentActions.ts | 143 ++-- .../documents/hooks/useDocumentsPanelProps.ts | 7 +- .../documents/hooks/useDocumentsSelection.ts | 25 +- .../src/documents/panel/DocumentsPanel.tsx | 12 +- .../documents/panel/DocumentsPanelHeader.tsx | 15 +- frontend/src/documents/tagTransfer.ts | 21 +- frontend/src/documents/useEntryPointer.ts | 6 +- frontend/src/documents/useInlineRename.ts | 16 +- frontend/src/folders/FolderManagerContext.tsx | 2 +- .../src/hooks/documents/useCorrespondents.ts | 8 +- .../useDocumentCorrespondentActions.ts | 7 +- .../documents/useDocumentDragHandlers.ts | 6 +- .../hooks/documents/useDocumentMutations.ts | 4 +- .../src/hooks/documents/useDocumentTagging.ts | 95 ++- .../src/hooks/documents/useDocumentUploads.ts | 2 +- frontend/src/hooks/documents/useDocuments.ts | 3 +- .../hooks/documents/useDocumentsWorkspace.ts | 5 +- frontend/src/hooks/documents/useFileDrop.ts | 2 +- frontend/src/hooks/documents/useFolderTree.ts | 18 +- .../hooks/documents/useFolderTreeActions.ts | 2 +- frontend/src/hooks/documents/useTags.ts | 11 +- .../src/hooks/documents/useTenantManager.ts | 9 +- .../documents/useWorkspaceBreadcrumbs.ts | 4 +- .../hooks/documents/useWorkspaceDeskProps.ts | 9 +- .../documents/useWorkspaceSelectionSync.ts | 9 +- .../hooks/documents/useWorkspaceTaxonomies.ts | 3 +- frontend/src/hooks/useAssetNavigator.ts | 7 +- frontend/src/lib/apiTypes.ts | 3 +- frontend/src/login/LoginView.tsx | 154 ++-- frontend/src/preview/DocumentViewerLayout.tsx | 16 +- frontend/src/preview/DocumentViewerPanel.tsx | 19 +- frontend/src/routes/DocumentViewerRoute.tsx | 3 +- .../components/CapabilityDropdown.tsx | 4 +- .../settings/sections/ApiTokensSection.tsx | 33 +- .../sections/CapabilitySetsSection.tsx | 5 +- .../src/settings/sections/PasskeysSection.tsx | 4 +- frontend/src/settings/useApiTokens.ts | 21 +- frontend/src/settings/useCapabilitySets.ts | 3 +- frontend/src/settings/usePasskeys.ts | 11 +- frontend/src/sidebar/Sidebar.tsx | 319 +++++---- frontend/src/sidebar/useSidebarProps.ts | 3 +- frontend/src/types/identifiers.ts | 11 + frontend/src/ui/QuickAddMenu.tsx | 6 +- frontend/src/utils/date.ts | 12 +- frontend/src/utils/ocr.ts | 4 +- 79 files changed, 1567 insertions(+), 1572 deletions(-) create mode 100644 frontend/src/types/identifiers.ts diff --git a/frontend/src/app/DocumentsRoute.tsx b/frontend/src/app/DocumentsRoute.tsx index aa47378..0142b62 100644 --- a/frontend/src/app/DocumentsRoute.tsx +++ b/frontend/src/app/DocumentsRoute.tsx @@ -11,8 +11,7 @@ import { DocumentsHeaderBreadcrumb } from '../documents/panel/DocumentsPanelHead import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext'; import { PanelManagerProvider, usePanelManager } from './PanelManagerContext'; import Sidebar from '../sidebar/Sidebar'; - -type Identifier = string | number; +import type { Identifier } from '../types/identifiers'; type EnsureAssetUrl = ( docId: Identifier, diff --git a/frontend/src/app/LoginRoute.tsx b/frontend/src/app/LoginRoute.tsx index 1915506..7f63101 100644 --- a/frontend/src/app/LoginRoute.tsx +++ b/frontend/src/app/LoginRoute.tsx @@ -28,7 +28,7 @@ interface StatusMessage { } interface TenantOption { - id?: string | number | null; + id?: string | null; name?: string | null; } @@ -321,7 +321,7 @@ const LoginRoute: React.FC = () => { const payload: { magic_token: string; username?: string; - preferred_tenant_id?: string | number; + preferred_tenant_id?: string; } = { magic_token: magicToken, }; @@ -520,14 +520,14 @@ const LoginRoute: React.FC = () => { onCancelSelection={handleCancelSelection} selectingTenantId={selectingTenantId} onPasskeyLogin={handlePasskeyLogin} - passkeySupported={passkeySupported} - passkeyLoading={passkeyLoading} - onSignup={handleSignup} - signupSupported={signupSupported} - signupLoading={signupLoading} - magicLoginPending={magicLoginPending} - initialUsername={magicLoginParams.username || ''} - /> + passkeySupported={passkeySupported} + passkeyLoading={passkeyLoading} + onSignup={handleSignup} + signupSupported={signupSupported} + signupLoading={signupLoading} + magicLoginPending={magicLoginPending} + initialUsername={magicLoginParams.username || ''} + /> ); }; diff --git a/frontend/src/app/UploadQueueOverlay.tsx b/frontend/src/app/UploadQueueOverlay.tsx index 98052e1..180f253 100644 --- a/frontend/src/app/UploadQueueOverlay.tsx +++ b/frontend/src/app/UploadQueueOverlay.tsx @@ -15,12 +15,12 @@ import PanelHeader from '../ui/PanelHeader'; type UploadStatus = 'pending' | 'uploading' | 'success' | 'duplicate' | 'error' | (string & {}); interface UploadQueueItem { - id: string | number; + id: string; name: string; status: UploadStatus; error?: string | null; - document?: { id?: string | number; title?: string }; - conflictDocumentId?: string | number; + document?: { id?: string; title?: string }; + conflictDocumentId?: string; } interface UploadQueueOverlayProps { @@ -191,7 +191,7 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp {item.error} ) : ( - {meta.label} + {meta.label} )} diff --git a/frontend/src/app/entryKey.ts b/frontend/src/app/entryKey.ts index 55bfbf6..a95d0ad 100644 --- a/frontend/src/app/entryKey.ts +++ b/frontend/src/app/entryKey.ts @@ -1,13 +1,15 @@ +import type { DocumentId, FolderId } from '../types/identifiers'; + // Entry key utilities for workspace selection // Entry keys are strings in the format "document:id" or "folder:id" const ENTRY_KEY_SEPARATOR = ':'; // Create entry key strings -export const createDocumentEntryKey = (documentId: string | number): string => +export const createDocumentEntryKey = (documentId: DocumentId): string => `document${ENTRY_KEY_SEPARATOR}${documentId}`; -export const createFolderEntryKey = (folderId: string | number): string => +export const createFolderEntryKey = (folderId: FolderId): string => `folder${ENTRY_KEY_SEPARATOR}${folderId}`; // Type guards for entry key strings diff --git a/frontend/src/app/useDetailPanel.ts b/frontend/src/app/useDetailPanel.ts index 7ad3840..671e5d0 100644 --- a/frontend/src/app/useDetailPanel.ts +++ b/frontend/src/app/useDetailPanel.ts @@ -1,19 +1,19 @@ import { useCallback, useEffect, useRef, useState } from 'react'; export interface DetailDocument { - id?: string | number; + id?: string; [key: string]: unknown; } interface UseDetailPanelOptions { - documentLookup: Map; + documentLookup: Map; orderedSelectedDocuments: DetailDocument[]; } interface OpenDetailPanelArgs { - documentId?: string | number; + documentId?: string; document?: DetailDocument | null; - documentIds?: Array; + documentIds?: Array; documents?: DetailDocument[]; } @@ -22,7 +22,7 @@ export const useDetailPanel = ({ orderedSelectedDocuments, }: UseDetailPanelOptions) => { const [detailPanelOpen, setDetailPanelOpen] = useState(false); - const [detailPanelDocId, setDetailPanelDocId] = useState(null); + const [detailPanelDocId, setDetailPanelDocId] = useState(null); const [detailPanelDocument, setDetailPanelDocument] = useState(null); const latestOrderedDocsRef = useRef([]); diff --git a/frontend/src/app/useDocumentPreview.ts b/frontend/src/app/useDocumentPreview.ts index 1048bb4..ccbd513 100644 --- a/frontend/src/app/useDocumentPreview.ts +++ b/frontend/src/app/useDocumentPreview.ts @@ -5,8 +5,8 @@ import type { SetStateAction, } from 'react'; import { fetchDocument } from '../lib/apiClient'; +import type { DocumentId } from '../types/identifiers'; -type DocumentId = string | number; type FolderId = DocumentId | 'root'; type DocumentLike = { @@ -102,7 +102,7 @@ const useDocumentPreview = ({ async (documentId: DocumentId, { force = false }: { force?: boolean } = {}): Promise => { if (!documentId) return null; - const existing = documentLinks.get(documentId); + const existing = documentLinks.get(documentId); const now = Date.now(); const expiresAt = existing?.expiresAt ?? null; if (!force && existing && (!expiresAt || expiresAt > now)) { diff --git a/frontend/src/app/useDocumentSelection.ts b/frontend/src/app/useDocumentSelection.ts index 11f9285..6005fc8 100644 --- a/frontend/src/app/useDocumentSelection.ts +++ b/frontend/src/app/useDocumentSelection.ts @@ -6,8 +6,7 @@ import { isFolderEntry, getEntryId, } from './entryKey'; - -type DocumentId = string | number; +import type { DocumentId } from '../types/identifiers'; interface SelectionEventLike { shiftKey?: boolean; @@ -21,7 +20,7 @@ interface UseDocumentSelectionOptions { } interface ApplySelectionOptions { - anchor?: string; + anchor: string | null; interactedKeys?: string[]; } @@ -35,8 +34,8 @@ export const useDocumentSelection = ({ const selectionOrderRef = useRef(initialEntries); const selectionAnchorRef = useRef(null); const selectionInitializedRef = useRef(false); - const [focusedDocumentId, setFocusedDocumentId] = useState(undefined); - const [focusedRowKey, setFocusedRowKey] = useState(undefined); + const [focusedDocumentId, setFocusedDocumentId] = useState(null); + const [focusedRowKey, setFocusedRowKey] = useState(null); const visibleRowKeySetRef = useRef>(new Set()); const navigableRowKeysRef = useRef([]); @@ -90,7 +89,7 @@ export const useDocumentSelection = ({ const applySelection = useCallback( ( rowKeys: Array, - { anchor, interactedKeys = [] }: ApplySelectionOptions = {}, + { anchor = null, interactedKeys = [] }: ApplySelectionOptions = { anchor: null, interactedKeys: [] }, ) => { const visibleRowKeySet = visibleRowKeySetRef.current; const unique: string[] = []; @@ -117,7 +116,7 @@ export const useDocumentSelection = ({ } }); - let resolvedAnchor = anchor; + let resolvedAnchor: string | null = anchor ?? null; if (resolvedAnchor && !unique.includes(resolvedAnchor)) { resolvedAnchor = null; } diff --git a/frontend/src/app/useDocumentsSearch.ts b/frontend/src/app/useDocumentsSearch.ts index be2ad34..5e024e3 100644 --- a/frontend/src/app/useDocumentsSearch.ts +++ b/frontend/src/app/useDocumentsSearch.ts @@ -2,8 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { Dispatch, SetStateAction } from 'react'; import { TAG_FILTER_UNTAGGED } from './workspaceUtils'; import { listDocuments } from '../lib/apiClient'; - -type Identifier = string | number; +import type { Identifier } from '../types/identifiers'; type DocumentLike = { id?: Identifier } & Record; diff --git a/frontend/src/app/useManagementModals.tsx b/frontend/src/app/useManagementModals.tsx index 159dfda..8f44ca2 100644 --- a/frontend/src/app/useManagementModals.tsx +++ b/frontend/src/app/useManagementModals.tsx @@ -9,13 +9,13 @@ const TAGS_MODAL = 'tags'; const CORRESPONDENTS_MODAL = 'correspondents'; interface TagRecord { - id?: string | number; + id?: string; label?: string; [key: string]: unknown; } interface CorrespondentRecord { - id?: string | number; + id?: string; name?: string; [key: string]: unknown; } diff --git a/frontend/src/app/useWorkspaceSelection.ts b/frontend/src/app/useWorkspaceSelection.ts index d53a1e3..f6811f3 100644 --- a/frontend/src/app/useWorkspaceSelection.ts +++ b/frontend/src/app/useWorkspaceSelection.ts @@ -10,8 +10,8 @@ interface SelectionEntry { } interface WorkspaceSelectionOptions { - onDocumentActivate?: (id: string | number) => void; - onInspectFolder?: (id: string | number) => void; + onDocumentActivate?: (id: string) => void; + onInspectFolder?: (id: string) => void; } const identity = (value: T) => value; @@ -71,7 +71,7 @@ export const useWorkspaceSelection = ({ ); const inspectDocument = useCallback( - (documentId?: string | number) => { + (documentId?: string) => { if (!documentId) return; onDocumentActivate(documentId); }, @@ -79,7 +79,7 @@ export const useWorkspaceSelection = ({ ); const inspectFolder = useCallback( - (folderId?: string | number) => { + (folderId?: string) => { if (!folderId) return; onInspectFolder(folderId); }, diff --git a/frontend/src/app/useWorkspaceSurface.tsx b/frontend/src/app/useWorkspaceSurface.tsx index 1a97459..4b5a819 100644 --- a/frontend/src/app/useWorkspaceSurface.tsx +++ b/frontend/src/app/useWorkspaceSurface.tsx @@ -5,8 +5,7 @@ import DocumentsPanel from '../documents/panel/DocumentsPanel'; import DocumentViewerPanel from '../preview/DocumentViewerPanel'; import { usePanelManager } from './PanelManagerContext'; import { FolderManagerProvider } from '../folders/FolderManagerContext'; - -type Identifier = string | number; +import type { Identifier } from '../types/identifiers'; type EnsureAssetUrl = (docId: Identifier, asset: unknown, options?: Record) => Promise | void; type EnsurePreviewData = (docId: Identifier, options?: Record) => Promise; diff --git a/frontend/src/asset_manager.ts b/frontend/src/asset_manager.ts index 1b207be..5899fc1 100644 --- a/frontend/src/asset_manager.ts +++ b/frontend/src/asset_manager.ts @@ -1,4 +1,4 @@ -export type Identifier = string | number; +import type { Identifier } from './types/identifiers'; type Nullable = T | null; diff --git a/frontend/src/correspondents/CorrespondentsPanel.tsx b/frontend/src/correspondents/CorrespondentsPanel.tsx index ef3f457..b8e8fa1 100644 --- a/frontend/src/correspondents/CorrespondentsPanel.tsx +++ b/frontend/src/correspondents/CorrespondentsPanel.tsx @@ -7,7 +7,7 @@ interface CorrespondentUsage { } export interface CorrespondentEntry { - id?: string | number; + id?: string; name?: string; usage?: CorrespondentUsage; [key: string]: unknown; @@ -17,8 +17,8 @@ export interface CorrespondentsPanelProps { correspondents?: CorrespondentEntry[]; onRefresh?: () => void | Promise; onCreate: (payload: { name: string }) => Promise; - onUpdate: (id: string | number, payload: { name: string }) => Promise; - onDelete: (id: string | number) => Promise; + onUpdate: (id: string, payload: { name: string }) => Promise; + onDelete: (id: string) => Promise; onNotify?: (message: string, variant?: string) => void; } @@ -30,12 +30,12 @@ function CorrespondentsPanel({ onDelete, onNotify, }: CorrespondentsPanelProps) { - const [editingId, setEditingId] = useState(null); + const [editingId, setEditingId] = useState(null); const [draftName, setDraftName] = useState(''); const [createName, setCreateName] = useState(''); const [saving, setSaving] = useState(false); const [creating, setCreating] = useState(false); - const [deletingId, setDeletingId] = useState(null); + const [deletingId, setDeletingId] = useState(null); const startEdit = useCallback((correspondent: CorrespondentEntry) => { setEditingId(correspondent.id); diff --git a/frontend/src/desktop/DesktopDocumentCard.tsx b/frontend/src/desktop/DesktopDocumentCard.tsx index 9a1504b..25c01ae 100644 --- a/frontend/src/desktop/DesktopDocumentCard.tsx +++ b/frontend/src/desktop/DesktopDocumentCard.tsx @@ -3,17 +3,18 @@ import DesktopPreviewCard from './DesktopPreviewCard'; import { resolveCorrespondents } from '../documents/correspondents'; import { getTagColorStyle } from '../utils/colors'; import { preventAll } from './events'; +import type { DocumentId } from '../types/identifiers'; type DocumentLike = { - id?: string | number; + id?: string; title?: string; - tags?: Array<{ id?: string | number; label?: string; color?: string | null }>; + tags?: Array<{ id?: string; label?: string; color?: string | null }>; [key: string]: unknown; }; interface PendingRemovalTag { - docId?: string | number; - tagId?: string | number; + docId?: string; + tagId?: string; } interface DesktopDocumentCardProps { @@ -30,10 +31,10 @@ interface DesktopDocumentCardProps { getDocumentAsset?: (...args: any[]) => unknown; handleNavigatorSnapshot?: (...args: any[]) => void; cardPointerHandlers?: React.HTMLAttributes; - onDocumentActivate?: (id: string | number) => void; - onTagDragEnter?: (event: React.DragEvent, docId: string | number) => void; - onTagDragOver?: (event: React.DragEvent, docId: string | number) => void; - onTagDragLeave?: (event: React.DragEvent, docId: string | number) => void; + onDocumentActivate?: (id: string) => void; + onTagDragEnter?: (event: React.DragEvent, docId: DocumentId) => void; + onTagDragOver?: (event: React.DragEvent, docId: DocumentId) => void; + onTagDragLeave?: (event: React.DragEvent, docId: DocumentId) => void; onTagDrop?: (event: React.DragEvent, doc: DocumentLike) => void; onDocTagPointerDown?: (event: React.PointerEvent, doc: DocumentLike, tag: any) => void; onDocTagDragStart?: (event: React.DragEvent, doc: DocumentLike, tag: any) => void; diff --git a/frontend/src/desktop/DesktopPreviewCard.tsx b/frontend/src/desktop/DesktopPreviewCard.tsx index 12c1501..9f73ec8 100644 --- a/frontend/src/desktop/DesktopPreviewCard.tsx +++ b/frontend/src/desktop/DesktopPreviewCard.tsx @@ -1,8 +1,7 @@ import { useEffect } from 'react'; import type { JSX } from 'react'; import { useAssetNavigator } from '../hooks/useAssetNavigator'; - -type Identifier = string | number; +import type { Identifier } from '../types/identifiers'; interface DocumentLike { id?: Identifier; @@ -21,7 +20,7 @@ interface AssetLike { type EnsureAssetUrl = ( documentId: Identifier, asset: AssetLike, - options?: { force?: boolean; [key: string]: unknown }, + options?: { force?: boolean;[key: string]: unknown }, ) => Promise; type GetDocumentAsset = (document: DocumentLike | null, assetType: string) => AssetLike | null; diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx index 49b74b2..7a95b93 100644 --- a/frontend/src/desktop/DesktopWorkspace.tsx +++ b/frontend/src/desktop/DesktopWorkspace.tsx @@ -29,8 +29,7 @@ import '../styles/workspace/workspace-layout.css'; import '../styles/workspace/workspace-items.css'; import '../styles/workspace/workspace-cards.css'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; - -type Identifier = string | number; +import type { DocumentId, Identifier } from '../types/identifiers'; type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null; type DocumentLinkLike = { url?: string | null; mimeType?: string | null }; @@ -79,7 +78,7 @@ interface DocumentSizeInfo { } interface PreviewMetadataEntry { - docId: string; + docId: DocumentId; width: number; height: number; } @@ -225,7 +224,7 @@ const DesktopWorkspace: React.FC = ({ const syntheticEvent = event || ({ metaKey: true, ctrlKey: true, - preventDefault: () => {}, + preventDefault: () => { }, } as unknown as PointerEvent); docIds.forEach((id) => { const key = getDocRowKey(id); @@ -354,7 +353,7 @@ const DesktopWorkspace: React.FC = ({ engine.recalcVisibleDocIds(); }, [engine]); - const setDraggingId = useCallback((value: string | number | null) => { + const setDraggingId = useCallback((value: string | null) => { engine.setDraggingId(value); }, [engine]); @@ -686,7 +685,7 @@ const DesktopWorkspace: React.FC = ({ }, [resolvePreviewDimensions], ); - + useEffect(() => { if (draggingId && !items.some((doc) => String(doc.id) === draggingId)) { @@ -1072,8 +1071,8 @@ function DesktopWorkspaceView({ const dragging = docKey ? draggingId === docKey : false; const docTagKeys = Array.isArray(doc?.tags) ? doc.tags - .map((tag) => (tag?.id != null ? String(tag.id) : null)) - .filter((id): id is string => Boolean(id)) + .map((tag) => (tag?.id != null ? String(tag.id) : null)) + .filter((id): id is string => Boolean(id)) : []; const matchesFilter = activeTagSet.size === 0 || docTagKeys.some((key) => activeTagSet.has(key)); diff --git a/frontend/src/desktop/db.ts b/frontend/src/desktop/db.ts index 85ca546..d4c3116 100644 --- a/frontend/src/desktop/db.ts +++ b/frontend/src/desktop/db.ts @@ -1,3 +1,6 @@ +import type { DocumentId } from '../types/identifiers'; +type TenantId = import('../types/identifiers').TenantId; + const DB_NAME = 'papercrate_desk'; const DB_VERSION = 1; const LAYOUT_STORE = 'layouts'; @@ -107,9 +110,9 @@ const withStore = async (mode: TransactionMode, handler: (store: IDBObjectSto }; interface LayoutRecord { - tenantId: string | number; - viewId: string | number; - documentId: string | number; + tenantId: TenantId; + viewId: string; + documentId: DocumentId; centerX?: number; centerY?: number; rotation?: number; @@ -117,7 +120,13 @@ interface LayoutRecord { updatedAt?: number; } -export const fetchLayoutRecords = async ({ tenantId, viewId }: { tenantId?: string | number; viewId?: string | number }): Promise => { +export const fetchLayoutRecords = async ({ + tenantId, + viewId, +}: { + tenantId?: TenantId; + viewId?: string; +}): Promise => { if (!tenantId || !viewId) { return []; } @@ -133,7 +142,22 @@ export const fetchLayoutRecords = async ({ tenantId, viewId }: { tenantId?: stri } }; -export const upsertLayoutRecords = async ({ tenantId, viewId, entries }: { tenantId?: string | number; viewId?: string | number; entries?: Array<{ documentId?: string | number; centerX?: number; centerY?: number; rotation?: number; zIndex?: number; updatedAt?: number }> }) => { +export const upsertLayoutRecords = async ({ + tenantId, + viewId, + entries, +}: { + tenantId?: TenantId; + viewId?: string; + entries?: Array<{ + documentId?: DocumentId; + centerX?: number; + centerY?: number; + rotation?: number; + zIndex?: number; + updatedAt?: number; + }>; +}) => { if (!tenantId || !viewId || !Array.isArray(entries) || !entries.length) { return; } @@ -162,7 +186,7 @@ export const upsertLayoutRecords = async ({ tenantId, viewId, entries }: { tenan } }; -export const deleteTenantLayouts = async (tenantId?: string | number) => { +export const deleteTenantLayouts = async (tenantId?: TenantId) => { if (!tenantId) { return; } diff --git a/frontend/src/desktop/hooks/usePreviewMetadata.ts b/frontend/src/desktop/hooks/usePreviewMetadata.ts index cbb9023..8dd63e5 100644 --- a/frontend/src/desktop/hooks/usePreviewMetadata.ts +++ b/frontend/src/desktop/hooks/usePreviewMetadata.ts @@ -1,23 +1,24 @@ import { useEffect, useState } from 'react'; +import type { DocumentId } from '../../types/identifiers'; interface DocumentLike { - id?: string | number; + id?: string; current_version?: unknown; tags?: unknown; } interface AssetLike { - id?: string | number; + id?: string; [key: string]: unknown; } interface PreviewMetadataEntry { - docId: string; + docId: DocumentId; width: number; height: number; } type GetDocumentAsset = (doc: DocumentLike, type: string) => AssetLike | null; -type EnsureAssetUrl = (docId: string | number, asset: AssetLike, options?: { force?: boolean }) => Promise; +type EnsureAssetUrl = (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise; const usePreviewMetadata = ( documents: DocumentLike[] | null, diff --git a/frontend/src/desktop/pointer/pointerUtils.ts b/frontend/src/desktop/pointer/pointerUtils.ts index ef4e7de..0d1e99f 100644 --- a/frontend/src/desktop/pointer/pointerUtils.ts +++ b/frontend/src/desktop/pointer/pointerUtils.ts @@ -1,4 +1,5 @@ import { safeInvoke } from '../events'; +import type { DocumentId } from '../../types/identifiers'; export const CLICK_ACTIONS = { selectSingle: 'selectSingle', @@ -25,9 +26,9 @@ export const LONG_PRESS_DURATION_MS = 450; export const withinThreshold = (dx: number, dy: number, thresholdSquared: number): boolean => (dx * dx + dy * dy) <= thresholdSquared; interface PointerIntentArgs { - doc: { id: string | number }; + doc: { id: string }; entryDescriptor: unknown; - selectedDocumentIds: Array; + selectedDocumentIds: Array; metaKey: boolean; pointerButton?: number; pointerType?: string; @@ -35,7 +36,7 @@ interface PointerIntentArgs { } export interface PointerIntent { - docId: string | number; + docId: DocumentId; entryDescriptor: unknown; pointerType?: string; pointerButton?: number; diff --git a/frontend/src/desktop/useDocumentDrag.ts b/frontend/src/desktop/useDocumentDrag.ts index a237e40..cae7c07 100644 --- a/frontend/src/desktop/useDocumentDrag.ts +++ b/frontend/src/desktop/useDocumentDrag.ts @@ -19,8 +19,7 @@ import { applyDomTransform, type WorkspaceEngine, } from './workspaceEngine'; - -type Identifier = string | number; +import type { DocumentId, Identifier } from '../types/identifiers'; interface DocumentLike { id?: Identifier | null; @@ -121,7 +120,7 @@ interface UseDocumentDragOptions { type EngineInertiaState = Parameters[1]; interface DragStateInternal extends EngineDragState { - docId: string; + docId: DocumentId; docKey: string; pointerId: number; originCenterX: number; @@ -208,15 +207,15 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { bringToFront, setDraggingId, canvasSize, - openOverlayForDoc, - recalcVisibleDocIds, - settings, - containerRef: providedContainerRef, - onDocumentActivate, - onDocumentStackSelect, - selectedDocumentIds = [], - markLayoutDirty, -} = options; + openOverlayForDoc, + recalcVisibleDocIds, + settings, + containerRef: providedContainerRef, + onDocumentActivate, + onDocumentStackSelect, + selectedDocumentIds = [], + markLayoutDirty, + } = options; const fallbackContainerRef = useRef(null); const containerRef = providedContainerRef ?? fallbackContainerRef; @@ -237,7 +236,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { const tapHandler = usePointerTap({ delay: 220, - onSingle: () => {}, + onSingle: () => { }, onDouble: ({ data, event }) => { if (!data?.docId) { return; @@ -281,8 +280,8 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { } const keys = Array.isArray(docIds) && docIds.length ? docIds - .map((id) => (id != null ? String(id) : null)) - .filter((value): value is string => Boolean(value)) + .map((id) => (id != null ? String(id) : null)) + .filter((value): value is string => Boolean(value)) : Array.from(map.keys()); keys.forEach((key) => { const transform = map.get(key); @@ -350,8 +349,8 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { const stackDocIdsOptionRaw = options?.stackDocIds; const stackDocIdsOption = Array.isArray(stackDocIdsOptionRaw) ? stackDocIdsOptionRaw - .map((value) => (value != null ? String(value) : null)) - .filter((value): value is string => Boolean(value)) + .map((value) => (value != null ? String(value) : null)) + .filter((value): value is string => Boolean(value)) : null; const stackSelectionAppliedInitial = Boolean(options?.stackSelectionApplied); const wasSelectedAtPointerDown = Boolean(options?.wasSelected); @@ -361,8 +360,8 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { let selectionIds: string[] = Array.isArray(selectedDocumentIds) ? selectedDocumentIds - .map((id) => (id != null ? String(id) : null)) - .filter((id): id is string => Boolean(id)) + .map((id) => (id != null ? String(id) : null)) + .filter((id): id is string => Boolean(id)) : []; if (!stackDocIdsOption && !pointerModifierActive && !wasSelectedAtPointerDown) { @@ -399,7 +398,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { if (isGroupDrag) { selectionIds.forEach((id) => { if (id !== docKey) { - engine?.cancelInertiaAnimation?.(id); + engine?.cancelInertiaAnimation?.(id); } }); } @@ -491,7 +490,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { offsetY: baseOffsetY, targetRotation: initialRotation, displayRotation: initialRotation, - + } satisfies DragGroupItemInternal; }); @@ -569,28 +568,28 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { setDraggingId(docKey); - if (isGroupDrag) { - groupItems.forEach((item) => { - if (item.docId === docKey) { - return; - } - const node = itemRefs.current.get(item.docId); - if (node) { - item.displayRotation = item.initialRotation; - const itemEntry = layoutRef.current.get(item.docId) || null; - applyDomTransform(node, { - centerX: item.currentCenterX, - centerY: item.currentCenterY, - width: item.width, - height: item.height, - rotation: item.displayRotation ?? 0, - scale: 1, - zIndex: itemEntry?.z, - }); - } - }); - } - }, [ + if (isGroupDrag) { + groupItems.forEach((item) => { + if (item.docId === docKey) { + return; + } + const node = itemRefs.current.get(item.docId); + if (node) { + item.displayRotation = item.initialRotation; + const itemEntry = layoutRef.current.get(item.docId) || null; + applyDomTransform(node, { + centerX: item.currentCenterX, + centerY: item.currentCenterY, + width: item.width, + height: item.height, + rotation: item.displayRotation ?? 0, + scale: 1, + zIndex: itemEntry?.z, + }); + } + }); + } + }, [ bringToFront, canvasPadding, containerRef, @@ -674,147 +673,147 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => { state.rotation = state.restRotation + state.dynamicRotation; }; - if (state.isGroup) { - const containerRect = containerRef.current?.getBoundingClientRect?.(); - if (containerRect) { - state.containerRectLeft = containerRect.left; - state.containerRectTop = containerRect.top; - } - - const pointerCanvasX = event.clientX - state.containerRectLeft; - const pointerCanvasY = event.clientY - state.containerRectTop; - const deltaX = event.clientX - state.startX; - const deltaY = event.clientY - state.startY; - - if (!state.moved) { - const distanceSquared = deltaX * deltaX + deltaY * deltaY; - if (distanceSquared < DRAG_HYSTERESIS_SQUARED) { - return; + if (state.isGroup) { + const containerRect = containerRef.current?.getBoundingClientRect?.(); + if (containerRect) { + state.containerRectLeft = containerRect.left; + state.containerRectTop = containerRect.top; } - state.moved = true; - if ( - !state.stackSelectionApplied - && Array.isArray(state.stackDocIds) - && state.stackDocIds.length > 0 - ) { - safeInvoke(onDocumentStackSelect, state.stackDocIds as Identifier[], event, { - replace: state.stackReplace, - }); - state.stackSelectionApplied = true; - } - if (!state.groupElevated) { - const layout = layoutRef.current; - const sortedGroup = state.activeDocIds - .filter((id) => id !== state.docKey) - .sort((a, b) => { - const aZ = layout.get(a)?.z ?? 0; - const bZ = layout.get(b)?.z ?? 0; - return aZ - bZ; + + const pointerCanvasX = event.clientX - state.containerRectLeft; + const pointerCanvasY = event.clientY - state.containerRectTop; + const deltaX = event.clientX - state.startX; + const deltaY = event.clientY - state.startY; + + if (!state.moved) { + const distanceSquared = deltaX * deltaX + deltaY * deltaY; + if (distanceSquared < DRAG_HYSTERESIS_SQUARED) { + return; + } + state.moved = true; + if ( + !state.stackSelectionApplied + && Array.isArray(state.stackDocIds) + && state.stackDocIds.length > 0 + ) { + safeInvoke(onDocumentStackSelect, state.stackDocIds as Identifier[], event, { + replace: state.stackReplace, }); + state.stackSelectionApplied = true; + } + if (!state.groupElevated) { + const layout = layoutRef.current; + const sortedGroup = state.activeDocIds + .filter((id) => id !== state.docKey) + .sort((a, b) => { + const aZ = layout.get(a)?.z ?? 0; + const bZ = layout.get(b)?.z ?? 0; + return aZ - bZ; + }); - sortedGroup.forEach((id) => bringToFront(id)); - bringToFront(state.docKey); - state.groupElevated = true; + sortedGroup.forEach((id) => bringToFront(id)); + bringToFront(state.docKey); + state.groupElevated = true; + } } + + const docWidth = state.width; + const docHeight = state.height; + const halfWidth = docWidth / 2; + const halfHeight = docHeight / 2; + const canvasWidth = canvasSize.width || defaultCanvasWidth; + const canvasHeight = canvasSize.height || defaultCanvasHeight; + const minCenterX = canvasPadding + halfWidth; + const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - halfWidth); + const minCenterY = canvasPadding + halfHeight; + const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - halfHeight); + + const desiredCenterX = pointerCanvasX - state.localPointerOffsetX; + const desiredCenterY = pointerCanvasY - state.localPointerOffsetY; + const centerX = clamp(desiredCenterX, minCenterX, maxCenterX); + const centerY = clamp(desiredCenterY, minCenterY, maxCenterY); + + state.currentCenterX = centerX; + state.currentCenterY = centerY; + + state.groupItems.forEach((item) => { + const isPrimary = item.docId === state.docKey; + + if (isPrimary) { + item.currentCenterX = centerX; + item.currentCenterY = centerY; + item.offsetX = item.baseOffsetX ?? 0; + item.offsetY = item.baseOffsetY ?? 0; + item.displayRotation = state.rotation ?? item.displayRotation ?? 0; + } else { + const decay = 0.82; + const currentOffsetX = (item.offsetX ?? item.baseOffsetX ?? 0) * decay; + const currentOffsetY = (item.offsetY ?? item.baseOffsetY ?? 0) * decay; + item.offsetX = Math.abs(currentOffsetX) < 0.5 ? 0 : currentOffsetX; + item.offsetY = Math.abs(currentOffsetY) < 0.5 ? 0 : currentOffsetY; + + const targetX = centerX + item.offsetX; + const targetY = centerY + item.offsetY; + const smoothing = 0.18; + item.currentCenterX += (targetX - item.currentCenterX) * smoothing; + item.currentCenterY += (targetY - item.currentCenterY) * smoothing; + + const halfW = item.width / 2; + const halfH = item.height / 2; + const minX = canvasPadding + halfW; + const maxX = Math.max(minX, canvasWidth - canvasPadding - halfW); + const minY = canvasPadding + halfH; + const maxY = Math.max(minY, canvasHeight - canvasPadding - halfH); + item.currentCenterX = clamp(item.currentCenterX, minX, maxX); + item.currentCenterY = clamp(item.currentCenterY, minY, maxY); + + const rotationBlend = 0.16; + item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend; + } + + const entry = layoutRef.current.get(item.docId) || null; + const payload = { + centerX: item.currentCenterX, + centerY: item.currentCenterY, + rotation: item.displayRotation ?? 0, + width: item.width, + height: item.height, + scale: isPrimary ? state.dragScale || 1 : 1, + zIndex: entry?.z, + }; + + setDragTransform(item.docId, payload); + const node = itemRefs.current.get(item.docId); + applyDomTransform(node, payload); + }); + + const currentTimestampGroup = + (Number.isFinite(event?.timeStamp)) + ? event.timeStamp + : performance?.now + ? performance.now() + : Date.now(); + const previousTimestampGroup = state.lastTimestamp ?? currentTimestampGroup; + let dtGroup = (currentTimestampGroup - previousTimestampGroup) / 1000; + if (!Number.isFinite(dtGroup) || dtGroup <= 0) { + dtGroup = MIN_TIMESTEP; + } + dtGroup = clamp(dtGroup, MIN_TIMESTEP, MAX_TIMESTEP); + + state.lastClientX = event.clientX; + state.lastClientY = event.clientY; + state.lastTimestamp = currentTimestampGroup; + + updatePointerAngularVelocity(pointerCanvasX, pointerCanvasY, centerX, centerY, dtGroup); + applyDynamicRotation(dtGroup, 0.96); + state.groupItems.forEach((item) => { + if (item.docId === state.docKey) { + item.displayRotation = state.rotation ?? item.displayRotation ?? 0; + } + }); + + return; } - - const docWidth = state.width; - const docHeight = state.height; - const halfWidth = docWidth / 2; - const halfHeight = docHeight / 2; - const canvasWidth = canvasSize.width || defaultCanvasWidth; - const canvasHeight = canvasSize.height || defaultCanvasHeight; - const minCenterX = canvasPadding + halfWidth; - const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - halfWidth); - const minCenterY = canvasPadding + halfHeight; - const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - halfHeight); - - const desiredCenterX = pointerCanvasX - state.localPointerOffsetX; - const desiredCenterY = pointerCanvasY - state.localPointerOffsetY; - const centerX = clamp(desiredCenterX, minCenterX, maxCenterX); - const centerY = clamp(desiredCenterY, minCenterY, maxCenterY); - - state.currentCenterX = centerX; - state.currentCenterY = centerY; - - state.groupItems.forEach((item) => { - const isPrimary = item.docId === state.docKey; - - if (isPrimary) { - item.currentCenterX = centerX; - item.currentCenterY = centerY; - item.offsetX = item.baseOffsetX ?? 0; - item.offsetY = item.baseOffsetY ?? 0; - item.displayRotation = state.rotation ?? item.displayRotation ?? 0; - } else { - const decay = 0.82; - const currentOffsetX = (item.offsetX ?? item.baseOffsetX ?? 0) * decay; - const currentOffsetY = (item.offsetY ?? item.baseOffsetY ?? 0) * decay; - item.offsetX = Math.abs(currentOffsetX) < 0.5 ? 0 : currentOffsetX; - item.offsetY = Math.abs(currentOffsetY) < 0.5 ? 0 : currentOffsetY; - - const targetX = centerX + item.offsetX; - const targetY = centerY + item.offsetY; - const smoothing = 0.18; - item.currentCenterX += (targetX - item.currentCenterX) * smoothing; - item.currentCenterY += (targetY - item.currentCenterY) * smoothing; - - const halfW = item.width / 2; - const halfH = item.height / 2; - const minX = canvasPadding + halfW; - const maxX = Math.max(minX, canvasWidth - canvasPadding - halfW); - const minY = canvasPadding + halfH; - const maxY = Math.max(minY, canvasHeight - canvasPadding - halfH); - item.currentCenterX = clamp(item.currentCenterX, minX, maxX); - item.currentCenterY = clamp(item.currentCenterY, minY, maxY); - - const rotationBlend = 0.16; - item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend; - } - - const entry = layoutRef.current.get(item.docId) || null; - const payload = { - centerX: item.currentCenterX, - centerY: item.currentCenterY, - rotation: item.displayRotation ?? 0, - width: item.width, - height: item.height, - scale: isPrimary ? state.dragScale || 1 : 1, - zIndex: entry?.z, - }; - - setDragTransform(item.docId, payload); - const node = itemRefs.current.get(item.docId); - applyDomTransform(node, payload); - }); - - const currentTimestampGroup = - (Number.isFinite(event?.timeStamp)) - ? event.timeStamp - : performance?.now - ? performance.now() - : Date.now(); - const previousTimestampGroup = state.lastTimestamp ?? currentTimestampGroup; - let dtGroup = (currentTimestampGroup - previousTimestampGroup) / 1000; - if (!Number.isFinite(dtGroup) || dtGroup <= 0) { - dtGroup = MIN_TIMESTEP; - } - dtGroup = clamp(dtGroup, MIN_TIMESTEP, MAX_TIMESTEP); - - state.lastClientX = event.clientX; - state.lastClientY = event.clientY; - state.lastTimestamp = currentTimestampGroup; - - updatePointerAngularVelocity(pointerCanvasX, pointerCanvasY, centerX, centerY, dtGroup); - applyDynamicRotation(dtGroup, 0.96); - state.groupItems.forEach((item) => { - if (item.docId === state.docKey) { - item.displayRotation = state.rotation ?? item.displayRotation ?? 0; - } - }); - - return; - } if (state.locked) { return; } diff --git a/frontend/src/desktop/workspaceEngine.ts b/frontend/src/desktop/workspaceEngine.ts index 4cf8e70..e3495d0 100644 --- a/frontend/src/desktop/workspaceEngine.ts +++ b/frontend/src/desktop/workspaceEngine.ts @@ -1,7 +1,7 @@ import { clamp, formatTransform } from '../utils/math'; import { fetchLayoutRecords, upsertLayoutRecords } from './db'; - -type DocumentId = string; +import type { DocumentId } from '../types/identifiers'; +type TenantId = import('../types/identifiers').TenantId; interface Point { x: number; @@ -63,7 +63,7 @@ interface BaseMetrics { } interface DragGroupItem { - docId?: string | number | null; + docId?: string | null; width: number; height: number; currentCenterX?: number; @@ -80,7 +80,7 @@ interface DragState { } interface InertiaSimulationState { - docId: string; + docId: DocumentId; restRotation: number; rotation: number; dynamicRotation: number; @@ -106,7 +106,7 @@ interface WorkspaceSnapshot { type WorkspaceSubscriber = () => void; -type DeskDocument = { id?: string | number | null } & Record; +type DeskDocument = { id?: string | null } & Record; type EnsureDocumentSize = (doc: DeskDocument) => DocumentSize | null; @@ -226,7 +226,7 @@ export const clampCardDimensions = (width: number, height: number): CardDimensio }; }; -export const computeFallbackCardSize = (docId: string | number): CardDimensions | null => { +export const computeFallbackCardSize = (docId: DocumentId): CardDimensions | null => { const baseSeed = seededRandom(`${docId}:fallback-size`); const aspectSeed = seededRandom(`${docId}:fallback-aspect`); @@ -259,7 +259,7 @@ function randomRangeFromSeed(seedKey: string, min: number, max: number): number return min + seed * span; } -function buildKey(docId: string | number, suffix: string): string { +function buildKey(docId: DocumentId, suffix: string): string { return `${docId}::${suffix}`; } @@ -525,63 +525,34 @@ const generateInitialLayout = ( export class WorkspaceEngine { allowLayoutPersistence: boolean; - - tenantId: string | null; - + tenantId: TenantId | null; viewId: string | null; - layout: Map; - layoutSnapshot: Map; - persistedLayout: Map; - layoutDirty: boolean; - zCounter: number; - canvasSize: { width: number; height: number }; - visibleDocIds: Set; - draggingId: string | null; - tagDropTargetId: string | null; - pendingTagDocId: string | null; - pendingRemovalTag: unknown; - dragInProgress: boolean; - activeDragDocIds: Set; - pendingSnapshotSync: boolean; - pendingPersistSync: boolean; - persistDebounceId: number | null; - items: DeskDocument[]; - documentLookup: Map; - ensureDocumentSize: EnsureDocumentSize; - resolveBaseMetrics: ResolveBaseMetrics; - snapshotCache: WorkspaceSnapshot; - subscribers: Set; - loadingPersisted: boolean; - pendingPersistence: unknown; - itemRefs: ItemRefs; - inertiaAnimations: Map; - initialLoadDone: boolean; constructor({ @@ -716,7 +687,7 @@ export class WorkspaceEngine { this.emit(); } - setDraggingId(docId: string | number | null): void { + setDraggingId(docId: DocumentId | null): void { const normalized = docId != null ? String(docId) : null; if (this.draggingId === normalized) { return; @@ -725,7 +696,7 @@ export class WorkspaceEngine { this.emit(); } - beginDrag(docIds: Array = []): void { + beginDrag(docIds: Array = []): void { this.dragInProgress = true; if (Array.isArray(docIds)) { this.activeDragDocIds = new Set(docIds.map((id) => (id != null ? String(id) : null)).filter(Boolean)); @@ -749,7 +720,7 @@ export class WorkspaceEngine { } } - setTagDropTargetId(docId: string | number | null): void { + setTagDropTargetId(docId: DocumentId | null): void { const normalized = docId != null ? String(docId) : null; if (this.tagDropTargetId === normalized) { return; @@ -758,7 +729,7 @@ export class WorkspaceEngine { this.emit(); } - setPendingTagDocId(docId: string | number | null): void { + setPendingTagDocId(docId: DocumentId | null): void { const normalized = docId != null ? String(docId) : null; if (this.pendingTagDocId === normalized) { return; @@ -779,7 +750,7 @@ export class WorkspaceEngine { this.layoutDirty = true; } - getLayout(docId: string | number | null): LayoutEntry | null { + getLayout(docId: DocumentId | null): LayoutEntry | null { if (docId == null) { return null; } @@ -788,7 +759,7 @@ export class WorkspaceEngine { } updateLayoutEntry( - docId: string | number | null, + docId: DocumentId | null, updater: (previous: LayoutEntry | null) => LayoutEntry | null, ): void { if (docId == null) { @@ -807,7 +778,7 @@ export class WorkspaceEngine { this.persistLayoutSnapshot(); } - bringToFront(docId: string | number | null): void { + bringToFront(docId: DocumentId | null): void { if (docId == null) { return; } @@ -825,7 +796,7 @@ export class WorkspaceEngine { } applyTransform( - docId: string | number | null, + docId: DocumentId | null, centerX: number, centerY: number, width: number, @@ -896,7 +867,7 @@ export class WorkspaceEngine { this.persistLayoutSnapshot(); } - cancelInertiaAnimation(docId: string | number | null): void { + cancelInertiaAnimation(docId: DocumentId | null): void { const key = docId != null ? String(docId) : null; if (!key) { return; @@ -995,7 +966,7 @@ export class WorkspaceEngine { return isSettled; } - startInertiaAnimation(docId: string | number | null, baseState: InertiaSimulationState): void { + startInertiaAnimation(docId: DocumentId | null, baseState: InertiaSimulationState): void { const raf = window.requestAnimationFrame; if (!raf) { return; diff --git a/frontend/src/detail/PreviewZoomOverlay.tsx b/frontend/src/detail/PreviewZoomOverlay.tsx index e095ca4..7cc477a 100644 --- a/frontend/src/detail/PreviewZoomOverlay.tsx +++ b/frontend/src/detail/PreviewZoomOverlay.tsx @@ -4,7 +4,7 @@ import { clamp } from '../utils/math'; import PdfViewer from '../preview/PdfViewer'; type DocumentLike = { - id?: string | number; + id?: string; title?: string; mime_type?: string | null; [key: string]: unknown; @@ -43,7 +43,7 @@ const determineDisplayKind = (entry?: DocumentLink | null): DisplayKind => { return 'image'; }; -const noop = () => {}; +const noop = () => { }; const PreviewZoomOverlay: React.FC = ({ open = false, @@ -214,22 +214,22 @@ const PreviewZoomOverlay: React.FC = ({ const key = event.key; if (key === ' ' || key === 'Space' || key === 'Spacebar') { - const target = event.target; - if (target instanceof HTMLElement) { - const tag = target.tagName ? target.tagName.toLowerCase() : ''; - if ( - target.isContentEditable - || tag === 'input' - || tag === 'textarea' - || tag === 'select' - ) { - return; + const target = event.target; + if (target instanceof HTMLElement) { + const tag = target.tagName ? target.tagName.toLowerCase() : ''; + if ( + target.isContentEditable + || tag === 'input' + || tag === 'textarea' + || tag === 'select' + ) { + return; + } } + event.preventDefault(); + onClose(); + return; } - event.preventDefault(); - onClose(); - return; - } if (key === 'Escape') { event.preventDefault(); @@ -291,19 +291,19 @@ const PreviewZoomOverlay: React.FC = ({ const contentStyle: CSSProperties = isNativeScale ? { - cursor: 'zoom-out', - width: naturalSize.width ? `${naturalSize.width}px` : 'auto', - height: naturalSize.height ? `${naturalSize.height}px` : 'auto', - maxWidth: 'none', - maxHeight: 'none', - touchAction: 'manipulation', - } + cursor: 'zoom-out', + width: naturalSize.width ? `${naturalSize.width}px` : 'auto', + height: naturalSize.height ? `${naturalSize.height}px` : 'auto', + maxWidth: 'none', + maxHeight: 'none', + touchAction: 'manipulation', + } : { - cursor: 'zoom-in', - maxWidth: '95vw', - maxHeight: '95vh', - touchAction: 'manipulation', - }; + cursor: 'zoom-in', + maxWidth: '95vw', + maxHeight: '95vh', + touchAction: 'manipulation', + }; if (!shouldRender) { return null; @@ -323,11 +323,11 @@ const PreviewZoomOverlay: React.FC = ({ >
{ - if (event.target === event.currentTarget) { - onClose(); - } - }} + onClick={(event) => { + if (event.target === event.currentTarget) { + onClose(); + } + }} >
; - onCorrespondentClick?: (id: string | number) => void; + activeCorrespondentIdSet?: Set; + onCorrespondentClick?: (id: string) => void; } const CorrespondentLinks: React.FC = ({ @@ -23,7 +23,7 @@ const CorrespondentLinks: React.FC = ({ return null; } - const activeSet = activeCorrespondentIdSet || new Set(); + const activeSet = activeCorrespondentIdSet || new Set(); const handleClick = (event: React.MouseEvent | React.KeyboardEvent, correspondent: CorrespondentLinkEntry) => { if (!onCorrespondentClick || correspondent.id == null) { return; diff --git a/frontend/src/documents/DocumentInfoPanel.tsx b/frontend/src/documents/DocumentInfoPanel.tsx index 7acfa1b..21e6257 100644 --- a/frontend/src/documents/DocumentInfoPanel.tsx +++ b/frontend/src/documents/DocumentInfoPanel.tsx @@ -36,7 +36,7 @@ export interface DocumentInfoPanelProps { activeTab?: string; onTabChange?: (tabId: string) => void; defaultTabId?: string; - resetKey?: string | number | null; + resetKey?: string | null; classNamePrefix?: string; hideTabNavWhenSingle?: boolean; summaryPlacement?: 'inline' | 'tabs'; @@ -204,11 +204,11 @@ const DocumentInfoPanel: React.FC = ({ const summaryNode = summaryInline ? ( - <> - {renderSummarySection()} - {renderDetailsSection()} - - ) + <> + {renderSummarySection()} + {renderDetailsSection()} + + ) : null; const visibleTabs = useMemo(() => { diff --git a/frontend/src/documents/DocumentSummarySection.tsx b/frontend/src/documents/DocumentSummarySection.tsx index 03b1b0c..8605740 100644 --- a/frontend/src/documents/DocumentSummarySection.tsx +++ b/frontend/src/documents/DocumentSummarySection.tsx @@ -14,11 +14,10 @@ import { import { describeDocumentSummary, type DocumentSummaryRow } from './documentSummary'; import { useFolderManager } from '../folders/FolderManagerContext'; - -type Identifier = string | number; +import type { FolderId, Identifier, TagId } from '../types/identifiers'; interface TagEntry { - id?: Identifier; + id?: TagId; label?: string; color?: string | null; } @@ -33,7 +32,7 @@ interface DocumentLike { id?: Identifier; title?: string; issued_at?: string | null; - folder_id?: string | null; + folder_id?: FolderId | null; current_version?: { version_number?: number } | null; tags?: TagEntry[]; correspondents?: CorrespondentEntry[]; @@ -64,17 +63,17 @@ interface CorrespondentSectionProps { export interface DocumentSummarySectionProps { document?: DocumentLike | null; - tagLookupById?: Map; + tagLookupById?: Map; tagOptions?: SelectionAssignmentMenuItem[]; onTagAdd?: (doc: DocumentLike, value: string, context?: { option?: unknown }) => void; - onTagRemove?: (docId: Identifier | undefined, tagId: Identifier | undefined) => void; + onTagRemove?: (docId: Identifier | undefined, tagId: TagId | undefined) => void; correspondents?: CorrespondentEntry[]; correspondentOptions?: SelectionAssignmentMenuItem[]; onCorrespondentAdd?: (payload: { document: DocumentLike; name: string; option?: unknown }) => void; onCorrespondentRemove?: (payload: { documentId: Identifier | undefined; correspondentId: Identifier | undefined }) => void; onUpdateTitle?: (docId: Identifier | undefined, title: string) => Promise | boolean; onUpdateIssued?: (docId: Identifier | undefined, timestamp: number | null) => Promise | boolean; - onFolderNavigate?: (folderId: string | null) => void; + onFolderNavigate?: (folderId: FolderId | null) => void; layout?: 'default' | 'compact'; } diff --git a/frontend/src/documents/DocumentThumbnailImage.tsx b/frontend/src/documents/DocumentThumbnailImage.tsx index 70548c9..8b9a1ae 100644 --- a/frontend/src/documents/DocumentThumbnailImage.tsx +++ b/frontend/src/documents/DocumentThumbnailImage.tsx @@ -17,7 +17,7 @@ const DEFAULT_THUMBNAIL_SIZE = 48; // Detect when an element becomes visible within a scroll container so we can delay loading. const useLazyVisibility = ( rootRef: MutableRefObject | null, - resetKey?: string | number | null, + resetKey?: string | null, ) => { const targetRef = useRef(null); const [isVisible, setIsVisible] = useState(false); diff --git a/frontend/src/documents/DocumentsGrid.tsx b/frontend/src/documents/DocumentsGrid.tsx index f9f0c99..c942d91 100644 --- a/frontend/src/documents/DocumentsGrid.tsx +++ b/frontend/src/documents/DocumentsGrid.tsx @@ -8,8 +8,7 @@ import { resolveCorrespondents } from './correspondents'; import { writeTagTransferData } from './tagTransfer'; import useInlineRename from './useInlineRename'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; - -export type Identifier = string | number; +import type { Identifier } from '../types/identifiers'; export interface FolderLike { id?: Identifier | 'root'; @@ -153,347 +152,347 @@ const DocumentsGrid: React.FC = ({ const totalSelectionCount = documentSelectionCount + folderSelectionCount; return ( -
{ - if (event.target === event.currentTarget) { +
{ + if (event.target === event.currentTarget) { clearSelection(); - } - }} - > - {entries.map((entry) => { - if (entry.type === 'folder') { - const folder = entry.folder; - if (!folder) { - return null; } - const canDragFolder = folder.id !== 'root'; - const isDraggingFolder = draggedFolderId === folder.id; - const isSelectedFolder = selectedFolderIdsSet?.has(folder.id); - const classes = ['document-card', 'folder-card']; - if (isDraggingFolder) classes.push('is-dragging'); - if (isSelectedFolder) classes.push('selected'); - const canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root'; - const isFolderEditing = editingFolderId === folder.id; - const folderDraftValue = isFolderEditing ? folderDraft : folder.name; - const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : ''; - const isFolderSaving = savingFolderId === folder.id; - const canSubmitFolder = - isFolderEditing && trimmedFolderDraft.length > 0 && trimmedFolderDraft !== folder.name; - const allowInlineFolderEdit = canRenameFolder && isSelectedFolder && totalSelectionCount === 1; + }} + > + {entries.map((entry) => { + if (entry.type === 'folder') { + const folder = entry.folder; + if (!folder) { + return null; + } + const canDragFolder = folder.id !== 'root'; + const isDraggingFolder = draggedFolderId === folder.id; + const isSelectedFolder = selectedFolderIdsSet?.has(folder.id); + const classes = ['document-card', 'folder-card']; + if (isDraggingFolder) classes.push('is-dragging'); + if (isSelectedFolder) classes.push('selected'); + const canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root'; + const isFolderEditing = editingFolderId === folder.id; + const folderDraftValue = isFolderEditing ? folderDraft : folder.name; + const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : ''; + const isFolderSaving = savingFolderId === folder.id; + const canSubmitFolder = + isFolderEditing && trimmedFolderDraft.length > 0 && trimmedFolderDraft !== folder.name; + const allowInlineFolderEdit = canRenameFolder && isSelectedFolder && totalSelectionCount === 1; - return ( -
onFolderClick?.(folder, event)} - onDoubleClick={(event) => { - event.preventDefault(); - onFolderSelect?.(folder.id); - }} - onDragOver={(event) => onFolderDragOver?.(event, folder.id)} - onDragLeave={onFolderDragLeave} - onDrop={(event) => onFolderDrop?.(event, folder.id)} - onDragStart={(event) => { - if (canDragFolder) { - onFolderDragStart?.(event, folder.id); - } - }} - onDragEnd={(event) => { - if (canDragFolder) { - onFolderDragEnd?.(event); - } - }} - > -
- -
-
- {isFolderEditing ? ( -
- setFolderDraft(event.target.value)} - onClick={(event) => event.stopPropagation()} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); + return ( +
onFolderClick?.(folder, event)} + onDoubleClick={(event) => { + event.preventDefault(); + onFolderSelect?.(folder.id); + }} + onDragOver={(event) => onFolderDragOver?.(event, folder.id)} + onDragLeave={onFolderDragLeave} + onDrop={(event) => onFolderDrop?.(event, folder.id)} + onDragStart={(event) => { + if (canDragFolder) { + onFolderDragStart?.(event, folder.id); + } + }} + onDragEnd={(event) => { + if (canDragFolder) { + onFolderDragEnd?.(event); + } + }} + > +
+ +
+
+ {isFolderEditing ? ( +
+ setFolderDraft(event.target.value)} + onClick={(event) => event.stopPropagation()} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + submitFolderEditing(folder); + } else if (event.key === 'Escape') { + event.preventDefault(); + cancelFolderEditing(event); + } + }} + onBlur={(event) => { + const nextFocus = event.relatedTarget; + if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) { + cancelFolderEditing(); + } + }} + /> + - -
- ) : ( -
- { - if (!allowInlineFolderEdit) { - return; - } - event.preventDefault(); - event.stopPropagation(); - beginFolderEditing(folder); - }} - onKeyDown={(event) => { - if (!allowInlineFolderEdit) { - return; - } - if (event.key === 'Enter') { + }} + > + + + +
+ ) : ( +
+ { + if (!allowInlineFolderEdit) { + return; + } event.preventDefault(); event.stopPropagation(); beginFolderEditing(folder); - } - }} - > - {folder.name} - -
- )} + }} + onKeyDown={(event) => { + if (!allowInlineFolderEdit) { + return; + } + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + beginFolderEditing(folder); + } + }} + > + {folder.name} + +
+ )} +
-
- ); - } + ); + } - const doc = entry.document; - if (!doc) { - return null; - } + const doc = entry.document; + if (!doc) { + return null; + } - const isSelected = selectedDocumentIdsSet?.has(doc.id); - const isDraggingDoc = draggingDocumentIdsSet?.has(doc.id); - const tagList = Array.isArray(doc.tags) ? doc.tags : []; - const visibleTags = tagList.slice(0, 3); - const remainingTagCount = tagList.length > 3 ? tagList.length - 3 : 0; - const correspondents = resolveCorrespondents(doc); - const cardClasses = ['document-card', 'document']; - if (isSelected) cardClasses.push('selected'); - if (isDraggingDoc) cardClasses.push('is-dragging'); - const isEditingDoc = editingDocumentId === doc.id; - const documentDraftValue = isEditingDoc ? documentDraft : doc.title; - const trimmedDocumentDraft = isEditingDoc ? documentDraft.trim() : ''; - const isDocumentSaving = savingDocumentId === doc.id; - const canSubmitDocument = - isEditingDoc && trimmedDocumentDraft.length > 0 && trimmedDocumentDraft !== doc.title; - const allowInlineDocumentEdit = onDocumentRename && isSelected && totalSelectionCount === 1; - return ( -
onDocumentClick?.(doc, event)} - onDoubleClick={(event) => onDocumentActivate?.(doc, event)} - draggable - onDragStart={(event) => onDocumentDragStart?.(event, doc)} - onDragEnd={(event) => onDocumentDragEnd?.(event)} - onDragOver={(event) => onDocumentTagDragOver?.(event)} - onDragOverCapture={(event) => onDocumentTagDragOver?.(event)} - onDragLeave={onDocumentTagDragLeave} - onDragLeaveCapture={onDocumentTagDragLeave} - onDrop={(event) => onDocumentTagDrop?.(event, doc.id)} - onDropCapture={(event) => onDocumentTagDrop?.(event, doc.id)} - > - -
-
- {correspondents.length > 0 ? ( - - - - ) : null} - {isEditingDoc ? ( -
- setDocumentDraft(event.target.value)} - onClick={(event) => event.stopPropagation()} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); + const isSelected = selectedDocumentIdsSet?.has(doc.id); + const isDraggingDoc = draggingDocumentIdsSet?.has(doc.id); + const tagList = Array.isArray(doc.tags) ? doc.tags : []; + const visibleTags = tagList.slice(0, 3); + const remainingTagCount = tagList.length > 3 ? tagList.length - 3 : 0; + const correspondents = resolveCorrespondents(doc); + const cardClasses = ['document-card', 'document']; + if (isSelected) cardClasses.push('selected'); + if (isDraggingDoc) cardClasses.push('is-dragging'); + const isEditingDoc = editingDocumentId === doc.id; + const documentDraftValue = isEditingDoc ? documentDraft : doc.title; + const trimmedDocumentDraft = isEditingDoc ? documentDraft.trim() : ''; + const isDocumentSaving = savingDocumentId === doc.id; + const canSubmitDocument = + isEditingDoc && trimmedDocumentDraft.length > 0 && trimmedDocumentDraft !== doc.title; + const allowInlineDocumentEdit = onDocumentRename && isSelected && totalSelectionCount === 1; + return ( +
onDocumentClick?.(doc, event)} + onDoubleClick={(event) => onDocumentActivate?.(doc, event)} + draggable + onDragStart={(event) => onDocumentDragStart?.(event, doc)} + onDragEnd={(event) => onDocumentDragEnd?.(event)} + onDragOver={(event) => onDocumentTagDragOver?.(event)} + onDragOverCapture={(event) => onDocumentTagDragOver?.(event)} + onDragLeave={onDocumentTagDragLeave} + onDragLeaveCapture={onDocumentTagDragLeave} + onDrop={(event) => onDocumentTagDrop?.(event, doc.id)} + onDropCapture={(event) => onDocumentTagDrop?.(event, doc.id)} + > + +
+
+ {correspondents.length > 0 ? ( + + + + ) : null} + {isEditingDoc ? ( +
+ setDocumentDraft(event.target.value)} + onClick={(event) => event.stopPropagation()} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + submitDocumentEditing(doc); + } else if (event.key === 'Escape') { + event.preventDefault(); + cancelDocumentEditing(event); + } + }} + onBlur={(event) => { + const nextFocus = event.relatedTarget; + if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) { + cancelDocumentEditing(); + } + }} + /> + - -
- ) : ( -
- { - if (!allowInlineDocumentEdit) { - return; - } - event.preventDefault(); - event.stopPropagation(); - beginDocumentEditing(doc); - }} - onKeyDown={(event) => { - if (!allowInlineDocumentEdit) { - return; - } - if (event.key === 'Enter') { + }} + > + + + +
+ ) : ( +
+ { + if (!allowInlineDocumentEdit) { + return; + } event.preventDefault(); event.stopPropagation(); beginDocumentEditing(doc); - } - }} - > - {doc.title} - -
- )} -
- {visibleTags.length > 0 && ( -
- {visibleTags.map((tag, index) => { - const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color; - const style = getTagColorStyle(colorSource); - const tagId = tag?.id ?? null; - const clickable = tagId != null && typeof onTagClick === 'function'; - const key = tagId ?? `${doc.id}-tag-${index}`; - return ( - { - event.stopPropagation(); - if (tagId == null) { + }} + onKeyDown={(event) => { + if (!allowInlineDocumentEdit) { return; } - onTagClick?.(tagId); - } : undefined} - draggable - onDragStart={(event) => { - event.stopPropagation(); - try { - if (event.dataTransfer) { - event.dataTransfer.effectAllowed = 'copyMove'; - } - } catch (error) { - console.warn('[documents] Failed to configure drag effect', error); - } - writeTagTransferData(event.dataTransfer, tag, doc.id); - }} - onDragEnd={(event) => { - event.stopPropagation(); - }} - onKeyDown={clickable ? (event) => { - if (event.key === 'Enter' || event.key === ' ') { + if (event.key === 'Enter') { event.preventDefault(); + event.stopPropagation(); + beginDocumentEditing(doc); + } + }} + > + {doc.title} + +
+ )} +
+ {visibleTags.length > 0 && ( +
+ {visibleTags.map((tag, index) => { + const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color; + const style = getTagColorStyle(colorSource); + const tagId = tag?.id ?? null; + const clickable = tagId != null && typeof onTagClick === 'function'; + const key = tagId ?? `${doc.id}-tag-${index}`; + return ( + { event.stopPropagation(); if (tagId == null) { return; } onTagClick?.(tagId); - } - } : undefined} - > - {tag.label} - - ); - })} - {remainingTagCount > 0 && ( - +{remainingTagCount} - )} -
- )} + } : undefined} + draggable + onDragStart={(event) => { + event.stopPropagation(); + try { + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'copyMove'; + } + } catch (error) { + console.warn('[documents] Failed to configure drag effect', error); + } + writeTagTransferData(event.dataTransfer, tag, doc.id); + }} + onDragEnd={(event) => { + event.stopPropagation(); + }} + onKeyDown={clickable ? (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + event.stopPropagation(); + if (tagId == null) { + return; + } + onTagClick?.(tagId); + } + } : undefined} + > + {tag.label} + + ); + })} + {remainingTagCount > 0 && ( + +{remainingTagCount} + )} +
+ )} +
-
- ); - })} -
+ ); + })} +
); }; diff --git a/frontend/src/documents/DocumentsList.tsx b/frontend/src/documents/DocumentsList.tsx index 570e71d..3ea8284 100644 --- a/frontend/src/documents/DocumentsList.tsx +++ b/frontend/src/documents/DocumentsList.tsx @@ -9,8 +9,7 @@ import { resolveCorrespondents } from './correspondents'; import { writeTagTransferData } from './tagTransfer'; import useInlineRename from './useInlineRename'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; - -export type Identifier = string | number; +import type { Identifier } from '../types/identifiers'; export interface FolderLike { id?: Identifier | 'root'; @@ -160,356 +159,356 @@ const DocumentsList: React.FC = ({ const totalSelectionCount = documentSelectionCount + folderSelectionCount; return ( - - { - clearSelection(); - }} - > - - - - - - - - - {entries.map((entry) => { - if (entry.type === 'folder') { - const folder = entry.folder; - if (!folder) { - return null; - } - const canDragFolder = folder.id !== 'root'; - const isDraggingFolder = draggedFolderId === folder.id; - const isSelectedFolder = selectedFolderIdsSet?.has(folder.id); - const canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root'; - const isFolderEditing = editingFolderId === folder.id; - const folderDraftValue = isFolderEditing ? folderDraft : folder.name; - const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : ''; - const isFolderSaving = savingFolderId === folder.id; - const canSubmitFolder = - isFolderEditing && trimmedFolderDraft.length > 0 && trimmedFolderDraft !== folder.name; - const allowInlineFolderEdit = canRenameFolder && isSelectedFolder && totalSelectionCount === 1; +
 NameIssuedAdded
+ { + clearSelection(); + }} + > + + + + + + + + + {entries.map((entry) => { + if (entry.type === 'folder') { + const folder = entry.folder; + if (!folder) { + return null; + } + const canDragFolder = folder.id !== 'root'; + const isDraggingFolder = draggedFolderId === folder.id; + const isSelectedFolder = selectedFolderIdsSet?.has(folder.id); + const canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root'; + const isFolderEditing = editingFolderId === folder.id; + const folderDraftValue = isFolderEditing ? folderDraft : folder.name; + const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : ''; + const isFolderSaving = savingFolderId === folder.id; + const canSubmitFolder = + isFolderEditing && trimmedFolderDraft.length > 0 && trimmedFolderDraft !== folder.name; + const allowInlineFolderEdit = canRenameFolder && isSelectedFolder && totalSelectionCount === 1; - return ( - onFolderClick?.(folder, event)} - onDoubleClick={(event) => { - event.preventDefault(); - onFolderSelect?.(folder.id); - }} - onDragOver={(event) => onFolderDragOver?.(event, folder.id)} - onDragLeave={onFolderDragLeave} - onDrop={(event) => onFolderDrop?.(event, folder.id)} - draggable={canDragFolder} - onDragStart={(event) => { - if (canDragFolder) { - onFolderDragStart?.(event, folder.id); - } - }} - onDragEnd={(event) => { - if (canDragFolder) { - onFolderDragEnd?.(event); - } - }} - > - - onFolderClick?.(folder, event)} + onDoubleClick={(event) => { + event.preventDefault(); + onFolderSelect?.(folder.id); + }} + onDragOver={(event) => onFolderDragOver?.(event, folder.id)} + onDragLeave={onFolderDragLeave} + onDrop={(event) => onFolderDrop?.(event, folder.id)} + draggable={canDragFolder} + onDragStart={(event) => { + if (canDragFolder) { + onFolderDragStart?.(event, folder.id); + } + }} + onDragEnd={(event) => { + if (canDragFolder) { + onFolderDragEnd?.(event); + } + }} + > + + - - - - ); - } - - const doc = entry.document; - if (!doc) { - return null; - } - const isSelected = selectedDocumentIdsSet?.has(doc.id); - const isDraggingDoc = draggingDocumentIdsSet?.has(doc.id); - const rowClasses = ['document']; - if (isSelected) rowClasses.push('selected'); - if (isDraggingDoc) rowClasses.push('is-dragging'); - const correspondents = resolveCorrespondents(doc); - const isEditingDoc = editingDocumentId === doc.id; - const documentDraftValue = isEditingDoc ? documentDraft : doc.title; - const trimmedDocumentDraft = isEditingDoc ? documentDraft.trim() : ''; - const isDocumentSaving = savingDocumentId === doc.id; - const canSubmitDocument = - isEditingDoc && trimmedDocumentDraft.length > 0 && trimmedDocumentDraft !== doc.title; - const allowInlineDocumentEdit = - onDocumentRename && isSelected && totalSelectionCount === 1; - const issuedLabel = formatDate(doc.issued_at); - const addedLabel = formatDate(doc.created_at || doc.uploaded_at); - return ( - onDocumentClick?.(doc, event)} - onDoubleClick={(event) => onDocumentActivate?.(doc, event)} - draggable - onDragStart={(event) => onDocumentDragStart?.(event, doc)} - onDragEnd={(event) => onDocumentDragEnd?.(event)} - onDragOver={onDocumentTagDragOver} - onDragLeave={onDocumentTagDragLeave} - onDrop={(event) => onDocumentTagDrop?.(event, doc.id)} - > - - + + + + ); + } + + const doc = entry.document; + if (!doc) { + return null; + } + const isSelected = selectedDocumentIdsSet?.has(doc.id); + const isDraggingDoc = draggingDocumentIdsSet?.has(doc.id); + const rowClasses = ['document']; + if (isSelected) rowClasses.push('selected'); + if (isDraggingDoc) rowClasses.push('is-dragging'); + const correspondents = resolveCorrespondents(doc); + const isEditingDoc = editingDocumentId === doc.id; + const documentDraftValue = isEditingDoc ? documentDraft : doc.title; + const trimmedDocumentDraft = isEditingDoc ? documentDraft.trim() : ''; + const isDocumentSaving = savingDocumentId === doc.id; + const canSubmitDocument = + isEditingDoc && trimmedDocumentDraft.length > 0 && trimmedDocumentDraft !== doc.title; + const allowInlineDocumentEdit = + onDocumentRename && isSelected && totalSelectionCount === 1; + const issuedLabel = formatDate(doc.issued_at); + const addedLabel = formatDate(doc.created_at || doc.uploaded_at); + return ( + onDocumentClick?.(doc, event)} + onDoubleClick={(event) => onDocumentActivate?.(doc, event)} + draggable + onDragStart={(event) => onDocumentDragStart?.(event, doc)} + onDragEnd={(event) => onDocumentDragEnd?.(event)} + onDragOver={onDocumentTagDragOver} + onDragLeave={onDocumentTagDragLeave} + onDrop={(event) => onDocumentTagDrop?.(event, doc.id)} + > + + - - - - ); - })} - -
 NameIssuedAdded
-
- -
-
-
- - - {isFolderEditing ? ( - - setFolderDraft(event.target.value)} - onClick={(event) => event.stopPropagation()} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); + return ( +
+
+ +
+
+
+ + + {isFolderEditing ? ( + + setFolderDraft(event.target.value)} + onClick={(event) => event.stopPropagation()} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + submitFolderEditing(folder); + } else if (event.key === 'Escape') { + event.preventDefault(); + cancelFolderEditing(event); + } + }} + onBlur={(event) => { + const nextFocus = event.relatedTarget; + if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) { + cancelFolderEditing(); + } + }} + /> + + + + ) : ( + { - event.stopPropagation(); - submitFolderEditing(folder); - }} - > - - - - - ) : ( - { - if (!allowInlineFolderEdit) { - return; - } - event.preventDefault(); - event.stopPropagation(); - beginFolderEditing(folder); - }} - onKeyDown={(event) => { - if (!allowInlineFolderEdit) { - return; - } - if (event.key === 'Enter') { + if (!allowInlineFolderEdit) { + return; + } event.preventDefault(); event.stopPropagation(); beginFolderEditing(folder); - } - }} - > - {folder.name} - - )} - - -
-
- - -
-
- - {correspondents.length > 0 ? ( - - + }} + onKeyDown={(event) => { + if (!allowInlineFolderEdit) { + return; + } + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + beginFolderEditing(folder); + } + }} + > + {folder.name} + + )} - ) : null} - - {isEditingDoc ? ( - - setDocumentDraft(event.target.value)} - onClick={(event) => event.stopPropagation()} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); - submitDocumentEditing(doc); - } else if (event.key === 'Escape') { - event.preventDefault(); - cancelDocumentEditing(event); - } - }} - onBlur={(event) => { - const nextFocus = event.relatedTarget; - if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) { - cancelDocumentEditing(); - } - }} - /> - - + +
+
+ + +
+
+ + {correspondents.length > 0 ? ( + + - ) : ( - { - if (!allowInlineDocumentEdit) { - return; - } - event.preventDefault(); - event.stopPropagation(); - beginDocumentEditing(doc); - }} - onKeyDown={(event) => { - if (!allowInlineDocumentEdit) { - return; - } - if (event.key === 'Enter') { + ) : null} + + {isEditingDoc ? ( + + setDocumentDraft(event.target.value)} + onClick={(event) => event.stopPropagation()} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + submitDocumentEditing(doc); + } else if (event.key === 'Escape') { + event.preventDefault(); + cancelDocumentEditing(event); + } + }} + onBlur={(event) => { + const nextFocus = event.relatedTarget; + if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) { + cancelDocumentEditing(); + } + }} + /> + + + + ) : ( + { + if (!allowInlineDocumentEdit) { + return; + } event.preventDefault(); event.stopPropagation(); beginDocumentEditing(doc); - } - }} - > - {doc.title} - - )} - - -
- {(doc.tags || []).length > 0 && ( -
- {(doc.tags || []).map((tag, index) => { - const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color; - const style = getTagColorStyle(colorSource); - const tagId = tag?.id ?? null; - const clickable = tagId != null && typeof onTagClick === 'function'; - const key = tagId ?? `${doc.id}-tag-${index}`; - return ( - { - event.stopPropagation(); - if (tagId == null) return; - onTagClick?.(tagId); - } : undefined} - draggable - onDragStart={(event) => { - event.stopPropagation(); - try { - if (event.dataTransfer) { - event.dataTransfer.effectAllowed = 'copyMove'; - } - } catch (error) { - console.warn('[documents] Failed to configure drag effect', error); - } - writeTagTransferData(event.dataTransfer, tag, doc.id); - }} - onDragEnd={(event) => { - event.stopPropagation(); - }} - onKeyDown={clickable ? (event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - event.stopPropagation(); - if (tagId == null) { + }} + onKeyDown={(event) => { + if (!allowInlineDocumentEdit) { return; } - onTagClick?.(tagId); - } - } : undefined} - > - {tag.label} - - ); - })} + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + beginDocumentEditing(doc); + } + }} + > + {doc.title} + + )} + +
- )} -
-
{issuedLabel}{addedLabel}
+ {(doc.tags || []).length > 0 && ( +
+ {(doc.tags || []).map((tag, index) => { + const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color; + const style = getTagColorStyle(colorSource); + const tagId = tag?.id ?? null; + const clickable = tagId != null && typeof onTagClick === 'function'; + const key = tagId ?? `${doc.id}-tag-${index}`; + return ( + { + event.stopPropagation(); + if (tagId == null) return; + onTagClick?.(tagId); + } : undefined} + draggable + onDragStart={(event) => { + event.stopPropagation(); + try { + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'copyMove'; + } + } catch (error) { + console.warn('[documents] Failed to configure drag effect', error); + } + writeTagTransferData(event.dataTransfer, tag, doc.id); + }} + onDragEnd={(event) => { + event.stopPropagation(); + }} + onKeyDown={clickable ? (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + event.stopPropagation(); + if (tagId == null) { + return; + } + onTagClick?.(tagId); + } + } : undefined} + > + {tag.label} + + ); + })} +
+ )} +
+ + {issuedLabel} + {addedLabel} + + ); + })} + + ); }; diff --git a/frontend/src/documents/DocumentsManager.ts b/frontend/src/documents/DocumentsManager.ts index 357ad5f..d39711c 100644 --- a/frontend/src/documents/DocumentsManager.ts +++ b/frontend/src/documents/DocumentsManager.ts @@ -1,6 +1,5 @@ import { shallowEqual } from 'react-redux'; - -type DocumentId = string | number; +import type { DocumentId } from '../types/identifiers'; export type ManagedDocument = { id?: DocumentId | null } & Record; diff --git a/frontend/src/documents/SelectionAssignmentMenu.tsx b/frontend/src/documents/SelectionAssignmentMenu.tsx index 3046c5f..bee9893 100644 --- a/frontend/src/documents/SelectionAssignmentMenu.tsx +++ b/frontend/src/documents/SelectionAssignmentMenu.tsx @@ -5,18 +5,18 @@ import { CheckIcon, CircleDashedCheckIcon, PlusIcon } from '../ui/icons'; export type AssignmentState = 'all' | 'partial' | 'none'; export interface SelectionAssignmentMenuItem { - id?: string | number; + id?: string; label?: string; state?: AssignmentState; count?: number | null; total?: number | null; color?: string | null; - value?: string | number; + value?: string; payload?: unknown; } export interface NormalizedSelectionAssignmentItem { - id: string | number; + id: string; label: string; state: AssignmentState; count: number | null; @@ -105,7 +105,7 @@ const SelectionAssignmentMenu: React.FC = ({ const inputRef = useRef(null); const [query, setQuery] = useState(''); const [pending, setPending] = useState(false); - const [sortSnapshot, setSortSnapshot] = useState | null>(null); + const [sortSnapshot, setSortSnapshot] = useState | null>(null); const { isOpen, @@ -175,10 +175,10 @@ const SelectionAssignmentMenu: React.FC = ({ const orderedItems = useMemo(() => { if (freezeSortOnOpen && sortSnapshot && sortByState) { - const itemMap = new Map( + const itemMap = new Map( sortedByStateItems.map((item) => [item.id, item]), ); - const seen = new Set(); + const seen = new Set(); const fromSnapshot = sortSnapshot .map((id) => { const entry = itemMap.get(id); diff --git a/frontend/src/documents/SelectionFloatingActions.tsx b/frontend/src/documents/SelectionFloatingActions.tsx index b4374e8..321035a 100644 --- a/frontend/src/documents/SelectionFloatingActions.tsx +++ b/frontend/src/documents/SelectionFloatingActions.tsx @@ -12,10 +12,10 @@ import SelectionAssignmentMenu, { SelectionAssignmentMenuItem } from './Selectio import SelectionSummary from './SelectionSummary'; import { useAppState } from '../app/appState'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; +import type { DocumentId } from '../types/identifiers'; const ROOT_FOLDER_LABEL = 'Documents'; -type DocumentId = string | number; type NullableDocumentId = DocumentId | null; type SelectedIdList = NullableDocumentId[] | null; @@ -145,7 +145,7 @@ const buildTagAssignments = ( return []; } - const map = new Map = ({ const value = isRecord(candidate) ? (candidate?.id ?? candidate?.value ?? null) : candidate; - if (!value && value !== 0) { + if (!value) { return; } await onMoveDocumentsToFolder(documentIdList, value as DocumentId); diff --git a/frontend/src/documents/context/DocumentsFilterContext.tsx b/frontend/src/documents/context/DocumentsFilterContext.tsx index 618b683..706865e 100644 --- a/frontend/src/documents/context/DocumentsFilterContext.tsx +++ b/frontend/src/documents/context/DocumentsFilterContext.tsx @@ -1,10 +1,9 @@ import React, { createContext, useContext } from 'react'; - -type Identifier = string | number; +import type { Identifier } from '../../types/identifiers'; export interface DocumentsFilterValue { query: string; - searchResultIds: Array | null; + searchResultIds: Array | null; searchLoading: boolean; includeDescendants: boolean; activeTagIds: Identifier[]; diff --git a/frontend/src/documents/correspondents.ts b/frontend/src/documents/correspondents.ts index 96f9fbc..8c5c0b5 100644 --- a/frontend/src/documents/correspondents.ts +++ b/frontend/src/documents/correspondents.ts @@ -1,5 +1,5 @@ export interface CorrespondentReference { - id?: string | number | null; + id?: string | null; name?: string | null; key?: string; } @@ -9,9 +9,9 @@ export interface DocumentLike { } export interface ResolvedCorrespondent { - id?: string | number | null; + id?: string | null; name: string; - key: string | number; + key: string; } export const resolveCorrespondents = (doc?: DocumentLike | null): ResolvedCorrespondent[] => { @@ -19,7 +19,7 @@ export const resolveCorrespondents = (doc?: DocumentLike | null): ResolvedCorres return []; } - const seen = new Set(); + const seen = new Set(); const results: ResolvedCorrespondent[] = []; doc.correspondents.forEach((entry = {}, index) => { diff --git a/frontend/src/documents/hooks/useBulkDocumentActions.ts b/frontend/src/documents/hooks/useBulkDocumentActions.ts index 88ae110..374dbe6 100644 --- a/frontend/src/documents/hooks/useBulkDocumentActions.ts +++ b/frontend/src/documents/hooks/useBulkDocumentActions.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import { assignCorrespondentsBulk } from '../../lib/apiClient'; - -export type Identifier = string | number; +import type { Identifier } from '../../types/identifiers'; type BulkAssignmentResponse = { assigned?: number; @@ -62,35 +61,35 @@ const useBulkDocumentActions = ({ if (!target?.id) { setStatusMessage('Unable to resolve correspondent.', 'error'); return; - } + } - const response: BulkAssignmentResponse = await assignCorrespondentsBulk({ - document_ids: targets, - assignments: [ - { - correspondent_id: target.id, - }, - ], - action: 'add', - }); - - const { assigned = 0, removed = 0 } = response; - - if (updateDocumentCaches && target.id) { - targets.forEach((docId) => { - updateDocumentCaches(docId, (doc) => { - if (!doc) return doc; - const current = Array.isArray((doc as any).correspondents) ? (doc as any).correspondents : []; - if (current.some((entry: any) => entry?.id === target.id)) { - return doc; - } - return { - ...(doc as any), - correspondents: [...current, { id: target.id, name: (target as any).name }], - }; - }); + const response: BulkAssignmentResponse = await assignCorrespondentsBulk({ + document_ids: targets, + assignments: [ + { + correspondent_id: target.id, + }, + ], + action: 'add', }); - } + + const { assigned = 0, removed = 0 } = response; + + if (updateDocumentCaches && target.id) { + targets.forEach((docId) => { + updateDocumentCaches(docId, (doc) => { + if (!doc) return doc; + const current = Array.isArray((doc as any).correspondents) ? (doc as any).correspondents : []; + if (current.some((entry: any) => entry?.id === target.id)) { + return doc; + } + return { + ...(doc as any), + correspondents: [...current, { id: target.id, name: (target as any).name }], + }; + }); + }); + } const assignedSuffix = assigned === 1 ? '' : 's'; if (removed > 0) { const removedSuffix = removed === 1 ? '' : 's'; @@ -105,18 +104,18 @@ const useBulkDocumentActions = ({ ); } - if (input) { - input.value = ''; - } - }, - [ - correspondentLookupByName, - handleCorrespondentCreate, - resolveTargetDocumentIds, - setStatusMessage, - updateDocumentCaches, - ], -); + if (input) { + input.value = ''; + } + }, + [ + correspondentLookupByName, + handleCorrespondentCreate, + resolveTargetDocumentIds, + setStatusMessage, + updateDocumentCaches, + ], + ); const handleBulkCorrespondentRemove = useCallback( async ({ assignments = [], documentIds }: { assignments?: CorrespondentAssignment[]; documentIds?: Identifier[] }) => { @@ -132,33 +131,33 @@ const useBulkDocumentActions = ({ return; } - const normalizedAssignments = assignments.map((entry) => ({ - correspondent_id: entry.correspondent_id, - })); + const normalizedAssignments = assignments.map((entry) => ({ + correspondent_id: entry.correspondent_id, + })); - const response: BulkAssignmentResponse = await assignCorrespondentsBulk({ - document_ids: targets, - assignments: normalizedAssignments, - action: 'remove', - }); - - const { assigned = 0, removed = 0 } = response; - if (updateDocumentCaches) { - targets.forEach((docId) => { - updateDocumentCaches(docId, (doc) => { - if (!doc || !Array.isArray((doc as any).correspondents)) { - return doc; - } - const filtered = (doc as any).correspondents.filter( - (entry: any) => - entry && !normalizedAssignments.some((assignment) => assignment.correspondent_id === entry.id), - ); - return filtered.length === (doc as any).correspondents.length - ? doc - : { ...(doc as any), correspondents: filtered }; - }); + const response: BulkAssignmentResponse = await assignCorrespondentsBulk({ + document_ids: targets, + assignments: normalizedAssignments, + action: 'remove', }); - } + + const { assigned = 0, removed = 0 } = response; + if (updateDocumentCaches) { + targets.forEach((docId) => { + updateDocumentCaches(docId, (doc) => { + if (!doc || !Array.isArray((doc as any).correspondents)) { + return doc; + } + const filtered = (doc as any).correspondents.filter( + (entry: any) => + entry && !normalizedAssignments.some((assignment) => assignment.correspondent_id === entry.id), + ); + return filtered.length === (doc as any).correspondents.length + ? doc + : { ...(doc as any), correspondents: filtered }; + }); + }); + } if (removed > 0) { const removedSuffix = removed === 1 ? '' : 's'; @@ -169,12 +168,12 @@ const useBulkDocumentActions = ({ } else if (assigned > 0) { const assignedSuffix = assigned === 1 ? '' : 's'; setStatusMessage(`Correspondent updated ${assigned} link${assignedSuffix}.`, 'info'); - } else { - setStatusMessage('No correspondents changed.', 'info'); - } - }, - [resolveTargetDocumentIds, setStatusMessage, updateDocumentCaches], -); + } else { + setStatusMessage('No correspondents changed.', 'info'); + } + }, + [resolveTargetDocumentIds, setStatusMessage, updateDocumentCaches], + ); const handleDeleteSelection = useCallback(async () => { const docIds = Array.isArray(selectedDocumentIds) ? selectedDocumentIds : []; diff --git a/frontend/src/documents/hooks/useDocumentsPanelProps.ts b/frontend/src/documents/hooks/useDocumentsPanelProps.ts index 2112984..861526f 100644 --- a/frontend/src/documents/hooks/useDocumentsPanelProps.ts +++ b/frontend/src/documents/hooks/useDocumentsPanelProps.ts @@ -1,7 +1,6 @@ import { useMemo } from 'react'; import type { WorkspaceSelectionValue } from '../../app/WorkspaceSelectionContext'; - -type Identifier = string | number; +import type { Identifier } from '../../types/identifiers'; interface DocumentLinkLike { url?: string | null; @@ -96,8 +95,8 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { searchLoading, tagLookupById, activeCorrespondentFilters, - ensureAssetUrl, - getDocumentAsset, + ensureAssetUrl, + getDocumentAsset, handleDocumentTagDrop, documentsViewMode, documentsSortField, diff --git a/frontend/src/documents/hooks/useDocumentsSelection.ts b/frontend/src/documents/hooks/useDocumentsSelection.ts index 46c874d..6c7cfa1 100644 --- a/frontend/src/documents/hooks/useDocumentsSelection.ts +++ b/frontend/src/documents/hooks/useDocumentsSelection.ts @@ -1,20 +1,21 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import { createDocumentEntryKey, createFolderEntryKey, isFolderEntry } from '../../app/entryKey'; +import type { DocumentId, FolderId } from '../../types/identifiers'; interface FolderEntry { - id: string | number; + id: FolderId; [key: string]: unknown; } interface DocumentEntry { - id: string | number; + id: DocumentId; [key: string]: unknown; } interface NavigableRow { key: string; type: 'folder' | 'document'; - id: string | number; + id: FolderId | DocumentId; } interface UseDocumentsSelectionOptions { @@ -25,19 +26,19 @@ interface UseDocumentsSelectionOptions { visibleRowKeySet: Set; selectedEntries: string[]; selectionAnchorRef: { current: string | null }; - promoteSelectionOrderRaw: (id: string | number) => void; - setFocusedDocumentId: (id: string | number | null) => void; - setActivePreviewId: (id: string | number | null) => void; + promoteSelectionOrderRaw: (id: DocumentId) => void; + setFocusedDocumentId: (id: DocumentId | null) => void; + setActivePreviewId: (id: DocumentId | null) => void; clearSelection: () => void; - focusedDocumentId: string | number | null; + focusedDocumentId: DocumentId | null; setFocusedRowKey: (value: string | null | ((current: string | null) => string | null)) => void; focusedRowKey: string | null; } const useDocumentsSelection = ({ showingSearchResults, - currentSubfolders, - visibleDocuments, + currentSubfolders = [], + visibleDocuments = [], configureSelectionEnvironment, visibleRowKeySet, selectedEntries, @@ -82,7 +83,7 @@ const useDocumentsSelection = ({ }, [configureSelectionEnvironment, visibleRowKeySet, navigableRowKeys]); const promoteSelectionOrder = useCallback( - (docId: string | number | null) => { + (docId: DocumentId | null) => { if (!docId) return; promoteSelectionOrderRaw(docId); const rowKey = createDocumentEntryKey(docId); @@ -99,7 +100,7 @@ const useDocumentsSelection = ({ clearSelection(); }, [clearSelection]); - const prevFocusedDocIdRef = useRef(focusedDocumentId); + const prevFocusedDocIdRef = useRef(focusedDocumentId); useEffect(() => { const previous = prevFocusedDocIdRef.current; if (previous === focusedDocumentId) { @@ -109,7 +110,7 @@ const useDocumentsSelection = ({ if (focusedDocumentId) { setFocusedRowKey(createDocumentEntryKey(focusedDocumentId)); } else { - setFocusedRowKey((current) => (isFolderEntry(current) ? current : null)); + setFocusedRowKey((current) => (current && isFolderEntry(current) ? current : null)); } }, [focusedDocumentId, setFocusedRowKey]); diff --git a/frontend/src/documents/panel/DocumentsPanel.tsx b/frontend/src/documents/panel/DocumentsPanel.tsx index 175add7..2079f7d 100644 --- a/frontend/src/documents/panel/DocumentsPanel.tsx +++ b/frontend/src/documents/panel/DocumentsPanel.tsx @@ -18,6 +18,7 @@ import DocumentsPanelHeader, { import { SelectionFloatingPanel } from '../SelectionFloatingActions'; import { createDocumentsTableHeaderActions } from './DocumentsToolbar'; import { useDocumentsFilter } from '../context/DocumentsFilterContext'; +import type { Identifier } from '../../types/identifiers'; const DEFAULT_GRID_ICON_SIZE = 144; @@ -73,7 +74,7 @@ const DocumentsPanelInner: React.FC = ({ documentLinks, ensureDownloadUrl, deskWorkspaceProps = null, - onRefresh = () => {}, + onRefresh = () => { }, sortField, sortDirection, onSortFieldChange, @@ -108,8 +109,8 @@ const DocumentsPanelInner: React.FC = ({ () => Array.isArray(searchResultIds) ? searchResultIds - .map((id) => documentLookup?.get?.(id) || null) - .filter((doc): doc is Record => Boolean(doc)) + .map((id) => documentLookup?.get?.(id) || null) + .filter((doc): doc is Record => Boolean(doc)) : null, [searchResultIds, documentLookup], ); @@ -256,8 +257,7 @@ const DocumentsPanelInner: React.FC = ({ const isGridView = viewMode === 'grid'; const isDeskView = viewMode === 'desk'; - type Identifier = string | number; -type ZoomSource = { url: string; alt?: string | null; mimeType?: string | null }; + type ZoomSource = { url: string; alt?: string | null; mimeType?: string | null }; const [previewDocId, setPreviewDocId] = useState(null); @@ -499,7 +499,7 @@ type ZoomSource = { url: string; alt?: string | null; mimeType?: string | null } setFocusedRowKey(targetRow.key); handleEntrySelection(targetRow.key, { shiftKey, - preventDefault: () => {}, + preventDefault: () => { }, }); }, [ diff --git a/frontend/src/documents/panel/DocumentsPanelHeader.tsx b/frontend/src/documents/panel/DocumentsPanelHeader.tsx index b58c73e..cae6b80 100644 --- a/frontend/src/documents/panel/DocumentsPanelHeader.tsx +++ b/frontend/src/documents/panel/DocumentsPanelHeader.tsx @@ -2,8 +2,7 @@ import React from 'react'; import type { ReactNode } from 'react'; import PanelHeader from '../../ui/PanelHeader'; import BreadcrumbTrail from '../../ui/BreadcrumbTrail'; - -type Identifier = string | number; +import type { Identifier } from '../../types/identifiers'; export interface DocumentsHeaderBreadcrumb { id?: Identifier; @@ -40,12 +39,12 @@ const DocumentsPanelHeader: React.FC = ({ const lastIndex = breadcrumbEntries.length - 1; const trailEntries = breadcrumbEntries.length ? breadcrumbEntries.map((crumb, index) => ({ - id: crumb.id ?? index, - label: crumb.name ?? crumb.label ?? crumb.title ?? '', - onClick: index < lastIndex && onBreadcrumbClick - ? () => onBreadcrumbClick(crumb) - : null, - })) + id: crumb.id ?? index, + label: crumb.name ?? crumb.label ?? crumb.title ?? '', + onClick: index < lastIndex && onBreadcrumbClick + ? () => onBreadcrumbClick(crumb) + : null, + })) : [{ id: 'current-location', label: header.title }]; const headerTitle = ( diff --git a/frontend/src/documents/tagTransfer.ts b/frontend/src/documents/tagTransfer.ts index 9555ebd..5c74201 100644 --- a/frontend/src/documents/tagTransfer.ts +++ b/frontend/src/documents/tagTransfer.ts @@ -1,14 +1,16 @@ +import type { DocumentId, TagId } from '../types/identifiers'; + const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag']; const TAG_TEXT_MIME_TYPE = 'text/plain'; interface TagPayload { - id: string | number; + id: TagId; label: string; - sourceDocId: string | number | null; + sourceDocId: DocumentId | null; } interface TagLike { - id?: string | number; + id?: TagId; label?: string | null; } @@ -21,7 +23,10 @@ const serializePayload = (payload: TagPayload): string | null => { } }; -export const createTagTransferPayload = (tag?: TagLike | null, sourceDocId: string | number | null = null): TagPayload | null => { +export const createTagTransferPayload = ( + tag?: TagLike | null, + sourceDocId: DocumentId | null = null, +): TagPayload | null => { if (!tag || tag.id == null) { return null; } @@ -33,7 +38,11 @@ export const createTagTransferPayload = (tag?: TagLike | null, sourceDocId: stri }; }; -export const writeTagTransferData = (dataTransfer: DataTransfer | null, tag: TagLike, sourceDocId: string | number | null = null): void => { +export const writeTagTransferData = ( + dataTransfer: DataTransfer | null, + tag: TagLike, + sourceDocId: DocumentId | null = null, +): void => { if (!dataTransfer) { return; } @@ -122,7 +131,7 @@ export const isTagTransferEvent = (event?: DragEventLike | null): boolean => { if (!types) { return false; } - const typeList = Array.isArray(types) ? [...types] : Array.from(types); + const typeList = Array.isArray(types) ? [...types] : Array.from(types); return TAG_MIME_TYPES.some((type) => typeList.includes(type)); }; diff --git a/frontend/src/documents/useEntryPointer.ts b/frontend/src/documents/useEntryPointer.ts index 35f3cdf..7e9451e 100644 --- a/frontend/src/documents/useEntryPointer.ts +++ b/frontend/src/documents/useEntryPointer.ts @@ -20,7 +20,7 @@ export const isPrimaryPointerEvent = (event?: PointerEventLike | null): boolean export type EntryType = 'document' | 'folder'; export interface WorkspaceEntry { - id: string | number; + id: string; key?: string; type: EntryType; [key: string]: unknown; @@ -28,7 +28,7 @@ export interface WorkspaceEntry { interface UseEntryPointerOptions { onSelectEntry?: (entry: WorkspaceEntry, event?: PointerEventLike | null, metadata?: EntryPointerMetadata) => void; - onDocumentActivate?: (id: string | number, metadata?: EntryPointerMetadata) => void; + onDocumentActivate?: (id: string, metadata?: EntryPointerMetadata) => void; } export interface EntryPointerMetadata { @@ -36,7 +36,7 @@ export interface EntryPointerMetadata { primaryClick: boolean; rowKey: string; type: EntryType; - id: string | number; + id: string; } export const useEntryPointer = ({ diff --git a/frontend/src/documents/useInlineRename.ts b/frontend/src/documents/useInlineRename.ts index d5863a2..e50293d 100644 --- a/frontend/src/documents/useInlineRename.ts +++ b/frontend/src/documents/useInlineRename.ts @@ -13,22 +13,22 @@ type FocusableInput = (HTMLInputElement | HTMLTextAreaElement) & { type InlineRenameOptions = { getCurrentValue?: (entity: TEntity) => string | null; - getEntityId?: (entity: TEntity) => string | number | null; + getEntityId?: (entity: TEntity) => string | null; }; type InlineRenameHandler = ( - id: string | number, + id: string, value: string, ) => boolean | void | Promise; type InlineRenameReturn = { - editingId: string | number | null; + editingId: string | null; draftValue: string; setDraftValue: Dispatch>; beginEditing: (entity?: TEntity | null, event?: SyntheticEvent | Event) => void; cancelEditing: (event?: SyntheticEvent | Event) => void; submitEditing: (entity?: TEntity | null) => Promise; - savingId: string | number | null; + savingId: string | null; attachInputRef: (node: FocusableInput | null) => void; }; @@ -51,18 +51,18 @@ const focusInput = (node: FocusableInput | null) => { const identity = (value: unknown) => value as string; const defaultGetEntityId = (entity?: T | null) => - (entity as { id?: string | number } | null)?.id ?? null; + (entity as { id?: string } | null)?.id ?? null; const useInlineRename = ( onRename?: InlineRenameHandler, { getCurrentValue = identity as (entity: TEntity) => string | null, - getEntityId = defaultGetEntityId as (entity: TEntity) => string | number | null, + getEntityId = defaultGetEntityId as (entity: TEntity) => string | null, }: InlineRenameOptions = {}, ): InlineRenameReturn => { - const [editingId, setEditingId] = useState(null); + const [editingId, setEditingId] = useState(null); const [draftValue, setDraftValue] = useState(''); - const [savingId, setSavingId] = useState(null); + const [savingId, setSavingId] = useState(null); const inputRef = useRef(null); const resetState = useCallback(() => { diff --git a/frontend/src/folders/FolderManagerContext.tsx b/frontend/src/folders/FolderManagerContext.tsx index 85fcc83..e258b7c 100644 --- a/frontend/src/folders/FolderManagerContext.tsx +++ b/frontend/src/folders/FolderManagerContext.tsx @@ -17,7 +17,7 @@ const FolderManagerContext = createContext(defaultManager); interface FolderManagerProviderProps { folderNodes?: Map; - ensureFolderData?: (folderId: string | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise; + ensureFolderData?: (folderId: FolderId | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise; children: ReactNode; } diff --git a/frontend/src/hooks/documents/useCorrespondents.ts b/frontend/src/hooks/documents/useCorrespondents.ts index 9a4541d..bbeb998 100644 --- a/frontend/src/hooks/documents/useCorrespondents.ts +++ b/frontend/src/hooks/documents/useCorrespondents.ts @@ -8,7 +8,7 @@ type ApiClient = { }; interface CorrespondentEntry { - id?: string | number; + id?: string; name?: string; [key: string]: unknown; } @@ -17,7 +17,7 @@ interface UseCorrespondentsOptions { apiClient: ApiClient; notifyApiError: (error: unknown, fallback: string) => void; setStatusMessage: (message: string, variant?: string) => void; - tenantIdRef: MutableRefObject; + tenantIdRef: MutableRefObject; mapDocumentCaches?: (mapper: (doc: any) => any) => void; } @@ -47,7 +47,7 @@ const useCorrespondents = ({ }, [apiClient, notifyApiError, tenantIdRef]); const handleCorrespondentUpdate = useCallback( - async (correspondentId: string | number, changes: { name?: string }) => { + async (correspondentId: string, changes: { name?: string }) => { if (correspondentId == null) { throw new Error('Missing correspondent identifier.'); } @@ -100,7 +100,7 @@ const useCorrespondents = ({ ); const handleCorrespondentDelete = useCallback( - async (correspondentId: string | number) => { + async (correspondentId: string) => { if (correspondentId == null) { throw new Error('Missing correspondent identifier.'); } diff --git a/frontend/src/hooks/documents/useDocumentCorrespondentActions.ts b/frontend/src/hooks/documents/useDocumentCorrespondentActions.ts index 22267b3..c835cad 100644 --- a/frontend/src/hooks/documents/useDocumentCorrespondentActions.ts +++ b/frontend/src/hooks/documents/useDocumentCorrespondentActions.ts @@ -1,4 +1,5 @@ import { useCallback, useMemo } from 'react'; +import type { Identifier } from '../../types/identifiers'; type ApiClient = { @@ -7,13 +8,11 @@ type ApiClient = { }; interface CorrespondentOption { - id?: string | number; + id?: string; name?: string; [key: string]: unknown; } -type Identifier = string | number; - interface UseDocumentCorrespondentActionsArgs { apiClient: ApiClient; correspondents: CorrespondentOption[]; @@ -135,7 +134,7 @@ const useDocumentCorrespondentActions = ({ }; const handleCorrespondentAdd = useCallback( - async ({ document, name, input = null, option = null }: { document?: { id?: string | number }; name?: string; input?: HTMLInputElement | null; option?: CorrespondentOption | string | null }) => { + async ({ document, name, input = null, option = null }: { document?: { id?: string }; name?: string; input?: HTMLInputElement | null; option?: CorrespondentOption | string | null }) => { if (!document?.id) { throw new Error('Missing document for correspondent assignment.'); } diff --git a/frontend/src/hooks/documents/useDocumentDragHandlers.ts b/frontend/src/hooks/documents/useDocumentDragHandlers.ts index d598466..ab9bd9c 100644 --- a/frontend/src/hooks/documents/useDocumentDragHandlers.ts +++ b/frontend/src/hooks/documents/useDocumentDragHandlers.ts @@ -1,9 +1,9 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import type { DragEvent } from 'react'; import { createDocumentEntryKey, createFolderEntryKey } from '../../app/entryKey'; +import type { FolderId, Identifier } from '../../types/identifiers'; -type Identifier = string | number; -type FolderIdentifier = string | 'root'; +type FolderIdentifier = FolderId | 'root'; type FolderInput = FolderIdentifier | number; interface DocumentLike { @@ -14,7 +14,7 @@ interface DocumentLike { type ApplySelectionFn = ( keys: string[], - options?: { anchor?: string | null; interactedKeys?: string[] }, + options?: { anchor: string | null; interactedKeys?: string[] }, ) => void; type HandleEntrySelectionFn = ( diff --git a/frontend/src/hooks/documents/useDocumentMutations.ts b/frontend/src/hooks/documents/useDocumentMutations.ts index 2f333a2..713fbc4 100644 --- a/frontend/src/hooks/documents/useDocumentMutations.ts +++ b/frontend/src/hooks/documents/useDocumentMutations.ts @@ -13,9 +13,9 @@ import { trashDocument, updateDocument, } from '../../lib/apiClient'; +import type { DocumentId, FolderId as FolderIdentifier } from '../../types/identifiers'; -type DocumentId = string | number; -type FolderId = DocumentId | 'root'; +type FolderId = FolderIdentifier | 'root'; type NullableFolderId = FolderId | null; type StatusLevel = 'success' | 'error' | 'info' | string; diff --git a/frontend/src/hooks/documents/useDocumentTagging.ts b/frontend/src/hooks/documents/useDocumentTagging.ts index 404cb54..d01968c 100644 --- a/frontend/src/hooks/documents/useDocumentTagging.ts +++ b/frontend/src/hooks/documents/useDocumentTagging.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; -type Identifier = string | number; +import type { Identifier } from '../../types/identifiers'; interface TagRecord { id?: Identifier; @@ -79,59 +79,59 @@ const useDocumentTagging = ({ } try { - if (action === 'add') { - const createdIds: Identifier[] = []; - const createdTags: TagRecord[] = []; - for (const label of normalized) { - let tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase()) || null; - if (!tag) { - const payload = tagManager.buildPayload({ label }); - const response = await apiClient.post<{ id?: Identifier; label: string }>('/tags', payload); - tag = 'data' in response ? response.data : response; - await refreshTags(); + if (action === 'add') { + const createdIds: Identifier[] = []; + const createdTags: TagRecord[] = []; + for (const label of normalized) { + let tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase()) || null; + if (!tag) { + const payload = tagManager.buildPayload({ label }); + const response = await apiClient.post<{ id?: Identifier; label: string }>('/tags', payload); + tag = 'data' in response ? response.data : response; + await refreshTags(); + } + createdIds.push(tag.id); + createdTags.push(tag); } - createdIds.push(tag.id); - createdTags.push(tag); - } - tagIds = Array.from(new Set(createdIds)); + tagIds = Array.from(new Set(createdIds)); - if (updateDocumentCaches) { - const tagById = new Map(); - tags.forEach((tag) => { - if (tag?.id != null) { - tagById.set(tag.id, tag); - } - }); - createdTags.forEach((tag) => { - if (tag?.id != null) { - tagById.set(tag.id, tag); - } - }); - targetDocumentIds.forEach((docId) => { - tagIds.forEach((tagId) => { - const cachedTag = tagById.get(tagId); - if (!cachedTag) { - return; + if (updateDocumentCaches) { + const tagById = new Map(); + tags.forEach((tag) => { + if (tag?.id != null) { + tagById.set(tag.id, tag); } - updateDocumentCaches(docId, (doc) => { - if (!doc) { - return doc; + }); + createdTags.forEach((tag) => { + if (tag?.id != null) { + tagById.set(tag.id, tag); + } + }); + targetDocumentIds.forEach((docId) => { + tagIds.forEach((tagId) => { + const cachedTag = tagById.get(tagId); + if (!cachedTag) { + return; } - const currentTags = Array.isArray((doc as any).tags) ? (doc as any).tags : []; - if (currentTags.some((entry: any) => entry?.id === tagId)) { - return doc; - } - return { - ...(doc as any), - tags: [...currentTags, { ...cachedTag }], - }; + updateDocumentCaches(docId, (doc) => { + if (!doc) { + return doc; + } + const currentTags = Array.isArray((doc as any).tags) ? (doc as any).tags : []; + if (currentTags.some((entry: any) => entry?.id === tagId)) { + return doc; + } + return { + ...(doc as any), + tags: [...currentTags, { ...cachedTag }], + }; + }); }); }); - }); + } } - } - tagIds = Array.from(new Set(tagIds)); + tagIds = Array.from(new Set(tagIds)); if (!tagIds.length) { return { ok: false, reason: 'no-tags' }; @@ -205,8 +205,7 @@ const useDocumentTagging = ({ if (result?.ok) { const { tagCount, docsCount } = result; setStatusMessage( - `Assigned ${tagCount} tag${tagCount === 1 ? '' : 's'} to ${docsCount} document${ - docsCount === 1 ? '' : 's' + `Assigned ${tagCount} tag${tagCount === 1 ? '' : 's'} to ${docsCount} document${docsCount === 1 ? '' : 's' }.`, 'success', ); diff --git a/frontend/src/hooks/documents/useDocumentUploads.ts b/frontend/src/hooks/documents/useDocumentUploads.ts index c4a3791..f239c74 100644 --- a/frontend/src/hooks/documents/useDocumentUploads.ts +++ b/frontend/src/hooks/documents/useDocumentUploads.ts @@ -3,8 +3,8 @@ import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; import useFileDrop from './useFileDrop'; import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/workspaceUtils'; import { fetchDocument } from '../../lib/apiClient'; +import type { Identifier } from '../../types/identifiers'; -type Identifier = string | number; type FolderId = Identifier | 'root' | null; type FileEntry = { diff --git a/frontend/src/hooks/documents/useDocuments.ts b/frontend/src/hooks/documents/useDocuments.ts index 8b82955..af8957e 100644 --- a/frontend/src/hooks/documents/useDocuments.ts +++ b/frontend/src/hooks/documents/useDocuments.ts @@ -7,8 +7,7 @@ import { useState, } from 'react'; import DocumentsManager from '../../documents/DocumentsManager'; - -type DocumentId = string | number; +import type { DocumentId } from '../../types/identifiers'; interface DocumentLike { id?: DocumentId; diff --git a/frontend/src/hooks/documents/useDocumentsWorkspace.ts b/frontend/src/hooks/documents/useDocumentsWorkspace.ts index 37fb461..95bd42b 100644 --- a/frontend/src/hooks/documents/useDocumentsWorkspace.ts +++ b/frontend/src/hooks/documents/useDocumentsWorkspace.ts @@ -52,6 +52,7 @@ import useWorkspaceTaxonomies from './useWorkspaceTaxonomies'; import useWorkspaceBreadcrumbs from './useWorkspaceBreadcrumbs'; import useWorkspaceDeskProps from './useWorkspaceDeskProps'; import useWorkspaceSelectionSync from './useWorkspaceSelectionSync'; +import type { DocumentId, FolderId as FolderIdentifier, Identifier } from '../../types/identifiers'; const EntryType = Object.freeze({ document: 'document', @@ -60,9 +61,7 @@ const EntryType = Object.freeze({ const noop = () => { }; -type Identifier = string | number; -type DocumentId = Identifier; -type FolderId = Identifier | 'root'; +type FolderId = FolderIdentifier | 'root'; interface DocumentLike { id?: DocumentId | null; diff --git a/frontend/src/hooks/documents/useFileDrop.ts b/frontend/src/hooks/documents/useFileDrop.ts index 8ab6a87..0b02fae 100644 --- a/frontend/src/hooks/documents/useFileDrop.ts +++ b/frontend/src/hooks/documents/useFileDrop.ts @@ -1,6 +1,6 @@ import { MutableRefObject, useEffect } from 'react'; -type FolderId = string | number | 'root' | null; +type FolderId = string | 'root' | null; interface DropOverlayState { active: boolean; diff --git a/frontend/src/hooks/documents/useFolderTree.ts b/frontend/src/hooks/documents/useFolderTree.ts index 7ac8dd4..b9b1e0e 100644 --- a/frontend/src/hooks/documents/useFolderTree.ts +++ b/frontend/src/hooks/documents/useFolderTree.ts @@ -8,9 +8,9 @@ import { createDocumentEntryKey, createFolderEntryKey, } from '../../app/entryKey'; +import type { FolderId as FolderIdentifier, Identifier } from '../../types/identifiers'; -type Identifier = string | number; -type FolderId = Identifier | 'root'; +type FolderId = FolderIdentifier | 'root'; interface DocumentLike { id?: Identifier | null; @@ -126,8 +126,8 @@ const useFolderTree = ({ .filter(Boolean), ); - let nextDocKeys = []; - let mergedSelection = []; + let nextDocKeys: string[] = []; + let mergedSelection: string[] = []; setSelectedEntries((previous) => { const previousFolderKeys = previous @@ -140,9 +140,11 @@ const useFolderTree = ({ }); const nextFocus = (() => { - const currentFocusedKey = createDocumentEntryKey(focusedDocumentId); - if (currentFocusedKey && availableDocKeySet.has(currentFocusedKey)) { - return focusedDocumentId; + if (focusedDocumentId) { + const currentFocusedKey = createDocumentEntryKey(focusedDocumentId); + if (currentFocusedKey && availableDocKeySet.has(currentFocusedKey)) { + return focusedDocumentId; + } } if (nextDocKeys.length) { const lastDocKey = nextDocKeys[nextDocKeys.length - 1]; @@ -172,7 +174,7 @@ const useFolderTree = ({ if (!targetId || targetId === 'root') { setFolderNodes((prev: Map) => { const root = prev.get('root'); - if (root?.expanded) return prev; + if (!root || root.expanded) return prev; const next = new Map(prev); next.set('root', { ...root, expanded: true }); return next; diff --git a/frontend/src/hooks/documents/useFolderTreeActions.ts b/frontend/src/hooks/documents/useFolderTreeActions.ts index 781840b..96c2ff3 100644 --- a/frontend/src/hooks/documents/useFolderTreeActions.ts +++ b/frontend/src/hooks/documents/useFolderTreeActions.ts @@ -7,8 +7,8 @@ import { moveFolder as moveFolderRequest, renameFolder as renameFolderRequest, } from '../../lib/apiClient'; +import type { FolderId } from '../../types/identifiers'; -type FolderId = string | number; type FolderKey = FolderId | 'root'; interface FolderNode { diff --git a/frontend/src/hooks/documents/useTags.ts b/frontend/src/hooks/documents/useTags.ts index b00e02d..815cef8 100644 --- a/frontend/src/hooks/documents/useTags.ts +++ b/frontend/src/hooks/documents/useTags.ts @@ -1,4 +1,5 @@ import { MutableRefObject, useCallback, useState } from 'react'; +import type { TagId, TenantId } from '../../types/identifiers'; type ApiClient = { get: (path: string) => Promise<{ data: unknown }> @@ -12,7 +13,7 @@ interface TagManagerInterface { } interface TagEntry { - id?: string | number; + id?: TagId; label?: string; color?: string | null; [key: string]: unknown; @@ -23,8 +24,8 @@ interface UseTagsOptions { notifyApiError: (error: unknown, fallback: string) => void; setStatusMessage: (message: string, variant?: string) => void; tagManager: TagManagerInterface; - tenantIdRef: MutableRefObject; - setActiveTagFilters: (updater: (prev: Array) => Array) => void; + tenantIdRef: MutableRefObject; + setActiveTagFilters: (updater: (prev: Array) => Array) => void; mapDocumentCaches?: (mapper: (doc: any) => any) => void; } @@ -56,7 +57,7 @@ const useTags = ({ }, [apiClient, notifyApiError, tenantIdRef]); const handleTagUpdate = useCallback( - async (tagId: string | number, changes: { label?: string; color?: string | null }) => { + async (tagId: TagId, changes: { label?: string; color?: string | null }) => { if (tagId == null) { throw new Error('Missing tag identifier.'); } @@ -104,7 +105,7 @@ const useTags = ({ ); const handleTagDelete = useCallback( - async (tagId: string | number) => { + async (tagId: TagId) => { if (tagId == null) { throw new Error('Missing tag identifier.'); } diff --git a/frontend/src/hooks/documents/useTenantManager.ts b/frontend/src/hooks/documents/useTenantManager.ts index 192e181..05766f0 100644 --- a/frontend/src/hooks/documents/useTenantManager.ts +++ b/frontend/src/hooks/documents/useTenantManager.ts @@ -1,5 +1,6 @@ import { MutableRefObject, useCallback } from 'react'; import type { NavigateFunction } from 'react-router-dom'; +import type { FolderId, TenantId } from '../../types/identifiers'; interface ApiClient { get: (path: string) => Promise<{ data: unknown }>; @@ -8,24 +9,24 @@ interface ApiClient { } interface TenantOption { - id?: string | number; + id?: TenantId; name?: string; } interface UseTenantManagerOptions { apiClient: ApiClient; appDispatch: (action: any) => void; - currentTenantId: string | number | null; + currentTenantId: TenantId | null; resetWorkspaceState: () => void; setStatusMessage: (message: string, variant?: string) => void; notifyApiError: (error: unknown, message: string) => void; refreshTags: () => Promise; refreshCorrespondents: () => Promise; - loadFolder: (folderId: string, options?: { preserveSearch?: boolean }) => Promise; + loadFolder: (folderId: FolderId, options?: { preserveSearch?: boolean }) => Promise; handleDocumentsViewModeChange: (mode: string) => void; navigate: NavigateFunction; tokenRef?: MutableRefObject; - tenantIdRef?: MutableRefObject; + tenantIdRef?: MutableRefObject; } const useTenantManager = ({ diff --git a/frontend/src/hooks/documents/useWorkspaceBreadcrumbs.ts b/frontend/src/hooks/documents/useWorkspaceBreadcrumbs.ts index 9d422e2..060afdf 100644 --- a/frontend/src/hooks/documents/useWorkspaceBreadcrumbs.ts +++ b/frontend/src/hooks/documents/useWorkspaceBreadcrumbs.ts @@ -1,8 +1,8 @@ import React, { useEffect, useMemo } from 'react'; import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils'; +import type { FolderId as FolderIdentifier } from '../../types/identifiers'; -type Identifier = string | number; -type FolderId = Identifier | 'root'; +type FolderId = FolderIdentifier | 'root'; interface UseWorkspaceBreadcrumbsArgs { selectedFolder: FolderId | null; diff --git a/frontend/src/hooks/documents/useWorkspaceDeskProps.ts b/frontend/src/hooks/documents/useWorkspaceDeskProps.ts index 3a37360..6fe6acb 100644 --- a/frontend/src/hooks/documents/useWorkspaceDeskProps.ts +++ b/frontend/src/hooks/documents/useWorkspaceDeskProps.ts @@ -1,11 +1,10 @@ import { useCallback, useMemo } from 'react'; import type { MutableRefObject } from 'react'; import { createDocumentEntryKey } from '../../app/entryKey'; - -type Identifier = string | number; +import type { Identifier } from '../../types/identifiers'; interface ApplySelectionFn { - (keys: string[], options?: { anchor?: string | null; interactedKeys?: string[] }): unknown; + (keys: string[], options?: { anchor: string | null; interactedKeys?: string[] }): unknown; } interface UseWorkspaceDeskPropsArgs { @@ -17,8 +16,8 @@ interface UseWorkspaceDeskPropsArgs { applySelection: ApplySelectionFn; showingSearchResults: boolean; searchQuery: string; - activeTagFilters: Array; - activeCorrespondentFilters: Array; + activeTagFilters: Array; + activeCorrespondentFilters: Array; selectedFolder: Identifier | 'root' | null; promoteSelectionOrder: () => void; handleDocumentTagDrop: (docId: Identifier, tagId: Identifier) => Promise | void; diff --git a/frontend/src/hooks/documents/useWorkspaceSelectionSync.ts b/frontend/src/hooks/documents/useWorkspaceSelectionSync.ts index 9f3236a..b3e2b15 100644 --- a/frontend/src/hooks/documents/useWorkspaceSelectionSync.ts +++ b/frontend/src/hooks/documents/useWorkspaceSelectionSync.ts @@ -1,14 +1,13 @@ import { useEffect } from 'react'; import type { MutableRefObject } from 'react'; - -type Identifier = string | number; +import type { Identifier } from '../../types/identifiers'; interface UseWorkspaceSelectionSyncArgs { showingSearchResults: boolean; searchQuery: string; - setSelectedEntries: (entries: Array) => void; - setSelectionOrder: (order: Array) => void; - selectionOrderRef: MutableRefObject>; + setSelectedEntries: (entries: Array) => void; + setSelectionOrder: (order: Array) => void; + selectionOrderRef: MutableRefObject>; selectionAnchorRef: MutableRefObject; setFocusedDocumentId: (id: Identifier | null) => void; selectedDocumentIds: Identifier[]; diff --git a/frontend/src/hooks/documents/useWorkspaceTaxonomies.ts b/frontend/src/hooks/documents/useWorkspaceTaxonomies.ts index f77756c..261aac1 100644 --- a/frontend/src/hooks/documents/useWorkspaceTaxonomies.ts +++ b/frontend/src/hooks/documents/useWorkspaceTaxonomies.ts @@ -5,8 +5,7 @@ import TagManager from '../../tag_manager'; import useCorrespondents from './useCorrespondents'; import useDocumentCorrespondentActions from './useDocumentCorrespondentActions'; import useTags from './useTags'; - -type Identifier = string | number; +import type { Identifier } from '../../types/identifiers'; interface UseWorkspaceTaxonomiesArgs { apiClient: any; diff --git a/frontend/src/hooks/useAssetNavigator.ts b/frontend/src/hooks/useAssetNavigator.ts index decf5de..112af21 100644 --- a/frontend/src/hooks/useAssetNavigator.ts +++ b/frontend/src/hooks/useAssetNavigator.ts @@ -1,7 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { resolveAssetUrl } from '../asset_manager'; - -type Identifier = string | number; +import type { Identifier } from '../types/identifiers'; type DocumentLike = { id?: Identifier; @@ -27,7 +26,7 @@ type AssetLike = { type EnsureAssetUrl = ( documentId: Identifier, asset: AssetLike, - options?: { force?: boolean; [key: string]: unknown }, + options?: { force?: boolean;[key: string]: unknown }, ) => Promise; type GetAsset = (document: DocumentLike, assetType: string) => AssetLike | null; @@ -92,7 +91,7 @@ export const useAssetNavigator = ({ let cancelled = false; setIsLoading(true); ensureAssetUrl(documentId, asset, { force: true }) - .catch(() => {}) + .catch(() => { }) .finally(() => { if (!cancelled) { setIsLoading(false); diff --git a/frontend/src/lib/apiTypes.ts b/frontend/src/lib/apiTypes.ts index 3c2638c..1abb117 100644 --- a/frontend/src/lib/apiTypes.ts +++ b/frontend/src/lib/apiTypes.ts @@ -1,6 +1,7 @@ // Types aligned with OpenAPI schemas for common endpoints. +import type { Identifier } from '../types/identifiers'; -export type Identifier = string | number; +export type { Identifier }; export interface DownloadLink { url: string; diff --git a/frontend/src/login/LoginView.tsx b/frontend/src/login/LoginView.tsx index 6ee1b44..3d5910a 100644 --- a/frontend/src/login/LoginView.tsx +++ b/frontend/src/login/LoginView.tsx @@ -12,7 +12,7 @@ const StatusBanner: React.FC = ({ status }) => { }; interface TenantOption { - id?: string | number; + id?: string; name?: string; } @@ -25,7 +25,7 @@ interface LoginViewProps { tenantSelection?: TenantSelectionState | null; onSelectTenant?: (tenant: TenantOption) => void; onCancelSelection?: () => void; - selectingTenantId?: string | number | null; + selectingTenantId?: string | null; onPasskeyLogin?: (username: string) => void; onSignup?: (username: string) => void; passkeySupported?: boolean; @@ -88,88 +88,88 @@ const LoginView: React.FC = ({ Papercrate logo -

Papercrate

-
- -
- {hasTenantSelection ? ( -
-

Select a tenant to finish signing in.

-
- {tenantSelection?.tenants?.map((tenant) => ( - - ))} -
- + width={72} + height={72} + decoding="async" + loading="lazy" + /> +

Papercrate

- ) : ( - <> -

Use your registered passkey to sign in or create a new account.

-
- - setUsername(event.target.value)} - placeholder="Username" - autoComplete="username" - disabled={passkeyLoading || magicLoginPending} - required - /> - {passkeySupported ? ( + +
+ {hasTenantSelection ? ( +
+

Select a tenant to finish signing in.

+
+ {tenantSelection?.tenants?.map((tenant) => ( + + ))} +
- ) : ( -

Passkeys are not supported in this browser.

- )} - - {signupSupported ? ( - - ) : null} - - )} - +
+ ) : ( + <> +

Use your registered passkey to sign in or create a new account.

+
+ + setUsername(event.target.value)} + placeholder="Username" + autoComplete="username" + disabled={passkeyLoading || magicLoginPending} + required + /> + {passkeySupported ? ( + + ) : ( +

Passkeys are not supported in this browser.

+ )} +
+ {signupSupported ? ( + + ) : null} + + )} + +
- ); }; diff --git a/frontend/src/preview/DocumentViewerLayout.tsx b/frontend/src/preview/DocumentViewerLayout.tsx index b23f133..73760bd 100644 --- a/frontend/src/preview/DocumentViewerLayout.tsx +++ b/frontend/src/preview/DocumentViewerLayout.tsx @@ -5,7 +5,7 @@ import { DownloadIcon } from '../ui/icons'; import PdfViewer from './PdfViewer'; interface DocumentLike { - id?: string | number; + id?: string; title?: string; mime_type?: string; filename?: string; @@ -40,7 +40,7 @@ interface DocumentViewerLayoutProps { summaryProps?: Record; metadataPayload?: unknown; contentTabConfig?: ContentTabConfig | null; - resetKey?: string | number | null; + resetKey?: string | null; classNamePrefix?: string; defaultTabId?: string; infoPanelProps?: Record; @@ -211,12 +211,12 @@ const DocumentViewerLayout = ({ const stackedLeadingTabs = useMemo(() => ( isStacked ? [ - { - id: 'preview', - label: 'Preview', - render: () => renderViewportPane(), - }, - ] + { + id: 'preview', + label: 'Preview', + render: () => renderViewportPane(), + }, + ] : [] ), [isStacked, renderViewportPane]); diff --git a/frontend/src/preview/DocumentViewerPanel.tsx b/frontend/src/preview/DocumentViewerPanel.tsx index 3926ba6..287e8df 100644 --- a/frontend/src/preview/DocumentViewerPanel.tsx +++ b/frontend/src/preview/DocumentViewerPanel.tsx @@ -28,14 +28,15 @@ import BreadcrumbTrail from '../ui/BreadcrumbTrail'; import DocumentViewerLayout from './DocumentViewerLayout'; import useViewerLayoutMode from './useViewerLayoutMode'; import { usePanelResizeBindings } from '../app/PanelManagerContext'; +import type { DocumentId, FolderId } from '../types/identifiers'; interface DocumentLike { - id?: string | number; + id?: DocumentId; title?: string; mime_type?: string | null; issued_at?: string | null; - folder_id?: string | null; - correspondents?: Array<{ id?: string | number; name?: string }>; + folder_id?: FolderId | null; + correspondents?: Array<{ id?: string; name?: string }>; current_version?: { version_number?: number; download?: { url?: string | null; expires_at?: number } | null; @@ -51,7 +52,7 @@ interface DocumentLike { } interface AssetLike { - id?: string | number; + id?: string; url?: string | null; metadata?: Record | null; [key: string]: unknown; @@ -59,16 +60,16 @@ interface AssetLike { interface DocumentViewerPanelProps extends DocumentSummarySectionProps { document: DocumentLike | null; - ensureAssetUrl?: (docId: string | number, asset: AssetLike, options?: { force?: boolean }) => Promise; + ensureAssetUrl?: (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise; getDocumentAsset?: (doc: DocumentLike | null, type: string) => AssetLike | null; - ensurePreviewData?: (docId: string | number, options?: { signal?: AbortSignal }) => Promise; + ensurePreviewData?: (docId: DocumentId, options?: { signal?: AbortSignal }) => Promise; notifyApiError?: (error: unknown, fallbackMessage?: string) => void; sidebarToggle?: ReactNode; onClosePanel?: () => void; - resolveFolderPath?: (doc: DocumentLike | null) => Array<{ id?: string | number; name?: string }>; + resolveFolderPath?: (doc: DocumentLike | null) => Array<{ id?: string; name?: string }>; variant?: 'viewer' | 'sidebar'; onCollapsePanel?: () => void; - onMaximizePanel?: (args: { documentIds: Array }) => void; + onMaximizePanel?: (args: { documentIds: Array }) => void; } @@ -163,7 +164,7 @@ const DocumentViewerPanel: React.FC = ({ }, [document, getDocumentAsset]); const navigateToFolder = useCallback( - (folderId) => { + (folderId: FolderId | null) => { const target = folderId == null ? '/documents' : `/documents/folder/${folderId}`; diff --git a/frontend/src/routes/DocumentViewerRoute.tsx b/frontend/src/routes/DocumentViewerRoute.tsx index d03941e..4e6540e 100644 --- a/frontend/src/routes/DocumentViewerRoute.tsx +++ b/frontend/src/routes/DocumentViewerRoute.tsx @@ -2,8 +2,7 @@ import React, { useEffect } from 'react'; import { Navigate, useNavigate, useParams } from 'react-router-dom'; import { useAppShell } from '../appShellContext'; - -type Identifier = string | number; +import type { Identifier } from '../types/identifiers'; interface DocumentViewerRouteContext { previewWorkspaceDocument?: { id?: Identifier } | null; diff --git a/frontend/src/settings/components/CapabilityDropdown.tsx b/frontend/src/settings/components/CapabilityDropdown.tsx index 0bf8841..29a0354 100644 --- a/frontend/src/settings/components/CapabilityDropdown.tsx +++ b/frontend/src/settings/components/CapabilityDropdown.tsx @@ -7,9 +7,7 @@ import { } from 'react'; import type { JSX } from 'react'; import { CheckIcon, ChevronDownIcon } from '../../ui/icons'; - - -type CapabilityValue = string | number; +import type { CapabilityValue } from '../../types/identifiers'; export interface CapabilityDropdownOption { value?: CapabilityValue | null; diff --git a/frontend/src/settings/sections/ApiTokensSection.tsx b/frontend/src/settings/sections/ApiTokensSection.tsx index d92bf58..0803b81 100644 --- a/frontend/src/settings/sections/ApiTokensSection.tsx +++ b/frontend/src/settings/sections/ApiTokensSection.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ChangeEvent, FormEvent } from 'react'; - -type Identifier = string | number; +import type { Identifier } from '../../types/identifiers'; interface ApiTokenEntry { id?: Identifier; @@ -125,21 +124,21 @@ const ApiTokensSection = ({ const capabilitySelectionOptions = useMemo(() => ( Array.isArray(capabilities) ? capabilities.map((capability) => { - const capabilityText = `${capability ?? ''}`; - if (!capabilityText.includes(':')) { - return { value: capability, label: capabilityText }; - } - const [namespace, action] = capabilityText.split(':'); - if (!namespace || !action) { - return { value: capability, label: capabilityText }; - } - const formattedNamespace = `${namespace.charAt(0).toUpperCase()}${namespace.slice(1)}`; - const formattedAction = action.replace(/_/g, ' '); - return { - value: capability, - label: `${formattedNamespace}: ${formattedAction}`, - }; - }) + const capabilityText = `${capability ?? ''}`; + if (!capabilityText.includes(':')) { + return { value: capability, label: capabilityText }; + } + const [namespace, action] = capabilityText.split(':'); + if (!namespace || !action) { + return { value: capability, label: capabilityText }; + } + const formattedNamespace = `${namespace.charAt(0).toUpperCase()}${namespace.slice(1)}`; + const formattedAction = action.replace(/_/g, ' '); + return { + value: capability, + label: `${formattedNamespace}: ${formattedAction}`, + }; + }) : [] ), [capabilities]); diff --git a/frontend/src/settings/sections/CapabilitySetsSection.tsx b/frontend/src/settings/sections/CapabilitySetsSection.tsx index 0945db2..3806f14 100644 --- a/frontend/src/settings/sections/CapabilitySetsSection.tsx +++ b/frontend/src/settings/sections/CapabilitySetsSection.tsx @@ -8,10 +8,7 @@ import React, { import type { SettingsSectionConfig } from '../SettingsModal'; import { IconX } from '../../ui/icons'; import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown'; - - -type CapabilityValue = string | number; -type CapabilitySetId = string | number; +import type { CapabilitySetId, CapabilityValue } from '../../types/identifiers'; interface CapabilitySet { id: CapabilitySetId; diff --git a/frontend/src/settings/sections/PasskeysSection.tsx b/frontend/src/settings/sections/PasskeysSection.tsx index c773a7f..a3dee9f 100644 --- a/frontend/src/settings/sections/PasskeysSection.tsx +++ b/frontend/src/settings/sections/PasskeysSection.tsx @@ -18,10 +18,10 @@ interface PasskeysSectionProps { passkeysSupported?: boolean | null; passkeysLoading?: boolean; registeringPasskey?: boolean; - revokingPasskeyId?: string | number | null; + revokingPasskeyId?: string | null; onRefreshPasskeys?: () => void | Promise; onRegisterPasskey?: (args: { nickname?: string }) => Promise; - onRevokePasskey?: (id: string | number, reason?: string) => Promise; + onRevokePasskey?: (id: string, reason?: string) => Promise; } const PasskeysSection = ({ diff --git a/frontend/src/settings/useApiTokens.ts b/frontend/src/settings/useApiTokens.ts index c950a9a..5e5e511 100644 --- a/frontend/src/settings/useApiTokens.ts +++ b/frontend/src/settings/useApiTokens.ts @@ -6,6 +6,7 @@ import { regenerateApiToken, type ApiTokenRecord, } from '../lib/apiClient'; +import type { ApiTokenId, CapabilitySetId } from '../types/identifiers'; interface ApiTokensResponse { token_info?: ApiTokenRecord; @@ -15,7 +16,7 @@ interface ApiTokensResponse { interface CreateTokenArgs { label?: string; expires_at?: string; - capability_set_id?: string | number; + capability_set_id?: CapabilitySetId; } interface UseApiTokensArgs { @@ -28,13 +29,13 @@ interface UseApiTokensResult { tokens: ApiTokenRecord[]; loading: boolean; creating: boolean; - deletingId: string | number | null; - regeneratingId: string | number | null; + deletingId: ApiTokenId | null; + regeneratingId: ApiTokenId | null; createdSecret: string | null; refresh: () => Promise; create: (args?: CreateTokenArgs) => Promise; - revoke: (tokenId?: string | number | null) => Promise; - regenerate: (tokenId?: string | number | null) => Promise; + revoke: (tokenId?: ApiTokenId | null) => Promise; + regenerate: (tokenId?: ApiTokenId | null) => Promise; dismissSecret: () => void; } @@ -42,8 +43,8 @@ const useApiTokens = ({ notifyApiError, setStatusMessage, token }: UseApiTokensA const [tokens, setTokens] = useState([]); const [loading] = useState(false); const [creating, setCreating] = useState(false); - const [deletingId, setDeletingId] = useState(null); - const [regeneratingId, setRegeneratingId] = useState(null); + const [deletingId, setDeletingId] = useState(null); + const [regeneratingId, setRegeneratingId] = useState(null); const [createdSecret, setCreatedSecret] = useState(null); const refresh = useCallback(async () => { @@ -65,7 +66,7 @@ const useApiTokens = ({ notifyApiError, setStatusMessage, token }: UseApiTokensA } setCreating(true); try { - const payload: { capability_set_id: string | number; label?: string; expires_at?: string } = { capability_set_id }; + const payload: { capability_set_id: CapabilitySetId; label?: string; expires_at?: string } = { capability_set_id }; if (label) { payload.label = label; } @@ -100,7 +101,7 @@ const useApiTokens = ({ notifyApiError, setStatusMessage, token }: UseApiTokensA ); const revoke = useCallback( - async (tokenId?: string | number | null) => { + async (tokenId?: string | null) => { if (!tokenId) { return false; } @@ -121,7 +122,7 @@ const useApiTokens = ({ notifyApiError, setStatusMessage, token }: UseApiTokensA ); const regenerate = useCallback( - async (tokenId?: string | number | null) => { + async (tokenId?: string | null) => { if (!tokenId) { return false; } diff --git a/frontend/src/settings/useCapabilitySets.ts b/frontend/src/settings/useCapabilitySets.ts index 5bef981..31dc014 100644 --- a/frontend/src/settings/useCapabilitySets.ts +++ b/frontend/src/settings/useCapabilitySets.ts @@ -5,8 +5,7 @@ import { listCapabilitySets, updateCapabilitySet as updateCapabilitySetRequest, } from '../lib/apiClient'; - -type Identifier = string | number; +import type { Identifier } from '../types/identifiers'; interface CapabilitySet { id?: Identifier; diff --git a/frontend/src/settings/usePasskeys.ts b/frontend/src/settings/usePasskeys.ts index 18d0821..058178d 100644 --- a/frontend/src/settings/usePasskeys.ts +++ b/frontend/src/settings/usePasskeys.ts @@ -12,6 +12,7 @@ import { listPasskeys, startPasskeyRegistration, } from '../lib/apiClient'; +import type { PasskeyId } from '../types/identifiers'; type StatusMessageFn = (message: string, variant?: string) => void; type NotifyApiErrorFn = (error: unknown, message: string) => void; @@ -27,7 +28,7 @@ type ApiError = { }; export interface PasskeyRecord { - id?: string | number; + id?: PasskeyId; nickname?: string; created_at?: string; createdAt?: string; @@ -79,11 +80,11 @@ interface UsePasskeysResult { passkeysSupported: boolean | null; passkeysLoading: boolean; registeringPasskey: boolean; - revokingPasskeyId: string | number | null; + revokingPasskeyId: PasskeyId | null; refreshPasskeys: () => Promise; registerPasskey: (options?: { nickname?: string }) => Promise; revokePasskey: ( - passkeyId: string | number, + passkeyId: PasskeyId, reason?: string, ) => Promise; } @@ -93,7 +94,7 @@ const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArg const [passkeysSupported, setPasskeysSupported] = useState(null); const [passkeysLoading, setPasskeysLoading] = useState(false); const [registeringPasskey, setRegisteringPasskey] = useState(false); - const [revokingPasskeyId, setRevokingPasskeyId] = useState(null); + const [revokingPasskeyId, setRevokingPasskeyId] = useState(null); const refreshPasskeys = useCallback(async (): Promise => { if (!token) { @@ -191,7 +192,7 @@ const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArg const revokePasskey = useCallback( async ( - passkeyId: string | number, + passkeyId: PasskeyId, reason?: string, ): Promise => { if (passkeyId == null) { diff --git a/frontend/src/sidebar/Sidebar.tsx b/frontend/src/sidebar/Sidebar.tsx index c3780f5..3cc0a91 100644 --- a/frontend/src/sidebar/Sidebar.tsx +++ b/frontend/src/sidebar/Sidebar.tsx @@ -31,6 +31,7 @@ import { getTagColorStyle } from '../utils/colors'; import { useSidebarContext } from './SidebarContext'; import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext'; import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore'; +import type { Identifier } from '../types/identifiers'; interface CommunityLink { label: string; @@ -60,7 +61,6 @@ const COMMUNITY_LINKS: CommunityLink[] = [ }, ]; -type Identifier = string | number; type FolderIdentifier = Identifier | 'root'; interface FolderTreeNode { @@ -528,94 +528,94 @@ const Sidebar: React.FC = ({ const themeMenuSection = neutralHue != null ? ( -
-
- Theme -
- - -
+
+
+ Theme +
+ +
- - -
- ) + + + +
+ ) : null; const communityMenuFooter = COMMUNITY_LINKS.length ? ( -
- {COMMUNITY_LINKS.map(({ href, label, title, Icon }) => ( - - - {label} - - ))} -
- ) +
+ {COMMUNITY_LINKS.map(({ href, label, title, Icon }) => ( + + + {label} + + ))} +
+ ) : null; const handleSearchInputChange = useCallback( @@ -736,62 +736,62 @@ const Sidebar: React.FC = ({ } const tenantMenuContent = tenantMenuOpen && tenantMenuStyle - ? createPortal( -
- {showTenantList ? ( -
-
Switch tenant
-
- {tenants.map((tenant) => { - const tenantId = tenant?.id || null; - const isActive = tenantId === activeTenantId; - const tenantLabel = tenant?.name || tenantId || 'Tenant'; - return ( - - ); - })} -
-
- ) : null} -
-
- - -
+ ? createPortal( +
+ {showTenantList ? ( +
+
Switch tenant
+
+ {tenants.map((tenant) => { + const tenantId = tenant?.id || null; + const isActive = tenantId === activeTenantId; + const tenantLabel = tenant?.name || tenantId || 'Tenant'; + return ( + + ); + })}
- {themeMenuSection} - {communityMenuFooter} -
, - document.body, - ) - : null; +
+ ) : null} +
+
+ + +
+
+ {themeMenuSection} + {communityMenuFooter} +
, + document.body, + ) + : null; return (
{untaggedFilterId ? ( @@ -955,25 +954,25 @@ const Sidebar: React.FC = ({ style={style || undefined} onClick={() => handleToggleTag(tag.id)} aria-pressed={isActive} - draggable - onDragStart={(event) => { - try { - const payload = JSON.stringify({ - id: tag.id, - label: tag.label, - color: tag.color || null, - }); - event.dataTransfer.effectAllowed = 'copy'; - event.dataTransfer.setData('application/x-papercrate-tag', payload); - event.dataTransfer.setData('text/papercrate-tag', payload); - } catch (error) { - console.warn('[sidebar] Failed to set tag drag payload', error); - } - }} - > - {tag.label} - - ); + draggable + onDragStart={(event) => { + try { + const payload = JSON.stringify({ + id: tag.id, + label: tag.label, + color: tag.color || null, + }); + event.dataTransfer.effectAllowed = 'copy'; + event.dataTransfer.setData('application/x-papercrate-tag', payload); + event.dataTransfer.setData('text/papercrate-tag', payload); + } catch (error) { + console.warn('[sidebar] Failed to set tag drag payload', error); + } + }} + > + {tag.label} + + ); })}
diff --git a/frontend/src/sidebar/useSidebarProps.ts b/frontend/src/sidebar/useSidebarProps.ts index 2699f7d..125b4fd 100644 --- a/frontend/src/sidebar/useSidebarProps.ts +++ b/frontend/src/sidebar/useSidebarProps.ts @@ -2,8 +2,7 @@ import { useMemo } from 'react'; import type { DragEvent } from 'react'; import { TAG_FILTER_UNTAGGED } from '../app/workspaceUtils'; import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore'; - -type Identifier = string | number; +import type { Identifier } from '../types/identifiers'; interface FolderTreeNode { id: Identifier; diff --git a/frontend/src/types/identifiers.ts b/frontend/src/types/identifiers.ts new file mode 100644 index 0000000..635b603 --- /dev/null +++ b/frontend/src/types/identifiers.ts @@ -0,0 +1,11 @@ +// Common string-based identifiers used across the app. +export type Identifier = string; + +export type DocumentId = Identifier; +export type FolderId = Identifier; +export type CapabilitySetId = Identifier; +export type CapabilityValue = Identifier; +export type TenantId = Identifier; +export type TagId = Identifier; +export type ApiTokenId = Identifier; +export type PasskeyId = Identifier; diff --git a/frontend/src/ui/QuickAddMenu.tsx b/frontend/src/ui/QuickAddMenu.tsx index f12d68e..e993557 100644 --- a/frontend/src/ui/QuickAddMenu.tsx +++ b/frontend/src/ui/QuickAddMenu.tsx @@ -4,10 +4,10 @@ import { PlusIcon } from './icons'; import useFloatingMenu from './useFloatingMenu'; -type QuickAddOption = string | number | { id?: string | number; label?: string; name?: string;[key: string]: unknown }; +type QuickAddOption = string | { id?: string; label?: string; name?: string;[key: string]: unknown }; interface NormalizedOption { - id?: string | number; + id?: string; label: string; original: QuickAddOption; index: number; @@ -47,7 +47,7 @@ interface FloatingMenuState { updatePosition: () => void; } -type CSSVarStyle = CSSProperties & Record; +type CSSVarStyle = CSSProperties & Record; export interface QuickAddMenuProps { onSelectOption?: (value: QuickAddOption, normalized: NormalizedOption) => Promise | void; diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts index befd37f..ae09654 100644 --- a/frontend/src/utils/date.ts +++ b/frontend/src/utils/date.ts @@ -1,4 +1,4 @@ -const ensureDate = (value: string | number | Date | null): Date | null => { +const ensureDate = (value: string | Date | null): Date | null => { if (!value) { return null; } @@ -12,7 +12,7 @@ interface FormatOptions { options?: Intl.DateTimeFormatOptions; } -export const formatDate = (value: string | number | Date | null, { fallback = '—', locale, options }: FormatOptions = {}): string => { +export const formatDate = (value: string | Date | null, { fallback = '—', locale, options }: FormatOptions = {}): string => { const date = ensureDate(value); if (!date) { return fallback; @@ -20,7 +20,7 @@ export const formatDate = (value: string | number | Date | null, { fallback = ' return date.toLocaleDateString(locale, options); }; -export const formatDateTime = (value: string | number | Date | null, { fallback = '—', locale, options }: FormatOptions = {}): string => { +export const formatDateTime = (value: string | Date | null, { fallback = '—', locale, options }: FormatOptions = {}): string => { const date = ensureDate(value); if (!date) { return fallback; @@ -28,7 +28,7 @@ export const formatDateTime = (value: string | number | Date | null, { fallback return date.toLocaleString(locale, options); }; -export const toDateInputValue = (value: string | number | Date | null): string => { +export const toDateInputValue = (value: string | Date | null): string => { const date = ensureDate(value); if (!date) { return ''; @@ -38,7 +38,7 @@ export const toDateInputValue = (value: string | number | Date | null): string = return localDate.toISOString().slice(0, 10); }; -export const toIssuedTimestamp = (dateString: string | null, fallback: string | number | Date | null): string | null => { +export const toIssuedTimestamp = (dateString: string | null, fallback: string | Date | null): string | null => { if (!dateString) { return null; } @@ -52,7 +52,7 @@ export const toIssuedTimestamp = (dateString: string | null, fallback: string | return Number.isNaN(candidate.getTime()) ? null : candidate.toISOString(); }; -export const parseDateValue = (value: string | number | Date | null): Date | null => ensureDate(value); +export const parseDateValue = (value: string | Date | null): Date | null => ensureDate(value); export default { formatDate, diff --git a/frontend/src/utils/ocr.ts b/frontend/src/utils/ocr.ts index c66b327..81b0d93 100644 --- a/frontend/src/utils/ocr.ts +++ b/frontend/src/utils/ocr.ts @@ -12,9 +12,9 @@ export interface DocumentLike extends AssetManagerDocumentLike { export type AssetLike = AssetManagerAssetLike; -export type EnsurePreviewData = (id: string | number) => Promise; +export type EnsurePreviewData = (id: string) => Promise; export type EnsureAssetUrl = ( - id: string | number, + id: string, asset: AssetLike, options?: { force?: boolean }, ) => Promise;