refactor: Introduce dedicated Document and Asset types and migrate codebase from DocumentLike.

This commit is contained in:
2025-11-26 00:38:01 +01:00
parent 93dcde471f
commit 342714528a
31 changed files with 282 additions and 442 deletions
+5 -10
View File
@@ -5,12 +5,7 @@ import { getTagColorStyle } from '../utils/colors';
import { preventAll } from './events';
import type { DocumentId } from '../types/identifiers';
type DocumentLike = {
id?: string;
title?: string;
tags?: Array<{ id?: string; label?: string; color?: string | null }>;
[key: string]: unknown;
};
import type { Document } from '../types/documents';
interface PendingRemovalTag {
docId?: string;
@@ -18,7 +13,7 @@ interface PendingRemovalTag {
}
interface DesktopDocumentCardProps {
doc: DocumentLike;
doc: Document;
style?: React.CSSProperties;
shouldLoad?: boolean;
dragging?: boolean;
@@ -35,9 +30,9 @@ interface DesktopDocumentCardProps {
onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: DocumentLike) => void;
onDocTagPointerDown?: (event: React.PointerEvent<HTMLElement>, doc: DocumentLike, tag: any) => void;
onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: DocumentLike, tag: any) => void;
onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
onDocTagPointerDown?: (event: React.PointerEvent<HTMLElement>, doc: Document, tag: any) => void;
onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: Document, tag: any) => void;
onDocTagDrag?: (event: React.DragEvent<HTMLElement>) => void;
onDocTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
pendingRemovalTag?: PendingRemovalTag | null;
+3 -8
View File
@@ -2,12 +2,7 @@ import { useEffect } from 'react';
import type { JSX } from 'react';
import { useAssetNavigator } from '../hooks/useAssetNavigator';
import type { Identifier } from '../types/identifiers';
interface DocumentLike {
id?: Identifier;
title?: string;
[key: string]: unknown;
}
import type { Document } from '../types/documents';
interface AssetLike {
id?: Identifier;
@@ -23,7 +18,7 @@ type EnsureAssetUrl = (
options?: { force?: boolean;[key: string]: unknown },
) => Promise<unknown>;
type GetDocumentAsset = (document: DocumentLike | null, assetType: string) => AssetLike | null;
type GetDocumentAsset = (document: Document | null, assetType: string) => AssetLike | null;
interface NavigatorSnapshot {
url: string | null;
@@ -33,7 +28,7 @@ interface NavigatorSnapshot {
}
interface DesktopPreviewCardProps {
doc: DocumentLike | null;
doc: Document | null;
title?: string;
ensureAssetUrl?: EnsureAssetUrl | null;
getDocumentAsset: GetDocumentAsset;
+6 -3
View File
@@ -173,6 +173,7 @@ interface DesktopWorkspaceViewProps extends Omit<DocumentsViewProps, 'entries' |
markLayoutDirty: () => void;
onSelect?: (descriptor: unknown, event?: unknown) => void;
onPromoteSelection?: (docId: Identifier, event?: unknown) => void;
selectionOrderRef: React.MutableRefObject<string[]>;
}
const defaultGetDocumentAsset: GetAsset = () => null;
@@ -194,6 +195,7 @@ const DesktopWorkspace: React.FC<DocumentsViewProps> = ({
clearSelection,
handleEntrySelection,
promoteSelectionOrder,
selectionOrderRef,
configureSelectionEnvironment,
} = useWorkspaceSelectionContext();
const items = useMemo<DeskDocument[]>(
@@ -814,6 +816,7 @@ const DesktopWorkspace: React.FC<DocumentsViewProps> = ({
recalcVisibleDocIds,
dragSettings,
markLayoutDirty,
selectionOrderRef,
};
return <DesktopWorkspaceView {...viewProps} />;
};
@@ -850,13 +853,14 @@ function DesktopWorkspaceView({
resolveBaseMetrics,
bringToFront,
setDraggingId,
canvasSize,
canvasSize: _canvasSize,
openOverlayForDoc,
recalcVisibleDocIds,
dragSettings,
onDocumentActivate,
markLayoutDirty,
onSelect,
selectionOrderRef,
}: DesktopWorkspaceViewProps) {
const handleDeskDocumentActivate = useCallback(
(docId: Identifier) => {
@@ -883,19 +887,18 @@ function DesktopWorkspaceView({
engine,
layoutRef,
dragTransformsRef,
itemRefs,
documentLookup,
ensureDocumentSize,
resolveBaseMetrics,
bringToFront,
setDraggingId,
canvasSize,
openOverlayForDoc,
recalcVisibleDocIds,
settings: dragSettings,
containerRef,
onDocumentActivate: handleDeskDocumentActivate,
markLayoutDirty,
selectionOrderRef,
selectedDocumentIds,
}) as {
handlePointerDown: (event: React.PointerEvent<HTMLElement>, docId: Identifier | null, options: PointerDownOptions) => void;
@@ -1,10 +1,6 @@
import { useEffect, useState } from 'react';
import type { DocumentId } from '../../types/identifiers';
interface DocumentLike {
id?: string;
current_version?: unknown;
tags?: unknown;
}
import type { Document } from '../../types/documents';
interface AssetLike {
id?: string;
@@ -17,11 +13,11 @@ interface PreviewMetadataEntry {
height: number;
}
type GetDocumentAsset = (doc: DocumentLike, type: string) => AssetLike | null;
type GetDocumentAsset = (doc: Document, type: string) => AssetLike | null;
type EnsureAssetUrl = (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise<AssetLike | null>;
const usePreviewMetadata = (
documents: DocumentLike[] | null,
documents: Document[] | null,
getDocumentAsset?: GetDocumentAsset,
ensureAssetUrl?: EnsureAssetUrl,
) => {
@@ -37,7 +33,7 @@ const usePreviewMetadata = (
};
}
const fetchMetadataForDoc = async (doc: DocumentLike) => {
const fetchMetadataForDoc = async (doc: Document) => {
if (!doc?.id) {
return null;
}
+24 -60
View File
@@ -7,10 +7,8 @@ import {
} from 'react';
import type { PointerEvent as ReactPointerEvent } from 'react';
import { preventAll } from './events';
import { clamp } from '../utils/math';
import usePointerTap from '../ui/usePointerTap';
import {
applyDomTransform,
type WorkspaceEngine,
type ActiveDragSession,
type DragGroupItem,
@@ -18,18 +16,10 @@ import {
CARD_BASE_WEIGHT_GRAMS,
CARD_PAGE_WEIGHT_GRAMS,
} from './workspaceEngine';
import { DRAG_HYSTERESIS_SQUARED, EDGE_COLLISION_THRESHOLD } from '../constants/desktop';
import type { DocumentId, Identifier } from '../types/identifiers';
interface DocumentLike {
id?: Identifier | null;
title?: string;
current_version?: {
metadata?: { page_count?: number | string | null } | null;
} | null;
metadata?: { page_count?: number | string | null } | null;
[key: string]: unknown;
}
import { DRAG_HYSTERESIS_SQUARED } from '../constants/desktop';
import type { Identifier } from '../types/identifiers';
import type { Document } from '../types/documents';
import { getEntryId, isDocumentEntry } from '../app/entryKey';
interface DocumentSizeInfo {
width: number;
@@ -56,10 +46,10 @@ interface DragTransform {
type EnsureDocumentSizeFn = (doc: DocumentLike | null) => DocumentSizeInfo | null;
type EnsureDocumentSizeFn = (doc: Document | null) => DocumentSizeInfo | null;
type ResolveBaseMetricsFn = (
doc: DocumentLike | null,
doc: Document | null,
width: number,
height: number,
) => { baseWidth: number; baseHeight: number; baseScale: number };
@@ -81,13 +71,12 @@ interface UseDocumentDragOptions {
engine?: WorkspaceEngine | null;
layoutRef: MutableRefObject<Map<string, LayoutEntry>>;
dragTransformsRef: MutableRefObject<Map<string, DragTransform>>;
itemRefs: MutableRefObject<Map<string, HTMLElement | null>>;
documentLookup: Map<string, DocumentLike>;
selectionOrderRef?: MutableRefObject<string[]>;
documentLookup: Map<string, Document>;
ensureDocumentSize: EnsureDocumentSizeFn;
resolveBaseMetrics: ResolveBaseMetricsFn;
bringToFront: (docId: Identifier | null) => void;
setDraggingId: (docKey: string | null) => void;
canvasSize: { width: number; height: number };
openOverlayForDoc?: (
docId: Identifier | null,
originInfo?: { rotation: number; scale: number; width: number; height: number },
@@ -107,8 +96,8 @@ interface DragTapMetadata {
docTitle: string;
}
const getDocumentPageCount = (doc?: DocumentLike | null): number | null => {
const raw = doc?.current_version?.metadata?.page_count ?? doc?.metadata?.page_count;
const getDocumentPageCount = (doc?: Document | null): number | null => {
const raw = doc?.current_version?.metadata?.page_count ?? (doc?.metadata as { page_count?: unknown })?.page_count;
if (raw == null) {
return null;
}
@@ -116,7 +105,7 @@ const getDocumentPageCount = (doc?: DocumentLike | null): number | null => {
return Number.isFinite(value) ? value : null;
};
const computeDocumentMassGrams = (doc?: DocumentLike | null): number => {
const computeDocumentMassGrams = (doc?: Document | null): number => {
const pages = Math.max(1, Math.round(getDocumentPageCount(doc) ?? 1));
return CARD_BASE_WEIGHT_GRAMS + pages * CARD_PAGE_WEIGHT_GRAMS;
};
@@ -145,19 +134,18 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
engine,
layoutRef,
dragTransformsRef,
itemRefs,
documentLookup,
ensureDocumentSize,
resolveBaseMetrics,
bringToFront,
setDraggingId,
canvasSize,
openOverlayForDoc,
recalcVisibleDocIds,
settings,
containerRef: providedContainerRef,
onDocumentActivate,
markLayoutDirty,
selectionOrderRef,
selectedDocumentIds,
} = options;
@@ -166,8 +154,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
const {
canvasPadding = 24,
defaultCanvasWidth = 1024,
defaultCanvasHeight = 680,
debugDrag = false,
} = settings || {};
@@ -195,21 +181,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
const dragStateRef = useRef<ActiveDragSession | null>(null);
const pendingDragRef = useRef<PendingDrag | null>(null);
const setDragTransform = useCallback((docKey: Identifier | null, transform: DragTransform | null) => {
if (!docKey) {
return;
}
const map = dragTransformsRef?.current;
if (!map) {
return;
}
if (transform) {
map.set(String(docKey), transform);
} else {
map.delete(String(docKey));
}
}, [dragTransformsRef]);
const clearDragTransforms = useCallback(() => {
const map = dragTransformsRef?.current;
if (!map?.clear) {
@@ -268,27 +239,19 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
);
const startDragSession = useCallback((pending: PendingDrag, event: PointerEventLike) => {
const { docId: docIdInput, modifierActive, wasSelected } = pending;
const { docId: docIdInput, modifierActive } = pending;
// 1. Get current global selection
let selectionIds: string[] = (selectedDocumentIds || []).map(String);
const docKey = String(docIdInput);
const selectionFromRef: string[] = Array.isArray(selectionOrderRef?.current)
? selectionOrderRef.current
.map((key) => (isDocumentEntry(key) ? getEntryId(key) : null))
.filter((id): id is string => Boolean(id))
.map(String)
: [];
// 2. Check if clicked doc was already selected BEFORE the click
if (!wasSelected) {
// Not selected before click. Determine what SHOULD be dragged.
const targets = (pending.stackHits && pending.stackHits.length > 0)
? pending.stackHits
: [docKey];
if (modifierActive) {
// Add to selection
selectionIds = [...selectionIds, ...targets];
} else {
// Replace selection
selectionIds = targets;
}
}
// 1. Get current global selection (prefer ref for immediate updates)
let selectionIds: string[] = selectionFromRef.length
? selectionFromRef
: (selectedDocumentIds || []).map(String);
// 3. Filter for valid documents
selectionIds = selectionIds.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id));
@@ -472,6 +435,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
}, [
selectedDocumentIds,
selectionOrderRef,
documentLookup,
layoutRef,
ensureDocumentSize,
+2 -5
View File
@@ -1,12 +1,9 @@
import { clamp, formatTransform, toNumber } from '../utils/math';
import { clamp, formatTransform } from '../utils/math';
import {
Point,
Polygon,
clipPolygon,
isPointInsideConvex,
polygonCentroid,
iterateEdges,
forEachVertex,
} from './utils/geometry';
import { computeCardBounds } from './utils/layoutUtils';
import { fetchLayoutRecords, upsertLayoutRecords } from './db';
@@ -631,7 +628,7 @@ export class WorkspaceEngine {
return this.state.type === 'dragging' ? this.state.session : null;
}
beginDrag(docIds: Array<string | null> = []): void {
beginDrag(_docIds: Array<string | null> = []): void {
// Legacy method support or internal helper
// If we are starting a drag, we should transition state
// But this method was used to set flags.