mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-13 07:55:09 -05:00
* refactor(tool-input): eliminate SyncWrappers, add canonical toggle and dependsOn gating Replace 17+ individual SyncWrapper components with a single centralized ToolSubBlockRenderer that bridges the subblock store with StoredTool.params via synthetic store keys. This reduces ~1000 lines of duplicated wrapper code and ensures tool-input renders subblock components identically to the standalone SubBlock path. - Add ToolSubBlockRenderer with bidirectional store sync - Add basic/advanced mode toggle (ArrowLeftRight) using collaborative functions - Add dependsOn gating via useDependsOnGate (fields disable instead of hiding) - Add paramVisibility field to SubBlockConfig for tool-input visibility control - Pass canonicalModeOverrides through getSubBlocksForToolInput - Show (optional) label for non-user-only fields (LLM can inject at runtime) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): restore optional indicator, fix folder selector and canonical toggle, extract components - Attach resolved paramVisibility to subblocks from getSubBlocksForToolInput - Add labelSuffix prop to SubBlock for "(optional)" badge on user-or-llm params - Fix folder selector missing for tools with canonicalParamId (e.g. Google Drive) - Fix canonical toggle not clickable by letting SubBlock handle dependsOn internally - Extract ParameterWithLabel, ToolSubBlockRenderer, ToolCredentialSelector to components/tools/ - Extract StoredTool interface to types.ts, selection helpers to utils.ts - Remove dead code (mcpError, refreshTools, oldParamIds, initialParams) - Strengthen typing: replace any with proper types on icon components and evaluateParameterCondition * add sibling values to subblock context since subblock store isn't relevant in tool input, and removed unused param * cleanup * fix(tool-input): render uncovered tool params alongside subblocks The SubBlock-first rendering path was hard-returning after rendering subblocks, so tool params without matching subblocks (like inputMapping for workflow tools) were never rendered. Now renders subblocks first, then any remaining displayParams not covered by subblocks via the legacy ParameterWithLabel fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): auto-refresh workflow inputs after redeploy After redeploying a child workflow via the stale badge, the workflow state cache was not invalidated, so WorkflowInputMapperInput kept showing stale input fields until page refresh. Now invalidates workflowKeys.state on deploy success. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): correct workflow selector visibility and tighten (optional) spacing - Set workflowId param to user-only in workflow_executor tool config so "Select Workflow" no longer shows "(optional)" indicator - Tighten (optional) label spacing with -ml-[3px] to counteract parent Label's gap-[6px], making it feel inline with the label text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): align (optional) text to baseline instead of center Use items-baseline instead of items-center on Label flex containers so the smaller (optional) text aligns with the label text baseline rather than sitting slightly below it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): increase top padding of expanded tool body Bump the expanded tool body container's top padding from 8px to 12px for more breathing room between the header bar and the first parameter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): apply extra top padding only to SubBlock-first path Revert container padding to py-[8px] (MCP tools were correct). Wrap SubBlock-first output in a div with pt-[4px] so only registry tools get extra breathing room from the container top. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tool-input): increase gap between SubBlock params for visual clarity SubBlock's internal gap (10px between label and input) matched the between-parameter gap (10px), making them indistinguishable. Increase the between-parameter gap to 14px so consecutive parameters are visually distinct, matching the separation seen in ParameterWithLabel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix spacing and optional tag * update styling + move predeploy checks earlier for first time deploys * update change detection to account for synthetic tool ids * fix remaining blocks who had files visibility set to hidden * cleanup * add catch --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
662 lines
21 KiB
TypeScript
662 lines
21 KiB
TypeScript
import { useEffect } from 'react'
|
|
import { createLogger } from '@sim/logger'
|
|
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { getNextWorkflowColor } from '@/lib/workflows/colors'
|
|
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
|
import { deploymentKeys } from '@/hooks/queries/deployments'
|
|
import {
|
|
createOptimisticMutationHandlers,
|
|
generateTempId,
|
|
} from '@/hooks/queries/utils/optimistic-mutation'
|
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
|
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
|
|
import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils'
|
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
|
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
|
|
|
const logger = createLogger('WorkflowQueries')
|
|
|
|
export const workflowKeys = {
|
|
all: ['workflows'] as const,
|
|
lists: () => [...workflowKeys.all, 'list'] as const,
|
|
list: (workspaceId: string | undefined) => [...workflowKeys.lists(), workspaceId ?? ''] as const,
|
|
deploymentStatus: (workflowId: string | undefined) =>
|
|
[...workflowKeys.all, 'deploymentStatus', workflowId ?? ''] as const,
|
|
deploymentVersions: () => [...workflowKeys.all, 'deploymentVersion'] as const,
|
|
deploymentVersion: (workflowId: string | undefined, version: number | undefined) =>
|
|
[...workflowKeys.deploymentVersions(), workflowId ?? '', version ?? 0] as const,
|
|
state: (workflowId: string | undefined) =>
|
|
[...workflowKeys.all, 'state', workflowId ?? ''] as const,
|
|
}
|
|
|
|
/**
|
|
* Fetches workflow state from the API.
|
|
* Used as the base query for both state preview and input fields extraction.
|
|
*/
|
|
async function fetchWorkflowState(workflowId: string): Promise<WorkflowState | null> {
|
|
const response = await fetch(`/api/workflows/${workflowId}`)
|
|
if (!response.ok) throw new Error('Failed to fetch workflow')
|
|
const { data } = await response.json()
|
|
return data?.state ?? null
|
|
}
|
|
|
|
/**
|
|
* Hook to fetch workflow state.
|
|
* Used by workflow blocks to show a preview of the child workflow
|
|
* and as a base query for input fields extraction.
|
|
*
|
|
* @param workflowId - The workflow ID to fetch state for
|
|
* @returns Query result with workflow state
|
|
*/
|
|
export function useWorkflowState(workflowId: string | undefined) {
|
|
return useQuery({
|
|
queryKey: workflowKeys.state(workflowId),
|
|
queryFn: () => fetchWorkflowState(workflowId!),
|
|
enabled: Boolean(workflowId),
|
|
staleTime: 30 * 1000, // 30 seconds
|
|
})
|
|
}
|
|
|
|
function mapWorkflow(workflow: any): WorkflowMetadata {
|
|
return {
|
|
id: workflow.id,
|
|
name: workflow.name,
|
|
description: workflow.description,
|
|
color: workflow.color,
|
|
workspaceId: workflow.workspaceId,
|
|
folderId: workflow.folderId,
|
|
sortOrder: workflow.sortOrder ?? 0,
|
|
createdAt: new Date(workflow.createdAt),
|
|
lastModified: new Date(workflow.updatedAt || workflow.createdAt),
|
|
}
|
|
}
|
|
|
|
async function fetchWorkflows(workspaceId: string): Promise<WorkflowMetadata[]> {
|
|
const response = await fetch(`/api/workflows?workspaceId=${workspaceId}`)
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch workflows')
|
|
}
|
|
|
|
const { data }: { data: any[] } = await response.json()
|
|
return data.map(mapWorkflow)
|
|
}
|
|
|
|
export function useWorkflows(workspaceId?: string, options?: { syncRegistry?: boolean }) {
|
|
const { syncRegistry = true } = options || {}
|
|
const beginMetadataLoad = useWorkflowRegistry((state) => state.beginMetadataLoad)
|
|
const completeMetadataLoad = useWorkflowRegistry((state) => state.completeMetadataLoad)
|
|
const failMetadataLoad = useWorkflowRegistry((state) => state.failMetadataLoad)
|
|
|
|
const query = useQuery({
|
|
queryKey: workflowKeys.list(workspaceId),
|
|
queryFn: () => fetchWorkflows(workspaceId as string),
|
|
enabled: Boolean(workspaceId),
|
|
placeholderData: keepPreviousData,
|
|
staleTime: 60 * 1000,
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (syncRegistry && workspaceId && query.status === 'pending') {
|
|
beginMetadataLoad(workspaceId)
|
|
}
|
|
}, [syncRegistry, workspaceId, query.status, beginMetadataLoad])
|
|
|
|
useEffect(() => {
|
|
if (syncRegistry && workspaceId && query.status === 'success' && query.data) {
|
|
completeMetadataLoad(workspaceId, query.data)
|
|
}
|
|
}, [syncRegistry, workspaceId, query.status, query.data, completeMetadataLoad])
|
|
|
|
useEffect(() => {
|
|
if (syncRegistry && workspaceId && query.status === 'error') {
|
|
const message =
|
|
query.error instanceof Error ? query.error.message : 'Failed to fetch workflows'
|
|
failMetadataLoad(workspaceId, message)
|
|
}
|
|
}, [syncRegistry, workspaceId, query.status, query.error, failMetadataLoad])
|
|
|
|
return query
|
|
}
|
|
|
|
interface CreateWorkflowVariables {
|
|
workspaceId: string
|
|
name?: string
|
|
description?: string
|
|
color?: string
|
|
folderId?: string | null
|
|
sortOrder?: number
|
|
}
|
|
|
|
interface CreateWorkflowResult {
|
|
id: string
|
|
name: string
|
|
description?: string
|
|
color: string
|
|
workspaceId: string
|
|
folderId?: string | null
|
|
sortOrder: number
|
|
}
|
|
|
|
interface DuplicateWorkflowVariables {
|
|
workspaceId: string
|
|
sourceId: string
|
|
name: string
|
|
description?: string
|
|
color: string
|
|
folderId?: string | null
|
|
}
|
|
|
|
interface DuplicateWorkflowResult {
|
|
id: string
|
|
name: string
|
|
description?: string
|
|
color: string
|
|
workspaceId: string
|
|
folderId?: string | null
|
|
sortOrder: number
|
|
blocksCount: number
|
|
edgesCount: number
|
|
subflowsCount: number
|
|
}
|
|
|
|
/**
|
|
* Creates optimistic mutation handlers for workflow operations
|
|
*/
|
|
function createWorkflowMutationHandlers<TVariables extends { workspaceId: string }>(
|
|
queryClient: ReturnType<typeof useQueryClient>,
|
|
name: string,
|
|
createOptimisticWorkflow: (variables: TVariables, tempId: string) => WorkflowMetadata
|
|
) {
|
|
return createOptimisticMutationHandlers<
|
|
CreateWorkflowResult | DuplicateWorkflowResult,
|
|
TVariables,
|
|
WorkflowMetadata
|
|
>(queryClient, {
|
|
name,
|
|
getQueryKey: (variables) => workflowKeys.list(variables.workspaceId),
|
|
getSnapshot: () => ({ ...useWorkflowRegistry.getState().workflows }),
|
|
generateTempId: () => generateTempId('temp-workflow'),
|
|
createOptimisticItem: createOptimisticWorkflow,
|
|
applyOptimisticUpdate: (tempId, item) => {
|
|
useWorkflowRegistry.setState((state) => ({
|
|
workflows: { ...state.workflows, [tempId]: item },
|
|
}))
|
|
},
|
|
replaceOptimisticEntry: (tempId, data) => {
|
|
useWorkflowRegistry.setState((state) => {
|
|
const { [tempId]: _, ...remainingWorkflows } = state.workflows
|
|
return {
|
|
workflows: {
|
|
...remainingWorkflows,
|
|
[data.id]: {
|
|
id: data.id,
|
|
name: data.name,
|
|
lastModified: new Date(),
|
|
createdAt: new Date(),
|
|
description: data.description,
|
|
color: data.color,
|
|
workspaceId: data.workspaceId,
|
|
folderId: data.folderId,
|
|
sortOrder: 'sortOrder' in data ? data.sortOrder : 0,
|
|
},
|
|
},
|
|
error: null,
|
|
}
|
|
})
|
|
},
|
|
rollback: (snapshot) => {
|
|
useWorkflowRegistry.setState({ workflows: snapshot })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useCreateWorkflow() {
|
|
const queryClient = useQueryClient()
|
|
|
|
const handlers = createWorkflowMutationHandlers<CreateWorkflowVariables>(
|
|
queryClient,
|
|
'CreateWorkflow',
|
|
(variables, tempId) => {
|
|
let sortOrder: number
|
|
if (variables.sortOrder !== undefined) {
|
|
sortOrder = variables.sortOrder
|
|
} else {
|
|
const currentWorkflows = useWorkflowRegistry.getState().workflows
|
|
const targetFolderId = variables.folderId || null
|
|
const workflowsInFolder = Object.values(currentWorkflows).filter(
|
|
(w) => w.folderId === targetFolderId
|
|
)
|
|
sortOrder = workflowsInFolder.reduce((min, w) => Math.min(min, w.sortOrder ?? 0), 1) - 1
|
|
}
|
|
|
|
return {
|
|
id: tempId,
|
|
name: variables.name || generateCreativeWorkflowName(),
|
|
lastModified: new Date(),
|
|
createdAt: new Date(),
|
|
description: variables.description || 'New workflow',
|
|
color: variables.color || getNextWorkflowColor(),
|
|
workspaceId: variables.workspaceId,
|
|
folderId: variables.folderId || null,
|
|
sortOrder,
|
|
}
|
|
}
|
|
)
|
|
|
|
return useMutation({
|
|
mutationFn: async (variables: CreateWorkflowVariables): Promise<CreateWorkflowResult> => {
|
|
const { workspaceId, name, description, color, folderId, sortOrder } = variables
|
|
|
|
logger.info(`Creating new workflow in workspace: ${workspaceId}`)
|
|
|
|
const createResponse = await fetch('/api/workflows', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: name || generateCreativeWorkflowName(),
|
|
description: description || 'New workflow',
|
|
color: color || getNextWorkflowColor(),
|
|
workspaceId,
|
|
folderId: folderId || null,
|
|
sortOrder,
|
|
}),
|
|
})
|
|
|
|
if (!createResponse.ok) {
|
|
const errorData = await createResponse.json()
|
|
throw new Error(
|
|
`Failed to create workflow: ${errorData.error || createResponse.statusText}`
|
|
)
|
|
}
|
|
|
|
const createdWorkflow = await createResponse.json()
|
|
const workflowId = createdWorkflow.id
|
|
|
|
logger.info(`Successfully created workflow ${workflowId}`)
|
|
|
|
const { workflowState } = buildDefaultWorkflowArtifacts()
|
|
|
|
const stateResponse = await fetch(`/api/workflows/${workflowId}/state`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(workflowState),
|
|
})
|
|
|
|
if (!stateResponse.ok) {
|
|
const text = await stateResponse.text()
|
|
logger.error('Failed to persist default Start block:', text)
|
|
} else {
|
|
logger.info('Successfully persisted default Start block')
|
|
}
|
|
|
|
return {
|
|
id: workflowId,
|
|
name: createdWorkflow.name,
|
|
description: createdWorkflow.description,
|
|
color: createdWorkflow.color,
|
|
workspaceId,
|
|
folderId: createdWorkflow.folderId,
|
|
sortOrder: createdWorkflow.sortOrder ?? 0,
|
|
}
|
|
},
|
|
...handlers,
|
|
onSuccess: (data, variables, context) => {
|
|
handlers.onSuccess(data, variables, context)
|
|
|
|
const { subBlockValues } = buildDefaultWorkflowArtifacts()
|
|
useSubBlockStore.setState((state) => ({
|
|
workflowValues: {
|
|
...state.workflowValues,
|
|
[data.id]: subBlockValues,
|
|
},
|
|
}))
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useDuplicateWorkflowMutation() {
|
|
const queryClient = useQueryClient()
|
|
|
|
const handlers = createWorkflowMutationHandlers<DuplicateWorkflowVariables>(
|
|
queryClient,
|
|
'DuplicateWorkflow',
|
|
(variables, tempId) => {
|
|
const currentWorkflows = useWorkflowRegistry.getState().workflows
|
|
const targetFolderId = variables.folderId || null
|
|
const workflowsInFolder = Object.values(currentWorkflows).filter(
|
|
(w) => w.folderId === targetFolderId
|
|
)
|
|
const minSortOrder = workflowsInFolder.reduce((min, w) => Math.min(min, w.sortOrder ?? 0), 1)
|
|
|
|
return {
|
|
id: tempId,
|
|
name: variables.name,
|
|
lastModified: new Date(),
|
|
createdAt: new Date(),
|
|
description: variables.description,
|
|
color: variables.color,
|
|
workspaceId: variables.workspaceId,
|
|
folderId: targetFolderId,
|
|
sortOrder: minSortOrder - 1,
|
|
}
|
|
}
|
|
)
|
|
|
|
return useMutation({
|
|
mutationFn: async (variables: DuplicateWorkflowVariables): Promise<DuplicateWorkflowResult> => {
|
|
const { workspaceId, sourceId, name, description, color, folderId } = variables
|
|
|
|
logger.info(`Duplicating workflow ${sourceId} in workspace: ${workspaceId}`)
|
|
|
|
const response = await fetch(`/api/workflows/${sourceId}/duplicate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name,
|
|
description,
|
|
color,
|
|
workspaceId,
|
|
folderId: folderId ?? null,
|
|
}),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}))
|
|
throw new Error(`Failed to duplicate workflow: ${errorData.error || response.statusText}`)
|
|
}
|
|
|
|
const duplicatedWorkflow = await response.json()
|
|
|
|
logger.info(`Successfully duplicated workflow ${sourceId} to ${duplicatedWorkflow.id}`, {
|
|
blocksCount: duplicatedWorkflow.blocksCount,
|
|
edgesCount: duplicatedWorkflow.edgesCount,
|
|
subflowsCount: duplicatedWorkflow.subflowsCount,
|
|
})
|
|
|
|
return {
|
|
id: duplicatedWorkflow.id,
|
|
name: duplicatedWorkflow.name || name,
|
|
description: duplicatedWorkflow.description || description,
|
|
color: duplicatedWorkflow.color || color,
|
|
workspaceId,
|
|
folderId: duplicatedWorkflow.folderId ?? folderId,
|
|
sortOrder: duplicatedWorkflow.sortOrder ?? 0,
|
|
blocksCount: duplicatedWorkflow.blocksCount || 0,
|
|
edgesCount: duplicatedWorkflow.edgesCount || 0,
|
|
subflowsCount: duplicatedWorkflow.subflowsCount || 0,
|
|
}
|
|
},
|
|
...handlers,
|
|
onSuccess: (data, variables, context) => {
|
|
handlers.onSuccess(data, variables, context)
|
|
|
|
// Copy subblock values from source if it's the active workflow
|
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
|
if (variables.sourceId === activeWorkflowId) {
|
|
const sourceSubblockValues =
|
|
useSubBlockStore.getState().workflowValues[variables.sourceId] || {}
|
|
useSubBlockStore.setState((state) => ({
|
|
workflowValues: {
|
|
...state.workflowValues,
|
|
[data.id]: { ...sourceSubblockValues },
|
|
},
|
|
}))
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
interface DeploymentVersionStateResponse {
|
|
deployedState: WorkflowState
|
|
}
|
|
|
|
/**
|
|
* Fetches the deployed state for a specific deployment version.
|
|
* Exported for reuse in other query hooks.
|
|
*/
|
|
export async function fetchDeploymentVersionState(
|
|
workflowId: string,
|
|
version: number
|
|
): Promise<WorkflowState> {
|
|
const response = await fetch(`/api/workflows/${workflowId}/deployments/${version}`)
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch deployment version: ${response.statusText}`)
|
|
}
|
|
|
|
const data: DeploymentVersionStateResponse = await response.json()
|
|
if (!data.deployedState) {
|
|
throw new Error('No deployed state returned')
|
|
}
|
|
|
|
return data.deployedState
|
|
}
|
|
|
|
/**
|
|
* Hook for fetching the workflow state of a specific deployment version.
|
|
* Used in the deploy modal to preview historical versions.
|
|
*/
|
|
export function useDeploymentVersionState(workflowId: string | null, version: number | null) {
|
|
return useQuery({
|
|
queryKey: workflowKeys.deploymentVersion(workflowId ?? undefined, version ?? undefined),
|
|
queryFn: () => fetchDeploymentVersionState(workflowId as string, version as number),
|
|
enabled: Boolean(workflowId) && version !== null,
|
|
staleTime: 5 * 60 * 1000, // 5 minutes - deployment versions don't change
|
|
})
|
|
}
|
|
|
|
interface RevertToVersionVariables {
|
|
workflowId: string
|
|
version: number
|
|
}
|
|
|
|
/**
|
|
* Mutation hook for reverting (loading) a deployment version into the current workflow.
|
|
*/
|
|
export function useRevertToVersion() {
|
|
return useMutation({
|
|
mutationFn: async ({ workflowId, version }: RevertToVersionVariables): Promise<void> => {
|
|
const response = await fetch(`/api/workflows/${workflowId}/deployments/${version}/revert`, {
|
|
method: 'POST',
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load deployment')
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
interface ReorderWorkflowsVariables {
|
|
workspaceId: string
|
|
updates: Array<{
|
|
id: string
|
|
sortOrder: number
|
|
folderId?: string | null
|
|
}>
|
|
}
|
|
|
|
export function useReorderWorkflows() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (variables: ReorderWorkflowsVariables): Promise<void> => {
|
|
const response = await fetch('/api/workflows/reorder', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(variables),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}))
|
|
throw new Error(error.error || 'Failed to reorder workflows')
|
|
}
|
|
},
|
|
onMutate: async (variables) => {
|
|
await queryClient.cancelQueries({ queryKey: workflowKeys.list(variables.workspaceId) })
|
|
|
|
const snapshot = { ...useWorkflowRegistry.getState().workflows }
|
|
|
|
useWorkflowRegistry.setState((state) => {
|
|
const updated = { ...state.workflows }
|
|
for (const update of variables.updates) {
|
|
if (updated[update.id]) {
|
|
updated[update.id] = {
|
|
...updated[update.id],
|
|
sortOrder: update.sortOrder,
|
|
folderId:
|
|
update.folderId !== undefined ? update.folderId : updated[update.id].folderId,
|
|
}
|
|
}
|
|
}
|
|
return { workflows: updated }
|
|
})
|
|
|
|
return { snapshot }
|
|
},
|
|
onError: (_error, _variables, context) => {
|
|
if (context?.snapshot) {
|
|
useWorkflowRegistry.setState({ workflows: context.snapshot })
|
|
}
|
|
},
|
|
onSettled: (_data, _error, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: workflowKeys.list(variables.workspaceId) })
|
|
},
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Child deployment status data returned from the API
|
|
*/
|
|
export interface ChildDeploymentStatus {
|
|
activeVersion: number | null
|
|
isDeployed: boolean
|
|
needsRedeploy: boolean
|
|
}
|
|
|
|
/**
|
|
* Fetches deployment status for a child workflow.
|
|
* Uses Promise.all to fetch status and deployments in parallel for better performance.
|
|
*/
|
|
async function fetchChildDeploymentStatus(workflowId: string): Promise<ChildDeploymentStatus> {
|
|
const fetchOptions = {
|
|
cache: 'no-store' as const,
|
|
headers: { 'Cache-Control': 'no-cache' },
|
|
}
|
|
|
|
const [statusRes, deploymentsRes] = await Promise.all([
|
|
fetch(`/api/workflows/${workflowId}/status`, fetchOptions),
|
|
fetch(`/api/workflows/${workflowId}/deployments`, fetchOptions),
|
|
])
|
|
|
|
if (!statusRes.ok) {
|
|
throw new Error('Failed to fetch workflow status')
|
|
}
|
|
|
|
const statusData = await statusRes.json()
|
|
|
|
let activeVersion: number | null = null
|
|
if (deploymentsRes.ok) {
|
|
const deploymentsJson = await deploymentsRes.json()
|
|
const versions = Array.isArray(deploymentsJson?.data?.versions)
|
|
? deploymentsJson.data.versions
|
|
: Array.isArray(deploymentsJson?.versions)
|
|
? deploymentsJson.versions
|
|
: []
|
|
|
|
const active = versions.find((v: { isActive?: boolean }) => v.isActive)
|
|
activeVersion = active ? Number(active.version) : null
|
|
}
|
|
|
|
return {
|
|
activeVersion,
|
|
isDeployed: statusData.isDeployed || false,
|
|
needsRedeploy: statusData.needsRedeployment || false,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hook to fetch deployment status for a child workflow.
|
|
* Used by workflow selector blocks to show deployment badges.
|
|
*/
|
|
export function useChildDeploymentStatus(workflowId: string | undefined) {
|
|
return useQuery({
|
|
queryKey: workflowKeys.deploymentStatus(workflowId),
|
|
queryFn: () => fetchChildDeploymentStatus(workflowId!),
|
|
enabled: Boolean(workflowId),
|
|
staleTime: 0,
|
|
retry: false,
|
|
})
|
|
}
|
|
|
|
interface DeployChildWorkflowVariables {
|
|
workflowId: string
|
|
}
|
|
|
|
interface DeployChildWorkflowResult {
|
|
isDeployed: boolean
|
|
deployedAt?: Date
|
|
apiKey?: string
|
|
}
|
|
|
|
/**
|
|
* Mutation hook for deploying a child workflow.
|
|
* Invalidates the deployment status query on success.
|
|
*/
|
|
export function useDeployChildWorkflow() {
|
|
const queryClient = useQueryClient()
|
|
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
|
|
|
|
return useMutation({
|
|
mutationFn: async ({
|
|
workflowId,
|
|
}: DeployChildWorkflowVariables): Promise<DeployChildWorkflowResult> => {
|
|
const response = await fetch(`/api/workflows/${workflowId}/deploy`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
deployChatEnabled: false,
|
|
}),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}))
|
|
throw new Error(errorData.error || 'Failed to deploy workflow')
|
|
}
|
|
|
|
const responseData = await response.json()
|
|
return {
|
|
isDeployed: responseData.isDeployed ?? false,
|
|
deployedAt: responseData.deployedAt ? new Date(responseData.deployedAt) : undefined,
|
|
apiKey: responseData.apiKey || '',
|
|
}
|
|
},
|
|
onSuccess: (data, variables) => {
|
|
logger.info('Child workflow deployed', { workflowId: variables.workflowId })
|
|
|
|
setDeploymentStatus(variables.workflowId, data.isDeployed, data.deployedAt, data.apiKey || '')
|
|
|
|
queryClient.invalidateQueries({
|
|
queryKey: workflowKeys.deploymentStatus(variables.workflowId),
|
|
})
|
|
// Invalidate workflow state so tool input mappings refresh
|
|
queryClient.invalidateQueries({
|
|
queryKey: workflowKeys.state(variables.workflowId),
|
|
})
|
|
// Also invalidate deployment queries
|
|
queryClient.invalidateQueries({
|
|
queryKey: deploymentKeys.info(variables.workflowId),
|
|
})
|
|
queryClient.invalidateQueries({
|
|
queryKey: deploymentKeys.versions(variables.workflowId),
|
|
})
|
|
},
|
|
onError: (error) => {
|
|
logger.error('Failed to deploy child workflow', { error })
|
|
},
|
|
})
|
|
}
|