Skip to content

Commit 5f5bf93

Browse files
authored
Fix(ang-403): Permissions (#496)
* fix(ang-403): added wiki permissions * fix(ang-403): fixed minor bug * fix(ang-403): added permissions for the nav menu
1 parent 1488d8e commit 5f5bf93

24 files changed

+217
-97
lines changed

src/app/core/components/nav-menu/nav-menu.component.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { select } from '@ngxs/store';
1+
import { createDispatchMap, select } from '@ngxs/store';
22

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

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

88
import { filter, map } from 'rxjs';
99

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

@@ -18,10 +18,10 @@ import { RouteContext } from '@osf/core/models';
1818
import { AuthService } from '@osf/core/services';
1919
import { UserSelectors } from '@osf/core/store/user';
2020
import { IconComponent } from '@osf/shared/components';
21-
import { CurrentResourceType, ReviewPermissions } from '@osf/shared/enums';
21+
import { CurrentResourceType, ResourceType, ReviewPermissions } from '@osf/shared/enums';
2222
import { getViewOnlyParam } from '@osf/shared/helpers';
2323
import { WrapFnPipe } from '@osf/shared/pipes';
24-
import { CurrentResourceSelectors } from '@osf/shared/stores';
24+
import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores';
2525

2626
@Component({
2727
selector: 'osf-nav-menu',
@@ -38,8 +38,38 @@ export class NavMenuComponent {
3838

3939
private readonly isAuthenticated = select(UserSelectors.isAuthenticated);
4040
private readonly currentResource = select(CurrentResourceSelectors.getCurrentResource);
41+
private readonly currentUserPermissions = select(CurrentResourceSelectors.getCurrentUserPermissions);
42+
private readonly isResourceDetailsLoading = select(CurrentResourceSelectors.isResourceDetailsLoading);
4143
private readonly provider = select(ProviderSelectors.getCurrentProvider);
4244

45+
readonly actions = createDispatchMap({ getResourceDetails: GetResourceDetails });
46+
47+
readonly resourceType = computed(() => {
48+
const type = this.currentResource()?.type;
49+
50+
switch (type) {
51+
case CurrentResourceType.Projects:
52+
return ResourceType.Project;
53+
case CurrentResourceType.Registrations:
54+
return ResourceType.Registration;
55+
case CurrentResourceType.Preprints:
56+
return ResourceType.Preprint;
57+
default:
58+
return ResourceType.Project;
59+
}
60+
});
61+
62+
constructor() {
63+
effect(() => {
64+
const resourceId = this.currentResourceId();
65+
const resourceType = this.resourceType();
66+
67+
if (resourceId && resourceType) {
68+
this.actions.getResourceDetails(resourceId, resourceType);
69+
}
70+
});
71+
}
72+
4373
readonly mainMenuItems = computed(() => {
4474
const isAuthenticated = this.isAuthenticated();
4575
const filtered = filterMenuItems(MENU_ITEMS, isAuthenticated);
@@ -65,7 +95,8 @@ export class NavMenuComponent {
6595
isCollections: this.isCollectionsRoute() || false,
6696
currentUrl: this.router.url,
6797
isViewOnly: !!getViewOnlyParam(this.router),
68-
permissions: this.currentResource()?.permissions,
98+
permissions: this.currentUserPermissions(),
99+
isResourceDetailsLoading: this.isResourceDetailsLoading(),
69100
};
70101

71102
const items = updateMenuItems(filtered, routeContext);

src/app/core/constants/nav-items.constant.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const VIEW_ONLY_PROJECT_MENU_ITEMS: string[] = [
1414
'project-files',
1515
'project-wiki',
1616
'project-analytics',
17+
'project-links',
1718
];
1819

1920
export const VIEW_ONLY_REGISTRY_MENU_ITEMS: string[] = [
@@ -22,6 +23,7 @@ export const VIEW_ONLY_REGISTRY_MENU_ITEMS: string[] = [
2223
'registration-wiki',
2324
'registration-analytics',
2425
'registration-components',
26+
'registration-recent-activity',
2527
];
2628

2729
export const PROJECT_MENU_ITEMS: MenuItem[] = [
@@ -437,3 +439,15 @@ export const MENU_ITEMS: MenuItem[] = [
437439
styleClass: 'my-5',
438440
},
439441
];
442+
443+
export const PROJECT_MENU_PERMISSIONS: Record<
444+
string,
445+
{
446+
requiresWrite?: boolean;
447+
requiresPermissions?: boolean;
448+
}
449+
> = {
450+
'project-addons': { requiresWrite: true },
451+
'project-contributors': { requiresPermissions: true },
452+
'project-settings': { requiresPermissions: true },
453+
};

src/app/core/helpers/nav-menu.helper.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
import { MenuItem } from 'primeng/api';
22

3+
import { UserPermissions } from '@osf/shared/enums';
34
import { getViewOnlyParamFromUrl } from '@osf/shared/helpers';
45

56
import {
67
AUTHENTICATED_MENU_ITEMS,
78
PREPRINT_MENU_ITEMS,
89
PROJECT_MENU_ITEMS,
10+
PROJECT_MENU_PERMISSIONS,
911
REGISTRATION_MENU_ITEMS,
1012
VIEW_ONLY_PROJECT_MENU_ITEMS,
1113
VIEW_ONLY_REGISTRY_MENU_ITEMS,
1214
} from '../constants';
1315
import { RouteContext } from '../models';
1416

17+
function shouldShowMenuItem(menuItemId: string, permissions: string[] | undefined): boolean {
18+
const permissionConfig = PROJECT_MENU_PERMISSIONS[menuItemId];
19+
20+
if (!permissionConfig) {
21+
return true;
22+
}
23+
24+
if (permissionConfig.requiresPermissions && (!permissions || !permissions.length)) {
25+
return false;
26+
}
27+
28+
if (permissionConfig.requiresWrite) {
29+
const hasWritePermission =
30+
permissions?.includes(UserPermissions.Write) || permissions?.includes(UserPermissions.Admin);
31+
return hasWritePermission || false;
32+
}
33+
34+
return true;
35+
}
36+
1537
export function filterMenuItems(items: MenuItem[], isAuthenticated: boolean): MenuItem[] {
1638
return items.map((item) => {
1739
const isAuthenticatedItem = AUTHENTICATED_MENU_ITEMS.includes(item.id || '');
@@ -101,13 +123,18 @@ function updateProjectMenuItem(item: MenuItem, ctx: RouteContext): MenuItem {
101123
};
102124
}
103125

104-
return menuItem;
126+
const isVisible = shouldShowMenuItem(menuItem.id || '', ctx.permissions);
127+
128+
return {
129+
...menuItem,
130+
visible: isVisible,
131+
};
105132
});
106133

107134
return {
108135
...subItem,
109136
visible: true,
110-
expanded: true,
137+
expanded: !ctx.isResourceDetailsLoading,
111138
items: menuItems.map((menuItem) => ({
112139
...menuItem,
113140
routerLink: [ctx.resourceId as string, menuItem.routerLink],

src/app/core/models/route-context.model.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { UserPermissions } from '@osf/shared/enums';
2-
31
export interface RouteContext {
42
resourceId: string | undefined;
53
providerId?: string;
@@ -13,5 +11,6 @@ export interface RouteContext {
1311
isCollections: boolean;
1412
currentUrl?: string;
1513
isViewOnly?: boolean;
16-
permissions?: UserPermissions[];
14+
permissions?: string[];
15+
isResourceDetailsLoading?: boolean;
1716
}

src/app/features/project/linked-services/linked-services.component.html

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@
4141
<p>
4242
{{ 'project.linkedServices.noLinkedServices' | translate }}
4343
</p>
44-
<p>
45-
{{ 'project.linkedServices.redirectMessage' | translate }}
46-
<a routerLink="../addons">
47-
{{ 'project.linkedServices.addonsLink' | translate }}
48-
</a>
49-
{{ 'project.linkedServices.redirectMessageSuffix' | translate }}
50-
</p>
44+
@if (canManageAddons()) {
45+
<p>
46+
{{ 'project.linkedServices.redirectMessage' | translate }}
47+
<a routerLink="../addons">
48+
{{ 'project.linkedServices.addonsLink' | translate }}
49+
</a>
50+
{{ 'project.linkedServices.redirectMessageSuffix' | translate }}
51+
</p>
52+
}
5153
</div>
5254
}
5355
</section>

src/app/features/project/linked-services/linked-services.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'
1212
import { AddonServiceNames } from '@shared/enums';
1313
import { convertCamelCaseToNormal } from '@shared/helpers';
1414
import { AddonsSelectors, GetAddonsResourceReference, GetConfiguredLinkAddons } from '@shared/stores';
15+
import { CurrentResourceSelectors } from '@shared/stores/current-resource';
1516

1617
@Component({
1718
selector: 'osf-linked-services',
@@ -29,6 +30,8 @@ export class LinkedServicesComponent implements OnInit {
2930
isResourceReferenceLoading = select(AddonsSelectors.getAddonsResourceReferenceLoading);
3031
isConfiguredLinkAddonsLoading = select(AddonsSelectors.getConfiguredLinkAddonsLoading);
3132
isCurrentUserLoading = select(UserSelectors.getCurrentUserLoading);
33+
hasWriteAccess = select(CurrentResourceSelectors.hasWriteAccess);
34+
hasAdminAccess = select(CurrentResourceSelectors.hasAdminAccess);
3235

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

51+
canManageAddons = computed(() => {
52+
return this.hasWriteAccess() || this.hasAdminAccess();
53+
});
54+
4855
actions = createDispatchMap({
4956
getConfiguredLinkAddons: GetConfiguredLinkAddons,
5057
getAddonsResourceReference: GetAddonsResourceReference,

src/app/features/project/overview/components/linked-resources/linked-resources.component.html

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,19 @@ <h2 class="flex align-items-center gap-2">
2121
<osf-icon [iconClass]="linkedResource.public ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
2222
<a class="linked-project-title" [href]="linkedResource.id">{{ linkedResource.title }}</a>
2323
</h2>
24-
25-
<div>
26-
<p-button
27-
class="danger-icon-btn"
28-
icon="fas fa-trash"
29-
severity="danger"
30-
text
31-
[ariaLabel]="'common.buttons.delete' | translate"
32-
(onClick)="openDeleteResourceModal(linkedResource.id)"
33-
>
34-
</p-button>
35-
</div>
24+
@if (canEdit()) {
25+
<div>
26+
<p-button
27+
class="danger-icon-btn"
28+
icon="fas fa-trash"
29+
severity="danger"
30+
text
31+
[ariaLabel]="'common.buttons.delete' | translate"
32+
(onClick)="openDeleteResourceModal(linkedResource.id)"
33+
>
34+
</p-button>
35+
</div>
36+
}
3637
</div>
3738

3839
<div class="component-name flex flex-wrap gap-1">

src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
</div>
2424
}
2525

26-
@if (isCollectionsRoute() || hasViewOnly()) {
26+
@if (isCollectionsRoute() || hasViewOnly() || !canEdit()) {
2727
@if (isPublic()) {
2828
<div class="flex gap-2">
2929
<i class="fas fa-lock-open"></i>

src/app/features/project/overview/project-overview.component.html

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
[isCollectionsRoute]="isCollectionsRoute()"
1818
[currentResource]="currentResource()"
1919
[projectDescription]="project.description"
20-
[canEdit]="isAdmin()"
20+
[canEdit]="hasAdminAccess()"
2121
/>
2222
</div>
2323

@@ -47,7 +47,7 @@
4747
}
4848

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

@@ -57,8 +57,11 @@
5757
[areComponentsLoading]="areComponentsLoading()"
5858
/>
5959

60-
<osf-project-components [canEdit]="canWrite() && !isCollectionsRoute()" />
61-
<osf-linked-resources [canEdit]="canWrite() && !isCollectionsRoute()" />
60+
@if (!hasViewOnly()) {
61+
<osf-project-components [canEdit]="hasWriteAccess() && !isCollectionsRoute()" />
62+
<osf-linked-resources [canEdit]="hasWriteAccess() && !isCollectionsRoute()" />
63+
}
64+
6265
<osf-recent-activity-list [pageSize]="activityPageSize" />
6366
</div>
6467

@@ -67,7 +70,8 @@
6770
[currentResource]="resourceOverview()"
6871
(customCitationUpdated)="onCustomCitationUpdated($event)"
6972
[isCollectionsRoute]="isCollectionsRoute()"
70-
[canEdit]="canWrite()"
73+
[canEdit]="hasAdminAccess()"
74+
[showEditButton]="hasWriteAccess()"
7175
/>
7276
</div>
7377
</div>

src/app/features/project/overview/project-overview.component.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
CollectionsModerationSelectors,
3232
GetSubmissionsReviewActions,
3333
} from '@osf/features/moderation/store/collections-moderation';
34-
import { Mode, ResourceType, UserPermissions } from '@osf/shared/enums';
34+
import { Mode, ResourceType } from '@osf/shared/enums';
3535
import { hasViewOnlyParam, IS_XSMALL } from '@osf/shared/helpers';
3636
import { MapProjectOverview } from '@osf/shared/mappers';
3737
import { MetaTagsService, ToastService } from '@osf/shared/services';
@@ -129,6 +129,8 @@ export class ProjectOverviewComponent implements OnInit {
129129
areSubjectsLoading = select(SubjectsSelectors.areSelectedSubjectsLoading);
130130
currentProject = select(ProjectOverviewSelectors.getProject);
131131
isAnonymous = select(ProjectOverviewSelectors.isProjectAnonymous);
132+
hasWriteAccess = select(ProjectOverviewSelectors.hasWriteAccess);
133+
hasAdminAccess = select(ProjectOverviewSelectors.hasAdminAccess);
132134

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

178-
isAdmin = computed(() => {
179-
return this.userPermissions().includes(UserPermissions.Admin);
180-
});
181-
182-
canWrite = computed(() => {
183-
return this.userPermissions().includes(UserPermissions.Write);
184-
});
185-
186180
resourceOverview = computed(() => {
187181
const project = this.currentProject();
188182
const subjects = this.subjects();

0 commit comments

Comments
 (0)