@isi-ui7/bos7-shared 0.3.3 → 0.3.4

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.
@@ -145,6 +145,26 @@ 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;
148
168
  };
149
169
  export type FormPageDef<TData extends Record<string, unknown>> = {
150
170
  label: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isi-ui7/bos7-shared",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Shared auth7 and layout primitives for bos7 applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -521,6 +521,14 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
521
521
  </div>
522
522
  ) : null}
523
523
 
524
+ {section.render ? (
525
+ section.render({
526
+ value,
527
+ setValue: (patch) => onChange?.(patch),
528
+ mode,
529
+ disabled,
530
+ })
531
+ ) : (
524
532
  <div
525
533
  className={UI7_FORM_VISUAL_CLASSNAMES.row}
526
534
  style={{
@@ -978,6 +986,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
978
986
  ];
979
987
  })}
980
988
  </div>
989
+ )}
981
990
  </section>
982
991
  );
983
992
  })}
@@ -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,26 @@ 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;
160
180
  };
161
181
 
162
182
  // ── Layout ────────────────────────────────────────────────────────────────────