From c6a16f2286040f58cee1ced2509baa1e0b404035 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Fri, 26 Sep 2025 18:50:37 +0300 Subject: [PATCH 1/3] fix(ang-403): added wiki permissions --- src/app/core/constants/nav-items.constant.ts | 1 + .../linked-resources.component.html | 25 ++++---- .../overview-toolbar.component.html | 2 +- .../overview/project-overview.component.html | 14 ++-- .../overview/project-overview.component.ts | 12 +--- .../store/project-overview.selectors.ts | 17 +++++ .../registrations.component.html | 2 +- .../registrations/registrations.component.ts | 7 +- .../features/project/wiki/wiki.component.html | 6 +- .../features/project/wiki/wiki.component.ts | 9 ++- .../registry-overview.component.html | 1 + .../registry-wiki.component.html | 1 - .../resource-citations.component.html | 64 +++++++++---------- .../resource-metadata.component.html | 2 +- .../resource-metadata.component.ts | 1 + .../wiki/wiki-list/wiki-list.component.html | 7 +- .../wiki/wiki-list/wiki-list.component.ts | 2 +- .../mappers/components/components.mapper.ts | 14 ++-- .../registration/registration.mapper.ts | 4 +- .../current-resource.selectors.ts | 16 +++++ 20 files changed, 124 insertions(+), 83 deletions(-) diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index b5b1d2fc4..cfdc5b390 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -22,6 +22,7 @@ export const VIEW_ONLY_REGISTRY_MENU_ITEMS: string[] = [ 'registration-wiki', 'registration-analytics', 'registration-components', + 'registration-recent-activity', ]; export const PROJECT_MENU_ITEMS: MenuItem[] = [ diff --git a/src/app/features/project/overview/components/linked-resources/linked-resources.component.html b/src/app/features/project/overview/components/linked-resources/linked-resources.component.html index 676352d56..55736a974 100644 --- a/src/app/features/project/overview/components/linked-resources/linked-resources.component.html +++ b/src/app/features/project/overview/components/linked-resources/linked-resources.component.html @@ -21,18 +21,19 @@

{{ linkedResource.title }}

- -
- - -
+ @if (canEdit()) { +
+ + +
+ }
diff --git a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html index f67364529..5c702e0fe 100644 --- a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html +++ b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html @@ -23,7 +23,7 @@
} - @if (isCollectionsRoute() || hasViewOnly()) { + @if (isCollectionsRoute() || hasViewOnly() || !canEdit()) { @if (isPublic()) {
diff --git a/src/app/features/project/overview/project-overview.component.html b/src/app/features/project/overview/project-overview.component.html index 238841f18..0800aad51 100644 --- a/src/app/features/project/overview/project-overview.component.html +++ b/src/app/features/project/overview/project-overview.component.html @@ -17,7 +17,7 @@ [isCollectionsRoute]="isCollectionsRoute()" [currentResource]="currentResource()" [projectDescription]="project.description" - [canEdit]="isAdmin()" + [canEdit]="hasAdminAccess()" />
@@ -47,7 +47,7 @@ }
- @if (canWrite()) { + @if (hasWriteAccess()) { } @@ -57,8 +57,11 @@ [areComponentsLoading]="areComponentsLoading()" /> - - + @if (!hasViewOnly()) { + + + } +
@@ -67,7 +70,8 @@ [currentResource]="resourceOverview()" (customCitationUpdated)="onCustomCitationUpdated($event)" [isCollectionsRoute]="isCollectionsRoute()" - [canEdit]="canWrite()" + [canEdit]="hasAdminAccess()" + [showEditButton]="hasWriteAccess()" /> diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index 368c9a877..53d447925 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -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'; @@ -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, @@ -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(); diff --git a/src/app/features/project/overview/store/project-overview.selectors.ts b/src/app/features/project/overview/store/project-overview.selectors.ts index f8e8e52e6..7b85f5c94 100644 --- a/src/app/features/project/overview/store/project-overview.selectors.ts +++ b/src/app/features/project/overview/store/project-overview.selectors.ts @@ -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'; @@ -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; + } } diff --git a/src/app/features/project/registrations/registrations.component.html b/src/app/features/project/registrations/registrations.component.html index a9bd9b0ad..34300b4eb 100644 --- a/src/app/features/project/registrations/registrations.component.html +++ b/src/app/features/project/registrations/registrations.component.html @@ -1,5 +1,5 @@ params['id'])) ?? of(undefined)); registrations = select(RegistrationsSelectors.getRegistrations); registrationsTotalCount = select(RegistrationsSelectors.getRegistrationsTotalCount); isRegistrationsLoading = select(RegistrationsSelectors.isRegistrationsLoading); - actions = createDispatchMap({ getRegistrations: GetRegistrations }); + actions = createDispatchMap({ getRegistrations: GetRegistrations, getResourceDetails: GetResourceDetails }); itemsPerPage = 10; first = 0; ngOnInit(): void { + this.actions.getResourceDetails(this.projectId(), ResourceType.Project); this.actions.getRegistrations(this.projectId(), 1, this.itemsPerPage); } diff --git a/src/app/features/project/wiki/wiki.component.html b/src/app/features/project/wiki/wiki.component.html index 99de7c58a..4fe159c43 100644 --- a/src/app/features/project/wiki/wiki.component.html +++ b/src/app/features/project/wiki/wiki.component.html @@ -5,7 +5,7 @@ [variant]="wikiModes().view ? undefined : 'outlined'" (onClick)="toggleMode(WikiModes.View)" /> - @if (!hasViewOnly()) { + @if (hasWriteAccess()) { @@ -46,7 +46,7 @@ (selectVersion)="onSelectVersion($event)" > } - @if (!hasViewOnly() && wikiModes().edit) { + @if (hasWriteAccess() && wikiModes().edit) { hasViewOnlyParam(this.router)); + hasWriteAccess = select(CurrentResourceSelectors.hasWriteAccess); + hasAdminAccess = select(CurrentResourceSelectors.hasAdminAccess); + actions = createDispatchMap({ getWikiModes: GetWikiModes, toggleMode: ToggleMode, @@ -95,11 +100,13 @@ export class WikiComponent { createWikiVersion: CreateWikiVersion, getWikiVersionContent: GetWikiVersionContent, getCompareVersionContent: GetCompareVersionContent, + getResourceDetails: GetResourceDetails, }); wikiIdFromQueryParams = this.route.snapshot.queryParams['wiki']; constructor() { + this.actions.getResourceDetails(this.projectId(), ResourceType.Project); this.actions .getWikiList(ResourceType.Project, this.projectId()) .pipe( @@ -108,7 +115,7 @@ export class WikiComponent { if (!this.wikiIdFromQueryParams) { this.navigateToWiki(this.wikiList()?.[0]?.id || ''); } - if (!this.wikiList()?.length) { + if (!this.wikiList()?.length && this.hasWriteAccess()) { this.actions.createWiki(ResourceType.Project, this.projectId(), this.homeWikiName); } }) diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.html b/src/app/features/registry/pages/registry-overview/registry-overview.component.html index d0ec326a6..12cf3c214 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.html +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.html @@ -135,6 +135,7 @@

{{ section.title }}

[currentResource]="resourceOverview()" (customCitationUpdated)="onCustomCitationUpdated($event)" [canEdit]="hasWriteAccess()" + [showEditButton]="hasWriteAccess()" /> diff --git a/src/app/features/registry/pages/registry-wiki/registry-wiki.component.html b/src/app/features/registry/pages/registry-wiki/registry-wiki.component.html index 0c8dfc404..799fd09be 100644 --- a/src/app/features/registry/pages/registry-wiki/registry-wiki.component.html +++ b/src/app/features/registry/pages/registry-wiki/registry-wiki.component.html @@ -21,7 +21,6 @@
{{ citation.title }} } } - @if (canEdit()) { - -

{{ 'project.overview.metadata.getMoreCitations' | translate }}

- - - {{ selectedOption.label }} - - - @if (styledCitation()) { -

{{ styledCitation()?.citation }}

- } - - @if (!hasViewOnly) { - - } + +

{{ 'project.overview.metadata.getMoreCitations' | translate }}

+ + + {{ selectedOption.label }} + + + @if (styledCitation()) { +

{{ styledCitation()?.citation }}

+ } + + @if (!hasViewOnly || canEdit()) { + } } } diff --git a/src/app/shared/components/resource-metadata/resource-metadata.component.html b/src/app/shared/components/resource-metadata/resource-metadata.component.html index e8a4488d6..c9f8f8b7c 100644 --- a/src/app/shared/components/resource-metadata/resource-metadata.component.html +++ b/src/app/shared/components/resource-metadata/resource-metadata.component.html @@ -5,7 +5,7 @@ - @if (!viewOnly() && list().length) { + @if (canEdit() && list().length) { @if (!isHomeWikiSelected()) { {{ item.label | translate }} - - @if (!viewOnly()) { + @if (canEdit()) { (); readonly isLoading = input(false); - readonly viewOnly = input(false); + readonly canEdit = input(false); readonly deleteWiki = output(); readonly createWiki = output(); diff --git a/src/app/shared/mappers/components/components.mapper.ts b/src/app/shared/mappers/components/components.mapper.ts index 0cd83baf1..762e19d95 100644 --- a/src/app/shared/mappers/components/components.mapper.ts +++ b/src/app/shared/mappers/components/components.mapper.ts @@ -9,14 +9,14 @@ export class ComponentsMapper { description: response.attributes.description, public: response.attributes.public, contributors: response.embeds.bibliographic_contributors.data.map((contributor) => ({ - id: contributor.embeds.users.data.id, - familyName: contributor.embeds.users.data.attributes.family_name, - fullName: contributor.embeds.users.data.attributes.full_name, - givenName: contributor.embeds.users.data.attributes.given_name, - middleName: contributor.embeds.users.data.attributes.middle_name, - type: contributor.embeds.users.data.type, + id: contributor.embeds.users?.data?.id, + familyName: contributor.embeds.users?.data?.attributes?.family_name, + fullName: contributor.embeds.users?.data?.attributes?.full_name, + givenName: contributor.embeds.users?.data?.attributes?.given_name, + middleName: contributor.embeds.users?.data?.attributes?.middle_name, + type: contributor.embeds.users?.data?.type, })), - currentUserPermissions: response.attributes.current_user_permissions || [], + currentUserPermissions: response.attributes?.current_user_permissions || [], }; } } diff --git a/src/app/shared/mappers/registration/registration.mapper.ts b/src/app/shared/mappers/registration/registration.mapper.ts index 916022d8c..0cf9aa652 100644 --- a/src/app/shared/mappers/registration/registration.mapper.ts +++ b/src/app/shared/mappers/registration/registration.mapper.ts @@ -95,8 +95,8 @@ export class RegistrationMapper { hasSupplements: registration.attributes.has_supplements, contributors: registration.embeds?.bibliographic_contributors?.data.map((contributor) => ({ - id: contributor.embeds.users.data.id, - fullName: contributor.embeds.users.data.attributes.full_name, + id: contributor.embeds.users?.data?.id, + fullName: contributor.embeds.users?.data?.attributes.full_name, })) || [], rootParentId: registration.relationships.root?.data?.id, currentUserPermissions: registration.attributes.current_user_permissions, diff --git a/src/app/shared/stores/current-resource/current-resource.selectors.ts b/src/app/shared/stores/current-resource/current-resource.selectors.ts index bb6ccae03..40534e119 100644 --- a/src/app/shared/stores/current-resource/current-resource.selectors.ts +++ b/src/app/shared/stores/current-resource/current-resource.selectors.ts @@ -1,6 +1,7 @@ import { Selector } from '@ngxs/store'; import { BaseNodeModel, CurrentResource, NodeShortInfoModel } from '@osf/shared/models'; +import { UserPermissions } from '@shared/enums'; import { CurrentResourceStateModel } from './current-resource.model'; import { CurrentResourceState } from './current-resource.state'; @@ -21,6 +22,21 @@ export class CurrentResourceSelectors { return state.resourceChildren.data; } + @Selector([CurrentResourceState]) + static hasWriteAccess(state: CurrentResourceStateModel): boolean { + return state.resourceDetails.data?.currentUserPermissions.includes(UserPermissions.Write) || false; + } + + @Selector([CurrentResourceState]) + static hasAdminAccess(state: CurrentResourceStateModel): boolean { + return state.resourceDetails.data?.currentUserPermissions.includes(UserPermissions.Admin) || false; + } + + @Selector([CurrentResourceState]) + static hasNoPermissions(state: CurrentResourceStateModel): boolean { + return !state.resourceDetails.data?.currentUserPermissions.length; + } + @Selector([CurrentResourceState]) static isResourceDetailsLoading(state: CurrentResourceStateModel): boolean { return state.resourceDetails.isLoading; From 36b2c5d6c8131f91548ae6f8c078488c3eea42ab Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Fri, 26 Sep 2025 19:19:38 +0300 Subject: [PATCH 2/3] fix(ang-403): fixed minor bug --- .../stores/current-resource/current-resource.selectors.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/shared/stores/current-resource/current-resource.selectors.ts b/src/app/shared/stores/current-resource/current-resource.selectors.ts index 40534e119..c63aa0cbd 100644 --- a/src/app/shared/stores/current-resource/current-resource.selectors.ts +++ b/src/app/shared/stores/current-resource/current-resource.selectors.ts @@ -24,17 +24,17 @@ export class CurrentResourceSelectors { @Selector([CurrentResourceState]) static hasWriteAccess(state: CurrentResourceStateModel): boolean { - return state.resourceDetails.data?.currentUserPermissions.includes(UserPermissions.Write) || false; + return state.resourceDetails.data?.currentUserPermissions?.includes(UserPermissions.Write) || false; } @Selector([CurrentResourceState]) static hasAdminAccess(state: CurrentResourceStateModel): boolean { - return state.resourceDetails.data?.currentUserPermissions.includes(UserPermissions.Admin) || false; + return state.resourceDetails.data?.currentUserPermissions?.includes(UserPermissions.Admin) || false; } @Selector([CurrentResourceState]) static hasNoPermissions(state: CurrentResourceStateModel): boolean { - return !state.resourceDetails.data?.currentUserPermissions.length; + return !state.resourceDetails.data?.currentUserPermissions?.length; } @Selector([CurrentResourceState]) From be66c485649c7a09a7e02b50f277a0704f665445 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Fri, 26 Sep 2025 21:30:24 +0300 Subject: [PATCH 3/3] fix(ang-403): added permissions for the nav menu --- .../components/nav-menu/nav-menu.component.ts | 41 ++++++++++++++++--- src/app/core/constants/nav-items.constant.ts | 13 ++++++ src/app/core/helpers/nav-menu.helper.ts | 31 +++++++++++++- src/app/core/models/route-context.model.ts | 5 +-- .../linked-services.component.html | 16 ++++---- .../linked-services.component.ts | 7 ++++ .../registrations/registrations.component.ts | 6 +-- .../features/project/wiki/wiki.component.ts | 3 -- .../current-resource.selectors.ts | 5 +++ 9 files changed, 103 insertions(+), 24 deletions(-) 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 e167f5f43..efbb43336 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.ts @@ -1,4 +1,4 @@ -import { select } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -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'; @@ -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', @@ -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); @@ -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); diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index cfdc5b390..586341e3a 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -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[] = [ @@ -438,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 }, +}; diff --git a/src/app/core/helpers/nav-menu.helper.ts b/src/app/core/helpers/nav-menu.helper.ts index 034c32c4a..49607ae4a 100644 --- a/src/app/core/helpers/nav-menu.helper.ts +++ b/src/app/core/helpers/nav-menu.helper.ts @@ -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 || ''); @@ -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], diff --git a/src/app/core/models/route-context.model.ts b/src/app/core/models/route-context.model.ts index 456173ee2..395a5ef37 100644 --- a/src/app/core/models/route-context.model.ts +++ b/src/app/core/models/route-context.model.ts @@ -1,5 +1,3 @@ -import { UserPermissions } from '@osf/shared/enums'; - export interface RouteContext { resourceId: string | undefined; providerId?: string; @@ -13,5 +11,6 @@ export interface RouteContext { isCollections: boolean; currentUrl?: string; isViewOnly?: boolean; - permissions?: UserPermissions[]; + permissions?: string[]; + isResourceDetailsLoading?: boolean; } diff --git a/src/app/features/project/linked-services/linked-services.component.html b/src/app/features/project/linked-services/linked-services.component.html index a36358f21..833c79190 100644 --- a/src/app/features/project/linked-services/linked-services.component.html +++ b/src/app/features/project/linked-services/linked-services.component.html @@ -41,13 +41,15 @@

{{ 'project.linkedServices.noLinkedServices' | translate }}

-

- {{ 'project.linkedServices.redirectMessage' | translate }} - - {{ 'project.linkedServices.addonsLink' | translate }} - - {{ 'project.linkedServices.redirectMessageSuffix' | translate }} -

+ @if (canManageAddons()) { +

+ {{ 'project.linkedServices.redirectMessage' | translate }} + + {{ 'project.linkedServices.addonsLink' | translate }} + + {{ 'project.linkedServices.redirectMessageSuffix' | translate }} +

+ } }
diff --git a/src/app/features/project/linked-services/linked-services.component.ts b/src/app/features/project/linked-services/linked-services.component.ts index ea280a770..ff1e5834f 100644 --- a/src/app/features/project/linked-services/linked-services.component.ts +++ b/src/app/features/project/linked-services/linked-services.component.ts @@ -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', @@ -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(); @@ -45,6 +48,10 @@ export class LinkedServicesComponent implements OnInit { })); }); + canManageAddons = computed(() => { + return this.hasWriteAccess() || this.hasAdminAccess(); + }); + actions = createDispatchMap({ getConfiguredLinkAddons: GetConfiguredLinkAddons, getAddonsResourceReference: GetAddonsResourceReference, diff --git a/src/app/features/project/registrations/registrations.component.ts b/src/app/features/project/registrations/registrations.component.ts index fcc3a2056..22bb24739 100644 --- a/src/app/features/project/registrations/registrations.component.ts +++ b/src/app/features/project/registrations/registrations.component.ts @@ -19,8 +19,7 @@ import { RegistrationCardComponent, SubHeaderComponent, } from '@osf/shared/components'; -import { ResourceType } from '@shared/enums'; -import { CurrentResourceSelectors, GetResourceDetails } from '@shared/stores'; +import { CurrentResourceSelectors } from '@shared/stores'; import { GetRegistrations, RegistrationsSelectors } from './store'; @@ -49,13 +48,12 @@ export class RegistrationsComponent implements OnInit { registrations = select(RegistrationsSelectors.getRegistrations); registrationsTotalCount = select(RegistrationsSelectors.getRegistrationsTotalCount); isRegistrationsLoading = select(RegistrationsSelectors.isRegistrationsLoading); - actions = createDispatchMap({ getRegistrations: GetRegistrations, getResourceDetails: GetResourceDetails }); + actions = createDispatchMap({ getRegistrations: GetRegistrations }); itemsPerPage = 10; first = 0; ngOnInit(): void { - this.actions.getResourceDetails(this.projectId(), ResourceType.Project); this.actions.getRegistrations(this.projectId(), 1, this.itemsPerPage); } diff --git a/src/app/features/project/wiki/wiki.component.ts b/src/app/features/project/wiki/wiki.component.ts index bb9eaf8f2..ba003aaf7 100644 --- a/src/app/features/project/wiki/wiki.component.ts +++ b/src/app/features/project/wiki/wiki.component.ts @@ -29,7 +29,6 @@ import { DeleteWiki, GetCompareVersionContent, GetComponentsWikiList, - GetResourceDetails, GetWikiContent, GetWikiList, GetWikiModes, @@ -100,13 +99,11 @@ export class WikiComponent { createWikiVersion: CreateWikiVersion, getWikiVersionContent: GetWikiVersionContent, getCompareVersionContent: GetCompareVersionContent, - getResourceDetails: GetResourceDetails, }); wikiIdFromQueryParams = this.route.snapshot.queryParams['wiki']; constructor() { - this.actions.getResourceDetails(this.projectId(), ResourceType.Project); this.actions .getWikiList(ResourceType.Project, this.projectId()) .pipe( diff --git a/src/app/shared/stores/current-resource/current-resource.selectors.ts b/src/app/shared/stores/current-resource/current-resource.selectors.ts index c63aa0cbd..1bf13ad6f 100644 --- a/src/app/shared/stores/current-resource/current-resource.selectors.ts +++ b/src/app/shared/stores/current-resource/current-resource.selectors.ts @@ -46,4 +46,9 @@ export class CurrentResourceSelectors { static isResourceWithChildrenLoading(state: CurrentResourceStateModel): boolean { return state.resourceChildren.isLoading; } + + @Selector([CurrentResourceState]) + static getCurrentUserPermissions(currentResourceState: CurrentResourceStateModel): string[] { + return currentResourceState.resourceDetails.data.currentUserPermissions || []; + } }