feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: Stream <Stream_2@qq.com> Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com> Co-authored-by: zhsama <torvalds@linux.do> Co-authored-by: Harry <xh001x@hotmail.com> Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: yessenia <yessenia.contact@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: WTW0313 <twwu@dify.ai> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,19 +1,32 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants'
|
||||
import { START_INITIAL_POSITION } from '@/app/components/workflow/constants'
|
||||
import { generateNewNode } from '@/app/components/workflow/utils'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import PluginDependency from '../../workflow/plugin-dependency'
|
||||
import {
|
||||
useAutoGenerateWebhookUrl,
|
||||
useDSL,
|
||||
usePanelInteractions,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import WorkflowHeader from './workflow-header'
|
||||
import WorkflowPanel from './workflow-panel'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type {
|
||||
PluginDefaultValue,
|
||||
TriggerDefaultValue,
|
||||
} from '@/app/components/workflow/block-selector/types'
|
||||
import { useAutoOnboarding } from '../hooks/use-auto-onboarding'
|
||||
import { useAvailableNodesMetaData } from '../hooks'
|
||||
|
||||
const Features = dynamic(() => import('@/app/components/workflow/features'), {
|
||||
ssr: false,
|
||||
@@ -24,6 +37,34 @@ const UpdateDSLModal = dynamic(() => import('@/app/components/workflow/update-ds
|
||||
const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const WorkflowOnboardingModal = dynamic(() => import('./workflow-onboarding-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
const getTriggerPluginNodeData = (
|
||||
triggerConfig: TriggerDefaultValue,
|
||||
fallbackTitle?: string,
|
||||
fallbackDesc?: string,
|
||||
) => {
|
||||
return {
|
||||
plugin_id: triggerConfig.plugin_id,
|
||||
provider_id: triggerConfig.provider_name,
|
||||
provider_type: triggerConfig.provider_type,
|
||||
provider_name: triggerConfig.provider_name,
|
||||
event_name: triggerConfig.event_name,
|
||||
event_label: triggerConfig.event_label,
|
||||
event_description: triggerConfig.event_description,
|
||||
title: triggerConfig.event_label || triggerConfig.title || fallbackTitle,
|
||||
desc: triggerConfig.event_description || fallbackDesc,
|
||||
output_schema: { ...triggerConfig.output_schema },
|
||||
parameters_schema: triggerConfig.paramSchemas ? [...triggerConfig.paramSchemas] : [],
|
||||
config: { ...triggerConfig.params },
|
||||
subscription_id: triggerConfig.subscription_id,
|
||||
plugin_unique_identifier: triggerConfig.plugin_unique_identifier,
|
||||
is_team_authorization: triggerConfig.is_team_authorization,
|
||||
meta: triggerConfig.meta ? { ...triggerConfig.meta } : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const WorkflowChildren = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@@ -31,6 +72,14 @@ const WorkflowChildren = () => {
|
||||
const showFeaturesPanel = useStore(s => s.showFeaturesPanel)
|
||||
const showImportDSLModal = useStore(s => s.showImportDSLModal)
|
||||
const setShowImportDSLModal = useStore(s => s.setShowImportDSLModal)
|
||||
const showOnboarding = useStore(s => s.showOnboarding)
|
||||
const setShowOnboarding = useStore(s => s.setShowOnboarding)
|
||||
const setHasSelectedStartNode = useStore(s => s.setHasSelectedStartNode)
|
||||
const setShouldAutoOpenStartNodeSelector = useStore(s => s.setShouldAutoOpenStartNodeSelector)
|
||||
const reactFlowStore = useStoreApi()
|
||||
const availableNodesMetaData = useAvailableNodesMetaData()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { handleOnboardingClose } = useAutoOnboarding()
|
||||
const {
|
||||
handlePaneContextmenuCancel,
|
||||
} = usePanelInteractions()
|
||||
@@ -44,12 +93,84 @@ const WorkflowChildren = () => {
|
||||
setSecretEnvList(v.payload.data as EnvironmentVariable[])
|
||||
})
|
||||
|
||||
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
|
||||
|
||||
const handleCloseOnboarding = useCallback(() => {
|
||||
handleOnboardingClose()
|
||||
}, [handleOnboardingClose])
|
||||
|
||||
const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
|
||||
const nodeDefault = availableNodesMetaData.nodesMap?.[nodeType]
|
||||
if (!nodeDefault?.defaultValue)
|
||||
return
|
||||
|
||||
const baseNodeData = { ...nodeDefault.defaultValue }
|
||||
|
||||
const mergedNodeData = (() => {
|
||||
if (nodeType !== BlockEnum.TriggerPlugin || !toolConfig) {
|
||||
return {
|
||||
...baseNodeData,
|
||||
...toolConfig,
|
||||
}
|
||||
}
|
||||
|
||||
const triggerNodeData = getTriggerPluginNodeData(
|
||||
toolConfig as TriggerDefaultValue,
|
||||
baseNodeData.title,
|
||||
baseNodeData.desc,
|
||||
)
|
||||
|
||||
return {
|
||||
...baseNodeData,
|
||||
...triggerNodeData,
|
||||
config: {
|
||||
...(baseNodeData as { config?: Record<string, any> }).config,
|
||||
...triggerNodeData.config,
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
const { newNode } = generateNewNode({
|
||||
data: {
|
||||
...mergedNodeData,
|
||||
} as any,
|
||||
position: START_INITIAL_POSITION,
|
||||
})
|
||||
|
||||
const { setNodes, setEdges } = reactFlowStore.getState()
|
||||
setNodes([newNode])
|
||||
setEdges([])
|
||||
|
||||
setShowOnboarding?.(false)
|
||||
setHasSelectedStartNode?.(true)
|
||||
setShouldAutoOpenStartNodeSelector?.(true)
|
||||
|
||||
handleSyncWorkflowDraft(true, false, {
|
||||
onSuccess: () => {
|
||||
autoGenerateWebhookUrl(newNode.id)
|
||||
console.log('Node successfully saved to draft')
|
||||
},
|
||||
onError: () => {
|
||||
console.error('Failed to save node to draft')
|
||||
},
|
||||
})
|
||||
}, [availableNodesMetaData, setShowOnboarding, setHasSelectedStartNode, reactFlowStore, handleSyncWorkflowDraft])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PluginDependency />
|
||||
{
|
||||
showFeaturesPanel && <Features />
|
||||
}
|
||||
{
|
||||
showOnboarding && (
|
||||
<WorkflowOnboardingModal
|
||||
isShow={showOnboarding}
|
||||
onClose={handleCloseOnboarding}
|
||||
onSelectStartNode={handleSelectStartNode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showImportDSLModal && (
|
||||
<UpdateDSLModal
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useEdges, useNodes, useStore as useReactflowStore } from 'reactflow'
|
||||
import { useEdges, useNodes } from 'reactflow'
|
||||
import { RiApps2AddLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useChecklistBeforePublish,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
// useWorkflowRunValidation,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import AppPublisher from '@/app/components/app/app-publisher'
|
||||
@@ -22,36 +23,44 @@ import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import type {
|
||||
CommonEdgeType,
|
||||
CommonNodeType,
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
BlockEnum,
|
||||
InputVarType,
|
||||
isTriggerNode,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useInvalidateAppWorkflow, usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
import { useInvalidateAppTriggers } from '@/service/use-tools'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useIsChatMode } from '@/app/components/workflow/hooks'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
|
||||
const FeaturesTrigger = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const isChatMode = useIsChatMode()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appID = appDetail?.id
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
const {
|
||||
nodesReadOnly,
|
||||
getNodesReadOnly,
|
||||
} = useNodesReadOnly()
|
||||
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||
const toolPublished = useStore(s => s.toolPublished)
|
||||
const startVariables = useReactflowStore(
|
||||
s => s.getNodes().find(node => node.data.type === BlockEnum.Start)?.data.variables,
|
||||
)
|
||||
const lastPublishedHasUserInput = useStore(s => s.lastPublishedHasUserInput)
|
||||
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const hasWorkflowNodes = nodes.length > 0
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const startVariables = (startNode as Node<StartNodeType>)?.data?.variables
|
||||
const edges = useEdges<CommonEdgeType>()
|
||||
|
||||
const fileSettings = useFeatures(s => s.features.file)
|
||||
const variables = useMemo(() => {
|
||||
const data = startVariables || []
|
||||
@@ -73,6 +82,22 @@ const FeaturesTrigger = () => {
|
||||
const { handleCheckBeforePublish } = useChecklistBeforePublish()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { notify } = useToastContext()
|
||||
const startNodeIds = useMemo(
|
||||
() => nodes.filter(node => node.data.type === BlockEnum.Start).map(node => node.id),
|
||||
[nodes],
|
||||
)
|
||||
const hasUserInputNode = useMemo(() => {
|
||||
if (!startNodeIds.length)
|
||||
return false
|
||||
return edges.some(edge => startNodeIds.includes(edge.source))
|
||||
}, [edges, startNodeIds])
|
||||
// Track trigger presence so the publisher can adjust UI (e.g. hide missing start section).
|
||||
const hasTriggerNode = useMemo(() => (
|
||||
nodes.some(node => isTriggerNode(node.data.type as BlockEnum))
|
||||
), [nodes])
|
||||
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
|
||||
const invalidateAppTriggers = useInvalidateAppTriggers()
|
||||
|
||||
const handleShowFeatures = useCallback(() => {
|
||||
const {
|
||||
@@ -85,8 +110,6 @@ const FeaturesTrigger = () => {
|
||||
setShowFeaturesPanel(!showFeaturesPanel)
|
||||
}, [workflowStore, getNodesReadOnly])
|
||||
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
|
||||
|
||||
const updateAppDetail = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetchAppDetail({ url: '/apps', id: appID! })
|
||||
@@ -96,14 +119,17 @@ const FeaturesTrigger = () => {
|
||||
console.error(error)
|
||||
}
|
||||
}, [appID, setAppDetail])
|
||||
|
||||
const { mutateAsync: publishWorkflow } = usePublishWorkflow()
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const edges = useEdges<CommonEdgeType>()
|
||||
// const { validateBeforeRun } = useWorkflowRunValidation()
|
||||
const needWarningNodes = useChecklist(nodes, edges)
|
||||
|
||||
const updatePublishedWorkflow = useInvalidateAppWorkflow()
|
||||
const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
|
||||
// First check if there are any items in the checklist
|
||||
// if (!validateBeforeRun())
|
||||
// throw new Error('Checklist has unresolved items')
|
||||
|
||||
if (needWarningNodes.length > 0) {
|
||||
notify({ type: 'error', message: t('workflow.panel.checklistTip') })
|
||||
throw new Error('Checklist has unresolved items')
|
||||
@@ -121,14 +147,16 @@ const FeaturesTrigger = () => {
|
||||
notify({ type: 'success', message: t('common.api.actionSuccess') })
|
||||
updatePublishedWorkflow(appID!)
|
||||
updateAppDetail()
|
||||
invalidateAppTriggers(appID!)
|
||||
workflowStore.getState().setPublishedAt(res.created_at)
|
||||
workflowStore.getState().setLastPublishedHasUserInput(hasUserInputNode)
|
||||
resetWorkflowVersionHistory()
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error('Checklist failed')
|
||||
}
|
||||
}, [needWarningNodes, handleCheckBeforePublish, publishWorkflow, notify, appID, t, updatePublishedWorkflow, updateAppDetail, workflowStore, resetWorkflowVersionHistory])
|
||||
}, [needWarningNodes, handleCheckBeforePublish, publishWorkflow, notify, appID, t, updatePublishedWorkflow, updateAppDetail, workflowStore, resetWorkflowVersionHistory, invalidateAppTriggers])
|
||||
|
||||
const onPublisherToggle = useCallback((state: boolean) => {
|
||||
if (state)
|
||||
@@ -141,27 +169,34 @@ const FeaturesTrigger = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className={cn(
|
||||
'text-components-button-secondary-text',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
onClick={handleShowFeatures}
|
||||
>
|
||||
<RiApps2AddLine className='mr-1 h-4 w-4 text-components-button-secondary-text' />
|
||||
{t('workflow.common.features')}
|
||||
</Button>
|
||||
{/* Feature button is only visible in chatflow mode (advanced-chat) */}
|
||||
{isChatMode && (
|
||||
<Button
|
||||
className={cn(
|
||||
'text-components-button-secondary-text',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
onClick={handleShowFeatures}
|
||||
>
|
||||
<RiApps2AddLine className='mr-1 h-4 w-4 text-components-button-secondary-text' />
|
||||
{t('workflow.common.features')}
|
||||
</Button>
|
||||
)}
|
||||
<AppPublisher
|
||||
{...{
|
||||
publishedAt,
|
||||
draftUpdatedAt,
|
||||
disabled: nodesReadOnly,
|
||||
disabled: nodesReadOnly || !hasWorkflowNodes,
|
||||
toolPublished,
|
||||
inputs: variables,
|
||||
onRefreshData: handleToolConfigureUpdate,
|
||||
onPublish,
|
||||
onToggle: onPublisherToggle,
|
||||
workflowToolAvailable: lastPublishedHasUserInput,
|
||||
crossAxisOffset: 4,
|
||||
missingStartNode: !startNode,
|
||||
hasTriggerNode,
|
||||
publishDisabled: !hasWorkflowNodes,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -41,8 +41,8 @@ const WorkflowHeader = () => {
|
||||
return {
|
||||
normal: {
|
||||
components: {
|
||||
left: <ChatVariableTrigger />,
|
||||
middle: <FeaturesTrigger />,
|
||||
chatVariableTrigger: <ChatVariableTrigger />,
|
||||
},
|
||||
runAndHistoryProps: {
|
||||
showRunButton: !isChatMode,
|
||||
|
||||
@@ -66,6 +66,10 @@ const WorkflowMain = ({
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowTriggerScheduleRunInWorkflow,
|
||||
handleWorkflowTriggerWebhookRunInWorkflow,
|
||||
handleWorkflowTriggerPluginRunInWorkflow,
|
||||
handleWorkflowRunAllTriggersInWorkflow,
|
||||
} = useWorkflowStartRun()
|
||||
const availableNodesMetaData = useAvailableNodesMetaData()
|
||||
const { getWorkflowRunAndTraceUrl } = useGetRunAndTraceUrl()
|
||||
@@ -108,6 +112,10 @@ const WorkflowMain = ({
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowTriggerScheduleRunInWorkflow,
|
||||
handleWorkflowTriggerWebhookRunInWorkflow,
|
||||
handleWorkflowTriggerPluginRunInWorkflow,
|
||||
handleWorkflowRunAllTriggersInWorkflow,
|
||||
availableNodesMetaData,
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
@@ -141,6 +149,10 @@ const WorkflowMain = ({
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowTriggerScheduleRunInWorkflow,
|
||||
handleWorkflowTriggerWebhookRunInWorkflow,
|
||||
handleWorkflowTriggerPluginRunInWorkflow,
|
||||
handleWorkflowRunAllTriggersInWorkflow,
|
||||
availableNodesMetaData,
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import StartNodeSelectionPanel from './start-node-selection-panel'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
type WorkflowOnboardingModalProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onSelectStartNode: (nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => void
|
||||
}
|
||||
|
||||
const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({
|
||||
isShow,
|
||||
onClose,
|
||||
onSelectStartNode,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
|
||||
const handleSelectUserInput = useCallback(() => {
|
||||
onSelectStartNode(BlockEnum.Start)
|
||||
onClose() // Close modal after selection
|
||||
}, [onSelectStartNode, onClose])
|
||||
|
||||
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
|
||||
onSelectStartNode(nodeType, toolConfig)
|
||||
onClose() // Close modal after selection
|
||||
}, [onSelectStartNode, onClose])
|
||||
|
||||
useEffect(() => {
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isShow)
|
||||
onClose()
|
||||
}
|
||||
document.addEventListener('keydown', handleEsc)
|
||||
return () => document.removeEventListener('keydown', handleEsc)
|
||||
}, [isShow, onClose])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className="w-[618px] max-w-[618px] rounded-2xl border border-effects-highlight bg-background-default-subtle shadow-lg"
|
||||
overlayOpacity
|
||||
closable
|
||||
clickOutsideNotClose
|
||||
>
|
||||
<div className="pb-4">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h3 className="title-2xl-semi-bold mb-2 text-text-primary">
|
||||
{t('workflow.onboarding.title')}
|
||||
</h3>
|
||||
<div className="body-xs-regular leading-4 text-text-tertiary">
|
||||
{t('workflow.onboarding.description')}{' '}
|
||||
<a
|
||||
href={docLink('/guides/workflow/node/start')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-text-accent-hover cursor-pointer text-text-accent underline"
|
||||
>
|
||||
{t('workflow.onboarding.learnMore')}
|
||||
</a>{' '}
|
||||
{t('workflow.onboarding.aboutStartNode')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<StartNodeSelectionPanel
|
||||
onSelectUserInput={handleSelectUserInput}
|
||||
onSelectTrigger={handleTriggerSelect}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* ESC tip below modal */}
|
||||
{isShow && (
|
||||
<div className="body-xs-regular pointer-events-none fixed left-1/2 top-1/2 z-[70] flex -translate-x-1/2 translate-y-[165px] items-center gap-1 text-text-quaternary">
|
||||
<span>{t('workflow.onboarding.escTip.press')}</span>
|
||||
<kbd className="system-kbd inline-flex h-4 min-w-4 items-center justify-center rounded bg-components-kbd-bg-gray px-1 text-text-tertiary">
|
||||
{t('workflow.onboarding.escTip.key')}
|
||||
</kbd>
|
||||
<span>{t('workflow.onboarding.escTip.toDismiss')}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowOnboardingModal
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type StartNodeOptionProps = {
|
||||
icon: ReactNode
|
||||
title: string
|
||||
subtitle?: string
|
||||
description: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const StartNodeOption: FC<StartNodeOptionProps> = ({
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'hover:border-components-panel-border-active flex h-40 w-[280px] cursor-pointer flex-col gap-2 rounded-xl border-[0.5px] border-components-option-card-option-border bg-components-panel-on-panel-item-bg p-4 shadow-sm transition-all hover:shadow-md',
|
||||
)}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="shrink-0">
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
{/* Text content */}
|
||||
<div className="flex h-[74px] flex-col gap-1 py-0.5">
|
||||
<div className="h-5 leading-5">
|
||||
<h3 className="system-md-semi-bold text-text-primary">
|
||||
{title}
|
||||
{subtitle && (
|
||||
<span className="system-md-regular text-text-quaternary"> {subtitle}</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="h-12 leading-4">
|
||||
<p className="system-xs-regular text-text-tertiary">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StartNodeOption
|
||||
@@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import StartNodeOption from './start-node-option'
|
||||
import NodeSelector from '@/app/components/workflow/block-selector'
|
||||
import { Home } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import { TabsEnum } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
type StartNodeSelectionPanelProps = {
|
||||
onSelectUserInput: () => void
|
||||
onSelectTrigger: (nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => void
|
||||
}
|
||||
|
||||
const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
|
||||
onSelectUserInput,
|
||||
onSelectTrigger,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [showTriggerSelector, setShowTriggerSelector] = useState(false)
|
||||
|
||||
const handleTriggerClick = useCallback(() => {
|
||||
setShowTriggerSelector(true)
|
||||
}, [])
|
||||
|
||||
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
|
||||
setShowTriggerSelector(false)
|
||||
onSelectTrigger(nodeType, toolConfig)
|
||||
}, [onSelectTrigger])
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<StartNodeOption
|
||||
icon={
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-[10px] border-[0.5px] border-transparent bg-util-colors-blue-brand-blue-brand-500 p-2">
|
||||
<Home className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
}
|
||||
title={t('workflow.onboarding.userInputFull')}
|
||||
description={t('workflow.onboarding.userInputDescription')}
|
||||
onClick={onSelectUserInput}
|
||||
/>
|
||||
|
||||
<NodeSelector
|
||||
open={showTriggerSelector}
|
||||
onOpenChange={setShowTriggerSelector}
|
||||
onSelect={handleTriggerSelect}
|
||||
placement="right"
|
||||
offset={-200}
|
||||
noBlocks={true}
|
||||
showStartTab={true}
|
||||
defaultActiveTab={TabsEnum.Start}
|
||||
forceShowStartContent={true}
|
||||
availableBlocksTypes={[
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]}
|
||||
trigger={() => (
|
||||
<StartNodeOption
|
||||
icon={
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-[10px] border-[0.5px] border-transparent bg-util-colors-blue-brand-blue-brand-500 p-2">
|
||||
<TriggerAll className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
}
|
||||
title={t('workflow.onboarding.trigger')}
|
||||
description={t('workflow.onboarding.triggerDescription')}
|
||||
onClick={handleTriggerClick}
|
||||
/>
|
||||
)}
|
||||
popupClassName="z-[1200]"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StartNodeSelectionPanel
|
||||
Reference in New Issue
Block a user