-
Notifications
You must be signed in to change notification settings - Fork 125
feat: Use cron input in start workflow form #1040
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
34b69c2
ae5a45f
4c394ee
275e6aa
7935c34
1718596
5b45ea7
710caa0
6079b6c
8e52843
3121a12
7678861
c505c30
8f3b07a
c349f99
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { type Result } from 'cron-validate/lib/result'; | ||
| import { type Options } from 'cron-validate/lib/types'; | ||
|
|
||
| import { cronValidate } from '@/utils/cron-validate/cron-validate'; | ||
| import { type CronData } from '@/utils/cron-validate/cron-validate.types'; | ||
|
|
||
| import { getCronFieldsError } from '../get-cron-fields-error'; | ||
|
|
||
| jest.mock('@/utils/cron-validate/cron-validate'); | ||
|
|
||
| const mockCronValidate = cronValidate as jest.MockedFunction< | ||
| typeof cronValidate | ||
| >; | ||
|
|
||
| describe('getCronFieldsError', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('should return null for valid cron expression', () => { | ||
| const { result } = setup({ | ||
| isValid: true, | ||
| }); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it('should handle field-specific errors correctly', () => { | ||
| const { result } = setup({ | ||
| isValid: false, | ||
| errors: [ | ||
| 'Invalid value for minutes field: x', | ||
| 'Invalid value for hours field: x', | ||
| 'Invalid value for daysOfMonth field: x', | ||
| 'Invalid value for months field: x', | ||
| 'Invalid value for daysOfWeek field: x', | ||
| ], | ||
| }); | ||
|
|
||
| expect(result).toEqual({ | ||
| minutes: 'Invalid value for minutes field: x', | ||
| hours: 'Invalid value for hours field: x', | ||
| daysOfWeek: 'Invalid value for daysOfWeek field: x', | ||
| daysOfMonth: 'Invalid value for daysOfMonth field: x', | ||
| months: 'Invalid value for months field: x', | ||
| }); | ||
| }); | ||
|
|
||
| it('should return general error when errors array has no field-specific matches', () => { | ||
| const { result } = setup({ | ||
| isValid: false, | ||
| errors: ['Malformed cron expression', 'Cannot parse input'], | ||
| }); | ||
|
|
||
| expect(result).toEqual({ | ||
| general: 'Malformed cron expression', | ||
| }); | ||
| }); | ||
|
|
||
| it('should handle mixed field-specific and general errors', () => { | ||
| const { result } = setup({ | ||
| isValid: false, | ||
| errors: [ | ||
| 'Invalid value for minutes field: abc', | ||
| 'Syntax error in expression', | ||
| 'Invalid value for hours field: xyz', | ||
| ], | ||
| }); | ||
|
|
||
| // Should prioritize field-specific errors over general ones | ||
| expect(result).toEqual({ | ||
| minutes: 'Invalid value for minutes field: abc', | ||
| hours: 'Invalid value for hours field: xyz', | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| function setup({ | ||
| cronText = '* * * * *', | ||
| isValid = true, | ||
| errors = [], | ||
| }: { | ||
| cronText?: string; | ||
| isValid?: boolean; | ||
| errors?: string[]; | ||
| }) { | ||
| // Giving type for cron result is not straightforward, because of the conditional type within it | ||
| // So we make sure we are correctly creating partial mock and cast it to any | ||
| const mockCronResult = { | ||
| isValid: jest.fn(() => isValid), | ||
| getError: jest.fn(() => errors), | ||
| } satisfies Partial<Result<Options | CronData, string[]>> as any; | ||
|
|
||
| mockCronValidate.mockReturnValue(mockCronResult); | ||
|
|
||
| const result = getCronFieldsError(cronText); | ||
|
|
||
| return { | ||
| result, | ||
| mockCronResult, | ||
| mockIsValid: mockCronResult.isValid, | ||
| mockGetError: mockCronResult.getError, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { CRON_FIELD_ORDER } from '@/components/cron-schedule-input/cron-schedule-input.constants'; | ||
| import { cronValidate } from '@/utils/cron-validate/cron-validate'; | ||
| import { type CronData } from '@/utils/cron-validate/cron-validate.types'; | ||
|
|
||
| export const getCronFieldsError = ( | ||
| cronString: string | ||
| ): Partial<Record<keyof CronData | 'general', string>> | null => { | ||
Assem-Uber marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const cronObj = cronValidate(cronString); | ||
|
|
||
| if (!cronObj.isValid()) { | ||
| const errors = cronObj.getError(); | ||
| const errorFieldsKeys = CRON_FIELD_ORDER; | ||
| const fieldsErrors: Partial<Record<keyof CronData, string>> = {}; | ||
| errors.forEach((e) => { | ||
| const errorKey = errorFieldsKeys.find((key) => | ||
| e.includes(`${key} field`) | ||
| ); | ||
| if (errorKey) fieldsErrors[errorKey] = e; | ||
| }); | ||
|
|
||
| if (!Object.keys(fieldsErrors).length) return { general: errors[0] }; | ||
| else return fieldsErrors; | ||
| } | ||
|
|
||
| return null; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,10 @@ import { DatePicker } from 'baseui/datepicker'; | |
| import { FormControl } from 'baseui/form-control'; | ||
| import { Input } from 'baseui/input'; | ||
| import { RadioGroup, Radio } from 'baseui/radio'; | ||
| import { get } from 'lodash'; | ||
| import { get, isObjectLike } from 'lodash'; | ||
| import { Controller, useWatch } from 'react-hook-form'; | ||
|
|
||
| import CronScheduleInput from '@/components/cron-schedule-input/cron-schedule-input'; | ||
| import MultiJsonInput from '@/components/multi-json-input/multi-json-input'; | ||
| import { WORKER_SDK_LANGUAGES } from '@/route-handlers/start-workflow/start-workflow.constants'; | ||
|
|
||
|
|
@@ -19,13 +20,16 @@ export default function WorkflowActionStartForm({ | |
| control, | ||
| clearErrors, | ||
| formData: _formData, | ||
Assem-Uber marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| trigger, | ||
| }: Props) { | ||
| const now = useMemo(() => new Date(), []); | ||
|
|
||
| const getErrorMessage = (field: string) => { | ||
|
||
| const error = get(fieldErrors, field); | ||
| if (Array.isArray(error)) { | ||
| return error.map((err) => err?.message); | ||
| } else if (isObjectLike(error) && !error.message) { | ||
| return error; | ||
| } | ||
| return error?.message; | ||
| }; | ||
|
|
@@ -221,20 +225,15 @@ export default function WorkflowActionStartForm({ | |
| <Controller | ||
| name="cronSchedule" | ||
| control={control} | ||
| defaultValue="" | ||
| render={({ field: { ref, ...field } }) => ( | ||
| <Input | ||
| {...field} | ||
| // @ts-expect-error - inputRef expects ref object while ref is a callback. It should support both. | ||
| inputRef={ref} | ||
| aria-label="Cron Schedule (UTC)" | ||
| size="compact" | ||
| onChange={(e) => { | ||
| field.onChange(e.target.value); | ||
| render={({ field }) => ( | ||
| <CronScheduleInput | ||
| value={field.value} | ||
| onChange={(value) => { | ||
| field.onChange(value); | ||
| trigger('cronSchedule'); | ||
| }} | ||
| onBlur={field.onBlur} | ||
| error={Boolean(getErrorMessage('cronSchedule'))} | ||
| placeholder="* * * * *" | ||
| error={getErrorMessage('cronSchedule')} | ||
| /> | ||
| )} | ||
| /> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.