mirror of
https://github.com/langgenius/dify.git
synced 2026-01-14 06:07:33 +08:00
feat(workflow): Plugin Trigger Node with Unified Entry Node System (#24205)
This commit is contained in:
parent
6eaea64b3f
commit
833c902b2b
@ -108,7 +108,7 @@ const BlockIcon: FC<BlockIconProps> = ({
|
||||
`}
|
||||
>
|
||||
{
|
||||
type !== BlockEnum.Tool && (
|
||||
type !== BlockEnum.Tool && type !== BlockEnum.TriggerPlugin && (
|
||||
getIcon(type,
|
||||
(type === BlockEnum.TriggerSchedule || type === BlockEnum.TriggerWebhook)
|
||||
? (size === 'xs' ? 'w-4 h-4' : 'w-4.5 h-4.5')
|
||||
@ -117,7 +117,7 @@ const BlockIcon: FC<BlockIconProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
type === BlockEnum.Tool && toolIcon && (
|
||||
(type === BlockEnum.Tool || type === BlockEnum.TriggerPlugin) && toolIcon && (
|
||||
<>
|
||||
{
|
||||
typeof toolIcon === 'string'
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
import { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { BlockEnum } from '../types'
|
||||
import type { ToolDefaultValue } from './types'
|
||||
import StartBlocks from './start-blocks'
|
||||
import TriggerPluginSelector from './trigger-plugin-selector'
|
||||
import { ENTRY_NODE_TYPES } from './constants'
|
||||
import cn from '@/utils/classnames'
|
||||
import Link from 'next/link'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
type AllStartBlocksProps = {
|
||||
className?: string
|
||||
searchText: string
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
}
|
||||
|
||||
const AllStartBlocks = ({
|
||||
className,
|
||||
searchText,
|
||||
onSelect,
|
||||
availableBlocksTypes,
|
||||
}: AllStartBlocksProps) => {
|
||||
const { t } = useTranslation()
|
||||
const wrapElemRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
|
||||
<div
|
||||
ref={wrapElemRef}
|
||||
className='max-h-[464px] overflow-y-auto'
|
||||
>
|
||||
<StartBlocks
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={ENTRY_NODE_TYPES as BlockEnum[]}
|
||||
/>
|
||||
|
||||
<TriggerPluginSelector
|
||||
onSelect={onSelect}
|
||||
searchText={searchText}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer - Same as Tools tab marketplace footer */}
|
||||
<Link
|
||||
className='system-sm-medium sticky bottom-0 z-10 flex h-8 cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg'
|
||||
href={getMarketplaceUrl('')}
|
||||
target='_blank'
|
||||
>
|
||||
<span>{t('plugin.findMoreInMarketplace')}</span>
|
||||
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AllStartBlocks
|
||||
@ -21,14 +21,16 @@ export const START_BLOCKS: Block[] = [
|
||||
title: 'Webhook Trigger',
|
||||
description: 'HTTP callback trigger',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.TriggerPlugin,
|
||||
title: 'Plugin Trigger',
|
||||
description: 'Third-party integration trigger',
|
||||
},
|
||||
]
|
||||
|
||||
// Entry node types that can start a workflow
|
||||
export const ENTRY_NODE_TYPES = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
] as const
|
||||
|
||||
export const BLOCKS: Block[] = [
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
|
||||
@ -93,7 +93,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
}, [])
|
||||
const searchPlaceholder = useMemo(() => {
|
||||
if (activeTab === TabsEnum.Start)
|
||||
return t('workflow.tabs.searchBlock')
|
||||
return t('workflow.tabs.searchTrigger')
|
||||
if (activeTab === TabsEnum.Blocks)
|
||||
return t('workflow.tabs.searchBlock')
|
||||
if (activeTab === TabsEnum.Tools)
|
||||
@ -137,7 +137,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
onActiveTabChange={handleActiveTabChange}
|
||||
filterElem={
|
||||
<div className='relative m-2' onClick={e => e.stopPropagation()}>
|
||||
{activeTab === TabsEnum.Blocks && (
|
||||
{(activeTab === TabsEnum.Start || activeTab === TabsEnum.Blocks) && (
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BlockIcon from '../block-icon'
|
||||
import type { BlockEnum } from '../types'
|
||||
import { BlockEnum as BlockEnumValues } from '../types'
|
||||
import { useNodesExtraData } from '../hooks'
|
||||
import { START_BLOCKS } from './constants'
|
||||
import type { ToolDefaultValue } from './types'
|
||||
@ -27,8 +28,12 @@ const StartBlocks = ({
|
||||
|
||||
const filteredBlocks = useMemo(() => {
|
||||
return START_BLOCKS.filter((block) => {
|
||||
return block.title.toLowerCase().includes(searchText.toLowerCase())
|
||||
&& availableBlocksTypes.includes(block.type)
|
||||
// Filter by search text
|
||||
if (!block.title.toLowerCase().includes(searchText.toLowerCase()))
|
||||
return false
|
||||
|
||||
// availableBlocksTypes now contains properly filtered entry node types from parent
|
||||
return availableBlocksTypes.includes(block.type)
|
||||
})
|
||||
}, [searchText, availableBlocksTypes])
|
||||
|
||||
@ -60,13 +65,18 @@ const StartBlocks = ({
|
||||
className='mr-2 shrink-0'
|
||||
type={block.type}
|
||||
/>
|
||||
<div className='grow text-sm text-text-secondary'>{block.title}</div>
|
||||
<div className='flex w-0 grow items-center justify-between text-sm text-text-secondary'>
|
||||
<span className='truncate'>{block.title}</span>
|
||||
{block.type === BlockEnumValues.Start && (
|
||||
<span className='system-xs-regular ml-2 shrink-0 text-text-quaternary'>{t('workflow.blocks.originalStartNode')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
), [nodesExtraData, onSelect])
|
||||
), [nodesExtraData, onSelect, t])
|
||||
|
||||
return (
|
||||
<div className='p-1'>
|
||||
<div className='min-w-[400px] max-w-[500px] p-1'>
|
||||
{isEmpty && (
|
||||
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>
|
||||
{t('workflow.tabs.noResult')}
|
||||
@ -74,7 +84,16 @@ const StartBlocks = ({
|
||||
)}
|
||||
{!isEmpty && (
|
||||
<div className='mb-1'>
|
||||
{filteredBlocks.map(renderBlock)}
|
||||
{filteredBlocks.map((block, index) => (
|
||||
<div key={block.type}>
|
||||
{renderBlock(block)}
|
||||
{block.type === BlockEnumValues.Start && index < filteredBlocks.length - 1 && (
|
||||
<div className='my-1 px-3'>
|
||||
<div className='border-t border-divider-subtle' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,7 @@ import { useTabs } from './hooks'
|
||||
import type { ToolDefaultValue } from './types'
|
||||
import { TabsEnum } from './types'
|
||||
import Blocks from './blocks'
|
||||
import StartBlocks from './start-blocks'
|
||||
import AllStartBlocks from './all-start-blocks'
|
||||
import AllTools from './all-tools'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
@ -66,7 +66,7 @@ const Tabs: FC<TabsProps> = ({
|
||||
{
|
||||
activeTab === TabsEnum.Start && !noBlocks && (
|
||||
<div className='border-t border-divider-subtle'>
|
||||
<StartBlocks
|
||||
<AllStartBlocks
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
import { memo } from 'react'
|
||||
import TriggerPluginList from './trigger-plugin/list'
|
||||
import type { BlockEnum } from '../types'
|
||||
import type { ToolDefaultValue } from './types'
|
||||
|
||||
type TriggerPluginSelectorProps = {
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
searchText: string
|
||||
}
|
||||
|
||||
const TriggerPluginSelector = ({
|
||||
onSelect,
|
||||
searchText,
|
||||
}: TriggerPluginSelectorProps) => {
|
||||
return (
|
||||
<TriggerPluginList
|
||||
onSelect={onSelect}
|
||||
searchText={searchText}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(TriggerPluginSelector)
|
||||
@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { ToolWithProvider } from '../../types'
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { ToolDefaultValue } from '../types'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
provider: ToolWithProvider
|
||||
payload: Tool
|
||||
disabled?: boolean
|
||||
isAdded?: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
}
|
||||
|
||||
const TriggerPluginActionItem: FC<Props> = ({
|
||||
provider,
|
||||
payload,
|
||||
onSelect,
|
||||
disabled,
|
||||
isAdded,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={payload.name}
|
||||
position='right'
|
||||
needsDelay={false}
|
||||
popupClassName='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
|
||||
popupContent={(
|
||||
<div>
|
||||
<BlockIcon
|
||||
size='md'
|
||||
className='mb-2'
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
toolIcon={provider.icon}
|
||||
/>
|
||||
<div className='mb-1 text-sm leading-5 text-text-primary'>{payload.label[language]}</div>
|
||||
<div className='text-xs leading-[18px] text-text-secondary'>{payload.description[language]}</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
key={payload.name}
|
||||
className='flex cursor-pointer items-center justify-between rounded-lg pl-[21px] pr-1 hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
if (disabled) return
|
||||
const params: Record<string, string> = {}
|
||||
if (payload.parameters) {
|
||||
payload.parameters.forEach((item) => {
|
||||
params[item.name] = ''
|
||||
})
|
||||
}
|
||||
onSelect(BlockEnum.TriggerPlugin, {
|
||||
provider_id: provider.id,
|
||||
provider_type: provider.type,
|
||||
provider_name: provider.name,
|
||||
tool_name: payload.name,
|
||||
tool_label: payload.label[language],
|
||||
tool_description: payload.description[language],
|
||||
title: payload.label[language],
|
||||
is_team_authorization: provider.is_team_authorization,
|
||||
output_schema: payload.output_schema,
|
||||
paramSchemas: payload.parameters,
|
||||
params,
|
||||
meta: provider.meta,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary')}>
|
||||
<span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span>
|
||||
</div>
|
||||
{isAdded && (
|
||||
<div className='system-xs-regular mr-4 text-text-tertiary'>{t('tools.addToolModal.added')}</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip >
|
||||
)
|
||||
}
|
||||
export default React.memo(TriggerPluginActionItem)
|
||||
@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useMemo, useRef } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { CollectionType } from '../../../tools/types'
|
||||
import type { ToolWithProvider } from '../../types'
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { ToolDefaultValue } from '../types'
|
||||
import TriggerPluginActionItem from './action-item'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
payload: ToolWithProvider
|
||||
hasSearchText: boolean
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
}
|
||||
|
||||
const TriggerPluginItem: FC<Props> = ({
|
||||
className,
|
||||
payload,
|
||||
hasSearchText,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const notShowProvider = payload.type === CollectionType.workflow
|
||||
const actions = payload.tools
|
||||
const hasAction = !notShowProvider
|
||||
const [isFold, setFold] = React.useState<boolean>(true)
|
||||
const ref = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSearchText && isFold) {
|
||||
setFold(false)
|
||||
return
|
||||
}
|
||||
if (!hasSearchText && !isFold)
|
||||
setFold(true)
|
||||
}, [hasSearchText])
|
||||
|
||||
const FoldIcon = isFold ? RiArrowRightSLine : RiArrowDownSLine
|
||||
|
||||
const groupName = useMemo(() => {
|
||||
if (payload.type === CollectionType.builtIn)
|
||||
return payload.author
|
||||
|
||||
if (payload.type === CollectionType.custom)
|
||||
return t('workflow.tabs.customTool')
|
||||
|
||||
if (payload.type === CollectionType.workflow)
|
||||
return t('workflow.tabs.workflowTool')
|
||||
|
||||
return ''
|
||||
}, [payload.author, payload.type, t])
|
||||
|
||||
return (
|
||||
<div
|
||||
key={payload.id}
|
||||
className={cn('mb-1 last-of-type:mb-0')}
|
||||
ref={ref}
|
||||
>
|
||||
<div className={cn(className)}>
|
||||
<div
|
||||
className='group/item flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
if (hasAction) {
|
||||
setFold(!isFold)
|
||||
return
|
||||
}
|
||||
|
||||
const tool = actions[0]
|
||||
const params: Record<string, string> = {}
|
||||
if (tool.parameters) {
|
||||
tool.parameters.forEach((item) => {
|
||||
params[item.name] = ''
|
||||
})
|
||||
}
|
||||
onSelect(BlockEnum.TriggerPlugin, {
|
||||
provider_id: payload.id,
|
||||
provider_type: payload.type,
|
||||
provider_name: payload.name,
|
||||
tool_name: tool.name,
|
||||
tool_label: tool.label[language],
|
||||
tool_description: tool.description[language],
|
||||
title: tool.label[language],
|
||||
is_team_authorization: payload.is_team_authorization,
|
||||
output_schema: tool.output_schema,
|
||||
paramSchemas: tool.parameters,
|
||||
params,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className='flex h-8 grow items-center'>
|
||||
<BlockIcon
|
||||
className='shrink-0'
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
toolIcon={payload.icon}
|
||||
/>
|
||||
<div className='ml-2 flex w-0 grow items-center text-sm text-text-primary'>
|
||||
<span className='max-w-[250px] truncate'>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
|
||||
<span className='system-xs-regular ml-2 shrink-0 text-text-quaternary'>{groupName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='ml-2 flex items-center'>
|
||||
{hasAction && (
|
||||
<FoldIcon className={cn('h-4 w-4 shrink-0 text-text-tertiary group-hover/item:text-text-tertiary', isFold && 'text-text-quaternary')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!notShowProvider && hasAction && !isFold && (
|
||||
actions.map(action => (
|
||||
<TriggerPluginActionItem
|
||||
key={action.name}
|
||||
provider={payload}
|
||||
payload={action}
|
||||
onSelect={onSelect}
|
||||
disabled={false}
|
||||
isAdded={false}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(TriggerPluginItem)
|
||||
@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { useAllBuiltInTools } from '@/service/use-tools'
|
||||
import TriggerPluginItem from './item'
|
||||
import type { BlockEnum } from '../../types'
|
||||
import type { ToolDefaultValue } from '../types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
|
||||
type TriggerPluginListProps = {
|
||||
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||
searchText: string
|
||||
}
|
||||
|
||||
const TriggerPluginList = ({
|
||||
onSelect,
|
||||
searchText,
|
||||
}: TriggerPluginListProps) => {
|
||||
const { data: buildInTools = [] } = useAllBuiltInTools()
|
||||
const language = useGetLanguage()
|
||||
|
||||
const triggerPlugins = useMemo(() => {
|
||||
return buildInTools.filter((toolWithProvider) => {
|
||||
if (toolWithProvider.tools.length === 0) return false
|
||||
|
||||
if (!searchText) return true
|
||||
|
||||
return toolWithProvider.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
|| toolWithProvider.tools.some(tool =>
|
||||
tool.label[language].toLowerCase().includes(searchText.toLowerCase()),
|
||||
)
|
||||
})
|
||||
}, [buildInTools, searchText, language])
|
||||
|
||||
if (!triggerPlugins.length)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="p-1">
|
||||
{triggerPlugins.map(plugin => (
|
||||
<TriggerPluginItem
|
||||
key={plugin.id}
|
||||
payload={plugin}
|
||||
onSelect={onSelect}
|
||||
hasSearchText={!!searchText}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(TriggerPluginList)
|
||||
@ -18,6 +18,7 @@ import {
|
||||
} from 'reactflow'
|
||||
import { unionBy } from 'lodash-es'
|
||||
import type { ToolDefaultValue } from '../block-selector/types'
|
||||
import { ENTRY_NODE_TYPES } from '../block-selector/constants'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
@ -63,6 +64,24 @@ import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history
|
||||
import useInspectVarsCrud from './use-inspect-vars-crud'
|
||||
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
|
||||
|
||||
// Helper function to check if a node is an entry node
|
||||
const isEntryNode = (nodeType: BlockEnum): boolean => {
|
||||
return ENTRY_NODE_TYPES.includes(nodeType as any)
|
||||
}
|
||||
|
||||
// Helper function to check if entry node can be deleted
|
||||
const canDeleteEntryNode = (nodes: Node[], nodeId: string): boolean => {
|
||||
const targetNode = nodes.find(node => node.id === nodeId)
|
||||
if (!targetNode || !isEntryNode(targetNode.data.type))
|
||||
return true // Non-entry nodes can always be deleted
|
||||
|
||||
// Count all entry nodes
|
||||
const entryNodes = nodes.filter(node => isEntryNode(node.data.type))
|
||||
|
||||
// Can delete if there's more than one entry node
|
||||
return entryNodes.length > 1
|
||||
}
|
||||
|
||||
export const useNodesInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
@ -548,15 +567,17 @@ export const useNodesInteractions = () => {
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
|
||||
// Check if entry node can be deleted (must keep at least one entry node)
|
||||
if (!canDeleteEntryNode(nodes, nodeId))
|
||||
return // Cannot delete the last entry node
|
||||
|
||||
const currentNodeIndex = nodes.findIndex(node => node.id === nodeId)
|
||||
const currentNode = nodes[currentNodeIndex]
|
||||
|
||||
if (!currentNode)
|
||||
return
|
||||
|
||||
if (currentNode.data.type === BlockEnum.Start)
|
||||
return
|
||||
|
||||
deleteNodeInspectorVars(nodeId)
|
||||
if (currentNode.data.type === BlockEnum.Iteration) {
|
||||
const iterationChildren = nodes.filter(node => node.parentId === currentNode.id)
|
||||
@ -1388,7 +1409,9 @@ export const useNodesInteractions = () => {
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start)
|
||||
const bundledNodes = nodes.filter(node =>
|
||||
node.data._isBundled && canDeleteEntryNode(nodes, node.id),
|
||||
)
|
||||
|
||||
if (bundledNodes.length) {
|
||||
bundledNodes.forEach(node => handleNodeDelete(node.id))
|
||||
@ -1400,7 +1423,9 @@ export const useNodesInteractions = () => {
|
||||
if (edgeSelected)
|
||||
return
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start)
|
||||
const selectedNode = nodes.find(node =>
|
||||
node.data.selected && canDeleteEntryNode(nodes, node.id),
|
||||
)
|
||||
|
||||
if (selectedNode)
|
||||
handleNodeDelete(selectedNode.id)
|
||||
|
||||
@ -506,7 +506,7 @@ export const useToolIcon = (data: Node['data']) => {
|
||||
const toolIcon = useMemo(() => {
|
||||
if (!data)
|
||||
return ''
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
if (data.type === BlockEnum.Tool || data.type === BlockEnum.TriggerPlugin) {
|
||||
let targetTools = workflowTools
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
targetTools = buildInTools
|
||||
|
||||
@ -1,24 +1,53 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { PluginTriggerNodeType } from './types'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.triggerPlugin'
|
||||
|
||||
const Node: FC<NodeProps<PluginTriggerNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { config = {} } = data
|
||||
const configKeys = Object.keys(config)
|
||||
|
||||
if (!data.plugin_name && configKeys.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="mb-1 px-3 py-1">
|
||||
<div className="text-xs text-gray-700">
|
||||
{t(`${i18nPrefix}.nodeTitle`)}
|
||||
</div>
|
||||
{data.plugin_name && (
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="mb-1 text-xs font-medium text-gray-700">
|
||||
{data.plugin_name}
|
||||
{data.event_type && (
|
||||
<div className="text-xs text-gray-500">
|
||||
{data.event_type}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{configKeys.length > 0 && (
|
||||
<div className="space-y-0.5">
|
||||
{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={String(config[key] || '')}
|
||||
className="w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary"
|
||||
>
|
||||
{typeof config[key] === 'string' && config[key].includes('secret')
|
||||
? '********'
|
||||
: String(config[key] || '')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,25 +1,35 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { PluginTriggerNodeType } from './types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.triggerPlugin'
|
||||
|
||||
const Panel: FC<NodePanelProps<PluginTriggerNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='space-y-4 px-4 pb-2'>
|
||||
<Field title={t(`${i18nPrefix}.title`)}>
|
||||
<div className="text-sm text-gray-500">
|
||||
{t(`${i18nPrefix}.configPlaceholder`)}
|
||||
</div>
|
||||
<Field title="Plugin Trigger">
|
||||
{data.plugin_name ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium">{data.plugin_name}</span>
|
||||
{data.event_type && (
|
||||
<span className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800">
|
||||
{data.event_type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Plugin trigger configured
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500">
|
||||
No plugin selected. Configure this trigger in the workflow canvas.
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import type { CollectionType } from '@/app/components/tools/types'
|
||||
|
||||
export type PluginTriggerNodeType = CommonNodeType & {
|
||||
plugin_id?: string
|
||||
plugin_name?: string
|
||||
event_type?: string
|
||||
config?: Record<string, any>
|
||||
provider_id?: string
|
||||
provider_type?: CollectionType
|
||||
provider_name?: string
|
||||
}
|
||||
|
||||
@ -224,6 +224,7 @@ const translation = {
|
||||
'start': 'Start',
|
||||
'blocks': 'Nodes',
|
||||
'searchTool': 'Search tool',
|
||||
'searchTrigger': 'Search triggers...',
|
||||
'tools': 'Tools',
|
||||
'allTool': 'All',
|
||||
'plugin': 'Plugin',
|
||||
@ -240,6 +241,7 @@ const translation = {
|
||||
},
|
||||
blocks: {
|
||||
'start': 'User Input',
|
||||
'originalStartNode': 'original start node',
|
||||
'end': 'End',
|
||||
'answer': 'Answer',
|
||||
'llm': 'LLM',
|
||||
@ -979,11 +981,6 @@ const translation = {
|
||||
nodeTitle: '🔗 Webhook Trigger',
|
||||
configPlaceholder: 'Webhook trigger configuration will be implemented here',
|
||||
},
|
||||
triggerPlugin: {
|
||||
title: 'Plugin Trigger',
|
||||
nodeTitle: '🔌 Plugin Trigger',
|
||||
configPlaceholder: 'Plugin trigger configuration will be implemented here',
|
||||
},
|
||||
},
|
||||
triggerStatus: {
|
||||
enabled: 'TRIGGER',
|
||||
|
||||
@ -223,6 +223,7 @@ const translation = {
|
||||
'searchBlock': 'ブロック検索',
|
||||
'blocks': 'ブロック',
|
||||
'searchTool': 'ツール検索',
|
||||
'searchTrigger': 'トリガー検索...',
|
||||
'tools': 'ツール',
|
||||
'allTool': 'すべて',
|
||||
'customTool': 'カスタム',
|
||||
@ -240,6 +241,7 @@ const translation = {
|
||||
},
|
||||
blocks: {
|
||||
'start': '開始',
|
||||
'originalStartNode': '元の開始ノード',
|
||||
'end': '終了',
|
||||
'answer': '回答',
|
||||
'llm': 'LLM',
|
||||
@ -979,11 +981,6 @@ const translation = {
|
||||
nodeTitle: '🔗 Webhook トリガー',
|
||||
configPlaceholder: 'Webhook トリガーの設定がここに実装されます',
|
||||
},
|
||||
triggerPlugin: {
|
||||
title: 'プラグイントリガー',
|
||||
nodeTitle: '🔌 プラグイントリガー',
|
||||
configPlaceholder: 'プラグイントリガーの設定がここに実装されます',
|
||||
},
|
||||
},
|
||||
tracing: {
|
||||
stopBy: '{{user}}によって停止',
|
||||
|
||||
@ -223,6 +223,7 @@ const translation = {
|
||||
'searchBlock': '搜索节点',
|
||||
'blocks': '节点',
|
||||
'searchTool': '搜索工具',
|
||||
'searchTrigger': '搜索触发器...',
|
||||
'tools': '工具',
|
||||
'allTool': '全部',
|
||||
'plugin': '插件',
|
||||
@ -240,6 +241,7 @@ const translation = {
|
||||
},
|
||||
blocks: {
|
||||
'start': '开始',
|
||||
'originalStartNode': '原始开始节点',
|
||||
'end': '结束',
|
||||
'answer': '直接回复',
|
||||
'llm': 'LLM',
|
||||
@ -979,11 +981,6 @@ const translation = {
|
||||
title: 'Webhook 触发器',
|
||||
nodeTitle: '🔗 Webhook 触发器',
|
||||
},
|
||||
triggerPlugin: {
|
||||
title: '插件触发器',
|
||||
nodeTitle: '🔌 插件触发器',
|
||||
configPlaceholder: '插件触发器配置将在此处实现',
|
||||
},
|
||||
},
|
||||
tracing: {
|
||||
stopBy: '由{{user}}终止',
|
||||
|
||||
25
web/service/use-triggers.ts
Normal file
25
web/service/use-triggers.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { get } from './base'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
|
||||
const NAME_SPACE = 'triggers'
|
||||
|
||||
// Get all plugins that support trigger functionality
|
||||
// TODO: Backend API not implemented yet - replace with actual triggers endpoint
|
||||
export const useAllTriggerPlugins = (enabled = true) => {
|
||||
return useQuery<ToolWithProvider[]>({
|
||||
queryKey: [NAME_SPACE, 'all'],
|
||||
queryFn: () => get<ToolWithProvider[]>('/workspaces/current/triggers/plugins'),
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
// Get trigger-capable plugins by type (schedule, webhook, etc.)
|
||||
// TODO: Backend API not implemented yet - replace with actual triggers endpoint
|
||||
export const useTriggerPluginsByType = (triggerType: string, enabled = true) => {
|
||||
return useQuery<ToolWithProvider[]>({
|
||||
queryKey: [NAME_SPACE, 'byType', triggerType],
|
||||
queryFn: () => get<ToolWithProvider[]>(`/workspaces/current/triggers/plugins?type=${triggerType}`),
|
||||
enabled: enabled && !!triggerType,
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user