Skip to content

Commit 1c485ae

Browse files
momesginMo Mesgin
andauthored
[ Apps > Charts ] Fix "Charts" side nav item losing its highlighted state (#17332)
* ignore query params when comparing paths to highlight sidenav items * address feedback * lint --------- Co-authored-by: Mo Mesgin <mmesgin@Mos-M2-MacBook-Pro.local>
1 parent 56ca458 commit 1c485ae

4 files changed

Lines changed: 102 additions & 16 deletions

File tree

shell/components/nav/Group.vue

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ export default {
8282
const validRoute = filterLocationValidParams(this.$router, overviewRoute || {});
8383
const route = this.$router.resolve(validRoute);
8484
85-
return this.$route.fullPath.split('#')[0] === route?.fullPath;
85+
// Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
86+
return this.$route.path === route?.path;
8687
}
8788
}
8889
@@ -204,14 +205,14 @@ export default {
204205
} else if (item.route) {
205206
const navLevels = ['cluster', 'product', 'resource'];
206207
const matchesNavLevel = navLevels.filter((param) => !this.$route.params[param] || this.$route.params[param] !== item.route.params[param]).length === 0;
207-
const withoutHash = this.$route.hash ? this.$route.fullPath.slice(0, this.$route.fullPath.indexOf(this.$route.hash)) : this.$route.fullPath;
208-
const withoutQuery = withoutHash.split('?')[0];
209208
const validItemRoute = filterLocationValidParams(this.$router, item.route);
210-
const itemFullPath = this.$router.resolve(validItemRoute).fullPath;
211209
212-
if (matchesNavLevel || itemFullPath === withoutQuery) {
210+
// Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
211+
const itemPath = this.$router.resolve(validItemRoute).path;
212+
213+
if (matchesNavLevel || itemPath === this.$route.path) {
213214
return true;
214-
} else if (parentPath && itemFullPath === parentPath) {
215+
} else if (parentPath && itemPath === parentPath) {
215216
return true;
216217
}
217218
}

shell/components/nav/Type.vue

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ export default {
6868
},
6969
7070
isActive() {
71-
const typeFullPath = this.$router.resolve(this.typeRoute)?.fullPath.toLowerCase();
72-
const pageFullPath = this.$route.fullPath?.toLowerCase().split('#')[0]; // Ignore the shebang when comparing routes
71+
// Use .path instead of .fullPath to ignore query parameters and hashes when comparing routes
72+
const typePath = this.$router.resolve(this.typeRoute)?.path.toLowerCase();
73+
const pagePath = this.$route.path?.toLowerCase();
7374
const routeMetaNav = this.$route.meta?.nav;
7475
7576
// If the route explicitly declares the nav path that should be highlighted, then use that
@@ -80,14 +81,14 @@ export default {
8081
.replace(':cluster', cluster)
8182
.replace(':product', product);
8283
83-
if (navPath === typeFullPath) {
84+
if (navPath === typePath) {
8485
return true;
8586
}
8687
}
8788
8889
if ( !this.type.exact) {
89-
const typeSplit = typeFullPath.split('/');
90-
const pageSplit = pageFullPath.split('/');
90+
const typeSplit = typePath.split('/');
91+
const pageSplit = pagePath.split('/');
9192
9293
for (let index = 0; index < typeSplit.length; ++index) {
9394
if ( index >= pageSplit.length || typeSplit[index] !== pageSplit[index] ) {
@@ -98,7 +99,7 @@ export default {
9899
return true;
99100
}
100101
101-
return typeFullPath === pageFullPath;
102+
return typePath === pagePath;
102103
},
103104
104105
typeRoute() {
@@ -131,7 +132,7 @@ export default {
131132
<router-link
132133
v-if="type.route"
133134
:key="type.name"
134-
v-slot="{ href, navigate,isExactActive }"
135+
v-slot="{ href, navigate, isExactActive }"
135136
custom
136137
:to="typeRoute"
137138
>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { shallowMount } from '@vue/test-utils';
2+
import Group from '@shell/components/nav/Group.vue';
3+
4+
describe('component: Group', () => {
5+
it('isOverview ignores query parameters and hash strings when checking active state', () => {
6+
const group = {
7+
name: 'test',
8+
children: [
9+
{
10+
route: { name: 'overview-route' },
11+
overview: true
12+
}
13+
]
14+
};
15+
16+
const wrapper = shallowMount(Group as any, {
17+
props: {
18+
group, canCollapse: true, idPrefix: ''
19+
},
20+
global: {
21+
mocks: {
22+
$route: { path: '/test/route', fullPath: '/test/route?query=val#hash' },
23+
$router: {
24+
resolve: jest.fn().mockReturnValue({ path: '/test/route', fullPath: '/test/route' }),
25+
getRoutes: jest.fn().mockReturnValue([])
26+
},
27+
t: (key: string) => key
28+
}
29+
}
30+
});
31+
32+
expect((wrapper.vm as any).isOverview).toBe(true);
33+
});
34+
35+
it('hasActiveRoute ignores query parameters when checking item paths', () => {
36+
const group = {
37+
name: 'test',
38+
children: [
39+
{ route: { name: 'child-route', params: {} } }
40+
]
41+
};
42+
43+
const wrapper = shallowMount(Group as any, {
44+
props: {
45+
group, canCollapse: true, idPrefix: ''
46+
},
47+
global: {
48+
mocks: {
49+
$route: {
50+
params: {},
51+
hash: '#hash',
52+
path: '/child/route',
53+
fullPath: '/child/route?query=val#hash',
54+
matched: []
55+
},
56+
$router: {
57+
resolve: jest.fn().mockReturnValue({ path: '/child/route', fullPath: '/child/route' }),
58+
getRoutes: jest.fn().mockReturnValue([])
59+
},
60+
t: (key: string) => key
61+
}
62+
}
63+
});
64+
65+
expect((wrapper.vm as any).hasActiveRoute()).toBe(true);
66+
});
67+
});

shell/components/nav/__tests__/Type.test.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ describe('component: Type', () => {
2828
};
2929
const routerMock = {
3030
resolve: jest.fn((route) => {
31-
return { fullPath: route };
31+
return { fullPath: route, path: route };
3232
})
3333
};
34-
const routeMock = { fullPath: 'route' };
34+
const routeMock = { fullPath: 'route', path: 'route' };
3535

3636
describe('should pass props correctly', () => {
3737
it('should forward Type props to router-link', () => {
@@ -104,7 +104,7 @@ describe('component: Type', () => {
104104
directives: { cleanHtml: (identity) => identity },
105105

106106
mocks: {
107-
$store: storeMock, $router: routerMock, $route: { fullPath: 'bad' }
107+
$store: storeMock, $router: routerMock, $route: { fullPath: 'bad', path: 'bad' }
108108
},
109109
stubs: { routerLink: createChildRenderingRouterLinkStub() },
110110
},
@@ -152,6 +152,23 @@ describe('component: Type', () => {
152152

153153
expect(elementWithSelector.exists()).toBe(false);
154154
});
155+
it('should use active and exact active classes when route matches but includes query and hash', () => {
156+
const wrapper = shallowMount(Type as any, {
157+
props: { type: defaultRouteTypeProp, highlightRoute: true },
158+
159+
global: {
160+
directives: { cleanHtml: (identity) => identity },
161+
162+
mocks: {
163+
$store: storeMock, $router: routerMock, $route: { fullPath: 'route?repo=test#myhash', path: 'route' }
164+
},
165+
stubs: { routerLink: createChildRenderingRouterLinkStub({ isExactActive: true }) },
166+
},
167+
});
168+
169+
expect(wrapper.find(`.${ activeClass }`).exists()).toBe(true);
170+
expect(wrapper.find(`.${ exactActiveClass }`).exists()).toBe(true);
171+
});
155172
});
156173

157174
describe('should use classes if preconditions are met', () => {

0 commit comments

Comments
 (0)