From 803b99556bea52df54b81a218d03e44125dd6c33 Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 15 Sep 2025 15:33:26 +0300 Subject: [PATCH 1/2] fix(contributors): Fixed contributors management permissions --- .../contributors-dialog.component.html | 16 ++++++----- .../contributors-dialog.component.ts | 27 +++++++++++++++++-- .../contributors/contributors.component.html | 14 ++++++---- .../contributors/contributors.component.ts | 12 +++++++++ .../contributors-list.component.html | 2 +- .../contributors-list.component.ts | 20 +++++++++++++- 6 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.html b/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.html index 11ef9c4cf..f9212d8cc 100644 --- a/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.html +++ b/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.html @@ -2,12 +2,14 @@

{{ 'project.contributors.addContributor' | translate }}

- + @if (isCurrentUserAdminContributor()) { + + }
@@ -20,6 +22,8 @@

{{ 'project.contributors.addContributor' | translate }}

[showEducation]="false" [showEmployment]="false" [isLoading]="isLoading()" + [isCurrentUserAdminContributor]="isCurrentUserAdminContributor()" + [currentUserId]="currentUser()?.id" (remove)="removeContributor($event)" > diff --git a/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts b/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts index 007daa5e7..c4e9cc827 100644 --- a/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts +++ b/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts @@ -7,17 +7,27 @@ import { DialogService, DynamicDialogConfig, DynamicDialogRef } from 'primeng/dy import { filter, forkJoin } from 'rxjs'; -import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + effect, + inject, + OnInit, + signal, +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule } from '@angular/forms'; +import { UserSelectors } from '@core/store/user'; import { SearchInputComponent } from '@osf/shared/components'; import { AddContributorDialogComponent, AddUnregisteredContributorDialogComponent, ContributorsListComponent, } from '@osf/shared/components/contributors'; -import { AddContributorType, ResourceType } from '@osf/shared/enums'; +import { AddContributorType, ContributorPermission, ResourceType } from '@osf/shared/enums'; import { findChangedItems } from '@osf/shared/helpers'; import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/models'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; @@ -52,6 +62,19 @@ export class ContributorsDialogComponent implements OnInit { isLoading = select(ContributorsSelectors.isContributorsLoading); initialContributors = select(ContributorsSelectors.getContributors); contributors = signal([]); + + currentUser = select(UserSelectors.getCurrentUser); + + isCurrentUserAdminContributor = computed(() => { + const currentUserId = this.currentUser()?.id; + const initialContributors = this.initialContributors(); + if (!currentUserId) return false; + + return initialContributors.some((contributor: ContributorModel) => { + return contributor.userId === currentUserId && contributor.permission === ContributorPermission.Admin; + }); + }); + actions = createDispatchMap({ updateSearchValue: UpdateSearchValue, updatePermissionFilter: UpdatePermissionFilter, diff --git a/src/app/features/project/contributors/contributors.component.html b/src/app/features/project/contributors/contributors.component.html index 264057038..e87bcbeda 100644 --- a/src/app/features/project/contributors/contributors.component.html +++ b/src/app/features/project/contributors/contributors.component.html @@ -1,11 +1,13 @@

{{ 'navigation.contributors' | translate }}

- + @if (isCurrentUserAdminContributor()) { + + }
@@ -64,6 +66,8 @@

{{ 'navigation.contributors' | translate } class="w-full" [contributors]="contributors()" [isLoading]="isContributorsLoading()" + [isCurrentUserAdminContributor]="isCurrentUserAdminContributor()" + [currentUserId]="currentUser()?.id" [showCurator]="true" (remove)="removeContributor($event)" > diff --git a/src/app/features/project/contributors/contributors.component.ts b/src/app/features/project/contributors/contributors.component.ts index 5c61531c4..2e047fad3 100644 --- a/src/app/features/project/contributors/contributors.component.ts +++ b/src/app/features/project/contributors/contributors.component.ts @@ -23,6 +23,7 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; +import { UserSelectors } from '@core/store/user'; import { SearchInputComponent, ViewOnlyTableComponent } from '@osf/shared/components'; import { AddContributorDialogComponent, @@ -105,9 +106,20 @@ export class ContributorsComponent implements OnInit { readonly isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); readonly isViewOnlyLinksLoading = select(ViewOnlyLinkSelectors.isViewOnlyLinksLoading); + readonly currentUser = select(UserSelectors.getCurrentUser); canCreateViewLink = computed(() => !!this.resourceDetails() && !!this.resourceId()); + isCurrentUserAdminContributor = computed(() => { + const currentUserId = this.currentUser()?.id; + const initialContributors = this.initialContributors(); + if (!currentUserId) return false; + + return initialContributors.some((contributor: ContributorModel) => { + return contributor.userId === currentUserId && contributor.permission === ContributorPermission.Admin; + }); + }); + actions = createDispatchMap({ getViewOnlyLinks: FetchViewOnlyLinks, getResourceDetails: GetResourceDetails, diff --git a/src/app/shared/components/contributors/contributors-list/contributors-list.component.html b/src/app/shared/components/contributors/contributors-list/contributors-list.component.html index a4464c84b..ce6dc9162 100644 --- a/src/app/shared/components/contributors/contributors-list/contributors-list.component.html +++ b/src/app/shared/components/contributors/contributors-list/contributors-list.component.html @@ -187,7 +187,7 @@

{{ 'project.contributors.curatorInfo.heading' | translate }}

} - @if (isCurrentUserAdminContributor() || currentUserId() === contributor.userId) { + @if (canRemoveContributor().get(contributor.userId)) { (undefined); isCurrentUserAdminContributor = input(true); + canRemoveContributor = computed(() => { + const contributors = this.contributors(); + const currentUserId = this.currentUserId(); + const isAdmin = this.isCurrentUserAdminContributor(); + const adminCount = contributors.filter((c) => c.permission === ContributorPermission.Admin).length; + + const result = new Map(); + + for (const c of contributors) { + const canRemove = + (isAdmin || currentUserId === c.userId) && !(c.permission === ContributorPermission.Admin && adminCount <= 1); + + result.set(c.userId, canRemove); + } + + return result; + }); + remove = output(); dialogService = inject(DialogService); translateService = inject(TranslateService); From deac04ffa33b6b7e7fb4e37061cd4186f17a8c3d Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 15 Sep 2025 15:58:55 +0300 Subject: [PATCH 2/2] fix(contributors): Fixed contributors mapping for registration card --- src/app/features/registry/models/index.ts | 1 - .../registry-contributor-json-api.model.ts | 96 ------------------- .../registration-card.component.html | 2 +- .../registration/registration.mapper.ts | 4 +- 4 files changed, 3 insertions(+), 100 deletions(-) delete mode 100644 src/app/features/registry/models/registry-contributor-json-api.model.ts diff --git a/src/app/features/registry/models/index.ts b/src/app/features/registry/models/index.ts index 7d096d036..90c7008df 100644 --- a/src/app/features/registry/models/index.ts +++ b/src/app/features/registry/models/index.ts @@ -6,7 +6,6 @@ export * from './linked-registrations-json-api.model'; export * from './linked-response.models'; export * from './registry-components.models'; export * from './registry-components-json-api.model'; -export * from './registry-contributor-json-api.model'; export * from './registry-metadata.models'; export * from './registry-overview.models'; export * from './resources'; diff --git a/src/app/features/registry/models/registry-contributor-json-api.model.ts b/src/app/features/registry/models/registry-contributor-json-api.model.ts deleted file mode 100644 index a75b4391c..000000000 --- a/src/app/features/registry/models/registry-contributor-json-api.model.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { MetaJsonApi } from '@osf/shared/models'; - -export interface RegistryContributorJsonApi { - id: string; - type: 'contributors'; - attributes: { - index: number; - bibliographic: boolean; - permission: string; - unregistered_contributor: string | null; - is_curator: boolean; - }; - relationships: { - users: { - links: { - related: { - href: string; - meta: Record; - }; - }; - data: { - id: string; - type: 'users'; - }; - }; - node: { - links: { - related: { - href: string; - meta: Record; - }; - }; - data: { - id: string; - type: 'nodes'; - }; - }; - }; - embeds?: { - users: { - data: { - id: string; - type: 'users'; - attributes: { - full_name: string; - given_name: string; - middle_names: string; - family_name: string; - suffix: string; - date_registered: string; - active: boolean; - timezone: string; - locale: string; - social: Record; - employment: unknown[]; - education: unknown[]; - }; - relationships: Record; - links: { - html: string; - profile_image: string; - self: string; - iri: string; - }; - }; - }; - }; - links: { - self: string; - }; -} - -export interface RegistryContributorJsonApiResponse { - data: RegistryContributorJsonApi; - links: { - self: string; - }; - meta: MetaJsonApi; -} - -export interface RegistryContributorUpdateRequest { - data: { - id: string; - type: 'contributors'; - attributes: Record; - relationships: Record; - }; -} - -export interface RegistryContributorAddRequest { - data: { - type: 'contributors'; - attributes: Record; - relationships: Record; - }; -} diff --git a/src/app/shared/components/registration-card/registration-card.component.html b/src/app/shared/components/registration-card/registration-card.component.html index c1650c5e4..752f09c58 100644 --- a/src/app/shared/components/registration-card/registration-card.component.html +++ b/src/app/shared/components/registration-card/registration-card.component.html @@ -39,7 +39,7 @@

{{ 'project.overview.metadata.contributors' | translate }}: @for (contributor of registrationData().contributors; track contributor) { - {{ contributor.fullName }} + {{ contributor.fullName }} @if (!$last) { , } diff --git a/src/app/shared/mappers/registration/registration.mapper.ts b/src/app/shared/mappers/registration/registration.mapper.ts index 6ef5830ce..ffcac4f5e 100644 --- a/src/app/shared/mappers/registration/registration.mapper.ts +++ b/src/app/shared/mappers/registration/registration.mapper.ts @@ -89,8 +89,8 @@ export class RegistrationMapper { revisionState: registration.attributes.revision_state, contributors: registration.embeds?.bibliographic_contributors?.data.map((contributor) => ({ - id: contributor.id, - fullName: contributor.embeds?.users?.data.attributes.full_name, + id: contributor.embeds.users.data.id, + fullName: contributor.embeds.users.data.attributes.full_name, })) || [], }; }