refactor
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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() : '';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?.() ?? '';
|
||||
|
||||
Reference in New Issue
Block a user