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
6 changes: 4 additions & 2 deletions src/app/core/services/json-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ export class JsonApiService {
});
}

patch<T>(url: string, body: unknown): Observable<T> {
return this.http.patch<JsonApiResponse<T, null>>(url, body).pipe(map((response) => response.data));
patch<T>(url: string, body: unknown, params?: Record<string, unknown>): Observable<T> {
return this.http
.patch<JsonApiResponse<T, null>>(url, body, { params: this.buildHttpParams(params) })
.pipe(map((response) => response.data));
}

put<T>(url: string, body: unknown, params?: Record<string, unknown>): Observable<T> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
<p class="mb-2">{{ 'registries.review.confirmation.remember' | translate }}</p>
<ul class="mb-4 pl-5">
<li class="list-disc">{{ 'registries.review.confirmation.text1' | translate }}</li>
<li class="list-disc">{{ 'registries.review.confirmation.text2' | translate }}</li>
<li class="list-disc">{{ 'registries.review.confirmation.text3' | translate }}</li>
<li class="list-disc">{{ 'registries.review.confirmation.text4' | translate }}</li>
</ul>
<p class="mb-2">
{{ 'registries.review.confirmation.text1' | translate }}
<strong>
{{ 'registries.review.confirmation.text2' | translate }}
</strong>
</p>
<p class="mb-2">{{ 'registries.review.confirmation.text3' | translate }}</p>
<div class="mb-4">
<p>{{ 'registries.review.confirmation.text4' | translate }}</p>
<p>
(
<a href="https://help.osf.io/article/152-withdraw-a-registration" target="_blank" rel="noopener noreferrer">
{{ 'common.labels.learnMore' | translate }}
</a>
)
</p>
</div>

<form [formGroup]="form">
<div class="flex align-items-center gap-2 mb-2">
<p-radioButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,12 @@ export class ConfirmRegistrationDialogComponent {
this.config.data.draftId,
this.form.value.embargoDate,
this.config.data.providerId,
this.config.data.projectId
this.config.data.projectId,
this.config.data.components
)
.subscribe({
complete: () => {
this.dialogRef.close();
this.dialogRef.close(true);
},
});
}
Expand Down
29 changes: 25 additions & 4 deletions src/app/features/registries/components/drafts/drafts.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core';

import { filter, tap } from 'rxjs';

import { ChangeDetectionStrategy, Component, computed, effect, inject, Signal, signal } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, effect, inject, OnDestroy, Signal, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';

import { StepperComponent, SubHeaderComponent } from '@osf/shared/components';
import { ResourceType } from '@osf/shared/enums';
import { StepOption } from '@osf/shared/models';
import { LoaderService } from '@osf/shared/services';
import { ContributorsSelectors, SubjectsSelectors } from '@osf/shared/stores';
import {
ContributorsSelectors,
FetchSelectedSubjects,
GetAllContributors,
SubjectsSelectors,
} from '@osf/shared/stores';

import { DEFAULT_STEPS } from '../../constants';
import { FetchDraft, FetchSchemaBlocks, RegistriesSelectors, UpdateStepValidation } from '../../store';
import { ClearState, FetchDraft, FetchSchemaBlocks, RegistriesSelectors, UpdateStepValidation } from '../../store';

@Component({
selector: 'osf-drafts',
Expand All @@ -24,7 +30,7 @@ import { FetchDraft, FetchSchemaBlocks, RegistriesSelectors, UpdateStepValidatio
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [TranslateService],
})
export class DraftsComponent {
export class DraftsComponent implements OnDestroy {
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
private readonly loaderService = inject(LoaderService);
Expand All @@ -36,11 +42,16 @@ export class DraftsComponent {
protected readonly stepsData = select(RegistriesSelectors.getStepsData);
protected selectedSubjects = select(SubjectsSelectors.getSelectedSubjects);
protected initialContributors = select(ContributorsSelectors.getContributors);
protected readonly contributors = select(ContributorsSelectors.getContributors);
protected readonly subjects = select(SubjectsSelectors.getSelectedSubjects);

private readonly actions = createDispatchMap({
getSchemaBlocks: FetchSchemaBlocks,
getDraftRegistration: FetchDraft,
updateStepValidation: UpdateStepValidation,
clearState: ClearState,
getContributors: GetAllContributors,
getSubjects: FetchSelectedSubjects,
});

get isReviewPage(): boolean {
Expand Down Expand Up @@ -116,6 +127,12 @@ export class DraftsComponent {
if (!this.draftRegistration()) {
this.actions.getDraftRegistration(this.registrationId);
}
if (!this.contributors()?.length) {
this.actions.getContributors(this.registrationId, ResourceType.DraftRegistration);
}
if (!this.subjects()?.length) {
this.actions.getSubjects(this.registrationId, ResourceType.DraftRegistration);
}
effect(() => {
const registrationSchemaId = this.draftRegistration()?.registrationSchemaId;
if (registrationSchemaId && !this.isLoaded) {
Expand Down Expand Up @@ -162,4 +179,8 @@ export class DraftsComponent {
const pageLink = this.steps()[step.index].routeLink;
this.router.navigate([`/registries/drafts/${this.registrationId}/`, pageLink]);
}

ngOnDestroy(): void {
this.actions.clearState();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ <h4 class="mb-2">{{ question.displayText }}</h4>
<p-button
type="button"
[label]="'registries.review.register' | translate"
[disabled]="isDraftInvalid()"
[disabled]="isDraftLoading() || isDraftInvalid()"
(click)="confirmRegistration()"
></p-button>
</div>
Expand Down
65 changes: 58 additions & 7 deletions src/app/features/registries/components/review/review.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Tag } from 'primeng/tag';

import { map, of } from 'rxjs';

import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, effect, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';

Expand All @@ -25,8 +25,9 @@ import {
} from '@osf/shared/stores';

import { FieldType } from '../../enums';
import { DeleteDraft, RegisterDraft, RegistriesSelectors } from '../../store';
import { DeleteDraft, FetchProjectChildren, RegistriesSelectors } from '../../store';
import { ConfirmRegistrationDialogComponent } from '../confirm-registration-dialog/confirm-registration-dialog.component';
import { SelectComponentsDialogComponent } from '../select-components-dialog/select-components-dialog.component';

@Component({
selector: 'osf-review',
Expand All @@ -46,17 +47,20 @@ export class ReviewComponent {

protected readonly pages = select(RegistriesSelectors.getPagesSchema);
protected readonly draftRegistration = select(RegistriesSelectors.getDraftRegistration);
protected readonly isDraftSubmitting = select(RegistriesSelectors.isDraftSubmitting);
protected readonly isDraftLoading = select(RegistriesSelectors.isDraftLoading);
protected readonly stepsData = select(RegistriesSelectors.getStepsData);
protected readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES;
protected readonly contributors = select(ContributorsSelectors.getContributors);
protected readonly subjects = select(SubjectsSelectors.getSelectedSubjects);
protected readonly components = select(RegistriesSelectors.getRegistrationComponents);
protected readonly FieldType = FieldType;

protected actions = createDispatchMap({
getContributors: GetAllContributors,
getSubjects: FetchSelectedSubjects,
deleteDraft: DeleteDraft,
registerDraft: RegisterDraft,
getProjectsComponents: FetchProjectChildren,
});

private readonly draftId = toSignal(this.route.params.pipe(map((params) => params['id'])) ?? of(undefined));
Expand All @@ -74,6 +78,19 @@ export class ReviewComponent {
if (!this.subjects()?.length) {
this.actions.getSubjects(this.draftId(), ResourceType.DraftRegistration);
}

let componentsLoaded = false;
effect(() => {
if (!this.isDraftSubmitting()) {
const draftRegistrations = this.draftRegistration();
if (draftRegistrations?.hasProject) {
if (!componentsLoaded) {
this.actions.getProjectsComponents(draftRegistrations?.branchedFrom?.id ?? '');
componentsLoaded = true;
}
}
}
});
}

goBack(): void {
Expand All @@ -97,6 +114,32 @@ export class ReviewComponent {
}

confirmRegistration(): void {
if (this.components()?.length) {
this.openSelectComponentsForRegistrationDialog();
} else {
this.openConfirmRegistrationDialog();
}
}

openSelectComponentsForRegistrationDialog(): void {
this.dialogService
.open(SelectComponentsDialogComponent, {
width: '552px',
focusOnShow: false,
header: this.translateService.instant('registries.review.selectComponents.title'),
closeOnEscape: true,
modal: true,
data: {
parent: this.draftRegistration()?.branchedFrom,
components: this.components(),
},
})
.onClose.subscribe((selectedComponents) => {
this.openConfirmRegistrationDialog(selectedComponents);
});
}

openConfirmRegistrationDialog(components?: string[]): void {
this.dialogService
.open(ConfirmRegistrationDialogComponent, {
width: '552px',
Expand All @@ -106,13 +149,21 @@ export class ReviewComponent {
modal: true,
data: {
draftId: this.draftId(),
projectId: this.draftRegistration()?.branchedFrom,
projectId: this.draftRegistration()?.branchedFrom?.id,
providerId: this.draftRegistration()?.providerId,
components,
},
})
.onClose.subscribe(() => {
this.toastService.showSuccess('registries.review.confirmation.successMessage');
// [NM] TODO: Navigate to the newly created registration page
.onClose.subscribe((res) => {
if (res) {
this.toastService.showSuccess('registries.review.confirmation.successMessage');
// [NM] TODO: Navigate to the newly created registration page
this.router.navigate([`registries/my-registrations`]);
} else {
if (this.components()?.length) {
this.openSelectComponentsForRegistrationDialog();
}
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<p class="mb-2">{{ 'registries.review.selectComponents.description' | translate }}</p>

<p-tree [value]="components" selectionMode="checkbox" class="w-full md:w-[30rem]" [(selection)]="selectedComponents" />

<div class="flex justify-content-end gap-2 mt-4">
<p-button
class="w-12rem btn-full-width"
[label]="'common.buttons.back' | translate"
severity="info"
(click)="dialogRef.close()"
/>
<p-button class="w-12rem btn-full-width" [label]="'common.buttons.continue' | translate" (click)="continue()" />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { SelectComponentsDialogComponent } from './select-components-dialog.component';

describe('SelectComponentsDialogComponent', () => {
let component: SelectComponentsDialogComponent;
let fixture: ComponentFixture<SelectComponentsDialogComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SelectComponentsDialogComponent],
}).compileComponents();

fixture = TestBed.createComponent(SelectComponentsDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { TranslatePipe } from '@ngx-translate/core';

import { TreeNode } from 'primeng/api';
import { Button } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { Tree } from 'primeng/tree';

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

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

@Component({
selector: 'osf-select-components-dialog',
imports: [TranslatePipe, Tree, Button],
templateUrl: './select-components-dialog.component.html',
styleUrl: './select-components-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SelectComponentsDialogComponent {
protected readonly dialogRef = inject(DynamicDialogRef);
readonly config = inject(DynamicDialogConfig);
selectedComponents: TreeNode[] = [];
parent: Project = this.config.data.parent;
components: TreeNode[] = [];

constructor() {
this.components = [
{
label: this.parent.title,
selectable: false,
expanded: true,
key: this.parent.id,
data: this.parent.id,
children: this.config.data.components.map(this.mapProjectToTreeNode),
},
];
this.selectedComponents.push({ key: this.parent.id });
}

private mapProjectToTreeNode = (project: Project): TreeNode => {
this.selectedComponents.push({
key: project.id,
});
return {
label: project.title,
data: project.id,
key: project.id,
expanded: true,
children: project.children?.map(this.mapProjectToTreeNode) ?? [],
};
};

continue() {
const selectedComponentsSet = new Set<string>([...this.selectedComponents.map((c) => c.key!), this.parent.id]);
this.dialogRef.close([...selectedComponentsSet]);
}
}
1 change: 1 addition & 0 deletions src/app/features/registries/models/project.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface Project {
id: string;
title: string;
children?: Project[];
}
28 changes: 27 additions & 1 deletion src/app/features/registries/services/projects.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { map, Observable } from 'rxjs';
import { forkJoin, map, Observable, of, switchMap } from 'rxjs';

import { inject, Injectable } from '@angular/core';

Expand All @@ -25,4 +25,30 @@ export class ProjectsService {
.get<ProjectsResponseJsonApi>(`${this.apiUrl}/users/me/nodes/`, params)
.pipe(map((response) => ProjectsMapper.fromProjectsResponse(response)));
}

getProjectChildren(id: string): Observable<Project[]> {
return this.jsonApiService
.get<ProjectsResponseJsonApi>(`${this.apiUrl}/nodes/${id}/children`)
.pipe(map((response) => ProjectsMapper.fromProjectsResponse(response)));
}

getComponentsTree(id: string): Observable<Project[]> {
return this.getProjectChildren(id).pipe(
switchMap((children) => {
if (!children.length) {
return of([]);
}
const childrenWithSubtrees$ = children.map((child) =>
this.getComponentsTree(child.id).pipe(
map((subChildren) => ({
...child,
children: subChildren,
}))
)
);

return childrenWithSubtrees$.length ? forkJoin(childrenWithSubtrees$) : of([]);
})
);
}
}
Loading