Skip to content

Commit 1576c41

Browse files
fix(tables): capture the layout owner when a schema action is dispatched
Insert-column and the delete chain persist layout from mutation callbacks through updateMetadataRef, which always targets the current sink — so a view switch mid-flight wrote the origin view's order/widths/pins into the destination. Undo got this guard already; the live paths never did. Both now capture viewLayoutKey at dispatch and compare at the callback. On a mismatch the layout work is skipped: the destination re-seeded its own layout, the new column lands there via the append effect when the refetch arrives, and the deleted column's dangling keys are pruned on read. pushUndo also takes the captured owner as an override — stamped at callback time it would record the destination, letting a later undo apply the origin's layout to it.
1 parent 02717f7 commit 1576c41

2 files changed

Lines changed: 61 additions & 34 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,12 @@ export function TableGrid({
445445
const [pinnedColumns, setPinnedColumns] = useState<string[]>([])
446446
const pinnedColumnsRef = useRef(pinnedColumns)
447447
pinnedColumnsRef.current = pinnedColumns
448+
/** Current layout owner (view id, or null for All) for capture-at-dispatch
449+
* guards: a mutation callback persisting layout must compare the owner it was
450+
* dispatched under against this, or a mid-flight view switch writes the
451+
* origin's layout into the destination — same rule as undo's entryOwnsLayout. */
452+
const viewLayoutKeyRef = useRef(viewLayoutKey)
453+
viewLayoutKeyRef.current = viewLayoutKey
448454
const metadataSeededRef = useRef(false)
449455
/** Which layout source the grid last seeded from, so a view switch re-seeds. */
450456
const seededLayoutKeyRef = useRef<string | null>(null)
@@ -3239,18 +3245,20 @@ export function TableGrid({
32393245
const index = schemaColumnsRef.current.findIndex((c) => getColumnId(c) === columnId)
32403246
if (index === -1) return
32413247
const name = generateColumnName()
3248+
const owner = viewLayoutKeyRef.current
32423249
addColumnMutation.mutate(
32433250
{ name, type: 'string', position: index },
32443251
{
32453252
onSuccess: (result) => {
32463253
const newId = result.data.columns.find((c) => c.name === name)?.id ?? name
3247-
pushUndoRef.current({
3248-
type: 'create-column',
3249-
columnName: name,
3250-
columnId: newId,
3251-
position: index,
3252-
})
3253-
insertColumnInOrder(columnId, newId, 'left')
3254+
pushUndoRef.current(
3255+
{ type: 'create-column', columnName: name, columnId: newId, position: index },
3256+
owner
3257+
)
3258+
// Skipped after a mid-flight view switch: the destination re-seeded
3259+
// its own order, and the append effect places the new column there
3260+
// when the schema refetch lands.
3261+
if (owner === viewLayoutKeyRef.current) insertColumnInOrder(columnId, newId, 'left')
32543262
},
32553263
}
32563264
)
@@ -3264,18 +3272,17 @@ export function TableGrid({
32643272
if (index === -1) return
32653273
const name = generateColumnName()
32663274
const position = index + 1
3275+
const owner = viewLayoutKeyRef.current
32673276
addColumnMutation.mutate(
32683277
{ name, type: 'string', position },
32693278
{
32703279
onSuccess: (result) => {
32713280
const newId = result.data.columns.find((c) => c.name === name)?.id ?? name
3272-
pushUndoRef.current({
3273-
type: 'create-column',
3274-
columnName: name,
3275-
columnId: newId,
3276-
position,
3277-
})
3278-
insertColumnInOrder(columnId, newId, 'right')
3281+
pushUndoRef.current(
3282+
{ type: 'create-column', columnName: name, columnId: newId, position },
3283+
owner
3284+
)
3285+
if (owner === viewLayoutKeyRef.current) insertColumnInOrder(columnId, newId, 'right')
32793286
},
32803287
}
32813288
)
@@ -3421,6 +3428,9 @@ export function TableGrid({
34213428
originalPositions.set(name, { position: def ? cols.indexOf(def) : cols.length, def })
34223429
}
34233430
const deletedOriginalPositions: number[] = []
3431+
// Layout owner when the chain was dispatched — every onDeleted below fires
3432+
// from a mutation callback, so it must not trust the sink current by then.
3433+
const owner = viewLayoutKeyRef.current
34243434

34253435
const deleteNext = (index: number) => {
34263436
if (index >= columnsToDelete.length) return
@@ -3438,24 +3448,36 @@ export function TableGrid({
34383448

34393449
const onDeleted = () => {
34403450
deletedOriginalPositions.push(entry.position)
3441-
pushUndoRef.current({
3442-
type: 'delete-column',
3443-
// `columnToDelete` is the stable id; record the display name for re-create.
3444-
columnName: entry.def?.name ?? columnToDelete,
3445-
columnId: columnToDelete,
3446-
columnType: entry.def?.type ?? 'string',
3447-
columnPosition: adjustedPosition >= 0 ? adjustedPosition : cols.length,
3448-
columnUnique: entry.def?.unique ?? false,
3449-
columnRequired: entry.def?.required ?? false,
3450-
// Without these a deleted select column can't be re-created — it is
3451-
// invalid with no options, and the saved cell data is option ids.
3452-
...(entry.def?.options ? { columnOptions: entry.def.options } : {}),
3453-
...(entry.def?.multiple ? { columnMultiple: true } : {}),
3454-
cellData,
3455-
previousOrder: orderSnapshot,
3456-
previousWidth,
3457-
previousPinnedColumns: pinnedSnapshot,
3458-
})
3451+
pushUndoRef.current(
3452+
{
3453+
type: 'delete-column',
3454+
// `columnToDelete` is the stable id; record the display name for re-create.
3455+
columnName: entry.def?.name ?? columnToDelete,
3456+
columnId: columnToDelete,
3457+
columnType: entry.def?.type ?? 'string',
3458+
columnPosition: adjustedPosition >= 0 ? adjustedPosition : cols.length,
3459+
columnUnique: entry.def?.unique ?? false,
3460+
columnRequired: entry.def?.required ?? false,
3461+
// Without these a deleted select column can't be re-created — it is
3462+
// invalid with no options, and the saved cell data is option ids.
3463+
...(entry.def?.options ? { columnOptions: entry.def.options } : {}),
3464+
...(entry.def?.multiple ? { columnMultiple: true } : {}),
3465+
cellData,
3466+
previousOrder: orderSnapshot,
3467+
previousWidth,
3468+
previousPinnedColumns: pinnedSnapshot,
3469+
},
3470+
owner
3471+
)
3472+
3473+
// The cleanup below edits the ORIGIN view's layout. After a mid-flight
3474+
// switch the grid has re-seeded to the destination; dangling keys for
3475+
// the deleted column there are pruned on read, so skip rather than push
3476+
// origin snapshots into the destination's state or its stored config.
3477+
if (owner !== viewLayoutKeyRef.current) {
3478+
deleteNext(index + 1)
3479+
return
3480+
}
34593481

34603482
const { [columnToDelete]: _removedWidth, ...cleanedWidths } = columnWidthsRef.current
34613483
setColumnWidths(cleanedWidths)

apps/sim/hooks/use-table-undo.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,13 @@ export function useTableUndo({
182182
}, [pruneLayoutActions, tableId, activeViewId])
183183

184184
const pushUndo = useCallback(
185-
(action: TableUndoAction) => {
186-
push(tableId, action, activeViewIdRef.current)
185+
/**
186+
* `owner` overrides the recorded view for actions pushed from a mutation
187+
* callback — by then the active view may have changed, and stamping the
188+
* destination would let a later undo apply the origin's layout to it.
189+
*/
190+
(action: TableUndoAction, owner?: string | null) => {
191+
push(tableId, action, owner === undefined ? activeViewIdRef.current : owner)
187192
},
188193
[push, tableId]
189194
)

0 commit comments

Comments
 (0)