Skip to content

Commit 4c355bf

Browse files
committed
refactor: update Rule Snooze Add form
1 parent 3f06097 commit 4c355bf

File tree

1 file changed

+168
-135
lines changed

1 file changed

+168
-135
lines changed
Lines changed: 168 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,23 @@
11
import { Callout } from '@app-builder/components';
22
import { ExternalLink } from '@app-builder/components/ExternalLink';
3-
import { FormErrorOrDescription } from '@app-builder/components/Form/FormErrorOrDescription';
4-
import { FormField } from '@app-builder/components/Form/FormField';
5-
import { FormInput } from '@app-builder/components/Form/FormInput';
6-
import { FormLabel } from '@app-builder/components/Form/FormLabel';
7-
import { FormSelect } from '@app-builder/components/Form/FormSelect';
8-
import { FormTextArea } from '@app-builder/components/Form/FormTextArea';
3+
import { FormErrorOrDescription } from '@app-builder/components/Form/Tanstack/FormErrorOrDescription';
4+
import { FormInput } from '@app-builder/components/Form/Tanstack/FormInput';
5+
import { FormLabel } from '@app-builder/components/Form/Tanstack/FormLabel';
96
import { setToastMessage } from '@app-builder/components/MarbleToaster';
107
import { LoadingIcon } from '@app-builder/components/Spinner';
11-
import { adaptDateTimeFieldCodes } from '@app-builder/models/duration';
8+
import { adaptDateTimeFieldCodes, type DurationUnit } from '@app-builder/models/duration';
129
import { isStatusConflictHttpError } from '@app-builder/models/http-errors';
1310
import { ruleSnoozesDocHref } from '@app-builder/services/documentation-href';
1411
import { serverServices } from '@app-builder/services/init.server';
1512
import { useFormatLanguage } from '@app-builder/utils/format';
1613
import { getRoute } from '@app-builder/utils/routes';
17-
import { FormProvider, getFormProps, getInputProps, useForm } from '@conform-to/react';
18-
import { getZodConstraint, parseWithZod } from '@conform-to/zod';
1914
import { type ActionFunctionArgs, json } from '@remix-run/node';
2015
import { useFetcher } from '@remix-run/react';
21-
import * as React from 'react';
16+
import { useForm } from '@tanstack/react-form';
17+
import { useEffect, useMemo, useState } from 'react';
2218
import { Trans, useTranslation } from 'react-i18next';
2319
import { Temporal } from 'temporal-polyfill';
24-
import { Button, ModalV2 } from 'ui-design-system';
20+
import { Button, ModalV2, Select, TextArea } from 'ui-design-system';
2521
import { z } from 'zod';
2622

2723
const durationUnitOptions = ['days', 'weeks', 'hours'] as const;
@@ -34,26 +30,37 @@ const addRuleSnoozeFormSchema = z.object({
3430
durationUnit: z.enum(durationUnitOptions),
3531
});
3632

33+
type AddRuleSnoozeForm = z.infer<typeof addRuleSnoozeFormSchema>;
34+
3735
export async function action({ request }: ActionFunctionArgs) {
3836
const {
3937
authService,
4038
i18nextService: { getFixedT },
4139
toastSessionService: { getSession, commitSession },
4240
} = serverServices;
43-
const { decision } = await authService.isAuthenticated(request, {
44-
failureRedirect: getRoute('/sign-in'),
45-
});
4641

47-
const formData = await request.formData();
48-
const submission = parseWithZod(formData, {
49-
schema: addRuleSnoozeFormSchema,
50-
});
42+
const [t, session, rawData, { decision }] = await Promise.all([
43+
getFixedT(request, ['common', 'cases']),
44+
getSession(request),
45+
request.json(),
46+
authService.isAuthenticated(request, {
47+
failureRedirect: getRoute('/sign-in'),
48+
}),
49+
]);
5150

52-
if (submission.status !== 'success') {
53-
return json(submission.reply());
51+
const { data, success, error } = addRuleSnoozeFormSchema.safeParse(rawData);
52+
53+
if (!success) {
54+
return json(
55+
{ status: 'error', errors: error.flatten() },
56+
{
57+
headers: { 'Set-Cookie': await commitSession(session) },
58+
},
59+
);
5460
}
5561

56-
const { decisionId, ruleId, comment, durationUnit, durationValue } = submission.value;
62+
const { decisionId, ruleId, comment, durationUnit, durationValue } = data;
63+
5764
const duration = Temporal.Duration.from({
5865
[durationUnit]: durationValue,
5966
});
@@ -63,13 +70,18 @@ export async function action({ request }: ActionFunctionArgs) {
6370
relativeTo: Temporal.Now.plainDateTime('gregory'),
6471
}) >= 0
6572
) {
66-
const t = await getFixedT(request, ['cases']);
6773
return json(
68-
submission.reply({
69-
fieldErrors: {
70-
durationValue: [t('cases:case_detail.add_rule_snooze.errors.max_duration')],
71-
},
72-
}),
74+
{
75+
status: 'error',
76+
errors: [
77+
{
78+
durationValue: [t('cases:case_detail.add_rule_snooze.errors.max_duration')],
79+
},
80+
],
81+
},
82+
{
83+
headers: { 'Set-Cookie': await commitSession(session) },
84+
},
7385
);
7486
}
7587

@@ -80,26 +92,21 @@ export async function action({ request }: ActionFunctionArgs) {
8092
comment,
8193
});
8294

83-
return json(submission.reply());
95+
return { status: 'success', errors: [] };
8496
} catch (error) {
85-
const session = await getSession(request);
86-
const t = await getFixedT(request, ['common', 'cases']);
87-
88-
let message: string;
89-
if (isStatusConflictHttpError(error)) {
90-
message = t('cases:case_detail.add_rule_snooze.errors.duplicate_rule_snooze');
91-
} else {
92-
message = t('common:errors.unknown');
93-
}
94-
9597
setToastMessage(session, {
9698
type: 'error',
97-
message,
99+
message: isStatusConflictHttpError(error)
100+
? t('cases:case_detail.add_rule_snooze.errors.duplicate_rule_snooze')
101+
: t('common:errors.unknown'),
98102
});
99103

100-
return json(submission.reply({ formErrors: [message] }), {
101-
headers: { 'Set-Cookie': await commitSession(session) },
102-
});
104+
return json(
105+
{ status: 'error', errors: [] },
106+
{
107+
headers: { 'Set-Cookie': await commitSession(session) },
108+
},
109+
);
103110
}
104111
}
105112

@@ -112,7 +119,7 @@ export function AddRuleSnooze({
112119
decisionId: string;
113120
ruleId: string;
114121
}) {
115-
const [open, setOpen] = React.useState(false);
122+
const [open, setOpen] = useState(false);
116123

117124
return (
118125
<ModalV2.Root open={open} setOpen={setOpen}>
@@ -135,121 +142,147 @@ function AddRuleSnoozeContent({
135142
}) {
136143
const { t } = useTranslation(['common', 'cases']);
137144
const language = useFormatLanguage();
138-
const dateTimeFieldNames = React.useMemo(
145+
const fetcher = useFetcher<typeof action>();
146+
const dateTimeFieldNames = useMemo(
139147
() =>
140148
new Intl.DisplayNames(language, {
141149
type: 'dateTimeField',
142150
}),
143151
[language],
144152
);
145153

146-
const fetcher = useFetcher<typeof action>();
147-
React.useEffect(() => {
154+
useEffect(() => {
148155
if (fetcher?.data?.status === 'success') {
149156
setOpen(false);
150157
}
151158
}, [setOpen, fetcher?.data?.status]);
152159

153-
const [form, fields] = useForm({
154-
shouldRevalidate: 'onInput',
155-
defaultValue: {
160+
const form = useForm<AddRuleSnoozeForm>({
161+
defaultValues: {
156162
decisionId,
157163
ruleId,
158164
durationValue: 1,
159165
durationUnit: 'days',
160166
},
161-
lastResult: fetcher.data,
162-
constraint: getZodConstraint(addRuleSnoozeFormSchema),
163-
onValidate({ formData }) {
164-
return parseWithZod(formData, {
165-
schema: addRuleSnoozeFormSchema,
166-
});
167+
onSubmit: ({ value, formApi }) => {
168+
if (formApi.state.isValid) {
169+
fetcher.submit(value, {
170+
method: 'POST',
171+
action: getRoute('/ressources/cases/add-rule-snooze'),
172+
encType: 'application/json',
173+
});
174+
}
175+
},
176+
validators: {
177+
onChangeAsync: addRuleSnoozeFormSchema,
178+
onBlurAsync: addRuleSnoozeFormSchema,
179+
onSubmitAsync: addRuleSnoozeFormSchema,
167180
},
168181
});
169182

170183
return (
171-
<FormProvider context={form.context}>
172-
<fetcher.Form
173-
method="post"
174-
action={getRoute('/ressources/cases/add-rule-snooze')}
175-
{...getFormProps(form)}
176-
>
177-
<ModalV2.Title>{t('cases:case_detail.add_rule_snooze.title')}</ModalV2.Title>
178-
<div className="flex flex-col gap-6 p-6">
179-
<ModalV2.Description render={<Callout variant="outlined" />}>
180-
<p className="whitespace-pre text-wrap">
181-
<Trans
182-
t={t}
183-
i18nKey="cases:case_detail.add_rule_snooze.callout"
184-
components={{
185-
DocLink: <ExternalLink href={ruleSnoozesDocHref} />,
186-
}}
187-
/>
188-
</p>
189-
</ModalV2.Description>
190-
<input
191-
{...getInputProps(fields.decisionId, {
192-
type: 'hidden',
193-
})}
194-
/>
195-
<input
196-
{...getInputProps(fields.ruleId, {
197-
type: 'hidden',
198-
})}
199-
/>
200-
201-
<FormField
202-
name={fields.comment.name}
203-
className="row-span-full grid grid-rows-subgrid gap-2"
204-
>
205-
<FormLabel>{t('cases:case_detail.add_rule_snooze.comment.label')}</FormLabel>
206-
<FormTextArea
207-
className="w-full"
208-
placeholder={t('cases:case_detail.add_rule_snooze.comment.placeholder')}
184+
<form
185+
onSubmit={(e) => {
186+
e.preventDefault();
187+
e.stopPropagation();
188+
form.handleSubmit();
189+
}}
190+
>
191+
<ModalV2.Title>{t('cases:case_detail.add_rule_snooze.title')}</ModalV2.Title>
192+
<div className="flex flex-col gap-6 p-6">
193+
<ModalV2.Description render={<Callout variant="outlined" />}>
194+
<p className="whitespace-pre text-wrap">
195+
<Trans
196+
t={t}
197+
i18nKey="cases:case_detail.add_rule_snooze.callout"
198+
components={{
199+
DocLink: <ExternalLink href={ruleSnoozesDocHref} />,
200+
}}
209201
/>
210-
</FormField>
211-
212-
<div className="grid w-full grid-cols-2 grid-rows-[repeat(3,_max-content)] gap-2">
213-
<FormField
214-
name={fields.durationValue.name}
215-
className="row-span-full grid grid-rows-subgrid gap-2"
216-
>
217-
<FormLabel>{t('cases:case_detail.add_rule_snooze.duration_value')}</FormLabel>
218-
<FormInput type="number" className="w-full" />
219-
<FormErrorOrDescription />
220-
</FormField>
221-
222-
<FormField
223-
name={fields.durationUnit.name}
224-
className="row-span-full grid grid-rows-subgrid gap-2"
225-
>
226-
<FormLabel>{t('cases:case_detail.add_rule_snooze.duration_unit')}</FormLabel>
227-
<FormSelect.Default className="h-10 w-full" options={durationUnitOptions}>
228-
{durationUnitOptions.map((unit) => (
229-
<FormSelect.DefaultItem key={unit} value={unit}>
230-
{dateTimeFieldNames.of(adaptDateTimeFieldCodes(unit))}
231-
</FormSelect.DefaultItem>
232-
))}
233-
</FormSelect.Default>
234-
<FormErrorOrDescription />
235-
</FormField>
236-
</div>
237-
238-
<div className="flex flex-1 flex-row gap-2">
239-
<ModalV2.Close render={<Button className="flex-1" variant="secondary" />}>
240-
{t('common:cancel')}
241-
</ModalV2.Close>
242-
<Button className="flex-1" variant="primary" type="submit" name="update">
243-
<LoadingIcon
244-
icon="snooze"
245-
className="size-5"
246-
loading={fetcher.state === 'submitting'}
202+
</p>
203+
</ModalV2.Description>
204+
205+
<form.Field name="comment">
206+
{(field) => (
207+
<div className="row-span-full grid grid-rows-subgrid gap-2">
208+
<FormLabel name={field.name}>
209+
{t('cases:case_detail.add_rule_snooze.comment.label')}
210+
</FormLabel>
211+
<TextArea
212+
className="w-full"
213+
defaultValue={field.state.value}
214+
onChange={(e) => field.handleChange(e.currentTarget.value)}
215+
name={field.name}
216+
onBlur={field.handleBlur}
217+
borderColor={field.state.meta.errors.length === 0 ? 'greyfigma-90' : 'redfigma-47'}
218+
placeholder={t('cases:case_detail.add_rule_snooze.comment.placeholder')}
247219
/>
248-
{t('cases:case_detail.add_rule_snooze.snooze_this_value')}
249-
</Button>
250-
</div>
220+
</div>
221+
)}
222+
</form.Field>
223+
224+
<div className="grid w-full grid-cols-2 grid-rows-[repeat(3,_max-content)] gap-2">
225+
<form.Field name="durationValue">
226+
{(field) => (
227+
<div className="row-span-full grid grid-rows-subgrid gap-2">
228+
<FormLabel name={field.name}>
229+
{t('cases:case_detail.add_rule_snooze.duration_value')}
230+
</FormLabel>
231+
<FormInput
232+
type="number"
233+
name={field.name}
234+
value={field.state.value}
235+
onChange={(e) => field.handleChange(+e.currentTarget.value)}
236+
onBlur={field.handleBlur}
237+
valid={field.state.meta.errors.length === 0}
238+
className="w-full"
239+
/>
240+
<FormErrorOrDescription errors={field.state.meta.errors} />
241+
</div>
242+
)}
243+
</form.Field>
244+
245+
<form.Field name="durationUnit">
246+
{(field) => (
247+
<div className="row-span-full grid grid-rows-subgrid gap-2">
248+
<FormLabel name={field.name}>
249+
{t('cases:case_detail.add_rule_snooze.duration_unit')}
250+
</FormLabel>
251+
<Select.Default
252+
className="h-10 w-full"
253+
defaultValue={field.state.value}
254+
onValueChange={(unit) =>
255+
field.handleChange(
256+
unit as Exclude<DurationUnit, 'seconds' | 'years' | 'minutes' | 'months'>,
257+
)
258+
}
259+
>
260+
{durationUnitOptions.map((unit) => (
261+
<Select.DefaultItem key={unit} value={unit}>
262+
{dateTimeFieldNames.of(adaptDateTimeFieldCodes(unit))}
263+
</Select.DefaultItem>
264+
))}
265+
</Select.Default>
266+
<FormErrorOrDescription />
267+
</div>
268+
)}
269+
</form.Field>
270+
</div>
271+
272+
<div className="flex flex-1 flex-row gap-2">
273+
<ModalV2.Close render={<Button className="flex-1" variant="secondary" />}>
274+
{t('common:cancel')}
275+
</ModalV2.Close>
276+
<Button className="flex-1" variant="primary" type="submit" name="update">
277+
<LoadingIcon
278+
icon="snooze"
279+
className="size-5"
280+
loading={fetcher.state === 'submitting'}
281+
/>
282+
{t('cases:case_detail.add_rule_snooze.snooze_this_value')}
283+
</Button>
251284
</div>
252-
</fetcher.Form>
253-
</FormProvider>
285+
</div>
286+
</form>
254287
);
255288
}

0 commit comments

Comments
 (0)