import { useT } from "@agent-native/core/client/i18n"; import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { arrayMove, horizontalListSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { IconChevronDown, IconGripVertical, IconX } from "@tabler/icons-react"; import { useMemo } from "react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; export type ChipSelectOption = { id: string; label: string; }; type ChipSelectProps = { label: string; options: ChipSelectOption[]; selectedIds: string[]; onSelectedIdsChange: (ids: string[]) => void; disabled?: boolean; limit?: number; addButtonLabel?: string; emptyLabel?: string; sortable?: boolean; }; function ChipSelectChip({ option, disabled, sortable, onRemove, }: { option: ChipSelectOption; disabled: boolean; sortable: boolean; onRemove: () => void; }) { const t = useT(); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: option.id, disabled: disabled || !sortable }); return (
{sortable ? ( ) : null} {option.label}
); } export function ChipSelect({ label, options, selectedIds, onSelectedIdsChange, disabled = false, limit, addButtonLabel, emptyLabel, sortable = true, }: ChipSelectProps) { const t = useT(); const resolvedAddButtonLabel = addButtonLabel ?? t("common.add"); const resolvedEmptyLabel = emptyLabel ?? t("common.noneSelected"); const optionsById = useMemo( () => new Map(options.map((option) => [option.id, option])), [options], ); const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]); const selectedOptions = useMemo( () => selectedIds .map((id) => optionsById.get(id)) .filter((option): option is ChipSelectOption => Boolean(option)), [optionsById, selectedIds], ); const availableOptions = useMemo( () => options.filter((option) => !selectedIdSet.has(option.id)), [options, selectedIdSet], ); const atLimit = limit !== undefined && selectedOptions.length >= limit; const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); function addOption(option: ChipSelectOption) { if (atLimit) return; onSelectedIdsChange([...selectedIds, option.id]); } function removeOption(option: ChipSelectOption) { onSelectedIdsChange(selectedIds.filter((id) => id !== option.id)); } function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (disabled || !sortable || !over || active.id === over.id) return; const activeId = String(active.id); const overId = String(over.id); const oldIndex = selectedOptions.findIndex( (option) => option.id === activeId, ); const newIndex = selectedOptions.findIndex( (option) => option.id === overId, ); if (oldIndex < 0 || newIndex < 0) return; onSelectedIdsChange( arrayMove(selectedOptions, oldIndex, newIndex).map((option) => option.id), ); } const chips = (
{selectedOptions.map((option) => ( removeOption(option)} /> ))}
); return (
{limit !== undefined ? ( {selectedOptions.length}/{limit} ) : null}
{selectedOptions.length === 0 ? ( {resolvedEmptyLabel} ) : sortable ? ( option.id)} strategy={horizontalListSortingStrategy} > {chips} ) : ( chips )} {availableOptions.map((option) => ( addOption(option)} > {option.label} ))}
); }