diff --git a/lib/core/src/lib/form/components/form-renderer.component.html b/lib/core/src/lib/form/components/form-renderer.component.html index 2ebe30faa1..337ad73c58 100644 --- a/lib/core/src/lib/form/components/form-renderer.component.html +++ b/lib/core/src/lib/form/components/form-renderer.component.html @@ -106,7 +106,10 @@ [class.adf-grid-list-container-disabled]="currentRootElement.field.readOnly" >
- {{ 'FORM.FORM_RENDERER.ROW_LABEL' | translate: {number: rowIndex + 1} }} + @if (currentRootElement.field.params.displayRowLabels !== false) { + @let customRowLabel = currentRootElement.field.params | adfRepeatableRowLabel: rowIndex; + {{ customRowLabel ?? ('FORM.FORM_RENDERER.ROW_LABEL' | translate: {number: rowIndex + 1}) }} + } @let shouldDisplayRemoveRowButton = !currentRootElement.field.rows[rowIndex].isInitial || (currentRootElement.field.rows[rowIndex].isInitial && currentRootElement.field.params.allowInitialRowsDelete); @if (shouldDisplayRemoveRowButton) { diff --git a/lib/core/src/lib/form/components/form-renderer.component.scss b/lib/core/src/lib/form/components/form-renderer.component.scss index 76ad886573..46f0eaad46 100644 --- a/lib/core/src/lib/form/components/form-renderer.component.scss +++ b/lib/core/src/lib/form/components/form-renderer.component.scss @@ -88,6 +88,7 @@ padding: 0; width: 30px; height: 30px; + margin-left: auto; display: flex; align-items: center; justify-content: center; diff --git a/lib/core/src/lib/form/components/form-renderer.component.spec.ts b/lib/core/src/lib/form/components/form-renderer.component.spec.ts index 0b14768fb6..efa2a546ff 100644 --- a/lib/core/src/lib/form/components/form-renderer.component.spec.ts +++ b/lib/core/src/lib/form/components/form-renderer.component.spec.ts @@ -1182,5 +1182,46 @@ describe('Form Renderer Component', () => { expect(removeRowButton).toBeFalsy(); }); }); + + describe('row labels', () => { + const labelSelector = '#field-RepeatableSection0tbw2y-container .adf-grid-list-row-label'; + + const buildFormWithLabelParams = (labelParams: Record): FormModel => { + const mock = JSON.parse(JSON.stringify(mockRepeatableSectionForm01)); + Object.assign(mock.formRepresentation.formDefinition.fields[0].params, labelParams); + return new FormModel(mock.formRepresentation); + }; + + const getRenderedLabels = (): string[] => + testingUtils.getAllByCSS(labelSelector).map((element) => element.nativeElement.textContent.trim()); + + it('should render the default translated row label when no label params are set', () => { + const labels = getRenderedLabels(); + + expect(labels.length).toBe(2); + labels.forEach((label) => expect(label).toBe('FORM.FORM_RENDERER.ROW_LABEL')); + }); + + it('should NOT render any row label when displayRowLabels is false', () => { + fixture.componentInstance.formDefinition = buildFormWithLabelParams({ displayRowLabels: false }); + fixture.detectChanges(); + + expect(testingUtils.getAllByCSS(labelSelector).length).toBe(0); + }); + + it('should render the custom label with the row number appended', () => { + fixture.componentInstance.formDefinition = buildFormWithLabelParams({ rowLabelText: 'Approver', appendRowNumber: true }); + fixture.detectChanges(); + + expect(getRenderedLabels()).toEqual(['Approver 1', 'Approver 2']); + }); + + it('should render the custom label without the row number when appendRowNumber is false', () => { + fixture.componentInstance.formDefinition = buildFormWithLabelParams({ rowLabelText: 'Approver', appendRowNumber: false }); + fixture.detectChanges(); + + expect(getRenderedLabels()).toEqual(['Approver', 'Approver']); + }); + }); }); }); diff --git a/lib/core/src/lib/form/components/form-renderer.component.ts b/lib/core/src/lib/form/components/form-renderer.component.ts index 7105dd2bb3..f62bf5bdba 100644 --- a/lib/core/src/lib/form/components/form-renderer.component.ts +++ b/lib/core/src/lib/form/components/form-renderer.component.ts @@ -49,6 +49,7 @@ import { ConfirmDialogComponent } from '../../../lib/dialogs/confirm-dialog/conf import { MatTooltipModule } from '@angular/material/tooltip'; import { IconModule } from '../../icon/icon.module'; import { FormLayoutColumn, getFormLayoutColumnWidth } from './helpers/column-width'; +import { RepeatableRowLabelPipe } from '../pipes/repeatable-row-label.pipe'; @Component({ selector: 'adf-form-renderer', @@ -79,7 +80,8 @@ import { FormLayoutColumn, getFormLayoutColumnWidth } from './helpers/column-wid HeaderWidgetComponent, FormSectionComponent, RepeatWidgetComponent, - MatTooltipModule + MatTooltipModule, + RepeatableRowLabelPipe ], encapsulation: ViewEncapsulation.None }) diff --git a/lib/core/src/lib/form/pipes/index.ts b/lib/core/src/lib/form/pipes/index.ts index 03773792c7..e8f84ea3e0 100644 --- a/lib/core/src/lib/form/pipes/index.ts +++ b/lib/core/src/lib/form/pipes/index.ts @@ -16,3 +16,4 @@ */ export * from './field-style.pipe'; +export * from './repeatable-row-label.pipe'; diff --git a/lib/core/src/lib/form/pipes/repeatable-row-label.pipe.spec.ts b/lib/core/src/lib/form/pipes/repeatable-row-label.pipe.spec.ts new file mode 100644 index 0000000000..c279c1a3dd --- /dev/null +++ b/lib/core/src/lib/form/pipes/repeatable-row-label.pipe.spec.ts @@ -0,0 +1,63 @@ +/*! + * @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 { RepeatableRowLabelPipe } from './repeatable-row-label.pipe'; + +describe('RepeatableRowLabelPipe', () => { + let pipe: RepeatableRowLabelPipe; + + beforeEach(() => { + pipe = new RepeatableRowLabelPipe(); + }); + + it('should return null when the label text is empty', () => { + expect(pipe.transform({ rowLabelText: '', appendRowNumber: true }, 0)).toBeNull(); + }); + + it('should return null when the label text is undefined', () => { + expect(pipe.transform({ appendRowNumber: true }, 0)).toBeNull(); + }); + + it('should return null when the label text is only whitespace', () => { + expect(pipe.transform({ rowLabelText: ' ', appendRowNumber: true }, 0)).toBeNull(); + }); + + it('should return null when the params are undefined', () => { + expect(pipe.transform(undefined, 0)).toBeNull(); + }); + + it('should return null when the label text is not a string', () => { + expect(pipe.transform({ rowLabelText: 5 as unknown as string, appendRowNumber: true }, 0)).toBeNull(); + }); + + it('should append the 1-based row number when appendRowNumber is true', () => { + expect(pipe.transform({ rowLabelText: 'Approver', appendRowNumber: true }, 0)).toBe('Approver 1'); + expect(pipe.transform({ rowLabelText: 'Approver', appendRowNumber: true }, 4)).toBe('Approver 5'); + }); + + it('should append the row number by default when appendRowNumber is undefined', () => { + expect(pipe.transform({ rowLabelText: 'Approver' }, 1)).toBe('Approver 2'); + }); + + it('should NOT append the row number when appendRowNumber is false', () => { + expect(pipe.transform({ rowLabelText: 'Approver', appendRowNumber: false }, 3)).toBe('Approver'); + }); + + it('should trim the label text', () => { + expect(pipe.transform({ rowLabelText: ' Approver ', appendRowNumber: true }, 0)).toBe('Approver 1'); + }); +}); diff --git a/lib/core/src/lib/form/pipes/repeatable-row-label.pipe.ts b/lib/core/src/lib/form/pipes/repeatable-row-label.pipe.ts new file mode 100644 index 0000000000..2d31204a89 --- /dev/null +++ b/lib/core/src/lib/form/pipes/repeatable-row-label.pipe.ts @@ -0,0 +1,38 @@ +/*! + * @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 { Pipe, PipeTransform } from '@angular/core'; + +export interface RepeatableRowLabelParams { + rowLabelText?: string; + appendRowNumber?: boolean; +} + +@Pipe({ + name: 'adfRepeatableRowLabel' +}) +export class RepeatableRowLabelPipe implements PipeTransform { + transform(params: RepeatableRowLabelParams | undefined, rowIndex: number): string | null { + const rowLabelText = typeof params?.rowLabelText === 'string' ? params.rowLabelText.trim() : ''; + + if (!rowLabelText) { + return null; + } + + return params?.appendRowNumber === false ? rowLabelText : `${rowLabelText} ${rowIndex + 1}`; + } +}