AAE-44184 Colspan setting on form fields is ignored or not properly applied (#11800)

* AAE-44184 Colspan setting on form fields is ignored or not properly applied

* AAE-44184 Better handling of empty columns

* AAE-44184 Fixing stale adf-form-field instances

* AAE-44184 Consolidating duplicate code
This commit is contained in:
David Olson
2026-05-20 14:19:17 +05:30
committed by Anamika Dey
parent 68010297a6
commit 1c2b91dadc
9 changed files with 475 additions and 129 deletions
@@ -1,73 +1,95 @@
<div id="adf-form-renderer" class="{{ formDefinition.className }} adf-form-renderer" <div id="adf-form-renderer" class="{{ formDefinition.className }} adf-form-renderer"
[ngClass]="{ 'adf-readonly-form': formDefinition.readOnly }"> [ngClass]="{ 'adf-readonly-form': formDefinition.readOnly }">
<div *ngIf="formDefinition.hasTabs()"> @if (formDefinition.hasTabs()) {
<div *ngIf="hasTabs()" class="alfresco-tabs-widget"> @if (hasTabs()) {
<div class="alfresco-tabs-widget">
<mat-tab-group [preserveContent]="true"> <mat-tab-group [preserveContent]="true">
<mat-tab *ngFor="let tab of visibleTabs()" [label]="tab.title | translate "> @for (tab of visibleTabs(); track tab) {
<mat-tab [label]="tab.title | translate ">
<ng-template matTabContent> <ng-template matTabContent>
<div class="adf-form-tab-content"> <div class="adf-form-tab-content">
<ng-template *ngTemplateOutlet="render; context: { fieldToRender: tab.fields }" /> <ng-template *ngTemplateOutlet="render; context: { fieldToRender: tab.fields }" />
</div> </div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
}
</mat-tab-group> </mat-tab-group>
</div> </div>
</div> }
}
<div *ngIf="!formDefinition.hasTabs() && formDefinition.hasFields()"> @if (!formDefinition.hasTabs() && formDefinition.hasFields()) {
<div>
<ng-template *ngTemplateOutlet="render; context: { fieldToRender: formDefinition.fields }" /> <ng-template *ngTemplateOutlet="render; context: { fieldToRender: formDefinition.fields }" />
</div> </div>
}
</div> </div>
<ng-template #render let-fieldToRender="fieldToRender"> <ng-template #render let-fieldToRender="fieldToRender">
<div *ngFor="let currentRootElement of fieldToRender"> @for (currentRootElement of fieldToRender; track currentRootElement) {
<div *ngIf="currentRootElement.type === 'section'" [id]="'field-' + currentRootElement?.id + '-container'" class="adf-container-widget"> @if (currentRootElement.type === 'section') {
<adf-form-section [field]="currentRootElement" /> <div [id]="'field-' + currentRootElement?.id + '-container'" class="adf-container-widget">
<adf-form-section [field]="currentRootElement.field" />
</div> </div>
}
<div *ngIf="currentRootElement.type === 'container' || currentRootElement.type === 'group'" @if (currentRootElement.type === 'container' || currentRootElement.type === 'group') {
<div
[id]="'field-' + currentRootElement?.id + '-container'" [id]="'field-' + currentRootElement?.id + '-container'"
class="adf-container-widget" class="adf-container-widget"
[hidden]="!currentRootElement?.isVisible"> [hidden]="!currentRootElement?.isVisible">
<adf-header-widget [element]="currentRootElement" /> <adf-header-widget [element]="currentRootElement" />
<div *ngIf="currentRootElement?.form?.enableFixedSpace; else fixingTemplate"> @if (currentRootElement?.form?.enableFixedSpace) {
<div class="adf-grid-list" @if (currentRootElement?.isExpanded) {
[ngStyle]="{ 'grid-template-columns': 'repeat(' + getNumberOfColumns(currentRootElement) + ', 1fr)' }" <div class="adf-grid-list"
*ngIf="currentRootElement?.isExpanded"> [ngStyle]="{ 'grid-template-columns': 'repeat(' + getNumberOfColumns(currentRootElement) + ', 1fr)' }">
<div class="adf-grid-list-item" @for (field of getContainerFields(currentRootElement); track field ?? $index) {
*ngFor="let field of getContainerFields(currentRootElement)" <div class="adf-grid-list-item"
[ngStyle]="{ 'grid-area': 'auto / auto / span ' + (field?.rowspan || 1) + ' / span ' + (field?.colspan || 1) }"> [ngStyle]="{ 'grid-area': 'auto / auto / span ' + (field?.rowspan || 1) + ' / span ' + (field?.colspan || 1) }">
<adf-form-field *ngIf="field" [field]="field" /> @if (field) {
<adf-form-field [field]="field" />
}
</div>
}
</div> </div>
</div> }
</div> } @else {
<ng-template [ngTemplateOutlet]="fixingTemplate" />
}
<ng-template #fixingTemplate> <ng-template #fixingTemplate>
<section class="adf-grid-list-column-view" *ngIf="currentRootElement?.isExpanded"> @if (currentRootElement?.isExpanded) {
<div class="adf-grid-list-single-column" <section class="adf-grid-list-column-view">
*ngFor="let column of currentRootElement?.columns" @for (column of currentRootElement?.columns; track column; let columnIndex = $index) {
[style.width.%]="getColumnWidth(currentRootElement)" <div
> class="adf-grid-list-single-column"
<ng-container *ngFor="let field of column?.fields"> [style.width.%]="getColumnWidth(currentRootElement, currentRootElement?.columns, columnIndex)">
<ng-container *ngIf="field.type === 'section'; else formField"> @for (field of column?.fields; track field) {
<adf-form-section [field]="field"/> @if (field.type === 'section') {
</ng-container> <adf-form-section [field]="field"/>
<ng-template #formField> } @else {
<div class="adf-grid-list-column-view-item"> <div class="adf-grid-list-column-view-item">
<adf-form-field [field]="field"/> <adf-form-field [field]="field"/>
</div> </div>
</ng-template> }
</ng-container> }
</div> </div>
}
</section> </section>
}
</ng-template> </ng-template>
<ng-template #columnViewItem let-column="column"> <ng-template #columnViewItem let-column="column">
<div class="adf-grid-list-column-view-item" *ngFor="let field of column?.fields"> @for (field of column?.fields; track field) {
<adf-form-field *ngIf="field" [field]="field" /> <div class="adf-grid-list-column-view-item">
</div> @if (field) {
<adf-form-field [field]="field" />
}
</div>
}
</ng-template> </ng-template>
</div> </div>
}
@if (currentRootElement.type === 'repeatable-section') { @if (currentRootElement.type === 'repeatable-section') {
<div <div
@@ -76,7 +98,7 @@
[hidden]="!currentRootElement?.isVisible" [hidden]="!currentRootElement?.isVisible"
> >
<adf-repeat-widget [element]="currentRootElement" [isEditor]="false"> <adf-repeat-widget [element]="currentRootElement" [isEditor]="false">
@for (row of currentRootElement.field.rows; track row.id; let rowIndex = $index) { @for (row of currentRootElement.field.rows; track row; let rowIndex = $index) {
@let hasMultipleRows = currentRootElement.field.rows.length > 1; @let hasMultipleRows = currentRootElement.field.rows.length > 1;
<div <div
class="adf-grid-list-container" class="adf-grid-list-container"
@@ -100,21 +122,20 @@
} }
</div> </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 column; let columnIndex = $index) {
<div <div
class="adf-grid-list-single-column" class="adf-grid-list-single-column"
[style.width.%]="getColumnWidth(currentRootElement)" [style.width.%]="getColumnWidth(currentRootElement, row.columns, columnIndex)">
> @for (field of column?.fields; track field) {
@for (field of column?.fields; track $index) { @if (field.type === 'section') {
@if (field.type === 'section') { <adf-form-section [field]="field"/>
<adf-form-section [field]="field"/> } @else {
} @else { <div class="adf-grid-list-column-view-item">
<div class="adf-grid-list-column-view-item"> <adf-form-field [field]="field"/>
<adf-form-field [field]="field"/> </div>
</div> }
} }
} </div>
</div>
} }
</section> </section>
</div> </div>
@@ -123,13 +144,16 @@
</div> </div>
} }
<div *ngIf="currentRootElement.type === 'dynamic-table'" class="adf-container-widget"> @if (currentRootElement.type === 'dynamic-table') {
<adf-form-field [field]="currentRootElement" /> <div class="adf-container-widget">
</div> <adf-form-field [field]="currentRootElement" />
</div>
}
<div class="adf-container-widget" @if (currentRootElement.type === 'readonly' && currentRootElement.field.params.field.type === 'dynamic-table') {
*ngIf="currentRootElement.type === 'readonly' && currentRootElement.field.params.field.type === 'dynamic-table'"> <div class="adf-container-widget">
<adf-form-field [field]="currentRootElement.field"/> <adf-form-field [field]="currentRootElement.field"/>
</div> </div>
</div> }
}
</ng-template> </ng-template>
@@ -24,6 +24,7 @@ import { FormRendererComponent } from './form-renderer.component';
import { import {
amountWidgetFormVisibilityMock, amountWidgetFormVisibilityMock,
checkboxWidgetFormVisibilityMock, checkboxWidgetFormVisibilityMock,
colspanAnyColumnsForm,
colspanForm, colspanForm,
customWidgetForm, customWidgetForm,
customWidgetFormWithVisibility, customWidgetFormWithVisibility,
@@ -439,6 +440,65 @@ describe('Form Renderer Component', () => {
expect(fullWidthElement.style['width']).toBe('100%'); expect(fullWidthElement.style['width']).toBe('100%');
}); });
it('Should display widths 25% and 75% in non-grid mode for 4-column container with colspan 1 and 3', async () => {
formRendererComponent.formDefinition = formService.parseForm(colspanAnyColumnsForm.formRepresentation.formDefinition, null, false, false);
fixture.detectChanges();
await fixture.whenStable();
const columns = testingUtils
.getAllByCSS('#field-4col-container-id-container section.adf-grid-list-column-view .adf-grid-list-single-column')
.map((element) => element.nativeElement as HTMLElement);
expect(columns.length).toBe(4);
expect(columns[0].style['width']).toBe('25%');
expect(columns[1].style['width']).toBe('75%');
expect(columns[2].style['width']).toBe('0%');
expect(columns[3].style['width']).toBe('0%');
});
it('Should display widths 33.333% and 66.666% in non-grid mode for 12-column container with colspan 4 and 8', async () => {
formRendererComponent.formDefinition = formService.parseForm(colspanAnyColumnsForm.formRepresentation.formDefinition, null, false, false);
fixture.detectChanges();
await fixture.whenStable();
const columns = testingUtils
.getAllByCSS('#field-12col-container-id-container section.adf-grid-list-column-view .adf-grid-list-single-column')
.map((element) => element.nativeElement as HTMLElement);
expect(columns.length).toBe(12);
expect(parseFloat(columns[0].style['width'])).toBeCloseTo(33.33, 2);
expect(columns[1].style['width']).toBe('0%');
expect(columns[2].style['width']).toBe('0%');
expect(columns[3].style['width']).toBe('0%');
expect(parseFloat(columns[4].style['width'])).toBeCloseTo(66.67, 2);
expect(columns[5].style['width']).toBe('0%');
expect(columns[6].style['width']).toBe('0%');
expect(columns[7].style['width']).toBe('0%');
expect(columns[8].style['width']).toBe('0%');
expect(columns[9].style['width']).toBe('0%');
expect(columns[10].style['width']).toBe('0%');
expect(columns[11].style['width']).toBe('0%');
});
it('Should preserve width for an intentionally empty leading spacer column in non-grid mode', async () => {
formRendererComponent.formDefinition = formService.parseForm(
amountWidgetFormVisibilityMock.formRepresentation.formDefinition,
null,
false,
false
);
fixture.detectChanges();
await fixture.whenStable();
const columns = testingUtils
.getAllByCSS('#field-ce32844c-20b2-4361-88b5-a56f28219aef-container section.adf-grid-list-column-view .adf-grid-list-single-column')
.map((element) => element.nativeElement as HTMLElement);
expect(columns.length).toBe(2);
expect(columns[0].style['width']).toBe('50%');
expect(columns[1].style['width']).toBe('50%');
});
it('[C309872] - Should display Text widget spans on 2 columns when colspan is set to 2', () => { it('[C309872] - Should display Text widget spans on 2 columns when colspan is set to 2', () => {
formRendererComponent.formDefinition = formService.parseForm(colspanForm.formRepresentation.formDefinition, null, false, false); formRendererComponent.formDefinition = formService.parseForm(colspanForm.formRepresentation.formDefinition, null, false, false);
fixture.detectChanges(); fixture.detectChanges();
@@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { NgClass, NgForOf, NgIf, NgStyle, NgTemplateOutlet } from '@angular/common'; import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
import { ChangeDetectorRef, Component, DestroyRef, inject, Injector, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { ChangeDetectorRef, Component, DestroyRef, inject, Injector, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { filter } from 'rxjs'; import { filter } from 'rxjs';
@@ -36,11 +36,12 @@ 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'; import { MatTooltipModule } from '@angular/material/tooltip';
import { IconModule } from '../../icon/icon.module'; import { IconModule } from '../../icon/icon.module';
import { FormLayoutColumn, getFormLayoutColumnWidth } from './helpers/column-width';
@Component({ @Component({
selector: 'adf-form-renderer', selector: 'adf-form-renderer',
templateUrl: './form-renderer.component.html', templateUrl: './form-renderer.component.html',
styleUrls: ['./form-renderer.component.scss'], styleUrl: './form-renderer.component.scss',
providers: [ providers: [
{ {
provide: FormRulesManager, provide: FormRulesManager,
@@ -54,9 +55,7 @@ import { IconModule } from '../../icon/icon.module';
} }
], ],
imports: [ imports: [
NgIf,
MatTabsModule, MatTabsModule,
NgForOf,
NgTemplateOutlet, NgTemplateOutlet,
TranslatePipe, TranslatePipe,
MatButtonModule, MatButtonModule,
@@ -182,16 +181,8 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy {
}); });
} }
/** getColumnWidth(container: ContainerModel, columns: FormLayoutColumn[], columnIndex: number): string {
* Calculate the column width based on the numberOfColumns and current field's colspan property return getFormLayoutColumnWidth(container.field?.numberOfColumns, columns, columnIndex);
*
* @param container container model
* @returns the column width for the given model
*/
getColumnWidth(container: ContainerModel): string {
const { field } = container;
const colspan = field ? field.colspan : 1;
return (100 / field.numberOfColumns) * colspan + '';
} }
private runMiddlewareServices(): void { private runMiddlewareServices(): void {
@@ -1,11 +1,13 @@
<div class="adf-grid-list-section-single-column" <div class="adf-grid-list-section-single-column"
[id]="'field-' + field?.id + '-container'" [id]="'field-' + field?.id + '-container'"
[style.display]="field?.isVisible ? 'flex' : 'none'"> [style.display]="field?.isVisible ? 'flex' : 'none'">
<div *ngFor="let sectionColumn of field.columns" @for (sectionColumn of field.columns; track sectionColumn; let columnIndex = $index) {
[style.width.%]="getSectionColumnWidth(field.numberOfColumns, sectionColumn.fields)" <div [style.width.%]="getSectionColumnWidth(field.numberOfColumns, field.columns, columnIndex)">
> @for (sectionField of sectionColumn.fields; track sectionField) {
<div *ngFor="let sectionField of sectionColumn.fields" class="adf-grid-list-section-column-view-item"> <div class="adf-grid-list-section-column-view-item">
<adf-form-field [field]="sectionField"/> <adf-form-field [field]="sectionField"/>
</div> </div>
</div> }
</div>
}
</div> </div>
@@ -38,17 +38,17 @@ describe('FormSectionComponent', () => {
it('should calculate the correct width for section columns', () => { it('should calculate the correct width for section columns', () => {
const numberOfColumns = 3; const numberOfColumns = 3;
const columnField = { colspan: 2 } as FormFieldModel; const columns = [{ fields: [{ colspan: 2 } as FormFieldModel] }];
const width = component.getSectionColumnWidth(numberOfColumns, [columnField]); const width = component.getSectionColumnWidth(numberOfColumns, columns, 0);
expect(width).toBe('66.66666666666667'); expect(width).toBe('66.66666666666667');
}); });
it('should handle columns with no colspan defined', () => { it('should handle columns with no colspan defined', () => {
const numberOfColumns = 3; const numberOfColumns = 3;
const columnField = {} as FormFieldModel; const columns = [{ fields: [{} as FormFieldModel] }];
const width = component.getSectionColumnWidth(numberOfColumns, [columnField]); const width = component.getSectionColumnWidth(numberOfColumns, columns, 0);
expect(width).toBe('33.333333333333336'); expect(width).toBe('33.333333333333336');
}); });
@@ -63,84 +63,93 @@ describe('FormSectionComponent', () => {
describe('getSectionColumnWidth', () => { describe('getSectionColumnWidth', () => {
it('should cap width at 100% when numberOfColumns is not a number', () => { it('should cap width at 100% when numberOfColumns is not a number', () => {
const columnField = { colspan: 2 } as FormFieldModel; const columns = [{ fields: [{ colspan: 2 } as FormFieldModel] }];
const width = component.getSectionColumnWidth('invalid' as unknown as number, [columnField]); const width = component.getSectionColumnWidth('invalid' as unknown as number, columns, 0);
expect(width).toBe('100'); expect(width).toBe('100');
}); });
it('should cap width at 100% when numberOfColumns is null', () => { it('should cap width at 100% when numberOfColumns is null', () => {
const columnField = { colspan: 3 } as FormFieldModel; const columns = [{ fields: [{ colspan: 3 } as FormFieldModel] }];
const width = component.getSectionColumnWidth(null as unknown as number, [columnField]); const width = component.getSectionColumnWidth(null as unknown as number, columns, 0);
expect(width).toBe('100'); expect(width).toBe('100');
}); });
it('should return 100% when numberOfColumns is undefined', () => { it('should return 100% when numberOfColumns is undefined', () => {
const columnField = { colspan: 1 } as FormFieldModel; const columns = [{ fields: [{ colspan: 1 } as FormFieldModel] }];
const width = component.getSectionColumnWidth(undefined as unknown as number, [columnField]); const width = component.getSectionColumnWidth(undefined as unknown as number, columns, 0);
expect(width).toBe('100'); expect(width).toBe('100');
}); });
it('should cap width at 100% when numberOfColumns is 0', () => { it('should cap width at 100% when numberOfColumns is 0', () => {
const columnField = { colspan: 2 } as FormFieldModel; const columns = [{ fields: [{ colspan: 2 } as FormFieldModel] }];
const width = component.getSectionColumnWidth(0, [columnField]); const width = component.getSectionColumnWidth(0, columns, 0);
expect(width).toBe('100'); expect(width).toBe('100');
}); });
it('should cap width at 100% when numberOfColumns is negative', () => { it('should cap width at 100% when numberOfColumns is negative', () => {
const columnField = { colspan: 3 } as FormFieldModel; const columns = [{ fields: [{ colspan: 3 } as FormFieldModel] }];
const width = component.getSectionColumnWidth(-1, [columnField]); const width = component.getSectionColumnWidth(-1, columns, 0);
expect(width).toBe('100'); expect(width).toBe('100');
}); });
it('should return 100 when numberOfColumns is falsy and no colspan is defined', () => { it('should return 100 when numberOfColumns is falsy and no colspan is defined', () => {
const columnField = {} as FormFieldModel; const columns = [{ fields: [{} as FormFieldModel] }];
const width = component.getSectionColumnWidth(null as unknown as number, [columnField]); const width = component.getSectionColumnWidth(null as unknown as number, columns, 0);
expect(width).toBe('100'); expect(width).toBe('100');
}); });
it('should calculate percentage width when numberOfColumns is a valid number', () => { it('should calculate percentage width when numberOfColumns is a valid number', () => {
const numberOfColumns = 4; const numberOfColumns = 4;
const columnField = { colspan: 2 } as FormFieldModel; const columns = [{ fields: [{ colspan: 2 } as FormFieldModel] }];
const width = component.getSectionColumnWidth(numberOfColumns, [columnField]); const width = component.getSectionColumnWidth(numberOfColumns, columns, 0);
expect(width).toBe('50'); expect(width).toBe('50');
}); });
it('should cap width at 100% when colspan exceeds numberOfColumns', () => { it('should cap width at 100% when colspan exceeds numberOfColumns', () => {
const numberOfColumns = 2; const numberOfColumns = 2;
const columnField = { colspan: 5 } as FormFieldModel; const columns = [{ fields: [{ colspan: 5 } as FormFieldModel] }];
const width = component.getSectionColumnWidth(numberOfColumns, [columnField]); const width = component.getSectionColumnWidth(numberOfColumns, columns, 0);
expect(width).toBe('100'); expect(width).toBe('100');
}); });
it('should use default colspan of 1 when field has no colspan and numberOfColumns is valid', () => { it('should use default colspan of 1 when field has no colspan and numberOfColumns is valid', () => {
const numberOfColumns = 5; const numberOfColumns = 5;
const columnField = {} as FormFieldModel; const columns = [{ fields: [{} as FormFieldModel] }];
const width = component.getSectionColumnWidth(numberOfColumns, [columnField]); const width = component.getSectionColumnWidth(numberOfColumns, columns, 0);
expect(width).toBe('20'); expect(width).toBe('20');
}); });
it('should handle empty columnFields array', () => { it('should return default width for an authored empty spacer column', () => {
const numberOfColumns = 3; const numberOfColumns = 3;
const columns = [{ fields: [] }, { fields: [{ colspan: 1 } as FormFieldModel] }];
const width = component.getSectionColumnWidth(numberOfColumns, []); const width = component.getSectionColumnWidth(numberOfColumns, columns, 0);
expect(parseFloat(width)).toBeCloseTo(33.33); expect(width).toBe('33.333333333333336');
}); });
it('should use first field colspan when multiple fields are provided', () => { it('should return 0 width for an empty column covered by a previous colspan', () => {
const numberOfColumns = 2; const numberOfColumns = 4;
const columnFields = [{ colspan: 1 } as FormFieldModel, { colspan: 3 } as FormFieldModel]; const columns = [{ fields: [{ colspan: 3 } as FormFieldModel] }, { fields: [] }, { fields: [] }, { fields: [] }];
const width = component.getSectionColumnWidth(numberOfColumns, columnFields); const width = component.getSectionColumnWidth(numberOfColumns, columns, 1);
expect(width).toBe('50'); expect(width).toBe('0');
});
it('should use max field colspan when multiple fields are provided', () => {
const numberOfColumns = 2;
const columns = [{ fields: [{ colspan: 1 } as FormFieldModel, { colspan: 3 } as FormFieldModel] }];
const width = component.getSectionColumnWidth(numberOfColumns, columns, 0);
expect(width).toBe('100');
}); });
}); });
}); });
@@ -19,14 +19,14 @@ import { Component, inject, Input, OnInit, ViewEncapsulation } from '@angular/co
import { WidgetVisibilityService } from '../../services/widget-visibility.service'; import { WidgetVisibilityService } from '../../services/widget-visibility.service';
import { FormFieldModel } from '../widgets/core/form-field.model'; import { FormFieldModel } from '../widgets/core/form-field.model';
import { FormFieldComponent } from '../form-field/form-field.component'; import { FormFieldComponent } from '../form-field/form-field.component';
import { NgFor } from '@angular/common'; import { FormLayoutColumn, getFormLayoutColumnWidth } from '../helpers/column-width';
@Component({ @Component({
selector: 'adf-form-section', selector: 'adf-form-section',
templateUrl: './form-section.component.html', templateUrl: './form-section.component.html',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
styleUrls: ['./form-section.component.scss'], styleUrl: './form-section.component.scss',
imports: [NgFor, FormFieldComponent] imports: [FormFieldComponent]
}) })
export class FormSectionComponent implements OnInit { export class FormSectionComponent implements OnInit {
@Input() @Input()
@@ -38,15 +38,7 @@ export class FormSectionComponent implements OnInit {
this.visibilityService.refreshVisibility(this.field.form); this.visibilityService.refreshVisibility(this.field.form);
} }
getSectionColumnWidth(numberOfColumns: number, columnFields: FormFieldModel[]): string { getSectionColumnWidth(numberOfColumns: number, columns: FormLayoutColumn[], columnIndex: number): string {
const firstColumnFieldIndex = 0; return getFormLayoutColumnWidth(numberOfColumns, columns, columnIndex);
const defaultColspan = 1;
const fieldColspan = columnFields[firstColumnFieldIndex]?.colspan ?? defaultColspan;
if (typeof numberOfColumns !== 'number' || !numberOfColumns || numberOfColumns <= 0) {
numberOfColumns = 1;
}
return Math.min(100, (100 / numberOfColumns) * fieldColspan) + '';
} }
} }
@@ -0,0 +1,82 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FormLayoutColumn, getFormLayoutColumnWidth } from './column-width';
describe('getFormLayoutColumnWidth', () => {
it('should return the default width when numberOfColumns is invalid', () => {
const columns: FormLayoutColumn[] = [{ fields: [{ colspan: 1 }] }];
const width = getFormLayoutColumnWidth(undefined, columns, 0);
expect(width).toBe('100');
});
it('should return the default width for an authored empty spacer column', () => {
const NUMBER_OF_COLUMNS = 3;
const columns: FormLayoutColumn[] = [{ fields: [] }, { fields: [{ colspan: 1 }] }];
const width = getFormLayoutColumnWidth(NUMBER_OF_COLUMNS, columns, 0);
expect(width).toBe('33.333333333333336');
});
it('should return zero width for an empty column covered by a previous colspan', () => {
const NUMBER_OF_COLUMNS = 4;
const columns: FormLayoutColumn[] = [{ fields: [{ colspan: 3 }] }, { fields: [] }, { fields: [] }, { fields: [] }];
const width = getFormLayoutColumnWidth(NUMBER_OF_COLUMNS, columns, 1);
expect(width).toBe('0');
});
it('should use the maximum colspan when a column contains multiple fields', () => {
const NUMBER_OF_COLUMNS = 4;
const columns: FormLayoutColumn[] = [{ fields: [{ colspan: 1 }, { colspan: 3 }] }];
const width = getFormLayoutColumnWidth(NUMBER_OF_COLUMNS, columns, 0);
expect(width).toBe('75');
});
it('should cap the width at 100 percent when colspan exceeds the available columns', () => {
const NUMBER_OF_COLUMNS = 2;
const columns: FormLayoutColumn[] = [{ fields: [{ colspan: 5 }] }];
const width = getFormLayoutColumnWidth(NUMBER_OF_COLUMNS, columns, 0);
expect(width).toBe('100');
});
it('should treat a missing colspan as one column', () => {
const NUMBER_OF_COLUMNS = 5;
const columns: FormLayoutColumn[] = [{ fields: [{}] }];
const width = getFormLayoutColumnWidth(NUMBER_OF_COLUMNS, columns, 0);
expect(width).toBe('20');
});
it('should not mark a later empty spacer as covered when an earlier column is also empty', () => {
const NUMBER_OF_COLUMNS = 4;
const columns: FormLayoutColumn[] = [{ fields: [] }, { fields: [] }, { fields: [{ colspan: 1 }] }, { fields: [] }];
const width = getFormLayoutColumnWidth(NUMBER_OF_COLUMNS, columns, 1);
expect(width).toBe('25');
});
});
@@ -0,0 +1,62 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface FormLayoutField {
colspan?: number | null;
}
export interface FormLayoutColumn {
fields?: FormLayoutField[] | null;
}
export const getFormLayoutColumnWidth = (
numberOfColumns: number | null | undefined,
columns: FormLayoutColumn[] | null | undefined,
columnIndex: number
): string => {
const normalizedColumnCount = typeof numberOfColumns === 'number' && numberOfColumns > 0 ? numberOfColumns : 1;
const defaultColumnWidth = 100 / normalizedColumnCount;
const columnFields = columns?.[columnIndex]?.fields ?? [];
if (columnFields.length === 0) {
return isColumnCoveredByPreviousField(columns, columnIndex) ? '0' : `${defaultColumnWidth}`;
}
const maxColspan = Math.max(...columnFields.map((field) => field.colspan || 1));
return `${Math.min(100, defaultColumnWidth * maxColspan)}`;
};
const isColumnCoveredByPreviousField = (columns: FormLayoutColumn[] | null | undefined, columnIndex: number): boolean => {
if (!columns || columnIndex <= 0) {
return false;
}
for (let previousColumnIndex = 0; previousColumnIndex < columnIndex; previousColumnIndex++) {
const previousFields = columns[previousColumnIndex]?.fields ?? [];
if (previousFields.length === 0) {
continue;
}
const previousColumnSpan = Math.max(...previousFields.map((field) => field.colspan || 1));
if (previousColumnIndex + previousColumnSpan > columnIndex) {
return true;
}
}
return false;
};
@@ -981,6 +981,130 @@ export const colspanForm = {
} }
}; };
export const colspanAnyColumnsForm = {
formRepresentation: {
id: 'form-any-columns-colspan',
name: 'any-columns-colspan',
description: '',
version: 0,
standAlone: true,
formDefinition: {
tabs: [],
fields: [
{
id: '4col-container-id',
name: 'Label',
type: 'container',
tab: null,
numberOfColumns: 4,
fields: {
'1': [
{
id: 'Text4ColLeft',
name: 'Left',
type: 'text',
readOnly: false,
required: false,
colspan: 1,
placeholder: null,
minLength: 0,
maxLength: 0,
regexPattern: null,
visibilityCondition: null,
params: {
existingColspan: 1,
maxColspan: 4
}
}
],
'2': [
{
id: 'Text4ColRight',
name: 'Right',
type: 'text',
readOnly: false,
required: false,
colspan: 3,
placeholder: null,
minLength: 0,
maxLength: 0,
regexPattern: null,
visibilityCondition: null,
params: {
existingColspan: 1,
maxColspan: 4
}
}
],
'3': [],
'4': []
}
},
{
id: '12col-container-id',
name: 'Label',
type: 'container',
tab: null,
numberOfColumns: 12,
fields: {
'1': [
{
id: 'Text12ColLeft',
name: 'Left',
type: 'text',
readOnly: false,
required: false,
colspan: 4,
placeholder: null,
minLength: 0,
maxLength: 0,
regexPattern: null,
visibilityCondition: null,
params: {
existingColspan: 1,
maxColspan: 12
}
}
],
'2': [],
'3': [],
'4': [],
'5': [
{
id: 'Text12ColRight',
name: 'Right',
type: 'text',
readOnly: false,
required: false,
colspan: 8,
placeholder: null,
minLength: 0,
maxLength: 0,
regexPattern: null,
visibilityCondition: null,
params: {
existingColspan: 1,
maxColspan: 12
}
}
],
'6': [],
'7': [],
'8': [],
'9': [],
'10': [],
'11': [],
'12': []
}
}
],
outcomes: [],
metadata: {},
variables: []
}
}
};
export const numberNotRequiredForm = { export const numberNotRequiredForm = {
formRepresentation: { formRepresentation: {
id: 'form-d4c462db-3838-442e-a006-171e6ccafe61', id: 'form-d4c462db-3838-442e-a006-171e6ccafe61',