From c316b162bd8c037bf3c0000129bc6476ce41e07d Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Tue, 15 Jul 2025 23:19:29 +0300 Subject: [PATCH 1/2] feat(registration): work on select components dialog --- ...confirm-registration-dialog.component.html | 25 ++++++-- .../confirm-registration-dialog.component.ts | 2 +- .../components/drafts/drafts.component.ts | 11 +++- .../components/review/review.component.ts | 64 +++++++++++++++++-- .../select-components-dialog.component.html | 13 ++++ .../select-components-dialog.component.scss | 0 ...select-components-dialog.component.spec.ts | 22 +++++++ .../select-components-dialog.component.ts | 47 ++++++++++++++ src/app/features/registries/models/project.ts | 1 + .../registries/services/projects.service.ts | 28 +++++++- .../store/handlers/projects.handlers.ts | 31 +++++++++ .../registries/store/registries.actions.ts | 9 +++ .../registries/store/registries.selectors.ts | 4 ++ .../registries/store/registries.state.ts | 12 ++++ .../registration/registration.mapper.ts | 4 +- .../registration/draft-registration.model.ts | 3 + .../registration-json-api.model.ts | 6 ++ src/assets/i18n/en.json | 18 ++++-- 18 files changed, 275 insertions(+), 25 deletions(-) create mode 100644 src/app/features/registries/components/select-components-dialog/select-components-dialog.component.html create mode 100644 src/app/features/registries/components/select-components-dialog/select-components-dialog.component.scss create mode 100644 src/app/features/registries/components/select-components-dialog/select-components-dialog.component.spec.ts create mode 100644 src/app/features/registries/components/select-components-dialog/select-components-dialog.component.ts diff --git a/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.html b/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.html index ce3f4ae63..9379b23c6 100644 --- a/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.html +++ b/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.html @@ -1,10 +1,21 @@ -

{{ 'registries.review.confirmation.remember' | translate }}

- +

+ {{ 'registries.review.confirmation.text1' | translate }} + + {{ 'registries.review.confirmation.text2' | translate }} + +

+

{{ 'registries.review.confirmation.text3' | translate }}

+
+

{{ 'registries.review.confirmation.text4' | translate }}

+

+ ( + + {{ 'common.labels.learnMore' | translate }} + + ) +

+
+
{ - this.dialogRef.close(); + this.dialogRef.close(true); }, }); } diff --git a/src/app/features/registries/components/drafts/drafts.component.ts b/src/app/features/registries/components/drafts/drafts.component.ts index b5056408b..a5550b06f 100644 --- a/src/app/features/registries/components/drafts/drafts.component.ts +++ b/src/app/features/registries/components/drafts/drafts.component.ts @@ -4,7 +4,7 @@ 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'; @@ -14,7 +14,7 @@ import { LoaderService } from '@osf/shared/services'; import { ContributorsSelectors, 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', @@ -24,7 +24,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); @@ -41,6 +41,7 @@ export class DraftsComponent { getSchemaBlocks: FetchSchemaBlocks, getDraftRegistration: FetchDraft, updateStepValidation: UpdateStepValidation, + clearState: ClearState, }); get isReviewPage(): boolean { @@ -162,4 +163,8 @@ export class DraftsComponent { const pageLink = this.steps()[step.index].routeLink; this.router.navigate([`/registries/drafts/${this.registrationId}/`, pageLink]); } + + ngOnDestroy(): void { + this.actions.clearState(); + } } diff --git a/src/app/features/registries/components/review/review.component.ts b/src/app/features/registries/components/review/review.component.ts index 76ba85832..90bcab3c9 100644 --- a/src/app/features/registries/components/review/review.component.ts +++ b/src/app/features/registries/components/review/review.component.ts @@ -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'; @@ -25,8 +25,9 @@ import { } from '@osf/shared/stores'; import { FieldType } from '../../enums'; -import { DeleteDraft, RegisterDraft, RegistriesSelectors } from '../../store'; +import { DeleteDraft, FetchProjectChildren, RegisterDraft, 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', @@ -46,10 +47,12 @@ export class ReviewComponent { protected readonly pages = select(RegistriesSelectors.getPagesSchema); protected readonly draftRegistration = select(RegistriesSelectors.getDraftRegistration); + protected readonly isDraftSubmitting = select(RegistriesSelectors.isDraftSubmitting); 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({ @@ -57,6 +60,7 @@ export class ReviewComponent { getSubjects: FetchSelectedSubjects, deleteDraft: DeleteDraft, registerDraft: RegisterDraft, + getProjectsComponents: FetchProjectChildren, }); private readonly draftId = toSignal(this.route.params.pipe(map((params) => params['id'])) ?? of(undefined)); @@ -74,6 +78,21 @@ export class ReviewComponent { if (!this.subjects()?.length) { this.actions.getSubjects(this.draftId(), ResourceType.DraftRegistration); } + + let componentsLoaded = false; + effect(() => { + console.log('components effect triggered', this.components(), this.isDraftSubmitting()); + if (!this.isDraftSubmitting()) { + const draftRegistrations = this.draftRegistration(); + if (draftRegistrations?.hasProject) { + console.log('Fetching project children for draft registration', draftRegistrations.branchedFrom); + if (!componentsLoaded) { + this.actions.getProjectsComponents(draftRegistrations.branchedFrom!); + componentsLoaded = true; + } + } + } + }); } goBack(): void { @@ -97,6 +116,34 @@ 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: { + components: this.components(), + }, + }) + .onClose.subscribe((selectedComponents) => { + console.log('Selected components for registration:', selectedComponents); + if (selectedComponents) { + this.openConfirmRegistrationDialog(); + } + }); + } + + openConfirmRegistrationDialog(): void { this.dialogService .open(ConfirmRegistrationDialogComponent, { width: '552px', @@ -110,9 +157,16 @@ export class ReviewComponent { providerId: this.draftRegistration()?.providerId, }, }) - .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(); + } + } }); } } diff --git a/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.html b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.html new file mode 100644 index 000000000..4b55e05b6 --- /dev/null +++ b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.html @@ -0,0 +1,13 @@ +

{{ 'registries.review.selectComponents.description' | translate }}

+ + + +
+ + +
diff --git a/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.scss b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.spec.ts b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.spec.ts new file mode 100644 index 000000000..7dacb1255 --- /dev/null +++ b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SelectComponentsDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SelectComponentsDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.ts b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.ts new file mode 100644 index 000000000..3a2670021 --- /dev/null +++ b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.ts @@ -0,0 +1,47 @@ +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[] = []; + components: TreeNode[] = []; + + constructor() { + this.components = this.config.data.components.map(this.mapProjectToTreeNode); + console.log('SelectComponentsDialogComponent initialized with components:', this.components); + } + + private mapProjectToTreeNode = (project: Project): TreeNode => { + this.selectedComponents.push({ + key: project.id, + }); + return { + label: project.title, + data: project.id, + key: project.id, + selectable: true, + expanded: true, + children: project.children?.map(this.mapProjectToTreeNode) ?? [], + }; + }; + + continue() { + this.dialogRef.close(this.selectedComponents); + } +} diff --git a/src/app/features/registries/models/project.ts b/src/app/features/registries/models/project.ts index 870a40589..dc1ae9d02 100644 --- a/src/app/features/registries/models/project.ts +++ b/src/app/features/registries/models/project.ts @@ -1,4 +1,5 @@ export interface Project { id: string; title: string; + children?: Project[]; } diff --git a/src/app/features/registries/services/projects.service.ts b/src/app/features/registries/services/projects.service.ts index 414e5e497..f4be15e04 100644 --- a/src/app/features/registries/services/projects.service.ts +++ b/src/app/features/registries/services/projects.service.ts @@ -1,4 +1,4 @@ -import { map, Observable } from 'rxjs'; +import { forkJoin, map, Observable, of, switchMap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; @@ -25,4 +25,30 @@ export class ProjectsService { .get(`${this.apiUrl}/users/me/nodes/`, params) .pipe(map((response) => ProjectsMapper.fromProjectsResponse(response))); } + + getProjectChildren(id: string): Observable { + return this.jsonApiService + .get(`${this.apiUrl}/nodes/${id}/children`) + .pipe(map((response) => ProjectsMapper.fromProjectsResponse(response))); + } + + getComponentsTree(id: string): Observable { + 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([]); + }) + ); + } } diff --git a/src/app/features/registries/store/handlers/projects.handlers.ts b/src/app/features/registries/store/handlers/projects.handlers.ts index d0f161842..75fce07c9 100644 --- a/src/app/features/registries/store/handlers/projects.handlers.ts +++ b/src/app/features/registries/store/handlers/projects.handlers.ts @@ -35,4 +35,35 @@ export class ProjectsHandlers { }, }); } + + fetchProjectChildren(ctx: StateContext, projectId: string) { + const state = ctx.getState(); + ctx.patchState({ + draftRegistration: { + data: state.draftRegistration.data, + isLoading: true, + error: null, + }, + }); + + return this.projectsService.getComponentsTree(projectId).subscribe({ + next: (children: Project[]) => { + ctx.patchState({ + draftRegistration: { + data: { + ...state.draftRegistration.data!, + components: [...state.draftRegistration.data!.components, ...children], + }, + isLoading: false, + error: null, + }, + }); + }, + error: (error) => { + ctx.patchState({ + projects: { ...state.projects, isLoading: false, error }, + }); + }, + }); + } } diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index cc17adbc9..f666417da 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -93,3 +93,12 @@ export class FetchSubmittedRegistrations { public pageSize = 10 ) {} } + +export class FetchProjectChildren { + static readonly type = '[Registries] Fetch Project Children'; + constructor(public projectId: string) {} +} + +export class ClearState { + static readonly type = '[Registries] Clear State'; +} diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts index 90d08517c..59f319b85 100644 --- a/src/app/features/registries/store/registries.selectors.ts +++ b/src/app/features/registries/store/registries.selectors.ts @@ -112,4 +112,8 @@ export class RegistriesSelectors { static getSubmittedRegistrationsTotalCount(state: RegistriesStateModel): number { return state.submittedRegistrations.totalCount; } + @Selector([RegistriesState]) + static getRegistrationComponents(state: RegistriesStateModel) { + return state.draftRegistration.data?.components || []; + } } diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index 990bf2a84..6c51468eb 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -16,11 +16,13 @@ import { ProjectsHandlers } from './handlers/projects.handlers'; import { ProvidersHandlers } from './handlers/providers.handlers'; import { DefaultState } from './default.state'; import { + ClearState, CreateDraft, DeleteDraft, FetchDraft, FetchDraftRegistrations, FetchLicenses, + FetchProjectChildren, FetchSchemaBlocks, FetchSubmittedRegistrations, GetProjects, @@ -77,6 +79,11 @@ export class RegistriesState { return this.projectsHandler.getProjects(ctx); } + @Action(FetchProjectChildren) + fetchProjectChildren(ctx: StateContext, { projectId }: FetchProjectChildren) { + return this.projectsHandler.fetchProjectChildren(ctx, projectId); + } + @Action(GetProviderSchemas) getProviders(ctx: StateContext, { providerId }: GetProviderSchemas) { return this.providersHandler.getProviderSchemas(ctx, providerId); @@ -306,4 +313,9 @@ export class RegistriesState { catchError((error) => handleSectionError(ctx, 'submittedRegistrations', error)) ); } + + @Action(ClearState) + clearState(ctx: StateContext) { + ctx.setState({ ...DefaultState }); + } } diff --git a/src/app/shared/mappers/registration/registration.mapper.ts b/src/app/shared/mappers/registration/registration.mapper.ts index ed50c01e3..249c50163 100644 --- a/src/app/shared/mappers/registration/registration.mapper.ts +++ b/src/app/shared/mappers/registration/registration.mapper.ts @@ -26,7 +26,9 @@ export class RegistrationMapper { tags: response.attributes.tags || [], stepsData: response.attributes.registration_responses || {}, branchedFrom: response.relationships.branched_from?.data?.id, - providerId: response.relationships.registration_schema?.data?.id || '', + providerId: response.relationships.provider?.data?.id || '', + hasProject: !!response.attributes.has_project, + components: [], }; } diff --git a/src/app/shared/models/registration/draft-registration.model.ts b/src/app/shared/models/registration/draft-registration.model.ts index 19bfa8f41..99e9e9287 100644 --- a/src/app/shared/models/registration/draft-registration.model.ts +++ b/src/app/shared/models/registration/draft-registration.model.ts @@ -1,4 +1,5 @@ import { LicenseOptions } from '../license.model'; +import { Project } from '../projects'; export interface DraftRegistrationModel { id: string; @@ -14,4 +15,6 @@ export interface DraftRegistrationModel { stepsData?: Record; branchedFrom?: string; providerId: string; + hasProject: boolean; + components: Partial[]; } diff --git a/src/app/shared/models/registration/registration-json-api.model.ts b/src/app/shared/models/registration/registration-json-api.model.ts index e79d94840..09aec50d6 100644 --- a/src/app/shared/models/registration/registration-json-api.model.ts +++ b/src/app/shared/models/registration/registration-json-api.model.ts @@ -58,6 +58,12 @@ export interface DraftRegistrationRelationshipsJsonApi { type: 'registration-schemas'; }; }; + provider?: { + data: { + id: string; + type: 'providers'; + }; + }; license?: { data: { id: string; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 0cbdc1a69..af93c7143 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -88,7 +88,8 @@ "available": "Available", "unavailable": "Unavailable", "notApplicable": "Not Applicable", - "none": "None" + "none": "None", + "learnMore": "Learn More" }, "deleteConfirmation": { "header": "Delete", @@ -1878,13 +1879,16 @@ "step": "Review", "title": "Review Registration", "register": "Register", + "selectComponents": { + "title": "Select Components", + "description": "Contributor changes for included components must be done on the component contributor page before submitting this registration." + }, "confirmation": { "title": "Almost done...", - "remember": "Remember:", - "text1": "Do not edit any files until the registration has completely archived.", - "text2": "This will be permanent and cannot be deleted once submitted.", - "text3": "This registration will be copied to Internet Archive as a backup.", - "text4": "Title and contributors cannot be updated once submitted.", + "text1": "Embargo your registration if you plan to submit to a journal that requires", + "text2": "blind peer review", + "text3": "Do not edit any files until the registration archives.", + "text4": "Registrations can't be deleted, but you can withdraw.", "options": { "public": "Make registration public immediately", "embargo": "Enter registration into embargo" @@ -2120,7 +2124,7 @@ "publicRegistrations": "Public Registrations" }, "institutionUsers": { - "allDepartments": "All departments", + "allDepartments": "All departments", "lastLogin": "Last Login", "lastActive": "Last Active", "accountCreated": "Account Created", From 1116288eed1df1805b41452f150662437ee17ef3 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Wed, 16 Jul 2025 15:50:18 +0300 Subject: [PATCH 2/2] feat(registration): add components to registration request --- src/app/core/services/json-api.service.ts | 6 ++++-- .../confirm-registration-dialog.component.ts | 3 ++- .../components/drafts/drafts.component.ts | 18 +++++++++++++++++- .../components/review/review.component.html | 2 +- .../components/review/review.component.ts | 19 ++++++++----------- .../select-components-dialog.component.ts | 18 ++++++++++++++---- .../registries/services/registries.service.ts | 15 +++++++++++---- .../registries/store/registries.actions.ts | 3 ++- .../registries/store/registries.selectors.ts | 5 +++++ .../registries/store/registries.state.ts | 4 ++-- .../registration/registration.mapper.ts | 14 ++++++++++++-- .../registration/draft-registration.model.ts | 2 +- .../registration-json-api.model.ts | 13 +++++++++++-- 13 files changed, 90 insertions(+), 32 deletions(-) diff --git a/src/app/core/services/json-api.service.ts b/src/app/core/services/json-api.service.ts index 351949b06..78a6521a7 100644 --- a/src/app/core/services/json-api.service.ts +++ b/src/app/core/services/json-api.service.ts @@ -43,8 +43,10 @@ export class JsonApiService { }); } - patch(url: string, body: unknown): Observable { - return this.http.patch>(url, body).pipe(map((response) => response.data)); + patch(url: string, body: unknown, params?: Record): Observable { + return this.http + .patch>(url, body, { params: this.buildHttpParams(params) }) + .pipe(map((response) => response.data)); } put(url: string, body: unknown, params?: Record): Observable { diff --git a/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.ts b/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.ts index 52b3370b6..a9f6a5e0b 100644 --- a/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.ts +++ b/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.ts @@ -66,7 +66,8 @@ 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: () => { diff --git a/src/app/features/registries/components/drafts/drafts.component.ts b/src/app/features/registries/components/drafts/drafts.component.ts index a5550b06f..2659e33e2 100644 --- a/src/app/features/registries/components/drafts/drafts.component.ts +++ b/src/app/features/registries/components/drafts/drafts.component.ts @@ -9,9 +9,15 @@ 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 { ClearState, FetchDraft, FetchSchemaBlocks, RegistriesSelectors, UpdateStepValidation } from '../../store'; @@ -36,12 +42,16 @@ export class DraftsComponent implements OnDestroy { 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 { @@ -117,6 +127,12 @@ export class DraftsComponent implements OnDestroy { 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) { diff --git a/src/app/features/registries/components/review/review.component.html b/src/app/features/registries/components/review/review.component.html index 468e37121..f54c10636 100644 --- a/src/app/features/registries/components/review/review.component.html +++ b/src/app/features/registries/components/review/review.component.html @@ -125,7 +125,7 @@

{{ question.displayText }}

diff --git a/src/app/features/registries/components/review/review.component.ts b/src/app/features/registries/components/review/review.component.ts index 90bcab3c9..4c542628f 100644 --- a/src/app/features/registries/components/review/review.component.ts +++ b/src/app/features/registries/components/review/review.component.ts @@ -25,7 +25,7 @@ import { } from '@osf/shared/stores'; import { FieldType } from '../../enums'; -import { DeleteDraft, FetchProjectChildren, 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'; @@ -48,6 +48,7 @@ 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); @@ -59,7 +60,6 @@ export class ReviewComponent { getContributors: GetAllContributors, getSubjects: FetchSelectedSubjects, deleteDraft: DeleteDraft, - registerDraft: RegisterDraft, getProjectsComponents: FetchProjectChildren, }); @@ -81,13 +81,11 @@ export class ReviewComponent { let componentsLoaded = false; effect(() => { - console.log('components effect triggered', this.components(), this.isDraftSubmitting()); if (!this.isDraftSubmitting()) { const draftRegistrations = this.draftRegistration(); if (draftRegistrations?.hasProject) { - console.log('Fetching project children for draft registration', draftRegistrations.branchedFrom); if (!componentsLoaded) { - this.actions.getProjectsComponents(draftRegistrations.branchedFrom!); + this.actions.getProjectsComponents(draftRegistrations?.branchedFrom?.id ?? ''); componentsLoaded = true; } } @@ -132,18 +130,16 @@ export class ReviewComponent { closeOnEscape: true, modal: true, data: { + parent: this.draftRegistration()?.branchedFrom, components: this.components(), }, }) .onClose.subscribe((selectedComponents) => { - console.log('Selected components for registration:', selectedComponents); - if (selectedComponents) { - this.openConfirmRegistrationDialog(); - } + this.openConfirmRegistrationDialog(selectedComponents); }); } - openConfirmRegistrationDialog(): void { + openConfirmRegistrationDialog(components?: string[]): void { this.dialogService .open(ConfirmRegistrationDialogComponent, { width: '552px', @@ -153,8 +149,9 @@ 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((res) => { diff --git a/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.ts b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.ts index 3a2670021..4daae3712 100644 --- a/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.ts +++ b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.ts @@ -20,11 +20,21 @@ 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 = this.config.data.components.map(this.mapProjectToTreeNode); - console.log('SelectComponentsDialogComponent initialized with components:', this.components); + 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 => { @@ -35,13 +45,13 @@ export class SelectComponentsDialogComponent { label: project.title, data: project.id, key: project.id, - selectable: true, expanded: true, children: project.children?.map(this.mapProjectToTreeNode) ?? [], }; }; continue() { - this.dialogRef.close(this.selectedComponents); + const selectedComponentsSet = new Set([...this.selectedComponents.map((c) => c.key!), this.parent.id]); + this.dialogRef.close([...selectedComponentsSet]); } } diff --git a/src/app/features/registries/services/registries.service.ts b/src/app/features/registries/services/registries.service.ts index e3d1f3aa7..31690e320 100644 --- a/src/app/features/registries/services/registries.service.ts +++ b/src/app/features/registries/services/registries.service.ts @@ -57,8 +57,11 @@ export class RegistriesService { } getDraft(draftId: string): Observable { + const params = { + embed: ['branched_from'], + }; return this.jsonApiService - .get(`${this.apiUrl}/draft_registrations/${draftId}/`) + .get(`${this.apiUrl}/draft_registrations/${draftId}/`, params) .pipe(map((response) => RegistrationMapper.fromDraftRegistrationResponse(response.data))); } @@ -75,9 +78,12 @@ export class RegistriesService { type: 'draft_registrations', // force the correct type }, }; + const params = { + embed: ['branched_from'], + }; return this.jsonApiService - .patch(`${this.apiUrl}/draft_registrations/${id}/`, payload) + .patch(`${this.apiUrl}/draft_registrations/${id}/`, payload, params) .pipe(map((response) => RegistrationMapper.fromDraftRegistrationResponse(response))); } @@ -89,9 +95,10 @@ export class RegistriesService { draftId: string, embargoDate: string, providerId: string, - projectId?: string + projectId?: string, + components?: string[] ): Observable { - const payload = RegistrationMapper.toRegistrationPayload(draftId, embargoDate, providerId, projectId); + const payload = RegistrationMapper.toRegistrationPayload(draftId, embargoDate, providerId, projectId, components); return this.jsonApiService .post(`${this.apiUrl}/registrations/`, payload) .pipe(map((response) => RegistrationMapper.fromRegistrationResponse(response.data))); diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index f666417da..10bd0a03f 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -47,7 +47,8 @@ export class RegisterDraft { public draftId: string, public embargoDate: string, public providerId: string, - public projectId?: string + public projectId?: string, + public components?: string[] ) {} } diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts index 59f319b85..8a4acbffe 100644 --- a/src/app/features/registries/store/registries.selectors.ts +++ b/src/app/features/registries/store/registries.selectors.ts @@ -28,6 +28,11 @@ export class RegistriesSelectors { return state.draftRegistration.isSubmitting ?? false; } + @Selector([RegistriesState]) + static isDraftLoading(state: RegistriesStateModel): boolean { + return state.draftRegistration.isLoading; + } + @Selector([RegistriesState]) static getDraftRegistration(state: RegistriesStateModel): DraftRegistrationModel | null { return state.draftRegistration.data; diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index 6c51468eb..4fc10dc29 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -198,7 +198,7 @@ export class RegistriesState { @Action(RegisterDraft) registerDraft( ctx: StateContext, - { draftId, embargoDate, providerId, projectId }: RegisterDraft + { draftId, embargoDate, providerId, projectId, components }: RegisterDraft ) { ctx.patchState({ registration: { @@ -207,7 +207,7 @@ export class RegistriesState { }, }); - return this.registriesService.registerDraft(draftId, embargoDate, providerId, projectId).pipe( + return this.registriesService.registerDraft(draftId, embargoDate, providerId, projectId, components).pipe( tap((registration) => { ctx.patchState({ registration: { diff --git a/src/app/shared/mappers/registration/registration.mapper.ts b/src/app/shared/mappers/registration/registration.mapper.ts index 249c50163..236f3c719 100644 --- a/src/app/shared/mappers/registration/registration.mapper.ts +++ b/src/app/shared/mappers/registration/registration.mapper.ts @@ -25,7 +25,10 @@ export class RegistrationMapper { }, tags: response.attributes.tags || [], stepsData: response.attributes.registration_responses || {}, - branchedFrom: response.relationships.branched_from?.data?.id, + branchedFrom: { + id: response.embeds?.branched_from?.data.id, + title: response.embeds?.branched_from?.data.attributes.title, + }, providerId: response.relationships.provider?.data?.id || '', hasProject: !!response.attributes.has_project, components: [], @@ -75,13 +78,20 @@ export class RegistrationMapper { }; } - static toRegistrationPayload(draftId: string, embargoDate: string, providerId: string, projectId?: string) { + static toRegistrationPayload( + draftId: string, + embargoDate: string, + providerId: string, + projectId?: string, + components?: string[] + ) { return { data: { type: 'registrations', attributes: { embargo_end_date: embargoDate, draft_registration: draftId, + included_node_ids: components, }, relationships: { registered_from: projectId diff --git a/src/app/shared/models/registration/draft-registration.model.ts b/src/app/shared/models/registration/draft-registration.model.ts index 99e9e9287..7b40cf319 100644 --- a/src/app/shared/models/registration/draft-registration.model.ts +++ b/src/app/shared/models/registration/draft-registration.model.ts @@ -13,7 +13,7 @@ export interface DraftRegistrationModel { tags: string[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any stepsData?: Record; - branchedFrom?: string; + branchedFrom?: Partial; providerId: string; hasProject: boolean; components: Partial[]; diff --git a/src/app/shared/models/registration/registration-json-api.model.ts b/src/app/shared/models/registration/registration-json-api.model.ts index 09aec50d6..ec4db52c3 100644 --- a/src/app/shared/models/registration/registration-json-api.model.ts +++ b/src/app/shared/models/registration/registration-json-api.model.ts @@ -126,8 +126,17 @@ export interface RegistrationEmbedsJsonApi { }; } -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface DraftRegistrationEmbedsJsonApi extends RegistrationEmbedsJsonApi {} +export interface DraftRegistrationEmbedsJsonApi extends RegistrationEmbedsJsonApi { + branched_from?: { + data: { + id: string; + type: 'nodes'; + attributes: { + title: string; + }; + }; + }; +} export interface CreateRegistrationPayloadJsonApi { data: {