@rsmlamongan/user-menu 0.0.0-stage → 0.1.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/LICENSE +61 -0
- package/README.md +273 -2
- package/dist/components/UserMenuHost.vue.d.ts +33 -0
- package/dist/errors.d.ts +6 -0
- package/dist/http.d.ts +17 -0
- package/dist/http.js +70 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +631 -0
- package/dist/mock.d.ts +11 -0
- package/dist/mock.js +58 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/state.d.ts +56 -0
- package/dist/style.css +2 -0
- package/dist/types.d.ts +52 -0
- package/dist/validation-ZwudKvW7.js +49 -0
- package/dist/validation.d.ts +5 -0
- package/docs/API-CONTRACT.md +221 -0
- package/docs/DEVELOPMENT.md +258 -0
- package/docs/RELEASING.md +114 -0
- package/package.json +92 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
LISENSI PROPRIETARY RSML USER MENU
|
|
2
|
+
|
|
3
|
+
Hak Cipta (c) 2026 Rumah Sakit Muhammadiyah Lamongan.
|
|
4
|
+
Seluruh hak yang tidak diberikan secara tegas tetap dilindungi.
|
|
5
|
+
|
|
6
|
+
1. Pemegang hak dan ruang lingkup
|
|
7
|
+
|
|
8
|
+
"RSML" berarti Rumah Sakit Muhammadiyah Lamongan. "Perangkat Lunak"
|
|
9
|
+
berarti kode, hasil build, dan dokumentasi RSML User Menu yang dibuat
|
|
10
|
+
oleh atau untuk RSML. Lisensi ini bukan lisensi open source.
|
|
11
|
+
|
|
12
|
+
2. Penggunaan untuk RSML
|
|
13
|
+
|
|
14
|
+
RSML berhak menggunakan, memasang, menyalin, mengembangkan, memodifikasi,
|
|
15
|
+
dan mengintegrasikan Perangkat Lunak dalam aplikasi yang dimiliki atau
|
|
16
|
+
dioperasikan untuk RSML. Pegawai, personel, dan kontraktor yang diberi
|
|
17
|
+
wewenang oleh RSML dapat menjalankan kegiatan tersebut semata-mata
|
|
18
|
+
atas nama dan untuk kepentingan RSML, selama kewenangan itu berlaku.
|
|
19
|
+
|
|
20
|
+
Izin tersebut mencakup pembuatan salinan untuk development, pengujian,
|
|
21
|
+
build, deployment, dan pemeliharaan aplikasi RSML.
|
|
22
|
+
|
|
23
|
+
3. Distribusi resmi dan pengguna aplikasi
|
|
24
|
+
|
|
25
|
+
RSML dan maintainer yang diberi wewenang dapat menerbitkan Perangkat
|
|
26
|
+
Lunak melalui kanal distribusi resmi, termasuk npm dan GitHub. Penyedia
|
|
27
|
+
kanal tersebut diperbolehkan menyimpan, menyalin, menganalisis, dan
|
|
28
|
+
mendistribusikan salinan untuk menyediakan layanan distribusi resmi
|
|
29
|
+
sesuai ketentuan layanan yang disetujui RSML.
|
|
30
|
+
|
|
31
|
+
RSML dapat menyertakan Perangkat Lunak dalam bundle JavaScript dan
|
|
32
|
+
mengirimkannya kepada pengguna aplikasi RSML. Pengguna aplikasi diberi
|
|
33
|
+
izin menjalankan salinan tersebut sebatas penggunaan aplikasi RSML.
|
|
34
|
+
Izin ini tidak memberikan hak untuk memakai Perangkat Lunak dalam
|
|
35
|
+
aplikasi pihak lain atau mendistribusikannya sebagai library tersendiri.
|
|
36
|
+
|
|
37
|
+
4. Pembatasan untuk pihak lain
|
|
38
|
+
|
|
39
|
+
Tersedianya package untuk diunduh secara publik tidak memberikan izin
|
|
40
|
+
penggunaan untuk kepentingan pihak lain. Tanpa izin tertulis dari RSML,
|
|
41
|
+
pihak lain tidak diperbolehkan menggunakan, memodifikasi, mengintegrasikan,
|
|
42
|
+
menjual, memberikan sublisensi, atau mendistribusikan Perangkat Lunak
|
|
43
|
+
maupun turunannya untuk kepentingannya sendiri atau pihak selain RSML.
|
|
44
|
+
|
|
45
|
+
Pernyataan hak cipta dan lisensi harus dipertahankan pada salinan yang
|
|
46
|
+
didistribusikan melalui kanal resmi. Tidak ada hak atas nama atau merek
|
|
47
|
+
RSML yang diberikan oleh lisensi ini.
|
|
48
|
+
|
|
49
|
+
5. Komponen pihak ketiga
|
|
50
|
+
|
|
51
|
+
Lisensi ini hanya berlaku atas materi yang haknya dimiliki RSML.
|
|
52
|
+
Komponen pihak ketiga tetap mengikuti hak cipta dan lisensinya
|
|
53
|
+
masing-masing; lisensi ini tidak membatasi hak yang diberikan oleh
|
|
54
|
+
lisensi komponen pihak ketiga tersebut.
|
|
55
|
+
|
|
56
|
+
6. Jaminan dan tanggung jawab
|
|
57
|
+
|
|
58
|
+
Perangkat Lunak disediakan "sebagaimana adanya", tanpa jaminan tegas
|
|
59
|
+
atau tersirat sejauh diperbolehkan oleh hukum yang berlaku. Sejauh
|
|
60
|
+
diperbolehkan oleh hukum yang berlaku, RSML tidak bertanggung jawab
|
|
61
|
+
atas kerugian yang timbul dari penggunaan Perangkat Lunak.
|
package/README.md
CHANGED
|
@@ -1,3 +1,274 @@
|
|
|
1
|
-
#
|
|
1
|
+
# RSML User Menu
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Library Vue + Vuetify untuk drawer pengguna di sisi kanan aplikasi. Pengguna dapat menyimpan halaman aktif sebagai favorit, membuka shortcut lintas aplikasi, menghapus favorit, dan menyimpan urutannya.
|
|
4
|
+
|
|
5
|
+
**Status: frontend, mock persisten, dan adapter HTTP tersedia.** Backend User Menu diimplementasikan dalam source kepegawaian-api. Identitas package adalah `@rsmlamongan/user-menu@0.0.0`, dengan lisensi proprietary milik Rumah Sakit Muhammadiyah Lamongan. Pemasangan tabel, deployment/integrasi produksi, eksekusi GitHub Actions, bootstrap npm, dan publikasi OIDC belum dibuktikan.
|
|
6
|
+
|
|
7
|
+
## Development lokal
|
|
8
|
+
|
|
9
|
+
Gunakan Bun 1.4.2 dan Node.js `^22.18.0 || >=24.12.0`. Target peer dependencies adalah Vue `>=3.5.42 <4` dan Vuetify `>=4.2.0 <5`. Pengujian lokal menggunakan versi yang dikunci dalam `bun.lock`, bukan seluruh kemungkinan versi pada rentang tersebut. Vuetify 3 belum didukung.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun install --frozen-lockfile
|
|
13
|
+
bun run dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Buka [playground](http://127.0.0.1:3000), lalu klik avatar di kanan atas. Playground menyediakan dua akun sintetis/logout, metadata halaman, tema terang/gelap, callback pengaturan, latensi, dan kegagalan operasi berikutnya. Favorit Ayu mencakup dua aplikasi; Bima mulai dengan daftar kosong.
|
|
17
|
+
|
|
18
|
+
| Perintah | Kegunaan |
|
|
19
|
+
| ----------------------------- | --------------------------------------------------------------------------------- |
|
|
20
|
+
| `bun run type-check` | Memeriksa library, playground, dan tooling/tes. |
|
|
21
|
+
| `bun run lint:check` | Oxlint dan ESLint mengikuti baseline tim. |
|
|
22
|
+
| `bun run format:check` | Memeriksa format Prettier. |
|
|
23
|
+
| `bun run format` | Menerapkan format. |
|
|
24
|
+
| `bun run test:unit` | Vitest, Vue Test Utils, dan jsdom. |
|
|
25
|
+
| `bun run test:release` | Guard versi/tag, checksum, registry, dan pemulihan GitHub Release dengan fixture. |
|
|
26
|
+
| `bun run test:e2e` | Chromium desktop/mobile terhadap development server. |
|
|
27
|
+
| `bun run build` | ESM, CSS, serta deklarasi tipe ke `dist/`. |
|
|
28
|
+
| `bun run build:playground` | Aplikasi contoh ke `playground-dist/`. |
|
|
29
|
+
| `bun run test:e2e:production` | Chromium terhadap hasil build playground. |
|
|
30
|
+
| `bun run preview` | Preview playground di `http://127.0.0.1:4173`. |
|
|
31
|
+
| `bun run verify:package` | Pack, audit isi, install consumer terpisah, type-check, build, dan smoke browser. |
|
|
32
|
+
|
|
33
|
+
Pasang browser untuk pengujian pada mesin baru dengan `bunx playwright install chromium`. Tes memakai data contoh dan browser context terpisah. `verify:package` membutuhkan perintah `tar`. Instalasi consumer pertama memakai cache Bun bila tersedia dan dapat mengunduh metadata atau dependency yang belum ada; instalasi kedua memakai lockfile hasilnya dalam mode frozen dan offline. Hasilnya disimpan di `.cache/`.
|
|
34
|
+
|
|
35
|
+
Konfigurasi disesuaikan dari `rsml-boilerplate-web`: Bun, TypeScript, import sorting, Oxlint/ESLint, Prettier, dan pola tooling tes tim dipertahankan. Router, Pinia, HTTP client, token, dan instance Vuetify tidak dibuat oleh library.
|
|
36
|
+
|
|
37
|
+
## Instalasi lokal dan rilis berikutnya
|
|
38
|
+
|
|
39
|
+
Setelah build dan pemasangan Chromium, verifikasi tarball development:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bun run build
|
|
43
|
+
bun run verify:package
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Salin tarball ke aplikasi contoh, lalu pilih salah satu perintah berikut. Path ini adalah contoh lokasi tarball lokal, bukan package yang sudah tersedia di npm:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bun add --exact ./npm-package.tgz
|
|
50
|
+
npm install --save-exact ./npm-package.tgz
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Verifier menghasilkan `.cache/npm-package.tgz` dan `.cache/SHA512SUMS`. Setelah versi yang dituju benar-benar diterbitkan, instal melalui registry; `0.1.0` berikut adalah **contoh versi rilis pertama**, bukan klaim bahwa versi tersebut sudah tersedia:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bun add --exact @rsmlamongan/user-menu@0.1.0
|
|
57
|
+
npm install --save-exact @rsmlamongan/user-menu@0.1.0
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Instalasi package publik tidak memerlukan token registry; publikasi tetap hanya oleh maintainer berwenang. Setiap aplikasi memilih versi pasti dan memperbarui lockfile, build, serta deployment sendiri. Tidak ada update otomatis pada aplikasi ter-deploy.
|
|
61
|
+
|
|
62
|
+
Package publik tetap dapat diunduh siapa pun. [Lisensi](LICENSE) membatasi penggunaan untuk aplikasi RSML dan personel yang bertindak atas nama RSML; penggunaan untuk kepentingan pihak lain memerlukan izin tertulis. [Panduan rilis](docs/RELEASING.md) menjelaskan akun, bootstrap satu kali, pengaturan OIDC, push tag, dan pemulihan kegagalan.
|
|
63
|
+
|
|
64
|
+
## Integrasi
|
|
65
|
+
|
|
66
|
+
Contoh memakai identitas package `@rsmlamongan/user-menu`, yang dapat dipasang dari tarball lokal sebelum publikasi pertama. Aplikasi sudah memiliki Vue, Vuetify beserta `vuetify/styles`, dan sesi auth sendiri. Tiga fungsi dari `./user-menu-integration` merupakan adapter yang dibuat oleh aplikasi pemakai, bukan file bawaan package.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { createUserMenu } from "@rsmlamongan/user-menu";
|
|
70
|
+
import { createMockUserMenuApi } from "@rsmlamongan/user-menu/mock";
|
|
71
|
+
import "@rsmlamongan/user-menu/style.css";
|
|
72
|
+
|
|
73
|
+
import { getCurrentPage, getSession, openUserSettings } from "./user-menu-integration";
|
|
74
|
+
|
|
75
|
+
// Development saja. Gunakan adapter HTTP berikut untuk backend kepegawaian-api.
|
|
76
|
+
const api = createMockUserMenuApi({ getSession });
|
|
77
|
+
|
|
78
|
+
app.use(
|
|
79
|
+
createUserMenu({
|
|
80
|
+
appId: "inventaris",
|
|
81
|
+
getSession,
|
|
82
|
+
getCurrentPage,
|
|
83
|
+
api,
|
|
84
|
+
settings: { label: "Pengaturan pengguna", onClick: openUserSettings },
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`getSession()` harus membaca state auth reaktif dan mengembalikan `UserMenuSession | null`. `key` membedakan siklus login dan bukan token. `getCurrentPage()` menyediakan judul serta URL HTTP(S) absolut terbaru, termasuk query/hash yang layak disimpan. Pengaturan juga dapat memakai `{ label, url }`.
|
|
90
|
+
|
|
91
|
+
Untuk backend, gunakan ekspor `/http` dengan HTTP client aplikasi yang sudah menangani token dan refresh:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { createHttpUserMenuApi } from "@rsmlamongan/user-menu/http";
|
|
95
|
+
|
|
96
|
+
const api = createHttpUserMenuApi({
|
|
97
|
+
baseUrl: "/api/v1", // Sesuaikan prefix proxy aplikasi.
|
|
98
|
+
request: async ({ method, url, body }) => {
|
|
99
|
+
const result = await client.request({
|
|
100
|
+
method,
|
|
101
|
+
url,
|
|
102
|
+
data: body,
|
|
103
|
+
validateStatus: () => true,
|
|
104
|
+
});
|
|
105
|
+
return { status: result.status, body: result.data };
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`client` adalah instance Axios milik aplikasi, bukan bawaan library. Pasang `api` ini pada konfigurasi plugin yang sama. Callback harus resolve juga untuk HTTP error; adapter membaca kode logis dalam envelope `RestResource`. Lihat [kontrak HTTP dan pemasangan backend](docs/API-CONTRACT.md#adapter-http).
|
|
111
|
+
|
|
112
|
+
Pasang satu host di dalam `v-app` yang sudah ada dan hubungkan avatar aplikasi:
|
|
113
|
+
|
|
114
|
+
```vue
|
|
115
|
+
<script setup lang="ts">
|
|
116
|
+
import { ref } from "vue";
|
|
117
|
+
|
|
118
|
+
import { UserMenuHost } from "@rsmlamongan/user-menu";
|
|
119
|
+
|
|
120
|
+
const menuOpen = ref(false);
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<template>
|
|
124
|
+
<v-app>
|
|
125
|
+
<v-app-bar>
|
|
126
|
+
<v-spacer />
|
|
127
|
+
<v-btn
|
|
128
|
+
icon
|
|
129
|
+
variant="text"
|
|
130
|
+
aria-label="Buka menu pengguna"
|
|
131
|
+
:aria-expanded="menuOpen"
|
|
132
|
+
@click="menuOpen = !menuOpen"
|
|
133
|
+
>
|
|
134
|
+
<v-avatar color="primary" size="32">U</v-avatar>
|
|
135
|
+
</v-btn>
|
|
136
|
+
</v-app-bar>
|
|
137
|
+
<v-main><!-- Konten aplikasi --></v-main>
|
|
138
|
+
<UserMenuHost v-model="menuOpen" />
|
|
139
|
+
</v-app>
|
|
140
|
+
</template>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`v-model` boolean wajib disediakan; inisialisasikan dengan `ref(false)`. Plugin menyediakan konfigurasi melalui provide/inject. State UI dibuat privat oleh host, terisolasi antar-host/aplikasi, dan dibersihkan ketika host atau aplikasi di-unmount. Logout atau pergantian key sesi/ID pengguna menyinkronkan model ke `false`, membuang draft, dan mengabaikan respons sesi lama. API visibilitas hanya melalui model, tanpa mode kompatibilitas imperatif.
|
|
144
|
+
|
|
145
|
+
Seluruh pengelolaan favorit berada di drawer. Mode edit memakai draft lokal dengan **Simpan/Batal**, hapus, drag-and-drop, serta tombol naik/turun. Kegagalan Simpan mempertahankan draft; penutupan dengan perubahan meminta konfirmasi. Selama mutasi, penutupan ditahan kecuali sesi berubah.
|
|
146
|
+
|
|
147
|
+
Ketika parent mengubah model menjadi `false` tetapi penutupan ditolak, library mengoreksinya kembali ke `true` dan drawer tetap terbuka. Draft berubah menampilkan konfirmasi; **Buang perubahan** menutup dan menghasilkan `false`, sedangkan **Lanjutkan edit** mempertahankan `true`. Selama mutasi, permintaan tutup diabaikan tanpa menutup otomatis setelah mutasi selesai. Perubahan singkat `false` lalu `true` dapat terjadi; jangan menganggap setiap assignment `false` sebagai penutupan yang sudah diterima.
|
|
148
|
+
|
|
149
|
+
Model awal `true` diproses setelah mounted. Tanpa sesi aktif, model dikoreksi menjadi `false` tanpa request daftar. Cleanup saat unmount tidak mengubah model parent; jika host dipasang kembali dengan model masih `true`, state baru dibuat dan daftar dimuat kembali.
|
|
150
|
+
|
|
151
|
+
### Daftar dari aplikasi melalui props
|
|
152
|
+
|
|
153
|
+
Gunakan prop `items` jika aplikasi sudah memuat daftar favorit sendiri:
|
|
154
|
+
|
|
155
|
+
```vue
|
|
156
|
+
<script setup lang="ts">
|
|
157
|
+
import { ref } from "vue";
|
|
158
|
+
|
|
159
|
+
import { type Favorite, UserMenuHost } from "@rsmlamongan/user-menu";
|
|
160
|
+
|
|
161
|
+
const open = ref(false);
|
|
162
|
+
const menuItems = ref<Favorite[]>([]);
|
|
163
|
+
// Isi menuItems dari daftar lengkap favorit milik pengguna yang sedang login.
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<template>
|
|
167
|
+
<UserMenuHost v-model="open" :items="menuItems" @update:items="menuItems = $event" />
|
|
168
|
+
<!-- Penulisan setara: <UserMenuHost v-model="open" v-model:items="menuItems" /> -->
|
|
169
|
+
</template>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- `items` berisi `Favorite[]` (array readonly juga diterima), dengan field `id`, `title`, `url`, dan `appId`. Urutan array menjadi urutan tampilan.
|
|
173
|
+
- Jika `items` diberikan, termasuk `[]`, host tidak memanggil `listFavorites()`. Perubahan array maupun field item di parent diikuti secara reaktif. Tanpa prop atau ketika nilainya `undefined`, host kembali memuat melalui API.
|
|
174
|
+
- Tambah/hapus/urut menggunakan callback `@create`/`@save` jika diberikan, atau adapter API pada plugin. Setelah mutasi berhasil, host memperbarui tampilannya dan mengirim **salinan daftar lengkap** melalui `update:items`. Batal, kegagalan, dan respons sesi lama tidak mengirim event ini.
|
|
175
|
+
- Props tidak dimutasi. Tangkap event seperti contoh, atau gunakan `v-model:items`, agar pembukaan berikutnya memakai hasil terbaru. Dengan `:items` saja, pembukaan berikutnya kembali memakai nilai props parent.
|
|
176
|
+
|
|
177
|
+
Aplikasi bertanggung jawab atas loading/error pemuatan eksternal, daftar lengkap dari sumber penyimpanan yang sama dengan adapter mutasi, serta pemisahan data per sesi. Bersihkan daftar ketika akun berubah dan abaikan respons lama pada loader parent; prop ini tidak menggantikan autentikasi atau otorisasi API. Data props divalidasi dengan aturan favorit yang sama; data tidak valid menampilkan error tanpa fallback request API.
|
|
178
|
+
|
|
179
|
+
Pembaruan props saat edit mempertahankan draft, tetapi perubahan yang membuat draft tertinggal menolak Simpan sebagai konflik. Batal menampilkan daftar parent terbaru. Hasil mutasi tertunda juga tidak menimpa props yang sudah berubah; transaksi API mungkin sudah selesai sehingga parent perlu memuat ulang sumbernya.
|
|
180
|
+
|
|
181
|
+
Di playground, aktifkan **Gunakan items dari aplikasi** untuk mencoba loader parent, sinkronisasi hasil mutasi, dan pemisahan akun. Sakelar default nonaktif.
|
|
182
|
+
|
|
183
|
+
### Delegasi tambah dan Simpan
|
|
184
|
+
|
|
185
|
+
Parent dapat menangani operasi penyimpanan sendiri melalui `@create` dan `@save`, secara independen dari sumber daftar:
|
|
186
|
+
|
|
187
|
+
```vue
|
|
188
|
+
<script setup lang="ts">
|
|
189
|
+
import { ref } from "vue";
|
|
190
|
+
|
|
191
|
+
import {
|
|
192
|
+
type Favorite,
|
|
193
|
+
type UserMenuCreateHandler,
|
|
194
|
+
UserMenuHost,
|
|
195
|
+
type UserMenuSaveHandler,
|
|
196
|
+
} from "@rsmlamongan/user-menu";
|
|
197
|
+
|
|
198
|
+
import { api } from "./user-menu-integration";
|
|
199
|
+
|
|
200
|
+
const open = ref(false);
|
|
201
|
+
const menuItems = ref<Favorite[]>([]);
|
|
202
|
+
const handleCreate: UserMenuCreateHandler = async (input) => {
|
|
203
|
+
return api.createFavorite(input);
|
|
204
|
+
};
|
|
205
|
+
const handleSave: UserMenuSaveHandler = async (input) => {
|
|
206
|
+
return api.saveFavoriteChanges(input);
|
|
207
|
+
};
|
|
208
|
+
</script>
|
|
209
|
+
|
|
210
|
+
<template>
|
|
211
|
+
<UserMenuHost
|
|
212
|
+
v-model="open"
|
|
213
|
+
v-model:items="menuItems"
|
|
214
|
+
@create="handleCreate"
|
|
215
|
+
@save="handleSave"
|
|
216
|
+
/>
|
|
217
|
+
</template>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`api` di contoh adalah client milik aplikasi, bukan objek yang wajib dipasang pada plugin. Isi `menuItems` melalui loader aplikasi seperti pada bagian sebelumnya. Handler create menerima `{ title, url, appId }` dan mengembalikan `Promise<Favorite>` dengan ID baru. Handler save menerima `{ orderedIds, deletedIds }` dan mengembalikan `Promise<Favorite[]>` sesuai urutan tersimpan. **Kembalikan hasil dengan `return` dan teruskan kegagalan dengan throw/reject**; jangan menelan error atau mengubahnya menjadi hasil sukses palsu.
|
|
221
|
+
|
|
222
|
+
Host menyediakan callback props `onCreate`/`onSave`, yang dapat ditulis sebagai `@create`/`@save` atau `:on-create`/`:on-save`. Library memanggil callback langsung dan menunggu Promise; kedua nama ini bukan event `$emit` biasa. Gunakan satu handler per operasi tanpa modifier seperti `.once`. Event `update:modelValue` dan `update:items` tetap event Vue biasa.
|
|
223
|
+
|
|
224
|
+
| Operasi | Prioritas |
|
|
225
|
+
| ------------------- | --------------------------------------------------------------- |
|
|
226
|
+
| Daftar | `items` jika diberikan; selain itu `api.listFavorites()`. |
|
|
227
|
+
| Tambah | `@create` jika diberikan; selain itu `api.createFavorite()`. |
|
|
228
|
+
| Simpan hapus/urutan | `@save` jika diberikan; selain itu `api.saveFavoriteChanges()`. |
|
|
229
|
+
|
|
230
|
+
Handler yang gagal tidak memicu fallback ke adapter. Library tetap mengelola loading, penahanan penutupan, validasi hasil, serta form/draft ketika gagal. Provider dipilih saat operasi dimulai; mengganti atau menghapus handler tidak mengalihkan request yang sedang berjalan.
|
|
231
|
+
|
|
232
|
+
Parent cukup mengembalikan hasil dan menerima pembaruan daftar melalui `update:items`/`v-model:items`. Jangan memperbarui `menuItems` di dalam handler sebelum hasil dikembalikan: perubahan props selama mutasi mengikuti pemeriksaan konflik yang sama dan hasil lama tidak akan menimpa data baru. Hasil sesi lama atau setelah unmount tidak diterapkan.
|
|
233
|
+
|
|
234
|
+
Dengan `items` serta kedua handler, plugin boleh tanpa `api`:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
app.use(createUserMenu({ appId: "inventaris", getSession, getCurrentPage }));
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Plugin, sesi reaktif, dan metadata halaman tetap diperlukan. Jika adapter diberikan, bentuk `UserMenuApi` lengkap berisi tiga operasi: `listFavorites`, `createFavorite`, dan `saveFavoriteChanges`. Tanpa provider tambah/edit, kontrol terkait dinonaktifkan; jika provider save hilang saat edit, Simpan dinonaktifkan sementara Batal tetap tersedia. Tanpa sumber daftar, drawer menampilkan pesan tidak tersedia dan retry dinonaktifkan. Tidak ada timeout atau pembatalan request otomatis; Promise handler harus selesai melalui resolve/reject.
|
|
241
|
+
|
|
242
|
+
Playground menyediakan **Delegasikan create/save ke parent (tanpa adapter)**, default nonaktif. Mode ini mengaktifkan daftar dari aplikasi dan menampilkan jumlah pemanggilan callback untuk pengujian latensi, kegagalan, serta pergantian akun.
|
|
243
|
+
|
|
244
|
+
### Slot header dan footer
|
|
245
|
+
|
|
246
|
+
Slot `header` pada `UserMenuHost` opsional dan kosong secara default: tanpa slot, tidak ada wrapper atau jarak tambahan. Library tidak menampilkan header identitas. Tombol tutup berada sejajar dengan judul **Favorit saya/Edit favorit**; nama aksesibel drawer tetap **Menu pengguna**.
|
|
247
|
+
|
|
248
|
+
Slot `footer` berada di dasar drawer, di luar area scroll. Konten kustom menggantikan seluruh fallback Pengaturan. Jika footer tidak diberikan tetapi opsi plugin `settings` tersedia, library menampilkan tombol Pengaturan pada area tersebut. Tanpa keduanya, area footer tidak dirender.
|
|
249
|
+
|
|
250
|
+
Footer menerima `openSettings(): Promise<void>` dan `settingsDisabled: boolean`. Gunakan keduanya untuk menjalankan callback/URL `settings` dengan pembatasan edit, operasi berlangsung, dan sesi. Slot header tidak menerima props. Consumer lama yang mengabaikan slot props tetap dapat digunakan.
|
|
251
|
+
|
|
252
|
+
```vue
|
|
253
|
+
<UserMenuHost v-model="menuOpen">
|
|
254
|
+
<template #footer="{ openSettings, settingsDisabled }">
|
|
255
|
+
<div class="account-actions">
|
|
256
|
+
<v-btn variant="text" :disabled="settingsDisabled" @click="openSettings">Pengaturan</v-btn>
|
|
257
|
+
<v-btn variant="text" @click="logout">Keluar</v-btn>
|
|
258
|
+
</div>
|
|
259
|
+
</template>
|
|
260
|
+
</UserMenuHost>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`logout` merupakan callback aplikasi pemakai. Beri `account-actions` layout grid dua kolom sama lebar dengan gap 8px agar kedua tombol tetap satu baris pada desktop/mobile. Isi slot header hanya jika aplikasi memang memerlukannya. Kedua slot tersedia pada mode normal, edit, loading, dan error. Di playground, **Tampilkan contoh slot** menyediakan header opsional dan Pengaturan/Bantuan dalam satu baris.
|
|
264
|
+
|
|
265
|
+
## Dokumentasi dan batas
|
|
266
|
+
|
|
267
|
+
- [Panduan development](docs/DEVELOPMENT.md): tanggung jawab, lifecycle, alur UI, tooling, dan penerimaan.
|
|
268
|
+
- [Kontrak API](docs/API-CONTRACT.md): model, tiga operasi adapter, error, mock, dan kebutuhan backend.
|
|
269
|
+
- [Panduan rilis](docs/RELEASING.md): CI, akun maintainer, bootstrap npm, OIDC, dan penanganan kegagalan.
|
|
270
|
+
- [Playground integrasi](playground/main.ts): contoh konfigurasi yang dapat dijalankan.
|
|
271
|
+
|
|
272
|
+
Mock `localStorage` hanya untuk data sintetis per origin. Pemisahan ID pengguna pada mock tidak membuktikan otorisasi server atau sinkronisasi lintas origin. Backend dan adapter HTTP tersedia di source; penerapan schema dan integrasi produksi tetap perlu diverifikasi. Pipeline rilis tersedia, tetapi keberhasilan publikasi harus diverifikasi setelah pengaturan GitHub/npm selesai. Tidak ada formulir akun, editor URL, folder favorit, atau dukungan Vuetify 3.
|
|
273
|
+
|
|
274
|
+
Referensi: [plugin Vue](https://vuejs.org/guide/reusability/plugins.html), [Vite Library Mode](https://vite.dev/guide/build.html#library-mode), dan [HTML Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type VNode } from "vue";
|
|
2
|
+
import type { Favorite, UserMenuCreateHandler, UserMenuSaveHandler } from "../types.js";
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
items?: readonly Favorite[];
|
|
5
|
+
onCreate?: UserMenuCreateHandler;
|
|
6
|
+
onSave?: UserMenuSaveHandler;
|
|
7
|
+
};
|
|
8
|
+
type __VLS_Slots = {
|
|
9
|
+
header?(): VNode[];
|
|
10
|
+
footer?(props: {
|
|
11
|
+
openSettings: () => Promise<void>;
|
|
12
|
+
settingsDisabled: boolean;
|
|
13
|
+
}): VNode[];
|
|
14
|
+
};
|
|
15
|
+
type __VLS_ModelProps = {
|
|
16
|
+
modelValue: boolean;
|
|
17
|
+
};
|
|
18
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
19
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
20
|
+
"update:items": (items: Favorite[]) => any;
|
|
21
|
+
"update:modelValue": (value: boolean) => any;
|
|
22
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
23
|
+
"onUpdate:items"?: ((items: Favorite[]) => any) | undefined;
|
|
24
|
+
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
25
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
26
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
27
|
+
declare const _default: typeof __VLS_export;
|
|
28
|
+
export default _default;
|
|
29
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
30
|
+
new (): {
|
|
31
|
+
$slots: S;
|
|
32
|
+
};
|
|
33
|
+
};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type UserMenuErrorCode = "unauthenticated" | "invalid-input" | "duplicate" | "conflict" | "unavailable";
|
|
2
|
+
export declare class UserMenuError extends Error {
|
|
3
|
+
readonly code: UserMenuErrorCode;
|
|
4
|
+
constructor(code: UserMenuErrorCode);
|
|
5
|
+
}
|
|
6
|
+
export declare function errorMessage(error: unknown, fallback: string): string;
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { UserMenuApi } from "./types.js";
|
|
2
|
+
export interface UserMenuHttpRequest {
|
|
3
|
+
method: "GET" | "POST" | "PUT";
|
|
4
|
+
url: string;
|
|
5
|
+
body?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface UserMenuHttpResponse {
|
|
8
|
+
status: number;
|
|
9
|
+
body: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface HttpUserMenuApiOptions {
|
|
12
|
+
/** Root API v1, including any reverse-proxy prefix. */
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
/** Resolve HTTP errors too; reject only for transport failures. Auth belongs to the host. */
|
|
15
|
+
request: (input: UserMenuHttpRequest) => Promise<UserMenuHttpResponse>;
|
|
16
|
+
}
|
|
17
|
+
export declare function createHttpUserMenuApi(options: HttpUserMenuApiOptions): UserMenuApi;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { a as e, i as t, n, r, t as i } from "./validation-ZwudKvW7.js";
|
|
2
|
+
//#region src/http.ts
|
|
3
|
+
function a(e) {
|
|
4
|
+
return typeof e == "object" && e && !Array.isArray(e) ? e : void 0;
|
|
5
|
+
}
|
|
6
|
+
function o(t, n) {
|
|
7
|
+
return t === 401 ? new e("unauthenticated") : t === 400 ? new e("invalid-input") : t === 409 ? new e(Array.isArray(n?.errors) && n.errors.includes("duplicate") ? "duplicate" : "conflict") : new e("unavailable");
|
|
8
|
+
}
|
|
9
|
+
function s(s) {
|
|
10
|
+
let c = r(s.baseUrl).replace(/\/+$/, "");
|
|
11
|
+
if (/[?#]/.test(c) || typeof s.request != "function") throw new e("invalid-input");
|
|
12
|
+
let l = `${c}/user-menu/favorites`, u = s.request;
|
|
13
|
+
async function d(t, n, r) {
|
|
14
|
+
let i;
|
|
15
|
+
try {
|
|
16
|
+
i = await u(r === void 0 ? {
|
|
17
|
+
method: t,
|
|
18
|
+
url: l
|
|
19
|
+
} : {
|
|
20
|
+
method: t,
|
|
21
|
+
url: l,
|
|
22
|
+
body: r
|
|
23
|
+
});
|
|
24
|
+
} catch (t) {
|
|
25
|
+
throw t instanceof e ? t : new e("unavailable");
|
|
26
|
+
}
|
|
27
|
+
if (!a(i) || !Number.isInteger(i.status) || i.status < 100 || i.status > 599) throw new e("unavailable");
|
|
28
|
+
let s = a(i.body), c = a(s?.metadata);
|
|
29
|
+
if (i.status < 200 || i.status >= 300) throw o(i.status, c);
|
|
30
|
+
let d = c?.code;
|
|
31
|
+
if (typeof d != "number" || !Number.isInteger(d)) throw new e("unavailable");
|
|
32
|
+
if (d >= 400) throw o(d, c);
|
|
33
|
+
if (d !== n) throw new e("unavailable");
|
|
34
|
+
return s?.response;
|
|
35
|
+
}
|
|
36
|
+
async function f(t, r) {
|
|
37
|
+
let i = await d(t, 200, r);
|
|
38
|
+
try {
|
|
39
|
+
return n(i);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new e("unavailable");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
listFavorites: () => f("GET"),
|
|
46
|
+
async createFavorite(n) {
|
|
47
|
+
let a = await d("POST", 201, {
|
|
48
|
+
title: r(n?.title),
|
|
49
|
+
url: t(n?.url),
|
|
50
|
+
appId: r(n?.appId)
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
return i(a);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new e("unavailable");
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
async saveFavoriteChanges(t) {
|
|
59
|
+
if (!Array.isArray(t?.orderedIds) || !Array.isArray(t?.deletedIds)) throw new e("invalid-input");
|
|
60
|
+
let n = {
|
|
61
|
+
orderedIds: [...t.orderedIds],
|
|
62
|
+
deletedIds: [...t.deletedIds]
|
|
63
|
+
};
|
|
64
|
+
for (let e of [...n.orderedIds, ...n.deletedIds]) r(e);
|
|
65
|
+
return f("PUT", n);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { s as createHttpUserMenuApi };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { UserMenuError } from "./errors.js";
|
|
2
|
+
export type { UserMenuErrorCode } from "./errors.js";
|
|
3
|
+
export { createUserMenu } from "./plugin.js";
|
|
4
|
+
export type { CreateFavoriteInput, CurrentPage, Favorite, SaveFavoriteChangesInput, UserMenuApi, UserMenuCreateHandler, UserMenuOptions, UserMenuSaveHandler, UserMenuSession, UserMenuSettings, UserMenuUser, } from "./types.js";
|
|
5
|
+
export { default as UserMenuHost } from "./components/UserMenuHost.vue";
|