Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nested navigation menu items #2609

Merged
merged 42 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0ad9508
created storybook component and written code for testing the result.
sadaf895 Oct 9, 2024
cf98f89
Written the html and ts file code.
sadaf895 Oct 11, 2024
7126967
initial commit
sadaf895 Oct 15, 2024
dae10b0
created component that include one icon+label+submenu
sadaf895 Oct 15, 2024
307465d
Improved the UI.
sadaf895 Oct 17, 2024
a89ae95
fix: prevent component init error (#2620)
tomwwinter Oct 18, 2024
87f8ab0
fix(attendance): attendance overview tab lists archived activities se…
Abhinegi2 Oct 18, 2024
f1ae07c
fix: upgrade webpack from 5.94.0 to 5.95.0
snyk-bot Oct 19, 2024
4f4779b
fix(deps): update dependencies and upgrade storybook
sleidig Oct 22, 2024
3b11a7e
fix(Template Export): required filename pattern field is marked as re…
sleidig Oct 21, 2024
cbc7ae1
fix: prevent error when attempting to log details of failed to fetch
sleidig Oct 22, 2024
2b46c4d
fix: ensure base types like Note are migrated
sleidig Oct 22, 2024
e830920
fix(attendance): display absent participants even if added via group
sleidig Oct 22, 2024
e7b8c7c
fix(dashboard): improve UI and performance of count & disaggregation …
sadaf895 Oct 23, 2024
2e8aaef
fix(i18n): update translations
sleidig Oct 24, 2024
fcbc9ef
refactor: remove old config migrations (#2536)
sleidig Oct 25, 2024
ad17bab
fix(dashboard): improve layout of entity-count widget (#2643)
sadaf895 Oct 31, 2024
c8fdb94
fix: attendance status now always reliably saved (#2641)
tomwwinter Oct 31, 2024
eca9dd7
fix: dashboard widget shows correct number excluding inactive records…
sadaf895 Nov 12, 2024
cded9be
ci: use internal server for chrome downloads (#2658)
tomwwinter Nov 13, 2024
e0e2298
feat(core): make subtitle and explanation for all dashboard widgets o…
sadaf895 Nov 14, 2024
4a05ce6
fix: prevent duplicate keys in filter options (#2640)
tomwwinter Nov 14, 2024
c0650b6
fix: progress dashboard shows correct overall percentage again (#2663)
sadaf895 Nov 15, 2024
4b81273
add recursive menu item permission checks
sleidig Nov 15, 2024
28a4747
Merge branch 'master' into Nested_menu_items
sleidig Nov 15, 2024
485a525
Update src/app/core/config/dynamic-routing/route-permissions.service.ts
sleidig Nov 15, 2024
1f5f1be
Update src/app/core/ui/navigation/navigation/navigation.component.ts
sleidig Nov 15, 2024
459053c
config menu
sleidig Nov 15, 2024
ce8657a
removed unnecessary import
sadaf895 Nov 18, 2024
4e642ed
Pass the activeLink as an @Input to menu component
sadaf895 Nov 20, 2024
5776df4
Revert the variable name to link.
sadaf895 Nov 20, 2024
662af98
Merge branch 'master' into Nested_menu_items
sleidig Nov 20, 2024
2fe2c1d
removed " as string"
sadaf895 Nov 20, 2024
4f44047
Merge branch 'Nested_menu_items' of https://github.com/Aam-Digital/nd…
sadaf895 Nov 20, 2024
309fc26
Update src/app/core/config/dynamic-routing/route-permissions.service.…
sleidig Nov 20, 2024
ecf35e3
Implement logic for active submenu
sadaf895 Nov 21, 2024
0b9a4ea
Merge branch 'Nested_menu_items' of https://github.com/Aam-Digital/nd…
sadaf895 Nov 21, 2024
3550a40
move to own component folder
sleidig Nov 25, 2024
23d225e
Merge branch 'master' into Nested_menu_items
sleidig Nov 25, 2024
f4b3a09
remove test config
sleidig Nov 25, 2024
37e2d17
clarify code comments
sleidig Nov 25, 2024
d622c4d
fix submenu highlighting
sleidig Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
670 changes: 338 additions & 332 deletions src/app/core/config/config-fix.ts

Large diffs are not rendered by default.

104 changes: 102 additions & 2 deletions src/app/core/config/dynamic-routing/route-permissions.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,33 @@ import { TestBed } from "@angular/core/testing";
import { RoutePermissionsService } from "./route-permissions.service";
import { UserRoleGuard } from "../../permissions/permission-guard/user-role.guard";
import { EntityPermissionGuard } from "../../permissions/permission-guard/entity-permission.guard";
import { MenuItem } from "app/core/ui/navigation/menu-item";

describe("RoutePermissionsService", () => {
let service: RoutePermissionsService;

let mockUserRoleGuard: jasmine.SpyObj<UserRoleGuard>;
let mockEntityPermissionGuard: jasmine.SpyObj<EntityPermissionGuard>;

beforeEach(() => {
mockEntityPermissionGuard = jasmine.createSpyObj(["checkRoutePermissions"]);
mockEntityPermissionGuard.checkRoutePermissions.and.resolveTo(true);

mockUserRoleGuard = jasmine.createSpyObj(["checkRoutePermissions"]);
mockUserRoleGuard.checkRoutePermissions.and.callFake(
async (path: string) => {
if (path === "allowed") {
return true;
} else {
return false;
}
},
);

TestBed.configureTestingModule({
providers: [
{ provide: UserRoleGuard, useValue: {} },
{ provide: EntityPermissionGuard, useValue: {} },
{ provide: UserRoleGuard, useValue: mockUserRoleGuard },
{ provide: EntityPermissionGuard, useValue: mockEntityPermissionGuard },
],
});
service = TestBed.inject(RoutePermissionsService);
Expand All @@ -20,4 +38,86 @@ describe("RoutePermissionsService", () => {
it("should be created", () => {
expect(service).toBeTruthy();
});

it("should filter menu-items where user doesn't have permission for its link", async () => {
const itemPermitted: MenuItem = {
label: "Visible Item",
link: "allowed",
};
const itemProtected: MenuItem = {
label: "Hidden Item",
link: "blocked",
};

const filteredItems: MenuItem[] = await service.filterPermittedRoutes([
itemPermitted,
itemProtected,
]);

expect(filteredItems).toEqual([itemPermitted]);
});

it("should filter each submenu item based on permissions", async () => {
const itemPermitted: MenuItem = {
label: "Visible Item",
link: "allowed",
};
const itemProtected: MenuItem = {
label: "Hidden Item",
link: "blocked",
};
const nestedItem: MenuItem = {
label: "Parent Item",
subMenu: [itemPermitted, itemProtected],
};

const filteredItems: MenuItem[] = await service.filterPermittedRoutes([
nestedItem,
]);

expect(filteredItems).toEqual([
{
label: "Parent Item",
subMenu: [itemPermitted],
},
]);
});

it("should filter parent item if all submenu items are filter due to permissions", async () => {
const nestedItem: MenuItem = {
label: "Parent Item",
subMenu: [
{
label: "Hidden Item 1",
link: "blocked",
},
{
label: "Hidden Item 2",
link: "blocked",
},
],
};

const filteredItems: MenuItem[] = await service.filterPermittedRoutes([
nestedItem,
]);

expect(filteredItems).toEqual([]);
});
});

/*

Simple:
item 1
item 2 x

Nested:
item 1
1.1
1.2 x

item 2 x
2.1 x

*/
17 changes: 15 additions & 2 deletions src/app/core/config/dynamic-routing/route-permissions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,28 @@ export class RoutePermissionsService {
*/
async filterPermittedRoutes(items: MenuItem[]): Promise<MenuItem[]> {
const accessibleRoutes: MenuItem[] = [];

for (const item of items) {
if (await this.isAccessibleRouteForUser(item.link)) {
if (item.link && (await this.isAccessibleRouteForUser(item.link))) {
accessibleRoutes.push(item);
} else if (item.subMenu) {
const accessibleSubItems: MenuItem[] = await this.filterPermittedRoutes(
item.subMenu,
);

if (accessibleSubItems.length > 0) {
// only adding the item if there is at least one accessible subMenu item
const filteredParentItem: MenuItem = Object.assign({}, item);
sleidig marked this conversation as resolved.
Show resolved Hide resolved
filteredParentItem.subMenu = accessibleSubItems;
accessibleRoutes.push(filteredParentItem);
}
}
}

return accessibleRoutes;
}

private async isAccessibleRouteForUser(path: string) {
private async isAccessibleRouteForUser(path: string): Promise<boolean> {
return (
(await this.roleGuard.checkRoutePermissions(path)) &&
(await this.permissionGuard.checkRoutePermissions(path))
Expand Down
4 changes: 3 additions & 1 deletion src/app/core/ui/navigation/menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export interface MenuItem {
/**
* The url fragment to which the item will route to (e.g. '/dashboard')
*/
link: string;
link?: string;

subMenu?: MenuItem[];
}

/**
Expand Down
31 changes: 31 additions & 0 deletions src/app/core/ui/navigation/menu-item/menu-item.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<mat-list-item
angulartics2On="click"
angularticsCategory="Navigation"
angularticsAction="app_navigation_link_click"
[angularticsLabel]="item.label"
[routerLink]="item.link ? [item.link] : undefined"
[class.matched-background]="item.link === activeLink"
[class.indent-item]="item.link === activeLink"
(click)="toggleSubMenu()"
>
<a class="flex-row gap-small">
<app-fa-dynamic-icon
class="nav-icon"
[icon]="item.icon"
></app-fa-dynamic-icon>
<div>{{ item.label }}</div>

<fa-icon
*ngIf="hasSubMenu(item)"
[icon]="isExpanded ? 'chevron-down' : 'chevron-right'"
></fa-icon>
</a>
</mat-list-item>

<!-- Render submenus if they exist and are expanded -->
<div *ngIf="isExpanded" class="submenu">
<ng-container *ngFor="let subItem of item.subMenu">
<!-- Pass activeLink to each submenu item to highlight the active submenu -->
<app-menu-item [item]="subItem" [activeLink]="activeLink"></app-menu-item>
</ng-container>
</div>
21 changes: 21 additions & 0 deletions src/app/core/ui/navigation/menu-item/menu-item.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@use "../../../../../styles/variables/sizes";
@use "../../../../../styles/variables/colors";

/* ensures that all icons have the same width */
.nav-icon {
min-width: sizes.$max-icon-width;
}

.matched-background {
background-color: colors.$background !important;
}

.submenu {
padding-left: 20px;
}

.indent-item {
margin-left: 8px;
padding-left: 8px;
width: auto;
}
48 changes: 48 additions & 0 deletions src/app/core/ui/navigation/menu-item/menu-item.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Component, Input } from "@angular/core";
import { CommonModule, NgForOf } from "@angular/common";
import { MatListModule } from "@angular/material/list";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { FaDynamicIconComponent } from "../../../common-components/fa-dynamic-icon/fa-dynamic-icon.component";
import { RouterLink } from "@angular/router";
import { Angulartics2Module } from "angulartics2";
import { MenuItem } from "../menu-item";
import { MatMenuModule } from "@angular/material/menu";

@Component({
selector: "app-menu-item",
templateUrl: "./menu-item.component.html",
styleUrls: ["./menu-item.component.scss"],
imports: [
CommonModule,
MatListModule,
FaIconComponent,
FaDynamicIconComponent,
RouterLink,
Angulartics2Module,
NgForOf,
MatMenuModule,
],
standalone: true,
})
export class MenuItemComponent {
/**
* The menu item to be displayed.
*/
@Input() item: MenuItem;

/**
* The menu item link that is currently displayed in the app
* in order to highlight the active menu.
*/
@Input() activeLink: string;

isExpanded: boolean = false;

toggleSubMenu(): void {
this.isExpanded = !this.isExpanded;
}

hasSubMenu(item: MenuItem): boolean {
return !!item.subMenu && item.subMenu.length > 0;
}
}
36 changes: 1 addition & 35 deletions src/app/core/ui/navigation/navigation/navigation.component.html
Original file line number Diff line number Diff line change
@@ -1,39 +1,5 @@
<!--
~ This file is part of ndb-core.
~
~ ndb-core is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ ndb-core is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
-->

<mat-nav-list>
<ng-container *ngFor="let item of menuItems">
<mat-list-item
angulartics2On="click"
angularticsCategory="Navigation"
angularticsAction="app_navigation_link_click"
[angularticsLabel]="item.label"
[routerLink]="[item.link]"
[class.matched-background]="item.link === activeLink"
onclick="this.blur();"
>
<a class="flex-row gap-small">
<app-fa-dynamic-icon
class="nav-icon"
[icon]="item.icon"
></app-fa-dynamic-icon>
<div>{{ item.label }}</div>
</a>
</mat-list-item>
<mat-divider></mat-divider>
<app-menu-item [item]="item" [activeLink]="activeLink"></app-menu-item>
</ng-container>
</mat-nav-list>
11 changes: 0 additions & 11 deletions src/app/core/ui/navigation/navigation/navigation.component.scss
Original file line number Diff line number Diff line change
@@ -1,11 +0,0 @@
@use "variables/sizes";
@use "variables/colors";

/* ensures that all icons have the same width */
.nav-icon {
min-width: sizes.$max-icon-width;
}

.matched-background {
background-color: colors.$background !important;
}
36 changes: 11 additions & 25 deletions src/app/core/ui/navigation/navigation/navigation.component.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,15 @@
/*
* This file is part of ndb-core.
*
* ndb-core is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ndb-core is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
*/

import { Component } from "@angular/core";
import { MenuItem, NavigationMenuConfig } from "../menu-item";
import { ConfigService } from "../../../config/config.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { NavigationEnd, Router, RouterLink } from "@angular/router";
import { NavigationEnd, Router } from "@angular/router";
import { filter, startWith } from "rxjs/operators";
import { MatListModule } from "@angular/material/list";
import { NgForOf } from "@angular/common";
import { CommonModule, NgForOf } from "@angular/common";
import { Angulartics2Module } from "angulartics2";
import { FaDynamicIconComponent } from "../../../common-components/fa-dynamic-icon/fa-dynamic-icon.component";
import { RoutePermissionsService } from "../../../config/dynamic-routing/route-permissions.service";
import { MatMenuModule } from "@angular/material/menu";
import { MenuItemComponent } from "../menu-item/menu-item.component";

/**
* Main app menu listing.
Expand All @@ -39,8 +23,9 @@ import { RoutePermissionsService } from "../../../config/dynamic-routing/route-p
MatListModule,
NgForOf,
Angulartics2Module,
RouterLink,
FaDynamicIconComponent,
MatMenuModule,
CommonModule,
MenuItemComponent,
],
standalone: true,
})
Expand Down Expand Up @@ -82,9 +67,10 @@ export class NavigationComponent {
*/
private computeActiveLink(newUrl: string): string {
// conservative filter matching all items that could fit to the given url
const items: MenuItem[] = this.menuItems.filter((item) =>
newUrl.startsWith(item.link),
);
// flatten nested submenu items to parse all
const items: MenuItem[] = this.menuItems
.reduce((acc, item) => acc.concat(item, item.subMenu || []), [])
.filter((item) => newUrl.startsWith(item.link));
switch (items.length) {
case 0:
return "";
Expand Down
Loading
Loading