mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 04:26:30 +08:00
feat: implement Schedule Trigger validation with multi-start node topology support (#24134)
This commit is contained in:
parent
d4ff1e031a
commit
f7bb3b852a
@ -131,19 +131,46 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorMessage || !validNodes.find(n => n.id === node.id)) {
|
// Start nodes and Trigger nodes should not show unConnected error if they have validation errors
|
||||||
|
// or if they are valid start nodes (even without incoming connections)
|
||||||
|
const isStartNode = node.data.type === BlockEnum.Start
|
||||||
|
|| node.data.type === BlockEnum.TriggerSchedule
|
||||||
|
|| node.data.type === BlockEnum.TriggerWebhook
|
||||||
|
|| node.data.type === BlockEnum.TriggerPlugin
|
||||||
|
|
||||||
|
const isUnconnected = !validNodes.find(n => n.id === node.id)
|
||||||
|
const shouldShowError = errorMessage || (isUnconnected && !isStartNode)
|
||||||
|
|
||||||
|
if (shouldShowError) {
|
||||||
list.push({
|
list.push({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: node.data.type,
|
type: node.data.type,
|
||||||
title: node.data.title,
|
title: node.data.title,
|
||||||
toolIcon,
|
toolIcon,
|
||||||
unConnected: !validNodes.find(n => n.id === node.id),
|
unConnected: isUnconnected && !isStartNode,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for start nodes (including triggers)
|
||||||
|
const startNodes = nodes.filter(node =>
|
||||||
|
node.data.type === BlockEnum.Start
|
||||||
|
|| node.data.type === BlockEnum.TriggerSchedule
|
||||||
|
|| node.data.type === BlockEnum.TriggerWebhook
|
||||||
|
|| node.data.type === BlockEnum.TriggerPlugin,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (startNodes.length === 0) {
|
||||||
|
list.push({
|
||||||
|
id: 'start-node-required',
|
||||||
|
type: BlockEnum.Start,
|
||||||
|
title: t('workflow.blocks.start'),
|
||||||
|
errorMessage: t('workflow.common.needStartNode'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) {
|
if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) {
|
||||||
list.push({
|
list.push({
|
||||||
id: 'answer-need-added',
|
id: 'answer-need-added',
|
||||||
@ -270,6 +297,18 @@ export const useChecklistBeforePublish = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startNodes = nodes.filter(node =>
|
||||||
|
node.data.type === BlockEnum.Start
|
||||||
|
|| node.data.type === BlockEnum.TriggerSchedule
|
||||||
|
|| node.data.type === BlockEnum.TriggerWebhook
|
||||||
|
|| node.data.type === BlockEnum.TriggerPlugin,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (startNodes.length === 0) {
|
||||||
|
notify({ type: 'error', message: t('workflow.common.needStartNode') })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) {
|
if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) {
|
||||||
notify({ type: 'error', message: t('workflow.common.needAnswerNode') })
|
notify({ type: 'error', message: t('workflow.common.needAnswerNode') })
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Schedule Trigger Node Default Tests
|
||||||
|
*
|
||||||
|
* Simple test for the Schedule Trigger node default configuration and validation.
|
||||||
|
* Tests core checkValid functionality following project patterns.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import nodeDefault from '../default'
|
||||||
|
import type { ScheduleTriggerNodeType } from '../types'
|
||||||
|
|
||||||
|
// Mock external dependencies
|
||||||
|
jest.mock('../utils/cron-parser', () => ({
|
||||||
|
isValidCronExpression: jest.fn((expr: string) => {
|
||||||
|
return expr === '0 9 * * 1' // Only this specific expression is valid
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../utils/execution-time-calculator', () => ({
|
||||||
|
getNextExecutionTimes: jest.fn(() => [new Date(Date.now() + 86400000)]),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Simple mock translation function
|
||||||
|
const mockT = (key: string, params?: any) => {
|
||||||
|
if (key.includes('fieldRequired')) return `${params?.field} is required`
|
||||||
|
if (key.includes('invalidCronExpression')) return 'Invalid cron expression'
|
||||||
|
if (key.includes('invalidTimezone')) return 'Invalid timezone'
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Schedule Trigger Node Default', () => {
|
||||||
|
describe('Basic Configuration', () => {
|
||||||
|
it('should have correct default value', () => {
|
||||||
|
expect(nodeDefault.defaultValue.mode).toBe('visual')
|
||||||
|
expect(nodeDefault.defaultValue.frequency).toBe('daily')
|
||||||
|
expect(nodeDefault.defaultValue.enabled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have empty prev nodes', () => {
|
||||||
|
const prevNodes = nodeDefault.getAvailablePrevNodes(false)
|
||||||
|
expect(prevNodes).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have available next nodes excluding Start', () => {
|
||||||
|
const nextNodes = nodeDefault.getAvailableNextNodes(false)
|
||||||
|
expect(nextNodes).toBeDefined()
|
||||||
|
expect(nextNodes.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Validation - checkValid', () => {
|
||||||
|
it('should validate successfully with valid visual config', () => {
|
||||||
|
const payload: ScheduleTriggerNodeType = {
|
||||||
|
mode: 'visual',
|
||||||
|
timezone: 'UTC',
|
||||||
|
frequency: 'daily',
|
||||||
|
visual_config: {
|
||||||
|
time: '9:00 AM',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = nodeDefault.checkValid(payload, mockT)
|
||||||
|
expect(result.isValid).toBe(true)
|
||||||
|
expect(result.errorMessage).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should require mode field', () => {
|
||||||
|
const payload = {
|
||||||
|
timezone: 'UTC',
|
||||||
|
} as ScheduleTriggerNodeType
|
||||||
|
|
||||||
|
const result = nodeDefault.checkValid(payload, mockT)
|
||||||
|
expect(result.isValid).toBe(false)
|
||||||
|
expect(result.errorMessage).toContain('required')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should require timezone field', () => {
|
||||||
|
const payload = {
|
||||||
|
mode: 'visual',
|
||||||
|
} as ScheduleTriggerNodeType
|
||||||
|
|
||||||
|
const result = nodeDefault.checkValid(payload, mockT)
|
||||||
|
expect(result.isValid).toBe(false)
|
||||||
|
expect(result.errorMessage).toContain('required')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should validate cron mode with valid expression', () => {
|
||||||
|
const payload: ScheduleTriggerNodeType = {
|
||||||
|
mode: 'cron',
|
||||||
|
timezone: 'UTC',
|
||||||
|
cron_expression: '0 9 * * 1',
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = nodeDefault.checkValid(payload, mockT)
|
||||||
|
expect(result.isValid).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reject invalid cron expression', () => {
|
||||||
|
const payload: ScheduleTriggerNodeType = {
|
||||||
|
mode: 'cron',
|
||||||
|
timezone: 'UTC',
|
||||||
|
cron_expression: 'invalid',
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = nodeDefault.checkValid(payload, mockT)
|
||||||
|
expect(result.isValid).toBe(false)
|
||||||
|
expect(result.errorMessage).toContain('Invalid cron expression')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reject invalid timezone', () => {
|
||||||
|
const payload = {
|
||||||
|
mode: 'visual',
|
||||||
|
timezone: 'Invalid/Timezone',
|
||||||
|
frequency: 'daily',
|
||||||
|
visual_config: { time: '9:00 AM' },
|
||||||
|
} as ScheduleTriggerNodeType
|
||||||
|
|
||||||
|
const result = nodeDefault.checkValid(payload, mockT)
|
||||||
|
expect(result.isValid).toBe(false)
|
||||||
|
expect(result.errorMessage).toContain('Invalid timezone')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should require frequency in visual mode', () => {
|
||||||
|
const payload = {
|
||||||
|
mode: 'visual',
|
||||||
|
timezone: 'UTC',
|
||||||
|
} as ScheduleTriggerNodeType
|
||||||
|
|
||||||
|
const result = nodeDefault.checkValid(payload, mockT)
|
||||||
|
expect(result.isValid).toBe(false)
|
||||||
|
expect(result.errorMessage).toContain('required')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -2,6 +2,131 @@ import { BlockEnum } from '../../types'
|
|||||||
import type { NodeDefault } from '../../types'
|
import type { NodeDefault } from '../../types'
|
||||||
import type { ScheduleTriggerNodeType } from './types'
|
import type { ScheduleTriggerNodeType } from './types'
|
||||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||||
|
import { isValidCronExpression } from './utils/cron-parser'
|
||||||
|
import { getNextExecutionTimes } from './utils/execution-time-calculator'
|
||||||
|
const isValidTimeFormat = (time: string): boolean => {
|
||||||
|
const timeRegex = /^(0?\d|1[0-2]):[0-5]\d (AM|PM)$/
|
||||||
|
if (!timeRegex.test(time)) return false
|
||||||
|
|
||||||
|
const [timePart, period] = time.split(' ')
|
||||||
|
const [hour, minute] = timePart.split(':')
|
||||||
|
const hourNum = Number.parseInt(hour, 10)
|
||||||
|
const minuteNum = Number.parseInt(minute, 10)
|
||||||
|
|
||||||
|
return hourNum >= 1 && hourNum <= 12
|
||||||
|
&& minuteNum >= 0 && minuteNum <= 59
|
||||||
|
&& ['AM', 'PM'].includes(period)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateHourlyConfig = (config: any, t: any): string => {
|
||||||
|
const i18nPrefix = 'workflow.errorMsg'
|
||||||
|
|
||||||
|
if (!config.datetime)
|
||||||
|
return t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.startTime') })
|
||||||
|
|
||||||
|
const startTime = new Date(config.datetime)
|
||||||
|
if (Number.isNaN(startTime.getTime()))
|
||||||
|
return t('workflow.nodes.triggerSchedule.invalidStartTime')
|
||||||
|
|
||||||
|
if (startTime <= new Date())
|
||||||
|
return t('workflow.nodes.triggerSchedule.startTimeMustBeFuture')
|
||||||
|
|
||||||
|
const recurEvery = config.recur_every || 1
|
||||||
|
if (recurEvery < 1 || recurEvery > 999)
|
||||||
|
return t('workflow.nodes.triggerSchedule.invalidRecurEvery')
|
||||||
|
|
||||||
|
if (!config.recur_unit || !['hours', 'minutes'].includes(config.recur_unit))
|
||||||
|
return t('workflow.nodes.triggerSchedule.invalidRecurUnit')
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateDailyConfig = (config: any, t: any): string => {
|
||||||
|
const i18nPrefix = 'workflow.errorMsg'
|
||||||
|
|
||||||
|
if (!config.time)
|
||||||
|
return t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.time') })
|
||||||
|
|
||||||
|
if (!isValidTimeFormat(config.time))
|
||||||
|
return t('workflow.nodes.triggerSchedule.invalidTimeFormat')
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateWeeklyConfig = (config: any, t: any): string => {
|
||||||
|
const dailyError = validateDailyConfig(config, t)
|
||||||
|
if (dailyError) return dailyError
|
||||||
|
|
||||||
|
const i18nPrefix = 'workflow.errorMsg'
|
||||||
|
|
||||||
|
if (!config.weekdays || config.weekdays.length === 0)
|
||||||
|
return t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.weekdays') })
|
||||||
|
|
||||||
|
const validWeekdays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
||||||
|
for (const day of config.weekdays) {
|
||||||
|
if (!validWeekdays.includes(day))
|
||||||
|
return t('workflow.nodes.triggerSchedule.invalidWeekday', { weekday: day })
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateMonthlyConfig = (config: any, t: any): string => {
|
||||||
|
const dailyError = validateDailyConfig(config, t)
|
||||||
|
if (dailyError) return dailyError
|
||||||
|
|
||||||
|
const i18nPrefix = 'workflow.errorMsg'
|
||||||
|
|
||||||
|
if (!config.monthly_day)
|
||||||
|
return t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.monthlyDay') })
|
||||||
|
|
||||||
|
if (config.monthly_day !== 'last'
|
||||||
|
&& (typeof config.monthly_day !== 'number'
|
||||||
|
|| config.monthly_day < 1
|
||||||
|
|| config.monthly_day > 31))
|
||||||
|
return t('workflow.nodes.triggerSchedule.invalidMonthlyDay')
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateOnceConfig = (config: any, t: any): string => {
|
||||||
|
const i18nPrefix = 'workflow.errorMsg'
|
||||||
|
|
||||||
|
if (!config.datetime)
|
||||||
|
return t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.executionTime') })
|
||||||
|
|
||||||
|
const executionTime = new Date(config.datetime)
|
||||||
|
if (Number.isNaN(executionTime.getTime()))
|
||||||
|
return t('workflow.nodes.triggerSchedule.invalidExecutionTime')
|
||||||
|
|
||||||
|
if (executionTime <= new Date())
|
||||||
|
return t('workflow.nodes.triggerSchedule.executionTimeMustBeFuture')
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateVisualConfig = (payload: ScheduleTriggerNodeType, t: any): string => {
|
||||||
|
const i18nPrefix = 'workflow.errorMsg'
|
||||||
|
const { visual_config } = payload
|
||||||
|
|
||||||
|
if (!visual_config)
|
||||||
|
return t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.visualConfig') })
|
||||||
|
|
||||||
|
switch (payload.frequency) {
|
||||||
|
case 'hourly':
|
||||||
|
return validateHourlyConfig(visual_config, t)
|
||||||
|
case 'daily':
|
||||||
|
return validateDailyConfig(visual_config, t)
|
||||||
|
case 'weekly':
|
||||||
|
return validateWeeklyConfig(visual_config, t)
|
||||||
|
case 'monthly':
|
||||||
|
return validateMonthlyConfig(visual_config, t)
|
||||||
|
case 'once':
|
||||||
|
return validateOnceConfig(visual_config, t)
|
||||||
|
default:
|
||||||
|
return t('workflow.nodes.triggerSchedule.invalidFrequency')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nodeDefault: NodeDefault<ScheduleTriggerNodeType> = {
|
const nodeDefault: NodeDefault<ScheduleTriggerNodeType> = {
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
@ -24,10 +149,50 @@ const nodeDefault: NodeDefault<ScheduleTriggerNodeType> = {
|
|||||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||||
return nodes.filter(type => type !== BlockEnum.Start)
|
return nodes.filter(type => type !== BlockEnum.Start)
|
||||||
},
|
},
|
||||||
checkValid(_payload: ScheduleTriggerNodeType, _t: any) {
|
checkValid(payload: ScheduleTriggerNodeType, t: any) {
|
||||||
|
const i18nPrefix = 'workflow.errorMsg'
|
||||||
|
let errorMessages = ''
|
||||||
|
if (!errorMessages && !payload.mode)
|
||||||
|
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.mode') })
|
||||||
|
|
||||||
|
if (!errorMessages && !payload.timezone)
|
||||||
|
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.timezone') })
|
||||||
|
if (!errorMessages && payload.timezone) {
|
||||||
|
try {
|
||||||
|
Intl.DateTimeFormat(undefined, { timeZone: payload.timezone })
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
errorMessages = t('workflow.nodes.triggerSchedule.invalidTimezone')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!errorMessages) {
|
||||||
|
if (payload.mode === 'cron') {
|
||||||
|
if (!payload.cron_expression || payload.cron_expression.trim() === '')
|
||||||
|
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.cronExpression') })
|
||||||
|
else if (!isValidCronExpression(payload.cron_expression))
|
||||||
|
errorMessages = t('workflow.nodes.triggerSchedule.invalidCronExpression')
|
||||||
|
}
|
||||||
|
else if (payload.mode === 'visual') {
|
||||||
|
if (!payload.frequency)
|
||||||
|
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.triggerSchedule.frequency') })
|
||||||
|
else
|
||||||
|
errorMessages = validateVisualConfig(payload, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!errorMessages) {
|
||||||
|
try {
|
||||||
|
const nextTimes = getNextExecutionTimes(payload, 1)
|
||||||
|
if (nextTimes.length === 0)
|
||||||
|
errorMessages = t('workflow.nodes.triggerSchedule.noValidExecutionTime')
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
errorMessages = t('workflow.nodes.triggerSchedule.executionTimeCalculationError')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isValid: true,
|
isValid: !errorMessages,
|
||||||
errorMessage: '',
|
errorMessage: errorMessages,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,19 +94,28 @@ export const getNodesConnectedSourceOrTargetHandleIdsMap = (changes: ConnectedSo
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getValidTreeNodes = (nodes: Node[], edges: Edge[]) => {
|
export const getValidTreeNodes = (nodes: Node[], edges: Edge[]) => {
|
||||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
// Find all start nodes (Start and Trigger nodes)
|
||||||
|
const startNodes = nodes.filter(node =>
|
||||||
|
node.data.type === BlockEnum.Start
|
||||||
|
|| node.data.type === BlockEnum.TriggerSchedule
|
||||||
|
|| node.data.type === BlockEnum.TriggerWebhook
|
||||||
|
|| node.data.type === BlockEnum.TriggerPlugin,
|
||||||
|
)
|
||||||
|
|
||||||
if (!startNode) {
|
if (startNodes.length === 0) {
|
||||||
return {
|
return {
|
||||||
validNodes: [],
|
validNodes: [],
|
||||||
maxDepth: 0,
|
maxDepth: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const list: Node[] = [startNode]
|
const list: Node[] = []
|
||||||
let maxDepth = 1
|
let maxDepth = 0
|
||||||
|
|
||||||
const traverse = (root: Node, depth: number) => {
|
const traverse = (root: Node, depth: number) => {
|
||||||
|
// Add the current node to the list
|
||||||
|
list.push(root)
|
||||||
|
|
||||||
if (depth > maxDepth)
|
if (depth > maxDepth)
|
||||||
maxDepth = depth
|
maxDepth = depth
|
||||||
|
|
||||||
@ -114,19 +123,19 @@ export const getValidTreeNodes = (nodes: Node[], edges: Edge[]) => {
|
|||||||
|
|
||||||
if (outgoers.length) {
|
if (outgoers.length) {
|
||||||
outgoers.forEach((outgoer) => {
|
outgoers.forEach((outgoer) => {
|
||||||
list.push(outgoer)
|
// Only traverse if we haven't processed this node yet (avoid cycles)
|
||||||
|
if (!list.find(n => n.id === outgoer.id)) {
|
||||||
|
if (outgoer.data.type === BlockEnum.Iteration)
|
||||||
|
list.push(...nodes.filter(node => node.parentId === outgoer.id))
|
||||||
|
if (outgoer.data.type === BlockEnum.Loop)
|
||||||
|
list.push(...nodes.filter(node => node.parentId === outgoer.id))
|
||||||
|
|
||||||
if (outgoer.data.type === BlockEnum.Iteration)
|
traverse(outgoer, depth + 1)
|
||||||
list.push(...nodes.filter(node => node.parentId === outgoer.id))
|
}
|
||||||
if (outgoer.data.type === BlockEnum.Loop)
|
|
||||||
list.push(...nodes.filter(node => node.parentId === outgoer.id))
|
|
||||||
|
|
||||||
traverse(outgoer, depth + 1)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
list.push(root)
|
// Leaf node - add iteration/loop children if any
|
||||||
|
|
||||||
if (root.data.type === BlockEnum.Iteration)
|
if (root.data.type === BlockEnum.Iteration)
|
||||||
list.push(...nodes.filter(node => node.parentId === root.id))
|
list.push(...nodes.filter(node => node.parentId === root.id))
|
||||||
if (root.data.type === BlockEnum.Loop)
|
if (root.data.type === BlockEnum.Loop)
|
||||||
@ -134,7 +143,11 @@ export const getValidTreeNodes = (nodes: Node[], edges: Edge[]) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
traverse(startNode, maxDepth)
|
// Start traversal from all start nodes
|
||||||
|
startNodes.forEach((startNode) => {
|
||||||
|
if (!list.find(n => n.id === startNode.id))
|
||||||
|
traverse(startNode, 1)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validNodes: uniqBy(list, 'id'),
|
validNodes: uniqBy(list, 'id'),
|
||||||
@ -196,7 +209,12 @@ export const getParallelInfo = (nodes: Node[], edges: Edge[], parentNodeId?: str
|
|||||||
startNode = nodes.find(node => node.id === (parentNode.data as (IterationNodeType | LoopNodeType)).start_node_id)
|
startNode = nodes.find(node => node.id === (parentNode.data as (IterationNodeType | LoopNodeType)).start_node_id)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
startNode = nodes.find(node =>
|
||||||
|
node.data.type === BlockEnum.Start
|
||||||
|
|| node.data.type === BlockEnum.TriggerSchedule
|
||||||
|
|| node.data.type === BlockEnum.TriggerWebhook
|
||||||
|
|| node.data.type === BlockEnum.TriggerPlugin,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (!startNode)
|
if (!startNode)
|
||||||
throw new Error('Start node not found')
|
throw new Error('Start node not found')
|
||||||
|
|||||||
@ -45,6 +45,7 @@ const translation = {
|
|||||||
needConnectTip: 'This step is not connected to anything',
|
needConnectTip: 'This step is not connected to anything',
|
||||||
maxTreeDepth: 'Maximum limit of {{depth}} nodes per branch',
|
maxTreeDepth: 'Maximum limit of {{depth}} nodes per branch',
|
||||||
needEndNode: 'The End node must be added',
|
needEndNode: 'The End node must be added',
|
||||||
|
needStartNode: 'A start node (Start or Trigger) must be added',
|
||||||
needAnswerNode: 'The Answer node must be added',
|
needAnswerNode: 'The Answer node must be added',
|
||||||
workflowProcess: 'Workflow Process',
|
workflowProcess: 'Workflow Process',
|
||||||
notRunning: 'Not running yet',
|
notRunning: 'Not running yet',
|
||||||
@ -953,6 +954,26 @@ const translation = {
|
|||||||
days: 'Days',
|
days: 'Days',
|
||||||
lastDay: 'Last day',
|
lastDay: 'Last day',
|
||||||
lastDayTooltip: 'Not all months have 31 days. Use the \'last day\' option to select each month\'s final day.',
|
lastDayTooltip: 'Not all months have 31 days. Use the \'last day\' option to select each month\'s final day.',
|
||||||
|
mode: 'Mode',
|
||||||
|
timezone: 'Timezone',
|
||||||
|
visualConfig: 'Visual Configuration',
|
||||||
|
monthlyDay: 'Monthly Day',
|
||||||
|
executionTime: 'Execution Time',
|
||||||
|
weekdays: 'Weekdays',
|
||||||
|
invalidTimezone: 'Invalid timezone',
|
||||||
|
invalidCronExpression: 'Invalid cron expression',
|
||||||
|
noValidExecutionTime: 'No valid execution time can be calculated',
|
||||||
|
executionTimeCalculationError: 'Failed to calculate execution times',
|
||||||
|
invalidFrequency: 'Invalid frequency',
|
||||||
|
invalidStartTime: 'Invalid start time',
|
||||||
|
startTimeMustBeFuture: 'Start time must be in the future',
|
||||||
|
invalidRecurEvery: 'Recur every must be between 1 and 999',
|
||||||
|
invalidRecurUnit: 'Invalid recur unit',
|
||||||
|
invalidTimeFormat: 'Invalid time format (expected HH:MM AM/PM)',
|
||||||
|
invalidWeekday: 'Invalid weekday: {{weekday}}',
|
||||||
|
invalidMonthlyDay: 'Monthly day must be between 1-31 or "last"',
|
||||||
|
invalidExecutionTime: 'Invalid execution time',
|
||||||
|
executionTimeMustBeFuture: 'Execution time must be in the future',
|
||||||
},
|
},
|
||||||
triggerWebhook: {
|
triggerWebhook: {
|
||||||
title: 'Webhook Trigger',
|
title: 'Webhook Trigger',
|
||||||
|
|||||||
@ -45,6 +45,7 @@ const translation = {
|
|||||||
needConnectTip: '接続されていないステップがあります',
|
needConnectTip: '接続されていないステップがあります',
|
||||||
maxTreeDepth: '1 ブランチあたりの最大ノード数:{{depth}}',
|
maxTreeDepth: '1 ブランチあたりの最大ノード数:{{depth}}',
|
||||||
needEndNode: '終了ブロックを追加する必要があります',
|
needEndNode: '終了ブロックを追加する必要があります',
|
||||||
|
needStartNode: '開始ノード(スタートまたはトリガー)を追加する必要があります',
|
||||||
needAnswerNode: '回答ブロックを追加する必要があります',
|
needAnswerNode: '回答ブロックを追加する必要があります',
|
||||||
workflowProcess: 'ワークフロー処理',
|
workflowProcess: 'ワークフロー処理',
|
||||||
notRunning: 'まだ実行されていません',
|
notRunning: 'まだ実行されていません',
|
||||||
@ -953,6 +954,26 @@ const translation = {
|
|||||||
lastDayTooltip: 'すべての月に31日があるわけではありません。「月末」オプションを使用して各月の最終日を選択してください。',
|
lastDayTooltip: 'すべての月に31日があるわけではありません。「月末」オプションを使用して各月の最終日を選択してください。',
|
||||||
useVisualPicker: 'ビジュアル設定を使用',
|
useVisualPicker: 'ビジュアル設定を使用',
|
||||||
nodeTitle: 'スケジュールトリガー',
|
nodeTitle: 'スケジュールトリガー',
|
||||||
|
mode: 'モード',
|
||||||
|
timezone: 'タイムゾーン',
|
||||||
|
visualConfig: 'ビジュアル設定',
|
||||||
|
monthlyDay: '月の日',
|
||||||
|
executionTime: '実行時間',
|
||||||
|
weekdays: '曜日',
|
||||||
|
invalidTimezone: '無効なタイムゾーン',
|
||||||
|
invalidCronExpression: '無効なCron式',
|
||||||
|
noValidExecutionTime: '有効な実行時間を計算できません',
|
||||||
|
executionTimeCalculationError: '実行時間の計算に失敗しました',
|
||||||
|
invalidFrequency: '無効な頻度',
|
||||||
|
invalidStartTime: '無効な開始時間',
|
||||||
|
startTimeMustBeFuture: '開始時間は未来の時間である必要があります',
|
||||||
|
invalidRecurEvery: '繰り返し間隔は1から999の間である必要があります',
|
||||||
|
invalidRecurUnit: '無効な繰り返し単位',
|
||||||
|
invalidTimeFormat: '無効な時間形式(期待される形式:HH:MM AM/PM)',
|
||||||
|
invalidWeekday: '無効な曜日:{{weekday}}',
|
||||||
|
invalidMonthlyDay: '月の日は1-31の間または"last"である必要があります',
|
||||||
|
invalidExecutionTime: '無効な実行時間',
|
||||||
|
executionTimeMustBeFuture: '実行時間は未来の時間である必要があります',
|
||||||
},
|
},
|
||||||
triggerWebhook: {
|
triggerWebhook: {
|
||||||
title: 'Webhook トリガー',
|
title: 'Webhook トリガー',
|
||||||
|
|||||||
@ -44,6 +44,7 @@ const translation = {
|
|||||||
needConnectTip: '此节点尚未连接到其他节点',
|
needConnectTip: '此节点尚未连接到其他节点',
|
||||||
maxTreeDepth: '每个分支最大限制 {{depth}} 个节点',
|
maxTreeDepth: '每个分支最大限制 {{depth}} 个节点',
|
||||||
needEndNode: '必须添加结束节点',
|
needEndNode: '必须添加结束节点',
|
||||||
|
needStartNode: '必须添加开始节点(开始或触发器)',
|
||||||
needAnswerNode: '必须添加直接回复节点',
|
needAnswerNode: '必须添加直接回复节点',
|
||||||
workflowProcess: '工作流',
|
workflowProcess: '工作流',
|
||||||
notRunning: '尚未运行',
|
notRunning: '尚未运行',
|
||||||
@ -953,6 +954,26 @@ const translation = {
|
|||||||
useVisualPicker: '使用可视化配置',
|
useVisualPicker: '使用可视化配置',
|
||||||
days: '天',
|
days: '天',
|
||||||
notConfigured: '未配置',
|
notConfigured: '未配置',
|
||||||
|
mode: '模式',
|
||||||
|
timezone: '时区',
|
||||||
|
visualConfig: '可视化配置',
|
||||||
|
monthlyDay: '月份日期',
|
||||||
|
executionTime: '执行时间',
|
||||||
|
weekdays: '工作日',
|
||||||
|
invalidTimezone: '无效的时区',
|
||||||
|
invalidCronExpression: '无效的 Cron 表达式',
|
||||||
|
noValidExecutionTime: '无法计算有效的执行时间',
|
||||||
|
executionTimeCalculationError: '执行时间计算失败',
|
||||||
|
invalidFrequency: '无效的频率',
|
||||||
|
invalidStartTime: '无效的开始时间',
|
||||||
|
startTimeMustBeFuture: '开始时间必须是将来的时间',
|
||||||
|
invalidRecurEvery: '重复间隔必须在 1 到 999 之间',
|
||||||
|
invalidRecurUnit: '无效的重复单位',
|
||||||
|
invalidTimeFormat: '无效的时间格式(预期格式:HH:MM AM/PM)',
|
||||||
|
invalidWeekday: '无效的工作日:{{weekday}}',
|
||||||
|
invalidMonthlyDay: '月份日期必须在 1-31 之间或为"last"',
|
||||||
|
invalidExecutionTime: '无效的执行时间',
|
||||||
|
executionTimeMustBeFuture: '执行时间必须是将来的时间',
|
||||||
},
|
},
|
||||||
triggerWebhook: {
|
triggerWebhook: {
|
||||||
configPlaceholder: 'Webhook 触发器配置将在此处实现',
|
configPlaceholder: 'Webhook 触发器配置将在此处实现',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user