Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/app/core/interceptors/auth.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ export const authInterceptor: HttpInterceptorFn = (
const csrfToken = cookieService.get('api-csrf');

if (!req.url.includes('/api.crossref.org/funders')) {
const headers: Record<string, string> = {
Accept: req.responseType === 'text' ? '*/*' : 'application/vnd.api+json;version=2.20',
'Content-Type': 'application/vnd.api+json',
};
const headers: Record<string, string> = {};

headers['Accept'] = req.responseType === 'text' ? '*/*' : 'application/vnd.api+json;version=2.20';

if (!req.headers.has('Content-Type')) {
headers['Content-Type'] = 'application/vnd.api+json';
}

if (csrfToken) {
headers['X-CSRFToken'] = csrfToken;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ <h3 class="font-normal mbx-2">

<osf-affiliated-institution-select
[institutions]="userInstitutions()"
[selectedInstitutions]="userInstitutions()"
[selectedInstitutions]="selectedInstitutions()"
[isLoading]="areUserInstitutionsLoading()"
(selectedInstitutionsChange)="setSelectedInstitutions($event)"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { InputText } from 'primeng/inputtext';
import { Select } from 'primeng/select';
import { Textarea } from 'primeng/textarea';

import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';

Expand Down Expand Up @@ -47,6 +47,7 @@ export class AddComponentDialogComponent implements OnInit {
destroyRef = inject(DestroyRef);
ComponentFormControls = ComponentFormControls;

selectedInstitutions = signal<Institution[]>([]);
storageLocations = select(RegionsSelectors.getRegions);
currentUser = select(UserSelectors.getCurrentUser);
currentProject = select(ProjectOverviewSelectors.getProject);
Expand Down Expand Up @@ -86,16 +87,7 @@ export class AddComponentDialogComponent implements OnInit {
});

constructor() {
effect(() => {
const storageLocations = this.storageLocations();
if (!storageLocations?.length) return;

const storageLocationControl = this.componentForm.controls[ComponentFormControls.StorageLocation];
if (!storageLocationControl.value) {
const defaultRegion = this.currentUser()?.defaultRegionId ?? storageLocations[0].id;
storageLocationControl.setValue(defaultRegion);
}
});
this.setupEffects();
}

ngOnInit(): void {
Expand Down Expand Up @@ -142,4 +134,30 @@ export class AddComponentDialogComponent implements OnInit {
},
});
}

private setupEffects(): void {
effect(() => {
const storageLocations = this.storageLocations();
if (!storageLocations?.length) return;

const storageLocationControl = this.componentForm.controls[ComponentFormControls.StorageLocation];
if (!storageLocationControl.value) {
const defaultRegion = this.currentUser()?.defaultRegionId ?? storageLocations[0].id;
storageLocationControl.setValue(defaultRegion);
}
});

effect(() => {
const projectInstitutions = this.currentProject()?.affiliatedInstitutions;
const userInstitutions = this.userInstitutions();

if (projectInstitutions && projectInstitutions.length && userInstitutions.length) {
const matchedInstitutions = projectInstitutions
.map((projInst) => userInstitutions.find((userInst) => userInst.id === projInst.id))
.filter((inst) => inst !== undefined);

this.selectedInstitutions.set(matchedInstitutions);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@if (hasAdminAccessForAllComponents()) {
<p>{{ 'project.deleteProject.dialog.listMessage' | translate }}</p>
@if (isLoading()) {
<p-skeleton styleClass="mt-2" width="100%" height="4rem" />
} @else {
<ul class="flex flex-column gap-1 mt-2 pl-4">
@for (project of projects(); track project.id) {
<li class="list-disc">{{ project.title }}</li>
}
</ul>
}

<p class="mt-4">{{ 'project.deleteProject.dialog.warningMessage' | translate }}</p>

<p class="mt-4">
{{ 'project.deleteProject.dialog.confirmation' | translate }}
<strong> {{ selectedScientist() }}</strong>
</p>

<input pInputText class="mt-3" [ngModel]="userInput()" (ngModelChange)="onInputChange($event)" />
} @else {
<p [innerHTML]="'project.deleteProject.dialog.noPermissionsMessage' | translate"></p>
}
<div class="flex pt-5 justify-content-end gap-3">
<p-button
class="btn-full-width"
styleClass="w-full"
[label]="'common.buttons.cancel' | translate"
severity="info"
(onClick)="dialogRef.close()"
[disabled]="isSubmitting()"
/>
<p-button
class="btn-full-width"
styleClass="w-full"
[label]="'common.buttons.delete' | translate"
(onClick)="handleDeleteProject()"
severity="danger"
[disabled]="isSubmitting() || isLoading() || !isInputValid()"
[loading]="isSubmitting()"
/>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { createDispatchMap, select } from '@ngxs/store';

import { TranslatePipe } from '@ngx-translate/core';

import { Button } from 'primeng/button';
import { 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 { Router } from '@angular/router';

import { DeleteProject, SettingsSelectors } from '@osf/features/project/settings/store';
import { ScientistsNames } from '@osf/shared/constants';
import { ToastService } from '@osf/shared/services';
import { CurrentResourceSelectors } from '@osf/shared/stores';
import { UserPermissions } from '@shared/enums';

@Component({
selector: 'osf-delete-project-dialog',
imports: [TranslatePipe, Button, InputText, FormsModule, Skeleton],
templateUrl: './delete-project-dialog.component.html',
styleUrl: './delete-project-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeleteProjectDialogComponent {
private toastService = inject(ToastService);
private router = inject(Router);
dialogRef = inject(DynamicDialogRef);
destroyRef = inject(DestroyRef);

scientistNames = ScientistsNames;
userInput = signal('');

isLoading = select(CurrentResourceSelectors.isResourceWithChildrenLoading);
isSubmitting = select(SettingsSelectors.isSettingsSubmitting);
projects = select(CurrentResourceSelectors.getResourceWithChildren);

hasAdminAccessForAllComponents = computed(() => {
const projects = this.projects();
if (!projects || !projects.length) return false;

return projects.every((project) => {
return project.permissions?.includes(UserPermissions.Admin);
});
});

selectedScientist = computed(() => {
const names = Object.values(this.scientistNames);
return names[Math.floor(Math.random() * names.length)];
});

actions = createDispatchMap({
deleteProject: DeleteProject,
});

isInputValid(): boolean {
return this.userInput() === this.selectedScientist();
}

onInputChange(value: string): void {
this.userInput.set(value);
}

handleDeleteProject(): void {
const projects = this.projects();

if (!projects || !projects.length) return;

this.actions
.deleteProject(projects)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
this.dialogRef.close({ success: true });
this.toastService.showSuccess('project.deleteProject.success');
this.router.navigate(['/']);
},
});
}
}
1 change: 1 addition & 0 deletions src/app/features/project/settings/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { DeleteProjectDialogComponent } from './delete-project-dialog/delete-project-dialog.component';
export { ProjectDetailSettingAccordionComponent } from './project-detail-setting-accordion/project-detail-setting-accordion.component';
export { ProjectSettingNotificationsComponent } from './project-setting-notifications/project-setting-notifications.component';
export { SettingsAccessRequestsCardComponent } from './settings-access-requests-card/settings-access-requests-card.component';
Expand Down
16 changes: 14 additions & 2 deletions src/app/features/project/settings/services/settings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ENVIRONMENT } from '@core/provider/environment.provider';
import { SubscriptionFrequency } from '@osf/shared/enums';
import { NotificationSubscriptionMapper } from '@osf/shared/mappers';
import {
NodeShortInfoModel,
NotificationSubscription,
NotificationSubscriptionGetResponseJsonApi,
ResponseJsonApi,
Expand Down Expand Up @@ -81,8 +82,19 @@ export class SettingsService {
.pipe(map((response) => SettingsMapper.fromNodeResponse(response)));
}

deleteProject(projectId: string): Observable<void> {
return this.jsonApiService.delete(`${this.apiUrl}/nodes/${projectId}/`);
deleteProject(projects: NodeShortInfoModel[]): Observable<void> {
const payload = {
data: projects.map((project) => ({
type: 'nodes',
id: project.id,
})),
};

const headers = {
'Content-Type': 'application/vnd.api+json; ext=bulk',
};

return this.jsonApiService.delete(`${this.apiUrl}/nodes/`, payload, headers);
}

deleteInstitution(institutionId: string, projectId: string): Observable<void> {
Expand Down
41 changes: 25 additions & 16 deletions src/app/features/project/settings/settings.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,25 @@ import { map, of } from 'rxjs';
import { ChangeDetectionStrategy, Component, computed, effect, inject, OnInit, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';

import { UserSelectors } from '@core/store/user';
import { LoadingSpinnerComponent, SubHeaderComponent } from '@osf/shared/components';
import { ResourceType, SubscriptionEvent, SubscriptionFrequency, UserPermissions } from '@osf/shared/enums';
import { IS_MEDIUM } from '@osf/shared/helpers';
import { Institution, UpdateNodeRequestModel, ViewOnlyLinkModel } from '@osf/shared/models';
import { CustomConfirmationService, LoaderService, ToastService } from '@osf/shared/services';
import { DeleteViewOnlyLink, FetchViewOnlyLinks, GetResource, ViewOnlyLinkSelectors } from '@osf/shared/stores';
import { CustomConfirmationService, CustomDialogService, LoaderService, ToastService } from '@osf/shared/services';
import {
CurrentResourceSelectors,
DeleteViewOnlyLink,
FetchViewOnlyLinks,
GetResource,
GetResourceWithChildren,
ViewOnlyLinkSelectors,
} from '@osf/shared/stores';

import {
DeleteProjectDialogComponent,
ProjectSettingNotificationsComponent,
SettingsAccessRequestsCardComponent,
SettingsProjectAffiliationComponent,
Expand Down Expand Up @@ -60,8 +69,8 @@ import {
})
export class SettingsComponent implements OnInit {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly customConfirmationService = inject(CustomConfirmationService);
private readonly customDialogService = inject(CustomDialogService);
private readonly toastService = inject(ToastService);
private readonly loaderService = inject(LoaderService);

Expand All @@ -75,6 +84,10 @@ export class SettingsComponent implements OnInit {
viewOnlyLinks = select(ViewOnlyLinkSelectors.getViewOnlyLinks);
isViewOnlyLinksLoading = select(ViewOnlyLinkSelectors.isViewOnlyLinksLoading);
currentUser = select(UserSelectors.getCurrentUser);
currentProject = select(CurrentResourceSelectors.getCurrentResource);
isMedium = toSignal(inject(IS_MEDIUM));

rootProjectId = computed(() => this.currentProject()?.rootResourceId);

actions = createDispatchMap({
getSettings: GetProjectSettings,
Expand All @@ -88,6 +101,7 @@ export class SettingsComponent implements OnInit {
deleteProject: DeleteProject,
deleteInstitution: DeleteInstitution,
refreshCurrentResource: GetResource,
getComponentsTree: GetResourceWithChildren,
});

accessRequest = signal(false);
Expand Down Expand Up @@ -176,18 +190,13 @@ export class SettingsComponent implements OnInit {
}

deleteProject(): void {
this.customConfirmationService.confirmDelete({
headerKey: 'project.deleteProject.title',
messageParams: { name: this.projectDetails().title },
messageKey: 'project.deleteProject.message',
onConfirm: () => {
this.loaderService.show();
this.actions.deleteProject(this.projectId()).subscribe(() => {
this.loaderService.hide();
this.toastService.showSuccess('project.deleteProject.success');
this.router.navigate(['/']);
});
},
const dialogWidth = this.isMedium() ? '500px' : '95vw';

this.actions.getComponentsTree(this.rootProjectId() || this.projectId(), this.projectId(), ResourceType.Project);

this.customDialogService.open(DeleteProjectDialogComponent, {
header: 'project.deleteProject.dialog.deleteProject',
width: dialogWidth,
});
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/features/project/settings/store/settings.actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SubscriptionFrequency } from '@osf/shared/enums';
import { UpdateNodeRequestModel } from '@shared/models';
import { NodeShortInfoModel, UpdateNodeRequestModel } from '@shared/models';

import { ProjectSettingsData } from '../models';

Expand Down Expand Up @@ -42,7 +42,7 @@ export class UpdateProjectNotificationSubscription {
export class DeleteProject {
static readonly type = '[Project Settings] Delete Project';

constructor(public projectId: string) {}
constructor(public projects: NodeShortInfoModel[]) {}
}

export class DeleteInstitution {
Expand Down
1 change: 1 addition & 0 deletions src/app/features/project/settings/store/settings.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const SETTINGS_STATE_DEFAULTS: SettingsStateModel = {
settings: {
data: {} as ProjectSettingsModel,
isLoading: false,
isSubmitting: false,
error: null,
},
projectDetails: {
Expand Down
5 changes: 5 additions & 0 deletions src/app/features/project/settings/store/settings.selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export class SettingsSelectors {
return state.projectDetails.isLoading;
}

@Selector([SettingsState])
static isSettingsSubmitting(state: SettingsStateModel): boolean {
return state.settings.isSubmitting || false;
}

@Selector([SettingsState])
static getNotificationSubscriptions(state: SettingsStateModel): NotificationSubscription[] {
return state.notifications.data;
Expand Down
Loading
Loading