Skip to content

Commit f0a4576

Browse files
committed
refactor: update Decision Review form
1 parent 4c355bf commit f0a4576

File tree

1 file changed

+156
-124
lines changed

1 file changed

+156
-124
lines changed
Lines changed: 156 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import { Callout } from '@app-builder/components';
22
import { ReviewStatusTag } from '@app-builder/components/Decisions/ReviewStatusTag';
33
import { ExternalLink } from '@app-builder/components/ExternalLink';
4-
import { FormErrorOrDescription } from '@app-builder/components/Form/FormErrorOrDescription';
5-
import { FormField } from '@app-builder/components/Form/FormField';
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';
4+
import { FormErrorOrDescription } from '@app-builder/components/Form/Tanstack/FormErrorOrDescription';
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';
118
import { nonPendingReviewStatuses } from '@app-builder/models/decision';
@@ -14,13 +11,12 @@ import { blockingReviewDocHref } from '@app-builder/services/documentation-href'
1411
import { serverServices } from '@app-builder/services/init.server';
1512
import { getRoute } from '@app-builder/utils/routes';
1613
import type * as Ariakit from '@ariakit/react';
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 } from 'react';
2218
import { Trans, useTranslation } from 'react-i18next';
23-
import { Button, ModalV2 } from 'ui-design-system';
19+
import { Button, ModalV2, Select, TextArea } from 'ui-design-system';
2420
import { z } from 'zod';
2521

2622
const reviewDecisionSchema = z.object({
@@ -35,37 +31,53 @@ export async function action({ request }: ActionFunctionArgs) {
3531
i18nextService: { getFixedT },
3632
toastSessionService: { getSession, commitSession },
3733
} = serverServices;
38-
const { cases } = await authService.isAuthenticated(request, {
39-
failureRedirect: getRoute('/sign-in'),
40-
});
41-
42-
const formData = await request.formData();
43-
const submission = parseWithZod(formData, {
44-
schema: reviewDecisionSchema,
45-
});
4634

47-
if (submission.status !== 'success') {
48-
return json(submission.reply());
35+
const [t, session, rawData, { cases }] = await Promise.all([
36+
getFixedT(request, ['common']),
37+
getSession(request),
38+
request.json(),
39+
authService.isAuthenticated(request, {
40+
failureRedirect: getRoute('/sign-in'),
41+
}),
42+
]);
43+
44+
const { data, success, error } = reviewDecisionSchema.safeParse(rawData);
45+
46+
if (!success) {
47+
return json(
48+
{ status: 'error', errors: error.flatten() },
49+
{
50+
headers: { 'Set-Cookie': await commitSession(session) },
51+
},
52+
);
4953
}
5054

5155
try {
52-
await cases.reviewDecision(submission.value);
53-
54-
return json(submission.reply());
55-
} catch (error) {
56-
const session = await getSession(request);
57-
const t = await getFixedT(request, ['common', 'cases']);
56+
await cases.reviewDecision(data);
5857

59-
const message = t('common:errors.unknown');
58+
setToastMessage(session, {
59+
type: 'success',
60+
message: t('common:success.save'),
61+
});
6062

63+
return json(
64+
{ status: 'success', errors: [] },
65+
{
66+
headers: { 'Set-Cookie': await commitSession(session) },
67+
},
68+
);
69+
} catch (error) {
6170
setToastMessage(session, {
6271
type: 'error',
63-
message,
72+
message: t('common:errors.unknown'),
6473
});
6574

66-
return json(submission.reply({ formErrors: [message] }), {
67-
headers: { 'Set-Cookie': await commitSession(session) },
68-
});
75+
return json(
76+
{ status: 'error', errors: [] },
77+
{
78+
headers: { 'Set-Cookie': await commitSession(session) },
79+
},
80+
);
6981
}
7082
}
7183

@@ -99,113 +111,133 @@ function ReviewDecisionContent({
99111
setOpen: (open: boolean) => void;
100112
}) {
101113
const { t } = useTranslation(['common', 'cases']);
102-
103114
const fetcher = useFetcher<typeof action>();
104-
React.useEffect(() => {
115+
116+
useEffect(() => {
105117
if (fetcher?.data?.status === 'success') {
106118
setOpen(false);
107119
}
108120
}, [setOpen, fetcher?.data?.status]);
109121

110-
const [form, fields] = useForm({
111-
shouldRevalidate: 'onInput',
112-
defaultValue: {
122+
const form = useForm({
123+
defaultValues: {
113124
decisionId,
125+
reviewComment: '',
126+
reviewStatus: 'decline',
114127
},
115-
lastResult: fetcher.data,
116-
constraint: getZodConstraint(reviewDecisionSchema),
117-
onValidate({ formData }) {
118-
return parseWithZod(formData, {
119-
schema: reviewDecisionSchema,
120-
});
128+
onSubmit: ({ value, formApi }) => {
129+
if (formApi.state.isValid) {
130+
fetcher.submit(value, {
131+
method: 'POST',
132+
action: getRoute('/ressources/cases/review-decision'),
133+
encType: 'application/json',
134+
});
135+
}
136+
},
137+
validators: {
138+
onChangeAsync: reviewDecisionSchema,
139+
onBlurAsync: reviewDecisionSchema,
140+
onSubmitAsync: reviewDecisionSchema,
121141
},
122142
});
123143

124144
return (
125-
<FormProvider context={form.context}>
126-
<fetcher.Form
127-
method="post"
128-
action={getRoute('/ressources/cases/review-decision')}
129-
{...getFormProps(form)}
130-
>
131-
<ModalV2.Title>{t('cases:case_detail.review_decision.title')}</ModalV2.Title>
132-
<div className="flex flex-col gap-6 p-6">
133-
<ModalV2.Description render={<Callout variant="outlined" />}>
134-
<p className="whitespace-pre text-wrap">
135-
<Trans
136-
t={t}
137-
i18nKey="cases:case_detail.review_decision.callout"
138-
components={{
139-
DocLink: <ExternalLink href={blockingReviewDocHref} />,
140-
}}
141-
/>
142-
</p>
143-
</ModalV2.Description>
144-
145-
<input
146-
{...getInputProps(fields.decisionId, {
147-
type: 'hidden',
148-
})}
149-
/>
150-
151-
<FormField name={fields.reviewStatus.name} className="flex flex-col gap-2">
152-
<FormLabel>{t('cases:case_detail.review_decision.review_status.label')}</FormLabel>
153-
<FormSelect.Default
154-
className="h-10 w-full"
155-
options={nonPendingReviewStatuses}
156-
placeholder={t('cases:case_detail.review_decision.review_status.placeholder')}
157-
contentClassName="max-w-[var(--radix-select-trigger-width)]"
158-
>
159-
{nonPendingReviewStatuses.map((reviewStatus) => {
160-
const disabled = sanctionCheck && sanctionCheck.status !== 'no_hit';
161-
162-
return disabled && reviewStatus === 'approve' ? (
163-
<div className="flex flex-col items-start gap-2 p-1">
164-
<ReviewStatusTag
165-
key={reviewStatus}
166-
disabled
167-
border="square"
168-
size="big"
169-
reviewStatus={reviewStatus}
170-
/>
171-
<span className="text-grey-50 text-xs">
172-
{t('cases:case_detail.review_decision.disabled_approve')}
173-
</span>
174-
</div>
175-
) : (
176-
<FormSelect.DefaultItem key={reviewStatus} value={reviewStatus}>
177-
<ReviewStatusTag border="square" size="big" reviewStatus={reviewStatus} />
178-
</FormSelect.DefaultItem>
179-
);
180-
})}
181-
</FormSelect.Default>
182-
<FormErrorOrDescription />
183-
</FormField>
184-
185-
<FormField name={fields.reviewComment.name} className="flex flex-col gap-2">
186-
<FormLabel>{t('cases:case_detail.review_decision.comment.label')}</FormLabel>
187-
<FormTextArea
188-
className="w-full"
189-
placeholder={t('cases:case_detail.review_decision.comment.placeholder')}
145+
<form
146+
onSubmit={(e) => {
147+
e.preventDefault();
148+
e.stopPropagation();
149+
form.handleSubmit();
150+
}}
151+
>
152+
<ModalV2.Title>{t('cases:case_detail.review_decision.title')}</ModalV2.Title>
153+
<div className="flex flex-col gap-6 p-6">
154+
<ModalV2.Description render={<Callout variant="outlined" />}>
155+
<p className="whitespace-pre text-wrap">
156+
<Trans
157+
t={t}
158+
i18nKey="cases:case_detail.review_decision.callout"
159+
components={{
160+
DocLink: <ExternalLink href={blockingReviewDocHref} />,
161+
}}
190162
/>
191-
<FormErrorOrDescription />
192-
</FormField>
193-
194-
<div className="flex flex-1 flex-row gap-2">
195-
<ModalV2.Close render={<Button className="flex-1" variant="secondary" />}>
196-
{t('common:cancel')}
197-
</ModalV2.Close>
198-
<Button className="flex-1" variant="primary" type="submit">
199-
<LoadingIcon
200-
icon="case-manager"
201-
className="size-5"
202-
loading={fetcher.state === 'submitting'}
163+
</p>
164+
</ModalV2.Description>
165+
166+
<form.Field name="reviewStatus">
167+
{(field) => (
168+
<div className="flex flex-col gap-2">
169+
<FormLabel name={field.name}>
170+
{t('cases:case_detail.review_decision.review_status.label')}
171+
</FormLabel>
172+
<Select.Default
173+
className="h-10 w-full"
174+
defaultValue={field.state.value}
175+
onValueChange={field.handleChange}
176+
placeholder={t('cases:case_detail.review_decision.review_status.placeholder')}
177+
//contentClassName="max-w-[var(--radix-select-trigger-width)]"
178+
>
179+
{nonPendingReviewStatuses.map((reviewStatus) => {
180+
const disabled = sanctionCheck && sanctionCheck.status !== 'no_hit';
181+
182+
return disabled && reviewStatus === 'approve' ? (
183+
<div className="flex flex-col items-start gap-2 p-1">
184+
<ReviewStatusTag
185+
key={reviewStatus}
186+
disabled
187+
border="square"
188+
size="big"
189+
reviewStatus={reviewStatus}
190+
/>
191+
<span className="text-grey-50 text-xs">
192+
{t('cases:case_detail.review_decision.disabled_approve')}
193+
</span>
194+
</div>
195+
) : (
196+
<Select.DefaultItem key={reviewStatus} value={reviewStatus}>
197+
<ReviewStatusTag border="square" size="big" reviewStatus={reviewStatus} />
198+
</Select.DefaultItem>
199+
);
200+
})}
201+
</Select.Default>
202+
<FormErrorOrDescription errors={field.state.meta.errors} />
203+
</div>
204+
)}
205+
</form.Field>
206+
207+
<form.Field name="reviewComment">
208+
{(field) => (
209+
<div className="flex flex-col gap-2">
210+
<FormLabel name={field.name}>
211+
{t('cases:case_detail.review_decision.comment.label')}
212+
</FormLabel>
213+
<TextArea
214+
className="w-full"
215+
name={field.name}
216+
defaultValue={field.state.value}
217+
onChange={(e) => field.handleChange(e.currentTarget.value)}
218+
onBlur={field.handleBlur}
219+
borderColor={field.state.meta.errors.length === 0 ? 'greyfigma-90' : 'redfigma-47'}
220+
placeholder={t('cases:case_detail.review_decision.comment.placeholder')}
203221
/>
204-
{t('cases:case_detail.review_decision')}
205-
</Button>
206-
</div>
207-
</div>
208-
</fetcher.Form>
209-
</FormProvider>
222+
<FormErrorOrDescription />
223+
</div>
224+
)}
225+
</form.Field>
226+
</div>
227+
228+
<div className="flex flex-1 flex-row gap-2">
229+
<ModalV2.Close render={<Button className="flex-1" variant="secondary" />}>
230+
{t('common:cancel')}
231+
</ModalV2.Close>
232+
<Button className="flex-1" variant="primary" type="submit">
233+
<LoadingIcon
234+
icon="case-manager"
235+
className="size-5"
236+
loading={fetcher.state === 'submitting'}
237+
/>
238+
{t('cases:case_detail.review_decision')}
239+
</Button>
240+
</div>
241+
</form>
210242
);
211243
}

0 commit comments

Comments
 (0)