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/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/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);
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,
})) || [],
};
}
|