Skip to content

Commit 91463ba

Browse files
committed
reafactor: improve dx on case information form
1 parent dc67792 commit 91463ba

File tree

2 files changed

+98
-43
lines changed

2 files changed

+98
-43
lines changed

packages/app-builder/src/routes/ressources+/cases+/edit-status.tsx

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import {
55
caseStatusVariants,
66
useCaseStatuses,
77
} from '@app-builder/components/Cases';
8+
import { setToastMessage } from '@app-builder/components/MarbleToaster';
89
import { caseStatuses } from '@app-builder/models/cases';
910
import { serverServices } from '@app-builder/services/init.server';
1011
import { getRoute } from '@app-builder/utils/routes';
11-
import { getFormProps, getInputProps, useForm } from '@conform-to/react';
12-
import { getZodConstraint, parseWithZod } from '@conform-to/zod';
1312
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
1413
import { type ActionFunctionArgs, json } from '@remix-run/node';
1514
import { useFetcher } from '@remix-run/react';
15+
import { useForm } from '@tanstack/react-form';
1616
import { type Namespace } from 'i18next';
1717
import * as React from 'react';
1818
import { Trans, useTranslation } from 'react-i18next';
@@ -29,27 +29,66 @@ const schema = z.object({
2929
status: z.enum(caseStatuses),
3030
nextStatus: z.enum(caseStatuses),
3131
});
32+
3233
type Schema = z.infer<typeof schema>;
3334

3435
export async function action({ request }: ActionFunctionArgs) {
35-
const { authService } = serverServices;
36-
const { cases } = await authService.isAuthenticated(request, {
37-
failureRedirect: getRoute('/sign-in'),
38-
});
36+
const {
37+
authService,
38+
i18nextService: { getFixedT },
39+
toastSessionService: { getSession, commitSession },
40+
} = serverServices;
3941

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

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

47-
await cases.updateCase({
48-
caseId: submission.value.caseId,
49-
body: { status: submission.value.nextStatus },
50-
});
62+
try {
63+
await cases.updateCase({
64+
caseId: data.caseId,
65+
body: { status: data.nextStatus },
66+
});
5167

52-
return json(submission.reply());
68+
setToastMessage(session, {
69+
type: 'success',
70+
message: t('common:success.save'),
71+
});
72+
73+
return json(
74+
{ status: 'success', errors: [] },
75+
{
76+
headers: { 'Set-Cookie': await commitSession(session) },
77+
},
78+
);
79+
} catch (error) {
80+
setToastMessage(session, {
81+
type: 'error',
82+
message: t('common:errors.unknown'),
83+
});
84+
85+
return json(
86+
{ status: 'error', errors: [] },
87+
{
88+
headers: { 'Set-Cookie': await commitSession(session) },
89+
},
90+
);
91+
}
5392
}
5493

5594
export function EditCaseStatus({ status, caseId }: Pick<Schema, 'caseId' | 'status'>) {
@@ -149,37 +188,40 @@ function ModalContent({
149188
const { t } = useTranslation(handle.i18n);
150189
const fetcher = useFetcher<typeof action>();
151190

152-
const [form, fields] = useForm({
153-
defaultValue: { caseId, status, nextStatus },
154-
lastResult: fetcher.data,
155-
constraint: getZodConstraint(schema),
156-
onValidate({ formData }) {
157-
return parseWithZod(formData, {
158-
schema,
159-
});
191+
const form = useForm({
192+
defaultValues: { caseId, status, nextStatus },
193+
onSubmit: ({ value, formApi }) => {
194+
if (formApi.state.isValid) {
195+
fetcher.submit(value, {
196+
method: 'PATCH',
197+
action: getRoute('/ressources/cases/edit-status'),
198+
encType: 'application/json',
199+
});
200+
}
201+
},
202+
validators: {
203+
onChangeAsync: schema,
204+
onBlurAsync: schema,
205+
onSubmitAsync: schema,
160206
},
161207
});
162208

163209
React.useEffect(() => {
164210
if (fetcher.data?.status === 'success') {
165211
onSubmitSuccess();
166212
}
167-
}, [fetcher.data?.intent, fetcher.data?.status, onSubmitSuccess]);
213+
}, [fetcher.data?.status, onSubmitSuccess]);
168214

169215
return (
170-
<fetcher.Form
171-
method="post"
172-
action={getRoute('/ressources/cases/edit-status')}
173-
{...getFormProps(form)}
216+
<form
217+
onSubmit={(e) => {
218+
e.preventDefault();
219+
e.stopPropagation();
220+
form.handleSubmit();
221+
}}
174222
>
175223
<Modal.Title>{t('cases:change_status_modal.title')}</Modal.Title>
176224
<div className="flex flex-col gap-6 p-6">
177-
<input {...getInputProps(fields.caseId, { type: 'hidden' })} key={fields.caseId.key} />
178-
<input {...getInputProps(fields.status, { type: 'hidden' })} key={fields.status.key} />
179-
<input
180-
{...getInputProps(fields.nextStatus, { type: 'hidden' })}
181-
key={fields.nextStatus.key}
182-
/>
183225
<div className="text-grey-00 text-s flex flex-row items-center justify-center gap-6 font-medium capitalize">
184226
<div className="flex w-full flex-1 flex-row items-center justify-end gap-2">
185227
<Trans
@@ -202,7 +244,7 @@ function ModalContent({
202244
</div>
203245
<div className="flex w-full flex-row gap-2">
204246
<Modal.Close asChild>
205-
<Button variant="secondary" className="flex-1 first-letter:capitalize">
247+
<Button variant="secondary" type="button" className="flex-1 first-letter:capitalize">
206248
{t('common:close')}
207249
</Button>
208250
</Modal.Close>
@@ -212,6 +254,6 @@ function ModalContent({
212254
</Button>
213255
</div>
214256
</div>
215-
</fetcher.Form>
257+
</form>
216258
);
217259
}

packages/app-builder/src/routes/ressources+/cases+/edit.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const editInboxSchema = z.object({
2222
tags: z.array(z.string()),
2323
inboxId: z.string().min(1),
2424
name: z.string().min(1),
25+
tagsHasChanged: z.boolean(),
2526
});
2627

2728
type EditInboxForm = z.infer<typeof editInboxSchema>;
@@ -54,16 +55,23 @@ export async function action({ request }: ActionFunctionArgs) {
5455
}
5556

5657
try {
57-
await Promise.all([
58+
const promises = [
5859
cases.updateCase({
5960
caseId: data.id,
6061
body: pick(data, ['inboxId', 'name']),
6162
}),
62-
cases.setTags({
63-
caseId: data.id,
64-
tagIds: data.tags,
65-
}),
66-
]);
63+
];
64+
65+
if (data.tagsHasChanged) {
66+
promises.push(
67+
cases.setTags({
68+
caseId: data.id,
69+
tagIds: data.tags,
70+
}),
71+
);
72+
}
73+
74+
await Promise.all(promises);
6775

6876
setToastMessage(session, {
6977
type: 'success',
@@ -101,6 +109,7 @@ export function EditCase({ detail, inboxes }: { detail: CaseDetail; inboxes: Inb
101109
defaultValues: {
102110
...pick(detail, ['id', 'name', 'inboxId']),
103111
tags: detail.tags.map(({ tagId }) => tagId),
112+
tagsHasChanged: false,
104113
},
105114
onSubmit: ({ value, formApi }) => {
106115
if (formApi.state.isValid) {
@@ -109,6 +118,7 @@ export function EditCase({ detail, inboxes }: { detail: CaseDetail; inboxes: Inb
109118
action: getRoute('/ressources/cases/edit'),
110119
encType: 'application/json',
111120
});
121+
formApi.setFieldValue('tagsHasChanged', false);
112122
}
113123
},
114124
validators: {
@@ -185,7 +195,10 @@ export function EditCase({ detail, inboxes }: { detail: CaseDetail; inboxes: Inb
185195
name={field.name}
186196
orgTags={orgTags}
187197
selectedTagIds={field.state.value}
188-
onChange={field.handleChange}
198+
onChange={(newTags) => {
199+
field.handleChange(newTags);
200+
form.setFieldValue('tagsHasChanged', true);
201+
}}
189202
/>
190203
</div>
191204
)}

0 commit comments

Comments
 (0)