mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(tables): scope optimistic stop-cancel to the active filtered view (#4996)
A filtered select-all Stop only cancels matching rows server-side, but the optimistic update flipped in-flight cells across every cached rows query — stale unfiltered views showed workflows as cancelled until the refetch. snapshotAndMutateRows gains an onlyKey option; the cancel mutation passes the active view's exact cache key (filter + sort) when a filter is present, and onSettled's invalidation reconciles other views. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bc92a2cc49
commit
c02c7b88a3
@@ -377,7 +377,9 @@ 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,
|
||||
@@ -385,7 +387,7 @@ export function Table({
|
||||
row_count: null,
|
||||
})
|
||||
},
|
||||
[cancelRunsMutate, tableId, workspaceId]
|
||||
[cancelRunsMutate, tableId, workspaceId, queryOptions.sort]
|
||||
)
|
||||
|
||||
const onSelectionChange = (next: SelectionSnapshot) => {
|
||||
|
||||
@@ -1224,6 +1224,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[]
|
||||
}
|
||||
@@ -1246,39 +1249,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) => {
|
||||
@@ -1794,14 +1815,20 @@ export async function snapshotAndMutateRows(
|
||||
queryClient: ReturnType<typeof useQueryClient>,
|
||||
tableId: string,
|
||||
transform: (row: TableRow) => TableRow | null,
|
||||
options?: { cancelInFlight?: boolean }
|
||||
): Promise<RowsCacheSnapshots> {
|
||||
if (options?.cancelInFlight !== false) {
|
||||
await queryClient.cancelQueries({ queryKey: tableKeys.rowsRoot(tableId) })
|
||||
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[]
|
||||
}
|
||||
const matching = queryClient.getQueriesData<RowsCacheEntry>({
|
||||
queryKey: tableKeys.rowsRoot(tableId),
|
||||
})
|
||||
): 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(scope)
|
||||
}
|
||||
const matching = queryClient.getQueriesData<RowsCacheEntry>(scope)
|
||||
const snapshots: RowsCacheSnapshots = []
|
||||
for (const [key, data] of matching) {
|
||||
if (!data) continue
|
||||
|
||||
Reference in New Issue
Block a user