mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(block-outputs): fix chat fields being hidden even if in inputFormat, cleanup code (#1819)
* improvement(block-outputs): fix chat fields being hidden even if in inputFormat, cleanup code" * fix type error
This commit is contained in:
+7
-1
@@ -46,7 +46,13 @@ const TYPE_CONFIG: Record<VariableType, { icon: string; placeholder: string }> =
|
||||
string: { icon: 'Abc', placeholder: 'Plain text value' },
|
||||
}
|
||||
|
||||
const VARIABLE_TYPES: VariableType[] = ['plain', 'number', 'boolean', 'object', 'array']
|
||||
const VARIABLE_TYPES: Exclude<VariableType, 'string'>[] = [
|
||||
'plain',
|
||||
'number',
|
||||
'boolean',
|
||||
'object',
|
||||
'array',
|
||||
]
|
||||
|
||||
export function Variables() {
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
|
||||
+1
-3
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { LoopType, ParallelType } from '@/lib/workflows/types'
|
||||
|
||||
// Mock hooks
|
||||
const mockCollaborativeUpdates = {
|
||||
@@ -170,8 +171,6 @@ describe('IterationBadges', () => {
|
||||
describe('Count Mode Detection', () => {
|
||||
it.concurrent('should be in count mode for loop + for combination', () => {
|
||||
type IterationType = 'loop' | 'parallel'
|
||||
type LoopType = 'for' | 'forEach'
|
||||
type ParallelType = 'count' | 'collection'
|
||||
|
||||
const iterationType: IterationType = 'loop'
|
||||
const currentType: LoopType = 'for'
|
||||
@@ -182,7 +181,6 @@ describe('IterationBadges', () => {
|
||||
|
||||
it.concurrent('should be in count mode for parallel + count combination', () => {
|
||||
type IterationType = 'loop' | 'parallel'
|
||||
type ParallelType = 'count' | 'collection'
|
||||
|
||||
const iterationType: IterationType = 'parallel'
|
||||
const currentType: ParallelType = 'count'
|
||||
|
||||
+2
-2
@@ -12,9 +12,9 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/themes/prism.css'
|
||||
|
||||
import type { LoopType, ParallelType } from '@/lib/workflows/types'
|
||||
|
||||
type IterationType = 'loop' | 'parallel'
|
||||
type LoopType = 'for' | 'forEach' | 'while' | 'doWhile'
|
||||
type ParallelType = 'count' | 'collection'
|
||||
|
||||
interface IterationNodeData {
|
||||
width?: number
|
||||
|
||||
+5
-6
@@ -4,14 +4,10 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { InputFormatField } from '@/lib/workflows/types'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
|
||||
interface InputFormatField {
|
||||
name: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
interface InputTriggerBlock {
|
||||
type: 'input_trigger' | 'start_trigger'
|
||||
subBlocks?: {
|
||||
@@ -42,7 +38,10 @@ function isStarterBlock(value: unknown): value is StarterBlockLegacy {
|
||||
return !!value && typeof value === 'object' && (value as { type?: unknown }).type === 'starter'
|
||||
}
|
||||
|
||||
function isInputFormatField(value: unknown): value is InputFormatField {
|
||||
type ValidatedInputFormatField = Required<Pick<InputFormatField, 'name'>> &
|
||||
Pick<InputFormatField, 'type'>
|
||||
|
||||
function isInputFormatField(value: unknown): value is ValidatedInputFormatField {
|
||||
if (typeof value !== 'object' || value === null) return false
|
||||
if (!('name' in value)) return false
|
||||
const { name, type } = value as { name: unknown; type?: unknown }
|
||||
|
||||
+4
-3
@@ -11,6 +11,7 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { parseCronToHumanReadable } from '@/lib/schedules/utils'
|
||||
import { cn, validateName } from '@/lib/utils'
|
||||
import { type DiffStatus, hasDiffStatus } from '@/lib/workflows/diff/types'
|
||||
import { TRIGGER_TYPES } from '@/lib/workflows/triggers'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
@@ -228,9 +229,9 @@ export const WorkflowBlock = memo(
|
||||
const params = useParams()
|
||||
const currentWorkflowId = params.workflowId as string
|
||||
|
||||
// Check if this is a starter block or trigger block
|
||||
const isStarterBlock = type === 'starter'
|
||||
const isWebhookTriggerBlock = type === 'webhook' || type === 'generic_webhook'
|
||||
const isStarterBlock = type === TRIGGER_TYPES.STARTER
|
||||
const isWebhookTriggerBlock =
|
||||
type === TRIGGER_TYPES.WEBHOOK || type === TRIGGER_TYPES.GENERIC_WEBHOOK
|
||||
|
||||
const reactivateSchedule = async (scheduleId: string) => {
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { shallow } from 'zustand/shallow'
|
||||
import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getBlockOutputPaths, getBlockOutputType } from '@/lib/workflows/block-outputs'
|
||||
import { TRIGGER_TYPES } from '@/lib/workflows/triggers'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
@@ -490,7 +491,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
|
||||
} else if (sourceBlock.type === 'starter') {
|
||||
blockTags = [normalizedBlockName]
|
||||
} else if (sourceBlock.type === 'generic_webhook') {
|
||||
} else if (sourceBlock.type === TRIGGER_TYPES.GENERIC_WEBHOOK) {
|
||||
blockTags = [normalizedBlockName]
|
||||
} else {
|
||||
blockTags = []
|
||||
@@ -522,7 +523,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
|
||||
blockTags = ensureRootTag(blockTags, normalizedBlockName)
|
||||
const shouldShowRootTag = sourceBlock.type === 'generic_webhook'
|
||||
const shouldShowRootTag = sourceBlock.type === TRIGGER_TYPES.GENERIC_WEBHOOK
|
||||
if (!shouldShowRootTag) {
|
||||
blockTags = blockTags.filter((tag) => tag !== normalizedBlockName)
|
||||
}
|
||||
@@ -763,7 +764,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
} else {
|
||||
blockTags = [normalizedBlockName]
|
||||
}
|
||||
} else if (accessibleBlock.type === 'generic_webhook') {
|
||||
} else if (accessibleBlock.type === TRIGGER_TYPES.GENERIC_WEBHOOK) {
|
||||
blockTags = [normalizedBlockName]
|
||||
} else {
|
||||
blockTags = []
|
||||
@@ -834,7 +835,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
|
||||
blockTags = ensureRootTag(blockTags, normalizedBlockName)
|
||||
const shouldShowRootTag = accessibleBlock.type === 'generic_webhook'
|
||||
const shouldShowRootTag = accessibleBlock.type === TRIGGER_TYPES.GENERIC_WEBHOOK
|
||||
if (!shouldShowRootTag) {
|
||||
blockTags = blockTags.filter((tag) => tag !== normalizedBlockName)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveStartCandidates,
|
||||
StartBlockPath,
|
||||
} from '@/lib/workflows/triggers'
|
||||
import type { InputFormatField } from '@/lib/workflows/types'
|
||||
import type { NormalizedBlockOutput, UserFile } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
|
||||
@@ -95,12 +96,6 @@ export function buildResolutionFromBlock(block: SerializedBlock): ExecutorStartR
|
||||
}
|
||||
}
|
||||
|
||||
type InputFormatField = {
|
||||
name?: string
|
||||
type?: string | null
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
|
||||
import { TRIGGER_TYPES } from '@/lib/workflows/triggers'
|
||||
import type { InputFormatField } from '@/lib/workflows/types'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
|
||||
@@ -8,11 +10,6 @@ const logger = createLogger('ExecutionFiles')
|
||||
|
||||
const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
|
||||
|
||||
interface InputFormatField {
|
||||
name: string
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files'
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single file for workflow execution - handles both base64 ('file' type) and URL pass-through ('url' type)
|
||||
*/
|
||||
@@ -112,7 +109,9 @@ export async function processExecutionFiles(
|
||||
/**
|
||||
* Extract inputFormat fields from a start block or trigger block
|
||||
*/
|
||||
function extractInputFormatFromBlock(block: SerializedBlock): InputFormatField[] {
|
||||
type ValidatedInputFormatField = Required<Pick<InputFormatField, 'name' | 'type'>>
|
||||
|
||||
function extractInputFormatFromBlock(block: SerializedBlock): ValidatedInputFormatField[] {
|
||||
const inputFormatValue = block.config?.params?.inputFormat
|
||||
|
||||
if (!Array.isArray(inputFormatValue) || inputFormatValue.length === 0) {
|
||||
@@ -120,7 +119,7 @@ function extractInputFormatFromBlock(block: SerializedBlock): InputFormatField[]
|
||||
}
|
||||
|
||||
return inputFormatValue.filter(
|
||||
(field): field is InputFormatField =>
|
||||
(field): field is ValidatedInputFormatField =>
|
||||
field &&
|
||||
typeof field === 'object' &&
|
||||
'name' in field &&
|
||||
@@ -148,11 +147,11 @@ export async function processInputFileFields(
|
||||
const startBlock = blocks.find((block) => {
|
||||
const blockType = block.metadata?.id
|
||||
return (
|
||||
blockType === 'start_trigger' ||
|
||||
blockType === 'api_trigger' ||
|
||||
blockType === 'input_trigger' ||
|
||||
blockType === 'generic_webhook' ||
|
||||
blockType === 'starter'
|
||||
blockType === TRIGGER_TYPES.START ||
|
||||
blockType === TRIGGER_TYPES.API ||
|
||||
blockType === TRIGGER_TYPES.INPUT ||
|
||||
blockType === TRIGGER_TYPES.GENERIC_WEBHOOK ||
|
||||
blockType === TRIGGER_TYPES.STARTER
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import { classifyStartBlockType, StartBlockPath } from '@/lib/workflows/triggers'
|
||||
import { classifyStartBlockType, StartBlockPath, TRIGGER_TYPES } from '@/lib/workflows/triggers'
|
||||
import {
|
||||
type InputFormatField,
|
||||
START_BLOCK_RESERVED_FIELDS,
|
||||
USER_FILE_ACCESSIBLE_PROPERTIES,
|
||||
USER_FILE_PROPERTY_TYPES,
|
||||
} from '@/lib/workflows/types'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { getTrigger, isTriggerValid } from '@/triggers'
|
||||
|
||||
type InputFormatField = { name?: string; type?: string | undefined | null }
|
||||
type OutputDefinition = Record<string, any>
|
||||
|
||||
const CHAT_OUTPUTS: OutputDefinition = {
|
||||
input: { type: 'string', description: 'User message' },
|
||||
conversationId: { type: 'string', description: 'Conversation ID' },
|
||||
files: { type: 'files', description: 'Uploaded files' },
|
||||
}
|
||||
|
||||
const UNIFIED_START_OUTPUTS: OutputDefinition = {
|
||||
input: { type: 'string', description: 'Primary user input or message' },
|
||||
conversationId: { type: 'string', description: 'Conversation thread identifier' },
|
||||
files: { type: 'files', description: 'User uploaded files' },
|
||||
}
|
||||
|
||||
function normalizeInputFormatValue(inputFormatValue: any): InputFormatField[] {
|
||||
if (
|
||||
@@ -23,8 +41,8 @@ function normalizeInputFormatValue(inputFormatValue: any): InputFormatField[] {
|
||||
|
||||
function applyInputFormatFields(
|
||||
inputFormat: InputFormatField[],
|
||||
outputs: Record<string, any>
|
||||
): Record<string, any> {
|
||||
outputs: OutputDefinition
|
||||
): OutputDefinition {
|
||||
for (const field of inputFormat) {
|
||||
const fieldName = field?.name?.trim()
|
||||
if (!fieldName) continue
|
||||
@@ -38,29 +56,103 @@ function applyInputFormatFields(
|
||||
return outputs
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective outputs for a block, including dynamic outputs from inputFormat
|
||||
* and trigger outputs for blocks in trigger mode
|
||||
*/
|
||||
function hasInputFormat(blockConfig: BlockConfig): boolean {
|
||||
return blockConfig.subBlocks?.some((sb) => sb.type === 'input-format') || false
|
||||
}
|
||||
|
||||
function getTriggerId(
|
||||
subBlocks: Record<string, any> | undefined,
|
||||
blockConfig: BlockConfig
|
||||
): string | undefined {
|
||||
const selectedTriggerIdValue = subBlocks?.selectedTriggerId?.value
|
||||
const triggerIdValue = subBlocks?.triggerId?.value
|
||||
|
||||
return (
|
||||
(typeof selectedTriggerIdValue === 'string' && isTriggerValid(selectedTriggerIdValue)
|
||||
? selectedTriggerIdValue
|
||||
: undefined) ||
|
||||
(typeof triggerIdValue === 'string' && isTriggerValid(triggerIdValue)
|
||||
? triggerIdValue
|
||||
: undefined) ||
|
||||
blockConfig.triggers?.available?.[0]
|
||||
)
|
||||
}
|
||||
|
||||
function getUnifiedStartOutputs(subBlocks: Record<string, any> | undefined): OutputDefinition {
|
||||
const outputs = { ...UNIFIED_START_OUTPUTS }
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
|
||||
return applyInputFormatFields(normalizedInputFormat, outputs)
|
||||
}
|
||||
|
||||
function getLegacyStarterOutputs(subBlocks: Record<string, any> | undefined): OutputDefinition {
|
||||
const startWorkflowValue = subBlocks?.startWorkflow?.value
|
||||
|
||||
if (startWorkflowValue === 'chat') {
|
||||
return { ...CHAT_OUTPUTS }
|
||||
}
|
||||
|
||||
if (
|
||||
startWorkflowValue === 'api' ||
|
||||
startWorkflowValue === 'run' ||
|
||||
startWorkflowValue === 'manual'
|
||||
) {
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
|
||||
return applyInputFormatFields(normalizedInputFormat, {})
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
function shouldClearBaseOutputs(
|
||||
blockType: string,
|
||||
normalizedInputFormat: InputFormatField[]
|
||||
): boolean {
|
||||
if (blockType === TRIGGER_TYPES.API || blockType === TRIGGER_TYPES.INPUT) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (blockType === TRIGGER_TYPES.GENERIC_WEBHOOK && normalizedInputFormat.length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function applyInputFormatToOutputs(
|
||||
blockType: string,
|
||||
blockConfig: BlockConfig,
|
||||
subBlocks: Record<string, any> | undefined,
|
||||
baseOutputs: OutputDefinition
|
||||
): OutputDefinition {
|
||||
if (!hasInputFormat(blockConfig) || !subBlocks?.inputFormat?.value) {
|
||||
return baseOutputs
|
||||
}
|
||||
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks.inputFormat.value)
|
||||
|
||||
if (!Array.isArray(subBlocks.inputFormat.value)) {
|
||||
if (blockType === TRIGGER_TYPES.API || blockType === TRIGGER_TYPES.INPUT) {
|
||||
return {}
|
||||
}
|
||||
return baseOutputs
|
||||
}
|
||||
|
||||
const shouldClear = shouldClearBaseOutputs(blockType, normalizedInputFormat)
|
||||
const outputs = shouldClear ? {} : { ...baseOutputs }
|
||||
|
||||
return applyInputFormatFields(normalizedInputFormat, outputs)
|
||||
}
|
||||
|
||||
export function getBlockOutputs(
|
||||
blockType: string,
|
||||
subBlocks?: Record<string, any>,
|
||||
triggerMode?: boolean
|
||||
): Record<string, any> {
|
||||
): OutputDefinition {
|
||||
const blockConfig = getBlock(blockType)
|
||||
if (!blockConfig) return {}
|
||||
|
||||
if (triggerMode && blockConfig.triggers?.enabled) {
|
||||
const selectedTriggerIdValue = subBlocks?.selectedTriggerId?.value
|
||||
const triggerIdValue = subBlocks?.triggerId?.value
|
||||
const triggerId =
|
||||
(typeof selectedTriggerIdValue === 'string' && isTriggerValid(selectedTriggerIdValue)
|
||||
? selectedTriggerIdValue
|
||||
: undefined) ||
|
||||
(typeof triggerIdValue === 'string' && isTriggerValid(triggerIdValue)
|
||||
? triggerIdValue
|
||||
: undefined) ||
|
||||
blockConfig.triggers?.available?.[0]
|
||||
const triggerId = getTriggerId(subBlocks, blockConfig)
|
||||
if (triggerId && isTriggerValid(triggerId)) {
|
||||
const trigger = getTrigger(triggerId)
|
||||
if (trigger.outputs) {
|
||||
@@ -69,155 +161,128 @@ export function getBlockOutputs(
|
||||
}
|
||||
}
|
||||
|
||||
let outputs = { ...(blockConfig.outputs || {}) }
|
||||
|
||||
const startPath = classifyStartBlockType(blockType)
|
||||
|
||||
if (startPath === StartBlockPath.UNIFIED) {
|
||||
outputs = {
|
||||
input: { type: 'string', description: 'Primary user input or message' },
|
||||
conversationId: { type: 'string', description: 'Conversation thread identifier' },
|
||||
files: { type: 'files', description: 'User uploaded files' },
|
||||
}
|
||||
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
|
||||
for (const field of normalizedInputFormat) {
|
||||
const fieldName = field?.name?.trim()
|
||||
if (!fieldName) continue
|
||||
|
||||
outputs[fieldName] = {
|
||||
type: (field?.type || 'any') as any,
|
||||
description: `Field from input format`,
|
||||
}
|
||||
}
|
||||
|
||||
return outputs
|
||||
return getUnifiedStartOutputs(subBlocks)
|
||||
}
|
||||
|
||||
// Special handling for starter block (legacy)
|
||||
if (startPath === StartBlockPath.LEGACY_STARTER) {
|
||||
const startWorkflowValue = subBlocks?.startWorkflow?.value
|
||||
|
||||
if (startWorkflowValue === 'chat') {
|
||||
// Chat mode outputs
|
||||
return {
|
||||
input: { type: 'string', description: 'User message' },
|
||||
conversationId: { type: 'string', description: 'Conversation ID' },
|
||||
files: { type: 'files', description: 'Uploaded files' },
|
||||
}
|
||||
}
|
||||
if (
|
||||
startWorkflowValue === 'api' ||
|
||||
startWorkflowValue === 'run' ||
|
||||
startWorkflowValue === 'manual'
|
||||
) {
|
||||
// API/manual mode - use inputFormat fields only
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
|
||||
outputs = {}
|
||||
return applyInputFormatFields(normalizedInputFormat, outputs)
|
||||
}
|
||||
return getLegacyStarterOutputs(subBlocks)
|
||||
}
|
||||
|
||||
// For blocks with inputFormat, add dynamic outputs
|
||||
if (hasInputFormat(blockConfig) && subBlocks?.inputFormat?.value) {
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks.inputFormat.value)
|
||||
const baseOutputs = { ...(blockConfig.outputs || {}) }
|
||||
return applyInputFormatToOutputs(blockType, blockConfig, subBlocks, baseOutputs)
|
||||
}
|
||||
|
||||
if (!Array.isArray(subBlocks.inputFormat.value)) {
|
||||
if (blockType === 'api_trigger' || blockType === 'input_trigger') {
|
||||
outputs = {}
|
||||
function shouldFilterReservedField(
|
||||
blockType: string,
|
||||
key: string,
|
||||
prefix: string,
|
||||
subBlocks: Record<string, any> | undefined
|
||||
): boolean {
|
||||
if (blockType !== TRIGGER_TYPES.START || prefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!START_BLOCK_RESERVED_FIELDS.includes(key as any)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
|
||||
const isExplicitlyDefined = normalizedInputFormat.some((field) => field?.name?.trim() === key)
|
||||
|
||||
return !isExplicitlyDefined
|
||||
}
|
||||
|
||||
function expandFileTypeProperties(path: string): string[] {
|
||||
return USER_FILE_ACCESSIBLE_PROPERTIES.map((prop) => `${path}.${prop}`)
|
||||
}
|
||||
|
||||
function collectOutputPaths(
|
||||
obj: OutputDefinition,
|
||||
blockType: string,
|
||||
subBlocks: Record<string, any> | undefined,
|
||||
prefix = ''
|
||||
): string[] {
|
||||
const paths: string[] = []
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key
|
||||
|
||||
if (shouldFilterReservedField(blockType, key, prefix, subBlocks)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object' && 'type' in value) {
|
||||
if (value.type === 'files') {
|
||||
paths.push(...expandFileTypeProperties(path))
|
||||
} else {
|
||||
paths.push(path)
|
||||
}
|
||||
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
paths.push(...collectOutputPaths(value, blockType, subBlocks, path))
|
||||
} else {
|
||||
if (
|
||||
blockType === 'api_trigger' ||
|
||||
blockType === 'input_trigger' ||
|
||||
blockType === 'generic_webhook'
|
||||
) {
|
||||
if (normalizedInputFormat.length > 0 || blockType !== 'generic_webhook') {
|
||||
outputs = {}
|
||||
}
|
||||
outputs = applyInputFormatFields(normalizedInputFormat, outputs)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!Array.isArray(subBlocks.inputFormat.value) &&
|
||||
(blockType === 'api_trigger' || blockType === 'input_trigger')
|
||||
) {
|
||||
// If no inputFormat defined, API/Input trigger has no outputs
|
||||
outputs = {}
|
||||
paths.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
return outputs
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block config has an inputFormat sub-block
|
||||
*/
|
||||
function hasInputFormat(blockConfig: BlockConfig): boolean {
|
||||
return blockConfig.subBlocks?.some((sb) => sb.type === 'input-format') || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output paths for a block (for tag dropdown)
|
||||
*/
|
||||
export function getBlockOutputPaths(
|
||||
blockType: string,
|
||||
subBlocks?: Record<string, any>,
|
||||
triggerMode?: boolean
|
||||
): string[] {
|
||||
const outputs = getBlockOutputs(blockType, subBlocks, triggerMode)
|
||||
return collectOutputPaths(outputs, blockType, subBlocks)
|
||||
}
|
||||
|
||||
// Recursively collect all paths from nested outputs
|
||||
const paths: string[] = []
|
||||
|
||||
function collectPaths(obj: Record<string, any>, prefix = ''): void {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key
|
||||
|
||||
// For start_trigger, skip reserved fields at root level (they're always present but hidden from dropdown)
|
||||
if (
|
||||
blockType === 'start_trigger' &&
|
||||
!prefix &&
|
||||
['input', 'conversationId', 'files'].includes(key)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If value has 'type' property, it's a leaf node (output definition)
|
||||
if (value && typeof value === 'object' && 'type' in value) {
|
||||
// Special handling for 'files' type - expand to show array element properties
|
||||
if (value.type === 'files') {
|
||||
// Show properties without [0] for cleaner display
|
||||
// The tag dropdown will add [0] automatically when inserting
|
||||
// Only expose user-accessible fields (matches UserFile interface)
|
||||
paths.push(`${path}.id`)
|
||||
paths.push(`${path}.name`)
|
||||
paths.push(`${path}.url`)
|
||||
paths.push(`${path}.size`)
|
||||
paths.push(`${path}.type`)
|
||||
} else {
|
||||
paths.push(path)
|
||||
}
|
||||
}
|
||||
// If value is an object without 'type', recurse into it
|
||||
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
collectPaths(value, path)
|
||||
}
|
||||
// Otherwise treat as a leaf node
|
||||
else {
|
||||
paths.push(path)
|
||||
}
|
||||
function getFilePropertyType(outputs: OutputDefinition, pathParts: string[]): string | null {
|
||||
const lastPart = pathParts[pathParts.length - 1]
|
||||
if (!lastPart || !USER_FILE_PROPERTY_TYPES[lastPart as keyof typeof USER_FILE_PROPERTY_TYPES]) {
|
||||
return null
|
||||
}
|
||||
|
||||
let current: any = outputs
|
||||
for (const part of pathParts.slice(0, -1)) {
|
||||
if (!current || typeof current !== 'object') {
|
||||
return null
|
||||
}
|
||||
current = current[part]
|
||||
}
|
||||
|
||||
if (current && typeof current === 'object' && 'type' in current && current.type === 'files') {
|
||||
return USER_FILE_PROPERTY_TYPES[lastPart as keyof typeof USER_FILE_PROPERTY_TYPES]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function traverseOutputPath(outputs: OutputDefinition, pathParts: string[]): any {
|
||||
let current: any = outputs
|
||||
|
||||
for (const part of pathParts) {
|
||||
if (!current || typeof current !== 'object') {
|
||||
return null
|
||||
}
|
||||
current = current[part]
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
function extractType(value: any): string {
|
||||
if (!value) return 'any'
|
||||
|
||||
if (typeof value === 'object' && 'type' in value) {
|
||||
return value.type
|
||||
}
|
||||
|
||||
collectPaths(outputs)
|
||||
return paths
|
||||
return typeof value === 'string' ? value : 'any'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of a specific output path (supports nested paths like "email.subject")
|
||||
*/
|
||||
export function getBlockOutputType(
|
||||
blockType: string,
|
||||
outputPath: string,
|
||||
@@ -226,44 +291,14 @@ export function getBlockOutputType(
|
||||
): string {
|
||||
const outputs = getBlockOutputs(blockType, subBlocks, triggerMode)
|
||||
|
||||
const arrayIndexRegex = /\[(\d+)\]/g
|
||||
const cleanPath = outputPath.replace(arrayIndexRegex, '')
|
||||
const cleanPath = outputPath.replace(/\[(\d+)\]/g, '')
|
||||
const pathParts = cleanPath.split('.').filter(Boolean)
|
||||
|
||||
const filePropertyTypes: Record<string, string> = {
|
||||
id: 'string',
|
||||
name: 'string',
|
||||
url: 'string',
|
||||
size: 'number',
|
||||
type: 'string',
|
||||
const filePropertyType = getFilePropertyType(outputs, pathParts)
|
||||
if (filePropertyType) {
|
||||
return filePropertyType
|
||||
}
|
||||
|
||||
const lastPart = pathParts[pathParts.length - 1]
|
||||
if (lastPart && filePropertyTypes[lastPart]) {
|
||||
let current: any = outputs
|
||||
for (const part of pathParts.slice(0, -1)) {
|
||||
if (!current || typeof current !== 'object') break
|
||||
current = current[part]
|
||||
}
|
||||
if (current && typeof current === 'object' && 'type' in current && current.type === 'files') {
|
||||
return filePropertyTypes[lastPart]
|
||||
}
|
||||
}
|
||||
|
||||
let current: any = outputs
|
||||
|
||||
for (const part of pathParts) {
|
||||
if (!current || typeof current !== 'object') {
|
||||
return 'any'
|
||||
}
|
||||
current = current[part]
|
||||
}
|
||||
|
||||
if (!current) return 'any'
|
||||
|
||||
if (typeof current === 'object' && 'type' in current) {
|
||||
return current.type
|
||||
}
|
||||
|
||||
return typeof current === 'string' ? current : 'any'
|
||||
const value = traverseOutputPath(outputs, pathParts)
|
||||
return extractType(value)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TRIGGER_TYPES } from '@/lib/workflows/triggers'
|
||||
import { getAllBlocks, getBlock } from '@/blocks'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
@@ -99,8 +100,7 @@ export function getTriggerDisplayName(blockType: string): string {
|
||||
const block = getBlock(blockType)
|
||||
if (!block) return blockType
|
||||
|
||||
// Special case for generic_webhook - show as "Webhook" in UI
|
||||
if (blockType === 'generic_webhook') {
|
||||
if (blockType === TRIGGER_TYPES.GENERIC_WEBHOOK) {
|
||||
return 'Webhook'
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export const TRIGGER_TYPES = {
|
||||
CHAT: 'chat_trigger',
|
||||
API: 'api_trigger',
|
||||
WEBHOOK: 'webhook',
|
||||
GENERIC_WEBHOOK: 'generic_webhook',
|
||||
SCHEDULE: 'schedule',
|
||||
START: 'start_trigger',
|
||||
STARTER: 'starter', // Legacy
|
||||
@@ -334,8 +335,7 @@ export class TriggerUtils {
|
||||
// Use the block's actual name from the registry
|
||||
const block = getBlock(triggerType)
|
||||
if (block) {
|
||||
// Special case for generic_webhook - show as "Webhook" in UI
|
||||
if (triggerType === 'generic_webhook') {
|
||||
if (triggerType === TRIGGER_TYPES.GENERIC_WEBHOOK) {
|
||||
return 'Webhook'
|
||||
}
|
||||
return block.name
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface InputFormatField {
|
||||
name?: string
|
||||
type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files' | string
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
export const USER_FILE_ACCESSIBLE_PROPERTIES = ['id', 'name', 'url', 'size', 'type'] as const
|
||||
|
||||
export type UserFileAccessibleProperty = (typeof USER_FILE_ACCESSIBLE_PROPERTIES)[number]
|
||||
|
||||
export const USER_FILE_PROPERTY_TYPES: Record<UserFileAccessibleProperty, string> = {
|
||||
id: 'string',
|
||||
name: 'string',
|
||||
url: 'string',
|
||||
size: 'number',
|
||||
type: 'string',
|
||||
} as const
|
||||
|
||||
export const START_BLOCK_RESERVED_FIELDS = ['input', 'conversationId', 'files'] as const
|
||||
|
||||
export type StartBlockReservedField = (typeof START_BLOCK_RESERVED_FIELDS)[number]
|
||||
|
||||
export type LoopType = 'for' | 'forEach' | 'while' | 'doWhile'
|
||||
|
||||
export type ParallelType = 'collection' | 'count'
|
||||
Reference in New Issue
Block a user