mirror of
https://github.com/langgenius/dify.git
synced 2026-06-13 04:01:12 +08:00
feat(workflow): update start node UI (#37348)
This commit is contained in:
parent
3575a3d1b3
commit
c69abf16ae
@ -5206,12 +5206,6 @@
|
||||
"web/app/components/workflow/block-selector/market-place-plugin/item.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/market-place-plugin/list.tsx": {
|
||||
@ -5243,14 +5237,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/start-blocks.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/tabs.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 2
|
||||
@ -5280,22 +5266,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/trigger-plugin/item.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 4
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0001 5.5V9.5H2.0001V5.5M4.7501 1.5H7.2501M4.7501 1.5L4.46716 3.76351C4.35191 4.68555 5.07085 5.5 6.0001 5.5C6.92935 5.5 7.64829 4.68555 7.53305 3.76351L7.2501 1.5M4.7501 1.5H2.2501L1.58797 3.61884C1.29639 4.5519 1.99345 5.5 2.97098 5.5C3.70173 5.5 4.31812 4.95585 4.40876 4.23075L4.7501 1.5ZM7.2501 1.5H9.7501L10.4122 3.61884C10.7038 4.5519 10.0067 5.5 9.02925 5.5C8.2985 5.5 7.6821 4.95585 7.59145 4.23075L7.2501 1.5Z" stroke="currentColor" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 608 B |
@ -0,0 +1,8 @@
|
||||
<svg width="14" height="14" viewBox="0 0 13.3333 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.66667 10C7.03486 10 7.33333 10.2985 7.33333 10.6667V12.6667C7.33333 13.0349 7.03486 13.3333 6.66667 13.3333C6.29848 13.3333 6 13.0349 6 12.6667V10.6667C6 10.2985 6.29848 10 6.66667 10Z" fill="currentColor"/>
|
||||
<path d="M1.95247 1.95247C2.21277 1.69219 2.63483 1.69229 2.89518 1.95247L7.13867 6.19531C7.19313 6.24981 7.23528 6.3119 7.26693 6.3776C7.28146 6.40773 7.29411 6.43892 7.30404 6.47135C7.33671 6.57832 7.34196 6.69172 7.31966 6.80078C7.30858 6.85501 7.2903 6.90651 7.26693 6.95508C7.23526 7.02098 7.19325 7.08337 7.13867 7.13802L2.89518 11.3809C2.63483 11.6409 2.21272 11.641 1.95247 11.3809C1.6924 11.1206 1.6925 10.6985 1.95247 10.4382L5.05794 7.33333H0.666667C0.298477 7.33333 0 7.03486 0 6.66667C0 6.29848 0.298477 6 0.666667 6H5.05794L1.95247 2.89518C1.69236 2.63487 1.69235 2.21277 1.95247 1.95247Z" fill="currentColor"/>
|
||||
<path d="M9.02409 9.02344C9.28445 8.76325 9.7065 8.76314 9.9668 9.02344L11.3809 10.4375C11.641 10.6978 11.641 11.1199 11.3809 11.3802C11.1206 11.6405 10.6985 11.6404 10.4382 11.3802L9.02409 9.96615C8.76374 9.7058 8.76374 9.28379 9.02409 9.02344Z" fill="currentColor"/>
|
||||
<path d="M12.6667 6C13.0349 6 13.3333 6.29848 13.3333 6.66667C13.3333 7.03486 13.0349 7.33333 12.6667 7.33333H10.6667C10.2985 7.33333 10 7.03486 10 6.66667C10 6.29848 10.2985 6 10.6667 6H12.6667Z" fill="currentColor"/>
|
||||
<path d="M10.4382 1.95247C10.6985 1.69225 11.1205 1.69226 11.3809 1.95247C11.6411 2.21281 11.6411 2.63486 11.3809 2.89518L9.9668 4.30924C9.70647 4.56956 9.28444 4.56951 9.02409 4.30924C8.76381 4.04888 8.76376 3.62686 9.02409 3.36654L10.4382 1.95247Z" fill="currentColor"/>
|
||||
<path d="M6.66667 0C7.03486 0 7.33333 0.298477 7.33333 0.666667V2.66667C7.33333 3.03486 7.03486 3.33333 6.66667 3.33333C6.29848 3.33333 6 3.03486 6 2.66667V0.666667C6 0.298477 6.29848 0 6.66667 0Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(2.5904 1.333)">
|
||||
<path d="M7.07562 3C7.07562 2.07953 6.32943 1.33333 5.40895 1.33333C4.48848 1.33333 3.74229 2.07953 3.74229 3C3.74229 3.92047 4.48848 4.66667 5.40895 4.66667C6.32943 4.66667 7.07562 3.92047 7.07562 3ZM8.40895 3C8.40895 4.65685 7.06581 6 5.40895 6C3.7521 6 2.40895 4.65685 2.40895 3C2.40895 1.34315 3.7521 0 5.40895 0C7.06581 0 8.40895 1.34315 8.40895 3Z" fill="currentColor"/>
|
||||
<path d="M5.40961 6.66667C5.94859 6.66667 6.46414 6.7352 6.94997 6.86523C7.30564 6.96044 7.51718 7.32597 7.42198 7.68164C7.32674 8.03718 6.96114 8.24808 6.60557 8.153C6.23516 8.05385 5.83538 8 5.40961 8C3.44272 8.00003 1.96767 9.15509 1.35687 10.8066C1.31131 10.9299 1.33437 11.0372 1.41677 11.1354C1.50836 11.2446 1.67346 11.3333 1.87445 11.3333H4.07627C4.44446 11.3333 4.74294 11.6318 4.74294 12C4.74294 12.3682 4.44446 12.6667 4.07627 12.6667H1.87445C1.28978 12.6667 0.746862 12.4112 0.395283 11.9922C0.0346179 11.5622 -0.120709 10.9573 0.106221 10.3438C0.900152 8.19736 2.85517 6.6667 5.40961 6.66667Z" fill="currentColor"/>
|
||||
<path d="M8.9376 8.1952C9.56613 7.56673 10.5851 7.56673 11.2136 8.1952C11.8422 8.82374 11.8421 9.84269 11.2136 10.4712L8.9376 12.7473C8.56255 13.1223 8.05388 13.3332 7.52354 13.3332H6.74229C6.3741 13.3332 6.07562 13.0347 6.07562 12.6666V11.886C6.07562 11.3555 6.28646 10.8463 6.66156 10.4712L8.9376 8.1952ZM7.40895 11.9999H7.52354C7.70025 11.9998 7.86989 11.9296 7.99489 11.8046L10.2709 9.52854C10.3787 9.42068 10.3788 9.24575 10.2709 9.13791C10.1631 9.03013 9.98814 9.03013 9.88031 9.13791L7.60426 11.414C7.47923 11.539 7.40895 11.7091 7.40895 11.886V11.9999Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
||||
{
|
||||
"prefix": "custom-vender",
|
||||
"name": "Dify Custom Vender",
|
||||
"total": 281,
|
||||
"total": 284,
|
||||
"version": "0.0.0-private",
|
||||
"author": {
|
||||
"name": "LangGenius, Inc.",
|
||||
|
||||
@ -2,6 +2,7 @@ import type { ReactNode } from 'react'
|
||||
import type { WorkflowProps } from '@/app/components/workflow'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import WorkflowMain from '../workflow-main'
|
||||
|
||||
const mockSetFeatures = vi.fn()
|
||||
@ -91,6 +92,12 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: <T,>(selector: (state: { appDetail: { mode: string } }) => T) => selector({
|
||||
appDetail: { mode: 'workflow' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useReactFlow: () => ({
|
||||
getNodes: () => [],
|
||||
@ -260,6 +267,18 @@ vi.mock('@/app/components/workflow-app/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas', () => ({
|
||||
useWorkflowDraftGraphForCanvas: () => ({
|
||||
getWorkflowDraftGraphForCanvas: (graph?: { nodes?: unknown[], edges?: unknown[], viewport?: unknown }) => ({
|
||||
nodes: graph?.nodes?.length
|
||||
? graph.nodes
|
||||
: [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }],
|
||||
edges: graph?.edges || [],
|
||||
viewport: graph?.viewport || { x: 0, y: 0, zoom: 1 },
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../workflow-children', () => ({
|
||||
default: () => <div data-testid="workflow-children">workflow-children</div>,
|
||||
}))
|
||||
@ -460,4 +479,36 @@ describe('WorkflowMain', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('restores a local start placeholder for empty collaboration workflow updates', async () => {
|
||||
collaborationRuntime.isEnabled = true
|
||||
mockFetchWorkflowDraft.mockResolvedValue({
|
||||
features: {},
|
||||
conversation_variables: [],
|
||||
environment_variables: [],
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
<WorkflowMain
|
||||
nodes={[]}
|
||||
edges={[]}
|
||||
viewport={{ x: 0, y: 0, zoom: 1 }}
|
||||
/>,
|
||||
)
|
||||
|
||||
await collaborationListeners.workflowUpdate?.()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({
|
||||
nodes: [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }],
|
||||
edges: [],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -11,9 +11,11 @@ import {
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import { useWorkflowDraftGraphForCanvas } from '@/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/use-collaboration'
|
||||
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
|
||||
@ -44,8 +46,10 @@ const WorkflowMain = ({
|
||||
const featuresStore = useFeaturesStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const appId = useStore(s => s.appId)
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const reactFlow = useReactFlow()
|
||||
const { getWorkflowDraftGraphForCanvas } = useWorkflowDraftGraphForCanvas(appDetail?.mode)
|
||||
|
||||
const reactFlowStore = useMemo(() => ({
|
||||
getState: () => ({
|
||||
@ -175,13 +179,8 @@ const WorkflowMain = ({
|
||||
handleWorkflowDataUpdate(response)
|
||||
|
||||
// Update workflow canvas (nodes, edges, viewport)
|
||||
if (response.graph) {
|
||||
handleUpdateWorkflowCanvas({
|
||||
nodes: response.graph.nodes || [],
|
||||
edges: response.graph.edges || [],
|
||||
viewport: response.graph.viewport || { x: 0, y: 0, zoom: 1 },
|
||||
})
|
||||
}
|
||||
if (response.graph)
|
||||
handleUpdateWorkflowCanvas(getWorkflowDraftGraphForCanvas(response.graph))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch updated workflow:', error)
|
||||
@ -189,7 +188,7 @@ const WorkflowMain = ({
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [appId, handleWorkflowDataUpdate, handleUpdateWorkflowCanvas, isCollaborationEnabled])
|
||||
}, [appId, getWorkflowDraftGraphForCanvas, handleWorkflowDataUpdate, handleUpdateWorkflowCanvas, isCollaborationEnabled])
|
||||
|
||||
// Listen for sync requests from other users (only processed by leader)
|
||||
useEffect(() => {
|
||||
|
||||
@ -32,6 +32,7 @@ describe('useAutoOnboarding', () => {
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: false,
|
||||
notInitialWorkflow: false,
|
||||
isWorkflowDataLoaded: true,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector,
|
||||
@ -56,11 +57,36 @@ describe('useAutoOnboarding', () => {
|
||||
expect(mockSetShouldAutoOpenStartNodeSelector).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should skip auto onboarding before workflow data is loaded', () => {
|
||||
mockWorkflowStore.getState.mockReturnValue({
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: false,
|
||||
notInitialWorkflow: false,
|
||||
isWorkflowDataLoaded: false,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector,
|
||||
hasSelectedStartNode: false,
|
||||
setHasSelectedStartNode: mockSetHasSelectedStartNode,
|
||||
})
|
||||
|
||||
renderHook(() => useAutoOnboarding())
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500)
|
||||
})
|
||||
|
||||
expect(mockSetShowOnboarding).not.toHaveBeenCalled()
|
||||
expect(mockSetHasShownOnboarding).not.toHaveBeenCalled()
|
||||
expect(mockSetShouldAutoOpenStartNodeSelector).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should skip auto onboarding when it is already visible or the workflow is not initial', () => {
|
||||
mockWorkflowStore.getState.mockReturnValue({
|
||||
showOnboarding: true,
|
||||
hasShownOnboarding: false,
|
||||
notInitialWorkflow: true,
|
||||
isWorkflowDataLoaded: true,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector,
|
||||
@ -84,6 +110,7 @@ describe('useAutoOnboarding', () => {
|
||||
showOnboarding: false,
|
||||
hasShownOnboarding: true,
|
||||
notInitialWorkflow: false,
|
||||
isWorkflowDataLoaded: true,
|
||||
setShowOnboarding: mockSetShowOnboarding,
|
||||
setHasShownOnboarding: mockSetHasShownOnboarding,
|
||||
setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector,
|
||||
|
||||
@ -37,6 +37,7 @@ describe('useAvailableNodesMetaData', () => {
|
||||
|
||||
expect(result.current.nodesMap?.[BlockEnum.Start]?.metaData.isUndeletable).toBe(false)
|
||||
expect(result.current.nodesMap?.[BlockEnum.End]).toBeDefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.StartPlaceholder]).toBeDefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.TriggerWebhook]).toBeDefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.TriggerSchedule]).toBeDefined()
|
||||
expect(result.current.nodesMap?.[BlockEnum.TriggerPlugin]).toBeDefined()
|
||||
|
||||
@ -2,6 +2,7 @@ import { act } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useNodesSyncDraft } from '../use-nodes-sync-draft'
|
||||
|
||||
const mockGetNodes = vi.fn()
|
||||
@ -134,7 +135,7 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
|
||||
},
|
||||
}
|
||||
mockGetNodesReadOnly.mockReturnValue(false)
|
||||
mockGetNodes.mockReturnValue([{ id: 'n1', position: { x: 0, y: 0 }, data: { type: 'start' } }])
|
||||
mockGetNodes.mockReturnValue([{ id: 'n1', position: { x: 0, y: 0 }, data: { type: BlockEnum.Start } }])
|
||||
mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new', updated_at: 1 })
|
||||
mockCollaborationIsConnected.mockReturnValue(false)
|
||||
mockCollaborationGetIsLeader.mockReturnValue(true)
|
||||
@ -199,13 +200,15 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
|
||||
...reactFlowState,
|
||||
edges: [
|
||||
{ id: 'edge-1', source: 'n1', target: 'n2', data: { _isTemp: false, _private: 'drop', stable: 'keep' } },
|
||||
{ id: 'placeholder-edge', source: 'start-placeholder', target: 'n1', data: { stable: 'drop' } },
|
||||
{ id: 'temp-edge', source: 'n2', target: 'n3', data: { _isTemp: true } },
|
||||
],
|
||||
transform: [10, 20, 1.5],
|
||||
}
|
||||
mockGetNodes.mockReturnValue([
|
||||
{ id: 'n1', position: { x: 0, y: 0 }, data: { type: 'start', _tempField: 'drop', label: 'Start' } },
|
||||
{ id: 'temp-node', position: { x: 1, y: 1 }, data: { type: 'answer', _isTempNode: true } },
|
||||
{ id: 'n1', position: { x: 0, y: 0 }, data: { type: BlockEnum.Start, _tempField: 'drop', label: 'Start' } },
|
||||
{ id: 'start-placeholder', position: { x: 1, y: 1 }, data: { type: BlockEnum.StartPlaceholder } },
|
||||
{ id: 'temp-node', position: { x: 2, y: 2 }, data: { type: BlockEnum.Answer, _isTempNode: true } },
|
||||
])
|
||||
workflowStoreState = {
|
||||
...workflowStoreState,
|
||||
@ -241,7 +244,7 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
|
||||
url: '/apps/app-1/workflows/draft',
|
||||
params: {
|
||||
graph: {
|
||||
nodes: [{ id: 'n1', position: { x: 0, y: 0 }, data: { type: 'start', label: 'Start' } }],
|
||||
nodes: [{ id: 'n1', position: { x: 0, y: 0 }, data: { type: BlockEnum.Start, label: 'Start' } }],
|
||||
edges: [{ id: 'edge-1', source: 'n1', target: 'n2', data: { stable: 'keep' } }],
|
||||
viewport: { x: 10, y: 20, zoom: 1.5 },
|
||||
},
|
||||
@ -292,6 +295,32 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
|
||||
}))
|
||||
})
|
||||
|
||||
it('should not post the local start placeholder when the page closes', () => {
|
||||
reactFlowState = {
|
||||
...reactFlowState,
|
||||
edges: [
|
||||
{ id: 'placeholder-edge', source: 'start-placeholder', target: 'n1', data: {} },
|
||||
],
|
||||
}
|
||||
mockGetNodes.mockReturnValue([
|
||||
{ id: 'start-placeholder', position: { x: 0, y: 0 }, data: { type: BlockEnum.StartPlaceholder } },
|
||||
{ id: 'n1', position: { x: 1, y: 1 }, data: { type: BlockEnum.Start } },
|
||||
])
|
||||
|
||||
const { result } = renderUseNodesSyncDraft()
|
||||
|
||||
act(() => {
|
||||
result.current.syncWorkflowDraftWhenPageClose()
|
||||
})
|
||||
|
||||
expect(mockPostWithKeepalive).toHaveBeenCalledWith('/api/apps/app-1/workflows/draft', expect.objectContaining({
|
||||
graph: expect.objectContaining({
|
||||
nodes: [{ id: 'n1', position: { x: 1, y: 1 }, data: { type: BlockEnum.Start } }],
|
||||
edges: [],
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
it('should emit sync request instead of syncing when current user is collaboration follower', async () => {
|
||||
isCollaborationEnabled = true
|
||||
mockCollaborationIsConnected.mockReturnValue(true)
|
||||
|
||||
@ -0,0 +1,103 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { useWorkflowDraftGraphForCanvas } from '../use-workflow-draft-graph-for-canvas'
|
||||
|
||||
let generateNewNodeCalls: Array<Record<string, unknown>> = []
|
||||
|
||||
vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/workflow/utils')>()
|
||||
return {
|
||||
...actual,
|
||||
generateNewNode: (args: { data: Record<string, unknown>, position: Record<string, unknown> }) => {
|
||||
generateNewNodeCalls.push(args)
|
||||
return {
|
||||
newNode: {
|
||||
id: `generated-${generateNewNodeCalls.length}`,
|
||||
type: 'custom',
|
||||
data: args.data,
|
||||
position: args.position,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('useWorkflowDraftGraphForCanvas', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
generateNewNodeCalls = []
|
||||
})
|
||||
|
||||
it('should restore a local start placeholder for workflow graphs without an entry node', () => {
|
||||
const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW))
|
||||
|
||||
const graph = result.current.getWorkflowDraftGraphForCanvas({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
})
|
||||
|
||||
expect(graph).toMatchObject({
|
||||
edges: [],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
})
|
||||
expect(graph.nodes).toHaveLength(1)
|
||||
expect(graph.nodes[0]).toMatchObject({
|
||||
data: {
|
||||
type: BlockEnum.StartPlaceholder,
|
||||
title: 'workflow.blocks.start-placeholder',
|
||||
desc: '',
|
||||
selected: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
BlockEnum.StartPlaceholder,
|
||||
])('should preserve existing %s entry nodes', (type) => {
|
||||
const node = { id: 'entry', data: { type } }
|
||||
const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW))
|
||||
|
||||
const graph = result.current.getWorkflowDraftGraphForCanvas({
|
||||
nodes: [node] as never,
|
||||
edges: [],
|
||||
})
|
||||
|
||||
expect(graph.nodes).toEqual([node])
|
||||
expect(generateNewNodeCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should not restore a start placeholder for non-workflow app modes', () => {
|
||||
const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.ADVANCED_CHAT))
|
||||
|
||||
const graph = result.current.getWorkflowDraftGraphForCanvas({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
})
|
||||
|
||||
expect(graph.nodes).toEqual([])
|
||||
expect(generateNewNodeCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should reuse the provided local start placeholder template when available', () => {
|
||||
const localStartPlaceholder = { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }
|
||||
const draftNode = { id: 'llm', data: { type: BlockEnum.LLM } }
|
||||
const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW))
|
||||
|
||||
const graph = result.current.getWorkflowDraftGraphForCanvas({
|
||||
nodes: [draftNode] as never,
|
||||
edges: [],
|
||||
viewport: { x: 1, y: 2, zoom: 0.5 },
|
||||
}, {
|
||||
localStartPlaceholderNodes: [localStartPlaceholder] as never,
|
||||
})
|
||||
|
||||
expect(graph.nodes).toEqual([localStartPlaceholder, draftNode])
|
||||
expect(graph.viewport).toEqual({ x: 1, y: 2, zoom: 0.5 })
|
||||
expect(generateNewNodeCalls).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@ -14,6 +14,7 @@ const mockWorkflowStoreSetState = vi.fn()
|
||||
const mockWorkflowStoreGetState = vi.fn()
|
||||
const mockFetchNodesDefaultConfigs = vi.fn()
|
||||
const mockFetchPublishedWorkflow = vi.fn()
|
||||
const mockSyncWorkflowDraft = vi.fn()
|
||||
|
||||
let appStoreState: {
|
||||
appDetail: {
|
||||
@ -43,7 +44,12 @@ vi.mock('@/app/components/app/store', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../use-workflow-template', () => ({
|
||||
useWorkflowTemplate: () => ({ nodes: [], edges: [] }),
|
||||
useWorkflowTemplate: () => ({
|
||||
nodes: appStoreState.appDetail.mode === 'workflow'
|
||||
? [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }]
|
||||
: [{ id: 'start', data: { type: BlockEnum.Start } }],
|
||||
edges: [],
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
@ -55,7 +61,6 @@ vi.mock('@/service/use-workflow', () => ({
|
||||
}))
|
||||
|
||||
const mockFetchWorkflowDraft = vi.fn()
|
||||
const mockSyncWorkflowDraft = vi.fn()
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
fetchWorkflowDraft: (...args: unknown[]) => mockFetchWorkflowDraft(...args),
|
||||
@ -71,13 +76,17 @@ const notExistError = () => ({
|
||||
|
||||
const draftResponse = {
|
||||
id: 'draft-id',
|
||||
graph: { nodes: [], edges: [] },
|
||||
graph: {
|
||||
nodes: [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }],
|
||||
edges: [],
|
||||
},
|
||||
hash: 'server-hash',
|
||||
created_at: 0,
|
||||
created_by: { id: '', name: '', email: '' },
|
||||
updated_at: 1,
|
||||
updated_by: { id: '', name: '', email: '' },
|
||||
tool_published: false,
|
||||
features: { retriever_resource: { enabled: true } },
|
||||
environment_variables: [],
|
||||
conversation_variables: [],
|
||||
version: '1',
|
||||
@ -85,7 +94,7 @@ const draftResponse = {
|
||||
marked_comment: '',
|
||||
}
|
||||
|
||||
describe('useWorkflowInit — hash fix (draft_workflow_not_exist)', () => {
|
||||
describe('useWorkflowInit', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
appStoreState = {
|
||||
@ -103,32 +112,115 @@ describe('useWorkflowInit — hash fix (draft_workflow_not_exist)', () => {
|
||||
mockFetchPublishedWorkflow.mockResolvedValue({ created_at: 0, graph: { nodes: [], edges: [] } })
|
||||
mockFetchWorkflowDraft
|
||||
.mockRejectedValueOnce(notExistError())
|
||||
.mockResolvedValueOnce(draftResponse)
|
||||
mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new-hash', updated_at: 1 })
|
||||
mockSyncWorkflowDraft.mockReset()
|
||||
})
|
||||
|
||||
it('should call setSyncWorkflowDraftHash with hash returned by syncWorkflowDraft', async () => {
|
||||
renderHook(() => useWorkflowInit())
|
||||
await waitFor(() => expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-hash'))
|
||||
})
|
||||
|
||||
it('should store hash BEFORE making the recursive fetchWorkflowDraft call', async () => {
|
||||
const order: string[] = []
|
||||
mockSetSyncWorkflowDraftHash.mockImplementation((h: string) => order.push(`hash:${h}`))
|
||||
it('should create an empty backend draft and restore a local start placeholder when the workflow draft does not exist', async () => {
|
||||
mockFetchWorkflowDraft
|
||||
.mockReset()
|
||||
.mockRejectedValueOnce(notExistError())
|
||||
.mockImplementationOnce(async () => {
|
||||
order.push('fetch:2')
|
||||
return draftResponse
|
||||
.mockResolvedValueOnce({
|
||||
...draftResponse,
|
||||
graph: { nodes: [], edges: [] },
|
||||
hash: 'new-workflow-hash',
|
||||
})
|
||||
mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new-hash', updated_at: 1 })
|
||||
|
||||
const { result } = renderHook(() => useWorkflowInit())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data?.graph.nodes).toEqual([
|
||||
{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } },
|
||||
])
|
||||
})
|
||||
|
||||
expect(mockWorkflowStoreSetState).toHaveBeenCalledWith(expect.objectContaining({
|
||||
showOnboarding: false,
|
||||
shouldAutoOpenStartNodeSelector: false,
|
||||
hasSelectedStartNode: false,
|
||||
hasShownOnboarding: true,
|
||||
}))
|
||||
expect(mockSyncWorkflowDraft).toHaveBeenCalledWith(expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
}),
|
||||
}))
|
||||
expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-hash')
|
||||
expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-workflow-hash')
|
||||
})
|
||||
|
||||
it('should keep creating the first backend draft for advanced chat apps', async () => {
|
||||
appStoreState = {
|
||||
appDetail: { id: 'app-1', name: 'Test', mode: 'advanced-chat' },
|
||||
}
|
||||
mockFetchWorkflowDraft
|
||||
.mockReset()
|
||||
.mockRejectedValueOnce(notExistError())
|
||||
.mockResolvedValueOnce(draftResponse)
|
||||
mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new-hash', updated_at: 1 })
|
||||
|
||||
renderHook(() => useWorkflowInit())
|
||||
|
||||
await waitFor(() => expect(order).toContain('fetch:2'))
|
||||
expect(order).toContain('hash:new-hash')
|
||||
expect(order.indexOf('hash:new-hash')).toBeLessThan(order.indexOf('fetch:2'))
|
||||
await waitFor(() => expect(mockSyncWorkflowDraft).toHaveBeenCalledWith(expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
graph: {
|
||||
nodes: [{ id: 'start', data: { type: BlockEnum.Start } }],
|
||||
edges: [],
|
||||
},
|
||||
}),
|
||||
})))
|
||||
expect(mockWorkflowStoreSetState).toHaveBeenCalledWith(expect.objectContaining({
|
||||
showOnboarding: false,
|
||||
shouldAutoOpenStartNodeSelector: false,
|
||||
hasShownOnboarding: false,
|
||||
}))
|
||||
expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-hash')
|
||||
})
|
||||
|
||||
it('should restore a local start placeholder when an existing workflow draft has an empty graph', async () => {
|
||||
mockFetchWorkflowDraft.mockReset().mockResolvedValue({
|
||||
...draftResponse,
|
||||
graph: { nodes: [], edges: [] },
|
||||
hash: 'empty-draft-hash',
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useWorkflowInit())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data?.graph.nodes).toEqual([
|
||||
{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } },
|
||||
])
|
||||
})
|
||||
|
||||
expect(mockSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('empty-draft-hash')
|
||||
})
|
||||
|
||||
it('should preserve existing draft nodes when restoring the local start placeholder', async () => {
|
||||
const existingNode = { id: 'llm', data: { type: BlockEnum.LLM } }
|
||||
const existingEdge = { source: 'llm', target: 'answer' }
|
||||
mockFetchWorkflowDraft.mockReset().mockResolvedValue({
|
||||
...draftResponse,
|
||||
graph: {
|
||||
nodes: [existingNode],
|
||||
edges: [existingEdge],
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useWorkflowInit())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data?.graph.nodes).toEqual([
|
||||
{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } },
|
||||
existingNode,
|
||||
])
|
||||
})
|
||||
|
||||
expect(result.current.data?.graph.edges).toEqual([existingEdge])
|
||||
expect(mockSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should hydrate draft state, preload defaults, and derive published workflow metadata on success', async () => {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
import { useWorkflowRefreshDraft } from '../use-workflow-refresh-draft'
|
||||
|
||||
@ -11,6 +13,11 @@ const mockSetEnvSecrets = vi.fn()
|
||||
const mockSetConversationVariables = vi.fn()
|
||||
const mockSetIsWorkflowDataLoaded = vi.fn()
|
||||
const mockCancel = vi.fn()
|
||||
let appStoreState: {
|
||||
appDetail: {
|
||||
mode: string
|
||||
}
|
||||
}
|
||||
|
||||
let workflowStoreState: {
|
||||
appId: string
|
||||
@ -30,6 +37,11 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: <T>(selector: (state: typeof appStoreState) => T): T =>
|
||||
selector(appStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useWorkflowUpdate: () => ({ handleUpdateWorkflowCanvas: mockHandleUpdateWorkflowCanvas }),
|
||||
}))
|
||||
@ -60,6 +72,9 @@ describe('useWorkflowRefreshDraft — notUpdateCanvas parameter', () => {
|
||||
setConversationVariables: mockSetConversationVariables,
|
||||
setIsWorkflowDataLoaded: mockSetIsWorkflowDataLoaded,
|
||||
}
|
||||
appStoreState = {
|
||||
appDetail: { mode: AppModeEnum.ADVANCED_CHAT },
|
||||
}
|
||||
mockFetchWorkflowDraft.mockResolvedValue(draftResponse)
|
||||
})
|
||||
|
||||
@ -141,6 +156,70 @@ describe('useWorkflowRefreshDraft — notUpdateCanvas parameter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should restore a local start placeholder for workflow drafts without an entry node', async () => {
|
||||
appStoreState = {
|
||||
appDetail: { mode: AppModeEnum.WORKFLOW },
|
||||
}
|
||||
mockFetchWorkflowDraft.mockResolvedValue({
|
||||
hash: 'server-hash',
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
environment_variables: [],
|
||||
conversation_variables: [],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useWorkflowRefreshDraft())
|
||||
|
||||
act(() => {
|
||||
result.current.handleRefreshWorkflowDraft()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({
|
||||
nodes: [
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
type: BlockEnum.StartPlaceholder,
|
||||
title: 'workflow.blocks.start-placeholder',
|
||||
desc: '',
|
||||
selected: true,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
edges: [],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not restore a local start placeholder for non-workflow app modes', async () => {
|
||||
mockFetchWorkflowDraft.mockResolvedValue({
|
||||
hash: 'server-hash',
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
environment_variables: [],
|
||||
conversation_variables: [],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useWorkflowRefreshDraft())
|
||||
|
||||
act(() => {
|
||||
result.current.handleRefreshWorkflowDraft()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should restore loaded state when refresh fails after workflow data was already loaded', async () => {
|
||||
mockFetchWorkflowDraft.mockRejectedValue(new Error('refresh failed'))
|
||||
|
||||
|
||||
@ -113,6 +113,23 @@ describe('useWorkflowStartRun', () => {
|
||||
expect(mockSetShowInputsPanel).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should not sync or run when only the start placeholder exists', async () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } },
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useWorkflowStartRun())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleWorkflowStartRunInWorkflow()
|
||||
})
|
||||
|
||||
expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
expect(mockHandleRun).not.toHaveBeenCalled()
|
||||
expect(mockSetShowDebugAndPreviewPanel).not.toHaveBeenCalled()
|
||||
expect(mockSetShowInputsPanel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open the input panel instead of running immediately when start inputs are required', async () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
{ id: 'inset-s-1', data: { type: BlockEnum.Start, variables: [{ name: 'query' }] } },
|
||||
|
||||
@ -1,13 +1,24 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { useWorkflowTemplate } from '../use-workflow-template'
|
||||
|
||||
const mockUseIsChatMode = vi.fn()
|
||||
let generateNewNodeCalls: Array<Record<string, unknown>> = []
|
||||
let appStoreState: {
|
||||
appDetail: {
|
||||
mode: string
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/workflow-app/hooks/use-is-chat-mode', () => ({
|
||||
useIsChatMode: () => mockUseIsChatMode(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: <T>(selector: (state: typeof appStoreState) => T): T =>
|
||||
selector(appStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/workflow/utils')>()
|
||||
return {
|
||||
@ -29,9 +40,12 @@ describe('useWorkflowTemplate', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
generateNewNodeCalls = []
|
||||
appStoreState = {
|
||||
appDetail: { mode: AppModeEnum.WORKFLOW },
|
||||
}
|
||||
})
|
||||
|
||||
it('should return only the start node template in workflow mode', () => {
|
||||
it('should return only the start placeholder template in workflow mode', () => {
|
||||
mockUseIsChatMode.mockReturnValue(false)
|
||||
|
||||
const { result } = renderHook(() => useWorkflowTemplate())
|
||||
@ -39,9 +53,35 @@ describe('useWorkflowTemplate', () => {
|
||||
expect(result.current.nodes).toHaveLength(1)
|
||||
expect(result.current.edges).toEqual([])
|
||||
expect(generateNewNodeCalls).toHaveLength(1)
|
||||
expect(generateNewNodeCalls[0]!.data).toMatchObject({
|
||||
type: 'start-placeholder',
|
||||
title: 'workflow.blocks.start-placeholder',
|
||||
selected: true,
|
||||
desc: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return the start node template for non-workflow app modes', () => {
|
||||
appStoreState = {
|
||||
appDetail: { mode: AppModeEnum.COMPLETION },
|
||||
}
|
||||
mockUseIsChatMode.mockReturnValue(false)
|
||||
|
||||
const { result } = renderHook(() => useWorkflowTemplate())
|
||||
|
||||
expect(result.current.nodes).toHaveLength(1)
|
||||
expect(result.current.edges).toEqual([])
|
||||
expect(generateNewNodeCalls).toHaveLength(1)
|
||||
expect(generateNewNodeCalls[0]!.data).toMatchObject({
|
||||
type: 'start',
|
||||
title: 'workflow.blocks.start',
|
||||
})
|
||||
})
|
||||
|
||||
it('should build start, llm, and answer templates with linked edges in chat mode', () => {
|
||||
appStoreState = {
|
||||
appDetail: { mode: AppModeEnum.ADVANCED_CHAT },
|
||||
}
|
||||
mockUseIsChatMode.mockReturnValue(true)
|
||||
|
||||
const { result } = renderHook(() => useWorkflowTemplate())
|
||||
|
||||
@ -12,11 +12,15 @@ export const useAutoOnboarding = () => {
|
||||
showOnboarding,
|
||||
hasShownOnboarding,
|
||||
notInitialWorkflow,
|
||||
isWorkflowDataLoaded,
|
||||
setShowOnboarding,
|
||||
setHasShownOnboarding,
|
||||
setShouldAutoOpenStartNodeSelector,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (!isWorkflowDataLoaded)
|
||||
return
|
||||
|
||||
// Skip if already showing onboarding or it's the initial workflow creation
|
||||
if (showOnboarding || notInitialWorkflow)
|
||||
return
|
||||
|
||||
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
|
||||
import AnswerDefault from '@/app/components/workflow/nodes/answer/default'
|
||||
import EndDefault from '@/app/components/workflow/nodes/end/default'
|
||||
import StartPlaceholderDefault from '@/app/components/workflow/nodes/start-placeholder/default'
|
||||
import StartDefault from '@/app/components/workflow/nodes/start/default'
|
||||
import TriggerPluginDefault from '@/app/components/workflow/nodes/trigger-plugin/default'
|
||||
import TriggerScheduleDefault from '@/app/components/workflow/nodes/trigger-schedule/default'
|
||||
@ -34,6 +35,7 @@ export const useAvailableNodesMetaData = () => {
|
||||
isChatMode
|
||||
? [AnswerDefault]
|
||||
: [
|
||||
StartPlaceholderDefault,
|
||||
EndDefault,
|
||||
TriggerWebhookDefault,
|
||||
TriggerScheduleDefault,
|
||||
|
||||
@ -9,6 +9,7 @@ import { collaborationManager } from '@/app/components/workflow/collaboration/co
|
||||
import { useSerialAsyncCallback } from '@/app/components/workflow/hooks/use-serial-async-callback'
|
||||
import { useNodesReadOnly } from '@/app/components/workflow/hooks/use-workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { postWithKeepalive } from '@/service/fetch'
|
||||
@ -32,7 +33,13 @@ export const useNodesSyncDraft = () => {
|
||||
edges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
const nodes = getNodes().filter(node => !node.data?._isTempNode)
|
||||
const allNodes = getNodes()
|
||||
const nodes = allNodes.filter(node => !node.data?._isTempNode && node.data?.type !== BlockEnum.StartPlaceholder)
|
||||
const skippedNodeIds = new Set(
|
||||
allNodes
|
||||
.filter(node => node.data?._isTempNode || node.data?.type === BlockEnum.StartPlaceholder)
|
||||
.map(node => node.id),
|
||||
)
|
||||
const [x, y, zoom] = transform
|
||||
const {
|
||||
appId,
|
||||
@ -54,7 +61,7 @@ export const useNodesSyncDraft = () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
const producedEdges = produce(edges.filter(edge => !edge.data?._isTemp), (draft) => {
|
||||
const producedEdges = produce(edges.filter(edge => !edge.data?._isTemp && !skippedNodeIds.has(edge.source) && !skippedNodeIds.has(edge.target)), (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
Object.keys(edge.data).forEach((key) => {
|
||||
if (key.startsWith('_'))
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
import type {
|
||||
Node,
|
||||
WorkflowDataUpdater,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { START_INITIAL_POSITION } from '@/app/components/workflow/constants'
|
||||
import startPlaceholderDefault from '@/app/components/workflow/nodes/start-placeholder/default'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { generateNewNode } from '@/app/components/workflow/utils'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
type HydrateWorkflowDraftGraphOptions = {
|
||||
localStartPlaceholderNodes?: Node[]
|
||||
}
|
||||
|
||||
const hasWorkflowEntryNode = (nodes: Node[] = []): boolean => {
|
||||
return nodes.some(node => (
|
||||
node?.data?.type === BlockEnum.Start
|
||||
|| node?.data?.type === BlockEnum.TriggerSchedule
|
||||
|| node?.data?.type === BlockEnum.TriggerWebhook
|
||||
|| node?.data?.type === BlockEnum.TriggerPlugin
|
||||
))
|
||||
}
|
||||
|
||||
const hasStartPlaceholderNode = (nodes: Node[] = []): boolean => {
|
||||
return nodes.some(node => node?.data?.type === BlockEnum.StartPlaceholder)
|
||||
}
|
||||
|
||||
export const useWorkflowDraftGraphForCanvas = (appMode?: AppModeEnum | string) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getNodesWithLocalStartPlaceholder = useCallback((
|
||||
nodes: Node[] = [],
|
||||
localStartPlaceholderNodes?: Node[],
|
||||
) => {
|
||||
if (appMode !== AppModeEnum.WORKFLOW || hasWorkflowEntryNode(nodes) || hasStartPlaceholderNode(nodes))
|
||||
return nodes
|
||||
|
||||
if (localStartPlaceholderNodes?.length)
|
||||
return [...localStartPlaceholderNodes, ...nodes]
|
||||
|
||||
const { newNode: startPlaceholderNode } = generateNewNode({
|
||||
data: {
|
||||
...startPlaceholderDefault.defaultValue,
|
||||
selected: true,
|
||||
type: startPlaceholderDefault.metaData.type,
|
||||
title: t(`blocks.${startPlaceholderDefault.metaData.type}`, { ns: 'workflow' }),
|
||||
desc: '',
|
||||
},
|
||||
position: START_INITIAL_POSITION,
|
||||
})
|
||||
|
||||
return [startPlaceholderNode, ...nodes]
|
||||
}, [appMode, t])
|
||||
|
||||
const getWorkflowDraftGraphForCanvas = useCallback((
|
||||
graph?: Partial<WorkflowDataUpdater>,
|
||||
options?: HydrateWorkflowDraftGraphOptions,
|
||||
): WorkflowDataUpdater => {
|
||||
const nodes = getNodesWithLocalStartPlaceholder(
|
||||
graph?.nodes || [],
|
||||
options?.localStartPlaceholderNodes,
|
||||
)
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges: graph?.edges || [],
|
||||
viewport: graph?.viewport || { x: 0, y: 0, zoom: 1 },
|
||||
}
|
||||
}, [getNodesWithLocalStartPlaceholder])
|
||||
|
||||
return {
|
||||
getWorkflowDraftGraphForCanvas,
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,7 @@ import {
|
||||
syncWorkflowDraft,
|
||||
} from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { useWorkflowDraftGraphForCanvas } from './use-workflow-draft-graph-for-canvas'
|
||||
import { useWorkflowTemplate } from './use-workflow-template'
|
||||
|
||||
const hasConnectedUserInput = (nodes: Node[] = [], edges: Edge[] = []): boolean => {
|
||||
@ -32,6 +33,7 @@ const hasConnectedUserInput = (nodes: Node[] = [], edges: Edge[] = []): boolean
|
||||
|
||||
return edges.some(edge => startNodeIds.includes(edge.source))
|
||||
}
|
||||
|
||||
export const useWorkflowInit = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const {
|
||||
@ -39,6 +41,7 @@ export const useWorkflowInit = () => {
|
||||
edges: edgesTemplate,
|
||||
} = useWorkflowTemplate()
|
||||
const appDetail = useAppStore(state => state.appDetail)!
|
||||
const { getWorkflowDraftGraphForCanvas } = useWorkflowDraftGraphForCanvas(appDetail.mode)
|
||||
const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash)
|
||||
const [data, setData] = useState<FetchWorkflowDraftResponse>()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@ -58,17 +61,24 @@ export const useWorkflowInit = () => {
|
||||
const handleGetInitialWorkflowData = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`)
|
||||
setData(res)
|
||||
const initialData = {
|
||||
...res,
|
||||
graph: getWorkflowDraftGraphForCanvas(res.graph, {
|
||||
localStartPlaceholderNodes: nodesTemplate,
|
||||
}),
|
||||
}
|
||||
|
||||
setData(initialData)
|
||||
workflowStore.setState({
|
||||
envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
envSecrets: (initialData.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
return acc
|
||||
}, {} as Record<string, string>),
|
||||
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
|
||||
conversationVariables: res.conversation_variables || [],
|
||||
environmentVariables: initialData.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
|
||||
conversationVariables: initialData.conversation_variables || [],
|
||||
isWorkflowDataLoaded: true,
|
||||
})
|
||||
setSyncWorkflowDraftHash(res.hash)
|
||||
setSyncWorkflowDraftHash(initialData.hash)
|
||||
setIsLoading(false)
|
||||
}
|
||||
catch (error: any) {
|
||||
@ -78,19 +88,18 @@ export const useWorkflowInit = () => {
|
||||
const isAdvancedChat = appDetail.mode === AppModeEnum.ADVANCED_CHAT
|
||||
workflowStore.setState({
|
||||
notInitialWorkflow: true,
|
||||
showOnboarding: !isAdvancedChat,
|
||||
shouldAutoOpenStartNodeSelector: !isAdvancedChat,
|
||||
hasShownOnboarding: false,
|
||||
showOnboarding: false,
|
||||
shouldAutoOpenStartNodeSelector: false,
|
||||
hasSelectedStartNode: false,
|
||||
hasShownOnboarding: !isAdvancedChat,
|
||||
})
|
||||
const nodesData = isAdvancedChat ? nodesTemplate : []
|
||||
const edgesData = isAdvancedChat ? edgesTemplate : []
|
||||
|
||||
syncWorkflowDraft({
|
||||
url: `/apps/${appDetail.id}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: nodesData,
|
||||
edges: edgesData,
|
||||
nodes: isAdvancedChat ? nodesTemplate : [],
|
||||
edges: isAdvancedChat ? edgesTemplate : [],
|
||||
},
|
||||
features: {
|
||||
retriever_resource: { enabled: true },
|
||||
@ -107,7 +116,7 @@ export const useWorkflowInit = () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash])
|
||||
}, [appDetail, getWorkflowDraftGraphForCanvas, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash])
|
||||
|
||||
useEffect(() => {
|
||||
handleGetInitialWorkflowData()
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import type { WorkflowDataUpdater } from '@/app/components/workflow/types'
|
||||
import { useCallback } from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useWorkflowUpdate } from '@/app/components/workflow/hooks'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { useWorkflowDraftGraphForCanvas } from './use-workflow-draft-graph-for-canvas'
|
||||
|
||||
export const useWorkflowRefreshDraft = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
|
||||
const { getWorkflowDraftGraphForCanvas } = useWorkflowDraftGraphForCanvas(appDetail?.mode)
|
||||
|
||||
const handleRefreshWorkflowDraft = useCallback((notUpdateCanvas?: boolean) => {
|
||||
const {
|
||||
@ -31,14 +34,8 @@ export const useWorkflowRefreshDraft = () => {
|
||||
fetchWorkflowDraft(`/apps/${appId}/workflows/draft`)
|
||||
.then((response) => {
|
||||
// Ensure we have a valid workflow structure with viewport
|
||||
if (!notUpdateCanvas) {
|
||||
const workflowData: WorkflowDataUpdater = {
|
||||
nodes: response.graph?.nodes || [],
|
||||
edges: response.graph?.edges || [],
|
||||
viewport: response.graph?.viewport || { x: 0, y: 0, zoom: 1 },
|
||||
}
|
||||
handleUpdateWorkflowCanvas(workflowData)
|
||||
}
|
||||
if (!notUpdateCanvas)
|
||||
handleUpdateWorkflowCanvas(getWorkflowDraftGraphForCanvas(response.graph))
|
||||
setSyncWorkflowDraftHash(response.hash)
|
||||
setEnvSecrets((response.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
@ -55,7 +52,7 @@ export const useWorkflowRefreshDraft = () => {
|
||||
.finally(() => {
|
||||
setIsSyncingWorkflowDraft(false)
|
||||
})
|
||||
}, [handleUpdateWorkflowCanvas, workflowStore])
|
||||
}, [getWorkflowDraftGraphForCanvas, handleUpdateWorkflowCanvas, workflowStore])
|
||||
|
||||
return {
|
||||
handleRefreshWorkflowDraft,
|
||||
|
||||
@ -34,6 +34,9 @@ export const useWorkflowStartRun = () => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
if (!startNode)
|
||||
return
|
||||
|
||||
const startVariables = startNode?.data.variables || []
|
||||
const fileSettings = featuresStore!.getState().features.file
|
||||
const {
|
||||
|
||||
@ -1,29 +1,39 @@
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
START_INITIAL_POSITION,
|
||||
} from '@/app/components/workflow/constants'
|
||||
import answerDefault from '@/app/components/workflow/nodes/answer/default'
|
||||
import llmDefault from '@/app/components/workflow/nodes/llm/default'
|
||||
import startPlaceholderDefault from '@/app/components/workflow/nodes/start-placeholder/default'
|
||||
import startDefault from '@/app/components/workflow/nodes/start/default'
|
||||
import { generateNewNode } from '@/app/components/workflow/utils'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { useIsChatMode } from './use-is-chat-mode'
|
||||
|
||||
export const useWorkflowTemplate = () => {
|
||||
const isChatMode = useIsChatMode()
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { newNode: startNode } = generateNewNode({
|
||||
data: {
|
||||
...startDefault.defaultValue as StartNodeType,
|
||||
type: startDefault.metaData.type,
|
||||
title: t(`blocks.${startDefault.metaData.type}`, { ns: 'workflow' }),
|
||||
},
|
||||
position: START_INITIAL_POSITION,
|
||||
})
|
||||
const createStartNode = () => {
|
||||
const { newNode: startNode } = generateNewNode({
|
||||
data: {
|
||||
...startDefault.defaultValue as StartNodeType,
|
||||
type: startDefault.metaData.type,
|
||||
title: t(`blocks.${startDefault.metaData.type}`, { ns: 'workflow' }),
|
||||
},
|
||||
position: START_INITIAL_POSITION,
|
||||
})
|
||||
|
||||
return startNode
|
||||
}
|
||||
|
||||
if (isChatMode) {
|
||||
const startNode = createStartNode()
|
||||
|
||||
const { newNode: llmNode } = generateNewNode({
|
||||
id: 'llm',
|
||||
data: {
|
||||
@ -77,10 +87,26 @@ export const useWorkflowTemplate = () => {
|
||||
edges: [startToLlmEdge, llmToAnswerEdge],
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (appDetail?.mode === AppModeEnum.WORKFLOW) {
|
||||
const { newNode: startPlaceholderNode } = generateNewNode({
|
||||
data: {
|
||||
...startPlaceholderDefault.defaultValue,
|
||||
selected: true,
|
||||
type: startPlaceholderDefault.metaData.type,
|
||||
title: t(`blocks.${startPlaceholderDefault.metaData.type}`, { ns: 'workflow' }),
|
||||
desc: '',
|
||||
},
|
||||
position: START_INITIAL_POSITION,
|
||||
})
|
||||
|
||||
return {
|
||||
nodes: [startNode],
|
||||
nodes: [startPlaceholderNode],
|
||||
edges: [],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: [createStartNode()],
|
||||
edges: [],
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ describe('BlockIcon', () => {
|
||||
|
||||
const iconContainer = container.firstElementChild
|
||||
expect(iconContainer).toHaveClass('w-4', 'h-4', 'bg-util-colors-blue-brand-blue-brand-500', 'extra-class')
|
||||
expect(iconContainer?.querySelector('svg')).toBeInTheDocument()
|
||||
expect(iconContainer?.querySelector('.i-custom-vender-workflow-user-input')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('normalizes protected plugin icon urls for tool-like nodes', () => {
|
||||
|
||||
@ -45,6 +45,7 @@ const ICON_CONTAINER_CLASSNAME_SIZE_MAP: Record<string, string> = {
|
||||
|
||||
const DEFAULT_ICON_MAP: Record<BlockEnum, React.ComponentType<{ className: string }>> = {
|
||||
[BlockEnum.Start]: Home,
|
||||
[BlockEnum.StartPlaceholder]: Home,
|
||||
[BlockEnum.LLM]: Llm,
|
||||
[BlockEnum.Code]: Code,
|
||||
[BlockEnum.End]: End,
|
||||
@ -96,6 +97,7 @@ const normalizeToolIconUrl = (toolIcon: string) => {
|
||||
|
||||
const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
|
||||
[BlockEnum.Start]: 'bg-util-colors-blue-brand-blue-brand-500',
|
||||
[BlockEnum.StartPlaceholder]: 'bg-util-colors-blue-brand-blue-brand-500',
|
||||
[BlockEnum.LLM]: 'bg-util-colors-indigo-indigo-500',
|
||||
[BlockEnum.Code]: 'bg-util-colors-blue-blue-500',
|
||||
[BlockEnum.End]: 'bg-util-colors-warning-warning-500',
|
||||
@ -129,12 +131,46 @@ const BlockIcon: FC<BlockIconProps> = ({
|
||||
className,
|
||||
toolIcon,
|
||||
}) => {
|
||||
const isStart = type === BlockEnum.Start
|
||||
const isStartPlaceholder = type === BlockEnum.StartPlaceholder
|
||||
const isToolOrDataSourceOrTriggerPlugin = type === BlockEnum.Tool || type === BlockEnum.DataSource || type === BlockEnum.TriggerPlugin
|
||||
const showDefaultIcon = !isToolOrDataSourceOrTriggerPlugin || !toolIcon
|
||||
const resolvedToolIcon = typeof toolIcon === 'string'
|
||||
? normalizeToolIconUrl(toolIcon)
|
||||
: toolIcon
|
||||
|
||||
if (isStart) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex items-center justify-center border-[0.5px] border-white/2 bg-util-colors-blue-brand-blue-brand-500 text-white',
|
||||
ICON_CONTAINER_CLASSNAME_SIZE_MAP[size],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn('i-custom-vender-workflow-user-input', size === 'xs' ? 'size-4' : 'size-4')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isStartPlaceholder) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex items-center justify-center border border-dashed border-components-panel-border bg-state-base-hover text-text-tertiary shadow-none',
|
||||
ICON_CONTAINER_CLASSNAME_SIZE_MAP[size],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn('i-custom-vender-workflow-start-placeholder text-text-primary opacity-30', size === 'xs' ? 'size-3' : 'size-3.5')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={
|
||||
cn(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { TriggerWithProvider } from '../types'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks'
|
||||
@ -14,6 +14,7 @@ import { useAvailableNodesMetaData } from '../../../workflow-app/hooks'
|
||||
import useNodes from '../../store/workflow/use-nodes'
|
||||
import { BlockEnum } from '../../types'
|
||||
import AllStartBlocks from '../all-start-blocks'
|
||||
import { createPlugin } from './factories'
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: vi.fn(),
|
||||
@ -49,7 +50,7 @@ vi.mock('@/utils/var', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/utils/var')>()
|
||||
return {
|
||||
...actual,
|
||||
getMarketplaceUrl: () => 'https://marketplace.test/start',
|
||||
getMarketplaceUrl: (path: string) => `https://marketplace.test${path}`,
|
||||
}
|
||||
})
|
||||
|
||||
@ -186,7 +187,7 @@ describe('AllStartBlocks', () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
|
||||
render(
|
||||
const { container } = render(
|
||||
<AllStartBlocks
|
||||
searchText=""
|
||||
onSelect={onSelect}
|
||||
@ -196,11 +197,14 @@ describe('AllStartBlocks', () => {
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('workflow.tabs.allTriggers')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument()
|
||||
expect(screen.queryByText('workflow.tabs.allTriggers')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.blocks.mostCommon')).toBeInTheDocument()
|
||||
expect(screen.getByText('Provider One')).toBeInTheDocument()
|
||||
expect(container.querySelectorAll('.bg-divider-subtle')).toHaveLength(0)
|
||||
|
||||
await user.click(screen.getByText('workflow.blocks.start'))
|
||||
expect(onSelect).toHaveBeenCalledWith(BlockEnum.Start)
|
||||
@ -225,7 +229,150 @@ describe('AllStartBlocks', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(await screen.findByRole('link', { name: /plugin\.findMoreInMarketplace/ })).toHaveAttribute('href', 'https://marketplace.test/start')
|
||||
const footer = await screen.findByRole('link', { name: /plugin\.findMoreInMarketplace/ })
|
||||
expect(footer).toHaveAttribute('href', 'https://marketplace.test/plugins/trigger')
|
||||
expect(footer).toHaveClass('system-sm-medium', 'h-8', 'rounded-b-lg', 'bg-components-panel-bg-blur', 'text-text-accent-light-mode-only', 'shadow-lg')
|
||||
expect(footer.querySelector('.i-custom-vender-main-nav-marketplace')).not.toBeInTheDocument()
|
||||
expect(footer.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep the panel marketplace footer icon style', async () => {
|
||||
enableMarketplaceForRender = true
|
||||
|
||||
render(
|
||||
<AllStartBlocks
|
||||
variant="panel"
|
||||
searchText=""
|
||||
onSelect={vi.fn()}
|
||||
availableBlocksTypes={[BlockEnum.TriggerPlugin]}
|
||||
/>,
|
||||
)
|
||||
|
||||
const footer = await screen.findByRole('link', { name: /workflow\.nodes\.startPlaceholder\.browseMoreOnMarketplace/ })
|
||||
expect(footer).toHaveAttribute('href', 'https://marketplace.test/plugins/trigger')
|
||||
expect(footer).toHaveClass('flex-col')
|
||||
expect(footer.querySelector('.w-8 .bg-divider-subtle')).toBeInTheDocument()
|
||||
expect(footer.querySelector('.i-custom-vender-workflow-marketplace')).toBeInTheDocument()
|
||||
expect(footer.querySelector('svg')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep the panel divider between user input and installed triggers', async () => {
|
||||
const { container } = render(
|
||||
<AllStartBlocks
|
||||
variant="panel"
|
||||
searchText=""
|
||||
onSelect={vi.fn()}
|
||||
availableBlocksTypes={[BlockEnum.Start, BlockEnum.TriggerPlugin]}
|
||||
allowUserInputSelection
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Provider One')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(container.querySelectorAll('.px-4.py-1 .bg-divider-subtle')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should render searched marketplace results after built-in and installed trigger options', async () => {
|
||||
enableMarketplaceForRender = true
|
||||
mockUseAllTriggerPlugins.mockReturnValue(createTriggerPluginsQueryResult([
|
||||
createTriggerProvider({
|
||||
label: { en_US: 'Start Provider', zh_Hans: 'Start Provider' },
|
||||
}),
|
||||
]))
|
||||
mockUseMarketplacePlugins.mockReturnValue(createMarketplacePluginsMock({
|
||||
plugins: [
|
||||
createPlugin({
|
||||
name: 'start-marketplace',
|
||||
label: { en_US: 'Start Marketplace', zh_Hans: 'Start Marketplace' },
|
||||
}),
|
||||
],
|
||||
}))
|
||||
|
||||
const { container } = render(
|
||||
<AllStartBlocks
|
||||
searchText="start"
|
||||
onSelect={vi.fn()}
|
||||
availableBlocksTypes={[BlockEnum.Start, BlockEnum.TriggerPlugin]}
|
||||
allowUserInputSelection
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Start Marketplace')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const text = container.textContent || ''
|
||||
expect(text.indexOf('workflow.blocks.start')).toBeLessThan(text.indexOf('Start Provider'))
|
||||
expect(text.indexOf('Start Provider')).toBeLessThan(text.indexOf('Start Marketplace'))
|
||||
expect(screen.getAllByRole('link', { name: /plugin\.searchInMarketplace/i })).toHaveLength(1)
|
||||
expect(container.querySelectorAll('.px-4.py-1 .bg-divider-subtle')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should show the user input conflict state without allowing another start selection', () => {
|
||||
const onSelect = vi.fn()
|
||||
enableMarketplaceForRender = true
|
||||
mockUseNodes.mockReturnValue([
|
||||
{
|
||||
id: 'start',
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
},
|
||||
},
|
||||
] as never)
|
||||
|
||||
render(
|
||||
<AllStartBlocks
|
||||
searchText=""
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={[BlockEnum.Start, BlockEnum.TriggerPlugin]}
|
||||
hasUserInputNode
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('workflow.nodes.startPlaceholder.userInputConflictTip')).toBeInTheDocument()
|
||||
expect(screen.queryByText('workflow.tabs.allTriggers')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.operation.added')).toBeInTheDocument()
|
||||
const footer = screen.getByRole('link', { name: /plugin\.findMoreInMarketplace/ })
|
||||
expect(footer).toHaveClass('system-sm-medium', 'h-8', 'rounded-b-lg', 'bg-components-panel-bg-blur', 'text-text-accent-light-mode-only', 'shadow-lg')
|
||||
expect(footer.querySelector('.i-custom-vender-main-nav-marketplace')).not.toBeInTheDocument()
|
||||
expect(footer.querySelector('svg')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('workflow.blocks.start'))
|
||||
fireEvent.click(screen.getByText('Provider One'))
|
||||
|
||||
expect(onSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should keep user input visible but disabled when another trigger already exists', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
|
||||
render(
|
||||
<AllStartBlocks
|
||||
searchText=""
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={[BlockEnum.Start, BlockEnum.TriggerSchedule]}
|
||||
hasTriggerNode
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText('workflow.tabs.allTriggers')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.blocks.mostCommon').closest('.opacity-30')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.blocks.start').closest('.cursor-not-allowed')).toBeInTheDocument()
|
||||
|
||||
await user.hover(screen.getByText('workflow.blocks.start'))
|
||||
|
||||
expect(await screen.findByText('workflow.nodes.startPlaceholder.userInputConflictTip')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('workflow.blocks.start'))
|
||||
expect(onSelect).not.toHaveBeenCalled()
|
||||
|
||||
await user.click(screen.getByText('workflow.blocks.trigger-schedule'))
|
||||
expect(onSelect).toHaveBeenCalledWith(BlockEnum.TriggerSchedule)
|
||||
})
|
||||
})
|
||||
|
||||
@ -256,11 +403,15 @@ describe('AllStartBlocks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
expect(screen.getByText('workflow.tabs.noPluginsFound')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.nodes.startPlaceholder.noTriggersFound')).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'workflow.tabs.requestToCommunity' })).toHaveAttribute(
|
||||
'href',
|
||||
'https://github.com/langgenius/dify-plugins/issues/new?template=plugin_request.yaml',
|
||||
)
|
||||
expect(screen.getByRole('link', { name: /plugin\.findMoreInMarketplace/ })).toHaveAttribute(
|
||||
'href',
|
||||
'https://marketplace.test/plugins/trigger',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -193,5 +193,37 @@ describe('FeaturedTriggers', () => {
|
||||
event_label: 'Created',
|
||||
}))
|
||||
})
|
||||
|
||||
it('should align featured item icons with the trigger list column', () => {
|
||||
const provider = createTriggerProvider()
|
||||
|
||||
render(
|
||||
<FeaturedTriggers
|
||||
plugins={[
|
||||
createPlugin({ plugin_id: 'plugin-1', latest_package_identifier: 'plugin-1@1.0.0' }),
|
||||
createPlugin({
|
||||
name: 'plugin-two',
|
||||
plugin_id: 'plugin-2',
|
||||
latest_package_identifier: 'plugin-2@1.0.0',
|
||||
label: { en_US: 'Plugin Two', zh_Hans: '插件二' },
|
||||
}),
|
||||
]}
|
||||
providerMap={new Map([
|
||||
['plugin-1', provider],
|
||||
['plugin-1@1.0.0', provider],
|
||||
])}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const installedRow = screen.getByText('Provider One').closest('.select-none')
|
||||
expect(installedRow).toHaveClass('h-8', 'pr-2', 'pl-3')
|
||||
expect(installedRow?.parentElement?.parentElement?.parentElement).toHaveClass('p-1')
|
||||
|
||||
const uninstalledRow = screen.getByText('Plugin Two').closest('.group')
|
||||
expect(uninstalledRow).toHaveClass('h-8', 'pr-2', 'pl-3')
|
||||
expect(uninstalledRow?.parentElement).toHaveClass('mb-1', 'last-of-type:mb-0')
|
||||
expect(uninstalledRow?.parentElement?.parentElement).toHaveClass('p-1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,14 +7,27 @@ describe('block-selector hooks', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('falls back to the first valid tab when the preferred start tab is disabled', () => {
|
||||
it('keeps the start tab enabled when a configured user input start node exists', () => {
|
||||
const { result } = renderHook(() => useTabs({
|
||||
noStart: false,
|
||||
hasUserInputNode: true,
|
||||
defaultActiveTab: TabsEnum.Start,
|
||||
}))
|
||||
|
||||
expect(result.current.tabs.find(tab => tab.key === TabsEnum.Start)?.disabled).toBe(true)
|
||||
expect(result.current.tabs.find(tab => tab.key === TabsEnum.Start)?.disabled).toBeFalsy()
|
||||
expect(result.current.activeTab).toBe(TabsEnum.Start)
|
||||
})
|
||||
|
||||
it('disables the start tab when an unconfigured start placeholder exists', () => {
|
||||
const { result } = renderHook(() => useTabs({
|
||||
noStart: false,
|
||||
hasStartPlaceholderNode: true,
|
||||
defaultActiveTab: TabsEnum.Start,
|
||||
}))
|
||||
|
||||
const startTab = result.current.tabs.find(tab => tab.key === TabsEnum.Start)
|
||||
expect(startTab?.disabled).toBe(true)
|
||||
expect(startTab?.disabledTip).toBe('workflow.tabs.unconfiguredStartDisabledTip')
|
||||
expect(startTab?.disabledTipLinkKey).toBe('startNodesDocs')
|
||||
expect(result.current.activeTab).toBe(TabsEnum.Blocks)
|
||||
})
|
||||
|
||||
@ -22,7 +35,6 @@ describe('block-selector hooks', () => {
|
||||
const props: Parameters<typeof useTabs>[0] = {
|
||||
noBlocks: false,
|
||||
noStart: false,
|
||||
hasUserInputNode: true,
|
||||
forceEnableStartTab: true,
|
||||
}
|
||||
|
||||
|
||||
@ -65,6 +65,7 @@ describe('NodeSelectorWrapper', () => {
|
||||
availableNodesMetaData: {
|
||||
nodes: [
|
||||
createBlock(BlockEnum.Start, 'Start'),
|
||||
createBlock(BlockEnum.StartPlaceholder, 'Start Placeholder'),
|
||||
createBlock(BlockEnum.Tool, 'Tool'),
|
||||
createBlock(BlockEnum.Code, 'Code'),
|
||||
createBlock(BlockEnum.DataSource, 'Data Source'),
|
||||
@ -79,6 +80,7 @@ describe('NodeSelectorWrapper', () => {
|
||||
|
||||
expect(await screen.findByText('Code')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Start')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Start Placeholder')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Tool')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,7 +7,7 @@ import { FlowType } from '@/types/common'
|
||||
import { renderWorkflowComponent } from '../../__tests__/workflow-test-env'
|
||||
import { BlockEnum } from '../../types'
|
||||
import NodeSelector from '../main'
|
||||
import { BlockClassificationEnum } from '../types'
|
||||
import { BlockClassificationEnum, TabsEnum } from '../types'
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({
|
||||
@ -22,6 +22,22 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
plugins: [],
|
||||
isLoading: false,
|
||||
}),
|
||||
useFeaturedTriggersRecommendations: () => ({
|
||||
plugins: [],
|
||||
isLoading: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/marketplace/hooks', () => ({
|
||||
useMarketplacePlugins: () => ({
|
||||
plugins: [],
|
||||
queryPluginsWithDebounced: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-triggers', () => ({
|
||||
useAllTriggerPlugins: () => ({ data: [] }),
|
||||
useInvalidateAllTriggerPlugins: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
@ -45,13 +61,18 @@ const createBlock = (type: BlockEnum, title: string): NodeDefault => ({
|
||||
checkValid: () => ({ isValid: true }),
|
||||
})
|
||||
|
||||
const renderNodeSelector = (ui: ReactElement) => {
|
||||
type RenderNodeSelectorOptions = Parameters<typeof renderWorkflowComponent>[1]
|
||||
|
||||
const renderNodeSelector = (ui: ReactElement, options?: RenderNodeSelectorOptions) => {
|
||||
return renderWorkflowComponent(ui, {
|
||||
...options,
|
||||
hooksStoreProps: {
|
||||
...options?.hooksStoreProps,
|
||||
configsMap: {
|
||||
flowId: 'app-1',
|
||||
flowType: FlowType.appFlow,
|
||||
fileSettings: {} as never,
|
||||
...options?.hooksStoreProps?.configsMap,
|
||||
|
||||
},
|
||||
},
|
||||
@ -230,4 +251,68 @@ describe('NodeSelector', () => {
|
||||
expect(trigger.closest('[aria-haspopup="dialog"]')).toBe(trigger)
|
||||
expect(screen.getByPlaceholderText('workflow.tabs.searchBlock')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('disables the start tab with a setup tooltip when an unconfigured start node is on the canvas', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderNodeSelector(
|
||||
<NodeSelector
|
||||
open
|
||||
onSelect={vi.fn()}
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.Start]}
|
||||
showStartTab
|
||||
defaultActiveTab={TabsEnum.Start}
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'start-placeholder',
|
||||
data: {
|
||||
type: BlockEnum.StartPlaceholder,
|
||||
},
|
||||
},
|
||||
] as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
await user.hover(screen.getByText('workflow.tabs.start'))
|
||||
|
||||
expect(await screen.findByText('workflow.tabs.unconfiguredStartDisabledTip')).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'workflow.tabs.startDisabledTipLearnMore' })).toHaveAttribute(
|
||||
'href',
|
||||
'https://docs.dify.ai/en/use-dify/nodes/trigger/overview',
|
||||
)
|
||||
expect(screen.getByPlaceholderText('workflow.tabs.searchBlock')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the start tab enabled when a configured user input start node is on the canvas', () => {
|
||||
renderNodeSelector(
|
||||
<NodeSelector
|
||||
open
|
||||
onSelect={vi.fn()}
|
||||
blocks={[createBlock(BlockEnum.LLM, 'LLM')]}
|
||||
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.Start, BlockEnum.TriggerPlugin]}
|
||||
showStartTab
|
||||
defaultActiveTab={TabsEnum.Start}
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'start',
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
},
|
||||
},
|
||||
] as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(screen.getByText('workflow.tabs.start')).toHaveAttribute('aria-disabled', 'false')
|
||||
expect(screen.getByText('workflow.nodes.startPlaceholder.userInputConflictTip')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { CommonNodeType } from '../../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { useAvailableNodesMetaData } from '../../../workflow-app/hooks'
|
||||
@ -76,5 +76,71 @@ describe('StartBlocks', () => {
|
||||
expect(screen.queryByText('workflow.blocks.start')).not.toBeInTheDocument()
|
||||
expect(onContentStateChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should show most common badge for user input in the start selector content', () => {
|
||||
render(
|
||||
<StartBlocks
|
||||
searchText=""
|
||||
onSelect={vi.fn()}
|
||||
availableBlocksTypes={[BlockEnum.Start]}
|
||||
showMostCommonBadge
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('workflow.blocks.mostCommon')).toBeInTheDocument()
|
||||
expect(screen.queryByText('workflow.blocks.originalStartNode')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render built-in start block preview titles and Dify Team author', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<StartBlocks
|
||||
searchText=""
|
||||
onSelect={vi.fn()}
|
||||
availableBlocksTypes={[BlockEnum.Start, BlockEnum.TriggerWebhook]}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.hover(screen.getByText('workflow.blocks.trigger-webhook'))
|
||||
|
||||
expect(screen.queryByText('workflow.customWebhook')).not.toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('workflow.blocks.trigger-webhook')).toHaveLength(2)
|
||||
})
|
||||
|
||||
await user.hover(screen.getByText('workflow.blocks.start'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.body).toHaveTextContent('tools.author workflow.difyTeam')
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep disabled user input reachable from the keyboard with the conflict reason', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
|
||||
render(
|
||||
<StartBlocks
|
||||
searchText=""
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={[BlockEnum.Start]}
|
||||
showMostCommonBadge
|
||||
showUserInputDisabled
|
||||
/>,
|
||||
)
|
||||
|
||||
const userInputButton = screen.getByRole('button', {
|
||||
name: /workflow\.blocks\.start.*workflow\.nodes\.startPlaceholder\.userInputConflictTip/,
|
||||
})
|
||||
expect(userInputButton).toHaveAttribute('aria-disabled', 'true')
|
||||
expect(userInputButton.querySelector('.i-custom-vender-workflow-user-input')?.closest('.opacity-30')).toBeInTheDocument()
|
||||
|
||||
await user.tab()
|
||||
expect(userInputButton).toHaveFocus()
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(onSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -33,7 +33,20 @@ import PluginList from './market-place-plugin/list'
|
||||
import StartBlocks from './start-blocks'
|
||||
import TriggerPluginList from './trigger-plugin/list'
|
||||
|
||||
const marketplaceFooterClassName = 'system-sm-medium z-10 flex h-8 flex-none 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'
|
||||
const popoverMarketplaceFooterClassName = 'system-sm-medium z-10 flex h-8 flex-none 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'
|
||||
const panelMarketplaceFooterClassName = 'system-xs-regular z-10 flex flex-none cursor-pointer flex-col items-start gap-2 px-4 pt-2 pb-4 text-text-tertiary hover:text-text-secondary'
|
||||
|
||||
const SectionDivider = () => (
|
||||
<div className="px-4 py-1" aria-hidden>
|
||||
<Divider type="horizontal" className="my-0 h-px bg-divider-subtle" />
|
||||
</div>
|
||||
)
|
||||
|
||||
const MarketplaceFooterDivider = () => (
|
||||
<div className="flex h-2 w-8 items-center" aria-hidden>
|
||||
<Divider type="horizontal" className="my-0 h-px w-8 bg-divider-subtle" />
|
||||
</div>
|
||||
)
|
||||
|
||||
type AllStartBlocksProps = {
|
||||
className?: string
|
||||
@ -42,6 +55,9 @@ type AllStartBlocksProps = {
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
tags?: string[]
|
||||
allowUserInputSelection?: boolean // Allow user input option even when trigger node already exists (e.g. when no Start node yet or changing node type).
|
||||
hasUserInputNode?: boolean
|
||||
hasTriggerNode?: boolean
|
||||
variant?: 'popover' | 'panel'
|
||||
}
|
||||
|
||||
const AllStartBlocks = ({
|
||||
@ -51,6 +67,9 @@ const AllStartBlocks = ({
|
||||
availableBlocksTypes,
|
||||
tags = [],
|
||||
allowUserInputSelection = false,
|
||||
hasUserInputNode = false,
|
||||
hasTriggerNode = false,
|
||||
variant = 'popover',
|
||||
}: AllStartBlocksProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false)
|
||||
@ -100,9 +119,8 @@ const AllStartBlocks = ({
|
||||
const shouldShowFeatured = enableTriggerPlugin
|
||||
&& enable_marketplace
|
||||
&& !hasFilter
|
||||
const hasTriggerOptions = entryNodeTypes.some(type => type !== BlockEnumValue.Start)
|
||||
const shouldShowTriggerListTitle = hasTriggerOptions && (hasStartBlocksContent || hasPluginContent)
|
||||
const shouldShowMarketplaceFooter = enable_marketplace && !hasFilter
|
||||
const shouldShowMarketplaceFooter = enable_marketplace
|
||||
const isPanelVariant = variant === 'panel'
|
||||
|
||||
const handleStartBlocksContentChange = useCallback((hasContent: boolean) => {
|
||||
setHasStartBlocksContent(hasContent)
|
||||
@ -115,6 +133,11 @@ const AllStartBlocks = ({
|
||||
const hasMarketplaceContent = enableTriggerPlugin && enable_marketplace && marketplacePlugins.length > 0
|
||||
const hasAnyContent = hasStartBlocksContent || hasPluginContent || shouldShowFeatured || hasMarketplaceContent
|
||||
const shouldShowEmptyState = hasFilter && !hasAnyContent
|
||||
const shouldShowInstalledTriggersDivider = isPanelVariant && hasStartBlocksContent && enableTriggerPlugin && hasPluginContent
|
||||
const shouldShowMarketplaceSectionDivider = enableTriggerPlugin
|
||||
&& enable_marketplace
|
||||
&& (hasStartBlocksContent || hasPluginContent)
|
||||
&& (shouldShowFeatured || hasMarketplaceContent)
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableTriggerPlugin && hasPluginContent)
|
||||
@ -134,16 +157,58 @@ const AllStartBlocks = ({
|
||||
}, [enableTriggerPlugin, enable_marketplace, hasFilter, fetchPlugins, searchText, tags])
|
||||
|
||||
return (
|
||||
<div className={cn('max-w-[500px] min-w-[400px]', className)}>
|
||||
<div className="flex max-h-[640px] flex-col">
|
||||
<div className={cn('max-w-[500px] min-w-[400px]', variant === 'panel' && 'h-full max-w-none min-w-0', className)}>
|
||||
<div className={cn('flex max-h-[640px] flex-col', variant === 'panel' && 'h-full max-h-none')}>
|
||||
<div
|
||||
ref={wrapElemRef}
|
||||
className="flex-1 overflow-y-auto"
|
||||
onScroll={() => pluginRef.current?.handleScroll()}
|
||||
>
|
||||
<div className={cn(shouldShowEmptyState && 'hidden')}>
|
||||
{shouldShowFeatured && (
|
||||
<>
|
||||
{hasUserInputNode && (
|
||||
<div className="relative flex items-start gap-0.5 overflow-hidden border-b-[0.5px] border-divider-subtle bg-components-panel-bg-blur px-3 py-2">
|
||||
<div className="absolute inset-0 bg-linear-to-r from-util-colors-blue-light-blue-light-500/20 to-transparent opacity-40" aria-hidden />
|
||||
<span className="relative flex shrink-0 items-center justify-center p-1" aria-hidden>
|
||||
<span className="i-ri-information-fill size-4 text-text-accent" />
|
||||
</span>
|
||||
<div className="relative py-1 system-xs-regular text-text-secondary">
|
||||
{t('nodes.startPlaceholder.userInputConflictTip', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={cn(hasUserInputNode && 'pointer-events-none opacity-30')}>
|
||||
<StartBlocks
|
||||
searchText={trimmedSearchText}
|
||||
onSelect={onSelect as OnSelectBlock}
|
||||
availableBlocksTypes={entryNodeTypes as unknown as BlockEnum[]}
|
||||
hideUserInput={!allowUserInputSelection && !hasTriggerNode}
|
||||
showMostCommonBadge
|
||||
showUserInputAdded={hasUserInputNode}
|
||||
showUserInputDisabled={hasTriggerNode && !hasUserInputNode}
|
||||
disabled={hasUserInputNode}
|
||||
onContentStateChange={handleStartBlocksContentChange}
|
||||
/>
|
||||
|
||||
{shouldShowInstalledTriggersDivider && (
|
||||
<SectionDivider />
|
||||
)}
|
||||
|
||||
{enableTriggerPlugin && (
|
||||
<TriggerPluginList
|
||||
onSelect={onSelect}
|
||||
searchText={trimmedSearchText}
|
||||
onContentStateChange={handlePluginContentChange}
|
||||
tags={tags}
|
||||
disabled={hasUserInputNode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowMarketplaceSectionDivider && (
|
||||
<SectionDivider />
|
||||
)}
|
||||
|
||||
{shouldShowFeatured && (
|
||||
<FeaturedTriggers
|
||||
plugins={featuredPlugins}
|
||||
providerMap={providerMap}
|
||||
@ -153,50 +218,26 @@ const AllStartBlocks = ({
|
||||
invalidateTriggers()
|
||||
}}
|
||||
/>
|
||||
<div className="px-3">
|
||||
<Divider className="h-px!" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{shouldShowTriggerListTitle && (
|
||||
<div className="px-3 pt-2 pb-1">
|
||||
<span className="system-xs-medium text-text-primary">{t('tabs.allTriggers', { ns: 'workflow' })}</span>
|
||||
</div>
|
||||
)}
|
||||
<StartBlocks
|
||||
searchText={trimmedSearchText}
|
||||
onSelect={onSelect as OnSelectBlock}
|
||||
availableBlocksTypes={entryNodeTypes as unknown as BlockEnum[]}
|
||||
hideUserInput={!allowUserInputSelection}
|
||||
onContentStateChange={handleStartBlocksContentChange}
|
||||
/>
|
||||
|
||||
{enableTriggerPlugin && (
|
||||
<TriggerPluginList
|
||||
onSelect={onSelect}
|
||||
searchText={trimmedSearchText}
|
||||
onContentStateChange={handlePluginContentChange}
|
||||
tags={tags}
|
||||
/>
|
||||
)}
|
||||
{enableTriggerPlugin && enable_marketplace && (
|
||||
<PluginList
|
||||
ref={pluginRef}
|
||||
wrapElemRef={wrapElemRef as RefObject<HTMLElement>}
|
||||
list={marketplacePlugins}
|
||||
searchText={trimmedSearchText}
|
||||
category={PluginCategoryEnum.trigger}
|
||||
tags={tags}
|
||||
hideFindMoreFooter
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
{enableTriggerPlugin && enable_marketplace && (
|
||||
<PluginList
|
||||
ref={pluginRef}
|
||||
wrapElemRef={wrapElemRef as RefObject<HTMLElement>}
|
||||
list={marketplacePlugins}
|
||||
searchText={trimmedSearchText}
|
||||
category={PluginCategoryEnum.trigger}
|
||||
tags={tags}
|
||||
hideFindMoreFooter
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{shouldShowEmptyState && (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 py-12 text-center">
|
||||
<SearchMenu className="size-8 text-text-quaternary" />
|
||||
<div className="text-sm font-medium text-text-secondary">
|
||||
{t('tabs.noPluginsFound', { ns: 'workflow' })}
|
||||
{t('nodes.startPlaceholder.noTriggersFound', { ns: 'workflow' })}
|
||||
</div>
|
||||
<Link
|
||||
href="https://github.com/langgenius/dify-plugins/issues/new?template=plugin_request.yaml"
|
||||
@ -214,15 +255,28 @@ const AllStartBlocks = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shouldShowMarketplaceFooter && !shouldShowEmptyState && (
|
||||
// Footer - Same as Tools tab marketplace footer
|
||||
{shouldShowMarketplaceFooter && (
|
||||
<Link
|
||||
className={marketplaceFooterClassName}
|
||||
href={getMarketplaceUrl('', { category: PluginCategoryEnum.trigger })}
|
||||
className={isPanelVariant ? panelMarketplaceFooterClassName : popoverMarketplaceFooterClassName}
|
||||
href={getMarketplaceUrl('/plugins/trigger')}
|
||||
target="_blank"
|
||||
>
|
||||
<span>{t('findMoreInMarketplace', { ns: 'plugin' })}</span>
|
||||
<RiArrowRightUpLine className="ml-0.5 size-3" />
|
||||
{isPanelVariant
|
||||
? (
|
||||
<>
|
||||
<MarketplaceFooterDivider />
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="i-custom-vender-workflow-marketplace size-3 shrink-0" aria-hidden />
|
||||
<span>{t('nodes.startPlaceholder.browseMoreOnMarketplace', { ns: 'workflow' })}</span>
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<span>{t('findMoreInMarketplace', { ns: 'plugin' })}</span>
|
||||
<RiArrowRightUpLine className="ml-0.5 size-3" />
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
|
||||
type SharedProps = {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
hoverable?: boolean
|
||||
}
|
||||
|
||||
type ButtonRowProps = SharedProps
|
||||
& Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'disabled'>
|
||||
& {
|
||||
as?: 'button'
|
||||
nativeDisabled?: boolean
|
||||
}
|
||||
|
||||
type DivRowProps = SharedProps
|
||||
& Omit<HTMLAttributes<HTMLDivElement>, 'className'>
|
||||
& {
|
||||
as: 'div'
|
||||
}
|
||||
|
||||
type BlockSelectorRowProps = ButtonRowProps | DivRowProps
|
||||
|
||||
const rowClassName = (className?: string, disabled = false, hoverable = true) => cn(
|
||||
'flex h-8 w-full items-center rounded-lg pr-2 pl-3',
|
||||
!disabled && hoverable && 'hover:bg-state-base-hover',
|
||||
disabled && 'cursor-not-allowed',
|
||||
className,
|
||||
)
|
||||
|
||||
const buttonClassName = (className?: string, disabled = false, hoverable = true) => cn(
|
||||
rowClassName(className, disabled, hoverable),
|
||||
!disabled && 'cursor-pointer',
|
||||
'border-0 bg-transparent text-left focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden',
|
||||
)
|
||||
|
||||
export function BlockSelectorRow(props: BlockSelectorRowProps) {
|
||||
if (props.as === 'div') {
|
||||
const {
|
||||
as: _as,
|
||||
children,
|
||||
className,
|
||||
disabled = false,
|
||||
hoverable,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={rowClassName(className, disabled, hoverable)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
as: _as,
|
||||
children,
|
||||
className,
|
||||
disabled = false,
|
||||
hoverable,
|
||||
nativeDisabled,
|
||||
type = 'button',
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
disabled={nativeDisabled}
|
||||
className={buttonClassName(className, disabled, hoverable)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@ -19,6 +19,7 @@ import { formatNumber } from '@/utils/format'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import { BlockSelectorRow } from './block-selector-row'
|
||||
import { TriggerPluginActionPreviewCard } from './trigger-plugin/action-item'
|
||||
import TriggerPluginItem from './trigger-plugin/item'
|
||||
|
||||
@ -114,10 +115,10 @@ const FeaturedTriggers = ({
|
||||
const showEmptyState = !isLoading && totalVisible === 0
|
||||
|
||||
return (
|
||||
<div className="px-3 pt-2 pb-3">
|
||||
<div className="pt-2 pb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center rounded-md px-0 py-1 text-left text-text-primary"
|
||||
className="flex w-full items-center rounded-md px-4 py-1 text-left text-text-primary"
|
||||
onClick={() => setIsCollapsed(prev => !prev)}
|
||||
>
|
||||
<span className="system-xs-medium text-text-primary">{t('tabs.featuredTools', { ns: 'workflow' })}</span>
|
||||
@ -133,7 +134,7 @@ const FeaturedTriggers = ({
|
||||
)}
|
||||
|
||||
{showEmptyState && (
|
||||
<p className="py-2 system-xs-regular text-text-tertiary">
|
||||
<p className="px-4 py-2 system-xs-regular text-text-tertiary">
|
||||
<Link className="text-text-accent" href={getMarketplaceUrl('', { category: 'trigger' })} target="_blank" rel="noopener noreferrer">
|
||||
{t('tabs.noFeaturedTriggers', { ns: 'workflow' })}
|
||||
</Link>
|
||||
@ -141,38 +142,31 @@ const FeaturedTriggers = ({
|
||||
)}
|
||||
|
||||
{!showEmptyState && !isLoading && (
|
||||
<>
|
||||
{visibleInstalledProviders.length > 0 && (
|
||||
<div className="mt-1">
|
||||
{visibleInstalledProviders.map(provider => (
|
||||
<TriggerPluginItem
|
||||
key={provider.id}
|
||||
payload={provider}
|
||||
hasSearchText={false}
|
||||
previewCardHandle={triggerActionPreviewCardHandle}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 p-1">
|
||||
{visibleInstalledProviders.map(provider => (
|
||||
<TriggerPluginItem
|
||||
key={provider.id}
|
||||
payload={provider}
|
||||
hasSearchText={false}
|
||||
previewCardHandle={triggerActionPreviewCardHandle}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
|
||||
{visibleUninstalledPlugins.length > 0 && (
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
{visibleUninstalledPlugins.map(plugin => (
|
||||
<FeaturedTriggerUninstalledItem
|
||||
key={plugin.plugin_id}
|
||||
plugin={plugin}
|
||||
language={language}
|
||||
previewCardHandle={previewCardHandle}
|
||||
onInstallSuccess={async () => {
|
||||
await onInstallSuccess?.()
|
||||
}}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
{visibleUninstalledPlugins.map(plugin => (
|
||||
<div key={plugin.plugin_id} className="mb-1 last-of-type:mb-0">
|
||||
<FeaturedTriggerUninstalledItem
|
||||
plugin={plugin}
|
||||
language={language}
|
||||
previewCardHandle={previewCardHandle}
|
||||
onInstallSuccess={async () => {
|
||||
await onInstallSuccess?.()
|
||||
}}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && totalVisible > 0 && canToggleVisibility && (
|
||||
@ -255,16 +249,14 @@ function FeaturedTriggerUninstalledItem({
|
||||
}, [actionOpen])
|
||||
|
||||
const row = (
|
||||
<div
|
||||
className="group flex h-8 w-full items-center rounded-lg pr-1 pl-3 hover:bg-state-base-hover"
|
||||
>
|
||||
<div className="flex h-full min-w-0 items-center">
|
||||
<BlockIcon type={BlockEnum.TriggerPlugin} toolIcon={plugin.icon} />
|
||||
<div className="ml-2 min-w-0">
|
||||
<BlockSelectorRow as="div" className="group select-none">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<BlockIcon className="mr-2 shrink-0" type={BlockEnum.TriggerPlugin} size="sm" toolIcon={plugin.icon} />
|
||||
<div className="min-w-0">
|
||||
<div className="truncate system-sm-medium text-text-secondary">{label}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto flex h-full items-center gap-1 pl-1">
|
||||
<div className="ml-auto flex h-6 items-center gap-1 pl-1">
|
||||
<span className={`system-xs-regular text-text-tertiary ${actionOpen ? 'hidden' : 'group-hover:hidden'}`}>{installCountLabel}</span>
|
||||
<div
|
||||
className={`flex h-full items-center gap-1 system-xs-medium text-components-button-secondary-accent-text [&_.action-btn]:size-6 [&_.action-btn]:min-h-0 [&_.action-btn]:rounded-lg [&_.action-btn]:p-0 ${actionOpen ? '' : 'hidden group-hover:flex'}`}
|
||||
@ -288,7 +280,7 @@ function FeaturedTriggerUninstalledItem({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BlockSelectorRow>
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@ -11,6 +12,8 @@ import {
|
||||
ToolTypeEnum,
|
||||
} from './types'
|
||||
|
||||
const startNodesDocsTipLinkKey = 'startNodesDocs' as const
|
||||
|
||||
export const useBlocks = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -29,7 +32,7 @@ export const useTabs = ({
|
||||
noSnippets,
|
||||
noStart = true,
|
||||
defaultActiveTab,
|
||||
hasUserInputNode = false,
|
||||
hasStartPlaceholderNode = false,
|
||||
disableStartTab = false,
|
||||
forceEnableStartTab = false, // When true, Start tab remains enabled even if trigger/user input nodes already exist.
|
||||
}: {
|
||||
@ -39,16 +42,18 @@ export const useTabs = ({
|
||||
noSnippets?: boolean
|
||||
noStart?: boolean
|
||||
defaultActiveTab?: TabsEnum
|
||||
hasUserInputNode?: boolean
|
||||
hasStartPlaceholderNode?: boolean
|
||||
disableStartTab?: boolean
|
||||
forceEnableStartTab?: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const shouldShowStartTab = !noStart
|
||||
const shouldDisableStartTab = disableStartTab || (!forceEnableStartTab && hasUserInputNode)
|
||||
const startDisabledTip = disableStartTab
|
||||
const shouldDisableStartTab = disableStartTab || (!forceEnableStartTab && hasStartPlaceholderNode)
|
||||
const startDisabledTip: ReactNode = disableStartTab
|
||||
? t('tabs.startNotSupportedTip', { ns: 'workflow' })
|
||||
: t('tabs.startDisabledTip', { ns: 'workflow' })
|
||||
: hasStartPlaceholderNode
|
||||
? t('tabs.unconfiguredStartDisabledTip', { ns: 'workflow' })
|
||||
: t('tabs.startDisabledTip', { ns: 'workflow' })
|
||||
const tabs = useMemo(() => {
|
||||
const tabConfigs = [{
|
||||
key: TabsEnum.Blocks,
|
||||
@ -68,6 +73,7 @@ export const useTabs = ({
|
||||
show: shouldShowStartTab,
|
||||
disabled: shouldDisableStartTab,
|
||||
disabledTip: shouldDisableStartTab ? startDisabledTip : undefined,
|
||||
disabledTipLinkKey: shouldDisableStartTab && !disableStartTab && hasStartPlaceholderNode ? startNodesDocsTipLinkKey : undefined,
|
||||
}, {
|
||||
key: TabsEnum.Snippets,
|
||||
name: t('tabs.snippets', { ns: 'workflow' }),
|
||||
@ -75,7 +81,7 @@ export const useTabs = ({
|
||||
}]
|
||||
|
||||
return tabConfigs.filter(tab => tab.show)
|
||||
}, [t, noBlocks, noSources, noTools, noSnippets, shouldShowStartTab, shouldDisableStartTab, startDisabledTip])
|
||||
}, [t, noBlocks, noSources, noTools, noSnippets, shouldShowStartTab, shouldDisableStartTab, startDisabledTip, disableStartTab, hasStartPlaceholderNode])
|
||||
|
||||
const getValidTabKey = useCallback((targetKey?: TabsEnum) => {
|
||||
if (!targetKey)
|
||||
|
||||
@ -18,6 +18,9 @@ const NodeSelectorWrapper = (props: NodeSelectorProps) => {
|
||||
if (block.metaData.type === BlockEnum.Start)
|
||||
return false
|
||||
|
||||
if (block.metaData.type === BlockEnum.StartPlaceholder)
|
||||
return false
|
||||
|
||||
if (block.metaData.type === BlockEnum.DataSource)
|
||||
return false
|
||||
|
||||
|
||||
@ -107,10 +107,11 @@ function NodeSelector({
|
||||
return nodes.filter(node => !ignoreSet.has(node.id))
|
||||
}, [nodes, ignoreNodeIds])
|
||||
|
||||
const { hasTriggerNode, hasUserInputNode } = useMemo(() => {
|
||||
const { hasTriggerNode, hasUserInputNode, hasStartPlaceholderNode } = useMemo(() => {
|
||||
const result = {
|
||||
hasTriggerNode: false,
|
||||
hasUserInputNode: false,
|
||||
hasStartPlaceholderNode: false,
|
||||
}
|
||||
for (const node of filteredNodes) {
|
||||
const nodeType = (node.data as CommonNodeType | undefined)?.type
|
||||
@ -118,9 +119,11 @@ function NodeSelector({
|
||||
continue
|
||||
if (nodeType === BlockEnum.Start)
|
||||
result.hasUserInputNode = true
|
||||
if (nodeType === BlockEnum.StartPlaceholder)
|
||||
result.hasStartPlaceholderNode = true
|
||||
if (isTriggerNode(nodeType))
|
||||
result.hasTriggerNode = true
|
||||
if (result.hasTriggerNode && result.hasUserInputNode)
|
||||
if (result.hasTriggerNode && result.hasUserInputNode && result.hasStartPlaceholderNode)
|
||||
break
|
||||
}
|
||||
return result
|
||||
@ -141,7 +144,7 @@ function NodeSelector({
|
||||
noSnippets: disableSnippetsTab,
|
||||
noStart: !showStartTab,
|
||||
defaultActiveTab,
|
||||
hasUserInputNode,
|
||||
hasStartPlaceholderNode,
|
||||
disableStartTab,
|
||||
forceEnableStartTab,
|
||||
})
|
||||
@ -265,6 +268,8 @@ function NodeSelector({
|
||||
activeTab={activeTab}
|
||||
blocks={blocks}
|
||||
allowStartNodeSelection={canSelectUserInput}
|
||||
hasUserInputNode={hasUserInputNode}
|
||||
hasTriggerNode={hasTriggerNode}
|
||||
onActiveTabChange={handleActiveTabChange}
|
||||
filterElem={activeTab === TabsEnum.Snippets
|
||||
? null
|
||||
|
||||
@ -95,7 +95,9 @@ describe('marketplace plugin selector components', () => {
|
||||
|
||||
expect(screen.getByText('plugin.fromMarketplace')).toBeInTheDocument()
|
||||
expect(screen.getByText('Filtered Plugin')).toBeInTheDocument()
|
||||
expect(screen.getAllByRole('link', { name: /plugin\.searchInMarketplace/i })[0]).toHaveAttribute('href', expect.stringContaining('q=filtered'))
|
||||
const marketplaceSearchLinks = screen.getAllByRole('link', { name: /plugin\.searchInMarketplace/i })
|
||||
expect(marketplaceSearchLinks).toHaveLength(1)
|
||||
expect(marketplaceSearchLinks[0]).toHaveAttribute('href', expect.stringContaining('q=filtered'))
|
||||
})
|
||||
|
||||
it('should hide the marketplace footer when requested and no filters are active', () => {
|
||||
|
||||
@ -35,7 +35,7 @@ const Item: FC<Props> = ({
|
||||
}] = useBoolean(false)
|
||||
|
||||
return (
|
||||
<div className="group/plugin flex rounded-lg py-1 pr-1 pl-3 hover:bg-state-base-hover">
|
||||
<div className="group/plugin flex rounded-lg py-1 pr-1 pl-3 select-none hover:bg-state-base-hover">
|
||||
<div
|
||||
className="relative h-6 w-6 shrink-0 rounded-md border-[0.5px] border-components-panel-border-subtle bg-contain bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: `url(${payload.icon})` }}
|
||||
@ -52,12 +52,13 @@ const Item: FC<Props> = ({
|
||||
</div>
|
||||
{/* Action */}
|
||||
<div className={cn(!open ? 'hidden' : 'flex', 'h-4 items-center space-x-1 system-xs-medium text-components-button-secondary-accent-text group-hover/plugin:flex')}>
|
||||
<div
|
||||
className="cursor-pointer rounded-md px-1.5 py-0.5 hover:bg-state-base-hover"
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer rounded-md border-0 bg-transparent px-1.5 py-0.5 hover:bg-state-base-hover"
|
||||
onClick={showInstallModal}
|
||||
>
|
||||
{t('installAction', { ns: 'plugin' })}
|
||||
</div>
|
||||
</button>
|
||||
<Action
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { Plugin, PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowRightUpLine, RiSearchLine } from '@remixicon/react'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -120,21 +120,6 @@ const List = ({
|
||||
onAction={noop}
|
||||
/>
|
||||
))}
|
||||
{hasRes && (
|
||||
<div className="mt-2 mb-3 flex items-center justify-center space-x-2">
|
||||
<div className="h-[2px] w-[90px] bg-linear-to-l from-[rgba(16,24,40,0.08)] to-[rgba(255,255,255,0.01)]"></div>
|
||||
<Link
|
||||
href={urlWithSearchText}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex h-4 shrink-0 items-center system-sm-medium text-text-accent-light-mode-only"
|
||||
>
|
||||
<RiSearchLine className="mr-0.5 size-3" />
|
||||
<span>{t('searchInMarketplace', { ns: 'plugin' })}</span>
|
||||
</Link>
|
||||
<div className="h-[2px] w-[90px] bg-linear-to-l from-[rgba(255,255,255,0.01)] to-[rgba(16,24,40,0.08)]"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import type { BlockEnum, CommonNodeType } from '../types'
|
||||
import type { TriggerDefaultValue } from './types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
createPreviewCardHandle,
|
||||
PreviewCard,
|
||||
PreviewCardContent,
|
||||
PreviewCardTrigger,
|
||||
} from '@langgenius/dify-ui/preview-card'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
@ -16,6 +18,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum as BlockEnumValues } from '../types'
|
||||
import { BlockSelectorRow } from './block-selector-row'
|
||||
// import { useNodeMetaData } from '../hooks'
|
||||
import { START_BLOCKS } from './constants'
|
||||
|
||||
@ -25,6 +28,10 @@ type StartBlocksProps = {
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
onContentStateChange?: (hasContent: boolean) => void
|
||||
hideUserInput?: boolean
|
||||
showMostCommonBadge?: boolean
|
||||
showUserInputAdded?: boolean
|
||||
showUserInputDisabled?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
type StartBlockPreviewPayload = {
|
||||
block: typeof START_BLOCKS[number]
|
||||
@ -36,6 +43,10 @@ const StartBlocks = ({
|
||||
availableBlocksTypes = [],
|
||||
onContentStateChange,
|
||||
hideUserInput = false, // Allow parent to explicitly hide Start node option (e.g. when one already exists).
|
||||
showMostCommonBadge = false,
|
||||
showUserInputAdded = false,
|
||||
showUserInputDisabled = false,
|
||||
disabled = false,
|
||||
}: StartBlocksProps) => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes()
|
||||
@ -54,8 +65,9 @@ const StartBlocks = ({
|
||||
}
|
||||
|
||||
return START_BLOCKS.filter((block) => {
|
||||
// Hide User Input (Start) if it already exists in workflow or if hideUserInput is true
|
||||
if (block.type === BlockEnumValues.Start && (hasStartNode || hideUserInput))
|
||||
// Hide User Input (Start) if it already exists in workflow or if hideUserInput is true.
|
||||
// In read-only conflict modes, keep it visible so the row can show Added or disabled tooltip state.
|
||||
if (block.type === BlockEnumValues.Start && (hasStartNode || hideUserInput) && !showUserInputAdded && !showUserInputDisabled)
|
||||
return false
|
||||
|
||||
// Filter by search text
|
||||
@ -66,7 +78,7 @@ const StartBlocks = ({
|
||||
// availableBlocksTypes now contains properly filtered entry node types from parent
|
||||
return availableBlocksTypes.includes(block.type)
|
||||
})
|
||||
}, [searchText, availableBlocksTypes, nodes, t, hideUserInput])
|
||||
}, [searchText, availableBlocksTypes, nodes, t, hideUserInput, showUserInputAdded, showUserInputDisabled])
|
||||
|
||||
const isEmpty = filteredBlocks.length === 0
|
||||
|
||||
@ -78,43 +90,84 @@ const StartBlocks = ({
|
||||
// reachable from the inspector + canvas once the row is clicked to insert
|
||||
// the start node, so hover/focus-only activation is a11y-safe. See
|
||||
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
|
||||
const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => (
|
||||
<PreviewCardTrigger
|
||||
key={block.type}
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{ block }}
|
||||
render={(
|
||||
<div
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
|
||||
onClick={() => onSelect(block.type)}
|
||||
>
|
||||
const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => {
|
||||
const isUserInput = block.type === BlockEnumValues.Start
|
||||
const isUserInputDisabled = isUserInput && showUserInputDisabled
|
||||
const isRowDisabled = disabled || (isUserInput && showUserInputAdded) || isUserInputDisabled
|
||||
const label = t(`blocks.${block.type}`, { ns: 'workflow' })
|
||||
const disabledReason = t('nodes.startPlaceholder.userInputConflictTip', { ns: 'workflow' })
|
||||
const row = (
|
||||
<BlockSelectorRow
|
||||
aria-disabled={isRowDisabled}
|
||||
aria-label={isUserInputDisabled ? `${label}. ${disabledReason}` : label}
|
||||
disabled={isRowDisabled}
|
||||
onClick={() => {
|
||||
if (isRowDisabled)
|
||||
return
|
||||
onSelect(block.type)
|
||||
}}
|
||||
>
|
||||
<div className={cn('flex min-w-0 flex-1 items-center', isUserInputDisabled && 'opacity-30')}>
|
||||
<BlockIcon
|
||||
className="mr-2 shrink-0"
|
||||
type={block.type}
|
||||
size="sm"
|
||||
/>
|
||||
<div className="flex w-0 grow items-center justify-between text-sm text-text-secondary">
|
||||
<span className="truncate">{t(`blocks.${block.type}`, { ns: 'workflow' })}</span>
|
||||
{block.type === BlockEnumValues.Start && (
|
||||
<span className="truncate system-sm-medium">{label}</span>
|
||||
{isUserInput && showUserInputAdded && (
|
||||
<span className="ml-2 shrink-0 system-xs-regular text-text-tertiary">
|
||||
{t('operation.added', { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
{isUserInput && showMostCommonBadge && !showUserInputAdded && (
|
||||
<span className="ml-2 shrink-0 rounded-[5px] border border-divider-deep px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
{t('blocks.mostCommon', { ns: 'workflow' })}
|
||||
</span>
|
||||
)}
|
||||
{isUserInput && !showMostCommonBadge && !showUserInputAdded && !showUserInputDisabled && (
|
||||
<span className="ml-2 shrink-0 system-xs-regular text-text-quaternary">{t('blocks.originalStartNode', { ns: 'workflow' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
), [onSelect, previewCardHandle, t])
|
||||
</BlockSelectorRow>
|
||||
)
|
||||
|
||||
if (isUserInputDisabled) {
|
||||
return (
|
||||
<Tooltip key={block.type}>
|
||||
<TooltipTrigger render={row} />
|
||||
<TooltipContent placement="right" sideOffset={8} className="max-w-[240px] rounded-xl border-[0.5px] border-components-panel-border bg-components-tooltip-bg px-4 py-3.5 shadow-lg">
|
||||
<p className="system-xs-regular text-text-secondary">
|
||||
{disabledReason}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PreviewCardTrigger
|
||||
key={block.type}
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{ block }}
|
||||
render={row}
|
||||
/>
|
||||
)
|
||||
}, [disabled, onSelect, previewCardHandle, showMostCommonBadge, showUserInputAdded, showUserInputDisabled, t])
|
||||
|
||||
if (isEmpty)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="p-1">
|
||||
<div className="mb-1">
|
||||
<div>
|
||||
{filteredBlocks.map((block, index) => (
|
||||
<div key={block.type}>
|
||||
{renderBlock(block)}
|
||||
{block.type === BlockEnumValues.Start && index < filteredBlocks.length - 1 && (
|
||||
{block.type === BlockEnumValues.Start && !showMostCommonBadge && index < filteredBlocks.length - 1 && (
|
||||
<div className="my-1 px-3">
|
||||
<div className="border-t border-divider-subtle" />
|
||||
</div>
|
||||
@ -147,9 +200,17 @@ function StartBlockPreviewCard({
|
||||
return null
|
||||
|
||||
const { block } = payload
|
||||
const description = block.type === BlockEnumValues.Start
|
||||
? t('nodes.start.userInputTipDescription', { ns: 'workflow' })
|
||||
: t(`blocksAbout.${block.type}`, { ns: 'workflow' })
|
||||
const showDifyTeamAuthor = [
|
||||
BlockEnumValues.Start,
|
||||
BlockEnumValues.TriggerWebhook,
|
||||
BlockEnumValues.TriggerSchedule,
|
||||
].includes(block.type)
|
||||
|
||||
return (
|
||||
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
|
||||
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 pt-3 pb-4">
|
||||
<div>
|
||||
<BlockIcon
|
||||
size="md"
|
||||
@ -157,14 +218,12 @@ function StartBlockPreviewCard({
|
||||
type={block.type}
|
||||
/>
|
||||
<div className="mb-1 system-md-medium text-text-primary">
|
||||
{block.type === BlockEnumValues.TriggerWebhook
|
||||
? t('customWebhook', { ns: 'workflow' })
|
||||
: t(`blocks.${block.type}`, { ns: 'workflow' })}
|
||||
{t(`blocks.${block.type}`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="system-xs-regular wrap-break-word text-text-secondary">
|
||||
{t(`blocksAbout.${block.type}`, { ns: 'workflow' })}
|
||||
{description}
|
||||
</div>
|
||||
{(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && (
|
||||
{showDifyTeamAuthor && (
|
||||
<div className="my-1 system-xs-regular text-text-tertiary">
|
||||
{t('author', { ns: 'tools' })}
|
||||
{' '}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||
import type { Dispatch, FC, ReactNode, SetStateAction } from 'react'
|
||||
import type {
|
||||
BlockEnum,
|
||||
NodeDefault,
|
||||
@ -10,6 +10,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools'
|
||||
@ -35,13 +36,16 @@ type TabsProps = {
|
||||
key: TabsEnum
|
||||
name: string
|
||||
disabled?: boolean
|
||||
disabledTip?: string
|
||||
disabledTip?: ReactNode
|
||||
disabledTipLinkKey?: 'startNodesDocs'
|
||||
}>
|
||||
filterElem: React.ReactNode
|
||||
noBlocks?: boolean
|
||||
noTools?: boolean
|
||||
forceShowStartContent?: boolean // Force show Start content even when noBlocks=true
|
||||
allowStartNodeSelection?: boolean // Allow user input option even when trigger node already exists (e.g. change-node flow or when no Start node yet).
|
||||
hasUserInputNode?: boolean
|
||||
hasTriggerNode?: boolean
|
||||
snippetsElem?: React.ReactNode
|
||||
}
|
||||
|
||||
@ -107,11 +111,15 @@ const TabHeaderItem = ({
|
||||
activeTab,
|
||||
onActiveTabChange,
|
||||
disabledTip,
|
||||
disabledTipLinkHref,
|
||||
disabledTipLinkLabel,
|
||||
}: {
|
||||
tab: TabsProps['tabs'][number]
|
||||
activeTab: TabsEnum
|
||||
onActiveTabChange: (activeTab: TabsEnum) => void
|
||||
disabledTip: string
|
||||
disabledTip: ReactNode
|
||||
disabledTipLinkHref?: string
|
||||
disabledTipLinkLabel?: string
|
||||
}) => {
|
||||
const className = cn(
|
||||
'relative mr-0.5 flex h-8 items-center rounded-t-lg px-3 system-sm-medium',
|
||||
@ -144,8 +152,21 @@ const TabHeaderItem = ({
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent placement="top" className="max-w-[200px]">
|
||||
{disabledTip}
|
||||
<TooltipContent placement="top" className="max-w-[230px] rounded-xl px-4 py-3.5">
|
||||
<div className="flex flex-col items-start gap-1 system-xs-regular text-text-secondary">
|
||||
<p>{disabledTip}</p>
|
||||
{disabledTipLinkHref && disabledTipLinkLabel && (
|
||||
<a
|
||||
className="text-text-accent hover:underline"
|
||||
href={disabledTipLinkHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{disabledTipLinkLabel}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
@ -179,9 +200,12 @@ const Tabs: FC<TabsProps> = ({
|
||||
noTools,
|
||||
forceShowStartContent = false,
|
||||
allowStartNodeSelection = false,
|
||||
hasUserInputNode = false,
|
||||
hasTriggerNode = false,
|
||||
snippetsElem,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
@ -234,6 +258,8 @@ const Tabs: FC<TabsProps> = ({
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={onActiveTabChange}
|
||||
disabledTip={tab.disabledTip || disabledTip}
|
||||
disabledTipLinkHref={tab.disabledTipLinkKey === 'startNodesDocs' ? docLink('/use-dify/nodes/trigger/overview') : undefined}
|
||||
disabledTipLinkLabel={tab.disabledTipLinkKey === 'startNodesDocs' ? t('tabs.startDisabledTipLearnMore', { ns: 'workflow' }) : undefined}
|
||||
/>
|
||||
))
|
||||
}
|
||||
@ -246,6 +272,8 @@ const Tabs: FC<TabsProps> = ({
|
||||
<div className="border-t border-divider-subtle">
|
||||
<AllStartBlocks
|
||||
allowUserInputSelection={allowStartNodeSelection}
|
||||
hasUserInputNode={hasUserInputNode}
|
||||
hasTriggerNode={hasTriggerNode}
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
|
||||
@ -116,6 +116,33 @@ describe('trigger plugin selector components', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('should select trigger plugin action items from the keyboard', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
const provider = createTriggerProvider()
|
||||
const event = createEvent('on_created', 'On Created')
|
||||
|
||||
render(
|
||||
<TriggerPluginActionItem
|
||||
provider={provider}
|
||||
payload={event}
|
||||
previewCardHandle={createPreviewCardHandle()}
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
const action = screen.getByRole('button', { name: 'On Created' })
|
||||
await user.tab()
|
||||
expect(action).toHaveFocus()
|
||||
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(BlockEnum.TriggerPlugin, expect.objectContaining({
|
||||
event_name: 'on_created',
|
||||
event_label: 'On Created',
|
||||
}))
|
||||
})
|
||||
|
||||
it('should expand providers and select workflow trigger providers directly', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
@ -135,6 +162,9 @@ describe('trigger plugin selector components', () => {
|
||||
)
|
||||
|
||||
await user.click(screen.getByText('Trigger Provider'))
|
||||
|
||||
expect(screen.getByLabelText('workflow.tabs.allTriggers')).toHaveClass('max-h-[240px]', 'overscroll-contain')
|
||||
|
||||
await user.click(screen.getByText('Second Event'))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(BlockEnum.TriggerPlugin, expect.objectContaining({
|
||||
@ -163,6 +193,34 @@ describe('trigger plugin selector components', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('should expand trigger providers from the keyboard', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
|
||||
render(
|
||||
<TriggerPluginItem
|
||||
payload={createTriggerProvider({
|
||||
events: [
|
||||
createEvent('first', 'First Event'),
|
||||
createEvent('second', 'Second Event'),
|
||||
],
|
||||
})}
|
||||
hasSearchText={false}
|
||||
previewCardHandle={createPreviewCardHandle()}
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
const provider = screen.getByRole('button', { name: /Trigger Provider/ })
|
||||
await user.tab()
|
||||
expect(provider).toHaveFocus()
|
||||
|
||||
await user.keyboard(' ')
|
||||
|
||||
expect(screen.getByRole('region', { name: 'workflow.tabs.allTriggers' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Second Event' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should filter trigger plugins and report whether content exists', async () => {
|
||||
const onContentStateChange = vi.fn()
|
||||
mockUseAllTriggerPlugins.mockReturnValue({
|
||||
|
||||
@ -40,9 +40,14 @@ const TriggerPluginActionItem: FC<Props> = ({
|
||||
const language = useGetLanguage()
|
||||
|
||||
const row = (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
key={payload.name}
|
||||
className="flex cursor-pointer items-center justify-between rounded-lg pr-1 pl-[21px] hover:bg-state-base-hover"
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between rounded-lg border-0 bg-transparent pr-1 pl-[21px] text-left focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden',
|
||||
disabled ? 'cursor-default' : 'cursor-pointer hover:bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (disabled)
|
||||
return
|
||||
@ -76,7 +81,7 @@ const TriggerPluginActionItem: FC<Props> = ({
|
||||
{isAdded && (
|
||||
<div className="mr-4 system-xs-regular text-text-tertiary">{t('addToolModal.added', { ns: 'tools' })}</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@ -3,6 +3,13 @@ import type { FC } from 'react'
|
||||
import type { TriggerPluginActionPreviewCardHandle } from './action-item'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
@ -14,6 +21,7 @@ import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { BlockSelectorRow } from '../block-selector-row'
|
||||
import TriggerPluginActionItem from './action-item'
|
||||
|
||||
const normalizeProviderIcon = (icon?: TriggerWithProvider['icon']) => {
|
||||
@ -30,6 +38,7 @@ type Props = Readonly<{
|
||||
hasSearchText: boolean
|
||||
previewCardHandle: TriggerPluginActionPreviewCardHandle
|
||||
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
|
||||
disabled?: boolean
|
||||
}>
|
||||
|
||||
const TriggerPluginItem: FC<Props> = ({
|
||||
@ -38,6 +47,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
hasSearchText,
|
||||
previewCardHandle,
|
||||
onSelect,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
@ -93,9 +103,13 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
ref={ref}
|
||||
>
|
||||
<div className={cn(className)}>
|
||||
<div
|
||||
className="group/item flex w-full cursor-pointer items-center justify-between rounded-lg pr-1 pl-3 select-none hover:bg-state-base-hover"
|
||||
<BlockSelectorRow
|
||||
nativeDisabled={disabled}
|
||||
disabled={disabled}
|
||||
className="group/item justify-between select-none"
|
||||
onClick={() => {
|
||||
if (disabled)
|
||||
return
|
||||
if (hasAction) {
|
||||
setIsFold(!isFold)
|
||||
return
|
||||
@ -125,13 +139,14 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className="flex h-8 grow items-center">
|
||||
<div className="flex min-w-0 grow items-center">
|
||||
<BlockIcon
|
||||
className="shrink-0"
|
||||
className="mr-2 shrink-0"
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
size="sm"
|
||||
toolIcon={providerIcon}
|
||||
/>
|
||||
<div className="ml-2 flex min-w-0 flex-1 items-center text-sm text-text-primary">
|
||||
<div className="flex min-w-0 flex-1 items-center text-sm text-text-primary">
|
||||
<span className="max-w-[200px] truncate">{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
|
||||
<span className="ml-2 truncate system-xs-regular text-text-quaternary">{groupName}</span>
|
||||
</div>
|
||||
@ -142,20 +157,33 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
<FoldIcon className={cn('size-4 shrink-0 text-text-tertiary group-hover/item:text-text-tertiary', isFold && 'text-text-quaternary')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BlockSelectorRow>
|
||||
|
||||
{!notShowProvider && hasAction && !isFold && (
|
||||
actions.map(action => (
|
||||
<TriggerPluginActionItem
|
||||
key={action.name}
|
||||
provider={providerWithResolvedIcon}
|
||||
payload={action}
|
||||
previewCardHandle={previewCardHandle}
|
||||
onSelect={onSelect}
|
||||
disabled={false}
|
||||
isAdded={false}
|
||||
/>
|
||||
))
|
||||
<ScrollAreaRoot className="relative max-h-[240px] overflow-hidden overscroll-contain">
|
||||
<ScrollAreaViewport
|
||||
aria-label={t('tabs.allTriggers', { ns: 'workflow' })}
|
||||
className="max-h-[240px] overscroll-contain"
|
||||
role="region"
|
||||
>
|
||||
<ScrollAreaContent>
|
||||
{actions.map(action => (
|
||||
<TriggerPluginActionItem
|
||||
key={action.name}
|
||||
provider={providerWithResolvedIcon}
|
||||
payload={action}
|
||||
previewCardHandle={previewCardHandle}
|
||||
onSelect={onSelect}
|
||||
disabled={disabled}
|
||||
isAdded={false}
|
||||
/>
|
||||
))}
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar className="data-[orientation=vertical]:my-1 data-[orientation=vertical]:me-1">
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -14,12 +14,14 @@ type TriggerPluginListProps = {
|
||||
searchText: string
|
||||
onContentStateChange?: (hasContent: boolean) => void
|
||||
tags?: string[]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const TriggerPluginList = ({
|
||||
onSelect,
|
||||
searchText,
|
||||
onContentStateChange,
|
||||
disabled = false,
|
||||
}: TriggerPluginListProps) => {
|
||||
const { data: triggerPluginsData } = useAllTriggerPlugins()
|
||||
const language = useGetLanguage()
|
||||
@ -99,6 +101,7 @@ const TriggerPluginList = ({
|
||||
key={plugin.id}
|
||||
payload={plugin}
|
||||
onSelect={onSelect}
|
||||
disabled={disabled}
|
||||
hasSearchText={!!searchText}
|
||||
previewCardHandle={previewCardHandle}
|
||||
/>
|
||||
|
||||
@ -11,6 +11,7 @@ vi.mock('@/context/i18n', () => ({ useGetLanguage: () => 'en' }))
|
||||
|
||||
const mockNodeTypes = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.StartPlaceholder,
|
||||
BlockEnum.End,
|
||||
BlockEnum.LLM,
|
||||
BlockEnum.Code,
|
||||
@ -56,6 +57,11 @@ describe('useAvailableBlocks', () => {
|
||||
expect(result.current.availablePrevBlocks).toEqual([])
|
||||
})
|
||||
|
||||
it('should return empty array for StartPlaceholder node', () => {
|
||||
const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.StartPlaceholder), { hooksStoreProps })
|
||||
expect(result.current.availablePrevBlocks).toEqual([])
|
||||
})
|
||||
|
||||
it('should return empty array for trigger nodes', () => {
|
||||
for (const trigger of [BlockEnum.TriggerPlugin, BlockEnum.TriggerWebhook, BlockEnum.TriggerSchedule]) {
|
||||
const { result } = renderWorkflowHook(() => useAvailableBlocks(trigger), { hooksStoreProps })
|
||||
@ -97,9 +103,15 @@ describe('useAvailableBlocks', () => {
|
||||
expect(result.current.availableNextBlocks).toEqual([])
|
||||
})
|
||||
|
||||
it('should return empty array for StartPlaceholder node', () => {
|
||||
const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.StartPlaceholder), { hooksStoreProps })
|
||||
expect(result.current.availableNextBlocks).toEqual([])
|
||||
})
|
||||
|
||||
it('should return all available nodes for regular block types', () => {
|
||||
const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps })
|
||||
expect(result.current.availableNextBlocks.length).toBeGreaterThan(0)
|
||||
expect(result.current.availableNextBlocks).not.toContain(BlockEnum.StartPlaceholder)
|
||||
})
|
||||
})
|
||||
|
||||
@ -144,6 +156,14 @@ describe('useAvailableBlocks', () => {
|
||||
expect(blocks.availablePrevBlocks).toEqual([])
|
||||
})
|
||||
|
||||
it('should return no blocks for StartPlaceholder node', () => {
|
||||
const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps })
|
||||
const blocks = result.current.getAvailableBlocks(BlockEnum.StartPlaceholder)
|
||||
|
||||
expect(blocks.availablePrevBlocks).toEqual([])
|
||||
expect(blocks.availableNextBlocks).toEqual([])
|
||||
})
|
||||
|
||||
it('should return empty nextBlocks for LoopEnd/KnowledgeBase and available nodes for End', () => {
|
||||
const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps })
|
||||
|
||||
|
||||
@ -256,6 +256,31 @@ describe('useChecklist', () => {
|
||||
expect(startRequired!.canNavigate).toBe(false)
|
||||
})
|
||||
|
||||
it('should not report the global missing start node item when a start placeholder is present', () => {
|
||||
mockNodesMap[BlockEnum.StartPlaceholder] = {
|
||||
checkValid: () => ({ errorMessage: 'workflow.nodes.startPlaceholder.validationRequired' }),
|
||||
metaData: { isStart: false, isRequired: false },
|
||||
}
|
||||
const placeholderNode = createNode({
|
||||
id: 'start-placeholder',
|
||||
data: { type: BlockEnum.StartPlaceholder, title: 'Workflow start' },
|
||||
})
|
||||
|
||||
const { result } = renderWorkflowHook(
|
||||
() => useChecklist([placeholderNode], []),
|
||||
)
|
||||
|
||||
expect(result.current.find((item: ChecklistItem) => item.id === 'start-node-required')).toBeUndefined()
|
||||
expect(result.current).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'start-placeholder',
|
||||
type: BlockEnum.StartPlaceholder,
|
||||
unConnected: false,
|
||||
errorMessages: ['workflow.nodes.startPlaceholder.validationRequired'],
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('should detect plugin not installed', () => {
|
||||
const startNode = createNode({ id: 'start', data: { type: BlockEnum.Start, title: 'Start' } })
|
||||
const toolNode = createNode({
|
||||
|
||||
@ -6,6 +6,9 @@ import { BlockEnum } from '../types'
|
||||
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||
|
||||
const availableBlocksFilter = (nodeType: BlockEnum, inContainer?: boolean) => {
|
||||
if (nodeType === BlockEnum.StartPlaceholder)
|
||||
return false
|
||||
|
||||
if (inContainer && (nodeType === BlockEnum.Iteration || nodeType === BlockEnum.Loop || nodeType === BlockEnum.End || nodeType === BlockEnum.DataSource || nodeType === BlockEnum.KnowledgeBase || nodeType === BlockEnum.HumanInput))
|
||||
return false
|
||||
|
||||
@ -21,7 +24,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
|
||||
} = useNodesMetaData()
|
||||
const availableNodesType = useMemo(() => availableNodes.map(node => node.metaData.type), [availableNodes])
|
||||
const availablePrevBlocks = useMemo(() => {
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.DataSource
|
||||
|| nodeType === BlockEnum.TriggerPlugin || nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerSchedule) {
|
||||
return []
|
||||
@ -30,7 +33,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
|
||||
return availableNodesType
|
||||
}, [availableNodesType, nodeType])
|
||||
const availableNextBlocks = useMemo(() => {
|
||||
if (!nodeType || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
|
||||
if (!nodeType || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
|
||||
return []
|
||||
|
||||
return availableNodesType
|
||||
@ -38,11 +41,11 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
|
||||
|
||||
const getAvailableBlocks = useCallback((nodeType?: BlockEnum, inContainer?: boolean) => {
|
||||
let availablePrevBlocks = availableNodesType
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource)
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.DataSource)
|
||||
availablePrevBlocks = []
|
||||
|
||||
let availableNextBlocks = availableNodesType
|
||||
if (!nodeType || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
|
||||
if (!nodeType || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
|
||||
availableNextBlocks = []
|
||||
|
||||
return {
|
||||
|
||||
@ -310,7 +310,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
}
|
||||
|
||||
const isStartNodeMeta = nodesExtraData?.[node!.data.type as BlockEnum]?.metaData.isStart ?? false
|
||||
const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true
|
||||
const isStartPlaceholderNode = node!.data.type === BlockEnum.StartPlaceholder
|
||||
const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta || isStartPlaceholderNode : true
|
||||
|
||||
const isUnconnected = !validNodes.some(n => n.id === node!.id)
|
||||
const shouldShowError = errorMessages.length > 0 || (isUnconnected && !canSkipConnectionCheck)
|
||||
@ -337,7 +338,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
// Check for start nodes (including triggers)
|
||||
if (shouldCheckStartNode) {
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
if (startNodesFiltered.length === 0) {
|
||||
const hasStartPlaceholderNode = nodes.some(node => node.data.type === BlockEnum.StartPlaceholder)
|
||||
if (startNodesFiltered.length === 0 && !hasStartPlaceholderNode) {
|
||||
list.push({
|
||||
id: 'start-node-required',
|
||||
type: BlockEnum.Start,
|
||||
|
||||
@ -12,7 +12,8 @@ const varsAppendStartNodeKeys = ['query', 'files']
|
||||
const useInspectVarsCrud = () => {
|
||||
const partOfNodesWithInspectVars = useStore(s => s.nodesWithInspectVars)
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const shouldSkipSharedVariableQueries = configsMap?.flowType === FlowType.ragPipeline || configsMap?.flowType === FlowType.snippet
|
||||
const shouldSkipSharedVariableQueries = configsMap?.flowType === FlowType.ragPipeline
|
||||
|| configsMap?.flowType === FlowType.snippet
|
||||
const variableFlowId = shouldSkipSharedVariableQueries ? '' : configsMap?.flowId
|
||||
const { data: conversationVars } = useConversationVarValues(configsMap?.flowType, variableFlowId)
|
||||
const { data: allSystemVars } = useSysVarValues(configsMap?.flowType, variableFlowId)
|
||||
|
||||
@ -8,9 +8,11 @@ import CustomNode, { Panel } from '../index'
|
||||
vi.mock('../components', () => ({
|
||||
NodeComponentMap: {
|
||||
[BlockEnum.Start]: () => <div>start-node-component</div>,
|
||||
[BlockEnum.StartPlaceholder]: () => <div>start-placeholder-node-component</div>,
|
||||
},
|
||||
PanelComponentMap: {
|
||||
[BlockEnum.Start]: () => <div>start-panel-component</div>,
|
||||
[BlockEnum.StartPlaceholder]: () => <div>start-placeholder-panel-component</div>,
|
||||
},
|
||||
}))
|
||||
|
||||
@ -32,9 +34,10 @@ vi.mock('../_base/node', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../_base/components/workflow-panel', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
vi.mock('../_base/components/workflow-panel', async () => {
|
||||
const React = await vi.importActual<typeof import('react')>('react')
|
||||
|
||||
const MockWorkflowPanel = ({
|
||||
id,
|
||||
data,
|
||||
children,
|
||||
@ -42,13 +45,23 @@ vi.mock('../_base/components/workflow-panel', () => ({
|
||||
id: string
|
||||
data: { type: BlockEnum }
|
||||
children: ReactElement
|
||||
}) => (
|
||||
<div>
|
||||
<div>{`base-panel:${id}:${data.type}`}</div>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
}) => {
|
||||
const [initialType] = React.useState(data.type)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{`base-panel:${id}:${data.type}`}</div>
|
||||
<div>{`base-panel-initial:${initialType}`}</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
default: MockWorkflowPanel,
|
||||
}
|
||||
})
|
||||
|
||||
const createNodeData = (): WorkflowNode['data'] => ({
|
||||
title: 'Start',
|
||||
@ -56,6 +69,12 @@ const createNodeData = (): WorkflowNode['data'] => ({
|
||||
type: BlockEnum.Start,
|
||||
})
|
||||
|
||||
const createStartPlaceholderData = (): WorkflowNode['data'] => ({
|
||||
title: 'Pick a start node',
|
||||
desc: '',
|
||||
type: BlockEnum.StartPlaceholder,
|
||||
})
|
||||
|
||||
const baseNodeProps = {
|
||||
type: CUSTOM_NODE,
|
||||
selected: false,
|
||||
@ -93,6 +112,29 @@ describe('workflow nodes index', () => {
|
||||
expect(screen.getByText('start-panel-component')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should remount the base panel when a node keeps its id but changes type', () => {
|
||||
const { rerender } = render(
|
||||
<Panel
|
||||
type={CUSTOM_NODE}
|
||||
id="node-1"
|
||||
data={createStartPlaceholderData()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('base-panel-initial:start-placeholder')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<Panel
|
||||
type={CUSTOM_NODE}
|
||||
id="node-1"
|
||||
data={createNodeData()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('base-panel:node-1:start')).toBeInTheDocument()
|
||||
expect(screen.getByText('base-panel-initial:start')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return null for non-custom panel types', () => {
|
||||
const { container } = render(
|
||||
<Panel
|
||||
|
||||
@ -115,7 +115,7 @@ describe('node sections', () => {
|
||||
expect(document.querySelector('.i-ri-pause-circle-fill')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render success icon when inspect vars exist without running status and hide description for loop nodes', () => {
|
||||
it('should render success icon when inspect vars exist without running status and hide description for non-description nodes', () => {
|
||||
const t = ((key: string) => key) as unknown as TFunction
|
||||
const { rerender } = render(
|
||||
<NodeHeaderMeta
|
||||
@ -131,5 +131,8 @@ describe('node sections', () => {
|
||||
|
||||
rerender(<NodeDescription data={{ type: BlockEnum.Loop, desc: 'hidden' } as never} />)
|
||||
expect(screen.queryByText('hidden')).not.toBeInTheDocument()
|
||||
|
||||
rerender(<NodeDescription data={{ type: BlockEnum.StartPlaceholder, desc: 'old placeholder description' } as never} />)
|
||||
expect(screen.queryByText('old placeholder description')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -24,6 +24,7 @@ describe('node helpers', () => {
|
||||
|
||||
it('should identify entry and container nodes', () => {
|
||||
expect(isEntryWorkflowNode(BlockEnum.Start)).toBe(true)
|
||||
expect(isEntryWorkflowNode(BlockEnum.StartPlaceholder)).toBe(true)
|
||||
expect(isEntryWorkflowNode(BlockEnum.TriggerWebhook)).toBe(true)
|
||||
expect(isEntryWorkflowNode(BlockEnum.Tool)).toBe(false)
|
||||
|
||||
|
||||
@ -91,6 +91,11 @@ import {
|
||||
} from './helpers'
|
||||
import LastRun from './last-run'
|
||||
import useLastRun from './last-run/use-last-run'
|
||||
import {
|
||||
StartPlaceholderPanelBody,
|
||||
StartPlaceholderPanelDescription,
|
||||
StartPlaceholderPanelTitle,
|
||||
} from './start-placeholder-panel'
|
||||
import { TriggerSubscription } from './trigger-subscription'
|
||||
import { TabType } from './types'
|
||||
|
||||
@ -480,6 +485,19 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
const singleRunActionLabel = isSingleRunning
|
||||
? t('debug.variableInspect.trigger.stop', { ns: 'workflow' })
|
||||
: runThisStepLabel
|
||||
const isStartPlaceholderPanel = data.type === BlockEnum.StartPlaceholder
|
||||
const panelChildren = cloneElement(children as any, {
|
||||
id,
|
||||
data,
|
||||
panelProps: {
|
||||
getInputVars,
|
||||
toVarInputs,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
runInputDataRef,
|
||||
},
|
||||
})
|
||||
|
||||
const panelTabs = (
|
||||
<TabsList>
|
||||
@ -519,16 +537,24 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
>
|
||||
<div className="sticky top-0 z-10 shrink-0 border-b-[0.5px] border-divider-regular bg-components-panel-bg">
|
||||
<div className="flex items-center px-4 pt-4 pb-1">
|
||||
<BlockIcon
|
||||
className="mr-1 shrink-0"
|
||||
type={data.type}
|
||||
toolIcon={toolIcon}
|
||||
size="md"
|
||||
/>
|
||||
<TitleInput
|
||||
value={data.title || ''}
|
||||
onBlur={handleTitleBlur}
|
||||
/>
|
||||
{!isStartPlaceholderPanel && (
|
||||
<BlockIcon
|
||||
className="mr-1 shrink-0"
|
||||
type={data.type}
|
||||
toolIcon={toolIcon}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
{isStartPlaceholderPanel
|
||||
? (
|
||||
<StartPlaceholderPanelTitle />
|
||||
)
|
||||
: (
|
||||
<TitleInput
|
||||
value={data.title || ''}
|
||||
onBlur={handleTitleBlur}
|
||||
/>
|
||||
)}
|
||||
{viewingUsers.length > 0 && (
|
||||
<div className="ml-3 shrink-0">
|
||||
<UserAvatarList
|
||||
@ -582,137 +608,147 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<DescriptionInput
|
||||
value={data.desc || ''}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
needsToolAuth && (
|
||||
<PluginAuth
|
||||
className="px-4 pb-2"
|
||||
pluginPayload={{
|
||||
provider: currToolCollection?.name || '',
|
||||
providerType: currToolCollection?.type || '',
|
||||
category: AuthCategory.tool,
|
||||
detail: currToolCollection as any,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between pr-3 pl-4">
|
||||
{panelTabs}
|
||||
<AuthorizedInNode
|
||||
{isStartPlaceholderPanel
|
||||
? (
|
||||
<StartPlaceholderPanelDescription />
|
||||
)
|
||||
: (
|
||||
<div className="p-2">
|
||||
<DescriptionInput
|
||||
value={data.desc || ''}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isStartPlaceholderPanel && (
|
||||
<>
|
||||
{
|
||||
needsToolAuth && (
|
||||
<PluginAuth
|
||||
className="px-4 pb-2"
|
||||
pluginPayload={{
|
||||
provider: currToolCollection?.name || '',
|
||||
providerType: currToolCollection?.type || '',
|
||||
category: AuthCategory.tool,
|
||||
detail: currToolCollection as any,
|
||||
}}
|
||||
onAuthorizationItemClick={handleAuthorizationItemClick}
|
||||
credentialId={data.credential_id}
|
||||
/>
|
||||
</div>
|
||||
</PluginAuth>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!currentDataSource && (
|
||||
<PluginAuthInDataSourceNode
|
||||
onJumpToDataSourcePage={handleJumpToDataSourcePage}
|
||||
isAuthorized={currentDataSource.is_authorized}
|
||||
>
|
||||
<div className="flex items-center justify-between pr-3 pl-4">
|
||||
{panelTabs}
|
||||
<AuthorizedInDataSourceNode
|
||||
>
|
||||
<div className="flex items-center justify-between pr-3 pl-4">
|
||||
{panelTabs}
|
||||
<AuthorizedInNode
|
||||
pluginPayload={{
|
||||
provider: currToolCollection?.name || '',
|
||||
providerType: currToolCollection?.type || '',
|
||||
category: AuthCategory.tool,
|
||||
detail: currToolCollection as any,
|
||||
}}
|
||||
onAuthorizationItemClick={handleAuthorizationItemClick}
|
||||
credentialId={data.credential_id}
|
||||
/>
|
||||
</div>
|
||||
</PluginAuth>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!currentDataSource && (
|
||||
<PluginAuthInDataSourceNode
|
||||
onJumpToDataSourcePage={handleJumpToDataSourcePage}
|
||||
authorizationsNum={3}
|
||||
/>
|
||||
</div>
|
||||
</PluginAuthInDataSourceNode>
|
||||
)
|
||||
}
|
||||
{
|
||||
currentTriggerPlugin && (
|
||||
<TriggerSubscription
|
||||
subscriptionIdSelected={data.subscription_id}
|
||||
onSubscriptionChange={handleSubscriptionChange}
|
||||
>
|
||||
{panelTabs}
|
||||
</TriggerSubscription>
|
||||
)
|
||||
}
|
||||
{
|
||||
!needsToolAuth && !currentDataSource && !currentTriggerPlugin && (
|
||||
<div className="flex items-center justify-between pr-3 pl-4">
|
||||
{panelTabs}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Split />
|
||||
isAuthorized={currentDataSource.is_authorized}
|
||||
>
|
||||
<div className="flex items-center justify-between pr-3 pl-4">
|
||||
{panelTabs}
|
||||
<AuthorizedInDataSourceNode
|
||||
onJumpToDataSourcePage={handleJumpToDataSourcePage}
|
||||
authorizationsNum={3}
|
||||
/>
|
||||
</div>
|
||||
</PluginAuthInDataSourceNode>
|
||||
)
|
||||
}
|
||||
{
|
||||
currentTriggerPlugin && (
|
||||
<TriggerSubscription
|
||||
subscriptionIdSelected={data.subscription_id}
|
||||
onSubscriptionChange={handleSubscriptionChange}
|
||||
>
|
||||
{panelTabs}
|
||||
</TriggerSubscription>
|
||||
)
|
||||
}
|
||||
{
|
||||
!needsToolAuth && !currentDataSource && !currentTriggerPlugin && (
|
||||
<div className="flex items-center justify-between pr-3 pl-4">
|
||||
{panelTabs}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Split />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<TabsPanel value={TabType.settings} className="flex flex-1 flex-col overflow-y-auto">
|
||||
<div>
|
||||
{cloneElement(children as any, {
|
||||
id,
|
||||
data,
|
||||
panelProps: {
|
||||
getInputVars,
|
||||
toVarInputs,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
runInputDataRef,
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
<Split />
|
||||
{
|
||||
hasRetryNode(data.type) && (
|
||||
<RetryOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasErrorHandleNode(data.type) && (
|
||||
<ErrorHandleOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!availableNextBlocks.length && (
|
||||
<div className="border-t-[0.5px] border-divider-regular p-4">
|
||||
<div className="mb-1 flex items-center system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className="mb-2 system-xs-regular text-text-tertiary">
|
||||
{t('panel.addNextStep', { ns: 'workflow' })}
|
||||
</div>
|
||||
<NextStep selectedNode={selectedNode} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{readmeEntranceComponent}
|
||||
</TabsPanel>
|
||||
|
||||
<TabsPanel value={TabType.lastRun} className="flex flex-1 flex-col">
|
||||
<LastRun
|
||||
appId={appDetail?.id || ''}
|
||||
nodeId={id}
|
||||
canSingleRun={isSupportSingleRun}
|
||||
runningStatus={runningStatus}
|
||||
isRunAfterSingleRun={isRunAfterSingleRun}
|
||||
updateNodeRunningStatus={updateNodeRunningStatus}
|
||||
onSingleRunClicked={handleSingleRun}
|
||||
nodeInfo={nodeInfo!}
|
||||
singleRunResult={runResult!}
|
||||
isPaused={isPaused}
|
||||
{...passedLogParams}
|
||||
/>
|
||||
</TabsPanel>
|
||||
{isStartPlaceholderPanel && (
|
||||
<StartPlaceholderPanelBody>
|
||||
{panelChildren}
|
||||
</StartPlaceholderPanelBody>
|
||||
)}
|
||||
|
||||
{!isStartPlaceholderPanel && (
|
||||
<TabsPanel value={TabType.settings} className="flex flex-1 flex-col overflow-y-auto">
|
||||
<div>
|
||||
{panelChildren}
|
||||
</div>
|
||||
<Split />
|
||||
{
|
||||
hasRetryNode(data.type) && (
|
||||
<RetryOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasErrorHandleNode(data.type) && (
|
||||
<ErrorHandleOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!availableNextBlocks.length && (
|
||||
<div className="border-t-[0.5px] border-divider-regular p-4">
|
||||
<div className="mb-1 flex items-center system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className="mb-2 system-xs-regular text-text-tertiary">
|
||||
{t('panel.addNextStep', { ns: 'workflow' })}
|
||||
</div>
|
||||
<NextStep selectedNode={selectedNode} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{readmeEntranceComponent}
|
||||
</TabsPanel>
|
||||
)}
|
||||
|
||||
{!isStartPlaceholderPanel && (
|
||||
<TabsPanel value={TabType.lastRun} className="flex flex-1 flex-col">
|
||||
<LastRun
|
||||
appId={appDetail?.id || ''}
|
||||
nodeId={id}
|
||||
canSingleRun={isSupportSingleRun}
|
||||
runningStatus={runningStatus}
|
||||
isRunAfterSingleRun={isRunAfterSingleRun}
|
||||
updateNodeRunningStatus={updateNodeRunningStatus}
|
||||
onSingleRunClicked={handleSingleRun}
|
||||
nodeInfo={nodeInfo!}
|
||||
singleRunResult={runResult!}
|
||||
isPaused={isPaused}
|
||||
{...passedLogParams}
|
||||
/>
|
||||
</TabsPanel>
|
||||
)}
|
||||
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@ -54,6 +54,7 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.DocExtractor]: useDocExtractorSingleRunFormParams,
|
||||
[BlockEnum.Loop]: useLoopSingleRunFormParams,
|
||||
[BlockEnum.Start]: useStartSingleRunFormParams,
|
||||
[BlockEnum.StartPlaceholder]: undefined,
|
||||
[BlockEnum.IfElse]: useIfElseSingleRunFormParams,
|
||||
[BlockEnum.VariableAggregator]: useVariableAggregatorSingleRunFormParams,
|
||||
[BlockEnum.Assigner]: useVariableAssignerSingleRunFormParams,
|
||||
@ -93,6 +94,7 @@ const getDataForCheckMoreHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.DocExtractor]: undefined,
|
||||
[BlockEnum.Loop]: undefined,
|
||||
[BlockEnum.Start]: undefined,
|
||||
[BlockEnum.StartPlaceholder]: undefined,
|
||||
[BlockEnum.IfElse]: undefined,
|
||||
[BlockEnum.VariableAggregator]: undefined,
|
||||
[BlockEnum.End]: undefined,
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function StartPlaceholderPanelTitle() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="mr-2 min-w-0 grow system-xl-semibold text-text-primary">
|
||||
{t('nodes.startPlaceholder.panelTitle', { ns: 'workflow' })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function StartPlaceholderPanelDescription() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="px-4 pb-3 system-xs-regular text-text-tertiary">
|
||||
{t('nodes.startPlaceholder.panelDescription', { ns: 'workflow' })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function StartPlaceholderPanelBody({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -83,7 +83,7 @@ export const NodeBody = ({
|
||||
}
|
||||
|
||||
export const NodeDescription = ({ data }: { data: NodeProps['data'] }) => {
|
||||
if (!data.desc || data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop)
|
||||
if (!data.desc || data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop || data.type === BlockEnum.StartPlaceholder)
|
||||
return null
|
||||
|
||||
return (
|
||||
|
||||
@ -24,7 +24,7 @@ export const getLoopIndexTextKey = (runningStatus: NodeRunningStatus | undefined
|
||||
}
|
||||
|
||||
export const isEntryWorkflowNode = (type: NodeProps['data']['type']) => {
|
||||
return isTriggerNode(type) || type === BlockEnum.Start
|
||||
return isTriggerNode(type) || type === BlockEnum.Start || type === BlockEnum.StartPlaceholder
|
||||
}
|
||||
|
||||
export const isContainerNode = (type: NodeProps['data']['type']) => {
|
||||
|
||||
@ -229,7 +229,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
!data._isCandidate && (
|
||||
data.type !== BlockEnum.StartPlaceholder && !data._isCandidate && (
|
||||
<NodeTargetHandle
|
||||
id={id}
|
||||
data={data}
|
||||
@ -239,7 +239,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && !data._isCandidate && (
|
||||
data.type !== BlockEnum.StartPlaceholder && data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && !data._isCandidate && (
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
data={data}
|
||||
@ -324,7 +324,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
</div>
|
||||
)
|
||||
|
||||
const isStartNode = data.type === BlockEnum.Start
|
||||
const isStartNode = data.type === BlockEnum.Start || data.type === BlockEnum.StartPlaceholder
|
||||
const isEntryNode = isEntryWorkflowNode(data.type)
|
||||
|
||||
return isEntryNode
|
||||
|
||||
@ -36,6 +36,8 @@ import ParameterExtractorNode from './parameter-extractor/node'
|
||||
import ParameterExtractorPanel from './parameter-extractor/panel'
|
||||
import QuestionClassifierNode from './question-classifier/node'
|
||||
import QuestionClassifierPanel from './question-classifier/panel'
|
||||
import StartPlaceholderNode from './start-placeholder/node'
|
||||
import StartPlaceholderPanel from './start-placeholder/panel'
|
||||
import StartNode from './start/node'
|
||||
import StartPanel from './start/panel'
|
||||
import TemplateTransformNode from './template-transform/node'
|
||||
@ -53,6 +55,7 @@ import VariableAssignerPanel from './variable-assigner/panel'
|
||||
|
||||
export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Start]: StartNode,
|
||||
[BlockEnum.StartPlaceholder]: StartPlaceholderNode,
|
||||
[BlockEnum.End]: EndNode,
|
||||
[BlockEnum.Answer]: AnswerNode,
|
||||
[BlockEnum.LLM]: LLMNode,
|
||||
@ -82,6 +85,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
||||
|
||||
export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Start]: StartPanel,
|
||||
[BlockEnum.StartPlaceholder]: StartPlaceholderPanel,
|
||||
[BlockEnum.End]: EndPanel,
|
||||
[BlockEnum.Answer]: AnswerPanel,
|
||||
[BlockEnum.LLM]: LLMPanel,
|
||||
|
||||
@ -46,7 +46,7 @@ export const Panel = memo((props: PanelProps) => {
|
||||
if (nodeClass === CUSTOM_NODE) {
|
||||
return (
|
||||
<BasePanel
|
||||
key={props.id}
|
||||
key={`${props.id}-${nodeData.type}`}
|
||||
id={props.id}
|
||||
data={props.data}
|
||||
>
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import Node from '../node'
|
||||
|
||||
describe('StartPlaceholderNode', () => {
|
||||
it('should show the right-panel hint while selected and the click hint after the panel closes', () => {
|
||||
const { rerender } = render(
|
||||
<Node
|
||||
id="start-placeholder"
|
||||
data={{
|
||||
type: BlockEnum.StartPlaceholder,
|
||||
selected: true,
|
||||
} as never}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('workflow.nodes.startPlaceholder.nodeDescription')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<Node
|
||||
id="start-placeholder"
|
||||
data={{
|
||||
type: BlockEnum.StartPlaceholder,
|
||||
selected: false,
|
||||
} as never}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('workflow.nodes.startPlaceholder.nodeCollapsedDescription')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,139 @@
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import Panel from '../panel'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
autoGenerateWebhookUrl: vi.fn(),
|
||||
handleSyncWorkflowDraft: vi.fn(),
|
||||
setHasSelectedStartNode: vi.fn(),
|
||||
setNodes: vi.fn(),
|
||||
setShouldAutoOpenStartNodeSelector: vi.fn(),
|
||||
}))
|
||||
|
||||
let currentNodes: Node[] = []
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
getNodes: () => currentNodes,
|
||||
setNodes: mocks.setNodes,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/marketplace/search-box', () => ({
|
||||
default: () => <input aria-label="Search trigger" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/block-selector/all-start-blocks', () => ({
|
||||
default: ({ onSelect }: { onSelect: (type: BlockEnum) => void }) => (
|
||||
<button type="button" onClick={() => onSelect(BlockEnum.Start)}>
|
||||
Select User Input
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useAutoGenerateWebhookUrl: () => mocks.autoGenerateWebhookUrl,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks-store', () => ({
|
||||
useHooksStore: (selector: (state: unknown) => unknown) => selector({
|
||||
availableNodesMetaData: {
|
||||
nodesMap: {
|
||||
[BlockEnum.Start]: {
|
||||
defaultValue: {
|
||||
title: 'User Input',
|
||||
desc: '',
|
||||
variables: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks/use-nodes-sync-draft', () => ({
|
||||
useNodesSyncDraft: () => ({
|
||||
handleSyncWorkflowDraft: mocks.handleSyncWorkflowDraft,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: (state: unknown) => unknown) => selector({
|
||||
setHasSelectedStartNode: mocks.setHasSelectedStartNode,
|
||||
setShouldAutoOpenStartNodeSelector: mocks.setShouldAutoOpenStartNodeSelector,
|
||||
}),
|
||||
}))
|
||||
|
||||
const createPlaceholderNode = (): Node => ({
|
||||
id: 'placeholder-1',
|
||||
type: 'custom',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.StartPlaceholder,
|
||||
title: 'Pick a start node',
|
||||
desc: '',
|
||||
selected: true,
|
||||
},
|
||||
})
|
||||
|
||||
describe('StartPlaceholderPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
currentNodes = [
|
||||
createPlaceholderNode(),
|
||||
{
|
||||
id: 'other-node',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.LLM,
|
||||
title: 'LLM',
|
||||
desc: '',
|
||||
selected: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
mocks.setNodes.mockImplementation((nodes: Node[]) => {
|
||||
currentNodes = nodes
|
||||
})
|
||||
})
|
||||
|
||||
describe('Start node selection', () => {
|
||||
it('should replace the placeholder with user input and auto-open the next node selector', () => {
|
||||
render(
|
||||
<Panel
|
||||
id="placeholder-1"
|
||||
data={createPlaceholderNode().data}
|
||||
panelProps={{} as never}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Select User Input' }))
|
||||
|
||||
expect(mocks.setNodes).toHaveBeenCalledTimes(1)
|
||||
expect(currentNodes[0]).toMatchObject({
|
||||
id: 'placeholder-1',
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
title: 'User Input',
|
||||
selected: true,
|
||||
variables: [],
|
||||
},
|
||||
})
|
||||
expect(currentNodes[1]?.data.selected).toBe(false)
|
||||
expect(mocks.setHasSelectedStartNode).toHaveBeenCalledWith(true)
|
||||
expect(mocks.setShouldAutoOpenStartNodeSelector).toHaveBeenCalledWith(true)
|
||||
expect(mocks.handleSyncWorkflowDraft).toHaveBeenCalledWith(true, false, expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
}))
|
||||
|
||||
const callback = mocks.handleSyncWorkflowDraft.mock.calls[0]?.[2] as { onSuccess: () => void }
|
||||
callback.onSuccess()
|
||||
|
||||
expect(mocks.autoGenerateWebhookUrl).toHaveBeenCalledWith('placeholder-1')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,29 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { StartPlaceholderNodeType } from './types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
|
||||
const metaData = genNodeMetaData({
|
||||
sort: 0.05,
|
||||
type: BlockEnum.StartPlaceholder,
|
||||
isRequired: false,
|
||||
isSingleton: true,
|
||||
isTypeFixed: true,
|
||||
helpLinkUri: 'user-input',
|
||||
})
|
||||
|
||||
const nodeDefault: NodeDefault<StartPlaceholderNodeType> = {
|
||||
metaData,
|
||||
defaultValue: {
|
||||
title: 'Workflow start',
|
||||
desc: '',
|
||||
},
|
||||
checkValid(_payload, t) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('nodes.startPlaceholder.validationRequired', { ns: 'workflow' }),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
25
web/app/components/workflow/nodes/start-placeholder/node.tsx
Normal file
25
web/app/components/workflow/nodes/start-placeholder/node.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import type { FC } from 'react'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const i18nPrefix = 'nodes.startPlaceholder'
|
||||
|
||||
const Node: FC<NodeProps> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const descriptionKey = data.selected ? 'nodeDescription' : 'nodeCollapsedDescription'
|
||||
|
||||
return (
|
||||
<div className="px-2.5 py-1">
|
||||
<div className="rounded-md bg-workflow-block-parma-bg px-1.5 py-[5px]">
|
||||
<div className="system-xs-regular wrap-break-word text-text-tertiary">
|
||||
{t(`${i18nPrefix}.${descriptionKey}`, { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
167
web/app/components/workflow/nodes/start-placeholder/panel.tsx
Normal file
167
web/app/components/workflow/nodes/start-placeholder/panel.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import type { FC } from 'react'
|
||||
import type { StartPlaceholderNodeType } from './types'
|
||||
import type {
|
||||
PluginDefaultValue,
|
||||
TriggerDefaultValue,
|
||||
} from '@/app/components/workflow/block-selector/types'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import SearchBox from '@/app/components/plugins/marketplace/search-box'
|
||||
import AllStartBlocks from '@/app/components/workflow/block-selector/all-start-blocks'
|
||||
import { useAutoGenerateWebhookUrl } from '@/app/components/workflow/hooks'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
|
||||
import { useStore as useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
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 Panel: FC<NodePanelProps<StartPlaceholderNodeType>> = ({
|
||||
id,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData)
|
||||
const setHasSelectedStartNode = useWorkflowStore(s => s.setHasSelectedStartNode)
|
||||
const setShouldAutoOpenStartNodeSelector = useWorkflowStore(s => s.setShouldAutoOpenStartNodeSelector)
|
||||
const reactFlowStore = useStoreApi()
|
||||
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
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, unknown> }).config,
|
||||
...triggerNodeData.config,
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
const { getNodes, setNodes } = reactFlowStore.getState()
|
||||
const nextNodes = getNodes().map((node) => {
|
||||
if (node.id !== id) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
selected: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...mergedNodeData,
|
||||
type: nodeType,
|
||||
selected: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
setNodes(nextNodes)
|
||||
setHasSelectedStartNode?.(true)
|
||||
setShouldAutoOpenStartNodeSelector?.(true)
|
||||
|
||||
handleSyncWorkflowDraft(true, false, {
|
||||
onSuccess: () => {
|
||||
autoGenerateWebhookUrl(id)
|
||||
},
|
||||
onError: () => {
|
||||
console.error('Failed to save start node selection to draft')
|
||||
},
|
||||
})
|
||||
}, [
|
||||
autoGenerateWebhookUrl,
|
||||
availableNodesMetaData?.nodesMap,
|
||||
handleSyncWorkflowDraft,
|
||||
id,
|
||||
reactFlowStore,
|
||||
setHasSelectedStartNode,
|
||||
setShouldAutoOpenStartNodeSelector,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="px-3 py-2">
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
placeholder={t('tabs.searchTrigger', { ns: 'workflow' })}
|
||||
inputClassName="grow"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<AllStartBlocks
|
||||
className="max-w-none min-w-0"
|
||||
searchText={searchText}
|
||||
onSelect={handleSelectStartNode}
|
||||
availableBlocksTypes={[
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]}
|
||||
tags={tags}
|
||||
allowUserInputSelection
|
||||
variant="panel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
@ -0,0 +1,3 @@
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
|
||||
export type StartPlaceholderNodeType = CommonNodeType
|
||||
@ -3,6 +3,7 @@ import { act, screen, waitFor } from '@testing-library/react'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { createNode } from '../../__tests__/fixtures'
|
||||
import { renderWorkflowFlowComponent } from '../../__tests__/workflow-test-env'
|
||||
import { TabsEnum } from '../../block-selector/types'
|
||||
import { BlockEnum } from '../../types'
|
||||
import AddBlock from '../add-block'
|
||||
|
||||
@ -20,6 +21,7 @@ type BlockSelectorMockProps = {
|
||||
popupClassName: string
|
||||
availableBlocksTypes: BlockEnum[]
|
||||
showStartTab: boolean
|
||||
defaultActiveTab?: TabsEnum
|
||||
}
|
||||
|
||||
const {
|
||||
@ -127,6 +129,7 @@ describe('AddBlock', () => {
|
||||
disabled: false,
|
||||
availableBlocksTypes: mockAvailableNextBlocks,
|
||||
showStartTab: true,
|
||||
defaultActiveTab: TabsEnum.Start,
|
||||
placement: 'right-start',
|
||||
popupClassName: 'min-w-[256px]!',
|
||||
})
|
||||
@ -151,6 +154,20 @@ describe('AddBlock', () => {
|
||||
|
||||
expect(latestBlockSelectorProps?.showStartTab).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerWebhook,
|
||||
])('should keep the normal default tab when a %s node already exists', async (type) => {
|
||||
renderWithReactFlow([
|
||||
createNode({ id: 'entry-node', position: { x: 0, y: 0 }, data: { type } }),
|
||||
])
|
||||
|
||||
await waitFor(() => expect(latestBlockSelectorProps).not.toBeNull())
|
||||
|
||||
expect(latestBlockSelectorProps?.showStartTab).toBe(true)
|
||||
expect(latestBlockSelectorProps?.defaultActiveTab).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// User interactions that bridge selector state and workflow state.
|
||||
|
||||
@ -10,12 +10,17 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import {
|
||||
useNodes,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import {
|
||||
BlockEnum,
|
||||
isTriggerNode,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { TabsEnum } from '../block-selector/types'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useIsChatMode,
|
||||
@ -50,10 +55,18 @@ const AddBlock = ({
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handlePaneContextmenuCancel } = usePanelInteractions()
|
||||
const [open, setOpen] = useState(false)
|
||||
const nodes = useNodes()
|
||||
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false)
|
||||
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||
const flowType = useHooksStore(s => s.configsMap?.flowType)
|
||||
const showStartTab = flowType !== FlowType.ragPipeline && !isChatMode
|
||||
const hasEntryNode = nodes.some((node) => {
|
||||
const nodeData = node.data as { type?: BlockEnum }
|
||||
const nodeType = nodeData.type
|
||||
return nodeType === BlockEnum.Start || (nodeType ? isTriggerNode(nodeType) : false)
|
||||
})
|
||||
|
||||
const defaultActiveTab = showStartTab && !hasEntryNode ? TabsEnum.Start : undefined
|
||||
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open)
|
||||
@ -121,6 +134,7 @@ const AddBlock = ({
|
||||
popupClassName="min-w-[256px]!"
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
showStartTab={showStartTab}
|
||||
defaultActiveTab={defaultActiveTab}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ import type {
|
||||
|
||||
export enum BlockEnum {
|
||||
Start = 'start',
|
||||
StartPlaceholder = 'start-placeholder',
|
||||
End = 'end',
|
||||
Answer = 'answer',
|
||||
LLM = 'llm',
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "حلقة",
|
||||
"blocks.loop-end": "خروج من الحلقة",
|
||||
"blocks.loop-start": "بداية الحلقة",
|
||||
"blocks.mostCommon": "الأكثر شيوعًا",
|
||||
"blocks.originalStartNode": "عقدة البداية الأصلية",
|
||||
"blocks.parameter-extractor": "مستخرج المعلمات",
|
||||
"blocks.question-classifier": "مصنف الأسئلة",
|
||||
"blocks.start": "إدخال المستخدم",
|
||||
"blocks.start-placeholder": "بداية workflow",
|
||||
"blocks.template-transform": "قالب",
|
||||
"blocks.tool": "أداة",
|
||||
"blocks.trigger-plugin": "مشغل الإضافة",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "استخدم LLM لاستخراج المعلمات الهيكلية من اللغة الطبيعية لاستدعاء الأدوات أو طلبات HTTP.",
|
||||
"blocksAbout.question-classifier": "تحديد شروط تصنيف أسئلة المستخدم، يمكن لـ LLM تحديد كيفية تقدم المحادثة بناءً على وصف التصنيف",
|
||||
"blocksAbout.start": "تحديد المعلمات الأولية لبدء سير العمل",
|
||||
"blocksAbout.start-placeholder": "اختر كيف يبدأ هذا workflow",
|
||||
"blocksAbout.template-transform": "تحويل البيانات إلى سلسلة باستخدام بنية قالب Jinja",
|
||||
"blocksAbout.tool": "استخدم الأدوات الخارجية لتوسيع قدرات سير العمل",
|
||||
"blocksAbout.trigger-plugin": "مشغل تكامل تابع لجهة خارجية يبدأ سير العمل من أحداث النظام الأساسي الخارجي",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "نوع الرسالة",
|
||||
"nodes.start.outputVars.query": "إدخال المستخدم",
|
||||
"nodes.start.required": "مطلوب",
|
||||
"nodes.start.userInputTipDescription": "حدّد المدخلات التي سيتم جمعها من المستخدمين النهائيين عند بدء workflow عند الطلب.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "تصفّح المزيد في Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "ابحث عن المزيد من الأدوات في Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "لم يتم العثور على أي مشغلات",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "انقر لتكوين عقدة البداية",
|
||||
"nodes.startPlaceholder.nodeDescription": "اختر عقدة بداية من اللوحة اليمنى",
|
||||
"nodes.startPlaceholder.nodeTitle": "بداية workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "تحدد عقدة البداية ما الذي يشغّل workflow لديك",
|
||||
"nodes.startPlaceholder.panelTitle": "اختر عقدة بداية",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "لا يمكن دمج إدخال المستخدم مع مشغلات أخرى",
|
||||
"nodes.startPlaceholder.validationRequired": "اختر عقدة بداية أولًا.",
|
||||
"nodes.templateTransform.code": "الكود",
|
||||
"nodes.templateTransform.codeSupportTip": "يدعم Jinja2 فقط",
|
||||
"nodes.templateTransform.inputVars": "متغيرات الإدخال",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "المصادر",
|
||||
"tabs.start": "البداية",
|
||||
"tabs.startDisabledTip": "تتعارض عقدة المشغل وعقدة إدخال المستخدم.",
|
||||
"tabs.startDisabledTipLearnMore": "تعرّف على المزيد حول عقد البداية",
|
||||
"tabs.startNotSupportedTip": "علامة التبويب \"ابدأ\" غير مدعومة في المقتطفات.",
|
||||
"tabs.tools": "الأدوات",
|
||||
"tabs.transform": "تحويل",
|
||||
"tabs.unconfiguredStartDisabledTip": "تمت إضافة عقدة بداية غير مكوّنة إلى اللوحة. أكمل الإعداد قبل المتابعة.",
|
||||
"tabs.usePlugin": "حدد الأداة",
|
||||
"tabs.utilities": "الأدوات المساعدة",
|
||||
"tabs.workflowTool": "سير العمل",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Schleife",
|
||||
"blocks.loop-end": "Schleife beenden",
|
||||
"blocks.loop-start": "Schleifenbeginn",
|
||||
"blocks.mostCommon": "Am häufigsten",
|
||||
"blocks.originalStartNode": "ursprünglicher Startknoten",
|
||||
"blocks.parameter-extractor": "Parameter-Extraktor",
|
||||
"blocks.question-classifier": "Fragenklassifizierer",
|
||||
"blocks.start": "Start",
|
||||
"blocks.start-placeholder": "Workflow-Start",
|
||||
"blocks.template-transform": "Vorlage",
|
||||
"blocks.tool": "Werkzeug",
|
||||
"blocks.trigger-plugin": "Plugin-Auslöser",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Verwenden Sie LLM, um strukturierte Parameter aus natürlicher Sprache für Werkzeugaufrufe oder HTTP-Anfragen zu extrahieren.",
|
||||
"blocksAbout.question-classifier": "Definieren Sie die Klassifizierungsbedingungen von Benutzerfragen, LLM kann basierend auf der Klassifikationsbeschreibung festlegen, wie die Konversation fortschreitet",
|
||||
"blocksAbout.start": "Definieren Sie die Anfangsparameter zum Starten eines Workflows",
|
||||
"blocksAbout.start-placeholder": "Wählen Sie aus, wie dieser Workflow startet",
|
||||
"blocksAbout.template-transform": "Daten in Zeichenfolgen mit Jinja-Vorlagensyntax umwandeln",
|
||||
"blocksAbout.tool": "Verwenden Sie externe Tools, um die Workflow-Funktionen zu erweitern",
|
||||
"blocksAbout.trigger-plugin": "Auslöser für die Integration von Drittanbietern, der Workflows anhand von Ereignissen externer Plattformen startet",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "Nachrichtentyp",
|
||||
"nodes.start.outputVars.query": "Benutzereingabe",
|
||||
"nodes.start.required": "erforderlich",
|
||||
"nodes.start.userInputTipDescription": "Definieren Sie Eingaben, die von Endbenutzern erfasst werden, wenn Ihr Workflow bei Bedarf gestartet wird.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Mehr im Marketplace durchsuchen",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Weitere Tools im Marketplace finden",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Keine Trigger gefunden",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Klicken, um den Startknoten zu konfigurieren",
|
||||
"nodes.startPlaceholder.nodeDescription": "Wählen Sie im rechten Bereich einen Startknoten aus",
|
||||
"nodes.startPlaceholder.nodeTitle": "Workflow-Start",
|
||||
"nodes.startPlaceholder.panelDescription": "Der Startknoten legt fest, wodurch Ihr Workflow ausgeführt wird",
|
||||
"nodes.startPlaceholder.panelTitle": "Startknoten auswählen",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Benutzereingabe kann nicht mit anderen Triggern kombiniert werden",
|
||||
"nodes.startPlaceholder.validationRequired": "Wählen Sie zuerst einen Startknoten aus.",
|
||||
"nodes.templateTransform.code": "Code",
|
||||
"nodes.templateTransform.codeSupportTip": "Unterstützt nur Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Eingabevariablen",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Quellen",
|
||||
"tabs.start": "Start",
|
||||
"tabs.startDisabledTip": "Trigger-Knoten und Benutzereingabeknoten schließen sich gegenseitig aus.",
|
||||
"tabs.startDisabledTipLearnMore": "Mehr über Startknoten erfahren",
|
||||
"tabs.startNotSupportedTip": "Die Registerkarte „Start“ wird in Snippets nicht unterstützt.",
|
||||
"tabs.tools": "Werkzeuge",
|
||||
"tabs.transform": "Transformieren",
|
||||
"tabs.unconfiguredStartDisabledTip": "Ein nicht konfigurierter Startknoten wurde zur Arbeitsfläche hinzugefügt. Schließen Sie die Einrichtung ab, bevor Sie fortfahren.",
|
||||
"tabs.usePlugin": "Werkzeug auswählen",
|
||||
"tabs.utilities": "Dienstprogramme",
|
||||
"tabs.workflowTool": "Arbeitsablauf",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Loop",
|
||||
"blocks.loop-end": "Exit Loop",
|
||||
"blocks.loop-start": "Loop Start",
|
||||
"blocks.mostCommon": "Most common",
|
||||
"blocks.originalStartNode": "original start node",
|
||||
"blocks.parameter-extractor": "Parameter Extractor",
|
||||
"blocks.question-classifier": "Question Classifier",
|
||||
"blocks.start": "User Input",
|
||||
"blocks.start-placeholder": "Workflow start",
|
||||
"blocks.template-transform": "Template",
|
||||
"blocks.tool": "Tool",
|
||||
"blocks.trigger-plugin": "Plugin Trigger",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Use LLM to extract structured parameters from natural language for tool invocations or HTTP requests.",
|
||||
"blocksAbout.question-classifier": "Define the classification conditions of user questions, LLM can define how the conversation progresses based on the classification description",
|
||||
"blocksAbout.start": "Define the initial parameters for launching a workflow",
|
||||
"blocksAbout.start-placeholder": "Choose how this workflow starts",
|
||||
"blocksAbout.template-transform": "Convert data to string using Jinja template syntax",
|
||||
"blocksAbout.tool": "Use external tools to extend workflow capabilities",
|
||||
"blocksAbout.trigger-plugin": "Third-party integration trigger that starts workflows from external platform events",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "message type",
|
||||
"nodes.start.outputVars.query": "User input",
|
||||
"nodes.start.required": "required",
|
||||
"nodes.start.userInputTipDescription": "Define inputs to collect from end users when your workflow starts on demand.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Browse more in Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Find more tools in Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "No triggers were found",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Click to configure the start node",
|
||||
"nodes.startPlaceholder.nodeDescription": "Pick a start node from the right panel",
|
||||
"nodes.startPlaceholder.nodeTitle": "Workflow start",
|
||||
"nodes.startPlaceholder.panelDescription": "The start node defines what triggers your workflow to run",
|
||||
"nodes.startPlaceholder.panelTitle": "Pick a start node",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "User Input cannot be combined with other triggers",
|
||||
"nodes.startPlaceholder.validationRequired": "Choose a start node first.",
|
||||
"nodes.templateTransform.code": "Code",
|
||||
"nodes.templateTransform.codeSupportTip": "Only supports Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Input Variables",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Sources",
|
||||
"tabs.start": "Start",
|
||||
"tabs.startDisabledTip": "Trigger node and user input node are mutually exclusive.",
|
||||
"tabs.startDisabledTipLearnMore": "Learn more about start nodes",
|
||||
"tabs.startNotSupportedTip": "The Start tab is not supported in snippets.",
|
||||
"tabs.tools": "Tools",
|
||||
"tabs.transform": "Transform",
|
||||
"tabs.unconfiguredStartDisabledTip": "An unconfigured start node has been added to canvas. Please complete the setup before continuing.",
|
||||
"tabs.usePlugin": "Select tool",
|
||||
"tabs.utilities": "Utilities",
|
||||
"tabs.workflowTool": "Workflow",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Bucle",
|
||||
"blocks.loop-end": "Salir del bucle",
|
||||
"blocks.loop-start": "Inicio del bucle",
|
||||
"blocks.mostCommon": "Más común",
|
||||
"blocks.originalStartNode": "nodo inicial original",
|
||||
"blocks.parameter-extractor": "Extractor de parámetros",
|
||||
"blocks.question-classifier": "Clasificador de preguntas",
|
||||
"blocks.start": "Inicio",
|
||||
"blocks.start-placeholder": "Inicio del workflow",
|
||||
"blocks.template-transform": "Plantilla",
|
||||
"blocks.tool": "Herramienta",
|
||||
"blocks.trigger-plugin": "Disparador de complemento",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Utiliza LLM para extraer parámetros estructurados del lenguaje natural para invocaciones de herramientas o solicitudes HTTP.",
|
||||
"blocksAbout.question-classifier": "Define las condiciones de clasificación de las preguntas de los usuarios, LLM puede definir cómo progresa la conversación en función de la descripción de clasificación",
|
||||
"blocksAbout.start": "Define los parámetros iniciales para iniciar un flujo de trabajo",
|
||||
"blocksAbout.start-placeholder": "Elige cómo empieza este workflow",
|
||||
"blocksAbout.template-transform": "Convierte datos en una cadena utilizando la sintaxis de plantillas Jinja",
|
||||
"blocksAbout.tool": "Utiliza herramientas externas para ampliar las capacidades del flujo de trabajo",
|
||||
"blocksAbout.trigger-plugin": "Disparador de integración de terceros que inicia flujos de trabajo a partir de eventos de plataformas externas",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "tipo de mensaje",
|
||||
"nodes.start.outputVars.query": "Entrada del usuario",
|
||||
"nodes.start.required": "requerido",
|
||||
"nodes.start.userInputTipDescription": "Define las entradas que se recopilarán de los usuarios finales cuando tu workflow se inicie bajo demanda.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Explorar más en Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Encontrar más herramientas en Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "No se encontraron disparadores",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Haz clic para configurar el nodo de inicio",
|
||||
"nodes.startPlaceholder.nodeDescription": "Elige un nodo de inicio en el panel derecho",
|
||||
"nodes.startPlaceholder.nodeTitle": "Inicio del workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "El nodo de inicio define qué activa la ejecución de tu workflow",
|
||||
"nodes.startPlaceholder.panelTitle": "Elige un nodo de inicio",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "La entrada de usuario no se puede combinar con otros disparadores",
|
||||
"nodes.startPlaceholder.validationRequired": "Elige primero un nodo de inicio.",
|
||||
"nodes.templateTransform.code": "Código",
|
||||
"nodes.templateTransform.codeSupportTip": "Solo admite Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Variables de entrada",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Fuentes",
|
||||
"tabs.start": "Iniciar",
|
||||
"tabs.startDisabledTip": "El nodo activador y el nodo de entrada del usuario son mutuamente excluyentes.",
|
||||
"tabs.startDisabledTipLearnMore": "Más información sobre los nodos de inicio",
|
||||
"tabs.startNotSupportedTip": "La pestaña Inicio no se admite en fragmentos.",
|
||||
"tabs.tools": "Herramientas",
|
||||
"tabs.transform": "Transformar",
|
||||
"tabs.unconfiguredStartDisabledTip": "Se ha añadido al lienzo un nodo de inicio sin configurar. Completa la configuración antes de continuar.",
|
||||
"tabs.usePlugin": "Seleccionar herramienta",
|
||||
"tabs.utilities": "Utilidades",
|
||||
"tabs.workflowTool": "Flujo de trabajo",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "حلقه",
|
||||
"blocks.loop-end": "خروج از حلقه",
|
||||
"blocks.loop-start": "شروع حلقه",
|
||||
"blocks.mostCommon": "رایجترین",
|
||||
"blocks.originalStartNode": "گره شروع اصلی",
|
||||
"blocks.parameter-extractor": "استخراجکننده پارامتر",
|
||||
"blocks.question-classifier": "دستهبندیکننده سؤال",
|
||||
"blocks.start": "شروع",
|
||||
"blocks.start-placeholder": "شروع workflow",
|
||||
"blocks.template-transform": "مبدل الگو",
|
||||
"blocks.tool": "ابزار",
|
||||
"blocks.trigger-plugin": "راهانداز پلاگین",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "استخراج پارامترهای ساختاریافته از زبان طبیعی توسط مدل زبانی بزرگ برای فراخوانی ابزارها یا درخواستهای HTTP",
|
||||
"blocksAbout.question-classifier": "تعریف شرایط دستهبندی سؤالات کاربر؛ مدل زبانی بزرگ بر اساس توضیحات دستهبندی، مسیر مکالمه را تعیین میکند",
|
||||
"blocksAbout.start": "تعریف پارامترهای اولیه برای آغاز گردش کار",
|
||||
"blocksAbout.start-placeholder": "نحوه شروع این workflow را انتخاب کنید",
|
||||
"blocksAbout.template-transform": "تبدیل دادهها به رشته با نحو الگوی Jinja",
|
||||
"blocksAbout.tool": "استفاده از ابزارهای خارجی برای گسترش قابلیتهای گردش کار",
|
||||
"blocksAbout.trigger-plugin": "یکپارچهسازی با سرویسهای ثالث برای آغاز گردش کار از رویدادهای پلتفرم خارجی",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "نوع پیام",
|
||||
"nodes.start.outputVars.query": "ورودی کاربر",
|
||||
"nodes.start.required": "الزامی",
|
||||
"nodes.start.userInputTipDescription": "ورودیهایی را تعریف کنید که هنگام شروع workflow بهصورت درخواستی از کاربران نهایی جمعآوری میشوند.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "موارد بیشتری را در Marketplace مرور کنید",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "ابزارهای بیشتری را در Marketplace پیدا کنید",
|
||||
"nodes.startPlaceholder.noTriggersFound": "هیچ تریگری یافت نشد",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "برای پیکربندی گره شروع کلیک کنید",
|
||||
"nodes.startPlaceholder.nodeDescription": "یک گره شروع را از پنل سمت راست انتخاب کنید",
|
||||
"nodes.startPlaceholder.nodeTitle": "شروع workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "گره شروع مشخص میکند چه چیزی اجرای workflow شما را فعال میکند",
|
||||
"nodes.startPlaceholder.panelTitle": "یک گره شروع انتخاب کنید",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "ورودی کاربر نمیتواند با تریگرهای دیگر ترکیب شود",
|
||||
"nodes.startPlaceholder.validationRequired": "ابتدا یک گره شروع انتخاب کنید.",
|
||||
"nodes.templateTransform.code": "کد",
|
||||
"nodes.templateTransform.codeSupportTip": "فقط از Jinja2 پشتیبانی میشود",
|
||||
"nodes.templateTransform.inputVars": "متغیرهای ورودی",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "منابع",
|
||||
"tabs.start": "شروع",
|
||||
"tabs.startDisabledTip": "گره تریگر و گره ورودی کاربر نمیتوانند همزمان فعال باشند.",
|
||||
"tabs.startDisabledTipLearnMore": "درباره گرههای شروع بیشتر بدانید",
|
||||
"tabs.startNotSupportedTip": "تب Start در قطعهها پشتیبانی نمیشود.",
|
||||
"tabs.tools": "ابزارها",
|
||||
"tabs.transform": "تبدیل",
|
||||
"tabs.unconfiguredStartDisabledTip": "یک گره شروع پیکربندینشده به بوم اضافه شده است. پیش از ادامه، تنظیمات را کامل کنید.",
|
||||
"tabs.usePlugin": "انتخاب ابزار",
|
||||
"tabs.utilities": "ابزارهای کاربردی",
|
||||
"tabs.workflowTool": "گردش کار",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Boucle",
|
||||
"blocks.loop-end": "Sortir de la boucle",
|
||||
"blocks.loop-start": "Début de boucle",
|
||||
"blocks.mostCommon": "Le plus courant",
|
||||
"blocks.originalStartNode": "nœud de départ original",
|
||||
"blocks.parameter-extractor": "Extracteur de paramètres",
|
||||
"blocks.question-classifier": "Classificateur de questions",
|
||||
"blocks.start": "Début",
|
||||
"blocks.start-placeholder": "Début du workflow",
|
||||
"blocks.template-transform": "Modèle",
|
||||
"blocks.tool": "Outil",
|
||||
"blocks.trigger-plugin": "Déclencheur de plugin",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Utiliser LLM pour extraire des paramètres structurés du langage naturel pour les invocations d'outils ou les requêtes HTTP.",
|
||||
"blocksAbout.question-classifier": "Définir les conditions de classification des questions des utilisateurs, LLM peut définir comment la conversation progresse en fonction de la description de la classification",
|
||||
"blocksAbout.start": "Définir les paramètres initiaux pour lancer un flux de travail",
|
||||
"blocksAbout.start-placeholder": "Choisissez comment ce workflow démarre",
|
||||
"blocksAbout.template-transform": "Convertir les données en chaîne en utilisant la syntaxe du template Jinja",
|
||||
"blocksAbout.tool": "Utilisez des outils externes pour étendre les capacités du flux de travail",
|
||||
"blocksAbout.trigger-plugin": "Déclencheur d’intégration tierce qui démarre des flux de travail à partir d’événements d’une plateforme externe",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "type de message",
|
||||
"nodes.start.outputVars.query": "Saisie utilisateur",
|
||||
"nodes.start.required": "requis",
|
||||
"nodes.start.userInputTipDescription": "Définissez les entrées à collecter auprès des utilisateurs finaux lorsque votre workflow démarre à la demande.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Parcourir plus sur le Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Trouver plus d’outils sur le Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Aucun déclencheur trouvé",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Cliquez pour configurer le nœud de départ",
|
||||
"nodes.startPlaceholder.nodeDescription": "Choisissez un nœud de départ dans le panneau de droite",
|
||||
"nodes.startPlaceholder.nodeTitle": "Début du workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "Le nœud de départ définit ce qui déclenche l’exécution de votre workflow",
|
||||
"nodes.startPlaceholder.panelTitle": "Choisissez un nœud de départ",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "L’entrée utilisateur ne peut pas être combinée avec d’autres déclencheurs",
|
||||
"nodes.startPlaceholder.validationRequired": "Choisissez d’abord un nœud de départ.",
|
||||
"nodes.templateTransform.code": "Code",
|
||||
"nodes.templateTransform.codeSupportTip": "Prend en charge uniquement Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Variables de saisie",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Sources",
|
||||
"tabs.start": "Démarrer",
|
||||
"tabs.startDisabledTip": "Le nœud de déclenchement et le nœud d'entrée utilisateur sont mutuellement exclusifs.",
|
||||
"tabs.startDisabledTipLearnMore": "En savoir plus sur les nœuds de départ",
|
||||
"tabs.startNotSupportedTip": "L'onglet Démarrer n'est pas pris en charge dans les extraits de code.",
|
||||
"tabs.tools": "Outils",
|
||||
"tabs.transform": "Transformer",
|
||||
"tabs.unconfiguredStartDisabledTip": "Un nœud de départ non configuré a été ajouté au canevas. Terminez la configuration avant de continuer.",
|
||||
"tabs.usePlugin": "Sélectionner l'outil",
|
||||
"tabs.utilities": "Utilitaires",
|
||||
"tabs.workflowTool": "Flux de travail",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "लूप",
|
||||
"blocks.loop-end": "लूप से बाहर निकलें",
|
||||
"blocks.loop-start": "लूप प्रारंभ",
|
||||
"blocks.mostCommon": "सबसे आम",
|
||||
"blocks.originalStartNode": "मूल प्रारंभ नोड",
|
||||
"blocks.parameter-extractor": "पैरामीटर निष्कर्षक",
|
||||
"blocks.question-classifier": "प्रश्न वर्गीकरण",
|
||||
"blocks.start": "प्रारंभ",
|
||||
"blocks.start-placeholder": "workflow प्रारंभ",
|
||||
"blocks.template-transform": "टेम्पलेट",
|
||||
"blocks.tool": "उपकरण",
|
||||
"blocks.trigger-plugin": "प्लगइन ट्रिगर",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "टूल आमंत्रणों या HTTP अनुरोधों के लिए प्राकृतिक भाषा से संरचित पैरामीटर निकालने के लिए LLM का उपयोग करें।",
|
||||
"blocksAbout.question-classifier": "उपयोगकर्ता प्रश्नों की वर्गीकरण शर्तों को परिभाषित करें, LLM वर्गीकरण विवरण के आधार पर संवाद कैसे आगे बढ़ता है, इसे परिभाषित कर सकता है",
|
||||
"blocksAbout.start": "वर्कफ़्लो लॉन्च करने के लिए प्रारंभिक पैरामीटर को परिभाषित करें",
|
||||
"blocksAbout.start-placeholder": "चुनें कि यह workflow कैसे शुरू होता है",
|
||||
"blocksAbout.template-transform": "Jinja टेम्पलेट सिंटैक्स का उपयोग करके डेटा को स्ट्रिंग में परिवर्तित करें",
|
||||
"blocksAbout.tool": "कार्यप्रवाह क्षमताओं को बढ़ाने के लिए बाहरी उपकरणों का उपयोग करें",
|
||||
"blocksAbout.trigger-plugin": "थर्ड-पार्टी इंटीग्रेशन ट्रिगर जो बाहरी प्लेटफ़ॉर्म घटनाओं से वर्कफ़्लो शुरू करता है",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "संदेश प्रकार",
|
||||
"nodes.start.outputVars.query": "यूजर इनपुट",
|
||||
"nodes.start.required": "आवश्यक",
|
||||
"nodes.start.userInputTipDescription": "जब आपका workflow मांग पर शुरू होता है, तब अंतिम उपयोगकर्ताओं से एकत्र किए जाने वाले इनपुट परिभाषित करें।",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace में और ब्राउज़ करें",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace में और टूल खोजें",
|
||||
"nodes.startPlaceholder.noTriggersFound": "कोई ट्रिगर नहीं मिला",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "प्रारंभ नोड कॉन्फ़िगर करने के लिए क्लिक करें",
|
||||
"nodes.startPlaceholder.nodeDescription": "दाएँ पैनल से एक प्रारंभ नोड चुनें",
|
||||
"nodes.startPlaceholder.nodeTitle": "workflow प्रारंभ",
|
||||
"nodes.startPlaceholder.panelDescription": "प्रारंभ नोड यह परिभाषित करता है कि आपका workflow किससे चलेगा",
|
||||
"nodes.startPlaceholder.panelTitle": "एक प्रारंभ नोड चुनें",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "उपयोगकर्ता इनपुट को अन्य ट्रिगर के साथ संयोजित नहीं किया जा सकता",
|
||||
"nodes.startPlaceholder.validationRequired": "पहले एक प्रारंभ नोड चुनें।",
|
||||
"nodes.templateTransform.code": "कोड",
|
||||
"nodes.templateTransform.codeSupportTip": "केवल Jinja2 का समर्थन करता है",
|
||||
"nodes.templateTransform.inputVars": "इनपुट वेरिएबल्स",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "स्रोत",
|
||||
"tabs.start": "शुरू करें",
|
||||
"tabs.startDisabledTip": "ट्रिगर नोड और उपयोगकर्ता इनपुट नोड परस्पर विशेष हैं।",
|
||||
"tabs.startDisabledTipLearnMore": "प्रारंभ नोड्स के बारे में और जानें",
|
||||
"tabs.startNotSupportedTip": "स्निपेट्स में स्टार्ट टैब समर्थित नहीं है।",
|
||||
"tabs.tools": "टूल्स",
|
||||
"tabs.transform": "परिवर्तन",
|
||||
"tabs.unconfiguredStartDisabledTip": "कैनवास में एक असंरचित प्रारंभ नोड जोड़ा गया है। जारी रखने से पहले सेटअप पूरा करें।",
|
||||
"tabs.usePlugin": "उपकरण चुनें",
|
||||
"tabs.utilities": "उपयोगिताएं",
|
||||
"tabs.workflowTool": "कार्यप्रवाह",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Perulangan",
|
||||
"blocks.loop-end": "Keluar Loop",
|
||||
"blocks.loop-start": "Mulai Loop",
|
||||
"blocks.mostCommon": "Paling umum",
|
||||
"blocks.originalStartNode": "node awal asli",
|
||||
"blocks.parameter-extractor": "Ekstraktor Parameter",
|
||||
"blocks.question-classifier": "Pengklasifikasi Pertanyaan",
|
||||
"blocks.start": "Mulai",
|
||||
"blocks.start-placeholder": "Awal workflow",
|
||||
"blocks.template-transform": "Templat",
|
||||
"blocks.tool": "Alat",
|
||||
"blocks.trigger-plugin": "Pemicu Plugin",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Gunakan LLM untuk mengekstrak parameter terstruktur dari bahasa alami untuk pemanggilan alat atau permintaan HTTP.",
|
||||
"blocksAbout.question-classifier": "Tentukan kondisi klasifikasi pertanyaan pengguna, LLM dapat menentukan bagaimana percakapan berlangsung berdasarkan deskripsi klasifikasi",
|
||||
"blocksAbout.start": "Menentukan parameter awal untuk meluncurkan alur kerja",
|
||||
"blocksAbout.start-placeholder": "Pilih bagaimana workflow ini dimulai",
|
||||
"blocksAbout.template-transform": "Mengonversi data menjadi string menggunakan sintaks templat Jinja",
|
||||
"blocksAbout.tool": "Gunakan alat eksternal untuk memperluas kemampuan alur kerja",
|
||||
"blocksAbout.trigger-plugin": "Pemicu integrasi pihak ketiga yang memulai alur kerja dari kejadian platform eksternal",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "Jenis pesan",
|
||||
"nodes.start.outputVars.query": "Masukan pengguna",
|
||||
"nodes.start.required": "Diperlukan",
|
||||
"nodes.start.userInputTipDescription": "Tentukan input yang akan dikumpulkan dari pengguna akhir saat workflow Anda dimulai sesuai permintaan.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Jelajahi lebih banyak di Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Temukan lebih banyak alat di Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Tidak ada pemicu yang ditemukan",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Klik untuk mengonfigurasi node awal",
|
||||
"nodes.startPlaceholder.nodeDescription": "Pilih node awal dari panel kanan",
|
||||
"nodes.startPlaceholder.nodeTitle": "Awal workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "Node awal menentukan apa yang memicu workflow Anda berjalan",
|
||||
"nodes.startPlaceholder.panelTitle": "Pilih node awal",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Input pengguna tidak dapat digabungkan dengan pemicu lain",
|
||||
"nodes.startPlaceholder.validationRequired": "Pilih node awal terlebih dahulu.",
|
||||
"nodes.templateTransform.code": "Kode",
|
||||
"nodes.templateTransform.codeSupportTip": "Hanya mendukung Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Variabel Masukan",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Sumber",
|
||||
"tabs.start": "Mulai",
|
||||
"tabs.startDisabledTip": "Node pemicu dan node input pengguna saling eksklusif.",
|
||||
"tabs.startDisabledTipLearnMore": "Pelajari lebih lanjut tentang node awal",
|
||||
"tabs.startNotSupportedTip": "Tab Mulai tidak didukung dalam cuplikan.",
|
||||
"tabs.tools": "Perkakas",
|
||||
"tabs.transform": "Mengubah",
|
||||
"tabs.unconfiguredStartDisabledTip": "Node awal yang belum dikonfigurasi telah ditambahkan ke kanvas. Selesaikan penyiapan sebelum melanjutkan.",
|
||||
"tabs.usePlugin": "Pilih alat",
|
||||
"tabs.utilities": "Utilitas",
|
||||
"tabs.workflowTool": "Alur Kerja",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Anello",
|
||||
"blocks.loop-end": "Uscire dal ciclo",
|
||||
"blocks.loop-start": "Inizio ciclo",
|
||||
"blocks.mostCommon": "Più comune",
|
||||
"blocks.originalStartNode": "nodo iniziale originale",
|
||||
"blocks.parameter-extractor": "Estrattore Parametri",
|
||||
"blocks.question-classifier": "Classificatore Domande",
|
||||
"blocks.start": "Inizio",
|
||||
"blocks.start-placeholder": "Avvio del workflow",
|
||||
"blocks.template-transform": "Template",
|
||||
"blocks.tool": "Strumento",
|
||||
"blocks.trigger-plugin": "Attivatore del plugin",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Usa LLM per estrarre parametri strutturati dal linguaggio naturale per invocazioni di strumenti o richieste HTTP.",
|
||||
"blocksAbout.question-classifier": "Definisci le condizioni di classificazione delle domande dell'utente, LLM può definire come prosegue la conversazione in base alla descrizione della classificazione",
|
||||
"blocksAbout.start": "Definisci i parametri iniziali per l'avvio di un flusso di lavoro",
|
||||
"blocksAbout.start-placeholder": "Scegli come inizia questo workflow",
|
||||
"blocksAbout.template-transform": "Converti i dati in stringa usando la sintassi del template Jinja",
|
||||
"blocksAbout.tool": "Usa strumenti esterni per estendere le capacità del flusso di lavoro",
|
||||
"blocksAbout.trigger-plugin": "Trigger di integrazione di terze parti che avvia flussi di lavoro da eventi di piattaforme esterne",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "tipo di messaggio",
|
||||
"nodes.start.outputVars.query": "Input Utente",
|
||||
"nodes.start.required": "richiesto",
|
||||
"nodes.start.userInputTipDescription": "Definisci gli input da raccogliere dagli utenti finali quando il workflow viene avviato su richiesta.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Sfoglia altro nel Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Trova altri strumenti nel Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Nessun trigger trovato",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Fai clic per configurare il nodo iniziale",
|
||||
"nodes.startPlaceholder.nodeDescription": "Scegli un nodo iniziale dal pannello a destra",
|
||||
"nodes.startPlaceholder.nodeTitle": "Avvio del workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "Il nodo iniziale definisce cosa attiva l’esecuzione del workflow",
|
||||
"nodes.startPlaceholder.panelTitle": "Scegli un nodo iniziale",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "L’input utente non può essere combinato con altri trigger",
|
||||
"nodes.startPlaceholder.validationRequired": "Scegli prima un nodo iniziale.",
|
||||
"nodes.templateTransform.code": "Codice",
|
||||
"nodes.templateTransform.codeSupportTip": "Supporta solo Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Variabili di Input",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Fonti",
|
||||
"tabs.start": "Inizia",
|
||||
"tabs.startDisabledTip": "Il nodo di attivazione e il nodo di input utente sono mutualmente esclusivi.",
|
||||
"tabs.startDisabledTipLearnMore": "Scopri di più sui nodi iniziali",
|
||||
"tabs.startNotSupportedTip": "La scheda Start non è supportata negli snippet.",
|
||||
"tabs.tools": "Strumenti",
|
||||
"tabs.transform": "Trasforma",
|
||||
"tabs.unconfiguredStartDisabledTip": "Un nodo iniziale non configurato è stato aggiunto alla tela. Completa la configurazione prima di continuare.",
|
||||
"tabs.usePlugin": "Strumento di selezione",
|
||||
"tabs.utilities": "Utility",
|
||||
"tabs.workflowTool": "Flusso di lavoro",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "ループ",
|
||||
"blocks.loop-end": "ループ完了",
|
||||
"blocks.loop-start": "ループ開始",
|
||||
"blocks.mostCommon": "最も一般的",
|
||||
"blocks.originalStartNode": "元の開始ノード",
|
||||
"blocks.parameter-extractor": "パラメータ抽出",
|
||||
"blocks.question-classifier": "質問分類器",
|
||||
"blocks.start": "ユーザー入力",
|
||||
"blocks.start-placeholder": "ワークフロー開始",
|
||||
"blocks.template-transform": "テンプレート",
|
||||
"blocks.tool": "ツール",
|
||||
"blocks.trigger-plugin": "プラグイントリガー",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "自然言語から構造化パラメータを抽出し、後続処理で利用します。",
|
||||
"blocksAbout.question-classifier": "質問の分類条件を定義し、LLM が分類に基づいて対話フローを制御します。",
|
||||
"blocksAbout.start": "ワークフロー開始時の初期パラメータを定義します。",
|
||||
"blocksAbout.start-placeholder": "このワークフローの開始方法を選択します",
|
||||
"blocksAbout.template-transform": "Jinja テンプレート構文でデータを文字列に変換します。",
|
||||
"blocksAbout.tool": "外部ツールを使用してワークフローの機能を拡張する",
|
||||
"blocksAbout.trigger-plugin": "サードパーティ統合トリガー、外部プラットフォームのイベントによってワークフローを開始します",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "メッセージ種別",
|
||||
"nodes.start.outputVars.query": "ユーザー入力",
|
||||
"nodes.start.required": "必須",
|
||||
"nodes.start.userInputTipDescription": "ワークフローがオンデマンドで開始されるときにエンドユーザーから収集する入力を定義します。",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace でもっと探す",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace でさらにツールを探す",
|
||||
"nodes.startPlaceholder.noTriggersFound": "トリガーが見つかりませんでした",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "クリックして開始ノードを設定",
|
||||
"nodes.startPlaceholder.nodeDescription": "右側のパネルから開始ノードを選択",
|
||||
"nodes.startPlaceholder.nodeTitle": "ワークフロー開始",
|
||||
"nodes.startPlaceholder.panelDescription": "開始ノードはワークフローを実行するトリガーを定義します",
|
||||
"nodes.startPlaceholder.panelTitle": "開始ノードを選択",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "ユーザー入力は他のトリガーと組み合わせることはできません",
|
||||
"nodes.startPlaceholder.validationRequired": "最初に開始ノードを選択してください。",
|
||||
"nodes.templateTransform.code": "コード",
|
||||
"nodes.templateTransform.codeSupportTip": "Jinja2 のみをサポートしています",
|
||||
"nodes.templateTransform.inputVars": "入力変数",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "ソース",
|
||||
"tabs.start": "始める",
|
||||
"tabs.startDisabledTip": "トリガーノードとユーザー入力ノードは互いに排他です。",
|
||||
"tabs.startDisabledTipLearnMore": "開始ノードの詳細を見る",
|
||||
"tabs.startNotSupportedTip": "[スタート] タブはスニペットではサポートされていません。",
|
||||
"tabs.tools": "ツール",
|
||||
"tabs.transform": "変換",
|
||||
"tabs.unconfiguredStartDisabledTip": "未設定の開始ノードがキャンバスに追加されています。続行する前に設定を完了してください。",
|
||||
"tabs.usePlugin": "ツールを選択",
|
||||
"tabs.utilities": "ツール",
|
||||
"tabs.workflowTool": "ワークフロー",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "루프",
|
||||
"blocks.loop-end": "루프 종료",
|
||||
"blocks.loop-start": "루프 시작",
|
||||
"blocks.mostCommon": "가장 일반적",
|
||||
"blocks.originalStartNode": "원래 시작 노드",
|
||||
"blocks.parameter-extractor": "매개변수 추출기",
|
||||
"blocks.question-classifier": "질문 분류기",
|
||||
"blocks.start": "시작",
|
||||
"blocks.start-placeholder": "워크플로 시작",
|
||||
"blocks.template-transform": "템플릿",
|
||||
"blocks.tool": "도구",
|
||||
"blocks.trigger-plugin": "플러그인 트리거",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "도구 호출 또는 HTTP 요청을 위해 자연어에서 구조화된 매개변수를 추출하기 위해 LLM 을 사용합니다.",
|
||||
"blocksAbout.question-classifier": "사용자 질문의 분류 조건을 정의합니다. LLM 은 분류 설명을 기반으로 대화의 진행 방식을 정의할 수 있습니다",
|
||||
"blocksAbout.start": "워크플로우를 시작하기 위한 초기 매개변수를 정의합니다",
|
||||
"blocksAbout.start-placeholder": "이 워크플로가 시작되는 방식을 선택하세요",
|
||||
"blocksAbout.template-transform": "Jinja 템플릿 구문을 사용하여 데이터를 문자열로 변환합니다",
|
||||
"blocksAbout.tool": "외부 도구를 사용하여 워크플로우 기능을 확장하세요",
|
||||
"blocksAbout.trigger-plugin": "외부 플랫폼 이벤트로 워크플로를 시작하는 타사 통합 트리거",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "메시지 유형",
|
||||
"nodes.start.outputVars.query": "사용자 입력",
|
||||
"nodes.start.required": "필수",
|
||||
"nodes.start.userInputTipDescription": "워크플로가 필요할 때 시작될 때 최종 사용자에게서 수집할 입력을 정의합니다.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace 에서 더 찾아보기",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace 에서 더 많은 도구 찾기",
|
||||
"nodes.startPlaceholder.noTriggersFound": "트리거를 찾을 수 없습니다",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "시작 노드를 구성하려면 클릭하세요",
|
||||
"nodes.startPlaceholder.nodeDescription": "오른쪽 패널에서 시작 노드를 선택하세요",
|
||||
"nodes.startPlaceholder.nodeTitle": "워크플로 시작",
|
||||
"nodes.startPlaceholder.panelDescription": "시작 노드는 워크플로 실행을 트리거하는 항목을 정의합니다",
|
||||
"nodes.startPlaceholder.panelTitle": "시작 노드 선택",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "사용자 입력은 다른 트리거와 함께 사용할 수 없습니다",
|
||||
"nodes.startPlaceholder.validationRequired": "먼저 시작 노드를 선택하세요.",
|
||||
"nodes.templateTransform.code": "코드",
|
||||
"nodes.templateTransform.codeSupportTip": "Jinja2 만 지원합니다",
|
||||
"nodes.templateTransform.inputVars": "입력 변수",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "소스",
|
||||
"tabs.start": "시작",
|
||||
"tabs.startDisabledTip": "트리거 노드와 사용자 입력 노드는 상호 배타적입니다.",
|
||||
"tabs.startDisabledTipLearnMore": "시작 노드 자세히 알아보기",
|
||||
"tabs.startNotSupportedTip": "시작 탭은 조각에서 지원되지 않습니다.",
|
||||
"tabs.tools": "도구",
|
||||
"tabs.transform": "변환",
|
||||
"tabs.unconfiguredStartDisabledTip": "구성되지 않은 시작 노드가 캔버스에 추가되었습니다. 계속하기 전에 설정을 완료하세요.",
|
||||
"tabs.usePlugin": "도구 선택",
|
||||
"tabs.utilities": "유틸리티",
|
||||
"tabs.workflowTool": "워크플로우",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Loop",
|
||||
"blocks.loop-end": "Exit Loop",
|
||||
"blocks.loop-start": "Loop Start",
|
||||
"blocks.mostCommon": "Meest gebruikt",
|
||||
"blocks.originalStartNode": "original start node",
|
||||
"blocks.parameter-extractor": "Parameter Extractor",
|
||||
"blocks.question-classifier": "Question Classifier",
|
||||
"blocks.start": "User Input",
|
||||
"blocks.start-placeholder": "Workflow-start",
|
||||
"blocks.template-transform": "Template",
|
||||
"blocks.tool": "Tool",
|
||||
"blocks.trigger-plugin": "Plugin Trigger",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Use LLM to extract structured parameters from natural language for tool invocations or HTTP requests.",
|
||||
"blocksAbout.question-classifier": "Define the classification conditions of user questions, LLM can define how the conversation progresses based on the classification description",
|
||||
"blocksAbout.start": "Define the initial parameters for launching a workflow",
|
||||
"blocksAbout.start-placeholder": "Kies hoe deze workflow start",
|
||||
"blocksAbout.template-transform": "Convert data to string using Jinja template syntax",
|
||||
"blocksAbout.tool": "Use external tools to extend workflow capabilities",
|
||||
"blocksAbout.trigger-plugin": "Third-party integration trigger that starts workflows from external platform events",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "message type",
|
||||
"nodes.start.outputVars.query": "User input",
|
||||
"nodes.start.required": "required",
|
||||
"nodes.start.userInputTipDescription": "Definieer invoer die bij eindgebruikers wordt verzameld wanneer je workflow op aanvraag start.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Meer bekijken in Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Meer tools vinden in Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Geen triggers gevonden",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Klik om de startnode te configureren",
|
||||
"nodes.startPlaceholder.nodeDescription": "Kies een startnode in het rechterpaneel",
|
||||
"nodes.startPlaceholder.nodeTitle": "Workflow-start",
|
||||
"nodes.startPlaceholder.panelDescription": "De startnode bepaalt wat je workflow activeert",
|
||||
"nodes.startPlaceholder.panelTitle": "Kies een startnode",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Gebruikersinvoer kan niet worden gecombineerd met andere triggers",
|
||||
"nodes.startPlaceholder.validationRequired": "Kies eerst een startnode.",
|
||||
"nodes.templateTransform.code": "Code",
|
||||
"nodes.templateTransform.codeSupportTip": "Only supports Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Input Variables",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Sources",
|
||||
"tabs.start": "Start",
|
||||
"tabs.startDisabledTip": "Trigger node and user input node are mutually exclusive.",
|
||||
"tabs.startDisabledTipLearnMore": "Meer informatie over startnodes",
|
||||
"tabs.startNotSupportedTip": "Het tabblad Start wordt niet ondersteund in fragmenten.",
|
||||
"tabs.tools": "Tools",
|
||||
"tabs.transform": "Transform",
|
||||
"tabs.unconfiguredStartDisabledTip": "Er is een niet-geconfigureerde startnode aan het canvas toegevoegd. Voltooi de configuratie voordat je doorgaat.",
|
||||
"tabs.usePlugin": "Select tool",
|
||||
"tabs.utilities": "Utilities",
|
||||
"tabs.workflowTool": "Workflow",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Pętla",
|
||||
"blocks.loop-end": "Wyjście z pętli",
|
||||
"blocks.loop-start": "Początek pętli",
|
||||
"blocks.mostCommon": "Najczęstsze",
|
||||
"blocks.originalStartNode": "oryginalny węzeł początkowy",
|
||||
"blocks.parameter-extractor": "Ekstraktor parametrów",
|
||||
"blocks.question-classifier": "Klasyfikator pytań",
|
||||
"blocks.start": "Start",
|
||||
"blocks.start-placeholder": "Start workflow",
|
||||
"blocks.template-transform": "Szablon",
|
||||
"blocks.tool": "Narzędzie",
|
||||
"blocks.trigger-plugin": "Wyzwalacz wtyczki",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Użyj LLM do wyodrębnienia strukturalnych parametrów z języka naturalnego do wywołań narzędzi lub żądań HTTP.",
|
||||
"blocksAbout.question-classifier": "Zdefiniuj warunki klasyfikacji pytań użytkowników, LLM może definiować, jak rozmowa postępuje na podstawie opisu klasyfikacji",
|
||||
"blocksAbout.start": "Zdefiniuj początkowe parametry uruchamiania przepływu pracy",
|
||||
"blocksAbout.start-placeholder": "Wybierz, jak rozpoczyna się ten workflow",
|
||||
"blocksAbout.template-transform": "Konwertuj dane na ciąg znaków przy użyciu składni szablonu Jinja",
|
||||
"blocksAbout.tool": "Używaj zewnętrznych narzędzi, aby rozszerzyć możliwości przepływu pracy",
|
||||
"blocksAbout.trigger-plugin": "Wyzwalacz integracji zewnętrznej, który uruchamia przepływy pracy na podstawie zdarzeń z platformy zewnętrznej",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "typ wiadomości",
|
||||
"nodes.start.outputVars.query": "Wprowadzenie użytkownika",
|
||||
"nodes.start.required": "wymagane",
|
||||
"nodes.start.userInputTipDescription": "Określ dane wejściowe zbierane od użytkowników końcowych, gdy workflow uruchamia się na żądanie.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Przeglądaj więcej w Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Znajdź więcej narzędzi w Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Nie znaleziono wyzwalaczy",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Kliknij, aby skonfigurować węzeł startowy",
|
||||
"nodes.startPlaceholder.nodeDescription": "Wybierz węzeł startowy z prawego panelu",
|
||||
"nodes.startPlaceholder.nodeTitle": "Start workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "Węzeł startowy określa, co uruchamia workflow",
|
||||
"nodes.startPlaceholder.panelTitle": "Wybierz węzeł startowy",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Danych wejściowych użytkownika nie można łączyć z innymi wyzwalaczami",
|
||||
"nodes.startPlaceholder.validationRequired": "Najpierw wybierz węzeł startowy.",
|
||||
"nodes.templateTransform.code": "Kod",
|
||||
"nodes.templateTransform.codeSupportTip": "Obsługuje tylko Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Zmienne wejściowe",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Źródeł",
|
||||
"tabs.start": "Start",
|
||||
"tabs.startDisabledTip": "Węzeł wyzwalacza i węzeł wprowadzania danych przez użytkownika wzajemnie się wykluczają.",
|
||||
"tabs.startDisabledTipLearnMore": "Dowiedz się więcej o węzłach startowych",
|
||||
"tabs.startNotSupportedTip": "Karta Start nie jest obsługiwana we fragmentach.",
|
||||
"tabs.tools": "Narzędzia",
|
||||
"tabs.transform": "Transformacja",
|
||||
"tabs.unconfiguredStartDisabledTip": "Do obszaru roboczego dodano nieskonfigurowany węzeł startowy. Przed kontynuowaniem dokończ konfigurację.",
|
||||
"tabs.usePlugin": "Wybierz narzędzie",
|
||||
"tabs.utilities": "Narzędzia pomocnicze",
|
||||
"tabs.workflowTool": "Przepływ pracy",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Laço",
|
||||
"blocks.loop-end": "Sair do Loop",
|
||||
"blocks.loop-start": "Início do Loop",
|
||||
"blocks.mostCommon": "Mais comum",
|
||||
"blocks.originalStartNode": "nó inicial original",
|
||||
"blocks.parameter-extractor": "Extrator de parâmetros",
|
||||
"blocks.question-classifier": "Classificador de perguntas",
|
||||
"blocks.start": "Iniciar",
|
||||
"blocks.start-placeholder": "Início do workflow",
|
||||
"blocks.template-transform": "Modelo",
|
||||
"blocks.tool": "Ferramenta",
|
||||
"blocks.trigger-plugin": "Acionador de Plugin",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Use LLM para extrair parâmetros estruturados da linguagem natural para invocações de ferramentas ou requisições HTTP.",
|
||||
"blocksAbout.question-classifier": "Definir as condições de classificação das perguntas dos usuários, LLM pode definir como a conversa progride com base na descrição da classificação",
|
||||
"blocksAbout.start": "Definir os parâmetros iniciais para iniciar um fluxo de trabalho",
|
||||
"blocksAbout.start-placeholder": "Escolha como este workflow começa",
|
||||
"blocksAbout.template-transform": "Converter dados em string usando a sintaxe de template Jinja",
|
||||
"blocksAbout.tool": "Use ferramentas externas para ampliar as capacidades do fluxo de trabalho",
|
||||
"blocksAbout.trigger-plugin": "Gatilho de integração de terceiros que inicia fluxos de trabalho a partir de eventos de plataformas externas",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "tipo de mensagem",
|
||||
"nodes.start.outputVars.query": "Entrada do usuário",
|
||||
"nodes.start.required": "requerido",
|
||||
"nodes.start.userInputTipDescription": "Defina entradas para coletar dos usuários finais quando seu workflow iniciar sob demanda.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Procurar mais no Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Encontrar mais ferramentas no Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Nenhum gatilho foi encontrado",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Clique para configurar o nó inicial",
|
||||
"nodes.startPlaceholder.nodeDescription": "Escolha um nó inicial no painel à direita",
|
||||
"nodes.startPlaceholder.nodeTitle": "Início do workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "O nó inicial define o que aciona a execução do seu workflow",
|
||||
"nodes.startPlaceholder.panelTitle": "Escolha um nó inicial",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Entrada do usuário não pode ser combinada com outros gatilhos",
|
||||
"nodes.startPlaceholder.validationRequired": "Escolha primeiro um nó inicial.",
|
||||
"nodes.templateTransform.code": "Código",
|
||||
"nodes.templateTransform.codeSupportTip": "Suporta apenas Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Variáveis de entrada",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Fontes",
|
||||
"tabs.start": "Começar",
|
||||
"tabs.startDisabledTip": "O nó de gatilho e o nó de entrada do usuário são mutuamente exclusivos.",
|
||||
"tabs.startDisabledTipLearnMore": "Saiba mais sobre nós iniciais",
|
||||
"tabs.startNotSupportedTip": "A guia Iniciar não é compatível com snippets.",
|
||||
"tabs.tools": "Ferramentas",
|
||||
"tabs.transform": "Transformar",
|
||||
"tabs.unconfiguredStartDisabledTip": "Um nó inicial não configurado foi adicionado à tela. Conclua a configuração antes de continuar.",
|
||||
"tabs.usePlugin": "Selecionar ferramenta",
|
||||
"tabs.utilities": "Utilitários",
|
||||
"tabs.workflowTool": "Fluxo de trabalho",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Loop",
|
||||
"blocks.loop-end": "Ieșire din buclă",
|
||||
"blocks.loop-start": "Întreținere buclă",
|
||||
"blocks.mostCommon": "Cel mai frecvent",
|
||||
"blocks.originalStartNode": "nod de start original",
|
||||
"blocks.parameter-extractor": "Extractor de parametri",
|
||||
"blocks.question-classifier": "Clasificator de întrebări",
|
||||
"blocks.start": "Începe",
|
||||
"blocks.start-placeholder": "Pornire workflow",
|
||||
"blocks.template-transform": "Șablon",
|
||||
"blocks.tool": "Unealtă",
|
||||
"blocks.trigger-plugin": "Declanșator plugin",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Utilizați LLM pentru a extrage parametrii structurați din limbajul natural pentru invocările de instrumente sau cererile HTTP.",
|
||||
"blocksAbout.question-classifier": "Definiți condițiile de clasificare a întrebărilor utilizatorului, LLM poate defini cum progresează conversația pe baza descrierii clasificării",
|
||||
"blocksAbout.start": "Definiți parametrii inițiali pentru lansarea unui flux de lucru",
|
||||
"blocksAbout.start-placeholder": "Alegeți cum începe acest workflow",
|
||||
"blocksAbout.template-transform": "Convertiți datele în șiruri de caractere folosind sintaxa șablonului Jinja",
|
||||
"blocksAbout.tool": "Utilizați instrumente externe pentru a extinde capacitățile fluxului de lucru",
|
||||
"blocksAbout.trigger-plugin": "Declanșator de integrare terță parte care pornește fluxuri de lucru din evenimente ale platformelor externe",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "tip mesaj",
|
||||
"nodes.start.outputVars.query": "Intrare utilizator",
|
||||
"nodes.start.required": "necesar",
|
||||
"nodes.start.userInputTipDescription": "Definiți intrările de colectat de la utilizatorii finali când workflow-ul pornește la cerere.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Răsfoiți mai multe în Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Găsiți mai multe instrumente în Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Nu au fost găsite declanșatoare",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Faceți clic pentru a configura nodul de pornire",
|
||||
"nodes.startPlaceholder.nodeDescription": "Alegeți un nod de pornire din panoul din dreapta",
|
||||
"nodes.startPlaceholder.nodeTitle": "Pornire workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "Nodul de pornire definește ce declanșează rularea workflow-ului",
|
||||
"nodes.startPlaceholder.panelTitle": "Alegeți un nod de pornire",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Intrarea utilizatorului nu poate fi combinată cu alte declanșatoare",
|
||||
"nodes.startPlaceholder.validationRequired": "Alegeți mai întâi un nod de pornire.",
|
||||
"nodes.templateTransform.code": "Cod",
|
||||
"nodes.templateTransform.codeSupportTip": "Suportă doar Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Variabile de intrare",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Surse",
|
||||
"tabs.start": "Începe",
|
||||
"tabs.startDisabledTip": "Nodul de declanșare și nodul de intrare a utilizatorului se exclud reciproc.",
|
||||
"tabs.startDisabledTipLearnMore": "Aflați mai multe despre nodurile de pornire",
|
||||
"tabs.startNotSupportedTip": "Fila Start nu este acceptată în fragmente.",
|
||||
"tabs.tools": "Instrumente",
|
||||
"tabs.transform": "Transformare",
|
||||
"tabs.unconfiguredStartDisabledTip": "Un nod de pornire neconfigurat a fost adăugat pe canvas. Finalizați configurarea înainte de a continua.",
|
||||
"tabs.usePlugin": "Selectează instrumentul",
|
||||
"tabs.utilities": "Utilități",
|
||||
"tabs.workflowTool": "Flux de lucru",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Цикл",
|
||||
"blocks.loop-end": "Конец цикла",
|
||||
"blocks.loop-start": "Начало цикла",
|
||||
"blocks.mostCommon": "Самый распространенный",
|
||||
"blocks.originalStartNode": "исходный начальный узел",
|
||||
"blocks.parameter-extractor": "Экстрактор параметров",
|
||||
"blocks.question-classifier": "Классификатор вопросов",
|
||||
"blocks.start": "Начало",
|
||||
"blocks.start-placeholder": "Запуск workflow",
|
||||
"blocks.template-transform": "Шаблон",
|
||||
"blocks.tool": "Инструмент",
|
||||
"blocks.trigger-plugin": "Триггер плагина",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Используйте LLM для извлечения структурированных параметров из естественного языка для вызова инструментов или HTTP-запросов.",
|
||||
"blocksAbout.question-classifier": "Определите условия классификации вопросов пользователей, LLM может определить, как будет развиваться разговор на основе описания классификации",
|
||||
"blocksAbout.start": "Определите начальные параметры для запуска рабочего процесса",
|
||||
"blocksAbout.start-placeholder": "Выберите, как запускается этот workflow",
|
||||
"blocksAbout.template-transform": "Преобразование данных в строку с использованием синтаксиса шаблонов Jinja",
|
||||
"blocksAbout.tool": "Используйте внешние инструменты для расширения возможностей рабочего процесса",
|
||||
"blocksAbout.trigger-plugin": "Триггер интеграции с третьими сторонами, который запускает рабочие процессы на основе событий внешней платформы",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "тип сообщения",
|
||||
"nodes.start.outputVars.query": "Ввод пользователя",
|
||||
"nodes.start.required": "обязательно",
|
||||
"nodes.start.userInputTipDescription": "Задайте входные данные, которые нужно собрать у конечных пользователей при запуске workflow по запросу.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Найти больше в Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Найти больше инструментов в Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Триггеры не найдены",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Нажмите, чтобы настроить начальный узел",
|
||||
"nodes.startPlaceholder.nodeDescription": "Выберите начальный узел на правой панели",
|
||||
"nodes.startPlaceholder.nodeTitle": "Запуск workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "Начальный узел определяет, что запускает выполнение вашего workflow",
|
||||
"nodes.startPlaceholder.panelTitle": "Выберите начальный узел",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Пользовательский ввод нельзя сочетать с другими триггерами",
|
||||
"nodes.startPlaceholder.validationRequired": "Сначала выберите начальный узел.",
|
||||
"nodes.templateTransform.code": "Код",
|
||||
"nodes.templateTransform.codeSupportTip": "Поддерживает только Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Входные переменные",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Источников",
|
||||
"tabs.start": "Начать",
|
||||
"tabs.startDisabledTip": "Узел триггера и узел ввода пользователя исключают друг друга.",
|
||||
"tabs.startDisabledTipLearnMore": "Подробнее о начальных узлах",
|
||||
"tabs.startNotSupportedTip": "Вкладка «Пуск» не поддерживается во фрагментах.",
|
||||
"tabs.tools": "Инструменты",
|
||||
"tabs.transform": "Преобразование",
|
||||
"tabs.unconfiguredStartDisabledTip": "На холст добавлен ненастроенный начальный узел. Завершите настройку, прежде чем продолжить.",
|
||||
"tabs.usePlugin": "Выбрать инструмент",
|
||||
"tabs.utilities": "Утилиты",
|
||||
"tabs.workflowTool": "Рабочий процесс",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Zanka",
|
||||
"blocks.loop-end": "Izhod iz zanke",
|
||||
"blocks.loop-start": "Začetek zanke",
|
||||
"blocks.mostCommon": "Najpogostejše",
|
||||
"blocks.originalStartNode": "izvorna začetna točka",
|
||||
"blocks.parameter-extractor": "Ekstraktor parametrov",
|
||||
"blocks.question-classifier": "Razvrščevalec vprašanj",
|
||||
"blocks.start": "Začni",
|
||||
"blocks.start-placeholder": "Začetek workflowa",
|
||||
"blocks.template-transform": "Predloga",
|
||||
"blocks.tool": "Orodje",
|
||||
"blocks.trigger-plugin": "Sprožilec vtičnika",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Uporabite LLM za pridobivanje strukturiranih parametrov iz naravnega jezika za klice orodij ali HTTP zahtev.",
|
||||
"blocksAbout.question-classifier": "Določite pogoje klasifikacije uporabniških vprašanj, LLM lahko določi, kako se pogovor razvija na podlagi opisa klasifikacije.",
|
||||
"blocksAbout.start": "Določite začetne parametre za zagon delovnega toka",
|
||||
"blocksAbout.start-placeholder": "Izberite, kako se ta workflow začne",
|
||||
"blocksAbout.template-transform": "Pretvori podatke v niz z uporabo Jinja predloge",
|
||||
"blocksAbout.tool": "Uporabite zunanja orodja za razširitev zmogljivosti delovnega toka",
|
||||
"blocksAbout.trigger-plugin": "Sprožilec integracije tretje osebe, ki začne delovne tokove iz dogodkov na zunanji platformi",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "vrsta sporočila",
|
||||
"nodes.start.outputVars.query": "Uporabniški vnos",
|
||||
"nodes.start.required": "zahtevano",
|
||||
"nodes.start.userInputTipDescription": "Določite vnose, ki jih želite zbrati od končnih uporabnikov, ko se workflow zažene na zahtevo.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Prebrskajte več v Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Poiščite več orodij v Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Ni najdenih sprožilcev",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Kliknite za konfiguracijo začetnega vozlišča",
|
||||
"nodes.startPlaceholder.nodeDescription": "Izberite začetno vozlišče na desni plošči",
|
||||
"nodes.startPlaceholder.nodeTitle": "Začetek workflowa",
|
||||
"nodes.startPlaceholder.panelDescription": "Začetno vozlišče določa, kaj sproži zagon vašega workflowa",
|
||||
"nodes.startPlaceholder.panelTitle": "Izberite začetno vozlišče",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Uporabniškega vnosa ni mogoče kombinirati z drugimi sprožilci",
|
||||
"nodes.startPlaceholder.validationRequired": "Najprej izberite začetno vozlišče.",
|
||||
"nodes.templateTransform.code": "Koda",
|
||||
"nodes.templateTransform.codeSupportTip": "Podpira samo Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Vhodne spremenljivke",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Virov",
|
||||
"tabs.start": "Začni",
|
||||
"tabs.startDisabledTip": "Vozlišče sprožilca in vozlišče vnosa uporabnika se med seboj izključujeta.",
|
||||
"tabs.startDisabledTipLearnMore": "Več o začetnih vozliščih",
|
||||
"tabs.startNotSupportedTip": "Zavihek Start ni podprt v izrezkih.",
|
||||
"tabs.tools": "Orodja",
|
||||
"tabs.transform": "Pretvori",
|
||||
"tabs.unconfiguredStartDisabledTip": "Na platno je bilo dodano nekonfigurirano začetno vozlišče. Pred nadaljevanjem dokončajte nastavitev.",
|
||||
"tabs.usePlugin": "Izberi orodje",
|
||||
"tabs.utilities": "Komunalne storitve",
|
||||
"tabs.workflowTool": "Delovni tok",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "ลูป",
|
||||
"blocks.loop-end": "ออกจากลูป",
|
||||
"blocks.loop-start": "เริ่มลูป",
|
||||
"blocks.mostCommon": "พบบ่อยที่สุด",
|
||||
"blocks.originalStartNode": "โหนดเริ่มต้นเดิม",
|
||||
"blocks.parameter-extractor": "ตัวแยกพารามิเตอร์",
|
||||
"blocks.question-classifier": "ตัวจําแนกคําถาม",
|
||||
"blocks.start": "เริ่ม",
|
||||
"blocks.start-placeholder": "เริ่มต้นเวิร์กโฟลว์",
|
||||
"blocks.template-transform": "แม่ แบบ",
|
||||
"blocks.tool": "เครื่องมือ",
|
||||
"blocks.trigger-plugin": "ทริกเกอร์ปลั๊กอิน",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "ใช้ LLM เพื่อแยกพารามิเตอร์ที่มีโครงสร้างจากภาษาธรรมชาติสําหรับการเรียกใช้เครื่องมือหรือคําขอ HTTP",
|
||||
"blocksAbout.question-classifier": "กําหนดเงื่อนไขการจําแนกประเภทของคําถามของผู้ใช้ LLM สามารถกําหนดความคืบหน้าของการสนทนาตามคําอธิบายการจําแนกประเภท",
|
||||
"blocksAbout.start": "กําหนดพารามิเตอร์เริ่มต้นสําหรับการเปิดใช้เวิร์กโฟลว์",
|
||||
"blocksAbout.start-placeholder": "เลือกวิธีเริ่มต้นเวิร์กโฟลว์นี้",
|
||||
"blocksAbout.template-transform": "แปลงข้อมูลเป็นสตริงโดยใช้ไวยากรณ์เทมเพลต Jinja",
|
||||
"blocksAbout.tool": "ใช้เครื่องมือภายนอกเพื่อขยายความสามารถของเวิร์กโฟลว์",
|
||||
"blocksAbout.trigger-plugin": "ทริกเกอร์การรวมจากบุคคลที่สามที่เริ่มการทำงานอัตโนมัติจากเหตุการณ์ของแพลตฟอร์มภายนอก",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "ประเภทข้อความ",
|
||||
"nodes.start.outputVars.query": "การป้อนข้อมูลของผู้ใช้",
|
||||
"nodes.start.required": "ต้องระบุ",
|
||||
"nodes.start.userInputTipDescription": "กำหนดอินพุตที่จะรวบรวมจากผู้ใช้ปลายทางเมื่อเวิร์กโฟลว์เริ่มทำงานตามคำขอ",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "เรียกดูเพิ่มเติมใน Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "ค้นหาเครื่องมือเพิ่มเติมใน Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "ไม่พบทริกเกอร์",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "คลิกเพื่อกำหนดค่าโหนดเริ่มต้น",
|
||||
"nodes.startPlaceholder.nodeDescription": "เลือกโหนดเริ่มต้นจากแผงด้านขวา",
|
||||
"nodes.startPlaceholder.nodeTitle": "เริ่มต้นเวิร์กโฟลว์",
|
||||
"nodes.startPlaceholder.panelDescription": "โหนดเริ่มต้นกำหนดสิ่งที่จะทริกเกอร์ให้เวิร์กโฟลว์ทำงาน",
|
||||
"nodes.startPlaceholder.panelTitle": "เลือกโหนดเริ่มต้น",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "อินพุตผู้ใช้ไม่สามารถใช้ร่วมกับทริกเกอร์อื่นได้",
|
||||
"nodes.startPlaceholder.validationRequired": "โปรดเลือกโหนดเริ่มต้นก่อน",
|
||||
"nodes.templateTransform.code": "รหัส",
|
||||
"nodes.templateTransform.codeSupportTip": "รองรับเฉพาะ Jinja2",
|
||||
"nodes.templateTransform.inputVars": "ตัวแปรอินพุต",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "แหล่ง",
|
||||
"tabs.start": "เริ่ม",
|
||||
"tabs.startDisabledTip": "โหนดทริกเกอร์และโหนดป้อนข้อมูลของผู้ใช้ไม่สามารถใช้ร่วมกันได้",
|
||||
"tabs.startDisabledTipLearnMore": "เรียนรู้เพิ่มเติมเกี่ยวกับโหนดเริ่มต้น",
|
||||
"tabs.startNotSupportedTip": "ตัวอย่างข้อมูลไม่รองรับแท็บเริ่มต้น",
|
||||
"tabs.tools": "เครื่อง มือ",
|
||||
"tabs.transform": "แปลง",
|
||||
"tabs.unconfiguredStartDisabledTip": "มีการเพิ่มโหนดเริ่มต้นที่ยังไม่ได้กำหนดค่าลงในแคนวาส โปรดตั้งค่าให้เสร็จก่อนดำเนินการต่อ",
|
||||
"tabs.usePlugin": "เลือกเครื่องมือ",
|
||||
"tabs.utilities": "สาธารณูปโภค",
|
||||
"tabs.workflowTool": "เวิร์กโฟลว์",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Döngü",
|
||||
"blocks.loop-end": "Döngüden Çık",
|
||||
"blocks.loop-start": "Döngü Başlangıcı",
|
||||
"blocks.mostCommon": "En yaygın",
|
||||
"blocks.originalStartNode": "orijinal başlangıç düğümü",
|
||||
"blocks.parameter-extractor": "Parametre Çıkarıcı",
|
||||
"blocks.question-classifier": "Soru Sınıflandırıcı",
|
||||
"blocks.start": "Başlat",
|
||||
"blocks.start-placeholder": "Workflow başlangıcı",
|
||||
"blocks.template-transform": "Şablon",
|
||||
"blocks.tool": "Araç",
|
||||
"blocks.trigger-plugin": "Eklenti Tetikleyicisi",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Aracı çağırmak veya HTTP istekleri için doğal dilden yapılandırılmış parametreler çıkarmak için LLM kullanın.",
|
||||
"blocksAbout.question-classifier": "Kullanıcı sorularının sınıflandırma koşullarını tanımlayın, LLM sınıflandırma açıklamasına dayalı olarak konuşmanın nasıl ilerleyeceğini tanımlayabilir",
|
||||
"blocksAbout.start": "Bir iş akışını başlatmak için başlangıç parametrelerini tanımlayın",
|
||||
"blocksAbout.start-placeholder": "Bu workflow’un nasıl başlayacağını seçin",
|
||||
"blocksAbout.template-transform": "Jinja şablon sözdizimini kullanarak verileri stringe dönüştürün",
|
||||
"blocksAbout.tool": "İş akışı yeteneklerini genişletmek için dış araçlar kullanın",
|
||||
"blocksAbout.trigger-plugin": "Üçüncü taraf entegrasyon tetikleyicisi, dış platform olaylarından iş akışlarını başlatır",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "mesaj türü",
|
||||
"nodes.start.outputVars.query": "Kullanıcı girişi",
|
||||
"nodes.start.required": "gerekli",
|
||||
"nodes.start.userInputTipDescription": "Workflow isteğe bağlı başladığında son kullanıcılardan toplanacak girişleri tanımlayın.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace’te daha fazlasına göz atın",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace’te daha fazla araç bulun",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Tetikleyici bulunamadı",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Başlangıç düğümünü yapılandırmak için tıklayın",
|
||||
"nodes.startPlaceholder.nodeDescription": "Sağ panelden bir başlangıç düğümü seçin",
|
||||
"nodes.startPlaceholder.nodeTitle": "Workflow başlangıcı",
|
||||
"nodes.startPlaceholder.panelDescription": "Başlangıç düğümü workflow’unuzu neyin çalıştıracağını tanımlar",
|
||||
"nodes.startPlaceholder.panelTitle": "Bir başlangıç düğümü seçin",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Kullanıcı girişi diğer tetikleyicilerle birleştirilemez",
|
||||
"nodes.startPlaceholder.validationRequired": "Önce bir başlangıç düğümü seçin.",
|
||||
"nodes.templateTransform.code": "Kod",
|
||||
"nodes.templateTransform.codeSupportTip": "Sadece Jinja2 destekler",
|
||||
"nodes.templateTransform.inputVars": "Giriş Değişkenleri",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Kaynak",
|
||||
"tabs.start": "Başlat",
|
||||
"tabs.startDisabledTip": "Tetikleyici düğümü ve kullanıcı girişi düğümü birbirini dışlar.",
|
||||
"tabs.startDisabledTipLearnMore": "Başlangıç düğümleri hakkında daha fazla bilgi edinin",
|
||||
"tabs.startNotSupportedTip": "Başlangıç sekmesi parçacıklarda desteklenmez.",
|
||||
"tabs.tools": "Araçlar",
|
||||
"tabs.transform": "Dönüştür",
|
||||
"tabs.unconfiguredStartDisabledTip": "Tuvale yapılandırılmamış bir başlangıç düğümü eklendi. Devam etmeden önce kurulumu tamamlayın.",
|
||||
"tabs.usePlugin": "Araç seç",
|
||||
"tabs.utilities": "Yardımcı Araçlar",
|
||||
"tabs.workflowTool": "İş Akışı",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Петля",
|
||||
"blocks.loop-end": "Вихід з циклу",
|
||||
"blocks.loop-start": "Початок циклу",
|
||||
"blocks.mostCommon": "Найпоширеніше",
|
||||
"blocks.originalStartNode": "оригінальний початковий вузол",
|
||||
"blocks.parameter-extractor": "Екстрактор параметрів",
|
||||
"blocks.question-classifier": "Класифікатор питань",
|
||||
"blocks.start": "Початок",
|
||||
"blocks.start-placeholder": "Початок workflow",
|
||||
"blocks.template-transform": "Шаблон",
|
||||
"blocks.tool": "Інструмент",
|
||||
"blocks.trigger-plugin": "Тригер плагіна",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Використовуйте LLM для вилучення структурованих параметрів з природної мови для викликів інструментів або HTTP-запитів.",
|
||||
"blocksAbout.question-classifier": "Визначте умови класифікації запитань користувачів, LLM може визначати, як розвивається розмова на основі опису класифікації",
|
||||
"blocksAbout.start": "Визначте початкові параметри для запуску робочого потоку",
|
||||
"blocksAbout.start-placeholder": "Виберіть, як запускається цей workflow",
|
||||
"blocksAbout.template-transform": "Перетворіть дані на рядок за допомогою синтаксису шаблону Jinja",
|
||||
"blocksAbout.tool": "Використовуйте зовнішні інструменти для розширення можливостей робочого процесу",
|
||||
"blocksAbout.trigger-plugin": "Тригер інтеграції сторонніх розробників, який запускає робочі процеси з подій зовнішньої платформи",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "тип повідомлення",
|
||||
"nodes.start.outputVars.query": "Введення користувача",
|
||||
"nodes.start.required": "обов'язковий",
|
||||
"nodes.start.userInputTipDescription": "Визначте вхідні дані, які потрібно збирати від кінцевих користувачів, коли workflow запускається на вимогу.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Переглянути більше в Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Знайти більше інструментів у Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Тригери не знайдено",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Натисніть, щоб налаштувати початковий вузол",
|
||||
"nodes.startPlaceholder.nodeDescription": "Виберіть початковий вузол на правій панелі",
|
||||
"nodes.startPlaceholder.nodeTitle": "Початок workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "Початковий вузол визначає, що запускає виконання вашого workflow",
|
||||
"nodes.startPlaceholder.panelTitle": "Виберіть початковий вузол",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Користувацьке введення не можна поєднувати з іншими тригерами",
|
||||
"nodes.startPlaceholder.validationRequired": "Спочатку виберіть початковий вузол.",
|
||||
"nodes.templateTransform.code": "Код",
|
||||
"nodes.templateTransform.codeSupportTip": "Підтримує лише Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Вхідні змінні",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Джерел",
|
||||
"tabs.start": "Почати",
|
||||
"tabs.startDisabledTip": "Вузол тригера та вузол введення користувача взаємовиключні.",
|
||||
"tabs.startDisabledTipLearnMore": "Докладніше про початкові вузли",
|
||||
"tabs.startNotSupportedTip": "Вкладка «Пуск» не підтримується у фрагментах.",
|
||||
"tabs.tools": "Інструменти",
|
||||
"tabs.transform": "Трансформація",
|
||||
"tabs.unconfiguredStartDisabledTip": "На полотно додано неналаштований початковий вузол. Завершіть налаштування, перш ніж продовжити.",
|
||||
"tabs.usePlugin": "Вибрати інструмент",
|
||||
"tabs.utilities": "Утиліти",
|
||||
"tabs.workflowTool": "Робочий потік",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "Vòng",
|
||||
"blocks.loop-end": "Thoát vòng lặp",
|
||||
"blocks.loop-start": "Bắt đầu vòng lặp",
|
||||
"blocks.mostCommon": "Phổ biến nhất",
|
||||
"blocks.originalStartNode": "nút bắt đầu gốc",
|
||||
"blocks.parameter-extractor": "Trình trích xuất tham số",
|
||||
"blocks.question-classifier": "Phân loại câu hỏi",
|
||||
"blocks.start": "Bắt đầu",
|
||||
"blocks.start-placeholder": "Bắt đầu workflow",
|
||||
"blocks.template-transform": "Mẫu",
|
||||
"blocks.tool": "Công cụ",
|
||||
"blocks.trigger-plugin": "Kích hoạt Plugin",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "Sử dụng LLM để trích xuất các tham số có cấu trúc từ ngôn ngữ tự nhiên để gọi công cụ hoặc yêu cầu HTTP.",
|
||||
"blocksAbout.question-classifier": "Định nghĩa các điều kiện phân loại câu hỏi của người dùng, LLM có thể định nghĩa cách cuộc trò chuyện tiến triển dựa trên mô tả phân loại",
|
||||
"blocksAbout.start": "Định nghĩa các tham số ban đầu để khởi chạy quy trình làm việc",
|
||||
"blocksAbout.start-placeholder": "Chọn cách workflow này bắt đầu",
|
||||
"blocksAbout.template-transform": "Chuyển đổi dữ liệu thành chuỗi bằng cú pháp mẫu Jinja",
|
||||
"blocksAbout.tool": "Sử dụng các công cụ bên ngoài để mở rộng khả năng quy trình làm việc",
|
||||
"blocksAbout.trigger-plugin": "Kích hoạt tích hợp bên thứ ba khởi chạy quy trình từ các sự kiện trên nền tảng bên ngoài",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "loại tin nhắn",
|
||||
"nodes.start.outputVars.query": "Đầu vào của người dùng",
|
||||
"nodes.start.required": "bắt buộc",
|
||||
"nodes.start.userInputTipDescription": "Xác định các đầu vào cần thu thập từ người dùng cuối khi workflow của bạn bắt đầu theo yêu cầu.",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "Duyệt thêm trên Marketplace",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "Tìm thêm công cụ trên Marketplace",
|
||||
"nodes.startPlaceholder.noTriggersFound": "Không tìm thấy trình kích hoạt nào",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "Nhấp để cấu hình nút bắt đầu",
|
||||
"nodes.startPlaceholder.nodeDescription": "Chọn một nút bắt đầu từ bảng bên phải",
|
||||
"nodes.startPlaceholder.nodeTitle": "Bắt đầu workflow",
|
||||
"nodes.startPlaceholder.panelDescription": "Nút bắt đầu xác định điều gì kích hoạt workflow của bạn chạy",
|
||||
"nodes.startPlaceholder.panelTitle": "Chọn một nút bắt đầu",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "Đầu vào người dùng không thể kết hợp với các trình kích hoạt khác",
|
||||
"nodes.startPlaceholder.validationRequired": "Trước tiên hãy chọn một nút bắt đầu.",
|
||||
"nodes.templateTransform.code": "Mã",
|
||||
"nodes.templateTransform.codeSupportTip": "Chỉ hỗ trợ Jinja2",
|
||||
"nodes.templateTransform.inputVars": "Biến đầu vào",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "Nguồn",
|
||||
"tabs.start": "Bắt đầu",
|
||||
"tabs.startDisabledTip": "Nút kích hoạt và nút nhập liệu của người dùng là loại trừ lẫn nhau.",
|
||||
"tabs.startDisabledTipLearnMore": "Tìm hiểu thêm về các nút bắt đầu",
|
||||
"tabs.startNotSupportedTip": "Tab bắt đầu không được hỗ trợ trong đoạn trích.",
|
||||
"tabs.tools": "Công cụ",
|
||||
"tabs.transform": "Chuyển đổi",
|
||||
"tabs.unconfiguredStartDisabledTip": "Một nút bắt đầu chưa được cấu hình đã được thêm vào canvas. Vui lòng hoàn tất thiết lập trước khi tiếp tục.",
|
||||
"tabs.usePlugin": "Chọn công cụ",
|
||||
"tabs.utilities": "Tiện ích",
|
||||
"tabs.workflowTool": "Quy trình làm việc",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "循环",
|
||||
"blocks.loop-end": "退出循环",
|
||||
"blocks.loop-start": "循环开始",
|
||||
"blocks.mostCommon": "最常用",
|
||||
"blocks.originalStartNode": "原始开始节点",
|
||||
"blocks.parameter-extractor": "参数提取器",
|
||||
"blocks.question-classifier": "问题分类器",
|
||||
"blocks.start": "用户输入",
|
||||
"blocks.start-placeholder": "工作流开始",
|
||||
"blocks.template-transform": "模板转换",
|
||||
"blocks.tool": "工具",
|
||||
"blocks.trigger-plugin": "插件触发器",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "利用 LLM 从自然语言内推理提取出结构化参数,用于后置的工具调用或 HTTP 请求。",
|
||||
"blocksAbout.question-classifier": "定义用户问题的分类条件,LLM 能够根据分类描述定义对话的进展方式",
|
||||
"blocksAbout.start": "定义一个 workflow 流程启动的初始参数",
|
||||
"blocksAbout.start-placeholder": "选择这个 workflow 的启动方式",
|
||||
"blocksAbout.template-transform": "使用 Jinja 模板语法将数据转换为字符串",
|
||||
"blocksAbout.tool": "使用外部工具扩展工作流功能",
|
||||
"blocksAbout.trigger-plugin": "从外部平台事件启动工作流的第三方集成触发器",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "消息类型",
|
||||
"nodes.start.outputVars.query": "用户输入",
|
||||
"nodes.start.required": "必填",
|
||||
"nodes.start.userInputTipDescription": "定义当 workflow 按需启动时需要向终端用户收集的输入。",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "在 Marketplace 浏览更多",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "在 Marketplace 查找更多工具",
|
||||
"nodes.startPlaceholder.noTriggersFound": "未找到触发器",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "点击配置开始节点",
|
||||
"nodes.startPlaceholder.nodeDescription": "从右侧面板选择开始节点",
|
||||
"nodes.startPlaceholder.nodeTitle": "工作流开始",
|
||||
"nodes.startPlaceholder.panelDescription": "开始节点定义 workflow 的触发方式",
|
||||
"nodes.startPlaceholder.panelTitle": "选择开始节点",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "用户输入不能和其他触发器组合使用",
|
||||
"nodes.startPlaceholder.validationRequired": "请先选择开始节点。",
|
||||
"nodes.templateTransform.code": "代码",
|
||||
"nodes.templateTransform.codeSupportTip": "只支持 Jinja2",
|
||||
"nodes.templateTransform.inputVars": "输入变量",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "数据源",
|
||||
"tabs.start": "开始",
|
||||
"tabs.startDisabledTip": "触发节点与用户输入节点互斥。",
|
||||
"tabs.startDisabledTipLearnMore": "了解更多开始节点",
|
||||
"tabs.startNotSupportedTip": "Snippet 暂不支持 Start 标签。",
|
||||
"tabs.tools": "工具",
|
||||
"tabs.transform": "转换",
|
||||
"tabs.unconfiguredStartDisabledTip": "画布上已有未配置的开始节点。请先完成设置后再继续。",
|
||||
"tabs.usePlugin": "选择工具",
|
||||
"tabs.utilities": "工具",
|
||||
"tabs.workflowTool": "工作流",
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
"blocks.loop": "循環",
|
||||
"blocks.loop-end": "退出循環",
|
||||
"blocks.loop-start": "循環開始",
|
||||
"blocks.mostCommon": "最常用",
|
||||
"blocks.originalStartNode": "原始起始節點",
|
||||
"blocks.parameter-extractor": "參數提取器",
|
||||
"blocks.question-classifier": "問題分類器",
|
||||
"blocks.start": "開始",
|
||||
"blocks.start-placeholder": "工作流程開始",
|
||||
"blocks.template-transform": "模板轉換",
|
||||
"blocks.tool": "工具",
|
||||
"blocks.trigger-plugin": "插件觸發器",
|
||||
@ -53,6 +55,7 @@
|
||||
"blocksAbout.parameter-extractor": "利用 LLM 從自然語言內推理提取出結構化參數,用於後置的工具調用或 HTTP 請求。",
|
||||
"blocksAbout.question-classifier": "定義用戶問題的分類條件,LLM 能夠根據分類描述定義對話的進展方式",
|
||||
"blocksAbout.start": "定義一個 workflow 流程啟動的參數",
|
||||
"blocksAbout.start-placeholder": "選擇此工作流程的啟動方式",
|
||||
"blocksAbout.template-transform": "使用 Jinja 模板語法將資料轉換為字符串",
|
||||
"blocksAbout.tool": "使用外部工具來擴展工作流程功能",
|
||||
"blocksAbout.trigger-plugin": "第三方整合觸發器,從外部平台事件啟動工作流程",
|
||||
@ -934,6 +937,17 @@
|
||||
"nodes.start.outputVars.memories.type": "消息類型",
|
||||
"nodes.start.outputVars.query": "用戶輸入",
|
||||
"nodes.start.required": "必填",
|
||||
"nodes.start.userInputTipDescription": "定義工作流程按需啟動時要向終端使用者收集的輸入。",
|
||||
"nodes.startPlaceholder.browseMoreOnMarketplace": "在 Marketplace 瀏覽更多",
|
||||
"nodes.startPlaceholder.findMoreToolsInMarketplace": "在 Marketplace 中尋找更多工具",
|
||||
"nodes.startPlaceholder.noTriggersFound": "找不到觸發器",
|
||||
"nodes.startPlaceholder.nodeCollapsedDescription": "點擊以設定開始節點",
|
||||
"nodes.startPlaceholder.nodeDescription": "從右側面板選擇開始節點",
|
||||
"nodes.startPlaceholder.nodeTitle": "工作流程開始",
|
||||
"nodes.startPlaceholder.panelDescription": "開始節點定義工作流程的觸發方式",
|
||||
"nodes.startPlaceholder.panelTitle": "選擇開始節點",
|
||||
"nodes.startPlaceholder.userInputConflictTip": "使用者輸入不能與其他觸發器組合使用",
|
||||
"nodes.startPlaceholder.validationRequired": "請先選擇開始節點。",
|
||||
"nodes.templateTransform.code": "模板程式碼",
|
||||
"nodes.templateTransform.codeSupportTip": "只支持 Jinja2",
|
||||
"nodes.templateTransform.inputVars": "輸入變數",
|
||||
@ -1209,9 +1223,11 @@
|
||||
"tabs.sources": "來源",
|
||||
"tabs.start": "開始",
|
||||
"tabs.startDisabledTip": "觸發節點與使用者輸入節點是互斥的。",
|
||||
"tabs.startDisabledTipLearnMore": "了解更多開始節點",
|
||||
"tabs.startNotSupportedTip": "代码段中不支持“开始”选项卡。",
|
||||
"tabs.tools": "工具",
|
||||
"tabs.transform": "轉換",
|
||||
"tabs.unconfiguredStartDisabledTip": "畫布上已有未設定的開始節點。請先完成設定後再繼續。",
|
||||
"tabs.usePlugin": "選取工具",
|
||||
"tabs.utilities": "工具",
|
||||
"tabs.workflowTool": "工作流",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user