- @for (option of question.options; track option) {
-
-
-
- @if (option.helpText) {
-
- }
+ @if (question.paragraphText) {
+
{{ question.paragraphText }}
+ }
+
+ @if (question.exampleText) {
+
+
+ {{ 'common.links.showExample' | translate }}
+
+
+
+
- }
-
+
{{ question.exampleText }}
+
+
}
- @case (FieldType.Checkbox) {
-
- @for (option of question.options; track option) {
-
+
+ @switch (question.fieldType) {
+ @case (FieldType.TextArea) {
+
+ @if (
+ stepForm.controls[question.responseKey!].errors?.['required'] &&
+ (stepForm.controls[question.responseKey!].touched || stepForm.controls[question.responseKey!].dirty)
+ ) {
+
+ {{ INPUT_VALIDATION_MESSAGES.required | translate }}
+
}
-
- }
+ }
+ @case (FieldType.Radio) {
+
+ @for (option of question.options; track option) {
+
+
+
+ @if (option.helpText) {
+
+ }
+
+ }
+
+ }
+ @case (FieldType.Checkbox) {
+
+ @for (option of question.options; track option) {
+
+ }
+
+ }
- @case (FieldType.Text) {
-
- }
- @case (FieldType.File) {
-
Upload File
-
You may attach up to 5 file(s) to this question. Files cannot total over 5GB in size.
-
- Uploaded files will automatically be archived in this registration. They will also be added to a related
- project that will be created for this registration.
-
+ @case (FieldType.Text) {
+
+ @if (
+ stepForm.controls[question.responseKey!].errors?.['required'] &&
+ (stepForm.controls[question.responseKey!].touched || stepForm.controls[question.responseKey!].dirty)
+ ) {
+
+ {{ INPUT_VALIDATION_MESSAGES.required | translate }}
+
+ }
+ }
+ @case (FieldType.File) {
+
Upload File
+
+ You may attach up to 5 file(s) to this question. Files cannot total over 5GB in size.
+
+
+ Uploaded files will automatically be archived in this registration. They will also be added to a
+ related project that will be created for this registration.
+
-
File input is not implemented yet.
+
File input is not implemented yet.
+ }
}
- }
-
+
+
}
}
+
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 930272fa8..d9c4fe06b 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
@@ -1,24 +1,29 @@
-import { select } from '@ngxs/store';
+import { createDispatchMap, select } from '@ngxs/store';
import { TranslatePipe } from '@ngx-translate/core';
+import { Button } from 'primeng/button';
import { Card } from 'primeng/card';
import { Checkbox } from 'primeng/checkbox';
import { Inplace } from 'primeng/inplace';
import { InputText } from 'primeng/inputtext';
+import { Message } from 'primeng/message';
import { RadioButton } from 'primeng/radiobutton';
import { Textarea } from 'primeng/textarea';
import { NgTemplateOutlet } from '@angular/common';
-import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
+import { ChangeDetectionStrategy, Component, computed, effect, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { FormsModule } from '@angular/forms';
-import { ActivatedRoute } from '@angular/router';
+import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
import { InfoIconComponent } from '@osf/shared/components';
+import { INPUT_VALIDATION_MESSAGES } from '@osf/shared/constants';
+import { CustomValidators, findChangedFields } from '@osf/shared/utils';
import { FieldType } from '../../enums';
-import { RegistriesSelectors } from '../../store';
+import { PageSchema } from '../../models';
+import { RegistriesSelectors, UpdateDraft, UpdateStepValidation } from '../../store';
@Component({
selector: 'osf-custom-step',
@@ -28,12 +33,15 @@ import { RegistriesSelectors } from '../../store';
RadioButton,
FormsModule,
Checkbox,
-
+ TranslatePipe,
InputText,
NgTemplateOutlet,
Inplace,
TranslatePipe,
InfoIconComponent,
+ Button,
+ ReactiveFormsModule,
+ Message,
],
templateUrl: './custom-step.component.html',
styleUrl: './custom-step.component.scss',
@@ -41,16 +49,116 @@ import { RegistriesSelectors } from '../../store';
})
export class CustomStepComponent {
private readonly route = inject(ActivatedRoute);
- step = signal(this.route.snapshot.params['step']);
+ private readonly router = inject(Router);
+ private readonly fb = inject(FormBuilder);
+
protected readonly pages = select(RegistriesSelectors.getPagesSchema);
- currentPage = computed(() => this.pages()[this.step() - 1]);
protected readonly FieldType = FieldType;
+ protected readonly stepsData = select(RegistriesSelectors.getStepsData);
+ protected stepsValidation = select(RegistriesSelectors.getStepsValidation);
+
+ protected actions = createDispatchMap({
+ updateDraft: UpdateDraft,
+ updateStepValidation: UpdateStepValidation,
+ });
+
+ readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES;
+
+ step = signal(this.route.snapshot.params['step']);
+ currentPage = computed(() => this.pages()[this.step() - 1]);
radio = null;
+ stepForm!: FormGroup;
+
constructor() {
this.route.params.pipe(takeUntilDestroyed()).subscribe((params) => {
+ this.updateStepState();
this.step.set(+params['step']);
});
+
+ effect(() => {
+ const page = this.currentPage();
+ if (page) {
+ this.initStepForm(page);
+ }
+ });
+ }
+
+ private initStepForm(page: PageSchema): void {
+ this.stepForm = this.fb.group({});
+
+ page.questions?.forEach((q) => {
+ const controlName = q.responseKey as string;
+ let control: FormControl;
+
+ switch (q.fieldType) {
+ case FieldType.Text:
+ case FieldType.TextArea:
+ control = this.fb.control(this.stepsData()[controlName], {
+ validators: q.required ? [CustomValidators.requiredTrimmed()] : [],
+ });
+ break;
+
+ case FieldType.Checkbox:
+ control = this.fb.control(this.stepsData()[controlName] || [], {
+ validators: q.required ? [Validators.required] : [],
+ });
+ break;
+
+ case FieldType.Radio:
+ case FieldType.Select:
+ control = this.fb.control(this.stepsData()[controlName], {
+ validators: q.required ? [Validators.required] : [],
+ });
+ break;
+
+ default:
+ console.warn(`Unsupported field type: ${q.fieldType}`);
+ return;
+ }
+
+ this.stepForm.addControl(controlName, control);
+ });
+ if (this.stepsValidation()?.[this.step()]?.invalid) {
+ this.stepForm.markAllAsTouched();
+ }
+ }
+
+ private updateDraft() {
+ const changedFields = findChangedFields(this.stepForm.value, this.stepsData());
+ if (Object.keys(changedFields).length > 0) {
+ const draftId = this.route.snapshot.params['id'];
+ const attributes = {
+ registration_responses: this.stepForm.value,
+ };
+ this.actions.updateDraft(draftId, attributes);
+ }
+ }
+
+ private updateStepState() {
+ if (this.stepForm) {
+ this.updateDraft();
+ this.stepForm.markAllAsTouched();
+ this.actions.updateStepValidation(this.step(), this.stepForm.invalid);
+ }
+ }
+
+ goBack(): void {
+ const previousStep = this.step() - 1;
+ if (previousStep > 0) {
+ this.router.navigate(['../', previousStep], { relativeTo: this.route });
+ } else {
+ this.router.navigate(['../', 'metadata'], { relativeTo: this.route });
+ }
+ }
+
+ goNext(): void {
+ const nextStep = this.step() + 1;
+ if (nextStep <= this.pages().length) {
+ this.router.navigate(['../', nextStep], { relativeTo: this.route });
+ } else {
+ this.router.navigate(['../', 'review'], { relativeTo: this.route });
+ }
}
}
diff --git a/src/app/features/registries/components/drafts/drafts.component.ts b/src/app/features/registries/components/drafts/drafts.component.ts
index fe7a21765..fffaa6c56 100644
--- a/src/app/features/registries/components/drafts/drafts.component.ts
+++ b/src/app/features/registries/components/drafts/drafts.component.ts
@@ -32,6 +32,7 @@ export class DraftsComponent {
protected readonly pages = select(RegistriesSelectors.getPagesSchema);
protected readonly draftRegistration = select(RegistriesSelectors.getDraftRegistration);
protected stepsValidation = select(RegistriesSelectors.getStepsValidation);
+ protected readonly stepsData = select(RegistriesSelectors.getStepsData);
private readonly actions = createDispatchMap({
getSchemaBlocks: FetchSchemaBlocks,
@@ -101,7 +102,7 @@ export class DraftsComponent {
}
effect(() => {
const registrationSchemaId = this.draftRegistration()?.registrationSchemaId;
- if (registrationSchemaId) {
+ if (registrationSchemaId && !this.pages().length) {
this.actions
.getSchemaBlocks(registrationSchemaId || '')
.pipe(
diff --git a/src/app/features/registries/mappers/registration.mapper.ts b/src/app/features/registries/mappers/registration.mapper.ts
index 2d307b2a7..6111229fd 100644
--- a/src/app/features/registries/mappers/registration.mapper.ts
+++ b/src/app/features/registries/mappers/registration.mapper.ts
@@ -18,6 +18,7 @@ export class RegistrationMapper {
: null,
},
tags: response.attributes.tags || [],
+ stepsData: response.attributes.registration_responses || {},
};
}
}
diff --git a/src/app/features/registries/models/registration.model.ts b/src/app/features/registries/models/registration.model.ts
index afe2de619..972d62566 100644
--- a/src/app/features/registries/models/registration.model.ts
+++ b/src/app/features/registries/models/registration.model.ts
@@ -10,4 +10,6 @@ export interface Registration {
options: LicenseOptions | null;
};
tags: string[];
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ stepsData?: Record
;
}
diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts
index 21f417634..7988e5879 100644
--- a/src/app/features/registries/store/registries.selectors.ts
+++ b/src/app/features/registries/store/registries.selectors.ts
@@ -82,4 +82,9 @@ export class RegistriesSelectors {
static getStepsValidation(state: RegistriesStateModel): Record {
return state.stepsValidation;
}
+
+ @Selector([RegistriesState])
+ static getStepsData(state: RegistriesStateModel) {
+ return state.draftRegistration.data?.stepsData || {};
+ }
}
diff --git a/src/app/shared/components/stepper/stepper.component.html b/src/app/shared/components/stepper/stepper.component.html
index 72651d17a..3b56e75cd 100644
--- a/src/app/shared/components/stepper/stepper.component.html
+++ b/src/app/shared/components/stepper/stepper.component.html
@@ -9,14 +9,14 @@
[class.current]="i === currentStep().index"
(click)="onStepClick(step)"
>
- @if (i < currentStep().index) {
- @if (step.invalid) {
-
- } @else {
+ @if (step.invalid && i !== currentStep().index) {
+
+ } @else {
+ @if (i < currentStep().index) {
+ } @else {
+ {{ i + 1 }}
}
- } @else {
- {{ i + 1 }}
}
@if (i < steps().length - 1) {