@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 CHANGED
@@ -17,6 +17,8 @@ export type BosSharedLabels = {
17
17
  toggleOff: string;
18
18
  emptyValue: string;
19
19
  showInactive: string;
20
+ filterApply: string;
21
+ filterReset: string;
20
22
  loading: string;
21
23
  saving: string;
22
24
  errorTitle: string;
@@ -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-C5VEsLZd.js").then((o) => o.j), { default: w } = await import("./jspdf.plugin.autotable-DcntYaes.js"), c = new x({ orientation: "landscape" });
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
- "bos7.toggleOn": "On",
27701
- "bos7.toggleOff": "Off",
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
- toggleOn: M("bos7.toggleOn", "On"),
28639
- toggleOff: M("bos7.toggleOff", "Off"),
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-C_114Ysm.js";
2
- import { _ as Xa } from "./jspdf.es.min-C5VEsLZd.js";
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-C_114Ysm.js";
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-B-MluT1E.js")).catch(function(k) {
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.4.6",
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.16",
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.3",
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.16",
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.3",
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",
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
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). For `type: "select"` they're set
92
- // together — a discrete pick, same as filterSelect's existing immediate
93
- // commit. For `type: "text"` the commit is DEBOUNCED: this mirrors the
94
- // codebase's own precedent for free-text-triggers-a-fetch
95
- // (OffsetDataTable's search box debounces the same way) committing
96
- // every keystroke straight to a network request is the one inconsistency
97
- // an un-debounced text field would introduce that filterSelect's select
98
- // never had.
99
- const FILTER_TEXT_DEBOUNCE_MS = 400;
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
- const filterFieldTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
103
- useEffect(() => {
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
- if (field.type === "text") {
112
- if (filterFieldTimers.current[field.param]) {
113
- clearTimeout(filterFieldTimers.current[field.param]);
114
- }
115
- filterFieldTimers.current[field.param] = setTimeout(() => {
116
- setFilterFieldCommitted((prev) => ({ ...prev, [field.param]: value }));
117
- }, FILTER_TEXT_DEBOUNCE_MS);
118
- } else {
119
- setFilterFieldCommitted((prev) => ({ ...prev, [field.param]: value }));
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, waitFor } from "@testing-library/react";
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 merender Select dan mengirim param SEGERA saat dipilih", async () => {
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 merender TextInput dan DEBOUNCE param (nol segera saat mengetik)", async () => {
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 TIDAK segera mengirim param -- kalau ini gagal,
94
- // field teks refetch di SETIAP keystroke (inkonsisten dgn search bar
95
- // bawaan, yang sudah didebounce di OffsetDataTable).
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 waitFor(() => expect(paramsRendered()).toEqual({ full_name: "Budi" }), { timeout: 1000 });
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
- toggleOn: t("bos7.toggleOn", "On"),
85
- toggleOff: t("bos7.toggleOff", "Off"),
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..."),