Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -377,15 +377,17 @@ export function Table({
/** Select-all Stop — filter-scoped when a filter is active; deselected rows keep running. */
const onStopAllRows = useCallback(
(filter?: Filter, excludeRowIds?: string[]) => {
cancelRunsMutate({ scope: 'all', filter, excludeRowIds })
// `sort` scopes the optimistic flip to the active view's cache (filtered stops
// only cancel matching rows server-side).
cancelRunsMutate({ scope: 'all', filter, sort: queryOptions.sort, excludeRowIds })
captureEvent(posthogRef.current, 'table_workflow_stopped', {
table_id: tableId,
workspace_id: workspaceId,
scope: 'all',
row_count: null,
})
},
[cancelRunsMutate, tableId, workspaceId]
[cancelRunsMutate, tableId, workspaceId, queryOptions.sort]
)

const onSelectionChange = (next: SelectionSnapshot) => {
Expand Down
95 changes: 61 additions & 34 deletions apps/sim/hooks/queries/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,9 @@ interface CancelRunsParams {
rowId?: string
/** Scope-`all` only: cancel just the cells on rows matching this filter (filtered select-all Stop). */
filter?: Filter
/** Active sort — with `filter` it identifies the exact rows query whose cells the optimistic
* cancel may flip (other cached views contain rows the server won't touch). */
sort?: Sort | null
/** Scope-`all` only: deselected rows whose cells keep running. */
excludeRowIds?: string[]
}
Expand All @@ -1274,39 +1277,57 @@ export function useCancelTableRuns({ workspaceId, tableId }: RowMutationContext)
body: { workspaceId, scope, rowId, filter, excludeRowIds },
})
},
onMutate: async ({ scope, rowId, excludeRowIds }) => {
onMutate: async ({ scope, rowId, filter, sort, excludeRowIds }) => {
const excludedRowIds =
excludeRowIds && excludeRowIds.length > 0 ? new Set(excludeRowIds) : null
const snapshots = await snapshotAndMutateRows(queryClient, tableId, (r) => {
if (scope === 'row' && r.id !== rowId) return null
if (excludedRowIds?.has(r.id)) return null
const executions = (r.executions ?? {}) as RowExecutions
let rowTouched = false
const nextExecutions: RowExecutions = { ...executions }
for (const gid in executions) {
const exec = executions[gid]
if (!isExecInFlight(exec)) continue
if (exec.executionId == null) {
// Optimistic-only or dispatcher-pre-stamp pending — server has not
// claimed the cell yet, so no SSE will arrive to reconcile a
// `cancelled` stamp. Strip the entry instead and let the renderer
// fall through to the cell's prior state (value / empty / etc.).
delete nextExecutions[gid]
// A filtered stop only cancels matching rows server-side — flipping every cached view
// would show rows outside the filter as cancelled until refetch. Scope the optimistic
// flip to the active filtered view; onSettled's invalidation reconciles the rest.
const onlyKey = filter
? tableKeys.infiniteRows(
tableId,
tableRowsParamsKey({
pageSize: TABLE_LIMITS.MAX_QUERY_LIMIT,
filter,
sort: sort ?? null,
})
)
: undefined
const snapshots = await snapshotAndMutateRows(
queryClient,
tableId,
(r) => {
if (scope === 'row' && r.id !== rowId) return null
if (excludedRowIds?.has(r.id)) return null
const executions = (r.executions ?? {}) as RowExecutions
let rowTouched = false
const nextExecutions: RowExecutions = { ...executions }
for (const gid in executions) {
const exec = executions[gid]
if (!isExecInFlight(exec)) continue
if (exec.executionId == null) {
// Optimistic-only or dispatcher-pre-stamp pending — server has not
// claimed the cell yet, so no SSE will arrive to reconcile a
// `cancelled` stamp. Strip the entry instead and let the renderer
// fall through to the cell's prior state (value / empty / etc.).
delete nextExecutions[gid]
rowTouched = true
continue
}
nextExecutions[gid] = {
status: 'cancelled',
executionId: exec.executionId,
jobId: null,
workflowId: exec.workflowId,
error: 'Cancelled',
...(exec.blockErrors ? { blockErrors: exec.blockErrors } : {}),
}
rowTouched = true
continue
}
nextExecutions[gid] = {
status: 'cancelled',
executionId: exec.executionId,
jobId: null,
workflowId: exec.workflowId,
error: 'Cancelled',
...(exec.blockErrors ? { blockErrors: exec.blockErrors } : {}),
}
rowTouched = true
}
return rowTouched ? { ...r, executions: nextExecutions } : null
})
return rowTouched ? { ...r, executions: nextExecutions } : null
},
{ onlyKey }
)
return { snapshots }
},
onError: (_err, _variables, context) => {
Expand Down Expand Up @@ -1822,14 +1843,20 @@ export async function snapshotAndMutateRows(
queryClient: ReturnType<typeof useQueryClient>,
tableId: string,
transform: (row: TableRow) => TableRow | null,
options?: { cancelInFlight?: boolean }
options?: {
cancelInFlight?: boolean
/** Restrict the walk to one exact cached query (e.g. the active filtered
* view) when the mutation's server effect doesn't cover other views. */
onlyKey?: readonly unknown[]
}
): Promise<RowsCacheSnapshots> {
const scope = options?.onlyKey
? ({ queryKey: options.onlyKey, exact: true } as const)
: ({ queryKey: tableKeys.rowsRoot(tableId) } as const)
if (options?.cancelInFlight !== false) {
await queryClient.cancelQueries({ queryKey: tableKeys.rowsRoot(tableId) })
await queryClient.cancelQueries(scope)
}
const matching = queryClient.getQueriesData<RowsCacheEntry>({
queryKey: tableKeys.rowsRoot(tableId),
})
const matching = queryClient.getQueriesData<RowsCacheEntry>(scope)
const snapshots: RowsCacheSnapshots = []
for (const [key, data] of matching) {
if (!data) continue
Expand Down
Loading