@isi-ui7/bos7-shared 0.3.3 → 0.3.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-types.d.ts +33 -0
- package/package.json +1 -1
- package/src/form-renderer.tsx +16 -0
- package/src/form-section-hidden.test.tsx +87 -0
- package/src/form-section-render.test.tsx +96 -0
- package/src/form-types.ts +33 -0
package/dist/form-types.d.ts
CHANGED
|
@@ -145,6 +145,39 @@ export type FormSection<TData extends Record<string, unknown>> = {
|
|
|
145
145
|
/** Number of equal columns in the section grid. Default 1. */
|
|
146
146
|
columns?: 1 | 2 | 3;
|
|
147
147
|
fields: FormField<TData>[];
|
|
148
|
+
/**
|
|
149
|
+
* Jalan keluar: render konten sendiri MENGGANTIKAN grid `fields`.
|
|
150
|
+
*
|
|
151
|
+
* Ada untuk panel yang harus MEMBACA dan MENULIS state form sekaligus, yang
|
|
152
|
+
* tidak bisa dinyatakan sebagai field — mis. panel "Cek Identitas" pada
|
|
153
|
+
* registrasi nasabah, yang memanggil backend lalu menyetel penanda hasilnya
|
|
154
|
+
* agar gerbang `canLeavePage` bisa membacanya.
|
|
155
|
+
*
|
|
156
|
+
* `description` sudah ReactNode, tapi ia tidak punya akses ke `value` maupun
|
|
157
|
+
* cara menulis balik — jadi tidak cukup untuk keperluan ini.
|
|
158
|
+
*
|
|
159
|
+
* Opsional: section tanpa `render` dirender persis seperti sebelumnya.
|
|
160
|
+
* `fields` tetap wajib (pakai `[]`) supaya tipe section tidak bercabang dua.
|
|
161
|
+
*/
|
|
162
|
+
render?: (ctx: {
|
|
163
|
+
value: TData;
|
|
164
|
+
setValue: (patch: Partial<TData>) => void;
|
|
165
|
+
mode: FormMode;
|
|
166
|
+
disabled: boolean;
|
|
167
|
+
}) => ReactNode;
|
|
168
|
+
/**
|
|
169
|
+
* Sembunyikan SELURUH section — judul, deskripsi, dan isinya.
|
|
170
|
+
*
|
|
171
|
+
* Bentuknya sengaja sama persis dengan `FormField.hidden` supaya tidak ada
|
|
172
|
+
* dua idiom untuk hal yang sama.
|
|
173
|
+
*
|
|
174
|
+
* Ada karena menyembunyikan tiap field satu per satu TIDAK cukup: judul dan
|
|
175
|
+
* deskripsi section tetap terender, meninggalkan kepala panel kosong yang
|
|
176
|
+
* terlihat seperti bug — dan pada form yang panelnya memang harus terkunci
|
|
177
|
+
* dulu, kepala kosong itu justru mengiklankan keberadaan bagian yang belum
|
|
178
|
+
* boleh diisi.
|
|
179
|
+
*/
|
|
180
|
+
hidden?: boolean | ((mode: FormMode, data: TData) => boolean);
|
|
148
181
|
};
|
|
149
182
|
export type FormPageDef<TData extends Record<string, unknown>> = {
|
|
150
183
|
label: string;
|
package/package.json
CHANGED
package/src/form-renderer.tsx
CHANGED
|
@@ -484,6 +484,13 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
|
|
|
484
484
|
style={{ display: "grid", gap: dt.sectionGap }}
|
|
485
485
|
>
|
|
486
486
|
{sections.map((section, sectionIndex) => {
|
|
487
|
+
// Section tersembunyi mengembalikan null — React tidak merender apa
|
|
488
|
+
// pun untuknya, termasuk judul & deskripsinya.
|
|
489
|
+
const sectionHidden =
|
|
490
|
+
typeof section.hidden === "function"
|
|
491
|
+
? section.hidden(mode, value)
|
|
492
|
+
: section.hidden === true;
|
|
493
|
+
if (sectionHidden) return null;
|
|
487
494
|
const cols = section.columns ?? 1;
|
|
488
495
|
const defaultSpan = Math.floor(12 / cols);
|
|
489
496
|
|
|
@@ -521,6 +528,14 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
|
|
|
521
528
|
</div>
|
|
522
529
|
) : null}
|
|
523
530
|
|
|
531
|
+
{section.render ? (
|
|
532
|
+
section.render({
|
|
533
|
+
value,
|
|
534
|
+
setValue: (patch) => onChange?.(patch),
|
|
535
|
+
mode,
|
|
536
|
+
disabled,
|
|
537
|
+
})
|
|
538
|
+
) : (
|
|
524
539
|
<div
|
|
525
540
|
className={UI7_FORM_VISUAL_CLASSNAMES.row}
|
|
526
541
|
style={{
|
|
@@ -978,6 +993,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
|
|
|
978
993
|
];
|
|
979
994
|
})}
|
|
980
995
|
</div>
|
|
996
|
+
)}
|
|
981
997
|
</section>
|
|
982
998
|
);
|
|
983
999
|
})}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { SchemaFormRenderer } from "./form-renderer";
|
|
4
|
+
import type { FormSection } from "./form-types";
|
|
5
|
+
|
|
6
|
+
type D = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
// `FormSection.hidden` — menyembunyikan SELURUH section, bukan hanya field-nya.
|
|
9
|
+
//
|
|
10
|
+
// Menyembunyikan tiap field satu per satu tidak cukup: judul dan deskripsi
|
|
11
|
+
// section tetap terender, meninggalkan kepala panel kosong. Pada form yang
|
|
12
|
+
// panelnya memang harus terkunci dulu, kepala kosong itu justru mengiklankan
|
|
13
|
+
// bagian yang belum boleh diisi.
|
|
14
|
+
|
|
15
|
+
const locked: FormSection<D> = {
|
|
16
|
+
title: "Data Diri",
|
|
17
|
+
description: "Terbuka setelah verifikasi",
|
|
18
|
+
fields: [{ key: "nama", label: "Nama Lengkap", type: "text", span: 4 }],
|
|
19
|
+
hidden: (_mode, data) => data.verified !== true,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const always: FormSection<D> = {
|
|
23
|
+
title: "Verifikasi Identitas",
|
|
24
|
+
fields: [{ key: "nik", label: "Nomor Identitas", type: "text", span: 4 }],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function renderWith(value: D) {
|
|
28
|
+
return render(
|
|
29
|
+
<SchemaFormRenderer<D>
|
|
30
|
+
mode="create"
|
|
31
|
+
value={value}
|
|
32
|
+
sections={[always, locked]}
|
|
33
|
+
onChange={() => {}}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("FormSection.hidden", () => {
|
|
39
|
+
it("menyembunyikan JUDUL dan DESKRIPSI, bukan cuma field-nya", () => {
|
|
40
|
+
renderWith({ verified: false });
|
|
41
|
+
expect(screen.queryByText("Data Diri")).toBeNull();
|
|
42
|
+
expect(screen.queryByText("Terbuka setelah verifikasi")).toBeNull();
|
|
43
|
+
expect(screen.queryByLabelText("Nama Lengkap")).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("menampilkan section ketika predikatnya false", () => {
|
|
47
|
+
renderWith({ verified: true });
|
|
48
|
+
expect(screen.getByText("Data Diri")).toBeTruthy();
|
|
49
|
+
expect(screen.getByLabelText("Nama Lengkap")).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("KONTROL: section tetangga tanpa hidden tidak ikut terpengaruh", () => {
|
|
53
|
+
// Kalau kontrol ini ikut gagal, yang diukur bukan `hidden` melainkan
|
|
54
|
+
// perenderan section secara umum.
|
|
55
|
+
renderWith({ verified: false });
|
|
56
|
+
expect(screen.getByText("Verifikasi Identitas")).toBeTruthy();
|
|
57
|
+
expect(screen.getByLabelText("Nomor Identitas")).toBeTruthy();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("menerima boolean, bukan hanya fungsi — bentuknya sama dgn FormField.hidden", () => {
|
|
61
|
+
render(
|
|
62
|
+
<SchemaFormRenderer<D>
|
|
63
|
+
mode="create"
|
|
64
|
+
value={{}}
|
|
65
|
+
sections={[{ title: "Selalu Tersembunyi", fields: [], hidden: true }]}
|
|
66
|
+
onChange={() => {}}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
expect(screen.queryByText("Selalu Tersembunyi")).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("predikat menerima mode — section bisa disembunyikan hanya saat create", () => {
|
|
73
|
+
const s: FormSection<D> = {
|
|
74
|
+
title: "Hanya Saat Edit",
|
|
75
|
+
fields: [],
|
|
76
|
+
hidden: (mode) => mode === "create",
|
|
77
|
+
};
|
|
78
|
+
const { unmount } = render(
|
|
79
|
+
<SchemaFormRenderer<D> mode="create" value={{}} sections={[s]} onChange={() => {}} />,
|
|
80
|
+
);
|
|
81
|
+
expect(screen.queryByText("Hanya Saat Edit")).toBeNull();
|
|
82
|
+
unmount();
|
|
83
|
+
|
|
84
|
+
render(<SchemaFormRenderer<D> mode="edit" value={{}} sections={[s]} onChange={() => {}} />);
|
|
85
|
+
expect(screen.getByText("Hanya Saat Edit")).toBeTruthy();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { SchemaFormRenderer } from "./form-renderer";
|
|
4
|
+
import type { FormSection } from "./form-types";
|
|
5
|
+
|
|
6
|
+
type D = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
// Kait `render` pada FormSection — jalan keluar untuk panel yang harus MEMBACA
|
|
9
|
+
// dan MENULIS state form sekaligus (mis. "Cek Identitas" di registrasi
|
|
10
|
+
// nasabah), yang tidak bisa dinyatakan sebagai field.
|
|
11
|
+
//
|
|
12
|
+
// Dua hal yang diuji, dan yang KEDUA sama pentingnya: perubahan ini menyentuh
|
|
13
|
+
// jalur render SETIAP section di seluruh platform. Kalau section tanpa `render`
|
|
14
|
+
// ikut berubah, kerusakannya menyebar ke semua form sekaligus — dan bentuknya
|
|
15
|
+
// bukan galat, melainkan field yang diam-diam hilang.
|
|
16
|
+
|
|
17
|
+
const fieldsOnly: FormSection<D> = {
|
|
18
|
+
title: "Biasa",
|
|
19
|
+
fields: [{ key: "nama", label: "Nama Lengkap", type: "text", span: 4 }],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("FormSection.render", () => {
|
|
23
|
+
it("section BER-render menampilkan konten kustom dan TIDAK merender fields-nya", () => {
|
|
24
|
+
const section: FormSection<D> = {
|
|
25
|
+
title: "Verifikasi",
|
|
26
|
+
fields: [{ key: "nama", label: "Nama Lengkap", type: "text", span: 4 }],
|
|
27
|
+
render: () => <div>PANEL KUSTOM</div>,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
render(
|
|
31
|
+
<SchemaFormRenderer<D> mode="create" value={{}} sections={[section]} onChange={() => {}} />,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(screen.getByText("PANEL KUSTOM")).toBeTruthy();
|
|
35
|
+
// `fields` sengaja tetap diisi di atas: kalau grid-nya ikut terender,
|
|
36
|
+
// panel dan field akan tampil bersamaan — bukan itu maksud "menggantikan".
|
|
37
|
+
expect(screen.queryByLabelText("Nama Lengkap")).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("menerima value & mode, dan setValue meneruskan patch ke onChange", () => {
|
|
41
|
+
const onChange = vi.fn();
|
|
42
|
+
const seen: Record<string, unknown>[] = [];
|
|
43
|
+
|
|
44
|
+
const section: FormSection<D> = {
|
|
45
|
+
title: "Verifikasi",
|
|
46
|
+
fields: [],
|
|
47
|
+
render: (ctx) => {
|
|
48
|
+
seen.push({ value: ctx.value, mode: ctx.mode, disabled: ctx.disabled });
|
|
49
|
+
return <button onClick={() => ctx.setValue({ verify_status: "NEW" })}>Cek</button>;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
render(
|
|
54
|
+
<SchemaFormRenderer<D>
|
|
55
|
+
mode="create"
|
|
56
|
+
value={{ identity_number: "327101" }}
|
|
57
|
+
sections={[section]}
|
|
58
|
+
onChange={onChange}
|
|
59
|
+
/>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(seen[0].value).toEqual({ identity_number: "327101" });
|
|
63
|
+
expect(seen[0].mode).toBe("create");
|
|
64
|
+
expect(seen[0].disabled).toBe(false);
|
|
65
|
+
|
|
66
|
+
screen.getByText("Cek").click();
|
|
67
|
+
expect(onChange).toHaveBeenCalledWith({ verify_status: "NEW" });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("KONTROL REGRESI: section TANPA render tetap merender field-nya seperti sebelumnya", () => {
|
|
71
|
+
render(
|
|
72
|
+
<SchemaFormRenderer<D> mode="create" value={{}} sections={[fieldsOnly]} onChange={() => {}} />,
|
|
73
|
+
);
|
|
74
|
+
expect(screen.getByLabelText("Nama Lengkap")).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("KONTROL REGRESI: dua section bercampur — yang polos tidak terpengaruh tetangganya", () => {
|
|
78
|
+
const custom: FormSection<D> = {
|
|
79
|
+
title: "Verifikasi",
|
|
80
|
+
fields: [],
|
|
81
|
+
render: () => <div>PANEL KUSTOM</div>,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
render(
|
|
85
|
+
<SchemaFormRenderer<D>
|
|
86
|
+
mode="create"
|
|
87
|
+
value={{}}
|
|
88
|
+
sections={[custom, fieldsOnly]}
|
|
89
|
+
onChange={() => {}}
|
|
90
|
+
/>,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(screen.getByText("PANEL KUSTOM")).toBeTruthy();
|
|
94
|
+
expect(screen.getByLabelText("Nama Lengkap")).toBeTruthy();
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/form-types.ts
CHANGED
|
@@ -157,6 +157,39 @@ export type FormSection<TData extends Record<string, unknown>> = {
|
|
|
157
157
|
/** Number of equal columns in the section grid. Default 1. */
|
|
158
158
|
columns?: 1 | 2 | 3;
|
|
159
159
|
fields: FormField<TData>[];
|
|
160
|
+
/**
|
|
161
|
+
* Jalan keluar: render konten sendiri MENGGANTIKAN grid `fields`.
|
|
162
|
+
*
|
|
163
|
+
* Ada untuk panel yang harus MEMBACA dan MENULIS state form sekaligus, yang
|
|
164
|
+
* tidak bisa dinyatakan sebagai field — mis. panel "Cek Identitas" pada
|
|
165
|
+
* registrasi nasabah, yang memanggil backend lalu menyetel penanda hasilnya
|
|
166
|
+
* agar gerbang `canLeavePage` bisa membacanya.
|
|
167
|
+
*
|
|
168
|
+
* `description` sudah ReactNode, tapi ia tidak punya akses ke `value` maupun
|
|
169
|
+
* cara menulis balik — jadi tidak cukup untuk keperluan ini.
|
|
170
|
+
*
|
|
171
|
+
* Opsional: section tanpa `render` dirender persis seperti sebelumnya.
|
|
172
|
+
* `fields` tetap wajib (pakai `[]`) supaya tipe section tidak bercabang dua.
|
|
173
|
+
*/
|
|
174
|
+
render?: (ctx: {
|
|
175
|
+
value: TData;
|
|
176
|
+
setValue: (patch: Partial<TData>) => void;
|
|
177
|
+
mode: FormMode;
|
|
178
|
+
disabled: boolean;
|
|
179
|
+
}) => ReactNode;
|
|
180
|
+
/**
|
|
181
|
+
* Sembunyikan SELURUH section — judul, deskripsi, dan isinya.
|
|
182
|
+
*
|
|
183
|
+
* Bentuknya sengaja sama persis dengan `FormField.hidden` supaya tidak ada
|
|
184
|
+
* dua idiom untuk hal yang sama.
|
|
185
|
+
*
|
|
186
|
+
* Ada karena menyembunyikan tiap field satu per satu TIDAK cukup: judul dan
|
|
187
|
+
* deskripsi section tetap terender, meninggalkan kepala panel kosong yang
|
|
188
|
+
* terlihat seperti bug — dan pada form yang panelnya memang harus terkunci
|
|
189
|
+
* dulu, kepala kosong itu justru mengiklankan keberadaan bagian yang belum
|
|
190
|
+
* boleh diisi.
|
|
191
|
+
*/
|
|
192
|
+
hidden?: boolean | ((mode: FormMode, data: TData) => boolean);
|
|
160
193
|
};
|
|
161
194
|
|
|
162
195
|
// ── Layout ────────────────────────────────────────────────────────────────────
|