({ workspaceId, tableId }: RowMutationContext)
| 1291 | * source of truth — the invalidation in `onSettled` reconciles any drift. |
| 1292 | */ |
| 1293 | export function useCancelTableRuns({ workspaceId, tableId }: RowMutationContext) { |
| 1294 | const queryClient = useQueryClient() |
| 1295 | |
| 1296 | return useMutation({ |
| 1297 | mutationFn: async ({ scope, rowId, filter, excludeRowIds }: CancelRunsParams) => { |
| 1298 | return requestJson(cancelTableRunsContract, { |
| 1299 | params: { tableId }, |
| 1300 | body: { workspaceId, scope, rowId, filter, excludeRowIds }, |
| 1301 | }) |
| 1302 | }, |
| 1303 | onMutate: async ({ scope, rowId, filter, sort, excludeRowIds }) => { |
| 1304 | const excludedRowIds = |
| 1305 | excludeRowIds && excludeRowIds.length > 0 ? new Set(excludeRowIds) : null |
| 1306 | // A filtered stop only cancels matching rows server-side — flipping every cached view |
| 1307 | // would show rows outside the filter as cancelled until refetch. Scope the optimistic |
| 1308 | // flip to the active filtered view; onSettled's invalidation reconciles the rest. |
| 1309 | const onlyKey = filter |
| 1310 | ? tableKeys.infiniteRows( |
| 1311 | tableId, |
| 1312 | tableRowsParamsKey({ |
| 1313 | pageSize: TABLE_LIMITS.MAX_QUERY_LIMIT, |
| 1314 | filter, |
| 1315 | sort: sort ?? null, |
| 1316 | }) |
| 1317 | ) |
| 1318 | : undefined |
| 1319 | const snapshots = await snapshotAndMutateRows( |
| 1320 | queryClient, |
| 1321 | tableId, |
| 1322 | (r) => { |
| 1323 | if (scope === 'row' && r.id !== rowId) return null |
| 1324 | if (excludedRowIds?.has(r.id)) return null |
| 1325 | const executions = (r.executions ?? {}) as RowExecutions |
| 1326 | let rowTouched = false |
| 1327 | const nextExecutions: RowExecutions = { ...executions } |
| 1328 | for (const gid in executions) { |
| 1329 | const exec = executions[gid] |
| 1330 | if (!isExecInFlight(exec)) continue |
| 1331 | if (exec.executionId == null) { |
| 1332 | // Optimistic-only or dispatcher-pre-stamp pending — server has not |
| 1333 | // claimed the cell yet, so no SSE will arrive to reconcile a |
| 1334 | // `cancelled` stamp. Strip the entry instead and let the renderer |
| 1335 | // fall through to the cell's prior state (value / empty / etc.). |
| 1336 | delete nextExecutions[gid] |
| 1337 | rowTouched = true |
| 1338 | continue |
| 1339 | } |
| 1340 | nextExecutions[gid] = { |
| 1341 | status: 'cancelled', |
| 1342 | executionId: exec.executionId, |
| 1343 | jobId: null, |
| 1344 | workflowId: exec.workflowId, |
| 1345 | error: 'Cancelled', |
| 1346 | ...(exec.blockErrors ? { blockErrors: exec.blockErrors } : {}), |
| 1347 | } |
| 1348 | rowTouched = true |
| 1349 | } |
| 1350 | return rowTouched ? { ...r, executions: nextExecutions } : null |
no test coverage detected