mirror of https://github.com/langgenius/dify.git
feat: replace mock data with dynamic workflow options in test run dropdown (#24320)
This commit is contained in:
parent
833c902b2b
commit
392e3530bf
|
|
@ -0,0 +1,487 @@
|
|||
/**
|
||||
* useDynamicTestRunOptions Hook Test
|
||||
*
|
||||
* Tests for the dynamic test run options generation hook that replaces mock data
|
||||
* with real workflow node data for the test run dropdown.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import type { Node } from 'reactflow'
|
||||
import { useDynamicTestRunOptions } from '@/app/components/workflow/hooks/use-dynamic-test-run-options'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
|
||||
// Mock react-i18next
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'workflow.blocks.start': 'User Input',
|
||||
'workflow.blocks.trigger-schedule': 'Schedule Trigger',
|
||||
'workflow.blocks.trigger-webhook': 'Webhook Trigger',
|
||||
'workflow.blocks.trigger-plugin': 'Plugin Trigger',
|
||||
'workflow.common.runAllTriggers': 'Run all triggers',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock reactflow
|
||||
const mockNodes: Node[] = []
|
||||
jest.mock('reactflow', () => ({
|
||||
useNodes: () => mockNodes,
|
||||
}))
|
||||
|
||||
// Mock workflow store
|
||||
const mockStore = {
|
||||
buildInTools: [
|
||||
{
|
||||
id: 'builtin-tool-1',
|
||||
name: 'Built-in Tool',
|
||||
icon: 'builtin-icon.png',
|
||||
},
|
||||
],
|
||||
customTools: [
|
||||
{
|
||||
id: 'custom-tool-1',
|
||||
name: 'Custom Tool',
|
||||
icon: { content: 'custom-icon', background: '#fff' },
|
||||
},
|
||||
],
|
||||
workflowTools: [
|
||||
{
|
||||
id: 'workflow-tool-1',
|
||||
name: 'Workflow Tool',
|
||||
icon: 'workflow-icon.png',
|
||||
},
|
||||
],
|
||||
mcpTools: [
|
||||
{
|
||||
id: 'mcp-tool-1',
|
||||
name: 'MCP Tool',
|
||||
icon: 'mcp-icon.png',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
jest.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: any) => selector(mockStore),
|
||||
}))
|
||||
|
||||
// Mock utils
|
||||
jest.mock('@/utils', () => ({
|
||||
canFindTool: (toolId: string, providerId: string) => toolId === providerId,
|
||||
}))
|
||||
|
||||
// Mock useGetIcon
|
||||
jest.mock('@/app/components/plugins/install-plugin/base/use-get-icon', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
getIconUrl: (icon: string) => `https://example.com/icons/${icon}`,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow entry utils
|
||||
jest.mock('@/app/components/workflow/utils/workflow-entry', () => ({
|
||||
getWorkflowEntryNode: (nodes: Node[]) => {
|
||||
return nodes.find(node => node.data.type === BlockEnum.Start) || null
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock icon components
|
||||
jest.mock('@/app/components/base/icons/src/vender/workflow/Home', () => {
|
||||
return function MockHome({ className }: { className: string }) {
|
||||
return <div data-testid="home-icon" className={className}>Home</div>
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/app/components/base/icons/src/vender/workflow', () => ({
|
||||
Schedule: function MockSchedule({ className }: { className: string }) {
|
||||
return <div data-testid="schedule-icon" className={className}>Schedule</div>
|
||||
},
|
||||
WebhookLine: function MockWebhookLine({ className }: { className: string }) {
|
||||
return <div data-testid="webhook-icon" className={className}>Webhook</div>
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/base/app-icon', () => {
|
||||
return function MockAppIcon({ icon, background, className }: any) {
|
||||
return (
|
||||
<div
|
||||
data-testid="app-icon"
|
||||
className={className}
|
||||
data-icon={icon}
|
||||
data-background={background}
|
||||
>
|
||||
AppIcon
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('useDynamicTestRunOptions', () => {
|
||||
beforeEach(() => {
|
||||
// Clear mock nodes before each test
|
||||
mockNodes.length = 0
|
||||
})
|
||||
|
||||
describe('Empty workflow', () => {
|
||||
it('should return empty options when no nodes exist', () => {
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.userInput).toBeUndefined()
|
||||
expect(result.current.triggers).toEqual([])
|
||||
expect(result.current.runAll).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Start node handling', () => {
|
||||
it('should create user input option from Start node', () => {
|
||||
mockNodes.push({
|
||||
id: 'start-1',
|
||||
type: 'start',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
title: 'Custom Start',
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.userInput).toEqual({
|
||||
id: 'start-1',
|
||||
type: 'user_input',
|
||||
name: 'Custom Start',
|
||||
icon: expect.any(Object),
|
||||
nodeId: 'start-1',
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should use fallback translation when Start node has no title', () => {
|
||||
mockNodes.push({
|
||||
id: 'start-1',
|
||||
type: 'start',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.userInput?.name).toBe('User Input')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Trigger nodes handling', () => {
|
||||
it('should create schedule trigger option', () => {
|
||||
mockNodes.push({
|
||||
id: 'schedule-1',
|
||||
type: 'trigger-schedule',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.TriggerSchedule,
|
||||
title: 'Daily Schedule',
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.triggers).toHaveLength(1)
|
||||
expect(result.current.triggers[0]).toEqual({
|
||||
id: 'schedule-1',
|
||||
type: 'schedule',
|
||||
name: 'Daily Schedule',
|
||||
icon: expect.any(Object),
|
||||
nodeId: 'schedule-1',
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create webhook trigger option', () => {
|
||||
mockNodes.push({
|
||||
id: 'webhook-1',
|
||||
type: 'trigger-webhook',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.TriggerWebhook,
|
||||
title: 'API Webhook',
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.triggers).toHaveLength(1)
|
||||
expect(result.current.triggers[0]).toEqual({
|
||||
id: 'webhook-1',
|
||||
type: 'webhook',
|
||||
name: 'API Webhook',
|
||||
icon: expect.any(Object),
|
||||
nodeId: 'webhook-1',
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create plugin trigger option with built-in tool', () => {
|
||||
mockNodes.push({
|
||||
id: 'plugin-1',
|
||||
type: 'trigger-plugin',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.TriggerPlugin,
|
||||
title: 'Plugin Trigger',
|
||||
provider_id: 'builtin-tool-1',
|
||||
provider_type: CollectionType.builtIn,
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.triggers).toHaveLength(1)
|
||||
expect(result.current.triggers[0]).toEqual({
|
||||
id: 'plugin-1',
|
||||
type: 'plugin',
|
||||
name: 'Plugin Trigger',
|
||||
icon: expect.any(Object),
|
||||
nodeId: 'plugin-1',
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should create plugin trigger option with custom tool', () => {
|
||||
mockNodes.push({
|
||||
id: 'plugin-2',
|
||||
type: 'trigger-plugin',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.TriggerPlugin,
|
||||
title: 'Custom Plugin',
|
||||
provider_id: 'custom-tool-1',
|
||||
provider_type: CollectionType.custom,
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.triggers).toHaveLength(1)
|
||||
expect(result.current.triggers[0].icon).toBeDefined()
|
||||
})
|
||||
|
||||
it('should create plugin trigger option with fallback icon when tool not found', () => {
|
||||
mockNodes.push({
|
||||
id: 'plugin-3',
|
||||
type: 'trigger-plugin',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.TriggerPlugin,
|
||||
title: 'Unknown Plugin',
|
||||
provider_id: 'unknown-tool',
|
||||
provider_type: CollectionType.builtIn,
|
||||
},
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.triggers).toHaveLength(1)
|
||||
expect(result.current.triggers[0].name).toBe('Unknown Plugin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Run all triggers option', () => {
|
||||
it('should create runAll option when multiple triggers exist', () => {
|
||||
mockNodes.push(
|
||||
{
|
||||
id: 'schedule-1',
|
||||
type: 'trigger-schedule',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { type: BlockEnum.TriggerSchedule, title: 'Schedule 1' },
|
||||
},
|
||||
{
|
||||
id: 'webhook-1',
|
||||
type: 'trigger-webhook',
|
||||
position: { x: 100, y: 0 },
|
||||
data: { type: BlockEnum.TriggerWebhook, title: 'Webhook 1' },
|
||||
},
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.runAll).toEqual({
|
||||
id: 'run-all',
|
||||
type: 'all',
|
||||
name: 'Run all triggers',
|
||||
icon: expect.any(Object),
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not create runAll option when only one trigger exists', () => {
|
||||
mockNodes.push({
|
||||
id: 'schedule-1',
|
||||
type: 'trigger-schedule',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { type: BlockEnum.TriggerSchedule, title: 'Schedule 1' },
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.runAll).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not create runAll option when no triggers exist', () => {
|
||||
mockNodes.push({
|
||||
id: 'start-1',
|
||||
type: 'start',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { type: BlockEnum.Start, title: 'Start' },
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.runAll).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Complex workflow scenarios', () => {
|
||||
it('should handle workflow with all node types', () => {
|
||||
mockNodes.push(
|
||||
{
|
||||
id: 'start-1',
|
||||
type: 'start',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { type: BlockEnum.Start, title: 'User Input' },
|
||||
},
|
||||
{
|
||||
id: 'schedule-1',
|
||||
type: 'trigger-schedule',
|
||||
position: { x: 100, y: 0 },
|
||||
data: { type: BlockEnum.TriggerSchedule, title: 'Schedule Trigger' },
|
||||
},
|
||||
{
|
||||
id: 'webhook-1',
|
||||
type: 'trigger-webhook',
|
||||
position: { x: 200, y: 0 },
|
||||
data: { type: BlockEnum.TriggerWebhook, title: 'Webhook Trigger' },
|
||||
},
|
||||
{
|
||||
id: 'plugin-1',
|
||||
type: 'trigger-plugin',
|
||||
position: { x: 300, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.TriggerPlugin,
|
||||
title: 'Plugin Trigger',
|
||||
provider_id: 'builtin-tool-1',
|
||||
provider_type: CollectionType.builtIn,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.userInput).toBeDefined()
|
||||
expect(result.current.triggers).toHaveLength(3)
|
||||
expect(result.current.runAll).toBeDefined()
|
||||
|
||||
// Verify node ID mapping for future click functionality
|
||||
expect(result.current.userInput?.nodeId).toBe('start-1')
|
||||
expect(result.current.triggers[0].nodeId).toBe('schedule-1')
|
||||
expect(result.current.triggers[1].nodeId).toBe('webhook-1')
|
||||
expect(result.current.triggers[2].nodeId).toBe('plugin-1')
|
||||
})
|
||||
|
||||
it('should handle nodes with missing data gracefully', () => {
|
||||
mockNodes.push(
|
||||
{
|
||||
id: 'invalid-1',
|
||||
type: 'unknown',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {}, // Missing type
|
||||
},
|
||||
{
|
||||
id: 'start-1',
|
||||
type: 'start',
|
||||
position: { x: 100, y: 0 },
|
||||
data: { type: BlockEnum.Start },
|
||||
},
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
// Should only process valid nodes
|
||||
expect(result.current.userInput).toBeDefined()
|
||||
expect(result.current.triggers).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should use workflow entry node as fallback when no direct Start node exists', () => {
|
||||
// No Start node, but workflow entry utility finds a fallback
|
||||
mockNodes.push({
|
||||
id: 'fallback-1',
|
||||
type: 'other',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { type: BlockEnum.Start, title: 'Fallback Start' },
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
expect(result.current.userInput).toBeDefined()
|
||||
expect(result.current.userInput?.nodeId).toBe('fallback-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Node ID mapping for click functionality', () => {
|
||||
it('should ensure all options have nodeId for proper click handling', () => {
|
||||
mockNodes.push(
|
||||
{
|
||||
id: 'start-node-123',
|
||||
type: 'start',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { type: BlockEnum.Start, title: 'Start' },
|
||||
},
|
||||
{
|
||||
id: 'trigger-node-456',
|
||||
type: 'trigger-schedule',
|
||||
position: { x: 100, y: 0 },
|
||||
data: { type: BlockEnum.TriggerSchedule, title: 'Schedule' },
|
||||
},
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
// Verify node ID mapping exists for click functionality
|
||||
expect(result.current.userInput?.nodeId).toBe('start-node-123')
|
||||
expect(result.current.triggers[0].nodeId).toBe('trigger-node-456')
|
||||
|
||||
// runAll doesn't need nodeId as it handles multiple nodes
|
||||
expect(result.current.runAll?.nodeId).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Icon rendering verification', () => {
|
||||
it('should render proper icon components for each node type', () => {
|
||||
mockNodes.push(
|
||||
{
|
||||
id: 'start-1',
|
||||
type: 'start',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { type: BlockEnum.Start },
|
||||
},
|
||||
{
|
||||
id: 'schedule-1',
|
||||
type: 'trigger-schedule',
|
||||
position: { x: 100, y: 0 },
|
||||
data: { type: BlockEnum.TriggerSchedule },
|
||||
},
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useDynamicTestRunOptions())
|
||||
|
||||
// Icons should be React elements
|
||||
expect(React.isValidElement(result.current.userInput?.icon)).toBe(true)
|
||||
expect(React.isValidElement(result.current.triggers[0]?.icon)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.34698 6.42505C5.10275 5.79268 5.67045 5.17005 6.29816 5.30916L6.42446 5.34758L13.0846 7.92049L13.1999 7.97518C13.7051 8.26089 13.7647 8.9802 13.3118 9.34432L13.207 9.41659L10.8196 10.8202L9.416 13.2076C9.08465 13.7711 8.28069 13.742 7.97459 13.2004L7.9199 13.0852L5.34698 6.42505ZM8.791 11.6392L9.73631 10.0325L9.73696 10.0318L9.7962 9.94458C9.86055 9.86164 9.94031 9.79125 10.0312 9.73755L10.0319 9.7369L11.6387 8.79159L6.99738 6.99797L8.791 11.6392Z" fill="white"/>
|
||||
<path d="M2.79751 8.9257C3.05781 8.66539 3.47985 8.66547 3.74021 8.9257C4.00057 9.18604 4.00056 9.60805 3.74021 9.86841L3.03318 10.5754C2.77283 10.8356 2.35078 10.8357 2.09047 10.5754C1.83032 10.3151 1.83033 9.89305 2.09047 9.63273L2.79751 8.9257Z" fill="white"/>
|
||||
<path d="M1.99998 5.66659C2.36817 5.66659 2.66665 5.96506 2.66665 6.33325C2.66665 6.70144 2.36817 6.99992 1.99998 6.99992H0.99998C0.63179 6.99992 0.333313 6.70144 0.333313 6.33325C0.333313 5.96506 0.63179 5.66659 0.99998 5.66659H1.99998Z" fill="white"/>
|
||||
<path d="M9.63279 2.09106C9.8931 1.83077 10.3151 1.83086 10.5755 2.09106C10.8358 2.35142 10.8359 2.77343 10.5755 3.03377L9.86847 3.7408C9.6081 4.00098 9.18605 4.0011 8.92576 3.7408C8.66559 3.4805 8.66562 3.05841 8.92576 2.7981L9.63279 2.09106Z" fill="white"/>
|
||||
<path d="M2.09113 2.09041C2.33521 1.84649 2.72126 1.83132 2.98305 2.04484L3.03383 2.09041L3.74087 2.79744L3.78644 2.84823C3.9999 3.11002 3.98476 3.49609 3.74087 3.74015C3.49682 3.9842 3.11079 3.9992 2.84894 3.78573L2.79816 3.74015L2.09113 3.03312L2.04555 2.98234C1.83199 2.72049 1.84705 2.33449 2.09113 2.09041Z" fill="white"/>
|
||||
<path d="M6.33331 0.333252C6.7015 0.333252 6.99998 0.631729 6.99998 0.999919V1.99992C6.99998 2.36811 6.7015 2.66659 6.33331 2.66659C5.96512 2.66659 5.66665 2.36811 5.66665 1.99992V0.999919C5.66665 0.631729 5.96512 0.333252 6.33331 0.333252Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "14",
|
||||
"height": "14",
|
||||
"viewBox": "0 0 14 14",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M5.34698 6.42505C5.10275 5.79268 5.67045 5.17005 6.29816 5.30916L6.42446 5.34758L13.0846 7.92049L13.1999 7.97518C13.7051 8.26089 13.7647 8.9802 13.3118 9.34432L13.207 9.41659L10.8196 10.8202L9.416 13.2076C9.08465 13.7711 8.28069 13.742 7.97459 13.2004L7.9199 13.0852L5.34698 6.42505ZM8.791 11.6392L9.73631 10.0325L9.73696 10.0318L9.7962 9.94458C9.86055 9.86164 9.94031 9.79125 10.0312 9.73755L10.0319 9.7369L11.6387 8.79159L6.99738 6.99797L8.791 11.6392Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M2.79751 8.9257C3.05781 8.66539 3.47985 8.66547 3.74021 8.9257C4.00057 9.18604 4.00056 9.60805 3.74021 9.86841L3.03318 10.5754C2.77283 10.8356 2.35078 10.8357 2.09047 10.5754C1.83032 10.3151 1.83033 9.89305 2.09047 9.63273L2.79751 8.9257Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M1.99998 5.66659C2.36817 5.66659 2.66665 5.96506 2.66665 6.33325C2.66665 6.70144 2.36817 6.99992 1.99998 6.99992H0.99998C0.63179 6.99992 0.333313 6.70144 0.333313 6.33325C0.333313 5.96506 0.63179 5.66659 0.99998 5.66659H1.99998Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M9.63279 2.09106C9.8931 1.83077 10.3151 1.83086 10.5755 2.09106C10.8358 2.35142 10.8359 2.77343 10.5755 3.03377L9.86847 3.7408C9.6081 4.00098 9.18605 4.0011 8.92576 3.7408C8.66559 3.4805 8.66562 3.05841 8.92576 2.7981L9.63279 2.09106Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M2.09113 2.09041C2.33521 1.84649 2.72126 1.83132 2.98305 2.04484L3.03383 2.09041L3.74087 2.79744L3.78644 2.84823C3.9999 3.11002 3.98476 3.49609 3.74087 3.74015C3.49682 3.9842 3.11079 3.9992 2.84894 3.78573L2.79816 3.74015L2.09113 3.03312L2.04555 2.98234C1.83199 2.72049 1.84705 2.33449 2.09113 2.09041Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M6.33331 0.333252C6.7015 0.333252 6.99998 0.631729 6.99998 0.999919V1.99992C6.99998 2.36811 6.7015 2.66659 6.33331 2.66659C5.96512 2.66659 5.66665 2.36811 5.66665 1.99992V0.999919C5.66665 0.631729 5.96512 0.333252 6.33331 0.333252Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "RunAllTriggers"
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './RunAllTriggers.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'RunAllTriggers'
|
||||
|
||||
export default Icon
|
||||
|
|
@ -19,6 +19,7 @@ export { default as LoopEnd } from './LoopEnd'
|
|||
export { default as Loop } from './Loop'
|
||||
export { default as ParameterExtractor } from './ParameterExtractor'
|
||||
export { default as QuestionClassifier } from './QuestionClassifier'
|
||||
export { default as RunAllTriggers } from './RunAllTriggers'
|
||||
export { default as Schedule } from './Schedule'
|
||||
export { default as TemplatingTransform } from './TemplatingTransform'
|
||||
export { default as VariableX } from './VariableX'
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const AllStartBlocks = ({
|
|||
<StartBlocks
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={ENTRY_NODE_TYPES as BlockEnum[]}
|
||||
availableBlocksTypes={ENTRY_NODE_TYPES as unknown as BlockEnum[]}
|
||||
/>
|
||||
|
||||
<TriggerPluginSelector
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import {
|
|||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BlockIcon from '../block-icon'
|
||||
import type { BlockEnum } from '../types'
|
||||
import type { BlockEnum, CommonNodeType } from '../types'
|
||||
import { BlockEnum as BlockEnumValues } from '../types'
|
||||
import { useNodesExtraData } from '../hooks'
|
||||
import { START_BLOCKS } from './constants'
|
||||
|
|
@ -24,10 +25,18 @@ const StartBlocks = ({
|
|||
availableBlocksTypes = [],
|
||||
}: StartBlocksProps) => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
|
||||
const filteredBlocks = useMemo(() => {
|
||||
// Check if Start node already exists in workflow
|
||||
const hasStartNode = nodes.some(node => (node.data as CommonNodeType)?.type === BlockEnumValues.Start)
|
||||
|
||||
return START_BLOCKS.filter((block) => {
|
||||
// Hide User Input (Start) if it already exists in workflow
|
||||
if (block.type === BlockEnumValues.Start && hasStartNode)
|
||||
return false
|
||||
|
||||
// Filter by search text
|
||||
if (!block.title.toLowerCase().includes(searchText.toLowerCase()))
|
||||
return false
|
||||
|
|
@ -35,7 +44,7 @@ const StartBlocks = ({
|
|||
// availableBlocksTypes now contains properly filtered entry node types from parent
|
||||
return availableBlocksTypes.includes(block.type)
|
||||
})
|
||||
}, [searchText, availableBlocksTypes])
|
||||
}, [searchText, availableBlocksTypes, nodes])
|
||||
|
||||
const isEmpty = filteredBlocks.length === 0
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ import {
|
|||
import { WorkflowRunningStatus } from '../types'
|
||||
import ViewHistory from './view-history'
|
||||
import Checklist from './checklist'
|
||||
import TestRunDropdown, { createMockOptions } from './test-run-dropdown'
|
||||
import TestRunDropdown from './test-run-dropdown'
|
||||
import type { TriggerOption } from './test-run-dropdown'
|
||||
import { useDynamicTestRunOptions } from '../hooks/use-dynamic-test-run-options'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
StopCircle,
|
||||
|
|
@ -32,14 +33,20 @@ const RunMode = memo(() => {
|
|||
const { handleStopRun } = useWorkflowRun()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running
|
||||
const dynamicOptions = useDynamicTestRunOptions()
|
||||
|
||||
const handleStop = () => {
|
||||
handleStopRun(workflowRunningData?.task_id || '')
|
||||
}
|
||||
|
||||
const handleTriggerSelect = (option: TriggerOption) => {
|
||||
console.log('Selected trigger:', option)
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
if (option.type === 'user_input') {
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
}
|
||||
else {
|
||||
// TODO: Implement trigger-specific execution logic for schedule, webhook, plugin types
|
||||
console.log('TODO: Handle trigger execution for type:', option.type, 'nodeId:', option.nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
|
@ -64,7 +71,7 @@ const RunMode = memo(() => {
|
|||
</div>
|
||||
)
|
||||
: (
|
||||
<TestRunDropdown options={createMockOptions()} onSelect={handleTriggerSelect}>
|
||||
<TestRunDropdown options={dynamicOptions} onSelect={handleTriggerSelect}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-7 items-center rounded-md px-2.5 text-[13px] font-medium text-components-button-secondary-accent-text',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Home from '@/app/components/base/icons/src/vender/workflow/Home'
|
||||
import Google from '@/app/components/base/icons/src/public/plugins/Google'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
|
|
@ -30,87 +28,6 @@ type TestRunDropdownProps = {
|
|||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const createMockOptions = (): TestRunOptions => {
|
||||
const userInput: TriggerOption = {
|
||||
id: 'user-input-1',
|
||||
type: 'user_input',
|
||||
name: 'User Input',
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded bg-util-colors-blue-brand-blue-brand-500">
|
||||
<Home className="h-4 w-4 text-text-primary-on-surface" />
|
||||
</div>
|
||||
),
|
||||
nodeId: 'start-node-1',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const runAll: TriggerOption = {
|
||||
id: 'run-all',
|
||||
type: 'all',
|
||||
name: 'Run all triggers',
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded bg-util-colors-purple-purple-500">
|
||||
<svg className="h-4 w-4 text-text-primary-on-surface" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
</div>
|
||||
),
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const triggers: TriggerOption[] = [
|
||||
{
|
||||
id: 'slack-trigger-1',
|
||||
type: 'plugin',
|
||||
name: 'Slack Trigger',
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center">
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52-2.523c0-1.393 1.125-2.528 2.52-2.528h2.52v2.528c0 1.393-1.125 2.523-2.52 2.523zM6.313 17c0-1.393 1.125-2.528 2.52-2.528s2.52 1.135 2.52 2.528v6.315c0 1.393-1.125 2.528-2.52 2.528s-2.52-1.135-2.52-2.528V17z" fill="#e01e5a"/>
|
||||
<path d="M8.835 5.042a2.528 2.528 0 0 1-2.523-2.52C6.312 1.127 7.447.002 8.835.002s2.523 1.125 2.523 2.52v2.52H8.835zM17 6.313c1.393 0 2.528 1.125 2.528 2.52s-1.135 2.52-2.528 2.52H10.685c-1.393 0-2.528-1.125-2.528-2.52s1.135-2.52 2.528-2.52H17z" fill="#36c5f0"/>
|
||||
<path d="M18.958 8.835a2.528 2.528 0 0 1 2.52-2.523c1.393 0 2.528 1.125 2.528 2.523s-1.125 2.523-2.528 2.523h-2.52V8.835zM17.687 17c0-1.393-1.125-2.528-2.52-2.528s-2.52 1.135-2.52 2.528v6.315c0 1.393 1.125 2.528 2.52 2.528s2.52-1.135 2.52-2.528V17z" fill="#2eb67d"/>
|
||||
<path d="M15.165 18.958a2.528 2.528 0 0 1 2.523-2.52c1.393 0 2.528 1.125 2.528 2.52s-1.125 2.523-2.528 2.523h-2.523v-2.523zM7 17.687c-1.393 0-2.528-1.125-2.528-2.52s1.135-2.sk 2.528-2.52h6.315c1.393 0 2.528 1.125 2.528 2.sk s-1.135 2.sk-2.528 2.sk H7z" fill="#ecb22e"/>
|
||||
</svg>
|
||||
</div>
|
||||
),
|
||||
nodeId: 'slack-trigger-1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'zapier-trigger-1',
|
||||
type: 'plugin',
|
||||
name: 'Zapier Trigger',
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded bg-util-colors-orange-orange-500">
|
||||
<svg className="h-4 w-4 text-text-primary-on-surface" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0L8.25 8.25H0l6.75 6.75L3 24l9-6.75L21 24l-3.75-9L24 8.25h-8.25L12 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
),
|
||||
nodeId: 'zapier-trigger-1',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'gmail-trigger-1',
|
||||
type: 'plugin',
|
||||
name: 'Gmail Sender',
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded bg-components-panel-bg">
|
||||
<Google className="h-5 w-5" />
|
||||
</div>
|
||||
),
|
||||
nodeId: 'gmail-trigger-1',
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
userInput,
|
||||
triggers,
|
||||
runAll: triggers.length > 1 ? runAll : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const TestRunDropdown: FC<TestRunDropdownProps> = ({
|
||||
options,
|
||||
onSelect,
|
||||
|
|
@ -130,11 +47,13 @@ const TestRunDropdown: FC<TestRunDropdownProps> = ({
|
|||
className='system-md-regular flex cursor-pointer items-center rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => handleSelect(option)}
|
||||
>
|
||||
<div className='flex items-center space-x-3'>
|
||||
{option.icon}
|
||||
<span>{option.name}</span>
|
||||
<div className='flex min-w-0 flex-1 items-center'>
|
||||
<div className='flex h-6 w-6 shrink-0 items-center justify-center'>
|
||||
{option.icon}
|
||||
</div>
|
||||
<span className='ml-2 truncate'>{option.name}</span>
|
||||
</div>
|
||||
<div className='ml-auto flex h-4 w-4 items-center justify-center rounded bg-state-base-hover-alt text-xs font-medium text-text-tertiary'>
|
||||
<div className='ml-2 flex h-4 w-4 shrink-0 items-center justify-center rounded bg-state-base-hover-alt text-xs font-medium text-text-tertiary'>
|
||||
{numberDisplay}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -158,8 +77,8 @@ const TestRunDropdown: FC<TestRunDropdownProps> = ({
|
|||
{children}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<div className='w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-1 shadow-lg'>
|
||||
<PortalToFollowElemContent className='z-[12]'>
|
||||
<div className='w-[284px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-1 shadow-lg'>
|
||||
<div className='mb-2 px-3 pt-2 text-sm font-medium text-text-primary'>
|
||||
{t('workflow.common.chooseStartNodeToRun')}
|
||||
</div>
|
||||
|
|
@ -182,5 +101,4 @@ const TestRunDropdown: FC<TestRunDropdownProps> = ({
|
|||
)
|
||||
}
|
||||
|
||||
export { createMockOptions }
|
||||
export default TestRunDropdown
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
import { useMemo } from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum, type CommonNodeType } from '../types'
|
||||
import { getWorkflowEntryNode } from '../utils/workflow-entry'
|
||||
import type { TestRunOptions, TriggerOption } from '../header/test-run-dropdown'
|
||||
import Home from '@/app/components/base/icons/src/vender/workflow/Home'
|
||||
import { RunAllTriggers, Schedule, WebhookLine } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useStore } from '../store'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
|
||||
export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const mcpTools = useStore(s => s.mcpTools)
|
||||
const { getIconUrl } = useGetIcon()
|
||||
|
||||
return useMemo(() => {
|
||||
const allTriggers: TriggerOption[] = []
|
||||
let userInput: TriggerOption | undefined
|
||||
|
||||
for (const node of nodes) {
|
||||
const nodeData = node.data as CommonNodeType
|
||||
|
||||
if (!nodeData?.type) continue
|
||||
|
||||
if (nodeData.type === BlockEnum.Start) {
|
||||
userInput = {
|
||||
id: node.id,
|
||||
type: 'user_input',
|
||||
name: nodeData.title || t('workflow.blocks.start'),
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 bg-util-colors-blue-brand-blue-brand-500 text-white shadow-md">
|
||||
<Home className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
),
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
else if (nodeData.type === BlockEnum.TriggerSchedule) {
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: 'schedule',
|
||||
name: nodeData.title || t('workflow.blocks.trigger-schedule'),
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 bg-util-colors-violet-violet-500 text-white shadow-md">
|
||||
<Schedule className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
),
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
else if (nodeData.type === BlockEnum.TriggerWebhook) {
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: 'webhook',
|
||||
name: nodeData.title || t('workflow.blocks.trigger-webhook'),
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 bg-util-colors-blue-blue-500 text-white shadow-md">
|
||||
<WebhookLine className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
),
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
else if (nodeData.type === BlockEnum.TriggerPlugin) {
|
||||
let icon
|
||||
let toolIcon: string | any
|
||||
|
||||
// 按照 use-workflow-search.tsx 的模式获取工具图标
|
||||
if (nodeData.provider_id) {
|
||||
let targetTools = workflowTools
|
||||
if (nodeData.provider_type === CollectionType.builtIn)
|
||||
targetTools = buildInTools
|
||||
else if (nodeData.provider_type === CollectionType.custom)
|
||||
targetTools = customTools
|
||||
else if (nodeData.provider_type === CollectionType.mcp)
|
||||
targetTools = mcpTools
|
||||
|
||||
toolIcon = targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, nodeData.provider_id!))?.icon
|
||||
}
|
||||
|
||||
if (typeof toolIcon === 'string') {
|
||||
const iconUrl = toolIcon.startsWith('http') ? toolIcon : getIconUrl(toolIcon)
|
||||
icon = (
|
||||
<div
|
||||
className="bg-util-colors-white-white-500 flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 text-white shadow-md"
|
||||
style={{
|
||||
backgroundImage: `url(${iconUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else if (toolIcon && typeof toolIcon === 'object' && 'content' in toolIcon) {
|
||||
icon = (
|
||||
<AppIcon
|
||||
className="!h-6 !w-6 rounded-lg border-[0.5px] border-white/2 shadow-md"
|
||||
size="tiny"
|
||||
icon={toolIcon.content}
|
||||
background={toolIcon.background}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else {
|
||||
icon = (
|
||||
<div className="bg-util-colors-white-white-500 flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 text-white shadow-md">
|
||||
<span className="text-xs font-medium text-text-tertiary">P</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: 'plugin',
|
||||
name: nodeData.title || (nodeData as any).plugin_name || t('workflow.blocks.trigger-plugin'),
|
||||
icon,
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!userInput) {
|
||||
const startNode = getWorkflowEntryNode(nodes as any[])
|
||||
if (startNode && startNode.data?.type === BlockEnum.Start) {
|
||||
userInput = {
|
||||
id: startNode.id,
|
||||
type: 'user_input',
|
||||
name: (startNode.data as CommonNodeType)?.title || t('workflow.blocks.start'),
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 bg-util-colors-blue-brand-blue-brand-500 text-white shadow-md">
|
||||
<Home className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
),
|
||||
nodeId: startNode.id,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const runAll: TriggerOption | undefined = allTriggers.length > 1 ? {
|
||||
id: 'run-all',
|
||||
type: 'all',
|
||||
name: t('workflow.common.runAllTriggers'),
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 bg-util-colors-purple-purple-500 text-white shadow-md">
|
||||
<RunAllTriggers className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
),
|
||||
enabled: true,
|
||||
} : undefined
|
||||
|
||||
return {
|
||||
userInput,
|
||||
triggers: allTriggers,
|
||||
runAll,
|
||||
}
|
||||
}, [nodes, buildInTools, customTools, workflowTools, mcpTools, getIconUrl, t])
|
||||
}
|
||||
|
|
@ -11,7 +11,8 @@ const translation = {
|
|||
publishUpdate: 'Publish Update',
|
||||
run: 'Test Run',
|
||||
running: 'Running',
|
||||
chooseStartNodeToRun: 'Choose the entry node to run',
|
||||
chooseStartNodeToRun: 'Choose the start node to run',
|
||||
runAllTriggers: 'Run all triggers',
|
||||
inRunMode: 'In Run Mode',
|
||||
inPreview: 'In Preview',
|
||||
inPreviewMode: 'In Preview Mode',
|
||||
|
|
|
|||
Loading…
Reference in New Issue