diff --git a/src/components/cron-schedule-input/cron-schedule-input-popover/__tests__/cron-schedule-input-popover.test.tsx b/src/components/cron-schedule-input/cron-schedule-input-popover/__tests__/cron-schedule-input-popover.test.tsx index e2cf30eac..946ed31ee 100644 --- a/src/components/cron-schedule-input/cron-schedule-input-popover/__tests__/cron-schedule-input-popover.test.tsx +++ b/src/components/cron-schedule-input/cron-schedule-input-popover/__tests__/cron-schedule-input-popover.test.tsx @@ -28,14 +28,14 @@ describe(CronScheduleInputPopover.name, () => { setup({ fieldType: 'daysOfMonth' }); expect(screen.getByText('Day of Month')).toBeInTheDocument(); - expect(screen.getByText('0-31')).toBeInTheDocument(); + expect(screen.getByText('1-31')).toBeInTheDocument(); }); it('should render with months field type and show month aliases', () => { setup({ fieldType: 'months' }); expect(screen.getByText('Month')).toBeInTheDocument(); - expect(screen.getByText('0-12')).toBeInTheDocument(); + expect(screen.getByText('1-12')).toBeInTheDocument(); expect(screen.getByText('JAN-DEC')).toBeInTheDocument(); expect(screen.getByText('alternative single values')).toBeInTheDocument(); }); @@ -82,8 +82,8 @@ describe(CronScheduleInputPopover.name, () => { const testCases = [ { fieldType: 'minutes' as const, expectedRange: '0-59' }, { fieldType: 'hours' as const, expectedRange: '0-23' }, - { fieldType: 'daysOfMonth' as const, expectedRange: '0-31' }, - { fieldType: 'months' as const, expectedRange: '0-12' }, + { fieldType: 'daysOfMonth' as const, expectedRange: '1-31' }, + { fieldType: 'months' as const, expectedRange: '1-12' }, { fieldType: 'daysOfWeek' as const, expectedRange: '0-6' }, ]; diff --git a/src/utils/cron-validate/__tests__/cron-validate.test.ts b/src/utils/cron-validate/__tests__/cron-validate.test.ts index ef3a4575c..6adfdd5f5 100644 --- a/src/utils/cron-validate/__tests__/cron-validate.test.ts +++ b/src/utils/cron-validate/__tests__/cron-validate.test.ts @@ -48,11 +48,11 @@ describe('cronValidate', () => { maxValue: 23, }); expect(CRON_VALIDATE_CADENCE_PRESET.daysOfMonth).toEqual({ - minValue: 0, + minValue: 1, maxValue: 31, }); expect(CRON_VALIDATE_CADENCE_PRESET.months).toEqual({ - minValue: 0, + minValue: 1, maxValue: 12, }); expect(CRON_VALIDATE_CADENCE_PRESET.daysOfWeek).toEqual({ diff --git a/src/utils/cron-validate/cron-validate.constants.ts b/src/utils/cron-validate/cron-validate.constants.ts index a2586cd0d..3afdbcd21 100644 --- a/src/utils/cron-validate/cron-validate.constants.ts +++ b/src/utils/cron-validate/cron-validate.constants.ts @@ -26,11 +26,11 @@ export const CRON_VALIDATE_CADENCE_PRESET = { maxValue: 23, }, daysOfMonth: { - minValue: 0, + minValue: 1, // starting from 1 instead of 0, 0 is not standard maxValue: 31, }, months: { - minValue: 0, + minValue: 1, // starting from 1 instead of 0, 0 is not standard maxValue: 12, }, daysOfWeek: { diff --git a/src/views/workflow-actions/workflow-action-reset-form/__tests__/workflow-action-reset-form.test.tsx b/src/views/workflow-actions/workflow-action-reset-form/__tests__/workflow-action-reset-form.test.tsx index ee79224a9..1c3658d8d 100644 --- a/src/views/workflow-actions/workflow-action-reset-form/__tests__/workflow-action-reset-form.test.tsx +++ b/src/views/workflow-actions/workflow-action-reset-form/__tests__/workflow-action-reset-form.test.tsx @@ -192,6 +192,7 @@ function TestWrapper({ formErrors, formData }: TestProps) { return ( { ); await user.click(screen.getByRole('radio', { name: 'Cron' })); - expect( - screen.getByRole('textbox', { name: 'Cron Schedule (UTC)' }) - ).toHaveAttribute('aria-invalid', 'true'); + expect(screen.getByLabelText('Minute')).toHaveAttribute( + 'aria-invalid', + 'true' + ); + expect(screen.getByLabelText('Hour')).toHaveAttribute( + 'aria-invalid', + 'true' + ); + expect(screen.getByLabelText('Day of Month')).toHaveAttribute( + 'aria-invalid', + 'true' + ); + expect(screen.getByLabelText('Month')).toHaveAttribute( + 'aria-invalid', + 'true' + ); + expect(screen.getByLabelText('Day of Week')).toHaveAttribute( + 'aria-invalid', + 'true' + ); }); it('handles input changes correctly', async () => { @@ -201,6 +218,7 @@ function TestWrapper({ formErrors, formData }: TestProps) { return ( ; + +type CronValidateResult = ReturnType; + +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[]; +}) { + const mockCronResult: Partial = { + isValid: jest.fn(() => isValid), + getError: jest.fn(() => errors), + }; + + mockCronValidate.mockReturnValue(mockCronResult as CronValidateResult); + + const result = getCronFieldsError(cronText); + + return { + result, + mockCronResult, + mockIsValid: mockCronResult.isValid, + mockGetError: mockCronResult.getError, + }; +} diff --git a/src/views/workflow-actions/workflow-action-start-form/helpers/__tests__/transform-start-workflow-form-to-submission.test.ts b/src/views/workflow-actions/workflow-action-start-form/helpers/__tests__/transform-start-workflow-form-to-submission.test.ts index 0b15bfd8d..e0acba92b 100644 --- a/src/views/workflow-actions/workflow-action-start-form/helpers/__tests__/transform-start-workflow-form-to-submission.test.ts +++ b/src/views/workflow-actions/workflow-action-start-form/helpers/__tests__/transform-start-workflow-form-to-submission.test.ts @@ -37,7 +37,13 @@ describe('transformStartWorkflowFormToSubmission', () => { const formData: StartWorkflowFormData = { ...baseFormData, scheduleType: 'CRON', - cronSchedule: '0 9 * * 1-5', + cronSchedule: { + minutes: '0', + hours: '9', + daysOfMonth: '*', + months: '*', + daysOfWeek: '1-5', + }, }; const result = transformStartWorkflowFormToSubmission(formData); @@ -50,7 +56,13 @@ describe('transformStartWorkflowFormToSubmission', () => { ...baseFormData, scheduleType: 'NOW', firstRunAt: '2024-01-01T10:00:00Z', - cronSchedule: '0 9 * * 1-5', + cronSchedule: { + minutes: '0', + hours: '9', + daysOfMonth: '*', + months: '*', + daysOfWeek: '1-5', + }, }; const result = transformStartWorkflowFormToSubmission(formData); diff --git a/src/views/workflow-actions/workflow-action-start-form/helpers/get-cron-fields-error.ts b/src/views/workflow-actions/workflow-action-start-form/helpers/get-cron-fields-error.ts new file mode 100644 index 000000000..e35cba164 --- /dev/null +++ b/src/views/workflow-actions/workflow-action-start-form/helpers/get-cron-fields-error.ts @@ -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'; + +import { type CronFieldsError } from '../workflow-action-start-form.types'; + +export const getCronFieldsError = (cronString: string): CronFieldsError => { + const cronObj = cronValidate(cronString); + + if (!cronObj.isValid()) { + const errors = cronObj.getError(); + const errorFieldsKeys = CRON_FIELD_ORDER; + const fieldsErrors: Partial> = {}; + 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; +}; diff --git a/src/views/workflow-actions/workflow-action-start-form/helpers/transform-start-workflow-form-to-submission.ts b/src/views/workflow-actions/workflow-action-start-form/helpers/transform-start-workflow-form-to-submission.ts index 408006c73..618b50b34 100644 --- a/src/views/workflow-actions/workflow-action-start-form/helpers/transform-start-workflow-form-to-submission.ts +++ b/src/views/workflow-actions/workflow-action-start-form/helpers/transform-start-workflow-form-to-submission.ts @@ -1,3 +1,4 @@ +import { CRON_FIELD_ORDER } from '@/components/cron-schedule-input/cron-schedule-input.constants'; import { type Json } from '@/route-handlers/start-workflow/start-workflow.types'; import { @@ -22,7 +23,9 @@ export default function transformStartWorkflowFormToSubmission( firstRunAt: formData.firstRunAt, }), ...(formData.scheduleType === 'CRON' && { - cronSchedule: formData.cronSchedule, + cronSchedule: CRON_FIELD_ORDER.map( + (key) => formData.cronSchedule?.[key] + ).join(' '), }), ...(formData.enableRetryPolicy && { retryPolicy: { diff --git a/src/views/workflow-actions/workflow-action-start-form/schemas/start-workflow-form-schema.ts b/src/views/workflow-actions/workflow-action-start-form/schemas/start-workflow-form-schema.ts index 906a8512b..4cd8bc97d 100644 --- a/src/views/workflow-actions/workflow-action-start-form/schemas/start-workflow-form-schema.ts +++ b/src/views/workflow-actions/workflow-action-start-form/schemas/start-workflow-form-schema.ts @@ -1,10 +1,13 @@ import { z } from 'zod'; +import { CRON_FIELD_ORDER } from '@/components/cron-schedule-input/cron-schedule-input.constants'; import { WORKER_SDK_LANGUAGES, WORKFLOW_ID_REUSE_POLICIES, } from '@/route-handlers/start-workflow/start-workflow.constants'; +import { getCronFieldsError } from '../helpers/get-cron-fields-error'; + const baseSchema = z.object({ workflowType: z.object({ name: z.string().min(1, 'Workflow type name is required'), @@ -81,8 +84,45 @@ const baseSchema = z.object({ // Schedule type fields scheduleType: z.enum(['NOW', 'LATER', 'CRON']), firstRunAt: z.string().optional(), - cronSchedule: z.string().optional(), + cronSchedule: z + .object({ + minutes: z.string().min(1, 'Minutes is required'), + hours: z.string().min(1, 'Hours is required'), + daysOfMonth: z.string().min(1, 'Days of month is required'), + months: z.string().min(1, 'Months is required'), + daysOfWeek: z.string().min(1, 'Days of week is required'), + }) + .superRefine((data, ctx) => { + const allFieldsHasValue = Object.values(data).every((value) => + Boolean(value) + ); + + // If there are missing fields, no need to validate the cron schedule format. + if (!allFieldsHasValue) { + return; + } + + const cronString = CRON_FIELD_ORDER.map((key) => data[key]).join(' '); + const cronFieldsErrors = getCronFieldsError(cronString); + if (!cronFieldsErrors) return; + + if (cronFieldsErrors?.general) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invalid cron schedule format', + }); + } else { + Object.entries(cronFieldsErrors).forEach(([errorKey, errorMessage]) => { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: errorMessage, + path: [errorKey], + }); + }); + } + }) + .optional(), // Retry policy fields enableRetryPolicy: z.boolean().optional(), limitRetries: z.enum(['ATTEMPTS', 'DURATION']).optional(), diff --git a/src/views/workflow-actions/workflow-action-start-form/workflow-action-start-form.tsx b/src/views/workflow-actions/workflow-action-start-form/workflow-action-start-form.tsx index 9d4ef374d..69c96c580 100644 --- a/src/views/workflow-actions/workflow-action-start-form/workflow-action-start-form.tsx +++ b/src/views/workflow-actions/workflow-action-start-form/workflow-action-start-form.tsx @@ -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 { Controller, useWatch } from 'react-hook-form'; +import { get, isObjectLike } from 'lodash'; +import { Controller, type GlobalError, 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'; @@ -18,14 +19,25 @@ export default function WorkflowActionStartForm({ fieldErrors, control, clearErrors, - formData: _formData, + formData, + trigger, }: Props) { const now = useMemo(() => new Date(), []); - const getErrorMessage = (field: string) => { + const getFieldErrorMessages = (field: string) => { const error = get(fieldErrors, field); if (Array.isArray(error)) { return error.map((err) => err?.message); + } else if (isObjectLike(error) && !error.message) { + return Object.entries(error).reduce( + (acc, [key, err]) => { + if (err?.message) { + acc[key] = err?.message; + } + return acc; + }, + {} as Record + ); } return error?.message; }; @@ -53,7 +65,7 @@ export default function WorkflowActionStartForm({ field.onChange(e.target.value); }} onBlur={field.onBlur} - error={Boolean(getErrorMessage('taskList.name'))} + error={Boolean(getFieldErrorMessages('taskList.name'))} size="compact" placeholder="Enter task list name" /> @@ -76,7 +88,7 @@ export default function WorkflowActionStartForm({ field.onChange(e.target.value); }} onBlur={field.onBlur} - error={Boolean(getErrorMessage('workflowType.name'))} + error={Boolean(getFieldErrorMessages('workflowType.name'))} size="compact" placeholder="Enter workflow type name" /> @@ -103,7 +115,7 @@ export default function WorkflowActionStartForm({ }} onBlur={field.onBlur} error={Boolean( - getErrorMessage('executionStartToCloseTimeoutSeconds') + getFieldErrorMessages('executionStartToCloseTimeoutSeconds') )} placeholder="Enter timeout in seconds" size="compact" @@ -127,7 +139,7 @@ export default function WorkflowActionStartForm({ onChange={(e) => { onChange(e.currentTarget.value); }} - error={Boolean(getErrorMessage('workerSDKLanguage'))} + error={Boolean(getFieldErrorMessages('workerSDKLanguage'))} align="horizontal" > {WORKER_SDK_LANGUAGES.map((language) => ( @@ -150,7 +162,7 @@ export default function WorkflowActionStartForm({ placeholder="Enter JSON input" value={field.value} onChange={field.onChange} - error={getErrorMessage('input')} + error={getFieldErrorMessages('input')} addButtonText="Add argument" /> )} @@ -173,7 +185,7 @@ export default function WorkflowActionStartForm({ clearErrors('cronSchedule'); onChange(e.currentTarget.value); }} - error={Boolean(getErrorMessage('scheduleType'))} + error={Boolean(getFieldErrorMessages('scheduleType'))} align="horizontal" > Now @@ -205,7 +217,7 @@ export default function WorkflowActionStartForm({ onChange(undefined); } }} - error={Boolean(getErrorMessage('firstRunAt'))} + error={Boolean(getFieldErrorMessages('firstRunAt'))} size="compact" timeSelectStart formatString="yyyy/MM/dd HH:mm" @@ -221,20 +233,15 @@ export default function WorkflowActionStartForm({ ( - { - field.onChange(e.target.value); + render={({ field }) => ( + { + field.onChange(value); + trigger('cronSchedule'); }} onBlur={field.onBlur} - error={Boolean(getErrorMessage('cronSchedule'))} - placeholder="* * * * *" + error={getFieldErrorMessages('cronSchedule')} /> )} /> @@ -244,8 +251,8 @@ export default function WorkflowActionStartForm({ ); diff --git a/src/views/workflow-actions/workflow-action-start-form/workflow-action-start-form.types.ts b/src/views/workflow-actions/workflow-action-start-form/workflow-action-start-form.types.ts index d37028d5c..41757b13c 100644 --- a/src/views/workflow-actions/workflow-action-start-form/workflow-action-start-form.types.ts +++ b/src/views/workflow-actions/workflow-action-start-form/workflow-action-start-form.types.ts @@ -1,6 +1,7 @@ import { type z } from 'zod'; import type startWorkflowRequestBodySchema from '@/route-handlers/start-workflow/schemas/start-workflow-request-body-schema'; +import { type CronData } from '@/utils/cron-validate/cron-validate.types'; import { type WorkflowActionFormProps } from '../workflow-actions.types'; @@ -12,7 +13,9 @@ export type SubFormProps = Pick< Props, 'control' | 'clearErrors' | 'formData' > & { - getErrorMessage: (field: string) => string | undefined; + getFieldErrorMessages: ( + field: string + ) => string | string[] | Record | undefined; }; export type StartWorkflowFormData = z.infer; @@ -20,3 +23,7 @@ export type StartWorkflowFormData = z.infer; export type StartWorkflowSubmissionData = z.infer< typeof startWorkflowRequestBodySchema >; + +export type CronFieldsError = Partial< + Record +> | null; diff --git a/src/views/workflow-actions/workflow-action-start-optional-section/__tests__/workflow-action-start-optional-section.test.tsx b/src/views/workflow-actions/workflow-action-start-optional-section/__tests__/workflow-action-start-optional-section.test.tsx index 629d91ba9..3db19442b 100644 --- a/src/views/workflow-actions/workflow-action-start-optional-section/__tests__/workflow-action-start-optional-section.test.tsx +++ b/src/views/workflow-actions/workflow-action-start-optional-section/__tests__/workflow-action-start-optional-section.test.tsx @@ -34,7 +34,7 @@ describe('WorkflowActionStartForm', () => { }; const { user } = await setup({ - getErrorMessage: (key) => get(formErrors, key)?.message, + getFieldErrorMessages: (key) => get(formErrors, key)?.message, }); await user.click(screen.getByText('Show Optional Configurations')); @@ -125,10 +125,10 @@ describe('WorkflowActionStartForm', () => { type TestProps = { formData: Props['formData']; - getErrorMessage: Props['getErrorMessage']; + getFieldErrorMessages: Props['getFieldErrorMessages']; }; -function TestWrapper({ formData, getErrorMessage }: TestProps) { +function TestWrapper({ formData, getFieldErrorMessages }: TestProps) { const methods = useForm({ defaultValues: formData, }); @@ -138,7 +138,7 @@ function TestWrapper({ formData, getErrorMessage }: TestProps) { control={methods.control} clearErrors={methods.clearErrors} formData={formData} - getErrorMessage={getErrorMessage} + getFieldErrorMessages={getFieldErrorMessages} /> ); } @@ -155,11 +155,16 @@ async function setup({ enableRetryPolicy: false, retryPolicy: undefined, }, - getErrorMessage = () => undefined, + getFieldErrorMessages = () => undefined, }: Partial) { const user = userEvent.setup(); - render(); + render( + + ); return { user }; } diff --git a/src/views/workflow-actions/workflow-action-start-optional-section/workflow-action-start-optional-section.tsx b/src/views/workflow-actions/workflow-action-start-optional-section/workflow-action-start-optional-section.tsx index 08a8899e9..fdfe658b2 100644 --- a/src/views/workflow-actions/workflow-action-start-optional-section/workflow-action-start-optional-section.tsx +++ b/src/views/workflow-actions/workflow-action-start-optional-section/workflow-action-start-optional-section.tsx @@ -24,8 +24,8 @@ import { type Props } from './workflow-action-start-optional-section.types'; export default function WorkflowActionStartOptionalSection({ control, clearErrors, - formData: _formData, - getErrorMessage, + formData, + getFieldErrorMessages, }: Props) { const { cls } = useStyletronClasses(cssStyles); @@ -72,7 +72,7 @@ export default function WorkflowActionStartOptionalSection({ field.onChange(e.target.value); }} onBlur={field.onBlur} - error={Boolean(getErrorMessage('workflowId'))} + error={Boolean(getFieldErrorMessages('workflowId'))} size="compact" placeholder="Enter workflow ID" /> @@ -96,7 +96,9 @@ export default function WorkflowActionStartOptionalSection({ onChange={(params) => { onChange(params.value[0]?.id || undefined); }} - error={Boolean(getErrorMessage('workflowIdReusePolicy'))} + error={Boolean( + getFieldErrorMessages('workflowIdReusePolicy') + )} size="compact" placeholder="Select reuse policy" clearable={false} @@ -109,8 +111,8 @@ export default function WorkflowActionStartOptionalSection({ @@ -129,7 +131,7 @@ export default function WorkflowActionStartOptionalSection({ }} overrides={overrides.jsonInput} onBlur={field.onBlur} - error={Boolean(getErrorMessage('header'))} + error={Boolean(getFieldErrorMessages('header'))} size="compact" placeholder='{"key":"value"}' rows={3} @@ -154,7 +156,7 @@ export default function WorkflowActionStartOptionalSection({ }} overrides={overrides.jsonInput} onBlur={field.onBlur} - error={Boolean(getErrorMessage('memo'))} + error={Boolean(getFieldErrorMessages('memo'))} size="compact" placeholder='{"key":"value"}' rows={3} @@ -179,7 +181,7 @@ export default function WorkflowActionStartOptionalSection({ }} overrides={overrides.jsonInput} onBlur={field.onBlur} - error={Boolean(getErrorMessage('searchAttributes'))} + error={Boolean(getFieldErrorMessages('searchAttributes'))} size="compact" placeholder='{"key":"value"}' rows={3} diff --git a/src/views/workflow-actions/workflow-action-start-retry-policy/__tests__/workflow-action-start-retry-policy.test.tsx b/src/views/workflow-actions/workflow-action-start-retry-policy/__tests__/workflow-action-start-retry-policy.test.tsx index 8caa93962..b24f5c490 100644 --- a/src/views/workflow-actions/workflow-action-start-retry-policy/__tests__/workflow-action-start-retry-policy.test.tsx +++ b/src/views/workflow-actions/workflow-action-start-retry-policy/__tests__/workflow-action-start-retry-policy.test.tsx @@ -32,7 +32,7 @@ describe('WorkflowActionStartForm', () => { }; const { user } = await setup({ - getErrorMessage: (key) => get(formErrors, key)?.message, + getFieldErrorMessages: (key) => get(formErrors, key)?.message, }); await user.click( @@ -146,10 +146,10 @@ describe('WorkflowActionStartForm', () => { type TestProps = { formData: Props['formData']; - getErrorMessage: Props['getErrorMessage']; + getFieldErrorMessages: Props['getFieldErrorMessages']; }; -function TestWrapper({ formData, getErrorMessage }: TestProps) { +function TestWrapper({ formData, getFieldErrorMessages }: TestProps) { const methods = useForm({ defaultValues: formData, }); @@ -159,7 +159,7 @@ function TestWrapper({ formData, getErrorMessage }: TestProps) { control={methods.control} clearErrors={methods.clearErrors} formData={formData} - getErrorMessage={getErrorMessage} + getFieldErrorMessages={getFieldErrorMessages} /> ); } @@ -176,11 +176,16 @@ async function setup({ enableRetryPolicy: false, retryPolicy: undefined, }, - getErrorMessage = () => undefined, + getFieldErrorMessages = () => undefined, }: Partial) { const user = userEvent.setup(); - render(); + render( + + ); return { user }; } diff --git a/src/views/workflow-actions/workflow-action-start-retry-policy/workflow-action-start-retry-policy.tsx b/src/views/workflow-actions/workflow-action-start-retry-policy/workflow-action-start-retry-policy.tsx index 134e25fb3..fdea2cb20 100644 --- a/src/views/workflow-actions/workflow-action-start-retry-policy/workflow-action-start-retry-policy.tsx +++ b/src/views/workflow-actions/workflow-action-start-retry-policy/workflow-action-start-retry-policy.tsx @@ -17,8 +17,7 @@ import { type Props } from './workflow-action-start-retry-policy.types'; export default function WorkflowActionStartRetryPolicy({ control, clearErrors, - formData: _formData, - getErrorMessage, + getFieldErrorMessages, }: Props) { const { cls } = useStyletronClasses(cssStyles); const enableRetryPolicy = useWatch({ @@ -58,7 +57,7 @@ export default function WorkflowActionStartRetryPolicy({ clearErrors('retryPolicy.expirationIntervalSeconds'); onChange(e.currentTarget.checked); }} - error={Boolean(getErrorMessage('enableRetryPolicy'))} + error={Boolean(getFieldErrorMessages('enableRetryPolicy'))} > Enable retry policy @@ -82,7 +81,7 @@ export default function WorkflowActionStartRetryPolicy({ type="number" onBlur={field.onBlur} error={Boolean( - getErrorMessage('retryPolicy.initialIntervalSeconds') + getFieldErrorMessages('retryPolicy.initialIntervalSeconds') )} size="compact" placeholder="Enter initial interval in seconds" @@ -107,7 +106,7 @@ export default function WorkflowActionStartRetryPolicy({ min={1} onBlur={field.onBlur} error={Boolean( - getErrorMessage('retryPolicy.backoffCoefficient') + getFieldErrorMessages('retryPolicy.backoffCoefficient') )} size="compact" placeholder="Enter backoff coefficient" @@ -131,7 +130,7 @@ export default function WorkflowActionStartRetryPolicy({ min={1} onBlur={field.onBlur} error={Boolean( - getErrorMessage('retryPolicy.maximumIntervalSeconds') + getFieldErrorMessages('retryPolicy.maximumIntervalSeconds') )} size="compact" placeholder="Enter maximum interval in seconds" @@ -157,7 +156,7 @@ export default function WorkflowActionStartRetryPolicy({ clearErrors('retryPolicy.expirationIntervalSeconds'); onChange(e.currentTarget.value); }} - error={Boolean(getErrorMessage('limitRetries'))} + error={Boolean(getFieldErrorMessages('limitRetries'))} align="horizontal" > Attempts @@ -183,7 +182,9 @@ export default function WorkflowActionStartRetryPolicy({ min={1} onBlur={field.onBlur} error={Boolean( - getErrorMessage('retryPolicy.expirationIntervalSeconds') + getFieldErrorMessages( + 'retryPolicy.expirationIntervalSeconds' + ) )} size="compact" placeholder="Enter expiration interval in seconds" @@ -209,7 +210,7 @@ export default function WorkflowActionStartRetryPolicy({ min={1} onBlur={field.onBlur} error={Boolean( - getErrorMessage('retryPolicy.maximumAttempts') + getFieldErrorMessages('retryPolicy.maximumAttempts') )} size="compact" placeholder="Enter maximum attempts" diff --git a/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx b/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx index 1cd4054d6..a4b2fa777 100644 --- a/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx +++ b/src/views/workflow-actions/workflow-actions-modal-content/workflow-actions-modal-content.tsx @@ -37,6 +37,7 @@ export default function WorkflowActionsModalContent< control, watch, clearErrors, + trigger, } = useForm({ resolver: action.modal.formSchema ? zodResolver(action.modal.formSchema) @@ -122,6 +123,7 @@ export default function WorkflowActionsModalContent< fieldErrors={validationErrors} clearErrors={clearErrors} control={control} + trigger={trigger} cluster={params.cluster} domain={params.domain} workflowId={params.workflowId} diff --git a/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.styles.ts b/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.styles.ts index 19e74a9a3..141a5134c 100644 --- a/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.styles.ts +++ b/src/views/workflow-actions/workflow-actions-modal/workflow-actions-modal.styles.ts @@ -10,5 +10,10 @@ export const overrides = { right: $theme.sizing.scale800, }), }, + Dialog: { + style: (): StyleObject => ({ + width: '600px', + }), + }, } satisfies ModalOverrides, }; diff --git a/src/views/workflow-actions/workflow-actions.types.ts b/src/views/workflow-actions/workflow-actions.types.ts index 48af133b6..9f665de87 100644 --- a/src/views/workflow-actions/workflow-actions.types.ts +++ b/src/views/workflow-actions/workflow-actions.types.ts @@ -6,6 +6,7 @@ import { type Control, type FieldErrors, type FieldValues, + type UseFormTrigger, } from 'react-hook-form'; import { type z } from 'zod'; @@ -33,6 +34,7 @@ export type WorkflowActionFormProps = { fieldErrors: FieldErrors; control: Control; clearErrors: UseFormClearErrors; + trigger: UseFormTrigger; cluster: string; domain: string; workflowId: string;