feat(trigger): enhance plugin and trigger integration with updated naming conventions

- Refactored `PluginFetchDynamicSelectOptionsApi` to replace the `extra` argument with `credential_id`, improving clarity in dynamic option fetching.
- Updated `ProviderConfigEncrypter` to rename `mask_tool_credentials` to `mask_credentials` for consistency, and added a new method to maintain backward compatibility.
- Enhanced `PluginParameterService` to utilize `credential_id` for fetching subscriptions, improving the handling of trigger credentials.
- Adjusted various components and types in the frontend to replace `tool_name` with `trigger_name`, ensuring consistency across the application.
- Introduced `multiple` property in `TriggerParameter` to support multi-select functionality.

These changes improve the integration of triggers and plugins, enhance code clarity, and align naming conventions across the codebase.
This commit is contained in:
Harry
2025-09-08 23:14:50 +08:00
parent 01b2f9cff6
commit 2a3ce6baa9
40 changed files with 734 additions and 163 deletions

View File

@ -209,7 +209,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
viewType={viewType}
onSelect={(_, tool) => {
onChange({
agent_strategy_name: tool!.tool_name,
agent_strategy_name: tool!.trigger_name,
agent_strategy_provider_name: tool!.provider_name,
agent_strategy_label: tool!.tool_label,
agent_output_schema: tool!.output_schema,

View File

@ -1,20 +1,19 @@
'use client'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import { type BaseResource, type BaseResourceProvider, type ResourceVarInputs, VarKindType } from '../types'
import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { VarType } from '@/app/components/workflow/types'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import FormInputTypeSwitch from './form-input-type-switch'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import Input from '@/app/components/base/input'
import { SimpleSelect } from '@/app/components/base/select'
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
import MixedVariableTextInput from './mixed-variable-text-input'
import FormInputBoolean from './form-input-boolean'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
@ -22,19 +21,20 @@ import VarReferencePicker from '@/app/components/workflow/nodes/_base/components
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import cn from '@/utils/classnames'
import type { Tool } from '@/app/components/tools/types'
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { RiCheckLine } from '@remixicon/react'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema
value: ToolVarInputs
value: ResourceVarInputs
onChange: (value: any) => void
inPanel?: boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
currentResource?: BaseResource
currentProvider?: BaseResourceProvider
extraParams?: Record<string, any>
providerType?: 'tool' | 'trigger'
providerType?: string
}
const FormInputItem: FC<Props> = ({
@ -44,10 +44,10 @@ const FormInputItem: FC<Props> = ({
value,
onChange,
inPanel,
currentTool,
currentResource,
currentProvider,
extraParams,
providerType = 'tool',
providerType,
}) => {
const language = useLanguage()
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
@ -59,6 +59,7 @@ const FormInputItem: FC<Props> = ({
type,
default: defaultValue,
options,
multiple,
scope,
} = schema as any
const varInput = value[variable]
@ -76,6 +77,7 @@ const FormInputItem: FC<Props> = ({
const showTypeSwitch = isNumber || isBoolean || isObject || isArray
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
const isMultipleSelect = multiple && (isSelect || isDynamicSelect)
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
@ -138,7 +140,7 @@ const FormInputItem: FC<Props> = ({
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
currentProvider?.plugin_id || '',
currentProvider?.name || '',
currentTool?.name || '',
currentResource?.name || '',
variable || '',
providerType,
extraParams,
@ -147,7 +149,7 @@ const FormInputItem: FC<Props> = ({
// Fetch dynamic options when component mounts or dependencies change
useEffect(() => {
const fetchOptions = async () => {
if (isDynamicSelect && currentTool && currentProvider) {
if (isDynamicSelect && currentResource && currentProvider) {
setIsLoadingOptions(true)
try {
const data = await fetchDynamicOptions()
@ -164,7 +166,7 @@ const FormInputItem: FC<Props> = ({
}
fetchOptions()
}, [isDynamicSelect, currentTool?.name, currentProvider?.name, variable, extraParams])
}, [isDynamicSelect, currentResource?.name, currentProvider?.name, variable, extraParams])
const handleTypeChange = (newType: string) => {
if (newType === VarKindType.variable) {
@ -200,6 +202,24 @@ const FormInputItem: FC<Props> = ({
})
}
const getSelectedLabels = (selectedValues: any[]) => {
if (!selectedValues || selectedValues.length === 0)
return ''
const optionsList = isDynamicSelect ? (dynamicOptions || options || []) : (options || [])
const selectedOptions = optionsList.filter((opt: any) =>
selectedValues.includes(opt.value),
)
if (selectedOptions.length <= 2) {
return selectedOptions
.map((opt: any) => opt.label?.[language] || opt.label?.en_US || opt.value)
.join(', ')
}
return `${selectedOptions.length} selected`
}
const handleAppOrModelSelect = (newValue: any) => {
onChange({
...value,
@ -250,7 +270,7 @@ const FormInputItem: FC<Props> = ({
onChange={handleValueChange}
/>
)}
{isSelect && (
{isSelect && !isMultipleSelect && (
<SimpleSelect
wrapperClassName='h-8 grow'
disabled={readOnly}
@ -277,7 +297,64 @@ const FormInputItem: FC<Props> = ({
) : undefined}
/>
)}
{isDynamicSelect && (
{isSelect && isMultipleSelect && (
<Listbox
multiple
value={varInput?.value || []}
onChange={handleValueChange}
disabled={readOnly}
>
<div className="relative">
<ListboxButton className="relative h-8 w-full cursor-pointer rounded-lg bg-components-input-bg-normal px-3 py-1.5 text-left text-sm focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300">
<span className="block truncate text-components-input-text-filled">
{getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-4 w-4 text-text-tertiary"
aria-hidden="true"
/>
</span>
</ListboxButton>
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
{options.filter((option: { show_on: any[] }) => {
if (option.show_on?.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => (
<ListboxOption
key={option.value}
value={option.value}
className={({ focus }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
focus ? 'bg-state-base-hover text-text-secondary' : 'text-text-primary'
}`
}
>
{({ selected }) => (
<>
<div className="flex items-center">
{option.icon && (
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
)}
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{option.label[language] || option.label.en_US}
</span>
</div>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600">
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
)}
{isDynamicSelect && !isMultipleSelect && (
<SimpleSelect
wrapperClassName='h-8 grow'
disabled={readOnly || isLoadingOptions}
@ -304,6 +381,65 @@ const FormInputItem: FC<Props> = ({
)}
/>
)}
{isDynamicSelect && isMultipleSelect && (
<Listbox
multiple
value={varInput?.value || []}
onChange={handleValueChange}
disabled={readOnly || isLoadingOptions}
>
<div className="relative">
<ListboxButton className="relative h-8 w-full cursor-pointer rounded-lg bg-components-input-bg-normal px-3 py-1.5 text-left text-sm focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300">
<span className="block truncate text-components-input-text-filled">
{isLoadingOptions
? 'Loading...'
: getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-4 w-4 text-text-tertiary"
aria-hidden="true"
/>
</span>
</ListboxButton>
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
{(dynamicOptions || options || []).filter((option: { show_on?: any[] }) => {
if (option.show_on?.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => (
<ListboxOption
key={option.value}
value={option.value}
className={({ focus }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
focus ? 'bg-state-base-hover text-text-secondary' : 'text-text-primary'
}`
}
>
{({ selected }) => (
<>
<div className="flex items-center">
{option.icon && (
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
)}
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{option.label[language] || option.label.en_US}
</span>
</div>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600">
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
)}
{isShowJSONEditor && isConstant && (
<div className='mt-1 w-full'>
<CodeEditor
@ -349,7 +485,7 @@ const FormInputItem: FC<Props> = ({
filterVar={getFilterVar()}
schema={schema}
valueTypePlaceHolder={targetVarType()}
currentTool={currentTool}
currentResource={currentResource}
currentProvider={currentProvider}
/>
)}

View File

@ -0,0 +1,62 @@
import {
memo,
} from 'react'
import { useTranslation } from 'react-i18next'
import PromptEditor from '@/app/components/base/prompt-editor'
import Placeholder from './placeholder'
import type {
Node,
NodeOutPutVar,
} from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type MixedVariableTextInputProps = {
readOnly?: boolean
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
value?: string
onChange?: (text: string) => void
}
const MixedVariableTextInput = ({
readOnly = false,
nodesOutputVars,
availableNodes = [],
value = '',
onChange,
}: MixedVariableTextInputProps) => {
const { t } = useTranslation()
return (
<PromptEditor
wrapperClassName={cn(
'w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
)}
className='caret:text-text-accent'
editable={!readOnly}
value={value}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
placeholder={<Placeholder />}
onChange={onChange}
/>
)
}
export default memo(MixedVariableTextInput)

View File

@ -0,0 +1,52 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { FOCUS_COMMAND } from 'lexical'
import { $insertNodes } from 'lexical'
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
import Badge from '@/app/components/base/badge'
const Placeholder = () => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const handleInsert = useCallback((text: string) => {
editor.update(() => {
const textNode = new CustomTextNode(text)
$insertNodes([textNode])
})
editor.dispatchCommand(FOCUS_COMMAND, undefined as any)
}, [editor])
return (
<div
className='pointer-events-auto flex h-full w-full cursor-text items-center px-2'
onClick={(e) => {
e.stopPropagation()
handleInsert('')
}}
>
<div className='flex grow items-center'>
{t('workflow.nodes.tool.insertPlaceholder1')}
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
<div
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
onMouseDown={((e) => {
e.preventDefault()
e.stopPropagation()
handleInsert('/')
})}
>
{t('workflow.nodes.tool.insertPlaceholder2')}
</div>
</div>
<Badge
className='shrink-0'
text='String'
uppercase={false}
/>
</div>
)
}
export default Placeholder

View File

@ -17,7 +17,7 @@ import VarReferencePopup from './var-reference-popup'
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils'
import ConstantField from './constant-field'
import cn from '@/utils/classnames'
import type { Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { BlockEnum } from '@/app/components/workflow/types'
@ -34,6 +34,7 @@ import {
useWorkflowVariables,
} from '@/app/components/workflow/hooks'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import type { BaseResource, BaseResourceProvider } from '@/app/components/workflow/nodes/_base/types'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import AddButton from '@/app/components/base/button/add-button'
import Badge from '@/app/components/base/badge'
@ -42,7 +43,6 @@ import { isExceptionVariable } from '@/app/components/workflow/utils'
import VarFullPathPanel from './var-full-path-panel'
import { noop } from 'lodash-es'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import type { Tool } from '@/app/components/tools/types'
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
const TRIGGER_DEFAULT_WIDTH = 227
@ -72,8 +72,8 @@ type Props = {
minWidth?: number
popupFor?: 'assigned' | 'toAssigned'
zIndex?: number
currentTool?: Tool
currentProvider?: ToolWithProvider
currentResource?: BaseResource
currentProvider?: BaseResourceProvider
}
const DEFAULT_VALUE_SELECTOR: Props['value'] = []
@ -103,7 +103,7 @@ const VarReferencePicker: FC<Props> = ({
minWidth,
popupFor,
zIndex,
currentTool,
currentResource,
currentProvider,
}) => {
const { t } = useTranslation()
@ -328,11 +328,11 @@ const VarReferencePicker: FC<Props> = ({
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
const [isLoading, setIsLoading] = useState(false)
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
currentProvider?.plugin_id || '', currentProvider?.name || '', currentTool?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '',
currentProvider?.plugin_id || '', currentProvider?.name || '', currentResource?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '',
'tool',
)
const handleFetchDynamicOptions = async () => {
if (schema?.type !== FormTypeEnum.dynamicSelect || !currentTool || !currentProvider)
if (schema?.type !== FormTypeEnum.dynamicSelect || !currentResource || !currentProvider)
return
setIsLoading(true)
try {
@ -345,7 +345,7 @@ const VarReferencePicker: FC<Props> = ({
}
useEffect(() => {
handleFetchDynamicOptions()
}, [currentTool, currentProvider, schema])
}, [currentResource, currentProvider, schema])
const schemaWithDynamicSelect = useMemo(() => {
if (schema?.type !== FormTypeEnum.dynamicSelect)

View File

@ -0,0 +1,27 @@
import type { ValueSelector } from '@/app/components/workflow/types'
// Generic variable types for all resource forms
export enum VarKindType {
variable = 'variable',
constant = 'constant',
mixed = 'mixed',
}
// Generic resource variable inputs
export type ResourceVarInputs = Record<string, {
type: VarKindType
value?: string | ValueSelector | any
}>
// Base resource interface
export type BaseResource = {
name: string
[key: string]: any
}
// Base resource provider interface
export type BaseResourceProvider = {
plugin_id?: string
name: string
[key: string]: any
}

View File

@ -44,7 +44,7 @@ const ImportFromTool: FC<Props> = ({
const workflowTools = useStore(s => s.workflowTools)
const handleSelectTool = useCallback((_type: BlockEnum, toolInfo?: ToolDefaultValue) => {
const { provider_id, provider_type, tool_name } = toolInfo!
const { provider_id, provider_type, trigger_name: tool_name } = toolInfo!
const currentTools = (() => {
switch (provider_type) {
case CollectionType.builtIn:

View File

@ -17,7 +17,6 @@ type Props = {
currentTool?: Tool
currentProvider?: ToolWithProvider
extraParams?: Record<string, any>
providerType?: 'tool' | 'trigger'
}
const ToolForm: FC<Props> = ({
@ -30,7 +29,6 @@ const ToolForm: FC<Props> = ({
currentTool,
currentProvider,
extraParams,
providerType = 'tool',
}) => {
return (
<div className='space-y-1'>
@ -47,7 +45,7 @@ const ToolForm: FC<Props> = ({
currentTool={currentTool}
currentProvider={currentProvider}
extraParams={extraParams}
providerType={providerType}
providerType='tool'
/>
))
}

View File

@ -91,7 +91,7 @@ const ToolFormItem: FC<Props> = ({
value={value}
onChange={onChange}
inPanel={inPanel}
currentTool={currentTool}
currentResource={currentTool}
currentProvider={currentProvider}
extraParams={extraParams}
providerType={providerType}

View File

@ -1,16 +1,10 @@
import type { CollectionType } from '@/app/components/tools/types'
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { ResourceVarInputs } from '../_base/types'
export enum VarType {
variable = 'variable',
constant = 'constant',
mixed = 'mixed',
}
export type ToolVarInputs = Record<string, {
type: VarType
value?: string | ValueSelector | any
}>
// Use base types directly
export { VarKindType as VarType } from '../_base/types'
export type ToolVarInputs = ResourceVarInputs
export type ToolNodeType = CommonNodeType & {
provider_id: string

View File

@ -32,7 +32,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
* tool_parameters: tool dynamic setting(form type = llm)
* output_schema: tool dynamic output
*/
const { provider_id, provider_type, tool_name, tool_configurations, output_schema, tool_parameters } = inputs
const { provider_id, provider_type, trigger_name: tool_name, tool_configurations, output_schema, tool_parameters } = inputs
const isBuiltIn = provider_type === CollectionType.builtIn
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)

View File

@ -6,7 +6,7 @@ import { ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/block
const nodeDefault: NodeDefault<PluginTriggerNodeType> = {
defaultValue: {
plugin_id: '',
tool_name: '',
trigger_name: '',
event_type: '',
config: {},
},

View File

@ -5,7 +5,7 @@ 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 ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
import TriggerForm from './trigger-form'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../llm/types'
@ -21,6 +21,8 @@ const Panel: FC<NodePanelProps<PluginTriggerNodeType>> = ({
outputSchema,
hasObjectOutput,
isAuthenticated,
currentProvider,
currentTrigger,
} = useConfig(id, data)
// Convert output schema to VarItem format
@ -36,13 +38,14 @@ const Panel: FC<NodePanelProps<PluginTriggerNodeType>> = ({
{isAuthenticated && triggerParameterSchema.length > 0 && (
<>
<div className='px-4 pb-4'>
<ToolForm
<TriggerForm
readOnly={readOnly}
nodeId={id}
schema={triggerParameterSchema as any}
value={triggerParameterValue}
onChange={setTriggerParameterValue}
providerType="trigger"
currentProvider={currentProvider}
currentTrigger={currentTrigger}
/>
</div>
<Split />

View File

@ -0,0 +1,54 @@
'use client'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Trigger } from '@/app/components/tools/types'
import type { FC } from 'react'
import type { PluginTriggerVarInputs } from '../types'
import TriggerFormItem from './item'
import type { TriggerWithProvider } from '../../../block-selector/types'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema[]
value: PluginTriggerVarInputs
onChange: (value: PluginTriggerVarInputs) => void
onOpen?: (index: number) => void
inPanel?: boolean
currentTrigger?: Trigger
currentProvider?: TriggerWithProvider
extraParams?: Record<string, any>
}
const TriggerForm: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
inPanel,
currentTrigger,
currentProvider,
extraParams,
}) => {
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}
currentTrigger={currentTrigger}
currentProvider={currentProvider}
extraParams={extraParams}
/>
))
}
</div>
)
}
export default TriggerForm

View File

@ -0,0 +1,109 @@
'use client'
import type { FC } from 'react'
import {
RiBracesLine,
} from '@remixicon/react'
import type { PluginTriggerVarInputs } from '../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 { Trigger } from '@/app/components/tools/types'
import type { TriggerWithProvider } from '../../../block-selector/types'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema
value: PluginTriggerVarInputs
onChange: (value: PluginTriggerVarInputs) => void
inPanel?: boolean
currentTrigger?: Trigger
currentProvider?: TriggerWithProvider
extraParams?: Record<string, any>
}
const TriggerFormItem: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
inPanel,
currentTrigger,
currentProvider,
extraParams,
}) => {
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}
currentResource={currentTrigger}
currentProvider={currentProvider}
providerType='trigger'
extraParams={extraParams}
/>
{isShowSchema && (
<SchemaModal
isShow
onClose={hideSchema}
rootName={name}
schema={input_schema!}
/>
)}
</div>
)
}
export default TriggerFormItem

View File

@ -1,12 +1,23 @@
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
trigger_name: string
trigger_label: string
trigger_parameters: PluginTriggerVarInputs
trigger_configurations: Record<string, any>
output_schema: Record<string, any>
parameters_schema?: Record<string, any>[]
version?: string
trigger_node_version?: string
plugin_id?: string
tool_name?: string
event_type?: string
config?: Record<string, any>
provider_id?: string
provider_type?: CollectionType
provider_name?: string
}
// Use base types directly
export { VarKindType as PluginTriggerVarType } from '../_base/types'
export type PluginTriggerVarInputs = ResourceVarInputs

View File

@ -3,27 +3,32 @@ import produce from 'immer'
import type { PluginTriggerNodeType } 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 {
useAllTriggerPlugins,
useTriggerSubscriptions,
} from '@/service/use-triggers'
import {
addDefaultValue,
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 { Tool } from '@/app/components/tools/types'
import type { Trigger } from '@/app/components/tools/types'
const useConfig = (id: string, payload: PluginTriggerNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { data: triggerPlugins = [] } = useAllTriggerPlugins()
const { inputs, setInputs: doSetInputs } = useNodeCrud<PluginTriggerNodeType>(id, payload)
const { inputs, setInputs: doSetInputs } = useNodeCrud<PluginTriggerNodeType>(
id,
payload,
)
const { provider_id, provider_name, tool_name, config } = inputs
const { provider_id, provider_name, trigger_name, config } = inputs
// Construct provider for authentication check
const authProvider = useMemo(() => {
if (provider_id && provider_name)
return `${provider_id}/${provider_name}`
if (provider_id && provider_name) return `${provider_id}/${provider_name}`
return provider_id || ''
}, [provider_id, provider_name])
@ -33,21 +38,26 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
)
const currentProvider = useMemo<TriggerWithProvider | undefined>(() => {
return triggerPlugins.find(provider =>
provider.name === provider_name
|| provider.id === provider_id
|| (provider_id && provider.plugin_id === provider_id),
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 currentTrigger = useMemo<Tool | undefined>(() => {
return currentProvider?.tools.find(tool => tool.name === tool_name)
}, [currentProvider, tool_name])
const currentTrigger = useMemo<Trigger | undefined>(() => {
return currentProvider?.triggers.find(
trigger => trigger.name === trigger_name,
)
}, [currentProvider, trigger_name])
// Dynamic subscription parameters (from subscription_schema.parameters_schema)
const subscriptionParameterSchema = useMemo(() => {
if (!currentProvider?.subscription_schema?.parameters_schema) return []
return toolParametersToFormSchemas(currentProvider.subscription_schema.parameters_schema as any)
return toolParametersToFormSchemas(
currentProvider.subscription_schema.parameters_schema as any,
)
}, [currentProvider])
// Dynamic trigger parameters (from specific trigger.parameters)
@ -66,22 +76,28 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
return addDefaultValue(config || {}, triggerParameterSchema)
}, [triggerParameterSchema, config])
const setTriggerParameterValue = useCallback((value: Record<string, any>) => {
const newInputs = produce(inputs, (draft) => {
draft.config = value
})
doSetInputs(newInputs)
}, [inputs, doSetInputs])
const setTriggerParameterValue = useCallback(
(value: Record<string, any>) => {
const newInputs = produce(inputs, (draft) => {
draft.config = value
})
doSetInputs(newInputs)
},
[inputs, doSetInputs],
)
const setInputVar = useCallback((variable: InputVar, varDetail: InputVar) => {
const newInputs = produce(inputs, (draft) => {
draft.config = {
...draft.config,
[variable.variable]: varDetail.variable,
}
})
doSetInputs(newInputs)
}, [inputs, doSetInputs])
const setInputVar = useCallback(
(variable: InputVar, varDetail: InputVar) => {
const newInputs = produce(inputs, (draft) => {
draft.config = {
...draft.config,
[variable.variable]: varDetail.variable,
}
})
doSetInputs(newInputs)
},
[inputs, doSetInputs],
)
// Get output schema
const outputSchema = useMemo(() => {
@ -91,7 +107,9 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
// Check if trigger has complex output structure
const hasObjectOutput = useMemo(() => {
const properties = outputSchema.properties || {}
return Object.values(properties).some((prop: any) => prop.type === 'object')
return Object.values(properties).some(
(prop: any) => prop.type === 'object',
)
}, [outputSchema])
// Authentication status check
@ -109,10 +127,16 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
const methods = []
if (currentProvider.oauth_client_schema && currentProvider.oauth_client_schema.length > 0)
if (
currentProvider.oauth_client_schema
&& currentProvider.oauth_client_schema.length > 0
)
methods.push('oauth')
if (currentProvider.credentials_schema && currentProvider.credentials_schema.length > 0)
if (
currentProvider.credentials_schema
&& currentProvider.credentials_schema.length > 0
)
methods.push('api_key')
return methods