@isi-ui7/bos7-shared 0.2.12 → 0.3.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.
@@ -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-Bylk5fh-.js")).catch(function(k) {
9219
+ return (Kt.canvg ? Promise.resolve(Kt.canvg) : import("./index.es-zuTIhIvp.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.2.12",
3
+ "version": "0.3.0",
4
4
  "description": "Shared auth7 and layout primitives for bos7 applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,11 +9,11 @@
9
9
  "directory": "packages/bos7-shared"
10
10
  },
11
11
  "type": "module",
12
- "main": "src/index.ts",
13
- "module": "src/index.ts",
14
- "types": "src/index.ts",
12
+ "main": "dist/index.js",
13
+ "module": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
15
  "exports": {
16
- ".": "./src/index.ts",
16
+ ".": "./dist/index.js",
17
17
  "./auth7": "./src/auth7/index.ts",
18
18
  "./page-layout": "./src/page-layout.tsx",
19
19
  "./modal-layout": "./src/modal-layout.tsx",
@@ -35,6 +35,7 @@
35
35
  "./workflow/use-workflow-tracker": "./src/workflow/use-workflow-tracker.ts",
36
36
  "./workflow/crud-client": "./src/workflow/crud-client.ts",
37
37
  "./data-table/cursor-adapter": "./src/data-table/cursor-adapter.ts",
38
+ "./data-table/proxy": "./src/data-table/proxy.ts",
38
39
  "./notification/routing": "./src/notification/routing.ts",
39
40
  "./shell/interaction": "./src/shell/interaction.ts",
40
41
  "./shell/branch": "./src/shell/branch.ts",
@@ -49,139 +50,90 @@
49
50
  "./bff/health": "./src/bff/health.ts",
50
51
  "./observability/otel-init": "./src/observability/otel-init.ts",
51
52
  "./form-types": "./src/form-types.ts",
52
- "./form-value-schema": "./src/form-value-schema.ts",
53
53
  "./form-numeric": "./src/form-numeric.ts",
54
54
  "./form-renderer": "./src/form-renderer.tsx",
55
55
  "./form-renderer-utils": "./src/form-renderer-utils.ts",
56
56
  "./form-rules": "./src/form-rules.ts",
57
57
  "./i18n": "./src/i18n.tsx",
58
58
  "./workflow/use-crud-form": "./src/workflow/use-crud-form.ts",
59
- "./styles": "./src/form-contract.css"
59
+ "./styles": "./src/form-contract.css",
60
+ "./form-value-schema": "./src/form-value-schema.ts"
60
61
  },
61
62
  "publishConfig": {
62
63
  "registry": "https://registry.npmjs.org",
63
- "access": "public",
64
- "main": "dist/index.js",
65
- "module": "dist/index.js",
66
- "types": "dist/index.d.ts",
67
- "exports": {
68
- ".": "./dist/index.js",
69
- "./auth7": "./src/auth7/index.ts",
70
- "./page-layout": "./src/page-layout.tsx",
71
- "./modal-layout": "./src/modal-layout.tsx",
72
- "./form-layout": "./src/form-layout.tsx",
73
- "./backend": "./src/backend.ts",
74
- "./export": "./src/export.ts",
75
- "./types/notification": "./src/types/notification.ts",
76
- "./nats/client": "./src/nats/client.ts",
77
- "./event-bus": "./src/event-bus/index.ts",
78
- "./event-bus/useEventBus": "./src/event-bus/useEventBus.ts",
79
- "./api": "./src/api.ts",
80
- "./types/core": "./src/types/core.ts",
81
- "./style-contract": "./src/style-contract.ts",
82
- "./crud-components": "./src/crud-components.tsx",
83
- "./workflow/backend": "./src/workflow/backend.ts",
84
- "./workflow/notif7": "./src/workflow/notif7.ts",
85
- "./workflow/api-client": "./src/workflow/api-client.ts",
86
- "./workflow/starter": "./src/workflow/starter.ts",
87
- "./workflow/use-workflow-tracker": "./src/workflow/use-workflow-tracker.ts",
88
- "./workflow/crud-client": "./src/workflow/crud-client.ts",
89
- "./data-table/cursor-adapter": "./src/data-table/cursor-adapter.ts",
90
- "./notification/routing": "./src/notification/routing.ts",
91
- "./shell/interaction": "./src/shell/interaction.ts",
92
- "./shell/branch": "./src/shell/branch.ts",
93
- "./notif7": "./src/notif7.ts",
94
- "./shell/provider": "./src/shell/provider.tsx",
95
- "./shell/app-shell-layout": "./src/shell/app-shell-layout.tsx",
96
- "./notifications-bff": "./src/notifications-bff.ts",
97
- "./bff/auth-me": "./src/bff/auth-me.ts",
98
- "./bff/auth-refresh": "./src/bff/auth-refresh.ts",
99
- "./bff/auth-logout": "./src/bff/auth-logout.ts",
100
- "./bff/auth-preferences": "./src/bff/auth-preferences.ts",
101
- "./bff/health": "./src/bff/health.ts",
102
- "./observability/otel-init": "./src/observability/otel-init.ts",
103
- "./form-types": "./src/form-types.ts",
104
- "./form-numeric": "./src/form-numeric.ts",
105
- "./form-renderer": "./src/form-renderer.tsx",
106
- "./form-renderer-utils": "./src/form-renderer-utils.ts",
107
- "./form-rules": "./src/form-rules.ts",
108
- "./i18n": "./src/i18n.tsx",
109
- "./workflow/use-crud-form": "./src/workflow/use-crud-form.ts",
110
- "./styles": "./src/form-contract.css",
111
- "./form-value-schema": "./src/form-value-schema.ts"
112
- }
64
+ "access": "public"
113
65
  },
114
66
  "files": [
115
67
  "dist",
116
68
  "src"
117
69
  ],
118
- "scripts": {
119
- "build": "vite build",
120
- "lint": "eslint \"src/**/*.{ts,tsx}\"",
121
- "test": "vitest run --passWithNoTests",
122
- "typecheck": "tsc --noEmit"
123
- },
124
70
  "peerDependencies": {
125
- "react": ">=18",
126
- "react-dom": ">=18",
127
- "next": "16.x",
128
- "@isi-ui7/corporate-themes": ">=0.2.0",
129
- "@isi-ui7/i18n": ">=0.1.0",
130
- "@isi-ui7/ui-shell": ">=0.2.0",
71
+ "@carbon/icons-react": "^11.71.0",
72
+ "@carbon/react": "^1.97.0",
73
+ "@isi-ui7/corporate-themes": ">=0.2.6",
74
+ "@isi-ui7/data-table": ">=0.2.4",
75
+ "@isi-ui7/editable-table": ">=0.2.3",
76
+ "@isi-ui7/i18n": ">=0.2.3",
77
+ "@isi-ui7/lookup-input": ">=0.2.4",
131
78
  "@isi-ui7/modal-manager": ">=0.2.0",
132
79
  "@isi-ui7/realtime": ">=0.2.0",
133
- "@carbon/react": "^1.97.0",
134
- "@carbon/icons-react": "^11.71.0",
135
- "@isi-ui7/data-table": ">=0.2.0",
136
- "@isi-ui7/lookup-input": ">=0.2.0",
137
- "@isi-ui7/editable-table": ">=0.2.0"
80
+ "@isi-ui7/ui-shell": ">=0.2.8",
81
+ "next": "16.x",
82
+ "react": ">=18",
83
+ "react-dom": ">=18"
138
84
  },
139
85
  "devDependencies": {
86
+ "@carbon/icons-react": "^11.71.0",
87
+ "@carbon/react": "^1.97.0",
140
88
  "@eslint/eslintrc": "^3.3.1",
141
89
  "@eslint/js": "^9.39.0",
90
+ "@testing-library/dom": "^10.0.0",
91
+ "@testing-library/react": "^16.0.0",
92
+ "@testing-library/user-event": "^14.5.0",
142
93
  "@types/react": "^19.0.0",
143
94
  "@types/react-dom": "^19.0.0",
144
95
  "@typescript-eslint/eslint-plugin": "^8.52.0",
145
96
  "@typescript-eslint/parser": "^8.52.0",
146
97
  "@vitejs/plugin-react": "^4.2.0",
147
- "@testing-library/dom": "^10.0.0",
148
- "@testing-library/react": "^16.0.0",
149
- "@testing-library/user-event": "^14.5.0",
150
98
  "eslint": "9.39.2",
151
99
  "eslint-config-prettier": "^9.1.0",
152
100
  "eslint-plugin-jsx-a11y": "^6.7.1",
153
101
  "eslint-plugin-react": "^7.33.2",
154
102
  "eslint-plugin-react-hooks": "^4.6.2",
155
103
  "jsdom": "^22.1.0",
104
+ "next": "16.1.1",
156
105
  "typescript": "^5.3.3",
157
106
  "vite": "^5.0.0",
158
107
  "vite-plugin-dts": "^3.6.0",
159
108
  "vitest": "^1.0.0",
160
- "next": "16.1.1",
161
- "@isi-ui7/corporate-themes": "workspace:*",
162
- "@isi-ui7/i18n": "workspace:*",
163
- "@isi-ui7/ui-shell": "workspace:*",
164
- "@isi-ui7/modal-manager": "workspace:*",
165
- "@isi-ui7/realtime": "workspace:*",
166
- "@carbon/react": "^1.97.0",
167
- "@carbon/icons-react": "^11.71.0",
168
- "@isi-ui7/data-table": "workspace:*",
169
- "@isi-ui7/lookup-input": "workspace:*",
170
- "@isi-ui7/editable-table": "workspace:*"
109
+ "@isi-ui7/editable-table": "0.2.3",
110
+ "@isi-ui7/lookup-input": "0.2.4",
111
+ "@isi-ui7/i18n": "0.2.3",
112
+ "@isi-ui7/data-table": "0.2.4",
113
+ "@isi-ui7/corporate-themes": "0.2.6",
114
+ "@isi-ui7/realtime": "0.2.3",
115
+ "@isi-ui7/modal-manager": "0.2.2",
116
+ "@isi-ui7/ui-shell": "0.2.8"
171
117
  },
172
118
  "dependencies": {
173
119
  "@carbon/icons-react": "^11.71.0",
174
120
  "@carbon/react": "^1.97.0",
175
- "@isi-ui7/data-table": "workspace:*",
176
- "@isi-ui7/lookup-input": "workspace:*",
177
- "@isi-ui7/editable-table": "workspace:*",
121
+ "@opentelemetry/auto-instrumentations-node": "^0.57.0",
122
+ "@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
123
+ "@opentelemetry/sdk-node": "^0.57.0",
124
+ "axios": "^1.7.7",
125
+ "exceljs": "^4.4.0",
178
126
  "jose": "^5.9.6",
179
- "xlsx": "^0.18.5",
180
127
  "jspdf": "^4.2.1",
181
128
  "jspdf-autotable": "^5.0.7",
182
- "axios": "^1.7.7",
183
- "@opentelemetry/sdk-node": "^0.57.0",
184
- "@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
185
- "@opentelemetry/auto-instrumentations-node": "^0.57.0"
129
+ "@isi-ui7/data-table": "0.2.4",
130
+ "@isi-ui7/editable-table": "0.2.3",
131
+ "@isi-ui7/lookup-input": "0.2.4"
132
+ },
133
+ "scripts": {
134
+ "build": "vite build",
135
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
136
+ "test": "vitest run --passWithNoTests",
137
+ "typecheck": "tsc --noEmit"
186
138
  }
187
- }
139
+ }
@@ -12,7 +12,11 @@ import {
12
12
  ModalHeader,
13
13
  ProgressIndicator,
14
14
  ProgressStep,
15
+ Select,
16
+ SelectItem,
17
+ TextArea,
15
18
  ToastNotification,
19
+ Toggle,
16
20
  } from "@carbon/react";
17
21
  import { ServerDataTable } from "@isi-ui7/data-table";
18
22
  import type { I_DataTblColumn } from "@isi-ui7/data-table";
@@ -50,14 +54,39 @@ export function SharedCrudListPage({
50
54
  schema,
51
55
  tableKey,
52
56
  onAdd,
53
- onPopupClick
57
+ onPopupClick,
58
+ toolbarExtra,
54
59
  }: {
55
60
  schema: CrudListSchema;
56
61
  tableKey?: number;
57
62
  onAdd: () => void;
58
63
  onPopupClick: (row: Record<string, unknown>, actionId: string) => void;
64
+ /**
65
+ * Optional extra toolbar action(s) rendered to the LEFT of the built-in Add
66
+ * button (e.g. a secondary "Bulk Create" button). Additive + backward-compatible:
67
+ * omit it and the toolbar renders exactly as before.
68
+ */
69
+ toolbarExtra?: ReactNode;
59
70
  }) {
60
71
  const labels = useBosSharedI18n();
72
+ // "Show Inactive" toggle state (opt-in via schema.showInactiveToggle). When
73
+ // on, the list request carries ?<param>=true so the backend surfaces
74
+ // deactivated / soft-deleted rows. Survives table remounts (tableKey).
75
+ const [showInactive, setShowInactive] = useState(false);
76
+ const inactiveParam = schema.includeInactiveParam ?? "include_inactive";
77
+ // Single-select toolbar filter (opt-in via schema.filterSelect) — same
78
+ // additionalParams wiring as showInactiveToggle, for an arbitrary enum
79
+ // column instead of a boolean.
80
+ const [filterValue, setFilterValue] = useState("");
81
+ const hasInactiveToggle = Boolean(schema.showInactiveToggle);
82
+ const hasFilterSelect = Boolean(schema.filterSelect);
83
+ const additionalParams =
84
+ hasInactiveToggle || hasFilterSelect
85
+ ? {
86
+ ...(hasInactiveToggle ? { [inactiveParam]: showInactive ? "true" : "" } : {}),
87
+ ...(hasFilterSelect ? { [schema.filterSelect!.param]: filterValue } : {}),
88
+ }
89
+ : undefined;
61
90
  return (
62
91
  <PageLayout>
63
92
  <PageHeader title={schema.title} subtitle={schema.subtitle} />
@@ -65,14 +94,51 @@ export function SharedCrudListPage({
65
94
  <ServerDataTable
66
95
  key={tableKey}
67
96
  apiPath={schema.apiPath}
97
+ additionalParams={additionalParams}
68
98
  columns={schema.columns as Record<string, I_DataTblColumn>}
69
99
  showSearch={schema.showSearch ?? true}
70
100
  showSort={schema.showSort ?? true}
71
- popupMenu={{ src: "local", items: schema.popupMenuItems, onClick: onPopupClick }}
101
+ popupMenu={{
102
+ src: "local",
103
+ items: schema.popupMenuItems,
104
+ itemsForRow: schema.popupMenuItemsForRow,
105
+ onClick: onPopupClick,
106
+ }}
72
107
  toolbarActions={
73
- <Button renderIcon={Add} size="sm" onClick={onAdd}>
74
- {schema.addButtonLabel || labels.next}
75
- </Button>
108
+ <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
109
+ {schema.showInactiveToggle ? (
110
+ <Toggle
111
+ id={`show-inactive-${schema.apiPath}`}
112
+ size="sm"
113
+ labelText=""
114
+ labelA={schema.showInactiveLabel ?? labels.showInactive}
115
+ labelB={schema.showInactiveLabel ?? labels.showInactive}
116
+ toggled={showInactive}
117
+ onToggle={(checked) => setShowInactive(checked)}
118
+ />
119
+ ) : null}
120
+ {schema.filterSelect ? (
121
+ <div style={{ minWidth: "10rem" }}>
122
+ <Select
123
+ id={`filter-select-${schema.apiPath}`}
124
+ labelText=""
125
+ hideLabel
126
+ size="sm"
127
+ value={filterValue}
128
+ onChange={(e) => setFilterValue(e.target.value)}
129
+ >
130
+ <SelectItem value="" text={schema.filterSelect.allLabel ?? "Semua"} />
131
+ {schema.filterSelect.options.map((opt) => (
132
+ <SelectItem key={opt.value} value={opt.value} text={opt.label} />
133
+ ))}
134
+ </Select>
135
+ </div>
136
+ ) : null}
137
+ {toolbarExtra}
138
+ <Button renderIcon={Add} size="sm" onClick={onAdd}>
139
+ {schema.addButtonLabel || labels.next}
140
+ </Button>
141
+ </div>
76
142
  }
77
143
  />
78
144
  </PageBody>
@@ -92,6 +158,7 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
92
158
  onEdit,
93
159
  deleteApiPath,
94
160
  onCustomAction,
161
+ toolbarExtra,
95
162
  }: {
96
163
  listSchema: CrudListSchema;
97
164
  deleteSchema?: CrudDeleteSchema<TData>;
@@ -99,6 +166,8 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
99
166
  onView?: (id: string) => void;
100
167
  onEdit?: (id: string) => void;
101
168
  deleteApiPath?: (id: string) => string;
169
+ /** Optional extra toolbar action(s) rendered beside the built-in Add button. */
170
+ toolbarExtra?: ReactNode;
102
171
  /**
103
172
  * Level 1: called for any popup action id not handled by built-in logic
104
173
  * (view / edit / delete) and not in listSchema.customActionSchemas (Level 2).
@@ -115,6 +184,7 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
115
184
  const [customActionRow, setCustomActionRow] = useState<Record<string, unknown> | null>(null);
116
185
  const [customActioning, setCustomActioning] = useState(false);
117
186
  const [customActionError, setCustomActionError] = useState<string | null>(null);
187
+ const [customActionReason, setCustomActionReason] = useState("");
118
188
 
119
189
  // Workflow completion toast
120
190
  const [wfToast, setWfToast] = useState<{ kind: 'success' | 'error'; title: string; subtitle?: string } | null>(null);
@@ -153,6 +223,7 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
153
223
  setCustomActionId(actionId);
154
224
  setCustomActionRow(row);
155
225
  setCustomActionError(null);
226
+ setCustomActionReason("");
156
227
  return;
157
228
  }
158
229
 
@@ -185,6 +256,7 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
185
256
  if (!schema) return;
186
257
  const id = String(customActionRow.id ?? "");
187
258
  const path = typeof schema.apiPath === "function" ? schema.apiPath(id) : schema.apiPath;
259
+ const body = schema.reasonRequired ? { reason: customActionReason.trim() } : {};
188
260
  setCustomActioning(true);
189
261
  setCustomActionError(null);
190
262
  try {
@@ -193,17 +265,17 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
193
265
  const res = await fetch(path, {
194
266
  method: schema.method ?? "POST",
195
267
  headers: { "Content-Type": "application/json" },
196
- body: JSON.stringify({}),
268
+ body: JSON.stringify(body),
197
269
  });
198
270
  if (!res.ok) {
199
- const body = (await res.json().catch(() => ({}))) as { error?: string };
200
- throw new Error(body.error ?? `Gagal (HTTP ${res.status})`);
271
+ const resBody = (await res.json().catch(() => ({}))) as { error?: string };
272
+ throw new Error(resBody.error ?? `Gagal (HTTP ${res.status})`);
201
273
  }
202
274
  setCustomActionId(null);
203
275
  setCustomActionRow(null);
204
276
  setTableKey((v) => v + 1);
205
277
  } else {
206
- const instanceId = await submitWorkflowForm(path, schema.method ?? "POST", {});
278
+ const instanceId = await submitWorkflowForm(path, schema.method ?? "POST", body);
207
279
  setCustomActionId(null);
208
280
  setCustomActionRow(null);
209
281
  setTableKey((v) => v + 1);
@@ -214,7 +286,7 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
214
286
  } finally {
215
287
  setCustomActioning(false);
216
288
  }
217
- }, [customActionId, customActionRow, listSchema.customActionSchemas, tracker]);
289
+ }, [customActionId, customActionRow, customActionReason, listSchema.customActionSchemas, tracker]);
218
290
 
219
291
  const activeCustomSchema =
220
292
  customActionId ? listSchema.customActionSchemas?.[customActionId] : undefined;
@@ -223,6 +295,7 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
223
295
  ? activeCustomSchema.description(customActionRow ?? {})
224
296
  : activeCustomSchema.description)
225
297
  : "";
298
+ const customReasonMissing = Boolean(activeCustomSchema?.reasonRequired) && customActionReason.trim().length === 0;
226
299
 
227
300
  return (
228
301
  <>
@@ -231,6 +304,7 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
231
304
  tableKey={tableKey}
232
305
  onAdd={onAdd}
233
306
  onPopupClick={onPopupClick}
307
+ toolbarExtra={toolbarExtra}
234
308
  />
235
309
 
236
310
  {wfToast ? (
@@ -268,6 +342,15 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
268
342
  <ModalBody hasScrollingContent={false}>
269
343
  <div style={{ display: "grid", gap: "0.5rem", fontSize: "0.875rem" }}>
270
344
  <p>{customDescription}</p>
345
+ {activeCustomSchema.reasonRequired ? (
346
+ <TextArea
347
+ labelText={activeCustomSchema.reasonLabel ?? "Alasan"}
348
+ value={customActionReason}
349
+ onChange={(e) => setCustomActionReason(e.target.value)}
350
+ disabled={customActioning}
351
+ rows={3}
352
+ />
353
+ ) : null}
271
354
  {customActionError ? (
272
355
  <InlineNotification
273
356
  kind="error"
@@ -289,7 +372,7 @@ export function CrudSchemaListPage<TData extends Record<string, unknown>>({
289
372
  <Button
290
373
  kind="primary"
291
374
  onClick={() => void doCustomAction()}
292
- disabled={customActioning || !customActionRow}
375
+ disabled={customActioning || !customActionRow || customReasonMissing}
293
376
  >
294
377
  {customActioning ? labels.saving : (activeCustomSchema.confirmLabel ?? labels.next)}
295
378
  </Button>
package/src/crud-types.ts CHANGED
@@ -37,6 +37,16 @@ export type CrudCustomActionSchema = {
37
37
  * Dipakai untuk aksi operasional seperti employee terminate/reactivate.
38
38
  */
39
39
  direct?: boolean;
40
+ /**
41
+ * Tampilkan textarea alasan di modal konfirmasi dan wajibkan isi non-kosong
42
+ * sebelum tombol confirm aktif. Nilai dikirim sebagai `{ reason }` di body
43
+ * request (digabung ke payload lain bila apiPath sama dipakai untuk field
44
+ * lain). Dipakai untuk aksi yang butuh audit trail eksplisit (mis. Set
45
+ * Non-Aktif produk) — lihat appdefs/{module}/index.def `withReason: true`.
46
+ */
47
+ reasonRequired?: boolean;
48
+ /** Label di atas textarea alasan. Default "Alasan". */
49
+ reasonLabel?: string;
40
50
  };
41
51
 
42
52
  export type CrudListSchema = {
@@ -57,6 +67,41 @@ export type CrudListSchema = {
57
67
  * Keyed by popup menu item id. Actions not listed here fall through to onCustomAction (Level 1).
58
68
  */
59
69
  customActionSchemas?: Record<string, CrudCustomActionSchema>;
70
+ /**
71
+ * Show a "Show Inactive" toolbar toggle. When on, the list request carries
72
+ * `?<includeInactiveParam>=true` so the backend surfaces soft-deleted /
73
+ * deactivated rows too. Default off keeps the list active-only. Pair with
74
+ * `popupMenuItemsForRow` so inactive rows expose "Reactivate".
75
+ */
76
+ showInactiveToggle?: boolean;
77
+ /** Query param name toggled by the Show-Inactive control. Default "include_inactive". */
78
+ includeInactiveParam?: string;
79
+ /** Label for the Show-Inactive toggle (i18n'd by the caller). Default "Tampilkan Nonaktif". */
80
+ showInactiveLabel?: string;
81
+ /**
82
+ * Per-row popup items. When provided, overrides `popupMenuItems` for each row
83
+ * so the menu can depend on row state — e.g. active rows show "Deactivate",
84
+ * inactive rows show "Reactivate". Returned item ids route the same way as
85
+ * `popupMenuItems` (view/edit/delete built-in; others via customActionSchemas
86
+ * or onCustomAction).
87
+ */
88
+ popupMenuItemsForRow?: (row: Record<string, unknown>) => CrudPopupMenuItem[];
89
+ /**
90
+ * Optional single-select toolbar filter. When an option other than "(all)"
91
+ * is picked, the list request carries `?<param>=<value>`; picking back to
92
+ * "(all)" removes the param. Mirrors `showInactiveToggle`'s additionalParams
93
+ * wiring but for an arbitrary enum column (e.g. `vendor_type`). Additive +
94
+ * backward-compatible: omit it and the toolbar renders exactly as before.
95
+ */
96
+ filterSelect?: {
97
+ /** Query param name the backend reads (e.g. "vendor_type"). */
98
+ param: string;
99
+ /** Toolbar label for the select. */
100
+ label: string;
101
+ options: Array<{ value: string; label: string }>;
102
+ /** Label for the "no filter" option. Default "Semua". */
103
+ allLabel?: string;
104
+ };
60
105
  };
61
106
 
62
107
  export type CrudDeleteSchema<TData extends Record<string, unknown>> = {
@@ -10,6 +10,15 @@ export interface ProxyBackendPostOptions {
10
10
  accessTokenCookie?: string;
11
11
  }
12
12
 
13
+ // Toolbar filters (showInactiveToggle, filterSelect) ride as query-string
14
+ // params on the incoming BFF request (see ServerDataTable's additionalParams)
15
+ // even though the body carries the DataTable sort/search/pagination payload.
16
+ // Forward that query string to the backend or those filters silently no-op.
17
+ function backendPathWithQuery(req: NextRequest, path: string): string {
18
+ const search = new URL(req.url).search;
19
+ return search ? `${path}${search}` : path;
20
+ }
21
+
13
22
  export async function proxyBackendPost(
14
23
  req: NextRequest,
15
24
  opts: ProxyBackendPostOptions,
@@ -17,7 +26,7 @@ export async function proxyBackendPost(
17
26
  const accessToken = req.cookies.get(opts.accessTokenCookie ?? "access_token")?.value;
18
27
  const result = await exchangeUserToken(accessToken ?? "", opts.audience);
19
28
  const body = await req.json();
20
- const res = await fetch(`${opts.backendUrl}${opts.path}`, {
29
+ const res = await fetch(`${opts.backendUrl}${backendPathWithQuery(req, opts.path)}`, {
21
30
  method: "POST",
22
31
  headers: {
23
32
  "Content-Type": "application/json",
@@ -57,7 +66,7 @@ export async function proxyQueryRoute(
57
66
  const accessToken = req.cookies.get(opts.accessTokenCookie ?? "access_token")?.value;
58
67
  const result = await exchangeUserToken(accessToken ?? "", opts.audience);
59
68
  const body = await req.json();
60
- const res = await fetch(`${opts.backendUrl}${opts.path}`, {
69
+ const res = await fetch(`${opts.backendUrl}${backendPathWithQuery(req, opts.path)}`, {
61
70
  method: "POST",
62
71
  headers: {
63
72
  "Content-Type": "application/json",