summary
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState, type ReactNode, type FormEvent } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||
import { EditIcon, IconX, CheckIcon, PlusIcon } from '../ui/icons';
|
||||
import SelectionAssignmentMenu, {
|
||||
SelectionAssignmentMenuItem,
|
||||
@@ -240,10 +240,17 @@ export const TagSection: React.FC<TagSectionProps> = ({
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (item.state === 'all' && onRemove) {
|
||||
const payload = isPlainObject(item.payload)
|
||||
? (item.payload as TagEntry)
|
||||
: tags.find((tag) => (tag.id ?? tag.label) === item.id) ?? { id: item.id, label: item.label };
|
||||
onRemove(payload);
|
||||
return;
|
||||
}
|
||||
const payload = item.payload ?? { label: item.label };
|
||||
handleSelect(payload);
|
||||
},
|
||||
[handleSelect],
|
||||
[handleSelect, onRemove, tags],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -280,7 +287,9 @@ export const TagSection: React.FC<TagSectionProps> = ({
|
||||
showCounts={false}
|
||||
positionStrategy="fixed"
|
||||
triggerClassName="quick-add__chip quick-add__trigger"
|
||||
triggerContent={<PlusIcon className="icon-inline" aria-hidden="true" />}
|
||||
triggerContent={<PlusIcon className="icon-inline" aria-hidden="true" />}
|
||||
closeOnSelection={false}
|
||||
freezeSortOnOpen
|
||||
/>
|
||||
) : null}
|
||||
{!tags.length && !showQuickAdd ? <span className="tag-list__empty meta">{emptyMessage}</span> : null}
|
||||
@@ -362,7 +371,17 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
|
||||
|
||||
const handleAssignmentSelect = useCallback(
|
||||
(item: NormalizedSelectionAssignmentItem) => {
|
||||
if (!onAdd || !item) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (item.state === 'all' && onRemove) {
|
||||
const payload = isPlainObject(item.payload)
|
||||
? (item.payload as CorrespondentEntry)
|
||||
: entries.find((entry) => (entry.id ?? entry.name) === item.id) ?? { id: item.id, name: item.label };
|
||||
onRemove(payload);
|
||||
return;
|
||||
}
|
||||
if (!onAdd) {
|
||||
return;
|
||||
}
|
||||
const source = (item.payload ?? item) as QuickAddOption | string | null;
|
||||
@@ -375,7 +394,7 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
|
||||
: { id: null, name: resolvedName };
|
||||
onAdd({ name: resolvedName, option: payload, input: null });
|
||||
},
|
||||
[onAdd],
|
||||
[entries, onAdd, onRemove],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -416,7 +435,9 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
|
||||
showCounts={false}
|
||||
positionStrategy="fixed"
|
||||
triggerClassName="quick-add__chip quick-add__trigger"
|
||||
triggerContent={<PlusIcon className="icon-inline" aria-hidden="true" />}
|
||||
triggerContent={<PlusIcon className="icon-inline" aria-hidden="true" />}
|
||||
closeOnSelection={false}
|
||||
freezeSortOnOpen
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -720,29 +741,23 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const summaryRowOverrides = useMemo(
|
||||
() => ({
|
||||
title: { valueContent: titleMetaDisplay, error: titleError },
|
||||
issued: { valueContent: issuedDisplay, error: issuedError },
|
||||
tags: { valueContent: tagsValueContent },
|
||||
correspondents: { valueContent: correspondentsValueContent },
|
||||
}),
|
||||
[titleMetaDisplay, titleError, issuedDisplay, issuedError, tagsValueContent, correspondentsValueContent],
|
||||
);
|
||||
const summaryRowOverrides = {
|
||||
title: { valueContent: titleMetaDisplay, error: titleError },
|
||||
issued: { valueContent: issuedDisplay, error: issuedError },
|
||||
tags: { valueContent: tagsValueContent },
|
||||
correspondents: { valueContent: correspondentsValueContent },
|
||||
} as Record<string, { valueContent?: React.ReactNode | null; error?: string | null }>;
|
||||
|
||||
const baseRows: MetaItem[] = useMemo(
|
||||
() => [...summaryRows, ...extraSummaryRows].map((row) => {
|
||||
const overrides = summaryRowOverrides[row.key] || {};
|
||||
return {
|
||||
key: row.key,
|
||||
label: row.label,
|
||||
valueContent: overrides.valueContent ?? null,
|
||||
fallbackValue: overrides.valueContent ? row.value : row.value,
|
||||
error: overrides.error ?? null,
|
||||
};
|
||||
}),
|
||||
[summaryRows, extraSummaryRows, summaryRowOverrides],
|
||||
);
|
||||
const baseRows: MetaItem[] = [...summaryRows, ...extraSummaryRows].map((row) => {
|
||||
const overrides = summaryRowOverrides[row.key] || {};
|
||||
return {
|
||||
key: row.key,
|
||||
label: row.label,
|
||||
valueContent: overrides.valueContent ?? null,
|
||||
fallbackValue: overrides.valueContent ? row.value : row.value,
|
||||
error: overrides.error ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
const allRows = baseRows;
|
||||
const summaryClass = `document-summary${isCompactLayout ? ' document-summary--compact' : ''}`;
|
||||
|
||||
@@ -41,6 +41,9 @@ export interface SelectionAssignmentMenuProps {
|
||||
onOpenMenu?: () => void;
|
||||
renderItemLabel?: (item: NormalizedSelectionAssignmentItem) => React.ReactNode;
|
||||
positionStrategy?: 'absolute' | 'fixed';
|
||||
closeOnSelection?: boolean;
|
||||
sortByState?: boolean;
|
||||
freezeSortOnOpen?: boolean;
|
||||
}
|
||||
|
||||
const STATE_ORDER: Record<AssignmentState, number> = {
|
||||
@@ -94,11 +97,15 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
|
||||
onOpenMenu,
|
||||
renderItemLabel,
|
||||
positionStrategy = 'absolute',
|
||||
closeOnSelection = true,
|
||||
sortByState = true,
|
||||
freezeSortOnOpen = false,
|
||||
}) => {
|
||||
const anchorRef = useRef<HTMLButtonElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [query, setQuery] = useState('');
|
||||
const [pending, setPending] = useState(false);
|
||||
const [sortSnapshot, setSortSnapshot] = useState<Array<string | number> | null>(null);
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@@ -145,20 +152,55 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
|
||||
|
||||
const normalizedItems = useMemo(() => normalizeItems(items), [items]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const search = query.trim().toLowerCase();
|
||||
const sorted = normalizedItems.slice().sort((a, b) => {
|
||||
const sortedByStateItems = useMemo(() => {
|
||||
if (!sortByState) {
|
||||
return normalizedItems;
|
||||
}
|
||||
return normalizedItems.slice().sort((a, b) => {
|
||||
const stateDiff = STATE_ORDER[a.state] - STATE_ORDER[b.state];
|
||||
if (stateDiff !== 0) {
|
||||
return stateDiff;
|
||||
}
|
||||
return a.label.localeCompare(b.label);
|
||||
});
|
||||
if (!search) {
|
||||
return sorted;
|
||||
}, [normalizedItems, sortByState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !freezeSortOnOpen || !sortByState) {
|
||||
setSortSnapshot(null);
|
||||
return;
|
||||
}
|
||||
return sorted.filter((item) => item.label.toLowerCase().includes(search));
|
||||
}, [normalizedItems, query]);
|
||||
setSortSnapshot((prev) => prev ?? sortedByStateItems.map((item) => item.id));
|
||||
}, [isOpen, freezeSortOnOpen, sortByState, sortedByStateItems]);
|
||||
|
||||
const orderedItems = useMemo(() => {
|
||||
if (freezeSortOnOpen && sortSnapshot && sortByState) {
|
||||
const itemMap = new Map<string | number, NormalizedSelectionAssignmentItem>(
|
||||
sortedByStateItems.map((item) => [item.id, item]),
|
||||
);
|
||||
const seen = new Set<string | number>();
|
||||
const fromSnapshot = sortSnapshot
|
||||
.map((id) => {
|
||||
const entry = itemMap.get(id);
|
||||
if (entry) {
|
||||
seen.add(entry.id);
|
||||
}
|
||||
return entry || null;
|
||||
})
|
||||
.filter((entry): entry is NormalizedSelectionAssignmentItem => Boolean(entry));
|
||||
const remaining = sortedByStateItems.filter((item) => !seen.has(item.id));
|
||||
return [...fromSnapshot, ...remaining];
|
||||
}
|
||||
return sortedByStateItems;
|
||||
}, [freezeSortOnOpen, sortSnapshot, sortByState, sortedByStateItems]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const search = query.trim().toLowerCase();
|
||||
if (!search) {
|
||||
return orderedItems;
|
||||
}
|
||||
return orderedItems.filter((item) => item.label.toLowerCase().includes(search));
|
||||
}, [orderedItems, query]);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
async (item: NormalizedSelectionAssignmentItem) => {
|
||||
@@ -169,13 +211,15 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
|
||||
try {
|
||||
await onToggle(item);
|
||||
setPending(false);
|
||||
close();
|
||||
if (closeOnSelection) {
|
||||
close();
|
||||
}
|
||||
} catch (error) {
|
||||
setPending(false);
|
||||
console.error('[selection-assignment] toggle failed', error);
|
||||
}
|
||||
},
|
||||
[onToggle, close],
|
||||
[onToggle, close, closeOnSelection],
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
|
||||
@@ -8,6 +8,7 @@ interface DocumentPageMetadata {
|
||||
interface DocumentVersion {
|
||||
size_bytes?: number | string | null;
|
||||
metadata?: DocumentPageMetadata | null;
|
||||
checksum?: string | null;
|
||||
}
|
||||
|
||||
interface TagEntry {
|
||||
@@ -21,6 +22,7 @@ interface CorrespondentEntry {
|
||||
export interface SummaryDocument {
|
||||
title?: string | null;
|
||||
original_name?: string | null;
|
||||
filename?: string | null;
|
||||
content_type?: string | null;
|
||||
current_version?: DocumentVersion | null;
|
||||
created_at?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user