mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
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:
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user