import React, { useCallback, useMemo, useRef, useState } from 'react'; import { IconTrash, IconAlertCircle, IconGripVertical, IconMinusVertical } from '@tabler/icons'; import { Tooltip } from 'react-tooltip'; import { uuid } from 'utils/common'; import StyledWrapper from './StyledWrapper'; const EditableTable = ({ columns, rows, onChange, defaultRow, getRowError, showCheckbox = true, showDelete = true, disableCheckbox = false, checkboxLabel = '', checkboxKey = 'enabled', reorderable = false, onReorder, showAddRow = true, testId = 'editable-table' }) => { const tableRef = useRef(null); const emptyRowUidRef = useRef(null); const [hoveredRow, setHoveredRow] = useState(null); const [dragStart, setDragStart] = useState(null); const createEmptyRow = useCallback(() => { const newUid = uuid(); emptyRowUidRef.current = newUid; return { uid: newUid, [checkboxKey]: true, ...defaultRow }; }, [defaultRow, checkboxKey]); const rowsWithEmpty = useMemo(() => { if (!showAddRow) { return rows; } if (rows.length === 0) { return [createEmptyRow()]; } const lastRow = rows[rows.length - 1]; const keyColumn = columns.find((col) => col.isKeyField); if (keyColumn) { const lastRowKeyValue = keyColumn.getValue ? keyColumn.getValue(lastRow) : lastRow[keyColumn.key]; const isLastRowEmpty = !lastRowKeyValue || (typeof lastRowKeyValue === 'string' && lastRowKeyValue.trim() === ''); if (isLastRowEmpty) { return rows; } } if (!emptyRowUidRef.current || rows.some((r) => r.uid === emptyRowUidRef.current)) { emptyRowUidRef.current = uuid(); } return [...rows, { uid: emptyRowUidRef.current, [checkboxKey]: true, ...defaultRow }]; }, [rows, columns, defaultRow, checkboxKey, createEmptyRow, showAddRow]); const isEmptyRow = useCallback((row) => { const keyColumn = columns.find((col) => col.isKeyField); if (!keyColumn) return false; const value = keyColumn.getValue ? keyColumn.getValue(row) : row[keyColumn.key]; return !value || (typeof value === 'string' && value.trim() === ''); }, [columns]); const isLastEmptyRow = useCallback((row, index) => { if (!showAddRow) return false; return index === rowsWithEmpty.length - 1 && isEmptyRow(row); }, [rowsWithEmpty.length, isEmptyRow, showAddRow]); const handleValueChange = useCallback((rowUid, key, value) => { const rowIndex = rowsWithEmpty.findIndex((r) => r.uid === rowUid); if (rowIndex === -1) return; const currentRow = rowsWithEmpty[rowIndex]; const isLast = rowIndex === rowsWithEmpty.length - 1; const wasEmpty = isEmptyRow(currentRow); const keyColumn = columns.find((col) => col.isKeyField); const isKeyFieldChange = keyColumn && keyColumn.key === key; let updatedRows = rowsWithEmpty.map((row) => { if (row.uid === rowUid) { return { ...row, [key]: value }; } return row; }); // Only add a new empty row when the key field is filled if (showAddRow && isLast && wasEmpty && isKeyFieldChange && value && value.trim() !== '') { emptyRowUidRef.current = uuid(); updatedRows.push({ uid: emptyRowUidRef.current, [checkboxKey]: true, ...defaultRow }); } const hasAnyValue = (row) => { for (const col of columns) { const val = col.getValue ? col.getValue(row) : row[col.key]; const defaultVal = defaultRow[col.key]; if (val && val !== defaultVal && (typeof val !== 'string' || val.trim() !== '')) { return true; } } return false; }; const result = updatedRows.filter((row, i) => { if (showAddRow && i === updatedRows.length - 1) { return hasAnyValue(row); } return true; }); onChange(result); }, [rowsWithEmpty, columns, onChange, checkboxKey, defaultRow, isEmptyRow, showAddRow]); const handleCheckboxChange = useCallback((rowUid, checked) => { handleValueChange(rowUid, checkboxKey, checked); }, [handleValueChange, checkboxKey]); const handleRemoveRow = useCallback((rowUid) => { const filteredRows = rows.filter((row) => row.uid !== rowUid); onChange(filteredRows); }, [rows, onChange]); const getColumnWidth = useCallback((column) => { if (column.width) return column.width; return 'auto'; }, []); const handleDragStart = useCallback((e, index) => { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', index); setDragStart(index); }, []); const handleDragOver = useCallback((e, index) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setHoveredRow(index); }, []); const handleDrop = useCallback((e, toIndex) => { e.preventDefault(); const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10); if (fromIndex !== toIndex && onReorder) { const reorderableRows = showAddRow ? rowsWithEmpty.slice(0, -1) : rowsWithEmpty; const updatedOrder = [...reorderableRows]; const [movedRow] = updatedOrder.splice(fromIndex, 1); if (!movedRow) { setDragStart(null); setHoveredRow(null); return; } updatedOrder.splice(toIndex, 0, movedRow); onReorder({ updateReorderedItem: updatedOrder.map((row) => row.uid) }); } setDragStart(null); setHoveredRow(null); }, [onReorder, rowsWithEmpty, showAddRow]); const handleDragEnd = useCallback(() => { setDragStart(null); setHoveredRow(null); }, []); const renderCell = useCallback((column, row, rowIndex) => { const isEmpty = isLastEmptyRow(row, rowIndex); const value = column.getValue ? column.getValue(row) : row[column.key]; const error = getRowError?.(row, rowIndex, column.key); const errorIcon = error && !isEmpty ? ( ) : null; if (column.render) { return (
{column.render({ row, value, rowIndex, isLastEmptyRow: isEmpty, onChange: (newValue) => handleValueChange(row.uid, column.key, newValue) })} {errorIcon}
); } return (
handleValueChange(row.uid, column.key, e.target.value)} /> {errorIcon}
); }, [isLastEmptyRow, getRowError, handleValueChange]); const reorderableRowCount = showAddRow ? rowsWithEmpty.length - 1 : rowsWithEmpty.length; return (
{showCheckbox && ( )} {columns.map((column) => ( ))} {showDelete && ( )} {rowsWithEmpty.map((row, rowIndex) => { const isEmpty = isLastEmptyRow(row, rowIndex); const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount; return ( handleDragStart(e, rowIndex) : undefined} onDragOver={canDrag ? (e) => handleDragOver(e, rowIndex) : undefined} onDrop={canDrag ? (e) => handleDrop(e, rowIndex) : undefined} onDragEnd={canDrag ? handleDragEnd : undefined} onMouseEnter={() => setHoveredRow(rowIndex)} onMouseLeave={() => setHoveredRow(null)} > {showCheckbox && ( )} {columns.map((column) => ( ))} {showDelete && ( )} ); })}
{checkboxLabel} {column.name}
{reorderable && canDrag && (
{hoveredRow === rowIndex && ( <> )}
)} {!isEmpty && ( handleCheckboxChange(row.uid, e.target.checked)} /> )}
{renderCell(column, row, rowIndex)} {!isEmpty && ( )}
); }; export default EditableTable;