feat: implement interactive data grid with batch editing, fill handle, and clipboard support in MainContent component

This commit is contained in:
Ümit Tunç
2026-04-24 22:39:58 +03:00
parent da6a4249dd
commit 22ff72d39c
+146 -34
View File
@@ -51,8 +51,11 @@ const MainContent: React.FC = () => {
const [isSaving, setIsSaving] = useState(false);
const [cellFocus, setCellFocus] = useState<{ id: string | number, field: string } | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [dragType, setDragType] = useState<'fill' | 'select' | null>(null);
const [dragStart, setDragStart] = useState<{ id: string | number, field: string } | null>(null);
const [dragCurrent, setDragCurrent] = useState<{ id: string | number, field: string } | null>(null);
const [selectionStart, setSelectionStart] = useState<{ id: string | number, field: string } | null>(null);
const [selectionEnd, setSelectionEnd] = useState<{ id: string | number, field: string } | null>(null);
const { columns, loading: loadingSchema, primaryKey } = useTableSchema(activeTable, activeDatabase);
const { rows, rowCount, loading: loadingData, refetch } = useTableData(activeTable, activeDatabase, paginationModel);
@@ -161,57 +164,128 @@ const MainContent: React.FC = () => {
event.preventDefault();
}
}, [cellFocus, rows, pendingChanges, primaryKey, activeTable]);
// Delete key to clear selection or focused cell
if (event.key === 'Delete') {
const updatedChanges = { ...pendingChanges };
let changed = false;
if (selectionStart && selectionEnd) {
const startRowIdx = rows.findIndex(r => r[primaryKey] === selectionStart.id);
const endRowIdx = rows.findIndex(r => r[primaryKey] === selectionEnd.id);
const startColIdx = columns.findIndex(c => c.field === selectionStart.field);
const endColIdx = columns.findIndex(c => c.field === selectionEnd.field);
if (startRowIdx !== -1 && endRowIdx !== -1 && startColIdx !== -1 && endColIdx !== -1) {
const rStart = Math.min(startRowIdx, endRowIdx);
const rEnd = Math.max(startRowIdx, endRowIdx);
const cStart = Math.min(startColIdx, endColIdx);
const cEnd = Math.max(startColIdx, endColIdx);
for (let r = rStart; r <= rEnd; r++) {
const rowId = rows[r][primaryKey];
for (let c = cStart; c <= cEnd; c++) {
const field = columns[c].field;
if (columns[c].editable !== false && field !== primaryKey) {
updatedChanges[rowId] = {
...(updatedChanges[rowId] || {}),
[primaryKey]: rowId,
[field]: ''
};
changed = true;
}
}
}
}
} else if (cellFocus) {
const col = columns.find(c => c.field === cellFocus.field);
if (col && col.editable !== false && col.field !== primaryKey) {
updatedChanges[cellFocus.id] = {
...(updatedChanges[cellFocus.id] || {}),
[primaryKey]: cellFocus.id,
[cellFocus.field]: ''
};
changed = true;
}
}
if (changed) {
setPendingChanges(updatedChanges);
}
}
}, [cellFocus, selectionStart, selectionEnd, rows, columns, pendingChanges, primaryKey, activeTable]);
const handleCellMouseDown = useCallback((params: any, event: React.MouseEvent) => {
// Only start drag if clicking near the bottom-right corner (simulating fill handle)
const rect = event.currentTarget.getBoundingClientRect();
const cellElement = (event.target as HTMLElement).closest('.MuiDataGrid-cell');
if (!cellElement) return;
const rect = cellElement.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// Check if click is in the bottom-right corner (approx 10x10px)
if (x > rect.width - 12 && y > rect.height - 12) {
// Check if click is in the bottom-right corner (approx 12x12px fill handle)
if (x > rect.width - 15 && y > rect.height - 15) {
setIsDragging(true);
setDragType('fill');
setDragStart({ id: params.id, field: params.field });
setDragCurrent({ id: params.id, field: params.field });
event.stopPropagation();
event.preventDefault();
} else {
// Start range selection
setIsDragging(true);
setDragType('select');
setSelectionStart({ id: params.id, field: params.field });
setSelectionEnd({ id: params.id, field: params.field });
// Reset dragCurrent to avoid visual confusion
setDragCurrent(null);
}
}, []);
const handleCellMouseEnter = useCallback((params: any) => {
if (isDragging) {
setDragCurrent({ id: params.id, field: params.field });
}
}, [isDragging]);
const handleMouseUp = useCallback(() => {
if (isDragging && dragStart && dragCurrent && dragStart.field === dragCurrent.field) {
const startIndex = rows.findIndex(r => r[primaryKey] === dragStart.id);
const endIndex = rows.findIndex(r => r[primaryKey] === dragCurrent.id);
if (startIndex !== -1 && endIndex !== -1) {
const start = Math.min(startIndex, endIndex);
const end = Math.max(startIndex, endIndex);
const valueToFill = (pendingChanges[dragStart.id]?.[dragStart.field]) ?? rows[startIndex][dragStart.field];
const updatedChanges = { ...pendingChanges };
for (let i = start; i <= end; i++) {
const targetRowId = rows[i][primaryKey];
updatedChanges[targetRowId] = {
...(updatedChanges[targetRowId] || {}),
[primaryKey]: targetRowId,
[dragStart.field]: valueToFill
};
}
setPendingChanges(updatedChanges);
if (dragType === 'fill') {
setDragCurrent({ id: params.id, field: params.field });
} else if (dragType === 'select') {
setSelectionEnd({ id: params.id, field: params.field });
}
}
}, [isDragging, dragType]);
const handleMouseUp = useCallback(() => {
if (isDragging) {
if (dragType === 'fill' && dragStart && dragCurrent) {
const startIndex = rows.findIndex(r => r[primaryKey] === dragStart.id);
const endIndex = rows.findIndex(r => r[primaryKey] === dragCurrent.id);
if (startIndex !== -1 && endIndex !== -1) {
const start = Math.min(startIndex, endIndex);
const end = Math.max(startIndex, endIndex);
const valueToFill = (pendingChanges[dragStart.id]?.[dragStart.field]) ?? rows[startIndex][dragStart.field];
const updatedChanges = { ...pendingChanges };
for (let i = start; i <= end; i++) {
const targetRowId = rows[i][primaryKey];
const targetCol = columns.find(c => c.field === dragStart.field);
if (targetCol && targetCol.editable !== false && targetCol.field !== primaryKey) {
updatedChanges[targetRowId] = {
...(updatedChanges[targetRowId] || {}),
[primaryKey]: targetRowId,
[dragStart.field]: valueToFill
};
}
}
setPendingChanges(updatedChanges);
}
}
// Selection stays until another click or clear
}
setIsDragging(false);
setDragType(null);
setDragStart(null);
setDragCurrent(null);
}, [isDragging, dragStart, dragCurrent, rows, primaryKey, pendingChanges]);
}, [isDragging, dragType, dragStart, dragCurrent, rows, columns, primaryKey, pendingChanges]);
useEffect(() => {
window.addEventListener('mouseup', handleMouseUp);
@@ -225,6 +299,11 @@ const MainContent: React.FC = () => {
const unsubs = [
api.subscribeEvent('cellFocusIn', (params) => {
setCellFocus({ id: params.id, field: params.field });
// Single click should reset selection unless dragging
if (!isDragging) {
setSelectionStart(null);
setSelectionEnd(null);
}
}),
api.subscribeEvent('cellMouseDown', handleCellMouseDown),
api.subscribeEvent('cellMouseOver', handleCellMouseEnter)
@@ -236,6 +315,8 @@ const MainContent: React.FC = () => {
useEffect(() => {
setPaginationModel({ page: 0, pageSize: 100 });
setPendingChanges({});
setSelectionStart(null);
setSelectionEnd(null);
}, [activeTable]);
const handleCloseNotification = () => setNotification({ ...notification, open: false });
@@ -426,19 +507,50 @@ const MainContent: React.FC = () => {
}
},
'& .MuiDataGrid-cell--dragging': {
bgcolor: 'rgba(0, 97, 255, 0.15) !important',
border: '1px dashed #0061ff !important',
},
'& .MuiDataGrid-cell--selected': {
bgcolor: 'rgba(0, 97, 255, 0.08) !important',
border: '1px double rgba(0, 97, 255, 0.3) !important',
}
}}
getCellClassName={(params) => {
if (isDragging && dragStart && dragCurrent && params.field === dragStart.field) {
const classNames: string[] = [];
// Filling preview
if (isDragging && dragType === 'fill' && dragStart && dragCurrent && params.field === dragStart.field) {
const startIndex = rows.findIndex(r => r[primaryKey] === dragStart.id);
const endIndex = rows.findIndex(r => r[primaryKey] === dragCurrent.id);
const currentIndex = rows.findIndex(r => r[primaryKey] === params.id);
if (currentIndex >= Math.min(startIndex, endIndex) && currentIndex <= Math.max(startIndex, endIndex)) {
return 'MuiDataGrid-cell--dragging';
classNames.push('MuiDataGrid-cell--dragging');
}
}
return '';
// Selection range
if (selectionStart && selectionEnd) {
const startRowIdx = rows.findIndex(r => r[primaryKey] === selectionStart.id);
const endRowIdx = rows.findIndex(r => r[primaryKey] === selectionEnd.id);
const startColIdx = columns.findIndex(c => c.field === selectionStart.field);
const endColIdx = columns.findIndex(c => c.field === selectionEnd.field);
const currentRowIdx = rows.findIndex(r => r[primaryKey] === params.id);
const currentColIdx = columns.findIndex(c => c.field === params.field);
if (currentRowIdx !== -1 && endRowIdx !== -1 && startColIdx !== -1 && endColIdx !== -1) {
if (
currentRowIdx >= Math.min(startRowIdx, endRowIdx) &&
currentRowIdx <= Math.max(startRowIdx, endRowIdx) &&
currentColIdx >= Math.min(startColIdx, endColIdx) &&
currentColIdx <= Math.max(startColIdx, endColIdx)
) {
classNames.push('MuiDataGrid-cell--selected');
}
}
}
return classNames.join(' ');
}}
/>
)}