@isi-ui7/bos7-shared 0.4.6 → 0.5.0
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/i18n.d.ts +2 -0
- package/dist/{index-C_114Ysm.js → index-CvKOQKt5.js} +31 -5
- package/dist/{index.es-B-MluT1E.js → index.es-BAzO0Iw8.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{jspdf.es.min-C5VEsLZd.js → jspdf.es.min-B3CTSFr_.js} +1 -1
- package/package.json +7 -7
- package/src/crud-components.tsx +67 -28
- package/src/crud-filter-fields.test.tsx +77 -8
- package/src/i18n-toggle-locale.test.ts +52 -0
- package/src/i18n.tsx +12 -2
package/dist/i18n.d.ts
CHANGED
|
@@ -27391,7 +27391,7 @@ function ba(M, L) {
|
|
|
27391
27391
|
return String(pr(M, L));
|
|
27392
27392
|
}
|
|
27393
27393
|
async function Io(M, L, r) {
|
|
27394
|
-
const { default: x } = await import("./jspdf.es.min-
|
|
27394
|
+
const { default: x } = await import("./jspdf.es.min-B3CTSFr_.js").then((o) => o.j), { default: w } = await import("./jspdf.plugin.autotable-DcntYaes.js"), c = new x({ orientation: "landscape" });
|
|
27395
27395
|
c.setFontSize(14), c.text(r, 14, 15), c.setFontSize(10), w(c, {
|
|
27396
27396
|
head: [L.map((o) => o.title)],
|
|
27397
27397
|
body: M.map((o) => L.map((a) => ba(o[a.field], a.displayFormat))),
|
|
@@ -27697,8 +27697,26 @@ const vr = "id", _i = "core7_locale", Ea = {
|
|
|
27697
27697
|
"bos7.rulePhone": "No HP tidak valid",
|
|
27698
27698
|
"bos7.boolYes": "Ya",
|
|
27699
27699
|
"bos7.boolNo": "Tidak",
|
|
27700
|
-
"
|
|
27701
|
-
|
|
27700
|
+
// 🔴 "Ya"/"Tidak", BUKAN "On"/"Off". Kamus ini Indonesia, dan locale
|
|
27701
|
+
// bawaan estate `id`.
|
|
27702
|
+
//
|
|
27703
|
+
// Terukur DG-00, butir 1.8: satu medan boolean menampilkan DUA kosakata di
|
|
27704
|
+
// modul yang SAMA —
|
|
27705
|
+
//
|
|
27706
|
+
// LIST is_printed -> "Ya" / "Tidak" (checkbox -> boolYes/boolNo)
|
|
27707
|
+
// VIEW is_printed -> "On" / "Off" (toggle -> toggleOn/toggleOff)
|
|
27708
|
+
//
|
|
27709
|
+
// Satu konsep boolean, dua widget, dua bahasa. Dan "Ya"/"Tidak" SUDAH ada
|
|
27710
|
+
// di kamus ini, nol terpakai oleh toggle.
|
|
27711
|
+
//
|
|
27712
|
+
// ⚠️ Kaidah user (ditekan 4x) berbunyi "sel menampilkan DESKRIPSI, bukan
|
|
27713
|
+
// nilai mentah". "On" memenuhi hurufnya dan meleset dari maksudnya:
|
|
27714
|
+
// deskripsi yang berbahasa lain dari layarnya nol lebih terbaca dari
|
|
27715
|
+
// `true`.
|
|
27716
|
+
//
|
|
27717
|
+
// EN_DICTIONARY TETAP "On"/"Off" — di sana ia benar.
|
|
27718
|
+
"bos7.toggleOn": "Ya",
|
|
27719
|
+
"bos7.toggleOff": "Tidak",
|
|
27702
27720
|
"bos7.emptyValue": "—",
|
|
27703
27721
|
"bos7.loading": "Memuat data...",
|
|
27704
27722
|
"bos7.saving": "Memproses...",
|
|
@@ -28635,10 +28653,18 @@ function tl() {
|
|
|
28635
28653
|
rulePhone: M("bos7.rulePhone", "No HP tidak valid"),
|
|
28636
28654
|
boolYes: M("bos7.boolYes", "Ya"),
|
|
28637
28655
|
boolNo: M("bos7.boolNo", "Tidak"),
|
|
28638
|
-
|
|
28639
|
-
|
|
28656
|
+
// Fallback Indonesia, sama dengan boolYes/boolNo dua baris di atas.
|
|
28657
|
+
//
|
|
28658
|
+
// 🔴 Sebelumnya "On"/"Off" — fallback berbahasa INGGRIS di blok yang tiap
|
|
28659
|
+
// baris lainnya berbahasa Indonesia. Ia cacat KEDUA dari temuan yang
|
|
28660
|
+
// sama: memperbaiki kamus saja meninggalkan jalur ini, dan jalur ini yang
|
|
28661
|
+
// dipakai kapan pun kuncinya nol ada di kamus locale aktif.
|
|
28662
|
+
toggleOn: M("bos7.toggleOn", "Ya"),
|
|
28663
|
+
toggleOff: M("bos7.toggleOff", "Tidak"),
|
|
28640
28664
|
emptyValue: M("bos7.emptyValue", "—"),
|
|
28641
28665
|
showInactive: M("bos7.showInactive", "Tampilkan Nonaktif"),
|
|
28666
|
+
filterApply: M("bos7.filterApply", "Cari"),
|
|
28667
|
+
filterReset: M("bos7.filterReset", "Reset"),
|
|
28642
28668
|
loading: M("bos7.loading", "Memuat data..."),
|
|
28643
28669
|
saving: M("bos7.saving", "Memproses..."),
|
|
28644
28670
|
errorTitle: M("bos7.errorTitle", "Gagal"),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as Ke, g as Do } from "./index-
|
|
2
|
-
import { _ as Xa } from "./jspdf.es.min-
|
|
1
|
+
import { c as Ke, g as Do } from "./index-CvKOQKt5.js";
|
|
2
|
+
import { _ as Xa } from "./jspdf.es.min-B3CTSFr_.js";
|
|
3
3
|
var vt = function(a) {
|
|
4
4
|
return a && a.Math === Math && a;
|
|
5
5
|
}, _ = (
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { m as s, a9 as o, A as t, Y as r, R as i, O as n, l, F as c, G as d, E as u, P as S, a3 as h, a2 as A, a1 as T, a0 as k, af as C, K as _, H as O, I as U, J as g, n as N, ah as P, ag as m, Q as E, i as F, D as p, f, y as I, S as R, j as x, V as v, a8 as y, o as L, h as b, L as B, M as D, T as M, W as H, C as V, e as W, z as q, a7 as K, a5 as w, a6 as z, v as G, aa as Q, ab as X, ac as Y, ad as j, k as J, U as Z, w as $, a4 as aa, Z as ea, _ as sa, p as oa, a as ta, N as ra, q as ia, r as na, $ as la, t as ca, ae as da, s as ua, B as Sa, x as ha, b as Aa, u as Ta, ai as ka, X as Ca, d as _a } from "./index-
|
|
1
|
+
import { m as s, a9 as o, A as t, Y as r, R as i, O as n, l, F as c, G as d, E as u, P as S, a3 as h, a2 as A, a1 as T, a0 as k, af as C, K as _, H as O, I as U, J as g, n as N, ah as P, ag as m, Q as E, i as F, D as p, f, y as I, S as R, j as x, V as v, a8 as y, o as L, h as b, L as B, M as D, T as M, W as H, C as V, e as W, z as q, a7 as K, a5 as w, a6 as z, v as G, aa as Q, ab as X, ac as Y, ad as j, k as J, U as Z, w as $, a4 as aa, Z as ea, _ as sa, p as oa, a as ta, N as ra, q as ia, r as na, $ as la, t as ca, ae as da, s as ua, B as Sa, x as ha, b as Aa, u as Ta, ai as ka, X as Ca, d as _a } from "./index-CvKOQKt5.js";
|
|
2
2
|
export {
|
|
3
3
|
s as AUTH7_SCOPES,
|
|
4
4
|
o as AppShellLayout,
|
|
@@ -9216,7 +9216,7 @@ function(n) {
|
|
|
9216
9216
|
var d = h.getContext("2d");
|
|
9217
9217
|
d.fillStyle = "#fff", d.fillRect(0, 0, h.width, h.height);
|
|
9218
9218
|
var m = { ignoreMouse: !0, ignoreAnimation: !0, ignoreDimensions: !0 }, _ = this;
|
|
9219
|
-
return (Kt.canvg ? Promise.resolve(Kt.canvg) : import("./index.es-
|
|
9219
|
+
return (Kt.canvg ? Promise.resolve(Kt.canvg) : import("./index.es-BAzO0Iw8.js")).catch(function(k) {
|
|
9220
9220
|
return Promise.reject(new Error("Could not load canvg: " + k));
|
|
9221
9221
|
}).then(function(k) {
|
|
9222
9222
|
return k.default ? k.default : k;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isi-ui7/bos7-shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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.18",
|
|
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.18",
|
|
110
110
|
"@isi-ui7/data-table": "0.2.5",
|
|
111
111
|
"@isi-ui7/editable-table": "0.2.3",
|
|
112
|
-
"@isi-ui7/i18n": "0.2.
|
|
113
|
-
"@isi-ui7/modal-manager": "0.2.2",
|
|
112
|
+
"@isi-ui7/i18n": "0.2.4",
|
|
114
113
|
"@isi-ui7/lookup-input": "0.2.4",
|
|
115
114
|
"@isi-ui7/realtime": "0.2.3",
|
|
116
|
-
"@isi-ui7/ui-shell": "0.2.8"
|
|
115
|
+
"@isi-ui7/ui-shell": "0.2.8",
|
|
116
|
+
"@isi-ui7/modal-manager": "0.2.2"
|
|
117
117
|
},
|
|
118
118
|
"dependencies": {
|
|
119
119
|
"@carbon/icons-react": "^11.71.0",
|
package/src/crud-components.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { type ReactNode, useCallback, useEffect,
|
|
4
|
-
import { Add } from "@carbon/icons-react";
|
|
3
|
+
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { Add, Search } from "@carbon/icons-react";
|
|
5
5
|
import {
|
|
6
6
|
Button,
|
|
7
7
|
ComposedModal,
|
|
@@ -88,36 +88,51 @@ export function SharedCrudListPage({
|
|
|
88
88
|
// Two states per field, not one: `filterFieldValues` is what the widget
|
|
89
89
|
// shows (updates on every keystroke, so typing feels immediate);
|
|
90
90
|
// `filterFieldCommitted` is what actually reaches additionalParams (and
|
|
91
|
-
// therefore triggers a refetch).
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
91
|
+
// therefore triggers a refetch).
|
|
92
|
+
//
|
|
93
|
+
// ── KEPUTUSAN LAMA, DIGANTI 2026-09-06 — dibiarkan tertulis, bukan dihapus.
|
|
94
|
+
//
|
|
95
|
+
// Semula: `select` commit SEGERA, `text` commit DEBOUNCED 400 ms, mengikuti
|
|
96
|
+
// preseden kotak cari OffsetDataTable. Itu keputusan yang SENGAJA, bukan
|
|
97
|
+
// kelalaian — dan komentarnya dipertahankan supaya orang berikutnya nol
|
|
98
|
+
// mengira debounce nol pernah dipikirkan lalu memasangnya kembali.
|
|
99
|
+
//
|
|
100
|
+
// ── YANG BERLAKU SEKARANG: terapkan MANUAL lewat tombol `Cari`.
|
|
101
|
+
//
|
|
102
|
+
// Diputuskan user 2026-09-06 sesudah melihat layarnya sendiri: "ada tombol
|
|
103
|
+
// search dan reset filter". Itu pernyataan bagaimana layar ini dioperasikan,
|
|
104
|
+
// bukan preferensi kosmetik.
|
|
105
|
+
//
|
|
106
|
+
// Dan alasan teknisnya BERBALIK ARAH sejak catatan lama ditulis: panel ini
|
|
107
|
+
// kini punya SEPULUH medan (Putaran 2, 2026-09-03). Terapkan-otomatis berarti
|
|
108
|
+
// tiap ketukan memicu kueri lintas sepuluh medan. Debounce dipilih saat
|
|
109
|
+
// panelnya kecil; panelnya sudah nol kecil.
|
|
110
|
+
//
|
|
111
|
+
// ⚠️ Kotak `Search` toolbar DataTable NOL disentuh — ia sumbu BERBEDA
|
|
112
|
+
// (full_name ATAU cif_number lewat AltSearchColumns), dan auto-apply
|
|
113
|
+
// ber-debounce di sana tetap benar.
|
|
100
114
|
const [filterFieldValues, setFilterFieldValues] = useState<Record<string, string>>({});
|
|
101
115
|
const [filterFieldCommitted, setFilterFieldCommitted] = useState<Record<string, string>>({});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const timers = filterFieldTimers.current;
|
|
105
|
-
return () => {
|
|
106
|
-
for (const t of Object.values(timers)) clearTimeout(t);
|
|
107
|
-
};
|
|
108
|
-
}, []);
|
|
116
|
+
// Mengetik/memilih hanya menggerakkan yang TAMPIL. Nol ada yang mencapai
|
|
117
|
+
// additionalParams sampai `Cari` diklik — itu seluruh isi keputusan di atas.
|
|
109
118
|
const setFilterFieldValue = useCallback((field: CrudFilterFieldDef, value: string) => {
|
|
110
119
|
setFilterFieldValues((prev) => ({ ...prev, [field.param]: value }));
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
}, []);
|
|
121
|
+
// Disalin, bukan dirujuk: `filterFieldCommitted` harus jadi POTRET nilai saat
|
|
122
|
+
// Cari diklik. Membaginya dgn `filterFieldValues` akan membuat ketukan
|
|
123
|
+
// berikutnya ikut terkirim tanpa ada yang mengklik apa pun.
|
|
124
|
+
const terapkanFilter = useCallback(() => {
|
|
125
|
+
setFilterFieldValues((v) => {
|
|
126
|
+
setFilterFieldCommitted({ ...v });
|
|
127
|
+
return v;
|
|
128
|
+
});
|
|
129
|
+
}, []);
|
|
130
|
+
// Mengosongkan KEDUANYA. Mengosongkan yang tampil saja akan menyisakan filter
|
|
131
|
+
// yang masih berlaku di balik layar sementara kotaknya terlihat kosong —
|
|
132
|
+
// pengguna melihat "nol ada filter" dan hasilnya tetap tersaring.
|
|
133
|
+
const resetFilter = useCallback(() => {
|
|
134
|
+
setFilterFieldValues({});
|
|
135
|
+
setFilterFieldCommitted({});
|
|
121
136
|
}, []);
|
|
122
137
|
const hasFilterFields = Boolean(schema.filterFields?.length);
|
|
123
138
|
|
|
@@ -207,10 +222,34 @@ export function SharedCrudListPage({
|
|
|
207
222
|
placeholder={f.placeholder ?? f.label}
|
|
208
223
|
value={filterFieldValues[f.param] ?? ""}
|
|
209
224
|
onChange={(e) => setFilterFieldValue(f, e.target.value)}
|
|
225
|
+
onKeyDown={(e) => {
|
|
226
|
+
if (e.key === "Enter") terapkanFilter();
|
|
227
|
+
}}
|
|
210
228
|
/>
|
|
211
229
|
</div>
|
|
212
230
|
),
|
|
213
231
|
)}
|
|
232
|
+
{hasFilterFields ? (
|
|
233
|
+
<>
|
|
234
|
+
<Button
|
|
235
|
+
kind="primary"
|
|
236
|
+
size="sm"
|
|
237
|
+
renderIcon={Search}
|
|
238
|
+
onClick={terapkanFilter}
|
|
239
|
+
data-testid="filter-apply"
|
|
240
|
+
>
|
|
241
|
+
{labels.filterApply}
|
|
242
|
+
</Button>
|
|
243
|
+
<Button
|
|
244
|
+
kind="ghost"
|
|
245
|
+
size="sm"
|
|
246
|
+
onClick={resetFilter}
|
|
247
|
+
data-testid="filter-reset"
|
|
248
|
+
>
|
|
249
|
+
{labels.filterReset}
|
|
250
|
+
</Button>
|
|
251
|
+
</>
|
|
252
|
+
) : null}
|
|
214
253
|
{toolbarExtra}
|
|
215
254
|
{(schema.showAddButton ?? true) ? (
|
|
216
255
|
<Button renderIcon={Add} size="sm" onClick={onAdd}>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { render, screen
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
|
|
5
5
|
// K-6 (user, 2026-08-26): CrudListSchema.filterFields — N granular toolbar
|
|
@@ -46,7 +46,7 @@ describe("CrudListSchema.filterFields", () => {
|
|
|
46
46
|
expect(paramsRendered()).toEqual({});
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
it("field type:select
|
|
49
|
+
it("field type:select MENUNGGU tombol Cari (dulu: kirim SEGERA saat dipilih)", async () => {
|
|
50
50
|
const user = userEvent.setup();
|
|
51
51
|
render(
|
|
52
52
|
<SharedCrudListPage
|
|
@@ -70,10 +70,16 @@ describe("CrudListSchema.filterFields", () => {
|
|
|
70
70
|
const select = screen.getByLabelText("Jenis Identitas") as HTMLSelectElement;
|
|
71
71
|
await user.selectOptions(select, "KTP");
|
|
72
72
|
|
|
73
|
+
// 🔑 Perilaku DIGANTI 2026-09-06 (keputusan user): memilih nol lagi
|
|
74
|
+
// mengirim apa pun. Sebelumnya baris ini berbunyi { identity_type: "KTP" }.
|
|
75
|
+
expect(paramsRendered()).toEqual({ identity_type: "" });
|
|
76
|
+
expect(select.value).toBe("KTP"); // widget tetap responsif
|
|
77
|
+
|
|
78
|
+
await user.click(screen.getByTestId("filter-apply"));
|
|
73
79
|
expect(paramsRendered()).toEqual({ identity_type: "KTP" });
|
|
74
80
|
});
|
|
75
81
|
|
|
76
|
-
it("field type:text
|
|
82
|
+
it("field type:text MENUNGGU tombol Cari (dulu: DEBOUNCE 400 ms)", async () => {
|
|
77
83
|
const user = userEvent.setup();
|
|
78
84
|
render(
|
|
79
85
|
<SharedCrudListPage
|
|
@@ -90,13 +96,69 @@ describe("CrudListSchema.filterFields", () => {
|
|
|
90
96
|
const input = screen.getByLabelText("Nama") as HTMLInputElement;
|
|
91
97
|
await user.type(input, "Budi");
|
|
92
98
|
|
|
93
|
-
// 🔑 Klaim inti: mengetik
|
|
94
|
-
|
|
95
|
-
//
|
|
99
|
+
// 🔑 Klaim inti: mengetik nol mengirim param.
|
|
100
|
+
expect(paramsRendered()).toEqual({ full_name: "" });
|
|
101
|
+
expect(input.value).toBe("Budi"); // widget tetap responsif
|
|
102
|
+
|
|
103
|
+
// 🔴 KONTROL WAKTU: dulu param mendarat sendiri sesudah debounce 400 ms.
|
|
104
|
+
// Menunggu LEBIH LAMA dari itu dan tetap kosong membuktikan auto-apply
|
|
105
|
+
// benar-benar HILANG — bukan sekadar lebih lambat.
|
|
106
|
+
await new Promise((r) => setTimeout(r, 700));
|
|
96
107
|
expect(paramsRendered()).toEqual({ full_name: "" });
|
|
97
|
-
expect(input.value).toBe("Budi"); // tapi WIDGET-nya tetap responsif
|
|
98
108
|
|
|
99
|
-
await
|
|
109
|
+
await user.click(screen.getByTestId("filter-apply"));
|
|
110
|
+
expect(paramsRendered()).toEqual({ full_name: "Budi" });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("Enter di medan teks setara mengklik Cari", async () => {
|
|
114
|
+
const user = userEvent.setup();
|
|
115
|
+
render(
|
|
116
|
+
<SharedCrudListPage
|
|
117
|
+
schema={{ ...dasar, filterFields: [{ type: "text", param: "full_name", label: "Nama" }] }}
|
|
118
|
+
onAdd={() => {}}
|
|
119
|
+
onPopupClick={() => {}}
|
|
120
|
+
/>,
|
|
121
|
+
);
|
|
122
|
+
const input = screen.getByLabelText("Nama") as HTMLInputElement;
|
|
123
|
+
await user.type(input, "Budi{Enter}");
|
|
124
|
+
expect(paramsRendered()).toEqual({ full_name: "Budi" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("Reset mengosongkan SELURUH medan — dihitung, bukan dilihat", async () => {
|
|
128
|
+
const user = userEvent.setup();
|
|
129
|
+
const banyak = Array.from({ length: 10 }, (_, i) => ({
|
|
130
|
+
type: "text" as const, param: `p${i}`, label: `Medan ${i}`,
|
|
131
|
+
}));
|
|
132
|
+
render(
|
|
133
|
+
<SharedCrudListPage
|
|
134
|
+
schema={{ ...dasar, filterFields: banyak }}
|
|
135
|
+
onAdd={() => {}}
|
|
136
|
+
onPopupClick={() => {}}
|
|
137
|
+
/>,
|
|
138
|
+
);
|
|
139
|
+
for (const f of banyak) {
|
|
140
|
+
await user.type(screen.getByLabelText(f.label) as HTMLInputElement, "x");
|
|
141
|
+
}
|
|
142
|
+
await user.click(screen.getByTestId("filter-apply"));
|
|
143
|
+
const terkirim = paramsRendered() as Record<string, string>;
|
|
144
|
+
// KONTROL POSITIF: kesepuluh benar-benar terisi lebih dulu. Tanpa ini,
|
|
145
|
+
// "Reset mengosongkan" lulus juga pada panel yang memang sudah kosong.
|
|
146
|
+
expect(Object.values(terkirim).filter((v) => v === "x")).toHaveLength(10);
|
|
147
|
+
|
|
148
|
+
await user.click(screen.getByTestId("filter-reset"));
|
|
149
|
+
const sesudah = paramsRendered() as Record<string, string>;
|
|
150
|
+
expect(Object.values(sesudah).filter((v) => v !== "")).toHaveLength(0);
|
|
151
|
+
// dan yang TAMPIL ikut kosong — mengosongkan salah satunya saja menyisakan
|
|
152
|
+
// filter yang masih berlaku di balik kotak yang terlihat bersih.
|
|
153
|
+
for (const f of banyak) {
|
|
154
|
+
expect((screen.getByLabelText(f.label) as HTMLInputElement).value).toBe("");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("tombol Cari/Reset NOL muncul saat filterFields diabaikan", () => {
|
|
159
|
+
render(<SharedCrudListPage schema={dasar} onAdd={() => {}} onPopupClick={() => {}} />);
|
|
160
|
+
expect(screen.queryByTestId("filter-apply")).toBeNull();
|
|
161
|
+
expect(screen.queryByTestId("filter-reset")).toBeNull();
|
|
100
162
|
});
|
|
101
163
|
|
|
102
164
|
it("filterSelect (lama) DAN filterFields (baru) coexist tanpa saling menimpa", async () => {
|
|
@@ -122,6 +184,10 @@ describe("CrudListSchema.filterFields", () => {
|
|
|
122
184
|
expect(paramsRendered()).toEqual({ status: "", branch_code: "" });
|
|
123
185
|
|
|
124
186
|
await user.selectOptions(screen.getByLabelText("Cabang"), "001");
|
|
187
|
+
await user.click(screen.getByTestId("filter-apply"));
|
|
188
|
+
// filterSelect (lama, `status`) NOL disentuh perubahan 2026-09-06 — ia
|
|
189
|
+
// sumbu terpisah dan tetap punya jalurnya sendiri; yang kini menunggu
|
|
190
|
+
// tombol hanyalah filterFields (`branch_code`).
|
|
125
191
|
expect(paramsRendered()).toEqual({ status: "", branch_code: "001" });
|
|
126
192
|
});
|
|
127
193
|
|
|
@@ -151,6 +217,9 @@ describe("CrudListSchema.filterFields", () => {
|
|
|
151
217
|
/>,
|
|
152
218
|
);
|
|
153
219
|
await user.selectOptions(screen.getByLabelText("Jenis Identitas"), "KTP");
|
|
220
|
+
await user.click(screen.getByTestId("filter-apply"));
|
|
221
|
+
// 🔑 Klaim yang dijaga uji ini NOL berubah: satu medan bergerak, yang lain
|
|
222
|
+
// tetap kosong. Yang berubah cuma KAPAN ia terkirim.
|
|
154
223
|
expect(paramsRendered()).toEqual({ identity_type: "KTP", branch_code: "" });
|
|
155
224
|
});
|
|
156
225
|
});
|
|
@@ -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
|
@@ -25,6 +25,8 @@ export type BosSharedLabels = {
|
|
|
25
25
|
toggleOff: string;
|
|
26
26
|
emptyValue: string;
|
|
27
27
|
showInactive: string;
|
|
28
|
+
filterApply: string;
|
|
29
|
+
filterReset: string;
|
|
28
30
|
|
|
29
31
|
loading: string;
|
|
30
32
|
saving: string;
|
|
@@ -81,10 +83,18 @@ export function useBosSharedI18n(): BosSharedLabels {
|
|
|
81
83
|
|
|
82
84
|
boolYes: t("bos7.boolYes", "Ya"),
|
|
83
85
|
boolNo: t("bos7.boolNo", "Tidak"),
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
// Fallback Indonesia, sama dengan boolYes/boolNo dua baris di atas.
|
|
87
|
+
//
|
|
88
|
+
// 🔴 Sebelumnya "On"/"Off" — fallback berbahasa INGGRIS di blok yang tiap
|
|
89
|
+
// baris lainnya berbahasa Indonesia. Ia cacat KEDUA dari temuan yang
|
|
90
|
+
// sama: memperbaiki kamus saja meninggalkan jalur ini, dan jalur ini yang
|
|
91
|
+
// dipakai kapan pun kuncinya nol ada di kamus locale aktif.
|
|
92
|
+
toggleOn: t("bos7.toggleOn", "Ya"),
|
|
93
|
+
toggleOff: t("bos7.toggleOff", "Tidak"),
|
|
86
94
|
emptyValue: t("bos7.emptyValue", "—"),
|
|
87
95
|
showInactive: t("bos7.showInactive", "Tampilkan Nonaktif"),
|
|
96
|
+
filterApply: t("bos7.filterApply", "Cari"),
|
|
97
|
+
filterReset: t("bos7.filterReset", "Reset"),
|
|
88
98
|
|
|
89
99
|
loading: t("bos7.loading", "Memuat data..."),
|
|
90
100
|
saving: t("bos7.saving", "Memproses..."),
|