From 42c4cb3ee9dfb2eaa7b58f9e4abc738a0f4e06a7 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Sat, 11 Oct 2025 16:58:06 +0300 Subject: [PATCH 1/2] fix(delete-component): fixed delete component modal behavior --- .../view-duplicates.component.ts | 59 ++++++++++++------- .../create-view-link-dialog.component.html | 2 +- .../delete-component-dialog.component.html | 34 ++++++++--- .../delete-component-dialog.component.ts | 40 +++++++++---- .../overview-components.component.ts | 32 ++++++++-- src/app/features/project/project.routes.ts | 1 + .../view-only-table.component.html | 7 ++- src/assets/i18n/en.json | 9 ++- 8 files changed, 129 insertions(+), 55 deletions(-) diff --git a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts index 90712bf8d..595a71d56 100644 --- a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts +++ b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts @@ -41,8 +41,8 @@ import { import { ResourceType, UserPermissions } from '@osf/shared/enums'; import { ToolbarResource } from '@osf/shared/models'; import { Duplicate } from '@osf/shared/models/duplicates'; -import { CustomDialogService } from '@osf/shared/services'; -import { ClearDuplicates, DuplicatesSelectors, GetAllDuplicates } from '@osf/shared/stores'; +import { CustomDialogService, LoaderService } from '@osf/shared/services'; +import { ClearDuplicates, DuplicatesSelectors, GetAllDuplicates, GetResourceWithChildren } from '@osf/shared/stores'; @Component({ selector: 'osf-view-duplicates', @@ -65,6 +65,7 @@ import { ClearDuplicates, DuplicatesSelectors, GetAllDuplicates } from '@osf/sha }) export class ViewDuplicatesComponent { private customDialogService = inject(CustomDialogService); + private loaderService = inject(LoaderService); private route = inject(ActivatedRoute); private router = inject(Router); private destroyRef = inject(DestroyRef); @@ -125,6 +126,7 @@ export class ViewDuplicatesComponent { clearDuplicates: ClearDuplicates, clearProject: ClearProjectOverview, clearRegistration: ClearRegistryOverview, + getComponentsTree: GetResourceWithChildren, }); constructor() { @@ -230,25 +232,38 @@ export class ViewDuplicatesComponent { } private handleDeleteFork(id: string): void { - this.customDialogService - .open(DeleteComponentDialogComponent, { - header: 'project.overview.dialog.deleteComponent.header', - width: '650px', - data: { - componentId: id, - resourceType: this.resourceType(), - isForksContext: true, - currentPage: parseInt(this.currentPage()), - pageSize: this.pageSize, - }, - }) - .onClose.subscribe((result) => { - if (result?.success) { - const resource = this.currentResource(); - if (resource) { - this.actions.getDuplicates(resource.id, resource.type, parseInt(this.currentPage()), this.pageSize); - } - } - }); + const resourceType = this.resourceType(); + if (!resourceType) return; + + this.loaderService.show(); + + this.actions.getComponentsTree(id, id, resourceType).subscribe({ + next: () => { + this.loaderService.hide(); + this.customDialogService + .open(DeleteComponentDialogComponent, { + header: 'project.overview.dialog.deleteComponent.header', + width: '650px', + data: { + componentId: id, + resourceType: resourceType, + isForksContext: true, + currentPage: parseInt(this.currentPage()), + pageSize: this.pageSize, + }, + }) + .onClose.subscribe((result) => { + if (result?.success) { + const resource = this.currentResource(); + if (resource) { + this.actions.getDuplicates(resource.id, resource.type, parseInt(this.currentPage()), this.pageSize); + } + } + }); + }, + error: () => { + this.loaderService.hide(); + }, + }); } } diff --git a/src/app/features/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html b/src/app/features/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html index f299fd481..472b5d1e9 100644 --- a/src/app/features/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html +++ b/src/app/features/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html @@ -57,7 +57,7 @@ styleClass="w-full" (onClick)="addLink()" [disabled]="linkName.invalid" - [label]="'project.contributors.addDialog.next' | translate" + [label]="'common.buttons.create' | translate" > diff --git a/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.html b/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.html index c657c5b9f..1fc424cb5 100644 --- a/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.html +++ b/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.html @@ -1,12 +1,30 @@ -

{{ 'project.overview.dialog.deleteComponent.message' | translate }}

-
-

- {{ 'project.overview.dialog.deleteComponent.confirmation' | translate }} - {{ selectedScientist() }} -

+@if (hasAdminAccessForAllComponents()) { + @if (hasSubcomponents()) { +

{{ 'project.overview.dialog.deleteComponent.listMessage' | translate }}

+ @if (isLoading()) { + + } @else { + + } - +

{{ 'project.overview.dialog.deleteComponent.warningMessage' | translate }}

+ } @else { +

{{ 'project.overview.dialog.deleteComponent.message' | translate }}

+ } +

+ {{ 'project.overview.dialog.deleteComponent.confirmation' | translate }} + {{ selectedScientist() }} +

+ + +} @else { +

+}
diff --git a/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.ts b/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.ts index 4b000cd31..85a36db14 100644 --- a/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.ts +++ b/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.ts @@ -5,21 +5,24 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { InputText } from 'primeng/inputtext'; +import { Skeleton } from 'primeng/skeleton'; import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; +import { DeleteProject, SettingsSelectors } from '@osf/features/project/settings/store'; import { RegistryOverviewSelectors } from '@osf/features/registry/store/registry-overview'; import { ScientistsNames } from '@osf/shared/constants'; -import { ResourceType } from '@osf/shared/enums'; +import { ResourceType, UserPermissions } from '@osf/shared/enums'; import { ToastService } from '@osf/shared/services'; +import { CurrentResourceSelectors } from '@osf/shared/stores'; -import { DeleteComponent, GetComponents, ProjectOverviewSelectors } from '../../store'; +import { GetComponents, ProjectOverviewSelectors } from '../../store'; @Component({ selector: 'osf-delete-component-dialog', - imports: [TranslatePipe, Button, InputText, FormsModule], + imports: [TranslatePipe, Button, InputText, FormsModule, Skeleton], templateUrl: './delete-component-dialog.component.html', styleUrl: './delete-component-dialog.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -33,7 +36,9 @@ export class DeleteComponentDialogComponent { scientistNames = ScientistsNames; project = select(ProjectOverviewSelectors.getProject); registration = select(RegistryOverviewSelectors.getRegistry); - isSubmitting = select(ProjectOverviewSelectors.getComponentsSubmitting); + isSubmitting = select(SettingsSelectors.isSettingsSubmitting); + isLoading = select(CurrentResourceSelectors.isResourceWithChildrenLoading); + components = select(CurrentResourceSelectors.getResourceWithChildren); userInput = signal(''); selectedScientist = computed(() => { const names = Object.values(this.scientistNames); @@ -52,9 +57,21 @@ export class DeleteComponentDialogComponent { return null; }); + hasAdminAccessForAllComponents = computed(() => { + const components = this.components(); + if (!components || !components.length) return false; + + return components.every((component) => component.permissions?.includes(UserPermissions.Admin)); + }); + + hasSubcomponents = computed(() => { + const components = this.components(); + return components && components.length > 1; + }); + actions = createDispatchMap({ getComponents: GetComponents, - deleteComponent: DeleteComponent, + deleteComponent: DeleteProject, }); isInputValid(): boolean { @@ -66,25 +83,24 @@ export class DeleteComponentDialogComponent { } handleDeleteComponent(): void { - const resource = this.currentResource(); - const componentId = this.componentId(); + const components = this.components(); - if (!componentId || !resource) return; + if (!components?.length) return; this.actions - .deleteComponent(componentId) + .deleteComponent(components) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { this.dialogRef.close({ success: true }); const isForksContext = this.dialogConfig.data.isForksContext; + const resource = this.currentResource(); - if (!isForksContext) { + if (!isForksContext && resource) { this.actions.getComponents(resource.id); } - }, - complete: () => { + this.toastService.showSuccess('project.overview.dialog.toast.deleteComponent.success'); }, }); diff --git a/src/app/features/project/overview/components/overview-components/overview-components.component.ts b/src/app/features/project/overview/components/overview-components/overview-components.component.ts index db526708f..0b2bfe543 100644 --- a/src/app/features/project/overview/components/overview-components/overview-components.component.ts +++ b/src/app/features/project/overview/components/overview-components/overview-components.component.ts @@ -1,4 +1,4 @@ -import { select } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -12,7 +12,8 @@ import { Router } from '@angular/router'; import { UserSelectors } from '@core/store/user'; import { ContributorsListComponent, IconComponent, TruncatedTextComponent } from '@osf/shared/components'; import { ResourceType, UserPermissions } from '@osf/shared/enums'; -import { CustomDialogService } from '@osf/shared/services'; +import { CustomDialogService, LoaderService } from '@osf/shared/services'; +import { GetResourceWithChildren } from '@osf/shared/stores'; import { ComponentOverview } from '@shared/models'; import { ProjectOverviewSelectors } from '../../store'; @@ -29,6 +30,7 @@ import { DeleteComponentDialogComponent } from '../delete-component-dialog/delet export class OverviewComponentsComponent { private router = inject(Router); private customDialogService = inject(CustomDialogService); + private loaderService = inject(LoaderService); canEdit = input.required(); @@ -36,6 +38,11 @@ export class OverviewComponentsComponent { currentUserId = computed(() => this.currentUser()?.id); components = select(ProjectOverviewSelectors.getComponents); isComponentsLoading = select(ProjectOverviewSelectors.getComponentsLoading); + project = select(ProjectOverviewSelectors.getProject); + + actions = createDispatchMap({ + getComponentsTree: GetResourceWithChildren, + }); readonly componentActionItems = (component: ComponentOverview) => { const baseItems = [ @@ -92,10 +99,23 @@ export class OverviewComponentsComponent { } private handleDeleteComponent(componentId: string): void { - this.customDialogService.open(DeleteComponentDialogComponent, { - header: 'project.overview.dialog.deleteComponent.header', - width: '650px', - data: { componentId, resourceType: ResourceType.Project }, + const project = this.project(); + if (!project) return; + + this.loaderService.show(); + + this.actions.getComponentsTree(project.rootParentId || project.id, componentId, ResourceType.Project).subscribe({ + next: () => { + this.loaderService.hide(); + this.customDialogService.open(DeleteComponentDialogComponent, { + header: 'project.overview.dialog.deleteComponent.header', + width: '650px', + data: { componentId, resourceType: ResourceType.Project }, + }); + }, + error: () => { + this.loaderService.hide(); + }, }); } } diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 1644d0901..8c34fdce1 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -46,6 +46,7 @@ export const projectRoutes: Routes = [ CollectionsModerationState, ActivityLogsState, SubjectsState, + SettingsState, ]), ], }, diff --git a/src/app/shared/components/view-only-table/view-only-table.component.html b/src/app/shared/components/view-only-table/view-only-table.component.html index 90d274386..efcf5c528 100644 --- a/src/app/shared/components/view-only-table/view-only-table.component.html +++ b/src/app/shared/components/view-only-table/view-only-table.component.html @@ -1,4 +1,4 @@ -@if (isLoading() || tableData().items.length) { +@if (isLoading() || tableData().items?.length) { @@ -35,14 +35,15 @@ {{ item.name }} -
+
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b4ad4e5f3..cf1b76a2b 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -823,11 +823,14 @@ } }, "deleteComponent": { - "header": "Are you sure you want to delete this component?", + "header": "Delete component", "confirmButton": "Delete", "cancelButton": "Cancel", "message": "It will no longer be available to other contributors on the project.", - "confirmation": "Type the following to continue:" + "listMessage": "You are about to delete the following:", + "warningMessage": "This component contains subcomponents. To delete this component, you must also delete all subcomponents. This action is irreversible.", + "confirmation": "Type the following to continue:", + "noPermissionsMessage": "You do not have permissions to delete this component and its subcomponents.

To delete, you must have admin permissions across all objects. Please check your permissions and try again.

Contact support at support@osf.io if you have any questions." }, "deleteNodeLink": { "header": "Delete Link", @@ -3062,4 +3065,4 @@ "software": "Software", "other": "Other" } -} +} \ No newline at end of file From 5117dfabc2a7c7d5eab26f214e14941d76a7bae0 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Sat, 11 Oct 2025 17:18:30 +0300 Subject: [PATCH 2/2] fix(delete-component): fixed undefined error --- .../components/view-only-table/view-only-table.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/components/view-only-table/view-only-table.component.html b/src/app/shared/components/view-only-table/view-only-table.component.html index efcf5c528..9d07abddf 100644 --- a/src/app/shared/components/view-only-table/view-only-table.component.html +++ b/src/app/shared/components/view-only-table/view-only-table.component.html @@ -1,4 +1,4 @@ -@if (isLoading() || tableData().items?.length) { +@if (isLoading() || (tableData().items && tableData().items.length)) {