@xen-orchestra/web-core 0.0.2 → 0.0.4

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 (61) hide show
  1. package/env.d.ts +1 -0
  2. package/lib/assets/css/_typography.pcss +1 -0
  3. package/lib/assets/css/typography/_utils.pcss +6 -0
  4. package/lib/assets/no-result.svg +81 -0
  5. package/lib/assets/under-construction.svg +195 -0
  6. package/lib/components/CardNumbers.vue +94 -0
  7. package/lib/components/DonutChart.vue +92 -0
  8. package/lib/components/LegendTitle.vue +31 -0
  9. package/lib/components/StatusPill.vue +2 -2
  10. package/lib/components/UiCard.vue +29 -0
  11. package/lib/components/UiLegend.vue +68 -0
  12. package/lib/components/UiTag.vue +3 -7
  13. package/lib/components/backdrop/Backdrop.vue +11 -0
  14. package/lib/components/card/CardSubtitle.vue +24 -0
  15. package/lib/components/card/CardTitle.vue +52 -0
  16. package/lib/components/cell-object/CellObject.vue +54 -0
  17. package/lib/components/cell-text/CellText.vue +40 -0
  18. package/lib/components/chip/UiChip.vue +5 -6
  19. package/lib/components/console/RemoteConsole.vue +2 -2
  20. package/lib/components/divider/Divider.vue +25 -0
  21. package/lib/components/dropdown/DropdownItem.vue +1 -4
  22. package/lib/components/dropdown/DropdownTitle.vue +7 -3
  23. package/lib/components/head-bar/HeadBar.vue +78 -0
  24. package/lib/components/icon/VmIcon.vue +1 -1
  25. package/lib/components/input/UiInput.vue +133 -0
  26. package/lib/components/menu/MenuItem.vue +1 -1
  27. package/lib/components/menu/MenuList.vue +5 -5
  28. package/lib/components/object-link/ObjectLink.vue +87 -0
  29. package/lib/components/search-bar/SearchBar.vue +60 -0
  30. package/lib/components/stacked-bar/StackedBar.vue +49 -0
  31. package/lib/components/stacked-bar/StackedBarSegment.vue +67 -0
  32. package/lib/components/state-hero/ComingSoonHero.vue +9 -0
  33. package/lib/components/state-hero/LoadingHero.vue +9 -0
  34. package/lib/components/state-hero/ObjectNotFoundHero.vue +13 -0
  35. package/lib/components/state-hero/StateHero.vue +53 -0
  36. package/lib/components/tab/TabList.vue +1 -0
  37. package/lib/components/table/ColumnTitle.vue +150 -0
  38. package/lib/components/table/UiTable.vue +64 -0
  39. package/lib/components/task/QuickTaskButton.vue +62 -0
  40. package/lib/components/task/QuickTaskItem.vue +91 -0
  41. package/lib/components/task/QuickTaskList.vue +48 -0
  42. package/lib/components/task/QuickTaskPanel.vue +65 -0
  43. package/lib/components/task/QuickTaskTabBar.vue +46 -0
  44. package/lib/components/tooltip/TooltipList.vue +0 -2
  45. package/lib/components/tree/TreeItem.vue +8 -8
  46. package/lib/components/tree/TreeItemLabel.vue +32 -16
  47. package/lib/composables/tab-list.composable.ts +33 -0
  48. package/lib/composables/tree/branch.ts +5 -5
  49. package/lib/composables/tree/types.ts +1 -1
  50. package/lib/i18n.ts +86 -1
  51. package/lib/layouts/CoreLayout.vue +6 -106
  52. package/lib/locales/de.json +37 -4
  53. package/lib/locales/en.json +66 -13
  54. package/lib/locales/fa.json +46 -0
  55. package/lib/locales/fr.json +66 -13
  56. package/lib/types/tab.type.ts +17 -0
  57. package/lib/types/task.type.ts +13 -0
  58. package/lib/types/utility.type.ts +2 -0
  59. package/lib/utils/if-else.utils.ts +1 -1
  60. package/lib/utils/open-url.utils.ts +3 -0
  61. package/package.json +16 -8
@@ -0,0 +1,60 @@
1
+ <!-- v1.1 -->
2
+ <template>
3
+ <form class="search-bar" @submit.prevent="emit('search', value)">
4
+ <label v-if="uiStore.isDesktop" :for="id" class="typo p2-regular label">
5
+ {{ $t('core.search-bar.label') }}
6
+ </label>
7
+ <UiInput
8
+ :id
9
+ v-model="value"
10
+ :aria-label="uiStore.isMobile ? $t('core.search-bar.label') : undefined"
11
+ :icon="uiStore.isDesktop ? faMagnifyingGlass : undefined"
12
+ :placeholder="$t('core.search-bar.placeholder')"
13
+ />
14
+ <template v-if="uiStore.isDesktop">
15
+ <UiButton type="submit">{{ $t('core.search') }}</UiButton>
16
+ <Divider type="stretch" />
17
+ <UiButton v-tooltip="$t('coming-soon')" level="secondary" :left-icon="faFilter" disabled>
18
+ {{ $t('core.search-bar.use-query-builder') }}
19
+ </UiButton>
20
+ </template>
21
+ <template v-else>
22
+ <ButtonIcon type="submit" :icon="faMagnifyingGlass" />
23
+ <ButtonIcon disabled :icon="faFilter" />
24
+ </template>
25
+ </form>
26
+ </template>
27
+
28
+ <script lang="ts" setup>
29
+ import ButtonIcon from '@core/components/button/ButtonIcon.vue'
30
+ import UiButton from '@core/components/button/UiButton.vue'
31
+ import Divider from '@core/components/divider/Divider.vue'
32
+ import UiInput from '@core/components/input/UiInput.vue'
33
+ import { vTooltip } from '@core/directives/tooltip.directive'
34
+ import { useUiStore } from '@core/stores/ui.store'
35
+ import { uniqueId } from '@core/utils/unique-id.util'
36
+ import { faFilter, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'
37
+ import { ref } from 'vue'
38
+
39
+ const emit = defineEmits<{
40
+ search: [value: string]
41
+ }>()
42
+
43
+ const id = uniqueId('search-input-')
44
+
45
+ const uiStore = useUiStore()
46
+
47
+ const value = ref<string>('')
48
+ </script>
49
+
50
+ <style lang="postcss" scoped>
51
+ .search-bar {
52
+ display: flex;
53
+ gap: 1.6rem;
54
+ align-items: center;
55
+ }
56
+
57
+ .label {
58
+ color: var(--color-grey-200);
59
+ }
60
+ </style>
@@ -0,0 +1,49 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <div class="stacked-bar">
4
+ <StackedBarSegment
5
+ v-for="(segment, index) in computedSegments"
6
+ :key="index"
7
+ :color="segment.color"
8
+ :percentage="segment.percentage"
9
+ />
10
+ </div>
11
+ </template>
12
+
13
+ <script lang="ts" setup>
14
+ import StackedBarSegment from '@core/components/stacked-bar/StackedBarSegment.vue'
15
+ import type { Color } from '@core/types/color.type'
16
+ import { computed } from 'vue'
17
+
18
+ type Segment = {
19
+ value: number
20
+ color: Color
21
+ }
22
+
23
+ const props = defineProps<{
24
+ segments: Segment[]
25
+ maxValue?: number
26
+ }>()
27
+
28
+ const totalValue = computed(() => props.segments.reduce((acc, bar) => acc + bar.value, 0))
29
+
30
+ const max = computed(() => Math.max(props.maxValue ?? 0, totalValue.value))
31
+
32
+ const computedSegments = computed(() => {
33
+ return props.segments.map(segment => ({
34
+ ...segment,
35
+ percentage: (segment.value / max.value) * 100,
36
+ }))
37
+ })
38
+ </script>
39
+
40
+ <style lang="postcss" scoped>
41
+ .stacked-bar {
42
+ width: 100%;
43
+ display: flex;
44
+ height: 4rem;
45
+ border-radius: 0.8rem;
46
+ overflow: hidden;
47
+ background-color: var(--background-color-purple-10);
48
+ }
49
+ </style>
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <div
3
+ v-tooltip="{ selector: '.text-ellipsis' }"
4
+ :class="color"
5
+ :style="{ width: percentage + '%' }"
6
+ class="stacked-bar-segment typo c4-semi-bold"
7
+ >
8
+ <div ref="ellipsisElement" :class="{ hidden }" class="text-ellipsis">
9
+ {{ $n(percentage / 100, 'percent') }}
10
+ </div>
11
+ </div>
12
+ </template>
13
+
14
+ <script lang="ts" setup>
15
+ import { vTooltip } from '@core/directives/tooltip.directive'
16
+ import type { Color } from '@core/types/color.type'
17
+ import { hasEllipsis } from '@core/utils/has-ellipsis.util'
18
+ import { useResizeObserver } from '@vueuse/core'
19
+ import { ref } from 'vue'
20
+
21
+ defineProps<{
22
+ color: Color
23
+ percentage: number
24
+ }>()
25
+
26
+ const hidden = ref(false)
27
+ const ellipsisElement = ref<HTMLElement | null>(null)
28
+
29
+ useResizeObserver(ellipsisElement, ([entry]) => {
30
+ hidden.value = hasEllipsis(entry.target)
31
+ })
32
+ </script>
33
+
34
+ <style lang="postcss" scoped>
35
+ /* COLOR VARIANT */
36
+ .stacked-bar-segment {
37
+ &.info {
38
+ --background-color: var(--color-purple-base);
39
+ }
40
+
41
+ &.success {
42
+ --background-color: var(--color-green-base);
43
+ }
44
+
45
+ &.warning {
46
+ --background-color: var(--color-orange-base);
47
+ }
48
+
49
+ &.error {
50
+ --background-color: var(--color-red-base);
51
+ }
52
+ }
53
+
54
+ /* IMPLEMENTATION */
55
+ .stacked-bar-segment {
56
+ display: flex;
57
+ justify-content: center;
58
+ align-items: center;
59
+ white-space: nowrap;
60
+ color: var(--color-grey-600);
61
+ background-color: var(--background-color);
62
+ }
63
+
64
+ .hidden {
65
+ visibility: hidden;
66
+ }
67
+ </style>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <StateHero image="under-construction" class="coming-soon-hero">
3
+ {{ $t('coming-soon') }}
4
+ </StateHero>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import StateHero from '@core/components/state-hero/StateHero.vue'
9
+ </script>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <StateHero busy class="loading-hero">
3
+ {{ $t('loading-in-progress') }}
4
+ </StateHero>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import StateHero from '@core/components/state-hero/StateHero.vue'
9
+ </script>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <StateHero image="no-result" class="object-not-found-hero">
3
+ {{ $t('object-not-found', { id }) }}
4
+ </StateHero>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import StateHero from '@core/components/state-hero/StateHero.vue'
9
+
10
+ defineProps<{
11
+ id: string
12
+ }>()
13
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <div class="state-hero">
3
+ <UiSpinner v-if="busy" class="spinner" />
4
+ <img v-else-if="imageSrc" :src="imageSrc" alt="" class="image" />
5
+ <p v-if="$slots.default" class="typo h2-black text">
6
+ <slot />
7
+ </p>
8
+ </div>
9
+ </template>
10
+
11
+ <script lang="ts" setup>
12
+ import UiSpinner from '@core/components/UiSpinner.vue'
13
+ import { computed } from 'vue'
14
+
15
+ const props = defineProps<{
16
+ busy?: boolean
17
+ image?: 'no-result' | 'under-construction' // TODO: 'offline' | 'no-data' | 'not-found' | 'all-good' | 'all-done' | 'error'
18
+ }>()
19
+
20
+ const imageSrc = computed(() => {
21
+ try {
22
+ return new URL(`../../assets/${props.image}.svg`, import.meta.url).href
23
+ } catch {
24
+ return undefined
25
+ }
26
+ })
27
+ </script>
28
+
29
+ <style lang="postcss" scoped>
30
+ .state-hero {
31
+ flex: 1;
32
+ display: flex;
33
+ flex-direction: column;
34
+ align-items: center;
35
+ justify-content: center;
36
+ gap: 4.2rem;
37
+ }
38
+
39
+ .image {
40
+ width: 90%;
41
+ max-width: 55rem;
42
+ }
43
+
44
+ .spinner {
45
+ color: var(--color-purple-base);
46
+ font-size: 10rem;
47
+ }
48
+
49
+ .text {
50
+ color: var(--color-purple-base);
51
+ margin-top: 4rem;
52
+ }
53
+ </style>
@@ -28,5 +28,6 @@ useContext(DisabledContext, () => props.disabled)
28
28
  background-color: var(--background-color-primary);
29
29
  max-width: 100%;
30
30
  overflow: auto;
31
+ flex-shrink: 0;
31
32
  }
32
33
  </style>
@@ -0,0 +1,150 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <MenuList :disabled placement="bottom-start" shadow>
4
+ <template #trigger="{ open, isOpen }">
5
+ <th
6
+ :class="{ interactive, disabled, focus: isOpen }"
7
+ class="column-header"
8
+ @click="ev => (interactive ? open(ev) : noop())"
9
+ >
10
+ <div class="content">
11
+ <span class="label">
12
+ <UiIcon :icon />
13
+ <slot />
14
+ </span>
15
+ <UiIcon :icon="currentInteraction?.icon" />
16
+ </div>
17
+ </th>
18
+ </template>
19
+ <MenuItem
20
+ v-for="interaction in interactions"
21
+ :key="interaction.id"
22
+ v-tooltip="$t('coming-soon')"
23
+ :disabled="interaction.disabled"
24
+ :on-click="() => updateInteraction(interaction)"
25
+ >
26
+ <UiIcon :icon="interaction.icon" />{{ interaction.label }}
27
+ <i v-if="currentInteraction?.id === interaction.id" class="current-interaction typo p3-regular-italic">
28
+ {{ $t('core.current').toLowerCase() }}
29
+ </i>
30
+ </MenuItem>
31
+ </MenuList>
32
+ </template>
33
+
34
+ <script lang="ts" setup>
35
+ import UiIcon from '@core/components/icon/UiIcon.vue'
36
+ import MenuItem from '@core/components/menu/MenuItem.vue'
37
+ import MenuList from '@core/components/menu/MenuList.vue'
38
+ import { vTooltip } from '@core/directives/tooltip.directive'
39
+ import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
40
+ import { faArrowDown, faArrowUp, faEyeSlash, faFilter, faLayerGroup } from '@fortawesome/free-solid-svg-icons'
41
+ import { noop } from '@vueuse/core'
42
+ import { computed, inject } from 'vue'
43
+ import { useI18n } from 'vue-i18n'
44
+ import { useRouter } from 'vue-router'
45
+
46
+ type InteractionId = 'sort-asc' | 'sort-desc' | 'group' | 'filter' | 'hide'
47
+ type Interaction = {
48
+ disabled?: boolean
49
+ id: InteractionId
50
+ icon: IconDefinition
51
+ label: string
52
+ }
53
+
54
+ const props = withDefaults(
55
+ defineProps<{
56
+ id?: string
57
+ icon?: IconDefinition
58
+ interactive?: boolean
59
+ disabled?: boolean
60
+ }>(),
61
+ {
62
+ disabled: false,
63
+ interactive: true,
64
+ }
65
+ )
66
+ const { t } = useI18n()
67
+ const router = useRouter()
68
+
69
+ const interactions = computed<Interaction[]>(() => [
70
+ { id: 'sort-asc', icon: faArrowDown, label: t('core.sort.ascending'), disabled: true },
71
+ { id: 'sort-desc', icon: faArrowUp, label: t('core.sort.descending'), disabled: true },
72
+ { id: 'group', icon: faLayerGroup, label: t('core.group'), disabled: true },
73
+ { id: 'filter', icon: faFilter, label: t('core.filter'), disabled: true },
74
+ { id: 'hide', icon: faEyeSlash, label: t('core.hide'), disabled: true },
75
+ ])
76
+
77
+ const tableName = inject<string>('tableName')
78
+
79
+ const currentInteraction = computed(() =>
80
+ interactions.value.find(interaction => router.currentRoute.value.query[columnName] === interaction.id)
81
+ )
82
+
83
+ const columnName = `${tableName}__${props.id}`
84
+
85
+ const updateInteraction = (interaction: Interaction) => {
86
+ router.replace({
87
+ query: {
88
+ [columnName]: interaction.id,
89
+ },
90
+ })
91
+ }
92
+ </script>
93
+
94
+ <style lang="postcss" scoped>
95
+ /* COLOR VARIANTS */
96
+ .column-header.interactive {
97
+ --color: var(--color-purple-base);
98
+ --background-color: var(--background-color-primary);
99
+
100
+ &.focus {
101
+ --color: var(--color-purple-base);
102
+ --background-color: var(--background-color-purple-10);
103
+ }
104
+
105
+ &:hover {
106
+ --color: var(--color-purple-d20);
107
+ --background-color: var(--background-color-purple-20);
108
+ }
109
+
110
+ &:active {
111
+ --color: var(--color-purple-d40);
112
+ --background-color: var(--background-color-purple-30);
113
+ }
114
+
115
+ &.disabled {
116
+ --color: var(--color-grey-400);
117
+ --background-color: var(--background-color-secondary);
118
+ }
119
+ }
120
+ /* IMPLEMENTATION */
121
+ .column-header.interactive {
122
+ cursor: pointer;
123
+ color: var(--color);
124
+ background-color: var(--background-color);
125
+ &.disabled {
126
+ cursor: not-allowed;
127
+ }
128
+ }
129
+
130
+ .current-interaction {
131
+ color: var(--color-grey-300);
132
+ }
133
+
134
+ .content {
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: space-between;
138
+ gap: 1rem;
139
+ }
140
+
141
+ .label {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 1rem;
145
+ }
146
+
147
+ .filter-icon {
148
+ cursor: pointer;
149
+ }
150
+ </style>
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <table :class="{ 'vertical-border': verticalBorder }" class="ui-table typo p2-regular">
3
+ <slot />
4
+ </table>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import { provide } from 'vue'
9
+
10
+ const props = defineProps<{
11
+ name?: string
12
+ verticalBorder?: boolean
13
+ }>()
14
+
15
+ provide('tableName', props.name)
16
+ </script>
17
+
18
+ <style lang="postcss" scoped>
19
+ .ui-table {
20
+ width: 100%;
21
+ border-spacing: 0;
22
+ background-color: var(--background-color-primary);
23
+ line-height: 2.4rem;
24
+ color: var(--color-grey-200);
25
+ border-collapse: collapse;
26
+ table-layout: fixed;
27
+
28
+ :deep(th),
29
+ :deep(td) {
30
+ padding: 1rem;
31
+ border-top: 0.1rem solid var(--color-grey-500);
32
+ text-align: left;
33
+ }
34
+
35
+ :deep(th) {
36
+ font-weight: 700;
37
+ }
38
+
39
+ :deep(thead) {
40
+ th,
41
+ td {
42
+ color: var(--color-purple-base);
43
+ font-size: 1.4rem;
44
+ font-weight: 400;
45
+ text-transform: uppercase;
46
+ }
47
+ }
48
+
49
+ &.vertical-border {
50
+ :deep(th),
51
+ :deep(td) {
52
+ border-right: 0.1rem solid var(--color-grey-500);
53
+
54
+ &:first-child {
55
+ border-left: none;
56
+ }
57
+
58
+ &:last-child {
59
+ border-right: none;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ </style>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <ButtonIcon
3
+ ref="buttonRef"
4
+ v-tooltip="{ content: $t('tasks.quick-view'), placement: 'bottom-end' }"
5
+ :dot="hasNewTask"
6
+ :icon="faBarsProgress"
7
+ size="large"
8
+ @click="isPanelOpen = true"
9
+ />
10
+ <Teleport v-if="isPanelOpen" to="body">
11
+ <Backdrop @click="isPanelOpen = false" />
12
+ <QuickTaskPanel ref="panelRef" :loading :tasks />
13
+ </Teleport>
14
+ </template>
15
+
16
+ <script lang="ts" setup>
17
+ import Backdrop from '@core/components/backdrop/Backdrop.vue'
18
+ import ButtonIcon from '@core/components/button/ButtonIcon.vue'
19
+ import QuickTaskPanel from '@core/components/task/QuickTaskPanel.vue'
20
+ import { vTooltip } from '@core/directives/tooltip.directive'
21
+ import type { Task } from '@core/types/task.type'
22
+ import { faBarsProgress } from '@fortawesome/free-solid-svg-icons'
23
+ import { unrefElement, watchArray, whenever } from '@vueuse/core'
24
+ import placementJs from 'placement.js'
25
+ import { computed, nextTick, ref } from 'vue'
26
+
27
+ const props = defineProps<{
28
+ tasks: Task[]
29
+ loading?: boolean
30
+ }>()
31
+
32
+ const ids = computed(() => props.tasks.map(task => task.id))
33
+
34
+ const isPanelOpen = ref(false)
35
+ const hasNewTask = ref(false)
36
+
37
+ watchArray(ids, (_newList, _oldList, addedIds) => {
38
+ if (addedIds.length > 0 && !isPanelOpen.value) {
39
+ hasNewTask.value = true
40
+ }
41
+ })
42
+
43
+ const buttonRef = ref<HTMLButtonElement | null>(null)
44
+ const panelRef = ref<HTMLDivElement | null>(null)
45
+
46
+ whenever(isPanelOpen, async () => {
47
+ await nextTick()
48
+
49
+ const button = unrefElement(buttonRef)
50
+ const panel = unrefElement(panelRef)
51
+
52
+ if (!button || !panel) {
53
+ return
54
+ }
55
+
56
+ placementJs(button, panel, {
57
+ placement: 'bottom-end',
58
+ })
59
+
60
+ hasNewTask.value = false
61
+ })
62
+ </script>
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <li class="vts-quick-task-item">
3
+ <div v-if="hasSubTasks" class="toggle" @click="toggleExpand()">
4
+ <ButtonIcon :icon="isExpanded ? faAngleDown : faAngleRight" size="small" />
5
+ </div>
6
+ <div class="content">
7
+ <div class="typo p1-medium">
8
+ {{ task.name }}
9
+ </div>
10
+ <div class="informations">
11
+ <div class="line-1">
12
+ <UiTag v-if="task.tag" color="grey">{{ task.tag }}</UiTag>
13
+ <div v-if="hasSubTasks" class="subtasks">
14
+ <UiIcon :icon="faCircleNotch" />
15
+ <span class="typo p4-medium">{{ $t('tasks.n-subtasks', { n: subTasksCount }) }}</span>
16
+ </div>
17
+ </div>
18
+ <div v-if="task.start" class="line-2 typo p4-regular">
19
+ {{ $d(task.start, 'datetime_short') }}
20
+ <template v-if="task.end">
21
+ <UiIcon :icon="faArrowRight" />
22
+ {{ $d(new Date(task.end), 'datetime_short') }}
23
+ </template>
24
+ </div>
25
+ </div>
26
+ <QuickTaskList v-if="hasSubTasks && isExpanded" :tasks="subTasks" sublist />
27
+ </div>
28
+ </li>
29
+ </template>
30
+
31
+ <script lang="ts" setup>
32
+ import ButtonIcon from '@core/components/button/ButtonIcon.vue'
33
+ import UiIcon from '@core/components/icon/UiIcon.vue'
34
+ import QuickTaskList from '@core/components/task/QuickTaskList.vue'
35
+ import UiTag from '@core/components/UiTag.vue'
36
+ import type { Task } from '@core/types/task.type'
37
+ import { faAngleDown, faAngleRight, faArrowRight, faCircleNotch } from '@fortawesome/free-solid-svg-icons'
38
+ import { useToggle } from '@vueuse/core'
39
+ import { computed } from 'vue'
40
+
41
+ const props = defineProps<{
42
+ task: Task
43
+ }>()
44
+
45
+ const [isExpanded, toggleExpand] = useToggle()
46
+
47
+ const subTasks = computed(() => props.task.subtasks ?? [])
48
+ const subTasksCount = computed(() => subTasks.value.length)
49
+ const hasSubTasks = computed(() => subTasksCount.value > 0)
50
+ </script>
51
+
52
+ <style lang="postcss" scoped>
53
+ .vts-quick-task-item {
54
+ display: flex;
55
+
56
+ &:not(:last-child) {
57
+ border-bottom: 0.1rem solid var(--color-grey-500);
58
+ }
59
+ }
60
+
61
+ .toggle {
62
+ padding: 0.4rem 0;
63
+ }
64
+
65
+ .content {
66
+ flex: 1;
67
+ padding: 0.4rem 0.4rem 0.4rem 0.8rem;
68
+ }
69
+
70
+ .informations {
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: 0.4rem;
74
+ }
75
+
76
+ .line-1 {
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 0.2rem 0.8rem;
80
+ }
81
+
82
+ .line-2 {
83
+ color: var(--color-grey-200);
84
+ }
85
+
86
+ .subtasks {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 0.4rem;
90
+ }
91
+ </style>
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <ul :class="{ sublist }" class="vts-quick-task-list">
3
+ <li v-if="loading">
4
+ <div class="loading">
5
+ <UiSpinner />
6
+ <div>{{ $t('loading-in-progress') }}</div>
7
+ </div>
8
+ </li>
9
+ <template v-else>
10
+ <li v-if="tasks.length === 0" class="typo p1-medium">{{ $t('tasks.no-tasks') }}</li>
11
+ <QuickTaskItem v-for="task of tasks" :key="task.id" :task />
12
+ </template>
13
+ </ul>
14
+ </template>
15
+
16
+ <script lang="ts" setup>
17
+ import QuickTaskItem from '@core/components/task/QuickTaskItem.vue'
18
+ import UiSpinner from '@core/components/UiSpinner.vue'
19
+ import type { Task } from '@core/types/task.type'
20
+
21
+ defineProps<{
22
+ tasks: Task[]
23
+ sublist?: boolean
24
+ loading?: boolean
25
+ }>()
26
+ </script>
27
+
28
+ <style lang="postcss" scoped>
29
+ .vts-quick-task-list {
30
+ display: flex;
31
+ flex-direction: column;
32
+ background-color: var(--background-color-primary);
33
+ padding: 1rem 0;
34
+
35
+ &:not(.sublist) {
36
+ padding: 1.6rem 2rem;
37
+ max-height: 40rem;
38
+ overflow: auto;
39
+ }
40
+ }
41
+
42
+ .loading {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ gap: 1rem;
47
+ }
48
+ </style>