@xen-orchestra/web-core 0.7.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/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/VtsStateHero.vue +30 -2
- 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 +2 -0
- package/lib/locales/en.json +4 -0
- package/lib/locales/fa.json +2 -0
- package/lib/locales/fr.json +4 -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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<VtsStateHero class="vts-error-hero" image="error" :type>{{ $t('error-no-data') }}</VtsStateHero>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script lang="ts" setup>
|
|
6
|
+
import VtsStateHero, { type StateHeroType } from '@core/components/state-hero/VtsStateHero.vue'
|
|
7
|
+
|
|
8
|
+
defineProps<{
|
|
9
|
+
type: StateHeroType
|
|
10
|
+
}>()
|
|
11
|
+
</script>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<VtsStateHero :type class="vts-no-selection-hero" image="no-selection">
|
|
3
|
+
{{ $t('select-to-see-details') }}
|
|
4
|
+
</VtsStateHero>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script lang="ts" setup>
|
|
8
|
+
import VtsStateHero, { type StateHeroType } from '@core/components/state-hero/VtsStateHero.vue'
|
|
9
|
+
|
|
10
|
+
defineProps<{
|
|
11
|
+
type: StateHeroType
|
|
12
|
+
}>()
|
|
13
|
+
</script>
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<VtsStateHero class="vts-object-not-found-hero" image="no-result" type
|
|
2
|
+
<VtsStateHero class="vts-object-not-found-hero" image="no-result" :type>
|
|
3
3
|
{{ $t('object-not-found', { id }) }}
|
|
4
4
|
</VtsStateHero>
|
|
5
5
|
</template>
|
|
6
6
|
|
|
7
7
|
<script lang="ts" setup>
|
|
8
|
-
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
|
8
|
+
import VtsStateHero, { type StateHeroType } from '@core/components/state-hero/VtsStateHero.vue'
|
|
9
9
|
|
|
10
10
|
defineProps<{
|
|
11
11
|
id: string
|
|
12
|
+
type: StateHeroType
|
|
12
13
|
}>()
|
|
13
14
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :class="type" class="vts-state-hero">
|
|
2
|
+
<div :class="[type, { error }]" class="vts-state-hero">
|
|
3
3
|
<UiLoader v-if="busy" class="loader" />
|
|
4
4
|
<img v-else-if="imageSrc" :src="imageSrc" alt="" class="image" />
|
|
5
5
|
<p v-if="slots.default" :class="typoClass" class="text">
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
|
|
13
13
|
import { computed } from 'vue'
|
|
14
14
|
|
|
15
|
-
export type StateHeroType = 'page' | 'card' | 'panel'
|
|
15
|
+
export type StateHeroType = 'page' | 'card' | 'panel' | 'table'
|
|
16
16
|
|
|
17
17
|
const props = defineProps<{
|
|
18
18
|
type: StateHeroType
|
|
@@ -25,6 +25,7 @@ const slots = defineSlots<{
|
|
|
25
25
|
}>()
|
|
26
26
|
|
|
27
27
|
const typoClass = computed(() => (props.type === 'page' ? 'typo h2-black' : 'typo h4-medium'))
|
|
28
|
+
const error = computed(() => !props.busy && props.image === 'error')
|
|
28
29
|
|
|
29
30
|
const imageSrc = computed(() => {
|
|
30
31
|
if (!props.image) {
|
|
@@ -43,6 +44,14 @@ const imageSrc = computed(() => {
|
|
|
43
44
|
align-items: center;
|
|
44
45
|
justify-content: center;
|
|
45
46
|
|
|
47
|
+
&.error {
|
|
48
|
+
background-color: var(--color-danger-background-selected);
|
|
49
|
+
|
|
50
|
+
.text {
|
|
51
|
+
color: var(--color-danger-txt-base);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
.loader,
|
|
47
56
|
.text {
|
|
48
57
|
color: var(--color-info-txt-base);
|
|
@@ -113,5 +122,24 @@ const imageSrc = computed(() => {
|
|
|
113
122
|
width: 80%;
|
|
114
123
|
}
|
|
115
124
|
}
|
|
125
|
+
|
|
126
|
+
&.table {
|
|
127
|
+
padding: 4rem;
|
|
128
|
+
gap: 2.4rem;
|
|
129
|
+
|
|
130
|
+
.text {
|
|
131
|
+
order: 3;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.image {
|
|
135
|
+
order: 2;
|
|
136
|
+
max-height: 20rem;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.loader {
|
|
140
|
+
order: 1;
|
|
141
|
+
font-size: 10rem;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
116
144
|
}
|
|
117
145
|
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- v3 -->
|
|
2
2
|
<template>
|
|
3
3
|
<svg class="ui-donut-chart" viewBox="0 0 100 100">
|
|
4
|
-
<circle class="segment" cx="50" cy="50" r="40" />
|
|
4
|
+
<circle class="segment accent--muted" cx="50" cy="50" r="40" />
|
|
5
5
|
<circle
|
|
6
6
|
v-for="(segment, index) in computedSegments"
|
|
7
7
|
:key="index"
|
|
@@ -44,8 +44,8 @@ const computedSegments = computed(() => {
|
|
|
44
44
|
let nextOffset = circumference / 4
|
|
45
45
|
|
|
46
46
|
return props.segments.map(segment => {
|
|
47
|
+
const percent = totalValue.value === 0 ? 0 : (segment.value / totalValue.value) * circumference
|
|
47
48
|
const offset = nextOffset
|
|
48
|
-
const percent = (segment.value / totalValue.value) * circumference
|
|
49
49
|
nextOffset -= percent
|
|
50
50
|
|
|
51
51
|
return {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!-- v3 -->
|
|
2
|
+
<template>
|
|
3
|
+
<button type="button" class="ui-dropdown-item" :class="{ selected }" :disabled="isDisabled">
|
|
4
|
+
<VtsIcon :icon accent="current" class="left-icon" fixed-width />
|
|
5
|
+
<span class="typo p1-regular label">
|
|
6
|
+
<slot />
|
|
7
|
+
</span>
|
|
8
|
+
<VtsIcon :icon="faAngleDown" accent="current" class="right-icon" fixed-width />
|
|
9
|
+
</button>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script lang="ts" setup>
|
|
13
|
+
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
|
14
|
+
import { useContext } from '@core/composables/context.composable'
|
|
15
|
+
import { DisabledContext } from '@core/context'
|
|
16
|
+
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
|
|
17
|
+
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(
|
|
20
|
+
defineProps<{
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
selected?: boolean
|
|
23
|
+
icon?: IconDefinition
|
|
24
|
+
}>(),
|
|
25
|
+
{ disabled: undefined }
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const isDisabled = useContext(DisabledContext, () => props.disabled)
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<style scoped lang="postcss">
|
|
32
|
+
.ui-dropdown-item {
|
|
33
|
+
display: inline-flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
padding-block: 0.4rem;
|
|
36
|
+
padding-inline: 1.6rem;
|
|
37
|
+
gap: 0.8rem;
|
|
38
|
+
background: var(--color-neutral-background-primary);
|
|
39
|
+
border: 0.1rem solid var(--color-info-txt-base);
|
|
40
|
+
border-radius: 9rem;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
position: relative;
|
|
43
|
+
color: var(--color-info-txt-base);
|
|
44
|
+
|
|
45
|
+
&:hover {
|
|
46
|
+
border-color: var(--color-info-txt-hover);
|
|
47
|
+
background-color: var(--color-info-background-hover);
|
|
48
|
+
color: var(--color-info-txt-hover);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&:active {
|
|
52
|
+
border-color: var(--color-info-txt-active);
|
|
53
|
+
background-color: var(--color-info-background-active);
|
|
54
|
+
color: var(--color-info-txt-active);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&.selected:not(:disabled) {
|
|
58
|
+
background-color: var(--color-info-background-selected);
|
|
59
|
+
outline: 0.1rem solid var(--color-info-txt-base);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&:focus-visible {
|
|
63
|
+
outline: none;
|
|
64
|
+
|
|
65
|
+
&::before {
|
|
66
|
+
content: '';
|
|
67
|
+
position: absolute;
|
|
68
|
+
inset: -0.5rem;
|
|
69
|
+
border: 0.2rem solid var(--color-info-txt-base);
|
|
70
|
+
border-radius: 0.4rem;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&:disabled {
|
|
75
|
+
cursor: not-allowed;
|
|
76
|
+
border-color: var(--color-neutral-txt-secondary);
|
|
77
|
+
background-color: var(--color-neutral-background-disabled);
|
|
78
|
+
color: var(--color-neutral-txt-secondary);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!-- v2 -->
|
|
2
|
+
<template>
|
|
3
|
+
<component :is="component" :class="classes" class="ui-link" v-bind="attributes">
|
|
4
|
+
<VtsIcon :icon accent="current" />
|
|
5
|
+
<slot />
|
|
6
|
+
<VtsIcon
|
|
7
|
+
v-if="attributes.target === '_blank'"
|
|
8
|
+
:icon="faArrowUpRightFromSquare"
|
|
9
|
+
accent="current"
|
|
10
|
+
class="external-icon"
|
|
11
|
+
/>
|
|
12
|
+
</component>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts" setup>
|
|
16
|
+
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
|
17
|
+
import { type LinkOptions, useLinkComponent } from '@core/composables/link-component.composable'
|
|
18
|
+
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
|
|
19
|
+
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
|
|
20
|
+
import { computed } from 'vue'
|
|
21
|
+
|
|
22
|
+
const props = defineProps<
|
|
23
|
+
LinkOptions & {
|
|
24
|
+
size: 'small' | 'medium'
|
|
25
|
+
icon?: IconDefinition
|
|
26
|
+
}
|
|
27
|
+
>()
|
|
28
|
+
|
|
29
|
+
const typoClasses = {
|
|
30
|
+
small: 'typo p3-regular-underline',
|
|
31
|
+
medium: 'typo p1-regular-underline',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { component, attributes, isDisabled } = useLinkComponent('span', () => props)
|
|
35
|
+
|
|
36
|
+
const classes = computed(() => [typoClasses[props.size], { disabled: isDisabled.value }])
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<style lang="postcss" scoped>
|
|
40
|
+
.ui-link {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 0.8rem;
|
|
44
|
+
color: var(--color-info-txt-base);
|
|
45
|
+
|
|
46
|
+
&:hover {
|
|
47
|
+
color: var(--color-info-txt-hover);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&:active {
|
|
51
|
+
color: var(--color-info-txt-active);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&:focus-visible {
|
|
55
|
+
outline: none;
|
|
56
|
+
|
|
57
|
+
&::before {
|
|
58
|
+
content: '';
|
|
59
|
+
position: absolute;
|
|
60
|
+
inset: -0.6rem;
|
|
61
|
+
border: 0.2rem solid var(--color-info-txt-base);
|
|
62
|
+
border-radius: 0.4rem;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&.disabled {
|
|
67
|
+
color: var(--color-neutral-txt-secondary);
|
|
68
|
+
cursor: not-allowed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.external-icon {
|
|
72
|
+
font-size: 0.75em;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import { computed, toValue } from 'vue'
|
|
3
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
4
|
+
|
|
5
|
+
export type LinkOptions = {
|
|
6
|
+
to?: RouteLocationRaw
|
|
7
|
+
href?: string
|
|
8
|
+
target?: '_blank' | '_self'
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useLinkComponent(defaultComponent: string, options: MaybeRefOrGetter<LinkOptions>) {
|
|
13
|
+
const config = computed(() => toValue(options))
|
|
14
|
+
|
|
15
|
+
const isDisabled = computed(() => config.value.disabled || (!config.value.to && !config.value.href))
|
|
16
|
+
|
|
17
|
+
const component = computed(() => {
|
|
18
|
+
if (isDisabled.value) {
|
|
19
|
+
return defaultComponent
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (config.value.href) {
|
|
23
|
+
return 'a'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return 'RouterLink'
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const attributes = computed(() => {
|
|
30
|
+
if (isDisabled.value) {
|
|
31
|
+
return {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (config.value.href) {
|
|
35
|
+
return {
|
|
36
|
+
rel: 'noopener noreferrer',
|
|
37
|
+
target: config.value.target ?? '_blank',
|
|
38
|
+
href: config.value.href,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
target: config.value.target,
|
|
44
|
+
to: config.value.to,
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
isDisabled,
|
|
50
|
+
component,
|
|
51
|
+
attributes,
|
|
52
|
+
}
|
|
53
|
+
}
|
package/lib/locales/cs.json
CHANGED
package/lib/locales/de.json
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
|
|
20
20
|
"dashboard": "Dashboard",
|
|
21
21
|
"documentation-name": "{name} Dokumentation",
|
|
22
|
+
"error-no-data": "Fehler beim Datenabruf.",
|
|
22
23
|
"fullscreen": "Vollbild",
|
|
24
|
+
"gateway": "Gateway",
|
|
23
25
|
"hosts": "Hosts",
|
|
24
26
|
"learn-more": "Mehr erfahren",
|
|
25
27
|
"loading-in-progress": "Ladevorgang läuft…",
|
package/lib/locales/en.json
CHANGED
|
@@ -45,8 +45,10 @@
|
|
|
45
45
|
"access-forum": "Access forum",
|
|
46
46
|
"dashboard": "Dashboard",
|
|
47
47
|
"documentation-name": "{name} documentation",
|
|
48
|
+
"error-no-data": "Error, can't collect data.",
|
|
48
49
|
"exit-fullscreen": "Exit fullscreen",
|
|
49
50
|
"fullscreen": "Fullscreen",
|
|
51
|
+
"gateway": "Gateway",
|
|
50
52
|
"hosts": "Hosts",
|
|
51
53
|
"learn-more": "Learn more",
|
|
52
54
|
"loading-in-progress": "Loading in progress…",
|
|
@@ -66,8 +68,10 @@
|
|
|
66
68
|
"receive": "Receive",
|
|
67
69
|
"running-vm": "Running VM | Running VMs",
|
|
68
70
|
"see-all": "See all",
|
|
71
|
+
"select-to-see-details": "Select an element to see details",
|
|
69
72
|
"send": "Send",
|
|
70
73
|
"send-ctrl-alt-del": "Send Ctrl+Alt+Del",
|
|
74
|
+
"speed": "Speed",
|
|
71
75
|
"stats": "Stats",
|
|
72
76
|
"storage": "Storage",
|
|
73
77
|
"system": "System",
|
package/lib/locales/fa.json
CHANGED
|
@@ -33,7 +33,9 @@
|
|
|
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": "خروج",
|
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…",
|
|
@@ -66,8 +68,10 @@
|
|
|
66
68
|
"receive": "Recevoir",
|
|
67
69
|
"running-vm": "VM en cours d'exécution | VMs en cours d'exécution",
|
|
68
70
|
"see-all": "Voir tout",
|
|
71
|
+
"select-to-see-details": "Sélectionnez un élement pour voir les details",
|
|
69
72
|
"send": "Envoyer",
|
|
70
73
|
"send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr",
|
|
74
|
+
"speed": "Vitesse",
|
|
71
75
|
"stats": "Stats",
|
|
72
76
|
"storage": "Stockage",
|
|
73
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
|
+
}
|