Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Select and Radio fields. Added basic tests on both components. … #26461

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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 @@ -17,4 +17,10 @@
field.hint
}}</small>
</div>

<dot-edit-content-select-field *ngSwitchCase="'Select'" [field]="field" />
zJaaal marked this conversation as resolved.
Show resolved Hide resolved

<dot-edit-content-radio-field
*ngSwitchCase="'Radio'"
[field]="field"></dot-edit-content-radio-field>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ import { InputTextModule } from 'primeng/inputtext';
import { DotCMSContentTypeField } from '@dotcms/dotcms-models';
import { DotFieldRequiredDirective } from '@dotcms/ui';

import { DotEditContentRadioFieldComponent } from '../../fields/dot-edit-content-radio-field/dot-edit-content-radio-field.component';
import { DotEditContentSelectFieldComponent } from '../../fields/dot-edit-content-select-field/dot-edit-content-select-field.component';

@Component({
selector: 'dot-edit-content-field',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, InputTextModule, DotFieldRequiredDirective],
imports: [
CommonModule,
zJaaal marked this conversation as resolved.
Show resolved Hide resolved
ReactiveFormsModule,
InputTextModule,
DotFieldRequiredDirective,
DotEditContentSelectFieldComponent,
DotEditContentRadioFieldComponent
],
templateUrl: './dot-edit-content-field.component.html',
styleUrls: ['./dot-edit-content-field.component.scss'],
viewProviders: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<form class="p-fluid" *ngIf="form" [formGroup]="form">
<ng-container *ngFor="let row of formData">
<ng-container *ngFor="let row of formData.layout">
<div class="row">
<div class="column" *ngFor="let column of row.columns">
<dot-edit-content-field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MockDotMessageService } from '@dotcms/utils-testing';

import { DotEditContentFormComponent } from './dot-edit-content-form.component';

import { EditContentFormData } from '../../interfaces/dot-edit-content-form.interface';
import { DotEditContentFieldComponent } from '../dot-edit-content-field/dot-edit-content-field.component';
import { FIELD_MOCK } from '../dot-edit-content-field/dot-edit-content-field.component.spec';

Expand Down Expand Up @@ -159,6 +160,15 @@ export const LAYOUT_MOCK: DotCMSContentTypeLayoutRow[] = [
}
];

export const VALUES_MOCK = {
name1: 'Name1',
text2: 'Text2'
};

export const CONTENT_FORM_DATA_MOCK: EditContentFormData = {
layout: LAYOUT_MOCK
};

describe('DotFormComponent', () => {
let spectator: Spectator<DotEditContentFormComponent>;
const createComponent = createComponentFactory({
Expand All @@ -183,15 +193,15 @@ describe('DotFormComponent', () => {
beforeEach(() => {
spectator = createComponent({
props: {
formData: LAYOUT_MOCK
formData: CONTENT_FORM_DATA_MOCK
}
});
});

describe('initilizeForm', () => {
it('should initialize the form group with form controls for each field in the `formData` array', () => {
const component = spectator.component;
component.formData = LAYOUT_MOCK;
component.formData = CONTENT_FORM_DATA_MOCK;
KevinDavilaDotCMS marked this conversation as resolved.
Show resolved Hide resolved
spectator.detectChanges();

expect(component.form.controls['name1']).toBeDefined();
Expand All @@ -211,7 +221,7 @@ describe('DotFormComponent', () => {
describe('saveContent', () => {
it('should emit the form value through the `formSubmit` event', () => {
const component = spectator.component;
component.formData = LAYOUT_MOCK;
component.formData = CONTENT_FORM_DATA_MOCK;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, that is the reason you need to add the line 225.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

component.initilizeForm();

jest.spyOn(component.formSubmit, 'emit');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angula

import { ButtonModule } from 'primeng/button';

import { DotCMSContentTypeField, DotCMSContentTypeLayoutRow } from '@dotcms/dotcms-models';
import { DotCMSContentTypeField } from '@dotcms/dotcms-models';
import { DotMessagePipe } from '@dotcms/ui';

import { EditContentFormData } from '../../interfaces/dot-edit-content-form.interface';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { EditContentFormData } from '../../interfaces/dot-edit-content-form.interface';
import { EditContentFormData } from '../../models/dot-edit-content-form.interface';

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing this one.

import { DotEditContentFieldComponent } from '../dot-edit-content-field/dot-edit-content-field.component';
@Component({
selector: 'dot-edit-content-form',
Expand All @@ -31,7 +32,7 @@ import { DotEditContentFieldComponent } from '../dot-edit-content-field/dot-edit
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DotEditContentFormComponent implements OnInit {
@Input() formData: DotCMSContentTypeLayoutRow[] = [];
@Input() formData!: EditContentFormData;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@Output() formSubmit = new EventEmitter();

private fb = inject(FormBuilder);
Expand All @@ -49,7 +50,7 @@ export class DotEditContentFormComponent implements OnInit {
*/
initilizeForm() {
this.form = this.fb.group({});
this.formData.forEach(({ columns }) => {
this.formData.layout.forEach(({ columns }) => {
columns?.forEach((column) => {
column.fields.forEach((field) => {
const fieldControl = this.initializeFormControl(field);
Expand All @@ -76,7 +77,9 @@ export class DotEditContentFormComponent implements OnInit {
}
}

return this.fb.control(null, { validators });
return this.fb.control(this.formData.values?.[field.variable] ?? field.defaultValue, {
validators
});
}

/**
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should unify /interfaces and /enums in /models that's what we have been doing.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum DotEditContentFieldDataType {
//TODO: Doc all posibles values of DataType fields
BOOL = 'BOOL',
INTEGER = 'INTEGER',
FLOAT = 'FLOAT'
}
zJaaal marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<ng-container *ngIf="formData$ | async as form; else noContent">
<dot-edit-content-form [formData]="form" (formSubmit)="saveContent($event)" />
<ng-container *ngIf="formData$ | async as formData; else noContent">
<dot-edit-content-form [formData]="formData" (formSubmit)="saveContent($event)" />
<p *ngIf="isContentSaved">Content saved!</p>
</ng-container>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { EMPTY } from 'rxjs';
import { EMPTY, Observable } from 'rxjs';

import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { switchMap } from 'rxjs/operators';
import { map, switchMap } from 'rxjs/operators';

import { DotEditContentFormComponent } from '../../components/dot-edit-content-form/dot-edit-content-form.component';
import { EditContentFormData } from '../../interfaces/dot-edit-content-form.interface';
import { DotEditContentService } from '../../services/dot-edit-content.service';

@Component({
selector: 'dot-edit-content-form-layout',
standalone: true,
Expand All @@ -25,27 +27,36 @@ export class EditContentLayoutComponent {

private readonly dotEditContentService = inject(DotEditContentService);
isContentSaved = false;
formData$ = this.identifier

formData$: Observable<EditContentFormData> = this.identifier
? this.dotEditContentService.getContentById(this.identifier).pipe(
switchMap(({ contentType }) => {
switchMap(({ contentType, ...other }) => {
if (contentType) {
zJaaal marked this conversation as resolved.
Show resolved Hide resolved
this.contentType = contentType;

return this.dotEditContentService.getContentTypeFormData(contentType);
return this.dotEditContentService
.getContentTypeFormData(contentType)
.pipe(map((res) => ({ values: { ...other }, layout: res })));
} else {
return EMPTY;
}
})
)
: this.dotEditContentService.getContentTypeFormData(this.contentType);
: this.dotEditContentService
.getContentTypeFormData(this.contentType)
.pipe(map((res) => ({ layout: res })));

/**
* Saves the contentlet with the given values.
* @param value - An object containing the key-value pairs of the contentlet to be saved.
*/
saveContent(value: { [key: string]: string }) {
this.dotEditContentService
.saveContentlet({ ...value, inode: this.identifier, contentType: this.contentType })
.saveContentlet({
...value,
inode: this.identifier,
contentType: this.contentType
})
.subscribe({
next: () => {
this.isContentSaved = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div class="field">
<label
[attr.data-testId]="'label-' + field.variable"
[for]="field.variable"
[checkIsRequiredControl]="field.variable"
dotFieldRequired
>{{ field.name }}</label
>
zJaaal marked this conversation as resolved.
Show resolved Hide resolved
<div class="field-checkbox" *ngFor="let option of options">
<p-radioButton
[inputId]="option.value"
[value]="option.value"
[formControlName]="field.variable"></p-radioButton>
<label class="ml-2" [for]="option.value">{{ option.label }}</label>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After you do the necessary changes, remember to make use of the :host

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can bind the class .field to the host.

<small *ngIf="field.hint" [attr.data-testId]="'hint-' + field.variable">{{ field.hint }}</small>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Spectator, createComponentFactory } from '@ngneat/spectator';

import { CommonModule } from '@angular/common';
import {
ControlContainer,
FormControl,
FormGroup,
FormGroupDirective,
ReactiveFormsModule
} from '@angular/forms';

import { RadioButtonModule } from 'primeng/radiobutton';

import { DotFieldRequiredDirective } from '@dotcms/ui';

import { DotEditContentRadioFieldComponent } from './dot-edit-content-radio-field.component';

export const FIELD_RADIO_MOCK = {
clazz: 'com.dotcms.contenttype.model.field.ImmutableRadioField',
contentTypeId: '40e0cb1b57b3b1b7ec34191e942316d5',
dataType: 'TEXT',
fieldType: 'Radio',
fieldTypeLabel: 'Radio',
fieldVariables: [],
fixed: false,
forceIncludeInApi: false,
iDate: 1697598313000,
id: '824b4e9907fe4f450ced438598cc0ce8',
indexed: false,
listed: false,
modDate: 1697662296000,
name: 'radio',
readOnly: false,
required: false,
searchable: false,
sortOrder: 8,
unique: false,
values: 'Uno|uno\r\nDos|dos',
variable: 'radio'
};

const FORM_GROUP_MOCK = new FormGroup({
radio: new FormControl('')
});
const FORM_GROUP_DIRECTIVE_MOCK: FormGroupDirective = new FormGroupDirective([], []);
FORM_GROUP_DIRECTIVE_MOCK.form = FORM_GROUP_MOCK;
zJaaal marked this conversation as resolved.
Show resolved Hide resolved

describe('DotEditContentRadioFieldComponent', () => {
let spectator: Spectator<DotEditContentRadioFieldComponent>;

const createComponent = createComponentFactory({
component: DotEditContentRadioFieldComponent,
imports: [CommonModule, RadioButtonModule, ReactiveFormsModule, DotFieldRequiredDirective],
componentViewProviders: [
{ provide: ControlContainer, useValue: FORM_GROUP_DIRECTIVE_MOCK }
],
providers: [FormGroupDirective]
});

beforeEach(() => {
spectator = createComponent({
props: {
field: FIELD_RADIO_MOCK
}
});
});
it('should have a options array', () => {
const expectedList = [
{
label: 'Uno',
value: 'uno'
},
{
label: 'Dos',
value: 'dos'
}
];
spectator.detectChanges();
expect(spectator.component.options).toEqual(expectedList);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input, OnInit, inject } from '@angular/core';
import { ControlContainer, ReactiveFormsModule } from '@angular/forms';

import { RadioButtonModule } from 'primeng/radiobutton';

import { DotCMSContentTypeField } from '@dotcms/dotcms-models';
import { DotFieldRequiredDirective } from '@dotcms/ui';

import { mapOptions } from '../../utils/functions.util';

@Component({
selector: 'dot-edit-content-radio-field',
standalone: true,
imports: [CommonModule, RadioButtonModule, ReactiveFormsModule, DotFieldRequiredDirective],
templateUrl: './dot-edit-content-radio-field.component.html',
styleUrls: ['./dot-edit-content-radio-field.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
viewProviders: [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true })
}
]
})
export class DotEditContentRadioFieldComponent implements OnInit {
@Input() field!: DotCMSContentTypeField;
private readonly controlContainer = inject(ControlContainer);

options = [];

ngOnInit() {
this.options = mapOptions(this.field.values || '', this.field.dataType);
if (!this.formControl.value) {
this.formControl.setValue(this.options[0]?.value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we take advantage of types forms here? for example this is infering:

(property) DotEditContentRadioFieldComponent.formControl: AbstractControl<any, any>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on this?

}
}

/**
* Returns the form control for the radio field.
* @returns {AbstractControl} The form control for the radio field.
*/
get formControl() {
return this.controlContainer.control.get(this.field.variable);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="field">
<label
[for]="field.variable"
[id]="'aria-' + field.variable"
[checkIsRequiredControl]="field.variable"
dotFieldRequired
>{{ field.name }}</label
>
<p-dropdown
[formControlName]="field.variable"
[options]="options"
[attr.aria-labelledby]="'aria-' + field.variable"
optionLabel="label"
optionValue="value"></p-dropdown>
<small *ngIf="field.hint" [attr.data-testId]="'hint-' + field.variable">{{ field.hint }}</small>
</div>
KevinDavilaDotCMS marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading