AAE-27107 Improve repeatable section widget (#11336)

* AAE-27107 Improve logic

* AAE-27107 Add tests

* AAE-27107 Fix logic

* AAE-27107 Fix test

* AAE-27107 Fix updateForm logic
This commit is contained in:
Diogo Bastos
2025-11-11 10:09:03 +00:00
committed by GitHub
parent 54f20b7b6e
commit 0d670dd333
11 changed files with 385 additions and 199 deletions
@@ -81,8 +81,24 @@
<div <div
class="adf-grid-list-container" class="adf-grid-list-container"
[class.adf-grid-list-container-multiple]="hasMultipleRows" [class.adf-grid-list-container-multiple]="hasMultipleRows"
[class.adf-grid-list-container-disabled]="currentRootElement.field.readOnly"
> >
<h4 class="adf-grid-list-container-label">{{ 'FORM.FORM_RENDERER.ROW_LABEL' | translate: {number: rowIndex + 1} }}</h4> <div class="adf-grid-list-row">
<span class="adf-grid-list-row-label">{{ 'FORM.FORM_RENDERER.ROW_LABEL' | translate: {number: rowIndex + 1} }}</span>
@let shouldDisplayRemoveRowButton = !currentRootElement.field.rows[rowIndex].isInitial || (currentRootElement.field.rows[rowIndex].isInitial && currentRootElement.field.params.allowInitialRowsDelete);
@if (shouldDisplayRemoveRowButton) {
<button
mat-icon-button
class="adf-grid-list-row-remove-button"
[disabled]="currentRootElement.field.readOnly"
[matTooltip]="'FORM.FORM_RENDERER.DELETE_ROW' | translate"
(click)="displayDialogToRemoveRow(currentRootElement.field, rowIndex)"
>
<mat-icon>close</mat-icon>
</button>
}
</div>
<section class="adf-grid-list-column-view"> <section class="adf-grid-list-column-view">
@for (column of row.columns; track $index) { @for (column of row.columns; track $index) {
<div <div
@@ -100,17 +116,6 @@
} }
</div> </div>
} }
@let shouldDisplayRemoveRowButton = !currentRootElement.field.rows[rowIndex].isInitial || (currentRootElement.field.rows[rowIndex].isInitial && currentRootElement.field.params.allowInitialRowsDelete);
@if (shouldDisplayRemoveRowButton) {
<button
mat-icon-button
class="adf-grid-list-remove-row-button"
(click)="displayDialogToRemoveRow(currentRootElement.field, rowIndex)"
>
<mat-icon>close</mat-icon>
</button>
}
</section> </section>
</div> </div>
} }
@@ -98,28 +98,48 @@
padding-right: 3px; padding-right: 3px;
} }
&-remove-row-button { &-row {
margin-top: 20px; display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
&-remove-button {
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
#{ms.$mat-icon} { #{ms.$mat-icon} {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 20px; font-size: 18px;
}
} }
} }
&-container { &-container {
padding: 0 10px; padding: 0 15px;
&-label {
margin: 5px 0 5px -10px;
}
&-multiple { &-multiple {
border-bottom: 1px solid rgba(0, 0, 0, 0.54); border-bottom: 1px solid rgba(0, 0, 0, 0.54);
margin-bottom: 25px; margin-bottom: 25px;
} }
&-disabled {
&.adf-grid-list-container-multiple {
border-bottom: 1px solid
var(--mdc-text-button-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));
}
.adf-grid-list-row-label {
color: var(--mdc-text-button-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));
cursor: default;
}
}
} }
} }
@@ -921,7 +921,7 @@ describe('Form Renderer Component', () => {
expect(row).toBeTruthy(); expect(row).toBeTruthy();
const removeRowButton = testingUtils.getByCSS( const removeRowButton = testingUtils.getByCSS(
'#field-RepeatableSection0tbw2y-container .adf-grid-list-container .adf-grid-list-remove-row-button' '#field-RepeatableSection0tbw2y-container .adf-grid-list-container .adf-grid-list-row-remove-button'
); );
expect(removeRowButton).toBeTruthy(); expect(removeRowButton).toBeTruthy();
@@ -938,7 +938,7 @@ describe('Form Renderer Component', () => {
expect(row).toBeTruthy(); expect(row).toBeTruthy();
const removeRowButton = testingUtils.getByCSS( const removeRowButton = testingUtils.getByCSS(
'#field-RepeatableSection0tbw2y-container .adf-grid-list-container .adf-grid-list-remove-row-button' '#field-RepeatableSection0tbw2y-container .adf-grid-list-container .adf-grid-list-row-remove-button'
); );
expect(removeRowButton).toBeFalsy(); expect(removeRowButton).toBeFalsy();
@@ -32,6 +32,7 @@ import { FormSectionComponent } from './form-section/form-section.component';
import { DecimalRenderMiddlewareService } from './middlewares/decimal-middleware.service'; import { DecimalRenderMiddlewareService } from './middlewares/decimal-middleware.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '../../../lib/dialogs/confirm-dialog/confirm.dialog'; import { ConfirmDialogComponent } from '../../../lib/dialogs/confirm-dialog/confirm.dialog';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({ @Component({
selector: 'adf-form-renderer', selector: 'adf-form-renderer',
@@ -63,7 +64,8 @@ import { ConfirmDialogComponent } from '../../../lib/dialogs/confirm-dialog/conf
NgClass, NgClass,
HeaderWidgetComponent, HeaderWidgetComponent,
FormSectionComponent, FormSectionComponent,
RepeatWidgetComponent RepeatWidgetComponent,
MatTooltipModule
], ],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
@@ -1272,20 +1272,7 @@ describe('FormFieldModel', () => {
let form: FormModel; let form: FormModel;
let field: FormFieldModel; let field: FormFieldModel;
beforeEach(() => { const fields = {
form = new FormModel();
field = new FormFieldModel(form, {
id: 'RepeatableSection0tbw2y',
name: 'Repeatable Section',
type: 'repeatable-section',
tab: null,
params: {
initialNumberOfRows: 2,
allowInitialRowsDelete: true,
newRowsLimit: 3
},
numberOfColumns: 2,
fields: {
'1': [ '1': [
{ {
id: 'Text0wwp7n', id: 'Text0wwp7n',
@@ -1325,8 +1312,81 @@ describe('FormFieldModel', () => {
} }
} }
] ]
};
const fieldsDisabled = {
'1': [
{
id: 'Text0wwp7n',
name: 'Text',
type: 'text',
readOnly: true,
required: false,
colspan: 1,
rowspan: 1,
placeholder: null,
minLength: 0,
maxLength: 0,
regexPattern: null,
visibilityCondition: null,
params: {
existingColspan: 1,
maxColspan: 2
} }
}); }
],
'2': [
{
id: 'Integer0rzkwq',
name: 'Integer',
type: 'integer',
readOnly: false,
colspan: 1,
rowspan: 1,
placeholder: null,
minValue: null,
maxValue: null,
required: false,
visibilityCondition: null,
params: {
existingColspan: 1,
maxColspan: 2
}
}
]
};
const json = {
id: 'RepeatableSection0tbw2y',
name: 'Repeatable Section',
type: 'repeatable-section',
tab: null,
params: {
initialNumberOfRows: 2,
allowInitialRowsDelete: true,
maxNumberOfRows: 5
},
numberOfColumns: 2,
fields
};
const jsonDisabled = {
id: 'RepeatableSection0tbw2y',
name: 'Repeatable Section',
type: 'repeatable-section',
tab: null,
params: {
initialNumberOfRows: 2,
allowInitialRowsDelete: true,
maxNumberOfRows: 5
},
numberOfColumns: 2,
fields: fieldsDisabled
};
beforeEach(() => {
form = new FormModel();
field = new FormFieldModel(form, json);
}); });
describe('add row', () => { describe('add row', () => {
@@ -1408,5 +1468,64 @@ describe('FormFieldModel', () => {
expect(form.values[field.id]).toEqual(formValues.removeState); expect(form.values[field.id]).toEqual(formValues.removeState);
}); });
}); });
describe('disabled state', () => {
const textWidgetId = 'Text0wwp7n';
/**
*
* @param expectation expectation function
*/
function checkChildrenWidgets(expectation: any) {
for (const row of field.rows) {
for (const column of row.columns) {
for (const child of column.fields) {
expectation(child);
}
}
}
}
it('should make all children fields disabled if repeatable section is disabled', () => {
field.readOnly = true;
checkChildrenWidgets((child) => {
expect(child.readOnly).toBeTruthy();
});
});
it('should allow for initial disabled children widgets if repeatable section is enabled', () => {
field = new FormFieldModel(form, jsonDisabled);
checkChildrenWidgets((child) => {
if (child.id.startsWith(textWidgetId)) {
expect(child.readOnly).toBeTruthy();
return;
}
expect(child.readOnly).toBeFalsy();
});
});
it('should keep initial disabled children widgets if repeatable section is re-enabled', () => {
field = new FormFieldModel(form, jsonDisabled);
field.rows[0].columns[0].fields[0].readOnly = true;
field.readOnly = true;
checkChildrenWidgets((child) => {
expect(child.readOnly).toBeTruthy();
});
field.readOnly = false;
checkChildrenWidgets((child) => {
if (child.id.startsWith(textWidgetId)) {
expect(child.readOnly).toBeTruthy();
return;
}
expect(child.readOnly).toBeFalsy();
});
});
});
}); });
}); });
@@ -137,6 +137,12 @@ export class FormFieldModel extends FormWidgetModel {
set readOnly(readOnly: boolean) { set readOnly(readOnly: boolean) {
this._readOnly = readOnly; this._readOnly = readOnly;
if (this.type === FormFieldTypes.REPEATABLE_SECTION) {
this.updateRepeatableSectionReadOnlyState(readOnly);
return;
}
this.updateForm(); this.updateForm();
} }
@@ -312,7 +318,7 @@ export class FormFieldModel extends FormWidgetModel {
} }
const col = new ContainerColumnModel(); const col = new ContainerColumnModel();
col.fields = (fields[currentField] || []).map((field: any) => new FormFieldModel(form, field)); col.fields = (fields[currentField] || []).map((field: any) => new FormFieldModel(form, field, this.setupParentConfig(field)));
col.rowspan = fields[currentField].length; col.rowspan = fields[currentField].length;
if (!FormFieldTypes.isSectionType(this.type)) { if (!FormFieldTypes.isSectionType(this.type)) {
@@ -324,6 +330,18 @@ export class FormFieldModel extends FormWidgetModel {
}); });
} }
private setupParentConfig(field: any) {
if (this.parent) {
return {
...this.parent,
uid: this.getUniqueId(field, this.parent.uid.split(ROW_ID_PREFIX)[1]),
value: this.parent.value?.[field.id]
};
}
return undefined;
}
private repeatableSectionFactory(json: any, form: any): void { private repeatableSectionFactory(json: any, form: any): void {
const { numberOfColumns = 1, params, value, fields = {} } = json; const { numberOfColumns = 1, params, value, fields = {} } = json;
@@ -333,26 +351,26 @@ export class FormFieldModel extends FormWidgetModel {
this.colspan = 1; this.colspan = 1;
this.rows = []; this.rows = [];
for (let i = 0; i < this.getNumberOfRows(params.initialNumberOfRows, params.newRowsLimit, value); i++) { for (let i = 0; i < this.getNumberOfRows(params.initialNumberOfRows, params.maxNumberOfRows, value); i++) {
this.rows.push(this.createRow(fields, form, i, value?.[i], i < params?.initialNumberOfRows)); this.rows.push(this.createRow(fields, form, i, value?.[i], i < params?.initialNumberOfRows));
} }
this.columns = this.rows[0].columns; this.columns = this.rows[0].columns;
} }
private getNumberOfRows(initialNrRows: number = 1, rowsLimit?: number, value?: any) { private getNumberOfRows(initialNrRows: number = 1, maxNrRows?: number, value?: any) {
return value?.length && !!rowsLimit ? Math.min(value?.length, initialNrRows + rowsLimit) : (value?.length ?? initialNrRows); return value?.length ? (maxNrRows ? Math.min(value.length, maxNrRows) : value.length) : initialNrRows;
} }
private createRow(fields: any, form: any, index: number, value?: any, isInitial: boolean = false) { private createRow(fields: any, form: any, index: number, value?: any, isInitial: boolean = false) {
const row = new ContainerRowModel(isInitial); const row = new ContainerRowModel(isInitial);
row.columns.push(...this.createColumns(fields, form, index, value)); row.columns.push(...this.createColumns(fields, form, row.id, index, value));
return row; return row;
} }
private createColumns(fields: any, form: any, index?: number, value?: any) { private createColumns(fields: any, form: any, rowId: string, index?: number, value?: any) {
const columns: ContainerColumnModel[] = []; const columns: ContainerColumnModel[] = [];
Object.keys(fields).forEach((currentField) => { Object.keys(fields).forEach((currentField) => {
@@ -365,10 +383,10 @@ export class FormFieldModel extends FormWidgetModel {
(field: any) => (field: any) =>
new FormFieldModel(form, field, { new FormFieldModel(form, field, {
id: this.id, id: this.id,
uid: this.getUniqueId(field), uid: this.getUniqueId(field, rowId),
fields: this.fields, fields: this.fields,
rowIndex: index ?? 0, rowIndex: index ?? 0,
value: value?.[field.id] value: field.type === FormFieldTypes.SECTION ? value : value?.[field.id]
}) })
); );
col.rowspan = fields[currentField].length; col.rowspan = fields[currentField].length;
@@ -384,8 +402,34 @@ export class FormFieldModel extends FormWidgetModel {
return columns; return columns;
} }
private getUniqueId(field: FormFieldModel): string { private updateRepeatableSectionReadOnlyState(state: boolean) {
return field.id + ROW_ID_PREFIX + window.crypto.getRandomValues(new Uint32Array(1))[0].toString(); for (const row of this.rows) {
for (const column of row.columns) {
for (const field of column.fields) {
if (field.type === FormFieldTypes.SECTION) {
this.updateInnerSectionReadOnlyState(field, state);
}
field.readOnly = this.getRepeatableSectionFieldReadOnlyState(field, state);
}
}
}
}
private updateInnerSectionReadOnlyState(section: FormFieldModel, state: boolean) {
for (const column of section.columns) {
for (const field of column.fields) {
field.readOnly = this.getRepeatableSectionFieldReadOnlyState(field, state);
}
}
}
private getRepeatableSectionFieldReadOnlyState(field: FormFieldModel, state: boolean): boolean {
return state || field.json.readOnly;
}
private getUniqueId(field: FormFieldModel, rowId: string): string {
return field.id + ROW_ID_PREFIX + rowId;
} }
private updateChildrenFieldsRowIndex() { private updateChildrenFieldsRowIndex() {
@@ -400,11 +444,23 @@ export class FormFieldModel extends FormWidgetModel {
private createInitialValue(fields: any) { private createInitialValue(fields: any) {
return Object.keys(fields) return Object.keys(fields)
.map((currentField) => (fields[currentField] || []).map((field) => field.id)) .map((currentField) => (fields[currentField] || []).map((field) => this.getFieldId(field)))
.flat(1) .flat(2)
.reduce((acc, curr) => ((acc[curr] = null), acc), {}); .reduce((acc, curr) => ((acc[curr] = null), acc), {});
} }
private getFieldId(field: any) {
if (field.type === FormFieldTypes.SECTION) {
const fields = field.fields;
return Object.keys(fields)
.map((currentField) => (fields[currentField] || []).map((e) => e.id))
.flat(1);
}
return field.id;
}
private updateContainerColspan(fields: FormFieldModel[]): void { private updateContainerColspan(fields: FormFieldModel[]): void {
fields.forEach((colField: FormFieldModel) => { fields.forEach((colField: FormFieldModel) => {
this.colspan = Math.max(this.colspan, colField.colspan); this.colspan = Math.max(this.colspan, colField.colspan);
@@ -420,7 +476,7 @@ export class FormFieldModel extends FormWidgetModel {
} }
private shouldAddRow(): boolean { private shouldAddRow(): boolean {
return !this.params.newRowsLimit || this.rows.length < this.params.initialNumberOfRows + this.params.newRowsLimit; return !this.params.maxNumberOfRows || this.rows.length < this.params.maxNumberOfRows;
} }
removeRow(index: number) { removeRow(index: number) {
@@ -526,7 +582,7 @@ export class FormFieldModel extends FormWidgetModel {
} }
if (this.isCheckboxField(json)) { if (this.isCheckboxField(json)) {
return json.value === 'true' || json.value === true; return json.value === 'true' || json.value === true || initialValue === true || initialValue === 'true';
} }
return value; return value;
@@ -537,16 +593,22 @@ export class FormFieldModel extends FormWidgetModel {
return; return;
} }
const formValue = this.getFormValue();
if (this.parent) { if (this.parent) {
this.updateRepeatableSectionValue(); this.updateRepeatableSectionValue(formValue);
return; } else {
this.updateValue(formValue);
} }
this.form.onFormFieldChanged(this);
}
getFormValue() {
switch (this.type) { switch (this.type) {
case FormFieldTypes.DROPDOWN: { case FormFieldTypes.DROPDOWN: {
if (!this.value) { if (!this.value) {
this.form.values[this.id] = null; return null;
break;
} }
/* /*
@@ -554,63 +616,57 @@ export class FormFieldModel extends FormWidgetModel {
but saving back as object: { id: <id>, name: <name> } but saving back as object: { id: <id>, name: <name> }
*/ */
if (Array.isArray(this.value)) { if (Array.isArray(this.value)) {
this.form.values[this.id] = this.value; return this.value;
break;
} }
if (typeof this.value === 'string') { if (typeof this.value === 'string') {
if (this.value === 'empty' || this.value === '') { if (this.value === 'empty' || this.value === '') {
this.form.values[this.id] = null; return null;
break;
} }
const matchingOption: FormFieldOption = this.options.find((opt) => opt.id === this.value); const matchingOption: FormFieldOption = this.options.find((opt) => opt.id === this.value);
this.form.values[this.id] = matchingOption || null; return matchingOption || null;
} }
if (typeof this.value === 'object') { if (typeof this.value === 'object') {
if (this.value.id === 'empty' || this.value.id === '') { if (this.value.id === 'empty' || this.value.id === '') {
this.form.values[this.id] = null; return null;
break;
} }
const matchingOption: FormFieldOption = this.options.find((opt) => opt.id === this.value.id); const matchingOption: FormFieldOption = this.options.find((opt) => opt.id === this.value.id);
this.form.values[this.id] = matchingOption; return matchingOption;
} }
break;
return null;
} }
case FormFieldTypes.RADIO_BUTTONS: { case FormFieldTypes.RADIO_BUTTONS: {
const radioButton: FormFieldOption = this.options.find((opt) => opt.id === this.value); const radioButton: FormFieldOption = this.options.find((opt) => opt.id === this.value);
if (this.optionType === 'rest') { if (this.optionType === 'rest') {
this.form.values[this.id] = radioButton return radioButton ? { ...radioButton, options: this.options } : { id: null, name: null, options: this.options };
? { ...radioButton, options: this.options }
: { id: null, name: null, options: this.options };
} else {
this.form.values[this.id] = radioButton ? { ...radioButton } : null;
} }
break; return radioButton ? { ...radioButton } : null;
} }
case FormFieldTypes.UPLOAD: { case FormFieldTypes.UPLOAD: {
this.form.hasUpload = true; this.form.hasUpload = true;
if (this.value && this.value.length > 0) { if (this.value && this.value.length > 0) {
this.form.values[this.id] = Array.isArray(this.value) ? this.value.map((elem) => elem.id).join(',') : [this.value]; return Array.isArray(this.value) ? this.value.map((elem) => elem.id).join(',') : [this.value];
} else {
this.form.values[this.id] = null;
} }
break;
return null;
} }
case FormFieldTypes.TYPEAHEAD: { case FormFieldTypes.TYPEAHEAD: {
const typeAheadEntry: FormFieldOption[] = this.options.filter((opt) => opt.id === this.value || opt.name === this.value); const typeAheadEntry: FormFieldOption[] = this.options.filter((opt) => opt.id === this.value || opt.name === this.value);
if (typeAheadEntry.length > 0) { if (typeAheadEntry.length > 0) {
this.form.values[this.id] = typeAheadEntry[0]; return typeAheadEntry[0];
} else if (this.options.length > 0) {
this.form.values[this.id] = null;
} }
break;
return null;
} }
case FormFieldTypes.DATE: { case FormFieldTypes.DATE: {
if (typeof this.value === 'string' && this.value === 'today') { if (typeof this.value === 'string' && this.value === 'today') {
@@ -618,6 +674,7 @@ export class FormFieldModel extends FormWidgetModel {
} }
let dateValue; let dateValue;
try { try {
dateValue = DateFnsUtils.parseDate(this.value, this.dateDisplayFormat); dateValue = DateFnsUtils.parseDate(this.value, this.dateDisplayFormat);
} catch { } catch {
@@ -626,12 +683,13 @@ export class FormFieldModel extends FormWidgetModel {
if (isValidDate(dateValue)) { if (isValidDate(dateValue)) {
const datePart = DateFnsUtils.formatDate(dateValue, 'yyyy-MM-dd'); const datePart = DateFnsUtils.formatDate(dateValue, 'yyyy-MM-dd');
this.form.values[this.id] = `${datePart}T00:00:00.000Z`;
} else { return `${datePart}T00:00:00.000Z`;
this.form.values[this.id] = null;
this._value = this.value;
} }
break;
this._value = this.value;
return null;
} }
case FormFieldTypes.DATETIME: { case FormFieldTypes.DATETIME: {
if (typeof this.value === 'string' && this.value === 'now') { if (typeof this.value === 'string' && this.value === 'now') {
@@ -641,39 +699,32 @@ export class FormFieldModel extends FormWidgetModel {
const dateTimeValue = this.value !== null ? DateFnsUtils.getDate(this.value) : null; const dateTimeValue = this.value !== null ? DateFnsUtils.getDate(this.value) : null;
if (isValidDate(dateTimeValue)) { if (isValidDate(dateTimeValue)) {
this.form.values[this.id] = dateTimeValue.toISOString(); return dateTimeValue.toISOString();
} else {
this.form.values[this.id] = null;
this._value = this.value;
} }
break;
this._value = this.value;
return null;
} }
case FormFieldTypes.NUMBER: { case FormFieldTypes.NUMBER: {
this.form.values[this.id] = this.enableFractions ? parseFloat(this.value) : parseInt(this.value, 10); return this.enableFractions ? parseFloat(this.value) : parseInt(this.value, 10);
break;
} }
case FormFieldTypes.AMOUNT: { case FormFieldTypes.AMOUNT: {
this.form.values[this.id] = this.enableFractions ? parseFloat(this.value) : parseInt(this.value, 10); return this.enableFractions ? parseFloat(this.value) : parseInt(this.value, 10);
break;
} }
case FormFieldTypes.DECIMAL: { case FormFieldTypes.DECIMAL: {
this.form.values[this.id] = parseFloat(this.value); return parseFloat(this.value);
break;
} }
case FormFieldTypes.BOOLEAN: { case FormFieldTypes.BOOLEAN: {
this.form.values[this.id] = this.value !== null && this.value !== undefined ? this.value : false; return this.value !== null && this.value !== undefined ? this.value : false;
break;
} }
case FormFieldTypes.PEOPLE: { case FormFieldTypes.PEOPLE: {
this.form.values[this.id] = this.value ? this.value : null; return this.value ? this.value : null;
break;
} }
case FormFieldTypes.FUNCTIONAL_GROUP: { case FormFieldTypes.FUNCTIONAL_GROUP: {
this.form.values[this.id] = this.value ? this.value : null; return this.value ? this.value : null;
break;
} }
case FormFieldTypes.REPEATABLE_SECTION: { case FormFieldTypes.REPEATABLE_SECTION: {
this.form.values[this.id] = this.value ? this.value : [];
this.repeatableSectionFactory( this.repeatableSectionFactory(
{ {
...this.json, ...this.json,
@@ -681,18 +732,31 @@ export class FormFieldModel extends FormWidgetModel {
}, },
this.form this.form
); );
break;
return this.value ? this.value : this.form.values[this.id];
} }
default: default:
if (this.shouldUpdateFormValues(this.type)) { if (this.shouldUpdateFormValues(this.type)) {
this.form.values[this.id] = this.value; return this.value;
}
return undefined;
} }
} }
this.form.onFormFieldChanged(this); private updateValue(value: any) {
if (value === undefined) {
return;
}
this.form.values[this.id] = value;
}
private updateRepeatableSectionValue(value: string) {
if (this.type === FormFieldTypes.SECTION) {
return;
} }
private updateRepeatableSectionValue() {
if (!this.form.values[this.parent.id]) { if (!this.form.values[this.parent.id]) {
this.form.values[this.parent.id] = []; this.form.values[this.parent.id] = [];
} }
@@ -701,9 +765,7 @@ export class FormFieldModel extends FormWidgetModel {
this.form.values[this.parent.id][this.parent.rowIndex] = this.createInitialValue(this.parent.fields); this.form.values[this.parent.id][this.parent.rowIndex] = this.createInitialValue(this.parent.fields);
} }
this.form.values[this.parent.id][this.parent.rowIndex][this.id.split(ROW_ID_PREFIX)[0]] = this.value; this.form.values[this.parent.id][this.parent.rowIndex][this.id.split(ROW_ID_PREFIX)[0]] = value;
this.form.onFormFieldChanged(this);
} }
/** /**
@@ -1,4 +1,4 @@
<div [style]="element | adfFieldStyle" class="adf-container-widget-repeat"> <div [style]="element | adfFieldStyle" class="adf-container-widget-repeat" [class.adf-readonly]="element.field.readOnly">
<h4 <h4
id="container-repeat" id="container-repeat"
class="adf-container-widget-repeat__text" class="adf-container-widget-repeat__text"
@@ -11,19 +11,19 @@
<ng-content /> <ng-content />
@if (!isEditor) { @if (!isEditor) {
@let shouldDisplayAddRowButton = !element.field.params.newRowsLimit || (element.field.params.newRowsLimit > getAddedRowsCount()); @let shouldDisplayAddRowButton = !element.field.params.maxNumberOfRows || (element.field.params.maxNumberOfRows > element.field.rows.length);
@if (shouldDisplayAddRowButton) { @if (shouldDisplayAddRowButton) {
<button <button
mat-button mat-button
color="primary" color="primary"
class="adf-container-widget-row-action" class="adf-container-widget-row-action"
[disabled]="element.field.readOnly"
(click)="addRow()" (click)="addRow()"
> >
<mat-icon>add</mat-icon> {{ 'FORM.FIELD.REPEATABLE_SECTION.ADD_ROW' | translate }} <mat-icon>add</mat-icon> {{ 'FORM.FIELD.REPEATABLE_SECTION.ADD_ROW' | translate }}
</button> </button>
} @else { } @else {
@let rowLimit = element.field.params.initialNumberOfRows + (element.field.params.newRowsLimit ?? 0); <span class="adf-container-widget-row-action adf-container-widget-row-limit">{{ 'FORM.FIELD.REPEATABLE_SECTION.ROW_LIMIT_REACHED' | translate: { limit: element.field.params.maxNumberOfRows } }}</span>
<span class="adf-container-widget-row-action adf-container-widget-row-limit">{{ 'FORM.FIELD.REPEATABLE_SECTION.ROW_LIMIT_REACHED' | translate: { limit: rowLimit } }}</span>
} }
} }
</div> </div>
@@ -8,10 +8,6 @@
font-weight: var(--adf-header-font-weight); font-weight: var(--adf-header-font-weight);
color: var(--adf-header-color); color: var(--adf-header-color);
line-height: normal; line-height: normal;
&.adf-collapsible {
cursor: pointer;
}
} }
&-row-action { &-row-action {
@@ -23,3 +19,18 @@
font-size: 12px; font-size: 12px;
} }
} }
.adf-readonly {
.adf-container-widget {
&-repeat__text {
color: var(--mdc-text-button-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));
border-bottom: 1px solid var(--mdc-text-button-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));
cursor: default;
}
&-row-limit {
color: var(--mdc-text-button-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));
font-size: 12px;
}
}
}
@@ -30,11 +30,11 @@ describe('RepeatWidgetComponent', () => {
/** /**
* *
* @param initialNumberOfRows initial number of rows * @param initialNumberOfRows initial number of rows
* @param newRowsLimit limit for additional rows * @param maxNumberOfRows maximum number of rows
* @param allowInitialRowsDelete should allow deleting rows * @param allowInitialRowsDelete should allow deleting rows
* @returns repeatable section json based on params * @returns repeatable section json based on params
*/ */
function getFormFieldJson(initialNumberOfRows: number = 2, newRowsLimit?: number, allowInitialRowsDelete: boolean = true) { function getFormFieldJson(initialNumberOfRows: number = 2, maxNumberOfRows?: number, allowInitialRowsDelete: boolean = true) {
return { return {
id: 'RepeatableSection0tbw2y', id: 'RepeatableSection0tbw2y',
name: 'Repeatable Section', name: 'Repeatable Section',
@@ -43,7 +43,7 @@ describe('RepeatWidgetComponent', () => {
params: { params: {
initialNumberOfRows, initialNumberOfRows,
allowInitialRowsDelete, allowInitialRowsDelete,
newRowsLimit maxNumberOfRows
}, },
numberOfColumns: 2, numberOfColumns: 2,
fields: { fields: {
@@ -126,7 +126,7 @@ describe('RepeatWidgetComponent', () => {
}); });
it('should display add row button if limit is defined but not reached', () => { it('should display add row button if limit is defined but not reached', () => {
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 1))); component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 3)));
fixture.detectChanges(); fixture.detectChanges();
@@ -135,8 +135,7 @@ describe('RepeatWidgetComponent', () => {
}); });
it('should NOT display add row button if limit is defined and reached', () => { it('should NOT display add row button if limit is defined and reached', () => {
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 1))); component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 2)));
spyOn(component, 'getAddedRowsCount').and.returnValue(1);
fixture.detectChanges(); fixture.detectChanges();
@@ -145,8 +144,7 @@ describe('RepeatWidgetComponent', () => {
}); });
it('should display row limit if limit has been reached', () => { it('should display row limit if limit has been reached', () => {
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 1))); component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 2)));
spyOn(component, 'getAddedRowsCount').and.returnValue(1);
fixture.detectChanges(); fixture.detectChanges();
@@ -165,31 +163,5 @@ describe('RepeatWidgetComponent', () => {
testingUtils.clickByCSS('button.adf-container-widget-row-action'); testingUtils.clickByCSS('button.adf-container-widget-row-action');
expect(component.addRow).toHaveBeenCalled(); expect(component.addRow).toHaveBeenCalled();
}); });
describe('getAddedRowsCount', () => {
it('should get correct rows count if initial rows are allowed to be deleted', () => {
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 1)));
fixture.detectChanges();
expect(component.getAddedRowsCount()).toBe(0);
component.addRow();
fixture.detectChanges();
expect(component.getAddedRowsCount()).toBe(1);
});
it('should get correct rows count if initial rows are NOT allowed to be deleted', () => {
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 1, false)));
expect(component.getAddedRowsCount()).toBe(0);
component.addRow();
expect(component.getAddedRowsCount()).toBe(1);
});
});
}); });
}); });
@@ -36,10 +36,4 @@ export class RepeatWidgetComponent {
addRow() { addRow() {
this.element.field.addRow(this.element.json.fields, this.element.form); this.element.field.addRow(this.element.json.fields, this.element.form);
} }
getAddedRowsCount(): number {
return this.element.json.params.allowInitialRowsDelete
? this.element.field.rows.length - this.element.json.params.initialNumberOfRows
: this.element.field.rows.filter((row) => !row.isInitial).length;
}
} }
+1
View File
@@ -78,6 +78,7 @@
"FORM_RENDERER": { "FORM_RENDERER": {
"NAMELESS_TASK": "Nameless task", "NAMELESS_TASK": "Nameless task",
"ROW_LABEL": "Row {{ number }}", "ROW_LABEL": "Row {{ number }}",
"DELETE_ROW": "Delete row",
"REMOVE_ROW_DIALOG": { "REMOVE_ROW_DIALOG": {
"TITLE": "Delete the row", "TITLE": "Delete the row",
"MESSAGE": "Are you sure you want to delete this row?", "MESSAGE": "Are you sure you want to delete this row?",