mirror of https://github.com/langgenius/dify.git
fix: simplify trigger-schedule hourly mode calculation and improve UI consistency (#24082)
Co-authored-by: zhangxuhe1 <xuhezhang6@gmail.com>
This commit is contained in:
parent
5c4bf7aabd
commit
6a3d135d49
|
|
@ -36,6 +36,7 @@ const DatePicker = ({
|
|||
renderTrigger,
|
||||
triggerWrapClassName,
|
||||
popupZIndexClassname = 'z-[11]',
|
||||
notClearable = false,
|
||||
}: DatePickerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
|
@ -200,7 +201,7 @@ const DatePicker = ({
|
|||
<PortalToFollowElem
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
placement='bottom-end'
|
||||
placement='bottom-start'
|
||||
>
|
||||
<PortalToFollowElemTrigger className={triggerWrapClassName}>
|
||||
{renderTrigger ? (renderTrigger({
|
||||
|
|
@ -224,15 +225,17 @@ const DatePicker = ({
|
|||
<RiCalendarLine className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-quaternary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
(displayValue || (isOpen && selectedDate)) && 'group-hover:hidden',
|
||||
(displayValue || (isOpen && selectedDate)) && !notClearable && 'group-hover:hidden',
|
||||
)} />
|
||||
<RiCloseCircleFill
|
||||
className={cn(
|
||||
'hidden h-4 w-4 shrink-0 text-text-quaternary',
|
||||
(displayValue || (isOpen && selectedDate)) && 'hover:text-text-secondary group-hover:inline-block',
|
||||
)}
|
||||
onClick={handleClear}
|
||||
/>
|
||||
{!notClearable && (
|
||||
<RiCloseCircleFill
|
||||
className={cn(
|
||||
'hidden h-4 w-4 shrink-0 text-text-quaternary',
|
||||
(displayValue || (isOpen && selectedDate)) && 'hover:text-text-secondary group-hover:inline-block',
|
||||
)}
|
||||
onClick={handleClear}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PortalToFollowElemTrigger>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const TimePicker = ({
|
|||
title,
|
||||
minuteFilter,
|
||||
popupClassName,
|
||||
notClearable = false,
|
||||
}: TimePickerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
|
@ -123,7 +124,7 @@ const TimePicker = ({
|
|||
<PortalToFollowElem
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
placement='bottom-end'
|
||||
placement='bottom-start'
|
||||
>
|
||||
<PortalToFollowElemTrigger>
|
||||
{renderTrigger ? (renderTrigger({
|
||||
|
|
@ -139,15 +140,17 @@ const TimePicker = ({
|
|||
<RiTimeLine className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-quaternary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
(displayValue || (isOpen && selectedTime)) && 'group-hover:hidden',
|
||||
(displayValue || (isOpen && selectedTime)) && !notClearable && 'group-hover:hidden',
|
||||
)} />
|
||||
<RiCloseCircleFill
|
||||
className={cn(
|
||||
'hidden h-4 w-4 shrink-0 text-text-quaternary',
|
||||
(displayValue || (isOpen && selectedTime)) && 'hover:text-text-secondary group-hover:inline-block',
|
||||
)}
|
||||
onClick={handleClear}
|
||||
/>
|
||||
{!notClearable && (
|
||||
<RiCloseCircleFill
|
||||
className={cn(
|
||||
'hidden h-4 w-4 shrink-0 text-text-quaternary',
|
||||
(displayValue || (isOpen && selectedTime)) && 'hover:text-text-secondary group-hover:inline-block',
|
||||
)}
|
||||
onClick={handleClear}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PortalToFollowElemTrigger>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export type DatePickerProps = {
|
|||
renderTrigger?: (props: TriggerProps) => React.ReactNode
|
||||
minuteFilter?: (minutes: string[]) => string[]
|
||||
popupZIndexClassname?: string
|
||||
notClearable?: boolean
|
||||
}
|
||||
|
||||
export type DatePickerHeaderProps = {
|
||||
|
|
@ -63,6 +64,7 @@ export type TimePickerProps = {
|
|||
title?: string
|
||||
minuteFilter?: (minutes: string[]) => string[]
|
||||
popupClassName?: string
|
||||
notClearable?: boolean
|
||||
}
|
||||
|
||||
export type TimePickerFooterProps = {
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
import React from 'react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import DateTimePicker from './date-time-picker'
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'workflow.nodes.triggerSchedule.selectDateTime': 'Select Date & Time',
|
||||
'common.operation.now': 'Now',
|
||||
'common.operation.ok': 'OK',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('DateTimePicker', () => {
|
||||
const mockOnChange = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('renders with default value', () => {
|
||||
render(<DateTimePicker onChange={mockOnChange} />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toBeInTheDocument()
|
||||
expect(button.textContent).toMatch(/\d+, \d{4} \d{1,2}:\d{2} [AP]M/)
|
||||
})
|
||||
|
||||
test('renders with provided value', () => {
|
||||
const testDate = new Date('2024-01-15T14:30:00.000Z')
|
||||
render(<DateTimePicker value={testDate.toISOString()} onChange={mockOnChange} />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('opens picker when button is clicked', () => {
|
||||
render(<DateTimePicker onChange={mockOnChange} />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(screen.getByText('Select Date & Time')).toBeInTheDocument()
|
||||
expect(screen.getByText('Now')).toBeInTheDocument()
|
||||
expect(screen.getByText('OK')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('closes picker when clicking outside', () => {
|
||||
render(<DateTimePicker onChange={mockOnChange} />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(screen.getByText('Select Date & Time')).toBeInTheDocument()
|
||||
|
||||
const overlay = document.querySelector('.fixed.inset-0')
|
||||
fireEvent.click(overlay!)
|
||||
|
||||
expect(screen.queryByText('Select Date & Time')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('does not call onChange when input changes without clicking OK', () => {
|
||||
render(<DateTimePicker onChange={mockOnChange} />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
const input = screen.getByDisplayValue(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)
|
||||
fireEvent.change(input, { target: { value: '2024-12-25T15:30' } })
|
||||
|
||||
const overlay = document.querySelector('.fixed.inset-0')
|
||||
fireEvent.click(overlay!)
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('calls onChange when clicking OK button', () => {
|
||||
render(<DateTimePicker onChange={mockOnChange} />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
const input = screen.getByDisplayValue(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)
|
||||
fireEvent.change(input, { target: { value: '2024-12-25T15:30' } })
|
||||
|
||||
const okButton = screen.getByText('OK')
|
||||
fireEvent.click(okButton)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(expect.stringMatching(/2024-12-25T.*:30.*Z/))
|
||||
})
|
||||
|
||||
test('calls onChange when clicking Now button', () => {
|
||||
render(<DateTimePicker onChange={mockOnChange} />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
const nowButton = screen.getByText('Now')
|
||||
fireEvent.click(nowButton)
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/))
|
||||
})
|
||||
|
||||
test('resets temp value when reopening picker', async () => {
|
||||
render(<DateTimePicker onChange={mockOnChange} />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
const input = screen.getByDisplayValue(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)
|
||||
const originalValue = input.getAttribute('value')
|
||||
|
||||
fireEvent.change(input, { target: { value: '2024-12-25T15:30' } })
|
||||
expect(input.getAttribute('value')).toBe('2024-12-25T15:30')
|
||||
|
||||
const overlay = document.querySelector('.fixed.inset-0')
|
||||
fireEvent.click(overlay!)
|
||||
|
||||
fireEvent.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
const newInput = screen.getByDisplayValue(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)
|
||||
expect(newInput.getAttribute('value')).toBe(originalValue)
|
||||
})
|
||||
})
|
||||
|
||||
test('displays current value in button text', () => {
|
||||
const testDate = new Date('2024-01-15T14:30:00.000Z')
|
||||
render(<DateTimePicker value={testDate.toISOString()} onChange={mockOnChange} />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.textContent).toMatch(/January 15, 2024/)
|
||||
expect(button.textContent).toMatch(/\d{1,2}:30 [AP]M/)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCalendarLine } from '@remixicon/react'
|
||||
import { getDefaultDateTime } from '../utils/execution-time-calculator'
|
||||
|
||||
type DateTimePickerProps = {
|
||||
value?: string
|
||||
onChange: (datetime: string) => void
|
||||
}
|
||||
|
||||
const DateTimePicker = ({ value, onChange }: DateTimePickerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [tempValue, setTempValue] = useState('')
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen)
|
||||
setTempValue('')
|
||||
}, [isOpen])
|
||||
|
||||
const getCurrentDateTime = () => {
|
||||
if (value) {
|
||||
try {
|
||||
const date = new Date(value)
|
||||
return `${date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})} ${date.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
})}`
|
||||
}
|
||||
catch {
|
||||
// fallback
|
||||
}
|
||||
}
|
||||
|
||||
const defaultDate = getDefaultDateTime()
|
||||
|
||||
return `${defaultDate.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})} ${defaultDate.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
})}`
|
||||
}
|
||||
|
||||
const handleDateTimeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const dateTimeValue = event.target.value
|
||||
setTempValue(dateTimeValue)
|
||||
}
|
||||
|
||||
const getInputValue = () => {
|
||||
if (tempValue)
|
||||
return tempValue
|
||||
|
||||
if (value) {
|
||||
try {
|
||||
const date = new Date(value)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`
|
||||
}
|
||||
catch {
|
||||
// fallback
|
||||
}
|
||||
}
|
||||
|
||||
const defaultDate = getDefaultDateTime()
|
||||
const year = defaultDate.getFullYear()
|
||||
const month = String(defaultDate.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(defaultDate.getDate()).padStart(2, '0')
|
||||
const hours = String(defaultDate.getHours()).padStart(2, '0')
|
||||
const minutes = String(defaultDate.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex h-9 w-full items-center justify-between rounded-lg bg-components-input-bg-normal px-3 py-1.5 text-sm text-text-secondary hover:bg-components-input-bg-hover"
|
||||
>
|
||||
<span>{getCurrentDateTime()}</span>
|
||||
<RiCalendarLine className="h-4 w-4 text-gray-400" />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-full z-50 mt-1 w-72 select-none rounded-xl border border-gray-200 bg-white p-4 shadow-lg">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-medium text-gray-900">{t('workflow.nodes.triggerSchedule.selectDateTime')}</h3>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 border-b border-gray-100" />
|
||||
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={getInputValue()}
|
||||
onChange={handleDateTimeChange}
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const now = new Date()
|
||||
onChange(now.toISOString())
|
||||
setTempValue('')
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
{t('common.operation.now')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (tempValue) {
|
||||
const date = new Date(tempValue)
|
||||
onChange(date.toISOString())
|
||||
}
|
||||
setTempValue('')
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className="flex-1 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
{t('common.operation.ok')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => {
|
||||
setTempValue('')
|
||||
setIsOpen(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DateTimePicker
|
||||
|
|
@ -27,7 +27,8 @@ const FrequencySelector = ({ frequency, onChange }: FrequencySelectorProps) => {
|
|||
defaultValue={frequency}
|
||||
onSelect={item => onChange(item.value as ScheduleFrequency)}
|
||||
placeholder={t('workflow.nodes.triggerSchedule.selectFrequency')}
|
||||
className="w-full"
|
||||
className="w-full py-2"
|
||||
wrapperClassName="h-auto"
|
||||
optionWrapClassName="min-w-40"
|
||||
notClearable={true}
|
||||
allowSearch={false}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const MonthlyDaysSelector = ({ selectedDay, onChange }: MonthlyDaysSelectorProps
|
|||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="mb-2 block text-xs font-medium text-gray-500">
|
||||
<label className="mb-2 block text-xs font-medium text-text-tertiary">
|
||||
{t('workflow.nodes.triggerSchedule.days')}
|
||||
</label>
|
||||
|
||||
|
|
@ -34,12 +34,12 @@ const MonthlyDaysSelector = ({ selectedDay, onChange }: MonthlyDaysSelectorProps
|
|||
key={day}
|
||||
type="button"
|
||||
onClick={() => onChange(day)}
|
||||
className={`rounded-lg py-1.5 text-xs transition-colors ${
|
||||
className={`rounded-lg border bg-components-option-card-option-bg py-1 text-xs transition-colors ${
|
||||
day === 'last' ? 'col-span-2 min-w-0' : ''
|
||||
} ${
|
||||
selectedDay === day
|
||||
? 'border-2 border-util-colors-blue-brand-blue-brand-600 text-text-secondary'
|
||||
: 'border-components-input-border-normal border text-text-tertiary hover:border-components-input-border-hover hover:text-text-secondary'
|
||||
? 'border-util-colors-blue-brand-blue-brand-600 text-text-secondary'
|
||||
: 'border-divider-subtle text-text-tertiary hover:border-divider-regular hover:text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
{day === 'last' ? (
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ type NextExecutionTimesProps = {
|
|||
const NextExecutionTimes = ({ data }: NextExecutionTimesProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Don't show next execution times for 'once' frequency
|
||||
if (data.frequency === 'once')
|
||||
// Don't show next execution times for 'once' frequency in visual mode
|
||||
if (data.mode === 'visual' && data.frequency === 'once')
|
||||
return null
|
||||
|
||||
const executionTimes = getFormattedExecutionTimes(data, 5)
|
||||
|
|
|
|||
|
|
@ -1,230 +0,0 @@
|
|||
import React, { useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiTimeLine } from '@remixicon/react'
|
||||
|
||||
const scrollbarHideStyles = {
|
||||
scrollbarWidth: 'none' as const,
|
||||
msOverflowStyle: 'none' as const,
|
||||
} as React.CSSProperties
|
||||
|
||||
type TimePickerProps = {
|
||||
value?: string
|
||||
onChange: (time: string) => void
|
||||
}
|
||||
|
||||
const TimePicker = ({ value = '11:30 AM', onChange }: TimePickerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [selectedHour, setSelectedHour] = useState(11)
|
||||
const [selectedMinute, setSelectedMinute] = useState(30)
|
||||
const [selectedPeriod, setSelectedPeriod] = useState<'AM' | 'PM'>('AM')
|
||||
const hourContainerRef = useRef<HTMLDivElement>(null)
|
||||
const minuteContainerRef = useRef<HTMLDivElement>(null)
|
||||
const periodContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (value) {
|
||||
const match = value.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/)
|
||||
if (match) {
|
||||
setSelectedHour(Number.parseInt(match[1], 10))
|
||||
setSelectedMinute(Number.parseInt(match[2], 10))
|
||||
setSelectedPeriod(match[3] as 'AM' | 'PM')
|
||||
}
|
||||
}
|
||||
else {
|
||||
setSelectedHour(11)
|
||||
setSelectedMinute(30)
|
||||
setSelectedPeriod('AM')
|
||||
}
|
||||
}
|
||||
}, [isOpen, value])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTimeout(() => {
|
||||
if (hourContainerRef.current) {
|
||||
const selectedHourElement = hourContainerRef.current.querySelector('.bg-state-base-active')
|
||||
if (selectedHourElement)
|
||||
selectedHourElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
if (minuteContainerRef.current) {
|
||||
const selectedMinuteElement = minuteContainerRef.current.querySelector('.bg-state-base-active')
|
||||
if (selectedMinuteElement)
|
||||
selectedMinuteElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
if (periodContainerRef.current) {
|
||||
const selectedPeriodElement = periodContainerRef.current.querySelector('.bg-state-base-active')
|
||||
if (selectedPeriodElement)
|
||||
selectedPeriodElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
}, [isOpen, selectedHour, selectedMinute, selectedPeriod])
|
||||
|
||||
const hours = Array.from({ length: 12 }, (_, i) => i + 1)
|
||||
const minutes = Array.from({ length: 60 }, (_, i) => i)
|
||||
const periods = ['AM', 'PM'] as const
|
||||
|
||||
// Create padding elements to ensure bottom options can scroll to top
|
||||
// Container shows 8 options (h-64), so we need 7 padding elements at bottom
|
||||
const createBottomPadding = () => Array.from({ length: 7 }, (_, i) => (
|
||||
<div key={`bottom-padding-${i}`} className="pointer-events-none h-8" />
|
||||
))
|
||||
|
||||
const handleNow = () => {
|
||||
const now = new Date()
|
||||
const hour = now.getHours()
|
||||
const minute = now.getMinutes()
|
||||
const period = hour >= 12 ? 'PM' : 'AM'
|
||||
let displayHour = hour
|
||||
if (hour === 0)
|
||||
displayHour = 12
|
||||
else if (hour > 12)
|
||||
displayHour = hour - 12
|
||||
|
||||
const timeString = `${displayHour}:${minute.toString().padStart(2, '0')} ${period}`
|
||||
onChange(timeString)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const handleOK = () => {
|
||||
const timeString = `${selectedHour}:${selectedMinute.toString().padStart(2, '0')} ${selectedPeriod}`
|
||||
onChange(timeString)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex h-9 w-full items-center justify-between rounded-lg bg-components-input-bg-normal px-3 py-1.5 text-sm text-text-secondary hover:bg-components-input-bg-hover"
|
||||
>
|
||||
<span>{value}</span>
|
||||
<RiTimeLine className="h-4 w-4 text-text-tertiary" />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-full z-50 mt-1 w-72 select-none rounded-xl border border-components-panel-border bg-components-panel-bg p-4 shadow-lg">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-semibold text-text-primary">{t('time.title.pickTime')}</h3>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 border-b border-components-panel-border-subtle" />
|
||||
|
||||
<div className="mb-4 flex gap-3">
|
||||
{/* Hours */}
|
||||
<div className="flex-1">
|
||||
<div
|
||||
ref={hourContainerRef}
|
||||
className="h-64 overflow-y-auto [&::-webkit-scrollbar]:hidden"
|
||||
style={scrollbarHideStyles}
|
||||
data-testid="hour-selector"
|
||||
>
|
||||
{hours.map(hour => (
|
||||
<button
|
||||
key={hour}
|
||||
type="button"
|
||||
className={`block w-full rounded-lg px-3 py-1.5 text-center text-sm transition-colors ${
|
||||
selectedHour === hour
|
||||
? 'bg-state-base-active text-text-primary'
|
||||
: 'text-text-secondary hover:bg-state-base-hover'
|
||||
}`}
|
||||
onClick={() => setSelectedHour(hour)}
|
||||
>
|
||||
{hour}
|
||||
</button>
|
||||
))}
|
||||
{createBottomPadding()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Minutes */}
|
||||
<div className="flex-1">
|
||||
<div
|
||||
ref={minuteContainerRef}
|
||||
className="h-64 overflow-y-auto [&::-webkit-scrollbar]:hidden"
|
||||
style={scrollbarHideStyles}
|
||||
data-testid="minute-selector"
|
||||
>
|
||||
{minutes.map(minute => (
|
||||
<button
|
||||
key={minute}
|
||||
type="button"
|
||||
className={`block w-full rounded-lg px-3 py-1.5 text-center text-sm transition-colors ${
|
||||
selectedMinute === minute
|
||||
? 'bg-state-base-active text-text-primary'
|
||||
: 'text-text-secondary hover:bg-state-base-hover'
|
||||
}`}
|
||||
onClick={() => setSelectedMinute(minute)}
|
||||
>
|
||||
{minute.toString().padStart(2, '0')}
|
||||
</button>
|
||||
))}
|
||||
{createBottomPadding()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AM/PM */}
|
||||
<div className="flex-1">
|
||||
<div
|
||||
ref={periodContainerRef}
|
||||
className="h-64 overflow-y-auto [&::-webkit-scrollbar]:hidden"
|
||||
style={scrollbarHideStyles}
|
||||
>
|
||||
{periods.map(period => (
|
||||
<button
|
||||
key={period}
|
||||
type="button"
|
||||
className={`block w-full rounded-lg px-3 py-1.5 text-center text-sm transition-colors ${
|
||||
selectedPeriod === period
|
||||
? 'bg-state-base-active text-text-primary'
|
||||
: 'text-text-secondary hover:bg-state-base-hover'
|
||||
}`}
|
||||
onClick={() => setSelectedPeriod(period)}
|
||||
>
|
||||
{period}
|
||||
</button>
|
||||
))}
|
||||
{createBottomPadding()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="my-4 border-b border-components-panel-border-subtle" />
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNow}
|
||||
className="flex-1 rounded-lg border border-components-button-secondary-border bg-components-button-secondary-bg px-3 py-1 text-sm font-medium text-text-accent hover:bg-components-button-secondary-bg-hover"
|
||||
>
|
||||
{t('common.operation.now')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOK}
|
||||
className="flex-1 rounded-lg bg-components-button-primary-bg px-3 py-1 text-sm font-medium text-white hover:bg-components-button-primary-bg-hover"
|
||||
>
|
||||
{t('common.operation.ok')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimePicker
|
||||
|
|
@ -27,7 +27,7 @@ const WeekdaySelector = ({ selectedDays, onChange }: WeekdaySelectorProps) => {
|
|||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="mb-2 block text-xs font-medium text-gray-500">
|
||||
<label className="mb-2 block text-xs font-medium text-text-tertiary">
|
||||
{t('workflow.nodes.triggerSchedule.weekdays')}
|
||||
</label>
|
||||
<div className="flex gap-1.5">
|
||||
|
|
@ -35,10 +35,10 @@ const WeekdaySelector = ({ selectedDays, onChange }: WeekdaySelectorProps) => {
|
|||
<button
|
||||
key={day.key}
|
||||
type="button"
|
||||
className={`flex-1 rounded-lg py-1.5 text-xs transition-colors ${
|
||||
className={`flex-1 rounded-lg border bg-components-option-card-option-bg py-1 text-xs transition-colors ${
|
||||
selectedDay === day.key
|
||||
? 'border-2 border-util-colors-blue-brand-blue-brand-600 text-text-secondary'
|
||||
: 'border-components-input-border-normal border text-text-tertiary hover:border-components-input-border-hover hover:text-text-secondary'
|
||||
? 'border-util-colors-blue-brand-blue-brand-600 text-text-secondary'
|
||||
: 'border-divider-subtle text-text-tertiary hover:border-divider-regular hover:text-text-secondary'
|
||||
}`}
|
||||
onClick={() => handleDaySelect(day.key)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import type { NodePanelProps } from '@/app/components/workflow/types'
|
|||
import ModeToggle from './components/mode-toggle'
|
||||
import FrequencySelector from './components/frequency-selector'
|
||||
import WeekdaySelector from './components/weekday-selector'
|
||||
import TimePicker from './components/time-picker'
|
||||
import DateTimePicker from './components/date-time-picker'
|
||||
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
|
||||
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import dayjs from 'dayjs'
|
||||
import NextExecutionTimes from './components/next-execution-times'
|
||||
import ExecuteNowButton from './components/execute-now-button'
|
||||
import RecurConfig from './components/recur-config'
|
||||
|
|
@ -74,23 +75,49 @@ const Panel: FC<NodePanelProps<ScheduleTriggerNodeType>> = ({
|
|||
}
|
||||
</label>
|
||||
{inputs.frequency === 'hourly' || inputs.frequency === 'once' ? (
|
||||
<DateTimePicker
|
||||
value={inputs.visual_config?.datetime}
|
||||
onChange={(datetime) => {
|
||||
<DatePicker
|
||||
notClearable={true}
|
||||
value={inputs.visual_config?.datetime ? dayjs(inputs.visual_config.datetime) : dayjs()}
|
||||
onChange={(date) => {
|
||||
const newInputs = {
|
||||
...inputs,
|
||||
visual_config: {
|
||||
...inputs.visual_config,
|
||||
datetime,
|
||||
datetime: date ? date.toISOString() : undefined,
|
||||
},
|
||||
}
|
||||
setInputs(newInputs)
|
||||
}}
|
||||
onClear={() => {
|
||||
const newInputs = {
|
||||
...inputs,
|
||||
visual_config: {
|
||||
...inputs.visual_config,
|
||||
datetime: undefined,
|
||||
},
|
||||
}
|
||||
setInputs(newInputs)
|
||||
}}
|
||||
placeholder={t('workflow.nodes.triggerSchedule.selectDateTime')}
|
||||
needTimePicker={true}
|
||||
/>
|
||||
) : (
|
||||
<TimePicker
|
||||
value={inputs.visual_config?.time || '11:30 AM'}
|
||||
onChange={handleTimeChange}
|
||||
notClearable={true}
|
||||
value={inputs.visual_config?.time
|
||||
? dayjs(`1/1/2000 ${inputs.visual_config.time}`)
|
||||
: dayjs('1/1/2000 11:30 AM')
|
||||
}
|
||||
onChange={(time) => {
|
||||
if (time) {
|
||||
const timeString = time.format('h:mm A')
|
||||
handleTimeChange(timeString)
|
||||
}
|
||||
}}
|
||||
onClear={() => {
|
||||
handleTimeChange('11:30 AM')
|
||||
}}
|
||||
placeholder={t('workflow.nodes.triggerSchedule.selectTime')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,15 @@ const useConfig = (id: string, payload: ScheduleTriggerNodeType) => {
|
|||
const newInputs = {
|
||||
...inputs,
|
||||
frequency,
|
||||
visual_config: {
|
||||
...inputs.visual_config,
|
||||
...(frequency === 'hourly' || frequency === 'once') && !inputs.visual_config?.datetime && {
|
||||
datetime: new Date().toISOString(),
|
||||
},
|
||||
...(frequency !== 'hourly' && frequency !== 'once') && {
|
||||
datetime: undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
|
|
|||
|
|
@ -204,9 +204,9 @@ describe('execution-time-calculator', () => {
|
|||
const result = getNextExecutionTimes(data, 3)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].getTime() - startTime.getTime()).toBe(2 * 60 * 60 * 1000)
|
||||
expect(result[1].getTime() - startTime.getTime()).toBe(4 * 60 * 60 * 1000)
|
||||
expect(result[2].getTime() - startTime.getTime()).toBe(6 * 60 * 60 * 1000)
|
||||
expect(result[0].getTime() - startTime.getTime()).toBe(0) // Starts from baseTime
|
||||
expect(result[1].getTime() - startTime.getTime()).toBe(2 * 60 * 60 * 1000)
|
||||
expect(result[2].getTime() - startTime.getTime()).toBe(4 * 60 * 60 * 1000)
|
||||
})
|
||||
|
||||
test('calculates minute intervals correctly', () => {
|
||||
|
|
@ -224,11 +224,12 @@ describe('execution-time-calculator', () => {
|
|||
const result = getNextExecutionTimes(data, 3)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].getTime() - startTime.getTime()).toBe(30 * 60 * 1000)
|
||||
expect(result[1].getTime() - startTime.getTime()).toBe(60 * 60 * 1000)
|
||||
expect(result[0].getTime() - startTime.getTime()).toBe(0) // Starts from baseTime
|
||||
expect(result[1].getTime() - startTime.getTime()).toBe(30 * 60 * 1000)
|
||||
expect(result[2].getTime() - startTime.getTime()).toBe(60 * 60 * 1000)
|
||||
})
|
||||
|
||||
test('handles past start time by calculating next interval', () => {
|
||||
test('calculates intervals from baseTime regardless of current time', () => {
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 14, 30, 0)) // Local time 2:30 PM
|
||||
const startTime = new Date(2024, 0, 15, 12, 0, 0) // Local time 12:00 PM
|
||||
|
||||
|
|
@ -243,11 +244,12 @@ describe('execution-time-calculator', () => {
|
|||
|
||||
const result = getNextExecutionTimes(data, 2)
|
||||
|
||||
expect(result[0].getHours()).toBe(15)
|
||||
expect(result[1].getHours()).toBe(16)
|
||||
// New logic: always starts from baseTime regardless of current time
|
||||
expect(result[0].getHours()).toBe(12) // Starts from baseTime (12:00 PM)
|
||||
expect(result[1].getHours()).toBe(13) // 1 hour after baseTime
|
||||
})
|
||||
|
||||
test('uses current time as default start time', () => {
|
||||
test('returns empty array when no datetime provided', () => {
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
|
|
@ -258,7 +260,7 @@ describe('execution-time-calculator', () => {
|
|||
|
||||
const result = getNextExecutionTimes(data, 1)
|
||||
|
||||
expect(result[0].getTime()).toBeGreaterThan(Date.now())
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('minute intervals should not have duplicates when recur_every changes', () => {
|
||||
|
|
@ -314,6 +316,220 @@ describe('execution-time-calculator', () => {
|
|||
expect(timeDiff).toBe(3 * 60 * 60 * 1000) // 3 hours in milliseconds
|
||||
}
|
||||
})
|
||||
|
||||
test('returns empty array when only time field provided (no datetime)', () => {
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 9, 0, 0)) // 9:00 AM
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '11:30 AM',
|
||||
recur_every: 1,
|
||||
recur_unit: 'hours',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 3)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
|
||||
test('prioritizes datetime over time field for hourly frequency', () => {
|
||||
const specificDateTime = new Date(2024, 0, 15, 14, 15, 0)
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '11:30 AM',
|
||||
datetime: specificDateTime.toISOString(),
|
||||
recur_every: 2,
|
||||
recur_unit: 'hours',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 2)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].getHours()).toBe(14) // Starts from datetime (14:15)
|
||||
expect(result[0].getMinutes()).toBe(15)
|
||||
expect(result[1].getHours()).toBe(16) // 2 hours after 14:15
|
||||
expect(result[1].getMinutes()).toBe(15)
|
||||
})
|
||||
|
||||
test('returns empty array when only time field provided (no datetime)', () => {
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 13, 0, 0)) // 1:00 PM
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '11:30 AM',
|
||||
recur_every: 2,
|
||||
recur_unit: 'hours',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 2)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
|
||||
test('returns empty array when only time field provided (no datetime) - PM test', () => {
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 9, 0, 0)) // 9:00 AM
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '2:30 PM',
|
||||
recur_every: 1,
|
||||
recur_unit: 'hours',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 2)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
|
||||
test('returns empty array when only time field provided (no datetime) - 12 AM test', () => {
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 22, 0, 0)) // 10:00 PM
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '12:00 AM',
|
||||
recur_every: 1,
|
||||
recur_unit: 'hours',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 2)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
|
||||
test('returns empty array when only time field provided (no datetime) - 12 PM test', () => {
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 9, 0, 0)) // 9:00 AM
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '12:00 PM',
|
||||
recur_every: 1,
|
||||
recur_unit: 'hours',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 2)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
|
||||
test('returns empty array when only time field provided (no datetime) - minute intervals', () => {
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 11, 0, 0)) // 11:00 AM
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '11:30 AM', // User sees 11:30 AM in UI
|
||||
recur_every: 1,
|
||||
recur_unit: 'minutes',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 5)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
|
||||
test('returns empty array when only time field provided (no datetime) - exact time test', () => {
|
||||
// Set current time to exactly 11:30 AM
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 11, 30, 0)) // 11:30 AM exactly
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '11:30 AM',
|
||||
recur_every: 1,
|
||||
recur_unit: 'minutes',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 3)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
|
||||
test('returns empty array when only time field provided (no datetime) - 5 minute intervals', () => {
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 11, 0, 0)) // 11:00 AM
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '11:30 AM',
|
||||
recur_every: 5,
|
||||
recur_unit: 'minutes',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 4)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
|
||||
test('returns empty array when only time field provided (no datetime) - past times', () => {
|
||||
// User sets time to 11:30 but current time is already 11:32
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 11, 32, 0)) // 11:32 AM
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '11:30 AM',
|
||||
recur_every: 1,
|
||||
recur_unit: 'minutes',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 3)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
|
||||
test('returns empty array when only time field provided (no datetime) - 3 minute intervals', () => {
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 11, 32, 0)) // 11:32 AM
|
||||
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '11:30 AM',
|
||||
recur_every: 3, // every 3 minutes
|
||||
recur_unit: 'minutes',
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(data, 4)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
|
||||
test('returns empty array when no datetime field (frequency switch scenario)', () => {
|
||||
jest.setSystemTime(new Date(2024, 0, 15, 11, 35, 0)) // 11:35 AM current time
|
||||
|
||||
// Simulate user switching FROM daily TO hourly
|
||||
// Daily frequency only has time field, no datetime
|
||||
const dataWithoutDatetime = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {
|
||||
time: '11:30 AM', // User sees this in UI
|
||||
recur_every: 1,
|
||||
recur_unit: 'minutes',
|
||||
// NO datetime field - this is the bug scenario
|
||||
},
|
||||
})
|
||||
|
||||
const result = getNextExecutionTimes(dataWithoutDatetime, 5)
|
||||
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNextExecutionTimes - cron mode', () => {
|
||||
|
|
@ -439,7 +655,8 @@ describe('execution-time-calculator', () => {
|
|||
|
||||
const result = getFormattedExecutionTimes(data, 1)
|
||||
|
||||
expect(result[0]).toMatch(/January 16, 2024 4:00 PM/)
|
||||
// New logic: starts from baseTime (2:00 PM), not baseTime + interval
|
||||
expect(result[0]).toMatch(/January 16, 2024 2:00 PM/)
|
||||
expect(result[0]).not.toMatch(/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/)
|
||||
})
|
||||
|
||||
|
|
@ -537,7 +754,7 @@ describe('execution-time-calculator', () => {
|
|||
expect(result).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('uses default values for missing config properties', () => {
|
||||
test('returns empty array for hourly when no datetime provided', () => {
|
||||
const data = createMockData({
|
||||
frequency: 'hourly',
|
||||
visual_config: {},
|
||||
|
|
@ -545,7 +762,7 @@ describe('execution-time-calculator', () => {
|
|||
|
||||
const result = getNextExecutionTimes(data, 1)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result).toHaveLength(0) // New logic requires datetime field for hourly
|
||||
})
|
||||
|
||||
test('handles malformed time strings gracefully', () => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const getCurrentTime = (): Date => {
|
|||
return new Date()
|
||||
}
|
||||
|
||||
// Helper function to get default datetime for once/hourly modes - consistent with DateTimePicker
|
||||
// Helper function to get default datetime for once/hourly modes - consistent with base DatePicker
|
||||
export const getDefaultDateTime = (): Date => {
|
||||
const defaultDate = new Date()
|
||||
defaultDate.setHours(11, 30, 0, 0)
|
||||
|
|
@ -25,26 +25,20 @@ export const getNextExecutionTimes = (data: ScheduleTriggerNodeType, count: numb
|
|||
const defaultTime = data.visual_config?.time || '11:30 AM'
|
||||
|
||||
if (data.frequency === 'hourly') {
|
||||
const recurEvery = data.visual_config?.recur_every || 1
|
||||
if (!data.visual_config?.datetime)
|
||||
return []
|
||||
|
||||
const baseTime = new Date(data.visual_config.datetime)
|
||||
const recurUnit = data.visual_config?.recur_unit || 'hours'
|
||||
const startTime = data.visual_config?.datetime ? new Date(data.visual_config.datetime) : getCurrentTime()
|
||||
const recurEvery = data.visual_config?.recur_every || 1
|
||||
|
||||
const intervalMs = recurUnit === 'hours'
|
||||
? recurEvery * 60 * 60 * 1000
|
||||
: recurEvery * 60 * 1000
|
||||
|
||||
// Calculate the initial offset if start time has passed
|
||||
const now = getCurrentTime()
|
||||
let initialOffset = 0
|
||||
|
||||
if (startTime <= now) {
|
||||
const timeDiff = now.getTime() - startTime.getTime()
|
||||
initialOffset = Math.floor(timeDiff / intervalMs)
|
||||
}
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const nextExecution = new Date(startTime.getTime() + (initialOffset + i + 1) * intervalMs)
|
||||
times.push(nextExecution)
|
||||
const executionTime = new Date(baseTime.getTime() + i * intervalMs)
|
||||
times.push(executionTime)
|
||||
}
|
||||
}
|
||||
else if (data.frequency === 'daily') {
|
||||
|
|
|
|||
Loading…
Reference in New Issue