@isi-ui7/bos7-shared 0.4.5 → 0.4.7
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/{index-DqWPV29B.js → index-C-h8WiEp.js} +99 -75
- package/dist/{index.es-9cvhYbjn.js → index.es-C4xrluJC.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{jspdf.es.min-CCBzPHw4.js → jspdf.es.min-OHmQQaGr.js} +2 -2
- package/dist/{jspdf.plugin.autotable-DFaknRns.js → jspdf.plugin.autotable-DcntYaes.js} +22 -19
- package/dist/purify.es-CiEWEeUM.js +605 -0
- package/package.json +9 -9
- package/src/detail-rows-invalid-text.test.tsx +125 -0
- package/src/detail-rows-renders-existing.test.tsx +176 -0
- package/src/form-renderer.tsx +25 -0
- package/src/i18n-toggle-locale.test.ts +52 -0
- package/src/i18n.tsx +8 -2
- package/dist/purify.es-Cm3utOpm.js +0 -560
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.7",
|
|
4
4
|
"description": "Shared auth7 and layout primitives for bos7 applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,10 +70,10 @@
|
|
|
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.17",
|
|
74
74
|
"@isi-ui7/data-table": ">=0.2.5",
|
|
75
75
|
"@isi-ui7/editable-table": ">=0.2.3",
|
|
76
|
-
"@isi-ui7/i18n": ">=0.2.
|
|
76
|
+
"@isi-ui7/i18n": ">=0.2.4",
|
|
77
77
|
"@isi-ui7/lookup-input": ">=0.2.4",
|
|
78
78
|
"@isi-ui7/modal-manager": ">=0.2.0",
|
|
79
79
|
"@isi-ui7/realtime": ">=0.2.0",
|
|
@@ -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.17",
|
|
110
110
|
"@isi-ui7/data-table": "0.2.5",
|
|
111
|
-
"@isi-ui7/editable-table": "0.2.3",
|
|
112
111
|
"@isi-ui7/lookup-input": "0.2.4",
|
|
113
112
|
"@isi-ui7/realtime": "0.2.3",
|
|
114
113
|
"@isi-ui7/modal-manager": "0.2.2",
|
|
115
|
-
"@isi-ui7/
|
|
116
|
-
"@isi-ui7/
|
|
114
|
+
"@isi-ui7/i18n": "0.2.4",
|
|
115
|
+
"@isi-ui7/editable-table": "0.2.3",
|
|
116
|
+
"@isi-ui7/ui-shell": "0.2.8"
|
|
117
117
|
},
|
|
118
118
|
"dependencies": {
|
|
119
119
|
"@carbon/icons-react": "^11.71.0",
|
|
@@ -126,9 +126,9 @@
|
|
|
126
126
|
"jose": "^5.9.6",
|
|
127
127
|
"jspdf": "^4.2.1",
|
|
128
128
|
"jspdf-autotable": "^5.0.7",
|
|
129
|
-
"@isi-ui7/
|
|
129
|
+
"@isi-ui7/lookup-input": "0.2.4",
|
|
130
130
|
"@isi-ui7/editable-table": "0.2.3",
|
|
131
|
-
"@isi-ui7/
|
|
131
|
+
"@isi-ui7/data-table": "0.2.5"
|
|
132
132
|
},
|
|
133
133
|
"scripts": {
|
|
134
134
|
"build": "vite build",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import { afterEach } from "vitest";
|
|
4
|
+
import { SchemaFormRenderer } from "./form-renderer";
|
|
5
|
+
import type { FormSection } from "./form-types";
|
|
6
|
+
|
|
7
|
+
afterEach(cleanup);
|
|
8
|
+
|
|
9
|
+
type D = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
// Medan `detail-rows` NOL meneruskan `invalidText` sampai 2026-09-06, sementara
|
|
12
|
+
// saudaranya `detail-modal` SUDAH (detail-modal-field.tsx:277).
|
|
13
|
+
//
|
|
14
|
+
// 🔴 Akibatnya: sebuah aturan validasi yang keyed ke medan detail-rows
|
|
15
|
+
// DIHITUNG, MENAHAN SUBMIT (use-crud-form.ts:167 `if (Object.keys(errs).length)
|
|
16
|
+
// return`), lalu NOL PERNAH DITAMPILKAN. Petugas melihat spanduk generik
|
|
17
|
+
// "Check required fields" dan tabel yang bersih — pesannya sudah ditulis, sudah
|
|
18
|
+
// benar, dan invisible.
|
|
19
|
+
//
|
|
20
|
+
// Terukur pada Produk Pembiayaan (bos7-financing, errors.costs). Radius ledak
|
|
21
|
+
// lintas estate: 5 formulir yang benar-benar menyetel error pada kunci
|
|
22
|
+
// detail-rows (financing ×2 `costs`, treasury ×3 `items`/`items`/`mappings`);
|
|
23
|
+
// 12 dari 17 berkas ber-detail-rows lainnya nol menyetelnya dan karena itu nol
|
|
24
|
+
// dapat melihat perbedaan.
|
|
25
|
+
|
|
26
|
+
const PESAN = "Setiap baris biaya harus memilih Element Biaya";
|
|
27
|
+
|
|
28
|
+
const section: FormSection<D> = {
|
|
29
|
+
title: "Parameter Biaya",
|
|
30
|
+
fields: [
|
|
31
|
+
{
|
|
32
|
+
key: "costs",
|
|
33
|
+
label: "Daftar Biaya",
|
|
34
|
+
type: "detail-rows",
|
|
35
|
+
span: 12,
|
|
36
|
+
detailRows: {
|
|
37
|
+
columns: [{ key: "cost_element_id", label: "Element Biaya", type: "text" }],
|
|
38
|
+
newRowFactory: () => ({ cost_element_id: "" }),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// 🔴 Menghitung SEMUA [role=alert] mengukur populasi yang salah: Carbon's
|
|
45
|
+
// TextInput selalu merender <span class="cds--text-input__counter-alert"> yang
|
|
46
|
+
// KOSONG, jadi asersi "tepat satu alert" gagal karena alasan yang nol
|
|
47
|
+
// berhubungan. Terukur saat menulis berkas ini — uji "galat nol bocor" berbunyi
|
|
48
|
+
// 2 padahal cuma satu medan bergalat.
|
|
49
|
+
//
|
|
50
|
+
// Yang dihitung: alert yang BERISI teks. Itu populasi yang pertanyaannya
|
|
51
|
+
// maksud.
|
|
52
|
+
function alertsBerteks(root: HTMLElement) {
|
|
53
|
+
return Array.from(root.querySelectorAll('[role="alert"]')).filter(
|
|
54
|
+
(el) => (el.textContent ?? "").trim() !== "",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderWith(errors: Record<string, string>) {
|
|
59
|
+
return render(
|
|
60
|
+
<SchemaFormRenderer<D>
|
|
61
|
+
mode="create"
|
|
62
|
+
value={{ costs: [] }}
|
|
63
|
+
sections={[section]}
|
|
64
|
+
errors={errors}
|
|
65
|
+
onChange={() => {}}
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("detail-rows menampilkan galat medannya", () => {
|
|
71
|
+
// ARAH 1 — merah sebelum perbaikan: pesannya nol pernah muncul.
|
|
72
|
+
it("error DISETEL ⇒ pesannya MUNCUL, dan isinya menyebut biaya", () => {
|
|
73
|
+
renderWith({ costs: PESAN });
|
|
74
|
+
const el = screen.getByText(PESAN);
|
|
75
|
+
expect(el).toBeTruthy();
|
|
76
|
+
// 🔴 Diasersi ISINYA, bukan "pesannya berubah". Generik yang diganti
|
|
77
|
+
// generik lain akan lulus asersi "bukan lagi Check required fields"
|
|
78
|
+
// sambil nol menutup AC-nya.
|
|
79
|
+
expect(el.textContent).toContain("baris biaya");
|
|
80
|
+
// Bentuk yang sama dgn DetailModalField: role=alert supaya pembaca layar
|
|
81
|
+
// mengumumkannya, bukan sekadar teks merah.
|
|
82
|
+
expect(el.getAttribute("role")).toBe("alert");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ARAH 2 — dan ini yang menjaga treasury: perubahan yang cuma dapat MENAMBAH
|
|
86
|
+
// nol dapat meregresi. Formulir yang nol menyetel error nol boleh melihat
|
|
87
|
+
// apa pun yang baru.
|
|
88
|
+
it("error KOSONG ⇒ NOL ada yang muncul, dan nol ada elemen alert", () => {
|
|
89
|
+
const { container } = renderWith({});
|
|
90
|
+
expect(screen.queryByText(PESAN)).toBeNull();
|
|
91
|
+
expect(alertsBerteks(container).length).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// KONTROL POSITIF utk instrumen: tabelnya memang dirender di kedua keadaan,
|
|
95
|
+
// jadi "nol ada alert" di atas berarti "nol ada galat", bukan "nol ada apa-apa".
|
|
96
|
+
it("KONTROL: tabelnya dirender di KEDUA keadaan", () => {
|
|
97
|
+
const a = renderWith({ costs: PESAN });
|
|
98
|
+
expect(a.container.querySelector("table")).toBeTruthy();
|
|
99
|
+
cleanup();
|
|
100
|
+
const b = renderWith({});
|
|
101
|
+
expect(b.container.querySelector("table")).toBeTruthy();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Medan LAIN di section yang sama nol boleh ikut menampilkan galat medan ini.
|
|
105
|
+
it("galat detail-rows nol bocor ke medan lain", () => {
|
|
106
|
+
const dua: FormSection<D> = {
|
|
107
|
+
...section,
|
|
108
|
+
fields: [
|
|
109
|
+
...section.fields,
|
|
110
|
+
{ key: "catatan", label: "Catatan", type: "text", span: 6 },
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
const { container } = render(
|
|
114
|
+
<SchemaFormRenderer<D>
|
|
115
|
+
mode="create"
|
|
116
|
+
value={{ costs: [], catatan: "" }}
|
|
117
|
+
sections={[dua]}
|
|
118
|
+
errors={{ costs: PESAN }}
|
|
119
|
+
onChange={() => {}}
|
|
120
|
+
/>,
|
|
121
|
+
);
|
|
122
|
+
expect(alertsBerteks(container).length).toBe(1);
|
|
123
|
+
expect(alertsBerteks(container)[0].textContent).toBe(PESAN);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import { afterEach } from "vitest";
|
|
4
|
+
import { SchemaFormRenderer } from "./form-renderer";
|
|
5
|
+
import type { FormSection } from "./form-types";
|
|
6
|
+
|
|
7
|
+
afterEach(cleanup);
|
|
8
|
+
|
|
9
|
+
type D = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
// Medan `detail-rows` MENAMPILKAN baris yang sudah ada — dan sampai berkas ini,
|
|
12
|
+
// NOL ADA satu uji pun di paket ini yang mengasersinya.
|
|
13
|
+
//
|
|
14
|
+
// ── Celah yang ditutup ───────────────────────────────────────────────────────
|
|
15
|
+
//
|
|
16
|
+
// Satu-satunya uji yang pernah merender `detail-rows` dengan nilai TAK-KOSONG
|
|
17
|
+
// adalah `detail-modal-field.test.tsx:444` (`value={{ limits: [{kode:
|
|
18
|
+
// "CASH_IN"}] }}`), dan asersinya:
|
|
19
|
+
//
|
|
20
|
+
// getByText("Limit Transaksi") <- LABEL medannya
|
|
21
|
+
// getByRole("button", {name:"Tambah Ahli Waris"}) <- tombol medan LAIN
|
|
22
|
+
//
|
|
23
|
+
// `CASH_IN` sendiri nol pernah disentuh. Uji itu jujur — ia berjudul "tetap
|
|
24
|
+
// jalan berdampingan" dan memang mengukur koeksistensi. Yang salah adalah nol
|
|
25
|
+
// ada yang lain. `detail-rows-invalid-text.test.tsx` (2026-09-06) juga nol
|
|
26
|
+
// menutupnya: ia merender `value={{ costs: [] }}` — nol baris.
|
|
27
|
+
//
|
|
28
|
+
// ── Kenapa kelas ini mahal ───────────────────────────────────────────────────
|
|
29
|
+
//
|
|
30
|
+
// form-renderer.tsx:623
|
|
31
|
+
//
|
|
32
|
+
// const rows = Array.isArray(rawValue) ? rawValue : [];
|
|
33
|
+
//
|
|
34
|
+
// Fallback `[]` itu DIAM. Dan setiap konsumen `detail-rows` di estate ini
|
|
35
|
+
// menulis dengan semantik GANTI-SEMUA — backend menghapus seluruh anak lalu
|
|
36
|
+
// menyisipkan ulang apa yang dikirim. Gabungannya:
|
|
37
|
+
//
|
|
38
|
+
// nilai non-array (atau nol ter-bind) ⇒ tabel render KOSONG
|
|
39
|
+
// ⇒ Save mengirim []
|
|
40
|
+
// ⇒ ganti-semua MENGHAPUS SEMUANYA
|
|
41
|
+
// ⇒ dan layarnya SUKSES
|
|
42
|
+
//
|
|
43
|
+
// ── Siapa yang dijaga berkas ini ─────────────────────────────────────────────
|
|
44
|
+
//
|
|
45
|
+
// Uji yang nol menyebut konsumennya akan dihapus oleh orang yang mengira ia
|
|
46
|
+
// mengukur satu instans. Keempatnya:
|
|
47
|
+
//
|
|
48
|
+
// holidays bos7-enterprise calendar/_schema.ts (DeleteCalendarHolidays + Insert)
|
|
49
|
+
// costs bos7-financing produk pembiayaan (2 layar)
|
|
50
|
+
// items bos7-accounting controlling/budgets (DeleteBudgetItemsByBudget + Insert)
|
|
51
|
+
// bos7-treasury journal-template, pof-account-group
|
|
52
|
+
// mappings bos7-treasury pof
|
|
53
|
+
//
|
|
54
|
+
// ⚠️ `detail-modal` (form-renderer.tsx:737) punya fallback `[]` yang IDENTIK dan
|
|
55
|
+
// tetap tanpa penjaga. Di luar lingkup berkas ini — sengaja disebut, bukan
|
|
56
|
+
// diam-diam dilewati.
|
|
57
|
+
|
|
58
|
+
const BARIS = [
|
|
59
|
+
{ description: "Idul Fitri", holiday_type: "NATIONAL" },
|
|
60
|
+
{ description: "Natal", holiday_type: "NATIONAL" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const section: FormSection<D> = {
|
|
64
|
+
title: "Hari Libur",
|
|
65
|
+
fields: [
|
|
66
|
+
{
|
|
67
|
+
key: "holidays",
|
|
68
|
+
label: "Daftar Hari Libur",
|
|
69
|
+
type: "detail-rows",
|
|
70
|
+
span: 12,
|
|
71
|
+
detailRows: {
|
|
72
|
+
columns: [
|
|
73
|
+
{ field: "description", header: "Keterangan", columnType: { type: "text" } },
|
|
74
|
+
{ field: "holiday_type", header: "Tipe", columnType: { type: "text" } },
|
|
75
|
+
],
|
|
76
|
+
newRowFactory: () => ({ description: "", holiday_type: "" }),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function renderRows(rows: unknown, mode: "edit" | "view" = "edit") {
|
|
83
|
+
return render(
|
|
84
|
+
<SchemaFormRenderer<D>
|
|
85
|
+
mode={mode}
|
|
86
|
+
value={{ holidays: rows }}
|
|
87
|
+
sections={[section]}
|
|
88
|
+
onChange={() => {}}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Sel-sel `<td>` sebuah tabel sebagai teks — populasi yang pertanyaannya maksud. */
|
|
94
|
+
function selTabel(root: HTMLElement): string[] {
|
|
95
|
+
return Array.from(root.querySelectorAll("tbody td")).map((td) =>
|
|
96
|
+
(td.textContent ?? "").trim(),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe("detail-rows menampilkan baris yang sudah ada", () => {
|
|
101
|
+
// ── ARAH 1 — yang merah kalau perendernya berhenti mem-bind nilainya.
|
|
102
|
+
//
|
|
103
|
+
// 🔴 Diasersi ISI SELNYA, bukan JUMLAH BARIS. "2 baris" dapat dipenuhi dua
|
|
104
|
+
// baris KOSONG — tepat kelas yang sama dengan uji lama yang mengasersi label,
|
|
105
|
+
// satu tingkat lebih halus.
|
|
106
|
+
it("dua baris terisi ⇒ isi tiap selnya muncul", () => {
|
|
107
|
+
const { container } = renderRows(BARIS);
|
|
108
|
+
|
|
109
|
+
// Nilai yang UNIK per baris: membuktikan KEDUANYA dirender, bukan satu
|
|
110
|
+
// baris yang kebetulan cocok dua kali.
|
|
111
|
+
expect(screen.getByText("Idul Fitri")).toBeTruthy();
|
|
112
|
+
expect(screen.getByText("Natal")).toBeTruthy();
|
|
113
|
+
|
|
114
|
+
// Kolom kedua sengaja bernilai SAMA di kedua baris — jadi ia hanya lulus
|
|
115
|
+
// kalau perendernya benar-benar mengulang per baris.
|
|
116
|
+
expect(screen.getAllByText("NATIONAL").length).toBe(2);
|
|
117
|
+
|
|
118
|
+
// Dan keduanya harus duduk di <tr> yang BERBEDA. Tanpa ini, sebuah
|
|
119
|
+
// perender yang menumpuk semua nilai di satu baris tetap lulus di atas.
|
|
120
|
+
const barisIdul = screen.getByText("Idul Fitri").closest("tr");
|
|
121
|
+
const barisNatal = screen.getByText("Natal").closest("tr");
|
|
122
|
+
expect(barisIdul).toBeTruthy();
|
|
123
|
+
expect(barisNatal).toBeTruthy();
|
|
124
|
+
expect(barisIdul).not.toBe(barisNatal);
|
|
125
|
+
|
|
126
|
+
expect(container.querySelectorAll("tbody tr").length).toBe(2);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── ARAH 2 — KONTROL yang membedakan "menampilkan yang ada" dari
|
|
130
|
+
// "selalu menampilkan sesuatu".
|
|
131
|
+
//
|
|
132
|
+
// Tanpa arah ini, sebuah perender yang MENGARANG baris (mis. selalu
|
|
133
|
+
// menyisipkan satu baris kosong dari newRowFactory) akan lulus arah 1 dan
|
|
134
|
+
// nol pernah merah. Dan nol-baris BUKAN kegagalan — ia keadaan sah sebuah
|
|
135
|
+
// kalender tanpa hari libur.
|
|
136
|
+
it("KONTROL: nol baris ⇒ nol sel, dan itu BUKAN kegagalan", () => {
|
|
137
|
+
const { container } = renderRows([]);
|
|
138
|
+
expect(selTabel(container)).toEqual([]);
|
|
139
|
+
expect(container.querySelectorAll("tbody tr").length).toBe(0);
|
|
140
|
+
expect(screen.queryByText("Idul Fitri")).toBeNull();
|
|
141
|
+
// Tabelnya sendiri TETAP dirender — jadi "nol ada sel" di atas berarti
|
|
142
|
+
// "nol ada baris", bukan "medannya nol ada".
|
|
143
|
+
expect(container.querySelector("table")).toBeTruthy();
|
|
144
|
+
expect(screen.getByText("Keterangan")).toBeTruthy(); // header kolom
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ── ARAH 3 — jalur yang mengubah fallback diam jadi TERUKUR.
|
|
148
|
+
//
|
|
149
|
+
// `Array.isArray(rawValue) ? rawValue : []` memperlakukan nilai non-array
|
|
150
|
+
// sebagai nol-baris tanpa satu galat pun. Itu perilaku yang DISENGAJA
|
|
151
|
+
// (perender nol boleh melempar atas data cacat), tapi ia harus punya uji —
|
|
152
|
+
// kalau suatu hari ia berubah jadi melempar, atau jadi merender sesuatu,
|
|
153
|
+
// seseorang harus melihatnya di sini.
|
|
154
|
+
it("nilai non-array diperlakukan sebagai nol-baris, tanpa melempar", () => {
|
|
155
|
+
for (const buruk of [undefined, null, "Idul Fitri", 42, { description: "Idul Fitri" }]) {
|
|
156
|
+
const { container } = renderRows(buruk);
|
|
157
|
+
expect(container.querySelector("table")).toBeTruthy();
|
|
158
|
+
expect(container.querySelectorAll("tbody tr").length).toBe(0);
|
|
159
|
+
// 🔴 Ini kalimat yang harus dibaca orang berikutnya: string
|
|
160
|
+
// "Idul Fitri" dan objek ber-`description` TIDAK muncul. Digabung
|
|
161
|
+
// ganti-semua, keadaan ini = hapus senyap.
|
|
162
|
+
expect(screen.queryByText("Idul Fitri")).toBeNull();
|
|
163
|
+
cleanup();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── ARAH 4 — layar `view` memakai <EditableTable> yang SAMA (readOnly).
|
|
168
|
+
// Kalau binding-nya putus hanya di jalur readonly, checker melihat tabel
|
|
169
|
+
// kosong pada layar approval sementara datanya utuh.
|
|
170
|
+
it("mode view: baris tetap muncul (readOnly, bukan kosong)", () => {
|
|
171
|
+
const { container } = renderRows(BARIS, "view");
|
|
172
|
+
expect(screen.getByText("Idul Fitri")).toBeTruthy();
|
|
173
|
+
expect(screen.getByText("Natal")).toBeTruthy();
|
|
174
|
+
expect(container.querySelectorAll("tbody tr").length).toBe(2);
|
|
175
|
+
});
|
|
176
|
+
});
|
package/src/form-renderer.tsx
CHANGED
|
@@ -186,12 +186,21 @@ function FieldShell({
|
|
|
186
186
|
required,
|
|
187
187
|
children,
|
|
188
188
|
helperText,
|
|
189
|
+
invalidText,
|
|
189
190
|
htmlFor,
|
|
190
191
|
}: {
|
|
191
192
|
label: ReactNode;
|
|
192
193
|
required?: boolean;
|
|
193
194
|
children: ReactNode;
|
|
194
195
|
helperText?: ReactNode;
|
|
196
|
+
/**
|
|
197
|
+
* Galat tingkat-medan. Bentuknya SAMA PERSIS dgn DetailModalField
|
|
198
|
+
* (detail-modal-field.tsx:277) — role="alert", kelas helper, token warna
|
|
199
|
+
* galat, dirender DI ATAS helperText. Tipe medan itu SUDAH menampilkan
|
|
200
|
+
* galatnya dan `detail-rows` NOL; ini menyamakan, bukan menciptakan bentuk
|
|
201
|
+
* kedua.
|
|
202
|
+
*/
|
|
203
|
+
invalidText?: ReactNode;
|
|
195
204
|
htmlFor?: string;
|
|
196
205
|
}) {
|
|
197
206
|
const labelStyle = { color: "var(--cds-text-secondary, #525252)", display: "flex", alignItems: "center", gap: "0.25rem" } as const;
|
|
@@ -211,6 +220,15 @@ function FieldShell({
|
|
|
211
220
|
<div className={UI7_FORM_VISUAL_CLASSNAMES.label} style={labelStyle}>{labelContent}</div>
|
|
212
221
|
)}
|
|
213
222
|
{children}
|
|
223
|
+
{invalidText ? (
|
|
224
|
+
<div
|
|
225
|
+
role="alert"
|
|
226
|
+
className={UI7_FORM_VISUAL_CLASSNAMES.helper}
|
|
227
|
+
style={{ color: "var(--cds-text-error, #da1e28)" }}
|
|
228
|
+
>
|
|
229
|
+
{invalidText}
|
|
230
|
+
</div>
|
|
231
|
+
) : null}
|
|
214
232
|
{helperText ? (
|
|
215
233
|
<div className={UI7_FORM_VISUAL_CLASSNAMES.helper}>{helperText}</div>
|
|
216
234
|
) : null}
|
|
@@ -622,6 +640,13 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
|
|
|
622
640
|
label={field.label}
|
|
623
641
|
required={effectiveRequired}
|
|
624
642
|
helperText={field.helperText}
|
|
643
|
+
// 🔴 `detail-rows` NOL meneruskan ini sampai 2026-09-06, sementara
|
|
644
|
+
// saudaranya `detail-modal` SUDAH. Akibatnya aturan validasi yang
|
|
645
|
+
// keyed ke medan detail-rows DIHITUNG, MENAHAN SUBMIT, lalu NOL
|
|
646
|
+
// PERNAH DITAMPILKAN — petugas melihat spanduk generik "Check
|
|
647
|
+
// required fields" dan tabel yang bersih. Terukur pada Produk
|
|
648
|
+
// Pembiayaan (errors.costs, bos7-financing#200).
|
|
649
|
+
invalidText={invalidText}
|
|
625
650
|
>
|
|
626
651
|
<EditableTable
|
|
627
652
|
columns={detail.columns}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { SHARED_DICTIONARIES } from "@isi-ui7/i18n";
|
|
3
|
+
|
|
4
|
+
// Butir 1.8 (DG-00): satu medan boolean menampilkan DUA kosakata di modul yang
|
|
5
|
+
// sama, karena dua widget membaca dua kunci —
|
|
6
|
+
//
|
|
7
|
+
// LIST is_printed -> "Ya" / "Tidak" (checkbox -> boolYes/boolNo)
|
|
8
|
+
// VIEW is_printed -> "On" / "Off" (toggle -> toggleOn/toggleOff)
|
|
9
|
+
//
|
|
10
|
+
// Locale bawaan estate `id`, dan "Ya"/"Tidak" SUDAH ada di kamus itu, nol
|
|
11
|
+
// terpakai oleh toggle.
|
|
12
|
+
//
|
|
13
|
+
// ⚠️ Kaidah user (ditekan 4x): "sel menampilkan DESKRIPSI, bukan nilai
|
|
14
|
+
// mentah". "On" memenuhi hurufnya dan meleset dari maksudnya — deskripsi yang
|
|
15
|
+
// berbahasa lain dari layarnya nol lebih terbaca dari `true`.
|
|
16
|
+
describe("kamus toggle — bahasa mengikuti locale-nya", () => {
|
|
17
|
+
const id = SHARED_DICTIONARIES.id;
|
|
18
|
+
const en = SHARED_DICTIONARIES.en;
|
|
19
|
+
|
|
20
|
+
it("KONTROL POSITIF: kedua kamus terbaca dan berisi", () => {
|
|
21
|
+
// Tanpa ini, setiap asersi di bawah bisa lulus atas objek kosong.
|
|
22
|
+
expect(Object.keys(id).length).toBeGreaterThan(20);
|
|
23
|
+
expect(Object.keys(en).length).toBeGreaterThan(20);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("ID: toggle memakai Ya/Tidak, bukan On/Off", () => {
|
|
27
|
+
expect(id["bos7.toggleOn"]).toBe("Ya");
|
|
28
|
+
expect(id["bos7.toggleOff"]).toBe("Tidak");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("KONTROL (+): boolYes/boolNo NOL berubah — LIST tetap Ya/Tidak", () => {
|
|
32
|
+
// Perbaikan ini menyentuh toggle saja. Kalau checkbox ikut berubah, yang
|
|
33
|
+
// terjadi bukan penyeragaman melainkan kerusakan di tempat lain.
|
|
34
|
+
expect(id["bos7.boolYes"]).toBe("Ya");
|
|
35
|
+
expect(id["bos7.boolNo"]).toBe("Tidak");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("KONTROL (−): kamus EN TETAP On/Off — di sana ia benar", () => {
|
|
39
|
+
expect(en["bos7.toggleOn"]).toBe("On");
|
|
40
|
+
expect(en["bos7.toggleOff"]).toBe("Off");
|
|
41
|
+
expect(en["bos7.boolYes"]).toBe("Yes");
|
|
42
|
+
expect(en["bos7.boolNo"]).toBe("No");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("ID nol memuat kata Inggris On/Off di keempat kunci boolean", () => {
|
|
46
|
+
// Asersi yang MEMISAHKAN: ia merah pada keadaan lama, dan tetap merah
|
|
47
|
+
// kalau seseorang "memperbaiki" satu kunci dan melupakan pasangannya.
|
|
48
|
+
for (const k of ["bos7.toggleOn", "bos7.toggleOff", "bos7.boolYes", "bos7.boolNo"]) {
|
|
49
|
+
expect(["On", "Off", "Yes", "No"], `${k} berbahasa Inggris di kamus ID`).not.toContain(id[k]);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/i18n.tsx
CHANGED
|
@@ -81,8 +81,14 @@ export function useBosSharedI18n(): BosSharedLabels {
|
|
|
81
81
|
|
|
82
82
|
boolYes: t("bos7.boolYes", "Ya"),
|
|
83
83
|
boolNo: t("bos7.boolNo", "Tidak"),
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// Fallback Indonesia, sama dengan boolYes/boolNo dua baris di atas.
|
|
85
|
+
//
|
|
86
|
+
// 🔴 Sebelumnya "On"/"Off" — fallback berbahasa INGGRIS di blok yang tiap
|
|
87
|
+
// baris lainnya berbahasa Indonesia. Ia cacat KEDUA dari temuan yang
|
|
88
|
+
// sama: memperbaiki kamus saja meninggalkan jalur ini, dan jalur ini yang
|
|
89
|
+
// dipakai kapan pun kuncinya nol ada di kamus locale aktif.
|
|
90
|
+
toggleOn: t("bos7.toggleOn", "Ya"),
|
|
91
|
+
toggleOff: t("bos7.toggleOff", "Tidak"),
|
|
86
92
|
emptyValue: t("bos7.emptyValue", "—"),
|
|
87
93
|
showInactive: t("bos7.showInactive", "Tampilkan Nonaktif"),
|
|
88
94
|
|