mirror of https://github.com/langgenius/dify.git
feat: implement workflow onboarding modal system (#24551)
This commit is contained in:
parent
2984dbc0df
commit
7129de98cd
|
|
@ -0,0 +1,572 @@
|
|||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
// Mock zustand store
|
||||
jest.mock('@/app/components/workflow/store')
|
||||
|
||||
// Mock ReactFlow store
|
||||
const mockGetNodes = jest.fn()
|
||||
jest.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
getNodes: mockGetNodes,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Workflow Onboarding Integration Logic', () => {
|
||||
const mockSetShowOnboarding = jest.fn()
|
||||
const mockSetHasSelectedStartNode = jest.fn()
|
||||
const mockSetHasShownOnboarding = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Mock store implementation
|
||||
;(useWorkflowStore as jest.Mock).mockReturnValue({
|
||||
showOnboarding: false,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
hasSelectedStartNode: false,
|
||||
setHasSelectedStartNode: mockSetHasSelectedStartNode,
|
||||
hasShownOnboarding: false,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
notInitialWorkflow: false,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Onboarding State Management', () => {
|
||||
it('should initialize onboarding state correctly', () => {
|
||||
const store = useWorkflowStore()
|
||||
|
||||
expect(store.showOnboarding).toBe(false)
|
||||
expect(store.hasSelectedStartNode).toBe(false)
|
||||
expect(store.hasShownOnboarding).toBe(false)
|
||||
})
|
||||
|
||||
it('should update onboarding visibility', () => {
|
||||
const store = useWorkflowStore()
|
||||
|
||||
store.setShowOnboarding(true)
|
||||
expect(mockSetShowOnboarding).toHaveBeenCalledWith(true)
|
||||
|
||||
store.setShowOnboarding(false)
|
||||
expect(mockSetShowOnboarding).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should track node selection state', () => {
|
||||
const store = useWorkflowStore()
|
||||
|
||||
store.setHasSelectedStartNode(true)
|
||||
expect(mockSetHasSelectedStartNode).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should track onboarding show state', () => {
|
||||
const store = useWorkflowStore()
|
||||
|
||||
store.setHasShownOnboarding(true)
|
||||
expect(mockSetHasShownOnboarding).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Node Validation Logic', () => {
|
||||
/**
|
||||
* Test the critical fix in use-nodes-sync-draft.ts
|
||||
* This ensures trigger nodes are recognized as valid start nodes
|
||||
*/
|
||||
it('should validate Start node as valid start node', () => {
|
||||
const mockNode = {
|
||||
data: { type: BlockEnum.Start },
|
||||
id: 'start-1',
|
||||
}
|
||||
|
||||
// Simulate the validation logic from use-nodes-sync-draft.ts
|
||||
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
expect(isValidStartNode).toBe(true)
|
||||
})
|
||||
|
||||
it('should validate TriggerSchedule as valid start node', () => {
|
||||
const mockNode = {
|
||||
data: { type: BlockEnum.TriggerSchedule },
|
||||
id: 'trigger-schedule-1',
|
||||
}
|
||||
|
||||
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
expect(isValidStartNode).toBe(true)
|
||||
})
|
||||
|
||||
it('should validate TriggerWebhook as valid start node', () => {
|
||||
const mockNode = {
|
||||
data: { type: BlockEnum.TriggerWebhook },
|
||||
id: 'trigger-webhook-1',
|
||||
}
|
||||
|
||||
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
expect(isValidStartNode).toBe(true)
|
||||
})
|
||||
|
||||
it('should validate TriggerPlugin as valid start node', () => {
|
||||
const mockNode = {
|
||||
data: { type: BlockEnum.TriggerPlugin },
|
||||
id: 'trigger-plugin-1',
|
||||
}
|
||||
|
||||
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
expect(isValidStartNode).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject non-trigger nodes as invalid start nodes', () => {
|
||||
const mockNode = {
|
||||
data: { type: BlockEnum.LLM },
|
||||
id: 'llm-1',
|
||||
}
|
||||
|
||||
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|
||||
|| mockNode.data.type === BlockEnum.TriggerSchedule
|
||||
|| mockNode.data.type === BlockEnum.TriggerWebhook
|
||||
|| mockNode.data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
expect(isValidStartNode).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle array of nodes with mixed types', () => {
|
||||
const mockNodes = [
|
||||
{ data: { type: BlockEnum.LLM }, id: 'llm-1' },
|
||||
{ data: { type: BlockEnum.TriggerWebhook }, id: 'webhook-1' },
|
||||
{ data: { type: BlockEnum.Answer }, id: 'answer-1' },
|
||||
]
|
||||
|
||||
// Simulate hasStartNode logic from use-nodes-sync-draft.ts
|
||||
const hasStartNode = mockNodes.find(node =>
|
||||
node.data.type === BlockEnum.Start
|
||||
|| node.data.type === BlockEnum.TriggerSchedule
|
||||
|| node.data.type === BlockEnum.TriggerWebhook
|
||||
|| node.data.type === BlockEnum.TriggerPlugin,
|
||||
)
|
||||
|
||||
expect(hasStartNode).toBeTruthy()
|
||||
expect(hasStartNode?.id).toBe('webhook-1')
|
||||
})
|
||||
|
||||
it('should return undefined when no valid start nodes exist', () => {
|
||||
const mockNodes = [
|
||||
{ data: { type: BlockEnum.LLM }, id: 'llm-1' },
|
||||
{ data: { type: BlockEnum.Answer }, id: 'answer-1' },
|
||||
]
|
||||
|
||||
const hasStartNode = mockNodes.find(node =>
|
||||
node.data.type === BlockEnum.Start
|
||||
|| node.data.type === BlockEnum.TriggerSchedule
|
||||
|| node.data.type === BlockEnum.TriggerWebhook
|
||||
|| node.data.type === BlockEnum.TriggerPlugin,
|
||||
)
|
||||
|
||||
expect(hasStartNode).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Auto-expand Logic for Node Handles', () => {
|
||||
/**
|
||||
* Test the auto-expand logic from node-handle.tsx
|
||||
* This ensures all trigger types auto-expand the block selector
|
||||
*/
|
||||
it('should auto-expand for Start node in new workflow', () => {
|
||||
const notInitialWorkflow = true
|
||||
const nodeType = BlockEnum.Start
|
||||
const isChatMode = false
|
||||
|
||||
const shouldAutoExpand = notInitialWorkflow && (
|
||||
nodeType === BlockEnum.Start
|
||||
|| nodeType === BlockEnum.TriggerSchedule
|
||||
|| nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerPlugin
|
||||
) && !isChatMode
|
||||
|
||||
expect(shouldAutoExpand).toBe(true)
|
||||
})
|
||||
|
||||
it('should auto-expand for TriggerSchedule in new workflow', () => {
|
||||
const notInitialWorkflow = true
|
||||
const nodeType = BlockEnum.TriggerSchedule
|
||||
const isChatMode = false
|
||||
|
||||
const shouldAutoExpand = notInitialWorkflow && (
|
||||
nodeType === BlockEnum.Start
|
||||
|| nodeType === BlockEnum.TriggerSchedule
|
||||
|| nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerPlugin
|
||||
) && !isChatMode
|
||||
|
||||
expect(shouldAutoExpand).toBe(true)
|
||||
})
|
||||
|
||||
it('should auto-expand for TriggerWebhook in new workflow', () => {
|
||||
const notInitialWorkflow = true
|
||||
const nodeType = BlockEnum.TriggerWebhook
|
||||
const isChatMode = false
|
||||
|
||||
const shouldAutoExpand = notInitialWorkflow && (
|
||||
nodeType === BlockEnum.Start
|
||||
|| nodeType === BlockEnum.TriggerSchedule
|
||||
|| nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerPlugin
|
||||
) && !isChatMode
|
||||
|
||||
expect(shouldAutoExpand).toBe(true)
|
||||
})
|
||||
|
||||
it('should auto-expand for TriggerPlugin in new workflow', () => {
|
||||
const notInitialWorkflow = true
|
||||
const nodeType = BlockEnum.TriggerPlugin
|
||||
const isChatMode = false
|
||||
|
||||
const shouldAutoExpand = notInitialWorkflow && (
|
||||
nodeType === BlockEnum.Start
|
||||
|| nodeType === BlockEnum.TriggerSchedule
|
||||
|| nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerPlugin
|
||||
) && !isChatMode
|
||||
|
||||
expect(shouldAutoExpand).toBe(true)
|
||||
})
|
||||
|
||||
it('should not auto-expand for non-trigger nodes', () => {
|
||||
const notInitialWorkflow = true
|
||||
const nodeType = BlockEnum.LLM
|
||||
const isChatMode = false
|
||||
|
||||
const shouldAutoExpand = notInitialWorkflow && (
|
||||
nodeType === BlockEnum.Start
|
||||
|| nodeType === BlockEnum.TriggerSchedule
|
||||
|| nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerPlugin
|
||||
) && !isChatMode
|
||||
|
||||
expect(shouldAutoExpand).toBe(false)
|
||||
})
|
||||
|
||||
it('should not auto-expand in chat mode', () => {
|
||||
const notInitialWorkflow = true
|
||||
const nodeType = BlockEnum.Start
|
||||
const isChatMode = true
|
||||
|
||||
const shouldAutoExpand = notInitialWorkflow && (
|
||||
nodeType === BlockEnum.Start
|
||||
|| nodeType === BlockEnum.TriggerSchedule
|
||||
|| nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerPlugin
|
||||
) && !isChatMode
|
||||
|
||||
expect(shouldAutoExpand).toBe(false)
|
||||
})
|
||||
|
||||
it('should not auto-expand for existing workflows', () => {
|
||||
const notInitialWorkflow = false
|
||||
const nodeType = BlockEnum.Start
|
||||
const isChatMode = false
|
||||
|
||||
const shouldAutoExpand = notInitialWorkflow && (
|
||||
nodeType === BlockEnum.Start
|
||||
|| nodeType === BlockEnum.TriggerSchedule
|
||||
|| nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerPlugin
|
||||
) && !isChatMode
|
||||
|
||||
expect(shouldAutoExpand).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Node Creation Without Auto-selection', () => {
|
||||
/**
|
||||
* Test that nodes are created without the 'selected: true' property
|
||||
* This prevents auto-opening the properties panel
|
||||
*/
|
||||
it('should create Start node without auto-selection', () => {
|
||||
const nodeData = { type: BlockEnum.Start, title: 'Start' }
|
||||
|
||||
// Simulate node creation logic from workflow-children.tsx
|
||||
const createdNodeData = {
|
||||
...nodeData,
|
||||
// Note: 'selected: true' should NOT be added
|
||||
}
|
||||
|
||||
expect(createdNodeData.selected).toBeUndefined()
|
||||
expect(createdNodeData.type).toBe(BlockEnum.Start)
|
||||
})
|
||||
|
||||
it('should create TriggerWebhook node without auto-selection', () => {
|
||||
const nodeData = { type: BlockEnum.TriggerWebhook, title: 'Webhook Trigger' }
|
||||
const toolConfig = { webhook_url: 'https://example.com/webhook' }
|
||||
|
||||
const createdNodeData = {
|
||||
...nodeData,
|
||||
...toolConfig,
|
||||
// Note: 'selected: true' should NOT be added
|
||||
}
|
||||
|
||||
expect(createdNodeData.selected).toBeUndefined()
|
||||
expect(createdNodeData.type).toBe(BlockEnum.TriggerWebhook)
|
||||
expect(createdNodeData.webhook_url).toBe('https://example.com/webhook')
|
||||
})
|
||||
|
||||
it('should preserve other node properties while avoiding auto-selection', () => {
|
||||
const nodeData = {
|
||||
type: BlockEnum.TriggerSchedule,
|
||||
title: 'Schedule Trigger',
|
||||
config: { interval: '1h' },
|
||||
}
|
||||
|
||||
const createdNodeData = {
|
||||
...nodeData,
|
||||
}
|
||||
|
||||
expect(createdNodeData.selected).toBeUndefined()
|
||||
expect(createdNodeData.type).toBe(BlockEnum.TriggerSchedule)
|
||||
expect(createdNodeData.title).toBe('Schedule Trigger')
|
||||
expect(createdNodeData.config).toEqual({ interval: '1h' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Workflow Initialization Logic', () => {
|
||||
/**
|
||||
* Test the initialization logic from use-workflow-init.ts
|
||||
* This ensures onboarding is triggered correctly for new workflows
|
||||
*/
|
||||
it('should trigger onboarding for new workflow when draft does not exist', () => {
|
||||
// Simulate the error handling logic from use-workflow-init.ts
|
||||
const error = {
|
||||
json: jest.fn().mockResolvedValue({ code: 'draft_workflow_not_exist' }),
|
||||
bodyUsed: false,
|
||||
}
|
||||
|
||||
const mockWorkflowStore = {
|
||||
setState: jest.fn(),
|
||||
}
|
||||
|
||||
// Simulate error handling
|
||||
if (error && error.json && !error.bodyUsed) {
|
||||
error.json().then((err: any) => {
|
||||
if (err.code === 'draft_workflow_not_exist') {
|
||||
mockWorkflowStore.setState({
|
||||
notInitialWorkflow: true,
|
||||
showOnboarding: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return error.json().then(() => {
|
||||
expect(mockWorkflowStore.setState).toHaveBeenCalledWith({
|
||||
notInitialWorkflow: true,
|
||||
showOnboarding: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not trigger onboarding for existing workflows', () => {
|
||||
// Simulate successful draft fetch
|
||||
const mockWorkflowStore = {
|
||||
setState: jest.fn(),
|
||||
}
|
||||
|
||||
// Normal initialization path should not set showOnboarding: true
|
||||
mockWorkflowStore.setState({
|
||||
environmentVariables: [],
|
||||
conversationVariables: [],
|
||||
})
|
||||
|
||||
expect(mockWorkflowStore.setState).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ showOnboarding: true }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should create empty draft with proper structure', () => {
|
||||
const mockSyncWorkflowDraft = jest.fn()
|
||||
const appId = 'test-app-id'
|
||||
|
||||
// Simulate the syncWorkflowDraft call from use-workflow-init.ts
|
||||
const draftParams = {
|
||||
url: `/apps/${appId}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: [], // Empty nodes initially
|
||||
edges: [],
|
||||
},
|
||||
features: {
|
||||
retriever_resource: { enabled: true },
|
||||
},
|
||||
environment_variables: [],
|
||||
conversation_variables: [],
|
||||
},
|
||||
}
|
||||
|
||||
mockSyncWorkflowDraft(draftParams)
|
||||
|
||||
expect(mockSyncWorkflowDraft).toHaveBeenCalledWith({
|
||||
url: `/apps/${appId}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
features: {
|
||||
retriever_resource: { enabled: true },
|
||||
},
|
||||
environment_variables: [],
|
||||
conversation_variables: [],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Auto-Detection for Empty Canvas', () => {
|
||||
beforeEach(() => {
|
||||
mockGetNodes.mockClear()
|
||||
})
|
||||
|
||||
it('should detect empty canvas and trigger onboarding', () => {
|
||||
// Mock empty canvas
|
||||
mockGetNodes.mockReturnValue([])
|
||||
|
||||
// Mock store with proper state for auto-detection
|
||||
;(useWorkflowStore as jest.Mock).mockReturnValue({
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: false,
|
||||
notInitialWorkflow: false,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
getState: () => ({
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: false,
|
||||
notInitialWorkflow: false,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
}),
|
||||
})
|
||||
|
||||
// Simulate empty canvas check logic
|
||||
const nodes = mockGetNodes()
|
||||
const startNodeTypes = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
const hasStartNode = nodes.some(node => startNodeTypes.includes(node.data?.type))
|
||||
const isEmpty = nodes.length === 0 || !hasStartNode
|
||||
|
||||
expect(isEmpty).toBe(true)
|
||||
expect(nodes.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should detect canvas with non-start nodes as empty', () => {
|
||||
// Mock canvas with non-start nodes
|
||||
mockGetNodes.mockReturnValue([
|
||||
{ id: '1', data: { type: BlockEnum.LLM } },
|
||||
{ id: '2', data: { type: BlockEnum.Code } },
|
||||
])
|
||||
|
||||
const nodes = mockGetNodes()
|
||||
const startNodeTypes = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
const hasStartNode = nodes.some(node => startNodeTypes.includes(node.data.type))
|
||||
const isEmpty = nodes.length === 0 || !hasStartNode
|
||||
|
||||
expect(isEmpty).toBe(true)
|
||||
expect(hasStartNode).toBe(false)
|
||||
})
|
||||
|
||||
it('should not detect canvas with start nodes as empty', () => {
|
||||
// Mock canvas with start node
|
||||
mockGetNodes.mockReturnValue([
|
||||
{ id: '1', data: { type: BlockEnum.Start } },
|
||||
])
|
||||
|
||||
const nodes = mockGetNodes()
|
||||
const startNodeTypes = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
const hasStartNode = nodes.some(node => startNodeTypes.includes(node.data.type))
|
||||
const isEmpty = nodes.length === 0 || !hasStartNode
|
||||
|
||||
expect(isEmpty).toBe(false)
|
||||
expect(hasStartNode).toBe(true)
|
||||
})
|
||||
|
||||
it('should not trigger onboarding if already shown in session', () => {
|
||||
// Mock empty canvas
|
||||
mockGetNodes.mockReturnValue([])
|
||||
|
||||
// Mock store with hasShownOnboarding = true
|
||||
;(useWorkflowStore as jest.Mock).mockReturnValue({
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: true, // Already shown in this session
|
||||
notInitialWorkflow: false,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
getState: () => ({
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: true,
|
||||
notInitialWorkflow: false,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
}),
|
||||
})
|
||||
|
||||
// Simulate the check logic with hasShownOnboarding = true
|
||||
const store = useWorkflowStore()
|
||||
const shouldTrigger = !store.hasShownOnboarding && !store.showOnboarding && !store.notInitialWorkflow
|
||||
|
||||
expect(shouldTrigger).toBe(false)
|
||||
})
|
||||
|
||||
it('should not trigger onboarding during initial workflow creation', () => {
|
||||
// Mock empty canvas
|
||||
mockGetNodes.mockReturnValue([])
|
||||
|
||||
// Mock store with notInitialWorkflow = true (initial creation)
|
||||
;(useWorkflowStore as jest.Mock).mockReturnValue({
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: false,
|
||||
notInitialWorkflow: true, // Initial workflow creation
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
getState: () => ({
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: false,
|
||||
notInitialWorkflow: true,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
}),
|
||||
})
|
||||
|
||||
// Simulate the check logic with notInitialWorkflow = true
|
||||
const store = useWorkflowStore()
|
||||
const shouldTrigger = !store.hasShownOnboarding && !store.showOnboarding && !store.notInitialWorkflow
|
||||
|
||||
expect(shouldTrigger).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -16,6 +16,8 @@ type IModal = {
|
|||
closable?: boolean
|
||||
overflowVisible?: boolean
|
||||
highPriority?: boolean // For modals that need to appear above dropdowns
|
||||
overlayOpacity?: boolean // For semi-transparent overlay instead of default
|
||||
clickOutsideNotClose?: boolean // Prevent closing when clicking outside modal
|
||||
}
|
||||
|
||||
export default function Modal({
|
||||
|
|
@ -29,13 +31,16 @@ export default function Modal({
|
|||
closable = false,
|
||||
overflowVisible = false,
|
||||
highPriority = false,
|
||||
overlayOpacity = false,
|
||||
clickOutsideNotClose = false,
|
||||
}: IModal) {
|
||||
return (
|
||||
<Transition appear show={isShow} as={Fragment}>
|
||||
<Dialog as="div" className={classNames('relative', highPriority ? 'z-[1100]' : 'z-[60]', wrapperClassName)} onClose={onClose}>
|
||||
<Dialog as="div" className={classNames('relative', highPriority ? 'z-[1100]' : 'z-[60]', wrapperClassName)} onClose={clickOutsideNotClose ? noop : onClose}>
|
||||
<TransitionChild>
|
||||
<div className={classNames(
|
||||
'fixed inset-0 bg-background-overlay',
|
||||
'fixed inset-0',
|
||||
overlayOpacity ? 'bg-workflow-canvas-canvas-overlay' : 'bg-background-overlay',
|
||||
'duration-300 ease-in data-[closed]:opacity-0',
|
||||
'data-[enter]:opacity-100',
|
||||
'data-[leave]:opacity-0',
|
||||
|
|
|
|||
|
|
@ -1,31 +1,51 @@
|
|||
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 { useNodesInitialData } from '@/app/components/workflow/hooks'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import PluginDependency from '@/app/components/workflow/plugin-dependency'
|
||||
import {
|
||||
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 { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import { useAutoOnboarding } from '../hooks/use-auto-onboarding'
|
||||
|
||||
const UpdateDSLModal = dynamic(() => import('@/app/components/workflow/update-dsl-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const WorkflowOnboardingModal = dynamic(() => import('./workflow-onboarding-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
const WorkflowChildren = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
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 reactFlowStore = useStoreApi()
|
||||
const nodesInitialData = useNodesInitialData()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { handleOnboardingClose } = useAutoOnboarding()
|
||||
const {
|
||||
handlePaneContextmenuCancel,
|
||||
} = usePanelInteractions()
|
||||
|
|
@ -39,9 +59,51 @@ const WorkflowChildren = () => {
|
|||
setSecretEnvList(v.payload.data as EnvironmentVariable[])
|
||||
})
|
||||
|
||||
const handleCloseOnboarding = useCallback(() => {
|
||||
handleOnboardingClose()
|
||||
}, [handleOnboardingClose])
|
||||
|
||||
const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: ToolDefaultValue) => {
|
||||
const nodeData = nodeType === BlockEnum.Start
|
||||
? nodesInitialData.start
|
||||
: { ...nodesInitialData[nodeType], ...toolConfig }
|
||||
|
||||
const { newNode } = generateNewNode({
|
||||
data: {
|
||||
...nodeData,
|
||||
},
|
||||
position: START_INITIAL_POSITION,
|
||||
})
|
||||
|
||||
const { setNodes, setEdges } = reactFlowStore.getState()
|
||||
setNodes([newNode])
|
||||
setEdges([])
|
||||
|
||||
setShowOnboarding(false)
|
||||
setHasSelectedStartNode(true)
|
||||
|
||||
handleSyncWorkflowDraft(true, false, {
|
||||
onSuccess: () => {
|
||||
console.log('Node successfully saved to draft')
|
||||
},
|
||||
onError: () => {
|
||||
console.error('Failed to save node to draft')
|
||||
},
|
||||
})
|
||||
}, [nodesInitialData, setShowOnboarding, setHasSelectedStartNode, reactFlowStore, handleSyncWorkflowDraft])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PluginDependency />
|
||||
{
|
||||
showOnboarding && (
|
||||
<WorkflowOnboardingModal
|
||||
isShow={showOnboarding}
|
||||
onClose={handleCloseOnboarding}
|
||||
onSelectStartNode={handleSelectStartNode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showImportDSLModal && (
|
||||
<UpdateDSLModal
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
'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 { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import StartNodeSelectionPanel from './start-node-selection-panel'
|
||||
|
||||
type WorkflowOnboardingModalProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onSelectStartNode: (nodeType: BlockEnum, toolConfig?: ToolDefaultValue) => void
|
||||
}
|
||||
|
||||
const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({
|
||||
isShow,
|
||||
onClose,
|
||||
onSelectStartNode,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleSelectUserInput = useCallback(() => {
|
||||
onSelectStartNode(BlockEnum.Start)
|
||||
onClose() // Close modal after selection
|
||||
}, [onSelectStartNode, onClose])
|
||||
|
||||
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: ToolDefaultValue) => {
|
||||
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')}{' '}
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-text-accent-hover cursor-pointer text-text-accent underline"
|
||||
onClick={() => {
|
||||
console.log('Navigate to start node documentation')
|
||||
}}
|
||||
>
|
||||
Learn more
|
||||
</button> about start node.
|
||||
</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-[160px] items-center gap-1 text-text-quaternary">
|
||||
<span>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">
|
||||
esc
|
||||
</kbd>
|
||||
<span>to dismiss</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 { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import { TabsEnum } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
type StartNodeSelectionPanelProps = {
|
||||
onSelectUserInput: () => void
|
||||
onSelectTrigger: (nodeType: BlockEnum, toolConfig?: ToolDefaultValue) => 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?: ToolDefaultValue) => {
|
||||
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
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { useCallback, useEffect } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
export const useAutoOnboarding = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const checkAndShowOnboarding = useCallback(() => {
|
||||
const { getNodes } = store.getState()
|
||||
const {
|
||||
showOnboarding,
|
||||
hasShownOnboarding,
|
||||
notInitialWorkflow,
|
||||
setShowOnboarding,
|
||||
setHasShownOnboarding,
|
||||
} = workflowStore.getState()
|
||||
|
||||
// Skip if already showing onboarding or it's the initial workflow creation
|
||||
if (showOnboarding || notInitialWorkflow)
|
||||
return
|
||||
|
||||
const nodes = getNodes()
|
||||
const startNodeTypes = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
|
||||
// Check if canvas is empty (no nodes or no start nodes)
|
||||
const hasStartNode = nodes.some(node => startNodeTypes.includes(node.data.type))
|
||||
const isEmpty = nodes.length === 0 || !hasStartNode
|
||||
|
||||
// Show onboarding if canvas is empty and we haven't shown it before in this session
|
||||
if (isEmpty && !hasShownOnboarding) {
|
||||
setShowOnboarding(true)
|
||||
setHasShownOnboarding(true)
|
||||
}
|
||||
}, [store, workflowStore])
|
||||
|
||||
const handleOnboardingClose = useCallback(() => {
|
||||
const { setShowOnboarding, setHasShownOnboarding } = workflowStore.getState()
|
||||
setShowOnboarding(false)
|
||||
setHasShownOnboarding(true)
|
||||
}, [workflowStore])
|
||||
|
||||
// Check on mount and when nodes change
|
||||
useEffect(() => {
|
||||
// Small delay to ensure the workflow data is loaded
|
||||
const timer = setTimeout(() => {
|
||||
checkAndShowOnboarding()
|
||||
}, 500)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [checkAndShowOnboarding])
|
||||
|
||||
return {
|
||||
checkAndShowOnboarding,
|
||||
handleOnboardingClose,
|
||||
}
|
||||
}
|
||||
|
|
@ -57,13 +57,17 @@ export const useWorkflowInit = () => {
|
|||
if (error && error.json && !error.bodyUsed && appDetail) {
|
||||
error.json().then((err: any) => {
|
||||
if (err.code === 'draft_workflow_not_exist') {
|
||||
workflowStore.setState({ notInitialWorkflow: true })
|
||||
workflowStore.setState({
|
||||
notInitialWorkflow: true,
|
||||
showOnboarding: true,
|
||||
hasShownOnboarding: false,
|
||||
})
|
||||
syncWorkflowDraft({
|
||||
url: `/apps/${appDetail.id}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: nodesTemplate,
|
||||
edges: edgesTemplate,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
features: {
|
||||
retriever_resource: { enabled: true },
|
||||
|
|
@ -83,7 +87,6 @@ export const useWorkflowInit = () => {
|
|||
|
||||
useEffect(() => {
|
||||
handleGetInitialWorkflowData()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleFetchPreloadData = useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ export type WorkflowSliceShape = {
|
|||
setNotInitialWorkflow: (notInitialWorkflow: boolean) => void
|
||||
nodesDefaultConfigs: Record<string, any>
|
||||
setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void
|
||||
showOnboarding: boolean
|
||||
setShowOnboarding: (showOnboarding: boolean) => void
|
||||
hasSelectedStartNode: boolean
|
||||
setHasSelectedStartNode: (hasSelectedStartNode: boolean) => void
|
||||
hasShownOnboarding: boolean
|
||||
setHasShownOnboarding: (hasShownOnboarding: boolean) => void
|
||||
}
|
||||
|
||||
export type CreateWorkflowSlice = StateCreator<WorkflowSliceShape>
|
||||
|
|
@ -15,4 +21,10 @@ export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
|
|||
setNotInitialWorkflow: notInitialWorkflow => set(() => ({ notInitialWorkflow })),
|
||||
nodesDefaultConfigs: {},
|
||||
setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })),
|
||||
showOnboarding: false,
|
||||
setShowOnboarding: showOnboarding => set(() => ({ showOnboarding })),
|
||||
hasSelectedStartNode: false,
|
||||
setHasSelectedStartNode: hasSelectedStartNode => set(() => ({ hasSelectedStartNode })),
|
||||
hasShownOnboarding: false,
|
||||
setHasShownOnboarding: hasShownOnboarding => set(() => ({ hasShownOnboarding })),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ type NodeSelectorProps = {
|
|||
disabled?: boolean
|
||||
noBlocks?: boolean
|
||||
showStartTab?: boolean
|
||||
defaultActiveTab?: TabsEnum
|
||||
forceShowStartContent?: boolean
|
||||
}
|
||||
const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
open: openFromProps,
|
||||
|
|
@ -61,6 +63,8 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||
disabled,
|
||||
noBlocks = false,
|
||||
showStartTab = false,
|
||||
defaultActiveTab,
|
||||
forceShowStartContent = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
|
@ -87,7 +91,9 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||
onSelect(type, toolDefaultValue)
|
||||
}, [handleOpenChange, onSelect])
|
||||
|
||||
const [activeTab, setActiveTab] = useState(noBlocks ? TabsEnum.Tools : TabsEnum.Blocks)
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
defaultActiveTab || (noBlocks ? TabsEnum.Tools : TabsEnum.Blocks),
|
||||
)
|
||||
const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
|
||||
setActiveTab(newActiveTab)
|
||||
}, [])
|
||||
|
|
@ -167,6 +173,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||
availableBlocksTypes={availableBlocksTypes}
|
||||
noBlocks={noBlocks}
|
||||
showStartTab={showStartTab}
|
||||
forceShowStartContent={forceShowStartContent}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export type TabsProps = {
|
|||
filterElem: React.ReactNode
|
||||
noBlocks?: boolean
|
||||
showStartTab?: boolean
|
||||
forceShowStartContent?: boolean // Force show Start content even when noBlocks=true
|
||||
}
|
||||
const Tabs: FC<TabsProps> = ({
|
||||
activeTab,
|
||||
|
|
@ -31,6 +32,7 @@ const Tabs: FC<TabsProps> = ({
|
|||
filterElem,
|
||||
noBlocks,
|
||||
showStartTab = false,
|
||||
forceShowStartContent = false,
|
||||
}) => {
|
||||
const tabs = useTabs(showStartTab)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
|
|
@ -64,7 +66,7 @@ const Tabs: FC<TabsProps> = ({
|
|||
}
|
||||
{filterElem}
|
||||
{
|
||||
activeTab === TabsEnum.Start && !noBlocks && (
|
||||
activeTab === TabsEnum.Start && (!noBlocks || forceShowStartContent) && (
|
||||
<div className='border-t border-divider-subtle'>
|
||||
<AllStartBlocks
|
||||
searchText={searchText}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export const NodeSourceHandle = memo(({
|
|||
}, [handleNodeAdd, id, handleId])
|
||||
|
||||
useEffect(() => {
|
||||
if (notInitialWorkflow && data.type === BlockEnum.Start && !isChatMode)
|
||||
if (notInitialWorkflow && (data.type === BlockEnum.Start || data.type === BlockEnum.TriggerSchedule || data.type === BlockEnum.TriggerWebhook || data.type === BlockEnum.TriggerPlugin) && !isChatMode)
|
||||
setOpen(true)
|
||||
}, [notInitialWorkflow, data.type, isChatMode])
|
||||
|
||||
|
|
|
|||
|
|
@ -1100,6 +1100,17 @@ const translation = {
|
|||
noDependents: 'No dependents',
|
||||
},
|
||||
},
|
||||
onboarding: {
|
||||
title: 'Select a start node to begin',
|
||||
description: 'Different start nodes have different capabilities. Don\'t worry, you can always change them later.',
|
||||
userInputFull: 'User Input (original start node)',
|
||||
userInputDescription: 'Start node that allows setting user input variables, with web app, service API, MCP server, and workflow as tool capabilities.',
|
||||
trigger: 'Trigger',
|
||||
triggerDescription: 'Triggers can serve as the start node of a workflow, such as scheduled tasks, custom webhooks, or integrations with other apps.',
|
||||
back: 'Back',
|
||||
learnMore: 'Learn more',
|
||||
aboutStartNode: 'about start node.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -1098,6 +1098,17 @@ const translation = {
|
|||
enabled: 'スタート',
|
||||
disabled: '開始 • 無効',
|
||||
},
|
||||
onboarding: {
|
||||
title: '開始するには開始ノードを選択してください',
|
||||
description: '異なる開始ノードには異なる機能があります。心配しないでください、いつでも変更できます。',
|
||||
userInputFull: 'ユーザー入力(元の開始ノード)',
|
||||
userInputDescription: 'ユーザー入力変数の設定を可能にする開始ノードで、Webアプリ、サービスAPI、MCPサーバー、およびツールとしてのワークフロー機能を持ちます。',
|
||||
trigger: 'トリガー',
|
||||
triggerDescription: 'トリガーは、スケジュールされたタスク、カスタムwebhook、または他のアプリとの統合など、ワークフローの開始ノードとして機能できます。',
|
||||
back: '戻る',
|
||||
learnMore: '詳細を見る',
|
||||
aboutStartNode: '開始ノードについて。',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
|
|
@ -1098,6 +1098,17 @@ const translation = {
|
|||
enabled: '开始',
|
||||
disabled: '开始 • 已禁用',
|
||||
},
|
||||
onboarding: {
|
||||
title: '选择开始节点来开始',
|
||||
description: '不同的开始节点具有不同的功能。不用担心,您随时可以更改它们。',
|
||||
userInputFull: '用户输入(原始开始节点)',
|
||||
userInputDescription: '允许设置用户输入变量的开始节点,具有Web应用程序、服务API、MCP服务器和工作流即工具功能。',
|
||||
trigger: '触发器',
|
||||
triggerDescription: '触发器可以作为工作流的开始节点,例如定时任务、自定义webhook或与其他应用程序的集成。',
|
||||
back: '返回',
|
||||
learnMore: '了解更多',
|
||||
aboutStartNode: '关于开始节点。',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
|||
Loading…
Reference in New Issue