This commit is contained in:
2025-11-16 00:39:37 +01:00
parent d0379c57ce
commit 7e0768a09a
3 changed files with 98 additions and 37 deletions
@@ -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(