@xen-orchestra/web-core 0.6.0 → 0.8.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/lib/assets/css/typography/_style.pcss +1 -0
- package/lib/assets/not-found.svg +129 -0
- package/lib/components/card/VtsCardRowKeyValue.vue +48 -0
- package/lib/components/layout/VtsLayoutSidebar.vue +1 -1
- package/lib/components/state-hero/VtsErrorNoDataHero.vue +11 -0
- package/lib/components/state-hero/VtsNoSelectionHero.vue +13 -0
- package/lib/components/state-hero/VtsObjectNotFoundHero.vue +3 -2
- package/lib/components/state-hero/VtsPageNotFoundHero.vue +30 -0
- package/lib/components/state-hero/VtsStateHero.vue +31 -3
- package/lib/components/ui/donut-chart/UiDonutChart.vue +2 -2
- package/lib/components/ui/dropdown-button/UiDropdownButton.vue +81 -0
- package/lib/components/ui/link/UiLink.vue +75 -0
- package/lib/components/ui/tag/UiTagsList.vue +14 -0
- package/lib/composables/link-component.composable.ts +53 -0
- package/lib/locales/cs.json +1 -0
- package/lib/locales/de.json +3 -0
- package/lib/locales/en.json +5 -0
- package/lib/locales/fa.json +3 -0
- package/lib/locales/fr.json +5 -0
- package/lib/packages/job/README.md +130 -0
- package/lib/packages/job/define-job-arg.ts +12 -0
- package/lib/packages/job/define-job.ts +130 -0
- package/lib/packages/job/index.ts +4 -0
- package/lib/packages/job/job-error.ts +14 -0
- package/lib/packages/job/use-job-store.ts +44 -0
- package/lib/packages/menu/README.md +194 -0
- package/lib/packages/menu/action.ts +101 -0
- package/lib/packages/menu/base.ts +26 -0
- package/lib/packages/menu/context.ts +27 -0
- package/lib/packages/menu/index.ts +10 -0
- package/lib/packages/menu/job.ts +15 -0
- package/lib/packages/menu/link.ts +56 -0
- package/lib/packages/menu/menu.ts +50 -0
- package/lib/packages/menu/router-link.ts +51 -0
- package/lib/packages/menu/structure.ts +88 -0
- package/lib/packages/menu/toggle-target.ts +59 -0
- package/lib/packages/menu/toggle-trigger.ts +72 -0
- package/lib/packages/menu/toggle.ts +43 -0
- package/package.json +2 -1
package/lib/locales/fa.json
CHANGED
|
@@ -33,13 +33,16 @@
|
|
|
33
33
|
|
|
34
34
|
"dashboard": "داشبورد",
|
|
35
35
|
"documentation-name": "اسناد {name}",
|
|
36
|
+
"error-no-data": "خطا، نمی توان داده ها را جمع آوری کرد.",
|
|
36
37
|
"fullscreen": "تمام صفحه",
|
|
38
|
+
"gateway": "دروازه",
|
|
37
39
|
"learn-more": "بیشتر بدانید",
|
|
38
40
|
"loading-in-progress": "بارگیری در حال انجام است…",
|
|
39
41
|
"log-out": "خروج",
|
|
40
42
|
"master": "میزبان اصلی",
|
|
41
43
|
"network": "شبکه",
|
|
42
44
|
"open-console-in-new-tab": "باز کردن کنسول در برگه جدید",
|
|
45
|
+
"page-not-found": "این صفحه پیدا نمی شود…",
|
|
43
46
|
"patches": "وصله ها",
|
|
44
47
|
"power-on-vm-for-console": "ماشین مجازی خود را روشن کنید تا به کنسول آن دسترسی داشته باشید",
|
|
45
48
|
"power-on-host-for-console": "هاست خود را برای دسترسی به کنسول آن روشن کنید",
|
package/lib/locales/fr.json
CHANGED
|
@@ -45,8 +45,10 @@
|
|
|
45
45
|
"access-forum": "Accès au forum",
|
|
46
46
|
"dashboard": "Tableau de bord",
|
|
47
47
|
"documentation-name": "Documentation {name}",
|
|
48
|
+
"error-no-data": "Erreur, impossible de collecter les données.",
|
|
48
49
|
"exit-fullscreen": "Quitter le plein écran",
|
|
49
50
|
"fullscreen": "Plein écran",
|
|
51
|
+
"gateway": "Passerelle",
|
|
50
52
|
"hosts": "Hôtes",
|
|
51
53
|
"learn-more": "En savoir plus",
|
|
52
54
|
"loading-in-progress": "Chargement en cours…",
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
"object-not-found": "L'objet {id} est introuvable…",
|
|
59
61
|
"open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet",
|
|
60
62
|
"other": "Autre",
|
|
63
|
+
"page-not-found": "Cette page est introuvable…",
|
|
61
64
|
"patches": "Patches",
|
|
62
65
|
"power-on-vm-for-console": "Allumez votre VM pour accéder à sa console",
|
|
63
66
|
"power-on-host-for-console": "Allumez votre hôte pour accéder à sa console",
|
|
@@ -65,8 +68,10 @@
|
|
|
65
68
|
"receive": "Recevoir",
|
|
66
69
|
"running-vm": "VM en cours d'exécution | VMs en cours d'exécution",
|
|
67
70
|
"see-all": "Voir tout",
|
|
71
|
+
"select-to-see-details": "Sélectionnez un élement pour voir les details",
|
|
68
72
|
"send": "Envoyer",
|
|
69
73
|
"send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr",
|
|
74
|
+
"speed": "Vitesse",
|
|
70
75
|
"stats": "Stats",
|
|
71
76
|
"storage": "Stockage",
|
|
72
77
|
"system": "Système",
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Job System Documentation
|
|
2
|
+
|
|
3
|
+
The Job System provides a type-safe way to manage asynchronous operations with built-in state handling and identity-based concurrency control.
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
- **Job**: An asynchronous operation with validation, state management, and concurrency control
|
|
8
|
+
- **Job Arguments**: Typed parameters that can be single values or arrays
|
|
9
|
+
- **Job Identity**: Values used to track concurrent executions of the same job
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### When to use argument identity?
|
|
14
|
+
|
|
15
|
+
You'll want to use a Job argument as identity when two different values means two different processes.
|
|
16
|
+
|
|
17
|
+
Let's imagine a job to save a document, which will take `id` and `content` as arguments.
|
|
18
|
+
|
|
19
|
+
Should the job be tracked by the document ID? Yes, because `isRunning` could be `true` for file A and `false` for file B.
|
|
20
|
+
|
|
21
|
+
Should the job be tracked by the document content? No, because `isRunning` should stay `true` for a specific file, even if the `content` value changes.
|
|
22
|
+
|
|
23
|
+
### Defining Job Arguments
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const userIdArg = defineJobArg<string>({
|
|
27
|
+
identify: true, // Automatically use value as identity for primitive types
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const itemsArg = defineJobArg<Item>({
|
|
31
|
+
identify: item => item.id, // `Item` not being a primitive, we need to use a function to get its identity
|
|
32
|
+
toArray: true, // this argument will always be converted to an array if needed
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const forceArg = defineJobArg<boolean>({
|
|
36
|
+
identify: false, // Don't track this argument for concurrency. Whether it to be true or false shouldn't affect the running state of the job
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Defining a Job
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const myJob = defineJob('processItems', [userIdArg, itemsArg, optionsArg], () => ({
|
|
44
|
+
validate(isRunning, userId, items) {
|
|
45
|
+
// You can use custom running check additionnal to internal one
|
|
46
|
+
if (isRunning || isProcessingItems(userId, items)) {
|
|
47
|
+
throw new JobRunningError('Items are being processed')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (items.length === 0) {
|
|
51
|
+
throw new JobError('No item to process')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!userId) {
|
|
55
|
+
throw new JobError('User ID required')
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
run(userId, items, force) {
|
|
59
|
+
// Job implementation
|
|
60
|
+
return procesItems(userId, items, force)
|
|
61
|
+
},
|
|
62
|
+
}))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Type System
|
|
66
|
+
|
|
67
|
+
### Argument Types
|
|
68
|
+
|
|
69
|
+
When defining a job argument with `defineJobArg<string>({ toArray?: false })`, then:
|
|
70
|
+
|
|
71
|
+
- the `defineJob`'s `run` handler will receive a `string`
|
|
72
|
+
- the `defineJob`'s `validate` handler will receive a `string | undefined`
|
|
73
|
+
- the generated `useJob` will receive a `MaybeRefOrGetter<string | undefined>`
|
|
74
|
+
|
|
75
|
+
When defining a job argument with `defineJobArg<string>({ toArray: true })`, then:
|
|
76
|
+
|
|
77
|
+
- the `defineJob`'s `run` handler will receive a `string[]`
|
|
78
|
+
- the `defineJob`'s `validate` handler will receive a `string[] | undefined`
|
|
79
|
+
- the generated `useJob` will receive a `MaybeRefOrGetter<MaybeArray<string | undefined>>`
|
|
80
|
+
|
|
81
|
+
### Using a Job
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Define reactive arguments
|
|
85
|
+
const userId = ref()
|
|
86
|
+
|
|
87
|
+
const selectedItems = computed(() => props.selectedItems)
|
|
88
|
+
|
|
89
|
+
// Create job instance with destructured properties
|
|
90
|
+
const {
|
|
91
|
+
run, // Execute the handler
|
|
92
|
+
canRun, // A `boolean` computed
|
|
93
|
+
errorMessage, // A `string | undefined` computed
|
|
94
|
+
isRunning, // A `boolean` computed
|
|
95
|
+
} = useMyJob(userId, selectedItems, false)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
When calling `run`, the job will be marked as "running" for the specified `userId` and `selectedItems` (`force` is ignored because it has been configured with `identify: false`)
|
|
99
|
+
|
|
100
|
+
When using an array, the tracking is done for each item individually.
|
|
101
|
+
|
|
102
|
+
Let's take a simplified example for a 5-second handling process:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const userId = ref(1)
|
|
106
|
+
|
|
107
|
+
const selectedItems = ref([{ id: 'A' }, { id: 'B' }])
|
|
108
|
+
|
|
109
|
+
const { run, isRunning } = useMyJob(userId, selectedItems)
|
|
110
|
+
|
|
111
|
+
run()
|
|
112
|
+
|
|
113
|
+
// isRunning = true
|
|
114
|
+
|
|
115
|
+
userId.value = 2
|
|
116
|
+
|
|
117
|
+
// isRunning = false
|
|
118
|
+
|
|
119
|
+
userId.value = 1
|
|
120
|
+
|
|
121
|
+
// isRunning = true
|
|
122
|
+
|
|
123
|
+
selectedItems.value = [{ id: 'A' }, { id: 'C' }]
|
|
124
|
+
|
|
125
|
+
// isRunning = true
|
|
126
|
+
|
|
127
|
+
selectedItems.value = [{ id: 'C' }]
|
|
128
|
+
|
|
129
|
+
// isRunning = false
|
|
130
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type JobIdentity = string | number | boolean | null | undefined
|
|
2
|
+
|
|
3
|
+
export type JobArg<TType = any, TToArray extends boolean = boolean> = {
|
|
4
|
+
identify: ((source: TType) => JobIdentity) | (TType extends JobIdentity ? boolean : false)
|
|
5
|
+
toArray: TToArray
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function defineJobArg<TType>(config: JobArg<TType, true>): JobArg<TType, true>
|
|
9
|
+
export function defineJobArg<TType>(config: JobArg<TType, false>): JobArg<TType, false>
|
|
10
|
+
export function defineJobArg<TType>(config: JobArg<TType>): JobArg<TType> {
|
|
11
|
+
return config
|
|
12
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { JobArg, JobIdentity } from '@core/packages/job/define-job-arg'
|
|
2
|
+
import { JobError, JobRunningError } from '@core/packages/job/job-error'
|
|
3
|
+
import { useJobStore } from '@core/packages/job/use-job-store'
|
|
4
|
+
import type { MaybeArray } from '@core/types/utility.type'
|
|
5
|
+
import { toArray as convertToArray } from '@core/utils/to-array.utils'
|
|
6
|
+
import { computed, type ComputedRef, type MaybeRefOrGetter, toValue } from 'vue'
|
|
7
|
+
|
|
8
|
+
export type JobRunArgs<TJobArgs> = TJobArgs extends [infer TJobArg, ...infer TRest]
|
|
9
|
+
? TJobArg extends JobArg<infer TType, infer TToArray>
|
|
10
|
+
? [TToArray extends true ? TType[] : TType, ...JobRunArgs<TRest>]
|
|
11
|
+
: []
|
|
12
|
+
: []
|
|
13
|
+
|
|
14
|
+
export type JobValidateArgs<TJobArgs> = TJobArgs extends [infer TJobArg, ...infer TRest]
|
|
15
|
+
? TJobArg extends JobArg<infer TType, infer TToArray>
|
|
16
|
+
? [TToArray extends true ? TType[] : TType | undefined, ...JobValidateArgs<TRest>]
|
|
17
|
+
: []
|
|
18
|
+
: []
|
|
19
|
+
|
|
20
|
+
export type JobUseArgs<TJobArgs> = TJobArgs extends [infer TJobArg, ...infer TRest]
|
|
21
|
+
? TJobArg extends JobArg<infer TType, infer TToArray>
|
|
22
|
+
? [
|
|
23
|
+
TToArray extends true ? MaybeRefOrGetter<MaybeArray<TType | undefined>> : MaybeRefOrGetter<TType | undefined>,
|
|
24
|
+
...JobUseArgs<TRest>,
|
|
25
|
+
]
|
|
26
|
+
: []
|
|
27
|
+
: []
|
|
28
|
+
|
|
29
|
+
export type JobSetup<TJobArgs extends JobArg[], TRunResult> = () => {
|
|
30
|
+
run: (...args: JobRunArgs<TJobArgs>) => TRunResult
|
|
31
|
+
validate: (isRunning: boolean, ...args: JobValidateArgs<TJobArgs>) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Job<TRunResult> = {
|
|
35
|
+
run: () => Promise<TRunResult>
|
|
36
|
+
canRun: ComputedRef<boolean>
|
|
37
|
+
error: ComputedRef<JobError | undefined>
|
|
38
|
+
errorMessage: ComputedRef<string | undefined>
|
|
39
|
+
isRunning: ComputedRef<boolean>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function defineJob<const TJobArgs extends JobArg[], TRunResult>(
|
|
43
|
+
name: string,
|
|
44
|
+
jobArgs: TJobArgs,
|
|
45
|
+
setup: JobSetup<TJobArgs, TRunResult>
|
|
46
|
+
) {
|
|
47
|
+
const jobId = Symbol('jobId')
|
|
48
|
+
|
|
49
|
+
return (...useArgs: JobUseArgs<TJobArgs>) => {
|
|
50
|
+
const config = setup()
|
|
51
|
+
const jobStore = useJobStore()
|
|
52
|
+
|
|
53
|
+
const args = computed(() =>
|
|
54
|
+
useArgs.map((useArg, index) => {
|
|
55
|
+
if (jobArgs[index].toArray) {
|
|
56
|
+
return convertToArray(toValue(useArg))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return toValue(useArg)
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const identities = computed<JobIdentity[][]>(() =>
|
|
64
|
+
args.value.map<JobIdentity[]>((arg, index) => {
|
|
65
|
+
const { toArray, identify } = jobArgs[index]
|
|
66
|
+
|
|
67
|
+
if (identify === false) {
|
|
68
|
+
return [undefined]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (identify === true) {
|
|
72
|
+
return convertToArray(arg)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (toArray) {
|
|
76
|
+
return arg.map(identify)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [identify(arg)]
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
function validate() {
|
|
84
|
+
config.validate(jobStore.isRunning(jobId, identities.value), ...(args.value as JobValidateArgs<TJobArgs>))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const error = computed(() => {
|
|
88
|
+
try {
|
|
89
|
+
validate()
|
|
90
|
+
|
|
91
|
+
return undefined
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof JobError) {
|
|
94
|
+
error.args = args.value
|
|
95
|
+
error.jobName = name
|
|
96
|
+
|
|
97
|
+
return error
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return new JobError('Unknown job error', name, args.value, error)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const isRunning = computed(() => error.value instanceof JobRunningError)
|
|
105
|
+
|
|
106
|
+
const errorMessage = computed(() => error.value?.message)
|
|
107
|
+
|
|
108
|
+
const canRun = computed(() => error.value === undefined)
|
|
109
|
+
|
|
110
|
+
async function run() {
|
|
111
|
+
validate()
|
|
112
|
+
|
|
113
|
+
const runId = jobStore.start(jobId, identities.value)
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
return await config.run(...(args.value as JobRunArgs<TJobArgs>))
|
|
117
|
+
} finally {
|
|
118
|
+
jobStore.stop(runId)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
run,
|
|
124
|
+
canRun,
|
|
125
|
+
error,
|
|
126
|
+
errorMessage,
|
|
127
|
+
isRunning,
|
|
128
|
+
} satisfies Job<TRunResult>
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class JobError extends Error {
|
|
2
|
+
jobName: string | undefined
|
|
3
|
+
args: any[] | undefined
|
|
4
|
+
previousError: any
|
|
5
|
+
|
|
6
|
+
constructor(message: string, jobName?: string, args?: any[], previousError?: any) {
|
|
7
|
+
super(message)
|
|
8
|
+
this.args = args
|
|
9
|
+
this.jobName = jobName
|
|
10
|
+
this.previousError = previousError
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class JobRunningError extends JobError {}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { JobIdentity } from '@core/packages/job/define-job-arg'
|
|
2
|
+
import { toArray } from '@core/utils/to-array.utils'
|
|
3
|
+
import { defineStore } from 'pinia'
|
|
4
|
+
import { shallowReactive } from 'vue'
|
|
5
|
+
|
|
6
|
+
export const useJobStore = defineStore('job', () => {
|
|
7
|
+
const runningJobs = shallowReactive(new Map<symbol, { jobId: symbol; identities: JobIdentity[][] }>())
|
|
8
|
+
|
|
9
|
+
function start(id: symbol, identities: JobIdentity[][]) {
|
|
10
|
+
const runId = Symbol(`Job run ID`)
|
|
11
|
+
|
|
12
|
+
runningJobs.set(runId, {
|
|
13
|
+
jobId: id,
|
|
14
|
+
identities,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
return runId
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stop(runId: symbol) {
|
|
21
|
+
runningJobs.delete(runId)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRunning(id: symbol, identitiesToCheck: JobIdentity[][]) {
|
|
25
|
+
return Array.from(runningJobs.values()).some(runningJob => {
|
|
26
|
+
if (runningJob.jobId !== id) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
identitiesToCheck.length === 0 ||
|
|
32
|
+
identitiesToCheck.every((identityToCheck, index) =>
|
|
33
|
+
toArray(identityToCheck).some(identity => runningJob.identities[index]?.includes(identity))
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
start,
|
|
41
|
+
stop,
|
|
42
|
+
isRunning,
|
|
43
|
+
}
|
|
44
|
+
})
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Menu System
|
|
2
|
+
|
|
3
|
+
A menu system for Vue applications supporting props binding for actions, links, router links, and nested submenus.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
```vue
|
|
8
|
+
<template>
|
|
9
|
+
<MenuList>
|
|
10
|
+
<li><MenuTrigger v-bind="menu.save">Save</MenuTrigger></li>
|
|
11
|
+
<li><MenuTrigger v-bind="menu.doc">Documentation</MenuTrigger></li>
|
|
12
|
+
<li><MenuTrigger v-bind="menu.profile">Profile</MenuTrigger></li>
|
|
13
|
+
<li>
|
|
14
|
+
<MenuTrigger v-bind="menu.more.$trigger">More...</MenuTrigger>
|
|
15
|
+
<MenuList v-bind="menu.more.$target">
|
|
16
|
+
<MenuTrigger v-bind="menu.more.settings">Settings</MenuTrigger>
|
|
17
|
+
<MenuTrigger v-bind="menu.more.logout">Logout</MenuTrigger>
|
|
18
|
+
</MenuList>
|
|
19
|
+
</li>
|
|
20
|
+
</MenuList>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script lang="ts" setup>
|
|
24
|
+
import { action, link, routerLink, toggle, useMenu } from '@core/packages/menu'
|
|
25
|
+
|
|
26
|
+
const menu = useMenu({
|
|
27
|
+
save: action(() => console.log('Saving...')),
|
|
28
|
+
doc: link('https://docs.example.com'),
|
|
29
|
+
profile: routerLink({ name: 'profile' }),
|
|
30
|
+
more: toggle({
|
|
31
|
+
settings: action(() => console.log('Settings clicked')),
|
|
32
|
+
logout: action(() => console.log('Logout clicked')),
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
</script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Core Composables
|
|
39
|
+
|
|
40
|
+
### useMenu
|
|
41
|
+
|
|
42
|
+
Create a root menu with multiple items and/or submenus:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const fileMenu = useMenu({
|
|
46
|
+
// Menu structure
|
|
47
|
+
edit: action(() => handleEdit()),
|
|
48
|
+
save: action(() => handleSave(), {
|
|
49
|
+
disabled: () => !canSave.value,
|
|
50
|
+
busy: isSaving,
|
|
51
|
+
}),
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### useMenuAction
|
|
56
|
+
|
|
57
|
+
Create an action button. It must be attached to a menu.
|
|
58
|
+
|
|
59
|
+
Mostly useful when splitting a menu into subcomponents.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const props = defineProps<{
|
|
63
|
+
menu: MenuLike
|
|
64
|
+
}>()
|
|
65
|
+
|
|
66
|
+
const action = useMenuAction(menu, () => handleClick(), {
|
|
67
|
+
disabled: isDisabled,
|
|
68
|
+
busy: isLoading,
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### useMenuToggle
|
|
73
|
+
|
|
74
|
+
Create a toggle menu (dropdown) with nested items.
|
|
75
|
+
|
|
76
|
+
You can pass a `parent: MenuLike` as option to attach the toggle to a parent Menu.
|
|
77
|
+
|
|
78
|
+
Skip this option to create a root menu toggle.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const dropdown = useMenuToggle(
|
|
82
|
+
{
|
|
83
|
+
behavior: 'click', // or 'mouseenter'
|
|
84
|
+
placement: 'bottom-start',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
edit: action(() => handleEdit()),
|
|
88
|
+
delete: action(() => handleDelete()),
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Component Integration
|
|
94
|
+
|
|
95
|
+
Once you create the menu, you can bind its items as props to your template.
|
|
96
|
+
|
|
97
|
+
For now, these bindings are meant to be used with a Vue component configured with the same props
|
|
98
|
+
|
|
99
|
+
For other component or HTML Element, you can still bind the props manually.
|
|
100
|
+
|
|
101
|
+
`action()` items will generate the following props binding:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
type Props = {
|
|
105
|
+
as: 'button'
|
|
106
|
+
type: 'button'
|
|
107
|
+
disabled: boolean
|
|
108
|
+
busy: boolean
|
|
109
|
+
tooltip: string | false
|
|
110
|
+
onMouseenter: () => void
|
|
111
|
+
onClick: () => void
|
|
112
|
+
'data-menu-id': string
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`link()` items will generate the following props binding:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
type Props = {
|
|
120
|
+
as: 'a'
|
|
121
|
+
href: string
|
|
122
|
+
rel: 'noreferrer noopener'
|
|
123
|
+
target: '_blank'
|
|
124
|
+
onMouseenter: () => void
|
|
125
|
+
onClick: () => void
|
|
126
|
+
'data-menu-id': string
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`routerLink()` items will generate the following props binding:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
type Props = {
|
|
134
|
+
as: RouterLink
|
|
135
|
+
to: RouteLocationRaw
|
|
136
|
+
onMouseenter: () => void
|
|
137
|
+
onClick: () => void
|
|
138
|
+
'data-menu-id': string
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`toggle()` items will generate an object containing `$trigger`, `$target` and `$isOpen` (`ComputedRef<boolean>`) properties with the following props binding:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// $trigger
|
|
146
|
+
type Props = {
|
|
147
|
+
as: 'button'
|
|
148
|
+
type: 'button'
|
|
149
|
+
submenu: true
|
|
150
|
+
ref: (el: any) => void
|
|
151
|
+
active: boolean
|
|
152
|
+
onClick: () => void
|
|
153
|
+
onMouseenter: () => void
|
|
154
|
+
'data-menu-id': string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// $target
|
|
158
|
+
type Props = {
|
|
159
|
+
ref: (el: any) => void
|
|
160
|
+
style: object
|
|
161
|
+
'data-menu-id': string
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Example
|
|
166
|
+
|
|
167
|
+
```vue
|
|
168
|
+
<template>
|
|
169
|
+
<MenuTrigger v-bind="menu.save">Save</MenuTrigger>
|
|
170
|
+
<MenuTrigger v-bind="menu.docs">Documentation</MenuTrigger>
|
|
171
|
+
<MenuTrigger v-bind="menu.profile">Profile</MenuTrigger>
|
|
172
|
+
|
|
173
|
+
<!-- Toggle/Dropdown menu -->
|
|
174
|
+
<MenuTrigger v-bind="menu.more.$trigger">More</MenuTrigger>
|
|
175
|
+
<div v-bind="menu.more.$target">
|
|
176
|
+
<MenuTrigger v-bind="menu.more.settings">Settings</MenuTrigger>
|
|
177
|
+
<MenuTrigger v-bind="menu.more.logout">Logout</MenuTrigger>
|
|
178
|
+
</div>
|
|
179
|
+
</template>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
If you want to bind props to an HTML element or a Vue component not supporting all the props,
|
|
183
|
+
you can use VueUse's `objectOmit` for example or bind the props completely manually.
|
|
184
|
+
|
|
185
|
+
```vue
|
|
186
|
+
<script>
|
|
187
|
+
import { objectOmit as omit } from '@vueuse/shared'
|
|
188
|
+
</script>
|
|
189
|
+
|
|
190
|
+
<template>
|
|
191
|
+
<MyCustomElement v-bind="omit(menu.save, ['as', 'type'])">Save</MyCustomElement>
|
|
192
|
+
<button v-bind="omit(menu.save, ['as', busy, 'tooltip'])" :class="{ busy: menu.save.busy }">Save</button>
|
|
193
|
+
</template>
|
|
194
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
|
|
2
|
+
import { computed, type MaybeRefOrGetter, reactive, ref, toValue } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface MenuActionConfig {
|
|
5
|
+
handler: () => any
|
|
6
|
+
disabled?: MaybeRefOrGetter<boolean | string | undefined>
|
|
7
|
+
busy?: MaybeRefOrGetter<boolean | string | undefined>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class MenuActionConfigHolder {
|
|
11
|
+
constructor(public config: MenuActionConfig) {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MenuActionProps {
|
|
15
|
+
as: 'button'
|
|
16
|
+
type: 'button'
|
|
17
|
+
disabled: boolean
|
|
18
|
+
busy: boolean
|
|
19
|
+
tooltip: string | false
|
|
20
|
+
onClick: () => void
|
|
21
|
+
onMouseenter: () => void
|
|
22
|
+
'data-menu-id': string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class MenuAction extends BaseItem {
|
|
26
|
+
isRunning = ref(false)
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
public menu: Menu,
|
|
30
|
+
public config: MenuActionConfig
|
|
31
|
+
) {
|
|
32
|
+
super(menu)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get busyConfig() {
|
|
36
|
+
return computed(() => toValue(this.config.busy) ?? false)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get isBusy() {
|
|
40
|
+
return computed(() => this.isRunning.value || this.busyConfig.value !== false)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get busyReason() {
|
|
44
|
+
return computed(() => (typeof this.busyConfig.value === 'string' ? this.busyConfig.value : undefined))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get disabledConfig() {
|
|
48
|
+
return computed(() => toValue(this.config.disabled) ?? false)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get isDisabled() {
|
|
52
|
+
return computed(() => this.isBusy.value || this.disabledConfig.value !== false)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get disabledReason() {
|
|
56
|
+
return computed(() => (typeof this.disabledConfig.value === 'string' ? this.disabledConfig.value : undefined))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get tooltip() {
|
|
60
|
+
return computed(() => this.disabledReason.value ?? this.busyReason.value ?? false)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get props(): MenuActionProps {
|
|
64
|
+
return reactive({
|
|
65
|
+
as: 'button',
|
|
66
|
+
type: 'button',
|
|
67
|
+
onClick: async () => {
|
|
68
|
+
if (this.isDisabled.value) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.isRunning.value = true
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await this.config.handler()
|
|
76
|
+
this.deactivate()
|
|
77
|
+
} finally {
|
|
78
|
+
this.isRunning.value = false
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
onMouseenter: () => this.activate(),
|
|
82
|
+
disabled: this.isDisabled,
|
|
83
|
+
busy: this.isBusy,
|
|
84
|
+
tooltip: this.tooltip,
|
|
85
|
+
'data-menu-id': this.menu.context.id,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function action(handler: () => void, config: Omit<MenuActionConfig, 'handler'> = {}): MenuActionConfigHolder {
|
|
91
|
+
return new MenuActionConfigHolder({
|
|
92
|
+
...config,
|
|
93
|
+
handler,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function useMenuAction(config: MenuActionConfig & { parent: MenuLike }) {
|
|
98
|
+
const { parent, handler, ...configRest } = config
|
|
99
|
+
|
|
100
|
+
return parseConfigHolder(parent, action(handler, configRest))
|
|
101
|
+
}
|