feat: implement interactive data grid with batch editing, fill handle, and clipboard support in MainContent component
This commit is contained in:
@@ -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(' ');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user