Skip to content

Commit 036db43

Browse files
fix(table): waiting state, optimistic UX, schema-mutation polling, exec cleanup
A bundle of small UX + correctness fixes around workflow-cell run state. cell-render.tsx - In-flight (queued/running/pending) now wins over the existing value, so re-runs surface immediately instead of looking like nothing happened until the worker writes the new value. - "Waiting on X" wins over a stale `cancelled` / `error` exec when deps are unmet — clearing a dep now reads as actionable instead of stuck. useRunColumn (hooks/queries/tables.ts) - onSettled now cancels in-flight polls before invalidating. Stops a poll that landed mid-mutation from clobbering the optimistic state with stale data, which produced the queued → cancelled → queued flicker. addWorkflowGroup / updateWorkflowGroup (autoRun toggle on) - Awaits scheduleRunsForTable instead of fire-and-forget. The route returned before the queued exec stamps committed, so the post-mutation refetch saw no in-flight cells and polling never started — cells looked stuck even though the server eventually stamped them. deleteColumn / deleteColumns - Strip orphaned executions[gid] keys when deleting a column orphans its parent group. Without this, stale running/queued exec records lingered on every row forever and inflated the page-header "N running" counter even on tables with no actually-running cells. UI - Action-bar leading label: "Selected N workflow cell(s)". - Context menu: Run / Refresh items mirror the action bar's Play / Refresh split, gated on the same selection-status flags so both surfaces show the actions that match the current state.
1 parent 6533967 commit 036db43

6 files changed

Lines changed: 109 additions & 17 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/context-menu/context-menu.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Eye,
1313
Pencil,
1414
PlayOutline,
15+
RefreshCw,
1516
Square,
1617
Trash,
1718
} from '@/components/emcn/icons'
@@ -29,8 +30,12 @@ interface ContextMenuProps {
2930
canViewExecution?: boolean
3031
canEditCell?: boolean
3132
selectedRowCount?: number
32-
/** Fires every workflow group on the row(s) the context menu is acting on. */
33+
/** Fires every workflow group on the row(s), skipping already-completed
34+
* cells. Mirrors the action bar's Play. */
3335
onRunWorkflows?: () => void
36+
/** Re-runs every workflow group on the row(s), including already-completed
37+
* cells. Mirrors the action bar's Refresh. */
38+
onRefreshWorkflows?: () => void
3439
/** Cancels every running/queued execution on the row(s) the context menu is acting on. */
3540
onStopWorkflows?: () => void
3641
/** Total running/queued executions across the row(s) under the context menu. Drives the Stop label and visibility. */
@@ -55,6 +60,7 @@ export function ContextMenu({
5560
canEditCell = true,
5661
selectedRowCount = 1,
5762
onRunWorkflows,
63+
onRefreshWorkflows,
5864
onStopWorkflows,
5965
runningInSelectionCount = 0,
6066
hasWorkflowColumns = false,
@@ -64,7 +70,11 @@ export function ContextMenu({
6470
}: ContextMenuProps) {
6571
const deleteLabel = selectedRowCount > 1 ? `Delete ${selectedRowCount} rows` : 'Delete row'
6672
const runLabel =
67-
selectedRowCount > 1 ? `Run workflows on ${selectedRowCount} rows` : 'Run workflows on row'
73+
selectedRowCount > 1
74+
? `Run empty or failed cells on ${selectedRowCount} rows`
75+
: 'Run empty or failed cells'
76+
const refreshLabel =
77+
selectedRowCount > 1 ? `Re-run all cells on ${selectedRowCount} rows` : 'Re-run all cells'
6878
const stopLabel =
6979
runningInSelectionCount === 1
7080
? 'Stop running workflow'
@@ -114,6 +124,12 @@ export function ContextMenu({
114124
{runLabel}
115125
</DropdownMenuItem>
116126
)}
127+
{hasWorkflowColumns && onRefreshWorkflows && (
128+
<DropdownMenuItem disabled={disableEdit} onSelect={onRefreshWorkflows}>
129+
<RefreshCw />
130+
{refreshLabel}
131+
</DropdownMenuItem>
132+
)}
117133
{hasWorkflowColumns && onStopWorkflows && runningInSelectionCount > 0 && (
118134
<DropdownMenuItem disabled={disableEdit} onSelect={onStopWorkflows}>
119135
<Square className='h-[14px] w-[14px] text-[var(--text-icon)]' />

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ export function TableActionBar({
9090
>
9191
<div className='pointer-events-auto flex items-center gap-2 rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1.5'>
9292
<span className='px-1 text-[var(--text-secondary)] text-small'>
93-
{selectedCellCount === 1 ? 'Cell' : `${selectedCellCount} cells`}
93+
{selectedCellCount === 1
94+
? 'Selected 1 workflow cell'
95+
: `Selected ${selectedCellCount} workflow cells`}
9496
</span>
9597

9698
<div className='flex items-center gap-[5px]'>

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,12 @@ export function resolveCellRender({
7575
const groupHasBlockErrors = !!(exec?.blockErrors && Object.keys(exec.blockErrors).length > 0)
7676

7777
if (blockError) return { kind: 'block-error' }
78-
if (!isNull) return { kind: 'value', text: stringifyValue(value) }
7978

79+
// In-flight wins over the existing value: when the group is being re-run,
80+
// the current value is about to be overwritten — surface the run state so
81+
// the user sees the cell is changing. Without this, a queued / running
82+
// re-run on a previously-completed cell looks like nothing happened until
83+
// the new value lands.
8084
const inFlight =
8185
exec?.status === 'running' || exec?.status === 'queued' || exec?.status === 'pending'
8286
if (inFlight && !(groupHasBlockErrors && !blockRunning)) {
@@ -87,11 +91,17 @@ export function resolveCellRender({
8791
return { kind: 'pending-upstream' }
8892
}
8993

90-
if (exec?.status === 'cancelled') return { kind: 'cancelled' }
91-
if (exec?.status === 'error') return { kind: 'error' }
94+
if (!isNull) return { kind: 'value', text: stringifyValue(value) }
95+
96+
// Waiting wins over a stale terminal state: if deps are unmet right now,
97+
// the prior `cancelled` / `error` is informational at best — the cell
98+
// can't actually run until the user fills the missing input. Surface the
99+
// actionable state instead of the stale one.
92100
if (waitingOnLabels && waitingOnLabels.length > 0) {
93101
return { kind: 'waiting', labels: waitingOnLabels }
94102
}
103+
if (exec?.status === 'cancelled') return { kind: 'cancelled' }
104+
if (exec?.status === 'error') return { kind: 'error' }
95105
return { kind: 'empty' }
96106
}
97107

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

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2581,13 +2581,16 @@ export function TableGrid({
25812581
}, [rows])
25822582

25832583
// Context-menu wrappers: act on `contextMenuRowIds`, then close the menu.
2584-
// `'incomplete'` mirrors the per-row gutter Play button — both gestures mean
2585-
// "fill in what's missing." Use the action bar's explicit Rerun button when
2586-
// re-running already-completed cells is wanted.
2584+
// Mirror the action bar's Play / Refresh split: Play fills empty/failed,
2585+
// Refresh re-runs everything (including completed cells).
25872586
const handleRunWorkflowsOnSelection = () => {
25882587
onRunRows(contextMenuRowIds, 'incomplete')
25892588
closeContextMenu()
25902589
}
2590+
const handleRefreshWorkflowsOnSelection = () => {
2591+
onRunRows(contextMenuRowIds, 'all')
2592+
closeContextMenu()
2593+
}
25912594
const handleStopWorkflowsOnSelection = () => {
25922595
onStopRows(contextMenuRowIds)
25932596
closeContextMenu()
@@ -2668,6 +2671,31 @@ export function TableGrid({
26682671
[tableWorkflowGroups]
26692672
)
26702673

2674+
// Status mix across the (row, group) cells the context menu is acting on.
2675+
// Drives Run vs Refresh visibility in the dropdown — same shape the action
2676+
// bar uses for its selectionStats so both surfaces stay in sync.
2677+
const contextMenuStats = useMemo(() => {
2678+
let hasIncompleteOrFailed = false
2679+
let hasCompleted = false
2680+
if (contextMenuRowIds.length === 0 || tableWorkflowGroupIds.length === 0) {
2681+
return { hasIncompleteOrFailed, hasCompleted }
2682+
}
2683+
const rowIdSet = new Set(contextMenuRowIds)
2684+
for (const row of rows) {
2685+
if (!rowIdSet.has(row.id)) continue
2686+
for (const groupId of tableWorkflowGroupIds) {
2687+
const status = readExecution(row, groupId)?.status
2688+
if (status === 'queued' || status === 'running' || status === 'pending') continue
2689+
if (status === 'completed') hasCompleted = true
2690+
else hasIncompleteOrFailed = true
2691+
if (hasIncompleteOrFailed && hasCompleted) {
2692+
return { hasIncompleteOrFailed, hasCompleted }
2693+
}
2694+
}
2695+
}
2696+
return { hasIncompleteOrFailed, hasCompleted }
2697+
}, [contextMenuRowIds, rows, tableWorkflowGroupIds])
2698+
26712699
// Run scope is derived from one of two selection sources:
26722700
// - checkedRows (whole-row selection) → those rows × every workflow group
26732701
// - normalizedSelection rectangle covering workflow-output columns →
@@ -3099,7 +3127,14 @@ export function TableGrid({
30993127
canEditCell={!contextMenuIsWorkflowColumn}
31003128
selectedRowCount={selectedRowCount}
31013129
onRunWorkflows={
3102-
userPermissions.canEdit && hasWorkflowColumns ? handleRunWorkflowsOnSelection : undefined
3130+
userPermissions.canEdit && hasWorkflowColumns && contextMenuStats.hasIncompleteOrFailed
3131+
? handleRunWorkflowsOnSelection
3132+
: undefined
3133+
}
3134+
onRefreshWorkflows={
3135+
userPermissions.canEdit && hasWorkflowColumns && contextMenuStats.hasCompleted
3136+
? handleRefreshWorkflowsOnSelection
3137+
: undefined
31033138
}
31043139
onStopWorkflows={
31053140
userPermissions.canEdit && hasWorkflowColumns ? handleStopWorkflowsOnSelection : undefined

apps/sim/hooks/queries/tables.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,8 +1186,13 @@ export function useRunColumn({ workspaceId, tableId }: RowMutationContext) {
11861186
onError: (_err, _variables, context) => {
11871187
if (context?.snapshots) restoreCachedWorkflowCells(queryClient, context.snapshots)
11881188
},
1189-
onSettled: () => {
1190-
queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(tableId) })
1189+
onSettled: async () => {
1190+
// Cancel any in-flight poll first — without this, a poll started during
1191+
// the mutation but lands AFTER it resolves can clobber the optimistic
1192+
// patch with stale data, producing a queued → cancelled → queued flicker
1193+
// before the authoritative refetch arrives.
1194+
await queryClient.cancelQueries({ queryKey: tableKeys.rowsRoot(tableId) })
1195+
await queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(tableId) })
11911196
},
11921197
})
11931198
}

apps/sim/lib/table/service.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2425,6 +2425,15 @@ export async function deleteColumn(
24252425
await trx.execute(
24262426
sql`UPDATE user_table_rows SET data = data - ${actualName}::text WHERE table_id = ${data.tableId} AND data ? ${actualName}::text`
24272427
)
2428+
// If deleting this column orphaned its parent group (last output gone),
2429+
// strip the group's exec entries from every row. Otherwise stale
2430+
// running/queued exec records linger forever and inflate the
2431+
// "N running" counter.
2432+
if (groupRemovedId) {
2433+
await trx.execute(
2434+
sql`UPDATE user_table_rows SET executions = executions - ${groupRemovedId}::text WHERE table_id = ${data.tableId} AND executions ? ${groupRemovedId}::text`
2435+
)
2436+
}
24282437
})
24292438

24302439
logger.info(`[${requestId}] Deleted column "${actualName}" from table ${data.tableId}`)
@@ -2527,6 +2536,13 @@ export async function deleteColumns(
25272536
sql`UPDATE user_table_rows SET data = data - ${name}::text WHERE table_id = ${data.tableId} AND data ? ${name}::text`
25282537
)
25292538
}
2539+
// Strip exec entries for any group orphaned by these column deletes —
2540+
// see `deleteColumn` for rationale.
2541+
for (const gid of removedGroupIds) {
2542+
await trx.execute(
2543+
sql`UPDATE user_table_rows SET executions = executions - ${gid}::text WHERE table_id = ${data.tableId} AND executions ? ${gid}::text`
2544+
)
2545+
}
25302546
})
25312547

25322548
logger.info(
@@ -2781,10 +2797,15 @@ export async function addWorkflowGroup(
27812797
// Schedule existing rows so already-filled deps trigger immediately. Skipped
27822798
// when the caller opted out (Mothership stages groups silently — `autoRun:
27832799
// false` — so the AI can compose multiple changes without firing rows mid-edit).
2800+
// Awaited (not `void`) so the response includes the queued exec state — the
2801+
// client's post-mutation refetch otherwise lands before the stamps commit
2802+
// and the rows query polling never starts.
27842803
if (data.autoRun !== false) {
2785-
void scheduleRunsForTable(updatedTable).catch((err) => {
2804+
try {
2805+
await scheduleRunsForTable(updatedTable)
2806+
} catch (err) {
27862807
logger.error(`[${requestId}] Failed to schedule runs after group add:`, err)
2787-
})
2808+
}
27882809
}
27892810

27902811
return updatedTable
@@ -3068,11 +3089,14 @@ export async function updateWorkflowGroup(
30683089

30693090
// autoRun toggled false → true: fire deps-satisfied rows now. Mirrors the
30703091
// post-add scheduling path so re-enabling auto-fire doesn't require manual
3071-
// run clicks for rows that are already eligible.
3092+
// run clicks for rows that are already eligible. Awaited so the post-
3093+
// mutation refetch sees the queued exec stamps.
30723094
if (group.autoRun === false && data.autoRun === true) {
3073-
void scheduleRunsForTable(updatedTable, { groupId: data.groupId }).catch((err) => {
3095+
try {
3096+
await scheduleRunsForTable(updatedTable, { groupId: data.groupId })
3097+
} catch (err) {
30743098
logger.error(`[${requestId}] Failed to schedule runs after autoRun toggled on:`, err)
3075-
})
3099+
}
30763100
}
30773101

30783102
return updatedTable

0 commit comments

Comments
 (0)