Skip to content

Commit bc1a0dd

Browse files
feat(tables): workflow version selection (live/deployed) and not-found/no-output badges
1 parent c786ada commit bc1a0dd

12 files changed

Lines changed: 148 additions & 12 deletions

File tree

apps/sim/app/api/table/[tableId]/groups/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ export const PATCH = withRouteHandler(async (request: NextRequest, { params }: R
116116
...(validated.inputMappings !== undefined
117117
? { inputMappings: validated.inputMappings }
118118
: {}),
119+
...(validated.deploymentMode !== undefined
120+
? { deploymentMode: validated.deploymentMode }
121+
: {}),
119122
...(validated.type !== undefined ? { type: validated.type } : {}),
120123
...(validated.autoRun !== undefined ? { autoRun: validated.autoRun } : {}),
121124
},

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type CellRenderKind =
2222
| { kind: 'error' }
2323
| { kind: 'waiting'; labels: string[] }
2424
| { kind: 'not-found' }
25+
| { kind: 'no-output' }
2526
// Plain typed cells
2627
| { kind: 'boolean'; checked: boolean }
2728
| { kind: 'json'; text: string }
@@ -106,6 +107,9 @@ export function resolveCellRender({
106107
if (exec?.status === 'error') return { kind: 'error' }
107108
// Enrichment ran to completion but matched nothing → "Not found".
108109
if (isEnrichmentOutput && exec?.status === 'completed') return { kind: 'not-found' }
110+
// Workflow output: the group's run completed but this block produced no
111+
// value for the cell → grey "No output" (distinct from a never-run blank).
112+
if (exec?.status === 'completed') return { kind: 'no-output' }
109113
return { kind: 'empty' }
110114
}
111115

@@ -394,6 +398,15 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
394398
</Wrap>
395399
)
396400

401+
case 'no-output':
402+
return (
403+
<Wrap isEditing={isEditing}>
404+
<Badge variant='gray' dot size='sm'>
405+
No output
406+
</Badge>
407+
</Wrap>
408+
)
409+
397410
case 'empty':
398411
return null
399412

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import React, { useCallback, useEffect, useRef, useState } from 'react'
4+
import { Badge, Tooltip } from '@/components/emcn'
45
import { ChevronDown } from '@/components/emcn/icons'
56
import { cn } from '@/lib/core/utils/cn'
67
import type { WorkflowGroup } from '@/lib/table'
@@ -237,6 +238,21 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
237238
setMenuOpen(true)
238239
}
239240

241+
const notFoundBadge = sourceInfo?.blockMissing ? (
242+
<Tooltip.Root>
243+
<Tooltip.Trigger asChild>
244+
<span className='ml-1.5 shrink-0'>
245+
<Badge variant='gray' dot size='sm'>
246+
Not found
247+
</Badge>
248+
</span>
249+
</Tooltip.Trigger>
250+
<Tooltip.Content side='top'>
251+
This column's source block no longer exists in the workflow.
252+
</Tooltip.Content>
253+
</Tooltip.Root>
254+
) : null
255+
240256
return (
241257
<th
242258
className={cn(
@@ -292,6 +308,7 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
292308
<span className='ml-1.5 min-w-0 overflow-clip text-ellipsis whitespace-nowrap font-medium text-[13px] text-[var(--text-primary)]'>
293309
{column.workflowGroupId ? column.headerLabel : column.name}
294310
</span>
311+
{notFoundBadge}
295312
</div>
296313
) : (
297314
<div className='flex h-full w-full min-w-0 items-center'>
@@ -309,6 +326,7 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
309326
<span className='ml-1.5 min-w-0 overflow-clip text-ellipsis whitespace-nowrap font-medium text-[var(--text-primary)] text-small'>
310327
{column.workflowGroupId ? column.headerLabel : column.name}
311328
</span>
329+
{notFoundBadge}
312330
</button>
313331
<button
314332
type='button'

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export interface BlockIconInfo {
99
export interface ColumnSourceInfo {
1010
blockIconInfo?: BlockIconInfo
1111
blockName?: string
12+
/** Workflow loaded but the column's source block no longer exists — the
13+
* header renders a "Not found" badge. Only set for loaded states. */
14+
blockMissing?: boolean
1215
}
1316

1417
/**

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
88
import { ExternalLink, RepeatIcon, SplitIcon, X } from 'lucide-react'
99
import {
1010
Button,
11+
ButtonGroup,
12+
ButtonGroupItem,
1113
Combobox,
1214
type ComboboxOptionGroup,
1315
FieldDivider,
@@ -34,6 +36,7 @@ import type {
3436
ColumnDefinition,
3537
WorkflowGroup,
3638
WorkflowGroupDependencies,
39+
WorkflowGroupDeploymentMode,
3740
WorkflowGroupInputMapping,
3841
WorkflowGroupOutput,
3942
} from '@/lib/table'
@@ -347,6 +350,11 @@ export function WorkflowSidebarBody({
347350
const [autoRun, setAutoRun] = useState<boolean>(() =>
348351
existingGroup ? existingGroup.autoRun !== false : false
349352
)
353+
// Which workflow state per-cell runs execute against. Defaults to `'live'`
354+
// (the editable draft) for both new and pre-feature groups.
355+
const [deploymentMode, setDeploymentMode] = useState<WorkflowGroupDeploymentMode>(
356+
() => existingGroup?.deploymentMode ?? 'live'
357+
)
350358
// Deps default to none selected. With auto-run on, at least one is required
351359
// (enforced via `depsValid` below); a legacy group with empty deps will
352360
// surface the error on first open until the user picks at least one column.
@@ -709,6 +717,7 @@ export function WorkflowSidebarBody({
709717
outputs: fullOutputs,
710718
...(newOutputColumns.length > 0 ? { newOutputColumns } : {}),
711719
inputMappings: inputMappingsList,
720+
deploymentMode,
712721
autoRun,
713722
})
714723
toast.success(`Saved "${existingGroup.name ?? 'Workflow'}"`)
@@ -740,6 +749,7 @@ export function WorkflowSidebarBody({
740749
dependencies,
741750
outputs: groupOutputs,
742751
inputMappings: inputMappingsList,
752+
deploymentMode,
743753
autoRun,
744754
}
745755
await addWorkflowGroup.mutateAsync({ group, outputColumns: newOutputColumns })
@@ -1027,12 +1037,31 @@ export function WorkflowSidebarBody({
10271037
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
10281038
</div>
10291039
{showAdvanced && (
1030-
<InputMappingSection
1031-
inputFields={startBlockInputs.existing}
1032-
columnOptions={depOptions}
1033-
value={inputMappings}
1034-
onChange={setInputMappings}
1035-
/>
1040+
<>
1041+
{!isEnrichment && (
1042+
<>
1043+
<div className='flex items-center justify-between pl-0.5'>
1044+
<Label>Workflow version</Label>
1045+
<ButtonGroup
1046+
value={deploymentMode}
1047+
onValueChange={(v) =>
1048+
setDeploymentMode(v === 'deployed' ? 'deployed' : 'live')
1049+
}
1050+
>
1051+
<ButtonGroupItem value='live'>Live</ButtonGroupItem>
1052+
<ButtonGroupItem value='deployed'>Deployed</ButtonGroupItem>
1053+
</ButtonGroup>
1054+
</div>
1055+
<FieldDivider />
1056+
</>
1057+
)}
1058+
<InputMappingSection
1059+
inputFields={startBlockInputs.existing}
1060+
columnOptions={depOptions}
1061+
value={inputMappings}
1062+
onChange={setInputMappings}
1063+
/>
1064+
</>
10361065
)}
10371066
</>
10381067
)}

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,10 @@ export function useTable({ workspaceId, tableId, queryOptions }: UseTableParams)
202202
? { icon: blockConfig.icon, color: blockConfig.bgColor || '#2F55FF' }
203203
: undefined
204204
const blockName = block?.name?.trim() || undefined
205-
map.set(out.columnName, { blockIconInfo, blockName })
205+
// Flag a missing source block only once the workflow state has loaded
206+
// (truthy `blocks`), so a still-loading workflow never flashes the badge.
207+
const blockMissing = Boolean(blocks && out.blockId && !block)
208+
map.set(out.columnName, { blockIconInfo, blockName, blockMissing })
206209
}
207210
}
208211
return map

apps/sim/background/workflow-column-execution.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,25 @@ async function runWorkflowAndWriteTerminal(
162162
table: TableDefinition,
163163
group: WorkflowGroup
164164
): Promise<'completed' | 'error' | 'paused' | 'blocked'> {
165-
const { tableId, tableName, rowId, groupId, workflowId, workspaceId, executionId, dispatchId } =
166-
payload
165+
const {
166+
tableId,
167+
tableName,
168+
rowId,
169+
groupId,
170+
workflowId,
171+
workspaceId,
172+
executionId,
173+
dispatchId,
174+
deploymentMode,
175+
} = payload
167176
const requestId = `wfgrp-${executionId}`
168177

169178
return runWithRequestContext({ requestId }, async () => {
170179
const { getRowById } = await import('@/lib/table/service')
171180
const { executeWorkflow } = await import('@/lib/workflows/executor/execute-workflow')
172-
const { loadWorkflowFromNormalizedTables } = await import('@/lib/workflows/persistence/utils')
181+
const { loadWorkflowFromNormalizedTables, loadDeployedWorkflowState } = await import(
182+
'@/lib/workflows/persistence/utils'
183+
)
173184
const { writeWorkflowGroupState, markWorkflowGroupPickedUp, buildOutputsByBlockId } =
174185
await import('@/lib/table/cell-write')
175186
const { stashCellContextForResume } = await import('@/lib/table/workflow-columns')
@@ -381,7 +392,26 @@ async function runWorkflowAndWriteTerminal(
381392
return 'error'
382393
}
383394

384-
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
395+
// `deployed` groups run the workflow's latest active deployment; `live`
396+
// (default) runs the editable draft. A `deployed` group whose workflow
397+
// has never been deployed fails the cell — no silent fallback to draft.
398+
let normalizedData: Awaited<ReturnType<typeof loadWorkflowFromNormalizedTables>>
399+
if (deploymentMode === 'deployed') {
400+
try {
401+
normalizedData = await loadDeployedWorkflowState(workflowId, workspaceId)
402+
} catch {
403+
await writeState({
404+
status: 'error',
405+
executionId,
406+
jobId: null,
407+
workflowId,
408+
error: 'Workflow has no deployed version',
409+
})
410+
return 'error'
411+
}
412+
} else {
413+
normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
414+
}
385415
const startBlock = normalizedData
386416
? Object.values(normalizedData.blocks).find((b) => b?.type === 'start_trigger')
387417
: undefined
@@ -664,7 +694,10 @@ async function runWorkflowAndWriteTerminal(
664694
executionMode: 'sync',
665695
workflowTriggerType: 'table',
666696
triggerBlockId: startBlock.id,
667-
useDraftState: true,
697+
// `deployed` groups execute the latest active deployment; everything
698+
// else runs the editable draft (the table default). Matches the
699+
// state loaded above for start-block / output-block resolution.
700+
useDraftState: deploymentMode !== 'deployed',
668701
abortSignal,
669702
onBlockStart,
670703
onBlockComplete,

apps/sim/hooks/queries/tables.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1563,6 +1563,7 @@ interface UpdateWorkflowGroupVariables {
15631563
newOutputColumns?: UpdateWorkflowGroupBodyInput['newOutputColumns']
15641564
mappingUpdates?: UpdateWorkflowGroupBodyInput['mappingUpdates']
15651565
inputMappings?: UpdateWorkflowGroupBodyInput['inputMappings']
1566+
deploymentMode?: UpdateWorkflowGroupBodyInput['deploymentMode']
15661567
type?: UpdateWorkflowGroupBodyInput['type']
15671568
autoRun?: boolean
15681569
}

apps/sim/lib/api/contracts/tables.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,11 @@ const workflowGroupDependenciesSchema = z.object({
731731

732732
const workflowGroupTypeSchema = z.enum(['manual', 'enrichment'])
733733

734+
/** Which workflow state a group's per-cell runs execute against: `'live'` (the
735+
* editable draft) or `'deployed'` (the latest active deployment). Defaults to
736+
* `'live'` when omitted. */
737+
const workflowGroupDeploymentModeSchema = z.enum(['live', 'deployed'])
738+
734739
/** One workflow Start-block input field ← one table column. */
735740
const workflowGroupInputMappingSchema = z.object({
736741
inputName: z.string().min(1, 'inputName cannot be empty'),
@@ -764,6 +769,8 @@ export const addWorkflowGroupBodySchema = z.object({
764769
outputs: z.array(workflowGroupOutputSchema).min(1),
765770
/** Maps the workflow's Start-block inputs to table columns. */
766771
inputMappings: z.array(workflowGroupInputMappingSchema).optional(),
772+
/** Which workflow state per-cell runs execute against. Defaults to `'live'`. */
773+
deploymentMode: workflowGroupDeploymentModeSchema.optional(),
767774
/** When `false`, the group never auto-fires from the scheduler — it can
768775
* only be triggered manually. Defaults to `true`. Persisted on the
769776
* group; distinct from the top-level `autoRun` below which is a
@@ -808,6 +815,8 @@ export const updateWorkflowGroupBodySchema = z.object({
808815
mappingUpdates: z.array(workflowGroupMappingUpdateSchema).optional(),
809816
/** Replace the group's input mappings. Omit to leave unchanged. */
810817
inputMappings: z.array(workflowGroupInputMappingSchema).optional(),
818+
/** Change which workflow state the group runs against. Omit to leave unchanged. */
819+
deploymentMode: workflowGroupDeploymentModeSchema.optional(),
811820
/** Update the group's provenance. Omit to leave unchanged. */
812821
type: workflowGroupTypeSchema.optional(),
813822
/** Toggle the group's persisted auto-run flag. Omit to leave unchanged. */
@@ -950,6 +959,8 @@ export const runColumnContract = defineRouteContract({
950959

951960
export type AddWorkflowGroupBodyInput = z.input<typeof addWorkflowGroupBodySchema>
952961
export type UpdateWorkflowGroupBodyInput = z.input<typeof updateWorkflowGroupBodySchema>
962+
/** Which workflow state a group runs against — shared by UI / hooks. */
963+
export type WorkflowGroupDeploymentMode = z.input<typeof workflowGroupDeploymentModeSchema>
953964
export type DeleteWorkflowGroupBodyInput = z.input<typeof deleteWorkflowGroupBodySchema>
954965
export type CancelTableRunsBodyInput = z.input<typeof cancelTableRunsBodySchema>
955966
export type RunColumnBodyInput = z.input<typeof runColumnBodySchema>

apps/sim/lib/table/service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3465,6 +3465,7 @@ export async function updateWorkflowGroup(
34653465
dependencies: data.dependencies ?? group.dependencies,
34663466
outputs: newOutputs,
34673467
...(data.inputMappings !== undefined ? { inputMappings: data.inputMappings } : {}),
3468+
...(data.deploymentMode !== undefined ? { deploymentMode: data.deploymentMode } : {}),
34683469
...(data.type !== undefined ? { type: data.type } : {}),
34693470
...(data.autoRun !== undefined ? { autoRun: data.autoRun } : {}),
34703471
}

0 commit comments

Comments
 (0)