From 3f941e710995f8e88c6dcb16ae35e72416a2f2ca Mon Sep 17 00:00:00 2001 From: nsemets Date: Sun, 28 Sep 2025 18:21:18 +0300 Subject: [PATCH] fix(nav-menu): updated permissions --- .../components/nav-menu/nav-menu.component.ts | 42 ++------------ src/app/core/constants/nav-items.constant.ts | 26 ++++----- src/app/core/helpers/nav-menu.helper.ts | 55 +++++++++---------- src/app/core/models/custom-menu-item.model.ts | 8 +++ src/app/core/models/index.ts | 1 + src/app/core/models/route-context.model.ts | 5 +- .../models/guid-response-json-api.model.ts | 2 +- src/app/shared/services/resource.service.ts | 2 +- 8 files changed, 56 insertions(+), 85 deletions(-) create mode 100644 src/app/core/models/custom-menu-item.model.ts diff --git a/src/app/core/components/nav-menu/nav-menu.component.ts b/src/app/core/components/nav-menu/nav-menu.component.ts index efbb43336..e4f8d4f3e 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.ts @@ -2,23 +2,22 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; -import { MenuItem } from 'primeng/api'; import { PanelMenuModule } from 'primeng/panelmenu'; import { filter, map } from 'rxjs'; -import { Component, computed, effect, inject, output } from '@angular/core'; +import { Component, computed, inject, output } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'; import { MENU_ITEMS } from '@core/constants'; import { ProviderSelectors } from '@core/store/provider'; import { filterMenuItems, updateMenuItems } from '@osf/core/helpers'; -import { RouteContext } from '@osf/core/models'; +import { CustomMenuItem, RouteContext } from '@osf/core/models'; import { AuthService } from '@osf/core/services'; import { UserSelectors } from '@osf/core/store/user'; import { IconComponent } from '@osf/shared/components'; -import { CurrentResourceType, ResourceType, ReviewPermissions } from '@osf/shared/enums'; +import { CurrentResourceType, ReviewPermissions } from '@osf/shared/enums'; import { getViewOnlyParam } from '@osf/shared/helpers'; import { WrapFnPipe } from '@osf/shared/pipes'; import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores'; @@ -38,38 +37,10 @@ export class NavMenuComponent { private readonly isAuthenticated = select(UserSelectors.isAuthenticated); private readonly currentResource = select(CurrentResourceSelectors.getCurrentResource); - private readonly currentUserPermissions = select(CurrentResourceSelectors.getCurrentUserPermissions); - private readonly isResourceDetailsLoading = select(CurrentResourceSelectors.isResourceDetailsLoading); private readonly provider = select(ProviderSelectors.getCurrentProvider); readonly actions = createDispatchMap({ getResourceDetails: GetResourceDetails }); - readonly resourceType = computed(() => { - const type = this.currentResource()?.type; - - switch (type) { - case CurrentResourceType.Projects: - return ResourceType.Project; - case CurrentResourceType.Registrations: - return ResourceType.Registration; - case CurrentResourceType.Preprints: - return ResourceType.Preprint; - default: - return ResourceType.Project; - } - }); - - constructor() { - effect(() => { - const resourceId = this.currentResourceId(); - const resourceType = this.resourceType(); - - if (resourceId && resourceType) { - this.actions.getResourceDetails(resourceId, resourceType); - } - }); - } - readonly mainMenuItems = computed(() => { const isAuthenticated = this.isAuthenticated(); const filtered = filterMenuItems(MENU_ITEMS, isAuthenticated); @@ -95,8 +66,7 @@ export class NavMenuComponent { isCollections: this.isCollectionsRoute() || false, currentUrl: this.router.url, isViewOnly: !!getViewOnlyParam(this.router), - permissions: this.currentUserPermissions(), - isResourceDetailsLoading: this.isResourceDetailsLoading(), + permissions: this.currentResource()?.permissions, }; const items = updateMenuItems(filtered, routeContext); @@ -135,7 +105,7 @@ export class NavMenuComponent { }; } - goToLink(item: MenuItem) { + goToLink(item: CustomMenuItem) { if (item.id === 'support' || item.id === 'donate') { window.open(item.url, '_blank'); } @@ -155,6 +125,6 @@ export class NavMenuComponent { } } - readonly hasVisibleChildren = (item: MenuItem): boolean => + readonly hasVisibleChildren = (item: CustomMenuItem): boolean => Array.isArray(item.items) && item.items.some((child) => !!child.visible); } diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index 586341e3a..6f389832e 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -1,5 +1,9 @@ import { MenuItem } from 'primeng/api'; +import { UserPermissions } from '@osf/shared/enums'; + +import { CustomMenuItem } from '../models/custom-menu-item.model'; + export const AUTHENTICATED_MENU_ITEMS: string[] = [ 'my-profile', 'my-resources', @@ -26,7 +30,7 @@ export const VIEW_ONLY_REGISTRY_MENU_ITEMS: string[] = [ 'registration-recent-activity', ]; -export const PROJECT_MENU_ITEMS: MenuItem[] = [ +export const PROJECT_MENU_ITEMS: CustomMenuItem[] = [ { id: 'project-overview', label: 'navigation.overview', @@ -67,6 +71,7 @@ export const PROJECT_MENU_ITEMS: MenuItem[] = [ label: 'navigation.contributors', routerLink: 'contributors', visible: true, + requiredPermission: UserPermissions.Read, routerLinkActiveOptions: { exact: true }, }, { @@ -81,6 +86,7 @@ export const PROJECT_MENU_ITEMS: MenuItem[] = [ label: 'navigation.addons', routerLink: 'addons', visible: true, + requiredPermission: UserPermissions.Write, routerLinkActiveOptions: { exact: true }, }, { @@ -95,6 +101,7 @@ export const PROJECT_MENU_ITEMS: MenuItem[] = [ label: 'navigation.settings', routerLink: 'settings', visible: true, + requiredPermission: UserPermissions.Read, routerLinkActiveOptions: { exact: true }, }, ]; @@ -109,7 +116,7 @@ export const PREPRINT_MENU_ITEMS: MenuItem[] = [ }, ]; -export const REGISTRATION_MENU_ITEMS: MenuItem[] = [ +export const REGISTRATION_MENU_ITEMS: CustomMenuItem[] = [ { id: 'registration-overview', label: 'navigation.overview', @@ -157,6 +164,7 @@ export const REGISTRATION_MENU_ITEMS: MenuItem[] = [ label: 'navigation.contributors', routerLink: 'contributors', visible: true, + requiredPermission: UserPermissions.Read, routerLinkActiveOptions: { exact: true }, }, { @@ -182,7 +190,7 @@ export const REGISTRATION_MENU_ITEMS: MenuItem[] = [ }, ]; -export const MENU_ITEMS: MenuItem[] = [ +export const MENU_ITEMS: CustomMenuItem[] = [ { id: 'home', routerLink: '/', @@ -439,15 +447,3 @@ export const MENU_ITEMS: MenuItem[] = [ styleClass: 'my-5', }, ]; - -export const PROJECT_MENU_PERMISSIONS: Record< - string, - { - requiresWrite?: boolean; - requiresPermissions?: boolean; - } -> = { - 'project-addons': { requiresWrite: true }, - 'project-contributors': { requiresPermissions: true }, - 'project-settings': { requiresPermissions: true }, -}; diff --git a/src/app/core/helpers/nav-menu.helper.ts b/src/app/core/helpers/nav-menu.helper.ts index 49607ae4a..279c15c42 100644 --- a/src/app/core/helpers/nav-menu.helper.ts +++ b/src/app/core/helpers/nav-menu.helper.ts @@ -1,5 +1,3 @@ -import { MenuItem } from 'primeng/api'; - import { UserPermissions } from '@osf/shared/enums'; import { getViewOnlyParamFromUrl } from '@osf/shared/helpers'; @@ -7,38 +5,26 @@ import { AUTHENTICATED_MENU_ITEMS, PREPRINT_MENU_ITEMS, PROJECT_MENU_ITEMS, - PROJECT_MENU_PERMISSIONS, REGISTRATION_MENU_ITEMS, VIEW_ONLY_PROJECT_MENU_ITEMS, VIEW_ONLY_REGISTRY_MENU_ITEMS, } from '../constants'; import { RouteContext } from '../models'; +import { CustomMenuItem } from '../models/custom-menu-item.model'; -function shouldShowMenuItem(menuItemId: string, permissions: string[] | undefined): boolean { - const permissionConfig = PROJECT_MENU_PERMISSIONS[menuItemId]; - - if (!permissionConfig) { +function shouldShowMenuItem(menuItem: CustomMenuItem, permissions: UserPermissions[] | undefined): boolean { + if (!menuItem.requiredPermission) { return true; } - if (permissionConfig.requiresPermissions && (!permissions || !permissions.length)) { - return false; - } - - if (permissionConfig.requiresWrite) { - const hasWritePermission = - permissions?.includes(UserPermissions.Write) || permissions?.includes(UserPermissions.Admin); - return hasWritePermission || false; - } - - return true; + return permissions?.length ? permissions.includes(menuItem.requiredPermission) : false; } -export function filterMenuItems(items: MenuItem[], isAuthenticated: boolean): MenuItem[] { +export function filterMenuItems(items: CustomMenuItem[], isAuthenticated: boolean): CustomMenuItem[] { return items.map((item) => { const isAuthenticatedItem = AUTHENTICATED_MENU_ITEMS.includes(item.id || ''); - let updatedItem: MenuItem = { ...item, visible: isAuthenticatedItem ? isAuthenticated : item.visible }; + let updatedItem: CustomMenuItem = { ...item, visible: isAuthenticatedItem ? isAuthenticated : item.visible }; if (item.id === 'home') { updatedItem = { @@ -64,7 +50,7 @@ export function filterMenuItems(items: MenuItem[], isAuthenticated: boolean): Me }); } -export function updateMenuItems(menuItems: MenuItem[], ctx: RouteContext): MenuItem[] { +export function updateMenuItems(menuItems: CustomMenuItem[], ctx: RouteContext): CustomMenuItem[] { return menuItems.map((item) => { if (item.id === 'my-resources') { return updateMyResourcesMenuItem(item, ctx); @@ -90,7 +76,7 @@ export function updateMenuItems(menuItems: MenuItem[], ctx: RouteContext): MenuI }); } -function updateMyResourcesMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { +function updateMyResourcesMenuItem(item: CustomMenuItem, ctx: RouteContext): CustomMenuItem { const currentUrl = ctx.currentUrl || ''; const isMyResourcesActive = currentUrl.startsWith('/my-projects') || @@ -103,7 +89,7 @@ function updateMyResourcesMenuItem(item: MenuItem, ctx: RouteContext): MenuItem }; } -function updateProjectMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { +function updateProjectMenuItem(item: CustomMenuItem, ctx: RouteContext): CustomMenuItem { const hasProject = ctx.isProject && !!ctx.resourceId; const items = (item.items || []).map((subItem) => { if (subItem.id === 'project-details') { @@ -116,15 +102,15 @@ function updateProjectMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { } menuItems = menuItems.map((menuItem) => { + const isVisible = shouldShowMenuItem(menuItem, ctx.permissions); + if (menuItem.id === 'project-wiki') { return { ...menuItem, - visible: ctx.wikiPageVisible, + visible: ctx.wikiPageVisible && isVisible, }; } - const isVisible = shouldShowMenuItem(menuItem.id || '', ctx.permissions); - return { ...menuItem, visible: isVisible, @@ -134,7 +120,7 @@ function updateProjectMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { return { ...subItem, visible: true, - expanded: !ctx.isResourceDetailsLoading, + expanded: true, items: menuItems.map((menuItem) => ({ ...menuItem, routerLink: [ctx.resourceId as string, menuItem.routerLink], @@ -150,7 +136,7 @@ function updateProjectMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { return { ...item, visible: hasProject, expanded: hasProject, items }; } -function updateRegistryMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { +function updateRegistryMenuItem(item: CustomMenuItem, ctx: RouteContext): CustomMenuItem { const hasRegistry = ctx.isRegistry && !!ctx.resourceId; const items = (item.items || []).map((subItem) => { if (subItem.id === 'registry-details') { @@ -162,6 +148,15 @@ function updateRegistryMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { menuItems = REGISTRATION_MENU_ITEMS.filter((menuItem) => allowedViewOnlyItems.includes(menuItem.id || '')); } + menuItems = menuItems.map((menuItem) => { + const isVisible = shouldShowMenuItem(menuItem, ctx.permissions); + + return { + ...menuItem, + visible: isVisible, + }; + }); + return { ...subItem, visible: true, @@ -192,7 +187,7 @@ function updateRegistryMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { return { ...item, expanded: ctx.isRegistry, items }; } -function updatePreprintMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { +function updatePreprintMenuItem(item: CustomMenuItem, ctx: RouteContext): CustomMenuItem { const hasPreprint = ctx.isPreprint && !!ctx.resourceId; const items = (item.items || []).map((subItem) => { if (subItem.id === 'preprints-details') { @@ -219,7 +214,7 @@ function updatePreprintMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { return { ...item, expanded: ctx.isPreprint, items }; } -function updateCollectionMenuItem(item: MenuItem, ctx: RouteContext): MenuItem { +function updateCollectionMenuItem(item: CustomMenuItem, ctx: RouteContext): CustomMenuItem { const isCollections = ctx.isCollections; const items = (item.items || []).map((subItem) => { diff --git a/src/app/core/models/custom-menu-item.model.ts b/src/app/core/models/custom-menu-item.model.ts new file mode 100644 index 000000000..c8af4ba3e --- /dev/null +++ b/src/app/core/models/custom-menu-item.model.ts @@ -0,0 +1,8 @@ +import { MenuItem } from 'primeng/api'; + +import { UserPermissions } from '@osf/shared/enums'; + +export interface CustomMenuItem extends MenuItem { + requiredPermission?: UserPermissions; + items?: CustomMenuItem[]; +} diff --git a/src/app/core/models/index.ts b/src/app/core/models/index.ts index 746090022..86f9cf210 100644 --- a/src/app/core/models/index.ts +++ b/src/app/core/models/index.ts @@ -1,3 +1,4 @@ +export * from './custom-menu-item.model'; export * from './route-context.model'; export * from './route-data.model'; export * from './sign-up.model'; diff --git a/src/app/core/models/route-context.model.ts b/src/app/core/models/route-context.model.ts index 395a5ef37..456173ee2 100644 --- a/src/app/core/models/route-context.model.ts +++ b/src/app/core/models/route-context.model.ts @@ -1,3 +1,5 @@ +import { UserPermissions } from '@osf/shared/enums'; + export interface RouteContext { resourceId: string | undefined; providerId?: string; @@ -11,6 +13,5 @@ export interface RouteContext { isCollections: boolean; currentUrl?: string; isViewOnly?: boolean; - permissions?: string[]; - isResourceDetailsLoading?: boolean; + permissions?: UserPermissions[]; } diff --git a/src/app/shared/models/guid-response-json-api.model.ts b/src/app/shared/models/guid-response-json-api.model.ts index b7acd9fda..6e0d26f71 100644 --- a/src/app/shared/models/guid-response-json-api.model.ts +++ b/src/app/shared/models/guid-response-json-api.model.ts @@ -10,7 +10,7 @@ interface GuidDataJsonApi { attributes: { guid: string; wiki_enabled: boolean; - permissions: UserPermissions[]; + current_user_permissions: UserPermissions[]; }; relationships: { target?: { diff --git a/src/app/shared/services/resource.service.ts b/src/app/shared/services/resource.service.ts index b8685244a..ff77bad0c 100644 --- a/src/app/shared/services/resource.service.ts +++ b/src/app/shared/services/resource.service.ts @@ -56,7 +56,7 @@ export class ResourceGuidService { ? res.data.relationships.provider?.data.type : res.data.relationships.target?.data.type, wikiEnabled: res.data.attributes.wiki_enabled, - permissions: res.data.attributes.permissions, + permissions: res.data.attributes.current_user_permissions, }) as CurrentResource ), finalize(() => this.loaderService.hide())