Skip to content

Commit a01f80c

Browse files
waleedlatif1claude
andauthored
feat(tables): column selection, keyboard shortcuts, drag reorder, and undo improvements (#4222)
* feat(tables): add column selection, missing keyboard shortcuts, and Sheets-aligned operations Click column headers to select entire columns, shift-click to extend to a column range. Delete, cut, and copy operations work on column selections with full undo/redo support. Adds Home, End, Ctrl+Home, Ctrl+End, PageUp, PageDown, Ctrl+Space, and all Shift variants. Changes Ctrl+A to select all cells instead of checkbox rows. Column header dropdown menu now opens on right-click instead of left-click. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): chevron opens dropdown, drag header to reorder columns Split column header into label area (click to select, draggable for reorder) and chevron button (click to open dropdown menu). Remove the grip handle — dragging the header itself now reorders columns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): full-column highlight during drag reorder Replace the thin 2px line drop indicator with a full-column highlight that spans the entire table height, matching Google Sheets behavior. The insertion line is still shown at the drop edge for precision. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): handle drag reorder edge cases, dim source column Suppress drop indicator when drag would result in no position change (dragging onto self or adjacent no-op positions). Dim the source column body cells during drag with a background overlay. Skip the API call when the computed order is identical to the current order. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(tables): add column reorder undo/redo, body drop targets, and escape cancel Column drag-and-drop now supports dropping anywhere in a column (not just headers), pressing Escape to cancel a drag, and full undo/redo integration for column reordering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): merge partial updates in updateRow to prevent column data loss When Mothership called updateRow directly (bypassing the PATCH API route), it passed only the changed fields — which were written as the entire row, wiping all other columns. Move the merge logic into updateRow itself so all callers get correct partial-update semantics, and remove the now-redundant pre-merge from both PATCH routes. * test(tables): add updateRow partial merge tests Covers the bug where partial updates wiped unmentioned columns — verifies that fields not in the update payload are preserved, nulling a field works, full-row updates are idempotent, and missing rows throw correctly. * feat(tables): add delete-column undo/redo, rename metadata sync, and comprehensive row ID patching - Delete column now captures column definition, cell data, order, and width for full undo/redo - Column rename undo/redo now properly syncs columnWidths and columnOrder metadata - patchRedoRowId/patchUndoRowId extended to handle all action types containing row IDs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): remove source column dimming during drag reorder Only show the insertion line at the drop position, matching Google Sheets behavior. Remove dragSourceBounds memo and isDragging prop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): preserve selection on right-click, auto-resize on double-click, fix escape during drag - Right-clicking within an existing selection now preserves it instead of resetting to a single cell, so context menu operations apply to the full range - Double-clicking a column border auto-resizes the column to fit its content - Escape during column drag now immediately clears refs before state update, preventing the dragend handler from executing the reorder Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): add aria-hidden value and aria-label for column header accessibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): tighten auto-resize padding to match Google Sheets Reduce header padding from +48px to +36px (icon + cell padding) and cell padding from +20px to +17px (cell padding + border) for a snug fit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): clean drag ghost and clear selection on drag start - Create a minimal custom drag image showing only the column name instead of the browser's default ghost that includes adjacent columns/checkboxes - Clear any existing cell/column selection when starting a column drag to prevent stale highlights from persisting during reorder Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(tables): add Shift+Space row selection and Ctrl+D fill down Shift+Space now selects the entire row (all columns) instead of toggling a checkbox, matching Google Sheets behavior. Ctrl+D copies the top cell's value down through the selected range with full undo/redo support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): show toast on incompatible column type change The server validates type compatibility and returns a clear error message (e.g. "3 row(s) have incompatible values"), but the client was silently swallowing it. Now surfaces the error via toast notification. Also moved the undo push to onSuccess so a failed type change doesn't pollute the undo stack. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): scroll-into-view for selection focus, Home/End origin, delete-column undo timing - Scroll-into-view now tracks selectionFocus (not just anchor), so Shift+Arrow extending selection off-screen properly auto-scrolls - Shift+Home/End now uses the current focus as origin (matching Shift+Arrow behavior) instead of always using anchor - Delete column undo entry is now pushed in onSuccess, preventing a corrupted undo stack if the server rejects the deletion - Dialog copy updated from "cannot be undone" to "You can undo this action" since undo/redo is supported Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve duplicate declarations from rebase against staging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix file upload * fix(tables): merge column widths on delete-column undo, try/finally for auto-resize - Delete-column undo now reads current column widths via getColumnWidths callback and merges the restored column's width into the full map, preventing other columns' widths from being wiped - Auto-resize measurement span is now wrapped in try/finally to ensure DOM cleanup if an exception occurs during measurement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: revert accidental home.tsx change from rebase conflict resolution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): clear isColumnSelection on double-click and right-click, skip scroll for column select - Clear isColumnSelection when double-clicking a cell to edit, preventing the column selection effect from fighting with the editing state - Clear isColumnSelection when right-clicking outside the current selection, preventing stale column selection from re-expanding - Skip scroll-into-view when isColumnSelection is true, preventing the viewport from jumping to the bottom row when clicking a column header Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): remove inline font override in auto-resize, guard undefined columnOrder - Remove `font:inherit` from measurement span inline style so Tailwind classes (font-medium, text-small) control font properties for accurate column width measurement - Only include columnOrder in metadata update when defined, preventing handleColumnRename from clearing a persisted column order when columnOrderRef is null Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): capture columnRequired in delete-column undo for full restoration The delete-column undo action captured columnUnique but not columnRequired, so undoing a delete on a required column would silently drop the constraint. Now captures and restores both constraints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): restore width independently of order on delete-column undo, batch fill-down - Column width restoration in delete-column undo no longer requires previousOrder to be non-null — width is restored independently - Ctrl+D fill-down now uses batchUpdateRef (single API call) instead of calling mutateRef per row in a loop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): multi-column delete, select-all cell model, cut flash, chevron alignment - Multi-select delete: detect column selection range and delete all selected columns sequentially with individual undo entries - Select all (header checkbox): use cell selection model instead of checkbox model for consistent highlighting - Cut flash: batch cell clears into single mutation to prevent stale data flashing from multiple onSettled invalidations - Chevron alignment: adjust right padding from pr-2 to pr-2.5 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): restore column width locally on delete-column undo Add onColumnWidthsChange callback to undo hook so restored column widths update local component state, not just server metadata. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): prevent Ctrl+D bookmark dialog, batch Delete/Backspace mutations - Move e.preventDefault() before early returns in Ctrl+D handler so the browser bookmark dialog is always suppressed - Replace per-row mutateRef calls with single batchUpdateRef call in both Delete/Backspace handlers (checked rows and cell selection), consistent with cut and fill-down Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): adjust column positions for multi-column delete undo Capture original schema positions upfront and adjust each by the count of previously-deleted columns with lower positions, so undo restores columns at correct server-side positions in LIFO order. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): only multi-delete when clicked column is within selection Check that the right-clicked column is within the selected column range before using multi-column delete. If the click is outside the selection, delete only the clicked column. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): prevent duplicate undo entry on column drag-drop Clear dragColumnNameRef immediately in handleColumnDragEnd so the second invocation (from dragend after drop already fired) is a no-op. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): clean up width on delete-column redo, suppress click during drag - Redo path for delete-column now removes the column's width from metadata and local state, preventing stale width entries - Add didDragRef to ColumnHeaderMenu to suppress the click event that fires after a drag operation, preventing selection flash Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): remove unstable mutation object from useCallback deps deleteTableMutation is not referentially stable — only .mutateAsync() is. Including the mutation object causes unnecessary callback recreation on every mutation state change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): fix auto-resize header padding, deduplicate rename metadata logic Increase header text measurement padding from 36px to 57px to account for the chevron dropdown button (pl-0.5 + 9px icon + pr-2.5) that always occupies layout space. Prevents header text truncation on auto-resize. Deduplicate column rename metadata logic by having columnRename.onSave call handleColumnRename instead of reimplementing the same width/order transfer and metadata persist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): log error on cell data restoration failure during undo Add onError handler to the batchUpdateRowsMutation inside delete-column undo so failures are logged instead of silently swallowed. The column schema restores first, and the cell data restoration is a separate async call that the outer try/catch cannot intercept. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): address audit findings across table, undo hook, and store - Add missing bounds check in handleCopy (c >= cols.length) matching handleCut for defensive consistency - Clear lastCheckboxRowRef in Ctrl+Space and Shift+Space to prevent stale shift-click checkbox range after keyboard selection - Fix stale snapshot race in patchRedoRowId/patchUndoRowId by reading state inside the set() updater instead of via get() outside it - Add metadata cleanup to create-column undo so column width is removed from both local state and server, symmetric with delete-column redo - Remove stale width key from columnWidths on column delete instead of persisting orphaned entries - Normalize undefined vs null in handleInlineSave change detection to prevent unnecessary mutations when oldValue is undefined - Use ghost.parentNode?.removeChild instead of document.body.removeChild in drag ghost cleanup to prevent throw on component unmount Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): reset didDragRef in handleDragEnd to prevent stale flag Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 524f33c commit a01f80c

11 files changed

Lines changed: 1045 additions & 287 deletions

File tree

apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -135,32 +135,11 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
135135
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
136136
}
137137

138-
const [existingRow] = await db
139-
.select({ data: userTableRows.data })
140-
.from(userTableRows)
141-
.where(
142-
and(
143-
eq(userTableRows.id, rowId),
144-
eq(userTableRows.tableId, tableId),
145-
eq(userTableRows.workspaceId, validated.workspaceId)
146-
)
147-
)
148-
.limit(1)
149-
150-
if (!existingRow) {
151-
return NextResponse.json({ error: 'Row not found' }, { status: 404 })
152-
}
153-
154-
const mergedData = {
155-
...(existingRow.data as RowData),
156-
...(validated.data as RowData),
157-
}
158-
159138
const updatedRow = await updateRow(
160139
{
161140
tableId,
162141
rowId,
163-
data: mergedData,
142+
data: validated.data as RowData,
164143
workspaceId: validated.workspaceId,
165144
},
166145
table,

apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -137,33 +137,11 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
137137
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
138138
}
139139

140-
// Fetch existing row to merge partial update
141-
const [existingRow] = await db
142-
.select({ data: userTableRows.data })
143-
.from(userTableRows)
144-
.where(
145-
and(
146-
eq(userTableRows.id, rowId),
147-
eq(userTableRows.tableId, tableId),
148-
eq(userTableRows.workspaceId, validated.workspaceId)
149-
)
150-
)
151-
.limit(1)
152-
153-
if (!existingRow) {
154-
return NextResponse.json({ error: 'Row not found' }, { status: 404 })
155-
}
156-
157-
const mergedData = {
158-
...(existingRow.data as RowData),
159-
...(validated.data as RowData),
160-
}
161-
162140
const updatedRow = await updateRow(
163141
{
164142
tableId,
165143
rowId,
166-
data: mergedData,
144+
data: validated.data as RowData,
167145
workspaceId: validated.workspaceId,
168146
},
169147
table,

apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
158158
isLoading: isSending,
159159
})
160160
const hasFiles = files.attachedFiles.some((f) => !f.uploading && f.key)
161+
const hasUploadingFiles = files.attachedFiles.some((f) => f.uploading)
161162

162163
const contextManagement = useContextManagement({ message: value })
163164

@@ -232,7 +233,7 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
232233
setSelectedContexts: contextManagement.setSelectedContexts,
233234
})
234235

235-
const canSubmit = (value.trim().length > 0 || hasFiles) && !isSending
236+
const canSubmit = (value.trim().length > 0 || hasFiles) && !isSending && !hasUploadingFiles
236237

237238
const valueRef = useRef(value)
238239
valueRef.current = value
@@ -507,6 +508,8 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
507508

508509
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
509510
e.preventDefault()
511+
// Mirror canSubmit's uploading guard; Enter reads refs, not rendered state.
512+
if (filesRef.current.attachedFiles.some((f) => f.uploading)) return
510513
const hasSubmitPayload =
511514
valueRef.current.trim().length > 0 ||
512515
filesRef.current.attachedFiles.some((file) => !file.uploading && file.key)

0 commit comments

Comments
 (0)