refactor: Introduce dedicated Document and Asset types and migrate codebase from DocumentLike.
This commit is contained in:
@@ -9,13 +9,7 @@ import type { DocumentId } from '../types/identifiers';
|
|||||||
|
|
||||||
type FolderId = DocumentId | 'root';
|
type FolderId = DocumentId | 'root';
|
||||||
|
|
||||||
type DocumentLike = {
|
import type { Document } from '../types/documents';
|
||||||
id?: DocumentId;
|
|
||||||
folder_id?: FolderId | null;
|
|
||||||
filename?: string | null;
|
|
||||||
current_version?: Record<string, unknown>;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DocumentLink = {
|
type DocumentLink = {
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -29,11 +23,11 @@ type NavigateHandler = (path: string, options?: { replace?: boolean }) => void;
|
|||||||
interface UseDocumentPreviewArgs {
|
interface UseDocumentPreviewArgs {
|
||||||
routeDocumentId?: DocumentId | null;
|
routeDocumentId?: DocumentId | null;
|
||||||
documentsManager: {
|
documentsManager: {
|
||||||
getById: (id: DocumentId) => DocumentLike | null;
|
getById: (id: DocumentId) => Document | null;
|
||||||
ensure: (id: DocumentId) => Promise<DocumentLike | null>;
|
ensure: (id: DocumentId) => Promise<Document | null>;
|
||||||
getMany: (ids: DocumentId[]) => DocumentLike[];
|
getMany: (ids: DocumentId[]) => Document[];
|
||||||
subscribe: (listener: () => void) => () => void;
|
subscribe: (listener: () => void) => () => void;
|
||||||
ingest: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean };
|
ingest: (docs: unknown[]) => { canonical: Document[]; changed: boolean };
|
||||||
};
|
};
|
||||||
selectedFolder?: FolderId | null;
|
selectedFolder?: FolderId | null;
|
||||||
notifyApiError: (error: unknown, message: string) => void;
|
notifyApiError: (error: unknown, message: string) => void;
|
||||||
@@ -50,7 +44,7 @@ interface UseDocumentPreviewArgs {
|
|||||||
interface UseDocumentPreviewResult {
|
interface UseDocumentPreviewResult {
|
||||||
documentLinks: Map<DocumentId, DocumentLink>;
|
documentLinks: Map<DocumentId, DocumentLink>;
|
||||||
ensureDownloadUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise<DocumentLink | null>;
|
ensureDownloadUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise<DocumentLink | null>;
|
||||||
ensurePreviewData: (documentId: DocumentId) => Promise<DocumentLike | null>;
|
ensurePreviewData: (documentId: DocumentId) => Promise<Document | null>;
|
||||||
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void;
|
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void;
|
||||||
closeDocumentPreview: (folderId?: FolderId) => void;
|
closeDocumentPreview: (folderId?: FolderId) => void;
|
||||||
resetPreviewState: () => void;
|
resetPreviewState: () => void;
|
||||||
@@ -149,7 +143,7 @@ const useDocumentPreview = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ensurePreviewData = useCallback(
|
const ensurePreviewData = useCallback(
|
||||||
async (documentId: DocumentId): Promise<DocumentLike | null> => {
|
async (documentId: DocumentId): Promise<Document | null> => {
|
||||||
if (!documentId) return null;
|
if (!documentId) return null;
|
||||||
|
|
||||||
const findInCache = () => documentsManager.getById(documentId);
|
const findInCache = () => documentsManager.getById(documentId);
|
||||||
@@ -163,7 +157,7 @@ const useDocumentPreview = ({
|
|||||||
if (!doc) {
|
if (!doc) {
|
||||||
const fetched = await fetchDocument(documentId);
|
const fetched = await fetchDocument(documentId);
|
||||||
const { canonical } = documentsManager.ingest([fetched as unknown]);
|
const { canonical } = documentsManager.ingest([fetched as unknown]);
|
||||||
doc = (canonical[0] as DocumentLike | undefined) || null;
|
doc = (canonical[0] as Document | undefined) || null;
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
throw new Error('Document metadata unavailable.');
|
throw new Error('Document metadata unavailable.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TAG_FILTER_UNTAGGED } from './workspaceUtils';
|
|||||||
import { listDocuments } from '../lib/apiClient';
|
import { listDocuments } from '../lib/apiClient';
|
||||||
import type { Identifier } from '../types/identifiers';
|
import type { Identifier } from '../types/identifiers';
|
||||||
|
|
||||||
type DocumentLike = { id?: Identifier } & Record<string, unknown>;
|
import type { Document } from '../types/documents';
|
||||||
|
|
||||||
type ApiClient = {
|
type ApiClient = {
|
||||||
get: <T = unknown>(url: string, config?: { params?: Record<string, unknown> }) => Promise<{ data: T }>;
|
get: <T = unknown>(url: string, config?: { params?: Record<string, unknown> }) => Promise<{ data: T }>;
|
||||||
@@ -23,7 +23,7 @@ interface UseDocumentsSearchArgs {
|
|||||||
notifyApiError: (error: unknown, message: string) => void;
|
notifyApiError: (error: unknown, message: string) => void;
|
||||||
setSearchIncludeDescendants: (value: boolean) => void;
|
setSearchIncludeDescendants: (value: boolean) => void;
|
||||||
documentsManager: {
|
documentsManager: {
|
||||||
ingest: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean };
|
ingest: (docs: unknown[]) => { canonical: Document[]; changed: boolean };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,10 @@
|
|||||||
import type { Identifier } from './types/identifiers';
|
import type { Identifier } from './types/identifiers';
|
||||||
|
import type { AssetObject, AssetLike } from './types/assets';
|
||||||
|
import type { DocumentVersion, Document } from './types/documents';
|
||||||
|
|
||||||
type Nullable<T> = T | null;
|
type Nullable<T> = T | null;
|
||||||
|
|
||||||
export interface AssetObject {
|
export type { AssetObject, AssetLike, DocumentVersion as DocumentVersionLike, Document };
|
||||||
ordinal?: number;
|
|
||||||
url?: string | null;
|
|
||||||
metadata?: Record<string, unknown> | null;
|
|
||||||
expires_at?: number;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssetLike {
|
|
||||||
id?: Identifier;
|
|
||||||
asset_type?: string;
|
|
||||||
cardinality?: number | null;
|
|
||||||
download?: { url: string; expires_at: number } | null;
|
|
||||||
metadata?: Record<string, unknown> | null;
|
|
||||||
assets?: Record<string, AssetLike> | AssetLike[] | null;
|
|
||||||
objects?: AssetObject[] | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocumentVersionLike {
|
|
||||||
assets?: Record<string, AssetLike> | AssetLike[] | null;
|
|
||||||
metadata?: Record<string, unknown> & { page_count?: number } | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocumentLike {
|
|
||||||
id?: Identifier;
|
|
||||||
current_version?: DocumentVersionLike | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const resolveAssetExpiresAt = (asset?: { download?: { expires_at: number } | null } | null): number | null =>
|
export const resolveAssetExpiresAt = (asset?: { download?: { expires_at: number } | null } | null): number | null =>
|
||||||
asset?.download?.expires_at ?? null;
|
asset?.download?.expires_at ?? null;
|
||||||
@@ -45,7 +18,7 @@ export type EnsureAssetUrl = (
|
|||||||
options?: { force?: boolean;[key: string]: unknown },
|
options?: { force?: boolean;[key: string]: unknown },
|
||||||
) => Promise<unknown>;
|
) => Promise<unknown>;
|
||||||
|
|
||||||
export type GetAsset = (document: DocumentLike, assetType: string) => Nullable<AssetLike>;
|
export type GetAsset = (document: Document, assetType: string) => Nullable<AssetLike>;
|
||||||
|
|
||||||
export const getAssetFromGroup = (
|
export const getAssetFromGroup = (
|
||||||
assets?: AssetLike[] | Record<string, AssetLike> | null,
|
assets?: AssetLike[] | Record<string, AssetLike> | null,
|
||||||
@@ -62,7 +35,7 @@ export const getAssetFromGroup = (
|
|||||||
return assets?.[assetType] || null;
|
return assets?.[assetType] || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssetFromVersion = (currentVersion: Nullable<DocumentVersionLike>, assetType: string) => {
|
export const getAssetFromVersion = (currentVersion: Nullable<DocumentVersion>, assetType: string) => {
|
||||||
if (!currentVersion) {
|
if (!currentVersion) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -162,7 +135,7 @@ export class AssetView {
|
|||||||
export const createAssetView = (asset?: AssetLike | null): AssetView => new AssetView(asset);
|
export const createAssetView = (asset?: AssetLike | null): AssetView => new AssetView(asset);
|
||||||
|
|
||||||
export const resolveDocumentAssetUrl = (
|
export const resolveDocumentAssetUrl = (
|
||||||
doc: Nullable<DocumentLike>,
|
doc: Nullable<Document>,
|
||||||
type: string,
|
type: string,
|
||||||
{
|
{
|
||||||
ensureAssetUrl,
|
ensureAssetUrl,
|
||||||
|
|||||||
@@ -5,12 +5,7 @@ import { getTagColorStyle } from '../utils/colors';
|
|||||||
import { preventAll } from './events';
|
import { preventAll } from './events';
|
||||||
import type { DocumentId } from '../types/identifiers';
|
import type { DocumentId } from '../types/identifiers';
|
||||||
|
|
||||||
type DocumentLike = {
|
import type { Document } from '../types/documents';
|
||||||
id?: string;
|
|
||||||
title?: string;
|
|
||||||
tags?: Array<{ id?: string; label?: string; color?: string | null }>;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PendingRemovalTag {
|
interface PendingRemovalTag {
|
||||||
docId?: string;
|
docId?: string;
|
||||||
@@ -18,7 +13,7 @@ interface PendingRemovalTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DesktopDocumentCardProps {
|
interface DesktopDocumentCardProps {
|
||||||
doc: DocumentLike;
|
doc: Document;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
shouldLoad?: boolean;
|
shouldLoad?: boolean;
|
||||||
dragging?: boolean;
|
dragging?: boolean;
|
||||||
@@ -35,9 +30,9 @@ interface DesktopDocumentCardProps {
|
|||||||
onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
||||||
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
||||||
onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
||||||
onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: DocumentLike) => void;
|
onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: Document) => void;
|
||||||
onDocTagPointerDown?: (event: React.PointerEvent<HTMLElement>, doc: DocumentLike, tag: any) => void;
|
onDocTagPointerDown?: (event: React.PointerEvent<HTMLElement>, doc: Document, tag: any) => void;
|
||||||
onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: DocumentLike, tag: any) => void;
|
onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: Document, tag: any) => void;
|
||||||
onDocTagDrag?: (event: React.DragEvent<HTMLElement>) => void;
|
onDocTagDrag?: (event: React.DragEvent<HTMLElement>) => void;
|
||||||
onDocTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
|
onDocTagDragEnd?: (event: React.DragEvent<HTMLElement>) => void;
|
||||||
pendingRemovalTag?: PendingRemovalTag | null;
|
pendingRemovalTag?: PendingRemovalTag | null;
|
||||||
|
|||||||
@@ -2,12 +2,7 @@ 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';
|
import type { Identifier } from '../types/identifiers';
|
||||||
|
import type { Document } from '../types/documents';
|
||||||
interface DocumentLike {
|
|
||||||
id?: Identifier;
|
|
||||||
title?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AssetLike {
|
interface AssetLike {
|
||||||
id?: Identifier;
|
id?: Identifier;
|
||||||
@@ -23,7 +18,7 @@ type EnsureAssetUrl = (
|
|||||||
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: Document | null, assetType: string) => AssetLike | null;
|
||||||
|
|
||||||
interface NavigatorSnapshot {
|
interface NavigatorSnapshot {
|
||||||
url: string | null;
|
url: string | null;
|
||||||
@@ -33,7 +28,7 @@ interface NavigatorSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DesktopPreviewCardProps {
|
interface DesktopPreviewCardProps {
|
||||||
doc: DocumentLike | null;
|
doc: Document | null;
|
||||||
title?: string;
|
title?: string;
|
||||||
ensureAssetUrl?: EnsureAssetUrl | null;
|
ensureAssetUrl?: EnsureAssetUrl | null;
|
||||||
getDocumentAsset: GetDocumentAsset;
|
getDocumentAsset: GetDocumentAsset;
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ interface DesktopWorkspaceViewProps extends Omit<DocumentsViewProps, 'entries' |
|
|||||||
markLayoutDirty: () => void;
|
markLayoutDirty: () => void;
|
||||||
onSelect?: (descriptor: unknown, event?: unknown) => void;
|
onSelect?: (descriptor: unknown, event?: unknown) => void;
|
||||||
onPromoteSelection?: (docId: Identifier, event?: unknown) => void;
|
onPromoteSelection?: (docId: Identifier, event?: unknown) => void;
|
||||||
|
selectionOrderRef: React.MutableRefObject<string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultGetDocumentAsset: GetAsset = () => null;
|
const defaultGetDocumentAsset: GetAsset = () => null;
|
||||||
@@ -194,6 +195,7 @@ const DesktopWorkspace: React.FC<DocumentsViewProps> = ({
|
|||||||
clearSelection,
|
clearSelection,
|
||||||
handleEntrySelection,
|
handleEntrySelection,
|
||||||
promoteSelectionOrder,
|
promoteSelectionOrder,
|
||||||
|
selectionOrderRef,
|
||||||
configureSelectionEnvironment,
|
configureSelectionEnvironment,
|
||||||
} = useWorkspaceSelectionContext();
|
} = useWorkspaceSelectionContext();
|
||||||
const items = useMemo<DeskDocument[]>(
|
const items = useMemo<DeskDocument[]>(
|
||||||
@@ -814,6 +816,7 @@ const DesktopWorkspace: React.FC<DocumentsViewProps> = ({
|
|||||||
recalcVisibleDocIds,
|
recalcVisibleDocIds,
|
||||||
dragSettings,
|
dragSettings,
|
||||||
markLayoutDirty,
|
markLayoutDirty,
|
||||||
|
selectionOrderRef,
|
||||||
};
|
};
|
||||||
return <DesktopWorkspaceView {...viewProps} />;
|
return <DesktopWorkspaceView {...viewProps} />;
|
||||||
};
|
};
|
||||||
@@ -850,13 +853,14 @@ function DesktopWorkspaceView({
|
|||||||
resolveBaseMetrics,
|
resolveBaseMetrics,
|
||||||
bringToFront,
|
bringToFront,
|
||||||
setDraggingId,
|
setDraggingId,
|
||||||
canvasSize,
|
canvasSize: _canvasSize,
|
||||||
openOverlayForDoc,
|
openOverlayForDoc,
|
||||||
recalcVisibleDocIds,
|
recalcVisibleDocIds,
|
||||||
dragSettings,
|
dragSettings,
|
||||||
onDocumentActivate,
|
onDocumentActivate,
|
||||||
markLayoutDirty,
|
markLayoutDirty,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
selectionOrderRef,
|
||||||
}: DesktopWorkspaceViewProps) {
|
}: DesktopWorkspaceViewProps) {
|
||||||
const handleDeskDocumentActivate = useCallback(
|
const handleDeskDocumentActivate = useCallback(
|
||||||
(docId: Identifier) => {
|
(docId: Identifier) => {
|
||||||
@@ -883,19 +887,18 @@ function DesktopWorkspaceView({
|
|||||||
engine,
|
engine,
|
||||||
layoutRef,
|
layoutRef,
|
||||||
dragTransformsRef,
|
dragTransformsRef,
|
||||||
itemRefs,
|
|
||||||
documentLookup,
|
documentLookup,
|
||||||
ensureDocumentSize,
|
ensureDocumentSize,
|
||||||
resolveBaseMetrics,
|
resolveBaseMetrics,
|
||||||
bringToFront,
|
bringToFront,
|
||||||
setDraggingId,
|
setDraggingId,
|
||||||
canvasSize,
|
|
||||||
openOverlayForDoc,
|
openOverlayForDoc,
|
||||||
recalcVisibleDocIds,
|
recalcVisibleDocIds,
|
||||||
settings: dragSettings,
|
settings: dragSettings,
|
||||||
containerRef,
|
containerRef,
|
||||||
onDocumentActivate: handleDeskDocumentActivate,
|
onDocumentActivate: handleDeskDocumentActivate,
|
||||||
markLayoutDirty,
|
markLayoutDirty,
|
||||||
|
selectionOrderRef,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
}) as {
|
}) as {
|
||||||
handlePointerDown: (event: React.PointerEvent<HTMLElement>, docId: Identifier | null, options: PointerDownOptions) => void;
|
handlePointerDown: (event: React.PointerEvent<HTMLElement>, docId: Identifier | null, options: PointerDownOptions) => void;
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { DocumentId } from '../../types/identifiers';
|
import type { DocumentId } from '../../types/identifiers';
|
||||||
interface DocumentLike {
|
import type { Document } from '../../types/documents';
|
||||||
id?: string;
|
|
||||||
current_version?: unknown;
|
|
||||||
tags?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AssetLike {
|
interface AssetLike {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -17,11 +13,11 @@ interface PreviewMetadataEntry {
|
|||||||
height: number;
|
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>;
|
type EnsureAssetUrl = (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise<AssetLike | null>;
|
||||||
|
|
||||||
const usePreviewMetadata = (
|
const usePreviewMetadata = (
|
||||||
documents: DocumentLike[] | null,
|
documents: Document[] | null,
|
||||||
getDocumentAsset?: GetDocumentAsset,
|
getDocumentAsset?: GetDocumentAsset,
|
||||||
ensureAssetUrl?: EnsureAssetUrl,
|
ensureAssetUrl?: EnsureAssetUrl,
|
||||||
) => {
|
) => {
|
||||||
@@ -37,7 +33,7 @@ const usePreviewMetadata = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMetadataForDoc = async (doc: DocumentLike) => {
|
const fetchMetadataForDoc = async (doc: Document) => {
|
||||||
if (!doc?.id) {
|
if (!doc?.id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import type { PointerEvent as ReactPointerEvent } from 'react';
|
import type { PointerEvent as ReactPointerEvent } from 'react';
|
||||||
import { preventAll } from './events';
|
import { preventAll } from './events';
|
||||||
import { clamp } from '../utils/math';
|
|
||||||
import usePointerTap from '../ui/usePointerTap';
|
import usePointerTap from '../ui/usePointerTap';
|
||||||
import {
|
import {
|
||||||
applyDomTransform,
|
|
||||||
type WorkspaceEngine,
|
type WorkspaceEngine,
|
||||||
type ActiveDragSession,
|
type ActiveDragSession,
|
||||||
type DragGroupItem,
|
type DragGroupItem,
|
||||||
@@ -18,18 +16,10 @@ import {
|
|||||||
CARD_BASE_WEIGHT_GRAMS,
|
CARD_BASE_WEIGHT_GRAMS,
|
||||||
CARD_PAGE_WEIGHT_GRAMS,
|
CARD_PAGE_WEIGHT_GRAMS,
|
||||||
} from './workspaceEngine';
|
} from './workspaceEngine';
|
||||||
import { DRAG_HYSTERESIS_SQUARED, EDGE_COLLISION_THRESHOLD } from '../constants/desktop';
|
import { DRAG_HYSTERESIS_SQUARED } from '../constants/desktop';
|
||||||
import type { DocumentId, Identifier } from '../types/identifiers';
|
import type { Identifier } from '../types/identifiers';
|
||||||
|
import type { Document } from '../types/documents';
|
||||||
interface DocumentLike {
|
import { getEntryId, isDocumentEntry } from '../app/entryKey';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DocumentSizeInfo {
|
interface DocumentSizeInfo {
|
||||||
width: number;
|
width: number;
|
||||||
@@ -56,10 +46,10 @@ interface DragTransform {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
type EnsureDocumentSizeFn = (doc: DocumentLike | null) => DocumentSizeInfo | null;
|
type EnsureDocumentSizeFn = (doc: Document | null) => DocumentSizeInfo | null;
|
||||||
|
|
||||||
type ResolveBaseMetricsFn = (
|
type ResolveBaseMetricsFn = (
|
||||||
doc: DocumentLike | null,
|
doc: Document | null,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
) => { baseWidth: number; baseHeight: number; baseScale: number };
|
) => { baseWidth: number; baseHeight: number; baseScale: number };
|
||||||
@@ -81,13 +71,12 @@ interface UseDocumentDragOptions {
|
|||||||
engine?: WorkspaceEngine | null;
|
engine?: WorkspaceEngine | null;
|
||||||
layoutRef: MutableRefObject<Map<string, LayoutEntry>>;
|
layoutRef: MutableRefObject<Map<string, LayoutEntry>>;
|
||||||
dragTransformsRef: MutableRefObject<Map<string, DragTransform>>;
|
dragTransformsRef: MutableRefObject<Map<string, DragTransform>>;
|
||||||
itemRefs: MutableRefObject<Map<string, HTMLElement | null>>;
|
selectionOrderRef?: MutableRefObject<string[]>;
|
||||||
documentLookup: Map<string, DocumentLike>;
|
documentLookup: Map<string, Document>;
|
||||||
ensureDocumentSize: EnsureDocumentSizeFn;
|
ensureDocumentSize: EnsureDocumentSizeFn;
|
||||||
resolveBaseMetrics: ResolveBaseMetricsFn;
|
resolveBaseMetrics: ResolveBaseMetricsFn;
|
||||||
bringToFront: (docId: Identifier | null) => void;
|
bringToFront: (docId: Identifier | null) => void;
|
||||||
setDraggingId: (docKey: string | null) => void;
|
setDraggingId: (docKey: string | null) => void;
|
||||||
canvasSize: { width: number; height: number };
|
|
||||||
openOverlayForDoc?: (
|
openOverlayForDoc?: (
|
||||||
docId: Identifier | null,
|
docId: Identifier | null,
|
||||||
originInfo?: { rotation: number; scale: number; width: number; height: number },
|
originInfo?: { rotation: number; scale: number; width: number; height: number },
|
||||||
@@ -107,8 +96,8 @@ interface DragTapMetadata {
|
|||||||
docTitle: string;
|
docTitle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDocumentPageCount = (doc?: DocumentLike | null): number | null => {
|
const getDocumentPageCount = (doc?: Document | null): number | null => {
|
||||||
const raw = doc?.current_version?.metadata?.page_count ?? doc?.metadata?.page_count;
|
const raw = doc?.current_version?.metadata?.page_count ?? (doc?.metadata as { page_count?: unknown })?.page_count;
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -116,7 +105,7 @@ const getDocumentPageCount = (doc?: DocumentLike | null): number | null => {
|
|||||||
return Number.isFinite(value) ? value : 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));
|
const pages = Math.max(1, Math.round(getDocumentPageCount(doc) ?? 1));
|
||||||
return CARD_BASE_WEIGHT_GRAMS + pages * CARD_PAGE_WEIGHT_GRAMS;
|
return CARD_BASE_WEIGHT_GRAMS + pages * CARD_PAGE_WEIGHT_GRAMS;
|
||||||
};
|
};
|
||||||
@@ -145,19 +134,18 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
|
|||||||
engine,
|
engine,
|
||||||
layoutRef,
|
layoutRef,
|
||||||
dragTransformsRef,
|
dragTransformsRef,
|
||||||
itemRefs,
|
|
||||||
documentLookup,
|
documentLookup,
|
||||||
ensureDocumentSize,
|
ensureDocumentSize,
|
||||||
resolveBaseMetrics,
|
resolveBaseMetrics,
|
||||||
bringToFront,
|
bringToFront,
|
||||||
setDraggingId,
|
setDraggingId,
|
||||||
canvasSize,
|
|
||||||
openOverlayForDoc,
|
openOverlayForDoc,
|
||||||
recalcVisibleDocIds,
|
recalcVisibleDocIds,
|
||||||
settings,
|
settings,
|
||||||
containerRef: providedContainerRef,
|
containerRef: providedContainerRef,
|
||||||
onDocumentActivate,
|
onDocumentActivate,
|
||||||
markLayoutDirty,
|
markLayoutDirty,
|
||||||
|
selectionOrderRef,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
@@ -166,8 +154,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
canvasPadding = 24,
|
canvasPadding = 24,
|
||||||
defaultCanvasWidth = 1024,
|
|
||||||
defaultCanvasHeight = 680,
|
|
||||||
debugDrag = false,
|
debugDrag = false,
|
||||||
} = settings || {};
|
} = settings || {};
|
||||||
|
|
||||||
@@ -195,21 +181,6 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
|
|||||||
const dragStateRef = useRef<ActiveDragSession | null>(null);
|
const dragStateRef = useRef<ActiveDragSession | null>(null);
|
||||||
const pendingDragRef = useRef<PendingDrag | 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 clearDragTransforms = useCallback(() => {
|
||||||
const map = dragTransformsRef?.current;
|
const map = dragTransformsRef?.current;
|
||||||
if (!map?.clear) {
|
if (!map?.clear) {
|
||||||
@@ -268,27 +239,19 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
|
|||||||
);
|
);
|
||||||
|
|
||||||
const startDragSession = useCallback((pending: PendingDrag, event: PointerEventLike) => {
|
const startDragSession = useCallback((pending: PendingDrag, event: PointerEventLike) => {
|
||||||
const { docId: docIdInput, modifierActive, wasSelected } = pending;
|
const { docId: docIdInput, modifierActive } = pending;
|
||||||
|
|
||||||
// 1. Get current global selection
|
const selectionFromRef: string[] = Array.isArray(selectionOrderRef?.current)
|
||||||
let selectionIds: string[] = (selectedDocumentIds || []).map(String);
|
? selectionOrderRef.current
|
||||||
const docKey = String(docIdInput);
|
.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
|
// 1. Get current global selection (prefer ref for immediate updates)
|
||||||
if (!wasSelected) {
|
let selectionIds: string[] = selectionFromRef.length
|
||||||
// Not selected before click. Determine what SHOULD be dragged.
|
? selectionFromRef
|
||||||
const targets = (pending.stackHits && pending.stackHits.length > 0)
|
: (selectedDocumentIds || []).map(String);
|
||||||
? pending.stackHits
|
|
||||||
: [docKey];
|
|
||||||
|
|
||||||
if (modifierActive) {
|
|
||||||
// Add to selection
|
|
||||||
selectionIds = [...selectionIds, ...targets];
|
|
||||||
} else {
|
|
||||||
// Replace selection
|
|
||||||
selectionIds = targets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Filter for valid documents
|
// 3. Filter for valid documents
|
||||||
selectionIds = selectionIds.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id));
|
selectionIds = selectionIds.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id));
|
||||||
@@ -472,6 +435,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions & { selectedDocumentIds
|
|||||||
|
|
||||||
}, [
|
}, [
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
|
selectionOrderRef,
|
||||||
documentLookup,
|
documentLookup,
|
||||||
layoutRef,
|
layoutRef,
|
||||||
ensureDocumentSize,
|
ensureDocumentSize,
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { clamp, formatTransform, toNumber } from '../utils/math';
|
import { clamp, formatTransform } from '../utils/math';
|
||||||
import {
|
import {
|
||||||
Point,
|
|
||||||
Polygon,
|
Polygon,
|
||||||
clipPolygon,
|
clipPolygon,
|
||||||
isPointInsideConvex,
|
isPointInsideConvex,
|
||||||
polygonCentroid,
|
polygonCentroid,
|
||||||
iterateEdges,
|
|
||||||
forEachVertex,
|
|
||||||
} from './utils/geometry';
|
} from './utils/geometry';
|
||||||
import { computeCardBounds } from './utils/layoutUtils';
|
import { computeCardBounds } from './utils/layoutUtils';
|
||||||
import { fetchLayoutRecords, upsertLayoutRecords } from './db';
|
import { fetchLayoutRecords, upsertLayoutRecords } from './db';
|
||||||
@@ -631,7 +628,7 @@ export class WorkspaceEngine {
|
|||||||
return this.state.type === 'dragging' ? this.state.session : null;
|
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
|
// Legacy method support or internal helper
|
||||||
// If we are starting a drag, we should transition state
|
// If we are starting a drag, we should transition state
|
||||||
// But this method was used to set flags.
|
// But this method was used to set flags.
|
||||||
|
|||||||
@@ -3,17 +3,12 @@ import { createPortal } from 'react-dom';
|
|||||||
import { clamp } from '../utils/math';
|
import { clamp } from '../utils/math';
|
||||||
import PdfViewer from '../preview/PdfViewer';
|
import PdfViewer from '../preview/PdfViewer';
|
||||||
|
|
||||||
type DocumentLike = {
|
import type { Document } from '../types/documents';
|
||||||
id?: string;
|
|
||||||
title?: string;
|
|
||||||
mime_type?: string | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PreviewZoomOverlayProps {
|
interface PreviewZoomOverlayProps {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
document?: DocumentLike | null;
|
document?: Document | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NaturalSize = { width: number | null; height: number | null };
|
type NaturalSize = { width: number | null; height: number | null };
|
||||||
@@ -26,7 +21,7 @@ type DocumentLink = {
|
|||||||
mimeType?: string | null;
|
mimeType?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DocumentLikeWithPreview = DocumentLike & { documentLink?: DocumentLink };
|
type DocumentWithPreview = Document & { documentLink?: DocumentLink };
|
||||||
|
|
||||||
const determineDisplayKind = (entry?: DocumentLink | null): DisplayKind => {
|
const determineDisplayKind = (entry?: DocumentLink | null): DisplayKind => {
|
||||||
const type = entry?.mimeType?.toLowerCase?.() || '';
|
const type = entry?.mimeType?.toLowerCase?.() || '';
|
||||||
@@ -55,7 +50,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
const [naturalSize, setNaturalSize] = useState<NaturalSize>({ width: null, height: null });
|
const [naturalSize, setNaturalSize] = useState<NaturalSize>({ width: null, height: null });
|
||||||
const [renderBackdrop, setRenderBackdrop] = useState(false);
|
const [renderBackdrop, setRenderBackdrop] = useState(false);
|
||||||
const [isBackdropVisible, setBackdropVisible] = useState(false);
|
const [isBackdropVisible, setBackdropVisible] = useState(false);
|
||||||
const [documentSnapshot, setDocumentSnapshot] = useState<DocumentLikeWithPreview | null>(null);
|
const [documentSnapshot, setDocumentSnapshot] = useState<DocumentWithPreview | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mediaRef = useRef<HTMLImageElement | null>(null);
|
const mediaRef = useRef<HTMLImageElement | null>(null);
|
||||||
const focusRef = useRef<FocusPoint>(null);
|
const focusRef = useRef<FocusPoint>(null);
|
||||||
@@ -63,7 +58,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
const visibilityTimerRef = useRef<number | null>(null);
|
const visibilityTimerRef = useRef<number | null>(null);
|
||||||
const displayTimerRef = useRef<number | null>(null);
|
const displayTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const currentDocument = overlayDocument as DocumentLikeWithPreview | null;
|
const currentDocument = overlayDocument;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentDocument?.documentLink?.url) {
|
if (currentDocument?.documentLink?.url) {
|
||||||
|
|||||||
@@ -7,13 +7,7 @@ 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';
|
import type { Identifier } from '../types/identifiers';
|
||||||
|
import type { Document } from '../types/documents';
|
||||||
interface DocumentLike {
|
|
||||||
id?: Identifier;
|
|
||||||
folder_id?: Identifier | 'root';
|
|
||||||
title?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FolderNode {
|
interface FolderNode {
|
||||||
id: Identifier | 'root';
|
id: Identifier | 'root';
|
||||||
@@ -22,10 +16,10 @@ interface FolderNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UseDetailWorkspaceArgs {
|
interface UseDetailWorkspaceArgs {
|
||||||
documents: DocumentLike[];
|
documents: Document[];
|
||||||
selectionOrder: string[];
|
selectionOrder: string[];
|
||||||
selectedDocumentIds: Identifier[];
|
selectedDocumentIds: Identifier[];
|
||||||
documentLookup: Map<Identifier, DocumentLike>;
|
documentLookup: Map<Identifier, Document>;
|
||||||
folderNodes: Map<Identifier | 'root', FolderNode>;
|
folderNodes: Map<Identifier | 'root', FolderNode>;
|
||||||
ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>;
|
ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>;
|
||||||
detailPanelControlRef: MutableRefObject<{ open?: (args?: { documentIds?: Identifier[] }) => void; close?: () => void } | null>;
|
detailPanelControlRef: MutableRefObject<{ open?: (args?: { documentIds?: Identifier[] }) => void; close?: () => void } | null>;
|
||||||
@@ -35,7 +29,7 @@ interface UseDetailWorkspaceArgs {
|
|||||||
openDocumentPreview?: (args: { documentIds: Identifier[] }) => void;
|
openDocumentPreview?: (args: { documentIds: Identifier[] }) => void;
|
||||||
handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise<boolean> | boolean;
|
handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise<boolean> | boolean;
|
||||||
handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise<boolean> | boolean;
|
handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise<boolean> | boolean;
|
||||||
handleDocumentTagAdd?: (doc: DocumentLike, value: string, context?: { option?: unknown }) => void;
|
handleDocumentTagAdd?: (doc: Document, value: string, context?: { option?: unknown }) => void;
|
||||||
handleTagRemove?: (...args: unknown[]) => void;
|
handleTagRemove?: (...args: unknown[]) => void;
|
||||||
ensureAssetUrl?: EnsureAssetUrl;
|
ensureAssetUrl?: EnsureAssetUrl;
|
||||||
getDocumentAsset?: GetDocumentAsset;
|
getDocumentAsset?: GetDocumentAsset;
|
||||||
@@ -55,8 +49,8 @@ interface UseDetailWorkspaceResult {
|
|||||||
handleDetailPanelClose: () => void;
|
handleDetailPanelClose: () => void;
|
||||||
inspectDocument: (docId: Identifier | null) => void;
|
inspectDocument: (docId: Identifier | null) => void;
|
||||||
previewActive: boolean;
|
previewActive: boolean;
|
||||||
previewWorkspaceDocument: DocumentLike | null;
|
previewWorkspaceDocument: Document | null;
|
||||||
resolveThumbnailUrlForDoc: (doc: DocumentLike | null) => string | null;
|
resolveThumbnailUrlForDoc: (doc: Document | null) => string | null;
|
||||||
resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>;
|
resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,16 +28,7 @@ interface CorrespondentEntry {
|
|||||||
count?: number;
|
count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DocumentLike {
|
import type { Document } from '../types/documents';
|
||||||
id?: Identifier;
|
|
||||||
title?: string;
|
|
||||||
issued_at?: string | null;
|
|
||||||
folder_id?: FolderId | null;
|
|
||||||
current_version?: { version_number?: number } | null;
|
|
||||||
tags?: TagEntry[];
|
|
||||||
correspondents?: CorrespondentEntry[];
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TagSectionProps {
|
interface TagSectionProps {
|
||||||
tags?: TagEntry[];
|
tags?: TagEntry[];
|
||||||
@@ -62,14 +53,14 @@ interface CorrespondentSectionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentSummarySectionProps {
|
export interface DocumentSummarySectionProps {
|
||||||
document?: DocumentLike | null;
|
document?: Document | null;
|
||||||
tagLookupById?: Map<TagId, TagEntry>;
|
tagLookupById?: Map<TagId, TagEntry>;
|
||||||
tagOptions?: SelectionAssignmentMenuItem[];
|
tagOptions?: SelectionAssignmentMenuItem[];
|
||||||
onTagAdd?: (doc: DocumentLike, value: string, context?: { option?: unknown }) => void;
|
onTagAdd?: (doc: Document, value: string, context?: { option?: unknown }) => void;
|
||||||
onTagRemove?: (docId: Identifier | undefined, tagId: TagId | 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: Document; 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;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { CSSProperties, JSX, MutableRefObject } from 'react';
|
import type { CSSProperties, JSX, MutableRefObject } from 'react';
|
||||||
|
import type { Document } from '../types/documents';
|
||||||
import {
|
import {
|
||||||
getAssetFromVersion,
|
getAssetFromVersion,
|
||||||
resolveDocumentAssetUrl,
|
resolveDocumentAssetUrl,
|
||||||
@@ -7,7 +8,6 @@ import {
|
|||||||
} from '../asset_manager';
|
} from '../asset_manager';
|
||||||
import { DEFAULT_THUMBNAIL_SIZE } from '../constants/documents';
|
import { DEFAULT_THUMBNAIL_SIZE } from '../constants/documents';
|
||||||
import type {
|
import type {
|
||||||
DocumentLike as AssetManagerDocumentLike,
|
|
||||||
AssetLike as AssetManagerAssetLike,
|
AssetLike as AssetManagerAssetLike,
|
||||||
EnsureAssetUrl as AssetManagerEnsureAssetUrl,
|
EnsureAssetUrl as AssetManagerEnsureAssetUrl,
|
||||||
GetAsset as AssetManagerGetAsset,
|
GetAsset as AssetManagerGetAsset,
|
||||||
@@ -65,18 +65,19 @@ const useLazyVisibility = (
|
|||||||
return { ref: targetRef, isVisible };
|
return { ref: targetRef, isVisible };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPageCount = (doc?: DocumentLike | null) => {
|
|
||||||
|
|
||||||
|
const getPageCount = (doc?: Document | null) => {
|
||||||
const count = doc?.current_version?.metadata?.page_count;
|
const count = doc?.current_version?.metadata?.page_count;
|
||||||
return Number.isFinite(count) ? Number(count) : null;
|
return Number.isFinite(count) ? Number(count) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DocumentLike = AssetManagerDocumentLike;
|
|
||||||
type AssetLike = AssetManagerAssetLike;
|
type AssetLike = AssetManagerAssetLike;
|
||||||
type EnsureAssetUrl = AssetManagerEnsureAssetUrl;
|
type EnsureAssetUrl = AssetManagerEnsureAssetUrl;
|
||||||
type GetDocumentAsset = AssetManagerGetAsset;
|
type GetDocumentAsset = AssetManagerGetAsset;
|
||||||
|
|
||||||
interface DocumentThumbnailImageProps {
|
interface DocumentThumbnailImageProps {
|
||||||
document?: DocumentLike | null;
|
document?: Document | null;
|
||||||
ensureAssetUrl?: EnsureAssetUrl;
|
ensureAssetUrl?: EnsureAssetUrl;
|
||||||
getDocumentAsset?: GetDocumentAsset;
|
getDocumentAsset?: GetDocumentAsset;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import useInlineRename from './useInlineRename';
|
|||||||
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
FolderLike,
|
Folder as FolderLike,
|
||||||
DocumentLike,
|
Document,
|
||||||
} from './DocumentsList';
|
} from '../types/documents';
|
||||||
import type { DocumentsViewProps } from './panel/DocumentsPanel';
|
import type { DocumentsViewProps } from './panel/DocumentsPanel';
|
||||||
|
|
||||||
interface DocumentsGridProps extends DocumentsViewProps {
|
interface DocumentsGridProps extends DocumentsViewProps {
|
||||||
@@ -63,9 +63,9 @@ const DocumentsGrid: React.FC<DocumentsGridProps> = ({
|
|||||||
submitEditing: submitDocumentEditing,
|
submitEditing: submitDocumentEditing,
|
||||||
savingId: savingDocumentId,
|
savingId: savingDocumentId,
|
||||||
attachInputRef: attachDocumentInputRef,
|
attachInputRef: attachDocumentInputRef,
|
||||||
} = useInlineRename<DocumentLike>(onDocumentRename, {
|
} = useInlineRename<Document>(onDocumentRename, {
|
||||||
getCurrentValue: (doc: DocumentLike) => doc?.title ?? '',
|
getCurrentValue: (doc: Document) => doc?.title ?? '',
|
||||||
getEntityId: (doc: DocumentLike) => doc?.id ?? null,
|
getEntityId: (doc: Document) => doc?.id ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import type { MouseEvent } from 'react';
|
|
||||||
import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons';
|
import { FolderIcon, CheckIcon, CloseIcon } from '../ui/icons';
|
||||||
import { getTagColorStyle } from '../utils/colors';
|
import { getTagColorStyle } from '../utils/colors';
|
||||||
import { formatDate } from '../utils/date';
|
import { formatDate } from '../utils/date';
|
||||||
@@ -9,54 +8,12 @@ import { resolveCorrespondents } from './correspondents';
|
|||||||
import { writeTagTransferData, parseTagTransferPayload } from './tagTransfer';
|
import { writeTagTransferData, parseTagTransferPayload } 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';
|
|
||||||
import type { DocumentsViewProps } from './panel/DocumentsPanel';
|
import type { DocumentsViewProps } from './panel/DocumentsPanel';
|
||||||
|
import type {
|
||||||
export interface FolderLike {
|
Document,
|
||||||
id?: Identifier | 'root';
|
Folder,
|
||||||
name?: string;
|
} from '../types/documents';
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocumentTag {
|
|
||||||
id?: Identifier;
|
|
||||||
label?: string;
|
|
||||||
color?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocumentCorrespondent {
|
|
||||||
id?: Identifier;
|
|
||||||
name?: string;
|
|
||||||
count?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocumentLike {
|
|
||||||
id?: Identifier;
|
|
||||||
title?: string;
|
|
||||||
issued_at?: string | null;
|
|
||||||
created_at?: string | null;
|
|
||||||
uploaded_at?: string | null;
|
|
||||||
tags?: DocumentTag[] | null;
|
|
||||||
correspondents?: DocumentCorrespondent[] | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FolderEntry = {
|
|
||||||
type: 'folder';
|
|
||||||
id: Identifier | 'root';
|
|
||||||
key: string;
|
|
||||||
folder: FolderLike;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DocumentEntry = {
|
|
||||||
type: 'document';
|
|
||||||
id: Identifier;
|
|
||||||
key: string;
|
|
||||||
document: DocumentLike;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DocumentsListEntry = FolderEntry | DocumentEntry;
|
|
||||||
|
|
||||||
export type FolderEventHandler = (folder: FolderLike, event: MouseEvent<HTMLTableRowElement>) => void;
|
|
||||||
export type DocumentEventHandler = (document: DocumentLike, event: MouseEvent<HTMLTableRowElement>) => void;
|
|
||||||
|
|
||||||
const DocumentsList: React.FC<DocumentsViewProps> = ({
|
const DocumentsList: React.FC<DocumentsViewProps> = ({
|
||||||
entries,
|
entries,
|
||||||
@@ -105,9 +62,9 @@ const DocumentsList: React.FC<DocumentsViewProps> = ({
|
|||||||
submitEditing: submitDocumentEditing,
|
submitEditing: submitDocumentEditing,
|
||||||
savingId: savingDocumentId,
|
savingId: savingDocumentId,
|
||||||
attachInputRef: attachDocumentInputRef,
|
attachInputRef: attachDocumentInputRef,
|
||||||
} = useInlineRename<DocumentLike>(onDocumentRename, {
|
} = useInlineRename<Document>(onDocumentRename, {
|
||||||
getCurrentValue: (doc: DocumentLike) => doc?.title ?? '',
|
getCurrentValue: (doc: Document) => doc?.title ?? '',
|
||||||
getEntityId: (doc: DocumentLike) => doc?.id ?? null,
|
getEntityId: (doc: Document) => doc?.id ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -119,9 +76,9 @@ const DocumentsList: React.FC<DocumentsViewProps> = ({
|
|||||||
submitEditing: submitFolderEditing,
|
submitEditing: submitFolderEditing,
|
||||||
savingId: savingFolderId,
|
savingId: savingFolderId,
|
||||||
attachInputRef: attachFolderInputRef,
|
attachInputRef: attachFolderInputRef,
|
||||||
} = useInlineRename<FolderLike>(onFolderRename, {
|
} = useInlineRename<Folder>(onFolderRename, {
|
||||||
getCurrentValue: (folder: FolderLike) => folder?.name ?? '',
|
getCurrentValue: (folder: Folder) => folder?.name ?? '',
|
||||||
getEntityId: (folder: FolderLike) => folder?.id ?? null,
|
getEntityId: (folder: Folder) => folder?.id ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -310,7 +267,6 @@ const DocumentsList: React.FC<DocumentsViewProps> = ({
|
|||||||
data-doc-id={doc.id}
|
data-doc-id={doc.id}
|
||||||
onClick={(event) => onDocumentClick?.(doc, event)}
|
onClick={(event) => onDocumentClick?.(doc, event)}
|
||||||
onDoubleClick={(event) => onDocumentActivate?.(doc, event)}
|
onDoubleClick={(event) => onDocumentActivate?.(doc, event)}
|
||||||
draggable
|
|
||||||
onDragStart={(event) => onDocumentDragStart?.(event, doc)}
|
onDragStart={(event) => onDocumentDragStart?.(event, doc)}
|
||||||
onDragEnd={(event) => onDocumentDragEnd?.(event)}
|
onDragEnd={(event) => onDocumentDragEnd?.(event)}
|
||||||
onDragOver={onDocumentTagDragOver}
|
onDragOver={onDocumentTagDragOver}
|
||||||
|
|||||||
@@ -40,12 +40,7 @@ interface CorrespondentOption {
|
|||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DocumentLike {
|
import type { Document } from '../types/documents';
|
||||||
id?: DocumentId;
|
|
||||||
tags?: TagOption[];
|
|
||||||
correspondents?: CorrespondentOption[];
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BulkTagMutationArgs {
|
interface BulkTagMutationArgs {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -68,7 +63,7 @@ export interface SelectionFloatingActionsProps {
|
|||||||
selectionCount?: number;
|
selectionCount?: number;
|
||||||
selectedDocumentIds?: SelectedIdList;
|
selectedDocumentIds?: SelectedIdList;
|
||||||
selectedFolderIds?: SelectedIdList;
|
selectedFolderIds?: SelectedIdList;
|
||||||
documentLookup?: Map<DocumentId, DocumentLike> | null;
|
documentLookup?: Map<DocumentId, Document> | null;
|
||||||
tags?: TagOption[] | null;
|
tags?: TagOption[] | null;
|
||||||
tagLookupById?: Map<DocumentId, TagOption> | null;
|
tagLookupById?: Map<DocumentId, TagOption> | null;
|
||||||
correspondents?: CorrespondentOption[] | null;
|
correspondents?: CorrespondentOption[] | null;
|
||||||
@@ -135,7 +130,7 @@ const buildFolderTreeOptions = (tree?: FolderTreeNode[] | null): SelectionAssign
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildTagAssignments = (
|
const buildTagAssignments = (
|
||||||
selectedDocuments: DocumentLike[],
|
selectedDocuments: Document[],
|
||||||
tagLookupById: Map<DocumentId, TagOption> | null,
|
tagLookupById: Map<DocumentId, TagOption> | null,
|
||||||
tags: TagOption[] | null,
|
tags: TagOption[] | null,
|
||||||
total: number,
|
total: number,
|
||||||
@@ -200,7 +195,7 @@ const buildTagAssignments = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildCorrespondentAssignments = (
|
const buildCorrespondentAssignments = (
|
||||||
selectedDocuments: DocumentLike[],
|
selectedDocuments: Document[],
|
||||||
correspondents: CorrespondentOption[] | null,
|
correspondents: CorrespondentOption[] | null,
|
||||||
total: number,
|
total: number,
|
||||||
): SelectionAssignmentMenuItem[] => {
|
): SelectionAssignmentMenuItem[] => {
|
||||||
@@ -280,7 +275,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
|
|||||||
const tenantId = tenant?.id ?? null;
|
const tenantId = tenant?.id ?? null;
|
||||||
|
|
||||||
const documentLookupMap = useMemo(() => (
|
const documentLookupMap = useMemo(() => (
|
||||||
documentLookup instanceof Map ? documentLookup : new Map<DocumentId, DocumentLike>()
|
documentLookup instanceof Map ? documentLookup : new Map<DocumentId, Document>()
|
||||||
), [documentLookup]);
|
), [documentLookup]);
|
||||||
const tagLookupMap = tagLookupById instanceof Map ? tagLookupById : null;
|
const tagLookupMap = tagLookupById instanceof Map ? tagLookupById : null;
|
||||||
|
|
||||||
@@ -350,13 +345,13 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
|
|||||||
const folderCount = folderIdList.length;
|
const folderCount = folderIdList.length;
|
||||||
const totalCount = selectionCount ?? documentCount + folderCount;
|
const totalCount = selectionCount ?? documentCount + folderCount;
|
||||||
|
|
||||||
const selectedDocuments = useMemo<DocumentLike[]>(() => {
|
const selectedDocuments = useMemo<Document[]>(() => {
|
||||||
if (!documentIdList.length || !(documentLookupMap instanceof Map)) {
|
if (!documentIdList.length || !(documentLookupMap instanceof Map)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return documentIdList
|
return documentIdList
|
||||||
.map((id) => documentLookupMap.get(id))
|
.map((id) => documentLookupMap.get(id))
|
||||||
.filter((doc): doc is DocumentLike => Boolean(doc));
|
.filter((doc): doc is Document => Boolean(doc));
|
||||||
}, [documentIdList, documentLookupMap]);
|
}, [documentIdList, documentLookupMap]);
|
||||||
|
|
||||||
const selectedDocCount = selectedDocuments.length;
|
const selectedDocCount = selectedDocuments.length;
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ export interface CorrespondentReference {
|
|||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentLike {
|
import type { Document } from '../types/documents';
|
||||||
correspondents?: CorrespondentReference[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResolvedCorrespondent {
|
export interface ResolvedCorrespondent {
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
@@ -14,7 +12,7 @@ export interface ResolvedCorrespondent {
|
|||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resolveCorrespondents = (doc?: DocumentLike | null): ResolvedCorrespondent[] => {
|
export const resolveCorrespondents = (doc?: Document | null): ResolvedCorrespondent[] => {
|
||||||
if (!doc || !Array.isArray(doc.correspondents)) {
|
if (!doc || !Array.isArray(doc.correspondents)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import type {
|
|||||||
EnsureAssetUrl,
|
EnsureAssetUrl,
|
||||||
EnsurePreviewData,
|
EnsurePreviewData,
|
||||||
GetDocumentAsset,
|
GetDocumentAsset,
|
||||||
DocumentLike as OcrDocumentLike,
|
|
||||||
} from '../utils/ocr';
|
} from '../utils/ocr';
|
||||||
|
import type { Document } from '../types/documents';
|
||||||
|
|
||||||
export type DocumentLike = OcrDocumentLike;
|
export type { Document };
|
||||||
|
|
||||||
const asyncFalse = async () => false;
|
const asyncFalse = async () => false;
|
||||||
|
|
||||||
const resolveDocumentDownloadHref = (document?: DocumentLike | null): string | null => {
|
const resolveDocumentDownloadHref = (document?: Document | null): string | null => {
|
||||||
if (!document) {
|
if (!document) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ const resolveDocumentDownloadHref = (document?: DocumentLike | null): string | n
|
|||||||
return downloadUrl;
|
return downloadUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasDocumentOcrAsset = (document?: DocumentLike | null, getDocumentAsset?: GetDocumentAsset | null): boolean => {
|
const hasDocumentOcrAsset = (document?: Document | null, getDocumentAsset?: GetDocumentAsset | null): boolean => {
|
||||||
if (!document || !getDocumentAsset) {
|
if (!document || !getDocumentAsset) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ const hasDocumentOcrAsset = (document?: DocumentLike | null, getDocumentAsset?:
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface CreateDocumentActionStateArgs {
|
interface CreateDocumentActionStateArgs {
|
||||||
document: DocumentLike | null;
|
document: Document | null;
|
||||||
ensurePreviewData: EnsurePreviewData;
|
ensurePreviewData: EnsurePreviewData;
|
||||||
ensureAssetUrl: EnsureAssetUrl;
|
ensureAssetUrl: EnsureAssetUrl;
|
||||||
getDocumentAsset?: GetDocumentAsset | null;
|
getDocumentAsset?: GetDocumentAsset | null;
|
||||||
|
|||||||
@@ -2,39 +2,7 @@ import { formatFileSize } from '../utils/format';
|
|||||||
import { formatDateTime as defaultFormatDateTime } from '../utils/date';
|
import { formatDateTime as defaultFormatDateTime } from '../utils/date';
|
||||||
import { DEFAULT_FOLDER_NAME } from '../app/workspaceUtils';
|
import { DEFAULT_FOLDER_NAME } from '../app/workspaceUtils';
|
||||||
|
|
||||||
interface DocumentPageMetadata {
|
import type { DocumentTag, DocumentCorrespondent, Document } from '../types/documents';
|
||||||
page_count?: number | string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DocumentVersion {
|
|
||||||
size_bytes?: number | string | null;
|
|
||||||
metadata?: DocumentPageMetadata | null;
|
|
||||||
checksum?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TagEntry {
|
|
||||||
label?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CorrespondentEntry {
|
|
||||||
name?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SummaryDocument {
|
|
||||||
title?: string | null;
|
|
||||||
original_name?: string | null;
|
|
||||||
filename?: string | null;
|
|
||||||
mime_type?: string | null;
|
|
||||||
folder_id?: string | null;
|
|
||||||
folder_name?: string;
|
|
||||||
current_version?: DocumentVersion | null;
|
|
||||||
created_at?: string | null;
|
|
||||||
updated_at?: string | null;
|
|
||||||
issued_at?: string | null;
|
|
||||||
folder_path?: string;
|
|
||||||
tags?: TagEntry[] | null;
|
|
||||||
correspondents?: CorrespondentEntry[] | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DescribeSummaryOptions {
|
interface DescribeSummaryOptions {
|
||||||
formatDateTime?: typeof defaultFormatDateTime;
|
formatDateTime?: typeof defaultFormatDateTime;
|
||||||
@@ -51,7 +19,7 @@ export interface DocumentSummaryRow {
|
|||||||
|
|
||||||
export type DocumentSummary = DocumentSummaryRow[];
|
export type DocumentSummary = DocumentSummaryRow[];
|
||||||
|
|
||||||
const coercePageCount = (metadata?: DocumentPageMetadata | null): number | null => {
|
const coercePageCount = (metadata?: { page_count?: number | string | null } | null): number | null => {
|
||||||
const raw = metadata?.page_count;
|
const raw = metadata?.page_count;
|
||||||
if (raw == null || raw === '') {
|
if (raw == null || raw === '') {
|
||||||
return null;
|
return null;
|
||||||
@@ -67,31 +35,26 @@ interface DocumentMetadataPayload {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetadataDocumentLike {
|
export const describeDocumentSummary = (document?: Document | null, options: DescribeSummaryOptions = {}): DocumentSummary => {
|
||||||
created_at?: string | null;
|
|
||||||
updated_at?: string | null;
|
|
||||||
filename?: string | null;
|
|
||||||
original_name?: string | null;
|
|
||||||
mime_type?: string | null;
|
|
||||||
metadata?: DocumentMetadataPayload | null;
|
|
||||||
current_version?: { checksum?: string | null } | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const describeDocumentSummary = (document?: SummaryDocument | null, options: DescribeSummaryOptions = {}): DocumentSummary => {
|
|
||||||
const {
|
const {
|
||||||
formatDateTime = defaultFormatDateTime,
|
formatDateTime = defaultFormatDateTime,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const formatDateLabel = (value?: string | null) => formatDateTime(value) || '—';
|
const formatDateLabel = (value?: string | number | null) => {
|
||||||
const doc = document ?? {};
|
if (typeof value === 'number') {
|
||||||
|
return formatDateTime(new Date(value)) || '—';
|
||||||
|
}
|
||||||
|
return formatDateTime(value) || '—';
|
||||||
|
};
|
||||||
|
const doc = document ?? ({} as Document);
|
||||||
const sizeBytes = Number(doc.current_version?.size_bytes);
|
const sizeBytes = Number(doc.current_version?.size_bytes);
|
||||||
const sizeLabel = Number.isFinite(sizeBytes) && sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
|
const sizeLabel = Number.isFinite(sizeBytes) && sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
|
||||||
const metadata = doc.current_version?.metadata || null;
|
const metadata = doc.current_version?.metadata || null;
|
||||||
const pageCount = coercePageCount(metadata);
|
const pageCount = coercePageCount(metadata);
|
||||||
const pageCountLabel = Number.isFinite(pageCount) ? String(pageCount) : '—';
|
const pageCountLabel = Number.isFinite(pageCount) ? String(pageCount) : '—';
|
||||||
const folderLabel = doc.folder_id == null ? DEFAULT_FOLDER_NAME : `Folder ${doc.folder_id}`;
|
const folderLabel = doc.folder_id == null ? DEFAULT_FOLDER_NAME : `Folder ${doc.folder_id}`;
|
||||||
const tags = sanitizeArray<TagEntry>(doc.tags);
|
const tags = sanitizeArray<DocumentTag>(doc.tags);
|
||||||
const correspondents = sanitizeArray<CorrespondentEntry>(doc.correspondents);
|
const correspondents = sanitizeArray<DocumentCorrespondent>(doc.correspondents);
|
||||||
const tagLabels = tags.map((tag) => tag.label).filter(Boolean) as string[];
|
const tagLabels = tags.map((tag) => tag.label).filter(Boolean) as string[];
|
||||||
const correspondentLabels = correspondents.map((entry) => entry.name).filter(Boolean) as string[];
|
const correspondentLabels = correspondents.map((entry) => entry.name).filter(Boolean) as string[];
|
||||||
const tagsSummary = tagLabels.length ? tagLabels.join(', ') : '—';
|
const tagsSummary = tagLabels.length ? tagLabels.join(', ') : '—';
|
||||||
@@ -113,13 +76,14 @@ export const describeDocumentSummary = (document?: SummaryDocument | null, optio
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extractDocumentMetadataPayload = (document?: MetadataDocumentLike | null): DocumentMetadataPayload | null => {
|
export const extractDocumentMetadataPayload = (document?: Document | null): DocumentMetadataPayload | null => {
|
||||||
if (!document?.metadata) {
|
const metadata = document?.['metadata'] as DocumentMetadataPayload | undefined;
|
||||||
|
if (!metadata) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const keys = Object.keys(document.metadata);
|
const keys = Object.keys(metadata);
|
||||||
if (!keys.length) {
|
if (!keys.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return document.metadata;
|
return metadata;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import type {
|
|||||||
DocumentsListEntry,
|
DocumentsListEntry,
|
||||||
FolderEventHandler,
|
FolderEventHandler,
|
||||||
DocumentEventHandler,
|
DocumentEventHandler,
|
||||||
DocumentLike,
|
Document,
|
||||||
DocumentTag,
|
DocumentTag,
|
||||||
} from '../DocumentsList';
|
} from '../../types/documents';
|
||||||
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
|
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
|
||||||
import { isTagTransferEvent } from '../tagTransfer';
|
import { isTagTransferEvent } from '../tagTransfer';
|
||||||
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
|
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
|
||||||
@@ -64,7 +64,7 @@ export interface DocumentsViewProps {
|
|||||||
onFolderRename?: (folderId: Identifier | 'root', nextName: string) => Promise<boolean> | boolean;
|
onFolderRename?: (folderId: Identifier | 'root', nextName: string) => Promise<boolean> | boolean;
|
||||||
onDocumentClick?: DocumentEventHandler;
|
onDocumentClick?: DocumentEventHandler;
|
||||||
onDocumentActivate?: DocumentEventHandler;
|
onDocumentActivate?: DocumentEventHandler;
|
||||||
onDocumentDragStart?: (event: DragEvent<HTMLElement>, document: DocumentLike) => void;
|
onDocumentDragStart?: (event: DragEvent<HTMLElement>, document: Document) => void;
|
||||||
onDocumentDragEnd?: (event: DragEvent<HTMLElement>) => void;
|
onDocumentDragEnd?: (event: DragEvent<HTMLElement>) => void;
|
||||||
onDocumentTagDragOver?: (event: DragEvent<HTMLElement>) => void;
|
onDocumentTagDragOver?: (event: DragEvent<HTMLElement>) => void;
|
||||||
onDocumentTagDragLeave?: (event: DragEvent<HTMLElement>) => void;
|
onDocumentTagDragLeave?: (event: DragEvent<HTMLElement>) => void;
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import type { FolderId, Identifier } from '../../types/identifiers';
|
|||||||
type FolderIdentifier = FolderId | 'root';
|
type FolderIdentifier = FolderId | 'root';
|
||||||
type FolderInput = FolderIdentifier | number;
|
type FolderInput = FolderIdentifier | number;
|
||||||
|
|
||||||
interface DocumentLike {
|
import type { Document } from '../../types/documents';
|
||||||
id?: Identifier | null;
|
|
||||||
title?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApplySelectionFn = (
|
type ApplySelectionFn = (
|
||||||
keys: string[],
|
keys: string[],
|
||||||
@@ -28,7 +24,7 @@ interface UseDocumentDragHandlersOptions {
|
|||||||
selectedFolderIds: FolderInput[];
|
selectedFolderIds: FolderInput[];
|
||||||
applySelection: ApplySelectionFn;
|
applySelection: ApplySelectionFn;
|
||||||
handleEntrySelection: HandleEntrySelectionFn;
|
handleEntrySelection: HandleEntrySelectionFn;
|
||||||
documentLookup: Map<Identifier, DocumentLike>;
|
documentLookup: Map<Identifier, Document>;
|
||||||
setDraggedDocumentIds: (ids: Identifier[] | []) => void;
|
setDraggedDocumentIds: (ids: Identifier[] | []) => void;
|
||||||
setDraggedFolderId: (id: FolderIdentifier | null) => void;
|
setDraggedFolderId: (id: FolderIdentifier | null) => void;
|
||||||
documentsViewMode: string;
|
documentsViewMode: string;
|
||||||
@@ -62,7 +58,7 @@ const useDocumentDragHandlers = ({
|
|||||||
useEffect(() => destroyDragPreview, [destroyDragPreview]);
|
useEffect(() => destroyDragPreview, [destroyDragPreview]);
|
||||||
|
|
||||||
const createDragPreview = useCallback(
|
const createDragPreview = useCallback(
|
||||||
({ documents = [], folders = [] }: { documents?: DocumentLike[]; folders?: FolderIdentifier[] } = {}) => {
|
({ documents = [], folders = [] }: { documents?: Document[]; folders?: FolderIdentifier[] } = {}) => {
|
||||||
destroyDragPreview();
|
destroyDragPreview();
|
||||||
|
|
||||||
const docEntries = (documents || []).filter(Boolean);
|
const docEntries = (documents || []).filter(Boolean);
|
||||||
@@ -202,9 +198,9 @@ const useDocumentDragHandlers = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleDocumentDragStart = useCallback(
|
const handleDocumentDragStart = useCallback(
|
||||||
(event: DragEvent<HTMLElement>, documentOrId: DocumentLike | Identifier | null) => {
|
(event: DragEvent<HTMLElement>, documentOrId: Document | Identifier | null) => {
|
||||||
const documentId: Identifier | null = Object(documentOrId) === documentOrId
|
const documentId: Identifier | null = Object(documentOrId) === documentOrId
|
||||||
? (documentOrId as DocumentLike)?.id ?? null
|
? (documentOrId as Document)?.id ?? null
|
||||||
: (documentOrId as Identifier | null);
|
: (documentOrId as Identifier | null);
|
||||||
if (!documentId) {
|
if (!documentId) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
updateDocument,
|
updateDocument,
|
||||||
} from '../../lib/apiClient';
|
} from '../../lib/apiClient';
|
||||||
import type { DocumentId, FolderId as FolderIdentifier } from '../../types/identifiers';
|
import type { DocumentId, FolderId as FolderIdentifier } from '../../types/identifiers';
|
||||||
|
import type { Document } from '../../types/documents';
|
||||||
|
|
||||||
type FolderId = FolderIdentifier | 'root';
|
type FolderId = FolderIdentifier | 'root';
|
||||||
type NullableFolderId = FolderId | null;
|
type NullableFolderId = FolderId | null;
|
||||||
@@ -21,8 +22,8 @@ type NullableFolderId = FolderId | null;
|
|||||||
type StatusLevel = 'success' | 'error' | 'info' | string;
|
type StatusLevel = 'success' | 'error' | 'info' | string;
|
||||||
|
|
||||||
type DocumentCacheMapper = (
|
type DocumentCacheMapper = (
|
||||||
doc: DocumentLike | null,
|
doc: Document | null,
|
||||||
) => DocumentLike | null;
|
) => Document | null;
|
||||||
|
|
||||||
type MapDocumentCaches = (mapper: DocumentCacheMapper) => void;
|
type MapDocumentCaches = (mapper: DocumentCacheMapper) => void;
|
||||||
|
|
||||||
@@ -53,19 +54,8 @@ interface Tag {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DocumentLike {
|
|
||||||
id?: DocumentId;
|
|
||||||
folder_id?: NullableFolderId;
|
|
||||||
folder_path?: string | null;
|
|
||||||
folder_name?: string | null;
|
|
||||||
issued_at?: number | null;
|
|
||||||
title?: string;
|
|
||||||
tags?: Tag[];
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FolderContents {
|
interface FolderContents {
|
||||||
documents?: DocumentLike[];
|
documents?: Document[];
|
||||||
subfolders?: Array<{ id?: FolderId;[key: string]: unknown }>;
|
subfolders?: Array<{ id?: FolderId;[key: string]: unknown }>;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
@@ -109,12 +99,12 @@ interface FolderDeleteOptions {
|
|||||||
|
|
||||||
interface UseDocumentMutationsArgs {
|
interface UseDocumentMutationsArgs {
|
||||||
token?: string | null;
|
token?: string | null;
|
||||||
documentLookup: Map<DocumentId, DocumentLike>;
|
documentLookup: Map<DocumentId, Document>;
|
||||||
folderLabelMap: Map<FolderId, string>;
|
folderLabelMap: Map<FolderId, string>;
|
||||||
ensureFolderData: EnsureFolderData;
|
ensureFolderData: EnsureFolderData;
|
||||||
selectedFolder: FolderId;
|
selectedFolder: FolderId;
|
||||||
setSelectedFolder: Dispatch<SetStateAction<FolderId>>;
|
setSelectedFolder: Dispatch<SetStateAction<FolderId>>;
|
||||||
setDocuments: Dispatch<SetStateAction<DocumentLike[]>>;
|
setDocuments: Dispatch<SetStateAction<Document[]>>;
|
||||||
setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContents>>>;
|
setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContents>>>;
|
||||||
setSearchResultIds: Dispatch<SetStateAction<DocumentId[] | null>>;
|
setSearchResultIds: Dispatch<SetStateAction<DocumentId[] | null>>;
|
||||||
setSelectedEntries: Dispatch<SetStateAction<string[]>>;
|
setSelectedEntries: Dispatch<SetStateAction<string[]>>;
|
||||||
@@ -140,13 +130,13 @@ interface UseDocumentMutationsArgs {
|
|||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
refreshTags: () => Promise<void>;
|
refreshTags: () => Promise<void>;
|
||||||
tagManager: TagManager;
|
tagManager: TagManager;
|
||||||
extractDocumentFromResponse?: (payload: unknown) => DocumentLike | null;
|
extractDocumentFromResponse?: (payload: unknown) => Document | null;
|
||||||
ingestDocuments?: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean };
|
ingestDocuments?: (docs: unknown[]) => { canonical: Document[]; changed: boolean };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseDocumentMutationsResult {
|
interface UseDocumentMutationsResult {
|
||||||
moveDocumentsToFolder: (
|
moveDocumentsToFolder: (
|
||||||
documentIds: Array<DocumentId | DocumentLike>,
|
documentIds: Array<DocumentId | Document>,
|
||||||
targetFolderId?: NullableFolderId,
|
targetFolderId?: NullableFolderId,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
handleThumbnailRegeneration: (documentId: DocumentId) => Promise<void>;
|
handleThumbnailRegeneration: (documentId: DocumentId) => Promise<void>;
|
||||||
@@ -155,7 +145,7 @@ interface UseDocumentMutationsResult {
|
|||||||
options?: DeleteOptions,
|
options?: DeleteOptions,
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
handleDocumentTagAdd: (
|
handleDocumentTagAdd: (
|
||||||
document: DocumentLike,
|
document: Document,
|
||||||
label: string,
|
label: string,
|
||||||
extras?: DocumentTagExtras | null,
|
extras?: DocumentTagExtras | null,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
@@ -218,7 +208,7 @@ const useDocumentMutations = ({
|
|||||||
ingestDocuments,
|
ingestDocuments,
|
||||||
}: UseDocumentMutationsArgs): UseDocumentMutationsResult => {
|
}: UseDocumentMutationsArgs): UseDocumentMutationsResult => {
|
||||||
const moveDocumentsToFolder = useCallback(
|
const moveDocumentsToFolder = useCallback(
|
||||||
async (documentIds: Array<DocumentId | DocumentLike>, targetFolderId?: NullableFolderId) => {
|
async (documentIds: Array<DocumentId | Document>, targetFolderId?: NullableFolderId) => {
|
||||||
const uniqueIds = Array.from(
|
const uniqueIds = Array.from(
|
||||||
new Set((documentIds || []).map((value) => normalizeDocumentId(value)).filter(Boolean) as DocumentId[]),
|
new Set((documentIds || []).map((value) => normalizeDocumentId(value)).filter(Boolean) as DocumentId[]),
|
||||||
);
|
);
|
||||||
@@ -241,9 +231,9 @@ const useDocumentMutations = ({
|
|||||||
document: doc,
|
document: doc,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean) as Array<{ id: DocumentId; sourceFolderId: NullableFolderId; document: DocumentLike }>;
|
.filter(Boolean) as Array<{ id: DocumentId; sourceFolderId: NullableFolderId; document: Document }>;
|
||||||
|
|
||||||
const updatedDocsMap = new Map<DocumentId, DocumentLike>();
|
const updatedDocsMap = new Map<DocumentId, Document>();
|
||||||
const resolveTargetName = () => {
|
const resolveTargetName = () => {
|
||||||
if (!targetLabel) {
|
if (!targetLabel) {
|
||||||
return null;
|
return null;
|
||||||
@@ -257,7 +247,7 @@ const useDocumentMutations = ({
|
|||||||
if (!document) {
|
if (!document) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const updated: DocumentLike = {
|
const updated: Document = {
|
||||||
...document,
|
...document,
|
||||||
folder_id: target,
|
folder_id: target,
|
||||||
};
|
};
|
||||||
@@ -572,7 +562,7 @@ const useDocumentMutations = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleDocumentTagAdd = useCallback(
|
const handleDocumentTagAdd = useCallback(
|
||||||
async (document: DocumentLike, label: string, extras: DocumentTagExtras | null = null) => {
|
async (document: Document, label: string, extras: DocumentTagExtras | null = null) => {
|
||||||
const normalizedLabel = tagManager.normalizeLabel(label);
|
const normalizedLabel = tagManager.normalizeLabel(label);
|
||||||
const optionCandidate = extras?.option ?? null;
|
const optionCandidate = extras?.option ?? null;
|
||||||
const input = extras?.input ?? null;
|
const input = extras?.input ?? null;
|
||||||
|
|||||||
@@ -8,20 +8,16 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import DocumentsManager from '../../documents/DocumentsManager';
|
import DocumentsManager from '../../documents/DocumentsManager';
|
||||||
import type { DocumentId } from '../../types/identifiers';
|
import type { DocumentId } from '../../types/identifiers';
|
||||||
|
import type { Document } from '../../types/documents';
|
||||||
interface DocumentLike {
|
|
||||||
id?: DocumentId;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FolderContentsEntry {
|
interface FolderContentsEntry {
|
||||||
documents?: DocumentLike[];
|
documents?: Document[];
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseDocumentsOptions {
|
interface UseDocumentsOptions {
|
||||||
setFolderContents: Dispatch<SetStateAction<Map<string, FolderContentsEntry>>>;
|
setFolderContents: Dispatch<SetStateAction<Map<string, FolderContentsEntry>>>;
|
||||||
fetchDocumentById?: (id: DocumentId) => Promise<DocumentLike | null>;
|
fetchDocumentById?: (id: DocumentId) => Promise<Document | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useDocuments = ({
|
const useDocuments = ({
|
||||||
@@ -29,16 +25,16 @@ const useDocuments = ({
|
|||||||
fetchDocumentById,
|
fetchDocumentById,
|
||||||
}: UseDocumentsOptions) => {
|
}: UseDocumentsOptions) => {
|
||||||
const managerRef = useRef(
|
const managerRef = useRef(
|
||||||
new DocumentsManager<DocumentLike>(fetchDocumentById),
|
new DocumentsManager<Document>(fetchDocumentById),
|
||||||
);
|
);
|
||||||
const [documents, setDocumentsState] = useState<DocumentLike[]>([]);
|
const [documents, setDocumentsState] = useState<Document[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
managerRef.current.setFetcher(fetchDocumentById);
|
managerRef.current.setFetcher(fetchDocumentById);
|
||||||
}, [fetchDocumentById]);
|
}, [fetchDocumentById]);
|
||||||
|
|
||||||
const setDocuments = useCallback(
|
const setDocuments = useCallback(
|
||||||
(value: DocumentLike[] | ((prev: DocumentLike[]) => DocumentLike[])) => {
|
(value: Document[] | ((prev: Document[]) => Document[])) => {
|
||||||
setDocumentsState((prev) => {
|
setDocumentsState((prev) => {
|
||||||
const resolved = typeof value === 'function' ? value(prev) : value;
|
const resolved = typeof value === 'function' ? value(prev) : value;
|
||||||
if (!Array.isArray(resolved)) {
|
if (!Array.isArray(resolved)) {
|
||||||
@@ -52,7 +48,7 @@ const useDocuments = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mapDocumentCaches = useCallback(
|
const mapDocumentCaches = useCallback(
|
||||||
(mapper: (doc: DocumentLike) => DocumentLike | undefined) => {
|
(mapper: (doc: Document) => Document | undefined) => {
|
||||||
managerRef.current.map(mapper);
|
managerRef.current.map(mapper);
|
||||||
const lookupSnapshot = managerRef.current.getSnapshot();
|
const lookupSnapshot = managerRef.current.getSnapshot();
|
||||||
|
|
||||||
@@ -64,7 +60,7 @@ const useDocuments = ({
|
|||||||
const next = prev.map((doc) => {
|
const next = prev.map((doc) => {
|
||||||
const id = doc?.id;
|
const id = doc?.id;
|
||||||
if (id != null && lookupSnapshot.has(id as DocumentId)) {
|
if (id != null && lookupSnapshot.has(id as DocumentId)) {
|
||||||
const canonical = lookupSnapshot.get(id as DocumentId) as DocumentLike;
|
const canonical = lookupSnapshot.get(id as DocumentId) as Document;
|
||||||
if (canonical !== doc) {
|
if (canonical !== doc) {
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
@@ -95,7 +91,7 @@ const useDocuments = ({
|
|||||||
const updatedDocs = docs.map((doc) => {
|
const updatedDocs = docs.map((doc) => {
|
||||||
const id = doc?.id;
|
const id = doc?.id;
|
||||||
if (id != null && lookupSnapshot.has(id as DocumentId)) {
|
if (id != null && lookupSnapshot.has(id as DocumentId)) {
|
||||||
const canonical = lookupSnapshot.get(id as DocumentId) as DocumentLike;
|
const canonical = lookupSnapshot.get(id as DocumentId) as Document;
|
||||||
if (canonical !== doc) {
|
if (canonical !== doc) {
|
||||||
docsChanged = true;
|
docsChanged = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,15 +62,11 @@ const noop = () => { };
|
|||||||
|
|
||||||
type FolderId = FolderIdentifier | 'root';
|
type FolderId = FolderIdentifier | 'root';
|
||||||
|
|
||||||
interface DocumentLike {
|
import type { Document } from '../../types/documents';
|
||||||
id?: DocumentId | null;
|
|
||||||
title?: string | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FolderContentsEntry {
|
interface FolderContentsEntry {
|
||||||
folder?: { id?: FolderId; name?: string | null } | null;
|
folder?: { id?: FolderId; name?: string | null } | null;
|
||||||
documents?: DocumentLike[];
|
documents?: Document[];
|
||||||
subfolders?: Array<{ id?: FolderId; name?: string | null;[key: string]: unknown }>;
|
subfolders?: Array<{ id?: FolderId; name?: string | null;[key: string]: unknown }>;
|
||||||
__includesDocuments?: boolean;
|
__includesDocuments?: boolean;
|
||||||
__sortField?: string | null;
|
__sortField?: string | null;
|
||||||
@@ -397,7 +393,7 @@ const useDocumentsWorkspace = ({
|
|||||||
() =>
|
() =>
|
||||||
visibleDocumentIds
|
visibleDocumentIds
|
||||||
.map((id) => documentLookup.get(id) || null)
|
.map((id) => documentLookup.get(id) || null)
|
||||||
.filter((doc): doc is DocumentLike => Boolean(doc)),
|
.filter((doc): doc is Document => Boolean(doc)),
|
||||||
[visibleDocumentIds, documentLookup],
|
[visibleDocumentIds, documentLookup],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,10 @@ import {
|
|||||||
createFolderEntryKey,
|
createFolderEntryKey,
|
||||||
} from '../../app/entryKey';
|
} from '../../app/entryKey';
|
||||||
import type { FolderId as FolderIdentifier, Identifier } from '../../types/identifiers';
|
import type { FolderId as FolderIdentifier, Identifier } from '../../types/identifiers';
|
||||||
|
import type { Document } from '../../types/documents';
|
||||||
|
|
||||||
type FolderId = FolderIdentifier | 'root';
|
type FolderId = FolderIdentifier | 'root';
|
||||||
|
|
||||||
interface DocumentLike {
|
|
||||||
id?: Identifier | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FolderSummary {
|
interface FolderSummary {
|
||||||
id?: FolderId;
|
id?: FolderId;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -31,7 +27,7 @@ interface FolderSummary {
|
|||||||
|
|
||||||
interface FolderContentsEntry {
|
interface FolderContentsEntry {
|
||||||
folder?: FolderSummary | null;
|
folder?: FolderSummary | null;
|
||||||
documents?: DocumentLike[];
|
documents?: Document[];
|
||||||
subfolders?: FolderSummary[];
|
subfolders?: FolderSummary[];
|
||||||
__includesDocuments?: boolean;
|
__includesDocuments?: boolean;
|
||||||
__sortField?: string | null;
|
__sortField?: string | null;
|
||||||
@@ -67,7 +63,7 @@ interface UseFolderTreeOptions {
|
|||||||
documentsSortFieldRef: MutableRefObject<string>;
|
documentsSortFieldRef: MutableRefObject<string>;
|
||||||
documentsSortDirectionRef: MutableRefObject<string>;
|
documentsSortDirectionRef: MutableRefObject<string>;
|
||||||
selectionHelpers: SelectionHelpers;
|
selectionHelpers: SelectionHelpers;
|
||||||
setDocuments: Dispatch<SetStateAction<DocumentLike[]>>;
|
setDocuments: Dispatch<SetStateAction<Document[]>>;
|
||||||
setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContentsEntry>>>;
|
setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContentsEntry>>>;
|
||||||
folderContentsRef: MutableRefObject<Map<FolderId, FolderContentsEntry>>;
|
folderContentsRef: MutableRefObject<Map<FolderId, FolderContentsEntry>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
|
|
||||||
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';
|
import type { Identifier } from '../types/identifiers';
|
||||||
|
import type { Document } from '../types/documents';
|
||||||
type DocumentLike = {
|
|
||||||
id?: Identifier;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AssetObject = {
|
type AssetObject = {
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -29,7 +26,7 @@ type EnsureAssetUrl = (
|
|||||||
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: Document, assetType: string) => AssetLike | null;
|
||||||
|
|
||||||
type AssetViewLike = {
|
type AssetViewLike = {
|
||||||
url: string | null;
|
url: string | null;
|
||||||
@@ -37,14 +34,14 @@ type AssetViewLike = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface UseAssetNavigatorOptions {
|
interface UseAssetNavigatorOptions {
|
||||||
document?: DocumentLike | null;
|
document?: Document | null;
|
||||||
assetType: string;
|
assetType: string;
|
||||||
ensureAssetUrl?: EnsureAssetUrl | null;
|
ensureAssetUrl?: EnsureAssetUrl | null;
|
||||||
getAsset?: GetAsset;
|
getAsset?: GetAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssetNavigatorReturn {
|
interface AssetNavigatorReturn {
|
||||||
document: DocumentLike | null;
|
document: Document | null;
|
||||||
documentId: Identifier | null;
|
documentId: Identifier | null;
|
||||||
asset: AssetLike | null;
|
asset: AssetLike | null;
|
||||||
assetType: string;
|
assetType: string;
|
||||||
|
|||||||
@@ -5,14 +5,7 @@ import { DownloadIcon } from '../ui/icons';
|
|||||||
import PdfViewer from './PdfViewer';
|
import PdfViewer from './PdfViewer';
|
||||||
import { AUDIO_EXTENSIONS, VIDEO_EXTENSIONS } from '../constants/preview';
|
import { AUDIO_EXTENSIONS, VIDEO_EXTENSIONS } from '../constants/preview';
|
||||||
|
|
||||||
interface DocumentLike {
|
import type { Document } from '../types/documents';
|
||||||
id?: string;
|
|
||||||
title?: string;
|
|
||||||
mime_type?: string;
|
|
||||||
filename?: string;
|
|
||||||
original_name?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DocumentLink {
|
interface DocumentLink {
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -36,7 +29,7 @@ interface ContentTabConfig {
|
|||||||
type LayoutMode = 'split' | 'stacked' | (string & {});
|
type LayoutMode = 'split' | 'stacked' | (string & {});
|
||||||
|
|
||||||
interface DocumentViewerLayoutProps {
|
interface DocumentViewerLayoutProps {
|
||||||
document?: DocumentLike | null;
|
document?: Document | null;
|
||||||
documentLink?: DocumentLink | null;
|
documentLink?: DocumentLink | null;
|
||||||
summaryProps?: Record<string, unknown>;
|
summaryProps?: Record<string, unknown>;
|
||||||
metadataPayload?: unknown;
|
metadataPayload?: unknown;
|
||||||
|
|||||||
@@ -29,53 +29,26 @@ 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';
|
import type { DocumentId, FolderId } from '../types/identifiers';
|
||||||
|
import type { Document } from '../types/documents';
|
||||||
interface DocumentLike {
|
import type { AssetLike } from '../types/assets';
|
||||||
id?: DocumentId;
|
|
||||||
title?: string;
|
|
||||||
mime_type?: string | null;
|
|
||||||
issued_at?: string | null;
|
|
||||||
folder_id?: FolderId | null;
|
|
||||||
correspondents?: Array<{ id?: string; name?: string }>;
|
|
||||||
current_version?: {
|
|
||||||
version_number?: number;
|
|
||||||
download?: { url?: string | null; expires_at?: number } | null;
|
|
||||||
mime_type?: string | null;
|
|
||||||
filename?: string | null;
|
|
||||||
} | null;
|
|
||||||
documentLink?: {
|
|
||||||
url: string;
|
|
||||||
alt?: string;
|
|
||||||
mimeType?: string | null;
|
|
||||||
} | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AssetLike {
|
|
||||||
id?: string;
|
|
||||||
url?: string | null;
|
|
||||||
metadata?: Record<string, unknown> | null;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
type SidebarMode = 'overlay' | 'inline';
|
type SidebarMode = 'overlay' | 'inline';
|
||||||
|
|
||||||
interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
|
interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
|
||||||
document: DocumentLike | null;
|
document: Document | null;
|
||||||
ensureAssetUrl?: (docId: DocumentId, 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: Document | null, type: string) => AssetLike | null;
|
||||||
ensurePreviewData?: (docId: DocumentId, options?: { signal?: AbortSignal }) => Promise<DocumentLike | null>;
|
ensurePreviewData?: (docId: DocumentId, options?: { signal?: AbortSignal }) => Promise<Document | 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; name?: string }>;
|
resolveFolderPath?: (doc: Document | null) => Array<{ id?: string; name?: string }>;
|
||||||
variant?: 'viewer' | 'sidebar';
|
variant?: 'viewer' | 'sidebar';
|
||||||
onCollapsePanel?: () => void;
|
onCollapsePanel?: () => void;
|
||||||
onMaximizePanel?: (args: { documentIds: Array<string> }) => void;
|
onMaximizePanel?: (args: { documentIds: Array<string> }) => void;
|
||||||
sidebarMode?: SidebarMode;
|
sidebarMode?: SidebarMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const createDocumentViewerHeaderActions = ({
|
export const createDocumentViewerHeaderActions = ({
|
||||||
document,
|
document,
|
||||||
actionState,
|
actionState,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Identifier } from './identifiers';
|
||||||
|
|
||||||
|
export interface AssetObject {
|
||||||
|
ordinal?: number;
|
||||||
|
url?: string | null;
|
||||||
|
metadata?: Record<string, unknown> | null;
|
||||||
|
expires_at?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetLike {
|
||||||
|
id?: Identifier;
|
||||||
|
asset_type?: string;
|
||||||
|
cardinality?: number | null;
|
||||||
|
download?: { url: string; expires_at: number } | null;
|
||||||
|
metadata?: Record<string, unknown> | null;
|
||||||
|
assets?: Record<string, AssetLike> | AssetLike[] | null;
|
||||||
|
objects?: AssetObject[] | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import type { Identifier } from './identifiers';
|
||||||
|
import type { AssetLike } from './assets';
|
||||||
|
|
||||||
|
export interface DocumentTag {
|
||||||
|
id?: Identifier;
|
||||||
|
label?: string | null;
|
||||||
|
color?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentCorrespondent {
|
||||||
|
id?: Identifier;
|
||||||
|
name?: string | null;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentVersion {
|
||||||
|
assets?: Record<string, AssetLike> | AssetLike[] | null;
|
||||||
|
metadata?: Record<string, unknown> & { page_count?: number } | null;
|
||||||
|
size_bytes?: number | string | null;
|
||||||
|
checksum?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Document {
|
||||||
|
id?: Identifier;
|
||||||
|
title?: string | null;
|
||||||
|
original_name?: string | null;
|
||||||
|
filename?: string | null;
|
||||||
|
mime_type?: string | null;
|
||||||
|
|
||||||
|
issued_at?: string | number | null;
|
||||||
|
created_at?: string | null;
|
||||||
|
uploaded_at?: string | null;
|
||||||
|
updated_at?: string | null;
|
||||||
|
|
||||||
|
folder_id?: Identifier | null;
|
||||||
|
folder_name?: string;
|
||||||
|
folder_path?: string;
|
||||||
|
|
||||||
|
tags?: DocumentTag[] | null;
|
||||||
|
correspondents?: DocumentCorrespondent[] | null;
|
||||||
|
|
||||||
|
current_version?: DocumentVersion | null;
|
||||||
|
|
||||||
|
// Allow for other properties as we unify loosely typed interfaces
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Folder {
|
||||||
|
id?: Identifier | 'root';
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FolderEntry = {
|
||||||
|
type: 'folder';
|
||||||
|
id: Identifier | 'root';
|
||||||
|
key: string;
|
||||||
|
folder: Folder;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocumentEntry = {
|
||||||
|
type: 'document';
|
||||||
|
id: Identifier;
|
||||||
|
key: string;
|
||||||
|
document: Document;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocumentsListEntry = FolderEntry | DocumentEntry;
|
||||||
|
|
||||||
|
// Event Handlers
|
||||||
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
|
export type FolderEventHandler = (folder: Folder, event: MouseEvent<HTMLElement>) => void;
|
||||||
|
export type DocumentEventHandler = (document: Document, event: MouseEvent<HTMLElement>) => void;
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
import { resolveDocumentAssetUrl, resolveAssetUrl } from '../asset_manager';
|
import { resolveDocumentAssetUrl, resolveAssetUrl } from '../asset_manager';
|
||||||
import type {
|
import type {
|
||||||
DocumentLike as AssetManagerDocumentLike,
|
|
||||||
DocumentVersionLike,
|
|
||||||
AssetLike as AssetManagerAssetLike,
|
|
||||||
GetAsset as AssetManagerGetAsset,
|
GetAsset as AssetManagerGetAsset,
|
||||||
} from '../asset_manager';
|
} from '../asset_manager';
|
||||||
|
import type {
|
||||||
|
Document,
|
||||||
|
DocumentVersion,
|
||||||
|
} from '../types/documents';
|
||||||
|
import type { AssetLike } from '../types/assets';
|
||||||
|
|
||||||
export interface DocumentLike extends AssetManagerDocumentLike {
|
export type { Document, DocumentVersion, AssetLike };
|
||||||
current_version?: DocumentVersionLike | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AssetLike = AssetManagerAssetLike;
|
export type EnsurePreviewData = (id: string) => Promise<Document | null>;
|
||||||
|
|
||||||
export type EnsurePreviewData = (id: string) => Promise<DocumentLike | null>;
|
|
||||||
export type EnsureAssetUrl = (
|
export type EnsureAssetUrl = (
|
||||||
id: string,
|
id: string,
|
||||||
asset: AssetLike,
|
asset: AssetLike,
|
||||||
@@ -21,13 +19,13 @@ export type EnsureAssetUrl = (
|
|||||||
export type GetDocumentAsset = AssetManagerGetAsset;
|
export type GetDocumentAsset = AssetManagerGetAsset;
|
||||||
|
|
||||||
interface ResolveOcrTextUrlOptions {
|
interface ResolveOcrTextUrlOptions {
|
||||||
document: DocumentLike | null;
|
document: Document | null;
|
||||||
ensurePreviewData?: EnsurePreviewData;
|
ensurePreviewData?: EnsurePreviewData;
|
||||||
getDocumentAsset?: GetDocumentAsset;
|
getDocumentAsset?: GetDocumentAsset;
|
||||||
ensureAssetUrl?: EnsureAssetUrl;
|
ensureAssetUrl?: EnsureAssetUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pickAsset = (doc?: DocumentLike | null, getDocumentAsset?: GetDocumentAsset): AssetLike | null => {
|
const pickAsset = (doc?: Document | null, getDocumentAsset?: GetDocumentAsset): AssetLike | null => {
|
||||||
if (!doc || !getDocumentAsset) {
|
if (!doc || !getDocumentAsset) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user