Skip to content

Commit 84b3de1

Browse files
authored
Custom default handling of MissingRequiredFields error (#1655)
1 parent 8ce298b commit 84b3de1

File tree

2 files changed

+66
-1
lines changed

2 files changed

+66
-1
lines changed

src/api/errorHandling/error.types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ApolloError } from '@apollo/client';
22
import { assert } from 'ts-essentials';
3+
import { GqlTypeMapMain } from '../schema';
34
import { ProductStep, Project } from '../schema.graphql';
45

56
interface CordErrorExtensions {
@@ -25,6 +26,7 @@ export interface ErrorMap {
2526
Input: InputError;
2627
Duplicate: DuplicateError;
2728
Unauthorized: InputError;
29+
MissingRequiredFields: MissingRequiredFieldsError;
2830
StepNotPlanned: StepNotPlannedError;
2931
EngagementDateOverrideConflict: EngagementDateOverrideConflictError;
3032

@@ -85,6 +87,17 @@ export interface InputError extends ErrorInfo {
8587

8688
export type DuplicateError = Required<InputError>;
8789

90+
export interface MissingRequiredFieldsError extends InputError {
91+
readonly resource: { name: keyof GqlTypeMapMain };
92+
readonly object: { id: string };
93+
readonly missing: ReadonlyArray<
94+
Readonly<{
95+
field: string;
96+
description: string;
97+
}>
98+
>;
99+
}
100+
88101
export interface StepNotPlannedError extends InputError {
89102
field: string;
90103
productId: string;

src/api/errorHandling/form-error-handling.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isNotFalsy, mapValues } from '@seedcompany/common';
1+
import { groupBy, isNotFalsy, mapValues } from '@seedcompany/common';
22
import { FORM_ERROR, FormApi, setIn, SubmissionErrors } from 'final-form';
33
import { isValidElement, ReactElement } from 'react';
44
import { Promisable } from 'type-fest';
@@ -63,6 +63,8 @@ export type ErrorHandlerResult =
6363
interface HandlerUtils {
6464
/** Does the form have this field registered? */
6565
hasField: (field: string) => boolean;
66+
/** Finds a registered field matching the given name */
67+
findField: (field: string | RegExp) => string | undefined;
6668
}
6769

6870
const expandDotNotation = (input: Record<string, any>) =>
@@ -87,6 +89,50 @@ export const defaultHandlers = {
8789
e.field && hasField(e.field) ? setIn({}, e.field, e.message) : next(e),
8890
Duplicate: (e, next, { hasField }) =>
8991
hasField(e.field) ? setIn({}, e.field, 'Already in use') : next(e),
92+
MissingRequiredFields: (e, next, { findField }) => {
93+
const { in: inForm, out: outOfForm } = e.missing.reduce(
94+
(res, missing) => {
95+
const formFieldName =
96+
findField(missing.field) ??
97+
// Also find with a path prefix: mouStart -> project.mouStart
98+
findField(RegExp(`\\.${missing.field}$`)) ??
99+
// Also find with Id suffix: primaryLocation -> primaryLocationId
100+
findField(missing.field + 'Id') ??
101+
findField(RegExp(`\\.${missing.field}Id$`));
102+
if (formFieldName) {
103+
res.in.push({
104+
...missing,
105+
field: formFieldName,
106+
});
107+
} else {
108+
res.out.push(missing);
109+
}
110+
return res;
111+
},
112+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
113+
{ in: [], out: [] } as Record<
114+
'in' | 'out',
115+
Array<(typeof e.missing)[number]>
116+
>
117+
);
118+
return {
119+
...inForm.reduce(
120+
(res, missing) =>
121+
setIn(res, missing.field, 'Required when ' + missing.description),
122+
{}
123+
),
124+
...(outOfForm.length > 0 && {
125+
[FORM_ERROR]: groupBy(outOfForm, (x) => x.description)
126+
.map((fields) =>
127+
[
128+
`The following fields are required when ${fields[0].description}:`,
129+
...fields.map((x) => ` - ${x.field}`),
130+
].join('\n')
131+
)
132+
.join('\n\n'),
133+
}),
134+
};
135+
},
90136

91137
// Assume server errors are handled separately
92138
// Return failure but no error message
@@ -109,6 +155,7 @@ export const handleFormError = async <T, P>(
109155

110156
const utils: HandlerUtils = {
111157
hasField: (field) => form.getRegisteredFields().includes(field),
158+
findField: makeFindField(form.getRegisteredFields()),
112159
};
113160

114161
const mergedHandlers = { ...defaultHandlers, ...handlers };
@@ -149,3 +196,8 @@ const resolveHandler =
149196
? { [FORM_ERROR]: result }
150197
: result;
151198
};
199+
200+
const makeFindField = (fields: readonly string[]) => (field: string | RegExp) =>
201+
typeof field === 'string' && fields.includes(field)
202+
? field
203+
: fields.find((f) => f.match(field));

0 commit comments

Comments
 (0)