@nside/wefa 0.3.0 → 0.4.1
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/README.md +46 -3
- package/dist/LegalConsent-9nOroDoA.cjs +1 -0
- package/dist/LegalConsent-CrPVZOxx.js +151 -0
- package/dist/LegalDocument-CVJiGmPJ.cjs +109 -0
- package/dist/{LegalDocument-BhoEpJ2O.js → LegalDocument-DwVhwjIf.js} +236 -215
- package/dist/{LoginView-kH440cCh.js → LoginView-DUPa_PsC.js} +3 -3
- package/dist/{LoginView-IIkXXw3R.cjs → LoginView-Dihs8n_X.cjs} +1 -1
- package/dist/{LogoutView-DGqh4bP7.js → LogoutView-DAh7MrFi.js} +3 -3
- package/dist/{LogoutView-B90MA-_Q.cjs → LogoutView-Fl3nfeJ0.cjs} +1 -1
- package/dist/{apiClient-DJdAL3tN.cjs → apiClient-BUS5ZclN.cjs} +1 -1
- package/dist/{apiClient-D-kcx_S1.js → apiClient-BbJl566D.js} +1 -1
- package/dist/axios-CZvsFspN.js +1887 -0
- package/dist/axios-DMqeKDaq.cjs +6 -0
- package/dist/containers.cjs +590 -5
- package/dist/containers.d.ts +39 -0
- package/dist/containers.js +3803 -977
- package/dist/{index-Coos428-.js → index--_rUTrqU.js} +308 -282
- package/dist/{index-B4vneBZh.cjs → index-B4oFnh1T.cjs} +6 -6
- package/dist/index-BHSxFTgZ.js +49 -0
- package/dist/{index-BSfhC_wu.cjs → index-BaA_oL1s.cjs} +1 -1
- package/dist/{index-CJmnkrIs.cjs → index-Becfy0pF.cjs} +1 -1
- package/dist/{index-Dj5oTSEE.js → index-C09d0pI4.js} +15 -15
- package/dist/{index-BXrnPbjr.cjs → index-CbQWytWd.cjs} +4 -4
- package/dist/{index-DmVIgb18.js → index-CgAb-gZi.js} +11 -11
- package/dist/{index-B53YL3vD.cjs → index-DFOQKDki.cjs} +2 -2
- package/dist/index-DFSkcsx-.cjs +943 -0
- package/dist/{index-CEz0St1t.js → index-DQFN7qxo.js} +7 -7
- package/dist/index-DRozw3P8.js +167 -0
- package/dist/index-DfCQoHSf.cjs +146 -0
- package/dist/index-DkuJMEY1.js +6731 -0
- package/dist/{index-bRjoenrr.js → index-Dv6jyKbT.js} +12 -12
- package/dist/{index-Bl3JVLei.cjs → index-EDm9-cRY.cjs} +1 -1
- package/dist/index-IGN7_cyg.cjs +2 -0
- package/dist/{index-DGvdYnh3.js → index-lFl6UsTa.js} +7 -7
- package/dist/index-lQmq7gxp.cjs +54 -0
- package/dist/{index-FS8xE7Mo.js → index-xUb0UC07.js} +5 -5
- package/dist/lib-C3DWunRS.js +26376 -0
- package/dist/lib-COvHzA2Y.cjs +2104 -0
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.ts +160 -7
- package/dist/lib.js +33 -30
- package/dist/libRoutes-B-H3e9wZ.js +22 -0
- package/dist/libRoutes-Cl3TklhN.cjs +1 -0
- package/dist/network.cjs +1 -1
- package/dist/network.d.ts +19 -0
- package/dist/network.js +3 -3
- package/dist/router.cjs +1 -1
- package/dist/router.d.ts +26 -4
- package/dist/router.js +10 -10
- package/package.json +55 -48
- package/src/assets/main.css +2 -2
- package/src/components/AutoroutedBreadcrumb/AutoroutedBreadcrumb.mdx +8 -8
- package/src/components/AutoroutedBreadcrumb/AutoroutedBreadcrumb.spec.ts +86 -45
- package/src/components/AutoroutedBreadcrumb/AutoroutedBreadcrumb.vue +29 -21
- package/src/components/AvatarComponent/AvatarComponent.mdx +63 -0
- package/src/components/AvatarComponent/AvatarComponent.stories.ts +98 -0
- package/src/components/AvatarComponent/AvatarComponent.vue +115 -0
- package/src/components/GanttChartComponent/GanttChartComponent.mdx +143 -0
- package/src/components/GanttChartComponent/GanttChartComponent.spec.ts +257 -0
- package/src/components/GanttChartComponent/GanttChartComponent.stories.ts +253 -0
- package/src/components/GanttChartComponent/GanttChartComponent.vue +220 -0
- package/src/components/GanttChartComponent/GanttChartGrid.vue +66 -0
- package/src/components/GanttChartComponent/GanttChartHeaderGrid.vue +167 -0
- package/src/components/GanttChartComponent/GanttChartHeaderLabel.vue +23 -0
- package/src/components/GanttChartComponent/GanttChartLinksOverlay.vue +105 -0
- package/src/components/GanttChartComponent/GanttChartRowGrid.vue +288 -0
- package/src/components/GanttChartComponent/GanttChartRowLabel.vue +32 -0
- package/src/components/GanttChartComponent/composables/useGanttLinks.ts +212 -0
- package/src/components/GanttChartComponent/composables/useGanttSizing.ts +42 -0
- package/src/components/GanttChartComponent/ganttChartLayout.ts +211 -0
- package/src/components/GanttChartComponent/ganttChartTypes.ts +24 -0
- package/src/components/GanttChartComponent/index.ts +1 -0
- package/src/components/NetworkButton/ApiMutationButton.vue +7 -5
- package/src/components/NetworkButton/ApiQueryButton.vue +6 -4
- package/src/components/PlotlyComponent/PlotlyComponent.stories.ts +74 -45
- package/src/containers/BareContainer/BareContainer.mdx +1 -1
- package/src/containers/LayoutContainer/LayoutContainer.mdx +128 -0
- package/src/containers/LayoutContainer/LayoutContainer.spec.ts +151 -0
- package/src/containers/LayoutContainer/LayoutContainer.stories.ts +292 -0
- package/src/containers/LayoutContainer/LayoutContainer.vue +53 -0
- package/src/containers/LayoutContainer/MobileNavigationComponent/MobileNavigationComponent.spec.ts +139 -0
- package/src/containers/LayoutContainer/MobileNavigationComponent/MobileNavigationComponent.vue +63 -0
- package/src/containers/LayoutContainer/SideNavigationComponent/BottomComponent/BottomComponent.spec.ts +39 -0
- package/src/containers/LayoutContainer/SideNavigationComponent/BottomComponent/BottomComponent.vue +9 -0
- package/src/containers/LayoutContainer/SideNavigationComponent/MainComponent/MainComponent.spec.ts +175 -0
- package/src/containers/LayoutContainer/SideNavigationComponent/MainComponent/MainComponent.vue +163 -0
- package/src/containers/LayoutContainer/SideNavigationComponent/MainComponent/NavigationLinkComponent.spec.ts +105 -0
- package/src/containers/LayoutContainer/SideNavigationComponent/MainComponent/NavigationLinkComponent.vue +45 -0
- package/src/containers/LayoutContainer/SideNavigationComponent/SideNavigationComponent.spec.ts +78 -0
- package/src/containers/LayoutContainer/SideNavigationComponent/SideNavigationComponent.vue +29 -0
- package/src/containers/LayoutContainer/SideNavigationComponent/TopComponent/TopComponent.spec.ts +60 -0
- package/src/containers/LayoutContainer/SideNavigationComponent/TopComponent/TopComponent.vue +56 -0
- package/src/containers/LayoutContainer/UserMenuTriggerComponent/UserMenuTriggerComponent.spec.ts +96 -0
- package/src/containers/LayoutContainer/UserMenuTriggerComponent/UserMenuTriggerComponent.vue +80 -0
- package/src/containers/LayoutContainer/index.ts +1 -0
- package/src/containers/NavbarContainer/NavbarContainer.mdx +1 -1
- package/src/containers/RoutedTabsComponent/RoutedTabsComponent.mdx +3 -3
- package/src/containers/SideMenuContainer/SideMenuContainer.mdx +1 -1
- package/src/containers/helpers.ts +6 -3
- package/src/containers/index.ts +2 -0
- package/src/containers/storybook/PlaceholderView.vue +1 -1
- package/src/containers/storybook/PrimeComponents.stories.ts +17 -0
- package/src/containers/storybook/PrimeComponentsShowcase.vue +587 -0
- package/src/containers/storybook/overview.mdx +36 -36
- package/src/demo/App.vue +7 -0
- package/src/{demo.ts → demo/main.ts} +8 -9
- package/src/demo/router.ts +65 -19
- package/src/demo/views/PlaygroundView.vue +86 -0
- package/src/demo/views/ShowcaseView.vue +41 -0
- package/src/lib.ts +3 -1
- package/src/locales/Translation.mdx +2 -2
- package/src/locales/en/avatar.json +3 -0
- package/src/locales/en/gantt_chart.json +6 -0
- package/src/locales/en/navigation.json +3 -1
- package/src/locales/index.ts +0 -4
- package/src/plugins/legalConsent/views/__tests__/LegalConsent.test.ts +12 -7
- package/src/router/guards.ts +4 -4
- package/src/router/libRoutes.ts +6 -2
- package/src/router/router.mdx +107 -66
- package/src/router/types.ts +24 -3
- package/src/stores/__tests__/backend/jwt.test.ts +4 -4
- package/src/stores/__tests__/backend/oauth.test.ts +104 -0
- package/src/stores/__tests__/backend/token.test.ts +4 -4
- package/src/stores/authentication.mdx +138 -0
- package/src/stores/backend/common.ts +89 -0
- package/src/stores/backend/constants.ts +22 -0
- package/src/stores/backend/schemes/jwt.ts +208 -0
- package/src/stores/backend/schemes/oauth.ts +142 -0
- package/src/stores/backend/schemes/token.ts +122 -0
- package/src/stores/backend/types.ts +96 -0
- package/src/stores/backend.ts +21 -427
- package/src/stores/index.ts +6 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/nside.ts +157 -0
- package/src/utils/color.spec.ts +24 -0
- package/src/utils/color.ts +100 -0
- package/src/utils/translations.ts +0 -4
- package/dist/LegalConsent-CEcXZml6.cjs +0 -1
- package/dist/LegalConsent-Dzq3fdnt.js +0 -277
- package/dist/LegalDocument-CS3MnOcV.cjs +0 -109
- package/dist/axios-ClRPr3Xn.js +0 -1777
- package/dist/axios-Dcidtc2l.cjs +0 -6
- package/dist/index-Bc699sOR.js +0 -4997
- package/dist/index-CL_OJMNr.cjs +0 -55
- package/dist/index-CTNsucOq.cjs +0 -147
- package/dist/index-CwLAV8WF.js +0 -210
- package/dist/index-FrfvunRp.cjs +0 -146
- package/dist/lib-BBJH9d11.cjs +0 -2792
- package/dist/lib-Y8FPgwH4.js +0 -20886
- package/dist/libRoutes-BsneoQ4G.js +0 -18
- package/dist/libRoutes-BzeZrIaK.cjs +0 -1
- package/src/demo/DemoApp.vue +0 -13
- package/src/demo/ShowcaseView.vue +0 -39
- package/src/demo/demo.css +0 -15
- /package/src/demo/{DemoContent.vue → views/DemoContent.vue} +0 -0
- /package/src/demo/{DemoView.vue → views/DemoView.vue} +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { defineComponent } from 'vue'
|
|
4
|
+
import BottomComponent from './BottomComponent.vue'
|
|
5
|
+
|
|
6
|
+
const UserMenuTriggerStub = defineComponent({
|
|
7
|
+
name: 'UserMenuTriggerComponent',
|
|
8
|
+
props: {
|
|
9
|
+
username: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
email: {
|
|
14
|
+
type: String,
|
|
15
|
+
default: '',
|
|
16
|
+
},
|
|
17
|
+
mode: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: 'detailed',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
template: '<div data-test="user-menu">{{ username }}|{{ email }}|{{ mode }}</div>',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('BottomComponent', () => {
|
|
26
|
+
it('renders UserMenuTriggerComponent with expected static props', () => {
|
|
27
|
+
const wrapper = mount(BottomComponent, {
|
|
28
|
+
global: {
|
|
29
|
+
stubs: {
|
|
30
|
+
UserMenuTriggerComponent: UserMenuTriggerStub,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
expect(wrapper.get('[data-test="user-menu"]').text()).toContain('John Doe')
|
|
36
|
+
expect(wrapper.get('[data-test="user-menu"]').text()).toContain('jdo@example.com')
|
|
37
|
+
expect(wrapper.get('[data-test="user-menu"]').text()).toContain('detailed')
|
|
38
|
+
})
|
|
39
|
+
})
|
package/src/containers/LayoutContainer/SideNavigationComponent/BottomComponent/BottomComponent.vue
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col border-t p-4 border-zinc-950/5">
|
|
3
|
+
<UserMenuTriggerComponent username="John Doe" email="jdo@example.com" mode="detailed" />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import UserMenuTriggerComponent from '@/containers/LayoutContainer/UserMenuTriggerComponent/UserMenuTriggerComponent.vue'
|
|
9
|
+
</script>
|
package/src/containers/LayoutContainer/SideNavigationComponent/MainComponent/MainComponent.spec.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { createRouter, createMemoryHistory, type RouteRecordRaw } from 'vue-router'
|
|
4
|
+
import { defineComponent } from 'vue'
|
|
5
|
+
import MainComponent from './MainComponent.vue'
|
|
6
|
+
|
|
7
|
+
const StubView = defineComponent({
|
|
8
|
+
template: '<div></div>',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const NavigationLinkStub = defineComponent({
|
|
12
|
+
name: 'NavigationLinkComponent',
|
|
13
|
+
props: {
|
|
14
|
+
route: {
|
|
15
|
+
type: String,
|
|
16
|
+
required: true,
|
|
17
|
+
},
|
|
18
|
+
icon: {
|
|
19
|
+
type: String,
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
label: {
|
|
23
|
+
type: String,
|
|
24
|
+
required: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
emits: ['navigation-item-click'],
|
|
28
|
+
template:
|
|
29
|
+
'<button class="nav-link" :data-route="route" :data-icon="icon" @click="$emit(\'navigation-item-click\')">{{ label }}</button>',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function createTestRouter() {
|
|
33
|
+
const routes: RouteRecordRaw[] = [
|
|
34
|
+
{
|
|
35
|
+
path: '/home',
|
|
36
|
+
name: 'home',
|
|
37
|
+
component: StubView,
|
|
38
|
+
meta: {
|
|
39
|
+
wefa: {
|
|
40
|
+
title: 'Home',
|
|
41
|
+
icon: 'pi pi-home',
|
|
42
|
+
showInNavigation: true,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
path: '/reports/',
|
|
48
|
+
name: 'reports',
|
|
49
|
+
component: StubView,
|
|
50
|
+
meta: {
|
|
51
|
+
wefa: {
|
|
52
|
+
title: 'Reports',
|
|
53
|
+
icon: 'pi pi-chart-line',
|
|
54
|
+
showInNavigation: true,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: '/settings',
|
|
60
|
+
name: 'settings',
|
|
61
|
+
component: StubView,
|
|
62
|
+
meta: {
|
|
63
|
+
wefa: {
|
|
64
|
+
title: 'Settings',
|
|
65
|
+
icon: 'pi pi-cog',
|
|
66
|
+
showInNavigation: true,
|
|
67
|
+
section: 'Administration',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
children: [
|
|
71
|
+
{
|
|
72
|
+
path: 'users/',
|
|
73
|
+
name: 'users',
|
|
74
|
+
component: StubView,
|
|
75
|
+
meta: {
|
|
76
|
+
wefa: {
|
|
77
|
+
title: 'Users',
|
|
78
|
+
icon: 'pi pi-users',
|
|
79
|
+
showInNavigation: true,
|
|
80
|
+
section: 'Administration',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
path: '/hidden',
|
|
88
|
+
name: 'hidden',
|
|
89
|
+
component: StubView,
|
|
90
|
+
meta: {
|
|
91
|
+
wefa: {
|
|
92
|
+
title: 'Hidden',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
return createRouter({
|
|
99
|
+
history: createMemoryHistory(),
|
|
100
|
+
routes,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe('MainComponent', () => {
|
|
105
|
+
let router: ReturnType<typeof createTestRouter>
|
|
106
|
+
|
|
107
|
+
beforeEach(async () => {
|
|
108
|
+
router = createTestRouter()
|
|
109
|
+
await router.push('/home')
|
|
110
|
+
await router.isReady()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('renders only routes configured for navigation', () => {
|
|
114
|
+
const wrapper = mount(MainComponent, {
|
|
115
|
+
global: {
|
|
116
|
+
plugins: [router],
|
|
117
|
+
stubs: {
|
|
118
|
+
NavigationLinkComponent: NavigationLinkStub,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(wrapper.text()).toContain('Home')
|
|
124
|
+
expect(wrapper.text()).toContain('Reports')
|
|
125
|
+
expect(wrapper.text()).toContain('Settings')
|
|
126
|
+
expect(wrapper.text()).toContain('Users')
|
|
127
|
+
expect(wrapper.text()).not.toContain('Hidden')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('groups section-based routes and renders section headers', () => {
|
|
131
|
+
const wrapper = mount(MainComponent, {
|
|
132
|
+
global: {
|
|
133
|
+
plugins: [router],
|
|
134
|
+
stubs: {
|
|
135
|
+
NavigationLinkComponent: NavigationLinkStub,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
expect(wrapper.text()).toContain('Administration')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('normalizes route paths for navigation entries', () => {
|
|
144
|
+
const wrapper = mount(MainComponent, {
|
|
145
|
+
global: {
|
|
146
|
+
plugins: [router],
|
|
147
|
+
stubs: {
|
|
148
|
+
NavigationLinkComponent: NavigationLinkStub,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const renderedPaths = wrapper
|
|
154
|
+
.findAll('.nav-link')
|
|
155
|
+
.map((element) => element.attributes('data-route'))
|
|
156
|
+
|
|
157
|
+
expect(renderedPaths).toContain('/reports')
|
|
158
|
+
expect(renderedPaths).toContain('/settings/users')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('forwards child navigation-item-click events', async () => {
|
|
162
|
+
const wrapper = mount(MainComponent, {
|
|
163
|
+
global: {
|
|
164
|
+
plugins: [router],
|
|
165
|
+
stubs: {
|
|
166
|
+
NavigationLinkComponent: NavigationLinkStub,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
await wrapper.get('.nav-link').trigger('click')
|
|
172
|
+
|
|
173
|
+
expect(wrapper.emitted('navigation-item-click')).toBeTruthy()
|
|
174
|
+
})
|
|
175
|
+
})
|
package/src/containers/LayoutContainer/SideNavigationComponent/MainComponent/MainComponent.vue
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col grow p-4 gap-4">
|
|
3
|
+
<section v-if="topLevelEntries.length > 0">
|
|
4
|
+
<NavigationLinkComponent
|
|
5
|
+
v-for="entry in topLevelEntries"
|
|
6
|
+
:key="entry.path"
|
|
7
|
+
:route="entry.path"
|
|
8
|
+
:icon="entry.icon"
|
|
9
|
+
:label="entry.label"
|
|
10
|
+
@navigation-item-click="emitNavigationItemClick"
|
|
11
|
+
/>
|
|
12
|
+
</section>
|
|
13
|
+
|
|
14
|
+
<section v-for="section in sectionEntries" :key="section.label" class="flex flex-col gap-2">
|
|
15
|
+
<h3 class="mb-1 px-2 text-sm font-medium text-zinc-500">{{ section.label }}</h3>
|
|
16
|
+
<div>
|
|
17
|
+
<NavigationLinkComponent
|
|
18
|
+
v-for="entry in section.entries"
|
|
19
|
+
:key="entry.path"
|
|
20
|
+
:route="entry.path"
|
|
21
|
+
:icon="entry.icon"
|
|
22
|
+
:label="entry.label"
|
|
23
|
+
@navigation-item-click="emitNavigationItemClick"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { computed } from 'vue'
|
|
32
|
+
import { type RouteRecordRaw, useRouter } from 'vue-router'
|
|
33
|
+
import type { WeFaRouteMeta } from '@/router'
|
|
34
|
+
import NavigationLinkComponent from '@/containers/LayoutContainer/SideNavigationComponent/MainComponent/NavigationLinkComponent.vue'
|
|
35
|
+
|
|
36
|
+
interface NavigationEntry {
|
|
37
|
+
path: string
|
|
38
|
+
label: string
|
|
39
|
+
icon?: string
|
|
40
|
+
section?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SectionNavigationEntries {
|
|
44
|
+
label: string
|
|
45
|
+
entries: NavigationEntry[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const router = useRouter()
|
|
49
|
+
const emit = defineEmits<{
|
|
50
|
+
(event: 'navigation-item-click'): void
|
|
51
|
+
}>()
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalizes route paths to avoid duplicate slashes and trailing slash noise.
|
|
55
|
+
* @param path Raw route path
|
|
56
|
+
* @returns Normalized absolute path
|
|
57
|
+
*/
|
|
58
|
+
function normalizePath(path: string): string {
|
|
59
|
+
if (!path) {
|
|
60
|
+
return '/'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const normalizedPath = path.replace(/\/{2,}/g, '/')
|
|
64
|
+
if (normalizedPath === '/') {
|
|
65
|
+
return normalizedPath
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return normalizedPath.replace(/\/$/, '')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolves a route path against its parent route path.
|
|
73
|
+
* @param parentPath Parent route path
|
|
74
|
+
* @param routePath Child route path
|
|
75
|
+
* @returns Resolved absolute path
|
|
76
|
+
*/
|
|
77
|
+
function resolvePath(parentPath: string, routePath: string): string {
|
|
78
|
+
if (routePath.startsWith('/')) {
|
|
79
|
+
return normalizePath(routePath)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!routePath) {
|
|
83
|
+
return normalizePath(parentPath)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (parentPath === '/') {
|
|
87
|
+
return normalizePath(`/${routePath}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return normalizePath(`${parentPath}/${routePath}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extracts navigation entries from the router tree based on showInNavigation metadata.
|
|
95
|
+
* @param routes Router records to inspect
|
|
96
|
+
* @param parentPath Parent path used to resolve relative child paths
|
|
97
|
+
* @returns Flattened navigation entries in declaration order
|
|
98
|
+
*/
|
|
99
|
+
function routeNavigationEntries(
|
|
100
|
+
routes: RouteRecordRaw[],
|
|
101
|
+
parentPath: string = '/'
|
|
102
|
+
): NavigationEntry[] {
|
|
103
|
+
const entries: NavigationEntry[] = []
|
|
104
|
+
|
|
105
|
+
for (const route of routes) {
|
|
106
|
+
const fullPath = resolvePath(parentPath, route.path)
|
|
107
|
+
const routeMeta = route.meta?.wefa as WeFaRouteMeta | undefined
|
|
108
|
+
const showInNavigation = routeMeta?.showInNavigation ?? false
|
|
109
|
+
const section = routeMeta?.section
|
|
110
|
+
|
|
111
|
+
if (showInNavigation === true) {
|
|
112
|
+
entries.push({
|
|
113
|
+
path: fullPath,
|
|
114
|
+
label: routeMeta?.title ?? String(route.name ?? fullPath),
|
|
115
|
+
icon: routeMeta?.icon,
|
|
116
|
+
section: section?.trim() || undefined,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (route.children?.length) {
|
|
121
|
+
entries.push(...routeNavigationEntries(route.children, fullPath))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return entries
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const navigationEntries = computed(() => {
|
|
129
|
+
return routeNavigationEntries(router.options.routes as RouteRecordRaw[])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const topLevelEntries = computed(() => {
|
|
133
|
+
return navigationEntries.value.filter((entry) => !entry.section)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const sectionEntries = computed<SectionNavigationEntries[]>(() => {
|
|
137
|
+
const groupedEntries = new Map<string, NavigationEntry[]>()
|
|
138
|
+
|
|
139
|
+
for (const entry of navigationEntries.value) {
|
|
140
|
+
if (!entry.section) {
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!groupedEntries.has(entry.section)) {
|
|
145
|
+
groupedEntries.set(entry.section, [])
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
groupedEntries.get(entry.section)?.push(entry)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return Array.from(groupedEntries.entries()).map(([label, entries]) => ({
|
|
152
|
+
label,
|
|
153
|
+
entries,
|
|
154
|
+
}))
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Forwards click events from individual navigation links.
|
|
159
|
+
*/
|
|
160
|
+
function emitNavigationItemClick() {
|
|
161
|
+
emit('navigation-item-click')
|
|
162
|
+
}
|
|
163
|
+
</script>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { defineComponent, h } from 'vue'
|
|
4
|
+
import NavigationLinkComponent from './NavigationLinkComponent.vue'
|
|
5
|
+
|
|
6
|
+
function createRouterLinkStub(isActive: boolean, isExactActive: boolean) {
|
|
7
|
+
return defineComponent({
|
|
8
|
+
name: 'RouterLink',
|
|
9
|
+
props: {
|
|
10
|
+
to: {
|
|
11
|
+
type: [String, Object],
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
emits: ['click'],
|
|
16
|
+
setup(props, { emit, slots }) {
|
|
17
|
+
return () =>
|
|
18
|
+
h(
|
|
19
|
+
'a',
|
|
20
|
+
{
|
|
21
|
+
'data-test': 'router-link',
|
|
22
|
+
href: typeof props.to === 'string' ? props.to : '#',
|
|
23
|
+
onClick: (event: MouseEvent) => emit('click', event),
|
|
24
|
+
},
|
|
25
|
+
slots.default?.({
|
|
26
|
+
isActive,
|
|
27
|
+
isExactActive,
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('NavigationLinkComponent', () => {
|
|
35
|
+
it('renders label and icon', () => {
|
|
36
|
+
const wrapper = mount(NavigationLinkComponent, {
|
|
37
|
+
props: {
|
|
38
|
+
route: '/products',
|
|
39
|
+
icon: 'pi pi-box',
|
|
40
|
+
label: 'Products',
|
|
41
|
+
},
|
|
42
|
+
global: {
|
|
43
|
+
stubs: {
|
|
44
|
+
RouterLink: createRouterLinkStub(false, false),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(wrapper.text()).toContain('Products')
|
|
50
|
+
expect(wrapper.find('i.pi-box').exists()).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('shows active indicator for active links', () => {
|
|
54
|
+
const wrapper = mount(NavigationLinkComponent, {
|
|
55
|
+
props: {
|
|
56
|
+
route: '/products',
|
|
57
|
+
icon: 'pi pi-box',
|
|
58
|
+
label: 'Products',
|
|
59
|
+
},
|
|
60
|
+
global: {
|
|
61
|
+
stubs: {
|
|
62
|
+
RouterLink: createRouterLinkStub(true, false),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
expect(wrapper.find('.absolute.inset-y-2.-left-4').exists()).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('does not show active indicator for inactive links', () => {
|
|
71
|
+
const wrapper = mount(NavigationLinkComponent, {
|
|
72
|
+
props: {
|
|
73
|
+
route: '/products',
|
|
74
|
+
icon: 'pi pi-box',
|
|
75
|
+
label: 'Products',
|
|
76
|
+
},
|
|
77
|
+
global: {
|
|
78
|
+
stubs: {
|
|
79
|
+
RouterLink: createRouterLinkStub(false, false),
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
expect(wrapper.find('.absolute.inset-y-2.-left-4').exists()).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('emits navigation-item-click when clicked', async () => {
|
|
88
|
+
const wrapper = mount(NavigationLinkComponent, {
|
|
89
|
+
props: {
|
|
90
|
+
route: '/products',
|
|
91
|
+
icon: 'pi pi-box',
|
|
92
|
+
label: 'Products',
|
|
93
|
+
},
|
|
94
|
+
global: {
|
|
95
|
+
stubs: {
|
|
96
|
+
RouterLink: createRouterLinkStub(false, false),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
await wrapper.get('[data-test="router-link"]').trigger('click')
|
|
102
|
+
|
|
103
|
+
expect((wrapper.emitted('navigation-item-click') ?? []).length).toBeGreaterThan(0)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<router-link v-slot="{ isActive, isExactActive }" :to="route" @click="emitNavigationClick">
|
|
3
|
+
<div class="relative" @click="emitNavigationClick">
|
|
4
|
+
<Transition
|
|
5
|
+
enter-active-class="transition opacity-0 scale-y-75 duration-200 ease-out"
|
|
6
|
+
enter-to-class="opacity-100 scale-y-100"
|
|
7
|
+
leave-active-class="transition opacity-100 scale-y-100 duration-200 ease-in"
|
|
8
|
+
leave-to-class="opacity-0 scale-y-75"
|
|
9
|
+
>
|
|
10
|
+
<span
|
|
11
|
+
v-if="isActive || isExactActive"
|
|
12
|
+
class="absolute inset-y-2 -left-4 rounded-full bg-zinc-950 w-1"
|
|
13
|
+
></span>
|
|
14
|
+
</Transition>
|
|
15
|
+
<div
|
|
16
|
+
:class="[isExactActive ? 'bg-zinc-500/5' : '']"
|
|
17
|
+
class="flex w-full items-center rounded-lg cursor-pointer text-base px-2 py-2 gap-3 hover:bg-zinc-950/5"
|
|
18
|
+
@click="emitNavigationClick"
|
|
19
|
+
>
|
|
20
|
+
<i v-if="icon" class="pi" :class="icon" style="font-size: 1.5rem"></i>
|
|
21
|
+
<span class="block truncate text-zinc-950">{{ label }}</span>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</router-link>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
export interface NavigationLinkComponentProps {
|
|
29
|
+
route: string
|
|
30
|
+
icon?: string
|
|
31
|
+
label: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { route, icon, label } = defineProps<NavigationLinkComponentProps>()
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
(event: 'navigation-item-click'): void
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Emits a navigation click event so parents can react (e.g. close a mobile drawer).
|
|
41
|
+
*/
|
|
42
|
+
function emitNavigationClick() {
|
|
43
|
+
emit('navigation-item-click')
|
|
44
|
+
}
|
|
45
|
+
</script>
|
package/src/containers/LayoutContainer/SideNavigationComponent/SideNavigationComponent.spec.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { defineComponent } from 'vue'
|
|
4
|
+
import SideNavigationComponent from './SideNavigationComponent.vue'
|
|
5
|
+
|
|
6
|
+
const TopComponentStub = defineComponent({
|
|
7
|
+
name: 'TopComponent',
|
|
8
|
+
props: {
|
|
9
|
+
projectTitle: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
projectLogo: {
|
|
14
|
+
type: String,
|
|
15
|
+
default: undefined,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
template: '<div data-test="top">{{ projectTitle }}|{{ projectLogo }}</div>',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const MainComponentStub = defineComponent({
|
|
22
|
+
name: 'MainComponent',
|
|
23
|
+
template: '<div data-test="main">Main Navigation</div>',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('SideNavigationComponent', () => {
|
|
27
|
+
it('renders top and main sections', () => {
|
|
28
|
+
const wrapper = mount(SideNavigationComponent, {
|
|
29
|
+
props: {
|
|
30
|
+
projectTitle: 'WeFa',
|
|
31
|
+
},
|
|
32
|
+
global: {
|
|
33
|
+
stubs: {
|
|
34
|
+
TopComponent: TopComponentStub,
|
|
35
|
+
MainComponent: MainComponentStub,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(wrapper.find('[data-test="top"]').exists()).toBe(true)
|
|
41
|
+
expect(wrapper.find('[data-test="main"]').exists()).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('passes project title to TopComponent', () => {
|
|
45
|
+
const wrapper = mount(SideNavigationComponent, {
|
|
46
|
+
props: {
|
|
47
|
+
projectTitle: 'Energy Forecast',
|
|
48
|
+
},
|
|
49
|
+
global: {
|
|
50
|
+
stubs: {
|
|
51
|
+
TopComponent: TopComponentStub,
|
|
52
|
+
MainComponent: MainComponentStub,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
expect(wrapper.get('[data-test="top"]').text()).toBe('Energy Forecast|')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('passes custom logo to TopComponent', () => {
|
|
61
|
+
const wrapper = mount(SideNavigationComponent, {
|
|
62
|
+
props: {
|
|
63
|
+
projectTitle: 'Energy Forecast',
|
|
64
|
+
projectLogo: 'https://example.test/logo.svg',
|
|
65
|
+
},
|
|
66
|
+
global: {
|
|
67
|
+
stubs: {
|
|
68
|
+
TopComponent: TopComponentStub,
|
|
69
|
+
MainComponent: MainComponentStub,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
expect(wrapper.get('[data-test="top"]').text()).toBe(
|
|
75
|
+
'Energy Forecast|https://example.test/logo.svg'
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="fixed inset-y-0 left-0 w-64 max-lg:hidden">
|
|
3
|
+
<nav class="flex h-full min-h-0 flex-col">
|
|
4
|
+
<TopComponent
|
|
5
|
+
:project-title="projectTitle"
|
|
6
|
+
:project-logo="projectLogo"
|
|
7
|
+
:project-logo-alt="projectLogoAlt"
|
|
8
|
+
/>
|
|
9
|
+
<MainComponent />
|
|
10
|
+
</nav>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import TopComponent from '@/containers/LayoutContainer/SideNavigationComponent/TopComponent/TopComponent.vue'
|
|
16
|
+
import MainComponent from '@/containers/LayoutContainer/SideNavigationComponent/MainComponent/MainComponent.vue'
|
|
17
|
+
|
|
18
|
+
export interface SideNavigationComponentProps {
|
|
19
|
+
projectTitle: string
|
|
20
|
+
projectLogo?: string
|
|
21
|
+
projectLogoAlt?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
projectTitle,
|
|
26
|
+
projectLogo = undefined,
|
|
27
|
+
projectLogoAlt = undefined,
|
|
28
|
+
} = defineProps<SideNavigationComponentProps>()
|
|
29
|
+
</script>
|