From 9f21d602fc64c87e5bf3374774f1cbc4a44aaf37 Mon Sep 17 00:00:00 2001
From: David Olson <157068235+DavidOlson-Hyland@users.noreply.github.com>
Date: Wed, 22 Jul 2026 18:48:32 -0500
Subject: [PATCH] AAE-48859 Allow repeatable section row labels to be
configurable (#12080)
---
.../components/form-renderer.component.html | 5 +-
.../components/form-renderer.component.scss | 1 +
.../form-renderer.component.spec.ts | 41 ++++++++++++
.../components/form-renderer.component.ts | 4 +-
lib/core/src/lib/form/pipes/index.ts | 1 +
.../pipes/repeatable-row-label.pipe.spec.ts | 63 +++++++++++++++++++
.../form/pipes/repeatable-row-label.pipe.ts | 38 +++++++++++
7 files changed, 151 insertions(+), 2 deletions(-)
create mode 100644 lib/core/src/lib/form/pipes/repeatable-row-label.pipe.spec.ts
create mode 100644 lib/core/src/lib/form/pipes/repeatable-row-label.pipe.ts
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}`;
+ }
+}