AAE-20480 Full Screen User Task Forms (#9341)

* AAE-20480 Full Screen User Task Forms

* AAE-20480 Attend review comments

* AAE-20480 Remove leftovers

* AAE-20480 Enable changing the display mode from task-form-cloud

* AAE-20480 Fix fullscreen mode header

* AAE-20480 Fix review comments

* AAE-20480 Allow hiding the full screen toolbar by configuration

* AAE-20480 Add review comments

* AAE-20480 Create display mode service
This commit is contained in:
Pablo Martinez
2024-03-05 15:10:50 +01:00
committed by GitHub
parent 530899fd51
commit dc88ef8ce7
15 changed files with 969 additions and 55 deletions

View File

@@ -95,6 +95,7 @@ The template defined inside `empty-form` will be shown when no form definition i
| showTitle | `boolean` | true | Toggle rendering of the form title. | | showTitle | `boolean` | true | Toggle rendering of the form title. |
| showValidationIcon | `boolean` | true | Toggle rendering of the validation icon next to the form title. | | showValidationIcon | `boolean` | true | Toggle rendering of the validation icon next to the form title. |
| taskId | `string` | | Task id to fetch corresponding form and values. | | taskId | `string` | | Task id to fetch corresponding form and values. |
| displayModeConfigurations | [`FormCloudDisplayModeConfiguration`](../../../lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts)`[]` | | The available display configurations for the form |
### Events ### Events
@@ -108,6 +109,8 @@ The template defined inside `empty-form` will be shown when no form definition i
| formError | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormFieldModel`](../../core/models/form-field.model.md)`[]>` | Emitted when the supplied form values have a validation error. | | formError | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormFieldModel`](../../core/models/form-field.model.md)`[]>` | Emitted when the supplied form values have a validation error. |
| formLoaded | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../../lib/core/src/lib/form/components/widgets/core/form.model.ts)`>` | Emitted when the form is loaded or reloaded. | | formLoaded | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../../lib/core/src/lib/form/components/widgets/core/form.model.ts)`>` | Emitted when the form is loaded or reloaded. |
| formSaved | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../../lib/core/src/lib/form/components/widgets/core/form.model.ts)`>` | Emitted when the form is submitted with the `Save` or custom outcomes. | | formSaved | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../../lib/core/src/lib/form/components/widgets/core/form.model.ts)`>` | Emitted when the form is submitted with the `Save` or custom outcomes. |
| displayModeOn | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormCloudDisplayModeConfiguration`](../../../lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts)`>` | Emitted when a display mode configuration is turned on. |
| displayModeOff | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormCloudDisplayModeConfiguration`](../../../lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts)`>` | Emitted when a display mode configuration is turned off. |
## Details ## Details
@@ -160,6 +163,41 @@ For an existing Task both the form and its values will be fetched and displayed.
In this case, only the form definition will be fetched. In this case, only the form definition will be fetched.
#### Enabling fullscreen display for the form
Provide a `displayModeConfiguration` array object containing the fullscreen configuration. You can use the configuration provided in the [`DisplayModeService`](../../../lib/process-services-cloud/src/lib/form/services/display-mode.service.ts) as a static member `DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS`, or configure your own if you want to customise the options for the fullscreen display mode.
**MyView.component.html**
```html
<button (click)="adfCloudForm.switchToDisplayMode('fullScreen')">Full screen</button>
<adf-cloud-form #adfCloudForm
[appName]="appName"
[taskId]="selectedTask?.id"
[showTitle]="false"
[showRefreshButton]="false"
[showValidationIcon]="false"
[displayModeConfigurations]="displayConfigurations">
</adf-cloud-form>
```
**MyView.component.ts**
```ts
import { DisplayModeService } from '@alfresco/adf-process-services-cloud';
export class MyView {
get displayConfigurations() {
return DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS;
}
}
```
When the `displayModeConfigurations` contains the configuration for the fullscreen display, in the header of the form, a button to switch to fullscreen is displayed. Keep in mind that the header of the form is visible only if any of the parameters `showTitle`, `showRefreshButton`or `showValidationIcon` is `true`, but it is also possible to switch to the fullscreen display using a button that you can place wherever you want as shown in the previous example.
### Controlling outcome execution behaviour ### Controlling outcome execution behaviour
In unusual circumstances, you may need to take complete control of form outcome execution. In unusual circumstances, you may need to take complete control of form outcome execution.

View File

@@ -43,6 +43,7 @@ Save and Complete buttons get disabled when at least one of the form's inputs ar
| showTitle | `boolean` | true | Toggle rendering of the form title. | | showTitle | `boolean` | true | Toggle rendering of the form title. |
| showValidationIcon | `boolean` | true | Toggle rendering of the `Validation` icon. | | showValidationIcon | `boolean` | true | Toggle rendering of the `Validation` icon. |
| taskId | `string` | | Task id to fetch corresponding form and values. | | taskId | `string` | | Task id to fetch corresponding form and values. |
| displayModeConfigurations | [`FormCloudDisplayModeConfiguration`](../../../lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts)`[]` | | The available display configurations for the form |
### Events ### Events
@@ -58,6 +59,43 @@ Save and Complete buttons get disabled when at least one of the form's inputs ar
| taskClaimed | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the task is claimed. | | taskClaimed | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the task is claimed. |
| taskCompleted | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the task is completed. | | taskCompleted | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the task is completed. |
| taskUnclaimed | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the task is unclaimed. | | taskUnclaimed | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the task is unclaimed. |
| displayModeOn | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormCloudDisplayModeConfiguration`](../../../lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts)`>` | Emitted when a display mode configuration is turned on. |
| displayModeOff | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormCloudDisplayModeConfiguration`](../../../lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts)`>` | Emitted when a display mode configuration is turned off. |
#### Enabling fullscreen display for the form of the task
Provide a `displayModeConfiguration` array object containing the fullscreen configuration. You can use the configuration provided in the [`DisplayModeService`](../../../lib/process-services-cloud/src/lib/form/services/display-mode.service.ts) as a static member `DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS`, or configure your own if you want to customise the options for the fullscreen display mode.
**MyView.component.html**
```html
<button (click)="adfCloudTaskForm.switchToDisplayMode('fullScreen')">Full screen</button>
<adf-cloud-task-form #adfCloudTaskForm
[appName]="appName"
[taskId]="selectedTask?.id"
[showTitle]="false"
[showRefreshButton]="false"
[showValidationIcon]="false"
[displayModeConfigurations]="displayConfigurations">
</adf-cloud-task-form>
```
**MyView.component.ts**
```ts
import { DisplayModeService } from '@alfresco/adf-process-services-cloud';
export class MyView {
get displayConfigurations() {
return DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS;
}
}
```
When the `displayModeConfigurations` contains the configuration for the fullscreen display, in the header of the form, a button to switch to fullscreen is displayed. Keep in mind that the header of the form is visible only if any of the parameters `showTitle`, `showRefreshButton`or `showValidationIcon` is `true`, but it is also possible to switch to the fullscreen display using a button that you can place wherever you want as shown in the previous example.
## See also ## See also

View File

@@ -57,6 +57,7 @@ export interface FormRepresentationModel {
[key: string]: any; [key: string]: any;
fields?: any[]; fields?: any[];
}; };
displayMode: string;
} }
export class FormModel implements ProcessFormModel { export class FormModel implements ProcessFormModel {
static UNSET_TASK_NAME: string = 'Nameless task'; static UNSET_TASK_NAME: string = 'Nameless task';
@@ -72,6 +73,7 @@ export class FormModel implements ProcessFormModel {
readonly processDefinitionId: string; readonly processDefinitionId: string;
readonly selectedOutcome: string; readonly selectedOutcome: string;
readonly enableFixedSpace: boolean; readonly enableFixedSpace: boolean;
readonly displayMode: any;
fieldsCache: FormFieldModel[] = []; fieldsCache: FormFieldModel[] = [];
@@ -113,6 +115,7 @@ export class FormModel implements ProcessFormModel {
this.processVariables = json.processVariables || []; this.processVariables = json.processVariables || [];
this.enableFixedSpace = enableFixedSpace; this.enableFixedSpace = enableFixedSpace;
this.confirmMessage = json.confirmMessage || {}; this.confirmMessage = json.confirmMessage || {};
this.displayMode = json.displayMode;
this.tabs = (json.tabs || []).map((tabJson) => new TabModel(this, tabJson)); this.tabs = (json.tabs || []).map((tabJson) => new TabModel(this, tabJson));

View File

@@ -3,47 +3,81 @@
</ng-content> </ng-content>
</div> </div>
<div *ngIf="hasForm()" class="adf-form-container"> <div *ngIf="hasForm()" class="adf-cloud-form-container adf-cloud-form-{{displayMode?.toLowerCase() || 'inline'}}-container">
<mat-card class="adf-form-container-card"> <div class="adf-cloud-form-content"
<mat-card-header *ngIf="showTitle || showRefreshButton || showValidationIcon"> [cdkTrapFocus]="displayMode === 'fullScreen'"
<mat-card-title> cdkTrapFocusAutoCapture>
<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="showRefreshButton" class="adf-form-reload-button">
<button mat-icon-button (click)="onRefreshClicked()">
<mat-icon>refresh</mat-icon>
</button>
</div>
<span *ngIf="isTitleEnabled()" class="adf-form-title" [matTooltip]="form.taskName">
{{form.taskName}}
<ng-container *ngIf="!form.taskName">
{{'FORM.FORM_RENDERER.NAMELESS_TASK' | translate}}
</ng-container>
</span>
</h4> <adf-toolbar class="adf-cloud-form-toolbar" *ngIf="displayMode === 'fullScreen' && findDisplayConfiguration('fullScreen')?.options?.displayToolbar">
</mat-card-title> <div class="adf-cloud-form__form-title">
</mat-card-header> <span class="adf-cloud-form__display-name" [matTooltip]="form.taskName">
<mat-card-content> {{form.taskName}}
<adf-form-renderer [formDefinition]="form"> <ng-container *ngIf="!form.taskName">
</adf-form-renderer> {{'FORM.FORM_RENDERER.NAMELESS_TASK' | translate}}
</mat-card-content> </ng-container>
<mat-card-actions *ngIf="form.hasOutcomes()" class="adf-form-mat-card-actions"> </span>
<ng-content select="adf-cloud-form-custom-outcomes"></ng-content> </div>
<ng-container *ngFor="let outcome of form.outcomes">
<button *ngIf="outcome.isVisible" [id]="'adf-form-'+ outcome.name | formatSpace" [color]="getColorForOutcome(outcome.name)" <adf-toolbar-divider></adf-toolbar-divider>
mat-button [disabled]="!isOutcomeButtonEnabled(outcome)" <button class="adf-cloud-form-close-button"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)" data-automation-id="adf-toolbar-right-back"
(click)="onOutcomeClicked(outcome)"> [attr.aria-label]="'ADF_VIEWER.ACTIONS.CLOSE' | translate"
{{outcome.name | translate | uppercase }} [attr.data-automation-id]="'adf-cloud-form-close-button'"
[matTooltip]="'ADF_VIEWER.ACTIONS.CLOSE' | translate"
mat-icon-button
title="{{ 'ADF_VIEWER.ACTIONS.CLOSE' | translate }}"
(click)="switchToDisplayMode()">
<mat-icon>close</mat-icon>
</button> </button>
</ng-container> </adf-toolbar>
</mat-card-actions>
</mat-card> <mat-card [class.adf-cloud-form-content-card]="displayMode === 'fullScreen'">
<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="displayMode !== '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>fullscreen</mat-icon>
</button>
</div>
<div *ngIf="showRefreshButton" class="adf-cloud-form-reload-button" [matTooltip]="'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate">
<button mat-icon-button (click)="onRefreshClicked()" [attr.aria-label]="'ADF_VIEWER.ACTIONS.FULLSCREEN' | translate">
<mat-icon>refresh</mat-icon>
</button>
</div>
<span *ngIf="isTitleEnabled()" class="adf-cloud-form-title" [matTooltip]="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>
<adf-form-renderer [formDefinition]="form">
</adf-form-renderer>
</mat-card-content>
<mat-card-actions *ngIf="form.hasOutcomes()" class="adf-form-mat-card-actions">
<ng-content select="adf-cloud-form-custom-outcomes"></ng-content>
<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)"
(click)="onOutcomeClicked(outcome)">
{{outcome.name | translate | uppercase }}
</button>
</ng-container>
</mat-card-actions>
</div>
</mat-card>
</div>
</div> </div>

View File

@@ -0,0 +1,100 @@
/* cspell: disable-next-line */
/* stylelint-disable scss/at-extend-no-missing-placeholder */
.adf-full-screen {
width: 100%;
height: 100%;
background-color: var(--adf-theme-background-card-color);
}
.adf-cloud-form {
&-container {
.adf-cloud-form-content {
@extend .adf-full-screen;
flex: 1;
flex-direction: column;
display: flex;
}
}
&-fullscreen-container {
.adf-cloud-form-content {
position: fixed;
top: 0;
left: 0;
z-index: 100000;
}
}
&-inline-container {
@extend .adf-full-screen;
}
&-toolbar {
/* stylelint-disable-next-line selector-class-pattern */
.mat-toolbar {
background-color: var(--adf-theme-background-card-color-087);
}
}
&-fullscreen-button {
position: absolute;
right: 70px;
top: 30px;
}
&__display-name {
font-size: var(--theme-subheading-2-font-size);
opacity: 0.87;
line-height: 1.5;
letter-spacing: -0.4px;
font-weight: normal;
font-style: normal;
font-stretch: normal;
max-width: 400px;
text-overflow: ellipsis;
overflow: hidden;
display: inline-block;
vertical-align: middle;
color: var(--adf-theme-foreground-text-color);
}
&__form-title {
text-align: center;
flex: 1 1 auto;
}
&-content-card {
padding: 0;
height: 100%;
width: 100%;
.adf-cloud-form-content-card-container {
display: flex;
flex-direction: column;
height: 100%;
mat-card-content {
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
mat-card-actions {
display: flex;
justify-content: flex-end;
}
}
}
&-sidebars {
display: flex;
flex: 1 1 auto;
adf-viewer-render {
order: 1;
flex: 1 1 auto;
display: flex;
}
}
}

View File

@@ -52,9 +52,12 @@ import {
} from '../mocks/cloud-form.mock'; } from '../mocks/cloud-form.mock';
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 { CloudFormRenderingService } from './cloud-form-rendering.service'; import { DisplayModeService } from '../services/display-mode.service';
import { FormCloudComponent } from './form-cloud.component'; import { FormCloudComponent } from './form-cloud.component';
import { ProcessServicesCloudModule } from '../../process-services-cloud.module'; import { ProcessServicesCloudModule } from '../../process-services-cloud.module';
import { MatButtonHarness } from '@angular/material/button/testing';
import { FormCloudDisplayMode } from '../../services/form-fields.interfaces';
import { CloudFormRenderingService } from './cloud-form-rendering.service';
const mockOauth2Auth: any = { const mockOauth2Auth: any = {
oauth2Auth: { oauth2Auth: {
@@ -71,6 +74,7 @@ describe('FormCloudComponent', () => {
let matDialog: MatDialog; let matDialog: MatDialog;
let visibilityService: WidgetVisibilityService; let visibilityService: WidgetVisibilityService;
let formRenderingService: CloudFormRenderingService; let formRenderingService: CloudFormRenderingService;
let displayModeService: DisplayModeService;
let documentRootLoader: HarnessLoader; let documentRootLoader: HarnessLoader;
@Component({ @Component({
@@ -111,6 +115,7 @@ describe('FormCloudComponent', () => {
formRenderingService = TestBed.inject(CloudFormRenderingService); formRenderingService = TestBed.inject(CloudFormRenderingService);
formCloudService = TestBed.inject(FormCloudService); formCloudService = TestBed.inject(FormCloudService);
displayModeService = TestBed.inject(DisplayModeService);
matDialog = TestBed.inject(MatDialog); matDialog = TestBed.inject(MatDialog);
@@ -1140,6 +1145,224 @@ describe('FormCloudComponent', () => {
}); });
}); });
describe('Full screen', async () => {
let displayModeOnSpy: jasmine.Spy;
let displayModeOffSpy: jasmine.Spy;
/**
* Helper function for loading the form in the tests
*
* @param form The form model to be loaded
*/
async function loadForm(form?: any): Promise<void> {
formComponent.ngOnChanges({ form: { currentValue: formComponent.parseForm(form || {}), firstChange: true, isFirstChange: () => true, previousValue: undefined } });
await fixture.whenStable();
fixture.detectChanges();
}
beforeEach(async () => {
displayModeOnSpy = spyOn(formComponent.displayModeOn, 'emit').and.stub();
displayModeOffSpy = spyOn(formComponent.displayModeOff, 'emit').and.stub();
spyOn(displayModeService, 'getDefaultDisplayModeConfigurations').and.callFake(() => DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS);
formComponent.taskId = 'any';
formComponent.appName = 'any';
formComponent.showRefreshButton = false;
formComponent.showTitle = false;
formComponent.showValidationIcon = false;
await loadForm();
displayModeOnSpy.calls.reset();
displayModeOffSpy.calls.reset();
});
it('should emit display mode turned on wit the inline configuration', async () => {
await loadForm();
expect(displayModeOnSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[0]);
});
it('should not be in fullScreen mode by default', () => {
const fullScreenModeContainer = fixture.debugElement.query(By.css('.adf-cloud-form-container.adf-cloud-form-fullscreen-container'));
const inlineModeContainer = fixture.debugElement.query(By.css('.adf-cloud-form-container.adf-cloud-form-inline-container'));
expect(fullScreenModeContainer).toBeNull();
expect(inlineModeContainer).not.toBeNull();
});
it('should be in fullScreen mode if it is forced', async () => {
await loadForm({ displayMode: FormCloudDisplayMode.fullScreen });
const fullScreenModeContainer = fixture.debugElement.query(By.css('.adf-cloud-form-container.adf-cloud-form-fullscreen-container'));
const inlineModeContainer = fixture.debugElement.query(By.css('.adf-cloud-form-container.adf-cloud-form-inline-container'));
expect(fullScreenModeContainer).not.toBeNull();
expect(inlineModeContainer).toBeNull();
});
it('should display full screen button on header when header is displayed an not in fullScreen mode', () => {
formComponent.showTitle = true;
fixture.detectChanges();
const fullScreenButton = fixture.debugElement.query(By.css('.adf-cloud-form-fullscreen-button'));
expect(fullScreenButton).not.toBeNull();
});
it('should set fullScreen mode on clicking on the full screen button', async () => {
formComponent.showTitle = true;
fixture.detectChanges();
const fullScreenButton = await documentRootLoader.getHarness(
MatButtonHarness.with({ selector: `[data-automation-id="adf-cloud-form-fullscreen-button"]` })
);
await fullScreenButton.click();
await fixture.whenStable();
expect(formComponent.displayMode).toBe(FormCloudDisplayMode.fullScreen);
expect(displayModeOffSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[0]);
expect(displayModeOnSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1]);
});
it('should not display full screen button on header when header is displayed but in fullScreen mode', async () => {
formComponent.showTitle = true;
await loadForm({ displayMode: FormCloudDisplayMode.fullScreen });
const fullScreenButton = fixture.debugElement.query(By.css('.adf-cloud-form-fullscreen-button'));
expect(fullScreenButton).toBeNull();
});
it('should not display full screen button when header is not displayed', () => {
const fullScreenButton = fixture.debugElement.query(By.css('.adf-cloud-form-fullscreen-button'));
expect(fullScreenButton).toBeNull();
});
it('should not set the styles for the card', () => {
const fullScreenCard = fixture.debugElement.query(By.css('.adf-cloud-form-content-card'));
expect(fullScreenCard).toBeNull();
});
it('should set fullScreen mode from the form service notification', () => {
DisplayModeService.changeDisplayMode({ displayMode: FormCloudDisplayMode.fullScreen, id: formComponent.id });
expect(displayModeOffSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[0]);
expect(displayModeOnSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1]);
expect(formComponent.displayMode).toBe(FormCloudDisplayMode.fullScreen);
displayModeOnSpy.calls.reset();
displayModeOffSpy.calls.reset();
DisplayModeService.changeDisplayMode({ displayMode: FormCloudDisplayMode.inline, id: formComponent.id });
expect(displayModeOffSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1]);
expect(displayModeOnSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[0]);
expect(formComponent.displayMode).toBe(FormCloudDisplayMode.inline);
});
it('should not change the display mode when the notification change is from a different id', () => {
DisplayModeService.changeDisplayMode({ displayMode: FormCloudDisplayMode.fullScreen, id: formComponent.id });
expect(formComponent.displayMode).toBe(FormCloudDisplayMode.fullScreen);
expect(displayModeOffSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[0]);
expect(displayModeOnSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1]);
displayModeOnSpy.calls.reset();
displayModeOffSpy.calls.reset();
DisplayModeService.changeDisplayMode({ displayMode: FormCloudDisplayMode.inline, id: 'otherId' });
expect(displayModeOffSpy).not.toHaveBeenCalled();
expect(displayModeOnSpy).not.toHaveBeenCalled();
expect(formComponent.displayMode).toBe(FormCloudDisplayMode.fullScreen);
});
describe('fullScreen Mode', () => {
beforeEach(async () => {
await loadForm({ displayMode: FormCloudDisplayMode.fullScreen });
});
it('should emit display mode turned on wit the fullScreen configuration', () => {
expect(displayModeOnSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1]);
});
it('should display the toolbar with the nameless task title when the task name is not provided and toolbar is enabled', () => {
const cloudFormToolbarDisplayName: HTMLSpanElement = fixture.debugElement.nativeElement.querySelector('.adf-cloud-form__display-name');
expect(cloudFormToolbarDisplayName).not.toBeNull();
expect(cloudFormToolbarDisplayName.textContent.trim()).toEqual('Nameless task');
});
it('should display the toolbar with the task title when the task name is provided and toolbar is enabled', async () => {
const taskName = 'task-name';
await loadForm({ displayMode: FormCloudDisplayMode.fullScreen, taskName });
const cloudFormToolbarDisplayName: HTMLSpanElement = fixture.debugElement.nativeElement.querySelector('.adf-cloud-form__display-name');
expect(cloudFormToolbarDisplayName).not.toBeNull();
expect(cloudFormToolbarDisplayName.textContent.trim()).toEqual(taskName);
});
it('should not display the toolbar with the task title when the toolbar option is disabled', async () => {
formComponent.displayModeConfigurations = [
{
displayMode: FormCloudDisplayMode.fullScreen,
options: {
onCompleteTask: () => { },
onDisplayModeOff: () => { },
onDisplayModeOn: () => { },
onSaveTask: () => { },
displayToolbar: false
}
}
];
await loadForm({ displayMode: FormCloudDisplayMode.fullScreen });
const cloudFormToolbar: HTMLSpanElement = fixture.debugElement.nativeElement.querySelector('.adf-cloud-form-toolbar');
expect(cloudFormToolbar).toBeNull();
});
it('should set the styles for the card', () => {
const fullScreenCard = fixture.debugElement.query(By.css('.adf-cloud-form-content-card'));
expect(fullScreenCard).not.toBeNull();
});
it('should close fullScreen when close button is clicked', async () => {
displayModeOnSpy.calls.reset();
displayModeOffSpy.calls.reset();
const closeButton = await documentRootLoader.getHarness(
MatButtonHarness.with({ selector: `[data-automation-id="adf-cloud-form-close-button"]` })
);
await closeButton.click();
await fixture.whenStable();
fixture.detectChanges();
expect(formComponent.displayMode).toEqual(FormCloudDisplayMode.inline);
expect(displayModeOffSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1]);
expect(displayModeOnSpy).toHaveBeenCalledWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[0]);
});
it('should close fullScreen when completing the task', () => {
const formRenderingServiceOnCompleteTaskSpy = spyOn(displayModeService, 'onCompleteTask').and.callThrough();
const onCompleteTaskSpy = spyOn(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1].options, 'onCompleteTask').and.callThrough();
const formRenderingServiceChangeDisplayModeSpy = spyOn(DisplayModeService, 'changeDisplayMode').and.callThrough();
displayModeOnSpy.calls.reset();
displayModeOffSpy.calls.reset();
formComponent.completeTaskForm();
expect(formRenderingServiceOnCompleteTaskSpy).toHaveBeenCalledOnceWith(formComponent.id, FormCloudDisplayMode.fullScreen, DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS);
expect(onCompleteTaskSpy).toHaveBeenCalledOnceWith(formComponent.id);
expect(formRenderingServiceChangeDisplayModeSpy).toHaveBeenCalledOnceWith({ id: formComponent.id, displayMode: FormCloudDisplayMode.inline });
expect(displayModeOffSpy).toHaveBeenCalledOnceWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1]);
expect(displayModeOnSpy).toHaveBeenCalledOnceWith(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[0]);
expect(formComponent.displayMode).toBe(FormCloudDisplayMode.inline);
});
});
});
}); });
describe('Multilingual Form', () => { describe('Multilingual Form', () => {

View File

@@ -15,9 +15,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, OnDestroy, HostListener } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, OnDestroy, HostListener, OnInit } from '@angular/core';
import { Observable, of, forkJoin, Subject, Subscription } from 'rxjs'; import { Observable, of, forkJoin, Subject, Subscription } from 'rxjs';
import { switchMap, takeUntil, map } from 'rxjs/operators'; import { switchMap, takeUntil, map, filter } from 'rxjs/operators';
import { import {
FormBaseComponent, FormBaseComponent,
FormFieldModel, FormFieldModel,
@@ -38,12 +38,16 @@ import { TaskVariableCloud } from '../models/task-variable-cloud.model';
import { TaskDetailsCloudModel } from '../../task/start-task/models/task-details-cloud.model'; import { TaskDetailsCloudModel } from '../../task/start-task/models/task-details-cloud.model';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '@alfresco/adf-content-services'; import { ConfirmDialogComponent } from '@alfresco/adf-content-services';
import { v4 as uuidGeneration } from 'uuid';
import { FormCloudDisplayMode, FormCloudDisplayModeConfiguration } from '../../services/form-fields.interfaces';
import { DisplayModeService } from '../public-api';
@Component({ @Component({
selector: 'adf-cloud-form', selector: 'adf-cloud-form',
templateUrl: './form-cloud.component.html' templateUrl: './form-cloud.component.html',
styleUrls: ['./form-cloud.component.scss']
}) })
export class FormCloudComponent extends FormBaseComponent implements OnChanges, OnDestroy { export class FormCloudComponent extends FormBaseComponent implements OnChanges, OnInit, OnDestroy {
/** App name to fetch corresponding form and values. */ /** App name to fetch corresponding form and values. */
@Input() @Input()
appName: string = ''; appName: string = '';
@@ -76,6 +80,12 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
@Input() @Input()
fieldValidators: FormFieldValidator[] = [...FORM_FIELD_VALIDATORS]; fieldValidators: FormFieldValidator[] = [...FORM_FIELD_VALIDATORS];
/**
* The available display configurations for the form
*/
@Input()
displayModeConfigurations: FormCloudDisplayModeConfiguration[];
/** Emitted when the form is submitted with the `Save` or custom outcomes. */ /** Emitted when the form is submitted with the `Save` or custom outcomes. */
@Output() @Output()
formSaved = new EventEmitter<FormModel>(); formSaved = new EventEmitter<FormModel>();
@@ -96,19 +106,32 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
@Output() @Output()
formContentClicked = new EventEmitter<ContentLinkModel>(); formContentClicked = new EventEmitter<ContentLinkModel>();
/** Emitted when a display mode configuration is turned on. */
@Output()
displayModeOn = new EventEmitter<FormCloudDisplayModeConfiguration>();
/** Emitted when a display mode configuration is turned off. */
@Output()
displayModeOff = new EventEmitter<FormCloudDisplayModeConfiguration>();
protected subscriptions: Subscription[] = []; protected subscriptions: Subscription[] = [];
nodeId: string; nodeId: string;
formCloudRepresentationJSON: any; formCloudRepresentationJSON: any;
protected onDestroy$ = new Subject<boolean>(); protected onDestroy$ = new Subject<boolean>();
readonly id: string;
displayMode: FormCloudDisplayMode;
constructor( constructor(
protected formCloudService: FormCloudService, protected formCloudService: FormCloudService,
protected formService: FormService, protected formService: FormService,
private dialog: MatDialog, private dialog: MatDialog,
protected visibilityService: WidgetVisibilityService protected visibilityService: WidgetVisibilityService,
private readonly displayModeService: DisplayModeService
) { ) {
super(); super();
this.id = uuidGeneration();
this.formService.formContentClicked.pipe(takeUntil(this.onDestroy$)).subscribe((content) => { this.formService.formContentClicked.pipe(takeUntil(this.onDestroy$)).subscribe((content) => {
if (content instanceof UploadWidgetContentLinkModel) { if (content instanceof UploadWidgetContentLinkModel) {
@@ -166,6 +189,36 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
this.refreshFormData(); this.refreshFormData();
return; return;
} }
const formRepresentation = changes['form'];
if (formRepresentation?.currentValue) {
this.form = formRepresentation.currentValue;
this.onFormLoaded(this.form);
return;
}
}
ngOnInit(): void {
DisplayModeService.displayMode$
.pipe(
filter(change => change.id === this.id),
takeUntil(this.onDestroy$)
).subscribe((displayModeChange) => {
const oldDisplayMode = this.displayMode;
this.displayMode = displayModeChange.displayMode;
const oldDisplayModeConfiguration = this.displayModeService.findConfiguration(oldDisplayMode, this.displayModeConfigurations);
const newDisplayModeConfiguration = this.displayModeService.findConfiguration(displayModeChange.displayMode, this.displayModeConfigurations);
if (oldDisplayModeConfiguration?.displayMode !== newDisplayModeConfiguration?.displayMode) {
if (oldDisplayModeConfiguration) {
this.displayModeOff.emit(oldDisplayModeConfiguration);
}
if (newDisplayModeConfiguration) {
this.displayModeOn.emit(newDisplayModeConfiguration);
}
}
});
} }
/** /**
@@ -264,6 +317,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
}, },
(error) => this.onTaskSavedError(error) (error) => this.onTaskSavedError(error)
); );
this.displayModeService.onSaveTask(this.id, this.displayMode, this.displayModeConfigurations);
} }
} }
@@ -284,6 +338,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
} else { } else {
this.completeForm(outcome); this.completeForm(outcome);
} }
this.displayModeService.onCompleteTask(this.id, this.displayMode, this.displayModeConfigurations);
} }
private completeForm(outcome?: string) { private completeForm(outcome?: string) {
@@ -342,6 +397,9 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
} }
protected onFormLoaded(form: FormModel) { protected onFormLoaded(form: FormModel) {
this.displayModeConfigurations = this.displayModeService.getDisplayModeConfigurations(this.displayModeConfigurations);
this.displayMode = this.displayModeService.switchToDisplayMode(this.id, this.form.json.displayMode, this.displayMode, this.displayModeConfigurations);
this.displayModeOn.emit(this.displayModeService.findConfiguration(this.displayMode, this.displayModeConfigurations));
this.formLoaded.emit(form); this.formLoaded.emit(form);
} }
@@ -376,10 +434,18 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
return !args.defaultPrevented; return !args.defaultPrevented;
} }
protected storeFormAsMetadata() {} protected storeFormAsMetadata() { }
ngOnDestroy() { ngOnDestroy() {
this.onDestroy$.next(true); this.onDestroy$.next(true);
this.onDestroy$.complete(); this.onDestroy$.complete();
} }
switchToDisplayMode(newDisplayMode?: string) {
this.displayModeService.switchToDisplayMode(this.id, FormCloudDisplayMode[newDisplayMode], this.displayMode, this.displayModeConfigurations);
}
findDisplayConfiguration(displayMode?: string): FormCloudDisplayModeConfiguration {
return this.displayModeService.findConfiguration(FormCloudDisplayMode[displayMode], this.displayModeConfigurations);
}
} }

View File

@@ -17,7 +17,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CoreModule } from '@alfresco/adf-core'; import { CoreModule, ToolbarModule } from '@alfresco/adf-core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MaterialModule } from '../material.module'; import { MaterialModule } from '../material.module';
import { FormCloudComponent } from './components/form-cloud.component'; import { FormCloudComponent } from './components/form-cloud.component';
@@ -41,6 +41,7 @@ import { FilePropertiesTableCloudComponent } from './components/widgets/attach-f
import { FileViewerWidgetComponent } from './components/widgets/file-viewer/file-viewer.widget'; import { FileViewerWidgetComponent } from './components/widgets/file-viewer/file-viewer.widget';
import { DisplayRichTextWidgetComponent } from './components/widgets/display-rich-text/display-rich-text.widget'; import { DisplayRichTextWidgetComponent } from './components/widgets/display-rich-text/display-rich-text.widget';
import { RichTextEditorModule } from '../rich-text-editor'; import { RichTextEditorModule } from '../rich-text-editor';
import { A11yModule } from '@angular/cdk/a11y';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -55,7 +56,9 @@ import { RichTextEditorModule } from '../rich-text-editor';
ContentMetadataModule, ContentMetadataModule,
UploadModule, UploadModule,
AlfrescoViewerModule, AlfrescoViewerModule,
RichTextEditorModule RichTextEditorModule,
ToolbarModule,
A11yModule
], ],
declarations: [ declarations: [
FormCloudComponent, FormCloudComponent,

View File

@@ -38,5 +38,6 @@ export * from './services/form-cloud.service';
export * from './services/form-definition-selector-cloud.service'; export * from './services/form-definition-selector-cloud.service';
export * from './services/content-cloud-node-selector.service'; export * from './services/content-cloud-node-selector.service';
export * from './services/process-cloud-content.service'; export * from './services/process-cloud-content.service';
export * from './services/display-mode.service';
export * from './form-cloud.module'; export * from './form-cloud.module';

View File

@@ -0,0 +1,164 @@
/*!
* @license
* Copyright © 2005-2023 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 { FormCloudDisplayMode, FormCloudDisplayModeConfiguration } from '../../services/form-fields.interfaces';
import { DisplayModeService } from './display-mode.service';
describe('DisplayModeService', () => {
let service: DisplayModeService;
let displayModeOnSpy: jasmine.Spy;
let displayModeOffSpy: jasmine.Spy;
let completeTaskSpy: jasmine.Spy;
let saveTaskSpy: jasmine.Spy;
let changeDisplayModeSpy: jasmine.Spy;
const formId = 'id';
beforeEach(() => {
service = new DisplayModeService();
spyOn(service, 'getDefaultDisplayModeConfigurations').and.callFake(() => DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS);
displayModeOnSpy = spyOn(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1].options, 'onDisplayModeOn');
displayModeOffSpy = spyOn(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1].options, 'onDisplayModeOff');
completeTaskSpy = spyOn(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1].options, 'onCompleteTask');
saveTaskSpy = spyOn(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1].options, 'onSaveTask');
changeDisplayModeSpy = spyOn(DisplayModeService, 'changeDisplayMode');
});
it('should return the default display mode configurations when no available configurations are provided', () => {
expect(service.getDisplayModeConfigurations()).toBe(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS);
expect(service.getDisplayModeConfigurations(null)).toBe(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS);
expect(service.getDisplayModeConfigurations([])).toBe(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS);
});
it('should return the provided display mode configurations when available configurations are provided', () => {
const availableConfigurations: FormCloudDisplayModeConfiguration[] = [
{
displayMode: FormCloudDisplayMode.fullScreen
}
];
expect(service.getDisplayModeConfigurations(availableConfigurations)).toBe(availableConfigurations);
});
it('should return the default display mode when no display mode is provided', () => {
expect(service.getDisplayMode()).toBe(DisplayModeService.DEFAULT_DISPLAY_MODE);
expect(service.getDisplayMode(null)).toBe(DisplayModeService.DEFAULT_DISPLAY_MODE);
});
it('should return the default display mode when no display mode does not exist in the configuration', () => {
expect(service.getDisplayMode('notExisting' as any)).toBe(DisplayModeService.DEFAULT_DISPLAY_MODE);
expect(service.getDisplayMode('notExisting' as any, [])).toBe(DisplayModeService.DEFAULT_DISPLAY_MODE);
expect(service.getDisplayMode('notExisting' as any, [{ displayMode: FormCloudDisplayMode.fullScreen }])).toBe(FormCloudDisplayMode.fullScreen);
expect(service.getDisplayMode('notExisting' as any, [{ displayMode: FormCloudDisplayMode.fullScreen }, { displayMode: FormCloudDisplayMode.inline }])).toBe(FormCloudDisplayMode.fullScreen);
expect(service.getDisplayMode('notExisting' as any, [{ displayMode: FormCloudDisplayMode.fullScreen, default: true }, { displayMode: FormCloudDisplayMode.inline }])).toBe(FormCloudDisplayMode.fullScreen);
});
it('should return the provided display mode when display mode is provided', () => {
expect(service.getDisplayMode(FormCloudDisplayMode.fullScreen)).toBe(FormCloudDisplayMode.fullScreen);
});
it('should find the display configuration', () => {
expect(service.findConfiguration()).toBeUndefined();
expect(service.findConfiguration(FormCloudDisplayMode.fullScreen)).toBe(DisplayModeService.IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS[1]);
expect(service.findConfiguration('notExisting' as any)).toBeUndefined();
expect(service.findConfiguration(FormCloudDisplayMode.fullScreen, [{ displayMode: FormCloudDisplayMode.fullScreen }])).toEqual({ displayMode: FormCloudDisplayMode.fullScreen });
expect(service.findConfiguration(FormCloudDisplayMode.fullScreen, [{ displayMode: FormCloudDisplayMode.inline }])).toBeUndefined();
});
it('should call the display mode on function in the configuration when it is found', () => {
service.onDisplayModeOn(formId, FormCloudDisplayMode.fullScreen);
expect(displayModeOnSpy).toHaveBeenCalledWith(formId);
});
it('should not call the on display mode on function in the configuration when it is not found', () => {
service.onDisplayModeOff(formId, FormCloudDisplayMode.inline);
expect(displayModeOnSpy).not.toHaveBeenCalled();
});
it('should call the display mode off function in the configuration when it is found', () => {
service.onDisplayModeOff(formId, FormCloudDisplayMode.fullScreen);
expect(displayModeOffSpy).toHaveBeenCalledWith(formId);
});
it('should not call the on display mode off function in the configuration when it is not found', () => {
service.onDisplayModeOff(formId, FormCloudDisplayMode.inline);
expect(displayModeOffSpy).not.toHaveBeenCalled();
});
it('should call the complete task function in the configuration when it is found', () => {
service.onCompleteTask(formId, FormCloudDisplayMode.fullScreen);
expect(completeTaskSpy).toHaveBeenCalledWith(formId);
});
it('should not call the complete task function in the configuration when it is not found', () => {
service.onCompleteTask(formId, FormCloudDisplayMode.inline);
expect(completeTaskSpy).not.toHaveBeenCalled();
});
it('should call the save task function in the configuration when it is found', () => {
service.onSaveTask(formId, FormCloudDisplayMode.fullScreen);
expect(saveTaskSpy).toHaveBeenCalledWith(formId);
});
it('should not call the save task function in the configuration when it is not found', () => {
service.onSaveTask(formId, FormCloudDisplayMode.inline);
expect(saveTaskSpy).not.toHaveBeenCalled();
});
it('should return the provided display mode when calling the switchToDisplayMode and display exists in configuration', () => {
expect(service.switchToDisplayMode(formId, FormCloudDisplayMode.fullScreen)).toBe(FormCloudDisplayMode.fullScreen);
expect(changeDisplayModeSpy).toHaveBeenCalledWith({ id: formId, displayMode: FormCloudDisplayMode.fullScreen });
});
it('should return the default display mode when calling the switchToDisplayMode and display does not exist in configuration', () => {
expect(service.switchToDisplayMode(formId, 'notExisting' as any)).toBe(DisplayModeService.DEFAULT_DISPLAY_MODE);
});
it('should not call the change display mode method when switchToDisplayMode and the mode to switch is the same as the old one', () => {
expect(service.switchToDisplayMode(formId, FormCloudDisplayMode.fullScreen, FormCloudDisplayMode.fullScreen)).toBe(FormCloudDisplayMode.fullScreen);
expect(changeDisplayModeSpy).not.toHaveBeenCalledWith();
});
it('should not call the change display mode method when switchToDisplayMode and the mode to switch does not exist and the previous mode was the default one', () => {
expect(service.switchToDisplayMode(formId, 'notExisting' as any, DisplayModeService.DEFAULT_DISPLAY_MODE)).toBe(DisplayModeService.DEFAULT_DISPLAY_MODE);
expect(changeDisplayModeSpy).not.toHaveBeenCalledWith();
});
it('should not call the change display mode method when switchToDisplayMode and the mode to switch does not exist and the previous mode was not provided', () => {
expect(service.switchToDisplayMode(formId, 'notExisting' as any, DisplayModeService.DEFAULT_DISPLAY_MODE)).toBe(DisplayModeService.DEFAULT_DISPLAY_MODE);
expect(changeDisplayModeSpy).not.toHaveBeenCalledWith();
});
it('should call the display mode off when switching the display mode and the old display mode is provided and it provides the off method', () => {
expect(service.switchToDisplayMode(formId, FormCloudDisplayMode.inline, FormCloudDisplayMode.fullScreen)).toBe(FormCloudDisplayMode.inline);
expect(displayModeOffSpy).toHaveBeenCalled();
});
it('should call the display mode on when switching the display mode and the new display mode provides the on method', () => {
expect(service.switchToDisplayMode(formId, FormCloudDisplayMode.fullScreen)).toBe(FormCloudDisplayMode.fullScreen);
expect(displayModeOnSpy).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,147 @@
/*!
* @license
* Copyright © 2005-2023 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 { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { FormCloudDisplayMode, FormCloudDisplayModeChange, FormCloudDisplayModeConfiguration } from '../../services/form-fields.interfaces';
@Injectable({
providedIn: 'root'
})
export class DisplayModeService {
public static readonly DEFAULT_DISPLAY_MODE_CONFIGURATIONS: FormCloudDisplayModeConfiguration[] = [
{
displayMode: FormCloudDisplayMode.inline,
default: true
}
];
public static readonly IMPLEMENTED_DISPLAY_MODE_CONFIGURATIONS: FormCloudDisplayModeConfiguration[] = [
...DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS,
{
displayMode: FormCloudDisplayMode.fullScreen,
options: {
onDisplayModeOn: () => { },
onDisplayModeOff: (id: string) => DisplayModeService.changeDisplayMode({ displayMode: FormCloudDisplayMode.inline, id }),
onCompleteTask: (id: string) => DisplayModeService.changeDisplayMode({ displayMode: FormCloudDisplayMode.inline, id }),
onSaveTask: () => { },
displayToolbar: true
}
}
];
public static readonly DEFAULT_DISPLAY_MODE: FormCloudDisplayMode = FormCloudDisplayMode.inline;
private static readonly displayMode = new Subject<FormCloudDisplayModeChange>();
static readonly displayMode$: Observable<FormCloudDisplayModeChange> = DisplayModeService.displayMode.asObservable();
static changeDisplayMode(change: FormCloudDisplayModeChange) {
DisplayModeService.displayMode.next(change);
}
getDefaultDisplayModeConfigurations(): FormCloudDisplayModeConfiguration[] {
return DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS;
}
getDisplayModeConfigurations(availableConfigurations?: FormCloudDisplayModeConfiguration[]): FormCloudDisplayModeConfiguration[] {
if (availableConfigurations && availableConfigurations.length > 0) {
return availableConfigurations;
} else {
return this.getDefaultDisplayModeConfigurations();
}
}
getDisplayMode(displayMode?: FormCloudDisplayMode, availableConfigurations?: FormCloudDisplayModeConfiguration[]): FormCloudDisplayMode {
const configuration = this.findConfiguration(displayMode, availableConfigurations);
if (configuration) {
return configuration.displayMode;
} else if (availableConfigurations && availableConfigurations.length > 0) {
return availableConfigurations.length === 1 ?
availableConfigurations[0].displayMode :
(availableConfigurations.find(config => config.default)?.displayMode || availableConfigurations[0].displayMode);
} else {
return DisplayModeService.DEFAULT_DISPLAY_MODE;
}
}
findConfiguration(displayMode?: FormCloudDisplayMode, availableConfigurations?: FormCloudDisplayModeConfiguration[]): FormCloudDisplayModeConfiguration {
return this.getDisplayModeConfigurations(availableConfigurations).find(config => config.displayMode === displayMode);
}
onCompleteTask(id?: string, displayMode?: FormCloudDisplayMode, availableConfigurations?: FormCloudDisplayModeConfiguration[]) {
const configuration = this.findConfiguration(displayMode, availableConfigurations);
if (configuration?.options?.onCompleteTask) {
configuration.options.onCompleteTask(id);
}
}
onSaveTask(id?: string, displayMode?: FormCloudDisplayMode, availableConfigurations?: FormCloudDisplayModeConfiguration[]) {
const configuration = this.findConfiguration(displayMode, availableConfigurations);
if (configuration?.options?.onSaveTask) {
configuration.options.onSaveTask(id);
}
}
onDisplayModeOff(id?: string, displayMode?: FormCloudDisplayMode, availableConfigurations?: FormCloudDisplayModeConfiguration[]) {
const configuration = this.findConfiguration(displayMode, availableConfigurations);
if (configuration?.options?.onDisplayModeOff) {
configuration.options.onDisplayModeOff(id);
}
}
onDisplayModeOn(id?: string, displayMode?: FormCloudDisplayMode, availableConfigurations?: FormCloudDisplayModeConfiguration[]) {
const configuration = this.findConfiguration(displayMode, availableConfigurations);
if (configuration?.options?.onDisplayModeOn) {
configuration.options.onDisplayModeOn(id);
}
}
switchToDisplayMode(
id?: string,
newDisplayMode?: FormCloudDisplayMode,
oldDisplayMode?: FormCloudDisplayMode,
availableConfigurations?: FormCloudDisplayModeConfiguration[]
): FormCloudDisplayMode {
const oldConfiguration = this.findConfiguration(oldDisplayMode, availableConfigurations);
const newConfiguration = this.findConfiguration(newDisplayMode, availableConfigurations);
if (oldConfiguration?.displayMode !== newConfiguration?.displayMode) {
if (oldConfiguration) {
this.onDisplayModeOff(id, oldDisplayMode, availableConfigurations);
}
if (newConfiguration) {
DisplayModeService.changeDisplayMode({ id, displayMode: newConfiguration.displayMode });
this.onDisplayModeOn(id, newDisplayMode, availableConfigurations);
return newConfiguration.displayMode;
} else {
const displayMode = this.getDisplayMode(newDisplayMode, availableConfigurations);
DisplayModeService.changeDisplayMode({ id, displayMode });
this.onDisplayModeOn(id, displayMode, availableConfigurations);
return displayMode;
}
} else {
return oldConfiguration?.displayMode || this.getDisplayMode(oldDisplayMode, availableConfigurations);
}
}
}

View File

@@ -26,6 +26,7 @@ export interface FormRepresentation {
version?: number; version?: number;
formDefinition?: FormDefinition; formDefinition?: FormDefinition;
standAlone?: boolean; standAlone?: boolean;
displayMode?: FormCloudDisplayMode;
} }
export interface FormTab { export interface FormTab {
@@ -59,7 +60,7 @@ export interface Container {
} }
export type FormFieldRepresentation = (DateField | DateTimeField | TextField | AttachFileField | DropDownField | export type FormFieldRepresentation = (DateField | DateTimeField | TextField | AttachFileField | DropDownField |
RadioField | TypeaheadField | PeopleField | AmountField | NumberField | CheckboxField | HyperlinkField ); RadioField | TypeaheadField | PeopleField | AmountField | NumberField | CheckboxField | HyperlinkField);
export interface AttachFileField extends FormField { export interface AttachFileField extends FormField {
required: boolean; required: boolean;
@@ -233,3 +234,28 @@ export enum FormFieldType {
displayText = 'readonly-text', displayText = 'readonly-text',
fileViewer = 'file-viewer' fileViewer = 'file-viewer'
} }
export interface FormCloudDisplayModeConfigurationOptions {
onCompleteTask(id?: string): void;
onSaveTask(id?: string): void;
onDisplayModeOn(id?: string): void;
onDisplayModeOff(id?: string): void;
[key: string]: any;
};
export interface FormCloudDisplayModeConfiguration {
displayMode: FormCloudDisplayMode;
options?: FormCloudDisplayModeConfigurationOptions;
default?: boolean;
};
// eslint-disable-next-line no-shadow
export enum FormCloudDisplayMode {
inline = 'inline',
fullScreen = 'fullScreen'
};
export interface FormCloudDisplayModeChange {
displayMode: FormCloudDisplayMode;
id?: string;
};

View File

@@ -1,5 +1,5 @@
<div *ngIf="!loading; else loadingTemplate"> <div *ngIf="!loading; else loadingTemplate">
<adf-cloud-form *ngIf="hasForm(); else withoutForm" <adf-cloud-form #adfCloudForm *ngIf="hasForm(); else withoutForm"
[appName]="appName" [appName]="appName"
[appVersion]="taskDetails.appVersion" [appVersion]="taskDetails.appVersion"
[taskId]="taskId" [taskId]="taskId"
@@ -10,12 +10,15 @@
[showValidationIcon]="showValidationIcon" [showValidationIcon]="showValidationIcon"
[showCompleteButton]="canCompleteTask()" [showCompleteButton]="canCompleteTask()"
[showSaveButton]="canCompleteTask()" [showSaveButton]="canCompleteTask()"
[displayModeConfigurations]="displayModeConfigurations"
(formSaved)="onFormSaved($event)" (formSaved)="onFormSaved($event)"
(formCompleted)="onFormCompleted($event)" (formCompleted)="onFormCompleted($event)"
(formError)="onError($event)" (formError)="onError($event)"
(error)="onError($event)" (error)="onError($event)"
(formContentClicked)="onFormContentClicked($event)" (formContentClicked)="onFormContentClicked($event)"
(executeOutcome)="onFormExecuteOutcome($event)"> (executeOutcome)="onFormExecuteOutcome($event)"
(displayModeOn)="onDisplayModeOn($event)"
(displayModeOff)="onDisplayModeOff($event)">
<adf-cloud-form-custom-outcomes> <adf-cloud-form-custom-outcomes>
<ng-template [ngTemplateOutlet]="taskFormCloudButtons"> <ng-template [ngTemplateOutlet]="taskFormCloudButtons">
</ng-template> </ng-template>

View File

@@ -36,6 +36,8 @@ import { IdentityUserService } from '../../../people/services/identity-user.serv
import { HarnessLoader } from '@angular/cdk/testing'; import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing'; import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing';
import { DisplayModeService } from '../../../form/services/display-mode.service';
import { FormCloudComponent } from '../../../form/components/form-cloud.component';
const taskDetails: TaskDetailsCloudModel = { const taskDetails: TaskDetailsCloudModel = {
appName: 'simple-app', appName: 'simple-app',
@@ -66,7 +68,8 @@ describe('TaskFormCloudComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), ProcessServiceCloudTestingModule] imports: [TranslateModule.forRoot(), ProcessServiceCloudTestingModule],
declarations: [FormCloudComponent]
}); });
taskDetails.status = TASK_ASSIGNED_STATE; taskDetails.status = TASK_ASSIGNED_STATE;
taskDetails.permissions = [TASK_VIEW_PERMISSION]; taskDetails.permissions = [TASK_VIEW_PERMISSION];
@@ -421,6 +424,26 @@ describe('TaskFormCloudComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(component.onTaskLoaded.emit).toHaveBeenCalledWith(taskDetails); expect(component.onTaskLoaded.emit).toHaveBeenCalledWith(taskDetails);
}); });
it('should emit displayModeOn when display mode is turned on', async () => {
spyOn(component.displayModeOn, 'emit').and.stub();
component.onDisplayModeOn(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]);
fixture.detectChanges();
await fixture.whenStable();
expect(component.displayModeOn.emit).toHaveBeenCalledWith(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]);
});
it('should emit displayModeOff when display mode is turned on', async () => {
spyOn(component.displayModeOff, 'emit').and.stub();
component.onDisplayModeOff(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]);
fixture.detectChanges();
await fixture.whenStable();
expect(component.displayModeOff.emit).toHaveBeenCalledWith(DisplayModeService.DEFAULT_DISPLAY_MODE_CONFIGURATIONS[0]);
});
}); });
it('should display task name as title on no form template if showTitle is true', () => { it('should display task name as title on no form template if showTitle is true', () => {
@@ -452,4 +475,18 @@ describe('TaskFormCloudComponent', () => {
expect(noFormTemplateTitle).toBeNull(); expect(noFormTemplateTitle).toBeNull();
}); });
it('should call children cloud task form change display mode when changing the display mode', () => {
const displayMode = 'displayMode';
component.taskDetails = { ...taskDetails, formKey: 'some-form' };
fixture.detectChanges();
expect(component.adfCloudForm).toBeDefined();
const switchToDisplayModeSpy = spyOn(component.adfCloudForm, 'switchToDisplayMode');
component.switchToDisplayMode(displayMode);
expect(switchToDisplayModeSpy).toHaveBeenCalledOnceWith(displayMode);
});
}); });

View File

@@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, OnInit, ViewEncapsulation, OnDestroy } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, OnInit, ViewEncapsulation, OnDestroy, ViewChild } from '@angular/core';
import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model'; import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model';
import { TaskCloudService } from '../../services/task-cloud.service'; import { TaskCloudService } from '../../services/task-cloud.service';
import { FormRenderingService, FormModel, ContentLinkModel, FormOutcomeEvent } from '@alfresco/adf-core'; import { FormRenderingService, FormModel, ContentLinkModel, FormOutcomeEvent } from '@alfresco/adf-core';
@@ -24,6 +24,8 @@ import { DropdownCloudWidgetComponent } from '../../../form/components/widgets/d
import { DateCloudWidgetComponent } from '../../../form/components/widgets/date/date-cloud.widget'; import { DateCloudWidgetComponent } from '../../../form/components/widgets/date/date-cloud.widget';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { FormCloudDisplayModeConfiguration } from '../../../services/form-fields.interfaces';
import { FormCloudComponent } from '../../../form/components/form-cloud.component';
@Component({ @Component({
selector: 'adf-cloud-task-form', selector: 'adf-cloud-task-form',
@@ -64,6 +66,12 @@ export class TaskFormCloudComponent implements OnInit, OnChanges, OnDestroy {
@Input() @Input()
readOnly = false; readOnly = false;
/**
* The available display configurations for the form
*/
@Input()
displayModeConfigurations: FormCloudDisplayModeConfiguration[];
/** Emitted when the form is saved. */ /** Emitted when the form is saved. */
@Output() @Output()
formSaved = new EventEmitter<FormModel>(); formSaved = new EventEmitter<FormModel>();
@@ -109,6 +117,17 @@ export class TaskFormCloudComponent implements OnInit, OnChanges, OnDestroy {
@Output() @Output()
onTaskLoaded = new EventEmitter<TaskDetailsCloudModel>(); /* eslint-disable-line */ onTaskLoaded = new EventEmitter<TaskDetailsCloudModel>(); /* eslint-disable-line */
/** Emitted when a display mode configuration is turned on. */
@Output()
displayModeOn = new EventEmitter<FormCloudDisplayModeConfiguration>();
/** Emitted when a display mode configuration is turned off. */
@Output()
displayModeOff = new EventEmitter<FormCloudDisplayModeConfiguration>();
@ViewChild('adfCloudForm', { static: false })
adfCloudForm: FormCloudComponent;
taskDetails: TaskDetailsCloudModel; taskDetails: TaskDetailsCloudModel;
candidateUsers: string[] = []; candidateUsers: string[] = [];
@@ -231,6 +250,18 @@ export class TaskFormCloudComponent implements OnInit, OnChanges, OnDestroy {
this.executeOutcome.emit(outcome); this.executeOutcome.emit(outcome);
} }
switchToDisplayMode(newDisplayMode?: string) {
this.adfCloudForm.switchToDisplayMode(newDisplayMode);
}
onDisplayModeOn(displayModeConfiguration: FormCloudDisplayModeConfiguration){
this.displayModeOn.emit(displayModeConfiguration);
}
onDisplayModeOff(displayModeConfiguration: FormCloudDisplayModeConfiguration){
this.displayModeOff.emit(displayModeConfiguration);
}
ngOnDestroy() { ngOnDestroy() {
this.onDestroy$.next(true); this.onDestroy$.next(true);
this.onDestroy$.complete(); this.onDestroy$.complete();