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
41 changes: 36 additions & 5 deletions src/app/core/components/nav-menu/nav-menu.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { select } from '@ngxs/store';
import { createDispatchMap, select } from '@ngxs/store';

import { TranslatePipe } from '@ngx-translate/core';

Expand All @@ -7,7 +7,7 @@ import { PanelMenuModule } from 'primeng/panelmenu';

import { filter, map } from 'rxjs';

import { Component, computed, inject, output } from '@angular/core';
import { Component, computed, effect, inject, output } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router';

Expand All @@ -18,10 +18,10 @@ import { 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, ReviewPermissions } from '@osf/shared/enums';
import { CurrentResourceType, ResourceType, ReviewPermissions } from '@osf/shared/enums';
import { getViewOnlyParam } from '@osf/shared/helpers';
import { WrapFnPipe } from '@osf/shared/pipes';
import { CurrentResourceSelectors } from '@osf/shared/stores';
import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores';

@Component({
selector: 'osf-nav-menu',
Expand All @@ -38,8 +38,38 @@ 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 @@ -65,7 +95,8 @@ export class NavMenuComponent {
isCollections: this.isCollectionsRoute() || false,
currentUrl: this.router.url,
isViewOnly: !!getViewOnlyParam(this.router),
permissions: this.currentResource()?.permissions,
permissions: this.currentUserPermissions(),
isResourceDetailsLoading: this.isResourceDetailsLoading(),
};

const items = updateMenuItems(filtered, routeContext);
Expand Down
14 changes: 14 additions & 0 deletions src/app/core/constants/nav-items.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const VIEW_ONLY_PROJECT_MENU_ITEMS: string[] = [
'project-files',
'project-wiki',
'project-analytics',
'project-links',
];

export const VIEW_ONLY_REGISTRY_MENU_ITEMS: string[] = [
Expand All @@ -22,6 +23,7 @@ export const VIEW_ONLY_REGISTRY_MENU_ITEMS: string[] = [
'registration-wiki',
'registration-analytics',
'registration-components',
'registration-recent-activity',
];

export const PROJECT_MENU_ITEMS: MenuItem[] = [
Expand Down Expand Up @@ -437,3 +439,15 @@ 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 },
};
31 changes: 29 additions & 2 deletions src/app/core/helpers/nav-menu.helper.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
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';

function shouldShowMenuItem(menuItemId: string, permissions: string[] | undefined): boolean {
const permissionConfig = PROJECT_MENU_PERMISSIONS[menuItemId];

if (!permissionConfig) {
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;
}

export function filterMenuItems(items: MenuItem[], isAuthenticated: boolean): MenuItem[] {
return items.map((item) => {
const isAuthenticatedItem = AUTHENTICATED_MENU_ITEMS.includes(item.id || '');
Expand Down Expand Up @@ -101,13 +123,18 @@ function updateProjectMenuItem(item: MenuItem, ctx: RouteContext): MenuItem {
};
}

return menuItem;
const isVisible = shouldShowMenuItem(menuItem.id || '', ctx.permissions);

return {
...menuItem,
visible: isVisible,
};
});

return {
...subItem,
visible: true,
expanded: true,
expanded: !ctx.isResourceDetailsLoading,
items: menuItems.map((menuItem) => ({
...menuItem,
routerLink: [ctx.resourceId as string, menuItem.routerLink],
Expand Down
5 changes: 2 additions & 3 deletions src/app/core/models/route-context.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { UserPermissions } from '@osf/shared/enums';

export interface RouteContext {
resourceId: string | undefined;
providerId?: string;
Expand All @@ -13,5 +11,6 @@ export interface RouteContext {
isCollections: boolean;
currentUrl?: string;
isViewOnly?: boolean;
permissions?: UserPermissions[];
permissions?: string[];
isResourceDetailsLoading?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@
<p>
{{ 'project.linkedServices.noLinkedServices' | translate }}
</p>
<p>
{{ 'project.linkedServices.redirectMessage' | translate }}
<a routerLink="../addons">
{{ 'project.linkedServices.addonsLink' | translate }}
</a>
{{ 'project.linkedServices.redirectMessageSuffix' | translate }}
</p>
@if (canManageAddons()) {
<p>
{{ 'project.linkedServices.redirectMessage' | translate }}
<a routerLink="../addons">
{{ 'project.linkedServices.addonsLink' | translate }}
</a>
{{ 'project.linkedServices.redirectMessageSuffix' | translate }}
</p>
}
</div>
}
</section>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'
import { AddonServiceNames } from '@shared/enums';
import { convertCamelCaseToNormal } from '@shared/helpers';
import { AddonsSelectors, GetAddonsResourceReference, GetConfiguredLinkAddons } from '@shared/stores';
import { CurrentResourceSelectors } from '@shared/stores/current-resource';

@Component({
selector: 'osf-linked-services',
Expand All @@ -29,6 +30,8 @@ export class LinkedServicesComponent implements OnInit {
isResourceReferenceLoading = select(AddonsSelectors.getAddonsResourceReferenceLoading);
isConfiguredLinkAddonsLoading = select(AddonsSelectors.getConfiguredLinkAddonsLoading);
isCurrentUserLoading = select(UserSelectors.getCurrentUserLoading);
hasWriteAccess = select(CurrentResourceSelectors.hasWriteAccess);
hasAdminAccess = select(CurrentResourceSelectors.hasAdminAccess);

isLoading = computed(() => {
return this.isConfiguredLinkAddonsLoading() || this.isResourceReferenceLoading() || this.isCurrentUserLoading();
Expand All @@ -45,6 +48,10 @@ export class LinkedServicesComponent implements OnInit {
}));
});

canManageAddons = computed(() => {
return this.hasWriteAccess() || this.hasAdminAccess();
});

actions = createDispatchMap({
getConfiguredLinkAddons: GetConfiguredLinkAddons,
getAddonsResourceReference: GetAddonsResourceReference,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,19 @@ <h2 class="flex align-items-center gap-2">
<osf-icon [iconClass]="linkedResource.public ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
<a class="linked-project-title" [href]="linkedResource.id">{{ linkedResource.title }}</a>
</h2>

<div>
<p-button
class="danger-icon-btn"
icon="fas fa-trash"
severity="danger"
text
[ariaLabel]="'common.buttons.delete' | translate"
(onClick)="openDeleteResourceModal(linkedResource.id)"
>
</p-button>
</div>
@if (canEdit()) {
<div>
<p-button
class="danger-icon-btn"
icon="fas fa-trash"
severity="danger"
text
[ariaLabel]="'common.buttons.delete' | translate"
(onClick)="openDeleteResourceModal(linkedResource.id)"
>
</p-button>
</div>
}
</div>

<div class="component-name flex flex-wrap gap-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</div>
}

@if (isCollectionsRoute() || hasViewOnly()) {
@if (isCollectionsRoute() || hasViewOnly() || !canEdit()) {
@if (isPublic()) {
<div class="flex gap-2">
<i class="fas fa-lock-open"></i>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
[isCollectionsRoute]="isCollectionsRoute()"
[currentResource]="currentResource()"
[projectDescription]="project.description"
[canEdit]="isAdmin()"
[canEdit]="hasAdminAccess()"
/>
</div>

Expand Down Expand Up @@ -47,7 +47,7 @@
}

<div class="flex flex-column gap-4 left-section">
@if (canWrite()) {
@if (hasWriteAccess()) {
<osf-overview-wiki [resourceId]="currentProject()!.id" />
}

Expand All @@ -57,8 +57,11 @@
[areComponentsLoading]="areComponentsLoading()"
/>

<osf-project-components [canEdit]="canWrite() && !isCollectionsRoute()" />
<osf-linked-resources [canEdit]="canWrite() && !isCollectionsRoute()" />
@if (!hasViewOnly()) {
<osf-project-components [canEdit]="hasWriteAccess() && !isCollectionsRoute()" />
<osf-linked-resources [canEdit]="hasWriteAccess() && !isCollectionsRoute()" />
}

<osf-recent-activity-list [pageSize]="activityPageSize" />
</div>

Expand All @@ -67,7 +70,8 @@
[currentResource]="resourceOverview()"
(customCitationUpdated)="onCustomCitationUpdated($event)"
[isCollectionsRoute]="isCollectionsRoute()"
[canEdit]="canWrite()"
[canEdit]="hasAdminAccess()"
[showEditButton]="hasWriteAccess()"
/>
</div>
</div>
Expand Down
12 changes: 3 additions & 9 deletions src/app/features/project/overview/project-overview.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
CollectionsModerationSelectors,
GetSubmissionsReviewActions,
} from '@osf/features/moderation/store/collections-moderation';
import { Mode, ResourceType, UserPermissions } from '@osf/shared/enums';
import { Mode, ResourceType } from '@osf/shared/enums';
import { hasViewOnlyParam, IS_XSMALL } from '@osf/shared/helpers';
import { MapProjectOverview } from '@osf/shared/mappers';
import { MetaTagsService, ToastService } from '@osf/shared/services';
Expand Down Expand Up @@ -129,6 +129,8 @@ export class ProjectOverviewComponent implements OnInit {
areSubjectsLoading = select(SubjectsSelectors.areSelectedSubjectsLoading);
currentProject = select(ProjectOverviewSelectors.getProject);
isAnonymous = select(ProjectOverviewSelectors.isProjectAnonymous);
hasWriteAccess = select(ProjectOverviewSelectors.hasWriteAccess);
hasAdminAccess = select(ProjectOverviewSelectors.hasAdminAccess);

private readonly actions = createDispatchMap({
getProject: GetProjectById,
Expand Down Expand Up @@ -175,14 +177,6 @@ export class ProjectOverviewComponent implements OnInit {
userPermissions = computed(() => this.currentProject()?.currentUserPermissions || []);
hasViewOnly = computed(() => hasViewOnlyParam(this.router));

isAdmin = computed(() => {
return this.userPermissions().includes(UserPermissions.Admin);
});

canWrite = computed(() => {
return this.userPermissions().includes(UserPermissions.Write);
});

resourceOverview = computed(() => {
const project = this.currentProject();
const subjects = this.subjects();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Selector } from '@ngxs/store';

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

import { ProjectOverviewStateModel } from './project-overview.model';
import { ProjectOverviewState } from './project-overview.state';

Expand Down Expand Up @@ -53,4 +55,19 @@ export class ProjectOverviewSelectors {
static getDuplicatedProject(state: ProjectOverviewStateModel) {
return state.duplicatedProject;
}

@Selector([ProjectOverviewState])
static hasWriteAccess(state: ProjectOverviewStateModel): boolean {
return state.project.data?.currentUserPermissions.includes(UserPermissions.Write) || false;
}

@Selector([ProjectOverviewState])
static hasAdminAccess(state: ProjectOverviewStateModel): boolean {
return state.project.data?.currentUserPermissions.includes(UserPermissions.Admin) || false;
}

@Selector([ProjectOverviewState])
static hasNoPermissions(state: ProjectOverviewStateModel): boolean {
return !state.project.data?.currentUserPermissions.length;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<osf-sub-header
[showButton]="true"
[showButton]="hasAdminAccess()"
[buttonLabel]="'project.registrations.addRegistration' | translate"
[title]="'navigation.registrations' | translate"
(buttonClick)="addRegistration()"
Expand Down
Loading
Loading