design-system-next 2.9.6 → 2.9.9
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/dist/design-system-next.js +7048 -6859
- package/dist/design-system-next.js.gz +0 -0
- package/dist/main.css +1 -1
- package/dist/main.css.gz +0 -0
- package/dist/package.json.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/filter/filter.ts +8 -0
- package/src/components/filter/filter.vue +13 -5
- package/src/components/filter/use-filter.ts +8 -2
- package/src/components/select/select-multiple/select-multiple.ts +4 -0
- package/src/components/select/select-multiple/select-multiple.vue +75 -30
- package/src/components/select/select-multiple/use-select-multiple.ts +103 -2
- package/src/components/sidenav/sidenav.ts +47 -7
- package/src/components/sidenav/sidenav.vue +6 -6
- package/src/components/sidenav/use-sidenav.ts +97 -3
- package/src/vite-env.d.ts +6 -0
|
@@ -25,38 +25,81 @@
|
|
|
25
25
|
>
|
|
26
26
|
<div ref="multiSelectRef">
|
|
27
27
|
<div @click="handleOptionsToggle">
|
|
28
|
-
<
|
|
29
|
-
:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class="spr-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
<template v-if="props.chipped">
|
|
29
|
+
<div :class="multiSelectClasses.chippedInputTextBaseClasses">
|
|
30
|
+
<div ref="chippedInputTextRef" :class="multiSelectClasses.chippedInputTextClasses">
|
|
31
|
+
<div class="spr-h-auto spr-w-full">
|
|
32
|
+
<template v-if="multiSelectedListItems.length > 0">
|
|
33
|
+
<template v-for="item in multiSelectedListItems" :key="item.value">
|
|
34
|
+
<spr-chips
|
|
35
|
+
class="spr-m-1 spr-inline-block"
|
|
36
|
+
:label="String(item.text)"
|
|
37
|
+
closable
|
|
38
|
+
visible
|
|
39
|
+
@close="handleChippedRemoveItem(String(item.value))"
|
|
40
|
+
/>
|
|
41
|
+
</template>
|
|
42
|
+
</template>
|
|
43
|
+
<template v-else>
|
|
44
|
+
<span class="spr-placeholder spr-px-3 spr-text-gray-400">{{ props.placeholder }}</span>
|
|
45
|
+
</template>
|
|
46
|
+
</div>
|
|
47
|
+
<div :class="multiSelectClasses.chippedIconClasses">
|
|
48
|
+
<div class="spr-flex spr-items-center spr-gap-1">
|
|
49
|
+
<Icon
|
|
50
|
+
v-if="props.clearable && inputText"
|
|
51
|
+
class="spr-cursor-pointer"
|
|
52
|
+
icon="ph:x"
|
|
53
|
+
@click.stop="handleClear"
|
|
54
|
+
/>
|
|
55
|
+
<Icon icon="ph:caret-down" />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
53
58
|
</div>
|
|
54
|
-
</
|
|
59
|
+
</div>
|
|
60
|
+
<div v-if="props.displayHelper" :class="multiSelectClasses.chippedHelperContainerClasses">
|
|
61
|
+
<div v-if="props.displayHelper" :class="multiSelectClasses.chippedHelperClasses">
|
|
62
|
+
<slot name="helperMessage">
|
|
63
|
+
<Icon v-if="props.helperIcon" :icon="props.helperIcon" width="20px" height="20px" />
|
|
64
|
+
<span>{{ props.helperText }}</span>
|
|
65
|
+
</slot>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
<template v-else>
|
|
70
|
+
<spr-input
|
|
71
|
+
:id="`input-${props.id}`"
|
|
72
|
+
v-model="inputText"
|
|
73
|
+
:class="{
|
|
74
|
+
'spr-cursor-pointer': true,
|
|
75
|
+
}"
|
|
76
|
+
:placeholder="props.placeholder"
|
|
77
|
+
autocomplete="off"
|
|
78
|
+
:helper-text="props.helperText"
|
|
79
|
+
:helper-icon="props.helperIcon"
|
|
80
|
+
:display-helper="props.displayHelper"
|
|
81
|
+
:active="props.active"
|
|
82
|
+
:readonly="true"
|
|
83
|
+
:disabled="props.disabled"
|
|
84
|
+
:error="props.error"
|
|
85
|
+
>
|
|
86
|
+
<template #icon>
|
|
87
|
+
<div class="spr-flex spr-items-center spr-gap-1">
|
|
88
|
+
<Icon
|
|
89
|
+
v-if="props.clearable && inputText"
|
|
90
|
+
class="spr-cursor-pointer"
|
|
91
|
+
icon="ph:x"
|
|
92
|
+
@click.stop="handleClear"
|
|
93
|
+
/>
|
|
94
|
+
<Icon icon="ph:caret-down" />
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
55
97
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
98
|
+
<template #helperMessage>
|
|
99
|
+
<slot name="helperMessage" />
|
|
100
|
+
</template>
|
|
101
|
+
</spr-input>
|
|
102
|
+
</template>
|
|
60
103
|
|
|
61
104
|
<!-- Hidden Select for QA automation -->
|
|
62
105
|
<select v-if="multiSelectOptions && multiSelectOptions.length" v-model="multiSelectModel" multiple hidden>
|
|
@@ -106,6 +149,7 @@ import 'floating-vue/dist/style.css';
|
|
|
106
149
|
|
|
107
150
|
import SprInput from '../../input/input.vue';
|
|
108
151
|
import SprList from '../../list/list.vue';
|
|
152
|
+
import SprChips from '../../chips/chips.vue';
|
|
109
153
|
|
|
110
154
|
import { multiSelectPropTypes, multiSelectEmitTypes } from './select-multiple';
|
|
111
155
|
|
|
@@ -124,6 +168,7 @@ const {
|
|
|
124
168
|
inputText,
|
|
125
169
|
isMultiSelectPopperDisabled,
|
|
126
170
|
handleMultiSelectedItem,
|
|
171
|
+
handleChippedRemoveItem,
|
|
127
172
|
handleClear,
|
|
128
173
|
handleOptionsToggle,
|
|
129
174
|
} = useMultiSelect(props, emit);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ref, toRefs, computed, ComputedRef, onMounted, watch } from 'vue';
|
|
2
|
-
import { onClickOutside, useVModel } from '@vueuse/core';
|
|
2
|
+
import { onClickOutside, useVModel, useFocus } from '@vueuse/core';
|
|
3
3
|
|
|
4
4
|
import classNames from 'classnames';
|
|
5
5
|
|
|
@@ -10,10 +10,15 @@ import type { MenuListType } from '../../list/list';
|
|
|
10
10
|
interface MultiSelectClasses {
|
|
11
11
|
baseClasses: string;
|
|
12
12
|
labelClasses: string;
|
|
13
|
+
chippedInputTextBaseClasses: string;
|
|
14
|
+
chippedInputTextClasses: string;
|
|
15
|
+
chippedIconClasses: string;
|
|
16
|
+
chippedHelperContainerClasses: string;
|
|
17
|
+
chippedHelperClasses: string;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<MultiSelectEmitTypes>['emit']) => {
|
|
16
|
-
const { displayText, options,
|
|
21
|
+
const { displayText, options, textField, valueField, active, disabled, error } = toRefs(props);
|
|
17
22
|
|
|
18
23
|
const multiSelectClasses: ComputedRef<MultiSelectClasses> = computed(() => {
|
|
19
24
|
const baseClasses = classNames('spr-flex spr-flex-col spr-gap-size-spacing-4xs');
|
|
@@ -22,9 +27,66 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
|
|
|
22
27
|
'spr-text-color-on-fill-disabled': disabled.value,
|
|
23
28
|
});
|
|
24
29
|
|
|
30
|
+
const chippedInputTextBaseClasses = classNames(
|
|
31
|
+
'spr-relative spr-flex spr-items-center spr-min-h-9 spr-rounded-border-radius-md spr-border-[1.5px] spr-border-solid',
|
|
32
|
+
{
|
|
33
|
+
'spr-cursor-pointer': !disabled.value,
|
|
34
|
+
|
|
35
|
+
// Border State
|
|
36
|
+
'spr-border-color-weak': !focused.value && !error.value && !disabled.value && !active.value,
|
|
37
|
+
'spr-border-color-brand-base': !focused.value && active.value,
|
|
38
|
+
'spr-border-color-danger-base': !focused.value && error.value,
|
|
39
|
+
|
|
40
|
+
// Border State Focused
|
|
41
|
+
'focus: spr-border-kangkong-700': focused.value && !error.value && !disabled.value && !active.value,
|
|
42
|
+
'focus: spr-border-tomato-600': focused.value && error.value,
|
|
43
|
+
'focus: spr-border-white-100': focused.value && disabled.value,
|
|
44
|
+
|
|
45
|
+
// Disabled State
|
|
46
|
+
'spr-background-color-disabled spr-cursor-not-allowed spr-border-mushroom-100': disabled.value,
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const chippedInputTextClasses = classNames(
|
|
51
|
+
'spr-flex spr-gap-1 spr-justify-between spr-w-full spr-outline-none spr-ring-0 spr-border-none spr-rounded-border-radius-md spr-font-size-200',
|
|
52
|
+
'spr-font-size-200 [font-weight:inherit]',
|
|
53
|
+
'placeholder:spr-text-mushroom-300',
|
|
54
|
+
{
|
|
55
|
+
'spr-text-color-strong': !disabled.value,
|
|
56
|
+
|
|
57
|
+
// Disabled State
|
|
58
|
+
'spr-text-color-on-fill-disabled !spr-cursor-not-allowed': disabled.value,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const chippedIconClasses = classNames(
|
|
63
|
+
'spr-flex spr-items-center spr-justify-center spr-h-inherit spr-px-2 [&>svg]:spr-min-h-4 [&>svg]:spr-min-w-4',
|
|
64
|
+
{
|
|
65
|
+
'spr-text-mushroom-300': !error.value,
|
|
66
|
+
'spr-text-tomato-600': error.value,
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const chippedHelperContainerClasses = classNames(
|
|
71
|
+
'spr-flex spr-flex-row spr-items-start spr-justify-between spr-w-full',
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const chippedHelperClasses = classNames(
|
|
75
|
+
'spr-body-sm-regular spr-flex spr-items-center spr-gap-size-spacing-5xs spr-flex-1',
|
|
76
|
+
{
|
|
77
|
+
'spr-text-color-danger-base': error.value,
|
|
78
|
+
'spr-text-color-supporting': !error.value,
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
25
82
|
return {
|
|
26
83
|
baseClasses,
|
|
27
84
|
labelClasses,
|
|
85
|
+
chippedInputTextBaseClasses,
|
|
86
|
+
chippedInputTextClasses,
|
|
87
|
+
chippedIconClasses,
|
|
88
|
+
chippedHelperContainerClasses,
|
|
89
|
+
chippedHelperClasses,
|
|
28
90
|
};
|
|
29
91
|
});
|
|
30
92
|
|
|
@@ -39,7 +101,9 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
|
|
|
39
101
|
|
|
40
102
|
const inputText = ref<string | number>('');
|
|
41
103
|
const inputTextBackup = ref<string | number>('');
|
|
104
|
+
const chippedInputTextRef = ref(null);
|
|
42
105
|
|
|
106
|
+
const { focused } = useFocus(chippedInputTextRef);
|
|
43
107
|
/**
|
|
44
108
|
* Returns the normalized value of the model as an array for internal use.
|
|
45
109
|
*/
|
|
@@ -148,6 +212,42 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
|
|
|
148
212
|
updateMultiSelectedItemsFromValue();
|
|
149
213
|
};
|
|
150
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Removes an item from the multi-select model by its value.
|
|
217
|
+
* Handles stringified objects and emits the updated model value.
|
|
218
|
+
*/
|
|
219
|
+
const handleChippedRemoveItem = (itemValue: string) => {
|
|
220
|
+
let currentValues = Array.isArray(multiSelectModel.value) ? [...multiSelectModel.value] : [multiSelectModel.value];
|
|
221
|
+
|
|
222
|
+
currentValues = currentValues.filter((val) => {
|
|
223
|
+
let valToCompare = val;
|
|
224
|
+
|
|
225
|
+
if (typeof valToCompare === 'string' && valToCompare.startsWith('{') && valToCompare.endsWith('}')) {
|
|
226
|
+
try {
|
|
227
|
+
valToCompare = JSON.parse(valToCompare);
|
|
228
|
+
} catch {
|
|
229
|
+
// ignore
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (typeof itemValue === 'string' && itemValue.startsWith('{') && itemValue.endsWith('}')) {
|
|
234
|
+
try {
|
|
235
|
+
itemValue = JSON.parse(itemValue);
|
|
236
|
+
} catch {
|
|
237
|
+
// ignore
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (typeof valToCompare === 'object' && typeof itemValue === 'object') {
|
|
242
|
+
return JSON.stringify(valToCompare) !== JSON.stringify(itemValue);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return valToCompare != itemValue;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
emit('update:modelValue', currentValues);
|
|
249
|
+
};
|
|
250
|
+
|
|
151
251
|
/**
|
|
152
252
|
* Updates the selected items in the multi-select based on the current model value.
|
|
153
253
|
* Handles stringified objects and updates the input text accordingly.
|
|
@@ -271,6 +371,7 @@ export const useMultiSelect = (props: MultiSelectPropTypes, emit: SetupContext<M
|
|
|
271
371
|
inputText,
|
|
272
372
|
isMultiSelectPopperDisabled,
|
|
273
373
|
handleMultiSelectedItem,
|
|
374
|
+
handleChippedRemoveItem,
|
|
274
375
|
handleClear,
|
|
275
376
|
handleOptionsToggle,
|
|
276
377
|
};
|
|
@@ -5,6 +5,7 @@ type QuickAction = {
|
|
|
5
5
|
items: QuickActionItem[];
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
type QuickActionItem = {
|
|
9
10
|
title: string;
|
|
10
11
|
description: string;
|
|
@@ -14,23 +15,29 @@ type QuickActionItem = {
|
|
|
14
15
|
hidden: boolean;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
type NavLinks = {
|
|
18
|
+
export type NavLinks = {
|
|
18
19
|
top: { parentLinks: ParentLinkItem[] }[];
|
|
19
20
|
bottom: { parentLinks: ParentLinkItem[] }[];
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
type
|
|
23
|
+
export type NavAPILinks = {
|
|
24
|
+
top: { parentLinks: NavItem[] }[];
|
|
25
|
+
bottom: { parentLinks: NavItem[] }[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ParentLinkItem = {
|
|
23
29
|
title: string;
|
|
24
30
|
icon: string;
|
|
25
|
-
link
|
|
26
|
-
redirect
|
|
31
|
+
link?: string;
|
|
32
|
+
redirect?: Redirect;
|
|
27
33
|
menuLinks: MenuLink[];
|
|
28
|
-
|
|
34
|
+
submenuLinks?: SubmenuLink[];
|
|
35
|
+
hidden?: boolean;
|
|
29
36
|
};
|
|
30
37
|
|
|
31
38
|
type MenuLink = {
|
|
32
39
|
menuHeading: string;
|
|
33
|
-
items: MenuLinkItem[];
|
|
40
|
+
items: MenuLinkItem[] | ParentLinkItem[];
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
type MenuLinkItem = {
|
|
@@ -42,7 +49,7 @@ type MenuLinkItem = {
|
|
|
42
49
|
|
|
43
50
|
type SubmenuLink = {
|
|
44
51
|
subMenuHeading: string;
|
|
45
|
-
items: SubmenuLinkItem[];
|
|
52
|
+
items: SubmenuLinkItem[] | ParentLinkItem[];
|
|
46
53
|
};
|
|
47
54
|
|
|
48
55
|
type SubmenuLinkItem = {
|
|
@@ -70,6 +77,34 @@ type UserMenuItem = {
|
|
|
70
77
|
hidden: boolean;
|
|
71
78
|
redirect: Redirect;
|
|
72
79
|
};
|
|
80
|
+
export interface NavItem {
|
|
81
|
+
groupId: string;
|
|
82
|
+
label: string;
|
|
83
|
+
icon?: string | null;
|
|
84
|
+
url?: string | null;
|
|
85
|
+
isNewTab?: boolean;
|
|
86
|
+
children?: NavItem[] | null;
|
|
87
|
+
groupName?: string | null;
|
|
88
|
+
hidden?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface MappedNavItem {
|
|
92
|
+
title: string;
|
|
93
|
+
icon?: string;
|
|
94
|
+
redirect?: {
|
|
95
|
+
openInNewTab: boolean;
|
|
96
|
+
isAbsoluteURL: boolean;
|
|
97
|
+
link: string;
|
|
98
|
+
};
|
|
99
|
+
menuLinks?: {
|
|
100
|
+
menuHeading: string;
|
|
101
|
+
items: MappedNavItem[];
|
|
102
|
+
}[];
|
|
103
|
+
submenuLinks?: {
|
|
104
|
+
subMenuHeading: string;
|
|
105
|
+
items: MappedNavItem[];
|
|
106
|
+
}[];
|
|
107
|
+
}
|
|
73
108
|
|
|
74
109
|
export const sidenavPropTypes = {
|
|
75
110
|
quickActions: {
|
|
@@ -120,6 +155,11 @@ export const sidenavPropTypes = {
|
|
|
120
155
|
validator: (value: unknown) => typeof value === 'boolean',
|
|
121
156
|
default: false,
|
|
122
157
|
},
|
|
158
|
+
isNavApi: {
|
|
159
|
+
type: Boolean,
|
|
160
|
+
validator: (value: unknown) => typeof value === 'boolean',
|
|
161
|
+
default: false,
|
|
162
|
+
}
|
|
123
163
|
};
|
|
124
164
|
|
|
125
165
|
export const sidenavEmitTypes = {
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
</div>
|
|
140
140
|
|
|
141
141
|
<!-- Grouped Nav Links -->
|
|
142
|
-
<template v-for="(navLink, navLinkIndex) in
|
|
142
|
+
<template v-for="(navLink, navLinkIndex) in navLinks.top" :key="navLinkIndex">
|
|
143
143
|
<template v-for="(parentLink, parentLinkIndex) in navLink.parentLinks" :key="parentLinkIndex">
|
|
144
144
|
<!-- Parent link with menu links -->
|
|
145
145
|
<template v-if="parentLink.menuLinks && parentLink.menuLinks.length > 0">
|
|
@@ -352,7 +352,7 @@
|
|
|
352
352
|
</template>
|
|
353
353
|
</template>
|
|
354
354
|
<div
|
|
355
|
-
v-if="
|
|
355
|
+
v-if="navLinks.top.length > 0 && navLinkIndex < navLinks.top.length - 1"
|
|
356
356
|
class="spr-background-color-hover spr-h-[2px] spr-w-full"
|
|
357
357
|
></div>
|
|
358
358
|
</template>
|
|
@@ -360,11 +360,11 @@
|
|
|
360
360
|
|
|
361
361
|
<!-- Bottom Section -->
|
|
362
362
|
<div
|
|
363
|
-
v-if="
|
|
363
|
+
v-if="navLinks.bottom && navLinks.bottom.length > 0"
|
|
364
364
|
class="spr-grid spr-justify-center spr-gap-2 spr-px-3 spr-pb-4 spr-pt-0"
|
|
365
365
|
>
|
|
366
366
|
<!-- Grouped Nav Links -->
|
|
367
|
-
<template v-for="(navLink, navLinkIndex) in
|
|
367
|
+
<template v-for="(navLink, navLinkIndex) in navLinks.bottom" :key="navLinkIndex">
|
|
368
368
|
<template v-for="(parentLink, parentLinkIndex) in navLink.parentLinks" :key="parentLinkIndex">
|
|
369
369
|
<!-- Parent link with menu links -->
|
|
370
370
|
<template v-if="parentLink.menuLinks && parentLink.menuLinks.length > 0">
|
|
@@ -573,7 +573,7 @@
|
|
|
573
573
|
</template>
|
|
574
574
|
</template>
|
|
575
575
|
<div
|
|
576
|
-
v-if="
|
|
576
|
+
v-if="navLinks.bottom.length > 0 && navLinkIndex < navLinks.bottom.length - 1"
|
|
577
577
|
class="spr-background-color-hover spr-h-[2px] spr-w-full"
|
|
578
578
|
></div>
|
|
579
579
|
</template>
|
|
@@ -767,7 +767,7 @@ import SprBadge from '../badge/badge.vue';
|
|
|
767
767
|
const props = defineProps(sidenavPropTypes);
|
|
768
768
|
const emit = defineEmits(sidenavEmitTypes);
|
|
769
769
|
|
|
770
|
-
const { isQuckActionMenuVisible, isUserMenuVisible, userProfileError, getUserInitials, handleRedirect, generateId } = useSidenav(
|
|
770
|
+
const { navLinks, isQuckActionMenuVisible, isUserMenuVisible, userProfileError, getUserInitials, handleRedirect, generateId } = useSidenav(
|
|
771
771
|
props,
|
|
772
772
|
emit,
|
|
773
773
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ref } from 'vue';
|
|
1
|
+
import { ref, onMounted } from 'vue';
|
|
2
2
|
import type { SetupContext } from 'vue';
|
|
3
3
|
|
|
4
|
-
import type { SidenavPropTypes, SidenavEmitTypes } from './sidenav';
|
|
4
|
+
import type { SidenavPropTypes, SidenavEmitTypes, ParentLinkItem, NavLinks, NavItem } from './sidenav';
|
|
5
5
|
|
|
6
6
|
interface ObjectItem {
|
|
7
7
|
redirect: {
|
|
@@ -17,6 +17,7 @@ interface ObjectItem {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const useSidenav = (props: SidenavPropTypes, emit: SetupContext<SidenavEmitTypes>['emit']) => {
|
|
20
|
+
const navLinks = ref<NavLinks>(props.navLinks);
|
|
20
21
|
const isQuckActionMenuVisible = ref(false);
|
|
21
22
|
|
|
22
23
|
const isUserMenuVisible = ref(false);
|
|
@@ -67,13 +68,106 @@ export const useSidenav = (props: SidenavPropTypes, emit: SetupContext<SidenavEm
|
|
|
67
68
|
const generateId = (...titles: string[]): string => {
|
|
68
69
|
return titles.map(transformToCamelCaseId).join('_');
|
|
69
70
|
}
|
|
71
|
+
const confirmIfOwnDomain = (url: string) => {
|
|
72
|
+
const domain = window.location.href;
|
|
73
|
+
const urlHostname = new URL(url).hostname;
|
|
74
|
+
const isOwnDomain = domain === urlHostname || window.location.hostname === 'localhost'
|
|
75
|
+
return isOwnDomain;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getPathFromUrl = (url: string): string => {
|
|
79
|
+
const parsedUrl = new URL(url);
|
|
80
|
+
return parsedUrl ? parsedUrl.pathname : '';
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const navLinkCondition = (link: NavItem) => {
|
|
85
|
+
if (confirmIfOwnDomain(link.url as string)) {
|
|
86
|
+
return getPathFromUrl(link.url as string);
|
|
87
|
+
} else {
|
|
88
|
+
return link.url;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const groupByGroupId = (items: NavItem[]) => {
|
|
93
|
+
const groups: Record<string, NavItem[]> = {};
|
|
94
|
+
items.forEach(item => {
|
|
95
|
+
if (!groups[item.groupId]) {
|
|
96
|
+
groups[item.groupId] = [];
|
|
97
|
+
}
|
|
98
|
+
groups[item.groupId].push(item);
|
|
99
|
+
});
|
|
100
|
+
return Object.values(groups).map(group => ({ parentLinks: group.map(mapItemToNav) }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const mapItemToNav = (item: NavItem): ParentLinkItem => {
|
|
104
|
+
return {
|
|
105
|
+
title: item.label,
|
|
106
|
+
icon: item.icon || "",
|
|
107
|
+
redirect: item.url
|
|
108
|
+
? {
|
|
109
|
+
openInNewTab: item.isNewTab || false,
|
|
110
|
+
isAbsoluteURL: !confirmIfOwnDomain(item.url),
|
|
111
|
+
link: navLinkCondition(item) || "",
|
|
112
|
+
}
|
|
113
|
+
: undefined,
|
|
114
|
+
menuLinks: item.children && item.children.length > 0
|
|
115
|
+
? [{
|
|
116
|
+
menuHeading: '',
|
|
117
|
+
items: item.children.map(child => mapItemToNav(child))
|
|
118
|
+
}]
|
|
119
|
+
: [],
|
|
120
|
+
submenuLinks: item.children && item.children.length > 0 && !item.children.some(c => c.children)
|
|
121
|
+
? [{
|
|
122
|
+
subMenuHeading: '',
|
|
123
|
+
items: item.children.map(child => mapItemToNav(child)),
|
|
124
|
+
}]
|
|
125
|
+
: [],
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Helper function to extract valid NavItems from an array of objects
|
|
130
|
+
const extractValidNavItems = <T extends Record<string, unknown>>(items: T[]): NavItem[] => {
|
|
131
|
+
return items.filter((item): item is NavItem & T => (
|
|
132
|
+
item !== null &&
|
|
133
|
+
'groupId' in item &&
|
|
134
|
+
'label' in item &&
|
|
135
|
+
typeof item.groupId === 'string' &&
|
|
136
|
+
typeof item.label === 'string'
|
|
137
|
+
)) as NavItem[];
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const transformedNavItems = async (apiData: NavLinks) => {
|
|
141
|
+
// Output type matches navLinks ref type
|
|
142
|
+
const transformedData: NavLinks = { top: [], bottom: [] };
|
|
143
|
+
|
|
144
|
+
if (apiData.top && Array.isArray(apiData.top)) {
|
|
145
|
+
const validTopItems = extractValidNavItems(apiData.top);
|
|
146
|
+
transformedData.top = groupByGroupId(validTopItems);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (apiData.bottom && Array.isArray(apiData.bottom)) {
|
|
150
|
+
const validBottomItems = extractValidNavItems(apiData.bottom);
|
|
151
|
+
transformedData.bottom = groupByGroupId(validBottomItems);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return transformedData;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
onMounted(async () => {
|
|
158
|
+
if (props.isNavApi) {
|
|
159
|
+
navLinks.value = await transformedNavItems(props.navLinks);
|
|
160
|
+
}
|
|
161
|
+
})
|
|
70
162
|
|
|
71
163
|
return {
|
|
164
|
+
navLinks,
|
|
72
165
|
isQuckActionMenuVisible,
|
|
73
166
|
isUserMenuVisible,
|
|
74
167
|
userProfileError,
|
|
75
168
|
getUserInitials,
|
|
76
169
|
handleRedirect,
|
|
77
|
-
generateId
|
|
170
|
+
generateId,
|
|
171
|
+
transformedNavItems,
|
|
78
172
|
};
|
|
79
173
|
};
|