Skip to content

Commit f4b9c07

Browse files
authored
Reset form component (#869)
Workflow reset form component
1 parent be74c5c commit f4b9c07

File tree

5 files changed

+223
-0
lines changed

5 files changed

+223
-0
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React from 'react';
2+
3+
import { type FieldErrors, useForm, type Control } from 'react-hook-form';
4+
5+
import { render, screen, userEvent } from '@/test-utils/rtl';
6+
7+
import WorkflowActionResetForm from '../workflow-action-reset-form';
8+
import { type ResetWorkflowFormData } from '../workflow-action-reset-form.types';
9+
10+
describe('WorkflowActionResetForm', () => {
11+
it('renders all form fields correctly', async () => {
12+
await setup({});
13+
14+
expect(screen.getByPlaceholderText('Find Event ID')).toBeInTheDocument();
15+
expect(screen.getByText('Skip signal re-apply')).toBeInTheDocument();
16+
expect(
17+
screen.getByPlaceholderText('Enter reason for reset')
18+
).toBeInTheDocument();
19+
});
20+
21+
it('displays error when form has errors', async () => {
22+
const formErrors = {
23+
decisionFinishEventId: {
24+
message: 'Event ID is required',
25+
type: 'required',
26+
},
27+
reason: { message: 'Reason is required', type: 'required' },
28+
};
29+
30+
await setup({ formErrors });
31+
32+
const eventIdInput = screen.getByPlaceholderText('Find Event ID');
33+
expect(eventIdInput).toHaveAttribute('aria-invalid', 'true');
34+
35+
const reasonInput = screen.getByPlaceholderText('Enter reason for reset');
36+
expect(reasonInput).toHaveAttribute('aria-invalid', 'true');
37+
});
38+
39+
it('handles input changes correctly', async () => {
40+
const { user } = await setup({});
41+
42+
const eventIdInput = screen.getByPlaceholderText('Find Event ID');
43+
await user.type(eventIdInput, '123');
44+
expect(eventIdInput).toHaveValue(123);
45+
46+
const reasonInput = screen.getByPlaceholderText('Enter reason for reset');
47+
await user.type(reasonInput, 'Test reason');
48+
expect(reasonInput).toHaveValue('Test reason');
49+
50+
const skipSignalCheckbox = screen.getByRole('checkbox', {
51+
name: /skip signal re-apply/i,
52+
});
53+
await user.click(skipSignalCheckbox);
54+
expect(skipSignalCheckbox).toBeChecked();
55+
});
56+
57+
it('renders with default values', async () => {
58+
await setup({});
59+
60+
const eventIdInput = screen.getByPlaceholderText('Find Event ID');
61+
expect(eventIdInput).toHaveValue(null);
62+
63+
const reasonInput = screen.getByPlaceholderText('Enter reason for reset');
64+
expect(reasonInput).toHaveValue('');
65+
66+
const skipSignalCheckbox = screen.getByRole('checkbox', {
67+
name: /skip signal re-apply/i,
68+
});
69+
expect(skipSignalCheckbox).not.toBeChecked();
70+
});
71+
});
72+
73+
type TestProps = {
74+
formErrors: FieldErrors<ResetWorkflowFormData>;
75+
formData: ResetWorkflowFormData;
76+
};
77+
78+
function TestWrapper({ formErrors, formData }: TestProps) {
79+
const methods = useForm<ResetWorkflowFormData>({
80+
defaultValues: formData,
81+
});
82+
83+
return (
84+
<WorkflowActionResetForm
85+
control={methods.control as Control<ResetWorkflowFormData>}
86+
fieldErrors={formErrors}
87+
formData={formData}
88+
/>
89+
);
90+
}
91+
92+
async function setup({
93+
formErrors = {},
94+
formData = {
95+
decisionFinishEventId: '',
96+
reason: '',
97+
skipSignalReapply: false,
98+
},
99+
}: Partial<TestProps>) {
100+
const user = userEvent.setup();
101+
102+
render(<TestWrapper formErrors={formErrors} formData={formData} />);
103+
104+
return { user };
105+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { z } from 'zod';
2+
3+
export const resetWorkflowFormSchema = z.object({
4+
decisionFinishEventId: z.string().min(1),
5+
reason: z.string().min(1),
6+
skipSignalReapply: z.boolean().optional(),
7+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
Checkbox,
3+
STYLE_TYPE as CHECKBOX_STYLE_TYPE,
4+
LABEL_PLACEMENT as CHECKBOX_LABEL_PLACEMENT,
5+
} from 'baseui/checkbox';
6+
import { FormControl } from 'baseui/form-control';
7+
import { Input } from 'baseui/input';
8+
import { Textarea } from 'baseui/textarea';
9+
import { Controller } from 'react-hook-form';
10+
11+
import { type Props } from './workflow-action-reset-form.types';
12+
13+
export default function WorkflowActionResetForm({
14+
fieldErrors,
15+
control,
16+
}: Props) {
17+
return (
18+
<div>
19+
<FormControl label="Event ID">
20+
<Controller
21+
name="decisionFinishEventId"
22+
control={control}
23+
defaultValue=""
24+
render={({ field: { ref, ...field } }) => (
25+
<Input
26+
{...field}
27+
// @ts-expect-error - inputRef expects ref object while ref is a callback. It should support both.
28+
inputRef={ref}
29+
onChange={(e) => {
30+
field.onChange(e.target.value);
31+
}}
32+
onBlur={field.onBlur}
33+
error={Boolean(fieldErrors.decisionFinishEventId?.message)}
34+
type="number"
35+
placeholder="Find Event ID"
36+
/>
37+
)}
38+
/>
39+
</FormControl>
40+
41+
<FormControl>
42+
<Controller
43+
name="skipSignalReapply"
44+
control={control}
45+
defaultValue={false}
46+
render={({ field: { value, onChange, ref, ...field } }) => (
47+
<Checkbox
48+
{...field}
49+
// @ts-expect-error - inputRef expects ref object while ref is a callback. It should support both.
50+
inputRef={ref}
51+
checkmarkType={CHECKBOX_STYLE_TYPE.toggle_round}
52+
labelPlacement={CHECKBOX_LABEL_PLACEMENT.right}
53+
checked={value}
54+
error={Boolean(fieldErrors.skipSignalReapply?.message)}
55+
onChange={(e) => onChange(e.currentTarget.checked)}
56+
>
57+
Skip signal re-apply
58+
</Checkbox>
59+
)}
60+
/>
61+
</FormControl>
62+
63+
<FormControl label="Reason">
64+
<Controller
65+
name="reason"
66+
control={control}
67+
defaultValue=""
68+
render={({ field: { ref, ...field } }) => (
69+
<Textarea
70+
{...field}
71+
// @ts-expect-error - inputRef expects ref object while ref is a callback. It should support both.
72+
inputRef={ref}
73+
onChange={(e) => {
74+
field.onChange(e.target.value);
75+
}}
76+
onBlur={field.onBlur}
77+
error={Boolean(fieldErrors.reason?.message)}
78+
placeholder="Enter reason for reset"
79+
/>
80+
)}
81+
/>
82+
</FormControl>
83+
</div>
84+
);
85+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { type z } from 'zod';
2+
3+
import type resetWorkflowRequestBodySchema from '@/route-handlers/reset-workflow/schemas/reset-workflow-request-body-schema';
4+
5+
import { type WorkflowActionFormProps } from '../workflow-actions.types';
6+
7+
import { type resetWorkflowFormSchema } from './schemas/reset-workflow-form-schema';
8+
9+
export type Props = WorkflowActionFormProps<ResetWorkflowFormData>;
10+
11+
export type ResetWorkflowFormData = z.infer<typeof resetWorkflowFormSchema>;
12+
13+
export type ResetWorkflowSubmissionData = z.infer<
14+
typeof resetWorkflowRequestBodySchema
15+
>;

src/views/workflow-actions/workflow-actions.types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { type ReactNode } from 'react';
22

33
import { type IconProps } from 'baseui/icon';
4+
import {
5+
type Control,
6+
type FieldValues,
7+
type FieldErrors,
8+
} from 'react-hook-form';
49

510
import { type WorkflowActionID as WorkflowActionIDFromConfig } from '@/config/dynamic/resolvers/workflow-actions-enabled.types';
611
import { type DescribeWorkflowResponse } from '@/route-handlers/describe-workflow/describe-workflow.types';
@@ -23,6 +28,12 @@ export type WorkflowActionSuccessMessageProps<R> = {
2328
inputParams: WorkflowActionInputParams;
2429
};
2530

31+
export type WorkflowActionFormProps<FormData extends FieldValues> = {
32+
formData: FormData;
33+
fieldErrors: FieldErrors<FormData>;
34+
control: Control<FormData>;
35+
};
36+
2637
export type WorkflowActionNonRunnableStatus =
2738
(typeof WORKFLOW_ACTIONS_NON_RUNNABLE_STATUSES_CONFIG)[number];
2839

0 commit comments

Comments
 (0)