diff --git a/frontend/src/components/MainContent.tsx b/frontend/src/components/MainContent.tsx index d0a0fb2..b836ee3 100644 --- a/frontend/src/components/MainContent.tsx +++ b/frontend/src/components/MainContent.tsx @@ -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(' '); }} /> )}