From 6a3d135d4936e0e645e8324cd64a6bc9286fd13b Mon Sep 17 00:00:00 2001 From: lyzno1 <92089059+lyzno1@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:37:57 +0800 Subject: [PATCH] fix: simplify trigger-schedule hourly mode calculation and improve UI consistency (#24082) Co-authored-by: zhangxuhe1 --- .../date-picker/index.tsx | 21 +- .../time-picker/index.tsx | 21 +- .../base/date-and-time-picker/types.ts | 2 + .../components/date-time-picker.spec.tsx | 139 ---------- .../components/date-time-picker.tsx | 158 ------------ .../components/frequency-selector.tsx | 3 +- .../components/monthly-days-selector.tsx | 8 +- .../components/next-execution-times.tsx | 4 +- .../components/time-picker.tsx | 230 ----------------- .../components/weekday-selector.tsx | 8 +- .../workflow/nodes/trigger-schedule/panel.tsx | 43 +++- .../nodes/trigger-schedule/use-config.ts | 9 + .../utils/execution-time-calculator.spec.ts | 243 +++++++++++++++++- .../utils/execution-time-calculator.ts | 22 +- 14 files changed, 320 insertions(+), 591 deletions(-) delete mode 100644 web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.spec.tsx delete mode 100644 web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.tsx delete mode 100644 web/app/components/workflow/nodes/trigger-schedule/components/time-picker.tsx diff --git a/web/app/components/base/date-and-time-picker/date-picker/index.tsx b/web/app/components/base/date-and-time-picker/date-picker/index.tsx index f99b8257c1..53cf383dad 100644 --- a/web/app/components/base/date-and-time-picker/date-picker/index.tsx +++ b/web/app/components/base/date-and-time-picker/date-picker/index.tsx @@ -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 = ({ {renderTrigger ? (renderTrigger({ @@ -224,15 +225,17 @@ const DatePicker = ({ - + {!notClearable && ( + + )} )} diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.tsx index 8ef10abc2e..830ba4bf0b 100644 --- a/web/app/components/base/date-and-time-picker/time-picker/index.tsx +++ b/web/app/components/base/date-and-time-picker/time-picker/index.tsx @@ -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 = ({ {renderTrigger ? (renderTrigger({ @@ -139,15 +140,17 @@ const TimePicker = ({ - + {!notClearable && ( + + )} )} diff --git a/web/app/components/base/date-and-time-picker/types.ts b/web/app/components/base/date-and-time-picker/types.ts index 4ac01c142a..68d6967c2b 100644 --- a/web/app/components/base/date-and-time-picker/types.ts +++ b/web/app/components/base/date-and-time-picker/types.ts @@ -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 = { diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.spec.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.spec.tsx deleted file mode 100644 index 4d5a55029a..0000000000 --- a/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.spec.tsx +++ /dev/null @@ -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 = { - '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() - - 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() - - const button = screen.getByRole('button') - expect(button).toBeInTheDocument() - }) - - test('opens picker when button is clicked', () => { - render() - - 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() - - 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() - - 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() - - 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() - - 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() - - 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() - - const button = screen.getByRole('button') - expect(button.textContent).toMatch(/January 15, 2024/) - expect(button.textContent).toMatch(/\d{1,2}:30 [AP]M/) - }) -}) diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.tsx deleted file mode 100644 index 5c8dffadd0..0000000000 --- a/web/app/components/workflow/nodes/trigger-schedule/components/date-time-picker.tsx +++ /dev/null @@ -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) => { - 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 ( -
- - - {isOpen && ( -
-
-

{t('workflow.nodes.triggerSchedule.selectDateTime')}

-
- -
- -
- -
- -
- - -
-
- )} - - {isOpen && ( -
{ - setTempValue('') - setIsOpen(false) - }} - /> - )} -
- ) -} - -export default DateTimePicker diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/frequency-selector.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/frequency-selector.tsx index fa48a66350..2eabb6d85f 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/components/frequency-selector.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/components/frequency-selector.tsx @@ -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} diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx index 4c9c8b75b6..68936bf253 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx @@ -22,7 +22,7 @@ const MonthlyDaysSelector = ({ selectedDay, onChange }: MonthlyDaysSelectorProps return (
-