typescript

This commit is contained in:
2025-11-13 01:07:55 +01:00
parent b812d748ea
commit ada089c05b
147 changed files with 7427 additions and 2534 deletions
@@ -1,27 +1,45 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { CSSProperties, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ArrowLeftIcon, ArrowRightIcon } from '../ui/icons';
import { clamp } from '../utils/math';
type PreviewNavigator = {
url: string;
alt?: string;
canGoPrev?: boolean;
canGoNext?: boolean;
goPrev?: () => void;
goNext?: () => void;
};
interface PreviewZoomOverlayProps {
open?: boolean;
display?: PreviewNavigator | null;
onClose?: () => void;
}
type NaturalSize = { width: number | null; height: number | null };
type FocusPoint = { xRatio: number; yRatio: number } | null;
const noop = () => {};
const PreviewZoomOverlay = ({
const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
open = false,
display = null,
onClose = noop,
}) => {
const portalTarget = document.body;
const portalTarget = typeof document !== 'undefined' ? document.body : null;
const [isNativeScale, setIsNativeScale] = useState(false);
const [naturalSize, setNaturalSize] = useState({ width: null, height: null });
const [naturalSize, setNaturalSize] = useState<NaturalSize>({ width: null, height: null });
const [renderBackdrop, setRenderBackdrop] = useState(false);
const [isBackdropVisible, setBackdropVisible] = useState(false);
const [displaySnapshot, setDisplaySnapshot] = useState(null);
const scrollRef = useRef(null);
const imageRef = useRef(null);
const focusRef = useRef(null);
const previouslyFocusedRef = useRef(null);
const visibilityTimerRef = useRef(null);
const displayTimerRef = useRef(null);
const [displaySnapshot, setDisplaySnapshot] = useState<PreviewNavigator | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
const imageRef = useRef<HTMLImageElement | null>(null);
const focusRef = useRef<FocusPoint>(null);
const previouslyFocusedRef = useRef<HTMLElement | null>(null);
const visibilityTimerRef = useRef<number | null>(null);
const displayTimerRef = useRef<number | null>(null);
useEffect(() => {
if (display?.url) {
@@ -55,7 +73,7 @@ const PreviewZoomOverlay = ({
}
setBackdropVisible(false);
visibilityTimerRef.current = setTimeout(() => {
visibilityTimerRef.current = window.setTimeout(() => {
setRenderBackdrop(false);
}, 260);
@@ -117,6 +135,9 @@ const PreviewZoomOverlay = ({
}, [open, isNativeScale, naturalSize.width, naturalSize.height]);
useEffect(() => {
if (typeof document === 'undefined') {
return;
}
if (!open) {
previouslyFocusedRef.current?.focus?.();
previouslyFocusedRef.current = null;
@@ -124,7 +145,7 @@ const PreviewZoomOverlay = ({
}
const active = document.activeElement;
previouslyFocusedRef.current = active?.focus ? active : null;
previouslyFocusedRef.current = active instanceof HTMLElement ? active : null;
}, [open]);
const activeDisplay = open && display?.url ? display : displaySnapshot;
@@ -154,7 +175,7 @@ const PreviewZoomOverlay = ({
return () => cancelAnimationFrame(frame);
}, [renderBackdrop, activeDisplay?.url]);
const handleKeyDown = (event) => {
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
event.stopPropagation();
if (!open) {
@@ -203,7 +224,7 @@ const PreviewZoomOverlay = ({
}
};
const toggleZoomAtPoint = (clientX, clientY) => {
const toggleZoomAtPoint = (clientX: number, clientY: number) => {
const img = imageRef.current;
setIsNativeScale((current) => {
if (!current && img) {
@@ -221,7 +242,7 @@ const PreviewZoomOverlay = ({
});
};
const handleImageClick = (event) => {
const handleImageClick = (event: React.MouseEvent<HTMLImageElement>) => {
event.stopPropagation();
toggleZoomAtPoint(event.clientX ?? 0, event.clientY ?? 0);
};
@@ -232,11 +253,7 @@ const PreviewZoomOverlay = ({
const effectiveDisplay = activeDisplay;
const navVisible = Boolean(effectiveDisplay?.canGoPrev || effectiveDisplay?.canGoNext);
const stageClassName = [
'preview-zoom__stage',
]
.filter(Boolean)
.join(' ');
const stageClassName = 'preview-zoom__stage';
const containerClassName = [
'preview-zoom__scroll',
@@ -252,7 +269,7 @@ const PreviewZoomOverlay = ({
.filter(Boolean)
.join(' ');
const imageStyle = isNativeScale
const imageStyle: CSSProperties = isNativeScale
? {
cursor: 'zoom-out',
width: naturalSize.width ? `${naturalSize.width}px` : 'auto',
@@ -277,37 +294,34 @@ const PreviewZoomOverlay = ({
aria-label="Enlarged document preview"
onClick={onClose}
>
<div
className={stageClassName}
onKeyDown={handleKeyDown}
>
<div
className={containerClassName}
ref={scrollRef}
tabIndex={-1}
onClick={(event) => event.stopPropagation()}
>
<img
src={effectiveDisplay.url}
alt={effectiveDisplay.alt || 'Document preview'}
className="preview-zoom__image"
ref={imageRef}
draggable={false}
onLoad={(event) => {
setNaturalSize({
width: event.currentTarget.naturalWidth || null,
height: event.currentTarget.naturalHeight || null,
});
}}
onClick={handleImageClick}
style={imageStyle}
/>
</div>
{navVisible ? (
<div className="preview-zoom__nav">
<button
type="button"
className="preview-zoom__nav-button"
<div className={stageClassName} onKeyDown={handleKeyDown}>
<div
className={containerClassName}
ref={scrollRef}
tabIndex={-1}
onClick={(event) => event.stopPropagation()}
>
<img
src={effectiveDisplay.url}
alt={effectiveDisplay.alt || 'Document preview'}
className="preview-zoom__image"
ref={imageRef}
draggable={false}
onLoad={(event) => {
setNaturalSize({
width: event.currentTarget.naturalWidth || null,
height: event.currentTarget.naturalHeight || null,
});
}}
onClick={handleImageClick}
style={imageStyle}
/>
</div>
{navVisible ? (
<div className="preview-zoom__nav">
<button
type="button"
className="preview-zoom__nav-button"
onClick={(event) => {
event.stopPropagation();
if (effectiveDisplay?.canGoPrev && effectiveDisplay?.goPrev) {
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo } from 'react';
import type { MutableRefObject } from 'react';
import { resolveDocumentAssetUrl } from '../asset_manager';
import { useDetailPanel } from '../app/useDetailPanel';
import {
@@ -6,6 +7,75 @@ import {
getRowId,
isDocumentRowKey,
} from '../app/appLayoutUtils';
import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel';
type Identifier = string | number;
interface DocumentLike {
id?: Identifier;
folder_id?: Identifier | 'root';
title?: string;
[key: string]: unknown;
}
interface FolderNode {
id: Identifier | 'root';
name?: string;
parentId?: Identifier | 'root';
}
type PreviewEntry = {
url?: string;
canGoPrev?: boolean;
canGoNext?: boolean;
goPrev?: () => void;
goNext?: () => void;
} | null;
interface UseDetailWorkspaceArgs {
documents: DocumentLike[];
searchResults?: DocumentLike[] | null;
previewDocuments?: Map<Identifier, DocumentLike> | null;
selectionOrder: string[];
selectedDocumentIds: Identifier[];
documentLookup: Map<Identifier, DocumentLike>;
folderNodes: Map<Identifier | 'root', FolderNode>;
ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>;
detailPanelControlRef: MutableRefObject<{ open?: (args?: { documentIds?: Identifier[] }) => void; close?: () => void } | null>;
detailFolderFetchRef: MutableRefObject<Set<Identifier | 'root'>>;
previewEntries: Map<Identifier, PreviewEntry>;
previewDocumentId?: Identifier | null;
activePreviewId?: Identifier | null;
openDocumentPreview?: (args: { documentIds: Identifier[] }) => void;
handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise<boolean> | boolean;
handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise<boolean> | boolean;
handleDocumentTagAdd?: (doc: DocumentLike, value: string, context?: { option?: unknown }) => void;
handleTagRemove?: (...args: unknown[]) => void;
ensureAssetUrl?: (...args: unknown[]) => unknown;
getDocumentAsset?: (...args: unknown[]) => unknown;
ensurePreviewData?: (docId: Identifier, options?: Record<string, unknown>) => Promise<DocumentLike | null | undefined>;
correspondents?: unknown[];
handleCorrespondentAdd?: (...args: unknown[]) => void;
handleCorrespondentRemove?: (...args: unknown[]) => void;
resolveApiPath?: (path: string) => string;
selectFolder?: (folderId?: Identifier | 'root') => void;
tags?: unknown[];
tagLookupById?: Map<Identifier, unknown> | null;
}
interface UseDetailWorkspaceResult {
detailPanelProps: DocumentInfoPanelProps;
detailPanelOpen: boolean;
openDetailPanel: ReturnType<typeof useDetailPanel>['openDetailPanel'];
closeDetailPanel: ReturnType<typeof useDetailPanel>['closeDetailPanel'];
handleDetailPanelClose: () => void;
inspectDocument: (docId: Identifier | null) => void;
previewActive: boolean;
previewWorkspaceDocument: DocumentLike | null;
previewWorkspaceEntry: PreviewEntry;
resolveThumbnailUrlForDoc: (doc: DocumentLike | null) => string | null;
resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>;
}
const useDetailWorkspace = ({
documents,
@@ -36,7 +106,7 @@ const useDetailWorkspace = ({
selectFolder,
tags,
tagLookupById,
}) => {
}: UseDetailWorkspaceArgs): UseDetailWorkspaceResult => {
const orderedSelectedDocuments = useMemo(() => {
const ordered = [];
const seen = new Set();