mirror of https://github.com/langgenius/dify.git
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({
|
||||
id: node.id,
|
||||
type: node.data.type,
|
||||
title: node.data.title,
|
||||
toolIcon,
|
||||
unConnected: !validNodes.find(n => n.id === node.id),
|
||||
unConnected: isUnconnected && !isStartNode,
|
||||
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)) {
|
||||
list.push({
|
||||
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)) {
|
||||
notify({ type: 'error', message: t('workflow.common.needAnswerNode') })
|
||||
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 { ScheduleTriggerNodeType } from './types'
|
||||
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> = {
|
||||
defaultValue: {
|
||||
|
|
@ -24,10 +149,50 @@ const nodeDefault: NodeDefault<ScheduleTriggerNodeType> = {
|
|||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
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 {
|
||||
isValid: true,
|
||||
errorMessage: '',
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,19 +94,28 @@ export const getNodesConnectedSourceOrTargetHandleIdsMap = (changes: ConnectedSo
|
|||
}
|
||||
|
||||
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 {
|
||||
validNodes: [],
|
||||
maxDepth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const list: Node[] = [startNode]
|
||||
let maxDepth = 1
|
||||
const list: Node[] = []
|
||||
let maxDepth = 0
|
||||
|
||||
const traverse = (root: Node, depth: number) => {
|
||||
// Add the current node to the list
|
||||
list.push(root)
|
||||
|
||||
if (depth > maxDepth)
|
||||
maxDepth = depth
|
||||
|
||||
|
|
@ -114,19 +123,19 @@ export const getValidTreeNodes = (nodes: Node[], edges: Edge[]) => {
|
|||
|
||||
if (outgoers.length) {
|
||||
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)
|
||||
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)
|
||||
traverse(outgoer, depth + 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
list.push(root)
|
||||
|
||||
else {
|
||||
// Leaf node - add iteration/loop children if any
|
||||
if (root.data.type === BlockEnum.Iteration)
|
||||
list.push(...nodes.filter(node => node.parentId === root.id))
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
throw new Error('Start node not found')
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const translation = {
|
|||
needConnectTip: 'This step is not connected to anything',
|
||||
maxTreeDepth: 'Maximum limit of {{depth}} nodes per branch',
|
||||
needEndNode: 'The End node must be added',
|
||||
needStartNode: 'A start node (Start or Trigger) must be added',
|
||||
needAnswerNode: 'The Answer node must be added',
|
||||
workflowProcess: 'Workflow Process',
|
||||
notRunning: 'Not running yet',
|
||||
|
|
@ -953,6 +954,26 @@ const translation = {
|
|||
days: 'Days',
|
||||
lastDay: 'Last 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: {
|
||||
title: 'Webhook Trigger',
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const translation = {
|
|||
needConnectTip: '接続されていないステップがあります',
|
||||
maxTreeDepth: '1 ブランチあたりの最大ノード数:{{depth}}',
|
||||
needEndNode: '終了ブロックを追加する必要があります',
|
||||
needStartNode: '開始ノード(スタートまたはトリガー)を追加する必要があります',
|
||||
needAnswerNode: '回答ブロックを追加する必要があります',
|
||||
workflowProcess: 'ワークフロー処理',
|
||||
notRunning: 'まだ実行されていません',
|
||||
|
|
@ -953,6 +954,26 @@ const translation = {
|
|||
lastDayTooltip: 'すべての月に31日があるわけではありません。「月末」オプションを使用して各月の最終日を選択してください。',
|
||||
useVisualPicker: 'ビジュアル設定を使用',
|
||||
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: {
|
||||
title: 'Webhook トリガー',
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const translation = {
|
|||
needConnectTip: '此节点尚未连接到其他节点',
|
||||
maxTreeDepth: '每个分支最大限制 {{depth}} 个节点',
|
||||
needEndNode: '必须添加结束节点',
|
||||
needStartNode: '必须添加开始节点(开始或触发器)',
|
||||
needAnswerNode: '必须添加直接回复节点',
|
||||
workflowProcess: '工作流',
|
||||
notRunning: '尚未运行',
|
||||
|
|
@ -953,6 +954,26 @@ const translation = {
|
|||
useVisualPicker: '使用可视化配置',
|
||||
days: '天',
|
||||
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: {
|
||||
configPlaceholder: 'Webhook 触发器配置将在此处实现',
|
||||
|
|
|
|||
Loading…
Reference in New Issue