refactor: introduce dedicated identifier types for improved clarity and type safety

This commit is contained in:
2025-11-24 23:47:36 +01:00
parent 22f0c040a2
commit 70e7bda87e
79 changed files with 1567 additions and 1572 deletions
+1 -2
View File
@@ -11,8 +11,7 @@ import { DocumentsHeaderBreadcrumb } from '../documents/panel/DocumentsPanelHead
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext'; import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
import { PanelManagerProvider, usePanelManager } from './PanelManagerContext'; import { PanelManagerProvider, usePanelManager } from './PanelManagerContext';
import Sidebar from '../sidebar/Sidebar'; import Sidebar from '../sidebar/Sidebar';
import type { Identifier } from '../types/identifiers';
type Identifier = string | number;
type EnsureAssetUrl = ( type EnsureAssetUrl = (
docId: Identifier, docId: Identifier,
+2 -2
View File
@@ -28,7 +28,7 @@ interface StatusMessage {
} }
interface TenantOption { interface TenantOption {
id?: string | number | null; id?: string | null;
name?: string | null; name?: string | null;
} }
@@ -321,7 +321,7 @@ const LoginRoute: React.FC = () => {
const payload: { const payload: {
magic_token: string; magic_token: string;
username?: string; username?: string;
preferred_tenant_id?: string | number; preferred_tenant_id?: string;
} = { } = {
magic_token: magicToken, magic_token: magicToken,
}; };
+3 -3
View File
@@ -15,12 +15,12 @@ import PanelHeader from '../ui/PanelHeader';
type UploadStatus = 'pending' | 'uploading' | 'success' | 'duplicate' | 'error' | (string & {}); type UploadStatus = 'pending' | 'uploading' | 'success' | 'duplicate' | 'error' | (string & {});
interface UploadQueueItem { interface UploadQueueItem {
id: string | number; id: string;
name: string; name: string;
status: UploadStatus; status: UploadStatus;
error?: string | null; error?: string | null;
document?: { id?: string | number; title?: string }; document?: { id?: string; title?: string };
conflictDocumentId?: string | number; conflictDocumentId?: string;
} }
interface UploadQueueOverlayProps { interface UploadQueueOverlayProps {
+4 -2
View File
@@ -1,13 +1,15 @@
import type { DocumentId, FolderId } from '../types/identifiers';
// Entry key utilities for workspace selection // Entry key utilities for workspace selection
// Entry keys are strings in the format "document:id" or "folder:id" // Entry keys are strings in the format "document:id" or "folder:id"
const ENTRY_KEY_SEPARATOR = ':'; const ENTRY_KEY_SEPARATOR = ':';
// Create entry key strings // Create entry key strings
export const createDocumentEntryKey = (documentId: string | number): string => export const createDocumentEntryKey = (documentId: DocumentId): string =>
`document${ENTRY_KEY_SEPARATOR}${documentId}`; `document${ENTRY_KEY_SEPARATOR}${documentId}`;
export const createFolderEntryKey = (folderId: string | number): string => export const createFolderEntryKey = (folderId: FolderId): string =>
`folder${ENTRY_KEY_SEPARATOR}${folderId}`; `folder${ENTRY_KEY_SEPARATOR}${folderId}`;
// Type guards for entry key strings // Type guards for entry key strings
+5 -5
View File
@@ -1,19 +1,19 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
export interface DetailDocument { export interface DetailDocument {
id?: string | number; id?: string;
[key: string]: unknown; [key: string]: unknown;
} }
interface UseDetailPanelOptions { interface UseDetailPanelOptions {
documentLookup: Map<string | number, DetailDocument>; documentLookup: Map<string, DetailDocument>;
orderedSelectedDocuments: DetailDocument[]; orderedSelectedDocuments: DetailDocument[];
} }
interface OpenDetailPanelArgs { interface OpenDetailPanelArgs {
documentId?: string | number; documentId?: string;
document?: DetailDocument | null; document?: DetailDocument | null;
documentIds?: Array<string | number>; documentIds?: Array<string>;
documents?: DetailDocument[]; documents?: DetailDocument[];
} }
@@ -22,7 +22,7 @@ export const useDetailPanel = ({
orderedSelectedDocuments, orderedSelectedDocuments,
}: UseDetailPanelOptions) => { }: UseDetailPanelOptions) => {
const [detailPanelOpen, setDetailPanelOpen] = useState(false); const [detailPanelOpen, setDetailPanelOpen] = useState(false);
const [detailPanelDocId, setDetailPanelDocId] = useState<string | number | null>(null); const [detailPanelDocId, setDetailPanelDocId] = useState<string | null>(null);
const [detailPanelDocument, setDetailPanelDocument] = useState<DetailDocument | null>(null); const [detailPanelDocument, setDetailPanelDocument] = useState<DetailDocument | null>(null);
const latestOrderedDocsRef = useRef<DetailDocument[]>([]); const latestOrderedDocsRef = useRef<DetailDocument[]>([]);
+1 -1
View File
@@ -5,8 +5,8 @@ import type {
SetStateAction, SetStateAction,
} from 'react'; } from 'react';
import { fetchDocument } from '../lib/apiClient'; import { fetchDocument } from '../lib/apiClient';
import type { DocumentId } from '../types/identifiers';
type DocumentId = string | number;
type FolderId = DocumentId | 'root'; type FolderId = DocumentId | 'root';
type DocumentLike = { type DocumentLike = {
+6 -7
View File
@@ -6,8 +6,7 @@ import {
isFolderEntry, isFolderEntry,
getEntryId, getEntryId,
} from './entryKey'; } from './entryKey';
import type { DocumentId } from '../types/identifiers';
type DocumentId = string | number;
interface SelectionEventLike { interface SelectionEventLike {
shiftKey?: boolean; shiftKey?: boolean;
@@ -21,7 +20,7 @@ interface UseDocumentSelectionOptions {
} }
interface ApplySelectionOptions { interface ApplySelectionOptions {
anchor?: string; anchor: string | null;
interactedKeys?: string[]; interactedKeys?: string[];
} }
@@ -35,8 +34,8 @@ export const useDocumentSelection = ({
const selectionOrderRef = useRef<string[]>(initialEntries); const selectionOrderRef = useRef<string[]>(initialEntries);
const selectionAnchorRef = useRef<string | null>(null); const selectionAnchorRef = useRef<string | null>(null);
const selectionInitializedRef = useRef(false); const selectionInitializedRef = useRef(false);
const [focusedDocumentId, setFocusedDocumentId] = useState<DocumentId | undefined>(undefined); const [focusedDocumentId, setFocusedDocumentId] = useState<DocumentId | null>(null);
const [focusedRowKey, setFocusedRowKey] = useState<string | undefined>(undefined); const [focusedRowKey, setFocusedRowKey] = useState<string | null>(null);
const visibleRowKeySetRef = useRef<Set<string>>(new Set()); const visibleRowKeySetRef = useRef<Set<string>>(new Set());
const navigableRowKeysRef = useRef<string[]>([]); const navigableRowKeysRef = useRef<string[]>([]);
@@ -90,7 +89,7 @@ export const useDocumentSelection = ({
const applySelection = useCallback( const applySelection = useCallback(
( (
rowKeys: Array<string | null>, rowKeys: Array<string | null>,
{ anchor, interactedKeys = [] }: ApplySelectionOptions = {}, { anchor = null, interactedKeys = [] }: ApplySelectionOptions = { anchor: null, interactedKeys: [] },
) => { ) => {
const visibleRowKeySet = visibleRowKeySetRef.current; const visibleRowKeySet = visibleRowKeySetRef.current;
const unique: string[] = []; const unique: string[] = [];
@@ -117,7 +116,7 @@ export const useDocumentSelection = ({
} }
}); });
let resolvedAnchor = anchor; let resolvedAnchor: string | null = anchor ?? null;
if (resolvedAnchor && !unique.includes(resolvedAnchor)) { if (resolvedAnchor && !unique.includes(resolvedAnchor)) {
resolvedAnchor = null; resolvedAnchor = null;
} }
+1 -2
View File
@@ -2,8 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import { TAG_FILTER_UNTAGGED } from './workspaceUtils'; import { TAG_FILTER_UNTAGGED } from './workspaceUtils';
import { listDocuments } from '../lib/apiClient'; import { listDocuments } from '../lib/apiClient';
import type { Identifier } from '../types/identifiers';
type Identifier = string | number;
type DocumentLike = { id?: Identifier } & Record<string, unknown>; type DocumentLike = { id?: Identifier } & Record<string, unknown>;
+2 -2
View File
@@ -9,13 +9,13 @@ const TAGS_MODAL = 'tags';
const CORRESPONDENTS_MODAL = 'correspondents'; const CORRESPONDENTS_MODAL = 'correspondents';
interface TagRecord { interface TagRecord {
id?: string | number; id?: string;
label?: string; label?: string;
[key: string]: unknown; [key: string]: unknown;
} }
interface CorrespondentRecord { interface CorrespondentRecord {
id?: string | number; id?: string;
name?: string; name?: string;
[key: string]: unknown; [key: string]: unknown;
} }
+4 -4
View File
@@ -10,8 +10,8 @@ interface SelectionEntry {
} }
interface WorkspaceSelectionOptions { interface WorkspaceSelectionOptions {
onDocumentActivate?: (id: string | number) => void; onDocumentActivate?: (id: string) => void;
onInspectFolder?: (id: string | number) => void; onInspectFolder?: (id: string) => void;
} }
const identity = <T,>(value: T) => value; const identity = <T,>(value: T) => value;
@@ -71,7 +71,7 @@ export const useWorkspaceSelection = ({
); );
const inspectDocument = useCallback( const inspectDocument = useCallback(
(documentId?: string | number) => { (documentId?: string) => {
if (!documentId) return; if (!documentId) return;
onDocumentActivate(documentId); onDocumentActivate(documentId);
}, },
@@ -79,7 +79,7 @@ export const useWorkspaceSelection = ({
); );
const inspectFolder = useCallback( const inspectFolder = useCallback(
(folderId?: string | number) => { (folderId?: string) => {
if (!folderId) return; if (!folderId) return;
onInspectFolder(folderId); onInspectFolder(folderId);
}, },
+1 -2
View File
@@ -5,8 +5,7 @@ import DocumentsPanel from '../documents/panel/DocumentsPanel';
import DocumentViewerPanel from '../preview/DocumentViewerPanel'; import DocumentViewerPanel from '../preview/DocumentViewerPanel';
import { usePanelManager } from './PanelManagerContext'; import { usePanelManager } from './PanelManagerContext';
import { FolderManagerProvider } from '../folders/FolderManagerContext'; import { FolderManagerProvider } from '../folders/FolderManagerContext';
import type { Identifier } from '../types/identifiers';
type Identifier = string | number;
type EnsureAssetUrl = (docId: Identifier, asset: unknown, options?: Record<string, unknown>) => Promise<unknown> | void; type EnsureAssetUrl = (docId: Identifier, asset: unknown, options?: Record<string, unknown>) => Promise<unknown> | void;
type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>; type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
+1 -1
View File
@@ -1,4 +1,4 @@
export type Identifier = string | number; import type { Identifier } from './types/identifiers';
type Nullable<T> = T | null; type Nullable<T> = T | null;
@@ -7,7 +7,7 @@ interface CorrespondentUsage {
} }
export interface CorrespondentEntry { export interface CorrespondentEntry {
id?: string | number; id?: string;
name?: string; name?: string;
usage?: CorrespondentUsage; usage?: CorrespondentUsage;
[key: string]: unknown; [key: string]: unknown;
@@ -17,8 +17,8 @@ export interface CorrespondentsPanelProps {
correspondents?: CorrespondentEntry[]; correspondents?: CorrespondentEntry[];
onRefresh?: () => void | Promise<void>; onRefresh?: () => void | Promise<void>;
onCreate: (payload: { name: string }) => Promise<CorrespondentEntry | void>; onCreate: (payload: { name: string }) => Promise<CorrespondentEntry | void>;
onUpdate: (id: string | number, payload: { name: string }) => Promise<void>; onUpdate: (id: string, payload: { name: string }) => Promise<void>;
onDelete: (id: string | number) => Promise<void>; onDelete: (id: string) => Promise<void>;
onNotify?: (message: string, variant?: string) => void; onNotify?: (message: string, variant?: string) => void;
} }
@@ -30,12 +30,12 @@ function CorrespondentsPanel({
onDelete, onDelete,
onNotify, onNotify,
}: CorrespondentsPanelProps) { }: CorrespondentsPanelProps) {
const [editingId, setEditingId] = useState<string | number | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
const [draftName, setDraftName] = useState(''); const [draftName, setDraftName] = useState('');
const [createName, setCreateName] = useState(''); const [createName, setCreateName] = useState('');
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [deletingId, setDeletingId] = useState<string | number | null>(null); const [deletingId, setDeletingId] = useState<string | null>(null);
const startEdit = useCallback((correspondent: CorrespondentEntry) => { const startEdit = useCallback((correspondent: CorrespondentEntry) => {
setEditingId(correspondent.id); setEditingId(correspondent.id);
+9 -8
View File
@@ -3,17 +3,18 @@ import DesktopPreviewCard from './DesktopPreviewCard';
import { resolveCorrespondents } from '../documents/correspondents'; import { resolveCorrespondents } from '../documents/correspondents';
import { getTagColorStyle } from '../utils/colors'; import { getTagColorStyle } from '../utils/colors';
import { preventAll } from './events'; import { preventAll } from './events';
import type { DocumentId } from '../types/identifiers';
type DocumentLike = { type DocumentLike = {
id?: string | number; id?: string;
title?: 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; [key: string]: unknown;
}; };
interface PendingRemovalTag { interface PendingRemovalTag {
docId?: string | number; docId?: string;
tagId?: string | number; tagId?: string;
} }
interface DesktopDocumentCardProps { interface DesktopDocumentCardProps {
@@ -30,10 +31,10 @@ interface DesktopDocumentCardProps {
getDocumentAsset?: (...args: any[]) => unknown; getDocumentAsset?: (...args: any[]) => unknown;
handleNavigatorSnapshot?: (...args: any[]) => void; handleNavigatorSnapshot?: (...args: any[]) => void;
cardPointerHandlers?: React.HTMLAttributes<HTMLDivElement>; cardPointerHandlers?: React.HTMLAttributes<HTMLDivElement>;
onDocumentActivate?: (id: string | number) => void; onDocumentActivate?: (id: string) => void;
onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: string | number) => void; onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: string | number) => void; onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: string | number) => void; onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: DocumentLike) => void; onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: DocumentLike) => void;
onDocTagPointerDown?: (event: React.PointerEvent<HTMLElement>, doc: DocumentLike, tag: any) => void; onDocTagPointerDown?: (event: React.PointerEvent<HTMLElement>, doc: DocumentLike, tag: any) => void;
onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: DocumentLike, tag: any) => void; onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: DocumentLike, tag: any) => void;
+2 -3
View File
@@ -1,8 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { JSX } from 'react'; import type { JSX } from 'react';
import { useAssetNavigator } from '../hooks/useAssetNavigator'; import { useAssetNavigator } from '../hooks/useAssetNavigator';
import type { Identifier } from '../types/identifiers';
type Identifier = string | number;
interface DocumentLike { interface DocumentLike {
id?: Identifier; id?: Identifier;
@@ -21,7 +20,7 @@ interface AssetLike {
type EnsureAssetUrl = ( type EnsureAssetUrl = (
documentId: Identifier, documentId: Identifier,
asset: AssetLike, asset: AssetLike,
options?: { force?: boolean; [key: string]: unknown }, options?: { force?: boolean;[key: string]: unknown },
) => Promise<unknown>; ) => Promise<unknown>;
type GetDocumentAsset = (document: DocumentLike | null, assetType: string) => AssetLike | null; type GetDocumentAsset = (document: DocumentLike | null, assetType: string) => AssetLike | null;
+4 -5
View File
@@ -29,8 +29,7 @@ import '../styles/workspace/workspace-layout.css';
import '../styles/workspace/workspace-items.css'; import '../styles/workspace/workspace-items.css';
import '../styles/workspace/workspace-cards.css'; import '../styles/workspace/workspace-cards.css';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
import type { DocumentId, Identifier } from '../types/identifiers';
type Identifier = string | number;
type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null; type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null;
type DocumentLinkLike = { url?: string | null; mimeType?: string | null }; type DocumentLinkLike = { url?: string | null; mimeType?: string | null };
@@ -79,7 +78,7 @@ interface DocumentSizeInfo {
} }
interface PreviewMetadataEntry { interface PreviewMetadataEntry {
docId: string; docId: DocumentId;
width: number; width: number;
height: number; height: number;
} }
@@ -225,7 +224,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
const syntheticEvent = event || ({ const syntheticEvent = event || ({
metaKey: true, metaKey: true,
ctrlKey: true, ctrlKey: true,
preventDefault: () => {}, preventDefault: () => { },
} as unknown as PointerEvent); } as unknown as PointerEvent);
docIds.forEach((id) => { docIds.forEach((id) => {
const key = getDocRowKey(id); const key = getDocRowKey(id);
@@ -354,7 +353,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
engine.recalcVisibleDocIds(); engine.recalcVisibleDocIds();
}, [engine]); }, [engine]);
const setDraggingId = useCallback((value: string | number | null) => { const setDraggingId = useCallback((value: string | null) => {
engine.setDraggingId(value); engine.setDraggingId(value);
}, [engine]); }, [engine]);
+30 -6
View File
@@ -1,3 +1,6 @@
import type { DocumentId } from '../types/identifiers';
type TenantId = import('../types/identifiers').TenantId;
const DB_NAME = 'papercrate_desk'; const DB_NAME = 'papercrate_desk';
const DB_VERSION = 1; const DB_VERSION = 1;
const LAYOUT_STORE = 'layouts'; const LAYOUT_STORE = 'layouts';
@@ -107,9 +110,9 @@ const withStore = async <T>(mode: TransactionMode, handler: (store: IDBObjectSto
}; };
interface LayoutRecord { interface LayoutRecord {
tenantId: string | number; tenantId: TenantId;
viewId: string | number; viewId: string;
documentId: string | number; documentId: DocumentId;
centerX?: number; centerX?: number;
centerY?: number; centerY?: number;
rotation?: number; rotation?: number;
@@ -117,7 +120,13 @@ interface LayoutRecord {
updatedAt?: number; updatedAt?: number;
} }
export const fetchLayoutRecords = async ({ tenantId, viewId }: { tenantId?: string | number; viewId?: string | number }): Promise<LayoutRecord[]> => { export const fetchLayoutRecords = async ({
tenantId,
viewId,
}: {
tenantId?: TenantId;
viewId?: string;
}): Promise<LayoutRecord[]> => {
if (!tenantId || !viewId) { if (!tenantId || !viewId) {
return []; 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) { if (!tenantId || !viewId || !Array.isArray(entries) || !entries.length) {
return; 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) { if (!tenantId) {
return; return;
} }
@@ -1,23 +1,24 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { DocumentId } from '../../types/identifiers';
interface DocumentLike { interface DocumentLike {
id?: string | number; id?: string;
current_version?: unknown; current_version?: unknown;
tags?: unknown; tags?: unknown;
} }
interface AssetLike { interface AssetLike {
id?: string | number; id?: string;
[key: string]: unknown; [key: string]: unknown;
} }
interface PreviewMetadataEntry { interface PreviewMetadataEntry {
docId: string; docId: DocumentId;
width: number; width: number;
height: number; height: number;
} }
type GetDocumentAsset = (doc: DocumentLike, type: string) => AssetLike | null; type GetDocumentAsset = (doc: DocumentLike, type: string) => AssetLike | null;
type EnsureAssetUrl = (docId: string | number, asset: AssetLike, options?: { force?: boolean }) => Promise<AssetLike | null>; type EnsureAssetUrl = (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise<AssetLike | null>;
const usePreviewMetadata = ( const usePreviewMetadata = (
documents: DocumentLike[] | null, documents: DocumentLike[] | null,
+4 -3
View File
@@ -1,4 +1,5 @@
import { safeInvoke } from '../events'; import { safeInvoke } from '../events';
import type { DocumentId } from '../../types/identifiers';
export const CLICK_ACTIONS = { export const CLICK_ACTIONS = {
selectSingle: 'selectSingle', 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; export const withinThreshold = (dx: number, dy: number, thresholdSquared: number): boolean => (dx * dx + dy * dy) <= thresholdSquared;
interface PointerIntentArgs { interface PointerIntentArgs {
doc: { id: string | number }; doc: { id: string };
entryDescriptor: unknown; entryDescriptor: unknown;
selectedDocumentIds: Array<string | number>; selectedDocumentIds: Array<string>;
metaKey: boolean; metaKey: boolean;
pointerButton?: number; pointerButton?: number;
pointerType?: string; pointerType?: string;
@@ -35,7 +36,7 @@ interface PointerIntentArgs {
} }
export interface PointerIntent { export interface PointerIntent {
docId: string | number; docId: DocumentId;
entryDescriptor: unknown; entryDescriptor: unknown;
pointerType?: string; pointerType?: string;
pointerButton?: number; pointerButton?: number;
+4 -5
View File
@@ -19,8 +19,7 @@ import {
applyDomTransform, applyDomTransform,
type WorkspaceEngine, type WorkspaceEngine,
} from './workspaceEngine'; } from './workspaceEngine';
import type { DocumentId, Identifier } from '../types/identifiers';
type Identifier = string | number;
interface DocumentLike { interface DocumentLike {
id?: Identifier | null; id?: Identifier | null;
@@ -121,7 +120,7 @@ interface UseDocumentDragOptions {
type EngineInertiaState = Parameters<WorkspaceEngine['startInertiaAnimation']>[1]; type EngineInertiaState = Parameters<WorkspaceEngine['startInertiaAnimation']>[1];
interface DragStateInternal extends EngineDragState { interface DragStateInternal extends EngineDragState {
docId: string; docId: DocumentId;
docKey: string; docKey: string;
pointerId: number; pointerId: number;
originCenterX: number; originCenterX: number;
@@ -216,7 +215,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
onDocumentStackSelect, onDocumentStackSelect,
selectedDocumentIds = [], selectedDocumentIds = [],
markLayoutDirty, markLayoutDirty,
} = options; } = options;
const fallbackContainerRef = useRef<HTMLElement | null>(null); const fallbackContainerRef = useRef<HTMLElement | null>(null);
const containerRef = providedContainerRef ?? fallbackContainerRef; const containerRef = providedContainerRef ?? fallbackContainerRef;
@@ -237,7 +236,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
const tapHandler = usePointerTap<DragTapMetadata>({ const tapHandler = usePointerTap<DragTapMetadata>({
delay: 220, delay: 220,
onSingle: () => {}, onSingle: () => { },
onDouble: ({ data, event }) => { onDouble: ({ data, event }) => {
if (!data?.docId) { if (!data?.docId) {
return; return;
+18 -47
View File
@@ -1,7 +1,7 @@
import { clamp, formatTransform } from '../utils/math'; import { clamp, formatTransform } from '../utils/math';
import { fetchLayoutRecords, upsertLayoutRecords } from './db'; import { fetchLayoutRecords, upsertLayoutRecords } from './db';
import type { DocumentId } from '../types/identifiers';
type DocumentId = string; type TenantId = import('../types/identifiers').TenantId;
interface Point { interface Point {
x: number; x: number;
@@ -63,7 +63,7 @@ interface BaseMetrics {
} }
interface DragGroupItem { interface DragGroupItem {
docId?: string | number | null; docId?: string | null;
width: number; width: number;
height: number; height: number;
currentCenterX?: number; currentCenterX?: number;
@@ -80,7 +80,7 @@ interface DragState {
} }
interface InertiaSimulationState { interface InertiaSimulationState {
docId: string; docId: DocumentId;
restRotation: number; restRotation: number;
rotation: number; rotation: number;
dynamicRotation: number; dynamicRotation: number;
@@ -106,7 +106,7 @@ interface WorkspaceSnapshot {
type WorkspaceSubscriber = () => void; type WorkspaceSubscriber = () => void;
type DeskDocument = { id?: string | number | null } & Record<string, unknown>; type DeskDocument = { id?: string | null } & Record<string, unknown>;
type EnsureDocumentSize = (doc: DeskDocument) => DocumentSize | null; 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 baseSeed = seededRandom(`${docId}:fallback-size`);
const aspectSeed = seededRandom(`${docId}:fallback-aspect`); const aspectSeed = seededRandom(`${docId}:fallback-aspect`);
@@ -259,7 +259,7 @@ function randomRangeFromSeed(seedKey: string, min: number, max: number): number
return min + seed * span; return min + seed * span;
} }
function buildKey(docId: string | number, suffix: string): string { function buildKey(docId: DocumentId, suffix: string): string {
return `${docId}::${suffix}`; return `${docId}::${suffix}`;
} }
@@ -525,63 +525,34 @@ const generateInitialLayout = (
export class WorkspaceEngine { export class WorkspaceEngine {
allowLayoutPersistence: boolean; allowLayoutPersistence: boolean;
tenantId: TenantId | null;
tenantId: string | null;
viewId: string | null; viewId: string | null;
layout: Map<DocumentId, LayoutEntry>; layout: Map<DocumentId, LayoutEntry>;
layoutSnapshot: Map<DocumentId, LayoutEntry>; layoutSnapshot: Map<DocumentId, LayoutEntry>;
persistedLayout: Map<DocumentId, LayoutEntry>; persistedLayout: Map<DocumentId, LayoutEntry>;
layoutDirty: boolean; layoutDirty: boolean;
zCounter: number; zCounter: number;
canvasSize: { width: number; height: number }; canvasSize: { width: number; height: number };
visibleDocIds: Set<DocumentId>; visibleDocIds: Set<DocumentId>;
draggingId: string | null; draggingId: string | null;
tagDropTargetId: string | null; tagDropTargetId: string | null;
pendingTagDocId: string | null; pendingTagDocId: string | null;
pendingRemovalTag: unknown; pendingRemovalTag: unknown;
dragInProgress: boolean; dragInProgress: boolean;
activeDragDocIds: Set<DocumentId>; activeDragDocIds: Set<DocumentId>;
pendingSnapshotSync: boolean; pendingSnapshotSync: boolean;
pendingPersistSync: boolean; pendingPersistSync: boolean;
persistDebounceId: number | null; persistDebounceId: number | null;
items: DeskDocument[]; items: DeskDocument[];
documentLookup: Map<string, DeskDocument>; documentLookup: Map<string, DeskDocument>;
ensureDocumentSize: EnsureDocumentSize; ensureDocumentSize: EnsureDocumentSize;
resolveBaseMetrics: ResolveBaseMetrics; resolveBaseMetrics: ResolveBaseMetrics;
snapshotCache: WorkspaceSnapshot; snapshotCache: WorkspaceSnapshot;
subscribers: Set<WorkspaceSubscriber>; subscribers: Set<WorkspaceSubscriber>;
loadingPersisted: boolean; loadingPersisted: boolean;
pendingPersistence: unknown; pendingPersistence: unknown;
itemRefs: ItemRefs; itemRefs: ItemRefs;
inertiaAnimations: Map<string, InertiaSimulationState>; inertiaAnimations: Map<string, InertiaSimulationState>;
initialLoadDone: boolean; initialLoadDone: boolean;
constructor({ constructor({
@@ -716,7 +687,7 @@ export class WorkspaceEngine {
this.emit(); this.emit();
} }
setDraggingId(docId: string | number | null): void { setDraggingId(docId: DocumentId | null): void {
const normalized = docId != null ? String(docId) : null; const normalized = docId != null ? String(docId) : null;
if (this.draggingId === normalized) { if (this.draggingId === normalized) {
return; return;
@@ -725,7 +696,7 @@ export class WorkspaceEngine {
this.emit(); this.emit();
} }
beginDrag(docIds: Array<string | number | null> = []): void { beginDrag(docIds: Array<string | null> = []): void {
this.dragInProgress = true; this.dragInProgress = true;
if (Array.isArray(docIds)) { if (Array.isArray(docIds)) {
this.activeDragDocIds = new Set(docIds.map((id) => (id != null ? String(id) : null)).filter(Boolean)); 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; const normalized = docId != null ? String(docId) : null;
if (this.tagDropTargetId === normalized) { if (this.tagDropTargetId === normalized) {
return; return;
@@ -758,7 +729,7 @@ export class WorkspaceEngine {
this.emit(); this.emit();
} }
setPendingTagDocId(docId: string | number | null): void { setPendingTagDocId(docId: DocumentId | null): void {
const normalized = docId != null ? String(docId) : null; const normalized = docId != null ? String(docId) : null;
if (this.pendingTagDocId === normalized) { if (this.pendingTagDocId === normalized) {
return; return;
@@ -779,7 +750,7 @@ export class WorkspaceEngine {
this.layoutDirty = true; this.layoutDirty = true;
} }
getLayout(docId: string | number | null): LayoutEntry | null { getLayout(docId: DocumentId | null): LayoutEntry | null {
if (docId == null) { if (docId == null) {
return null; return null;
} }
@@ -788,7 +759,7 @@ export class WorkspaceEngine {
} }
updateLayoutEntry( updateLayoutEntry(
docId: string | number | null, docId: DocumentId | null,
updater: (previous: LayoutEntry | null) => LayoutEntry | null, updater: (previous: LayoutEntry | null) => LayoutEntry | null,
): void { ): void {
if (docId == null) { if (docId == null) {
@@ -807,7 +778,7 @@ export class WorkspaceEngine {
this.persistLayoutSnapshot(); this.persistLayoutSnapshot();
} }
bringToFront(docId: string | number | null): void { bringToFront(docId: DocumentId | null): void {
if (docId == null) { if (docId == null) {
return; return;
} }
@@ -825,7 +796,7 @@ export class WorkspaceEngine {
} }
applyTransform( applyTransform(
docId: string | number | null, docId: DocumentId | null,
centerX: number, centerX: number,
centerY: number, centerY: number,
width: number, width: number,
@@ -896,7 +867,7 @@ export class WorkspaceEngine {
this.persistLayoutSnapshot(); this.persistLayoutSnapshot();
} }
cancelInertiaAnimation(docId: string | number | null): void { cancelInertiaAnimation(docId: DocumentId | null): void {
const key = docId != null ? String(docId) : null; const key = docId != null ? String(docId) : null;
if (!key) { if (!key) {
return; return;
@@ -995,7 +966,7 @@ export class WorkspaceEngine {
return isSettled; return isSettled;
} }
startInertiaAnimation(docId: string | number | null, baseState: InertiaSimulationState): void { startInertiaAnimation(docId: DocumentId | null, baseState: InertiaSimulationState): void {
const raf = window.requestAnimationFrame; const raf = window.requestAnimationFrame;
if (!raf) { if (!raf) {
return; return;
+2 -2
View File
@@ -4,7 +4,7 @@ import { clamp } from '../utils/math';
import PdfViewer from '../preview/PdfViewer'; import PdfViewer from '../preview/PdfViewer';
type DocumentLike = { type DocumentLike = {
id?: string | number; id?: string;
title?: string; title?: string;
mime_type?: string | null; mime_type?: string | null;
[key: string]: unknown; [key: string]: unknown;
@@ -43,7 +43,7 @@ const determineDisplayKind = (entry?: DocumentLink | null): DisplayKind => {
return 'image'; return 'image';
}; };
const noop = () => {}; const noop = () => { };
const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
open = false, open = false,
+1 -2
View File
@@ -6,8 +6,7 @@ import { DEFAULT_FOLDER_NAME } from '../app/workspaceUtils';
import { getEntryId, isDocumentEntry } from '../app/entryKey'; import { getEntryId, isDocumentEntry } from '../app/entryKey';
import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel'; import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel';
import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr'; import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr';
import type { Identifier } from '../types/identifiers';
type Identifier = string | number;
interface DocumentLike { interface DocumentLike {
id?: Identifier; id?: Identifier;
@@ -3,15 +3,15 @@ import React from 'react';
const NBSP = String.fromCharCode(160); const NBSP = String.fromCharCode(160);
export interface CorrespondentLinkEntry { export interface CorrespondentLinkEntry {
id?: string | number | null; id?: string | null;
name?: string | null; name?: string | null;
key?: string; key?: string;
} }
interface CorrespondentLinksProps { interface CorrespondentLinksProps {
correspondents?: CorrespondentLinkEntry[]; correspondents?: CorrespondentLinkEntry[];
activeCorrespondentIdSet?: Set<string | number>; activeCorrespondentIdSet?: Set<string>;
onCorrespondentClick?: (id: string | number) => void; onCorrespondentClick?: (id: string) => void;
} }
const CorrespondentLinks: React.FC<CorrespondentLinksProps> = ({ const CorrespondentLinks: React.FC<CorrespondentLinksProps> = ({
@@ -23,7 +23,7 @@ const CorrespondentLinks: React.FC<CorrespondentLinksProps> = ({
return null; return null;
} }
const activeSet = activeCorrespondentIdSet || new Set<string | number>(); const activeSet = activeCorrespondentIdSet || new Set<string>();
const handleClick = (event: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>, correspondent: CorrespondentLinkEntry) => { const handleClick = (event: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>, correspondent: CorrespondentLinkEntry) => {
if (!onCorrespondentClick || correspondent.id == null) { if (!onCorrespondentClick || correspondent.id == null) {
return; return;
+1 -1
View File
@@ -36,7 +36,7 @@ export interface DocumentInfoPanelProps {
activeTab?: string; activeTab?: string;
onTabChange?: (tabId: string) => void; onTabChange?: (tabId: string) => void;
defaultTabId?: string; defaultTabId?: string;
resetKey?: string | number | null; resetKey?: string | null;
classNamePrefix?: string; classNamePrefix?: string;
hideTabNavWhenSingle?: boolean; hideTabNavWhenSingle?: boolean;
summaryPlacement?: 'inline' | 'tabs'; summaryPlacement?: 'inline' | 'tabs';
@@ -14,11 +14,10 @@ import {
import { describeDocumentSummary, type DocumentSummaryRow } from './documentSummary'; import { describeDocumentSummary, type DocumentSummaryRow } from './documentSummary';
import { useFolderManager } from '../folders/FolderManagerContext'; import { useFolderManager } from '../folders/FolderManagerContext';
import type { FolderId, Identifier, TagId } from '../types/identifiers';
type Identifier = string | number;
interface TagEntry { interface TagEntry {
id?: Identifier; id?: TagId;
label?: string; label?: string;
color?: string | null; color?: string | null;
} }
@@ -33,7 +32,7 @@ interface DocumentLike {
id?: Identifier; id?: Identifier;
title?: string; title?: string;
issued_at?: string | null; issued_at?: string | null;
folder_id?: string | null; folder_id?: FolderId | null;
current_version?: { version_number?: number } | null; current_version?: { version_number?: number } | null;
tags?: TagEntry[]; tags?: TagEntry[];
correspondents?: CorrespondentEntry[]; correspondents?: CorrespondentEntry[];
@@ -64,17 +63,17 @@ interface CorrespondentSectionProps {
export interface DocumentSummarySectionProps { export interface DocumentSummarySectionProps {
document?: DocumentLike | null; document?: DocumentLike | null;
tagLookupById?: Map<Identifier, TagEntry>; tagLookupById?: Map<TagId, TagEntry>;
tagOptions?: SelectionAssignmentMenuItem[]; tagOptions?: SelectionAssignmentMenuItem[];
onTagAdd?: (doc: DocumentLike, value: string, context?: { option?: unknown }) => void; 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[]; correspondents?: CorrespondentEntry[];
correspondentOptions?: SelectionAssignmentMenuItem[]; correspondentOptions?: SelectionAssignmentMenuItem[];
onCorrespondentAdd?: (payload: { document: DocumentLike; name: string; option?: unknown }) => void; onCorrespondentAdd?: (payload: { document: DocumentLike; name: string; option?: unknown }) => void;
onCorrespondentRemove?: (payload: { documentId: Identifier | undefined; correspondentId: Identifier | undefined }) => void; onCorrespondentRemove?: (payload: { documentId: Identifier | undefined; correspondentId: Identifier | undefined }) => void;
onUpdateTitle?: (docId: Identifier | undefined, title: string) => Promise<boolean> | boolean; onUpdateTitle?: (docId: Identifier | undefined, title: string) => Promise<boolean> | boolean;
onUpdateIssued?: (docId: Identifier | undefined, timestamp: number | null) => Promise<boolean> | boolean; onUpdateIssued?: (docId: Identifier | undefined, timestamp: number | null) => Promise<boolean> | boolean;
onFolderNavigate?: (folderId: string | null) => void; onFolderNavigate?: (folderId: FolderId | null) => void;
layout?: 'default' | 'compact'; layout?: 'default' | 'compact';
} }
@@ -17,7 +17,7 @@ const DEFAULT_THUMBNAIL_SIZE = 48;
// Detect when an element becomes visible within a scroll container so we can delay loading. // Detect when an element becomes visible within a scroll container so we can delay loading.
const useLazyVisibility = ( const useLazyVisibility = (
rootRef: MutableRefObject<Element | null> | null, rootRef: MutableRefObject<Element | null> | null,
resetKey?: string | number | null, resetKey?: string | null,
) => { ) => {
const targetRef = useRef<HTMLDivElement | null>(null); const targetRef = useRef<HTMLDivElement | null>(null);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
+1 -2
View File
@@ -8,8 +8,7 @@ import { resolveCorrespondents } from './correspondents';
import { writeTagTransferData } from './tagTransfer'; import { writeTagTransferData } from './tagTransfer';
import useInlineRename from './useInlineRename'; import useInlineRename from './useInlineRename';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
import type { Identifier } from '../types/identifiers';
export type Identifier = string | number;
export interface FolderLike { export interface FolderLike {
id?: Identifier | 'root'; id?: Identifier | 'root';
+1 -2
View File
@@ -9,8 +9,7 @@ import { resolveCorrespondents } from './correspondents';
import { writeTagTransferData } from './tagTransfer'; import { writeTagTransferData } from './tagTransfer';
import useInlineRename from './useInlineRename'; import useInlineRename from './useInlineRename';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
import type { Identifier } from '../types/identifiers';
export type Identifier = string | number;
export interface FolderLike { export interface FolderLike {
id?: Identifier | 'root'; id?: Identifier | 'root';
+1 -2
View File
@@ -1,6 +1,5 @@
import { shallowEqual } from 'react-redux'; import { shallowEqual } from 'react-redux';
import type { DocumentId } from '../types/identifiers';
type DocumentId = string | number;
export type ManagedDocument = { id?: DocumentId | null } & Record<string, unknown>; export type ManagedDocument = { id?: DocumentId | null } & Record<string, unknown>;
@@ -5,18 +5,18 @@ import { CheckIcon, CircleDashedCheckIcon, PlusIcon } from '../ui/icons';
export type AssignmentState = 'all' | 'partial' | 'none'; export type AssignmentState = 'all' | 'partial' | 'none';
export interface SelectionAssignmentMenuItem { export interface SelectionAssignmentMenuItem {
id?: string | number; id?: string;
label?: string; label?: string;
state?: AssignmentState; state?: AssignmentState;
count?: number | null; count?: number | null;
total?: number | null; total?: number | null;
color?: string | null; color?: string | null;
value?: string | number; value?: string;
payload?: unknown; payload?: unknown;
} }
export interface NormalizedSelectionAssignmentItem { export interface NormalizedSelectionAssignmentItem {
id: string | number; id: string;
label: string; label: string;
state: AssignmentState; state: AssignmentState;
count: number | null; count: number | null;
@@ -105,7 +105,7 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [pending, setPending] = useState(false); const [pending, setPending] = useState(false);
const [sortSnapshot, setSortSnapshot] = useState<Array<string | number> | null>(null); const [sortSnapshot, setSortSnapshot] = useState<Array<string> | null>(null);
const { const {
isOpen, isOpen,
@@ -175,10 +175,10 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
const orderedItems = useMemo(() => { const orderedItems = useMemo(() => {
if (freezeSortOnOpen && sortSnapshot && sortByState) { if (freezeSortOnOpen && sortSnapshot && sortByState) {
const itemMap = new Map<string | number, NormalizedSelectionAssignmentItem>( const itemMap = new Map<string, NormalizedSelectionAssignmentItem>(
sortedByStateItems.map((item) => [item.id, item]), sortedByStateItems.map((item) => [item.id, item]),
); );
const seen = new Set<string | number>(); const seen = new Set<string>();
const fromSnapshot = sortSnapshot const fromSnapshot = sortSnapshot
.map((id) => { .map((id) => {
const entry = itemMap.get(id); const entry = itemMap.get(id);
@@ -12,10 +12,10 @@ import SelectionAssignmentMenu, { SelectionAssignmentMenuItem } from './Selectio
import SelectionSummary from './SelectionSummary'; import SelectionSummary from './SelectionSummary';
import { useAppState } from '../app/appState'; import { useAppState } from '../app/appState';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
import type { DocumentId } from '../types/identifiers';
const ROOT_FOLDER_LABEL = 'Documents'; const ROOT_FOLDER_LABEL = 'Documents';
type DocumentId = string | number;
type NullableDocumentId = DocumentId | null; type NullableDocumentId = DocumentId | null;
type SelectedIdList = NullableDocumentId[] | null; type SelectedIdList = NullableDocumentId[] | null;
@@ -145,7 +145,7 @@ const buildTagAssignments = (
return []; return [];
} }
const map = new Map<string | number, { const map = new Map<string, {
id?: DocumentId; id?: DocumentId;
label: string; label: string;
color: string | null; color: string | null;
@@ -209,7 +209,7 @@ const buildCorrespondentAssignments = (
return []; return [];
} }
const map = new Map<string | number, { const map = new Map<string, {
id?: DocumentId; id?: DocumentId;
label: string; label: string;
count: number; count: number;
@@ -491,7 +491,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const value = isRecord(candidate) const value = isRecord(candidate)
? (candidate?.id ?? candidate?.value ?? null) ? (candidate?.id ?? candidate?.value ?? null)
: candidate; : candidate;
if (!value && value !== 0) { if (!value) {
return; return;
} }
await onMoveDocumentsToFolder(documentIdList, value as DocumentId); await onMoveDocumentsToFolder(documentIdList, value as DocumentId);
@@ -1,10 +1,9 @@
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import type { Identifier } from '../../types/identifiers';
type Identifier = string | number;
export interface DocumentsFilterValue { export interface DocumentsFilterValue {
query: string; query: string;
searchResultIds: Array<string | number> | null; searchResultIds: Array<string> | null;
searchLoading: boolean; searchLoading: boolean;
includeDescendants: boolean; includeDescendants: boolean;
activeTagIds: Identifier[]; activeTagIds: Identifier[];
+4 -4
View File
@@ -1,5 +1,5 @@
export interface CorrespondentReference { export interface CorrespondentReference {
id?: string | number | null; id?: string | null;
name?: string | null; name?: string | null;
key?: string; key?: string;
} }
@@ -9,9 +9,9 @@ export interface DocumentLike {
} }
export interface ResolvedCorrespondent { export interface ResolvedCorrespondent {
id?: string | number | null; id?: string | null;
name: string; name: string;
key: string | number; key: string;
} }
export const resolveCorrespondents = (doc?: DocumentLike | null): ResolvedCorrespondent[] => { export const resolveCorrespondents = (doc?: DocumentLike | null): ResolvedCorrespondent[] => {
@@ -19,7 +19,7 @@ export const resolveCorrespondents = (doc?: DocumentLike | null): ResolvedCorres
return []; return [];
} }
const seen = new Set<string | number>(); const seen = new Set<string>();
const results: ResolvedCorrespondent[] = []; const results: ResolvedCorrespondent[] = [];
doc.correspondents.forEach((entry = {}, index) => { doc.correspondents.forEach((entry = {}, index) => {
@@ -1,7 +1,6 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { assignCorrespondentsBulk } from '../../lib/apiClient'; import { assignCorrespondentsBulk } from '../../lib/apiClient';
import type { Identifier } from '../../types/identifiers';
export type Identifier = string | number;
type BulkAssignmentResponse = { type BulkAssignmentResponse = {
assigned?: number; assigned?: number;
@@ -116,7 +115,7 @@ const useBulkDocumentActions = ({
setStatusMessage, setStatusMessage,
updateDocumentCaches, updateDocumentCaches,
], ],
); );
const handleBulkCorrespondentRemove = useCallback( const handleBulkCorrespondentRemove = useCallback(
async ({ assignments = [], documentIds }: { assignments?: CorrespondentAssignment[]; documentIds?: Identifier[] }) => { async ({ assignments = [], documentIds }: { assignments?: CorrespondentAssignment[]; documentIds?: Identifier[] }) => {
@@ -174,7 +173,7 @@ const useBulkDocumentActions = ({
} }
}, },
[resolveTargetDocumentIds, setStatusMessage, updateDocumentCaches], [resolveTargetDocumentIds, setStatusMessage, updateDocumentCaches],
); );
const handleDeleteSelection = useCallback(async () => { const handleDeleteSelection = useCallback(async () => {
const docIds = Array.isArray(selectedDocumentIds) ? selectedDocumentIds : []; const docIds = Array.isArray(selectedDocumentIds) ? selectedDocumentIds : [];
@@ -1,7 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { WorkspaceSelectionValue } from '../../app/WorkspaceSelectionContext'; import type { WorkspaceSelectionValue } from '../../app/WorkspaceSelectionContext';
import type { Identifier } from '../../types/identifiers';
type Identifier = string | number;
interface DocumentLinkLike { interface DocumentLinkLike {
url?: string | null; url?: string | null;
@@ -1,20 +1,21 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useCallback, useEffect, useMemo, useRef } from 'react';
import { createDocumentEntryKey, createFolderEntryKey, isFolderEntry } from '../../app/entryKey'; import { createDocumentEntryKey, createFolderEntryKey, isFolderEntry } from '../../app/entryKey';
import type { DocumentId, FolderId } from '../../types/identifiers';
interface FolderEntry { interface FolderEntry {
id: string | number; id: FolderId;
[key: string]: unknown; [key: string]: unknown;
} }
interface DocumentEntry { interface DocumentEntry {
id: string | number; id: DocumentId;
[key: string]: unknown; [key: string]: unknown;
} }
interface NavigableRow { interface NavigableRow {
key: string; key: string;
type: 'folder' | 'document'; type: 'folder' | 'document';
id: string | number; id: FolderId | DocumentId;
} }
interface UseDocumentsSelectionOptions { interface UseDocumentsSelectionOptions {
@@ -25,19 +26,19 @@ interface UseDocumentsSelectionOptions {
visibleRowKeySet: Set<string>; visibleRowKeySet: Set<string>;
selectedEntries: string[]; selectedEntries: string[];
selectionAnchorRef: { current: string | null }; selectionAnchorRef: { current: string | null };
promoteSelectionOrderRaw: (id: string | number) => void; promoteSelectionOrderRaw: (id: DocumentId) => void;
setFocusedDocumentId: (id: string | number | null) => void; setFocusedDocumentId: (id: DocumentId | null) => void;
setActivePreviewId: (id: string | number | null) => void; setActivePreviewId: (id: DocumentId | null) => void;
clearSelection: () => void; clearSelection: () => void;
focusedDocumentId: string | number | null; focusedDocumentId: DocumentId | null;
setFocusedRowKey: (value: string | null | ((current: string | null) => string | null)) => void; setFocusedRowKey: (value: string | null | ((current: string | null) => string | null)) => void;
focusedRowKey: string | null; focusedRowKey: string | null;
} }
const useDocumentsSelection = ({ const useDocumentsSelection = ({
showingSearchResults, showingSearchResults,
currentSubfolders, currentSubfolders = [],
visibleDocuments, visibleDocuments = [],
configureSelectionEnvironment, configureSelectionEnvironment,
visibleRowKeySet, visibleRowKeySet,
selectedEntries, selectedEntries,
@@ -82,7 +83,7 @@ const useDocumentsSelection = ({
}, [configureSelectionEnvironment, visibleRowKeySet, navigableRowKeys]); }, [configureSelectionEnvironment, visibleRowKeySet, navigableRowKeys]);
const promoteSelectionOrder = useCallback( const promoteSelectionOrder = useCallback(
(docId: string | number | null) => { (docId: DocumentId | null) => {
if (!docId) return; if (!docId) return;
promoteSelectionOrderRaw(docId); promoteSelectionOrderRaw(docId);
const rowKey = createDocumentEntryKey(docId); const rowKey = createDocumentEntryKey(docId);
@@ -99,7 +100,7 @@ const useDocumentsSelection = ({
clearSelection(); clearSelection();
}, [clearSelection]); }, [clearSelection]);
const prevFocusedDocIdRef = useRef<string | number | null>(focusedDocumentId); const prevFocusedDocIdRef = useRef<DocumentId | null>(focusedDocumentId);
useEffect(() => { useEffect(() => {
const previous = prevFocusedDocIdRef.current; const previous = prevFocusedDocIdRef.current;
if (previous === focusedDocumentId) { if (previous === focusedDocumentId) {
@@ -109,7 +110,7 @@ const useDocumentsSelection = ({
if (focusedDocumentId) { if (focusedDocumentId) {
setFocusedRowKey(createDocumentEntryKey(focusedDocumentId)); setFocusedRowKey(createDocumentEntryKey(focusedDocumentId));
} else { } else {
setFocusedRowKey((current) => (isFolderEntry(current) ? current : null)); setFocusedRowKey((current) => (current && isFolderEntry(current) ? current : null));
} }
}, [focusedDocumentId, setFocusedRowKey]); }, [focusedDocumentId, setFocusedRowKey]);
@@ -18,6 +18,7 @@ import DocumentsPanelHeader, {
import { SelectionFloatingPanel } from '../SelectionFloatingActions'; import { SelectionFloatingPanel } from '../SelectionFloatingActions';
import { createDocumentsTableHeaderActions } from './DocumentsToolbar'; import { createDocumentsTableHeaderActions } from './DocumentsToolbar';
import { useDocumentsFilter } from '../context/DocumentsFilterContext'; import { useDocumentsFilter } from '../context/DocumentsFilterContext';
import type { Identifier } from '../../types/identifiers';
const DEFAULT_GRID_ICON_SIZE = 144; const DEFAULT_GRID_ICON_SIZE = 144;
@@ -73,7 +74,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
documentLinks, documentLinks,
ensureDownloadUrl, ensureDownloadUrl,
deskWorkspaceProps = null, deskWorkspaceProps = null,
onRefresh = () => {}, onRefresh = () => { },
sortField, sortField,
sortDirection, sortDirection,
onSortFieldChange, onSortFieldChange,
@@ -256,8 +257,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
const isGridView = viewMode === 'grid'; const isGridView = viewMode === 'grid';
const isDeskView = viewMode === 'desk'; 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<Identifier | null>(null); const [previewDocId, setPreviewDocId] = useState<Identifier | null>(null);
@@ -499,7 +499,7 @@ type ZoomSource = { url: string; alt?: string | null; mimeType?: string | null }
setFocusedRowKey(targetRow.key); setFocusedRowKey(targetRow.key);
handleEntrySelection(targetRow.key, { handleEntrySelection(targetRow.key, {
shiftKey, shiftKey,
preventDefault: () => {}, preventDefault: () => { },
}); });
}, },
[ [
@@ -2,8 +2,7 @@ import React from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import PanelHeader from '../../ui/PanelHeader'; import PanelHeader from '../../ui/PanelHeader';
import BreadcrumbTrail from '../../ui/BreadcrumbTrail'; import BreadcrumbTrail from '../../ui/BreadcrumbTrail';
import type { Identifier } from '../../types/identifiers';
type Identifier = string | number;
export interface DocumentsHeaderBreadcrumb { export interface DocumentsHeaderBreadcrumb {
id?: Identifier; id?: Identifier;
+14 -5
View File
@@ -1,14 +1,16 @@
import type { DocumentId, TagId } from '../types/identifiers';
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag']; const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
const TAG_TEXT_MIME_TYPE = 'text/plain'; const TAG_TEXT_MIME_TYPE = 'text/plain';
interface TagPayload { interface TagPayload {
id: string | number; id: TagId;
label: string; label: string;
sourceDocId: string | number | null; sourceDocId: DocumentId | null;
} }
interface TagLike { interface TagLike {
id?: string | number; id?: TagId;
label?: string | null; 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) { if (!tag || tag.id == null) {
return 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) { if (!dataTransfer) {
return; return;
} }
+3 -3
View File
@@ -20,7 +20,7 @@ export const isPrimaryPointerEvent = (event?: PointerEventLike | null): boolean
export type EntryType = 'document' | 'folder'; export type EntryType = 'document' | 'folder';
export interface WorkspaceEntry { export interface WorkspaceEntry {
id: string | number; id: string;
key?: string; key?: string;
type: EntryType; type: EntryType;
[key: string]: unknown; [key: string]: unknown;
@@ -28,7 +28,7 @@ export interface WorkspaceEntry {
interface UseEntryPointerOptions { interface UseEntryPointerOptions {
onSelectEntry?: (entry: WorkspaceEntry, event?: PointerEventLike | null, metadata?: EntryPointerMetadata) => void; 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 { export interface EntryPointerMetadata {
@@ -36,7 +36,7 @@ export interface EntryPointerMetadata {
primaryClick: boolean; primaryClick: boolean;
rowKey: string; rowKey: string;
type: EntryType; type: EntryType;
id: string | number; id: string;
} }
export const useEntryPointer = ({ export const useEntryPointer = ({
+8 -8
View File
@@ -13,22 +13,22 @@ type FocusableInput = (HTMLInputElement | HTMLTextAreaElement) & {
type InlineRenameOptions<TEntity> = { type InlineRenameOptions<TEntity> = {
getCurrentValue?: (entity: TEntity) => string | null; getCurrentValue?: (entity: TEntity) => string | null;
getEntityId?: (entity: TEntity) => string | number | null; getEntityId?: (entity: TEntity) => string | null;
}; };
type InlineRenameHandler = ( type InlineRenameHandler = (
id: string | number, id: string,
value: string, value: string,
) => boolean | void | Promise<boolean | void>; ) => boolean | void | Promise<boolean | void>;
type InlineRenameReturn<TEntity> = { type InlineRenameReturn<TEntity> = {
editingId: string | number | null; editingId: string | null;
draftValue: string; draftValue: string;
setDraftValue: Dispatch<SetStateAction<string>>; setDraftValue: Dispatch<SetStateAction<string>>;
beginEditing: (entity?: TEntity | null, event?: SyntheticEvent | Event) => void; beginEditing: (entity?: TEntity | null, event?: SyntheticEvent | Event) => void;
cancelEditing: (event?: SyntheticEvent | Event) => void; cancelEditing: (event?: SyntheticEvent | Event) => void;
submitEditing: (entity?: TEntity | null) => Promise<boolean>; submitEditing: (entity?: TEntity | null) => Promise<boolean>;
savingId: string | number | null; savingId: string | null;
attachInputRef: (node: FocusableInput | null) => void; attachInputRef: (node: FocusableInput | null) => void;
}; };
@@ -51,18 +51,18 @@ const focusInput = (node: FocusableInput | null) => {
const identity = (value: unknown) => value as string; const identity = (value: unknown) => value as string;
const defaultGetEntityId = <T,>(entity?: T | null) => const defaultGetEntityId = <T,>(entity?: T | null) =>
(entity as { id?: string | number } | null)?.id ?? null; (entity as { id?: string } | null)?.id ?? null;
const useInlineRename = <TEntity,>( const useInlineRename = <TEntity,>(
onRename?: InlineRenameHandler, onRename?: InlineRenameHandler,
{ {
getCurrentValue = identity as (entity: TEntity) => string | null, getCurrentValue = identity as (entity: TEntity) => string | null,
getEntityId = defaultGetEntityId as (entity: TEntity) => string | number | null, getEntityId = defaultGetEntityId as (entity: TEntity) => string | null,
}: InlineRenameOptions<TEntity> = {}, }: InlineRenameOptions<TEntity> = {},
): InlineRenameReturn<TEntity> => { ): InlineRenameReturn<TEntity> => {
const [editingId, setEditingId] = useState<string | number | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
const [draftValue, setDraftValue] = useState(''); const [draftValue, setDraftValue] = useState('');
const [savingId, setSavingId] = useState<string | number | null>(null); const [savingId, setSavingId] = useState<string | null>(null);
const inputRef = useRef<FocusableInput | null>(null); const inputRef = useRef<FocusableInput | null>(null);
const resetState = useCallback(() => { const resetState = useCallback(() => {
@@ -17,7 +17,7 @@ const FolderManagerContext = createContext<FolderManager>(defaultManager);
interface FolderManagerProviderProps { interface FolderManagerProviderProps {
folderNodes?: Map<string | 'root', { name?: string | null }>; folderNodes?: Map<string | 'root', { name?: string | null }>;
ensureFolderData?: (folderId: string | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>; ensureFolderData?: (folderId: FolderId | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>;
children: ReactNode; children: ReactNode;
} }
@@ -8,7 +8,7 @@ type ApiClient = {
}; };
interface CorrespondentEntry { interface CorrespondentEntry {
id?: string | number; id?: string;
name?: string; name?: string;
[key: string]: unknown; [key: string]: unknown;
} }
@@ -17,7 +17,7 @@ interface UseCorrespondentsOptions {
apiClient: ApiClient; apiClient: ApiClient;
notifyApiError: (error: unknown, fallback: string) => void; notifyApiError: (error: unknown, fallback: string) => void;
setStatusMessage: (message: string, variant?: string) => void; setStatusMessage: (message: string, variant?: string) => void;
tenantIdRef: MutableRefObject<string | number | null>; tenantIdRef: MutableRefObject<string | null>;
mapDocumentCaches?: (mapper: (doc: any) => any) => void; mapDocumentCaches?: (mapper: (doc: any) => any) => void;
} }
@@ -47,7 +47,7 @@ const useCorrespondents = ({
}, [apiClient, notifyApiError, tenantIdRef]); }, [apiClient, notifyApiError, tenantIdRef]);
const handleCorrespondentUpdate = useCallback( const handleCorrespondentUpdate = useCallback(
async (correspondentId: string | number, changes: { name?: string }) => { async (correspondentId: string, changes: { name?: string }) => {
if (correspondentId == null) { if (correspondentId == null) {
throw new Error('Missing correspondent identifier.'); throw new Error('Missing correspondent identifier.');
} }
@@ -100,7 +100,7 @@ const useCorrespondents = ({
); );
const handleCorrespondentDelete = useCallback( const handleCorrespondentDelete = useCallback(
async (correspondentId: string | number) => { async (correspondentId: string) => {
if (correspondentId == null) { if (correspondentId == null) {
throw new Error('Missing correspondent identifier.'); throw new Error('Missing correspondent identifier.');
} }
@@ -1,4 +1,5 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { Identifier } from '../../types/identifiers';
type ApiClient = { type ApiClient = {
@@ -7,13 +8,11 @@ type ApiClient = {
}; };
interface CorrespondentOption { interface CorrespondentOption {
id?: string | number; id?: string;
name?: string; name?: string;
[key: string]: unknown; [key: string]: unknown;
} }
type Identifier = string | number;
interface UseDocumentCorrespondentActionsArgs { interface UseDocumentCorrespondentActionsArgs {
apiClient: ApiClient; apiClient: ApiClient;
correspondents: CorrespondentOption[]; correspondents: CorrespondentOption[];
@@ -135,7 +134,7 @@ const useDocumentCorrespondentActions = ({
}; };
const handleCorrespondentAdd = useCallback( 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) { if (!document?.id) {
throw new Error('Missing document for correspondent assignment.'); throw new Error('Missing document for correspondent assignment.');
} }
@@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useCallback, useEffect, useMemo, useRef } from 'react';
import type { DragEvent } from 'react'; import type { DragEvent } from 'react';
import { createDocumentEntryKey, createFolderEntryKey } from '../../app/entryKey'; import { createDocumentEntryKey, createFolderEntryKey } from '../../app/entryKey';
import type { FolderId, Identifier } from '../../types/identifiers';
type Identifier = string | number; type FolderIdentifier = FolderId | 'root';
type FolderIdentifier = string | 'root';
type FolderInput = FolderIdentifier | number; type FolderInput = FolderIdentifier | number;
interface DocumentLike { interface DocumentLike {
@@ -14,7 +14,7 @@ interface DocumentLike {
type ApplySelectionFn = ( type ApplySelectionFn = (
keys: string[], keys: string[],
options?: { anchor?: string | null; interactedKeys?: string[] }, options?: { anchor: string | null; interactedKeys?: string[] },
) => void; ) => void;
type HandleEntrySelectionFn = ( type HandleEntrySelectionFn = (
@@ -13,9 +13,9 @@ import {
trashDocument, trashDocument,
updateDocument, updateDocument,
} from '../../lib/apiClient'; } from '../../lib/apiClient';
import type { DocumentId, FolderId as FolderIdentifier } from '../../types/identifiers';
type DocumentId = string | number; type FolderId = FolderIdentifier | 'root';
type FolderId = DocumentId | 'root';
type NullableFolderId = FolderId | null; type NullableFolderId = FolderId | null;
type StatusLevel = 'success' | 'error' | 'info' | string; type StatusLevel = 'success' | 'error' | 'info' | string;
@@ -1,6 +1,6 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
type Identifier = string | number; import type { Identifier } from '../../types/identifiers';
interface TagRecord { interface TagRecord {
id?: Identifier; id?: Identifier;
@@ -205,8 +205,7 @@ const useDocumentTagging = ({
if (result?.ok) { if (result?.ok) {
const { tagCount, docsCount } = result; const { tagCount, docsCount } = result;
setStatusMessage( setStatusMessage(
`Assigned ${tagCount} tag${tagCount === 1 ? '' : 's'} to ${docsCount} document${ `Assigned ${tagCount} tag${tagCount === 1 ? '' : 's'} to ${docsCount} document${docsCount === 1 ? '' : 's'
docsCount === 1 ? '' : 's'
}.`, }.`,
'success', 'success',
); );
@@ -3,8 +3,8 @@ import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import useFileDrop from './useFileDrop'; import useFileDrop from './useFileDrop';
import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/workspaceUtils'; import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/workspaceUtils';
import { fetchDocument } from '../../lib/apiClient'; import { fetchDocument } from '../../lib/apiClient';
import type { Identifier } from '../../types/identifiers';
type Identifier = string | number;
type FolderId = Identifier | 'root' | null; type FolderId = Identifier | 'root' | null;
type FileEntry = { type FileEntry = {
+1 -2
View File
@@ -7,8 +7,7 @@ import {
useState, useState,
} from 'react'; } from 'react';
import DocumentsManager from '../../documents/DocumentsManager'; import DocumentsManager from '../../documents/DocumentsManager';
import type { DocumentId } from '../../types/identifiers';
type DocumentId = string | number;
interface DocumentLike { interface DocumentLike {
id?: DocumentId; id?: DocumentId;
@@ -52,6 +52,7 @@ import useWorkspaceTaxonomies from './useWorkspaceTaxonomies';
import useWorkspaceBreadcrumbs from './useWorkspaceBreadcrumbs'; import useWorkspaceBreadcrumbs from './useWorkspaceBreadcrumbs';
import useWorkspaceDeskProps from './useWorkspaceDeskProps'; import useWorkspaceDeskProps from './useWorkspaceDeskProps';
import useWorkspaceSelectionSync from './useWorkspaceSelectionSync'; import useWorkspaceSelectionSync from './useWorkspaceSelectionSync';
import type { DocumentId, FolderId as FolderIdentifier, Identifier } from '../../types/identifiers';
const EntryType = Object.freeze({ const EntryType = Object.freeze({
document: 'document', document: 'document',
@@ -60,9 +61,7 @@ const EntryType = Object.freeze({
const noop = () => { }; const noop = () => { };
type Identifier = string | number; type FolderId = FolderIdentifier | 'root';
type DocumentId = Identifier;
type FolderId = Identifier | 'root';
interface DocumentLike { interface DocumentLike {
id?: DocumentId | null; id?: DocumentId | null;
+1 -1
View File
@@ -1,6 +1,6 @@
import { MutableRefObject, useEffect } from 'react'; import { MutableRefObject, useEffect } from 'react';
type FolderId = string | number | 'root' | null; type FolderId = string | 'root' | null;
interface DropOverlayState { interface DropOverlayState {
active: boolean; active: boolean;
@@ -8,9 +8,9 @@ import {
createDocumentEntryKey, createDocumentEntryKey,
createFolderEntryKey, createFolderEntryKey,
} from '../../app/entryKey'; } from '../../app/entryKey';
import type { FolderId as FolderIdentifier, Identifier } from '../../types/identifiers';
type Identifier = string | number; type FolderId = FolderIdentifier | 'root';
type FolderId = Identifier | 'root';
interface DocumentLike { interface DocumentLike {
id?: Identifier | null; id?: Identifier | null;
@@ -126,8 +126,8 @@ const useFolderTree = ({
.filter(Boolean), .filter(Boolean),
); );
let nextDocKeys = []; let nextDocKeys: string[] = [];
let mergedSelection = []; let mergedSelection: string[] = [];
setSelectedEntries((previous) => { setSelectedEntries((previous) => {
const previousFolderKeys = previous const previousFolderKeys = previous
@@ -140,10 +140,12 @@ const useFolderTree = ({
}); });
const nextFocus = (() => { const nextFocus = (() => {
if (focusedDocumentId) {
const currentFocusedKey = createDocumentEntryKey(focusedDocumentId); const currentFocusedKey = createDocumentEntryKey(focusedDocumentId);
if (currentFocusedKey && availableDocKeySet.has(currentFocusedKey)) { if (currentFocusedKey && availableDocKeySet.has(currentFocusedKey)) {
return focusedDocumentId; return focusedDocumentId;
} }
}
if (nextDocKeys.length) { if (nextDocKeys.length) {
const lastDocKey = nextDocKeys[nextDocKeys.length - 1]; const lastDocKey = nextDocKeys[nextDocKeys.length - 1];
return getEntryId(lastDocKey) || null; return getEntryId(lastDocKey) || null;
@@ -172,7 +174,7 @@ const useFolderTree = ({
if (!targetId || targetId === 'root') { if (!targetId || targetId === 'root') {
setFolderNodes((prev: Map<FolderId, FolderTreeNode>) => { setFolderNodes((prev: Map<FolderId, FolderTreeNode>) => {
const root = prev.get('root'); const root = prev.get('root');
if (root?.expanded) return prev; if (!root || root.expanded) return prev;
const next = new Map<FolderId, FolderTreeNode>(prev); const next = new Map<FolderId, FolderTreeNode>(prev);
next.set('root', { ...root, expanded: true }); next.set('root', { ...root, expanded: true });
return next; return next;
@@ -7,8 +7,8 @@ import {
moveFolder as moveFolderRequest, moveFolder as moveFolderRequest,
renameFolder as renameFolderRequest, renameFolder as renameFolderRequest,
} from '../../lib/apiClient'; } from '../../lib/apiClient';
import type { FolderId } from '../../types/identifiers';
type FolderId = string | number;
type FolderKey = FolderId | 'root'; type FolderKey = FolderId | 'root';
interface FolderNode { interface FolderNode {
+6 -5
View File
@@ -1,4 +1,5 @@
import { MutableRefObject, useCallback, useState } from 'react'; import { MutableRefObject, useCallback, useState } from 'react';
import type { TagId, TenantId } from '../../types/identifiers';
type ApiClient = { type ApiClient = {
get: (path: string) => Promise<{ data: unknown }> get: (path: string) => Promise<{ data: unknown }>
@@ -12,7 +13,7 @@ interface TagManagerInterface {
} }
interface TagEntry { interface TagEntry {
id?: string | number; id?: TagId;
label?: string; label?: string;
color?: string | null; color?: string | null;
[key: string]: unknown; [key: string]: unknown;
@@ -23,8 +24,8 @@ interface UseTagsOptions {
notifyApiError: (error: unknown, fallback: string) => void; notifyApiError: (error: unknown, fallback: string) => void;
setStatusMessage: (message: string, variant?: string) => void; setStatusMessage: (message: string, variant?: string) => void;
tagManager: TagManagerInterface; tagManager: TagManagerInterface;
tenantIdRef: MutableRefObject<string | number | null>; tenantIdRef: MutableRefObject<TenantId | null>;
setActiveTagFilters: (updater: (prev: Array<string | number>) => Array<string | number>) => void; setActiveTagFilters: (updater: (prev: Array<TagId>) => Array<TagId>) => void;
mapDocumentCaches?: (mapper: (doc: any) => any) => void; mapDocumentCaches?: (mapper: (doc: any) => any) => void;
} }
@@ -56,7 +57,7 @@ const useTags = ({
}, [apiClient, notifyApiError, tenantIdRef]); }, [apiClient, notifyApiError, tenantIdRef]);
const handleTagUpdate = useCallback( 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) { if (tagId == null) {
throw new Error('Missing tag identifier.'); throw new Error('Missing tag identifier.');
} }
@@ -104,7 +105,7 @@ const useTags = ({
); );
const handleTagDelete = useCallback( const handleTagDelete = useCallback(
async (tagId: string | number) => { async (tagId: TagId) => {
if (tagId == null) { if (tagId == null) {
throw new Error('Missing tag identifier.'); throw new Error('Missing tag identifier.');
} }
@@ -1,5 +1,6 @@
import { MutableRefObject, useCallback } from 'react'; import { MutableRefObject, useCallback } from 'react';
import type { NavigateFunction } from 'react-router-dom'; import type { NavigateFunction } from 'react-router-dom';
import type { FolderId, TenantId } from '../../types/identifiers';
interface ApiClient { interface ApiClient {
get: (path: string) => Promise<{ data: unknown }>; get: (path: string) => Promise<{ data: unknown }>;
@@ -8,24 +9,24 @@ interface ApiClient {
} }
interface TenantOption { interface TenantOption {
id?: string | number; id?: TenantId;
name?: string; name?: string;
} }
interface UseTenantManagerOptions { interface UseTenantManagerOptions {
apiClient: ApiClient; apiClient: ApiClient;
appDispatch: (action: any) => void; appDispatch: (action: any) => void;
currentTenantId: string | number | null; currentTenantId: TenantId | null;
resetWorkspaceState: () => void; resetWorkspaceState: () => void;
setStatusMessage: (message: string, variant?: string) => void; setStatusMessage: (message: string, variant?: string) => void;
notifyApiError: (error: unknown, message: string) => void; notifyApiError: (error: unknown, message: string) => void;
refreshTags: () => Promise<void>; refreshTags: () => Promise<void>;
refreshCorrespondents: () => Promise<void>; refreshCorrespondents: () => Promise<void>;
loadFolder: (folderId: string, options?: { preserveSearch?: boolean }) => Promise<void>; loadFolder: (folderId: FolderId, options?: { preserveSearch?: boolean }) => Promise<void>;
handleDocumentsViewModeChange: (mode: string) => void; handleDocumentsViewModeChange: (mode: string) => void;
navigate: NavigateFunction; navigate: NavigateFunction;
tokenRef?: MutableRefObject<string | null>; tokenRef?: MutableRefObject<string | null>;
tenantIdRef?: MutableRefObject<string | number | null>; tenantIdRef?: MutableRefObject<TenantId | null>;
} }
const useTenantManager = ({ const useTenantManager = ({
@@ -1,8 +1,8 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils'; import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils';
import type { FolderId as FolderIdentifier } from '../../types/identifiers';
type Identifier = string | number; type FolderId = FolderIdentifier | 'root';
type FolderId = Identifier | 'root';
interface UseWorkspaceBreadcrumbsArgs { interface UseWorkspaceBreadcrumbsArgs {
selectedFolder: FolderId | null; selectedFolder: FolderId | null;
@@ -1,11 +1,10 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
import { createDocumentEntryKey } from '../../app/entryKey'; import { createDocumentEntryKey } from '../../app/entryKey';
import type { Identifier } from '../../types/identifiers';
type Identifier = string | number;
interface ApplySelectionFn { interface ApplySelectionFn {
(keys: string[], options?: { anchor?: string | null; interactedKeys?: string[] }): unknown; (keys: string[], options?: { anchor: string | null; interactedKeys?: string[] }): unknown;
} }
interface UseWorkspaceDeskPropsArgs { interface UseWorkspaceDeskPropsArgs {
@@ -17,8 +16,8 @@ interface UseWorkspaceDeskPropsArgs {
applySelection: ApplySelectionFn; applySelection: ApplySelectionFn;
showingSearchResults: boolean; showingSearchResults: boolean;
searchQuery: string; searchQuery: string;
activeTagFilters: Array<string | number>; activeTagFilters: Array<string>;
activeCorrespondentFilters: Array<string | number>; activeCorrespondentFilters: Array<string>;
selectedFolder: Identifier | 'root' | null; selectedFolder: Identifier | 'root' | null;
promoteSelectionOrder: () => void; promoteSelectionOrder: () => void;
handleDocumentTagDrop: (docId: Identifier, tagId: Identifier) => Promise<void> | void; handleDocumentTagDrop: (docId: Identifier, tagId: Identifier) => Promise<void> | void;
@@ -1,14 +1,13 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
import type { Identifier } from '../../types/identifiers';
type Identifier = string | number;
interface UseWorkspaceSelectionSyncArgs { interface UseWorkspaceSelectionSyncArgs {
showingSearchResults: boolean; showingSearchResults: boolean;
searchQuery: string; searchQuery: string;
setSelectedEntries: (entries: Array<string | number>) => void; setSelectedEntries: (entries: Array<string>) => void;
setSelectionOrder: (order: Array<string | number>) => void; setSelectionOrder: (order: Array<string>) => void;
selectionOrderRef: MutableRefObject<Array<string | number>>; selectionOrderRef: MutableRefObject<Array<string>>;
selectionAnchorRef: MutableRefObject<Identifier | string | null>; selectionAnchorRef: MutableRefObject<Identifier | string | null>;
setFocusedDocumentId: (id: Identifier | null) => void; setFocusedDocumentId: (id: Identifier | null) => void;
selectedDocumentIds: Identifier[]; selectedDocumentIds: Identifier[];
@@ -5,8 +5,7 @@ import TagManager from '../../tag_manager';
import useCorrespondents from './useCorrespondents'; import useCorrespondents from './useCorrespondents';
import useDocumentCorrespondentActions from './useDocumentCorrespondentActions'; import useDocumentCorrespondentActions from './useDocumentCorrespondentActions';
import useTags from './useTags'; import useTags from './useTags';
import type { Identifier } from '../../types/identifiers';
type Identifier = string | number;
interface UseWorkspaceTaxonomiesArgs { interface UseWorkspaceTaxonomiesArgs {
apiClient: any; apiClient: any;
+3 -4
View File
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { resolveAssetUrl } from '../asset_manager'; import { resolveAssetUrl } from '../asset_manager';
import type { Identifier } from '../types/identifiers';
type Identifier = string | number;
type DocumentLike = { type DocumentLike = {
id?: Identifier; id?: Identifier;
@@ -27,7 +26,7 @@ type AssetLike = {
type EnsureAssetUrl = ( type EnsureAssetUrl = (
documentId: Identifier, documentId: Identifier,
asset: AssetLike, asset: AssetLike,
options?: { force?: boolean; [key: string]: unknown }, options?: { force?: boolean;[key: string]: unknown },
) => Promise<unknown>; ) => Promise<unknown>;
type GetAsset = (document: DocumentLike, assetType: string) => AssetLike | null; type GetAsset = (document: DocumentLike, assetType: string) => AssetLike | null;
@@ -92,7 +91,7 @@ export const useAssetNavigator = ({
let cancelled = false; let cancelled = false;
setIsLoading(true); setIsLoading(true);
ensureAssetUrl(documentId, asset, { force: true }) ensureAssetUrl(documentId, asset, { force: true })
.catch(() => {}) .catch(() => { })
.finally(() => { .finally(() => {
if (!cancelled) { if (!cancelled) {
setIsLoading(false); setIsLoading(false);
+2 -1
View File
@@ -1,6 +1,7 @@
// Types aligned with OpenAPI schemas for common endpoints. // 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 { export interface DownloadLink {
url: string; url: string;
+2 -2
View File
@@ -12,7 +12,7 @@ const StatusBanner: React.FC<StatusBannerProps> = ({ status }) => {
}; };
interface TenantOption { interface TenantOption {
id?: string | number; id?: string;
name?: string; name?: string;
} }
@@ -25,7 +25,7 @@ interface LoginViewProps {
tenantSelection?: TenantSelectionState | null; tenantSelection?: TenantSelectionState | null;
onSelectTenant?: (tenant: TenantOption) => void; onSelectTenant?: (tenant: TenantOption) => void;
onCancelSelection?: () => void; onCancelSelection?: () => void;
selectingTenantId?: string | number | null; selectingTenantId?: string | null;
onPasskeyLogin?: (username: string) => void; onPasskeyLogin?: (username: string) => void;
onSignup?: (username: string) => void; onSignup?: (username: string) => void;
passkeySupported?: boolean; passkeySupported?: boolean;
@@ -5,7 +5,7 @@ import { DownloadIcon } from '../ui/icons';
import PdfViewer from './PdfViewer'; import PdfViewer from './PdfViewer';
interface DocumentLike { interface DocumentLike {
id?: string | number; id?: string;
title?: string; title?: string;
mime_type?: string; mime_type?: string;
filename?: string; filename?: string;
@@ -40,7 +40,7 @@ interface DocumentViewerLayoutProps {
summaryProps?: Record<string, unknown>; summaryProps?: Record<string, unknown>;
metadataPayload?: unknown; metadataPayload?: unknown;
contentTabConfig?: ContentTabConfig | null; contentTabConfig?: ContentTabConfig | null;
resetKey?: string | number | null; resetKey?: string | null;
classNamePrefix?: string; classNamePrefix?: string;
defaultTabId?: string; defaultTabId?: string;
infoPanelProps?: Record<string, unknown>; infoPanelProps?: Record<string, unknown>;
+10 -9
View File
@@ -28,14 +28,15 @@ import BreadcrumbTrail from '../ui/BreadcrumbTrail';
import DocumentViewerLayout from './DocumentViewerLayout'; import DocumentViewerLayout from './DocumentViewerLayout';
import useViewerLayoutMode from './useViewerLayoutMode'; import useViewerLayoutMode from './useViewerLayoutMode';
import { usePanelResizeBindings } from '../app/PanelManagerContext'; import { usePanelResizeBindings } from '../app/PanelManagerContext';
import type { DocumentId, FolderId } from '../types/identifiers';
interface DocumentLike { interface DocumentLike {
id?: string | number; id?: DocumentId;
title?: string; title?: string;
mime_type?: string | null; mime_type?: string | null;
issued_at?: string | null; issued_at?: string | null;
folder_id?: string | null; folder_id?: FolderId | null;
correspondents?: Array<{ id?: string | number; name?: string }>; correspondents?: Array<{ id?: string; name?: string }>;
current_version?: { current_version?: {
version_number?: number; version_number?: number;
download?: { url?: string | null; expires_at?: number } | null; download?: { url?: string | null; expires_at?: number } | null;
@@ -51,7 +52,7 @@ interface DocumentLike {
} }
interface AssetLike { interface AssetLike {
id?: string | number; id?: string;
url?: string | null; url?: string | null;
metadata?: Record<string, unknown> | null; metadata?: Record<string, unknown> | null;
[key: string]: unknown; [key: string]: unknown;
@@ -59,16 +60,16 @@ interface AssetLike {
interface DocumentViewerPanelProps extends DocumentSummarySectionProps { interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
document: DocumentLike | null; document: DocumentLike | null;
ensureAssetUrl?: (docId: string | number, asset: AssetLike, options?: { force?: boolean }) => Promise<unknown>; ensureAssetUrl?: (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise<unknown>;
getDocumentAsset?: (doc: DocumentLike | null, type: string) => AssetLike | null; getDocumentAsset?: (doc: DocumentLike | null, type: string) => AssetLike | null;
ensurePreviewData?: (docId: string | number, options?: { signal?: AbortSignal }) => Promise<DocumentLike | null>; ensurePreviewData?: (docId: DocumentId, options?: { signal?: AbortSignal }) => Promise<DocumentLike | null>;
notifyApiError?: (error: unknown, fallbackMessage?: string) => void; notifyApiError?: (error: unknown, fallbackMessage?: string) => void;
sidebarToggle?: ReactNode; sidebarToggle?: ReactNode;
onClosePanel?: () => void; onClosePanel?: () => void;
resolveFolderPath?: (doc: DocumentLike | null) => Array<{ id?: string | number; name?: string }>; resolveFolderPath?: (doc: DocumentLike | null) => Array<{ id?: string; name?: string }>;
variant?: 'viewer' | 'sidebar'; variant?: 'viewer' | 'sidebar';
onCollapsePanel?: () => void; onCollapsePanel?: () => void;
onMaximizePanel?: (args: { documentIds: Array<string | number> }) => void; onMaximizePanel?: (args: { documentIds: Array<string> }) => void;
} }
@@ -163,7 +164,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
}, [document, getDocumentAsset]); }, [document, getDocumentAsset]);
const navigateToFolder = useCallback( const navigateToFolder = useCallback(
(folderId) => { (folderId: FolderId | null) => {
const target = folderId == null const target = folderId == null
? '/documents' ? '/documents'
: `/documents/folder/${folderId}`; : `/documents/folder/${folderId}`;
+1 -2
View File
@@ -2,8 +2,7 @@ import React, { useEffect } from 'react';
import { Navigate, useNavigate, useParams } from 'react-router-dom'; import { Navigate, useNavigate, useParams } from 'react-router-dom';
import { useAppShell } from '../appShellContext'; import { useAppShell } from '../appShellContext';
import type { Identifier } from '../types/identifiers';
type Identifier = string | number;
interface DocumentViewerRouteContext { interface DocumentViewerRouteContext {
previewWorkspaceDocument?: { id?: Identifier } | null; previewWorkspaceDocument?: { id?: Identifier } | null;
@@ -7,9 +7,7 @@ import {
} from 'react'; } from 'react';
import type { JSX } from 'react'; import type { JSX } from 'react';
import { CheckIcon, ChevronDownIcon } from '../../ui/icons'; import { CheckIcon, ChevronDownIcon } from '../../ui/icons';
import type { CapabilityValue } from '../../types/identifiers';
type CapabilityValue = string | number;
export interface CapabilityDropdownOption { export interface CapabilityDropdownOption {
value?: CapabilityValue | null; value?: CapabilityValue | null;
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ChangeEvent, FormEvent } from 'react'; import type { ChangeEvent, FormEvent } from 'react';
import type { Identifier } from '../../types/identifiers';
type Identifier = string | number;
interface ApiTokenEntry { interface ApiTokenEntry {
id?: Identifier; id?: Identifier;
@@ -8,10 +8,7 @@ import React, {
import type { SettingsSectionConfig } from '../SettingsModal'; import type { SettingsSectionConfig } from '../SettingsModal';
import { IconX } from '../../ui/icons'; import { IconX } from '../../ui/icons';
import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown'; import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown';
import type { CapabilitySetId, CapabilityValue } from '../../types/identifiers';
type CapabilityValue = string | number;
type CapabilitySetId = string | number;
interface CapabilitySet { interface CapabilitySet {
id: CapabilitySetId; id: CapabilitySetId;
@@ -18,10 +18,10 @@ interface PasskeysSectionProps {
passkeysSupported?: boolean | null; passkeysSupported?: boolean | null;
passkeysLoading?: boolean; passkeysLoading?: boolean;
registeringPasskey?: boolean; registeringPasskey?: boolean;
revokingPasskeyId?: string | number | null; revokingPasskeyId?: string | null;
onRefreshPasskeys?: () => void | Promise<void>; onRefreshPasskeys?: () => void | Promise<void>;
onRegisterPasskey?: (args: { nickname?: string }) => Promise<RegisterPasskeyResult | undefined>; onRegisterPasskey?: (args: { nickname?: string }) => Promise<RegisterPasskeyResult | undefined>;
onRevokePasskey?: (id: string | number, reason?: string) => Promise<void>; onRevokePasskey?: (id: string, reason?: string) => Promise<void>;
} }
const PasskeysSection = ({ const PasskeysSection = ({
+11 -10
View File
@@ -6,6 +6,7 @@ import {
regenerateApiToken, regenerateApiToken,
type ApiTokenRecord, type ApiTokenRecord,
} from '../lib/apiClient'; } from '../lib/apiClient';
import type { ApiTokenId, CapabilitySetId } from '../types/identifiers';
interface ApiTokensResponse { interface ApiTokensResponse {
token_info?: ApiTokenRecord; token_info?: ApiTokenRecord;
@@ -15,7 +16,7 @@ interface ApiTokensResponse {
interface CreateTokenArgs { interface CreateTokenArgs {
label?: string; label?: string;
expires_at?: string; expires_at?: string;
capability_set_id?: string | number; capability_set_id?: CapabilitySetId;
} }
interface UseApiTokensArgs { interface UseApiTokensArgs {
@@ -28,13 +29,13 @@ interface UseApiTokensResult {
tokens: ApiTokenRecord[]; tokens: ApiTokenRecord[];
loading: boolean; loading: boolean;
creating: boolean; creating: boolean;
deletingId: string | number | null; deletingId: ApiTokenId | null;
regeneratingId: string | number | null; regeneratingId: ApiTokenId | null;
createdSecret: string | null; createdSecret: string | null;
refresh: () => Promise<void>; refresh: () => Promise<void>;
create: (args?: CreateTokenArgs) => Promise<ApiTokensResponse | false>; create: (args?: CreateTokenArgs) => Promise<ApiTokensResponse | false>;
revoke: (tokenId?: string | number | null) => Promise<boolean>; revoke: (tokenId?: ApiTokenId | null) => Promise<boolean>;
regenerate: (tokenId?: string | number | null) => Promise<boolean>; regenerate: (tokenId?: ApiTokenId | null) => Promise<boolean>;
dismissSecret: () => void; dismissSecret: () => void;
} }
@@ -42,8 +43,8 @@ const useApiTokens = ({ notifyApiError, setStatusMessage, token }: UseApiTokensA
const [tokens, setTokens] = useState<ApiTokenRecord[]>([]); const [tokens, setTokens] = useState<ApiTokenRecord[]>([]);
const [loading] = useState(false); const [loading] = useState(false);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [deletingId, setDeletingId] = useState<string | number | null>(null); const [deletingId, setDeletingId] = useState<ApiTokenId | null>(null);
const [regeneratingId, setRegeneratingId] = useState<string | number | null>(null); const [regeneratingId, setRegeneratingId] = useState<ApiTokenId | null>(null);
const [createdSecret, setCreatedSecret] = useState<string | null>(null); const [createdSecret, setCreatedSecret] = useState<string | null>(null);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
@@ -65,7 +66,7 @@ const useApiTokens = ({ notifyApiError, setStatusMessage, token }: UseApiTokensA
} }
setCreating(true); setCreating(true);
try { 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) { if (label) {
payload.label = label; payload.label = label;
} }
@@ -100,7 +101,7 @@ const useApiTokens = ({ notifyApiError, setStatusMessage, token }: UseApiTokensA
); );
const revoke = useCallback( const revoke = useCallback(
async (tokenId?: string | number | null) => { async (tokenId?: string | null) => {
if (!tokenId) { if (!tokenId) {
return false; return false;
} }
@@ -121,7 +122,7 @@ const useApiTokens = ({ notifyApiError, setStatusMessage, token }: UseApiTokensA
); );
const regenerate = useCallback( const regenerate = useCallback(
async (tokenId?: string | number | null) => { async (tokenId?: string | null) => {
if (!tokenId) { if (!tokenId) {
return false; return false;
} }
+1 -2
View File
@@ -5,8 +5,7 @@ import {
listCapabilitySets, listCapabilitySets,
updateCapabilitySet as updateCapabilitySetRequest, updateCapabilitySet as updateCapabilitySetRequest,
} from '../lib/apiClient'; } from '../lib/apiClient';
import type { Identifier } from '../types/identifiers';
type Identifier = string | number;
interface CapabilitySet { interface CapabilitySet {
id?: Identifier; id?: Identifier;
+6 -5
View File
@@ -12,6 +12,7 @@ import {
listPasskeys, listPasskeys,
startPasskeyRegistration, startPasskeyRegistration,
} from '../lib/apiClient'; } from '../lib/apiClient';
import type { PasskeyId } from '../types/identifiers';
type StatusMessageFn = (message: string, variant?: string) => void; type StatusMessageFn = (message: string, variant?: string) => void;
type NotifyApiErrorFn = (error: unknown, message: string) => void; type NotifyApiErrorFn = (error: unknown, message: string) => void;
@@ -27,7 +28,7 @@ type ApiError = {
}; };
export interface PasskeyRecord { export interface PasskeyRecord {
id?: string | number; id?: PasskeyId;
nickname?: string; nickname?: string;
created_at?: string; created_at?: string;
createdAt?: string; createdAt?: string;
@@ -79,11 +80,11 @@ interface UsePasskeysResult {
passkeysSupported: boolean | null; passkeysSupported: boolean | null;
passkeysLoading: boolean; passkeysLoading: boolean;
registeringPasskey: boolean; registeringPasskey: boolean;
revokingPasskeyId: string | number | null; revokingPasskeyId: PasskeyId | null;
refreshPasskeys: () => Promise<void>; refreshPasskeys: () => Promise<void>;
registerPasskey: (options?: { nickname?: string }) => Promise<RegisterPasskeyResult>; registerPasskey: (options?: { nickname?: string }) => Promise<RegisterPasskeyResult>;
revokePasskey: ( revokePasskey: (
passkeyId: string | number, passkeyId: PasskeyId,
reason?: string, reason?: string,
) => Promise<RevokePasskeyResult>; ) => Promise<RevokePasskeyResult>;
} }
@@ -93,7 +94,7 @@ const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArg
const [passkeysSupported, setPasskeysSupported] = useState<boolean | null>(null); const [passkeysSupported, setPasskeysSupported] = useState<boolean | null>(null);
const [passkeysLoading, setPasskeysLoading] = useState(false); const [passkeysLoading, setPasskeysLoading] = useState(false);
const [registeringPasskey, setRegisteringPasskey] = useState(false); const [registeringPasskey, setRegisteringPasskey] = useState(false);
const [revokingPasskeyId, setRevokingPasskeyId] = useState<string | number | null>(null); const [revokingPasskeyId, setRevokingPasskeyId] = useState<PasskeyId | null>(null);
const refreshPasskeys = useCallback(async (): Promise<void> => { const refreshPasskeys = useCallback(async (): Promise<void> => {
if (!token) { if (!token) {
@@ -191,7 +192,7 @@ const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArg
const revokePasskey = useCallback( const revokePasskey = useCallback(
async ( async (
passkeyId: string | number, passkeyId: PasskeyId,
reason?: string, reason?: string,
): Promise<RevokePasskeyResult> => { ): Promise<RevokePasskeyResult> => {
if (passkeyId == null) { if (passkeyId == null) {
+2 -3
View File
@@ -31,6 +31,7 @@ import { getTagColorStyle } from '../utils/colors';
import { useSidebarContext } from './SidebarContext'; import { useSidebarContext } from './SidebarContext';
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext'; import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore'; import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
import type { Identifier } from '../types/identifiers';
interface CommunityLink { interface CommunityLink {
label: string; label: string;
@@ -60,7 +61,6 @@ const COMMUNITY_LINKS: CommunityLink[] = [
}, },
]; ];
type Identifier = string | number;
type FolderIdentifier = Identifier | 'root'; type FolderIdentifier = Identifier | 'root';
interface FolderTreeNode { interface FolderTreeNode {
@@ -925,8 +925,7 @@ const Sidebar: React.FC<SidebarProps> = ({
</div> </div>
</div> </div>
<div <div
className={`sidebar-tag-cloud${ className={`sidebar-tag-cloud${activeTagSet.size ? ' sidebar-tag-cloud--has-active' : ''
activeTagSet.size ? ' sidebar-tag-cloud--has-active' : ''
}`} }`}
role="list" role="list"
> >
+1 -2
View File
@@ -2,8 +2,7 @@ import { useMemo } from 'react';
import type { DragEvent } from 'react'; import type { DragEvent } from 'react';
import { TAG_FILTER_UNTAGGED } from '../app/workspaceUtils'; import { TAG_FILTER_UNTAGGED } from '../app/workspaceUtils';
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore'; import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
import type { Identifier } from '../types/identifiers';
type Identifier = string | number;
interface FolderTreeNode { interface FolderTreeNode {
id: Identifier; id: Identifier;
+11
View File
@@ -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;
+3 -3
View File
@@ -4,10 +4,10 @@ import { PlusIcon } from './icons';
import useFloatingMenu from './useFloatingMenu'; 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 { interface NormalizedOption {
id?: string | number; id?: string;
label: string; label: string;
original: QuickAddOption; original: QuickAddOption;
index: number; index: number;
@@ -47,7 +47,7 @@ interface FloatingMenuState {
updatePosition: () => void; updatePosition: () => void;
} }
type CSSVarStyle = CSSProperties & Record<string, string | number>; type CSSVarStyle = CSSProperties & Record<string, string>;
export interface QuickAddMenuProps { export interface QuickAddMenuProps {
onSelectOption?: (value: QuickAddOption, normalized: NormalizedOption) => Promise<void> | void; onSelectOption?: (value: QuickAddOption, normalized: NormalizedOption) => Promise<void> | void;
+6 -6
View File
@@ -1,4 +1,4 @@
const ensureDate = (value: string | number | Date | null): Date | null => { const ensureDate = (value: string | Date | null): Date | null => {
if (!value) { if (!value) {
return null; return null;
} }
@@ -12,7 +12,7 @@ interface FormatOptions {
options?: Intl.DateTimeFormatOptions; 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); const date = ensureDate(value);
if (!date) { if (!date) {
return fallback; return fallback;
@@ -20,7 +20,7 @@ export const formatDate = (value: string | number | Date | null, { fallback = '
return date.toLocaleDateString(locale, options); 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); const date = ensureDate(value);
if (!date) { if (!date) {
return fallback; return fallback;
@@ -28,7 +28,7 @@ export const formatDateTime = (value: string | number | Date | null, { fallback
return date.toLocaleString(locale, options); 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); const date = ensureDate(value);
if (!date) { if (!date) {
return ''; return '';
@@ -38,7 +38,7 @@ export const toDateInputValue = (value: string | number | Date | null): string =
return localDate.toISOString().slice(0, 10); 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) { if (!dateString) {
return null; return null;
} }
@@ -52,7 +52,7 @@ export const toIssuedTimestamp = (dateString: string | null, fallback: string |
return Number.isNaN(candidate.getTime()) ? null : candidate.toISOString(); 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 { export default {
formatDate, formatDate,
+2 -2
View File
@@ -12,9 +12,9 @@ export interface DocumentLike extends AssetManagerDocumentLike {
export type AssetLike = AssetManagerAssetLike; export type AssetLike = AssetManagerAssetLike;
export type EnsurePreviewData = (id: string | number) => Promise<DocumentLike | null>; export type EnsurePreviewData = (id: string) => Promise<DocumentLike | null>;
export type EnsureAssetUrl = ( export type EnsureAssetUrl = (
id: string | number, id: string,
asset: AssetLike, asset: AssetLike,
options?: { force?: boolean }, options?: { force?: boolean },
) => Promise<AssetLike | null>; ) => Promise<AssetLike | null>;