mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-44098 Bottom tab navigation buttons (#11834)
* feat: enable tab navigation buttons * feat: better strings and button positioning * feat: reduce overall padding * feat: adjust buttons based on ux mockups * test: add some tests * fix: build error * fix: rename property to showBottomTabNavButtons for clarity * feat: simplify changes * test: copilot suggestion * fix: prevent upstream visual regression issues. * fix: copilot suggestions * fix: height change
This commit is contained in:
@@ -86,6 +86,30 @@ const expectElementToBeValid = (testingUtils: UnitTestingUtils, fieldId: string)
|
|||||||
expect(invalidElementContainer).toBeFalsy();
|
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', () => {
|
describe('Form Renderer Component', () => {
|
||||||
let formRendererComponent: FormRendererComponent<any>;
|
let formRendererComponent: FormRendererComponent<any>;
|
||||||
let fixture: ComponentFixture<FormRendererComponent<any>>;
|
let fixture: ComponentFixture<FormRendererComponent<any>>;
|
||||||
@@ -909,6 +933,117 @@ 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', () => {
|
describe('Repeatable section', () => {
|
||||||
const repeatableSectionField = new FormFieldModel(new FormModel(), {
|
const repeatableSectionField = new FormFieldModel(new FormModel(), {
|
||||||
id: 'RepeatableSection0tbw2y',
|
id: 'RepeatableSection0tbw2y',
|
||||||
|
|||||||
@@ -16,12 +16,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
|
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
|
||||||
import { ChangeDetectorRef, Component, DestroyRef, inject, Injector, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
DestroyRef,
|
||||||
|
inject,
|
||||||
|
Injector,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
signal,
|
||||||
|
ViewChild,
|
||||||
|
ViewEncapsulation
|
||||||
|
} from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { filter } from 'rxjs';
|
import { filter, Subscription } from 'rxjs';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
import { MatTabGroup, MatTabsModule } from '@angular/material/tabs';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import { FormRulesManager, formRulesManagerFactory } from '../models/form-rules.model';
|
import { FormRulesManager, formRulesManagerFactory } from '../models/form-rules.model';
|
||||||
import { FormService } from '../services/form.service';
|
import { FormService } from '../services/form.service';
|
||||||
@@ -82,11 +94,54 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy {
|
|||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
formDefinition: FormModel;
|
set formDefinition(formDefinition: FormModel) {
|
||||||
|
this._formDefinition = formDefinition;
|
||||||
|
this.syncCurrentTabIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
get formDefinition(): FormModel {
|
||||||
|
return this._formDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
readOnly = false;
|
readOnly = false;
|
||||||
|
|
||||||
|
@ViewChild(MatTabGroup)
|
||||||
|
set tabGroup(tabGroup: MatTabGroup | undefined) {
|
||||||
|
this.tabGroupSelectionSubscription?.unsubscribe();
|
||||||
|
this._tabGroup = tabGroup;
|
||||||
|
|
||||||
|
if (tabGroup) {
|
||||||
|
this.syncCurrentTabIndex(tabGroup.selectedIndex);
|
||||||
|
this.tabGroupSelectionSubscription = tabGroup.selectedIndexChange.subscribe((index) => this.syncCurrentTabIndex(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get tabGroup(): MatTabGroup {
|
||||||
|
return this._tabGroup as MatTabGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly currentTabIndex = signal(0);
|
||||||
|
private _formDefinition: FormModel;
|
||||||
|
private _tabGroup?: MatTabGroup;
|
||||||
|
private tabGroupSelectionSubscription?: Subscription;
|
||||||
|
|
||||||
|
get canNavigateNext(): boolean {
|
||||||
|
return this.currentTabIndex() < this.visibleTabCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canNavigatePrevious(): boolean {
|
||||||
|
return this.currentTabIndex() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get visibleTabCount(): number {
|
||||||
|
return this.visibleTabs().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedTabIndex(): number {
|
||||||
|
return this.currentTabIndex();
|
||||||
|
}
|
||||||
|
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
|
|
||||||
fields: FormFieldModel[];
|
fields: FormFieldModel[];
|
||||||
@@ -106,15 +161,37 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
this.tabGroupSelectionSubscription?.unsubscribe();
|
||||||
this.formRulesManager.destroy();
|
this.formRulesManager.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
hasTabs(): boolean {
|
hasTabs(): boolean {
|
||||||
return this.formDefinition.tabs && this.formDefinition.tabs.length > 0;
|
return this.formDefinition?.tabs && this.formDefinition.tabs.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
visibleTabs(): TabModel[] {
|
visibleTabs(): TabModel[] {
|
||||||
return this.formDefinition.tabs.filter((tab) => tab.isVisible);
|
return this.formDefinition?.tabs?.filter((tab) => tab.isVisible) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToNextTab(): void {
|
||||||
|
if (this.tabGroup && this.canNavigateNext) {
|
||||||
|
this.tabGroup.selectedIndex = (this.tabGroup.selectedIndex ?? 0) + 1;
|
||||||
|
this.syncCurrentTabIndex(this.tabGroup.selectedIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToPreviousTab(): void {
|
||||||
|
if (this.tabGroup && this.canNavigatePrevious) {
|
||||||
|
this.tabGroup.selectedIndex = (this.tabGroup.selectedIndex ?? 0) - 1;
|
||||||
|
this.syncCurrentTabIndex(this.tabGroup.selectedIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncCurrentTabIndex(index = this.tabGroup?.selectedIndex): void {
|
||||||
|
const maxTabIndex = Math.max(this.visibleTabCount - 1, 0);
|
||||||
|
const currentIndex = index ?? 0;
|
||||||
|
|
||||||
|
this.currentTabIndex.set(Math.min(Math.max(currentIndex, 0), maxTabIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
getNumberOfColumns(content: ContainerModel): number {
|
getNumberOfColumns(content: ContainerModel): number {
|
||||||
|
|||||||
@@ -87,6 +87,12 @@
|
|||||||
"NO_LABEL": "Cancel"
|
"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": {
|
"FIELD_STYLE": {
|
||||||
"FONT_SIZE": "Font size",
|
"FONT_SIZE": "Font size",
|
||||||
"FONT_WEIGHT": "Font weight",
|
"FONT_WEIGHT": "Font weight",
|
||||||
|
|||||||
@@ -1,107 +1,146 @@
|
|||||||
<div *ngIf="!hasForm()">
|
@if (!hasForm()) {
|
||||||
<ng-content select="[empty-form]" />
|
<div>
|
||||||
</div>
|
<ng-content select="[empty-form]" />
|
||||||
|
|
||||||
<div
|
|
||||||
*ngIf="hasForm()"
|
|
||||||
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>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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()"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<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>
|
||||||
</div>
|
} @else {
|
||||||
|
<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>
|
||||||
|
|
||||||
|
@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"
|
||||||
|
>
|
||||||
|
<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" aria-hidden="true"></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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</mat-card-actions>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@use '../../mat-selectors' as ms;
|
@use '../../mat-selectors' as ms;
|
||||||
|
@use '../../flex' as flex;
|
||||||
|
|
||||||
/* cspell: disable-next-line */
|
/* cspell: disable-next-line */
|
||||||
/* stylelint-disable scss/at-extend-no-missing-placeholder */
|
/* stylelint-disable scss/at-extend-no-missing-placeholder */
|
||||||
@@ -138,3 +139,40 @@
|
|||||||
white-space: normal;
|
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;
|
||||||
|
|
||||||
|
@include flex.layout-bp(lt-sm) {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.adf-tab-navigation-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
@include flex.layout-bp(lt-sm) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.adf-tab-navigation-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: var(--mat-sys-outline-variant, rgba(0, 0, 0, 0.12));
|
||||||
|
|
||||||
|
@include flex.layout-bp(lt-sm) {
|
||||||
|
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 { MatDialogHarness } from '@angular/material/dialog/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { TranslateLoader, TranslateService, provideTranslateService, provideTranslateLoader } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateService, provideTranslateService, provideTranslateLoader } from '@ngx-translate/core';
|
||||||
import { firstValueFrom, Observable, of, throwError } from 'rxjs';
|
import { BehaviorSubject, firstValueFrom, Observable, of, throwError } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
cloudFormMock,
|
cloudFormMock,
|
||||||
conditionalUploadWidgetsMock,
|
conditionalUploadWidgetsMock,
|
||||||
@@ -59,7 +59,7 @@ import {
|
|||||||
import { FormCloudRepresentation } from '../models/form-cloud-representation.model';
|
import { FormCloudRepresentation } from '../models/form-cloud-representation.model';
|
||||||
import { FormCloudService } from '../services/form-cloud.service';
|
import { FormCloudService } from '../services/form-cloud.service';
|
||||||
import { DisplayModeService } from '../services/display-mode.service';
|
import { DisplayModeService } from '../services/display-mode.service';
|
||||||
import { FORM_CLOUD_FIELD_VALIDATORS_TOKEN, FormCloudComponent } from './form-cloud.component';
|
import { ADF_FORM_TAB_NAV_ENABLED, FORM_CLOUD_FIELD_VALIDATORS_TOKEN, FormCloudComponent } from './form-cloud.component';
|
||||||
import { MatButtonHarness } from '@angular/material/button/testing';
|
import { MatButtonHarness } from '@angular/material/button/testing';
|
||||||
import { FormCloudDisplayMode } from '../../services/form-fields.interfaces';
|
import { FormCloudDisplayMode } from '../../services/form-fields.interfaces';
|
||||||
import { CloudFormRenderingService } from './cloud-form-rendering.service';
|
import { CloudFormRenderingService } from './cloud-form-rendering.service';
|
||||||
@@ -1701,6 +1701,106 @@ describe('FormCloudComponent', () => {
|
|||||||
expect(outcomeButton.nativeElement.textContent.trim()).toBe('COMPLETE');
|
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', () => {
|
describe('Multilingual Form', () => {
|
||||||
@@ -1991,3 +2091,109 @@ describe('retrieve metadata on submit', () => {
|
|||||||
expect(formComponent['formCloudService'].completeTaskForm).toHaveBeenCalled();
|
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,9 +27,10 @@ import {
|
|||||||
OnChanges,
|
OnChanges,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
SimpleChanges
|
SimpleChanges,
|
||||||
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { forkJoin, Observable, of, Subscription } from 'rxjs';
|
import { forkJoin, isObservable, Observable, of, Subscription } from 'rxjs';
|
||||||
import { filter, map, switchMap } from 'rxjs/operators';
|
import { filter, map, switchMap } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
ConfirmDialogComponent,
|
ConfirmDialogComponent,
|
||||||
@@ -60,18 +61,19 @@ import { FormCloudDisplayMode, FormCloudDisplayModeConfiguration } from '../../s
|
|||||||
import { FormCloudSpinnerService } from '../services/spinner/form-cloud-spinner.service';
|
import { FormCloudSpinnerService } from '../services/spinner/form-cloud-spinner.service';
|
||||||
import { DisplayModeService } from '../services/display-mode.service';
|
import { DisplayModeService } from '../services/display-mode.service';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { CommonModule } from '@angular/common';
|
import { UpperCasePipe } from '@angular/common';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { A11yModule } from '@angular/cdk/a11y';
|
import { A11yModule } from '@angular/cdk/a11y';
|
||||||
|
|
||||||
export const FORM_CLOUD_FIELD_VALIDATORS_TOKEN = new InjectionToken<FormFieldValidator[]>('FORM_CLOUD_FIELD_VALIDATORS_TOKEN');
|
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({
|
@Component({
|
||||||
selector: 'adf-cloud-form',
|
selector: 'adf-cloud-form',
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
UpperCasePipe,
|
||||||
TranslatePipe,
|
TranslatePipe,
|
||||||
FormatSpacePipe,
|
FormatSpacePipe,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
@@ -178,6 +180,23 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
|||||||
displayConfiguration: FormCloudDisplayModeConfiguration = DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0];
|
displayConfiguration: FormCloudDisplayModeConfiguration = DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0];
|
||||||
style: string = '';
|
style: string = '';
|
||||||
|
|
||||||
|
@ViewChild(FormRendererComponent)
|
||||||
|
formRenderer!: FormRendererComponent<any>;
|
||||||
|
|
||||||
|
private tabNavEnabledByHost = true;
|
||||||
|
|
||||||
|
get shouldShowTabNavigation(): boolean {
|
||||||
|
return this.tabNavEnabledByHost && this.currentForm?.json?.showBottomTabNavButtons === true && this.visibleTabCount > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canNavigatePreviousTab(): boolean {
|
||||||
|
return this.formRenderer?.canNavigatePrevious ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canNavigateNextTab(): boolean {
|
||||||
|
return this.formRenderer?.canNavigateNext ?? this.visibleTabCount > 1;
|
||||||
|
}
|
||||||
|
|
||||||
protected formCloudService = inject(FormCloudService);
|
protected formCloudService = inject(FormCloudService);
|
||||||
protected formService = inject(FormService);
|
protected formService = inject(FormService);
|
||||||
protected visibilityService = inject(WidgetVisibilityService);
|
protected visibilityService = inject(WidgetVisibilityService);
|
||||||
@@ -188,8 +207,29 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
|||||||
|
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
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.formRenderer?.canNavigatePrevious) {
|
||||||
|
this.formRenderer?.navigateToPreviousTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToNextTab(): void {
|
||||||
|
if (this.formRenderer?.canNavigateNext) {
|
||||||
|
this.formRenderer?.navigateToNextTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const injectedFieldValidators = inject(FORM_CLOUD_FIELD_VALIDATORS_TOKEN, { optional: true });
|
const injectedFieldValidators = inject(FORM_CLOUD_FIELD_VALIDATORS_TOKEN, { optional: true });
|
||||||
|
const tabNavEnabledToken = inject(ADF_FORM_TAB_NAV_ENABLED, { optional: true });
|
||||||
|
|
||||||
super();
|
super();
|
||||||
this.loadInjectedFieldValidators(injectedFieldValidators);
|
this.loadInjectedFieldValidators(injectedFieldValidators);
|
||||||
@@ -197,6 +237,17 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
|||||||
|
|
||||||
this.id = uuidGeneration();
|
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) => {
|
this.formService.formContentClicked.pipe(takeUntilDestroyed()).subscribe((content) => {
|
||||||
if (content instanceof UploadWidgetContentLinkModel) {
|
if (content instanceof UploadWidgetContentLinkModel) {
|
||||||
this.form.setNodeIdValueForViewersLinkedToUploadWidget(content);
|
this.form.setNodeIdValueForViewersLinkedToUploadWidget(content);
|
||||||
|
|||||||
Reference in New Issue
Block a user