@xen-orchestra/web-core 0.31.1 → 0.33.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.
Files changed (75) hide show
  1. package/lib/assets/css/_colors.pcss +8 -0
  2. package/lib/components/button-group/VtsButtonGroup.vue +5 -1
  3. package/lib/components/menu/MenuList.vue +1 -2
  4. package/lib/components/menu/MenuTrigger.vue +5 -11
  5. package/lib/components/modal/VtsModal.vue +82 -0
  6. package/lib/components/modal/VtsModalButton.vue +36 -0
  7. package/lib/components/modal/VtsModalCancelButton.vue +37 -0
  8. package/lib/components/modal/VtsModalConfirmButton.vue +21 -0
  9. package/lib/components/modal/VtsModalList.vue +34 -0
  10. package/lib/components/object-icon/VtsObjectIcon.vue +3 -8
  11. package/lib/components/status/VtsStatus.vue +66 -0
  12. package/lib/components/task/VtsQuickTaskList.vue +17 -5
  13. package/lib/components/tree/VtsTreeItem.vue +2 -2
  14. package/lib/components/ui/breadcrumb/UiBreadcrumb.vue +79 -0
  15. package/lib/components/ui/button/UiButton.vue +13 -67
  16. package/lib/components/ui/modal/UiModal.vue +164 -0
  17. package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +2 -2
  18. package/lib/composables/context.composable.ts +3 -5
  19. package/lib/composables/link-component.composable.ts +3 -2
  20. package/lib/composables/pagination.composable.ts +3 -2
  21. package/lib/composables/tree-filter.composable.ts +5 -3
  22. package/lib/icons/fa-icons.ts +13 -1
  23. package/lib/icons/index.ts +17 -0
  24. package/lib/locales/cs.json +60 -2
  25. package/lib/locales/de.json +40 -2
  26. package/lib/locales/en.json +27 -1
  27. package/lib/locales/es.json +51 -5
  28. package/lib/locales/fa.json +10 -10
  29. package/lib/locales/fr.json +28 -2
  30. package/lib/locales/it.json +4 -0
  31. package/lib/locales/nl.json +64 -14
  32. package/lib/locales/pt_BR.json +3 -3
  33. package/lib/locales/ru.json +41 -2
  34. package/lib/locales/sv.json +55 -1
  35. package/lib/locales/uk.json +4 -4
  36. package/lib/packages/collection/use-collection.ts +3 -2
  37. package/lib/packages/form-select/use-form-option-controller.ts +3 -2
  38. package/lib/packages/form-select/use-form-select.ts +8 -7
  39. package/lib/packages/menu/action.ts +4 -3
  40. package/lib/packages/menu/link.ts +5 -4
  41. package/lib/packages/menu/router-link.ts +3 -2
  42. package/lib/packages/menu/toggle-target.ts +3 -2
  43. package/lib/packages/modal/ModalProvider.vue +17 -0
  44. package/lib/packages/modal/README.md +253 -0
  45. package/lib/packages/modal/create-modal-opener.ts +103 -0
  46. package/lib/packages/modal/modal.store.ts +22 -0
  47. package/lib/packages/modal/types.ts +92 -0
  48. package/lib/packages/modal/use-modal.ts +53 -0
  49. package/lib/packages/progress/use-progress.ts +4 -3
  50. package/lib/packages/table/README.md +336 -0
  51. package/lib/packages/table/apply-extensions.ts +26 -0
  52. package/lib/packages/table/define-columns.ts +62 -0
  53. package/lib/packages/table/define-renderer/define-table-cell-renderer.ts +27 -0
  54. package/lib/packages/table/define-renderer/define-table-renderer.ts +47 -0
  55. package/lib/packages/table/define-renderer/define-table-row-renderer.ts +29 -0
  56. package/lib/packages/table/define-renderer/define-table-section-renderer.ts +29 -0
  57. package/lib/packages/table/define-table/define-multi-source-table.ts +39 -0
  58. package/lib/packages/table/define-table/define-table.ts +13 -0
  59. package/lib/packages/table/define-table/define-typed-table.ts +18 -0
  60. package/lib/packages/table/index.ts +11 -0
  61. package/lib/packages/table/transform-sources.ts +13 -0
  62. package/lib/packages/table/types/extensions.ts +16 -0
  63. package/lib/packages/table/types/index.ts +47 -0
  64. package/lib/packages/table/types/table-cell.ts +18 -0
  65. package/lib/packages/table/types/table-row.ts +20 -0
  66. package/lib/packages/table/types/table-section.ts +19 -0
  67. package/lib/packages/table/types/table.ts +28 -0
  68. package/lib/packages/threshold/use-threshold.ts +4 -3
  69. package/lib/types/vue-virtual-scroller.d.ts +101 -0
  70. package/lib/utils/injection-keys.util.ts +3 -0
  71. package/lib/utils/progress.util.ts +2 -1
  72. package/lib/utils/to-computed.util.ts +15 -0
  73. package/package.json +3 -2
  74. package/lib/components/backup-state/VtsBackupState.vue +0 -37
  75. package/lib/components/connection-status/VtsConnectionStatus.vue +0 -36
@@ -1,5 +1,8 @@
1
1
  {
2
+ "2nd-last": "2:a från slutet",
3
+ "3rd-last": "3:e från slutet",
2
4
  "about": "Om",
5
+ "accept-self-signed-certificates": "Acceptera självsignerade certifikat",
3
6
  "access-forum": "Gå till forum",
4
7
  "access-xoa": "Anslut till XOA",
5
8
  "account-organization-more": "Konto, organisation och mer…",
@@ -22,8 +25,11 @@
22
25
  "alarm-type.sr_io_throughput_total_per_host": "SR IO genomströmning totalt per host överskrider @:n-percent",
23
26
  "alarm-type.unknown": "Okänd larm-typ",
24
27
  "alarms": "Larm",
28
+ "all-done": "Allt klart!",
25
29
  "all-good": "Allt är bra!",
30
+ "all-quiet-launchpad": "Allt tyst på launchpad:en",
26
31
  "allow-self-signed-ssl": "Du kan behöva tillåta själv-signerade SSL-certifikat i din webbläsare",
32
+ "api-error-details": "API fel-detaljer",
27
33
  "appearance": "Utseende",
28
34
  "ascending": "stigande",
29
35
  "auto-generated": "Automatiskt generad",
@@ -32,9 +38,24 @@
32
38
  "available-properties-for-advanced-filter": "Tillgängliga egenskaper för avancerade filter:",
33
39
  "back-pool-dashboard": "Gå tillbaka till din Pool-översikt",
34
40
  "back-site-dashboard": "Gå tillbaka till din översikt",
41
+ "backed-up-pools": "Säkerhetskopierade Pooler",
42
+ "backed-up-vms": "Säkerhetskopierade VM:ar",
35
43
  "backup": "Säkerhetskopiera",
36
44
  "backup-issues": "Säkerhetskopieringsproblem",
45
+ "backup-jobs": "Säkerhetskopieringsjobb",
46
+ "backup-network": "Säkerhetskopieringsnätverk",
47
+ "backup-repositories": "Säkerhetskopieringsförvaring",
37
48
  "backup-repository": "Säkerhetskopieringsförvaring (lokalt, NFS, SMB)",
49
+ "backup-targets": "Säkerhetskopieringsmål",
50
+ "backup.continuous-replication": "Kontinuerlig replikering",
51
+ "backup.disaster-recovery": "Katastrofåterställning",
52
+ "backup.full": "Fullständig säkerhetskopiering",
53
+ "backup.incremental": "Inkrementell säkerhetskopiering",
54
+ "backup.metadata": "Säkerhetskopiering av metadata",
55
+ "backup.mirror": "Speglad säkerhetskopiering",
56
+ "backup.pool-metadata": "Pool-metadata",
57
+ "backup.rolling-snapshot": "Rullande ögonblicksbild",
58
+ "backup.xo-config": "XO-konfig",
38
59
  "backups": "Säkerhetskopieringar",
39
60
  "backups.jobs": "Jobb",
40
61
  "backups.jobs.at-least-one-skipped": "Minst en hoppades över",
@@ -62,19 +83,28 @@
62
83
  "bytes.ki": "KiB",
63
84
  "bytes.mi": "MiB",
64
85
  "cancel": "Avbryt",
86
+ "cbt-destroy-snapshot-data": "Rensa ögonblicksdata när CBT används",
65
87
  "change-state": "Ändra",
66
88
  "check-errors": "Kontrollera felen:",
67
89
  "check-summing": "TX checksumma",
90
+ "checkpoint-snapshot": "Kontrollpunkt för ögonblicksbild",
68
91
  "click-to-display-alarms": "Klicka för att visa larm:",
69
92
  "click-to-return-default-pool": "Klicka här för att återgå till standardpoolen",
70
93
  "close": "Stäng",
71
94
  "coming-soon": "Kommer snart!",
72
95
  "community": "Community",
73
96
  "community-name": "{name} community",
97
+ "compression": "Komprimering",
98
+ "concurrency": "Samtidighet",
74
99
  "configuration": "Konfiguration",
100
+ "configure-in-xo-5": "Konfigurera i XO 5",
75
101
  "confirm-cancel": "Är du säker på att du vill avbryta?",
76
102
  "confirm-delete": "Du kommer att radera {0}",
103
+ "connect": "Anslut",
104
+ "connect-another-pool": "Anslut annan pool",
105
+ "connect-pool": "Anslut pool",
77
106
  "connected": "Ansluten",
107
+ "connected-to-ip": "Anslut till {ip}",
78
108
  "connecting": "Ansluter",
79
109
  "connection": "Anslutning",
80
110
  "connection-failed": "Anslutning misslyckades",
@@ -82,7 +112,9 @@
82
112
  "console": "Konsol",
83
113
  "console-actions": "Konsolverktyg",
84
114
  "console-clipboard": "Konsolurklipp",
115
+ "console-offline": "Konsoll nedstängd",
85
116
  "console-unavailable": "Konsolen är inte tillgänglig",
117
+ "console-unavailable-reason": "Konsollen är inte längre tillgänglig då {type} är avstängd.",
86
118
  "control-domain-memory": "Kontrolldomänens minne",
87
119
  "copy": "Kopiera",
88
120
  "copy-all": "Kopiera allt",
@@ -98,6 +130,7 @@
98
130
  "core.group": "Grupp",
99
131
  "core.hide": "Göm",
100
132
  "core.open": "Öppna",
133
+ "core.open-in-new-tab": "Öppna i ny tabb",
101
134
  "core.pagination.all": "Alla",
102
135
  "core.pagination.show-by": "Sortera efter",
103
136
  "core.query-search-bar.label": "Sökmotor",
@@ -127,14 +160,20 @@
127
160
  "cpu-usage": "CPU-nyttjande",
128
161
  "cpu-weight": "CPU-vikt",
129
162
  "cpus": "CPU:er",
163
+ "crash-dump-storage-repository": "Lagringsplats för crashloggar",
130
164
  "create": "Skapa",
165
+ "created-by": "Skapad av",
166
+ "created-on": "Skapad",
167
+ "cron-pattern": "Cron-mönster",
131
168
  "custom-config": "Skräddarsydd konfiguration",
132
169
  "dark-mode.auto": "Automatiskt mörkt läge",
133
170
  "dark-mode.disable": "Inaktivera mörkt läge",
134
171
  "dark-mode.enable": "Aktivera mörkt läge",
135
172
  "dashboard": "Kontrollpanel",
173
+ "date": "Datum",
136
174
  "default-behavior": "Standardbeteende",
137
175
  "default-locking-mode": "Standard låsläge",
176
+ "default-storage-repository": "Standard lagringsplats",
138
177
  "delete": "Radera",
139
178
  "delete-vms": "Radera 1 VM | Radera {n} VMar",
140
179
  "deploy": "Driftsätt",
@@ -164,10 +203,12 @@
164
203
  "edit-config": "Ändra konfiguration",
165
204
  "enabled": "Aktiverad",
166
205
  "end-of-life": "Slutdatum",
206
+ "engines-off": "Motor avslagen, satellitbana stabil",
167
207
  "eol": "EOL",
168
208
  "error": "Fel",
169
209
  "error-no-data": "Fel, kan inte samla data.",
170
210
  "error-occurred": "Ett fel har uppstått",
211
+ "excluded-vms-tags": "Exkluderade VM-taggar",
171
212
  "exit-fullscreen": "Stäng fullskärm",
172
213
  "expiration-date": "Utgångsdatum",
173
214
  "export": "Exportera",
@@ -177,6 +218,7 @@
177
218
  "export-vm": "Exportera VM",
178
219
  "export-vms": "Exportera VMar",
179
220
  "export-vms-manually-information": "Vissa VM-exporter kunde inte startas automatiskt, troligen på grund av dina webbläsarinställningar. För att exportera dem, klicka på varje VM. (Alternativt, kopiera länken.)",
221
+ "failure": "Misslyckande",
180
222
  "fast-clone": "Snabb-kloning",
181
223
  "fetching-fresh-data": "Hämtar nya data",
182
224
  "filter.comparison.contains": "Innehåller",
@@ -206,14 +248,20 @@
206
248
  "go-back": "Gå tillbaka",
207
249
  "gpus": "GPU:er",
208
250
  "graphics-display": "Grafik & Skärm",
251
+ "guest-tools": "Gästverktyg",
209
252
  "gzip": "gzip",
210
253
  "hardware": "Hårdvara",
211
254
  "hardware-specifications": "Hårdvaruspecifikationer",
255
+ "heartbeat-storage-repository": "Lagringsplats för hjärtslag",
212
256
  "here": "Här",
257
+ "hide-successful-items": "Göm lyckade objekt i misslyckanderapporter",
213
258
  "high-availability": "Hög tillgänglighet (HA)",
214
259
  "host": "Host",
260
+ "host-currently-shutdown": "Denna host är i nuläget nedstängd, data kan inte hämtas.",
215
261
  "host-description": "Host-beskrivning",
216
262
  "host-internal-networks": "Host-interna nätverk",
263
+ "host-not-running": "Host:en kör inte",
264
+ "host-off": "Hosten är avstängd",
217
265
  "host-status.halted": "Stoppad",
218
266
  "host-status.running": "Kör",
219
267
  "host-status.unknown": "Okänt",
@@ -229,6 +277,7 @@
229
277
  "hyper-threading": "Multitrådning (SMT)",
230
278
  "id": "ID",
231
279
  "in-last-three-jobs": "I de senaste tre jobben",
280
+ "in-progress": "Pågående",
232
281
  "install-settings": "Installationsinställningar",
233
282
  "interfaces": "Gränssnitt | Gränssnitt | Gränssnitten",
234
283
  "interrupted": "Avbruten",
@@ -236,9 +285,11 @@
236
285
  "ip-address": "IP-adress",
237
286
  "ip-addresses": "IP-adresser",
238
287
  "ip-mode": "IP-läge",
288
+ "ip-port-placeholder": "adress[:port]",
239
289
  "is-primary-host": "{name} är den primära hosten",
240
290
  "iscsi-iqn": "iSCSI IQN",
241
291
  "iso-dvd": "ISO/DVD",
292
+ "job-name": "Jobbnamn",
242
293
  "job.vm-copy.bad-power-state": "VMen måste vara stoppad",
243
294
  "job.vm-copy.in-progress": "Kopiering pågår…",
244
295
  "job.vm-copy.missing-vms": "Ingen VM att kopiera",
@@ -286,6 +337,9 @@
286
337
  "keep-me-logged": "Håll mig inloggad",
287
338
  "keep-page-open": "Uppdatera inte och stäng inte tabben innan driftsättningen är klar.",
288
339
  "language": "Språk",
340
+ "last": "Senaste",
341
+ "last-n-runs": "Senaste körning | Senaste {n} körningar",
342
+ "last-run-number": "Senaste körning #{n}",
289
343
  "last-week": "Föregående vecka",
290
344
  "learn-more": "Läs mer",
291
345
  "license-socket": "Licensiera sockel",
@@ -293,8 +347,8 @@
293
347
  "licensing": "Licensiering",
294
348
  "load-average": "Genomsnittslast",
295
349
  "load-now": "Ladda nu",
296
- "loading-hosts": "Ladda hostar…",
297
350
  "loading": "Laddning pågår…",
351
+ "loading-hosts": "Ladda hostar…",
298
352
  "locking-mode": "Låsläge",
299
353
  "locking-mode-default": "Standard låsläge",
300
354
  "log-out": "Logga ut",
@@ -3,9 +3,9 @@
3
3
  "3rd-last": "3-й останній",
4
4
  "about": "Про нас",
5
5
  "accept-self-signed-certificates": "Приймати самопідписані сертифікати",
6
- "access-forum": "Перейти на форум",
6
+ "access-forum": "Доступ до форуму",
7
7
  "access-xoa": "Доступ до XOA",
8
- "account-organization-more": "Обліковий запис, організація та багато іншого…",
8
+ "account-organization-more": "Обліковий запис, організація та інше…",
9
9
  "add": "Додати",
10
10
  "add-filter": "Додати фільтр",
11
11
  "add-or": "+АБО",
@@ -201,11 +201,11 @@
201
201
  "expiration-date": "Термін придатності",
202
202
  "export": "Експорт",
203
203
  "export-n-vms": "Експортовано 1 VM | Експортовано {n} VMs",
204
- "export-n-vms-manually": "Експорт однієї ВМ вручну | Експорт {n} ВМ вручну",
204
+ "export-n-vms-manually": "Експорт однієї ВМ вручну | Експорт {n} ВМ вручну",
205
205
  "export-table-to": "Експорт таблиці в {type}",
206
206
  "export-vm": "Експорт VM",
207
207
  "export-vms": "Експорт VMs",
208
- "export-vms-manually-information": "Експорт деяких ВМ не вдалося запустити автоматично, ймовірно, через ваші налаштування браузера. Для експорту натисніть на кожну ВМ. (Або скопіюйте посилання).",
208
+ "export-vms-manually-information": "Експорт деяких ВМ не вдалося запустити автоматично, ймовірно, через ваші налаштування браузера. Для експорту натисніть на кожну ВМ. (Або скопіюйте посилання.)",
209
209
  "failure": "Критична помилка",
210
210
  "fast-clone": "Швидке клонування",
211
211
  "fetching-fresh-data": "Отримання свіжих даних",
@@ -1,5 +1,6 @@
1
1
  import { guessItemId } from '@core/packages/collection/guess-item-id.ts'
2
2
  import type { EmptyObject } from '@core/types/utility.type.ts'
3
+ import { toComputed } from '@core/utils/to-computed.util.ts'
3
4
  import type {
4
5
  Collection,
5
6
  CollectionConfigFlags,
@@ -8,7 +9,7 @@ import type {
8
9
  ExtractSourceId,
9
10
  GetItemId,
10
11
  } from './types.ts'
11
- import { computed, type MaybeRefOrGetter, toValue } from 'vue'
12
+ import { computed, type MaybeRefOrGetter } from 'vue'
12
13
  import { createCollection } from './create-collection.ts'
13
14
  import { createItem } from './create-item.ts'
14
15
  import { useFlagRegistry } from './use-flag-registry.ts'
@@ -82,7 +83,7 @@ export function useCollection<
82
83
  ): Collection<TSource, TFlag, TProperties, $TId> {
83
84
  const flagRegistry = useFlagRegistry<TFlag, $TId>(config?.flags)
84
85
 
85
- const sources = computed(() => toValue(_sources))
86
+ const sources = toComputed(_sources)
86
87
 
87
88
  const items = computed(() =>
88
89
  sources.value.map(source => {
@@ -1,5 +1,6 @@
1
+ import { toComputed } from '@core/utils/to-computed.util.ts'
1
2
  import { unrefElement, useEventListener } from '@vueuse/core'
2
- import { computed, inject, type MaybeRefOrGetter, ref, toValue, watchEffect } from 'vue'
3
+ import { inject, type MaybeRefOrGetter, ref, watchEffect } from 'vue'
3
4
  import { type FormOption, IK_FORM_SELECT_CONTROLLER } from './types.ts'
4
5
 
5
6
  export function useFormOptionController<TOption extends FormOption>(_option: MaybeRefOrGetter<TOption>) {
@@ -9,7 +10,7 @@ export function useFormOptionController<TOption extends FormOption>(_option: May
9
10
  throw new Error('useFormOption needs a FormSelectController to be injected')
10
11
  }
11
12
 
12
- const option = computed(() => toValue(_option))
13
+ const option = toComputed(_option)
13
14
 
14
15
  const elementRef = ref<HTMLDivElement>()
15
16
 
@@ -7,6 +7,7 @@ import {
7
7
  } from '@core/packages/collection'
8
8
  import type { EmptyObject, MaybeArray } from '@core/types/utility.type.ts'
9
9
  import { toArray } from '@core/utils/to-array.utils.ts'
10
+ import { toComputed } from '@core/utils/to-computed.util.ts'
10
11
  import { computed, type ComputedRef, type MaybeRefOrGetter, provide, ref, type Ref, toRaw, toValue, watch } from 'vue'
11
12
  import { guessLabel } from './guess-label.ts'
12
13
  import { guessValue } from './guess-value.ts'
@@ -259,19 +260,19 @@ export function useFormSelect<
259
260
 
260
261
  const normalizedSearchTerm = computed(() => normalizeSearchTerm(searchTerm))
261
262
 
262
- const isMultiple = computed(() => toValue(config?.multiple) ?? false) as ComputedRef<TMultiple>
263
+ const isMultiple = toComputed(config?.multiple, false) as ComputedRef<TMultiple>
263
264
 
264
- const isDisabled = computed(() => toValue(config?.disabled) ?? false)
265
+ const isDisabled = toComputed(config?.disabled, false)
265
266
 
266
- const isLoading = computed(() => toValue(config?.loading) ?? false)
267
+ const isLoading = toComputed(config?.loading, false)
267
268
 
268
- const isRequired = computed(() => toValue(config?.required) ?? false)
269
+ const isRequired = toComputed(config?.required, false)
269
270
 
270
- const placeholder = computed(() => toValue(config?.placeholder) ?? '')
271
+ const placeholder = toComputed(config?.placeholder, '')
271
272
 
272
- const searchPlaceholder = computed(() => toValue(config?.searchPlaceholder) ?? '')
273
+ const searchPlaceholder = toComputed(config?.searchPlaceholder, '')
273
274
 
274
- const isSearchable = computed(() => toValue(config?.searchable) ?? false)
275
+ const isSearchable = toComputed(config?.searchable, false)
275
276
 
276
277
  const sources = computed(() =>
277
278
  config?.emptyOption !== undefined ? [EMPTY_OPTION, ...toValue(baseSources)] : toValue(baseSources)
@@ -1,5 +1,6 @@
1
1
  import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
2
- import { computed, type MaybeRefOrGetter, reactive, ref, toValue } from 'vue'
2
+ import { toComputed } from '@core/utils/to-computed.util'
3
+ import { computed, type MaybeRefOrGetter, reactive, ref } from 'vue'
3
4
 
4
5
  export interface MenuActionConfig {
5
6
  handler: () => any
@@ -33,7 +34,7 @@ export class MenuAction extends BaseItem {
33
34
  }
34
35
 
35
36
  get busyConfig() {
36
- return computed(() => toValue(this.config.busy) ?? false)
37
+ return toComputed(this.config.busy, false)
37
38
  }
38
39
 
39
40
  get isBusy() {
@@ -45,7 +46,7 @@ export class MenuAction extends BaseItem {
45
46
  }
46
47
 
47
48
  get disabledConfig() {
48
- return computed(() => toValue(this.config.disabled) ?? false)
49
+ return toComputed(this.config.disabled, false)
49
50
  }
50
51
 
51
52
  get isDisabled() {
@@ -1,5 +1,6 @@
1
1
  import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
2
- import { computed, type MaybeRefOrGetter, reactive, toValue } from 'vue'
2
+ import { toComputed } from '@core/utils/to-computed.util'
3
+ import { type MaybeRefOrGetter, reactive } from 'vue'
3
4
 
4
5
  export interface MenuLinkConfig {
5
6
  href: MaybeRefOrGetter<string>
@@ -34,9 +35,9 @@ export class MenuLink extends BaseItem {
34
35
  as: 'a',
35
36
  onMouseenter: () => this.activate(),
36
37
  onClick: () => this.deactivate(),
37
- href: computed(() => toValue(this.config.href)),
38
- rel: computed(() => toValue(this.config.rel) ?? 'noreferrer noopener'),
39
- target: computed(() => toValue(this.config.target) ?? '_blank'),
38
+ href: toComputed(this.config.href),
39
+ rel: toComputed(this.config.rel, 'noreferrer noopener'),
40
+ target: toComputed(this.config.target, '_blank'),
40
41
  'data-menu-id': this.menu.context.id,
41
42
  })
42
43
  }
@@ -1,5 +1,6 @@
1
1
  import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
2
- import { computed, markRaw, type MaybeRefOrGetter, reactive, toValue } from 'vue'
2
+ import { toComputed } from '@core/utils/to-computed.util'
3
+ import { markRaw, type MaybeRefOrGetter, reactive } from 'vue'
3
4
  import { type RouteLocationRaw, RouterLink } from 'vue-router'
4
5
 
5
6
  export interface MenuRouterLinkConfig {
@@ -31,7 +32,7 @@ export class MenuRouterLink extends BaseItem {
31
32
  as: markRaw(RouterLink),
32
33
  onMouseenter: () => this.activate(),
33
34
  onClick: () => this.deactivate(),
34
- to: computed(() => toValue(this.config.to)),
35
+ to: toComputed(this.config.to),
35
36
  'data-menu-id': this.menu.context.id,
36
37
  })
37
38
  }
@@ -1,7 +1,8 @@
1
1
  import type { MenuToggleTrigger } from '@core/packages/menu'
2
+ import { toComputed } from '@core/utils/to-computed.util'
2
3
  import { autoUpdate, flip, type Placement, shift, useFloating, type UseFloatingReturn } from '@floating-ui/vue'
3
4
  import { unrefElement } from '@vueuse/core'
4
- import { computed, reactive, ref, type Ref, toValue, type UnwrapRef } from 'vue'
5
+ import { computed, reactive, ref, type Ref, type UnwrapRef } from 'vue'
5
6
 
6
7
  export interface MenuToggleTargetConfig {
7
8
  placement?: Placement
@@ -26,7 +27,7 @@ export class MenuToggleTarget {
26
27
  public config: MenuToggleTargetConfig
27
28
  ) {
28
29
  const { floatingStyles } = useFloating(trigger.element, this.element, {
29
- placement: computed(() => toValue(config.placement) ?? 'bottom-start'),
30
+ placement: toComputed(config.placement, 'bottom-start'),
30
31
  open: trigger.isOpen,
31
32
  whileElementsMounted: autoUpdate,
32
33
  middleware: [shift(), flip()],
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <slot />
3
+ </template>
4
+
5
+ <script lang="ts" setup>
6
+ import { IK_MODAL, type RegisteredModal } from '@core/packages/modal/types.ts'
7
+ import { computed, provide } from 'vue'
8
+
9
+ const { modal } = defineProps<{
10
+ modal: RegisteredModal
11
+ }>()
12
+
13
+ provide(
14
+ IK_MODAL,
15
+ computed(() => modal)
16
+ )
17
+ </script>
@@ -0,0 +1,253 @@
1
+ # Modal System
2
+
3
+ ### Modals list component
4
+
5
+ First, create a component which will display the modals.
6
+
7
+ If needed, you can use the `ModalProvider` component to provide the `modal` to children components.
8
+
9
+ For example:
10
+
11
+ ```vue
12
+ <template>
13
+ <div v-if="modalStore.modals.length > 0" class="modals">
14
+ <ModalProvider v-for="modal of modalStore.modals" :key="modal.id" :modal>
15
+ <component :is="modal.component" class="modal" v-bind="modal.bindings" />
16
+ </ModalProvider>
17
+ </div>
18
+ </template>
19
+
20
+ <script lang="ts" setup>
21
+ const modalStore = useModalStore()
22
+ </script>
23
+
24
+ <style lang="postcss" scoped>
25
+ .modals {
26
+ position: fixed;
27
+ inset: 0;
28
+ background-color: var(--color-opacity-primary);
29
+ z-index: 1010;
30
+
31
+ .modal:not(:last-child) {
32
+ filter: brightness(0.8);
33
+ }
34
+ }
35
+ </style>
36
+ ```
37
+
38
+ ### Modal component
39
+
40
+ You then need to create a Modal component which will emit `confirm` and `cancel` events, as needed.
41
+
42
+ ```vue
43
+ <template>
44
+ <div class="modal-container">
45
+ <div class="modal">
46
+ <h1>My modal</h1>
47
+ <button @click="emit('confirm')">Confirm</button>
48
+ <button @click="emit('cancel')">Cancel</button>
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <script lang="ts" setup>
54
+ const emit = defineEmits<{
55
+ confirm: []
56
+ cancel: []
57
+ }>()
58
+ </script>
59
+
60
+ <style lang="postcss" scoped>
61
+ .modal-container {
62
+ position: fixed;
63
+ inset: 0;
64
+ display: flex;
65
+ justify-content: center;
66
+ align-items: center;
67
+ }
68
+
69
+ .modal {
70
+ background: white;
71
+ border-radius: 8px;
72
+ padding: 2rem;
73
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
74
+ }
75
+ </style>
76
+ ```
77
+
78
+ ### Opening a modal (`useModal` composable)
79
+
80
+ You can create a function to open a modal with `useModal`
81
+
82
+ ```ts
83
+ const openModal = useModal(config)
84
+ ```
85
+
86
+ | Property | Type | Required | Default | Description |
87
+ | --------------------- | ------------------------------------- | :------: | :---------: | -------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | component | `Promise<Component>` | ✓ | | The promise of the modal component to open. (e.g: `component: import('path/to/modal.vue')` |
89
+ | props | `Record<string, MaybeRef<any>>` | | `{}` | The props to pass to the modal component |
90
+ | onConfirm | `(...args: any[]) => TConfirmPayload` | | `undefined` | An optional callback to call when the modal is confirmed. It will take the args of the `confirm` event of the modal's `defineEmits`, if any. |
91
+ | onCancel | `(...args: any[]) => TCancelPayload` | | `undefined` | An optional callback to call when the modal is cancelled. It will take the args of the `cancel` event of the modal's `defineEmits`, if any. |
92
+ | keepOpenOnRouteChange | `boolean` | | `false` | By default, the modal will close when the use navigates away from the page. If `true`, the modal will stay open until it is closed manually. |
93
+
94
+ The result of `openModal()` will be `Promise<ModalConfirmResponse<TConfirmPayload> | ModalCancelResponse<TCancelPayload>>`
95
+
96
+ If `onConfirm` / `onCancel` returns a `Promise`, then the modal will not close until the promise is resolved, and its `busy` state will be set to `true`.
97
+
98
+ ```ts
99
+ const openMyModal = useModal({
100
+ component: import('path/to/MyModal.vue'),
101
+ props: {
102
+ foo: 'Foo',
103
+ bar: computed(() => someBar),
104
+ },
105
+ onConfirm: () => console.log('Confirmed!'),
106
+ onCancel: () => console.log('Cancelled!'),
107
+ })
108
+ ```
109
+
110
+ ```html
111
+ <button @click="openModal()">Open modal</button>
112
+ ```
113
+
114
+ ## Modal payload
115
+
116
+ Modal payloads are fully typed.
117
+
118
+ ```ts
119
+ const openMyModal = useModal({
120
+ component: import('path/to/MyModal.vue'),
121
+ onConfirm: () => 'Hello',
122
+ onCancel: () => 1234,
123
+ })
124
+
125
+ const result = await openMyModal()
126
+
127
+ if (result.confirmed) {
128
+ result.payload // string
129
+ } else {
130
+ result.payload // number
131
+ }
132
+ ```
133
+
134
+ ## Opening a modal with arguments
135
+
136
+ You can pass a function returning a config instead of a config object.
137
+
138
+ ```ts
139
+ const openMyModal = useModal((name: string) => ({
140
+ component: import('path/to/MyModal.vue'),
141
+ props: { name },
142
+ onConfirm: () => console.log('Confirmed for', name),
143
+ onCancel: () => console.log('Cancelled for', name),
144
+ })
145
+ ```
146
+
147
+ ```html
148
+ <button @click="openMyModal('John')">Open John modal</button>
149
+ <button @click="openMyModal('Jane')">Open Jane modal</button>
150
+ ```
151
+
152
+ ## `onConfirm` and `onCancel` event args
153
+
154
+ If your Modal component defines args for `confirm` and `cancel` events, you'll get them as arguments of the `onConfirm` and `onCancel` callbacks.
155
+
156
+ ```vue
157
+ <!-- MyModal.vue -->
158
+ <template>
159
+ <div class="modal-container">
160
+ <div class="modal">
161
+ <h1>My modal</h1>
162
+ <button @click="emit('confirm', 1)">Select 1</button>
163
+ <button @click="emit('confirm', 10)">Select 10</button>
164
+ <button @click="emit('cancel')">Cancel</button>
165
+ </div>
166
+ </div>
167
+ </template>
168
+
169
+ <script lang="ts" setup>
170
+ defineEmits<{
171
+ confirm: [count: number]
172
+ cancel: []
173
+ }>()
174
+ ```
175
+
176
+ ```ts
177
+ const openModal = useModal({
178
+ component: import('path/to/MyModal.vue'),
179
+ onConfirm: (count: number) => console.log('Confirmed with count', count),
180
+ })
181
+ </script>
182
+ ```
183
+
184
+ ## Opening a modal from elsewhere
185
+
186
+ `useModal` muse be called at the root of your component.
187
+
188
+ If you need to open a modal from elsewhere, you can use `useModal()` with no arguments to get a function that you can call later to open the modal.
189
+
190
+ ```ts
191
+ const openModal = useModal()
192
+
193
+ function myHandler() {
194
+ return someApiManager.doSomething(myArg, openModal)
195
+ }
196
+
197
+ // api-manager.ts
198
+
199
+ function doSomething(arg: string, openModal: OpenModal) {
200
+ // ... do something
201
+
202
+ openModal('some-id', {
203
+ component: import('path/to/MyModal.vue'),
204
+ props: { arg },
205
+ onConfirm: () => console.log('Confirmed!'),
206
+ onCancel: () => console.log('Cancelled!'),
207
+ })
208
+ }
209
+ ```
210
+
211
+ ## Chaining modals
212
+
213
+ You can open a modal from another modal.
214
+
215
+ For example, you can open a confirmation modal ("Are you sure...") from the `onConfirm` of a delete modal, or from the `onCancel` of a form modal which has unsaved changes.
216
+
217
+ If the second modal is canceled, the first modal will stay open.
218
+
219
+ ```ts
220
+ const openModal = useModal()
221
+
222
+ const openDeleteModal = useModal({
223
+ component: import('path/to/DeleteModal.vue'),
224
+ onConfirm: () =>
225
+ openModal('confirm-delete', {
226
+ component: import('path/to/ConfirmModal.vue'),
227
+ props: { message: 'Are you sure?' },
228
+ onConfirm: async () => {
229
+ console.log('Deleting...')
230
+ await deleteResource()
231
+ },
232
+ }),
233
+ })
234
+ ```
235
+
236
+ ## Aborting the closing of a modal
237
+
238
+ If you want to prevent a modal from closing manually, you can return the `ABORT_MODAL` symbol from the `onConfirm` or `onCancel` handlers.
239
+
240
+ ```ts
241
+ const openModal = useModal()
242
+
243
+ const openDeleteModal = useModal({
244
+ component: import('path/to/DeleteModal.vue'),
245
+ onConfirm: async () => {
246
+ try {
247
+ await tryingToDeleteResource()
248
+ } catch (e) {
249
+ return ABORT_MODAL
250
+ }
251
+ },
252
+ })
253
+ ```