detailpanel
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
ChevronsRightIcon,
|
||||
@@ -10,13 +9,18 @@ import {
|
||||
TextScanIcon,
|
||||
} from '../ui/icons';
|
||||
import PanelHeader from '../ui/PanelHeader';
|
||||
import { getTagColorStyle } from '../utils/colors';
|
||||
import { formatFileSize } from '../utils/format';
|
||||
import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager';
|
||||
import { useAssetNavigator } from '../hooks/useAssetNavigator';
|
||||
import { describeDocumentSummary } from '../documents/documentSummary';
|
||||
import { createDocumentActionState } from '../documents/documentActions';
|
||||
import PreviewZoomOverlay from './PreviewZoomOverlay';
|
||||
import DocumentSummarySection, {
|
||||
TagSection,
|
||||
CorrespondentSection,
|
||||
sortCorrespondents,
|
||||
buildCorrespondentOptions,
|
||||
} from '../documents/DocumentSummarySection';
|
||||
|
||||
const MAX_PREVIEW_STACK_ITEMS = 15;
|
||||
|
||||
@@ -29,156 +33,6 @@ const derivePreviewOrientation = (metadata) => {
|
||||
return 'landscape';
|
||||
};
|
||||
|
||||
const sortCorrespondents = (entries = []) =>
|
||||
entries
|
||||
.filter((entry) => entry && entry.name)
|
||||
.map(({ id, name, count }) => ({ id, name, count }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const CorrespondentPills = ({ entries = [], onRemove, showCount = false }) => (
|
||||
<div className="correspondent-list">
|
||||
{entries.length ? (
|
||||
entries.map((entry, index) => {
|
||||
const key = entry.id ?? `${entry.name}-${index}`;
|
||||
return (
|
||||
<span key={key} className="correspondent-pill">
|
||||
<span className="correspondent-pill__label">
|
||||
<span>
|
||||
{entry.name}
|
||||
{showCount && entry.count ? ` (${entry.count})` : ''}
|
||||
</span>
|
||||
</span>
|
||||
{onRemove ? (
|
||||
<button
|
||||
type="button"
|
||||
className="correspondent-pill__remove"
|
||||
onClick={() => onRemove(entry)}
|
||||
aria-label={`Remove ${entry.name}`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className="meta">No correspondents yet.</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const TagSection = ({
|
||||
title,
|
||||
tags = [],
|
||||
onRemove,
|
||||
onAdd,
|
||||
emptyMessage = 'No tags yet.',
|
||||
addPlaceholder = 'Add or create tag',
|
||||
addButtonLabel = 'Add',
|
||||
datalistId,
|
||||
datalistOptions = [],
|
||||
className,
|
||||
}) => (
|
||||
<div className={className}>
|
||||
<dt>{title}</dt>
|
||||
<div className="tag-list">
|
||||
{tags.length ? (
|
||||
tags.map((tag) => {
|
||||
const key = tag.id ?? tag.label;
|
||||
const style = getTagColorStyle(tag.color);
|
||||
const removable = Boolean(onRemove);
|
||||
const className = removable ? 'badge tag-chip tag-chip--removable' : 'badge tag-chip';
|
||||
return (
|
||||
<span key={key} className={className} style={style || undefined}>
|
||||
<span className="tag-chip__label">{tag.label}</span>
|
||||
{removable ? (
|
||||
<button
|
||||
type="button"
|
||||
className="tag-chip__remove"
|
||||
onClick={() => onRemove(tag)}
|
||||
aria-label={`Remove tag ${tag.label}`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className="meta">{emptyMessage}</span>
|
||||
)}
|
||||
</div>
|
||||
{onAdd ? (
|
||||
<form
|
||||
className="inline"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
const input = event.currentTarget.elements.tag;
|
||||
const value = input.value.trim();
|
||||
if (!value) return;
|
||||
onAdd({ value, input });
|
||||
}}
|
||||
>
|
||||
<input name="tag" placeholder={addPlaceholder} list={datalistId} />
|
||||
<button type="submit">{addButtonLabel}</button>
|
||||
{datalistId ? (
|
||||
<datalist id={datalistId}>
|
||||
{datalistOptions.map((option) => (
|
||||
<option key={option.id} />
|
||||
))}
|
||||
</datalist>
|
||||
) : null}
|
||||
</form>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
const CorrespondentSection = ({
|
||||
title,
|
||||
entries = [],
|
||||
onRemove,
|
||||
onAdd,
|
||||
showCount = false,
|
||||
addPlaceholder = 'Add or create correspondent',
|
||||
addButtonLabel = 'Add',
|
||||
datalistId,
|
||||
datalistOptions = [],
|
||||
className,
|
||||
}) => (
|
||||
<div className={className}>
|
||||
<dt>{title}</dt>
|
||||
<CorrespondentPills entries={entries} onRemove={onRemove} showCount={showCount} />
|
||||
{onAdd ? (
|
||||
<form
|
||||
className="correspondent-form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget;
|
||||
const nameInput = form.elements.correspondent;
|
||||
const value = nameInput.value.trim();
|
||||
if (!value) return;
|
||||
onAdd({ name: value, input: nameInput });
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
name="correspondent"
|
||||
placeholder={addPlaceholder}
|
||||
list={datalistId}
|
||||
/>
|
||||
<button type="submit">{addButtonLabel}</button>
|
||||
{datalistId ? (
|
||||
<datalist id={datalistId}>
|
||||
{datalistOptions.map((name) => (
|
||||
<option key={name} value={name} />
|
||||
))}
|
||||
</datalist>
|
||||
) : null}
|
||||
</form>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
const computeStackAngle = (docId, index) => {
|
||||
if (index === 0) return 0;
|
||||
let hash = 0;
|
||||
@@ -294,6 +148,7 @@ const DetailPanel = ({
|
||||
onBulkCorrespondentRemove,
|
||||
onPromoteSelection,
|
||||
onUpdateTitle = async () => false,
|
||||
onUpdateIssued = async () => false,
|
||||
ensureAssetUrl = null,
|
||||
getDocumentAsset = () => null,
|
||||
ensurePreviewData = () => Promise.resolve(),
|
||||
@@ -337,10 +192,6 @@ const DetailPanel = ({
|
||||
return `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
|
||||
}, [selectedCount, detailSummary]);
|
||||
|
||||
const [titleEditDocId, setTitleEditDocId] = useState(null);
|
||||
const [titleDraft, setTitleDraft] = useState('');
|
||||
const [titleSaving, setTitleSaving] = useState(false);
|
||||
const [titleError, setTitleError] = useState(null);
|
||||
const [zoomedPreview, setZoomedPreview] = useState(null);
|
||||
|
||||
const bulkDocumentIds = useMemo(
|
||||
@@ -348,67 +199,10 @@ const DetailPanel = ({
|
||||
[selectedDocuments],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!singleDoc) {
|
||||
setTitleEditDocId(null);
|
||||
setTitleDraft('');
|
||||
setTitleError(null);
|
||||
setTitleSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (titleEditDocId && titleEditDocId !== singleDoc.id) {
|
||||
setTitleEditDocId(null);
|
||||
setTitleDraft('');
|
||||
setTitleError(null);
|
||||
setTitleSaving(false);
|
||||
}
|
||||
}, [singleDoc, titleEditDocId]);
|
||||
|
||||
useEffect(() => {
|
||||
setZoomedPreview(null);
|
||||
}, [selectionKey]);
|
||||
|
||||
const startTitleEdit = useCallback(() => {
|
||||
if (!singleDoc) return;
|
||||
setTitleEditDocId(singleDoc.id);
|
||||
setTitleDraft(singleDoc.title || singleDoc.original_name);
|
||||
setTitleError(null);
|
||||
}, [singleDoc]);
|
||||
|
||||
const cancelTitleEdit = useCallback(() => {
|
||||
setTitleEditDocId(null);
|
||||
setTitleDraft('');
|
||||
setTitleError(null);
|
||||
setTitleSaving(false);
|
||||
}, []);
|
||||
|
||||
const submitTitleEdit = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
if (!singleDoc) return;
|
||||
const trimmed = titleDraft.trim();
|
||||
if (!trimmed) {
|
||||
setTitleError('Title cannot be empty.');
|
||||
return;
|
||||
}
|
||||
setTitleSaving(true);
|
||||
try {
|
||||
const ok = await onUpdateTitle(singleDoc.id, trimmed);
|
||||
if (ok) {
|
||||
setTitleEditDocId(null);
|
||||
setTitleDraft('');
|
||||
setTitleError(null);
|
||||
} else {
|
||||
setTitleError('Failed to update title.');
|
||||
}
|
||||
} finally {
|
||||
setTitleSaving(false);
|
||||
}
|
||||
},
|
||||
[singleDoc, titleDraft, onUpdateTitle],
|
||||
);
|
||||
|
||||
const handlePreviewActivate = useCallback(
|
||||
(docId) => {
|
||||
if (!docId) return;
|
||||
@@ -569,134 +363,16 @@ const DetailPanel = ({
|
||||
}, 0);
|
||||
}, [stackPreviews, selectedDocuments]);
|
||||
|
||||
const availableCorrespondents = useMemo(
|
||||
() => (Array.isArray(correspondents) ? correspondents : []),
|
||||
const correspondentOptions = useMemo(
|
||||
() => buildCorrespondentOptions(Array.isArray(correspondents) ? correspondents : []),
|
||||
[correspondents],
|
||||
);
|
||||
|
||||
const correspondentOptions = useMemo(() => {
|
||||
const seen = new Set();
|
||||
return availableCorrespondents.reduce((options, entry) => {
|
||||
if (typeof entry?.name !== 'string') {
|
||||
return options;
|
||||
}
|
||||
const name = entry.name.trim();
|
||||
if (!name) {
|
||||
return options;
|
||||
}
|
||||
const lower = name.toLowerCase();
|
||||
if (seen.has(lower)) {
|
||||
return options;
|
||||
}
|
||||
seen.add(lower);
|
||||
options.push(name);
|
||||
return options;
|
||||
}, []);
|
||||
}, [availableCorrespondents]);
|
||||
|
||||
const singleCorrespondents = useMemo(() => {
|
||||
if (!singleDoc) return [];
|
||||
return sortCorrespondents(singleDoc.correspondents || []);
|
||||
}, [singleDoc]);
|
||||
|
||||
const singleFolderPath = useMemo(() => {
|
||||
if (!singleDoc?.folder_id) {
|
||||
return null;
|
||||
}
|
||||
if (typeof resolveFolderPath !== 'function') {
|
||||
return null;
|
||||
}
|
||||
const segments = resolveFolderPath(singleDoc.folder_id);
|
||||
if (!Array.isArray(segments) || !segments.some((segment) => segment?.id && segment.id !== 'root')) {
|
||||
return null;
|
||||
}
|
||||
return segments;
|
||||
}, [singleDoc?.folder_id, resolveFolderPath]);
|
||||
|
||||
const folderLabel = detailSummary.folderLabel;
|
||||
|
||||
const folderDisplayNode = useMemo(() => {
|
||||
if (!singleDoc) {
|
||||
return folderLabel || '—';
|
||||
}
|
||||
if (!singleFolderPath?.length) {
|
||||
return folderLabel || '—';
|
||||
}
|
||||
return (
|
||||
<span className="detail-folder-path">
|
||||
{singleFolderPath.map((segment, index) => {
|
||||
const label = segment?.name || '…';
|
||||
const targetId = segment?.id || null;
|
||||
const key = `${targetId || label}-${index}`;
|
||||
const isClickable = Boolean(targetId) && typeof onFolderNavigate === 'function';
|
||||
const href = !isClickable
|
||||
? null
|
||||
: targetId === 'root'
|
||||
? '/documents'
|
||||
: `/documents/folder/${targetId}`;
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
{index > 0 ? <span className="detail-folder-path__separator">/</span> : null}
|
||||
{isClickable ? (
|
||||
<a
|
||||
href={href}
|
||||
className="detail-folder-path__link"
|
||||
onClick={(event) => {
|
||||
if (
|
||||
event.button !== 0 ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onFolderNavigate(targetId);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
) : (
|
||||
<span className="detail-folder-path__segment">{label}</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}, [singleDoc, singleFolderPath, folderLabel, onFolderNavigate]);
|
||||
|
||||
const detailInfoRows = useMemo(() => {
|
||||
if (!singleDoc) {
|
||||
return [];
|
||||
}
|
||||
const allowedKeys = new Set(['uploaded', 'size', 'type', 'issued', 'pages', 'created', 'updated', 'folder']);
|
||||
const rows = detailSummary.summaryRows
|
||||
.filter((row) => {
|
||||
if (!allowedKeys.has(row.key)) {
|
||||
return false;
|
||||
}
|
||||
if (row.key === 'pages') {
|
||||
return Number.isFinite(detailSummary.pageCount);
|
||||
}
|
||||
if (row.key === 'folder') {
|
||||
return Boolean(singleFolderPath?.length);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((row) => (row.key === 'folder' ? { ...row, value: folderDisplayNode } : row));
|
||||
|
||||
rows.push({
|
||||
key: 'original-name',
|
||||
label: 'Original filename',
|
||||
value: singleDoc.original_name || '—',
|
||||
});
|
||||
|
||||
return rows;
|
||||
}, [singleDoc, detailSummary, folderDisplayNode, singleFolderPath]);
|
||||
|
||||
const bulkCorrespondents = useMemo(() => {
|
||||
if (selectedDocuments.length <= 1) {
|
||||
const doc = selectedDocuments[0];
|
||||
@@ -971,15 +647,11 @@ const DetailPanel = ({
|
||||
return <p className="meta">Select a document to view metadata, tags and actions.</p>;
|
||||
}
|
||||
|
||||
const displayName = singleDoc.title || singleDoc.original_name;
|
||||
const isEditingTitle = titleEditDocId === singleDoc.id;
|
||||
const tagsForDoc = Array.isArray(singleDoc.tags) ? singleDoc.tags : [];
|
||||
const metadata =
|
||||
singleDoc.metadata && Object.keys(singleDoc.metadata).length > 0 ? singleDoc.metadata : null;
|
||||
const effectiveCardinality = singleEffectiveCardinality;
|
||||
const effectiveCardinality =
|
||||
singlePreviewNavigator.cardinality || (singlePreviewNavigator.currentUrl ? 1 : 0);
|
||||
const canGoPrev = singlePreviewNavigator.canGoPrev;
|
||||
const canGoNext = singlePreviewNavigator.canGoNext;
|
||||
const hasPreviewImage = singleHasPreview;
|
||||
const hasPreviewImage = Boolean(singlePreviewNavigator.currentUrl);
|
||||
const interceptNavPointer = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -997,142 +669,59 @@ const DetailPanel = ({
|
||||
onZoomPreview={handleSingleZoom}
|
||||
/>
|
||||
{hasPreviewImage && (effectiveCardinality > 1 || canGoPrev || canGoNext) ? (
|
||||
<div className="preview-pane__nav preview-pane__nav--overlay">
|
||||
<button
|
||||
type="button"
|
||||
className="preview-pane__nav-button preview-pane__nav-button--prev"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
singlePreviewNavigator.goPrev();
|
||||
}}
|
||||
onPointerDown={interceptNavPointer}
|
||||
onPointerUp={interceptNavPointer}
|
||||
onMouseDown={interceptNavPointer}
|
||||
onMouseUp={interceptNavPointer}
|
||||
disabled={!canGoPrev}
|
||||
aria-label="Previous preview"
|
||||
>
|
||||
<ArrowLeftIcon />
|
||||
</button>
|
||||
<div className="preview-pane__nav preview-pane__nav--overlay">
|
||||
<button
|
||||
type="button"
|
||||
className="preview-pane__nav-button preview-pane__nav-button--next"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
singlePreviewNavigator.goNext();
|
||||
}}
|
||||
onPointerDown={interceptNavPointer}
|
||||
onPointerUp={interceptNavPointer}
|
||||
onMouseDown={interceptNavPointer}
|
||||
onMouseUp={interceptNavPointer}
|
||||
disabled={!canGoNext}
|
||||
aria-label="Next preview"
|
||||
>
|
||||
<ArrowRightIcon />
|
||||
</button>
|
||||
className="preview-pane__nav-button preview-pane__nav-button--prev"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
singlePreviewNavigator.goPrev();
|
||||
}}
|
||||
onPointerDown={interceptNavPointer}
|
||||
onPointerUp={interceptNavPointer}
|
||||
onMouseDown={interceptNavPointer}
|
||||
onMouseUp={interceptNavPointer}
|
||||
disabled={!canGoPrev}
|
||||
aria-label="Previous preview"
|
||||
>
|
||||
<ArrowLeftIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="preview-pane__nav-button preview-pane__nav-button--next"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
singlePreviewNavigator.goNext();
|
||||
}}
|
||||
onPointerDown={interceptNavPointer}
|
||||
onPointerUp={interceptNavPointer}
|
||||
onMouseDown={interceptNavPointer}
|
||||
onMouseUp={interceptNavPointer}
|
||||
disabled={!canGoNext}
|
||||
aria-label="Next preview"
|
||||
>
|
||||
<ArrowRightIcon />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="doc-title-row">
|
||||
{isEditingTitle ? (
|
||||
<form className="doc-title-edit" onSubmit={submitTitleEdit}>
|
||||
<input
|
||||
value={titleDraft}
|
||||
onChange={(event) => {
|
||||
setTitleDraft(event.target.value);
|
||||
if (titleError) {
|
||||
setTitleError(null);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelTitleEdit();
|
||||
}
|
||||
}}
|
||||
aria-label="Document title"
|
||||
autoFocus
|
||||
/>
|
||||
<button type="submit" disabled={titleSaving}>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={cancelTitleEdit}
|
||||
disabled={titleSaving}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<h3 style={{ margin: 0 }}>{displayName}</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={startTitleEdit}
|
||||
aria-label="Edit title"
|
||||
title="Edit title"
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{titleError ? <div className="status-inline error">{titleError}</div> : null}
|
||||
<div className="meta">
|
||||
{detailInfoRows.map((row) => {
|
||||
const rawValue = row.value;
|
||||
const displayValue =
|
||||
rawValue === null || rawValue === undefined || rawValue === '' ? '—' : rawValue;
|
||||
return (
|
||||
<div key={row.key}>
|
||||
<strong>{row.label}:</strong>{' '}
|
||||
{displayValue}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<TagSection
|
||||
title="Tags"
|
||||
tags={tagsForDoc.map((tag) => ({
|
||||
id: tag.id,
|
||||
label: tag.label,
|
||||
color: tag.color || tagLookupById.get(tag.id)?.color,
|
||||
}))}
|
||||
onRemove={(tag) => onTagRemove(singleDoc.id, tag.id)}
|
||||
onAdd={({ value, input }) => onTagAdd(singleDoc, value, input)}
|
||||
datalistId="tag-catalog-single"
|
||||
datalistOptions={tags}
|
||||
<DocumentSummarySection
|
||||
document={singleDoc}
|
||||
tagLookupById={tagLookupById}
|
||||
tagOptions={tags}
|
||||
onTagAdd={(doc, value, extras) => onTagAdd(doc, value, extras)}
|
||||
onTagRemove={(docId, tagId) => onTagRemove(docId, tagId)}
|
||||
correspondents={singleCorrespondents}
|
||||
correspondentOptions={correspondentOptions}
|
||||
onCorrespondentAdd={onCorrespondentAdd}
|
||||
onCorrespondentRemove={onCorrespondentRemove}
|
||||
onUpdateTitle={onUpdateTitle}
|
||||
onUpdateIssued={onUpdateIssued}
|
||||
resolveFolderPath={resolveFolderPath}
|
||||
onFolderNavigate={onFolderNavigate}
|
||||
/>
|
||||
<CorrespondentSection
|
||||
title="Correspondents"
|
||||
entries={singleCorrespondents}
|
||||
onRemove={(entry) =>
|
||||
onCorrespondentRemove?.({
|
||||
documentId: singleDoc.id,
|
||||
correspondentId: entry.id,
|
||||
})
|
||||
}
|
||||
onAdd={({ name, input }) =>
|
||||
onCorrespondentAdd?.({
|
||||
document: singleDoc,
|
||||
name,
|
||||
input,
|
||||
})
|
||||
}
|
||||
datalistId="correspondent-catalog-single"
|
||||
datalistOptions={correspondentOptions}
|
||||
/>
|
||||
{metadata && (
|
||||
<div>
|
||||
<dt>Metadata</dt>
|
||||
<pre className="detail-metadata__block">{JSON.stringify(metadata, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1140,6 +729,7 @@ const DetailPanel = ({
|
||||
const renderBulk = () => {
|
||||
const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
|
||||
const sizeLabel = stackTotalSizeBytes ? formatFileSize(stackTotalSizeBytes) : '—';
|
||||
const headerLabel = `${countLabel}${sizeLabel ? ` (${sizeLabel})` : ''}`;
|
||||
const topDocIdLocal = topDocId;
|
||||
const topCardinalityLocal = topEffectiveCardinality;
|
||||
const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);
|
||||
@@ -1200,35 +790,26 @@ const DetailPanel = ({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<h3 style={{ margin: 0 }}>{countLabel}</h3>
|
||||
<div className="meta">
|
||||
<div>
|
||||
<strong>Total size (stack):</strong> {sizeLabel}
|
||||
</div>
|
||||
</div>
|
||||
<h3 style={{ margin: 0 }}>{headerLabel}</h3>
|
||||
<TagSection
|
||||
title="Tags"
|
||||
tags={bulkTagUnion}
|
||||
emptyMessage="No tags assigned."
|
||||
onRemove={(tag) => onBulkTagRemove?.({ label: tag.label, documentIds })}
|
||||
onAdd={({ value, input }) =>
|
||||
onBulkTagAdd?.({ label: value, input, documentIds })
|
||||
onAdd={({ value }) =>
|
||||
onBulkTagAdd?.({ label: value, input: null, documentIds })
|
||||
}
|
||||
addPlaceholder="Add tag to selection"
|
||||
addButtonLabel="Add tag"
|
||||
datalistId="tag-catalog-bulk"
|
||||
datalistOptions={tags}
|
||||
className="bulk-tags"
|
||||
/>
|
||||
<CorrespondentSection
|
||||
title="Correspondents"
|
||||
entries={bulkCorrespondents}
|
||||
onRemove={handleBulkCorrespondentRemove}
|
||||
onAdd={({ name, input }) =>
|
||||
onBulkCorrespondentAdd?.({ name, input, documentIds })
|
||||
onAdd={({ name }) =>
|
||||
onBulkCorrespondentAdd?.({ name, input: null, documentIds })
|
||||
}
|
||||
addPlaceholder="Add correspondent to selection"
|
||||
datalistId="correspondent-catalog-bulk"
|
||||
datalistOptions={correspondentOptions}
|
||||
showCount
|
||||
className="bulk-correspondents"
|
||||
|
||||
Reference in New Issue
Block a user