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." },