Revert "AAE-44098 Bottom Tab nav buttons (#11801)" (#11832)

This reverts commit 7864f1a8e0.
This commit is contained in:
Joshua Cain
2026-05-20 14:19:21 +05:30
committed by Anamika Dey
parent 88414c5651
commit 82cfd14cd7
7 changed files with 112 additions and 637 deletions
@@ -86,30 +86,6 @@ const expectElementToBeValid = (testingUtils: UnitTestingUtils, fieldId: string)
expect(invalidElementContainer).toBeFalsy();
};
const buildTabbedForm = (tabCount: number, hiddenTabIndices: number[] = []): FormModel => {
const tabs = Array.from({ length: tabCount }, (_, i) => ({
id: `tab-${i}`,
title: `Tab ${i}`
}));
const fields = tabs.map((tab) => ({
id: `container-${tab.id}`,
type: 'container',
tab: tab.id,
numberOfColumns: 1,
fields: { 1: [{ id: `text-${tab.id}`, type: 'text', name: `Text in ${tab.title}` }] }
}));
const form = new FormModel({ tabs, fields });
hiddenTabIndices.forEach((tabIndex) => {
if (form.tabs[tabIndex]) {
form.tabs[tabIndex].isVisible = false;
}
});
return form;
};
describe('Form Renderer Component', () => {
let formRendererComponent: FormRendererComponent<any>;
let fixture: ComponentFixture<FormRendererComponent<any>>;
@@ -933,117 +909,6 @@ describe('Form Renderer Component', () => {
});
});
describe('Tab navigation', () => {
describe('visibleTabs', () => {
it('should return only tabs where isVisible is true', () => {
formRendererComponent.formDefinition = buildTabbedForm(3, [1]);
expect(formRendererComponent.visibleTabs().length).toBe(2);
expect(formRendererComponent.visibleTabs().every((t) => t.isVisible)).toBeTrue();
});
it('should return all tabs when none are hidden', () => {
formRendererComponent.formDefinition = buildTabbedForm(3);
expect(formRendererComponent.visibleTabs().length).toBe(3);
});
it('should return empty array when all tabs are hidden', () => {
formRendererComponent.formDefinition = buildTabbedForm(3, [0, 1, 2]);
expect(formRendererComponent.visibleTabs().length).toBe(0);
});
});
describe('canNavigateNext and canNavigatePrevious', () => {
beforeEach(() => {
fixture.componentRef.setInput('formDefinition', buildTabbedForm(3));
fixture.detectChanges();
});
it('should not allow navigating previous on the first tab', () => {
expect(formRendererComponent.canNavigatePrevious).toBeFalse();
expect(formRendererComponent.canNavigateNext).toBeTrue();
});
it('should allow navigating previous and next between the first and last tabs', async () => {
formRendererComponent.navigateToNextTab();
fixture.detectChanges();
await fixture.whenStable();
expect(formRendererComponent.canNavigatePrevious).toBeTrue();
expect(formRendererComponent.canNavigateNext).toBeTrue();
});
it('should not allow navigating next on the last tab', async () => {
formRendererComponent.navigateToNextTab();
fixture.detectChanges();
await fixture.whenStable();
formRendererComponent.tabGroup.selectedIndexChange.emit(1);
fixture.detectChanges();
formRendererComponent.navigateToNextTab();
fixture.detectChanges();
await fixture.whenStable();
formRendererComponent.tabGroup.selectedIndexChange.emit(2);
fixture.detectChanges();
expect(formRendererComponent.canNavigatePrevious).toBeTrue();
expect(formRendererComponent.canNavigateNext).toBeFalse();
});
});
describe('navigateToNextTab and navigateToPreviousTab', () => {
beforeEach(() => {
fixture.componentRef.setInput('formDefinition', buildTabbedForm(3));
fixture.detectChanges();
expect(formRendererComponent.tabGroup).toBeDefined();
});
it('should increment selectedIndex when navigating to next tab', async () => {
const initialIndex = formRendererComponent.tabGroup.selectedIndex;
formRendererComponent.navigateToNextTab();
fixture.detectChanges();
await fixture.whenStable();
expect(formRendererComponent.tabGroup.selectedIndex).toBe(initialIndex + 1);
});
it('should decrement selectedIndex when navigating to previous tab', async () => {
formRendererComponent.navigateToNextTab();
fixture.detectChanges();
await fixture.whenStable();
const indexAfterNext = formRendererComponent.tabGroup.selectedIndex;
formRendererComponent.navigateToPreviousTab();
fixture.detectChanges();
await fixture.whenStable();
expect(formRendererComponent.tabGroup.selectedIndex).toBe(indexAfterNext - 1);
});
it('should not go below 0 when navigating previous on the first tab', async () => {
formRendererComponent.navigateToPreviousTab();
fixture.detectChanges();
await fixture.whenStable();
expect(formRendererComponent.tabGroup.selectedIndex).toBe(0);
});
it('should not exceed last index when navigating next on the last tab', async () => {
formRendererComponent.navigateToNextTab();
fixture.detectChanges();
await fixture.whenStable();
formRendererComponent.tabGroup.selectedIndexChange.emit(1);
fixture.detectChanges();
formRendererComponent.navigateToNextTab();
fixture.detectChanges();
await fixture.whenStable();
formRendererComponent.tabGroup.selectedIndexChange.emit(2);
fixture.detectChanges();
const lastIndex = formRendererComponent.tabGroup.selectedIndex;
formRendererComponent.navigateToNextTab();
fixture.detectChanges();
await fixture.whenStable();
expect(formRendererComponent.tabGroup.selectedIndex).toBe(lastIndex);
});
});
});
describe('Repeatable section', () => {
const repeatableSectionField = new FormFieldModel(new FormModel(), {
id: 'RepeatableSection0tbw2y',
@@ -16,25 +16,12 @@
*/
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
import {
AfterViewInit,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Injector,
Input,
OnDestroy,
OnInit,
signal,
ViewChild,
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 { filter } from 'rxjs';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatTabGroup, MatTabsModule } from '@angular/material/tabs';
import { MatTabsModule } from '@angular/material/tabs';
import { TranslatePipe } from '@ngx-translate/core';
import { FormRulesManager, formRulesManagerFactory } from '../models/form-rules.model';
import { FormService } from '../services/form.service';
@@ -84,7 +71,7 @@ import { FormLayoutColumn, getFormLayoutColumnWidth } from './helpers/column-wid
],
encapsulation: ViewEncapsulation.None
})
export class FormRendererComponent<T> implements OnInit, OnDestroy, AfterViewInit {
export class FormRendererComponent<T> implements OnInit, OnDestroy {
private readonly middlewareServices = inject<FormFieldModelRenderMiddleware[]>(FORM_FIELD_MODEL_RENDER_MIDDLEWARE, { optional: true }) ?? [];
public readonly formService = inject(FormService);
@@ -100,22 +87,6 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy, AfterViewIni
@Input()
readOnly = false;
@ViewChild(MatTabGroup) tabGroup!: MatTabGroup;
private readonly currentTabIndex = signal(0);
get canNavigateNext(): boolean {
return this.currentTabIndex() < this.visibleTabCount - 1;
}
get canNavigatePrevious(): boolean {
return this.currentTabIndex() > 0;
}
get visibleTabCount(): number {
return this.visibleTabs().length;
}
debugMode: boolean;
fields: FormFieldModel[];
@@ -134,12 +105,6 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy, AfterViewIni
.subscribe(() => this.visibilityService.refreshVisibility(this.formDefinition));
}
ngAfterViewInit(): void {
if (this.tabGroup) {
this.tabGroup.selectedIndexChange.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((index) => this.currentTabIndex.set(index));
}
}
ngOnDestroy() {
this.formRulesManager.destroy();
}
@@ -152,18 +117,6 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy, AfterViewIni
return this.formDefinition.tabs.filter((tab) => tab.isVisible);
}
navigateToNextTab(): void {
if (this.tabGroup && this.canNavigateNext) {
this.tabGroup.selectedIndex = this.tabGroup.selectedIndex + 1;
}
}
navigateToPreviousTab(): void {
if (this.tabGroup && this.canNavigatePrevious) {
this.tabGroup.selectedIndex = this.tabGroup.selectedIndex - 1;
}
}
getNumberOfColumns(content: ContainerModel): number {
return (content.json?.numberOfColumns || 1) > (content.columns?.length || 1)
? content.json?.numberOfColumns || 1
-6
View File
@@ -87,12 +87,6 @@
"NO_LABEL": "Cancel"
}
},
"BUTTON": {
"PREVIOUS_TAB": "Previous",
"PREVIOUS_TAB_TITLE": "Navigate to previous tab",
"NEXT_TAB": "Next",
"NEXT_TAB_TITLE": "Navigate to next tab"
},
"FIELD_STYLE": {
"FONT_SIZE": "Font size",
"FONT_WEIGHT": "Font weight",
@@ -1,146 +1,107 @@
@if (!hasForm()) {
<div>
<ng-content select="[empty-form]" />
</div>
} @else {
<div *ngIf="!hasForm()">
<ng-content select="[empty-form]" />
</div>
<div
*ngIf="hasForm()"
class="adf-cloud-form-container adf-cloud-form-{{ displayConfiguration?.options?.fullscreen ? 'fullscreen' : 'inline' }}-container"
[style]="formStyle"
>
<div
class="adf-cloud-form-container adf-cloud-form-{{ displayConfiguration?.options?.fullscreen ? 'fullscreen' : 'inline' }}-container"
[style]="formStyle"
>
<div
class="adf-cloud-form-content"
[class.adf-cloud-form-content-standalone-fullscreen]="displayMode === 'standalone' && displayConfiguration?.options?.fullscreen"
[class.adf-cloud-form-content-toolbar]="!!displayConfiguration?.options?.displayToolbar"
[cdkTrapFocus]="displayConfiguration?.options?.trapFocus"
cdkTrapFocusAutoCapture>
@if (displayConfiguration?.options?.displayToolbar) {
<adf-toolbar class="adf-cloud-form-toolbar">
<div class="adf-cloud-form__form-title">
<span class="adf-cloud-form__display-name" [title]="form.taskName">
{{ form.taskName }}
@if (!form.taskName) {
{{ 'FORM.FORM_RENDERER.NAMELESS_TASK' | translate }}
}
</span>
</div>
class="adf-cloud-form-content"
[class.adf-cloud-form-content-standalone-fullscreen]="displayMode === 'standalone' && displayConfiguration?.options?.fullscreen"
[class.adf-cloud-form-content-toolbar]="!!displayConfiguration?.options?.displayToolbar"
[cdkTrapFocus]="displayConfiguration?.options?.trapFocus"
cdkTrapFocusAutoCapture>
<adf-toolbar class="adf-cloud-form-toolbar" *ngIf="displayConfiguration?.options?.displayToolbar">
<div class="adf-cloud-form__form-title">
<span class="adf-cloud-form__display-name" [title]="form.taskName">
{{ form.taskName }}
<ng-container *ngIf="!form.taskName">
{{ 'FORM.FORM_RENDERER.NAMELESS_TASK' | translate }}
</ng-container>
</span>
</div>
@if (displayConfiguration?.options?.displayCloseButton) {
<adf-toolbar-divider />
<button
class="adf-cloud-form-close-button"
data-automation-id="adf-toolbar-right-back"
[attr.aria-label]="'ADF_VIEWER.ACTIONS.CLOSE' | translate"
[attr.data-automation-id]="'adf-cloud-form-close-button'"
[title]="'ADF_VIEWER.ACTIONS.CLOSE' | translate"
mat-icon-button
(click)="switchToDisplayMode()"
>
<mat-icon adf-icon="close" />
</button>
}
</adf-toolbar>
}
<mat-card
appearance="outlined"
class="adf-cloud-form-content-card"
[class.adf-cloud-form-content-card-fullscreen]="displayMode === 'fullScreen'"
[class.adf-cloud-form-content-card-fullscreen-toolbar]="displayMode === 'fullScreen' && displayConfiguration?.options?.displayToolbar"
<adf-toolbar-divider *ngIf="displayConfiguration?.options?.displayCloseButton" />
<button
*ngIf="displayConfiguration?.options?.displayCloseButton"
class="adf-cloud-form-close-button"
data-automation-id="adf-toolbar-right-back"
[attr.aria-label]="'ADF_VIEWER.ACTIONS.CLOSE' | translate"
[attr.data-automation-id]="'adf-cloud-form-close-button'"
[title]="'ADF_VIEWER.ACTIONS.CLOSE' | translate"
mat-icon-button
title="{{ 'ADF_VIEWER.ACTIONS.CLOSE' | translate }}"
(click)="switchToDisplayMode()"
>
<div class="adf-cloud-form-content-card-container">
@if (showTitle || showRefreshButton || showValidationIcon) {
<mat-card-header>
<mat-card-title>
<h4>
@if (showValidationIcon) {
<div class="adf-form-validation-button">
@if (form.isValid) {
<i id="adf-valid-form-icon" class="material-icons">check_circle</i>
} @else {
<i id="adf-invalid-form-icon" class="material-icons adf-invalid-color">error</i>
}
</div>
}
@if (!displayConfiguration?.options?.fullscreen && findDisplayConfiguration('fullScreen')) {
<div class="adf-cloud-form-fullscreen-button">
<button
mat-icon-button
(click)="switchToDisplayMode('fullScreen')"
[attr.data-automation-id]="'adf-cloud-form-fullscreen-button'"
>
<mat-icon adf-icon="fullscreen" />
</button>
</div>
}
@if (showRefreshButton) {
<div class="adf-cloud-form-reload-button" [title]="'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate">
<button mat-icon-button (click)="onRefreshClicked()" [attr.aria-label]="'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate">
<mat-icon adf-icon="refresh" />
</button>
</div>
}
@if (isTitleEnabled()) {
<span class="adf-cloud-form-title" [title]="form.taskName">
{{ form.taskName }}
@if (!form.taskName) {
{{ 'FORM.FORM_RENDERER.NAMELESS_TASK' | translate }}
}
</span>
}
</h4>
</mat-card-title>
</mat-card-header>
}
<mat-card-content class="adf-form-container-card-content">
<adf-form-renderer [formDefinition]="form" [readOnly]="readOnly" />
</mat-card-content>
@if (form.hasOutcomes() || shouldShowTabNavigation) {
<mat-card-actions class="adf-cloud-form-content-card-actions" [class.adf-has-tab-navigation]="shouldShowTabNavigation" align="end">
@if (shouldShowTabNavigation) {
<div class="adf-tab-navigation-buttons">
<button
mat-button
[title]="'FORM.BUTTON.PREVIOUS_TAB_TITLE' | translate"
[disabled]="!canNavigatePreviousTab"
(click)="navigateToPreviousTab()"
data-automation-id="tab-nav-previous-button">
<mat-icon adf-icon="keyboard_arrow_left" />
{{ 'FORM.BUTTON.PREVIOUS_TAB' | translate }}
</button>
<button
mat-button
[title]="'FORM.BUTTON.NEXT_TAB_TITLE' | translate"
[disabled]="!canNavigateNextTab"
(click)="navigateToNextTab()"
data-automation-id="tab-nav-next-button">
{{ 'FORM.BUTTON.NEXT_TAB' | translate }}
<mat-icon adf-icon="keyboard_arrow_right" iconPositionEnd />
</button>
</div>
<span class="adf-tab-navigation-divider"></span>
}
<div class="adf-cloud-form-outcome-buttons">
<ng-content select="adf-cloud-form-custom-outcomes" />
@for (outcome of form.outcomes; track outcome.name) {
@if (outcome.isVisible) {
<button
[id]="'adf-form-' + outcome.name | formatSpace"
[color]="getColorForOutcome(outcome.name)"
mat-button
[disabled]="!isOutcomeButtonEnabled(outcome)"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"
class="adf-cloud-form-custom-outcome-button"
(click)="onOutcomeClicked(outcome)"
>
{{ getCustomOutcomeButtonText(outcome) || (outcome.name | translate | uppercase) }}
</button>
}
}
<mat-icon adf-icon="close" />
</button>
</adf-toolbar>
<mat-card
appearance="outlined"
class="adf-cloud-form-content-card"
[class.adf-cloud-form-content-card-fullscreen]="displayMode === 'fullScreen'"
[class.adf-cloud-form-content-card-fullscreen-toolbar]="displayMode === 'fullScreen' && displayConfiguration?.options?.displayToolbar"
>
<div class="adf-cloud-form-content-card-container">
<mat-card-header *ngIf="showTitle || showRefreshButton || showValidationIcon">
<mat-card-title>
<h4>
<div *ngIf="showValidationIcon" class="adf-form-validation-button">
<i id="adf-valid-form-icon" class="material-icons" *ngIf="form.isValid; else no_valid_form">check_circle</i>
<ng-template #no_valid_form>
<i id="adf-invalid-form-icon" class="material-icons adf-invalid-color">error</i>
</ng-template>
</div>
</mat-card-actions>
}
</div>
</mat-card>
</div>
<div
*ngIf="!displayConfiguration?.options?.fullscreen && findDisplayConfiguration('fullScreen')"
class="adf-cloud-form-fullscreen-button"
>
<button
mat-icon-button
(click)="switchToDisplayMode('fullScreen')"
[attr.data-automation-id]="'adf-cloud-form-fullscreen-button'"
>
<mat-icon adf-icon="fullscreen" />
</button>
</div>
<div *ngIf="showRefreshButton" class="adf-cloud-form-reload-button" [title]="'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate">
<button mat-icon-button (click)="onRefreshClicked()" [attr.aria-label]="'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate">
<mat-icon adf-icon="refresh" />
</button>
</div>
<span *ngIf="isTitleEnabled()" class="adf-cloud-form-title" [title]="form.taskName"
>{{ form.taskName }}
<ng-container *ngIf="!form.taskName">
{{ 'FORM.FORM_RENDERER.NAMELESS_TASK' | translate }}
</ng-container>
</span>
</h4>
</mat-card-title>
</mat-card-header>
<mat-card-content class="adf-form-container-card-content">
<adf-form-renderer [formDefinition]="form" [readOnly]="readOnly" />
</mat-card-content>
<mat-card-actions *ngIf="form.hasOutcomes()" class="adf-cloud-form-content-card-actions" align="end">
<ng-content select="adf-cloud-form-custom-outcomes" />
<ng-container *ngFor="let outcome of form.outcomes">
<button
*ngIf="outcome.isVisible"
[id]="'adf-form-' + outcome.name | formatSpace"
[color]="getColorForOutcome(outcome.name)"
mat-button
[disabled]="!isOutcomeButtonEnabled(outcome)"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"
class="adf-cloud-form-custom-outcome-button"
(click)="onOutcomeClicked(outcome)"
>
{{ getCustomOutcomeButtonText(outcome) || (outcome.name | translate | uppercase) }}
</button>
</ng-container>
</mat-card-actions>
</div>
</mat-card>
</div>
}
</div>
@@ -90,6 +90,7 @@
padding-bottom: 2rem;
overflow-y: auto;
position: static;
height: 70%;
border-radius: 14px;
&-fullscreen {
@@ -137,40 +138,3 @@
white-space: normal;
}
}
.adf-cloud-form-content-card-actions.adf-has-tab-navigation {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
@media (width <= 600px) {
flex-direction: column;
align-items: stretch;
}
}
.adf-tab-navigation-buttons {
display: flex;
align-items: center;
gap: 4px;
@media (width <= 600px) {
justify-content: center;
}
}
.adf-tab-navigation-divider {
width: 1px;
height: 24px;
background-color: var(--mat-sys-outline-variant, rgba(0, 0, 0, 0.12));
@media (width <= 600px) {
display: none;
}
}
.adf-cloud-form-outcome-buttons {
display: flex;
align-items: center;
}
@@ -46,7 +46,7 @@ import { MatDialog } from '@angular/material/dialog';
import { MatDialogHarness } from '@angular/material/dialog/testing';
import { By } from '@angular/platform-browser';
import { TranslateLoader, TranslateService, provideTranslateService, provideTranslateLoader } from '@ngx-translate/core';
import { BehaviorSubject, firstValueFrom, Observable, of, throwError } from 'rxjs';
import { firstValueFrom, Observable, of, throwError } from 'rxjs';
import {
cloudFormMock,
conditionalUploadWidgetsMock,
@@ -59,7 +59,7 @@ import {
import { FormCloudRepresentation } from '../models/form-cloud-representation.model';
import { FormCloudService } from '../services/form-cloud.service';
import { DisplayModeService } from '../services/display-mode.service';
import { ADF_FORM_TAB_NAV_ENABLED, FORM_CLOUD_FIELD_VALIDATORS_TOKEN, FormCloudComponent } from './form-cloud.component';
import { FORM_CLOUD_FIELD_VALIDATORS_TOKEN, FormCloudComponent } from './form-cloud.component';
import { MatButtonHarness } from '@angular/material/button/testing';
import { FormCloudDisplayMode } from '../../services/form-fields.interfaces';
import { CloudFormRenderingService } from './cloud-form-rendering.service';
@@ -1701,106 +1701,6 @@ describe('FormCloudComponent', () => {
expect(outcomeButton.nativeElement.textContent.trim()).toBe('COMPLETE');
});
});
describe('Tab navigation', () => {
const tabbedFormJson = {
id: 'tabbed-form',
name: 'TabbedForm',
tabs: [
{ id: 'tab1', title: 'Tab 1' },
{ id: 'tab2', title: 'Tab 2' },
{ id: 'tab3', title: 'Tab 3' }
],
fields: [
{
id: 'container1',
type: 'container',
tab: 'tab1',
numberOfColumns: 1,
fields: { 1: [{ id: 'text1', type: 'text', name: 'Text 1' }] }
},
{
id: 'container2',
type: 'container',
tab: 'tab2',
numberOfColumns: 1,
fields: { 1: [{ id: 'text2', type: 'text', name: 'Text 2' }] }
},
{
id: 'container3',
type: 'container',
tab: 'tab3',
numberOfColumns: 1,
fields: { 1: [{ id: 'text3', type: 'text', name: 'Text 3' }] }
}
],
outcomes: [],
showBottomTabNavButtons: true
};
describe('shouldShowTabNavigation', () => {
it('should return false when form json showBottomTabNavButtons is false', () => {
formComponent.form = new FormModel({ ...tabbedFormJson, showBottomTabNavButtons: false });
fixture.detectChanges();
expect(formComponent.shouldShowTabNavigation).toBeFalse();
});
it('should return false when form json showBottomTabNavButtons is undefined', () => {
const { showBottomTabNavButtons: _unused, ...jsonWithoutFlag } = tabbedFormJson;
formComponent.form = new FormModel(jsonWithoutFlag);
fixture.detectChanges();
expect(formComponent.shouldShowTabNavigation).toBeFalse();
});
it('should return false when form has only one visible tab', () => {
const singleTabJson = {
...tabbedFormJson,
tabs: [{ id: 'tab1', title: 'Tab 1' }],
fields: [tabbedFormJson.fields[0]]
};
formComponent.form = new FormModel(singleTabJson);
fixture.detectChanges();
expect(formComponent.shouldShowTabNavigation).toBeFalse();
});
it('should return true when form json showBottomTabNavButtons is true and there is more than one visible tab', () => {
formComponent.form = new FormModel(tabbedFormJson);
fixture.detectChanges();
expect(formComponent.shouldShowTabNavigation).toBeTrue();
});
});
describe('template rendering', () => {
it('should not render tab navigation buttons when shouldShowTabNavigation is false', () => {
formComponent.form = new FormModel({ ...tabbedFormJson, showBottomTabNavButtons: false });
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.adf-tab-navigation-buttons'))).toBeNull();
expect(fixture.debugElement.query(By.css('[data-automation-id="tab-nav-previous-button"]'))).toBeNull();
expect(fixture.debugElement.query(By.css('[data-automation-id="tab-nav-next-button"]'))).toBeNull();
});
it('should render Previous and Next tab navigation buttons when shouldShowTabNavigation is true', () => {
formComponent.form = new FormModel(tabbedFormJson);
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.adf-tab-navigation-buttons'))).toBeTruthy();
expect(fixture.debugElement.query(By.css('[data-automation-id="tab-nav-previous-button"]'))).toBeTruthy();
expect(fixture.debugElement.query(By.css('[data-automation-id="tab-nav-next-button"]'))).toBeTruthy();
});
it('should render the form actions container when only tab nav exists and the form has no outcomes', () => {
formComponent.form = new FormModel({ ...tabbedFormJson, outcomes: [] });
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.adf-cloud-form-content-card-actions'))).toBeTruthy();
});
});
});
});
describe('Multilingual Form', () => {
@@ -2091,109 +1991,3 @@ describe('retrieve metadata on submit', () => {
expect(formComponent['formCloudService'].completeTaskForm).toHaveBeenCalled();
});
});
describe('FormCloudComponent - ADF_FORM_TAB_NAV_ENABLED token', () => {
let formComponent: FormCloudComponent;
let fixture: ComponentFixture<FormCloudComponent>;
const tabbedFormJson = {
id: 'tabbed-form',
name: 'TabbedForm',
tabs: [
{ id: 'tab1', title: 'Tab 1' },
{ id: 'tab2', title: 'Tab 2' }
],
fields: [
{
id: 'container1',
type: 'container',
tab: 'tab1',
numberOfColumns: 1,
fields: { 1: [{ id: 'text1', type: 'text', name: 'Text 1' }] }
},
{
id: 'container2',
type: 'container',
tab: 'tab2',
numberOfColumns: 1,
fields: { 1: [{ id: 'text2', type: 'text', name: 'Text 2' }] }
}
],
outcomes: [],
showBottomTabNavButtons: true
};
const createFixture = (tokenValue: any) => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [NoopTranslateModule, NoopAuthModule, FormCloudComponent],
providers: [
{ provide: VersionCompatibilityService, useValue: {} },
{ provide: FormRenderingService, useClass: CloudFormRenderingService },
{ provide: ADF_FORM_TAB_NAV_ENABLED, useValue: tokenValue }
]
});
const apiService = TestBed.inject(AlfrescoApiService);
spyOn(apiService, 'getInstance').and.returnValue(mockOauth2Auth);
fixture = TestBed.createComponent(FormCloudComponent);
formComponent = fixture.componentInstance;
};
it('should render tab navigation buttons when token is absent and form model opts in', () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [NoopTranslateModule, NoopAuthModule, FormCloudComponent],
providers: [
{ provide: VersionCompatibilityService, useValue: {} },
{ provide: FormRenderingService, useClass: CloudFormRenderingService }
]
});
const apiService = TestBed.inject(AlfrescoApiService);
spyOn(apiService, 'getInstance').and.returnValue(mockOauth2Auth);
fixture = TestBed.createComponent(FormCloudComponent);
formComponent = fixture.componentInstance;
formComponent.form = new FormModel(tabbedFormJson);
fixture.detectChanges();
expect(formComponent.shouldShowTabNavigation).toBeTrue();
});
it('should hide tab navigation buttons when token resolves to static false even if form opts in', () => {
createFixture(false);
formComponent.form = new FormModel(tabbedFormJson);
fixture.detectChanges();
expect(formComponent.shouldShowTabNavigation).toBeFalse();
});
it('should show tab navigation buttons when token resolves to static true and form opts in', () => {
createFixture(true);
formComponent.form = new FormModel(tabbedFormJson);
fixture.detectChanges();
expect(formComponent.shouldShowTabNavigation).toBeTrue();
});
it('should react to observable token emissions', () => {
const subject = new BehaviorSubject<boolean>(false);
createFixture(subject);
formComponent.form = new FormModel(tabbedFormJson);
fixture.detectChanges();
expect(formComponent.shouldShowTabNavigation).toBeFalse();
subject.next(true);
fixture.detectChanges();
expect(formComponent.shouldShowTabNavigation).toBeTrue();
subject.next(false);
fixture.detectChanges();
expect(formComponent.shouldShowTabNavigation).toBeFalse();
});
});
@@ -27,10 +27,9 @@ import {
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild
SimpleChanges
} from '@angular/core';
import { forkJoin, isObservable, Observable, of, Subscription } from 'rxjs';
import { forkJoin, Observable, of, Subscription } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
import {
ConfirmDialogComponent,
@@ -61,19 +60,18 @@ import { FormCloudDisplayMode, FormCloudDisplayModeConfiguration } from '../../s
import { FormCloudSpinnerService } from '../services/spinner/form-cloud-spinner.service';
import { DisplayModeService } from '../services/display-mode.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { UpperCasePipe } from '@angular/common';
import { CommonModule } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { A11yModule } from '@angular/cdk/a11y';
export const FORM_CLOUD_FIELD_VALIDATORS_TOKEN = new InjectionToken<FormFieldValidator[]>('FORM_CLOUD_FIELD_VALIDATORS_TOKEN');
export const ADF_FORM_TAB_NAV_ENABLED = new InjectionToken<Observable<boolean> | boolean>('ADF_FORM_TAB_NAV_ENABLED');
@Component({
selector: 'adf-cloud-form',
imports: [
UpperCasePipe,
CommonModule,
TranslatePipe,
FormatSpacePipe,
MatButtonModule,
@@ -180,24 +178,6 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
displayConfiguration: FormCloudDisplayModeConfiguration = DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0];
style: string = '';
@ViewChild(FormRendererComponent)
formRenderer!: FormRendererComponent<any>;
private tabNavEnabledByHost = true;
private currentTabIndex = 0;
get shouldShowTabNavigation(): boolean {
return this.tabNavEnabledByHost && this.currentForm?.json?.showBottomTabNavButtons === true && this.visibleTabCount > 1;
}
get canNavigatePreviousTab(): boolean {
return this.currentTabIndex > 0;
}
get canNavigateNextTab(): boolean {
return this.currentTabIndex < this.visibleTabCount - 1;
}
protected formCloudService = inject(FormCloudService);
protected formService = inject(FormService);
protected visibilityService = inject(WidgetVisibilityService);
@@ -208,31 +188,8 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
private readonly destroyRef = inject(DestroyRef);
private get currentForm(): FormModel | undefined {
return super.form;
}
private get visibleTabCount(): number {
return this.currentForm?.tabs?.filter((tab) => tab.isVisible).length ?? 0;
}
navigateToPreviousTab(): void {
if (this.canNavigatePreviousTab) {
this.currentTabIndex--;
this.formRenderer?.navigateToPreviousTab();
}
}
navigateToNextTab(): void {
if (this.canNavigateNextTab) {
this.currentTabIndex++;
this.formRenderer?.navigateToNextTab();
}
}
constructor() {
const injectedFieldValidators = inject(FORM_CLOUD_FIELD_VALIDATORS_TOKEN, { optional: true });
const tabNavEnabledToken = inject(ADF_FORM_TAB_NAV_ENABLED, { optional: true });
super();
this.loadInjectedFieldValidators(injectedFieldValidators);
@@ -240,17 +197,6 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
this.id = uuidGeneration();
if (tabNavEnabledToken != null) {
if (isObservable(tabNavEnabledToken)) {
tabNavEnabledToken.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled) => {
this.tabNavEnabledByHost = enabled ?? false;
this.changeDetector.markForCheck();
});
} else {
this.tabNavEnabledByHost = tabNavEnabledToken;
}
}
this.formService.formContentClicked.pipe(takeUntilDestroyed()).subscribe((content) => {
if (content instanceof UploadWidgetContentLinkModel) {
this.form.setNodeIdValueForViewersLinkedToUploadWidget(content);
@@ -566,8 +512,6 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
}
protected onFormLoaded(form: FormModel) {
this.currentTabIndex = 0;
if (form) {
this.displayModeConfigurations = this.displayModeService.getDisplayModeConfigurations(this.displayModeConfigurations);
this.displayMode = this.displayModeService.switchToDisplayMode(