import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core' import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable' import { IconButton, Menu, SvgIcon } from '@mui/material' import { useCallback, useEffect, useMemo, useState, type MouseEvent, } from 'react' import { widgetStoreActions } from '../../stores/widget-store' import type { ChangeColumnProps } from './types' import { actionButtonStyles } from '../shared/styles' import { Tooltip } from '../../../components' import type { TableColumn, TableWidgetState } from '../../table/types' import { ChangeColumnIcon } from './change-column-icon' import { SortableColumnItem } from './sortable-column-item' import { useWidgetSelector } from '../../stores/use-widget-selector' export const CHANGE_COLUMN_TOOL_ID = 'change-column' /** * Widget action to reorder columns in a table widget via drag-and-drop. * * This action reads the columns from the widget store and allows users to * drag and drop columns to reorder them. All columns are displayed and * can be reordered. * * Registers as a config pipeline tool so that column order is automatically * re-applied when the base config is updated (e.g., by WidgetLoader). * * Returns null if there are fewer than 2 columns. * * @example * ```tsx * * ``` */ export function ChangeColumn({ id, labels, Icon, IconButtonProps, MenuProps, }: ChangeColumnProps) { const [anchorEl, setAnchorEl] = useState(null) const { columns } = useWidgetSelector(id, (w) => ({ columns: (w as TableWidgetState | undefined)?.columns, })) /** * Config tool function that reorders columns to match the current widget state. * Reads desired order from the widget store (set by handleDragEnd via setWidget). * Preserves referential identity when the order already matches to prevent * re-render loops in the config pipeline. */ const reorderFn = useCallback( (currentConfig: unknown): unknown => { const widgetState = widgetStoreActions.getWidget(id) const currentColumns = widgetState?.columns if (!currentColumns || currentColumns.length === 0) return currentConfig const config = currentConfig as Record const configColumns = config.columns as TableColumn[] | undefined if (!configColumns || configColumns.length === 0) return currentConfig // Check if config columns are already in the same order as widget columns const alreadyMatches = configColumns.length === currentColumns.length && configColumns.every((col, i) => col.id === currentColumns[i]?.id) if (alreadyMatches) return currentConfig // Reorder config columns to match widget column order const columnMap = new Map(configColumns.map((col) => [col.id, col])) const reordered: TableColumn[] = [] for (const widgetCol of currentColumns) { const col = columnMap.get(widgetCol.id) if (col) { reordered.push(col) columnMap.delete(widgetCol.id) } } // Append any new columns not in the widget order for (const col of columnMap.values()) { reordered.push(col) } // If result matches current widget columns, reuse the same array reference // to prevent downstream subscribers from detecting a change const matchesWidget = reordered.length === currentColumns.length && reordered.every((col, i) => col.id === currentColumns[i]?.id) return { ...config, columns: matchesWidget ? currentColumns : reordered } }, [id], ) const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ) const columnIds = useMemo( () => columns?.map((col) => col.id) ?? [], [columns], ) // Register config tool on mount useEffect(() => { widgetStoreActions.registerTool(id, { id: CHANGE_COLUMN_TOOL_ID, type: 'config', order: 100, enabled: true, fn: reorderFn, }) return () => widgetStoreActions.unregisterTool(id, CHANGE_COLUMN_TOOL_ID) }, [id, reorderFn]) const handleToggle = useCallback((event: MouseEvent) => { event.stopPropagation() setAnchorEl(event.currentTarget) }, []) const handleClose = useCallback(() => { setAnchorEl(null) }, []) const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event if (!over || active.id === over.id || !columns) return const oldIndex = columns.findIndex((col) => col.id === active.id) const newIndex = columns.findIndex((col) => col.id === over.id) if (oldIndex !== -1 && newIndex !== -1) { const newColumns = arrayMove(columns, oldIndex, newIndex) widgetStoreActions.setWidget(id, { columns: newColumns }) } } // Return null if there are fewer than 2 columns if (!columns || columns.length < 2) { return null } const tooltipLabel = labels?.tooltip ?? 'Change column' const isOpen = Boolean(anchorEl) return ( <> {Icon ?? ( )} {columns.map((column) => ( ))} ) }