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 {
+
+ @for (component of components(); track component.id) {
+ {{ component.title }}
+ }
+
+ }
-
+ {{ '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