import { useCallback, useMemo, useState, type ComponentType } from 'react' import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core' import { SortableContext, arrayMove, sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable' import { IconButton, Menu, type SvgIconProps } from '@mui/material' import { Tooltip } from '../../../components' import { getWidgetStore, useWidget, useWidgetId } from '../../stores' import { ChangeColumnIcon } from './change-column-icon' import { DEFAULT_CHANGE_COLUMN_LABELS, type ChangeColumnLabels } from './labels' import { SortableColumnItem } from './sortable-column-item' import { styles } from './style' import type { ChangeColumnWidgetState } from './types' import type { ReactNode } from 'react' export interface ChangeColumnItem { id: string label: ReactNode } export interface ChangeColumnProps { /** * Master list of columns (id + label) in their default order. The current * effective order is derived by combining this list with the widget's * `columnOrder` extension state. */ columns: readonly ChangeColumnItem[] labels?: Partial icon?: ComponentType iconProps?: SvgIconProps dragHandleIcon?: ComponentType } /** * Action that lets users reorder columns via drag-and-drop. The new order is * persisted on the widget store as `columnOrder` (an array of ids) on the * {@link ChangeColumnWidgetState} extension. `` (and other column-aware * bridges) merge that order with their own `columns` prop via * `resolveColumns()` from the table primitives. * * Returns `null` when there are fewer than two columns to reorder. */ export function ChangeColumn({ columns, labels, icon: Icon = ChangeColumnIcon, iconProps, dragHandleIcon, }: ChangeColumnProps) { const id = useWidgetId() const _labels = { ...DEFAULT_CHANGE_COLUMN_LABELS, ...labels } const [anchorEl, setAnchorEl] = useState(null) const columnOrder = useWidget< readonly string[] | undefined, ChangeColumnWidgetState >(id, (s) => s.columnOrder) const orderedIds = useMemo(() => { const masterIds = columns.map((c) => c.id) if (!columnOrder || columnOrder.length === 0) return masterIds // Set lookup so membership tests stay O(1) regardless of column count. const masterIdSet = new Set(masterIds) const seen = new Set() const out: string[] = [] for (const cid of columnOrder) { if (masterIdSet.has(cid) && !seen.has(cid)) { out.push(cid) seen.add(cid) } } for (const cid of masterIds) { if (!seen.has(cid)) out.push(cid) } return out }, [columnOrder, columns]) const orderedColumns = useMemo(() => { const byId = new Map(columns.map((c) => [c.id, c])) // Single pass instead of map().filter() — skips unknown ids inline. const out: ChangeColumnItem[] = [] for (const cid of orderedIds) { const c = byId.get(cid) if (c != null) out.push(c) } return out }, [orderedIds, columns]) const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ) const handleOpen = useCallback((e: React.MouseEvent) => { e.stopPropagation() setAnchorEl(e.currentTarget) }, []) const handleClose = useCallback(() => setAnchorEl(null), []) const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event if (!over || active.id === over.id) return const oldIndex = orderedIds.indexOf(String(active.id)) const newIndex = orderedIds.indexOf(String(over.id)) if (oldIndex === -1 || newIndex === -1) return const next = arrayMove([...orderedIds], oldIndex, newIndex) getWidgetStore(id).setState({ columnOrder: next, } as Partial) }, [id, orderedIds], ) if (columns.length < 2) return null const isOpen = Boolean(anchorEl) return ( <> {orderedColumns.map((column) => ( ))} ) }