This commit is contained in:
2025-11-14 02:35:17 +01:00
parent 6f4ff68062
commit a3c5494bf7
53 changed files with 269 additions and 340 deletions
+8 -7
View File
@@ -88,13 +88,14 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
const contentConfig = contentConfigProp || null;
const contentEnabled = Boolean(contentConfig && (contentConfig.enabled ?? true));
const loadContent = contentConfig?.loadContent ?? null;
const showContentTab = Boolean(contentConfig && (contentConfig.forceDisplay ?? contentEnabled));
const [contentState, setContentState] = useState<ContentState | null>(() => {
if (!contentConfig) {
return null;
}
if (!contentEnabled || typeof contentConfig.loadContent !== 'function') {
if (!contentEnabled || !loadContent) {
return { status: contentEnabled ? 'idle' : 'unavailable', data: null, error: null };
}
return { status: 'idle', data: null, error: null };
@@ -106,7 +107,7 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
return undefined;
}
if (!contentEnabled || typeof contentConfig.loadContent !== 'function') {
if (!contentEnabled || !loadContent) {
setContentState({ status: contentEnabled ? 'idle' : 'unavailable', data: null, error: null });
return undefined;
}
@@ -116,7 +117,7 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
setContentState({ status: 'loading', data: null, error: null });
Promise.resolve(contentConfig.loadContent({ signal: controller.signal }))
Promise.resolve(loadContent({ signal: controller.signal }))
.then((result) => {
if (cancelled) {
return;
@@ -143,7 +144,7 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
controller.abort();
contentConfig.onCancel?.();
};
}, [contentConfig, contentEnabled, showContentTab, document?.id, resetKey]);
}, [contentConfig, contentEnabled, showContentTab, document?.id, resetKey, loadContent]);
const renderSummarySection = useCallback(() => (
<DocumentSummarySection
@@ -345,10 +346,10 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
if (!tab) {
return null;
}
if (typeof tab.render === 'function') {
return tab.render(context);
if (!tab.render) {
return null;
}
return null;
return tab.render(context);
};
const isControlled = controlledActiveTab !== undefined && controlledActiveTab !== null;
@@ -9,6 +9,7 @@ import {
toIssuedTimestamp,
} from '../utils/date';
import { describeDocumentSummary } from './documentSummary';
import { isPlainObject } from '../utils/typeGuards';
type Identifier = string | number;
@@ -110,43 +111,33 @@ interface QuickAddEntry {
original: QuickAddOption | string;
}
const resolveOptionName = (source: unknown): string => {
const resolveOptionName = (source?: QuickAddOption | string | null): string => {
if (!source) {
return '';
}
if (typeof source === 'string') {
return source.trim();
if (isPlainObject(source)) {
const raw = source.name ?? source.label ?? '';
return `${raw}`.trim();
}
if (typeof source === 'object') {
const candidate = source as { name?: string; label?: string; trim?: () => string };
if (typeof candidate.name === 'string' && candidate.name.trim()) {
return candidate.name.trim();
}
if (typeof candidate.label === 'string' && candidate.label.trim()) {
return candidate.label.trim();
}
if (typeof candidate.trim === 'function') {
const viaTrim = candidate.trim();
if (typeof viaTrim === 'string' && viaTrim.trim()) {
return viaTrim.trim();
}
}
}
return '';
return `${source}`.trim();
};
const normalizeQuickAddOption = (option: QuickAddOption | string | null | undefined): QuickAddEntry | null => {
if (option == null) {
return null;
}
const isObject = typeof option === 'object';
const labelSource = isObject ? option.label ?? option.name ?? '' : option;
const label = typeof labelSource === 'string' ? labelSource.trim() : '';
const label = (() => {
if (isPlainObject(option)) {
const sourceLabel = option.label ?? option.name ?? '';
return `${sourceLabel}`.trim();
}
return `${option}`.trim();
})();
if (!label) {
return null;
}
return {
id: isObject && option.id ? option.id : label,
id: isPlainObject(option) && option.id ? option.id : label,
label,
original: option,
};
@@ -170,10 +161,7 @@ export const TagSection: React.FC<TagSectionProps> = ({
const handleSelect = useCallback(
(option: { label?: string; name?: string } | string | null) => {
if (!onAdd) return;
const labelSource = option && typeof option === 'object'
? option.label ?? option.name ?? ''
: option;
const label = typeof labelSource === 'string' ? labelSource.trim() : '';
const label = resolveOptionName(option as QuickAddOption | string | null);
if (!label) {
return;
}
@@ -372,13 +360,13 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
if (!onAdd || !item) {
return;
}
const source = item.payload ?? item;
const source = (item.payload ?? item) as QuickAddOption | string | null;
const resolvedName = resolveOptionName(source);
if (!resolvedName) {
return;
}
const payload = (source && typeof source === 'object')
? { ...(source as Record<string, unknown>), name: resolvedName }
const payload = isPlainObject(source)
? { ...source, name: resolvedName }
: { id: null, name: resolvedName };
onAdd({ name: resolvedName, option: payload, input: null });
},
@@ -38,12 +38,12 @@ const useLazyVisibility = (
return undefined;
}
if (typeof window === 'undefined' || !('IntersectionObserver' in window)) {
if (!window.IntersectionObserver) {
setIsVisible(true);
return undefined;
}
const observer = new IntersectionObserver(
const observer = new window.IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
+1 -1
View File
@@ -175,7 +175,7 @@ const DocumentsGrid: React.FC<DocumentsGridProps> = ({
const classes = ['document-card', 'folder-card'];
if (isDraggingFolder) classes.push('is-dragging');
if (isSelectedFolder) classes.push('selected');
const canRenameFolder = folder.id !== 'root' && typeof onFolderRename === 'function';
const canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root';
const isFolderEditing = editingFolderId === folder.id;
const folderDraftValue = isFolderEditing ? folderDraft : folder.name;
const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : '';
+1 -1
View File
@@ -186,7 +186,7 @@ const DocumentsList: React.FC<DocumentsListProps> = ({
const isDraggingFolder = draggedFolderId === folder.id;
const isSelectedFolder = selectedFolderIdsSet?.has(folder.id);
const rowKey = `folder:${folder.id}`;
const canRenameFolder = folder.id !== 'root' && typeof onFolderRename === 'function';
const canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root';
const isFolderEditing = editingFolderId === folder.id;
const folderDraftValue = isFolderEditing ? folderDraft : folder.name;
const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : '';
@@ -64,12 +64,14 @@ const normalizeItems = (items?: SelectionAssignmentMenuItem[]): NormalizedSelect
: item.state === 'partial'
? 'partial'
: 'none';
const numericCount = Number.isFinite(item.count) ? Number(item.count) : null;
const numericTotal = Number.isFinite(item.total) ? Number(item.total) : null;
return {
id: item.id ?? trimmedLabel,
label: trimmedLabel,
state,
count: typeof item.count === 'number' ? item.count : null,
total: typeof item.total === 'number' ? item.total : null,
count: numericCount,
total: numericTotal,
payload: item.payload ?? item,
};
})
@@ -160,7 +162,7 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
const handleToggle = useCallback(
async (item: NormalizedSelectionAssignmentItem) => {
if (!item || typeof onToggle !== 'function') {
if (!item || !onToggle) {
return;
}
setPending(true);
@@ -179,7 +181,7 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
const handleCreate = useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault?.();
if (typeof onCreate !== 'function') {
if (!onCreate) {
return;
}
const value = query.trim();
@@ -89,6 +89,13 @@ const normalizeDocumentList = (selectedIds?: SelectedIdList): DocumentId[] =>
? selectedIds.filter((value): value is DocumentId => value !== null && value !== undefined)
: [];
const isRecord = (value: unknown): value is Record<string, unknown> => value != null && Object(value) === value;
const splitLabelSegments = (input: unknown): string[] => {
const text = `${input ?? ''}`.trim();
return text ? text.split('/') : [];
};
const buildFolderTreeOptions = (tree?: FolderTreeNode[] | null): SelectionAssignmentMenuItem[] => {
const entries: SelectionAssignmentMenuItem[] = [];
@@ -346,7 +353,9 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const documentCount = documentIdList.length;
const folderCount = folderIdList.length;
const totalCount = typeof selectionCount === 'number' ? selectionCount : documentCount + folderCount;
const totalCount = Number.isFinite(selectionCount)
? Number(selectionCount)
: documentCount + folderCount;
const selectedDocuments = useMemo<DocumentLike[]>(() => {
if (!documentIdList.length || !(documentLookupMap instanceof Map)) {
@@ -370,7 +379,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
return null;
}
const label = option?.label || option?.payload?.label || option?.name || String(id);
const segments = typeof label === 'string' ? label.split('/') : [label];
const segments = splitLabelSegments(label);
const depth = Math.max(segments.length - 1, 0);
return {
id,
@@ -399,7 +408,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const renderFolderLabel = useCallback((item: SelectionAssignmentMenuItem) => {
const payload = (item?.payload as { segments?: string[]; depth?: number }) || {};
const segments = payload.segments || (typeof item.label === 'string' ? item.label.split('/') : []);
const segments = payload.segments || splitLabelSegments(item.label);
const depth = payload.depth ?? Math.max(segments.length - 1, 0);
const clampedDepth = Math.min(depth, 6);
const indentWidth = clampedDepth > 0 ? clampedDepth * 0.9 : 0;
@@ -481,12 +490,12 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const handleMoveSelectionToFolder = useCallback(
async (option: unknown) => {
if (!documentIdList.length || typeof onMoveDocumentsToFolder !== 'function') {
if (!documentIdList.length || !onMoveDocumentsToFolder) {
return;
}
const candidate = option as { id?: DocumentId; value?: DocumentId } | DocumentId | null | undefined;
const value = typeof candidate === 'object'
? candidate?.id ?? candidate?.value ?? null
const value = isRecord(candidate)
? (candidate?.id ?? candidate?.value ?? null)
: candidate;
if (!value && value !== 0) {
return;
@@ -506,7 +515,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const showPrimaryButtons = Boolean(onBulkReanalyze || onDeleteSelection || onClearSelection);
const moveMenu = typeof onMoveDocumentsToFolder === 'function' ? (
const moveMenu = onMoveDocumentsToFolder ? (
<SelectionAssignmentMenu
label="Move"
triggerContent={(
@@ -534,7 +543,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const primaryButtons = showPrimaryButtons ? (
<div className="panel-floating__buttons">
{typeof onBulkReanalyze === 'function' ? (
{onBulkReanalyze ? (
<button
type="button"
className="icon-button panel-floating-actions__button"
@@ -546,7 +555,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
<AnalyzeIcon className="icon-inline" />
</button>
) : null}
{typeof onDeleteSelection === 'function' ? (
{onDeleteSelection ? (
<button
type="button"
className="icon-button danger panel-floating-actions__button"
@@ -557,7 +566,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
<TrashIcon className="icon-inline" />
</button>
) : null}
{typeof onClearSelection === 'function' ? (
{onClearSelection ? (
<button
type="button"
className="icon-button panel-floating-actions__button"
+4 -9
View File
@@ -61,16 +61,11 @@ export interface DocumentSummary {
const coercePageCount = (metadata: DocumentMetadata | null | undefined): number | null => {
const raw = metadata?.page_count;
if (typeof raw === 'number') {
return Number.isFinite(raw) && raw >= 0 ? raw : null;
if (raw == null || raw === '') {
return null;
}
if (raw != null && raw !== '') {
const parsed = Number.parseInt(String(raw), 10);
if (Number.isFinite(parsed) && parsed >= 0) {
return parsed;
}
}
return null;
const parsed = Number.parseInt(String(raw), 10);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
};
const sanitizeArray = <T>(entries: (T | null | undefined)[] | null | undefined): T[] =>
@@ -192,7 +192,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
if (!doc || !doc.id) {
return;
}
const previewAsset = typeof getDocumentAsset === 'function' ? getDocumentAsset(doc, 'preview') : null;
const previewAsset = getDocumentAsset(doc, 'preview');
if (!previewAsset) {
return;
}
@@ -207,12 +207,8 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
return;
}
if (event) {
if (typeof event.preventDefault === 'function') {
event.preventDefault();
}
if (typeof event.stopPropagation === 'function') {
event.stopPropagation();
}
event.preventDefault();
event.stopPropagation();
}
if (event?.altKey) {
handleDocumentPreviewZoom(doc);
@@ -489,16 +485,14 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
const handleDocumentClick = useCallback(
(doc, event) => {
if (!doc || suppressDocumentClickRef.current) {
if (!doc || suppressDocumentClickRef.current || !onEntryPointer) {
return;
}
if (typeof onEntryPointer === 'function') {
onEntryPointer(
{ type: EntryType.document, id: doc.id, key: `document:${doc.id}`, document: doc },
event,
);
}
onEntryPointer(
{ type: EntryType.document, id: doc.id, key: `document:${doc.id}`, document: doc },
event,
);
},
[onEntryPointer],
);
@@ -509,7 +503,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
return;
}
if (typeof onEntryPointer === 'function') {
if (onEntryPointer) {
onEntryPointer(
{ type: EntryType.folder, id: folder.id, key: `folder:${folder.id}`, folder },
event,
@@ -49,7 +49,7 @@ export const createDocumentsTableHeaderActions = ({
? 'Sorting Z → A. Click to switch to ascending.'
: 'Sorting A → Z. Click to switch to descending.';
const includeDescendantsToggle = isFilterActive && typeof onToggleIncludeDescendants === 'function'
const includeDescendantsToggle = isFilterActive && onToggleIncludeDescendants
? (
<button
type="button"
@@ -66,11 +66,11 @@ export const createDocumentsTableHeaderActions = ({
)
: null;
const sortControls = typeof onSortFieldChange === 'function'
const sortControls = onSortFieldChange
? (
<div className="documents-actions__sort-group">
<SortFieldQuickMenu sortField={sortField} onChange={onSortFieldChange} />
{typeof onSortDirectionToggle === 'function' ? (
{onSortDirectionToggle ? (
<button
type="button"
className="icon-button documents-toolbar__toggle documents-sort__direction"
+11 -9
View File
@@ -85,12 +85,13 @@ type DragEventLike = DragEvent | DataTransfer | {
};
export const parseTagTransferPayload = (input: DataTransfer | DragEventLike | null): TagPayload | null => {
let dataTransfer: DataTransfer | null = null;
if (input) {
if (typeof DataTransfer !== 'undefined' && input instanceof DataTransfer) {
dataTransfer = input;
} else if (typeof input === 'object' && 'dataTransfer' in input && input.dataTransfer) {
dataTransfer = input.dataTransfer;
let dataTransfer: DataTransfer | null = null;
if (input instanceof DataTransfer) {
dataTransfer = input;
} else if (input && Object(input) === input && 'dataTransfer' in (input as Record<string, unknown>)) {
const candidate = (input as { dataTransfer?: DataTransfer | null }).dataTransfer;
if (candidate) {
dataTransfer = candidate;
}
}
const raw = readTagTransferData(dataTransfer || null);
@@ -112,10 +113,11 @@ export const isTagTransferEvent = (event?: DragEventLike | null): boolean => {
return false;
}
let types: DOMStringList | ReadonlyArray<string> | undefined;
if (typeof DataTransfer !== 'undefined' && event instanceof DataTransfer) {
if (event instanceof DataTransfer) {
types = event.types;
} else if ('dataTransfer' in event && event.dataTransfer) {
types = event.dataTransfer.types;
} else if (Object(event) === event && 'dataTransfer' in (event as Record<string, unknown>)) {
const payload = (event as { dataTransfer?: DataTransfer | null }).dataTransfer;
types = payload?.types;
}
if (!types) {
return false;
+1 -1
View File
@@ -9,7 +9,7 @@ export const isPrimaryPointerEvent = (event?: PointerEventLike | null): boolean
if (!event) {
return true;
}
if (typeof event.button === 'number' && event.button !== 0) {
if (event.button !== 0) {
return false;
}
const type = event?.type?.toLowerCase?.() ?? '';