mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(tables): workflow version selection (live/deployed) and not-found/no-output badges (#4889)
* feat(tables): workflow version selection (live/deployed) and not-found/no-output badges * fix(tables): draw row-selection left edge as checkbox cell border so it cannot be cut off * fix(tables): per-group version in cascade, accurate deploy error, skip not-found for deployed groups * fix(tables): render selection left edge as continuous strip overlapping row gridlines * feat(tables): not-found column icon, optional workflow inputs, mothership deploymentMode --------- Co-authored-by: waleed <walif6@gmail.com>
This commit is contained in:
@@ -116,6 +116,9 @@ export const PATCH = withRouteHandler(async (request: NextRequest, { params }: R
|
||||
...(validated.inputMappings !== undefined
|
||||
? { inputMappings: validated.inputMappings }
|
||||
: {}),
|
||||
...(validated.deploymentMode !== undefined
|
||||
? { deploymentMode: validated.deploymentMode }
|
||||
: {}),
|
||||
...(validated.type !== undefined ? { type: validated.type } : {}),
|
||||
...(validated.autoRun !== undefined ? { autoRun: validated.autoRun } : {}),
|
||||
},
|
||||
|
||||
+13
@@ -22,6 +22,7 @@ export type CellRenderKind =
|
||||
| { kind: 'error' }
|
||||
| { kind: 'waiting'; labels: string[] }
|
||||
| { kind: 'not-found' }
|
||||
| { kind: 'no-output' }
|
||||
// Plain typed cells
|
||||
| { kind: 'boolean'; checked: boolean }
|
||||
| { kind: 'json'; text: string }
|
||||
@@ -106,6 +107,9 @@ export function resolveCellRender({
|
||||
if (exec?.status === 'error') return { kind: 'error' }
|
||||
// Enrichment ran to completion but matched nothing → "Not found".
|
||||
if (isEnrichmentOutput && exec?.status === 'completed') return { kind: 'not-found' }
|
||||
// Workflow output: the group's run completed but this block produced no
|
||||
// value for the cell → grey "No output" (distinct from a never-run blank).
|
||||
if (exec?.status === 'completed') return { kind: 'no-output' }
|
||||
return { kind: 'empty' }
|
||||
}
|
||||
|
||||
@@ -394,6 +398,15 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
|
||||
</Wrap>
|
||||
)
|
||||
|
||||
case 'no-output':
|
||||
return (
|
||||
<Wrap isEditing={isEditing}>
|
||||
<Badge variant='gray' dot size='sm'>
|
||||
No output
|
||||
</Badge>
|
||||
</Wrap>
|
||||
)
|
||||
|
||||
case 'empty':
|
||||
return null
|
||||
|
||||
|
||||
+19
-1
@@ -194,10 +194,28 @@ export const DataRow = React.memo(function DataRow({
|
||||
}, [workflowGroups, row])
|
||||
const isMultiCell = sel !== null && (sel.startRow !== sel.endRow || sel.startCol !== sel.endCol)
|
||||
const isRowSelected = isRowChecked
|
||||
/**
|
||||
* Whether the selection's left edge sits at column 0 for this row. The blue
|
||||
* edge is drawn inside the sticky checkbox cell — over its gray right
|
||||
* border — rather than as the col-0 overlay's `border-l`, so the sticky
|
||||
* cell can never paint over it and the gray/blue lines never double up at
|
||||
* the column boundary. The strip overlaps the row gridlines (`-top-px` /
|
||||
* `-bottom-px`) so consecutive selected rows form one continuous line.
|
||||
*/
|
||||
const rowInRange = sel !== null && rowIndex >= sel.startRow && rowIndex <= sel.endRow
|
||||
const isLeftEdgeSelected = isRowChecked || (isMultiCell && rowInRange && sel!.startCol === 0)
|
||||
|
||||
return (
|
||||
<tr onContextMenu={(e) => onContextMenu(e, row)}>
|
||||
<td className={cn(CELL_CHECKBOX, 'cursor-pointer')}>
|
||||
{isLeftEdgeSelected && (
|
||||
<div
|
||||
className={cn(
|
||||
'-right-px -bottom-px pointer-events-none absolute w-px bg-[var(--selection)]',
|
||||
isFirstRow ? 'top-0' : '-top-px'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center',
|
||||
@@ -322,7 +340,7 @@ export const DataRow = React.memo(function DataRow({
|
||||
isFirstRow && isTopEdge && 'top-0',
|
||||
isTopEdge && 'border-t border-t-[var(--selection)]',
|
||||
isBottomEdge && 'border-b border-b-[var(--selection)]',
|
||||
isLeftEdge && 'border-l border-l-[var(--selection)]',
|
||||
isLeftEdge && colIndex !== 0 && 'border-l border-l-[var(--selection)]',
|
||||
isRightEdge && 'border-r border-r-[var(--selection)]'
|
||||
)}
|
||||
/>
|
||||
|
||||
+7
@@ -237,6 +237,10 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
|
||||
setMenuOpen(true)
|
||||
}
|
||||
|
||||
// Column whose workflow source block was deleted — the header icon swaps to
|
||||
// `WorkflowX` with an explanatory tooltip.
|
||||
const blockMissing = Boolean(sourceInfo?.blockMissing)
|
||||
|
||||
return (
|
||||
<th
|
||||
className={cn(
|
||||
@@ -268,6 +272,7 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
|
||||
type={column.type}
|
||||
isWorkflowColumn={!!column.workflowGroupId && ownGroup?.type !== 'enrichment'}
|
||||
blockIconInfo={sourceInfo?.blockIconInfo}
|
||||
blockMissing={blockMissing}
|
||||
/>
|
||||
<input
|
||||
ref={renameInputRef}
|
||||
@@ -288,6 +293,7 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
|
||||
type={column.type}
|
||||
isWorkflowColumn={!!column.workflowGroupId && ownGroup?.type !== 'enrichment'}
|
||||
blockIconInfo={sourceInfo?.blockIconInfo}
|
||||
blockMissing={blockMissing}
|
||||
/>
|
||||
<span className='ml-1.5 min-w-0 overflow-clip text-ellipsis whitespace-nowrap font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
{column.workflowGroupId ? column.headerLabel : column.name}
|
||||
@@ -305,6 +311,7 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
|
||||
type={column.type}
|
||||
isWorkflowColumn={!!column.workflowGroupId && ownGroup?.type !== 'enrichment'}
|
||||
blockIconInfo={sourceInfo?.blockIconInfo}
|
||||
blockMissing={blockMissing}
|
||||
/>
|
||||
<span className='ml-1.5 min-w-0 overflow-clip text-ellipsis whitespace-nowrap font-medium text-[var(--text-primary)] text-small'>
|
||||
{column.workflowGroupId ? column.headerLabel : column.name}
|
||||
|
||||
+27
-2
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import {
|
||||
Calendar as CalendarIcon,
|
||||
PlayOutline,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
TypeJson,
|
||||
TypeNumber,
|
||||
TypeText,
|
||||
WorkflowX,
|
||||
} from '@/components/emcn/icons'
|
||||
import type { BlockIconInfo } from '../types'
|
||||
|
||||
@@ -32,16 +34,39 @@ interface ColumnTypeIconProps {
|
||||
* ignored — icons render in the plain `text-[var(--text-icon)]` tone like
|
||||
* every other column-type icon, no per-block tint. */
|
||||
blockIconInfo?: BlockIconInfo
|
||||
/** Workflow-output column whose source block no longer exists in the
|
||||
* workflow — renders the `WorkflowX` "not found" icon with a tooltip. */
|
||||
blockMissing?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiny icon shown next to a column header. Workflow-output columns get the
|
||||
* producing block's icon (falling back to `PlayOutline`); plain columns get
|
||||
* their scalar type icon. Both render in the same `text-[var(--text-icon)]`
|
||||
* tone — no per-workflow color, no colored swatch.
|
||||
* tone — no per-workflow color, no colored swatch. A workflow column whose
|
||||
* source block was deleted renders a `WorkflowX` with an explanatory tooltip.
|
||||
*/
|
||||
export function ColumnTypeIcon({ type, isWorkflowColumn, blockIconInfo }: ColumnTypeIconProps) {
|
||||
export function ColumnTypeIcon({
|
||||
type,
|
||||
isWorkflowColumn,
|
||||
blockIconInfo,
|
||||
blockMissing,
|
||||
}: ColumnTypeIconProps) {
|
||||
if (isWorkflowColumn) {
|
||||
if (blockMissing) {
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span className='flex shrink-0 items-center'>
|
||||
<WorkflowX className='size-3 shrink-0 text-[var(--text-icon)]' />
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
This column's source block no longer exists in the workflow.
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
const Icon = blockIconInfo?.icon ?? PlayOutline
|
||||
return <Icon className='size-3 shrink-0 text-[var(--text-icon)]' />
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ export interface BlockIconInfo {
|
||||
export interface ColumnSourceInfo {
|
||||
blockIconInfo?: BlockIconInfo
|
||||
blockName?: string
|
||||
/** Workflow loaded but the column's source block no longer exists — the
|
||||
* header renders a "Not found" badge. Only set for loaded states. */
|
||||
blockMissing?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
-1
@@ -39,7 +39,6 @@ export function InputMappingSection({
|
||||
<div className='flex flex-col gap-[9.5px]'>
|
||||
<Label className='flex items-baseline gap-1.5 whitespace-nowrap pl-0.5'>
|
||||
Workflow inputs
|
||||
<span className='ml-0.5'>*</span>
|
||||
</Label>
|
||||
{namedFields.length === 0 ? (
|
||||
<p className='pl-0.5 text-[var(--text-tertiary)] text-caption'>
|
||||
|
||||
+35
-6
@@ -8,6 +8,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { ExternalLink, RepeatIcon, SplitIcon, X } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ButtonGroupItem,
|
||||
Combobox,
|
||||
type ComboboxOptionGroup,
|
||||
FieldDivider,
|
||||
@@ -34,6 +36,7 @@ import type {
|
||||
ColumnDefinition,
|
||||
WorkflowGroup,
|
||||
WorkflowGroupDependencies,
|
||||
WorkflowGroupDeploymentMode,
|
||||
WorkflowGroupInputMapping,
|
||||
WorkflowGroupOutput,
|
||||
} from '@/lib/table'
|
||||
@@ -347,6 +350,11 @@ export function WorkflowSidebarBody({
|
||||
const [autoRun, setAutoRun] = useState<boolean>(() =>
|
||||
existingGroup ? existingGroup.autoRun !== false : false
|
||||
)
|
||||
// Which workflow state per-cell runs execute against. Defaults to `'live'`
|
||||
// (the editable draft) for both new and pre-feature groups.
|
||||
const [deploymentMode, setDeploymentMode] = useState<WorkflowGroupDeploymentMode>(
|
||||
() => existingGroup?.deploymentMode ?? 'live'
|
||||
)
|
||||
// Deps default to none selected. With auto-run on, at least one is required
|
||||
// (enforced via `depsValid` below); a legacy group with empty deps will
|
||||
// surface the error on first open until the user picks at least one column.
|
||||
@@ -709,6 +717,7 @@ export function WorkflowSidebarBody({
|
||||
outputs: fullOutputs,
|
||||
...(newOutputColumns.length > 0 ? { newOutputColumns } : {}),
|
||||
inputMappings: inputMappingsList,
|
||||
deploymentMode,
|
||||
autoRun,
|
||||
})
|
||||
toast.success(`Saved "${existingGroup.name ?? 'Workflow'}"`)
|
||||
@@ -740,6 +749,7 @@ export function WorkflowSidebarBody({
|
||||
dependencies,
|
||||
outputs: groupOutputs,
|
||||
inputMappings: inputMappingsList,
|
||||
deploymentMode,
|
||||
autoRun,
|
||||
}
|
||||
await addWorkflowGroup.mutateAsync({ group, outputColumns: newOutputColumns })
|
||||
@@ -1027,12 +1037,31 @@ export function WorkflowSidebarBody({
|
||||
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
|
||||
</div>
|
||||
{showAdvanced && (
|
||||
<InputMappingSection
|
||||
inputFields={startBlockInputs.existing}
|
||||
columnOptions={depOptions}
|
||||
value={inputMappings}
|
||||
onChange={setInputMappings}
|
||||
/>
|
||||
<>
|
||||
{!isEnrichment && (
|
||||
<>
|
||||
<div className='flex items-center justify-between pl-0.5'>
|
||||
<Label>Workflow version</Label>
|
||||
<ButtonGroup
|
||||
value={deploymentMode}
|
||||
onValueChange={(v) =>
|
||||
setDeploymentMode(v === 'deployed' ? 'deployed' : 'live')
|
||||
}
|
||||
>
|
||||
<ButtonGroupItem value='live'>Live</ButtonGroupItem>
|
||||
<ButtonGroupItem value='deployed'>Deployed</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<FieldDivider />
|
||||
</>
|
||||
)}
|
||||
<InputMappingSection
|
||||
inputFields={startBlockInputs.existing}
|
||||
columnOptions={depOptions}
|
||||
value={inputMappings}
|
||||
onChange={setInputMappings}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -195,6 +195,10 @@ export function useTable({ workspaceId, tableId, queryOptions }: UseTableParams)
|
||||
if (group.type === 'enrichment') continue
|
||||
const state = workflowStates.get(group.workflowId)
|
||||
const blocks = (state as { blocks?: Record<string, FlattenOutputsBlockInput> } | null)?.blocks
|
||||
// `useWorkflowStates` only fetches the live draft, so we can only judge
|
||||
// "block missing" for live-mode groups. A deployed-mode group runs a
|
||||
// different graph we don't load client-side — don't risk a false badge.
|
||||
const isLiveMode = group.deploymentMode !== 'deployed'
|
||||
for (const out of group.outputs) {
|
||||
const block = blocks?.[out.blockId]
|
||||
const blockConfig = block?.type ? getBlock(block.type) : undefined
|
||||
@@ -202,7 +206,10 @@ export function useTable({ workspaceId, tableId, queryOptions }: UseTableParams)
|
||||
? { icon: blockConfig.icon, color: blockConfig.bgColor || '#2F55FF' }
|
||||
: undefined
|
||||
const blockName = block?.name?.trim() || undefined
|
||||
map.set(out.columnName, { blockIconInfo, blockName })
|
||||
// Flag a missing source block only once the workflow state has loaded
|
||||
// (truthy `blocks`), so a still-loading workflow never flashes the badge.
|
||||
const blockMissing = Boolean(isLiveMode && blocks && out.blockId && !block)
|
||||
map.set(out.columnName, { blockIconInfo, blockName, blockMissing })
|
||||
}
|
||||
}
|
||||
return map
|
||||
|
||||
@@ -165,12 +165,18 @@ async function runWorkflowAndWriteTerminal(
|
||||
): Promise<'completed' | 'error' | 'paused' | 'blocked'> {
|
||||
const { tableId, tableName, rowId, groupId, workflowId, workspaceId, executionId, dispatchId } =
|
||||
payload
|
||||
// Read from the live `group`, not the payload: in a cascade the payload is the
|
||||
// first group's snapshot, so a downstream group with a different version must
|
||||
// use its own setting (same reason `workflowId` is re-derived per iteration).
|
||||
const deploymentMode = group.deploymentMode
|
||||
const requestId = `wfgrp-${executionId}`
|
||||
|
||||
return runWithRequestContext({ requestId }, async () => {
|
||||
const { getRowById } = await import('@/lib/table/service')
|
||||
const { executeWorkflow } = await import('@/lib/workflows/executor/execute-workflow')
|
||||
const { loadWorkflowFromNormalizedTables } = await import('@/lib/workflows/persistence/utils')
|
||||
const { loadWorkflowFromNormalizedTables, loadDeployedWorkflowState } = await import(
|
||||
'@/lib/workflows/persistence/utils'
|
||||
)
|
||||
const { writeWorkflowGroupState, markWorkflowGroupPickedUp, buildOutputsByBlockId } =
|
||||
await import('@/lib/table/cell-write')
|
||||
const { stashCellContextForResume } = await import('@/lib/table/workflow-columns')
|
||||
@@ -382,7 +388,28 @@ async function runWorkflowAndWriteTerminal(
|
||||
return 'error'
|
||||
}
|
||||
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
|
||||
// `deployed` groups run the workflow's latest active deployment; `live`
|
||||
// (default) runs the editable draft. A `deployed` group whose workflow
|
||||
// has never been deployed fails the cell — no silent fallback to draft.
|
||||
let normalizedData: Awaited<ReturnType<typeof loadWorkflowFromNormalizedTables>>
|
||||
if (deploymentMode === 'deployed') {
|
||||
try {
|
||||
normalizedData = await loadDeployedWorkflowState(workflowId, workspaceId)
|
||||
} catch (err) {
|
||||
// Surface the real reason (missing deployment vs. transient DB/migration
|
||||
// failure) rather than always claiming the workflow isn't deployed.
|
||||
await writeState({
|
||||
status: 'error',
|
||||
executionId,
|
||||
jobId: null,
|
||||
workflowId,
|
||||
error: toError(err).message,
|
||||
})
|
||||
return 'error'
|
||||
}
|
||||
} else {
|
||||
normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
|
||||
}
|
||||
const startBlock = normalizedData
|
||||
? Object.values(normalizedData.blocks).find((b) => b?.type === 'start_trigger')
|
||||
: undefined
|
||||
@@ -665,7 +692,10 @@ async function runWorkflowAndWriteTerminal(
|
||||
executionMode: 'sync',
|
||||
workflowTriggerType: 'table',
|
||||
triggerBlockId: startBlock.id,
|
||||
useDraftState: true,
|
||||
// `deployed` groups execute the latest active deployment; everything
|
||||
// else runs the editable draft (the table default). Matches the
|
||||
// state loaded above for start-block / output-block resolution.
|
||||
useDraftState: deploymentMode !== 'deployed',
|
||||
abortSignal,
|
||||
onBlockStart,
|
||||
onBlockComplete,
|
||||
|
||||
@@ -1692,6 +1692,7 @@ interface UpdateWorkflowGroupVariables {
|
||||
newOutputColumns?: UpdateWorkflowGroupBodyInput['newOutputColumns']
|
||||
mappingUpdates?: UpdateWorkflowGroupBodyInput['mappingUpdates']
|
||||
inputMappings?: UpdateWorkflowGroupBodyInput['inputMappings']
|
||||
deploymentMode?: UpdateWorkflowGroupBodyInput['deploymentMode']
|
||||
type?: UpdateWorkflowGroupBodyInput['type']
|
||||
autoRun?: boolean
|
||||
}
|
||||
|
||||
@@ -791,6 +791,11 @@ const workflowGroupDependenciesSchema = z.object({
|
||||
|
||||
const workflowGroupTypeSchema = z.enum(['manual', 'enrichment'])
|
||||
|
||||
/** Which workflow state a group's per-cell runs execute against: `'live'` (the
|
||||
* editable draft) or `'deployed'` (the latest active deployment). Defaults to
|
||||
* `'live'` when omitted. */
|
||||
const workflowGroupDeploymentModeSchema = z.enum(['live', 'deployed'])
|
||||
|
||||
/** One workflow Start-block input field ← one table column. */
|
||||
const workflowGroupInputMappingSchema = z.object({
|
||||
inputName: z.string().min(1, 'inputName cannot be empty'),
|
||||
@@ -824,6 +829,8 @@ export const addWorkflowGroupBodySchema = z.object({
|
||||
outputs: z.array(workflowGroupOutputSchema).min(1),
|
||||
/** Maps the workflow's Start-block inputs to table columns. */
|
||||
inputMappings: z.array(workflowGroupInputMappingSchema).optional(),
|
||||
/** Which workflow state per-cell runs execute against. Defaults to `'live'`. */
|
||||
deploymentMode: workflowGroupDeploymentModeSchema.optional(),
|
||||
/** When `false`, the group never auto-fires from the scheduler — it can
|
||||
* only be triggered manually. Defaults to `true`. Persisted on the
|
||||
* group; distinct from the top-level `autoRun` below which is a
|
||||
@@ -868,6 +875,8 @@ export const updateWorkflowGroupBodySchema = z.object({
|
||||
mappingUpdates: z.array(workflowGroupMappingUpdateSchema).optional(),
|
||||
/** Replace the group's input mappings. Omit to leave unchanged. */
|
||||
inputMappings: z.array(workflowGroupInputMappingSchema).optional(),
|
||||
/** Change which workflow state the group runs against. Omit to leave unchanged. */
|
||||
deploymentMode: workflowGroupDeploymentModeSchema.optional(),
|
||||
/** Update the group's provenance. Omit to leave unchanged. */
|
||||
type: workflowGroupTypeSchema.optional(),
|
||||
/** Toggle the group's persisted auto-run flag. Omit to leave unchanged. */
|
||||
|
||||
@@ -54,6 +54,7 @@ import type {
|
||||
TableDefinition,
|
||||
WorkflowGroup,
|
||||
WorkflowGroupDependencies,
|
||||
WorkflowGroupDeploymentMode,
|
||||
WorkflowGroupInputMapping,
|
||||
WorkflowGroupOutput,
|
||||
} from '@/lib/table/types'
|
||||
@@ -139,6 +140,16 @@ function validateOutputsAgainstWorkflow(
|
||||
return `Invalid output(s) for workflow ${workflowId}:\n${invalidList}\n\nValid options${flattened.length > 12 ? ' (first 12)' : ''}:\n${sample}\n\nCall list_workflow_outputs with workflowId="${workflowId}" to see all valid (blockId, path) picks.`
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrows a raw `deploymentMode` arg to the `'live' | 'deployed'` union, or
|
||||
* `undefined` when absent/invalid (leaving the group's existing value — which
|
||||
* itself defaults to `'live'`). Lets Mothership choose whether a group's
|
||||
* per-cell runs execute the live draft or the latest active deployment.
|
||||
*/
|
||||
function parseDeploymentMode(value: unknown): WorkflowGroupDeploymentMode | undefined {
|
||||
return value === 'live' || value === 'deployed' ? value : undefined
|
||||
}
|
||||
|
||||
async function batchInsertAll(
|
||||
tableId: string,
|
||||
rows: RowData[],
|
||||
@@ -1186,11 +1197,13 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
|
||||
}
|
||||
const dependencies = args.dependencies as WorkflowGroupDependencies | undefined
|
||||
const name = args.name as string | undefined
|
||||
const deploymentMode = parseDeploymentMode(args.deploymentMode)
|
||||
const group: WorkflowGroup = {
|
||||
id: groupId,
|
||||
workflowId,
|
||||
...(name ? { name } : {}),
|
||||
...(dependencies ? { dependencies } : {}),
|
||||
...(deploymentMode ? { deploymentMode } : {}),
|
||||
outputs,
|
||||
}
|
||||
const requestId = generateId().slice(0, 8)
|
||||
@@ -1269,6 +1282,7 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
|
||||
mappingUpdates: args.mappingUpdates as
|
||||
| Array<{ columnName: string; blockId: string; path: string }>
|
||||
| undefined,
|
||||
deploymentMode: parseDeploymentMode(args.deploymentMode),
|
||||
autoRun: typeof args.autoRun === 'boolean' ? args.autoRun : undefined,
|
||||
},
|
||||
requestId
|
||||
|
||||
@@ -3869,6 +3869,7 @@ export async function updateWorkflowGroup(
|
||||
dependencies: data.dependencies ?? group.dependencies,
|
||||
outputs: newOutputs,
|
||||
...(data.inputMappings !== undefined ? { inputMappings: data.inputMappings } : {}),
|
||||
...(data.deploymentMode !== undefined ? { deploymentMode: data.deploymentMode } : {}),
|
||||
...(data.type !== undefined ? { type: data.type } : {}),
|
||||
...(data.autoRun !== undefined ? { autoRun: data.autoRun } : {}),
|
||||
}
|
||||
|
||||
@@ -63,6 +63,13 @@ export interface WorkflowGroupDependencies {
|
||||
*/
|
||||
export type WorkflowGroupType = 'manual' | 'enrichment'
|
||||
|
||||
/**
|
||||
* Which workflow state a group's per-cell runs execute against: `'live'` runs
|
||||
* the editable draft (current behavior); `'deployed'` runs the workflow's
|
||||
* latest active deployment. Defaults to `'live'` when absent.
|
||||
*/
|
||||
export type WorkflowGroupDeploymentMode = 'live' | 'deployed'
|
||||
|
||||
/** One workflow Start-block input field ← one table column. */
|
||||
export interface WorkflowGroupInputMapping {
|
||||
/** `inputFormat` field name on the workflow's Start block. */
|
||||
@@ -88,6 +95,12 @@ export interface WorkflowGroup {
|
||||
* supply each per-row value. Absent / empty means no mapping configured yet.
|
||||
*/
|
||||
inputMappings?: WorkflowGroupInputMapping[]
|
||||
/**
|
||||
* Which workflow state per-cell runs execute against. Defaults to `'live'`
|
||||
* (editable draft) when absent. `'deployed'` runs the workflow's latest
|
||||
* active deployment. Only meaningful for `manual` groups.
|
||||
*/
|
||||
deploymentMode?: WorkflowGroupDeploymentMode
|
||||
/**
|
||||
* When `false`, the group never auto-fires from the scheduler — it can only
|
||||
* be triggered manually via the "Run" actions. Defaults to `true` so
|
||||
@@ -473,6 +486,8 @@ export interface UpdateWorkflowGroupData {
|
||||
mappingUpdates?: Array<{ columnName: string; blockId: string; path: string }>
|
||||
/** Replace the group's input mappings. Omit to leave them unchanged. */
|
||||
inputMappings?: WorkflowGroupInputMapping[]
|
||||
/** Change which workflow state the group runs against. Omit to leave unchanged. */
|
||||
deploymentMode?: WorkflowGroupDeploymentMode
|
||||
/** Update the group's provenance. Omit to leave it unchanged. */
|
||||
type?: WorkflowGroupType
|
||||
/** Toggle the group's auto-run flag. Omit to leave it unchanged. */
|
||||
|
||||
Reference in New Issue
Block a user