From a5f1ed2aa598e0ed7f20cdfae92f09a7c549365f Mon Sep 17 00:00:00 2001 From: Oleh Paduchak Date: Fri, 14 Nov 2025 13:47:11 +0200 Subject: [PATCH 1/5] feat(contributors): delete contributors from all components --- .../contributors/contributors.component.ts | 50 ++++++++++++------- .../shared/components/contributors/index.ts | 1 + .../remove-contributor-dialog.component.html | 32 ++++++++++++ .../remove-contributor-dialog.component.scss | 0 ...emove-contributor-dialog.component.spec.ts | 22 ++++++++ .../remove-contributor-dialog.component.ts | 38 ++++++++++++++ .../shared/services/contributors.service.ts | 12 ++++- .../contributors/contributors.actions.ts | 3 +- .../stores/contributors/contributors.state.ts | 2 +- src/assets/i18n/en.json | 2 + 10 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html create mode 100644 src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.scss create mode 100644 src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts create mode 100644 src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts diff --git a/src/app/features/contributors/contributors.component.ts b/src/app/features/contributors/contributors.component.ts index 8c2ef4b23..4978b94ef 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: { + messageKey: 'project.contributors.removeDialog.message', + messageParams: { name: contributor.fullName }, + }, + }) + .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..838bd02f1 --- /dev/null +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.html @@ -0,0 +1,32 @@ +
+

+ +
+ + + +
+
+ + +
+ +
+ + +
+
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..65273fab7 --- /dev/null +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RemoveContributorDialogComponent } from './remove-contributor-dialog.component'; + +describe('RemoveContributorDialogComponent', () => { + let component: RemoveContributorDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RemoveContributorDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RemoveContributorDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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..041d5c90c --- /dev/null +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.ts @@ -0,0 +1,38 @@ +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 messageKey(): string | undefined { + return this.config?.data?.messageKey as string | undefined; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get messageParams(): any { + return this.config?.data?.messageParams; + } + + 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." }, From b7938e37074099eb02bfcc7ba8bf17a8ef813e23 Mon Sep 17 00:00:00 2001 From: Andriy Sheredko Date: Wed, 26 Nov 2025 09:34:33 +0200 Subject: [PATCH 2/5] feat(contributors): delete contributors from all components --- .../contributors/contributors.component.ts | 4 ++-- .../remove-contributor-dialog.component.html | 14 +++++++---- ...emove-contributor-dialog.component.spec.ts | 24 +++++++++++++++++++ .../remove-contributor-dialog.component.ts | 9 ++++--- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/app/features/contributors/contributors.component.ts b/src/app/features/contributors/contributors.component.ts index 4978b94ef..dd8db40e2 100644 --- a/src/app/features/contributors/contributors.component.ts +++ b/src/app/features/contributors/contributors.component.ts @@ -403,8 +403,8 @@ export class ContributorsComponent implements OnInit, OnDestroy { header: 'project.contributors.removeDialog.title', width: '448px', data: { - messageKey: 'project.contributors.removeDialog.message', - messageParams: { name: contributor.fullName }, + name: contributor.fullName, + hasChildren: !!this.resourceChildren()?.length, }, }) .onClose.pipe( 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 index 838bd02f1..fe28bcf57 100644 --- 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 @@ -1,5 +1,5 @@
-

+

{{ 'project.contributors.removeDialog.message' | translate: { name: name } }}

@@ -7,7 +7,13 @@
- + + @@ -17,14 +23,14 @@ 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 index 65273fab7..b699581b8 100644 --- 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 @@ -1,3 +1,5 @@ +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RemoveContributorDialogComponent } from './remove-contributor-dialog.component'; @@ -5,10 +7,17 @@ import { RemoveContributorDialogComponent } from './remove-contributor-dialog.co describe('RemoveContributorDialogComponent', () => { let component: RemoveContributorDialogComponent; let fixture: ComponentFixture; + let dialogRef: DynamicDialogRef; beforeEach(async () => { + dialogRef = { close: jasmine.createSpy('close') } as any; + await TestBed.configureTestingModule({ imports: [RemoveContributorDialogComponent], + providers: [ + { provide: DynamicDialogRef, useValue: dialogRef }, + { provide: DynamicDialogConfig, useValue: { data: { name: 'John Doe', hasChildren: true } } }, + ], }).compileComponents(); fixture = TestBed.createComponent(RemoveContributorDialogComponent); @@ -19,4 +28,19 @@ describe('RemoveContributorDialogComponent', () => { 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 index 041d5c90c..c5515cf46 100644 --- 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 @@ -19,13 +19,12 @@ export class RemoveContributorDialogComponent { readonly config = inject(DynamicDialogConfig); selectedOption = false; - get messageKey(): string | undefined { - return this.config?.data?.messageKey as string | undefined; + get name(): string | undefined { + return this.config?.data?.name; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - get messageParams(): any { - return this.config?.data?.messageParams; + get hasChildren(): boolean { + return this.config?.data?.hasChildren ?? false; } confirm(): void { From aa3979a5e6e6d7a8aea79121356cfcce0d7f2aa6 Mon Sep 17 00:00:00 2001 From: Andriy Sheredko Date: Wed, 26 Nov 2025 09:39:12 +0200 Subject: [PATCH 3/5] feat(contributors): fix linting --- .../remove-contributor-dialog.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index fe28bcf57..6b579178e 100644 --- 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 @@ -12,7 +12,8 @@ [value]="true" [(ngModel)]="selectedOption" [disabled]="!hasChildren" - inputId="projectAll"> + inputId="projectAll" + >