@isi-ui7/bos7-shared 0.4.3 → 0.4.5
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/dist/form-renderer-utils.d.ts +40 -1
- package/package.json +6 -6
- package/src/form-renderer-utils.ts +64 -1
- package/src/spanduk-galat-medan.test.ts +44 -0
- package/src/workflow/use-crud-form-hidden.test.tsx +62 -0
- package/src/workflow/use-crud-form-spanduk.test.tsx +66 -0
- package/src/workflow/use-crud-form.ts +8 -2
|
@@ -4,7 +4,46 @@ import { BosSharedLabels } from './i18n';
|
|
|
4
4
|
export declare function resolveApiPath(path: ApiPath, params: Record<string, string>): string;
|
|
5
5
|
type ValidationI18n = Pick<BosSharedLabels, "validRequired" | "validMinLength" | "validMaxLength" | "validMin" | "validMax" | "validPattern">;
|
|
6
6
|
export declare function runFieldValidation<TData extends Record<string, unknown>>(fields: FormField<TData>[], data: TData, mode: FormMode, i18n?: Partial<ValidationI18n>): Partial<Record<keyof TData, string>>;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Merangkum `fieldErrors` jadi satu kalimat yang MENYEBUT medannya.
|
|
9
|
+
*
|
|
10
|
+
* 🔑 Ada karena daftar medannya SUDAH dihitung dan nol pernah sampai ke layar.
|
|
11
|
+
* Spanduk lama satu string tetap ("Periksa field wajib") untuk seluruh galat,
|
|
12
|
+
* jadi "medan kosong" dan "medan terisi tapi format salah" terbaca identik —
|
|
13
|
+
* dan medan yang nol punya field ter-render terbaca sama pula.
|
|
14
|
+
*
|
|
15
|
+
* Ongkosnya terukur: dua medan (`cif_number`, `branch_code`) menahan create
|
|
16
|
+
* Nasabah Perorangan 21 hari (2026-08-15 s/d 09-05). Keduanya duduk di
|
|
17
|
+
* `fieldErrors` sepanjang waktu itu. Satu penyelidikan penuh dihabiskan untuk
|
|
18
|
+
* mencari nama yang aplikasinya sudah tahu sejak detik pertama.
|
|
19
|
+
*
|
|
20
|
+
* Label dipakai bila ada; kalau nol ada, JATUH ke kuncinya — jangan sel kosong
|
|
21
|
+
* dan jangan sembunyikan barisnya, karena medan tanpa label justru yang paling
|
|
22
|
+
* mungkin jadi medan yatim seperti di atas.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ringkasGalatMedan<TData extends Record<string, unknown>>(fieldErrors: Partial<Record<keyof TData, string>>, fields: FormField<TData>[], maks?: number): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Seluruh field yang BOLEH divalidasi — section `hidden` DILEWATI, sama seperti
|
|
27
|
+
* `collectPageFields`.
|
|
28
|
+
*
|
|
29
|
+
* 🔴 Dulu ia nol menyaring apa pun, bahkan `hidden: true` literal. Akibatnya
|
|
30
|
+
* `validateAll` (Save) menuntut field yang aplikasinya sendiri sembunyikan,
|
|
31
|
+
* sementara `validatePage` (Next) melewatinya dengan benar — jadi operator
|
|
32
|
+
* lolos di setiap halaman lalu ditolak di langkah terakhir, untuk field yang
|
|
33
|
+
* nol ada di layar mana pun dan nol bisa diisi gestur apa pun.
|
|
34
|
+
*
|
|
35
|
+
* `mode`/`data` opsional supaya pemanggil lama tetap jalan; tanpa keduanya
|
|
36
|
+
* hanya `hidden: true` literal yang bisa dievaluasi — predikat fungsi
|
|
37
|
+
* diperlakukan TIDAK tersembunyi, arah aman untuk validasi.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ Konsekuensi yang wajib dijaga di sisi SKEMA: kalau satu key punya dua
|
|
40
|
+
* salinan field (satu tampil di `create`, satu di `edit`), keduanya WAJIB
|
|
41
|
+
* membawa `validation`. Sebelum perbaikan ini, salinan tersembunyi ikut
|
|
42
|
+
* divalidasi sehingga satu salinan bervalidasi terasa cukup — ketergantungan
|
|
43
|
+
* terbalik yang kini nol berlaku lagi. Skema yang bersandar padanya akan
|
|
44
|
+
* kehilangan "wajib diisi" tanpa satu galat pun.
|
|
45
|
+
*/
|
|
46
|
+
export declare function collectAllFields<TData extends Record<string, unknown>>(layout: FormLayout<TData>, mode?: FormMode, data?: TData): FormField<TData>[];
|
|
8
47
|
/**
|
|
9
48
|
* Field yang BOLEH divalidasi pada satu halaman.
|
|
10
49
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isi-ui7/bos7-shared",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Shared auth7 and layout primitives for bos7 applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"@carbon/icons-react": "^11.71.0",
|
|
72
72
|
"@carbon/react": "^1.97.0",
|
|
73
|
-
"@isi-ui7/corporate-themes": ">=0.2.
|
|
73
|
+
"@isi-ui7/corporate-themes": ">=0.2.15",
|
|
74
74
|
"@isi-ui7/data-table": ">=0.2.5",
|
|
75
75
|
"@isi-ui7/editable-table": ">=0.2.3",
|
|
76
76
|
"@isi-ui7/i18n": ">=0.2.3",
|
|
@@ -106,14 +106,14 @@
|
|
|
106
106
|
"vite": "^5.0.0",
|
|
107
107
|
"vite-plugin-dts": "^3.6.0",
|
|
108
108
|
"vitest": "^1.0.0",
|
|
109
|
-
"@isi-ui7/corporate-themes": "0.2.
|
|
109
|
+
"@isi-ui7/corporate-themes": "0.2.15",
|
|
110
110
|
"@isi-ui7/data-table": "0.2.5",
|
|
111
111
|
"@isi-ui7/editable-table": "0.2.3",
|
|
112
|
-
"@isi-ui7/i18n": "0.2.3",
|
|
113
112
|
"@isi-ui7/lookup-input": "0.2.4",
|
|
114
|
-
"@isi-ui7/modal-manager": "0.2.2",
|
|
115
113
|
"@isi-ui7/realtime": "0.2.3",
|
|
116
|
-
"@isi-ui7/
|
|
114
|
+
"@isi-ui7/modal-manager": "0.2.2",
|
|
115
|
+
"@isi-ui7/ui-shell": "0.2.8",
|
|
116
|
+
"@isi-ui7/i18n": "0.2.3"
|
|
117
117
|
},
|
|
118
118
|
"dependencies": {
|
|
119
119
|
"@carbon/icons-react": "^11.71.0",
|
|
@@ -113,15 +113,78 @@ export function runFieldValidation<TData extends Record<string, unknown>>(
|
|
|
113
113
|
return errors;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Merangkum `fieldErrors` jadi satu kalimat yang MENYEBUT medannya.
|
|
118
|
+
*
|
|
119
|
+
* 🔑 Ada karena daftar medannya SUDAH dihitung dan nol pernah sampai ke layar.
|
|
120
|
+
* Spanduk lama satu string tetap ("Periksa field wajib") untuk seluruh galat,
|
|
121
|
+
* jadi "medan kosong" dan "medan terisi tapi format salah" terbaca identik —
|
|
122
|
+
* dan medan yang nol punya field ter-render terbaca sama pula.
|
|
123
|
+
*
|
|
124
|
+
* Ongkosnya terukur: dua medan (`cif_number`, `branch_code`) menahan create
|
|
125
|
+
* Nasabah Perorangan 21 hari (2026-08-15 s/d 09-05). Keduanya duduk di
|
|
126
|
+
* `fieldErrors` sepanjang waktu itu. Satu penyelidikan penuh dihabiskan untuk
|
|
127
|
+
* mencari nama yang aplikasinya sudah tahu sejak detik pertama.
|
|
128
|
+
*
|
|
129
|
+
* Label dipakai bila ada; kalau nol ada, JATUH ke kuncinya — jangan sel kosong
|
|
130
|
+
* dan jangan sembunyikan barisnya, karena medan tanpa label justru yang paling
|
|
131
|
+
* mungkin jadi medan yatim seperti di atas.
|
|
132
|
+
*/
|
|
133
|
+
export function ringkasGalatMedan<TData extends Record<string, unknown>>(
|
|
134
|
+
fieldErrors: Partial<Record<keyof TData, string>>,
|
|
135
|
+
fields: FormField<TData>[],
|
|
136
|
+
maks = 5,
|
|
137
|
+
): string | null {
|
|
138
|
+
const kunci = Object.keys(fieldErrors).filter((k) => fieldErrors[k as keyof TData]);
|
|
139
|
+
if (kunci.length === 0) return null;
|
|
140
|
+
|
|
141
|
+
const label = new Map<string, string>();
|
|
142
|
+
for (const f of fields) {
|
|
143
|
+
const k = String(f.key);
|
|
144
|
+
const l = typeof f.label === "string" ? f.label.trim() : "";
|
|
145
|
+
if (l && !label.has(k)) label.set(k, l);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const butir = kunci.slice(0, maks).map((k) => {
|
|
149
|
+
const nama = label.get(k) || k; // JATUH ke kunci, bukan kosong
|
|
150
|
+
return `${nama}: ${fieldErrors[k as keyof TData]}`;
|
|
151
|
+
});
|
|
152
|
+
const sisa = kunci.length - butir.length;
|
|
153
|
+
return butir.join(" · ") + (sisa > 0 ? ` · +${sisa} lagi` : "");
|
|
154
|
+
}
|
|
155
|
+
|
|
116
156
|
// ── Layout helpers ────────────────────────────────────────────────────────────
|
|
117
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Seluruh field yang BOLEH divalidasi — section `hidden` DILEWATI, sama seperti
|
|
160
|
+
* `collectPageFields`.
|
|
161
|
+
*
|
|
162
|
+
* 🔴 Dulu ia nol menyaring apa pun, bahkan `hidden: true` literal. Akibatnya
|
|
163
|
+
* `validateAll` (Save) menuntut field yang aplikasinya sendiri sembunyikan,
|
|
164
|
+
* sementara `validatePage` (Next) melewatinya dengan benar — jadi operator
|
|
165
|
+
* lolos di setiap halaman lalu ditolak di langkah terakhir, untuk field yang
|
|
166
|
+
* nol ada di layar mana pun dan nol bisa diisi gestur apa pun.
|
|
167
|
+
*
|
|
168
|
+
* `mode`/`data` opsional supaya pemanggil lama tetap jalan; tanpa keduanya
|
|
169
|
+
* hanya `hidden: true` literal yang bisa dievaluasi — predikat fungsi
|
|
170
|
+
* diperlakukan TIDAK tersembunyi, arah aman untuk validasi.
|
|
171
|
+
*
|
|
172
|
+
* ⚠️ Konsekuensi yang wajib dijaga di sisi SKEMA: kalau satu key punya dua
|
|
173
|
+
* salinan field (satu tampil di `create`, satu di `edit`), keduanya WAJIB
|
|
174
|
+
* membawa `validation`. Sebelum perbaikan ini, salinan tersembunyi ikut
|
|
175
|
+
* divalidasi sehingga satu salinan bervalidasi terasa cukup — ketergantungan
|
|
176
|
+
* terbalik yang kini nol berlaku lagi. Skema yang bersandar padanya akan
|
|
177
|
+
* kehilangan "wajib diisi" tanpa satu galat pun.
|
|
178
|
+
*/
|
|
118
179
|
export function collectAllFields<TData extends Record<string, unknown>>(
|
|
119
180
|
layout: FormLayout<TData>,
|
|
181
|
+
mode?: FormMode,
|
|
182
|
+
data?: TData,
|
|
120
183
|
): FormField<TData>[] {
|
|
121
184
|
const sections = layout.type === "single-page"
|
|
122
185
|
? layout.sections
|
|
123
186
|
: layout.pages.flatMap((p) => p.sections);
|
|
124
|
-
return sections.flatMap((s) => s.fields);
|
|
187
|
+
return sections.filter((s) => !isSectionHidden(s, mode, data)).flatMap((s) => s.fields);
|
|
125
188
|
}
|
|
126
189
|
|
|
127
190
|
/**
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { ringkasGalatMedan } from "./form-renderer-utils";
|
|
3
|
+
import type { FormField } from "./form-types";
|
|
4
|
+
|
|
5
|
+
type D = Record<string, unknown>;
|
|
6
|
+
const f = (key: string, label?: string) => ({ key, label, type: "text" } as unknown as FormField<D>);
|
|
7
|
+
|
|
8
|
+
describe("spanduk galat menyebut MEDANNYA", () => {
|
|
9
|
+
it("menyebut label + alasan, bukan satu kalimat tetap", () => {
|
|
10
|
+
const s = ringkasGalatMedan<D>(
|
|
11
|
+
{ identity_number: "NIK harus 16 digit angka", birth_date: "Tanggal Lahir wajib diisi" },
|
|
12
|
+
[f("identity_number", "NIK"), f("birth_date", "Tanggal Lahir")],
|
|
13
|
+
);
|
|
14
|
+
expect(s).toContain("NIK");
|
|
15
|
+
expect(s).toContain("16 digit");
|
|
16
|
+
expect(s).toContain("Tanggal Lahir");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("PEMANCAR: nol ada galat -> spanduk DIAM (null), bukan kalimat kosong", () => {
|
|
20
|
+
expect(ringkasGalatMedan<D>({}, [f("a", "A")])).toBeNull();
|
|
21
|
+
expect(ringkasGalatMedan<D>({ a: undefined } as never, [f("a", "A")])).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("medan tanpa label JATUH ke kuncinya — nol disembunyikan", () => {
|
|
25
|
+
// Justru medan tanpa label yang paling mungkin jadi medan yatim
|
|
26
|
+
// (cif_number/branch_code, cacat 21 hari). Ia WAJIB kelihatan.
|
|
27
|
+
const s = ringkasGalatMedan<D>({ branch_code: "Cabang wajib diisi" }, []);
|
|
28
|
+
expect(s).toContain("branch_code");
|
|
29
|
+
expect(s).toContain("Cabang wajib diisi");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("dipotong kalau panjang, dan SEBUT berapa yang tersisa", () => {
|
|
33
|
+
const errs: Record<string, string> = {};
|
|
34
|
+
const fields: FormField<D>[] = [];
|
|
35
|
+
for (let i = 0; i < 9; i++) { errs["k" + i] = "wajib diisi"; fields.push(f("k" + i, "L" + i)); }
|
|
36
|
+
const s = ringkasGalatMedan<D>(errs, fields, 5)!;
|
|
37
|
+
expect(s.split(" · ").filter((x) => !x.startsWith("+")).length).toBe(5);
|
|
38
|
+
expect(s).toContain("+4 lagi");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("label kosong/spasi diperlakukan sbg TIDAK ADA", () => {
|
|
42
|
+
expect(ringkasGalatMedan<D>({ x: "wajib" }, [f("x", " ")])).toContain("x:");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { act, renderHook } from "@testing-library/react";
|
|
3
|
+
import { useCrudForm } from "./use-crud-form";
|
|
4
|
+
import type { CrudForm } from "../form-types";
|
|
5
|
+
|
|
6
|
+
type D = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `validateAll` (Save) dulu memakai `collectAllFields` TANPA mode, jadi ia
|
|
10
|
+
* menuntut field di section TERSEMBUNYI — sementara `validatePage` (Next)
|
|
11
|
+
* melewatinya dengan benar. Operator lolos di tiap halaman lalu ditolak di
|
|
12
|
+
* langkah terakhir, untuk field yang nol ada di layarnya.
|
|
13
|
+
*
|
|
14
|
+
* Diuji lewat PERILAKU handleSave, bukan lewat collectAllFields langsung:
|
|
15
|
+
* uji atas helper tetap hijau meski hook nol memakainya.
|
|
16
|
+
*/
|
|
17
|
+
function schema(sembunyi: boolean | ((m: string, d: D) => boolean)): CrudForm<D> {
|
|
18
|
+
return {
|
|
19
|
+
title: { create: "c", edit: "e", view: "v" },
|
|
20
|
+
emptyData: { tampak: "", tersembunyi: "" } as D,
|
|
21
|
+
createApiPath: "/api/x",
|
|
22
|
+
layout: {
|
|
23
|
+
type: "single-page",
|
|
24
|
+
sections: [
|
|
25
|
+
{ title: "tampak", fields: [
|
|
26
|
+
{ key: "tampak", label: "Tampak", type: "text", span: 4, validation: { required: true } }] },
|
|
27
|
+
{ title: "rahasia", hidden: sembunyi as never, fields: [
|
|
28
|
+
{ key: "tersembunyi", label: "Tersembunyi", type: "text", span: 4, validation: { required: true } }] },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
} as unknown as CrudForm<D>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function galat(s: CrudForm<D>, isi: D) {
|
|
35
|
+
const { result } = renderHook(() => useCrudForm<D>({ schema: s, mode: "create" }));
|
|
36
|
+
act(() => { result.current.setForm(isi); });
|
|
37
|
+
await act(async () => { await result.current.handleSave(); });
|
|
38
|
+
return { pesan: String(result.current.error ?? ""), medan: result.current.fieldErrors as Record<string,string> };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("Save nol menuntut field di section tersembunyi", () => {
|
|
42
|
+
it("hidden literal: field tersembunyi NOL memblokir Save", async () => {
|
|
43
|
+
const r = await galat(schema(true), { tampak: "x", tersembunyi: "" });
|
|
44
|
+
expect(r.medan.tersembunyi, "field tersembunyi nol boleh dituntut").toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("hidden predikat: sama", async () => {
|
|
48
|
+
const r = await galat(schema(() => true), { tampak: "x", tersembunyi: "" });
|
|
49
|
+
expect(r.medan.tersembunyi).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("KONTROL POSITIF: section TERLIHAT tetap dituntut", async () => {
|
|
53
|
+
const r = await galat(schema(true), { tampak: "", tersembunyi: "" });
|
|
54
|
+
expect(r.medan.tampak, "penjaga masih menggigit utk yg terlihat").toBeTruthy();
|
|
55
|
+
expect(r.pesan).toContain("Tampak");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("KONTROL POSITIF: section yang TIDAK tersembunyi tetap dituntut", async () => {
|
|
59
|
+
const r = await galat(schema(false), { tampak: "x", tersembunyi: "" });
|
|
60
|
+
expect(r.medan.tersembunyi, "hidden=false ⇒ tetap divalidasi").toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { act, renderHook } from "@testing-library/react";
|
|
3
|
+
import { useCrudForm } from "./use-crud-form";
|
|
4
|
+
import type { CrudForm } from "../form-types";
|
|
5
|
+
|
|
6
|
+
type D = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Spanduk galat diuji lewat PERILAKU `handleSave`, bukan lewat helper-nya.
|
|
10
|
+
*
|
|
11
|
+
* Uji atas `ringkasGalatMedan` sendirian TETAP HIJAU meski nilai balik `error`
|
|
12
|
+
* nol pernah memakainya — diukur: mencabut penyambungan di `use-crud-form`
|
|
13
|
+
* meninggalkan 154 uji hijau. Yang membedakan "terpasang" dari "sekadar ada"
|
|
14
|
+
* cuma membaca `error` yang benar-benar keluar dari hook.
|
|
15
|
+
*/
|
|
16
|
+
function schema(): CrudForm<D> {
|
|
17
|
+
return {
|
|
18
|
+
title: { create: "c", edit: "e", view: "v" },
|
|
19
|
+
emptyData: { nik: "", lahir: "", yatim: "" } as D,
|
|
20
|
+
createApiPath: "/api/x",
|
|
21
|
+
layout: {
|
|
22
|
+
type: "single-page",
|
|
23
|
+
sections: [
|
|
24
|
+
{
|
|
25
|
+
title: "s",
|
|
26
|
+
fields: [
|
|
27
|
+
{ key: "nik", label: "NIK", type: "text", span: 4, validation: { required: true } },
|
|
28
|
+
{ key: "lahir", label: "Tanggal Lahir", type: "text", span: 4, validation: { required: true } },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
// Menuntut medan yang NOL punya field — cacat cif_number/branch_code.
|
|
34
|
+
validate: (d: D) => (String(d.yatim ?? "").trim() ? {} : { yatim: "Cabang wajib diisi" }),
|
|
35
|
+
} as unknown as CrudForm<D>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("spanduk galat: PERILAKU handleSave", () => {
|
|
39
|
+
it("menyebut LABEL medan yang gagal, bukan satu kalimat tetap", async () => {
|
|
40
|
+
const { result } = renderHook(() => useCrudForm<D>({ schema: schema(), mode: "create" }));
|
|
41
|
+
await act(async () => { await result.current.handleSave(); });
|
|
42
|
+
|
|
43
|
+
const e = String(result.current.error ?? "");
|
|
44
|
+
expect(e).toContain("NIK");
|
|
45
|
+
expect(e).toContain("Tanggal Lahir");
|
|
46
|
+
expect(e).not.toBe("Periksa field wajib");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("medan YATIM (dituntut validate, nol punya field) tetap DISEBUT", async () => {
|
|
50
|
+
const { result } = renderHook(() => useCrudForm<D>({ schema: schema(), mode: "create" }));
|
|
51
|
+
await act(async () => { await result.current.handleSave(); });
|
|
52
|
+
// Justru inilah yang menahan create Nasabah Perorangan 21 hari tanpa nama.
|
|
53
|
+
expect(String(result.current.error ?? "")).toContain("yatim");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("PEMANCAR: nol ada galat validasi -> spanduk DIAM", async () => {
|
|
57
|
+
const { result } = renderHook(() => useCrudForm<D>({ schema: schema(), mode: "create" }));
|
|
58
|
+
act(() => { result.current.setForm({ nik: "1", lahir: "2", yatim: "3" } as D); });
|
|
59
|
+
await act(async () => { await result.current.handleSave(); });
|
|
60
|
+
// Save akan gagal di jaringan (nol ada server) -> itu serverError, BUKAN
|
|
61
|
+
// spanduk validasi. Yang diasersi: ia nol lagi berbunyi soal medan.
|
|
62
|
+
const e = String(result.current.error ?? "");
|
|
63
|
+
expect(e).not.toContain("NIK:");
|
|
64
|
+
expect(e).not.toContain("Tanggal Lahir:");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
4
4
|
import type { CrudForm, FormMode } from "../form-types";
|
|
5
5
|
import {
|
|
6
6
|
collectAllFields,
|
|
7
|
+
ringkasGalatMedan,
|
|
7
8
|
collectPageFields,
|
|
8
9
|
getLayoutPageCount,
|
|
9
10
|
resolveApiPath,
|
|
@@ -108,7 +109,7 @@ export function useCrudForm<TData extends Record<string, unknown>>({
|
|
|
108
109
|
};
|
|
109
110
|
|
|
110
111
|
const validateAll = () => {
|
|
111
|
-
const errs = runFieldValidation(collectAllFields(schema.layout), form, mode, labels);
|
|
112
|
+
const errs = runFieldValidation(collectAllFields(schema.layout, mode, form), form, mode, labels);
|
|
112
113
|
if (schema.validate) {
|
|
113
114
|
return { ...errs, ...schema.validate(form) };
|
|
114
115
|
}
|
|
@@ -206,7 +207,12 @@ export function useCrudForm<TData extends Record<string, unknown>>({
|
|
|
206
207
|
setForm,
|
|
207
208
|
loading,
|
|
208
209
|
saving,
|
|
209
|
-
|
|
210
|
+
// Cabang validasi MENYEBUT medannya; cabang serverError nol tersentuh —
|
|
211
|
+
// keduanya menjawab pertanyaan berbeda dan nol boleh dilebur.
|
|
212
|
+
error: hasValidationError
|
|
213
|
+
? ringkasGalatMedan(fieldErrors, collectAllFields(schema.layout)) ??
|
|
214
|
+
labels.validationError
|
|
215
|
+
: serverError,
|
|
210
216
|
fieldErrors,
|
|
211
217
|
isDirty,
|
|
212
218
|
page,
|