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}
))}
);
}