@sjolystinnovation/app-kit 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/package.json +1 -1
- package/src/api/apiFieldErrors.ts +23 -0
- package/src/api/index.ts +1 -0
- package/src/validation/fieldErrors.ts +34 -0
- package/src/validation/index.ts +2 -0
package/README.md
CHANGED
|
@@ -264,6 +264,10 @@ export const getRecipe = (id: number) =>
|
|
|
264
264
|
`target`, so an edit shows at once. It posts to `/api/revalidate` unless `endpoint` says otherwise.
|
|
265
265
|
It never throws. On failure the pages' own revalidate window is the fallback.
|
|
266
266
|
|
|
267
|
+
`apiFieldErrors(error)` reads the per-field messages of an ASP.NET validation problem from a failed
|
|
268
|
+
call or its body, keyed in camelCase to match the form: `{ errors: { OrgNumber: ["..."] } }` becomes
|
|
269
|
+
`{ orgNumber: ["..."] }`. It answers null when there are none, so the form falls back to one message.
|
|
270
|
+
|
|
267
271
|
`createPublicClient(baseURL)` is the same without the session, for the API's anonymous endpoints. An
|
|
268
272
|
empty `baseURL` throws, and requests only go to `baseURL`.
|
|
269
273
|
|
|
@@ -1016,6 +1020,20 @@ const gargeRule = passwordSchema().max(128, "Be at most 128 characters long.");
|
|
|
1016
1020
|
|
|
1017
1021
|
`PASSWORD_MIN_LENGTH` holds the minimum, for hint text.
|
|
1018
1022
|
|
|
1023
|
+
### `zodFieldErrors(error, options?)`
|
|
1024
|
+
|
|
1025
|
+
From `@sjolystinnovation/app-kit/validation`. A failed parse's issues by the top-level field they
|
|
1026
|
+
belong to, for showing beside each input. Each field gets its first issue's message, or every message
|
|
1027
|
+
in order with `all: true`. `message` replaces zod's text, such as with the app's own translations.
|
|
1028
|
+
Issues that belong to no field are left out.
|
|
1029
|
+
|
|
1030
|
+
```ts
|
|
1031
|
+
import { zodFieldErrors } from "@sjolystinnovation/app-kit/validation";
|
|
1032
|
+
|
|
1033
|
+
const parsed = contactSchema.safeParse(form);
|
|
1034
|
+
if (!parsed.success) setErrors(zodFieldErrors(parsed.error, { message: (issue) => issueMessage(issue, dict) }));
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1019
1037
|
## Theming
|
|
1020
1038
|
|
|
1021
1039
|
The components draw every colour, corner radius and label size from theme variables with a light
|
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The per-field messages of an ASP.NET validation problem, keyed by the field's name in camelCase so
|
|
5
|
+
* they match the form's own names: `{ errors: { OrgNumber: ["..."] } }` becomes
|
|
6
|
+
* `{ orgNumber: ["..."] }`. Takes the failed axios call or the response body. Answers null when there
|
|
7
|
+
* are no field errors, so the caller falls back to one message for the whole form.
|
|
8
|
+
*/
|
|
9
|
+
export function apiFieldErrors(error: unknown): Record<string, string[]> | null {
|
|
10
|
+
const data = axios.isAxiosError(error) ? error.response?.data : error;
|
|
11
|
+
const errors = data && typeof data === "object" ? (data as { errors?: unknown }).errors : undefined;
|
|
12
|
+
if (!errors || typeof errors !== "object" || Array.isArray(errors)) return null;
|
|
13
|
+
|
|
14
|
+
// A Map, so a field named like an Object property such as "constructor" is still just a field.
|
|
15
|
+
const fields = new Map<string, string[]>();
|
|
16
|
+
for (const [key, value] of Object.entries(errors as Record<string, unknown>)) {
|
|
17
|
+
const messages = (Array.isArray(value) ? value : [value]).filter((m): m is string => typeof m === "string" && m !== "");
|
|
18
|
+
if (messages.length === 0) continue;
|
|
19
|
+
const field = key.charAt(0).toLowerCase() + key.slice(1);
|
|
20
|
+
fields.set(field, [...(fields.get(field) ?? []), ...messages]);
|
|
21
|
+
}
|
|
22
|
+
return fields.size > 0 ? Object.fromEntries(fields) : null;
|
|
23
|
+
}
|
package/src/api/index.ts
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** Picks the text shown for an issue, such as a translated message by the issue's code. */
|
|
4
|
+
export type IssueMessage = (issue: z.core.$ZodIssue) => string;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A failed parse's issues by the top-level field they belong to, for showing beside each input. By
|
|
8
|
+
* default each field gets its first issue's message. With `all: true` it gets every message, in order.
|
|
9
|
+
* `message` replaces zod's text, such as with the app's own translations. Issues without a field,
|
|
10
|
+
* from a refinement of the whole object, are left out.
|
|
11
|
+
*/
|
|
12
|
+
export function zodFieldErrors<T>(
|
|
13
|
+
error: z.ZodError<T>,
|
|
14
|
+
options?: { all?: false; message?: IssueMessage },
|
|
15
|
+
): Partial<Record<Extract<keyof T, string>, string>>;
|
|
16
|
+
export function zodFieldErrors<T>(
|
|
17
|
+
error: z.ZodError<T>,
|
|
18
|
+
options: { all: true; message?: IssueMessage },
|
|
19
|
+
): Partial<Record<Extract<keyof T, string>, string[]>>;
|
|
20
|
+
export function zodFieldErrors<T>(
|
|
21
|
+
error: z.ZodError<T>,
|
|
22
|
+
options: { all?: boolean; message?: IssueMessage } = {},
|
|
23
|
+
): object {
|
|
24
|
+
// A Map, so a field named like an Object property such as "constructor" is still just a field.
|
|
25
|
+
const errors = new Map<string, string[]>();
|
|
26
|
+
for (const issue of error.issues) {
|
|
27
|
+
const field = issue.path[0];
|
|
28
|
+
if (typeof field !== "string") continue;
|
|
29
|
+
const messages = errors.get(field) ?? [];
|
|
30
|
+
messages.push(options.message ? options.message(issue) : issue.message);
|
|
31
|
+
errors.set(field, messages);
|
|
32
|
+
}
|
|
33
|
+
return Object.fromEntries([...errors].map(([field, messages]) => [field, options.all ? messages : messages[0]]));
|
|
34
|
+
}
|
package/src/validation/index.ts
CHANGED