@isi-ui7/bos7-shared 0.3.9 → 0.3.11
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-types.d.ts +16 -0
- package/dist/{index-DqWPV29B.js → index-C_114Ysm.js} +71 -71
- package/dist/{index.es-9cvhYbjn.js → index.es-B-MluT1E.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{jspdf.es.min-CCBzPHw4.js → jspdf.es.min-C5VEsLZd.js} +2 -2
- package/dist/{jspdf.plugin.autotable-DFaknRns.js → jspdf.plugin.autotable-DcntYaes.js} +22 -19
- package/dist/purify.es-CiEWEeUM.js +605 -0
- package/dist/vitest.setup.d.ts +0 -0
- package/package.json +30 -81
- package/src/crud-components.tsx +154 -62
- package/src/crud-types.ts +16 -0
- package/src/list-add-button.test.tsx +74 -0
- package/src/vitest.setup.ts +22 -0
- package/src/wizard-mode-nav.test.tsx +173 -0
- package/dist/purify.es-Cm3utOpm.js +0 -560
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isi-ui7/bos7-shared",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
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",
|
|
@@ -50,85 +50,28 @@
|
|
|
50
50
|
"./bff/health": "./src/bff/health.ts",
|
|
51
51
|
"./observability/otel-init": "./src/observability/otel-init.ts",
|
|
52
52
|
"./form-types": "./src/form-types.ts",
|
|
53
|
-
"./form-value-schema": "./src/form-value-schema.ts",
|
|
54
53
|
"./form-numeric": "./src/form-numeric.ts",
|
|
55
54
|
"./form-renderer": "./src/form-renderer.tsx",
|
|
56
55
|
"./form-renderer-utils": "./src/form-renderer-utils.ts",
|
|
57
56
|
"./form-rules": "./src/form-rules.ts",
|
|
58
57
|
"./i18n": "./src/i18n.tsx",
|
|
59
58
|
"./workflow/use-crud-form": "./src/workflow/use-crud-form.ts",
|
|
60
|
-
"./styles": "./src/form-contract.css"
|
|
59
|
+
"./styles": "./src/form-contract.css",
|
|
60
|
+
"./form-value-schema": "./src/form-value-schema.ts"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
63
63
|
"registry": "https://registry.npmjs.org",
|
|
64
|
-
"access": "public"
|
|
65
|
-
"main": "dist/index.js",
|
|
66
|
-
"module": "dist/index.js",
|
|
67
|
-
"types": "dist/index.d.ts",
|
|
68
|
-
"exports": {
|
|
69
|
-
".": "./dist/index.js",
|
|
70
|
-
"./auth7": "./src/auth7/index.ts",
|
|
71
|
-
"./page-layout": "./src/page-layout.tsx",
|
|
72
|
-
"./modal-layout": "./src/modal-layout.tsx",
|
|
73
|
-
"./form-layout": "./src/form-layout.tsx",
|
|
74
|
-
"./backend": "./src/backend.ts",
|
|
75
|
-
"./export": "./src/export.ts",
|
|
76
|
-
"./types/notification": "./src/types/notification.ts",
|
|
77
|
-
"./nats/client": "./src/nats/client.ts",
|
|
78
|
-
"./event-bus": "./src/event-bus/index.ts",
|
|
79
|
-
"./event-bus/useEventBus": "./src/event-bus/useEventBus.ts",
|
|
80
|
-
"./api": "./src/api.ts",
|
|
81
|
-
"./types/core": "./src/types/core.ts",
|
|
82
|
-
"./style-contract": "./src/style-contract.ts",
|
|
83
|
-
"./crud-components": "./src/crud-components.tsx",
|
|
84
|
-
"./workflow/backend": "./src/workflow/backend.ts",
|
|
85
|
-
"./workflow/notif7": "./src/workflow/notif7.ts",
|
|
86
|
-
"./workflow/api-client": "./src/workflow/api-client.ts",
|
|
87
|
-
"./workflow/starter": "./src/workflow/starter.ts",
|
|
88
|
-
"./workflow/use-workflow-tracker": "./src/workflow/use-workflow-tracker.ts",
|
|
89
|
-
"./workflow/crud-client": "./src/workflow/crud-client.ts",
|
|
90
|
-
"./data-table/cursor-adapter": "./src/data-table/cursor-adapter.ts",
|
|
91
|
-
"./data-table/proxy": "./src/data-table/proxy.ts",
|
|
92
|
-
"./notification/routing": "./src/notification/routing.ts",
|
|
93
|
-
"./shell/interaction": "./src/shell/interaction.ts",
|
|
94
|
-
"./shell/branch": "./src/shell/branch.ts",
|
|
95
|
-
"./notif7": "./src/notif7.ts",
|
|
96
|
-
"./shell/provider": "./src/shell/provider.tsx",
|
|
97
|
-
"./shell/app-shell-layout": "./src/shell/app-shell-layout.tsx",
|
|
98
|
-
"./notifications-bff": "./src/notifications-bff.ts",
|
|
99
|
-
"./bff/auth-me": "./src/bff/auth-me.ts",
|
|
100
|
-
"./bff/auth-refresh": "./src/bff/auth-refresh.ts",
|
|
101
|
-
"./bff/auth-logout": "./src/bff/auth-logout.ts",
|
|
102
|
-
"./bff/auth-preferences": "./src/bff/auth-preferences.ts",
|
|
103
|
-
"./bff/health": "./src/bff/health.ts",
|
|
104
|
-
"./observability/otel-init": "./src/observability/otel-init.ts",
|
|
105
|
-
"./form-types": "./src/form-types.ts",
|
|
106
|
-
"./form-numeric": "./src/form-numeric.ts",
|
|
107
|
-
"./form-renderer": "./src/form-renderer.tsx",
|
|
108
|
-
"./form-renderer-utils": "./src/form-renderer-utils.ts",
|
|
109
|
-
"./form-rules": "./src/form-rules.ts",
|
|
110
|
-
"./i18n": "./src/i18n.tsx",
|
|
111
|
-
"./workflow/use-crud-form": "./src/workflow/use-crud-form.ts",
|
|
112
|
-
"./styles": "./src/form-contract.css",
|
|
113
|
-
"./form-value-schema": "./src/form-value-schema.ts"
|
|
114
|
-
}
|
|
64
|
+
"access": "public"
|
|
115
65
|
},
|
|
116
66
|
"files": [
|
|
117
67
|
"dist",
|
|
118
68
|
"src"
|
|
119
69
|
],
|
|
120
|
-
"scripts": {
|
|
121
|
-
"build": "vite build",
|
|
122
|
-
"prepublishOnly": "pnpm run build",
|
|
123
|
-
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
124
|
-
"test": "vitest run --passWithNoTests",
|
|
125
|
-
"typecheck": "tsc --noEmit"
|
|
126
|
-
},
|
|
127
70
|
"peerDependencies": {
|
|
128
71
|
"@carbon/icons-react": "^11.71.0",
|
|
129
72
|
"@carbon/react": "^1.97.0",
|
|
130
|
-
"@isi-ui7/corporate-themes": ">=0.2.
|
|
131
|
-
"@isi-ui7/data-table": ">=0.2.
|
|
73
|
+
"@isi-ui7/corporate-themes": ">=0.2.8",
|
|
74
|
+
"@isi-ui7/data-table": ">=0.2.5",
|
|
132
75
|
"@isi-ui7/editable-table": ">=0.2.3",
|
|
133
76
|
"@isi-ui7/i18n": ">=0.2.3",
|
|
134
77
|
"@isi-ui7/lookup-input": ">=0.2.4",
|
|
@@ -144,14 +87,6 @@
|
|
|
144
87
|
"@carbon/react": "^1.97.0",
|
|
145
88
|
"@eslint/eslintrc": "^3.3.1",
|
|
146
89
|
"@eslint/js": "^9.39.0",
|
|
147
|
-
"@isi-ui7/corporate-themes": "workspace:*",
|
|
148
|
-
"@isi-ui7/data-table": "workspace:*",
|
|
149
|
-
"@isi-ui7/editable-table": "workspace:*",
|
|
150
|
-
"@isi-ui7/i18n": "workspace:*",
|
|
151
|
-
"@isi-ui7/lookup-input": "workspace:*",
|
|
152
|
-
"@isi-ui7/modal-manager": "workspace:*",
|
|
153
|
-
"@isi-ui7/realtime": "workspace:*",
|
|
154
|
-
"@isi-ui7/ui-shell": "workspace:*",
|
|
155
90
|
"@testing-library/dom": "^10.0.0",
|
|
156
91
|
"@testing-library/react": "^16.0.0",
|
|
157
92
|
"@testing-library/user-event": "^14.5.0",
|
|
@@ -170,14 +105,19 @@
|
|
|
170
105
|
"typescript": "^5.3.3",
|
|
171
106
|
"vite": "^5.0.0",
|
|
172
107
|
"vite-plugin-dts": "^3.6.0",
|
|
173
|
-
"vitest": "^1.0.0"
|
|
108
|
+
"vitest": "^1.0.0",
|
|
109
|
+
"@isi-ui7/corporate-themes": "0.2.8",
|
|
110
|
+
"@isi-ui7/data-table": "0.2.5",
|
|
111
|
+
"@isi-ui7/editable-table": "0.2.3",
|
|
112
|
+
"@isi-ui7/lookup-input": "0.2.4",
|
|
113
|
+
"@isi-ui7/modal-manager": "0.2.2",
|
|
114
|
+
"@isi-ui7/i18n": "0.2.3",
|
|
115
|
+
"@isi-ui7/realtime": "0.2.3",
|
|
116
|
+
"@isi-ui7/ui-shell": "0.2.8"
|
|
174
117
|
},
|
|
175
118
|
"dependencies": {
|
|
176
119
|
"@carbon/icons-react": "^11.71.0",
|
|
177
120
|
"@carbon/react": "^1.97.0",
|
|
178
|
-
"@isi-ui7/data-table": "workspace:*",
|
|
179
|
-
"@isi-ui7/editable-table": "workspace:*",
|
|
180
|
-
"@isi-ui7/lookup-input": "workspace:*",
|
|
181
121
|
"@opentelemetry/auto-instrumentations-node": "^0.57.0",
|
|
182
122
|
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
|
183
123
|
"@opentelemetry/sdk-node": "^0.57.0",
|
|
@@ -185,6 +125,15 @@
|
|
|
185
125
|
"exceljs": "^4.4.0",
|
|
186
126
|
"jose": "^5.9.6",
|
|
187
127
|
"jspdf": "^4.2.1",
|
|
188
|
-
"jspdf-autotable": "^5.0.7"
|
|
128
|
+
"jspdf-autotable": "^5.0.7",
|
|
129
|
+
"@isi-ui7/data-table": "0.2.5",
|
|
130
|
+
"@isi-ui7/lookup-input": "0.2.4",
|
|
131
|
+
"@isi-ui7/editable-table": "0.2.3"
|
|
132
|
+
},
|
|
133
|
+
"scripts": {
|
|
134
|
+
"build": "vite build",
|
|
135
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
136
|
+
"test": "vitest run --passWithNoTests",
|
|
137
|
+
"typecheck": "tsc --noEmit"
|
|
189
138
|
}
|
|
190
|
-
}
|
|
139
|
+
}
|
package/src/crud-components.tsx
CHANGED
|
@@ -10,8 +10,6 @@ import {
|
|
|
10
10
|
ModalBody,
|
|
11
11
|
ModalFooter,
|
|
12
12
|
ModalHeader,
|
|
13
|
-
ProgressIndicator,
|
|
14
|
-
ProgressStep,
|
|
15
13
|
Select,
|
|
16
14
|
SelectItem,
|
|
17
15
|
TextArea,
|
|
@@ -21,7 +19,7 @@ import {
|
|
|
21
19
|
import { ServerDataTable } from "@isi-ui7/data-table";
|
|
22
20
|
import type { I_DataTblColumn } from "@isi-ui7/data-table";
|
|
23
21
|
import type { CrudCustomActionSchema, CrudDeleteSchema, CrudListSchema } from "./crud-types";
|
|
24
|
-
import type { CrudForm, FormMode } from "./form-types";
|
|
22
|
+
import type { CrudForm, FormMode, FormPageDef } from "./form-types";
|
|
25
23
|
import type { Ui7FormDensity } from "./style-contract";
|
|
26
24
|
import { SchemaFormRenderer } from "./form-renderer";
|
|
27
25
|
import { WizardStepper, type WizardStepState } from "./wizard-stepper";
|
|
@@ -136,9 +134,11 @@ export function SharedCrudListPage({
|
|
|
136
134
|
</div>
|
|
137
135
|
) : null}
|
|
138
136
|
{toolbarExtra}
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
|
|
137
|
+
{(schema.showAddButton ?? true) ? (
|
|
138
|
+
<Button renderIcon={Add} size="sm" onClick={onAdd}>
|
|
139
|
+
{schema.addButtonLabel || labels.next}
|
|
140
|
+
</Button>
|
|
141
|
+
) : null}
|
|
142
142
|
</div>
|
|
143
143
|
}
|
|
144
144
|
/>
|
|
@@ -489,6 +489,40 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
489
489
|
|
|
490
490
|
const layout = schema.layout;
|
|
491
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Wizard adalah alat bantu PENGISIAN, bukan cara menyusun halaman.
|
|
494
|
+
*
|
|
495
|
+
* Ia ada untuk memandu petugas melalui formulir panjang yang belum pernah
|
|
496
|
+
* diisi, dan — pada registrasi nasabah — untuk MENAHAN langkah berikutnya
|
|
497
|
+
* sampai gerbang `canLeavePage` terpenuhi. Kedua alasan itu hanya berlaku
|
|
498
|
+
* saat data DIBUAT.
|
|
499
|
+
*
|
|
500
|
+
* Pada `edit` dan `view` datanya sudah ada dan sudah lolos gerbangnya.
|
|
501
|
+
* Memaksa operator melangkah lima kali untuk membaca satu field di langkah
|
|
502
|
+
* 5 bukan panduan, ia penghalang — dan pada `view` bahkan tidak bisa
|
|
503
|
+
* dilewati: footer mode-view sengaja nol tombol Lanjut, jadi satu-satunya
|
|
504
|
+
* jalan maju adalah menebak bahwa stepper-nya bisa diklik.
|
|
505
|
+
*
|
|
506
|
+
* Karena itu di luar `create` wizard dirender sebagai TAB biasa: halaman
|
|
507
|
+
* tetap terpisah dan bisa dilompati bebas, tanpa stepper, tanpa urutan
|
|
508
|
+
* wajib, tanpa Lanjut/Kembali. Yang HILANG hanya PEMANDUANNYA; isinya
|
|
509
|
+
* tidak ada yang dibuang.
|
|
510
|
+
*
|
|
511
|
+
* (2026-08-19 ini sempat dirender DATAR — satu aliran panjang. Diganti
|
|
512
|
+
* 2026-08-20: pada form 5 langkah hasilnya 13 panel beruntun dalam satu
|
|
513
|
+
* kolom yang sangat panjang. Yang dilarang adalah wizard MEMANDU, bukan
|
|
514
|
+
* halamannya terpisah; tab memisahkan tanpa memandu.)
|
|
515
|
+
*
|
|
516
|
+
* Aman terhadap validasi: `handleSave` memakai `validateAll()`, yang sudah
|
|
517
|
+
* mengumpulkan field dari SELURUH halaman tanpa peduli halaman aktif —
|
|
518
|
+
* jadi menyimpan dari bentuk bertab memeriksa hal yang sama persis.
|
|
519
|
+
*
|
|
520
|
+
* Keputusan PM 2026-08-19, direvisi 2026-08-20. Sengaja dipasang di sini, BUKAN di skema tiap
|
|
521
|
+
* app: ini keputusan tentang cara me-render sebuah layout, dan menaruhnya
|
|
522
|
+
* di skema berarti tiap app harus mengingat untuk mengulanginya.
|
|
523
|
+
*/
|
|
524
|
+
const wizardNav = layout.type === "wizard" && mode === "create";
|
|
525
|
+
|
|
492
526
|
const renderBody = () => {
|
|
493
527
|
if (layout.type === "single-page") {
|
|
494
528
|
return (
|
|
@@ -508,66 +542,116 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
508
542
|
);
|
|
509
543
|
}
|
|
510
544
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
545
|
+
/**
|
|
546
|
+
* Halaman ber-tab. Dipakai DUA layout: `tabs`, dan `wizard` di luar
|
|
547
|
+
* `create` (lihat `wizardNav`).
|
|
548
|
+
*
|
|
549
|
+
* Sengaja SATU fungsi, bukan dua blok kembar. Keduanya wajib tetap
|
|
550
|
+
* terlihat sama, dan salinan kedua akan tertinggal saat yang satu
|
|
551
|
+
* disunting — tanpa berbunyi sebagai galat, karena keduanya tetap
|
|
552
|
+
* me-render tab yang bekerja.
|
|
553
|
+
*/
|
|
554
|
+
const renderTabbedPages = (pages: FormPageDef<TData>[]) => (
|
|
555
|
+
<PageBody>
|
|
556
|
+
{errBanner}
|
|
557
|
+
<div>
|
|
558
|
+
<div
|
|
559
|
+
role="tablist"
|
|
560
|
+
style={{
|
|
561
|
+
display: "flex",
|
|
562
|
+
borderBottom: "1px solid var(--cds-border-subtle-01, #e0e0e0)",
|
|
563
|
+
marginBottom: 0,
|
|
564
|
+
}}
|
|
565
|
+
>
|
|
566
|
+
{pages.map((pg, i) => (
|
|
567
|
+
<button
|
|
568
|
+
key={i}
|
|
569
|
+
type="button"
|
|
570
|
+
role="tab"
|
|
571
|
+
aria-selected={i === activeTab}
|
|
572
|
+
onClick={() => setActiveTab(i)}
|
|
573
|
+
style={{
|
|
574
|
+
padding: "0.75rem 1rem",
|
|
575
|
+
background: "none",
|
|
576
|
+
border: "none",
|
|
577
|
+
borderBottom:
|
|
578
|
+
i === activeTab
|
|
579
|
+
? "2px solid var(--cds-interactive-01, #0f62fe)"
|
|
580
|
+
: "2px solid transparent",
|
|
581
|
+
marginBottom: "-1px",
|
|
582
|
+
cursor: "pointer",
|
|
583
|
+
fontSize: "0.875rem",
|
|
584
|
+
lineHeight: "1.25rem",
|
|
585
|
+
fontWeight: i === activeTab ? 600 : 400,
|
|
586
|
+
color:
|
|
587
|
+
i === activeTab
|
|
588
|
+
? "var(--cds-text-primary, #161616)"
|
|
589
|
+
: "var(--cds-text-secondary, #525252)",
|
|
590
|
+
}}
|
|
591
|
+
>
|
|
592
|
+
{pg.label}
|
|
593
|
+
</button>
|
|
594
|
+
))}
|
|
595
|
+
</div>
|
|
596
|
+
{pages.map((pg, i) => (
|
|
516
597
|
<div
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
borderBottom: "1px solid var(--cds-border-subtle-01, #e0e0e0)",
|
|
521
|
-
marginBottom: 0,
|
|
522
|
-
}}
|
|
598
|
+
key={i}
|
|
599
|
+
role="tabpanel"
|
|
600
|
+
style={{ display: i === activeTab ? "block" : "none" }}
|
|
523
601
|
>
|
|
524
|
-
{
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
602
|
+
{/* `sublabel` dirender di dalam panel, bukan di tombol tab —
|
|
603
|
+
tombolnya cuma memuat label supaya barisnya tidak melebar.
|
|
604
|
+
Ia HARUS ada di salah satu dari keduanya: pada wizard, tempat
|
|
605
|
+
`sublabel` pernah tampil adalah steppernya, dan stepper itu
|
|
606
|
+
yang barusan dibuang. Tanpa baris ini 5 string lenyap dari
|
|
607
|
+
layar tanpa satu pun galat. */}
|
|
608
|
+
{pg.sublabel ? (
|
|
609
|
+
<div
|
|
531
610
|
style={{
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
borderBottom:
|
|
536
|
-
i === activeTab
|
|
537
|
-
? "2px solid var(--cds-interactive-01, #0f62fe)"
|
|
538
|
-
: "2px solid transparent",
|
|
539
|
-
marginBottom: "-1px",
|
|
540
|
-
cursor: "pointer",
|
|
541
|
-
fontSize: "0.875rem",
|
|
542
|
-
lineHeight: "1.25rem",
|
|
543
|
-
fontWeight: i === activeTab ? 600 : 400,
|
|
544
|
-
color:
|
|
545
|
-
i === activeTab
|
|
546
|
-
? "var(--cds-text-primary, #161616)"
|
|
547
|
-
: "var(--cds-text-secondary, #525252)",
|
|
611
|
+
fontSize: "0.75rem",
|
|
612
|
+
color: "var(--cds-text-secondary, #525252)",
|
|
613
|
+
margin: "0.75rem 0 0",
|
|
548
614
|
}}
|
|
549
615
|
>
|
|
550
|
-
{pg.
|
|
551
|
-
</
|
|
552
|
-
)
|
|
616
|
+
{pg.sublabel}
|
|
617
|
+
</div>
|
|
618
|
+
) : null}
|
|
619
|
+
<SchemaFormRenderer
|
|
620
|
+
mode={mode}
|
|
621
|
+
value={form}
|
|
622
|
+
sections={pg.sections}
|
|
623
|
+
errors={fieldErrors}
|
|
624
|
+
disabled={saving}
|
|
625
|
+
onChange={setForm}
|
|
626
|
+
density={density}
|
|
627
|
+
// `width` apa adanya, BUKAN `width ?? "full"` seperti cabang
|
|
628
|
+
// wizard ber-stepper. `full` di sana berarti "penuhi kolom
|
|
629
|
+
// KIRI", jalan keluar untuk ruang yang dipersempit stepper.
|
|
630
|
+
// Di sini nol stepper, jadi alasannya hilang dan default
|
|
631
|
+
// kontrak ("two-thirds") kembali berlaku.
|
|
632
|
+
width={width}
|
|
633
|
+
/>
|
|
553
634
|
</div>
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
635
|
+
))}
|
|
636
|
+
</div>
|
|
637
|
+
</PageBody>
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
if (layout.type === "tabs") {
|
|
641
|
+
return renderTabbedPages(layout.pages);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Wizard di luar `create`. Lihat `wizardNav`.
|
|
645
|
+
//
|
|
646
|
+
// Syaratnya ditulis LENGKAP (`layout.type === "wizard" && !wizardNav`),
|
|
647
|
+
// bukan `!wizardNav` saja. Keduanya setara hari ini, tapi bentuk pendek
|
|
648
|
+
// benar hanya karena `single-page` dan `tabs` sudah return di atas — ia
|
|
649
|
+
// cabang *sisa*, bukan cabang *wizard*. Varian layout ke-5 yang lupa
|
|
650
|
+
// ditaruh di atas sini akan ikut dirender bertab tanpa galat, tanpa lint,
|
|
651
|
+
// tanpa uji yang menahan. Dengan bentuk lengkap, kelalaian yang sama
|
|
652
|
+
// menjadi galat `tsc` di `layout.pages`.
|
|
653
|
+
if (layout.type === "wizard" && !wizardNav) {
|
|
654
|
+
return renderTabbedPages(layout.pages);
|
|
571
655
|
}
|
|
572
656
|
|
|
573
657
|
const currentPage = layout.pages[page];
|
|
@@ -640,7 +724,9 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
640
724
|
);
|
|
641
725
|
}
|
|
642
726
|
|
|
643
|
-
|
|
727
|
+
// Lanjut/Kembali hanya saat wizard benar-benar memandu — lihat `wizardNav`.
|
|
728
|
+
// Di `edit`, footer jatuh ke bentuk baku: satu tombol Simpan.
|
|
729
|
+
if (wizardNav) {
|
|
644
730
|
return (
|
|
645
731
|
<PageFooter spread>
|
|
646
732
|
<Button kind="secondary" size="sm" onClick={onBack} disabled={saving}>
|
|
@@ -657,7 +743,13 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
657
743
|
kind="primary"
|
|
658
744
|
size="sm"
|
|
659
745
|
onClick={handleSave}
|
|
660
|
-
|
|
746
|
+
// Nol penjaga `isDirty` di sini: blok ini kini hanya terjangkau
|
|
747
|
+
// saat `create` (lihat `wizardNav`), dan form baru yang belum
|
|
748
|
+
// disentuh memang belum "kotor" — memakai penjaga edit di sini
|
|
749
|
+
// akan mematikan tombol Simpan pada satu-satunya mode yang
|
|
750
|
+
// masih memakainya. tsc menandainya sebagai perbandingan yang
|
|
751
|
+
// tak mungkin benar; ia benar.
|
|
752
|
+
disabled={saving}
|
|
661
753
|
>
|
|
662
754
|
{saving ? labels.saving : submitLabel}
|
|
663
755
|
</Button>
|
package/src/crud-types.ts
CHANGED
|
@@ -56,6 +56,22 @@ export type CrudListSchema = {
|
|
|
56
56
|
columns: Record<string, unknown>;
|
|
57
57
|
popupMenuItems: CrudPopupMenuItem[];
|
|
58
58
|
addButtonLabel: string;
|
|
59
|
+
/**
|
|
60
|
+
* Tampilkan tombol "Tambah" di toolbar daftar. Default **true**.
|
|
61
|
+
*
|
|
62
|
+
* Ada karena sebagian modul menaruh entri "… Baru" di SIDE NAV, sehingga
|
|
63
|
+
* tombol di toolbar jadi jalan KEDUA ke halaman yang sama. Dua jalan ke satu
|
|
64
|
+
* halaman bukan sekadar mubazir: keduanya harus dijaga tetap sama, dan yang
|
|
65
|
+
* satu akan tertinggal saat rutenya berubah — tanpa berbunyi sebagai galat.
|
|
66
|
+
*
|
|
67
|
+
* Opsional dan default true SUPAYA 109 pemakai lain nol berubah; hanya yang
|
|
68
|
+
* menyetel `false` yang kehilangan tombolnya.
|
|
69
|
+
*
|
|
70
|
+
* ⚠️ Jangan setel `false` kalau modulnya nol punya jalur lain ke halaman
|
|
71
|
+
* create — halamannya tetap ada, hanya nol bisa dicapai siapa pun, dan itu
|
|
72
|
+
* bentuk kerusakan yang nol terlihat di uji mana pun.
|
|
73
|
+
*/
|
|
74
|
+
showAddButton?: boolean;
|
|
59
75
|
/** Enable column sorting. Defaults to true. */
|
|
60
76
|
showSort?: boolean;
|
|
61
77
|
/** Enable search bar. Defaults to true. */
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
// ServerDataTable menarik jaringan & Carbon DataTable penuh; yang diuji di sini
|
|
5
|
+
// TOOLBAR-nya, jadi tabelnya dimock dan `toolbarActions` dirender apa adanya.
|
|
6
|
+
vi.mock("@isi-ui7/data-table", () => ({
|
|
7
|
+
ServerDataTable: ({ toolbarActions }: { toolbarActions?: React.ReactNode }) => (
|
|
8
|
+
<div data-testid="tabel">{toolbarActions}</div>
|
|
9
|
+
),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { SharedCrudListPage } from "./crud-components";
|
|
13
|
+
import type { CrudListSchema } from "./crud-types";
|
|
14
|
+
|
|
15
|
+
const dasar: CrudListSchema = {
|
|
16
|
+
title: "Daftar",
|
|
17
|
+
apiPath: "/api/x",
|
|
18
|
+
columns: { a: { title: "A" } },
|
|
19
|
+
popupMenuItems: [],
|
|
20
|
+
addButtonLabel: "Tambah Sesuatu",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `showAddButton` — opsional, default TRUE.
|
|
25
|
+
*
|
|
26
|
+
* Ada karena sebagian modul menaruh entri "… Baru" di SIDE NAV, sehingga tombol
|
|
27
|
+
* toolbar jadi jalan KEDUA ke halaman yang sama. Dua jalan ke satu halaman harus
|
|
28
|
+
* dijaga tetap sama, dan yang satu akan tertinggal saat rutenya berubah — tanpa
|
|
29
|
+
* berbunyi sebagai galat.
|
|
30
|
+
*/
|
|
31
|
+
describe("tombol Tambah di daftar", () => {
|
|
32
|
+
it("DEFAULT tetap tampil — 109 pemakai lain nol boleh berubah", () => {
|
|
33
|
+
render(<SharedCrudListPage schema={dasar} onAdd={() => {}} onPopupClick={() => {}} />);
|
|
34
|
+
expect(screen.queryByRole("button", { name: /Tambah Sesuatu/ })).not.toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("`showAddButton: true` eksplisit juga tampil", () => {
|
|
38
|
+
render(
|
|
39
|
+
<SharedCrudListPage
|
|
40
|
+
schema={{ ...dasar, showAddButton: true }}
|
|
41
|
+
onAdd={() => {}}
|
|
42
|
+
onPopupClick={() => {}}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
expect(screen.queryByRole("button", { name: /Tambah Sesuatu/ })).not.toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("🔑 `showAddButton: false` MENYEMBUNYIKANNYA", () => {
|
|
49
|
+
render(
|
|
50
|
+
<SharedCrudListPage
|
|
51
|
+
schema={{ ...dasar, showAddButton: false }}
|
|
52
|
+
onAdd={() => {}}
|
|
53
|
+
onPopupClick={() => {}}
|
|
54
|
+
/>,
|
|
55
|
+
);
|
|
56
|
+
expect(screen.queryByRole("button", { name: /Tambah Sesuatu/ })).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("kontrol negatif — toolbarExtra TETAP dirender saat tombol Tambah disembunyikan", () => {
|
|
60
|
+
// Kalau perbaikannya diterapkan terlalu lebar (mis. seluruh blok toolbar
|
|
61
|
+
// dibungkus kondisi), aksi toolbar lain ikut hilang — dan hilangnya nol
|
|
62
|
+
// berbunyi karena tabelnya tetap tampil.
|
|
63
|
+
render(
|
|
64
|
+
<SharedCrudListPage
|
|
65
|
+
schema={{ ...dasar, showAddButton: false }}
|
|
66
|
+
onAdd={() => {}}
|
|
67
|
+
onPopupClick={() => {}}
|
|
68
|
+
toolbarExtra={<button type="button">Aksi Lain</button>}
|
|
69
|
+
/>,
|
|
70
|
+
);
|
|
71
|
+
expect(screen.queryByRole("button", { name: /Aksi Lain/ })).not.toBeNull();
|
|
72
|
+
expect(screen.queryByRole("button", { name: /Tambah Sesuatu/ })).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Tambalan lingkungan jsdom — BUKAN tambalan produk.
|
|
2
|
+
//
|
|
3
|
+
// jsdom nol mengimplementasikan `Element.prototype.scrollIntoView`. Carbon
|
|
4
|
+
// `ComposedModal` memanggilnya saat memindahkan fokus, jadi setiap uji yang
|
|
5
|
+
// membuka modal melempar `TypeError` di luar rantai promise uji. Vitest
|
|
6
|
+
// menghitungnya sebagai *unhandled error*, bukan sebagai uji gagal — hasilnya
|
|
7
|
+
// `Tests 108 passed` berdampingan dengan `Errors 11` dan **exit code 1**.
|
|
8
|
+
//
|
|
9
|
+
// 🔑 Itu bentuk kegagalan yang paling mudah dilaporkan keliru: ringkasan yang
|
|
10
|
+
// dibaca manusia berbunyi hijau sepenuhnya, sementara gerbang CI yang membaca
|
|
11
|
+
// exit code menolak paket ini. Sesi yang menambahkan tambalan ini sempat
|
|
12
|
+
// melaporkan "105/105 lulus" atas suite yang MERAH, dan baru tahu setelah
|
|
13
|
+
// verifikasi adversarial memeriksa exit code-nya secara terpisah.
|
|
14
|
+
//
|
|
15
|
+
// Fungsinya no-op: yang diuji adalah apa yang terender, bukan apakah browser
|
|
16
|
+
// menggulir. Menambal di lapisan lingkungan menjaga kode produk tetap bersih
|
|
17
|
+
// dari pengetahuan tentang jsdom.
|
|
18
|
+
if (typeof Element !== "undefined" && !Element.prototype.scrollIntoView) {
|
|
19
|
+
Element.prototype.scrollIntoView = function scrollIntoView() {
|
|
20
|
+
/* no-op: jsdom nol punya viewport untuk digulir */
|
|
21
|
+
};
|
|
22
|
+
}
|