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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,103 +21,139 @@ <h3 class="mb-2">{{ section.title }}</h3>

<ng-template #questionList>
@for (question of questions; track question.id) {
<p-card>
<label [for]="question.groupKey">
<h3 class="mb-2">
{{ question.displayText }}
@if (question.required) {
<span class="text-red-500">*</span>
<form [formGroup]="stepForm">
<p-card>
<label [for]="question.groupKey">
<h3 class="mb-2">
{{ question.displayText }}
@if (question.required) {
<span class="text-red-500">*</span>
}
</h3>
@if (question.helpText) {
<p class="mb-3">{{ question.helpText }}</p>
}
</h3>
@if (question.helpText) {
<p class="mb-3">{{ question.helpText }}</p>
}
@if (question.paragraphText) {
<p class="mb-3">{{ question.paragraphText }}</p>
}
</label>
@if (question.exampleText) {
<p-inplace #inplaceRef styleClass="mb-4">
<ng-template #display>
<span class="text-primary">{{ 'common.links.showExample' | translate }} </span>
</ng-template>
<ng-template #content>
<div class="p-inplace-display">
<button
class="text-primary border-none cursor-pointer bg-transparent text-sm"
tabindex="0"
role="button"
(click)="inplaceRef.deactivate()"
(keyup.enter)="inplaceRef.deactivate()"
(keyup.space)="inplaceRef.deactivate()"
>
{{ 'common.links.hideExample' | translate }}
</button>
</div>
<p class="m-0">{{ question.exampleText }}</p>
</ng-template>
</p-inplace>
}

@switch (question.fieldType) {
@case (FieldType.TextArea) {
<textarea id="{{ question.groupKey }}" class="w-full" rows="5" cols="30" pTextarea></textarea>
}
@case (FieldType.Radio) {
<div class="flex flex-column gap-2">
@for (option of question.options; track option) {
<div class="flex align -items-center gap-2">
<p-radioButton
[inputId]="option.value"
[(ngModel)]="radio"
[name]="question.groupKey || ''"
[value]="option.value"
></p-radioButton>
<label [for]="option.value" class="ml-2">{{ option.label }}</label>
@if (option.helpText) {
<osf-info-icon [tooltipText]="option.helpText"></osf-info-icon>
}
@if (question.paragraphText) {
<p class="mb-3">{{ question.paragraphText }}</p>
}
</label>
@if (question.exampleText) {
<p-inplace #inplaceRef styleClass="mb-4">
<ng-template #display>
<span class="text-primary">{{ 'common.links.showExample' | translate }} </span>
</ng-template>
<ng-template #content>
<div class="p-inplace-display">
<button
class="text-primary border-none cursor-pointer bg-transparent text-sm"
tabindex="0"
role="button"
(click)="inplaceRef.deactivate()"
(keyup.enter)="inplaceRef.deactivate()"
(keyup.space)="inplaceRef.deactivate()"
>
{{ 'common.links.hideExample' | translate }}
</button>
</div>
}
</div>
<p class="m-0">{{ question.exampleText }}</p>
</ng-template>
</p-inplace>
}
@case (FieldType.Checkbox) {
<div class="flex flex-column gap-2">
@for (option of question.options; track option) {
<div class="flex align-items-center gap-2">
<p-checkbox
[inputId]="option.value"
[name]="question.groupKey || ''"
[value]="option.value"
></p-checkbox>
<label [for]="option.value" class="ml-2">{{ option.label }}</label>
</div>

@switch (question.fieldType) {
@case (FieldType.TextArea) {
<textarea
id="{{ question.groupKey }}"
class="w-full"
rows="5"
cols="30"
pTextarea
[formControlName]="question.responseKey!"
></textarea>
@if (
stepForm.controls[question.responseKey!].errors?.['required'] &&
(stepForm.controls[question.responseKey!].touched || stepForm.controls[question.responseKey!].dirty)
) {
<p-message class="simple-variant flex mt-1" severity="error" variant="simple" size="small">
{{ INPUT_VALIDATION_MESSAGES.required | translate }}
</p-message>
}
</div>
}
}
@case (FieldType.Radio) {
<div class="flex flex-column gap-2">
@for (option of question.options; track option) {
<div class="flex align -items-center gap-2">
<p-radioButton
[formControlName]="question.responseKey!"
[inputId]="option.value"
[value]="option.value"
></p-radioButton>
<label [for]="option.value" class="ml-2">{{ option.label }}</label>
@if (option.helpText) {
<osf-info-icon [tooltipText]="option.helpText"></osf-info-icon>
}
</div>
}
</div>
}
@case (FieldType.Checkbox) {
<div class="flex flex-column gap-2">
@for (option of question.options; track option) {
<div class="flex align-items-center gap-2">
<p-checkbox
[inputId]="option.value"
[formControlName]="question.responseKey!"
[value]="option.value"
></p-checkbox>
<label [for]="option.value" class="ml-2">{{ option.label }}</label>
</div>
}
</div>
}

@case (FieldType.Text) {
<input
id="{{ question.groupKey }}"
type="text"
class="w-full"
[placeholder]="question.exampleText"
pInputText
/>
}
@case (FieldType.File) {
<h3 class="mb-2">Upload File</h3>
<p class="mb-1">You may attach up to 5 file(s) to this question. Files cannot total over 5GB in size.</p>
<p>
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.
</p>
@case (FieldType.Text) {
<input
[formControlName]="question.responseKey!"
type="text"
class="w-full"
[placeholder]="question.exampleText"
pInputText
/>
@if (
stepForm.controls[question.responseKey!].errors?.['required'] &&
(stepForm.controls[question.responseKey!].touched || stepForm.controls[question.responseKey!].dirty)
) {
<p-message class="simple-variant flex mt-1" severity="error" variant="simple" size="small">
{{ INPUT_VALIDATION_MESSAGES.required | translate }}
</p-message>
}
}
@case (FieldType.File) {
<h3 class="mb-2">Upload File</h3>
<p class="mb-1">
You may attach up to 5 file(s) to this question. Files cannot total over 5GB in size.
</p>
<p>
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.
</p>

<p>File input is not implemented yet.</p>
<p>File input is not implemented yet.</p>
}
}
}
</p-card>
</p-card>
</form>
}
</ng-template>
}
<div class="flex justify-content-end">
<p-button
type="button"
[label]="'common.buttons.back' | translate"
severity="info"
class="mr-2"
(click)="goBack()"
></p-button>
<p-button type="button" [label]="'common.buttons.next' | translate" (click)="goNext()"></p-button>
</div>
</section>
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -28,29 +33,132 @@ 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',
changeDetection: ChangeDetectionStrategy.OnPush,
})
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 });
}
}
}
Loading