mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-39559 Update to handle 0 initial rows for repeatable sections (#12113)
* AAE-39559 Update to handle 0 initial rows for repeatable sections * address code review comments
This commit is contained in:
@@ -50,6 +50,7 @@ Contains the value and metadata for a field of a [`Form`](../../../lib/process-s
|
|||||||
| numberOfColumns | number | 1 | Number of columns defined by a container field |
|
| numberOfColumns | number | 1 | Number of columns defined by a container field |
|
||||||
| fields | [`FormFieldModel`](../../core/models/form-field.model.md)\[] | \[] | Fields contained within a container field |
|
| fields | [`FormFieldModel`](../../core/models/form-field.model.md)\[] | \[] | Fields contained within a container field |
|
||||||
| columns | [`ContainerColumnModel`](../../../lib/core/src/lib/form/components/widgets/core/container-column.model.ts)\[] | \[] | Column definitions for a container field |
|
| columns | [`ContainerColumnModel`](../../../lib/core/src/lib/form/components/widgets/core/container-column.model.ts)\[] | \[] | Column definitions for a container field |
|
||||||
|
| rows | [`ContainerRowModel`](../../../lib/core/src/lib/form/components/widgets/core/container-row.model.ts)\[] | \[] | Row definitions for a repeatable section field |
|
||||||
| emptyOption | [`FormFieldOption`](../../../lib/core/src/lib/form/components/widgets/core/form-field-option.ts) | | Dropdown menu item to use when no option is chosen |
|
| emptyOption | [`FormFieldOption`](../../../lib/core/src/lib/form/components/widgets/core/form-field-option.ts) | | Dropdown menu item to use when no option is chosen |
|
||||||
| validationSummary | string | | Error/information message added during field validation (see [`FormFieldValidator`](../../../lib/core/src/lib/form/components/widgets/core/form-field-validator.ts) interface) |
|
| validationSummary | string | | Error/information message added during field validation (see [`FormFieldValidator`](../../../lib/core/src/lib/form/components/widgets/core/form-field-validator.ts) interface) |
|
||||||
|
|
||||||
@@ -95,6 +96,17 @@ The [REST Call Task 101](https://community.alfresco.com/community/bpm/blog/2016/
|
|||||||
tutorial on the [APS community site](https://community.alfresco.com/community/bpm)
|
tutorial on the [APS community site](https://community.alfresco.com/community/bpm)
|
||||||
contains full details about how the REST calls work, along with a worked example.
|
contains full details about how the REST calls work, along with a worked example.
|
||||||
|
|
||||||
|
### Repeatable sections
|
||||||
|
|
||||||
|
Repeatable section fields (`type: 'repeatable-section'`) support an `initialNumberOfRows` parameter in `params`.
|
||||||
|
Setting `initialNumberOfRows` to `0` is valid: the section starts with no runtime rows and users add rows via the **Add row** action.
|
||||||
|
|
||||||
|
When no rows exist, `columns` holds a design-time column template (used by form editors and form-rules).
|
||||||
|
Template fields are bound to the form for rendering but are marked as templates and do not write to `form.values`.
|
||||||
|
No row data is written to `form.values` until the user adds a row or saved values are loaded from the server.
|
||||||
|
|
||||||
|
If saved values are present on the field (`value` array), row count is derived from `value.length` (subject to `maxNumberOfRows`), regardless of `initialNumberOfRows`.
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
- [Extensibility](../../user-guide/extensibility.md)
|
- [Extensibility](../../user-guide/extensibility.md)
|
||||||
|
|||||||
@@ -1542,6 +1542,103 @@ describe('FormFieldModel', () => {
|
|||||||
field = new FormFieldModel(form, json);
|
field = new FormFieldModel(form, json);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('zero initial rows', () => {
|
||||||
|
const zeroInitialRowsJson = {
|
||||||
|
...json,
|
||||||
|
params: {
|
||||||
|
...json.params,
|
||||||
|
initialNumberOfRows: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should create column template without rows when initialNumberOfRows is 0', () => {
|
||||||
|
const freshForm = new FormModel();
|
||||||
|
const zeroInitialRowsField = new FormFieldModel(freshForm, zeroInitialRowsJson);
|
||||||
|
|
||||||
|
expect(zeroInitialRowsField.rows.length).toBe(0);
|
||||||
|
expect(zeroInitialRowsField.columns.length).toBe(2);
|
||||||
|
|
||||||
|
const templateFieldIds = zeroInitialRowsField.columns.flatMap((column) => column.fields.map((templateField) => templateField.id));
|
||||||
|
expect(templateFieldIds.some((id) => id.startsWith('Text0wwp7n-Row'))).toBe(true);
|
||||||
|
expect(templateFieldIds.some((id) => id.startsWith('Integer0rzkwq-Row'))).toBe(true);
|
||||||
|
zeroInitialRowsField.columns
|
||||||
|
.flatMap((column) => column.fields)
|
||||||
|
.forEach((templateField) => {
|
||||||
|
expect(templateField.form).toBe(freshForm);
|
||||||
|
expect(templateField.parent?.isTemplate).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(freshForm.values[json.id]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create rows after reload when initialNumberOfRows is 0 and no values were saved', () => {
|
||||||
|
const freshForm = new FormModel();
|
||||||
|
new FormFieldModel(freshForm, zeroInitialRowsJson);
|
||||||
|
|
||||||
|
expect(freshForm.values[json.id]).toBeUndefined();
|
||||||
|
|
||||||
|
const reloadedField = new FormFieldModel(freshForm, {
|
||||||
|
...zeroInitialRowsJson,
|
||||||
|
value: freshForm.values[json.id]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(reloadedField.rows.length).toBe(0);
|
||||||
|
expect(freshForm.values[json.id]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create rows from saved values when initialNumberOfRows is 0', () => {
|
||||||
|
const savedValuesJson = {
|
||||||
|
...zeroInitialRowsJson,
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
Text0wwp7n: 'saved text',
|
||||||
|
Dropdown0e7tn4: null,
|
||||||
|
Integer0rzkwq: 42,
|
||||||
|
Dropdown0wgm63: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const freshForm = new FormModel();
|
||||||
|
const fieldWithSavedValues = new FormFieldModel(freshForm, savedValuesJson);
|
||||||
|
|
||||||
|
expect(fieldWithSavedValues.rows.length).toBe(1);
|
||||||
|
expect(fieldWithSavedValues.rows[0].columns[0].fields[0].value).toBe('saved text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reuse column template on subsequent updateForm calls when initialNumberOfRows is 0', () => {
|
||||||
|
const freshForm = new FormModel();
|
||||||
|
const zeroInitialRowsField = new FormFieldModel(freshForm, zeroInitialRowsJson);
|
||||||
|
const templateColumns = zeroInitialRowsField.columns;
|
||||||
|
|
||||||
|
zeroInitialRowsField.updateForm();
|
||||||
|
|
||||||
|
expect(zeroInitialRowsField.columns).toBe(templateColumns);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply read-only state to column template when initialNumberOfRows is 0', () => {
|
||||||
|
const freshForm = new FormModel();
|
||||||
|
const zeroInitialRowsField = new FormFieldModel(freshForm, zeroInitialRowsJson);
|
||||||
|
|
||||||
|
zeroInitialRowsField.readOnly = true;
|
||||||
|
|
||||||
|
zeroInitialRowsField.columns
|
||||||
|
.flatMap((column) => column.fields)
|
||||||
|
.forEach((templateField) => {
|
||||||
|
expect(templateField.readOnly).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add first row when initialNumberOfRows is 0', () => {
|
||||||
|
const zeroInitialRowsField = new FormFieldModel(form, zeroInitialRowsJson);
|
||||||
|
|
||||||
|
zeroInitialRowsField.addRow(zeroInitialRowsField.fields, form);
|
||||||
|
|
||||||
|
expect(zeroInitialRowsField.rows.length).toBe(1);
|
||||||
|
expect(zeroInitialRowsField.rows[0].columns[0].fields[0].id).toContain('Text0wwp7n-Row');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('add row', () => {
|
describe('add row', () => {
|
||||||
const assignFormRulesEventSubject = (): Subject<FormRulesEvent> => {
|
const assignFormRulesEventSubject = (): Subject<FormRulesEvent> => {
|
||||||
const formRulesEvent = new Subject<FormRulesEvent>();
|
const formRulesEvent = new Subject<FormRulesEvent>();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { DataColumn } from '../../../../datatable/data/data-column.model';
|
|||||||
import { DateFnsUtils } from '../../../../common';
|
import { DateFnsUtils } from '../../../../common';
|
||||||
import { isValid as isValidDate } from 'date-fns';
|
import { isValid as isValidDate } from 'date-fns';
|
||||||
import { ContainerRowModel } from './container-row.model';
|
import { ContainerRowModel } from './container-row.model';
|
||||||
import { RepeatableSectionModel, ROW_ID_PREFIX } from './repeatable-section.model';
|
import { RepeatableSectionModel, ROW_ID_PREFIX, TEMPLATE_ROW_ID } from './repeatable-section.model';
|
||||||
import { formFieldRuleHandler } from './handlers/form-field-rule.handler';
|
import { formFieldRuleHandler } from './handlers/form-field-rule.handler';
|
||||||
import { formFieldVisibilityConditionHandler } from './handlers/form-field-visibility-condition.handler';
|
import { formFieldVisibilityConditionHandler } from './handlers/form-field-visibility-condition.handler';
|
||||||
|
|
||||||
@@ -351,7 +351,12 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
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;
|
if (this.rows.length > 0) {
|
||||||
|
this.columns = this.rows[0].columns;
|
||||||
|
} else if (this.columns.length === 0) {
|
||||||
|
// Design-time column template for Studio and form-rules; isTemplate suppresses form.values writes.
|
||||||
|
this.columns = this.createColumns(fields, form, TEMPLATE_ROW_ID, 0, undefined, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNumberOfRows(initialNrRows: number = 1, maxNrRows: number | null = null, value?: any) {
|
private getNumberOfRows(initialNrRows: number = 1, maxNrRows: number | null = null, value?: any) {
|
||||||
@@ -366,7 +371,7 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createColumns(fields: any, form: any, rowId: string, index?: number, value?: any) {
|
private createColumns(fields: any, form: any, rowId: string, index?: number, value?: any, isTemplate: boolean = false) {
|
||||||
const columns: ContainerColumnModel[] = [];
|
const columns: ContainerColumnModel[] = [];
|
||||||
|
|
||||||
Object.keys(fields).forEach((currentField) => {
|
Object.keys(fields).forEach((currentField) => {
|
||||||
@@ -382,7 +387,8 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
uid: this.getUniqueId(field, rowId),
|
uid: this.getUniqueId(field, rowId),
|
||||||
fields: this.fields,
|
fields: this.fields,
|
||||||
rowIndex: index ?? 0,
|
rowIndex: index ?? 0,
|
||||||
value: field.type === FormFieldTypes.SECTION ? value : value?.[field.id]
|
value: field.type === FormFieldTypes.SECTION ? value : value?.[field.id],
|
||||||
|
isTemplate
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
col.rowspan = fields[currentField].length;
|
col.rowspan = fields[currentField].length;
|
||||||
@@ -399,15 +405,24 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateRepeatableSectionReadOnlyState(state: boolean) {
|
private updateRepeatableSectionReadOnlyState(state: boolean) {
|
||||||
for (const row of this.rows) {
|
if (this.rows.length > 0) {
|
||||||
for (const column of row.columns) {
|
for (const row of this.rows) {
|
||||||
for (const field of column.fields) {
|
this.updateRepeatableSectionColumnsReadOnlyState(row.columns, state);
|
||||||
if (field.type === FormFieldTypes.SECTION) {
|
}
|
||||||
this.updateInnerSectionReadOnlyState(field, state);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
field.readOnly = this.getRepeatableSectionFieldReadOnlyState(field, state);
|
this.updateRepeatableSectionColumnsReadOnlyState(this.columns, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateRepeatableSectionColumnsReadOnlyState(columns: ContainerColumnModel[], state: boolean) {
|
||||||
|
for (const column of columns) {
|
||||||
|
for (const field of column.fields) {
|
||||||
|
if (field.type === FormFieldTypes.SECTION) {
|
||||||
|
this.updateInnerSectionReadOnlyState(field, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
field.readOnly = this.getRepeatableSectionFieldReadOnlyState(field, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -594,7 +609,7 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateForm() {
|
updateForm() {
|
||||||
if (!this.form) {
|
if (!this.form || this.parent?.isTemplate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,15 @@
|
|||||||
|
|
||||||
export const ROW_ID_PREFIX = '-Row';
|
export const ROW_ID_PREFIX = '-Row';
|
||||||
|
|
||||||
|
/** Placeholder row id for design-time column templates when no rows exist (not used for runtime rows). */
|
||||||
|
export const TEMPLATE_ROW_ID = '0';
|
||||||
|
|
||||||
export interface RepeatableSectionModel {
|
export interface RepeatableSectionModel {
|
||||||
id: string;
|
id: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
fields: any;
|
fields: any;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
value?: any;
|
value?: any;
|
||||||
|
/** When true, field is a design-time column template and must not write to form.values. */
|
||||||
|
isTemplate?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user