Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 6 additions & 36 deletions src/app/core/components/nav-menu/nav-menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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');
}
Expand All @@ -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);
}
26 changes: 11 additions & 15 deletions src/app/core/constants/nav-items.constant.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -67,6 +71,7 @@ export const PROJECT_MENU_ITEMS: MenuItem[] = [
label: 'navigation.contributors',
routerLink: 'contributors',
visible: true,
requiredPermission: UserPermissions.Read,
routerLinkActiveOptions: { exact: true },
},
{
Expand All @@ -81,6 +86,7 @@ export const PROJECT_MENU_ITEMS: MenuItem[] = [
label: 'navigation.addons',
routerLink: 'addons',
visible: true,
requiredPermission: UserPermissions.Write,
routerLinkActiveOptions: { exact: true },
},
{
Expand All @@ -95,6 +101,7 @@ export const PROJECT_MENU_ITEMS: MenuItem[] = [
label: 'navigation.settings',
routerLink: 'settings',
visible: true,
requiredPermission: UserPermissions.Read,
routerLinkActiveOptions: { exact: true },
},
];
Expand All @@ -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',
Expand Down Expand Up @@ -157,6 +164,7 @@ export const REGISTRATION_MENU_ITEMS: MenuItem[] = [
label: 'navigation.contributors',
routerLink: 'contributors',
visible: true,
requiredPermission: UserPermissions.Read,
routerLinkActiveOptions: { exact: true },
},
{
Expand All @@ -182,7 +190,7 @@ export const REGISTRATION_MENU_ITEMS: MenuItem[] = [
},
];

export const MENU_ITEMS: MenuItem[] = [
export const MENU_ITEMS: CustomMenuItem[] = [
{
id: 'home',
routerLink: '/',
Expand Down Expand Up @@ -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 },
};
55 changes: 25 additions & 30 deletions src/app/core/helpers/nav-menu.helper.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,30 @@
import { MenuItem } from 'primeng/api';

import { UserPermissions } from '@osf/shared/enums';
import { getViewOnlyParamFromUrl } from '@osf/shared/helpers';

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 = {
Expand All @@ -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);
Expand All @@ -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') ||
Expand All @@ -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') {
Expand All @@ -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,
Expand All @@ -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],
Expand All @@ -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') {
Expand All @@ -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,
Expand Down Expand Up @@ -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') {
Expand All @@ -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) => {
Expand Down
8 changes: 8 additions & 0 deletions src/app/core/models/custom-menu-item.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { MenuItem } from 'primeng/api';

import { UserPermissions } from '@osf/shared/enums';

export interface CustomMenuItem extends MenuItem {
requiredPermission?: UserPermissions;
items?: CustomMenuItem[];
}
1 change: 1 addition & 0 deletions src/app/core/models/index.ts
Original file line number Diff line number Diff line change
@@ -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';
5 changes: 3 additions & 2 deletions src/app/core/models/route-context.model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UserPermissions } from '@osf/shared/enums';

export interface RouteContext {
resourceId: string | undefined;
providerId?: string;
Expand All @@ -11,6 +13,5 @@ export interface RouteContext {
isCollections: boolean;
currentUrl?: string;
isViewOnly?: boolean;
permissions?: string[];
isResourceDetailsLoading?: boolean;
permissions?: UserPermissions[];
}
2 changes: 1 addition & 1 deletion src/app/shared/models/guid-response-json-api.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface GuidDataJsonApi {
attributes: {
guid: string;
wiki_enabled: boolean;
permissions: UserPermissions[];
current_user_permissions: UserPermissions[];
};
relationships: {
target?: {
Expand Down
2 changes: 1 addition & 1 deletion src/app/shared/services/resource.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading