From 8e8572182c055a1893a6266a4bfd6ccbfff498e2 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Fri, 4 Jul 2025 22:20:42 +0300 Subject: [PATCH 1/3] feat(registries): some refactoring --- .../registries/mappers/registration.mapper.ts | 1 + src/app/features/registries/models/index.ts | 1 - .../models/licenses-json-api.model.ts | 21 ------------------- .../models/registration-json-api.model.ts | 20 +++++++++++++++--- .../registries/models/registration.model.ts | 1 + .../registries/services/licenses.service.ts | 4 ++-- .../registries/services/registries.service.ts | 13 +++--------- 7 files changed, 24 insertions(+), 37 deletions(-) delete mode 100644 src/app/features/registries/models/licenses-json-api.model.ts diff --git a/src/app/features/registries/mappers/registration.mapper.ts b/src/app/features/registries/mappers/registration.mapper.ts index 0e5ee737b..cf5f06c37 100644 --- a/src/app/features/registries/mappers/registration.mapper.ts +++ b/src/app/features/registries/mappers/registration.mapper.ts @@ -12,6 +12,7 @@ export class RegistrationMapper { id: response.data.relationships.license?.data?.id || '', options: response.data.attributes.node_license, }, + tags: response.data.attributes.tags || [], }; } } diff --git a/src/app/features/registries/models/index.ts b/src/app/features/registries/models/index.ts index 937fb5772..4942a7845 100644 --- a/src/app/features/registries/models/index.ts +++ b/src/app/features/registries/models/index.ts @@ -1,4 +1,3 @@ -export * from './licenses-json-api.model'; export * from './page-schema.model'; export * from './project'; export * from './projects-json-api.model'; diff --git a/src/app/features/registries/models/licenses-json-api.model.ts b/src/app/features/registries/models/licenses-json-api.model.ts deleted file mode 100644 index dde5dfe58..000000000 --- a/src/app/features/registries/models/licenses-json-api.model.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { LicenseRecordJsonApi } from '@shared/models'; - -export interface LicenseRelationshipJsonApi { - license: { - data: { - id: string; - type: 'licenses'; - }; - }; -} - -export interface LicensePayloadJsonApi { - data: { - type: 'draft_registrations'; - id: string; - relationships: LicenseRelationshipJsonApi; - attributes: { - node_license?: LicenseRecordJsonApi; - }; - }; -} diff --git a/src/app/features/registries/models/registration-json-api.model.ts b/src/app/features/registries/models/registration-json-api.model.ts index 4e4502248..d315f9301 100644 --- a/src/app/features/registries/models/registration-json-api.model.ts +++ b/src/app/features/registries/models/registration-json-api.model.ts @@ -1,5 +1,5 @@ import { ApiData, MetaJsonApi, PaginationLinksJsonApi } from '@osf/core/models'; -import { LicenseOptions } from '@osf/shared/models'; +import { LicenseOptions, LicenseRecordJsonApi } from '@osf/shared/models'; export interface RegistrationResponseJsonApi { data: RegistrationDataJsonApi; @@ -29,16 +29,30 @@ interface RegistrationAttributesJsonApi { } interface RegistrationRelationshipsJsonApi { - registration_schema: { + registration_schema?: { data: { id: string; type: 'registration-schemas'; }; }; - license: { + license?: { data: { id: string; type: 'licenses'; }; }; } + +export interface RegistrationPayloadJsonApi { + data: { + type: 'draft_registrations'; + id: string; + relationships: RegistrationRelationshipsJsonApi; + attributes: { + title?: string; + description?: string; + node_license?: LicenseRecordJsonApi; + tags?: string[]; + }; + }; +} diff --git a/src/app/features/registries/models/registration.model.ts b/src/app/features/registries/models/registration.model.ts index 7415a2156..e5aa8b4b5 100644 --- a/src/app/features/registries/models/registration.model.ts +++ b/src/app/features/registries/models/registration.model.ts @@ -9,4 +9,5 @@ export interface Registration { id: string; options: LicenseOptions; }; + tags: string[]; } diff --git a/src/app/features/registries/services/licenses.service.ts b/src/app/features/registries/services/licenses.service.ts index f6b7d0f10..03c0db321 100644 --- a/src/app/features/registries/services/licenses.service.ts +++ b/src/app/features/registries/services/licenses.service.ts @@ -6,7 +6,7 @@ import { JsonApiService } from '@osf/core/services'; import { License, LicenseOptions, LicensesResponseJsonApi } from '@osf/shared/models'; import { LicensesMapper } from '../mappers'; -import { LicensePayloadJsonApi, RegistrationDataJsonApi } from '../models'; +import { RegistrationDataJsonApi, RegistrationPayloadJsonApi } from '../models'; import { environment } from 'src/environments/environment'; @@ -50,7 +50,7 @@ export class LicensesService { } updateLicense(registrationId: string, licenseId: string, licenseOptions?: LicenseOptions) { - const payload: LicensePayloadJsonApi = { + const payload: RegistrationPayloadJsonApi = { data: { type: 'draft_registrations', id: registrationId, diff --git a/src/app/features/registries/services/registries.service.ts b/src/app/features/registries/services/registries.service.ts index 103bdeb29..74a48b5ec 100644 --- a/src/app/features/registries/services/registries.service.ts +++ b/src/app/features/registries/services/registries.service.ts @@ -10,6 +10,7 @@ import { PageSchema, Registration, RegistrationDataJsonApi, + RegistrationPayloadJsonApi, RegistrationResponseJsonApi, SchemaBlocksResponseJsonApi, } from '../models'; @@ -56,16 +57,8 @@ export class RegistriesService { .pipe(map((response) => RegistrationMapper.fromRegistrationResponse(response))); } - updateDraft(draftId: string, data: Registration): Observable { - const payload = { - data: { - id: draftId, - type: 'draft_registrations', - attributes: { ...data }, - relationships: {}, - }, - }; - return this.jsonApiService.patch(`${this.apiUrl}/draft_registrations/${draftId}/`, payload); + updateDraft({ data }: RegistrationPayloadJsonApi): Observable { + return this.jsonApiService.patch(`${this.apiUrl}/draft_registrations/${data.id}/`, data); } deleteDraft(draftId: string): Observable { From 9bcc65454669a68746fe8d8ee2084716e06e7029 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Sun, 6 Jul 2025 23:32:15 +0300 Subject: [PATCH 2/3] feat(registreie): refactoring steper, add some validation functionality --- .../constants/submit-preprint-steps.const.ts | 64 ++++++++++--------- .../submit-preprint-stepper.component.html | 12 ++-- .../submit-preprint-stepper.component.ts | 8 ++- .../custom-step/custom-step.component.ts | 4 +- .../components/drafts/drafts.component.ts | 51 +++++++++------ .../metadata/metadata.component.html | 39 +++++------ .../components/metadata/metadata.component.ts | 30 +++++++-- .../registries-license.component.html | 24 +++++-- .../registries-license.component.ts | 6 +- .../registries-subjects.component.html | 2 + .../registries-subjects.component.ts | 22 ++++++- .../registries/constants/defaultSteps.ts | 2 + .../components/stepper/stepper.component.html | 34 +++++----- .../components/stepper/stepper.component.ts | 8 +-- .../subjects/subjects.component.html | 5 ++ .../components/subjects/subjects.component.ts | 6 +- src/app/shared/models/step-option.model.ts | 1 + 17 files changed, 202 insertions(+), 116 deletions(-) diff --git a/src/app/features/preprints/constants/submit-preprint-steps.const.ts b/src/app/features/preprints/constants/submit-preprint-steps.const.ts index de95b0d01..902e82173 100644 --- a/src/app/features/preprints/constants/submit-preprint-steps.const.ts +++ b/src/app/features/preprints/constants/submit-preprint-steps.const.ts @@ -1,33 +1,35 @@ import { SubmitSteps } from '@osf/features/preprints/enums'; -import { CustomOption } from '@shared/models'; +import { StepOption } from '@shared/models'; -export const submitPreprintSteps: CustomOption[] = Object.entries(SubmitSteps) - .filter(([, value]) => typeof value === 'number') - .map(([, value]) => { - let label = ''; - switch (value) { - case SubmitSteps.TitleAndAbstract: - label = 'Title and Abstract'; - break; - case SubmitSteps.File: - label = 'File'; - break; - case SubmitSteps.Metadata: - label = 'Metadata'; - break; - case SubmitSteps.AuthorAssertions: - label = 'Author Assertions'; - break; - case SubmitSteps.Supplements: - label = 'Supplements'; - break; - case SubmitSteps.Review: - label = 'Review'; - break; - } - - return { - label, - value: value as SubmitSteps, - }; - }); +export const submitPreprintSteps: StepOption[] = [ + { + index: SubmitSteps.TitleAndAbstract, + label: 'Title and Abstract', + value: SubmitSteps.TitleAndAbstract, + }, + { + index: SubmitSteps.File, + label: 'File', + value: SubmitSteps.File, + }, + { + index: SubmitSteps.Metadata, + label: 'Metadata', + value: SubmitSteps.Metadata, + }, + { + index: SubmitSteps.AuthorAssertions, + label: 'Author Assertions', + value: SubmitSteps.AuthorAssertions, + }, + { + index: SubmitSteps.Supplements, + label: 'Supplements', + value: SubmitSteps.Supplements, + }, + { + index: SubmitSteps.Review, + label: 'Review', + value: SubmitSteps.Review, + }, +]; diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html index 0e34f65fd..5a0babd6c 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html @@ -29,21 +29,21 @@

{{ 'Add a ' + preprintProvider()!.preprintWor
- @switch (currentStep()) { + @switch (currentStep().value) { @case (SubmitStepsEnum.TitleAndAbstract) { - + } @case (SubmitStepsEnum.File) { } @case (SubmitStepsEnum.Metadata) { - + } @case (SubmitStepsEnum.AuthorAssertions) { - + } @default {

No such step

diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts index aea1de1c9..0aee68d95 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts @@ -30,6 +30,7 @@ import { ResetStateAndDeletePreprint, SetSelectedPreprintProviderId, } from '@osf/features/preprints/store/submit-preprint'; +import { StepOption } from '@osf/shared/models'; import { StepperComponent } from '@shared/components'; import { BrandService } from '@shared/services'; import { BrowserTabHelper, HeaderStyleHelper, IS_WEB } from '@shared/utils'; @@ -66,7 +67,7 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy { preprintProvider = select(PreprintProvidersSelectors.getPreprintProviderDetails(this.providerId())); isPreprintProviderLoading = select(PreprintProvidersSelectors.isPreprintProviderDetailsLoading); - currentStep = signal(0); + currentStep = signal(submitPreprintSteps[0]); isWeb = toSignal(inject(IS_WEB)); constructor() { @@ -97,8 +98,9 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy { this.actions.resetStateAndDeletePreprint(); } - stepChange(step: number) { - if (step >= this.currentStep()) { + stepChange(step: StepOption): void { + const currentStepIndex = this.currentStep()?.index ?? 0; + if (step.index >= currentStepIndex) { return; } diff --git a/src/app/features/registries/components/custom-step/custom-step.component.ts b/src/app/features/registries/components/custom-step/custom-step.component.ts index fa37803cf..930272fa8 100644 --- a/src/app/features/registries/components/custom-step/custom-step.component.ts +++ b/src/app/features/registries/components/custom-step/custom-step.component.ts @@ -41,7 +41,7 @@ import { RegistriesSelectors } from '../../store'; }) export class CustomStepComponent { private readonly route = inject(ActivatedRoute); - step = signal(this.route.snapshot.params['step'].split('-')[0]); + step = signal(this.route.snapshot.params['step']); protected readonly pages = select(RegistriesSelectors.getPagesSchema); currentPage = computed(() => this.pages()[this.step() - 1]); protected readonly FieldType = FieldType; @@ -50,7 +50,7 @@ export class CustomStepComponent { constructor() { this.route.params.pipe(takeUntilDestroyed()).subscribe((params) => { - this.step.set(+params['step'].split('-')[0]); + this.step.set(+params['step']); }); } } diff --git a/src/app/features/registries/components/drafts/drafts.component.ts b/src/app/features/registries/components/drafts/drafts.component.ts index d99033fb4..55c413f72 100644 --- a/src/app/features/registries/components/drafts/drafts.component.ts +++ b/src/app/features/registries/components/drafts/drafts.component.ts @@ -2,10 +2,11 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { tap } from 'rxjs'; +import { filter, tap } from 'rxjs'; import { ChangeDetectionStrategy, Component, computed, effect, inject, Signal, signal } from '@angular/core'; -import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; import { StepperComponent, SubHeaderComponent } from '@osf/shared/components'; import { StepOption } from '@osf/shared/models'; @@ -46,20 +47,39 @@ export class DraftsComponent { })); steps: Signal = computed(() => { - const customSteps = this.pages().map((page) => ({ + const customSteps = this.pages().map((page, index) => ({ + index: index + 1, label: page.title, value: page.id, + routeLink: `${index + 1}`, + invalid: false, })); - return [this.defaultSteps[0], ...customSteps, this.defaultSteps[1]]; + return [this.defaultSteps[0], ...customSteps, { ...this.defaultSteps[1], index: customSteps.length + 1 }]; }); - currentStep = signal( - this.route.snapshot.children[0]?.params['step'] ? +this.route.snapshot.children[0]?.params['step'].split('-')[0] : 0 + currentStepIndex = signal( + this.route.snapshot.firstChild?.params['step'] ? +this.route.snapshot.firstChild?.params['step'] : 0 ); - registrationId = this.route.snapshot.children[0]?.params['id'] || ''; + currentStep = computed(() => { + return this.steps()[this.currentStepIndex()]; + }); + + registrationId = this.route.snapshot.firstChild?.params['id'] || ''; constructor() { + this.router.events + .pipe( + takeUntilDestroyed(), + filter((event): event is NavigationEnd => event instanceof NavigationEnd) + ) + .subscribe(() => { + const step = this.route.firstChild?.snapshot.params['step']; + if (step) { + this.currentStepIndex.set(+step); + } + }); + this.loaderService.show(); if (!this.draftRegistration()) { this.actions.getDraftRegistration(this.registrationId); @@ -79,24 +99,17 @@ export class DraftsComponent { }); effect(() => { - const reviewStepNumber = this.pages().length + 1; + const reviewStepIndex = this.pages().length + 1; if (this.isReviewPage) { - this.currentStep.set(reviewStepNumber); + this.currentStepIndex.set(reviewStepIndex); } }); } - stepChange(step: number): void { + stepChange(step: StepOption): void { // [NM] TODO: before navigating, validate the current step - this.currentStep.set(step); - const pageStep = this.steps()[step]; - - let pageLink = ''; - if (!pageStep.value) { - pageLink = `${pageStep.routeLink}`; - } else { - pageLink = `${step}-${pageStep.value}`; - } + this.currentStepIndex.set(step.index); + const pageLink = this.steps()[step.index].routeLink; this.router.navigate([`/registries/drafts/${this.registrationId}/`, pageLink]); } } diff --git a/src/app/features/registries/components/metadata/metadata.component.html b/src/app/features/registries/components/metadata/metadata.component.html index 5c44bd71e..7e520b5d8 100644 --- a/src/app/features/registries/components/metadata/metadata.component.html +++ b/src/app/features/registries/components/metadata/metadata.component.html @@ -1,16 +1,16 @@
-
-

{{ 'registries.metadata.title' | translate }}

-

{{ 'registries.metadata.description' | translate }}

- -
+ +
+

{{ 'registries.metadata.title' | translate }}

+

{{ 'registries.metadata.description' | translate }}

+ -
+
+ + {{ INPUT_VALIDATION_MESSAGES.required | translate }} +
- - -
- - - - -
- - - -
+
+
+ + + + +
+ + + +
+
diff --git a/src/app/features/registries/components/metadata/metadata.component.ts b/src/app/features/registries/components/metadata/metadata.component.ts index d7b289ec1..620930999 100644 --- a/src/app/features/registries/components/metadata/metadata.component.ts +++ b/src/app/features/registries/components/metadata/metadata.component.ts @@ -4,17 +4,19 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; +import { Message } from 'primeng/message'; import { TextareaModule } from 'primeng/textarea'; import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core'; -import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { TextInputComponent } from '@osf/shared/components'; -import { InputLimits } from '@osf/shared/constants'; +import { INPUT_VALIDATION_MESSAGES, InputLimits } from '@osf/shared/constants'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { CustomValidators } from '@osf/shared/utils'; +import { Registration } from '../../models'; import { DeleteDraft, RegistriesSelectors } from '../../store'; import { ContributorsComponent } from './contributors/contributors.component'; @@ -35,6 +37,7 @@ import { RegistriesTagsComponent } from './registries-tags/registries-tags.compo RegistriesSubjectsComponent, RegistriesTagsComponent, RegistriesLicenseComponent, + Message, ], templateUrl: './metadata.component.html', styleUrl: './metadata.component.scss', @@ -54,26 +57,39 @@ export class MetadataComponent { deleteDraft: DeleteDraft, }); protected inputLimits = InputLimits; + readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; metadataForm = this.fb.group({ title: ['', CustomValidators.requiredTrimmed()], description: ['', CustomValidators.requiredTrimmed()], + // contributors: [[], Validators.required], + subjects: [[], Validators.required], + tags: [[]], + license: [null as Registration['license'] | null, Validators.required], }); constructor() { effect(() => { const draft = this.draftRegistration(); if (draft) { - this.metadataForm.patchValue({ - title: draft.title, - description: draft.description, - }); + this.initForm(draft); } }); } + private initForm(data: Registration): void { + this.metadataForm.patchValue({ + title: data.title, + description: data.description, + license: data.license, + }); + } + submitMetadata(): void { - console.log('Metadata submitted'); + console.log('Metadata submitted', this.metadataForm); + this.router.navigate(['../1'], { + relativeTo: this.route, + }); } deleteDraft(): void { diff --git a/src/app/features/registries/components/metadata/registries-license/registries-license.component.html b/src/app/features/registries/components/metadata/registries-license/registries-license.component.html index 3d5788724..f8c587b39 100644 --- a/src/app/features/registries/components/metadata/registries-license/registries-license.component.html +++ b/src/app/features/registries/components/metadata/registries-license/registries-license.component.html @@ -1,7 +1,17 @@ - + +

{{ 'shared.license.title' | translate }}

+ +

{{ 'shared.license.description' | translate }}

+

+ {{ 'shared.license.helpText' | translate }} + {{ 'common.links.helpGuide' | translate }}. +

+ +
diff --git a/src/app/features/registries/components/metadata/registries-license/registries-license.component.ts b/src/app/features/registries/components/metadata/registries-license/registries-license.component.ts index 90b22d921..8a1ef5b4e 100644 --- a/src/app/features/registries/components/metadata/registries-license/registries-license.component.ts +++ b/src/app/features/registries/components/metadata/registries-license/registries-license.component.ts @@ -1,5 +1,9 @@ import { createDispatchMap, select } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + +import { Card } from 'primeng/card'; + import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; @@ -12,7 +16,7 @@ import { CustomValidators } from '@osf/shared/utils'; @Component({ selector: 'osf-registries-license', - imports: [FormsModule, ReactiveFormsModule, LicenseComponent], + imports: [FormsModule, ReactiveFormsModule, LicenseComponent, Card, TranslatePipe], templateUrl: './registries-license.component.html', styleUrl: './registries-license.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/registries/components/metadata/registries-subjects/registries-subjects.component.html b/src/app/features/registries/components/metadata/registries-subjects/registries-subjects.component.html index 1e83c52cd..32e402b21 100644 --- a/src/app/features/registries/components/metadata/registries-subjects/registries-subjects.component.html +++ b/src/app/features/registries/components/metadata/registries-subjects/registries-subjects.component.html @@ -1,4 +1,6 @@ (); private readonly route = inject(ActivatedRoute); private readonly draftId = this.route.snapshot.params['id']; @@ -52,6 +54,24 @@ export class RegistriesSubjectsComponent { } updateSelectedSubjects(subjects: Subject[]) { + this.updateControlState(subjects); this.actions.updateRegistrationSubjects(this.draftId, subjects); } + + onFocusOut() { + if (this.control()) { + this.control().markAsTouched(); + this.control().markAsDirty(); + this.control().updateValueAndValidity(); + } + } + + updateControlState(value: Subject[]) { + if (this.control()) { + this.control().setValue(value); + this.control().markAsTouched(); + this.control().markAsDirty(); + this.control().updateValueAndValidity(); + } + } } diff --git a/src/app/features/registries/constants/defaultSteps.ts b/src/app/features/registries/constants/defaultSteps.ts index ba7808628..417ed494b 100644 --- a/src/app/features/registries/constants/defaultSteps.ts +++ b/src/app/features/registries/constants/defaultSteps.ts @@ -2,6 +2,7 @@ import { StepOption } from '@osf/shared/models'; export const defaultSteps: StepOption[] = [ { + index: 0, label: 'navigation.project.metadata', value: '', routeLink: 'metadata', @@ -10,5 +11,6 @@ export const defaultSteps: StepOption[] = [ label: 'registries.review.step', value: '', routeLink: 'review', + index: 1, }, ]; diff --git a/src/app/shared/components/stepper/stepper.component.html b/src/app/shared/components/stepper/stepper.component.html index 39c6c28c5..72651d17a 100644 --- a/src/app/shared/components/stepper/stepper.component.html +++ b/src/app/shared/components/stepper/stepper.component.html @@ -1,25 +1,27 @@
@for (step of steps(); track step.value; let i = $index) { - + @if (i < steps().length - 1) { +
} - - @if (i < steps().length - 1) { -
} }
diff --git a/src/app/shared/components/stepper/stepper.component.ts b/src/app/shared/components/stepper/stepper.component.ts index 4844d1243..4924f3192 100644 --- a/src/app/shared/components/stepper/stepper.component.ts +++ b/src/app/shared/components/stepper/stepper.component.ts @@ -14,15 +14,15 @@ import { StepOption } from '@shared/models'; }) export class StepperComponent { steps = input.required(); - currentStep = model.required(); + currentStep = model.required(); linear = input(true); - onStepClick(step: number) { - if (step === this.currentStep()) { + onStepClick(step: StepOption) { + if (step.index === this.currentStep().index) { return; } - if (this.linear() && step > this.currentStep()) { + if (this.linear() && step.index > this.currentStep().index) { return; } diff --git a/src/app/shared/components/subjects/subjects.component.html b/src/app/shared/components/subjects/subjects.component.html index 2ab6c967b..aeec5abdb 100644 --- a/src/app/shared/components/subjects/subjects.component.html +++ b/src/app/shared/components/subjects/subjects.component.html @@ -58,4 +58,9 @@

{{ 'shared.subjects.title' | translate }}

> } + @if (control()?.errors?.['required'] && (control()?.touched || control()?.dirty)) { + + {{ INPUT_VALIDATION_MESSAGES.required | translate }} + + } diff --git a/src/app/shared/components/subjects/subjects.component.ts b/src/app/shared/components/subjects/subjects.component.ts index d6fc72244..387172bfd 100644 --- a/src/app/shared/components/subjects/subjects.component.ts +++ b/src/app/shared/components/subjects/subjects.component.ts @@ -4,6 +4,7 @@ import { TreeNode } from 'primeng/api'; import { Card } from 'primeng/card'; import { Checkbox, CheckboxChangeEvent } from 'primeng/checkbox'; import { Chip } from 'primeng/chip'; +import { Message } from 'primeng/message'; import { Skeleton } from 'primeng/skeleton'; import { Tree, TreeModule } from 'primeng/tree'; @@ -12,18 +13,20 @@ import { debounceTime, distinctUntilChanged } from 'rxjs'; import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; import { FormControl } from '@angular/forms'; +import { INPUT_VALIDATION_MESSAGES } from '@osf/shared/constants'; import { Subject } from '@osf/shared/models'; import { SearchInputComponent } from '../search-input/search-input.component'; @Component({ selector: 'osf-subjects', - imports: [Card, TranslatePipe, Chip, SearchInputComponent, Tree, TreeModule, Checkbox, Skeleton], + imports: [Card, TranslatePipe, Chip, SearchInputComponent, Tree, TreeModule, Checkbox, Skeleton, Message], templateUrl: './subjects.component.html', styleUrl: './subjects.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class SubjectsComponent { + control = input>(); list = input([]); searchedSubjects = input([]); loading = input(false); @@ -39,6 +42,7 @@ export class SubjectsComponent { expanded: Record = {}; protected searchControl = new FormControl(''); + readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; constructor() { this.searchControl.valueChanges.pipe(debounceTime(300), distinctUntilChanged()).subscribe((value) => { diff --git a/src/app/shared/models/step-option.model.ts b/src/app/shared/models/step-option.model.ts index 90c18cae2..4c347de19 100644 --- a/src/app/shared/models/step-option.model.ts +++ b/src/app/shared/models/step-option.model.ts @@ -1,4 +1,5 @@ export interface StepOption { + index: number; label: string; value: number | string; invalid?: boolean; From 0026e7e4560de55b066c8ae6a54833b1b4c6112e Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Mon, 7 Jul 2025 16:38:27 +0300 Subject: [PATCH 3/3] feat(registries): add validation state for steps --- .../components/drafts/drafts.component.ts | 37 ++++++++---- .../metadata/metadata.component.html | 13 +++-- .../components/metadata/metadata.component.ts | 58 +++++++++++++++---- .../registries-license.component.html | 7 ++- .../registries-license.component.ts | 33 +++++++++-- .../registries-tags.component.html | 4 +- .../registries-tags.component.ts | 16 ++++- .../registries/mappers/registration.mapper.ts | 23 +++++--- .../models/registration-json-api.model.ts | 17 ++---- .../registries/models/registration.model.ts | 2 +- .../registries/services/registries.service.ts | 26 +++++++-- .../registries/store/default.state.ts | 1 + .../registries/store/registries.actions.ts | 19 ++++++ .../registries/store/registries.model.ts | 1 + .../registries/store/registries.selectors.ts | 10 ++++ .../registries/store/registries.state.ts | 37 ++++++++++++ src/assets/i18n/en.json | 3 +- 17 files changed, 246 insertions(+), 61 deletions(-) diff --git a/src/app/features/registries/components/drafts/drafts.component.ts b/src/app/features/registries/components/drafts/drafts.component.ts index 55c413f72..fe7a21765 100644 --- a/src/app/features/registries/components/drafts/drafts.component.ts +++ b/src/app/features/registries/components/drafts/drafts.component.ts @@ -31,6 +31,7 @@ export class DraftsComponent { protected readonly pages = select(RegistriesSelectors.getPagesSchema); protected readonly draftRegistration = select(RegistriesSelectors.getDraftRegistration); + protected stepsValidation = select(RegistriesSelectors.getStepsValidation); private readonly actions = createDispatchMap({ getSchemaBlocks: FetchSchemaBlocks, @@ -41,20 +42,29 @@ export class DraftsComponent { return this.router.url.includes('/review'); } - defaultSteps: StepOption[] = defaultSteps.map((step) => ({ - ...step, - label: this.translateService.instant(step.label), - })); + defaultSteps: StepOption[] = []; steps: Signal = computed(() => { - const customSteps = this.pages().map((page, index) => ({ - index: index + 1, - label: page.title, - value: page.id, - routeLink: `${index + 1}`, - invalid: false, + this.defaultSteps = defaultSteps.map((step) => ({ + ...step, + label: this.translateService.instant(step.label), + invalid: this.stepsValidation()?.[step.index]?.invalid || false, })); - return [this.defaultSteps[0], ...customSteps, { ...this.defaultSteps[1], index: customSteps.length + 1 }]; + + const customSteps = this.pages().map((page, index) => { + return { + index: index + 1, + label: page.title, + value: page.id, + routeLink: `${index + 1}`, + invalid: this.stepsValidation()?.[index + 1]?.invalid || false, + }; + }); + return [ + this.defaultSteps[0], + ...customSteps, + { ...this.defaultSteps[1], index: customSteps.length + 1, invalid: false }, + ]; }); currentStepIndex = signal( @@ -77,6 +87,11 @@ export class DraftsComponent { const step = this.route.firstChild?.snapshot.params['step']; if (step) { this.currentStepIndex.set(+step); + } else if (this.isReviewPage) { + const reviewStepIndex = this.pages().length + 1; + this.currentStepIndex.set(reviewStepIndex); + } else { + this.currentStepIndex.set(0); } }); diff --git a/src/app/features/registries/components/metadata/metadata.component.html b/src/app/features/registries/components/metadata/metadata.component.html index 7e520b5d8..3acb43554 100644 --- a/src/app/features/registries/components/metadata/metadata.component.html +++ b/src/app/features/registries/components/metadata/metadata.component.html @@ -20,15 +20,20 @@

{{ 'registries.metadata.title' | translate }}

cols="30" pTextarea > - - {{ INPUT_VALIDATION_MESSAGES.required | translate }} - + @if ( + metadataForm.controls['description'].errors?.['required'] && + (metadataForm.controls['description'].touched || metadataForm.controls['description'].dirty) + ) { + + {{ INPUT_VALIDATION_MESSAGES.required | translate }} + + }
- +
diff --git a/src/app/features/registries/components/metadata/metadata.component.ts b/src/app/features/registries/components/metadata/metadata.component.ts index 620930999..e17999dc5 100644 --- a/src/app/features/registries/components/metadata/metadata.component.ts +++ b/src/app/features/registries/components/metadata/metadata.component.ts @@ -7,17 +7,20 @@ import { Card } from 'primeng/card'; import { Message } from 'primeng/message'; import { TextareaModule } from 'primeng/textarea'; -import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core'; +import { tap } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, effect, inject, OnDestroy } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { TextInputComponent } from '@osf/shared/components'; import { INPUT_VALIDATION_MESSAGES, InputLimits } from '@osf/shared/constants'; +import { Subject } from '@osf/shared/models'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; -import { CustomValidators } from '@osf/shared/utils'; +import { CustomValidators, findChangedFields } from '@osf/shared/utils'; import { Registration } from '../../models'; -import { DeleteDraft, RegistriesSelectors } from '../../store'; +import { DeleteDraft, RegistriesSelectors, UpdateDraft, UpdateStepValidation } from '../../store'; import { ContributorsComponent } from './contributors/contributors.component'; import { RegistriesLicenseComponent } from './registries-license/registries-license.component'; @@ -43,7 +46,7 @@ import { RegistriesTagsComponent } from './registries-tags/registries-tags.compo styleUrl: './metadata.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MetadataComponent { +export class MetadataComponent implements OnDestroy { private readonly fb = inject(FormBuilder); private readonly toastService = inject(ToastService); private readonly route = inject(ActivatedRoute); @@ -52,9 +55,12 @@ export class MetadataComponent { private readonly draftId = this.route.snapshot.params['id']; protected readonly draftRegistration = select(RegistriesSelectors.getDraftRegistration); + protected selectedSubjects = select(RegistriesSelectors.getSelectedSubjects); protected actions = createDispatchMap({ deleteDraft: DeleteDraft, + updateDraft: UpdateDraft, + updateStepValidation: UpdateStepValidation, }); protected inputLimits = InputLimits; readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; @@ -63,9 +69,11 @@ export class MetadataComponent { title: ['', CustomValidators.requiredTrimmed()], description: ['', CustomValidators.requiredTrimmed()], // contributors: [[], Validators.required], - subjects: [[], Validators.required], + subjects: [[] as Subject[], Validators.required], tags: [[]], - license: [null as Registration['license'] | null, Validators.required], + license: this.fb.group({ + id: ['', Validators.required], + }), }); constructor() { @@ -75,6 +83,13 @@ export class MetadataComponent { this.initForm(draft); } }); + + effect(() => { + const subjects = this.selectedSubjects(); + if (subjects) { + this.metadataForm.patchValue({ subjects }); + } + }); } private initForm(data: Registration): void { @@ -86,10 +101,21 @@ export class MetadataComponent { } submitMetadata(): void { - console.log('Metadata submitted', this.metadataForm); - this.router.navigate(['../1'], { - relativeTo: this.route, - }); + this.actions + .updateDraft(this.draftId, { + title: this.metadataForm.value.title?.trim(), + description: this.metadataForm.value.description?.trim(), + }) + .pipe( + tap(() => { + this.metadataForm.markAllAsTouched(); + this.router.navigate(['../1'], { + relativeTo: this.route, + onSameUrlNavigation: 'reload', + }); + }) + ) + .subscribe(); } deleteDraft(): void { @@ -105,4 +131,16 @@ export class MetadataComponent { }, }); } + + ngOnDestroy(): void { + this.actions.updateStepValidation('0', this.metadataForm.invalid); + const changedFields = findChangedFields( + { title: this.metadataForm.value.title!, description: this.metadataForm.value.description! }, + { title: this.draftRegistration()?.title, description: this.draftRegistration()?.description } + ); + if (Object.keys(changedFields).length > 0) { + this.metadataForm.markAllAsTouched(); + this.actions.updateDraft(this.draftId, changedFields); + } + } } diff --git a/src/app/features/registries/components/metadata/registries-license/registries-license.component.html b/src/app/features/registries/components/metadata/registries-license/registries-license.component.html index f8c587b39..f765fbd7f 100644 --- a/src/app/features/registries/components/metadata/registries-license/registries-license.component.html +++ b/src/app/features/registries/components/metadata/registries-license/registries-license.component.html @@ -1,4 +1,4 @@ - +

{{ 'shared.license.title' | translate }}

{{ 'shared.license.description' | translate }}

@@ -14,4 +14,9 @@

{{ 'shared.license.title' | translate }}

(createLicense)="createLicense($event)" (selectLicense)="selectLicense($event)" /> + @if (control().invalid && (control().touched || control().dirty)) { + + {{ INPUT_VALIDATION_MESSAGES.required | translate }} + + }
diff --git a/src/app/features/registries/components/metadata/registries-license/registries-license.component.ts b/src/app/features/registries/components/metadata/registries-license/registries-license.component.ts index 8a1ef5b4e..1ade2f249 100644 --- a/src/app/features/registries/components/metadata/registries-license/registries-license.component.ts +++ b/src/app/features/registries/components/metadata/registries-license/registries-license.component.ts @@ -3,25 +3,28 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { Card } from 'primeng/card'; +import { Message } from 'primeng/message'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, effect, inject, input } from '@angular/core'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { FetchLicenses, RegistriesSelectors, SaveLicense } from '@osf/features/registries/store'; import { LicenseComponent } from '@osf/shared/components'; -import { InputLimits } from '@osf/shared/constants'; +import { INPUT_VALIDATION_MESSAGES, InputLimits } from '@osf/shared/constants'; import { License, LicenseOptions } from '@osf/shared/models'; import { CustomValidators } from '@osf/shared/utils'; @Component({ selector: 'osf-registries-license', - imports: [FormsModule, ReactiveFormsModule, LicenseComponent, Card, TranslatePipe], + imports: [FormsModule, ReactiveFormsModule, LicenseComponent, Card, TranslatePipe, Message], templateUrl: './registries-license.component.html', styleUrl: './registries-license.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegistriesLicenseComponent { + control = input.required(); + private readonly route = inject(ActivatedRoute); private readonly draftId = this.route.snapshot.params['id']; private readonly fb = inject(FormBuilder); @@ -38,8 +41,20 @@ export class RegistriesLicenseComponent { copyrightHolders: ['', CustomValidators.requiredTrimmed()], }); + readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; + constructor() { this.actions.fetchLicenses(); + + effect(() => { + const selectedLicense = this.selectedLicense(); + if (selectedLicense) { + this.control().patchValue({ + id: selectedLicense.id, + // [NM] TODO: Add validation for license options + }); + } + }); } createLicense(licenseDetails: { id: string; licenseOptions: LicenseOptions }) { @@ -47,6 +62,16 @@ export class RegistriesLicenseComponent { } selectLicense(license: License) { + this.control().markAsDirty(); + this.control().updateValueAndValidity(); this.actions.saveLicense(this.draftId, license.id); } + + onFocusOut() { + if (this.control()) { + this.control().markAsTouched(); + this.control().markAsDirty(); + this.control().updateValueAndValidity(); + } + } } diff --git a/src/app/features/registries/components/metadata/registries-tags/registries-tags.component.html b/src/app/features/registries/components/metadata/registries-tags/registries-tags.component.html index 2d8798d8a..5dde35e0a 100644 --- a/src/app/features/registries/components/metadata/registries-tags/registries-tags.component.html +++ b/src/app/features/registries/components/metadata/registries-tags/registries-tags.component.html @@ -1,7 +1,7 @@
-

{{ 'project.overview.metadata.tags' | translate }}

+

{{ 'project.overview.metadata.tags' | translate }} ({{ 'common.labels.optional' | translate }})

- +
diff --git a/src/app/features/registries/components/metadata/registries-tags/registries-tags.component.ts b/src/app/features/registries/components/metadata/registries-tags/registries-tags.component.ts index bc0ca52f9..1d9fbd869 100644 --- a/src/app/features/registries/components/metadata/registries-tags/registries-tags.component.ts +++ b/src/app/features/registries/components/metadata/registries-tags/registries-tags.component.ts @@ -1,9 +1,13 @@ +import { createDispatchMap, select } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; import { Card } from 'primeng/card'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { RegistriesSelectors, UpdateDraft } from '@osf/features/registries/store'; import { TagsInputComponent } from '@osf/shared/components'; @Component({ @@ -14,7 +18,15 @@ import { TagsInputComponent } from '@osf/shared/components'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegistriesTagsComponent { + private readonly route = inject(ActivatedRoute); + private readonly draftId = this.route.snapshot.params['id']; + protected selectedTags = select(RegistriesSelectors.getSelectedTags); + + protected actions = createDispatchMap({ + updateDraft: UpdateDraft, + }); + onTagsChanged(tags: string[]): void { - console.log('Tags changed:', tags); + this.actions.updateDraft(this.draftId, { tags }); } } diff --git a/src/app/features/registries/mappers/registration.mapper.ts b/src/app/features/registries/mappers/registration.mapper.ts index cf5f06c37..2d307b2a7 100644 --- a/src/app/features/registries/mappers/registration.mapper.ts +++ b/src/app/features/registries/mappers/registration.mapper.ts @@ -1,18 +1,23 @@ -import { RegistrationResponseJsonApi } from '../models'; +import { RegistrationDataJsonApi } from '../models'; import { Registration } from '../models/registration.model'; export class RegistrationMapper { - static fromRegistrationResponse(response: RegistrationResponseJsonApi): Registration { + static fromRegistrationResponse(response: RegistrationDataJsonApi): Registration { return { - id: response.data.id, - title: response.data.attributes.title, - description: response.data.attributes.description, - registrationSchemaId: response.data.relationships.registration_schema?.data?.id || '', + id: response.id, + title: response.attributes.title, + description: response.attributes.description, + registrationSchemaId: response.relationships.registration_schema?.data?.id || '', license: { - id: response.data.relationships.license?.data?.id || '', - options: response.data.attributes.node_license, + id: response.relationships.license?.data?.id || '', + options: response.attributes.node_license + ? { + year: response.attributes.node_license.year, + copyrightHolders: response.attributes.node_license.copyright_holders.join(','), + } + : null, }, - tags: response.data.attributes.tags || [], + tags: response.attributes.tags || [], }; } } diff --git a/src/app/features/registries/models/registration-json-api.model.ts b/src/app/features/registries/models/registration-json-api.model.ts index d315f9301..59742ccf7 100644 --- a/src/app/features/registries/models/registration-json-api.model.ts +++ b/src/app/features/registries/models/registration-json-api.model.ts @@ -1,5 +1,5 @@ import { ApiData, MetaJsonApi, PaginationLinksJsonApi } from '@osf/core/models'; -import { LicenseOptions, LicenseRecordJsonApi } from '@osf/shared/models'; +import { LicenseRecordJsonApi } from '@osf/shared/models'; export interface RegistrationResponseJsonApi { data: RegistrationDataJsonApi; @@ -14,21 +14,21 @@ export type RegistrationDataJsonApi = ApiData< null >; -interface RegistrationAttributesJsonApi { +export interface RegistrationAttributesJsonApi { category: string; current_user_permissions: string[]; date_created: string; datetime_updated: string; description: string; has_project: boolean; - node_license: LicenseOptions; + node_license: LicenseRecordJsonApi; registration_metadata: Record; registration_responses: Record; tags: string[]; title: string; } -interface RegistrationRelationshipsJsonApi { +export interface RegistrationRelationshipsJsonApi { registration_schema?: { data: { id: string; @@ -47,12 +47,7 @@ export interface RegistrationPayloadJsonApi { data: { type: 'draft_registrations'; id: string; - relationships: RegistrationRelationshipsJsonApi; - attributes: { - title?: string; - description?: string; - node_license?: LicenseRecordJsonApi; - tags?: string[]; - }; + relationships?: RegistrationRelationshipsJsonApi; + attributes?: Partial; }; } diff --git a/src/app/features/registries/models/registration.model.ts b/src/app/features/registries/models/registration.model.ts index e5aa8b4b5..afe2de619 100644 --- a/src/app/features/registries/models/registration.model.ts +++ b/src/app/features/registries/models/registration.model.ts @@ -7,7 +7,7 @@ export interface Registration { registrationSchemaId: string; license: { id: string; - options: LicenseOptions; + options: LicenseOptions | null; }; tags: string[]; } diff --git a/src/app/features/registries/services/registries.service.ts b/src/app/features/registries/services/registries.service.ts index 74a48b5ec..1fc0c1c86 100644 --- a/src/app/features/registries/services/registries.service.ts +++ b/src/app/features/registries/services/registries.service.ts @@ -9,8 +9,9 @@ import { RegistrationMapper } from '../mappers/registration.mapper'; import { PageSchema, Registration, + RegistrationAttributesJsonApi, RegistrationDataJsonApi, - RegistrationPayloadJsonApi, + RegistrationRelationshipsJsonApi, RegistrationResponseJsonApi, SchemaBlocksResponseJsonApi, } from '../models'; @@ -48,17 +49,32 @@ export class RegistriesService { }; return this.jsonApiService .post(`${this.apiUrl}/draft_registrations/`, payload) - .pipe(map((response) => RegistrationMapper.fromRegistrationResponse(response))); + .pipe(map((response) => RegistrationMapper.fromRegistrationResponse(response.data))); } getDraft(draftId: string): Observable { return this.jsonApiService .get(`${this.apiUrl}/draft_registrations/${draftId}/`) - .pipe(map((response) => RegistrationMapper.fromRegistrationResponse(response))); + .pipe(map((response) => RegistrationMapper.fromRegistrationResponse(response.data))); } - updateDraft({ data }: RegistrationPayloadJsonApi): Observable { - return this.jsonApiService.patch(`${this.apiUrl}/draft_registrations/${data.id}/`, data); + updateDraft( + id: string, + attributes: Partial, + relationships?: Partial + ): Observable { + const payload = { + data: { + id, + attributes, + relationships, + type: 'draft_registrations', // force the correct type + }, + }; + + return this.jsonApiService + .patch(`${this.apiUrl}/draft_registrations/${id}/`, payload) + .pipe(map((response) => RegistrationMapper.fromRegistrationResponse(response))); } deleteDraft(draftId: string): Observable { diff --git a/src/app/features/registries/store/default.state.ts b/src/app/features/registries/store/default.state.ts index a9e3c4d4c..c3a80bff7 100644 --- a/src/app/features/registries/store/default.state.ts +++ b/src/app/features/registries/store/default.state.ts @@ -42,4 +42,5 @@ export const DefaultState: RegistriesStateModel = { isLoading: false, error: null, }, + stepsValidation: {}, }; diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index 6de2aaa41..62295c395 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -1,6 +1,8 @@ import { ContributorAddModel, ContributorModel } from '@osf/shared/components/contributors/models'; import { LicenseOptions, Subject } from '@osf/shared/models'; +import { RegistrationAttributesJsonApi, RegistrationRelationshipsJsonApi } from '../models'; + export class GetRegistries { static readonly type = '[Registries] Get Registries'; } @@ -23,6 +25,15 @@ export class FetchDraft { constructor(public draftId: string) {} } +export class UpdateDraft { + static readonly type = '[Registries] Update Registration Tags'; + constructor( + public draftId: string, + public attributes: Partial, + public relationships?: Partial + ) {} +} + export class DeleteDraft { static readonly type = '[Registries] Delete Draft'; constructor(public draftId: string) {} @@ -91,3 +102,11 @@ export class UpdateRegistrationSubjects { public subjects: Subject[] ) {} } + +export class UpdateStepValidation { + static readonly type = '[Registries] Update Step Validation'; + constructor( + public step: string, + public invalid: boolean + ) {} +} diff --git a/src/app/features/registries/store/registries.model.ts b/src/app/features/registries/store/registries.model.ts index b36e022de..f77513b60 100644 --- a/src/app/features/registries/store/registries.model.ts +++ b/src/app/features/registries/store/registries.model.ts @@ -13,4 +13,5 @@ export interface RegistriesStateModel { licenses: AsyncStateModel; registrationSubjects: AsyncStateModel; pagesSchema: AsyncStateModel; + stepsValidation: Record; } diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts index c3fb6dcf2..a57c0c7b8 100644 --- a/src/app/features/registries/store/registries.selectors.ts +++ b/src/app/features/registries/store/registries.selectors.ts @@ -77,4 +77,14 @@ export class RegistriesSelectors { static isSubjectsUpdating(state: RegistriesStateModel): boolean { return state.registrationSubjects.isLoading; } + + @Selector([RegistriesState]) + static getSelectedTags(state: RegistriesStateModel): string[] { + return state.draftRegistration.data?.tags || []; + } + + @Selector([RegistriesState]) + static getStepsValidation(state: RegistriesStateModel): Record { + return state.stepsValidation; + } } diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index 4b04264e6..5473bdd7a 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -32,7 +32,9 @@ import { GetRegistries, SaveLicense, UpdateContributor, + UpdateDraft, UpdateRegistrationSubjects, + UpdateStepValidation, } from './registries.actions'; import { RegistriesStateModel } from './registries.model'; @@ -169,6 +171,30 @@ export class RegistriesState { ); } + @Action(UpdateDraft) + updateDraft(ctx: StateContext, { draftId, attributes, relationships }: UpdateDraft) { + ctx.patchState({ + draftRegistration: { + ...ctx.getState().draftRegistration, + isSubmitting: true, + }, + }); + + return this.registriesService.updateDraft(draftId, attributes, relationships).pipe( + tap((updatedDraft) => { + ctx.patchState({ + draftRegistration: { + data: { ...updatedDraft }, + isLoading: false, + isSubmitting: false, + error: null, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'draftRegistration', error)) + ); + } + @Action(FetchSchemaBlocks) fetchSchemaBlocks(ctx: StateContext, action: FetchSchemaBlocks) { const state = ctx.getState(); @@ -189,6 +215,17 @@ export class RegistriesState { ); } + @Action(UpdateStepValidation) + updateStepValidation(ctx: StateContext, { step, invalid }: UpdateStepValidation) { + const state = ctx.getState(); + ctx.patchState({ + stepsValidation: { + ...state.stepsValidation, + [step]: { invalid }, + }, + }); + } + @Action(FetchContributors) fetchContributors(ctx: StateContext, action: FetchContributors) { return this.contributorsHandler.fetchContributors(ctx, action); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index ad612fb3b..60e4657f6 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -65,7 +65,8 @@ "public": "Public", "title": "Title", "description": "Description", - "year": "Year" + "year": "Year", + "optional": "Optional" }, "deleteConfirmation": { "header": "Delete",