@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.
- package/dist/crud-components.d.ts +10 -2
- package/dist/crud-types.d.ts +48 -0
- package/dist/export.d.ts +22 -1
- package/dist/i18n.d.ts +1 -0
- package/dist/index-1k-PSj-b.js +28765 -0
- package/dist/{index.es-Bylk5fh-.js → index.es-zuTIhIvp.js} +4 -7
- package/dist/index.js +72 -21147
- package/dist/{jspdf.es.min-Dzm5j8wC.js → jspdf.es.min-CnDMxfNp.js} +1 -1
- package/package.json +49 -97
- package/src/crud-components.tsx +94 -11
- package/src/crud-types.ts +45 -0
- package/src/data-table/proxy.ts +11 -2
- package/src/export.ts +144 -19
- package/src/i18n.tsx +2 -0
|
@@ -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-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.
|
|
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": "
|
|
13
|
-
"module": "
|
|
14
|
-
"types": "
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"module": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
15
|
"exports": {
|
|
16
|
-
".": "./
|
|
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": "
|
|
126
|
-
"react
|
|
127
|
-
"
|
|
128
|
-
"@isi-ui7/
|
|
129
|
-
"@isi-ui7/
|
|
130
|
-
"@isi-ui7/
|
|
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
|
-
"@
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
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
|
-
"
|
|
161
|
-
"@isi-ui7/
|
|
162
|
-
"@isi-ui7/i18n": "
|
|
163
|
-
"@isi-ui7/
|
|
164
|
-
"@isi-ui7/
|
|
165
|
-
"@isi-ui7/realtime": "
|
|
166
|
-
"@
|
|
167
|
-
"@
|
|
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
|
-
"@
|
|
176
|
-
"@
|
|
177
|
-
"@
|
|
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
|
-
"
|
|
183
|
-
"@
|
|
184
|
-
"@
|
|
185
|
-
|
|
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
|
+
}
|
package/src/crud-components.tsx
CHANGED
|
@@ -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={{
|
|
101
|
+
popupMenu={{
|
|
102
|
+
src: "local",
|
|
103
|
+
items: schema.popupMenuItems,
|
|
104
|
+
itemsForRow: schema.popupMenuItemsForRow,
|
|
105
|
+
onClick: onPopupClick,
|
|
106
|
+
}}
|
|
72
107
|
toolbarActions={
|
|
73
|
-
<
|
|
74
|
-
{schema.
|
|
75
|
-
|
|
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
|
|
200
|
-
throw new Error(
|
|
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>> = {
|
package/src/data-table/proxy.ts
CHANGED
|
@@ -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",
|