@isi-ui7/bos7-shared 0.4.0 → 0.4.2

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.
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { SchemaFormRenderer } from "./form-renderer";
5
+ import type { FormSection } from "./form-types";
6
+
7
+ type D = Record<string, unknown>;
8
+
9
+ // Laporan DG-00 (agent7, 2026-08-31): field "date" di form-renderer.tsx punya
10
+ // DUA cacat. Cacat B (TERBUKTI + DIPERBAIKI di sini): `value={asString(rawValue)}`
11
+ // mengirim string ISO mentah ke DatePicker Carbon (flatpickr) yang
12
+ // dikonfigurasi dateFormat="d-m-Y", jadi flatpickr mem-parse ULANG string ISO
13
+ // itu SEOLAH ia format d-m-Y — tanggal TERSIMPAN tampil SALAH saat re-render.
14
+ // Diperbaiki dgn `fromLocalISODate()` (Date, bukan string) di form-renderer.tsx.
15
+ //
16
+ // Cacat A (diklaim DG-00 dari fiber React browser sungguhan: tanggal yg
17
+ // DIKETIK nol pernah masuk state) — TIDAK bisa direproduksi di tes terisolasi
18
+ // ini, bahkan pada kode SEBELUM perbaikan (lihat ledger dg-05 2026-08-31):
19
+ // `allowInput` Carbon versi ini SUDAH default `true` (DatePicker.js:377,
20
+ // `allowInput ?? true`), dan `fireEvent.blur` mengonfirmasi commit-on-blur
21
+ // bekerja di kedua kode lama & baru. Tes di bawah HANYA membuktikan alur
22
+ // commit bekerja di form-renderer terisolasi — ia BUKAN bukti Cacat A sudah
23
+ // tuntas di halaman produksi (mungkin bergantung interaksi re-render field
24
+ // lain yg tak tercakup harness minimal ini). Perlu re-verifikasi Tester/
25
+ // DG-00 langsung di browser thd kode yg sudah diperbaiki.
26
+
27
+ const dateSection: FormSection<D> = {
28
+ title: "Tanggal",
29
+ fields: [{ key: "tanggal_lahir", label: "Tanggal Lahir", type: "date", span: 4 }],
30
+ };
31
+
32
+ describe("field date — re-render dari value tersimpan (Cacat B)", () => {
33
+ it("ISO tersimpan tampil sbg tanggal yg SAMA dlm format d-m-Y, bukan digeser", () => {
34
+ render(
35
+ <SchemaFormRenderer<D>
36
+ mode="edit"
37
+ value={{ tanggal_lahir: "1990-06-15" }}
38
+ sections={[dateSection]}
39
+ onChange={() => {}}
40
+ />,
41
+ );
42
+ const input = screen.getByLabelText("Tanggal Lahir") as HTMLInputElement;
43
+ // Harus 15-06-1990 (hari yg SAMA, cuma beda format tampilan) — BUKAN
44
+ // tanggal lain hasil salah-parse "1990-06-15" seolah format d-m-Y.
45
+ expect(input.value).toBe("15-06-1990");
46
+ });
47
+
48
+ it("kontrol: tanggal akhir-tahun tetap utuh (bukan kebetulan lolos di satu kasus)", () => {
49
+ render(
50
+ <SchemaFormRenderer<D>
51
+ mode="edit"
52
+ value={{ tanggal_lahir: "2026-12-31" }}
53
+ sections={[dateSection]}
54
+ onChange={() => {}}
55
+ />,
56
+ );
57
+ const input = screen.getByLabelText("Tanggal Lahir") as HTMLInputElement;
58
+ expect(input.value).toBe("31-12-2026");
59
+ });
60
+ });
61
+
62
+ describe("field date — ketik tangan lalu commit (Cacat A)", () => {
63
+ it("tanggal yg DIKETIK (bukan dipilih dari kalender) masuk ke onChange", async () => {
64
+ const user = userEvent.setup();
65
+ const onChange = vi.fn();
66
+
67
+ render(
68
+ <SchemaFormRenderer<D>
69
+ mode="create"
70
+ value={{}}
71
+ sections={[dateSection]}
72
+ onChange={onChange}
73
+ />,
74
+ );
75
+ const input = screen.getByLabelText("Tanggal Lahir") as HTMLInputElement;
76
+ await user.type(input, "15-06-1990");
77
+ fireEvent.blur(input); // commit alami operator (pindah fokus keluar kotak)
78
+
79
+ const patches = onChange.mock.calls.map((c) => c[0]);
80
+ const got = patches.find((p) => p?.tanggal_lahir);
81
+ expect(got?.tanggal_lahir).toBe("1990-06-15");
82
+ });
83
+ });
@@ -185,3 +185,37 @@ export function toLocalISODate(date: Date | undefined | null): string {
185
185
  const d = String(date.getDate()).padStart(2, "0");
186
186
  return `${y}-${m}-${d}`;
187
187
  }
188
+
189
+ /**
190
+ * Kebalikan `toLocalISODate` — mem-parse "YYYY-MM-DD" jadi `Date` lokal
191
+ * (bukan `new Date(iso)` yang mem-parse sbg UTC dan bisa menggeser sehari,
192
+ * kelas bug yang sama dgn di atas, arah terbalik).
193
+ *
194
+ * Dipakai supaya prop `value` DatePicker Carbon menerima `Date`, BUKAN
195
+ * string ISO mentah — string ISO yang diberikan ke DatePicker dgn
196
+ * `dateFormat="d-m-Y"` di-parse ULANG flatpickr SEOLAH ia format d-m-Y,
197
+ * menghasilkan tanggal lain sama sekali (laporan DG-00, agent7 2026-08-31).
198
+ */
199
+ export function fromLocalISODate(iso: string | undefined | null): Date | undefined {
200
+ if (!iso) return undefined;
201
+ // Regex TIDAK ber-akhiran `$` dengan sengaja: field "date" (bukan
202
+ // "datetime") menerima "YYYY-MM-DD" polos MAUPUN RFC3339 penuh
203
+ // ("2029-12-31T00:00:00Z" atau ber-offset) — keduanya nyata dikirim
204
+ // backend, tergantung tipe kolom Go di sisi server (time.Time vs string
205
+ // yang sudah diformat). Sebelum perbaikan ini, RFC3339 gagal match diam-
206
+ // diam dan DatePicker menerima `undefined` — field tampak KOSONG tanpa
207
+ // satu pun galat (laporan PM, ditemukan lewat verifikasi browser
208
+ // sungguhan atas core7-service-cif FT-573/574, 2026-08-31).
209
+ //
210
+ // Ambil HANYA digit tanggalnya, JANGAN `new Date(iso)` — itu persis bug
211
+ // asli yang diperbaiki lewat fungsi ini (lihat komentar di atas):
212
+ // `new Date("2029-12-31T00:00:00Z")` mem-parse sbg UTC, dan di zona
213
+ // timur UTC (WIB) itu bisa MUNDUR sehari. Regex + konstruktor
214
+ // komponen-lokal di bawah membaca digitnya apa adanya, sama seperti
215
+ // jalur "YYYY-MM-DD" polos.
216
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
217
+ if (!m) return undefined;
218
+ const [, y, mo, d] = m;
219
+ const date = new Date(Number(y), Number(mo) - 1, Number(d));
220
+ return Number.isNaN(date.getTime()) ? undefined : date;
221
+ }
@@ -33,7 +33,7 @@ import {
33
33
  import type { Ui7FormDensity, Ui7FormWidth } from "./style-contract";
34
34
  import { useBosSharedI18n } from "./i18n";
35
35
  import type { FormField, FormMode, FormSection } from "./form-types";
36
- import { toLocalISODate } from "./form-renderer-utils";
36
+ import { toLocalISODate, fromLocalISODate } from "./form-renderer-utils";
37
37
  import { DetailModalField } from "./detail-modal-field";
38
38
 
39
39
  export type SchemaFormRendererProps<TData extends Record<string, unknown>> = {
@@ -847,7 +847,12 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
847
847
  // DISIMPAN tetap ISO. Dua hal berbeda: yang satu untuk
848
848
  // dibaca manusia, yang satu kontrak dgn backend.
849
849
  dateFormat="d-m-Y"
850
- value={asString(rawValue)}
850
+ // `value` HARUS `Date`, bukan string ISO — string ISO
851
+ // di sini di-parse ULANG flatpickr seolah format
852
+ // tampilan (d-m-Y), menghasilkan tanggal lain sama
853
+ // sekali pada setiap re-render (laporan DG-00,
854
+ // agent7 2026-08-31, kena SEMUA field tanggal).
855
+ value={fromLocalISODate(asString(rawValue))}
851
856
  onChange={([date]) =>
852
857
  setValue(field.key, toLocalISODate(date) as TData[keyof TData])
853
858
  }