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 ? (
| {checkboxLabel} | )} {columns.map((column) => ({column.name} | ))} {showDelete && ()} |
|
{reorderable && canDrag && (
{hoveredRow === rowIndex && (
<>
)}
{!isEmpty && (
handleCheckboxChange(row.uid, e.target.checked)}
/>
)}
|
)}
{columns.map((column) => (
{renderCell(column, row, rowIndex)} | ))} {showDelete && ({!isEmpty && ( )} | )}