@isi-ui7/bos7-shared 0.3.10 → 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/package.json +8 -8
- package/src/crud-components.tsx +106 -118
- package/src/crud-types.ts +16 -0
- package/src/list-add-button.test.tsx +74 -0
- package/src/wizard-mode-nav.test.tsx +58 -21
package/dist/crud-types.d.ts
CHANGED
|
@@ -75,6 +75,22 @@ export type CrudListSchema = {
|
|
|
75
75
|
columns: Record<string, unknown>;
|
|
76
76
|
popupMenuItems: CrudPopupMenuItem[];
|
|
77
77
|
addButtonLabel: string;
|
|
78
|
+
/**
|
|
79
|
+
* Tampilkan tombol "Tambah" di toolbar daftar. Default **true**.
|
|
80
|
+
*
|
|
81
|
+
* Ada karena sebagian modul menaruh entri "… Baru" di SIDE NAV, sehingga
|
|
82
|
+
* tombol di toolbar jadi jalan KEDUA ke halaman yang sama. Dua jalan ke satu
|
|
83
|
+
* halaman bukan sekadar mubazir: keduanya harus dijaga tetap sama, dan yang
|
|
84
|
+
* satu akan tertinggal saat rutenya berubah — tanpa berbunyi sebagai galat.
|
|
85
|
+
*
|
|
86
|
+
* Opsional dan default true SUPAYA 109 pemakai lain nol berubah; hanya yang
|
|
87
|
+
* menyetel `false` yang kehilangan tombolnya.
|
|
88
|
+
*
|
|
89
|
+
* ⚠️ Jangan setel `false` kalau modulnya nol punya jalur lain ke halaman
|
|
90
|
+
* create — halamannya tetap ada, hanya nol bisa dicapai siapa pun, dan itu
|
|
91
|
+
* bentuk kerusakan yang nol terlihat di uji mana pun.
|
|
92
|
+
*/
|
|
93
|
+
showAddButton?: boolean;
|
|
78
94
|
/** Enable column sorting. Defaults to true. */
|
|
79
95
|
showSort?: boolean;
|
|
80
96
|
/** Enable search bar. Defaults to true. */
|
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": {
|
|
@@ -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.
|
|
73
|
+
"@isi-ui7/corporate-themes": ">=0.2.8",
|
|
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.3",
|
|
@@ -106,14 +106,14 @@
|
|
|
106
106
|
"vite": "^5.0.0",
|
|
107
107
|
"vite-plugin-dts": "^3.6.0",
|
|
108
108
|
"vitest": "^1.0.0",
|
|
109
|
-
"@isi-ui7/corporate-themes": "0.2.
|
|
109
|
+
"@isi-ui7/corporate-themes": "0.2.8",
|
|
110
110
|
"@isi-ui7/data-table": "0.2.5",
|
|
111
111
|
"@isi-ui7/editable-table": "0.2.3",
|
|
112
|
-
"@isi-ui7/i18n": "0.2.3",
|
|
113
112
|
"@isi-ui7/lookup-input": "0.2.4",
|
|
114
113
|
"@isi-ui7/modal-manager": "0.2.2",
|
|
115
|
-
"@isi-ui7/
|
|
116
|
-
"@isi-ui7/realtime": "0.2.3"
|
|
114
|
+
"@isi-ui7/i18n": "0.2.3",
|
|
115
|
+
"@isi-ui7/realtime": "0.2.3",
|
|
116
|
+
"@isi-ui7/ui-shell": "0.2.8"
|
|
117
117
|
},
|
|
118
118
|
"dependencies": {
|
|
119
119
|
"@carbon/icons-react": "^11.71.0",
|
|
@@ -127,8 +127,8 @@
|
|
|
127
127
|
"jspdf": "^4.2.1",
|
|
128
128
|
"jspdf-autotable": "^5.0.7",
|
|
129
129
|
"@isi-ui7/data-table": "0.2.5",
|
|
130
|
-
"@isi-ui7/
|
|
131
|
-
"@isi-ui7/
|
|
130
|
+
"@isi-ui7/lookup-input": "0.2.4",
|
|
131
|
+
"@isi-ui7/editable-table": "0.2.3"
|
|
132
132
|
},
|
|
133
133
|
"scripts": {
|
|
134
134
|
"build": "vite build",
|
package/src/crud-components.tsx
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { ServerDataTable } from "@isi-ui7/data-table";
|
|
20
20
|
import type { I_DataTblColumn } from "@isi-ui7/data-table";
|
|
21
21
|
import type { CrudCustomActionSchema, CrudDeleteSchema, CrudListSchema } from "./crud-types";
|
|
22
|
-
import type { CrudForm, FormMode } from "./form-types";
|
|
22
|
+
import type { CrudForm, FormMode, FormPageDef } from "./form-types";
|
|
23
23
|
import type { Ui7FormDensity } from "./style-contract";
|
|
24
24
|
import { SchemaFormRenderer } from "./form-renderer";
|
|
25
25
|
import { WizardStepper, type WizardStepState } from "./wizard-stepper";
|
|
@@ -134,9 +134,11 @@ export function SharedCrudListPage({
|
|
|
134
134
|
</div>
|
|
135
135
|
) : null}
|
|
136
136
|
{toolbarExtra}
|
|
137
|
-
|
|
138
|
-
{
|
|
139
|
-
|
|
137
|
+
{(schema.showAddButton ?? true) ? (
|
|
138
|
+
<Button renderIcon={Add} size="sm" onClick={onAdd}>
|
|
139
|
+
{schema.addButtonLabel || labels.next}
|
|
140
|
+
</Button>
|
|
141
|
+
) : null}
|
|
140
142
|
</div>
|
|
141
143
|
}
|
|
142
144
|
/>
|
|
@@ -501,15 +503,21 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
501
503
|
* dilewati: footer mode-view sengaja nol tombol Lanjut, jadi satu-satunya
|
|
502
504
|
* jalan maju adalah menebak bahwa stepper-nya bisa diklik.
|
|
503
505
|
*
|
|
504
|
-
* Karena itu di luar `create` wizard dirender
|
|
505
|
-
*
|
|
506
|
-
* Yang HILANG hanya
|
|
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.)
|
|
507
515
|
*
|
|
508
516
|
* Aman terhadap validasi: `handleSave` memakai `validateAll()`, yang sudah
|
|
509
517
|
* mengumpulkan field dari SELURUH halaman tanpa peduli halaman aktif —
|
|
510
|
-
* jadi menyimpan dari bentuk
|
|
518
|
+
* jadi menyimpan dari bentuk bertab memeriksa hal yang sama persis.
|
|
511
519
|
*
|
|
512
|
-
* Keputusan PM 2026-08-19. Sengaja dipasang di sini, BUKAN di skema tiap
|
|
520
|
+
* Keputusan PM 2026-08-19, direvisi 2026-08-20. Sengaja dipasang di sini, BUKAN di skema tiap
|
|
513
521
|
* app: ini keputusan tentang cara me-render sebuah layout, dan menaruhnya
|
|
514
522
|
* di skema berarti tiap app harus mengingat untuk mengulanginya.
|
|
515
523
|
*/
|
|
@@ -534,117 +542,80 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
534
542
|
);
|
|
535
543
|
}
|
|
536
544
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
marginBottom: "-1px",
|
|
566
|
-
cursor: "pointer",
|
|
567
|
-
fontSize: "0.875rem",
|
|
568
|
-
lineHeight: "1.25rem",
|
|
569
|
-
fontWeight: i === activeTab ? 600 : 400,
|
|
570
|
-
color:
|
|
571
|
-
i === activeTab
|
|
572
|
-
? "var(--cds-text-primary, #161616)"
|
|
573
|
-
: "var(--cds-text-secondary, #525252)",
|
|
574
|
-
}}
|
|
575
|
-
>
|
|
576
|
-
{pg.label}
|
|
577
|
-
</button>
|
|
578
|
-
))}
|
|
579
|
-
</div>
|
|
580
|
-
{layout.pages.map((pg, i) => (
|
|
581
|
-
<div key={i} style={{ display: i === activeTab ? "block" : "none" }}>
|
|
582
|
-
<SchemaFormRenderer
|
|
583
|
-
mode={mode}
|
|
584
|
-
value={form}
|
|
585
|
-
sections={pg.sections}
|
|
586
|
-
errors={fieldErrors}
|
|
587
|
-
disabled={saving}
|
|
588
|
-
onChange={setForm}
|
|
589
|
-
density={density}
|
|
590
|
-
width={width}
|
|
591
|
-
/>
|
|
592
|
-
</div>
|
|
593
|
-
))}
|
|
594
|
-
</div>
|
|
595
|
-
</PageBody>
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Wizard di luar `create` — datar, tanpa stepper. Lihat `wizardNav`.
|
|
600
|
-
//
|
|
601
|
-
// Syaratnya ditulis LENGKAP (`layout.type === "wizard" && !wizardNav`),
|
|
602
|
-
// bukan `!wizardNav` saja. Keduanya setara hari ini, tapi bentuk pendek
|
|
603
|
-
// benar hanya karena `single-page` dan `tabs` sudah return di atas — ia
|
|
604
|
-
// cabang *sisa*, bukan cabang *wizard*. Varian layout ke-5 yang lupa
|
|
605
|
-
// ditaruh di atas sini akan ikut dirender datar tanpa galat, tanpa lint,
|
|
606
|
-
// tanpa uji yang menahan. Dengan bentuk lengkap, kelalaian yang sama
|
|
607
|
-
// menjadi galat `tsc` di `layout.pages` beberapa baris di bawah.
|
|
608
|
-
if (layout.type === "wizard" && !wizardNav) {
|
|
609
|
-
return (
|
|
610
|
-
<PageBody>
|
|
611
|
-
{errBanner}
|
|
612
|
-
{layout.pages.map((pg, i) => (
|
|
613
|
-
<div key={i} style={{ marginBottom: "2.5rem" }}>
|
|
614
|
-
{/* Label halaman tetap dirender — sebagai JUDUL KELOMPOK, bukan
|
|
615
|
-
navigasi. Membuang steppernya tidak boleh ikut membuang satu-
|
|
616
|
-
satunya tempat `pg.label`/`pg.sublabel` pernah tampil: pada
|
|
617
|
-
form 5 langkah, hasilnya 13 panel beruntun tanpa satu pun
|
|
618
|
-
pemisah tingkat-halaman. Yang dilarang PM adalah navigasinya,
|
|
619
|
-
bukan judulnya. */}
|
|
620
|
-
<div
|
|
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)}
|
|
621
573
|
style={{
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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)",
|
|
625
590
|
}}
|
|
626
591
|
>
|
|
592
|
+
{pg.label}
|
|
593
|
+
</button>
|
|
594
|
+
))}
|
|
595
|
+
</div>
|
|
596
|
+
{pages.map((pg, i) => (
|
|
597
|
+
<div
|
|
598
|
+
key={i}
|
|
599
|
+
role="tabpanel"
|
|
600
|
+
style={{ display: i === activeTab ? "block" : "none" }}
|
|
601
|
+
>
|
|
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 ? (
|
|
627
609
|
<div
|
|
628
610
|
style={{
|
|
629
|
-
fontSize: "
|
|
630
|
-
|
|
631
|
-
|
|
611
|
+
fontSize: "0.75rem",
|
|
612
|
+
color: "var(--cds-text-secondary, #525252)",
|
|
613
|
+
margin: "0.75rem 0 0",
|
|
632
614
|
}}
|
|
633
615
|
>
|
|
634
|
-
{pg.
|
|
616
|
+
{pg.sublabel}
|
|
635
617
|
</div>
|
|
636
|
-
|
|
637
|
-
<div
|
|
638
|
-
style={{
|
|
639
|
-
fontSize: "0.75rem",
|
|
640
|
-
color: "var(--cds-text-secondary, #525252)",
|
|
641
|
-
marginTop: "0.125rem",
|
|
642
|
-
}}
|
|
643
|
-
>
|
|
644
|
-
{pg.sublabel}
|
|
645
|
-
</div>
|
|
646
|
-
) : null}
|
|
647
|
-
</div>
|
|
618
|
+
) : null}
|
|
648
619
|
<SchemaFormRenderer
|
|
649
620
|
mode={mode}
|
|
650
621
|
value={form}
|
|
@@ -654,16 +625,33 @@ export function CrudSchemaPage<TData extends Record<string, unknown>>({
|
|
|
654
625
|
onChange={setForm}
|
|
655
626
|
density={density}
|
|
656
627
|
// `width` apa adanya, BUKAN `width ?? "full"` seperti cabang
|
|
657
|
-
// wizard. `full` di sana berarti "penuhi kolom
|
|
658
|
-
// keluar untuk ruang yang dipersempit stepper.
|
|
659
|
-
// stepper, jadi alasannya hilang dan default
|
|
660
|
-
// ("two-thirds") kembali berlaku.
|
|
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.
|
|
661
632
|
width={width}
|
|
662
633
|
/>
|
|
663
634
|
</div>
|
|
664
635
|
))}
|
|
665
|
-
</
|
|
666
|
-
|
|
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);
|
|
667
655
|
}
|
|
668
656
|
|
|
669
657
|
const currentPage = layout.pages[page];
|
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
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
|
3
3
|
|
|
4
4
|
// Dimock supaya mode `edit`/`view` tidak menggantung di InlineLoading: tanpa
|
|
5
5
|
// `loadApiPath` yang selesai, `loading` tidak pernah kembali false dan yang
|
|
@@ -40,6 +40,27 @@ const stepper = () => screen.queryByRole("navigation");
|
|
|
40
40
|
// ketiga mode, sehingga uji ini membandingkan hal yang sama di semua mode.
|
|
41
41
|
const fieldTampil = (nama: string) => screen.queryByText(nama) !== null;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* ADA DI DOM ≠ TERLIHAT.
|
|
45
|
+
*
|
|
46
|
+
* Panel tab yang tidak aktif tetap dirender, hanya `display: none`. Jadi
|
|
47
|
+
* `queryByText` menemukannya, dan uji yang hanya bertanya "ada?" akan HIJAU
|
|
48
|
+
* untuk halaman yang tidak bisa dilihat siapa pun. Helper ini menaiki pohon ke
|
|
49
|
+
* `[role=tabpanel]` terdekat dan membaca `display`-nya, sehingga yang diukur
|
|
50
|
+
* adalah apa yang benar-benar tampil di layar.
|
|
51
|
+
*
|
|
52
|
+
* `null` = teksnya tidak ada sama sekali (beda dari "ada tapi tersembunyi").
|
|
53
|
+
*/
|
|
54
|
+
const panelTampil = (nama: string): boolean | null => {
|
|
55
|
+
const el = screen.queryByText(nama);
|
|
56
|
+
if (!el) return null;
|
|
57
|
+
const panel = el.closest('[role="tabpanel"]') as HTMLElement | null;
|
|
58
|
+
if (!panel) return true; // bukan layout bertab — terender langsung
|
|
59
|
+
return panel.style.display !== "none";
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const tabs = () => screen.queryAllByRole("tab");
|
|
63
|
+
|
|
43
64
|
beforeEach(() => {
|
|
44
65
|
vi.clearAllMocks();
|
|
45
66
|
});
|
|
@@ -55,17 +76,22 @@ describe("navigasi wizard hanya pada mode create", () => {
|
|
|
55
76
|
expect(fieldTampil("Field Tiga")).toBe(false);
|
|
56
77
|
});
|
|
57
78
|
|
|
58
|
-
it("view — stepper TIDAK ada, dan
|
|
79
|
+
it("view — stepper TIDAK ada, diganti TAB, dan tiap halaman terjangkau", async () => {
|
|
59
80
|
render(<CrudSchemaPage schema={schema} mode="view" />);
|
|
60
81
|
await waitFor(() => expect(fieldTampil("Field Satu")).toBe(true));
|
|
61
82
|
|
|
62
83
|
expect(stepper(), "view tidak boleh punya navigasi langkah").toBeNull();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
expect(tabs().length, "wizard di luar create harus jadi tab, bukan datar").toBe(3);
|
|
85
|
+
|
|
86
|
+
// Inti perbaikannya: nol stepper TIDAK boleh berarti isi yang tak
|
|
87
|
+
// terjangkau. Sebelum ini `view` merender satu halaman saja sementara
|
|
88
|
+
// footernya nol tombol Lanjut — langkah 2-5 tidak bisa dibaca sama sekali.
|
|
89
|
+
expect(panelTampil("Field Satu")).toBe(true);
|
|
90
|
+
expect(panelTampil("Field Tiga"), "halaman 3 belum dibuka").toBe(false);
|
|
91
|
+
|
|
92
|
+
fireEvent.click(screen.getByRole("tab", { name: "Langkah Tiga" }));
|
|
93
|
+
expect(panelTampil("Field Tiga"), "klik tab 3 harus menampilkannya").toBe(true);
|
|
94
|
+
expect(panelTampil("Field Satu"), "tab 1 harus ikut tertutup").toBe(false);
|
|
69
95
|
});
|
|
70
96
|
|
|
71
97
|
// Ditambahkan setelah verifikasi adversarial membantah klaim "nol isi yang
|
|
@@ -79,12 +105,20 @@ describe("navigasi wizard hanya pada mode create", () => {
|
|
|
79
105
|
render(<CrudSchemaPage schema={schema} mode={mode} />);
|
|
80
106
|
await waitFor(() => expect(fieldTampil("Field Satu")).toBe(true));
|
|
81
107
|
|
|
82
|
-
expect(stepper(), "
|
|
108
|
+
expect(stepper(), "tab bukan landmark navigasi").toBeNull();
|
|
109
|
+
// Label halaman kini jadi teks tombol tab.
|
|
83
110
|
for (const l of ["Langkah Satu", "Langkah Dua", "Langkah Tiga"]) {
|
|
84
|
-
expect(
|
|
111
|
+
expect(
|
|
112
|
+
tabs().some((t) => t.textContent === l),
|
|
113
|
+
`${l} hilang dari ${mode}`,
|
|
114
|
+
).toBe(true);
|
|
85
115
|
}
|
|
86
|
-
|
|
87
|
-
|
|
116
|
+
// Sublabel TIDAK muat di tombol tab, jadi ia pindah ke dalam panel.
|
|
117
|
+
// Diperiksa terpisah justru karena itu: tempatnya berbeda dari labelnya,
|
|
118
|
+
// dan tempat yang berbeda adalah tempat yang bisa terlupa.
|
|
119
|
+
for (const [i, sub] of ["Sub Satu", "Sub Dua", "Sub Tiga"].entries()) {
|
|
120
|
+
fireEvent.click(tabs()[i]);
|
|
121
|
+
expect(panelTampil(sub), `sublabel ${sub} hilang dari ${mode}`).toBe(true);
|
|
88
122
|
}
|
|
89
123
|
},
|
|
90
124
|
);
|
|
@@ -94,19 +128,22 @@ describe("navigasi wizard hanya pada mode create", () => {
|
|
|
94
128
|
await waitFor(() => expect(fieldTampil("Field Satu")).toBe(true));
|
|
95
129
|
|
|
96
130
|
expect(stepper(), "edit tidak boleh punya navigasi langkah").toBeNull();
|
|
97
|
-
expect(
|
|
131
|
+
expect(tabs().length).toBe(3);
|
|
132
|
+
fireEvent.click(screen.getByRole("tab", { name: "Langkah Tiga" }));
|
|
133
|
+
expect(panelTampil("Field Tiga")).toBe(true);
|
|
98
134
|
|
|
99
135
|
const tombol = screen.getAllByRole("button").map((b) => b.textContent ?? "");
|
|
100
136
|
expect(tombol.some((t) => /lanjut|next/i.test(t)), `masih ada tombol Lanjut: ${tombol.join("|")}`).toBe(false);
|
|
101
137
|
expect(tombol.some((t) => /kembali|prev/i.test(t)), `masih ada tombol Kembali: ${tombol.join("|")}`).toBe(false);
|
|
102
138
|
});
|
|
103
139
|
|
|
104
|
-
// Kontrol
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
// Kontrol negatif — kini SEMAKIN perlu, bukan semakin tidak: sejak kedua
|
|
141
|
+
// layout memakai perender yang SAMA, cacat di perender itu memukul keduanya
|
|
142
|
+
// sekaligus, dan uji wizard sendirian tidak akan menunjukkan bahwa 4 layar
|
|
143
|
+
// bos7-enterprise/bos7-financing ikut rusak. `tabs` di sini punya 2 halaman
|
|
144
|
+
// (wizard 3) supaya keduanya tidak bisa saling menyamar.
|
|
145
|
+
it("kontrol negatif — layout tabs ikut sehat memakai perender bersama", async () => {
|
|
146
|
+
const skemaTabs: CrudForm<D> = {
|
|
110
147
|
...schema,
|
|
111
148
|
layout: {
|
|
112
149
|
type: "tabs",
|
|
@@ -116,11 +153,11 @@ describe("navigasi wizard hanya pada mode create", () => {
|
|
|
116
153
|
],
|
|
117
154
|
},
|
|
118
155
|
};
|
|
119
|
-
render(<CrudSchemaPage schema={
|
|
156
|
+
render(<CrudSchemaPage schema={skemaTabs} mode="view" />);
|
|
120
157
|
await waitFor(() => expect(fieldTampil("Field Satu")).toBe(true));
|
|
121
158
|
|
|
122
159
|
// Tab masih tab: tombol role=tab ada, dan hanya tab aktif yang terlihat.
|
|
123
|
-
expect(
|
|
160
|
+
expect(tabs().length, "layout tabs kehilangan tabnya").toBe(2);
|
|
124
161
|
expect(stepper()).toBeNull();
|
|
125
162
|
});
|
|
126
163
|
|