diff --git a/src/app/features/contributors/contributors.component.ts b/src/app/features/contributors/contributors.component.ts
index 8c2ef4b23..dd8db40e2 100644
--- a/src/app/features/contributors/contributors.component.ts
+++ b/src/app/features/contributors/contributors.component.ts
@@ -28,6 +28,7 @@ import {
AddContributorDialogComponent,
AddUnregisteredContributorDialogComponent,
ContributorsTableComponent,
+ RemoveContributorDialogComponent,
RequestAccessTableComponent,
} from '@osf/shared/components/contributors';
import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component';
@@ -397,26 +398,37 @@ export class ContributorsComponent implements OnInit, OnDestroy {
removeContributor(contributor: ContributorModel) {
const isDeletingSelf = contributor.userId === this.currentUser()?.id;
- this.customConfirmationService.confirmDelete({
- headerKey: 'project.contributors.removeDialog.title',
- messageKey: 'project.contributors.removeDialog.message',
- messageParams: { name: contributor.fullName },
- acceptLabelKey: 'common.buttons.remove',
- onConfirm: () => {
- this.actions
- .deleteContributor(this.resourceId(), this.resourceType(), contributor.userId, isDeletingSelf)
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe(() => {
- this.toastService.showSuccess('project.contributors.removeDialog.successMessage', {
- name: contributor.fullName,
- });
+ this.customDialogService
+ .open(RemoveContributorDialogComponent, {
+ header: 'project.contributors.removeDialog.title',
+ width: '448px',
+ data: {
+ name: contributor.fullName,
+ hasChildren: !!this.resourceChildren()?.length,
+ },
+ })
+ .onClose.pipe(
+ filter((res) => res !== undefined),
+ switchMap((removeFromChildren: boolean) =>
+ this.actions.deleteContributor(
+ this.resourceId(),
+ this.resourceType(),
+ contributor.userId,
+ isDeletingSelf,
+ removeFromChildren
+ )
+ ),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe(() => {
+ this.toastService.showSuccess('project.contributors.removeDialog.successMessage', {
+ name: contributor.fullName,
+ });
- if (isDeletingSelf) {
- this.router.navigate(['/']);
- }
- });
- },
- });
+ if (isDeletingSelf) {
+ this.router.navigate(['/']);
+ }
+ });
}
loadMoreContributors(): void {
diff --git a/src/app/shared/components/contributors/index.ts b/src/app/shared/components/contributors/index.ts
index bd33928cd..5ab666f29 100644
--- a/src/app/shared/components/contributors/index.ts
+++ b/src/app/shared/components/contributors/index.ts
@@ -1,4 +1,5 @@
export * from './add-contributor-dialog/add-contributor-dialog.component';
export * from './add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component';
export * from './contributors-table/contributors-table.component';
+export * from './remove-contributor-dialog/remove-contributor-dialog.component';
export * from './request-access-table/request-access-table.component';
diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html
new file mode 100644
index 000000000..6b579178e
--- /dev/null
+++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html
@@ -0,0 +1,39 @@
+
+
{{ 'project.contributors.removeDialog.message' | translate: { name: name } }}
+
+
+
+
+
+
diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.scss b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts
new file mode 100644
index 000000000..c3790ed9e
--- /dev/null
+++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts
@@ -0,0 +1,51 @@
+import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RemoveContributorDialogComponent } from './remove-contributor-dialog.component';
+
+import { OSFTestingModule } from '@testing/osf.testing.module';
+
+describe('RemoveContributorDialogComponent', () => {
+ let component: RemoveContributorDialogComponent;
+ let fixture: ComponentFixture;
+ let dialogRef: DynamicDialogRef;
+
+ beforeEach(async () => {
+ dialogRef = { close: jest.fn() } as any;
+
+ await TestBed.configureTestingModule({
+ imports: [RemoveContributorDialogComponent, OSFTestingModule],
+ providers: [
+ { provide: DynamicDialogRef, useValue: dialogRef },
+ {
+ provide: DynamicDialogConfig,
+ useValue: { data: { name: 'John Doe', hasChildren: true } },
+ },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(RemoveContributorDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should pass name from config', () => {
+ expect(component.name).toBe('John Doe');
+ });
+
+ it('should close dialog with selected option on confirm', () => {
+ component.selectedOption = true;
+ component.confirm();
+ expect(dialogRef.close).toHaveBeenCalledWith(true);
+ });
+
+ it('should close dialog without value on cancel', () => {
+ component.cancel();
+ expect(dialogRef.close).toHaveBeenCalledWith();
+ });
+});
diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts
new file mode 100644
index 000000000..c5515cf46
--- /dev/null
+++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts
@@ -0,0 +1,37 @@
+import { TranslatePipe } from '@ngx-translate/core';
+
+import { Button } from 'primeng/button';
+import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
+import { RadioButton } from 'primeng/radiobutton';
+
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+ selector: 'osf-remove-contributor-dialog',
+ imports: [RadioButton, FormsModule, Button, TranslatePipe],
+ templateUrl: './remove-contributor-dialog.component.html',
+ styleUrl: './remove-contributor-dialog.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class RemoveContributorDialogComponent {
+ readonly dialogRef = inject(DynamicDialogRef);
+ readonly config = inject(DynamicDialogConfig);
+ selectedOption = false;
+
+ get name(): string | undefined {
+ return this.config?.data?.name;
+ }
+
+ get hasChildren(): boolean {
+ return this.config?.data?.hasChildren ?? false;
+ }
+
+ confirm(): void {
+ this.dialogRef.close(this.selectedOption);
+ }
+
+ cancel(): void {
+ this.dialogRef.close();
+ }
+}
diff --git a/src/app/shared/services/contributors.service.ts b/src/app/shared/services/contributors.service.ts
index c441b9ca0..60e4ab31b 100644
--- a/src/app/shared/services/contributors.service.ts
+++ b/src/app/shared/services/contributors.service.ts
@@ -170,8 +170,16 @@ export class ContributorsService {
return this.jsonApiService.patch(baseUrl, contributorData);
}
- deleteContributor(resourceType: ResourceType, resourceId: string, userId: string): Observable {
- const baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/${userId}/`;
+ deleteContributor(
+ resourceType: ResourceType,
+ resourceId: string,
+ userId: string,
+ removeFromChildren = false
+ ): Observable {
+ let baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/${userId}/`;
+ if (removeFromChildren) {
+ baseUrl = baseUrl.concat('?propagate_to_children=true');
+ }
return this.jsonApiService.delete(baseUrl);
}
diff --git a/src/app/shared/stores/contributors/contributors.actions.ts b/src/app/shared/stores/contributors/contributors.actions.ts
index 2bb634cfc..d9f0d59c0 100644
--- a/src/app/shared/stores/contributors/contributors.actions.ts
+++ b/src/app/shared/stores/contributors/contributors.actions.ts
@@ -80,7 +80,8 @@ export class DeleteContributor {
public resourceId: string | undefined | null,
public resourceType: ResourceType | undefined,
public contributorId: string,
- public skipRefresh = false
+ public skipRefresh = false,
+ public removeFromChildren = false
) {}
}
diff --git a/src/app/shared/stores/contributors/contributors.state.ts b/src/app/shared/stores/contributors/contributors.state.ts
index 0e738afec..9e45e7599 100644
--- a/src/app/shared/stores/contributors/contributors.state.ts
+++ b/src/app/shared/stores/contributors/contributors.state.ts
@@ -251,7 +251,7 @@ export class ContributorsState {
});
return this.contributorsService
- .deleteContributor(action.resourceType, action.resourceId, action.contributorId)
+ .deleteContributor(action.resourceType, action.resourceId, action.contributorId, action.removeFromChildren)
.pipe(
tap(() => {
if (!action.skipRefresh) {
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 740df40fa..733ef0f6a 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -618,6 +618,8 @@
},
"removeDialog": {
"title": "Remove contributor",
+ "thisProjectOnly": "This project only",
+ "thisProjectAndComponents": "This project and all it's components",
"message": "Are you sure you want to remove {{name}} contributor?",
"successMessage": "Contributor {{name}} successfully removed."
},