mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-27107 Create repeatable section widget (#11290)
* AAE-27107 Create widget * AAE-27107 Add tests * AAE-27107 Add tests * AAE-27107 Add tests * AAE-27107 Fix random * AAE-27107 Fix repeat widget tests * AAE-27107 Fix tests * AAE-27107 Add column id property and allow for initial value
This commit is contained in:
@@ -69,6 +69,55 @@
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@if (currentRootElement.type === 'repeatable-section') {
|
||||
<div
|
||||
[id]="'field-' + currentRootElement?.id + '-container'"
|
||||
class="adf-container-widget"
|
||||
[hidden]="!currentRootElement?.isVisible"
|
||||
>
|
||||
<adf-repeat-widget [element]="currentRootElement" [isEditor]="false">
|
||||
@for (row of currentRootElement.field.rows; track row.id; let rowIndex = $index) {
|
||||
@let hasMultipleRows = currentRootElement.field.rows.length > 1;
|
||||
<div
|
||||
class="adf-grid-list-container"
|
||||
[class.adf-grid-list-container-multiple]="hasMultipleRows"
|
||||
>
|
||||
<h4 class="adf-grid-list-container-label">{{ 'FORM.FORM_RENDERER.ROW_LABEL' | translate: {number: rowIndex + 1} }}</h4>
|
||||
<section class="adf-grid-list-column-view">
|
||||
@for (column of row.columns; track $index) {
|
||||
<div
|
||||
class="adf-grid-list-single-column"
|
||||
[style.width.%]="getColumnWidth(currentRootElement)"
|
||||
>
|
||||
@for (field of column?.fields; track $index) {
|
||||
@if (field.type === 'section') {
|
||||
<adf-form-section [field]="field"/>
|
||||
} @else {
|
||||
<div class="adf-grid-list-column-view-item">
|
||||
<adf-form-field [field]="field"/>
|
||||
</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>
|
||||
</div>
|
||||
}
|
||||
</adf-repeat-widget>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div *ngIf="currentRootElement.type === 'dynamic-table'" class="adf-container-widget">
|
||||
<adf-form-field [field]="currentRootElement" />
|
||||
</div>
|
||||
|
||||
@@ -97,6 +97,30 @@
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
&-remove-row-button {
|
||||
margin-top: 20px;
|
||||
|
||||
#{ms.$mat-icon} {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
padding: 0 10px;
|
||||
|
||||
&-label {
|
||||
margin: 5px 0 5px -10px;
|
||||
}
|
||||
|
||||
&-multiple {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.54);
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include flex.layout-bp(lt-md) {
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
formNumberWidgetVisibility,
|
||||
formRequiredNumberWidget,
|
||||
mockSectionVisibilityForm,
|
||||
mockRepeatableSectionForm01,
|
||||
mockRepeatableSectionForm02,
|
||||
multilineWidgetFormVisibilityMock,
|
||||
numberMinMaxForm,
|
||||
numberNotRequiredForm,
|
||||
@@ -46,7 +48,9 @@ import {
|
||||
radioWidgetVisibilityForm,
|
||||
textWidgetVisibility
|
||||
} from './mock/form-renderer.component.mock';
|
||||
import { FormModel, TextWidgetComponent } from './widgets';
|
||||
import { FormFieldModel, FormModel, TextWidgetComponent } from './widgets';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
const typeIntoInput = (testingUtils: UnitTestingUtils, selector: string, message: string) => {
|
||||
testingUtils.fillInputByCSS(selector, message);
|
||||
@@ -87,6 +91,7 @@ describe('Form Renderer Component', () => {
|
||||
let formRenderingService: FormRenderingService;
|
||||
let rulesManager: FormRulesManager<any>;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
let dialog: MatDialog;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -96,6 +101,7 @@ describe('Form Renderer Component', () => {
|
||||
formRendererComponent = fixture.componentInstance;
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
formService = TestBed.inject(FormService);
|
||||
dialog = TestBed.inject(MatDialog);
|
||||
formRenderingService = TestBed.inject(FormRenderingService);
|
||||
rulesManager = fixture.debugElement.injector.get(FormRulesManager);
|
||||
});
|
||||
@@ -799,4 +805,144 @@ describe('Form Renderer Component', () => {
|
||||
expectElementToBeVisible(testingUtils, mockSectionFieldId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Repeatable section', () => {
|
||||
const repeatableSectionField = new FormFieldModel(new FormModel(), {
|
||||
id: 'RepeatableSection0tbw2y',
|
||||
name: 'Repeatable Section',
|
||||
type: 'repeatable-section',
|
||||
tab: null,
|
||||
params: {
|
||||
initialNumberOfRows: 2,
|
||||
allowInitialRowsDelete: true,
|
||||
newRowsLimit: 1
|
||||
},
|
||||
numberOfColumns: 2,
|
||||
fields: {
|
||||
'1': [
|
||||
{
|
||||
id: 'Text0wwp7n',
|
||||
name: 'Text',
|
||||
type: 'text',
|
||||
readOnly: false,
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const formDefinition = new FormModel(mockRepeatableSectionForm01.formRepresentation);
|
||||
fixture.componentInstance.formDefinition = formDefinition;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should display repeatable-section field', () => {
|
||||
const container = testingUtils.getByCSS('#field-RepeatableSection0tbw2y-container');
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should remove row if confimation dialog is true', () => {
|
||||
spyOn(dialog, 'open').and.returnValue({
|
||||
beforeClosed: () => of(true)
|
||||
} as any);
|
||||
|
||||
spyOn(repeatableSectionField, 'removeRow').and.callThrough();
|
||||
|
||||
const rowIndex = 0;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.displayDialogToRemoveRow(repeatableSectionField, rowIndex);
|
||||
|
||||
expect(dialog.open).toHaveBeenCalled();
|
||||
expect(repeatableSectionField.removeRow).toHaveBeenCalledWith(rowIndex);
|
||||
});
|
||||
|
||||
it('should NOT remove row if confirmation dialog is false', () => {
|
||||
spyOn(dialog, 'open').and.returnValue({
|
||||
beforeClosed: () => of(false)
|
||||
} as any);
|
||||
|
||||
spyOn(repeatableSectionField, 'removeRow').and.callThrough();
|
||||
|
||||
const rowIndex = 0;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.displayDialogToRemoveRow(repeatableSectionField, rowIndex);
|
||||
|
||||
expect(dialog.open).toHaveBeenCalled();
|
||||
expect(repeatableSectionField.removeRow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should display the correct number of initial rows', () => {
|
||||
const rows = testingUtils.getAllByCSS('#field-RepeatableSection0tbw2y-container .adf-grid-list-container');
|
||||
|
||||
expect(rows.length).toBeTruthy(2);
|
||||
});
|
||||
|
||||
describe('remove row button', () => {
|
||||
it('should display remove button if allowed', () => {
|
||||
const row = testingUtils.getByCSS('#field-RepeatableSection0tbw2y-container .adf-grid-list-container');
|
||||
|
||||
expect(row).toBeTruthy();
|
||||
|
||||
const removeRowButton = testingUtils.getByCSS(
|
||||
'#field-RepeatableSection0tbw2y-container .adf-grid-list-container .adf-grid-list-remove-row-button'
|
||||
);
|
||||
|
||||
expect(removeRowButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should NOT display remove button if NOT allowed', () => {
|
||||
const formDefinition = new FormModel(mockRepeatableSectionForm02.formRepresentation);
|
||||
fixture.componentInstance.formDefinition = formDefinition;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const row = testingUtils.getByCSS('#field-RepeatableSection0tbw2y-container .adf-grid-list-container');
|
||||
|
||||
expect(row).toBeTruthy();
|
||||
|
||||
const removeRowButton = testingUtils.getByCSS(
|
||||
'#field-RepeatableSection0tbw2y-container .adf-grid-list-container .adf-grid-list-remove-row-button'
|
||||
);
|
||||
|
||||
expect(removeRowButton).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { NgClass, NgForOf, NgIf, NgStyle, NgTemplateOutlet } from '@angular/common';
|
||||
import { Component, Inject, Injector, Input, OnDestroy, OnInit, Optional, ViewEncapsulation } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, Inject, inject, Injector, Input, OnDestroy, OnInit, Optional, ViewEncapsulation } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@@ -26,10 +26,12 @@ import { FormRulesManager, formRulesManagerFactory } from '../models/form-rules.
|
||||
import { FormService } from '../services/form.service';
|
||||
import { FormFieldComponent } from './form-field/form-field.component';
|
||||
import { FORM_FIELD_MODEL_RENDER_MIDDLEWARE, FormFieldModelRenderMiddleware } from './middlewares/middleware';
|
||||
import { ContainerModel, FormFieldModel, FormModel, TabModel } from './widgets';
|
||||
import { ContainerModel, FormFieldModel, FormModel, TabModel, RepeatWidgetComponent } from './widgets';
|
||||
import { HeaderWidgetComponent } from './widgets/header/header.widget';
|
||||
import { FormSectionComponent } from './form-section/form-section.component';
|
||||
import { DecimalRenderMiddlewareService } from './middlewares/decimal-middleware.service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from '../../../lib/dialogs/confirm-dialog/confirm.dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-form-renderer',
|
||||
@@ -60,11 +62,17 @@ import { DecimalRenderMiddlewareService } from './middlewares/decimal-middleware
|
||||
FormsModule,
|
||||
NgClass,
|
||||
HeaderWidgetComponent,
|
||||
FormSectionComponent
|
||||
FormSectionComponent,
|
||||
RepeatWidgetComponent
|
||||
],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class FormRendererComponent<T> implements OnInit, OnDestroy {
|
||||
public readonly formService = inject(FormService);
|
||||
private readonly formRulesManager = inject(FormRulesManager<T>);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
|
||||
@Input({ required: true })
|
||||
formDefinition: FormModel;
|
||||
|
||||
@@ -76,8 +84,6 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy {
|
||||
fields: FormFieldModel[];
|
||||
|
||||
constructor(
|
||||
public formService: FormService,
|
||||
private formRulesManager: FormRulesManager<T>,
|
||||
@Optional()
|
||||
@Inject(FORM_FIELD_MODEL_RENDER_MIDDLEWARE)
|
||||
private middlewareServices?: FormFieldModelRenderMiddleware[]
|
||||
@@ -145,6 +151,27 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy {
|
||||
return maxFieldSize;
|
||||
}
|
||||
|
||||
displayDialogToRemoveRow(field: FormFieldModel, rowIndex: number) {
|
||||
this.dialog
|
||||
.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
title: 'FORM.FORM_RENDERER.REMOVE_ROW_DIALOG.TITLE',
|
||||
message: 'FORM.FORM_RENDERER.REMOVE_ROW_DIALOG.MESSAGE',
|
||||
yesLabel: 'FORM.FORM_RENDERER.REMOVE_ROW_DIALOG.YES_LABEL',
|
||||
noLabel: 'FORM.FORM_RENDERER.REMOVE_ROW_DIALOG.NO_LABEL'
|
||||
},
|
||||
minWidth: '500px',
|
||||
closeOnNavigation: true
|
||||
})
|
||||
.beforeClosed()
|
||||
.subscribe((shouldRemove) => {
|
||||
if (shouldRemove) {
|
||||
field.removeRow(rowIndex);
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the column width based on the numberOfColumns and current field's colspan property
|
||||
*
|
||||
|
||||
@@ -2314,6 +2314,147 @@ export const mockSectionVisibilityForm = {
|
||||
variables: []
|
||||
}
|
||||
};
|
||||
export const mockRepeatableSectionForm01 = {
|
||||
formRepresentation: {
|
||||
id: 'form-c3ac7a6b-88c8-4111-9f04-e9cc28f352d9',
|
||||
name: 'repeatable-section-mock',
|
||||
key: 'repeatable-section-mock-rthbq',
|
||||
description: '',
|
||||
version: 0,
|
||||
formDefinition: {
|
||||
tabs: [],
|
||||
fields: [
|
||||
{
|
||||
id: 'RepeatableSection0tbw2y',
|
||||
name: 'Repeatable Section',
|
||||
type: 'repeatable-section',
|
||||
tab: null,
|
||||
params: {
|
||||
initialNumberOfRows: 2,
|
||||
allowInitialRowsDelete: true,
|
||||
newRowsLimit: 3
|
||||
},
|
||||
numberOfColumns: 2,
|
||||
fields: {
|
||||
'1': [
|
||||
{
|
||||
id: 'Text0wwp7n',
|
||||
name: 'Text',
|
||||
type: 'text',
|
||||
readOnly: false,
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
outcomes: [],
|
||||
metadata: {},
|
||||
variables: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const mockRepeatableSectionForm02 = {
|
||||
formRepresentation: {
|
||||
id: 'form-c3ac7a6b-88c8-4111-9f04-e9cc28f352d9',
|
||||
name: 'repeatable-section-mock',
|
||||
key: 'repeatable-section-mock-rthbq',
|
||||
description: '',
|
||||
version: 0,
|
||||
formDefinition: {
|
||||
tabs: [],
|
||||
fields: [
|
||||
{
|
||||
id: 'RepeatableSection0tbw2y',
|
||||
name: 'Repeatable Section',
|
||||
type: 'repeatable-section',
|
||||
tab: null,
|
||||
params: {
|
||||
initialNumberOfRows: 2,
|
||||
allowInitialRowsDelete: false,
|
||||
newRowsLimit: 3
|
||||
},
|
||||
numberOfColumns: 2,
|
||||
fields: {
|
||||
'1': [
|
||||
{
|
||||
id: 'Text0wwp7n',
|
||||
name: 'Text',
|
||||
type: 'text',
|
||||
readOnly: false,
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
outcomes: [],
|
||||
metadata: {},
|
||||
variables: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const displayDynamicTableMock = {
|
||||
id: 1,
|
||||
|
||||
@@ -18,11 +18,16 @@
|
||||
import { FormFieldModel } from './form-field.model';
|
||||
|
||||
export class ContainerColumnModel {
|
||||
id: string;
|
||||
size: number = 12;
|
||||
fields: FormFieldModel[] = [];
|
||||
colspan: number = 1;
|
||||
rowspan: number = 1;
|
||||
|
||||
constructor() {
|
||||
this.id = window.crypto.getRandomValues(new Uint32Array(1))[0].toString();
|
||||
}
|
||||
|
||||
hasFields(): boolean {
|
||||
return !!this.fields?.length;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*!
|
||||
* @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 { ContainerColumnModel } from './container-column.model';
|
||||
|
||||
export class ContainerRowModel {
|
||||
id: string;
|
||||
isInitial: boolean;
|
||||
columns: ContainerColumnModel[] = [];
|
||||
|
||||
size: number = 12;
|
||||
colspan: number = 1;
|
||||
rowspan: number = 1;
|
||||
|
||||
constructor(isInitial: boolean = false) {
|
||||
this.isInitial = isInitial;
|
||||
this.id = window.crypto.getRandomValues(new Uint32Array(1))[0].toString();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { MaybeReactiveFormWidget, ReactiveFormWidget } from '../reactive-widget.
|
||||
export class FormFieldTypes {
|
||||
static CONTAINER: string = 'container';
|
||||
static GROUP: string = 'group';
|
||||
static REPEATABLE_SECTION: string = 'repeatable-section';
|
||||
static SECTION: string = 'section';
|
||||
static DYNAMIC_TABLE: string = 'dynamic-table';
|
||||
static TEXT: string = 'text';
|
||||
@@ -88,4 +89,8 @@ export class FormFieldTypes {
|
||||
static isSectionType(type: string): boolean {
|
||||
return type === FormFieldTypes.SECTION;
|
||||
}
|
||||
|
||||
static isRepeatableSectionType(type: string): boolean {
|
||||
return type === FormFieldTypes.REPEATABLE_SECTION;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,7 +852,8 @@ describe('FormFieldModel', () => {
|
||||
FormFieldTypes.READONLY_TYPES.forEach((typeName) => {
|
||||
const field = new FormFieldModel(form, {
|
||||
id: typeName,
|
||||
type: typeName
|
||||
type: typeName,
|
||||
params: {}
|
||||
});
|
||||
|
||||
field.value = '<some value>';
|
||||
@@ -1266,4 +1267,146 @@ describe('FormFieldModel', () => {
|
||||
|
||||
expect(field.tooltip).toBe('');
|
||||
});
|
||||
|
||||
describe('repeatable section', () => {
|
||||
let form: FormModel;
|
||||
let field: FormFieldModel;
|
||||
|
||||
beforeEach(() => {
|
||||
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': [
|
||||
{
|
||||
id: 'Text0wwp7n',
|
||||
name: 'Text',
|
||||
type: 'text',
|
||||
readOnly: false,
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('add row', () => {
|
||||
it('should add row if allowed by limit param', () => {
|
||||
expect(field.rows.length).toBe(2);
|
||||
|
||||
field.addRow(field.fields, form);
|
||||
expect(field.rows.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should add row if no limit param is defined', () => {
|
||||
field.params.newRowsLimit = null;
|
||||
expect(field.rows.length).toBe(2);
|
||||
|
||||
field.addRow(field.fields, form);
|
||||
expect(field.rows.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should NOT add row if NOT allowed by limit param', () => {
|
||||
expect(field.rows.length).toBe(2);
|
||||
|
||||
field.addRow(field.fields, form);
|
||||
field.addRow(field.fields, form);
|
||||
field.addRow(field.fields, form);
|
||||
expect(field.rows.length).toBe(5);
|
||||
|
||||
field.addRow(field.fields, form);
|
||||
expect(field.rows.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove row', () => {
|
||||
it('should remove row if target index exists', () => {
|
||||
expect(field.rows.length).toBe(2);
|
||||
|
||||
field.removeRow(1);
|
||||
expect(field.rows.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should NOT remove row if target index does NOT exist', () => {
|
||||
expect(field.rows.length).toBe(2);
|
||||
|
||||
field.removeRow(2);
|
||||
expect(field.rows.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should update children fields rowIndex', () => {
|
||||
expect(field.rows[0].columns[0].fields[0].parent.rowIndex).toBe(0);
|
||||
expect(field.rows[1].columns[0].fields[0].parent.rowIndex).toBe(1);
|
||||
|
||||
field.removeRow(0);
|
||||
expect(field.rows[0].columns[0].fields[0].parent.rowIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should update form value', () => {
|
||||
const formValues = {
|
||||
initialState: [
|
||||
{
|
||||
Text0wwp7n: 'mock-1',
|
||||
Integer0rzkwq: 1
|
||||
},
|
||||
{
|
||||
Text0wwp7n: 'mock-2',
|
||||
Integer0rzkwq: 2
|
||||
}
|
||||
],
|
||||
removeState: [
|
||||
{
|
||||
Text0wwp7n: 'mock-2',
|
||||
Integer0rzkwq: 2
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
form.values[field.id] = formValues.initialState;
|
||||
expect(form.values[field.id]).toEqual(formValues.initialState);
|
||||
|
||||
field.removeRow(0);
|
||||
expect(form.values[field.id]).toEqual(formValues.removeState);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,11 +30,22 @@ import { VariableConfig } from './form-field-variable-options';
|
||||
import { DataColumn } from '../../../../datatable/data/data-column.model';
|
||||
import { DateFnsUtils } from '../../../../common';
|
||||
import { isValid as isValidDate } from 'date-fns';
|
||||
import { ContainerRowModel } from './container-row.model';
|
||||
|
||||
export type FieldOptionType = 'rest' | 'manual' | 'variable';
|
||||
export type FieldSelectionType = 'single' | 'multiple';
|
||||
export type FieldAlignmentType = 'vertical' | 'horizontal';
|
||||
|
||||
interface RepeatableSectionModel {
|
||||
id: string;
|
||||
uid: string;
|
||||
fields: FormFieldModel[];
|
||||
rowIndex: number;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
const ROW_ID_PREFIX = '-Row';
|
||||
|
||||
// Maps to FormFieldRepresentation
|
||||
export class FormFieldModel extends FormWidgetModel {
|
||||
private _value: string;
|
||||
@@ -94,11 +105,13 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
schemaDefinition: DataColumn[];
|
||||
externalProperty?: string;
|
||||
style?: string;
|
||||
parent?: RepeatableSectionModel;
|
||||
|
||||
// container model members
|
||||
numberOfColumns: number = 1;
|
||||
fields: FormFieldModel[] = [];
|
||||
columns: ContainerColumnModel[] = [];
|
||||
rows: ContainerRowModel[] = [];
|
||||
|
||||
// util members
|
||||
emptyOption: FormFieldOption;
|
||||
@@ -172,11 +185,11 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
return !this.readOnly || FormFieldTypes.isValidatableType(this.type);
|
||||
}
|
||||
|
||||
constructor(form: any, json?: any) {
|
||||
constructor(form: any, json?: any, parent?: RepeatableSectionModel) {
|
||||
super(form, json);
|
||||
if (json) {
|
||||
this.fieldType = json.fieldType;
|
||||
this.id = json.id;
|
||||
this.id = parent ? parent.uid : json.id;
|
||||
this.name = json.name;
|
||||
this.type = json.type;
|
||||
this.roles = json.roles;
|
||||
@@ -221,8 +234,9 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
this.schemaDefinition = json.schemaDefinition;
|
||||
this.precision = json.precision;
|
||||
this.externalProperty = json.externalProperty;
|
||||
this._value = this.parseValue(json);
|
||||
this._value = this.parseValue(json, parent?.value);
|
||||
this.style = json.style;
|
||||
this.parent = parent;
|
||||
|
||||
if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') {
|
||||
this.placeholder = json.placeholder;
|
||||
@@ -237,6 +251,10 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
if (FormFieldTypes.isContainerType(this.type) || FormFieldTypes.isSectionType(this.type)) {
|
||||
this.containerFactory(json, form);
|
||||
}
|
||||
|
||||
if (FormFieldTypes.isRepeatableSectionType(this.type)) {
|
||||
this.repeatableSectionFactory(json, form);
|
||||
}
|
||||
}
|
||||
|
||||
if (form?.json) {
|
||||
@@ -306,14 +324,123 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
});
|
||||
}
|
||||
|
||||
private repeatableSectionFactory(json: any, form: any): void {
|
||||
const { numberOfColumns = 1, params, value, fields = {} } = json;
|
||||
|
||||
this.numberOfColumns = numberOfColumns;
|
||||
this.fields = fields;
|
||||
this.rowspan = 1;
|
||||
this.colspan = 1;
|
||||
this.rows = [];
|
||||
|
||||
for (let i = 0; i < this.getNumberOfRows(params.initialNumberOfRows, params.newRowsLimit, value); i++) {
|
||||
this.rows.push(this.createRow(fields, form, i, value?.[i], i < params?.initialNumberOfRows));
|
||||
}
|
||||
|
||||
this.columns = this.rows[0].columns;
|
||||
}
|
||||
|
||||
private getNumberOfRows(initialNrRows: number = 1, rowsLimit?: number, value?: any) {
|
||||
return value?.length && !!rowsLimit ? Math.min(value?.length, initialNrRows + rowsLimit) : (value?.length ?? initialNrRows);
|
||||
}
|
||||
|
||||
private createRow(fields: any, form: any, index: number, value?: any, isInitial: boolean = false) {
|
||||
const row = new ContainerRowModel(isInitial);
|
||||
|
||||
row.columns.push(...this.createColumns(fields, form, index, value));
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private createColumns(fields: any, form: any, index?: number, value?: any) {
|
||||
const columns: ContainerColumnModel[] = [];
|
||||
|
||||
Object.keys(fields).forEach((currentField) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(fields, currentField)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const col = new ContainerColumnModel();
|
||||
col.fields = (fields[currentField] || []).map(
|
||||
(field: any) =>
|
||||
new FormFieldModel(form, field, {
|
||||
id: this.id,
|
||||
uid: this.getUniqueId(field),
|
||||
fields: this.fields,
|
||||
rowIndex: index ?? 0,
|
||||
value: value?.[field.id]
|
||||
})
|
||||
);
|
||||
col.rowspan = fields[currentField].length;
|
||||
|
||||
if (!FormFieldTypes.isSectionType(this.type)) {
|
||||
this.updateContainerColspan(col.fields);
|
||||
}
|
||||
|
||||
this.rowspan = Math.max(this.rowspan, col.rowspan);
|
||||
columns.push(col);
|
||||
});
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
private getUniqueId(field: FormFieldModel): string {
|
||||
return field.id + ROW_ID_PREFIX + window.crypto.getRandomValues(new Uint32Array(1))[0].toString();
|
||||
}
|
||||
|
||||
private updateChildrenFieldsRowIndex() {
|
||||
this.rows.forEach((row: ContainerRowModel, index: number) => {
|
||||
for (const column of row.columns) {
|
||||
for (const field of column.fields) {
|
||||
field.parent.rowIndex = index;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private createInitialValue(fields: any) {
|
||||
return Object.keys(fields)
|
||||
.map((currentField) => (fields[currentField] || []).map((field) => field.id))
|
||||
.flat(1)
|
||||
.reduce((acc, curr) => ((acc[curr] = null), acc), {});
|
||||
}
|
||||
|
||||
private updateContainerColspan(fields: FormFieldModel[]): void {
|
||||
fields.forEach((colField: FormFieldModel) => {
|
||||
this.colspan = Math.max(this.colspan, colField.colspan);
|
||||
});
|
||||
}
|
||||
|
||||
parseValue(json: any): any {
|
||||
const value = Object.prototype.hasOwnProperty.call(json, 'value') && json.value !== undefined ? json.value : null;
|
||||
addRow(fields: any, form: any) {
|
||||
if (!this.shouldAddRow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rows.push(this.createRow(fields, form, this.rows.length));
|
||||
}
|
||||
|
||||
private shouldAddRow(): boolean {
|
||||
return !this.params.newRowsLimit || this.rows.length < this.params.initialNumberOfRows + this.params.newRowsLimit;
|
||||
}
|
||||
|
||||
removeRow(index: number) {
|
||||
if (!this.shouldRemoveRow(index)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rows.splice(index, 1);
|
||||
this.updateChildrenFieldsRowIndex();
|
||||
|
||||
this.form.values[this.id].splice(index, 1);
|
||||
this.form.onFormFieldChanged(this);
|
||||
}
|
||||
|
||||
private shouldRemoveRow(index: number): boolean {
|
||||
return this.rows.length > index;
|
||||
}
|
||||
|
||||
parseValue(json: any, initialValue?: any): any {
|
||||
const value = initialValue ?? (Object.prototype.hasOwnProperty.call(json, 'value') && json.value !== undefined ? json.value : null);
|
||||
|
||||
/*
|
||||
This is needed due to Activiti issue related to reading dropdown values as value string
|
||||
@@ -410,6 +537,11 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.parent) {
|
||||
this.updateRepeatableSectionValue();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.type) {
|
||||
case FormFieldTypes.DROPDOWN: {
|
||||
if (!this.value) {
|
||||
@@ -540,6 +672,17 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
this.form.values[this.id] = this.value ? this.value : null;
|
||||
break;
|
||||
}
|
||||
case FormFieldTypes.REPEATABLE_SECTION: {
|
||||
this.form.values[this.id] = this.value ? this.value : [];
|
||||
this.repeatableSectionFactory(
|
||||
{
|
||||
...this.json,
|
||||
value: this.value
|
||||
},
|
||||
this.form
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (this.shouldUpdateFormValues(this.type)) {
|
||||
this.form.values[this.id] = this.value;
|
||||
@@ -549,6 +692,20 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
this.form.onFormFieldChanged(this);
|
||||
}
|
||||
|
||||
private updateRepeatableSectionValue() {
|
||||
if (!this.form.values[this.parent.id]) {
|
||||
this.form.values[this.parent.id] = [];
|
||||
}
|
||||
|
||||
if (!this.form.values[this.parent.id][this.parent.rowIndex]) {
|
||||
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.onFormFieldChanged(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the field type is invalid, requires a type to be a `container`
|
||||
*
|
||||
|
||||
@@ -56,6 +56,7 @@ export * from './text/text-mask.component';
|
||||
// widgets with schema
|
||||
export * from './display-text';
|
||||
export * from './header';
|
||||
export * from './repeat/repeat.widget';
|
||||
|
||||
export const WIDGET_DIRECTIVES = [
|
||||
UnknownWidgetComponent,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<div [style]="element | adfFieldStyle" class="adf-container-widget-repeat">
|
||||
<h4
|
||||
id="container-repeat"
|
||||
class="adf-container-widget-repeat__text"
|
||||
>
|
||||
<span [id]="'container-repeat-label-' + element?.id" role="button" tabindex="0">
|
||||
{{ element.name | translate }}
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<ng-content />
|
||||
|
||||
@if (!isEditor) {
|
||||
@let shouldDisplayAddRowButton = !element.field.params.newRowsLimit || (element.field.params.newRowsLimit > getAddedRowsCount());
|
||||
@if (shouldDisplayAddRowButton) {
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
class="adf-container-widget-row-action"
|
||||
(click)="addRow()"
|
||||
>
|
||||
<mat-icon>add</mat-icon> {{ 'FORM.FIELD.REPEATABLE_SECTION.ADD_ROW' | translate }}
|
||||
</button>
|
||||
} @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: rowLimit } }}</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
.adf-container-widget {
|
||||
&-repeat__text {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.87);
|
||||
padding-bottom: 10px;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
font-size: var(--adf-header-font-size);
|
||||
font-weight: var(--adf-header-font-weight);
|
||||
color: var(--adf-header-color);
|
||||
line-height: normal;
|
||||
|
||||
&.adf-collapsible {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-row-action {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&-row-limit {
|
||||
color: rgba(0, 0, 0, 0.67);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*!
|
||||
* @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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ContainerModel } from '../core/container.model';
|
||||
import { FormFieldModel } from '../core/form-field.model';
|
||||
import { RepeatWidgetComponent } from './repeat.widget';
|
||||
import { FormModel } from '../core';
|
||||
import { UnitTestingUtils } from '../../../../testing';
|
||||
|
||||
describe('RepeatWidgetComponent', () => {
|
||||
let component: RepeatWidgetComponent;
|
||||
let fixture: ComponentFixture<RepeatWidgetComponent>;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param initialNumberOfRows initial number of rows
|
||||
* @param newRowsLimit limit for additional rows
|
||||
* @param allowInitialRowsDelete should allow deleting rows
|
||||
* @returns repeatable section json based on params
|
||||
*/
|
||||
function getFormFieldJson(initialNumberOfRows: number = 2, newRowsLimit?: number, allowInitialRowsDelete: boolean = true) {
|
||||
return {
|
||||
id: 'RepeatableSection0tbw2y',
|
||||
name: 'Repeatable Section',
|
||||
type: 'repeatable-section',
|
||||
tab: null,
|
||||
params: {
|
||||
initialNumberOfRows,
|
||||
allowInitialRowsDelete,
|
||||
newRowsLimit
|
||||
},
|
||||
numberOfColumns: 2,
|
||||
fields: {
|
||||
'1': [
|
||||
{
|
||||
id: 'Text0wwp7n',
|
||||
name: 'Text',
|
||||
type: 'text',
|
||||
readOnly: false,
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RepeatWidgetComponent]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(RepeatWidgetComponent);
|
||||
component = fixture.componentInstance;
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
});
|
||||
|
||||
describe('is editor', () => {
|
||||
it('should NOT display add row button or row limit', () => {
|
||||
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson()));
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByCSS('button.adf-container-widget-row-action')).toBeFalsy();
|
||||
expect(testingUtils.getByCSS('span.adf-container-widget-row-action')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('is NOT editor', () => {
|
||||
beforeEach(() => {
|
||||
component.isEditor = false;
|
||||
});
|
||||
|
||||
it('should display add row button if no limit is defined', () => {
|
||||
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson()));
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByCSS('button.adf-container-widget-row-action')).toBeTruthy();
|
||||
expect(testingUtils.getByCSS('span.adf-container-widget-row-action')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should display add row button if limit is defined but not reached', () => {
|
||||
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 1)));
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByCSS('button.adf-container-widget-row-action')).toBeTruthy();
|
||||
expect(testingUtils.getByCSS('span.adf-container-widget-row-action')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should NOT display add row button if limit is defined and reached', () => {
|
||||
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 1)));
|
||||
spyOn(component, 'getAddedRowsCount').and.returnValue(1);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByCSS('button.adf-container-widget-row-action')).toBeFalsy();
|
||||
expect(testingUtils.getByCSS('span.adf-container-widget-row-action')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display row limit if limit has been reached', () => {
|
||||
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson(2, 1)));
|
||||
spyOn(component, 'getAddedRowsCount').and.returnValue(1);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testingUtils.getByCSS('span.adf-container-widget-row-action')).toBeTruthy();
|
||||
expect(testingUtils.getByCSS('span.adf-container-widget-row-action').nativeElement.textContent.trim()).toBe(
|
||||
'FORM.FIELD.REPEATABLE_SECTION.ROW_LIMIT_REACHED'
|
||||
);
|
||||
});
|
||||
|
||||
it('should add row when add row button is clicked', () => {
|
||||
component.element = new ContainerModel(new FormFieldModel(new FormModel(), getFormFieldJson()));
|
||||
spyOn(component, 'addRow').and.callThrough();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
testingUtils.clickByCSS('button.adf-container-widget-row-action');
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*!
|
||||
* @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 { Component, Input, ViewEncapsulation } from '@angular/core';
|
||||
import { FieldStylePipe } from '../../../pipes/field-style.pipe';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { ContainerModel } from '../core/container.model';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-repeat-widget',
|
||||
templateUrl: './repeat.widget.html',
|
||||
styleUrls: ['./repeat.widget.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
imports: [FieldStylePipe, MatIconModule, MatButtonModule, TranslatePipe]
|
||||
})
|
||||
export class RepeatWidgetComponent {
|
||||
@Input() element: ContainerModel;
|
||||
@Input() isEditor: boolean = true;
|
||||
|
||||
addRow() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -69,10 +69,21 @@
|
||||
"NO_LONGER_THAN": "Enter no more than {{ maxLength }} characters"
|
||||
},
|
||||
"FILE_ALREADY_UPLOADED": "A file with the same name is already uploaded.",
|
||||
"ATTACH": "Attach"
|
||||
"ATTACH": "Attach",
|
||||
"REPEATABLE_SECTION": {
|
||||
"ADD_ROW": "add",
|
||||
"ROW_LIMIT_REACHED": "Rows limit ({{ limit }}) reached."
|
||||
}
|
||||
},
|
||||
"FORM_RENDERER": {
|
||||
"NAMELESS_TASK": "Nameless task"
|
||||
"NAMELESS_TASK": "Nameless task",
|
||||
"ROW_LABEL": "Row {{ number }}",
|
||||
"REMOVE_ROW_DIALOG": {
|
||||
"TITLE": "Delete the row",
|
||||
"MESSAGE": "Are you sure you want to delete this row?",
|
||||
"YES_LABEL": "Delete row",
|
||||
"NO_LABEL": "Cancel"
|
||||
}
|
||||
},
|
||||
"FIELD_STYLE": {
|
||||
"FONT_SIZE": "Font size",
|
||||
|
||||
Reference in New Issue
Block a user