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