@isi-ui7/bos7-shared 0.4.7 → 0.5.1

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-OHmQQaGr.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))),
@@ -28663,6 +28663,8 @@ function tl() {
28663
28663
  toggleOff: M("bos7.toggleOff", "Tidak"),
28664
28664
  emptyValue: M("bos7.emptyValue", "—"),
28665
28665
  showInactive: M("bos7.showInactive", "Tampilkan Nonaktif"),
28666
+ filterApply: M("bos7.filterApply", "Cari"),
28667
+ filterReset: M("bos7.filterReset", "Reset"),
28666
28668
  loading: M("bos7.loading", "Memuat data..."),
28667
28669
  saving: M("bos7.saving", "Memproses..."),
28668
28670
  errorTitle: M("bos7.errorTitle", "Gagal"),
@@ -1,5 +1,5 @@
1
- import { c as Ke, g as Do } from "./index-C-h8WiEp.js";
2
- import { _ as Xa } from "./jspdf.es.min-OHmQQaGr.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-h8WiEp.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-C4xrluJC.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.7",
3
+ "version": "0.5.1",
4
4
  "description": "Shared auth7 and layout primitives for bos7 applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,7 +70,7 @@
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.17",
73
+ "@isi-ui7/corporate-themes": ">=0.2.19",
74
74
  "@isi-ui7/data-table": ">=0.2.5",
75
75
  "@isi-ui7/editable-table": ">=0.2.3",
76
76
  "@isi-ui7/i18n": ">=0.2.4",
@@ -106,13 +106,13 @@
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.17",
109
+ "@isi-ui7/corporate-themes": "0.2.19",
110
+ "@isi-ui7/i18n": "0.2.4",
110
111
  "@isi-ui7/data-table": "0.2.5",
111
112
  "@isi-ui7/lookup-input": "0.2.4",
112
- "@isi-ui7/realtime": "0.2.3",
113
113
  "@isi-ui7/modal-manager": "0.2.2",
114
- "@isi-ui7/i18n": "0.2.4",
115
114
  "@isi-ui7/editable-table": "0.2.3",
115
+ "@isi-ui7/realtime": "0.2.3",
116
116
  "@isi-ui7/ui-shell": "0.2.8"
117
117
  },
118
118
  "dependencies": {
@@ -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/data-table": "0.2.5",
129
130
  "@isi-ui7/lookup-input": "0.2.4",
130
- "@isi-ui7/editable-table": "0.2.3",
131
- "@isi-ui7/data-table": "0.2.5"
131
+ "@isi-ui7/editable-table": "0.2.3"
132
132
  },
133
133
  "scripts": {
134
134
  "build": "vite build",
@@ -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
 
@@ -151,7 +166,24 @@ export function SharedCrudListPage({
151
166
  onClick: onPopupClick,
152
167
  }}
153
168
  toolbarActions={
154
- <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
169
+ // `flexWrap` WAJIB. Panel ini tumbuh dari beberapa medan jadi
170
+ // SEPULUH (Putaran 2, 2026-09-03) dan tiap medan ber-`minWidth:
171
+ // 10rem`; ditambah toggle, filterSelect, dan tombol Cari/Reset,
172
+ // lebar minimum satu barisnya ~112rem. Tanpa membungkus baris,
173
+ // panelnya meluber horizontal alih-alih turun ke baris kedua.
174
+ //
175
+ // Ongkos "sepuluh medan" sudah ditimbang di sumbu PERILAKU saat itu
176
+ // (debounce diganti tombol `Cari` — lihat komentar besar di atas)
177
+ // dan NOL ditimbang di sumbu TATA LETAK. Ini melunasinya.
178
+ <div
179
+ style={{
180
+ display: "flex",
181
+ flexWrap: "wrap",
182
+ alignItems: "center",
183
+ gap: "1rem",
184
+ rowGap: "0.5rem",
185
+ }}
186
+ >
155
187
  {schema.showInactiveToggle ? (
156
188
  <Toggle
157
189
  id={`show-inactive-${schema.apiPath}`}
@@ -207,10 +239,34 @@ export function SharedCrudListPage({
207
239
  placeholder={f.placeholder ?? f.label}
208
240
  value={filterFieldValues[f.param] ?? ""}
209
241
  onChange={(e) => setFilterFieldValue(f, e.target.value)}
242
+ onKeyDown={(e) => {
243
+ if (e.key === "Enter") terapkanFilter();
244
+ }}
210
245
  />
211
246
  </div>
212
247
  ),
213
248
  )}
249
+ {hasFilterFields ? (
250
+ <>
251
+ <Button
252
+ kind="primary"
253
+ size="sm"
254
+ renderIcon={Search}
255
+ onClick={terapkanFilter}
256
+ data-testid="filter-apply"
257
+ >
258
+ {labels.filterApply}
259
+ </Button>
260
+ <Button
261
+ kind="ghost"
262
+ size="sm"
263
+ onClick={resetFilter}
264
+ data-testid="filter-reset"
265
+ >
266
+ {labels.filterReset}
267
+ </Button>
268
+ </>
269
+ ) : null}
214
270
  {toolbarExtra}
215
271
  {(schema.showAddButton ?? true) ? (
216
272
  <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
  });
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;
@@ -91,6 +93,8 @@ export function useBosSharedI18n(): BosSharedLabels {
91
93
  toggleOff: t("bos7.toggleOff", "Tidak"),
92
94
  emptyValue: t("bos7.emptyValue", "—"),
93
95
  showInactive: t("bos7.showInactive", "Tampilkan Nonaktif"),
96
+ filterApply: t("bos7.filterApply", "Cari"),
97
+ filterReset: t("bos7.filterReset", "Reset"),
94
98
 
95
99
  loading: t("bos7.loading", "Memuat data..."),
96
100
  saving: t("bos7.saving", "Memproses..."),