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 ?? (
)}
>
)
}