This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
'use client'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Event } from '@/app/components/tools/types'
import type { FC } from 'react'
import type { PluginTriggerVarInputs } from '@/app/components/workflow/nodes/trigger-plugin/types'
import TriggerFormItem from './item'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema[]
value: PluginTriggerVarInputs
onChange: (value: PluginTriggerVarInputs) => void
onOpen?: (index: number) => void
inPanel?: boolean
currentEvent?: Event
currentProvider?: TriggerWithProvider
extraParams?: Record<string, any>
disableVariableInsertion?: boolean
}
const TriggerForm: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
inPanel,
currentEvent,
currentProvider,
extraParams,
disableVariableInsertion = false,
}) => {
return (
<div className='space-y-1'>
{
schema.map((schema, index) => (
<TriggerFormItem
key={index}
readOnly={readOnly}
nodeId={nodeId}
schema={schema}
value={value}
onChange={onChange}
inPanel={inPanel}
currentEvent={currentEvent}
currentProvider={currentProvider}
extraParams={extraParams}
disableVariableInsertion={disableVariableInsertion}
/>
))
}
</div>
)
}
export default TriggerForm

View File

@@ -0,0 +1,112 @@
'use client'
import type { FC } from 'react'
import {
RiBracesLine,
} from '@remixicon/react'
import type { PluginTriggerVarInputs } from '@/app/components/workflow/nodes/trigger-plugin/types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item'
import { useBoolean } from 'ahooks'
import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal'
import type { Event } from '@/app/components/tools/types'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema
value: PluginTriggerVarInputs
onChange: (value: PluginTriggerVarInputs) => void
inPanel?: boolean
currentEvent?: Event
currentProvider?: TriggerWithProvider
extraParams?: Record<string, any>
disableVariableInsertion?: boolean
}
const TriggerFormItem: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
inPanel,
currentEvent,
currentProvider,
extraParams,
disableVariableInsertion = false,
}) => {
const language = useLanguage()
const { name, label, type, required, tooltip, input_schema } = schema
const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array
const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
const [isShowSchema, {
setTrue: showSchema,
setFalse: hideSchema,
}] = useBoolean(false)
return (
<div className='space-y-0.5 py-1'>
<div>
<div className='flex h-6 items-center'>
<div className='system-sm-medium text-text-secondary'>{label[language] || label.en_US}</div>
{required && (
<div className='system-xs-regular ml-1 text-text-destructive-secondary'>*</div>
)}
{!showDescription && tooltip && (
<Tooltip
popupContent={<div className='w-[200px]'>
{tooltip[language] || tooltip.en_US}
</div>}
triggerClassName='ml-1 w-4 h-4'
asChild={false}
/>
)}
{showSchemaButton && (
<>
<div className='system-xs-regular ml-1 mr-0.5 text-text-quaternary'>·</div>
<Button
variant='ghost'
size='small'
onClick={showSchema}
className='system-xs-regular px-1 text-text-tertiary'
>
<RiBracesLine className='mr-1 size-3.5' />
<span>JSON Schema</span>
</Button>
</>
)}
</div>
{showDescription && tooltip && (
<div className='body-xs-regular pb-0.5 text-text-tertiary'>{tooltip[language] || tooltip.en_US}</div>
)}
</div>
<FormInputItem
readOnly={readOnly}
nodeId={nodeId}
schema={schema}
value={value}
onChange={onChange}
inPanel={inPanel}
currentTool={currentEvent}
currentProvider={currentProvider}
providerType='trigger'
extraParams={extraParams}
disableVariableInsertion={disableVariableInsertion}
/>
{isShowSchema && (
<SchemaModal
isShow
onClose={hideSchema}
rootName={name}
schema={input_schema!}
/>
)}
</div>
)
}
export default TriggerFormItem

View File

@@ -0,0 +1,297 @@
import type { SchemaTypeDefinition } from '@/service/use-common'
import type { NodeDefault, Var } from '../../types'
import { BlockEnum, VarType } from '../../types'
import { genNodeMetaData } from '../../utils'
import { VarKindType } from '../_base/types'
import { type Field, type StructuredOutput, Type } from '../llm/types'
import type { PluginTriggerNodeType } from './types'
const normalizeJsonSchemaType = (schema: any): string | undefined => {
if (!schema) return undefined
const { type, properties, items, oneOf, anyOf, allOf } = schema
if (Array.isArray(type))
return type.find((item: string | null) => item && item !== 'null') || type[0]
if (typeof type === 'string')
return type
const compositeCandidates = [oneOf, anyOf, allOf]
.filter((entry): entry is any[] => Array.isArray(entry))
.flat()
for (const candidate of compositeCandidates) {
const normalized = normalizeJsonSchemaType(candidate)
if (normalized)
return normalized
}
if (properties)
return 'object'
if (items)
return 'array'
return undefined
}
const pickItemSchema = (schema: any) => {
if (!schema || !schema.items)
return undefined
return Array.isArray(schema.items) ? schema.items[0] : schema.items
}
const extractSchemaType = (schema: any, _schemaTypeDefinitions?: SchemaTypeDefinition[]): string | undefined => {
if (!schema)
return undefined
const schemaTypeFromSchema = schema.schema_type || schema.schemaType
if (typeof schemaTypeFromSchema === 'string' && schemaTypeFromSchema.trim().length > 0)
return schemaTypeFromSchema
return undefined
}
const resolveVarType = (
schema: any,
schemaTypeDefinitions?: SchemaTypeDefinition[],
): { type: VarType; schemaType?: string } => {
const schemaType = extractSchemaType(schema, schemaTypeDefinitions)
const normalizedType = normalizeJsonSchemaType(schema)
switch (normalizedType) {
case 'string':
return { type: VarType.string, schemaType }
case 'number':
return { type: VarType.number, schemaType }
case 'integer':
return { type: VarType.integer, schemaType }
case 'boolean':
return { type: VarType.boolean, schemaType }
case 'object':
return { type: VarType.object, schemaType }
case 'array': {
const itemSchema = pickItemSchema(schema)
if (!itemSchema)
return { type: VarType.array, schemaType }
const { type: itemType, schemaType: itemSchemaType } = resolveVarType(itemSchema, schemaTypeDefinitions)
const resolvedSchemaType = schemaType || itemSchemaType
if (itemSchemaType === 'file')
return { type: VarType.arrayFile, schemaType: resolvedSchemaType }
switch (itemType) {
case VarType.string:
return { type: VarType.arrayString, schemaType: resolvedSchemaType }
case VarType.number:
case VarType.integer:
return { type: VarType.arrayNumber, schemaType: resolvedSchemaType }
case VarType.boolean:
return { type: VarType.arrayBoolean, schemaType: resolvedSchemaType }
case VarType.object:
return { type: VarType.arrayObject, schemaType: resolvedSchemaType }
case VarType.file:
return { type: VarType.arrayFile, schemaType: resolvedSchemaType }
default:
return { type: VarType.array, schemaType: resolvedSchemaType }
}
}
default:
return { type: VarType.any, schemaType }
}
}
const toFieldType = (normalizedType: string | undefined, schemaType?: string): Type => {
if (schemaType === 'file')
return normalizedType === 'array' ? Type.array : Type.file
switch (normalizedType) {
case 'number':
case 'integer':
return Type.number
case 'boolean':
return Type.boolean
case 'object':
return Type.object
case 'array':
return Type.array
case 'string':
default:
return Type.string
}
}
const toArrayItemType = (type: Type): Exclude<Type, Type.array> => {
if (type === Type.array)
return Type.object
return type as Exclude<Type, Type.array>
}
const convertJsonSchemaToField = (schema: any, schemaTypeDefinitions?: SchemaTypeDefinition[]): Field => {
const schemaType = extractSchemaType(schema, schemaTypeDefinitions)
const normalizedType = normalizeJsonSchemaType(schema)
const fieldType = toFieldType(normalizedType, schemaType)
const field: Field = {
type: fieldType,
}
if (schema?.description)
field.description = schema.description
if (schemaType)
field.schemaType = schemaType
if (Array.isArray(schema?.enum))
field.enum = schema.enum
if (fieldType === Type.object) {
const properties = schema?.properties || {}
field.properties = Object.entries(properties).reduce((acc, [key, value]) => {
acc[key] = convertJsonSchemaToField(value, schemaTypeDefinitions)
return acc
}, {} as Record<string, Field>)
const required = Array.isArray(schema?.required) ? schema.required.filter(Boolean) : undefined
field.required = required && required.length > 0 ? required : undefined
field.additionalProperties = false
}
if (fieldType === Type.array) {
const itemSchema = pickItemSchema(schema)
if (itemSchema) {
const itemField = convertJsonSchemaToField(itemSchema, schemaTypeDefinitions)
const { type, ...rest } = itemField
field.items = {
...rest,
type: toArrayItemType(type),
}
}
}
return field
}
const buildOutputVars = (schema: Record<string, any>, schemaTypeDefinitions?: SchemaTypeDefinition[]): Var[] => {
if (!schema || typeof schema !== 'object')
return []
const properties = schema.properties as Record<string, any> | undefined
if (!properties)
return []
return Object.entries(properties).map(([name, propertySchema]) => {
const { type, schemaType } = resolveVarType(propertySchema, schemaTypeDefinitions)
const normalizedType = normalizeJsonSchemaType(propertySchema)
const varItem: Var = {
variable: name,
type,
des: propertySchema?.description,
...(schemaType ? { schemaType } : {}),
}
if (normalizedType === 'object') {
const childProperties = propertySchema?.properties
? Object.entries(propertySchema.properties).reduce((acc, [key, value]) => {
acc[key] = convertJsonSchemaToField(value, schemaTypeDefinitions)
return acc
}, {} as Record<string, Field>)
: {}
const required = Array.isArray(propertySchema?.required) ? propertySchema.required.filter(Boolean) : undefined
varItem.children = {
schema: {
type: Type.object,
properties: childProperties,
required: required && required.length > 0 ? required : undefined,
additionalProperties: false,
},
} as StructuredOutput
}
return varItem
})
}
const metaData = genNodeMetaData({
sort: 1,
type: BlockEnum.TriggerPlugin,
helpLinkUri: 'plugin-trigger',
isStart: true,
})
const nodeDefault: NodeDefault<PluginTriggerNodeType> = {
metaData,
defaultValue: {
plugin_id: '',
event_name: '',
event_parameters: {},
// event_type: '',
config: {},
},
checkValid(payload: PluginTriggerNodeType, t: any, moreDataForCheckValid: {
triggerInputsSchema?: Array<{
variable: string
label: string
required?: boolean
}>
isReadyForCheckValid?: boolean
} = {}) {
let errorMessage = ''
if (!payload.subscription_id)
errorMessage = t('workflow.nodes.triggerPlugin.subscriptionRequired')
const {
triggerInputsSchema = [],
isReadyForCheckValid = true,
} = moreDataForCheckValid || {}
if (!errorMessage && isReadyForCheckValid) {
triggerInputsSchema.filter(field => field.required).forEach((field) => {
if (errorMessage)
return
const rawParam = payload.event_parameters?.[field.variable]
?? (payload.config as Record<string, any> | undefined)?.[field.variable]
if (!rawParam) {
errorMessage = t('workflow.errorMsg.fieldRequired', { field: field.label })
return
}
const targetParam = typeof rawParam === 'object' && rawParam !== null && 'type' in rawParam
? rawParam as { type: VarKindType; value: any }
: { type: VarKindType.constant, value: rawParam }
const { type, value } = targetParam
if (type === VarKindType.variable) {
if (!value || (Array.isArray(value) && value.length === 0))
errorMessage = t('workflow.errorMsg.fieldRequired', { field: field.label })
}
else {
if (
value === undefined
|| value === null
|| value === ''
|| (Array.isArray(value) && value.length === 0)
)
errorMessage = t('workflow.errorMsg.fieldRequired', { field: field.label })
}
})
}
return {
isValid: !errorMessage,
errorMessage,
}
},
getOutputVars(payload, _allPluginInfoList, _ragVars, { schemaTypeDefinitions } = { schemaTypeDefinitions: [] }) {
const schema = payload.output_schema || {}
return buildOutputVars(schema, schemaTypeDefinitions)
},
}
export default nodeDefault

View File

@@ -0,0 +1,162 @@
import { useCallback, useState } from 'react'
import {
useBuildTriggerSubscription,
useCreateTriggerSubscriptionBuilder,
useUpdateTriggerSubscriptionBuilder,
useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
// Helper function to serialize complex values to strings for backend encryption
const serializeFormValues = (values: Record<string, any>): Record<string, string> => {
const result: Record<string, string> = {}
for (const [key, value] of Object.entries(values)) {
if (value === null || value === undefined)
result[key] = ''
else if (typeof value === 'object')
result[key] = JSON.stringify(value)
else
result[key] = String(value)
}
return result
}
export type AuthFlowStep = 'auth' | 'params' | 'complete'
export type AuthFlowState = {
step: AuthFlowStep
builderId: string
isLoading: boolean
error: string | null
}
export type AuthFlowActions = {
startAuth: () => Promise<void>
verifyAuth: (credentials: Record<string, any>) => Promise<void>
completeConfig: (parameters: Record<string, any>, properties?: Record<string, any>, name?: string) => Promise<void>
reset: () => void
}
export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState & AuthFlowActions => {
const [step, setStep] = useState<AuthFlowStep>('auth')
const [builderId, setBuilderId] = useState<string>('')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const createBuilder = useCreateTriggerSubscriptionBuilder()
const updateBuilder = useUpdateTriggerSubscriptionBuilder()
const verifyBuilder = useVerifyTriggerSubscriptionBuilder()
const buildSubscription = useBuildTriggerSubscription()
const startAuth = useCallback(async () => {
if (builderId) return // Prevent multiple calls if already started
setIsLoading(true)
setError(null)
try {
const response = await createBuilder.mutateAsync({
provider: provider.name,
})
setBuilderId(response.subscription_builder.id)
setStep('auth')
}
catch (err: any) {
setError(err.message || 'Failed to start authentication flow')
throw err
}
finally {
setIsLoading(false)
}
}, [provider.name, createBuilder, builderId])
const verifyAuth = useCallback(async (credentials: Record<string, any>) => {
if (!builderId) {
setError('No builder ID available')
return
}
setIsLoading(true)
setError(null)
try {
await updateBuilder.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
credentials: serializeFormValues(credentials),
})
await verifyBuilder.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
})
setStep('params')
}
catch (err: any) {
setError(err.message || 'Authentication verification failed')
throw err
}
finally {
setIsLoading(false)
}
}, [provider.name, builderId, updateBuilder, verifyBuilder])
const completeConfig = useCallback(async (
parameters: Record<string, any>,
properties: Record<string, any> = {},
name?: string,
) => {
if (!builderId) {
setError('No builder ID available')
return
}
setIsLoading(true)
setError(null)
try {
await updateBuilder.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
parameters: serializeFormValues(parameters),
properties: serializeFormValues(properties),
name,
})
await buildSubscription.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
})
setStep('complete')
}
catch (err: any) {
setError(err.message || 'Configuration failed')
throw err
}
finally {
setIsLoading(false)
}
}, [provider.name, builderId, updateBuilder, buildSubscription])
const reset = useCallback(() => {
setStep('auth')
setBuilderId('')
setIsLoading(false)
setError(null)
}, [])
return {
step,
builderId,
isLoading,
error,
startAuth,
verifyAuth,
completeConfig,
reset,
}
}

View File

@@ -0,0 +1,126 @@
import NodeStatus, { NodeStatusEnum } from '@/app/components/base/node-status'
import type { NodeProps } from '@/app/components/workflow/types'
import type { FC } from 'react'
import React, { useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks/use-node-data-update'
import type { PluginTriggerNodeType } from './types'
import useConfig from './use-config'
const formatConfigValue = (rawValue: any): string => {
if (rawValue === null || rawValue === undefined)
return ''
if (typeof rawValue === 'string' || typeof rawValue === 'number' || typeof rawValue === 'boolean')
return String(rawValue)
if (Array.isArray(rawValue))
return rawValue.join('.')
if (typeof rawValue === 'object') {
const { value } = rawValue as { value?: any }
if (value === null || value === undefined)
return ''
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
return String(value)
if (Array.isArray(value))
return value.join('.')
try {
return JSON.stringify(value)
}
catch {
return ''
}
}
return ''
}
const Node: FC<NodeProps<PluginTriggerNodeType>> = ({
id,
data,
}) => {
const { subscriptions } = useConfig(id, data)
const { config = {}, subscription_id } = data
const configKeys = Object.keys(config)
const {
isChecking,
isMissing,
uniqueIdentifier,
canInstall,
onInstallSuccess,
shouldDim,
} = useNodePluginInstallation(data)
const { handleNodeDataUpdate } = useNodeDataUpdate()
const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier
const shouldLock = !isChecking && isMissing && canInstall && Boolean(uniqueIdentifier)
useEffect(() => {
if (data._pluginInstallLocked === shouldLock && data._dimmed === shouldDim)
return
handleNodeDataUpdate({
id,
data: {
_pluginInstallLocked: shouldLock,
_dimmed: shouldDim,
},
})
}, [data._pluginInstallLocked, data._dimmed, handleNodeDataUpdate, id, shouldDim, shouldLock])
const { t } = useTranslation()
const isValidSubscription = useMemo(() => {
return subscription_id && subscriptions?.some(sub => sub.id === subscription_id)
}, [subscription_id, subscriptions])
return (
<div className="relative mb-1 px-3 py-1">
{showInstallButton && (
<div className="pointer-events-auto absolute right-3 top-[-32px] z-40">
<InstallPluginButton
size="small"
extraIdentifiers={[
data.plugin_id,
data.provider_id,
data.provider_name,
].filter(Boolean) as string[]}
className="!font-medium !text-text-accent"
uniqueIdentifier={uniqueIdentifier!}
onSuccess={onInstallSuccess}
/>
</div>
)}
<div className="space-y-0.5" aria-disabled={shouldDim}>
{!isValidSubscription && <NodeStatus status={NodeStatusEnum.warning} message={t('pluginTrigger.node.status.warning')} />}
{isValidSubscription && configKeys.map((key, index) => (
<div
key={index}
className="flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary"
>
<div
title={key}
className="max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary"
>
{key}
</div>
<div
title={formatConfigValue(config[key])}
className="w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary"
>
{(() => {
const displayValue = formatConfigValue(config[key])
if (displayValue.includes('secret'))
return '********'
return displayValue
})()}
</div>
</div>
))}
</div>
</div>
)
}
export default React.memo(Node)

View File

@@ -0,0 +1,94 @@
import type { FC } from 'react'
import React from 'react'
import type { PluginTriggerNodeType } from './types'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import type { NodePanelProps } from '@/app/components/workflow/types'
import useConfig from './use-config'
import TriggerForm from './components/trigger-form'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../llm/types'
import { BlockEnum } from '@/app/components/workflow/types'
const Panel: FC<NodePanelProps<PluginTriggerNodeType>> = ({
id,
data,
}) => {
const {
readOnly,
triggerParameterSchema,
triggerParameterValue,
setTriggerParameterValue,
outputSchema,
hasObjectOutput,
currentProvider,
currentEvent,
subscriptionSelected,
} = useConfig(id, data)
const disableVariableInsertion = data.type === BlockEnum.TriggerPlugin
// Convert output schema to VarItem format
const outputVars = Object.entries(outputSchema.properties || {}).map(([name, schema]: [string, any]) => ({
name,
type: schema.type || 'string',
description: schema.description || '',
}))
return (
<div className='mt-2'>
{/* Dynamic Parameters Form - Only show when authenticated */}
{triggerParameterSchema.length > 0 && subscriptionSelected && (
<>
<div className='px-4 pb-4'>
<TriggerForm
readOnly={readOnly}
nodeId={id}
schema={triggerParameterSchema as any}
value={triggerParameterValue}
onChange={setTriggerParameterValue}
currentProvider={currentProvider}
currentEvent={currentEvent}
disableVariableInsertion={disableVariableInsertion}
/>
</div>
<Split />
</>
)}
{/* Output Variables - Always show */}
<OutputVars>
<>
{outputVars.map(varItem => (
<VarItem
key={varItem.name}
name={varItem.name}
type={varItem.type}
description={varItem.description}
isIndent={hasObjectOutput}
/>
))}
{Object.entries(outputSchema.properties || {}).map(([name, schema]: [string, any]) => (
<div key={name}>
{schema.type === 'object' ? (
<StructureOutputItem
rootClassName='code-sm-semibold text-text-secondary'
payload={{
schema: {
type: Type.object,
properties: {
[name]: schema,
},
additionalProperties: false,
},
}}
/>
) : null}
</div>
))}
</>
</OutputVars>
</div>
)
}
export default React.memo(Panel)

View File

@@ -0,0 +1,24 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { CollectionType } from '@/app/components/tools/types'
import type { ResourceVarInputs } from '../_base/types'
export type PluginTriggerNodeType = CommonNodeType & {
provider_id: string
provider_type: CollectionType
provider_name: string
event_name: string
event_label: string
event_parameters: PluginTriggerVarInputs
event_configurations: Record<string, any>
output_schema: Record<string, any>
parameters_schema?: Record<string, any>[]
version?: string
event_node_version?: string
plugin_id?: string
config?: Record<string, any>
plugin_unique_identifier?: string
}
// Use base types directly
export { VarKindType as PluginTriggerVarType } from '../_base/types'
export type PluginTriggerVarInputs = ResourceVarInputs

View File

@@ -0,0 +1,27 @@
import { useCallback } from 'react'
import type { PluginTriggerNodeType } from './types'
import { useAllTriggerPlugins } from '@/service/use-triggers'
import { useGetLanguage } from '@/context/i18n'
import { getTriggerCheckParams } from '@/app/components/workflow/utils/trigger'
type Params = {
id: string
payload: PluginTriggerNodeType
}
const useGetDataForCheckMore = ({
payload,
}: Params) => {
const { data: triggerPlugins } = useAllTriggerPlugins()
const language = useGetLanguage()
const getData = useCallback(() => {
return getTriggerCheckParams(payload, triggerPlugins, language)
}, [payload, triggerPlugins, language])
return {
getData,
}
}
export default useGetDataForCheckMore

View File

@@ -0,0 +1,233 @@
import { useCallback, useEffect, useMemo } from 'react'
import { produce } from 'immer'
import type { PluginTriggerNodeType } from './types'
import type { PluginTriggerVarInputs } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import {
useAllTriggerPlugins,
useTriggerSubscriptions,
} from '@/service/use-triggers'
import {
getConfiguredValue,
toolParametersToFormSchemas,
} from '@/app/components/tools/utils/to-form-schema'
import type { InputVar } from '@/app/components/workflow/types'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
import type { Event } from '@/app/components/tools/types'
import { VarKindType } from '../_base/types'
const normalizeEventParameters = (
params: PluginTriggerVarInputs | Record<string, unknown> | null | undefined,
{ allowScalars = false }: { allowScalars?: boolean } = {},
): PluginTriggerVarInputs => {
if (!params || typeof params !== 'object' || Array.isArray(params))
return {} as PluginTriggerVarInputs
return Object.entries(params).reduce((acc, [key, entry]) => {
if (!entry && entry !== 0 && entry !== false)
return acc
if (
typeof entry === 'object'
&& !Array.isArray(entry)
&& 'type' in entry
&& 'value' in entry
) {
const normalizedEntry = { ...(entry as PluginTriggerVarInputs[string]) }
if (normalizedEntry.type === VarKindType.mixed)
normalizedEntry.type = VarKindType.constant
acc[key] = normalizedEntry
return acc
}
if (!allowScalars)
return acc
if (typeof entry === 'string') {
acc[key] = {
type: VarKindType.constant,
value: entry,
}
return acc
}
if (typeof entry === 'number' || typeof entry === 'boolean') {
acc[key] = {
type: VarKindType.constant,
value: entry,
}
return acc
}
if (Array.isArray(entry) && entry.every(item => typeof item === 'string')) {
acc[key] = {
type: VarKindType.variable,
value: entry,
}
}
return acc
}, {} as PluginTriggerVarInputs)
}
const useConfig = (id: string, payload: PluginTriggerNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { data: triggerPlugins = [] } = useAllTriggerPlugins()
const { inputs, setInputs: doSetInputs } = useNodeCrud<PluginTriggerNodeType>(
id,
payload,
)
const {
provider_id,
provider_name,
event_name,
config = {},
event_parameters: rawEventParameters = {},
subscription_id,
} = inputs
const event_parameters = useMemo(
() => normalizeEventParameters(rawEventParameters as PluginTriggerVarInputs),
[rawEventParameters],
)
const legacy_config_parameters = useMemo(
() => normalizeEventParameters(config as PluginTriggerVarInputs, { allowScalars: true }),
[config],
)
const currentProvider = useMemo<TriggerWithProvider | undefined>(() => {
return triggerPlugins.find(
provider =>
provider.name === provider_name
|| provider.id === provider_id
|| (provider_id && provider.plugin_id === provider_id),
)
}, [triggerPlugins, provider_name, provider_id])
const { data: subscriptions = [] } = useTriggerSubscriptions(provider_id || '')
const subscriptionSelected = useMemo(() => {
return subscriptions?.find(s => s.id === subscription_id)
}, [subscriptions, subscription_id])
const currentEvent = useMemo<Event | undefined>(() => {
return currentProvider?.events.find(
event => event.name === event_name,
)
}, [currentProvider, event_name])
// Dynamic trigger parameters (from specific trigger.parameters)
const triggerSpecificParameterSchema = useMemo(() => {
if (!currentEvent) return []
return toolParametersToFormSchemas(currentEvent.parameters)
}, [currentEvent])
// Combined parameter schema (subscription + trigger specific)
const triggerParameterSchema = useMemo(() => {
const schemaMap = new Map()
triggerSpecificParameterSchema.forEach((schema) => {
schemaMap.set(schema.variable || schema.name, schema)
})
return Array.from(schemaMap.values())
}, [triggerSpecificParameterSchema])
const triggerParameterValue = useMemo(() => {
if (!triggerParameterSchema.length)
return {} as PluginTriggerVarInputs
const hasStoredParameters = event_parameters && Object.keys(event_parameters).length > 0
const baseValue = hasStoredParameters ? event_parameters : legacy_config_parameters
const configuredValue = getConfiguredValue(baseValue, triggerParameterSchema) as PluginTriggerVarInputs
return normalizeEventParameters(configuredValue)
}, [triggerParameterSchema, event_parameters, legacy_config_parameters])
useEffect(() => {
if (!triggerParameterSchema.length)
return
if (event_parameters && Object.keys(event_parameters).length > 0)
return
if (!triggerParameterValue || Object.keys(triggerParameterValue).length === 0)
return
const newInputs = produce(inputs, (draft) => {
draft.event_parameters = triggerParameterValue
draft.config = triggerParameterValue
})
doSetInputs(newInputs)
}, [
doSetInputs,
event_parameters,
inputs,
triggerParameterSchema,
triggerParameterValue,
])
const setTriggerParameterValue = useCallback(
(value: PluginTriggerVarInputs) => {
const sanitizedValue = normalizeEventParameters(value)
const newInputs = produce(inputs, (draft) => {
draft.event_parameters = sanitizedValue
draft.config = sanitizedValue
})
doSetInputs(newInputs)
},
[inputs, doSetInputs],
)
const setInputVar = useCallback(
(variable: InputVar, varDetail: InputVar) => {
const newInputs = produce(inputs, (draft) => {
const nextEventParameters = normalizeEventParameters({
...draft.event_parameters,
[variable.variable]: {
type: VarKindType.variable,
value: varDetail.variable,
},
} as PluginTriggerVarInputs)
draft.event_parameters = nextEventParameters
draft.config = nextEventParameters
})
doSetInputs(newInputs)
},
[inputs, doSetInputs],
)
// Get output schema
const outputSchema = useMemo(() => {
return currentEvent?.output_schema || {}
}, [currentEvent])
// Check if trigger has complex output structure
const hasObjectOutput = useMemo(() => {
const properties = outputSchema.properties || {}
return Object.values(properties).some(
(prop: any) => prop.type === 'object',
)
}, [outputSchema])
return {
readOnly,
inputs,
currentProvider,
currentEvent,
triggerParameterSchema,
triggerParameterValue,
setTriggerParameterValue,
setInputVar,
outputSchema,
hasObjectOutput,
subscriptions,
subscriptionSelected,
}
}
export default useConfig

View File

@@ -0,0 +1,308 @@
import { deepSanitizeFormValues, findMissingRequiredField, sanitizeFormValues } from '../form-helpers'
describe('Form Helpers', () => {
describe('sanitizeFormValues', () => {
it('should convert null values to empty strings', () => {
const input = { field1: null, field2: 'value', field3: undefined }
const result = sanitizeFormValues(input)
expect(result).toEqual({
field1: '',
field2: 'value',
field3: '',
})
})
it('should convert undefined values to empty strings', () => {
const input = { field1: undefined, field2: 'test' }
const result = sanitizeFormValues(input)
expect(result).toEqual({
field1: '',
field2: 'test',
})
})
it('should convert non-string values to strings', () => {
const input = { number: 123, boolean: true, string: 'test' }
const result = sanitizeFormValues(input)
expect(result).toEqual({
number: '123',
boolean: 'true',
string: 'test',
})
})
it('should handle empty objects', () => {
const result = sanitizeFormValues({})
expect(result).toEqual({})
})
it('should handle objects with mixed value types', () => {
const input = {
null_field: null,
undefined_field: undefined,
zero: 0,
false_field: false,
empty_string: '',
valid_string: 'test',
}
const result = sanitizeFormValues(input)
expect(result).toEqual({
null_field: '',
undefined_field: '',
zero: '0',
false_field: 'false',
empty_string: '',
valid_string: 'test',
})
})
})
describe('deepSanitizeFormValues', () => {
it('should handle nested objects', () => {
const input = {
level1: {
field1: null,
field2: 'value',
level2: {
field3: undefined,
field4: 'nested',
},
},
simple: 'test',
}
const result = deepSanitizeFormValues(input)
expect(result).toEqual({
level1: {
field1: '',
field2: 'value',
level2: {
field3: '',
field4: 'nested',
},
},
simple: 'test',
})
})
it('should handle arrays correctly', () => {
const input = {
array: [1, 2, 3],
nested: {
array: ['a', null, 'c'],
},
}
const result = deepSanitizeFormValues(input)
expect(result).toEqual({
array: [1, 2, 3],
nested: {
array: ['a', null, 'c'],
},
})
})
it('should handle null and undefined at root level', () => {
const input = {
null_field: null,
undefined_field: undefined,
nested: {
null_nested: null,
},
}
const result = deepSanitizeFormValues(input)
expect(result).toEqual({
null_field: '',
undefined_field: '',
nested: {
null_nested: '',
},
})
})
it('should handle deeply nested structures', () => {
const input = {
level1: {
level2: {
level3: {
field: null,
},
},
},
}
const result = deepSanitizeFormValues(input)
expect(result).toEqual({
level1: {
level2: {
level3: {
field: '',
},
},
},
})
})
it('should preserve non-null values in nested structures', () => {
const input = {
config: {
client_id: 'valid_id',
client_secret: null,
options: {
timeout: 5000,
enabled: true,
message: undefined,
},
},
}
const result = deepSanitizeFormValues(input)
expect(result).toEqual({
config: {
client_id: 'valid_id',
client_secret: '',
options: {
timeout: 5000,
enabled: true,
message: '',
},
},
})
})
})
describe('findMissingRequiredField', () => {
const requiredFields = [
{ name: 'client_id', label: 'Client ID' },
{ name: 'client_secret', label: 'Client Secret' },
{ name: 'scope', label: 'Scope' },
]
it('should return null when all required fields are present', () => {
const formData = {
client_id: 'test_id',
client_secret: 'test_secret',
scope: 'read',
optional_field: 'optional',
}
const result = findMissingRequiredField(formData, requiredFields)
expect(result).toBeNull()
})
it('should return the first missing field', () => {
const formData = {
client_id: 'test_id',
scope: 'read',
}
const result = findMissingRequiredField(formData, requiredFields)
expect(result).toEqual({ name: 'client_secret', label: 'Client Secret' })
})
it('should treat empty strings as missing fields', () => {
const formData = {
client_id: '',
client_secret: 'test_secret',
scope: 'read',
}
const result = findMissingRequiredField(formData, requiredFields)
expect(result).toEqual({ name: 'client_id', label: 'Client ID' })
})
it('should treat null values as missing fields', () => {
const formData = {
client_id: 'test_id',
client_secret: null,
scope: 'read',
}
const result = findMissingRequiredField(formData, requiredFields)
expect(result).toEqual({ name: 'client_secret', label: 'Client Secret' })
})
it('should treat undefined values as missing fields', () => {
const formData = {
client_id: 'test_id',
client_secret: 'test_secret',
scope: undefined,
}
const result = findMissingRequiredField(formData, requiredFields)
expect(result).toEqual({ name: 'scope', label: 'Scope' })
})
it('should handle empty required fields array', () => {
const formData = {
client_id: 'test_id',
}
const result = findMissingRequiredField(formData, [])
expect(result).toBeNull()
})
it('should handle empty form data', () => {
const result = findMissingRequiredField({}, requiredFields)
expect(result).toEqual({ name: 'client_id', label: 'Client ID' })
})
it('should handle multilingual labels', () => {
const multilingualFields = [
{ name: 'field1', label: { en_US: 'Field 1 EN', zh_Hans: 'Field 1 CN' } },
]
const formData = {}
const result = findMissingRequiredField(formData, multilingualFields)
expect(result).toEqual({
name: 'field1',
label: { en_US: 'Field 1 EN', zh_Hans: 'Field 1 CN' },
})
})
it('should return null for form data with extra fields', () => {
const formData = {
client_id: 'test_id',
client_secret: 'test_secret',
scope: 'read',
extra_field1: 'extra1',
extra_field2: 'extra2',
}
const result = findMissingRequiredField(formData, requiredFields)
expect(result).toBeNull()
})
})
describe('Edge cases', () => {
it('should handle objects with non-string keys', () => {
const input = { [Symbol('test')]: 'value', regular: 'field' } as any
const result = sanitizeFormValues(input)
expect(result.regular).toBe('field')
})
it('should handle objects with getter properties', () => {
const obj = {}
Object.defineProperty(obj, 'getter', {
get: () => 'computed_value',
enumerable: true,
})
const result = sanitizeFormValues(obj)
expect(result.getter).toBe('computed_value')
})
it('should handle circular references in deepSanitizeFormValues gracefully', () => {
const obj: any = { field: 'value' }
obj.circular = obj
expect(() => deepSanitizeFormValues(obj)).not.toThrow()
})
})
})

View File

@@ -0,0 +1,55 @@
/**
* Utility functions for form data handling in trigger plugin components
*/
/**
* Sanitizes form values by converting null/undefined to empty strings
* This ensures React form inputs don't receive null values which can cause warnings
*/
export const sanitizeFormValues = (values: Record<string, any>): Record<string, string> => {
return Object.fromEntries(
Object.entries(values).map(([key, value]) => [
key,
value === null || value === undefined ? '' : String(value),
]),
)
}
/**
* Deep sanitizes form values while preserving nested objects structure
* Useful for complex form schemas with nested properties
*/
export const deepSanitizeFormValues = (values: Record<string, any>, visited = new WeakSet()): Record<string, any> => {
if (visited.has(values))
return {}
visited.add(values)
const result: Record<string, any> = {}
for (const [key, value] of Object.entries(values)) {
if (value === null || value === undefined)
result[key] = ''
else if (typeof value === 'object' && !Array.isArray(value))
result[key] = deepSanitizeFormValues(value, visited)
else
result[key] = value
}
return result
}
/**
* Validates required fields in form data
* Returns the first missing required field or null if all are present
*/
export const findMissingRequiredField = (
formData: Record<string, any>,
requiredFields: Array<{ name: string; label: any }>,
): { name: string; label: any } | null => {
for (const field of requiredFields) {
if (!formData[field.name] || formData[field.name] === '')
return field
}
return null
}