[ADF-3797] Task management view - Task with Form (#4534)

* [ADF-4248] Created form cloud service

* [ADF-4248] Created form cloud model

* [ADF-4248] Created new cloud form

* [ADF-4248] Exported cloud from module

* [ADF-4248] Added form saving feature

* [ADF-4248] Added form to task details

* [ADF-4248] Added services to save form

* [ADF-4248] Added data support

* [ADF-4248] Added outcome support in form model

* [ADF-4248] Modified demo component to show form

* [ADF-4248] Copied tests

* [ADF-4248] Added form parsing service

* [ADF-4248] Added form cloud demo

* [ADF-4248] Added form input to fom-cloud

* [ADF-4248] Added tests for form cloud model

* [ADF-4248] Improved form model json parsing

* [ADF-4248]  Added test for form could

* [ADF-4248] Refactored types in the form model

* [ADF-4248] Improved tests

* [ADF-4248] Added tests for form cloud service

* [ADF-4248] Added tests for form services

* [ADF-4248] Refactored form services

* [ADF-4248] Handled form events in demo shell

* [ADF-4248] Improved form value parsing

* [ADF-4248] Added form-cloud demo to routing

* [ADF-4248] Added field validation without handler

* [ADF-4248] Added task variable model

* [ADF-4248] Added adf-cloud prefix to css classes

* [ADF-4248] Translated name of nameless task

* [ADF-4248] Added docs for cloud form component

* [ADF-4248] Added docs for cloud form service

* create base component

* [ADF-4248] Created formBase and formModelbase

* [ADF-4248] Used base classes in cloud package

* Update form-cloud.component.md

* Update form-cloud.service.md

* [ADF-4248] Created form cloud service

* [ADF-4248] Created form cloud model

* [ADF-4248] Created new cloud form

* [ADF-4248] Exported cloud from module

* [ADF-4248] Added form saving feature

* [ADF-4248] Added form to task details

* [ADF-4248] Added services to save form

* [ADF-4248] Added data support

* [ADF-4248] Added outcome support in form model

* [ADF-4248] Modified demo component to show form

* [ADF-4248] Copied tests

* [ADF-4248] Added form parsing service

* [ADF-4248] Added form cloud demo

* [ADF-4248] Added form input to fom-cloud

* [ADF-4248] Added tests for form cloud model

* [ADF-4248] Improved form model json parsing

* [ADF-4248]  Added test for form could

* [ADF-4248] Refactored types in the form model

* [ADF-4248] Improved tests

* [ADF-4248] Added tests for form cloud service

* [ADF-4248] Added tests for form services

* [ADF-4248] Refactored form services

* [ADF-4248] Handled form events in demo shell

* [ADF-4248] Improved form value parsing

* [ADF-4248] Added form-cloud demo to routing

* [ADF-4248] Added field validation without handler

* [ADF-4248] Added task variable model

* [ADF-4248] Added adf-cloud prefix to css classes

* [ADF-4248] Translated name of nameless task

* [ADF-4248] Added docs for cloud form component

* [ADF-4248] Added docs for cloud form service

* create base component

* [ADF-4248] Created formBase and formModelbase

* [ADF-4248] Used base classes in cloud package

* [ADF-4248] Moved documentation to process services

* [ADF-4248] Removed duplicate import

* [ADF-4248] Fixed wrong imports

* [ADF-4248] Renamed form renderer input

* [ADF-4248] Show translated name for nameless form

* Enable the uploadWidget

* Make the form great again!

* Move the class style on the parent

* Fix the debugMode
This commit is contained in:
Deepak Paul
2019-04-10 21:40:56 +05:30
committed by Eugenio Romano
parent 61ee1f1d53
commit 558ee4c031
76 changed files with 5029 additions and 450 deletions

View File

@@ -1,6 +1,6 @@
module.exports = { module.exports = {
"/alfresco": { "/alfresco": {
"target": "http://localhost:8080", "target": "http://aps2staging.envalfresco.com",
"secure": false, "secure": false,
"pathRewrite": { "pathRewrite": {
"^/alfresco/alfresco": "" "^/alfresco/alfresco": ""

View File

@@ -64,7 +64,7 @@ import { ContentModule } from '@alfresco/adf-content-services';
import { InsightsModule } from '@alfresco/adf-insights'; import { InsightsModule } from '@alfresco/adf-insights';
import { ProcessModule } from '@alfresco/adf-process-services'; import { ProcessModule } from '@alfresco/adf-process-services';
import { AuthBearerInterceptor } from './services'; import { AuthBearerInterceptor } from './services';
import { ProcessServicesCloudModule, GroupCloudModule, TaskDirectiveModule } from '@alfresco/adf-process-services-cloud'; import { ProcessServicesCloudModule } from '@alfresco/adf-process-services-cloud';
import { AppExtensionsModule } from './app-extension.module'; import { AppExtensionsModule } from './app-extension.module';
import { TreeViewSampleComponent } from './components/tree-view/tree-view-sample.component'; import { TreeViewSampleComponent } from './components/tree-view/tree-view-sample.component';
import { CloudLayoutComponent } from './components/cloud/cloud-layout.component'; import { CloudLayoutComponent } from './components/cloud/cloud-layout.component';
@@ -82,6 +82,7 @@ import { PeopleGroupCloudDemoComponent } from './components/cloud/people-groups-
import { CloudSettingsComponent } from './components/cloud/cloud-settings.component'; import { CloudSettingsComponent } from './components/cloud/cloud-settings.component';
import { NestedMenuPositionDirective } from './components/cloud/directives/nested-menu-position.directive'; import { NestedMenuPositionDirective } from './components/cloud/directives/nested-menu-position.directive';
import { ConfirmDialogExampleComponent } from './components/confirm-dialog/confirm-dialog-example.component'; import { ConfirmDialogExampleComponent } from './components/confirm-dialog/confirm-dialog-example.component';
import { FormCloudDemoComponent } from './components/app-layout/cloud/form-demo/cloud-form-demo.component';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -102,10 +103,7 @@ import { ConfirmDialogExampleComponent } from './components/confirm-dialog/confi
ExtensionsModule.forRoot(), ExtensionsModule.forRoot(),
ThemePickerModule, ThemePickerModule,
ChartsModule, ChartsModule,
MonacoEditorModule.forRoot(), MonacoEditorModule.forRoot()
ProcessServicesCloudModule,
GroupCloudModule,
TaskDirectiveModule
], ],
declarations: [ declarations: [
AppComponent, AppComponent,
@@ -150,6 +148,8 @@ import { ConfirmDialogExampleComponent } from './components/confirm-dialog/confi
PeopleGroupCloudDemoComponent, PeopleGroupCloudDemoComponent,
CloudSettingsComponent, CloudSettingsComponent,
NestedMenuPositionDirective, NestedMenuPositionDirective,
ConfirmDialogExampleComponent,
FormCloudDemoComponent,
ConfirmDialogExampleComponent ConfirmDialogExampleComponent
], ],
providers: [ providers: [

View File

@@ -49,6 +49,7 @@ import { StartProcessCloudDemoComponent } from './components/cloud/start-process
import { TaskDetailsCloudDemoComponent } from './components/cloud/task-details-cloud-demo.component'; import { TaskDetailsCloudDemoComponent } from './components/cloud/task-details-cloud-demo.component';
import { ProcessDetailsCloudDemoComponent } from './components/cloud/process-details-cloud-demo.component'; import { ProcessDetailsCloudDemoComponent } from './components/cloud/process-details-cloud-demo.component';
import { TemplateDemoComponent } from './components/template-list/template-demo.component'; import { TemplateDemoComponent } from './components/template-list/template-demo.component';
import { FormCloudDemoComponent } from './components/app-layout/cloud/form-demo/cloud-form-demo.component';
import { ConfirmDialogExampleComponent } from './components/confirm-dialog/confirm-dialog-example.component'; import { ConfirmDialogExampleComponent } from './components/confirm-dialog/confirm-dialog-example.component';
export const appRoutes: Routes = [ export const appRoutes: Routes = [
@@ -355,6 +356,7 @@ export const appRoutes: Routes = [
path: 'icons', path: 'icons',
loadChildren: './components/icons/icons.module#AppIconsModule' loadChildren: './components/icons/icons.module#AppIconsModule'
}, },
{ path: 'form-cloud', component: FormCloudDemoComponent },
{ path: 'form', component: FormComponent }, { path: 'form', component: FormComponent },
{ path: 'form-list', component: FormListComponent }, { path: 'form-list', component: FormListComponent },
{ path: 'form-loading', component: FormLoadingComponent }, { path: 'form-loading', component: FormLoadingComponent },

View File

@@ -48,6 +48,7 @@ export class AppLayoutComponent implements OnInit {
{ href: '/task-list', icon: 'assignment', title: 'APP_LAYOUT.TASK_LIST' }, { href: '/task-list', icon: 'assignment', title: 'APP_LAYOUT.TASK_LIST' },
{ href: '/cloud', icon: 'cloud', title: 'APP_LAYOUT.PROCESS_CLOUD', children: [ { href: '/cloud', icon: 'cloud', title: 'APP_LAYOUT.PROCESS_CLOUD', children: [
{ href: '/cloud/', icon: 'cloud', title: 'APP_LAYOUT.HOME' }, { href: '/cloud/', icon: 'cloud', title: 'APP_LAYOUT.HOME' },
{ href: '/form-cloud', icon: 'poll', title: 'APP_LAYOUT.FORM' },
{ href: '/cloud/people-group-cloud', icon: 'group', title: 'APP_LAYOUT.PEOPLE_GROUPS_CLOUD' } { href: '/cloud/people-group-cloud', icon: 'group', title: 'APP_LAYOUT.PEOPLE_GROUPS_CLOUD' }
]}, ]},
{ href: '/activiti', icon: 'device_hub', title: 'APP_LAYOUT.PROCESS_SERVICES', children: [ { href: '/activiti', icon: 'device_hub', title: 'APP_LAYOUT.PROCESS_SERVICES', children: [

View File

@@ -0,0 +1,56 @@
<div class="main-content">
<mat-tab-group>
<mat-tab label="Form">
<div class="adf-form-container">
<adf-cloud-form
[showRefreshButton]="false"
[form]="form"
(formSaved)="onFormSaved()"
(formError)="logErrors($event)">
</adf-cloud-form>
</div>
<div class="adf-console" #console>
<h3>Error log:</h3>
<p *ngFor="let error of errorFields">Error {{ error.name }} {{error.validationSummary.message |
translate}}</p>
</div>
</mat-tab>
<mat-tab label="Editor">
<ngx-monaco-editor
id="adf-form-config-editor"
class="adf-form-config-editor"
[options]="editorOptions"
[(ngModel)]="formConfig"
(onInit)="onInitFormEditor($event)">
</ngx-monaco-editor>
<div class="adf-form-editor-buttons">
<button mat-raised-button id="adf-form-config-save" (click)="onSaveFormConfig()" color="primary">Save
form config
</button>
<button mat-raised-button id="adf-form-config-clear" (click)="onClearFormConfig()" color="primary">Clear
form config
</button>
</div>
<div class="adf-upload-config-button">
<a mat-raised-button color="primary" >
<mat-icon>file_upload</mat-icon>
<label for="upload-config-file">Upload JSON File</label>
<input
id="upload-config-file"
data-automation-id="upload-single-file"
type="file"
name="uploadConfig"
accept=".json"
(change)="onConfigAdded($event)">
</a>
</div>
</mat-tab>
</mat-tab-group>
</div>

View File

@@ -0,0 +1,57 @@
.adf-form-container {
padding: 10px;
}
.adf-main-content {
padding: 0 15px;
}
.adf-card-view {
width: 30%;
display: inline-block;
}
.adf-console {
width: 60%;
display: inline-block;
vertical-align: top;
margin-left: 10px;
height: 500px;
overflow: scroll;
padding-bottom: 30px;
h3 {
margin-top: 0;
}
p {
display: block;
font-family: monospace, monospace;
margin: 0;
}
}
.adf-form-config-editor {
height: 500px !important;
}
.adf-form-editor-buttons {
display: flex;
justify-content: space-evenly;
}
.adf-upload-config-button {
display: flex;
justify-content: center;
input {
cursor: pointer;
height: 100%;
right: 0;
opacity: 0;
position: absolute;
top: 0;
width: 300px;
z-index: 4;
}
}

View File

@@ -0,0 +1,109 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { Component, OnDestroy, OnInit } from '@angular/core';
import { FormFieldModel, NotificationService, FormRenderingService } from '@alfresco/adf-core';
import { FormCloud, FormCloudService, UploadCloudWidgetComponent } from '@alfresco/adf-process-services-cloud';
import { Subscription } from 'rxjs';
import { formDefinition } from './demo-form';
@Component({
templateUrl: 'cloud-form-demo.component.html',
styleUrls: ['cloud-form-demo.component.scss']
})
export class FormCloudDemoComponent implements OnInit, OnDestroy {
form: FormCloud;
errorFields: FormFieldModel[] = [];
formConfig: string;
editor: any;
private subscriptions: Subscription[] = [];
editorOptions = {
theme: 'vs-dark',
language: 'json',
autoIndent: true,
formatOnPaste: true,
formatOnType: true,
automaticLayout: true
};
constructor(
private notificationService: NotificationService,
private formRenderingService: FormRenderingService,
private formService: FormCloudService) {
this.formRenderingService.setComponentTypeResolver('upload', () => UploadCloudWidgetComponent, true);
}
logErrors(errorFields: FormFieldModel[]) {
this.errorFields = errorFields;
}
ngOnInit() {
this.formConfig = formDefinition;
this.parseForm();
}
onFormSaved() {
this.notificationService.openSnackMessage('Task has been saved successfully');
}
ngOnDestroy() {
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
this.subscriptions = [];
}
onInitFormEditor(editor) {
this.editor = editor;
setTimeout(() => {
this.editor.getAction('editor.action.formatDocument').run();
}, 1000);
}
parseForm() {
this.form = this.formService.parseForm(JSON.parse(this.formConfig));
}
onSaveFormConfig() {
try {
this.parseForm();
} catch (error) {
this.notificationService.openSnackMessage(
'Wrong form configuration',
4000
);
}
}
onClearFormConfig() {
this.formConfig = '';
}
onConfigAdded($event: any): void {
const file = $event.currentTarget.files[0];
const fileReader = new FileReader();
fileReader.onload = () => {
this.formConfig = <string> fileReader.result;
};
fileReader.readAsText(file);
this.onInitFormEditor(this.editor);
$event.target.value = '';
}
}

View File

@@ -0,0 +1,96 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
export const formDefinition = `{
"formRepresentation": {
"id": "text-form",
"name": "test-start-form",
"version": 0,
"description": "",
"formDefinition": {
"tabs": [],
"fields": [
{
"id": "1511517333638",
"type": "container",
"fieldType": "ContainerRepresentation",
"name": "Label",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "texttest",
"name": "texttest",
"type": "text",
"value": null,
"required": false,
"placeholder": "text",
"params": {
"existingColspan": 2,
"maxColspan": 6,
"inputMaskReversed": true,
"inputMask": "0#",
"inputMaskPlaceholder": "(0-9)"
}
}
],
"2": [{
"fieldType": "AttachFileFieldRepresentation",
"id": "attachfiletest",
"name": "attachfiletest",
"type": "upload",
"required": true,
"colspan": 2,
"placeholder": "attachfile",
"params": {
"existingColspan": 2,
"maxColspan": 2,
"fileSource": {
"serviceId": "local-file",
"name": "Local File"
},
"multiple": true,
"link": false
},
"visibilityCondition": {
}
}]
}
}
],
"outcomes": [],
"metadata": {
"property1": "value1",
"property2": "value2"
},
"variables": [
{
"name": "variable1",
"type": "string",
"value": "value1"
},
{
"name": "variable2",
"type": "string",
"value": "value2"
}
]
}
}}
`;

View File

@@ -1,19 +1,30 @@
<h4 data-automation-id="task-details-header">Simple page to show the taskId: {{ taskId }} of the app: {{ appName }}</h4> <h4 data-automation-id="task-details-header">Simple page to show the taskId: {{ taskId }} of the app: {{ appName }}</h4>
<div class="adf-task-detail-container"> <div fxLayout="column" fxFill fxLayoutGap="2px">
<div class="adf-task-control"> <div fxLayout="row" fxFill>
<button mat-button (click)="goBack()">Cancel</button> <div fxLayout="column" fxFlex="80%">
<button mat-button color="primary" *ngIf="canCompleteTask()" adf-cloud-complete-task [appName]="appName" [taskId]="taskId" <div class="adf-task-control">
(success)="onCompletedTask()">{{ 'ADF_TASK_LIST.DETAILS.BUTTON.COMPLETE' | translate }}</button> <button mat-button (click)="goBack()">Cancel</button>
<button mat-button color="primary" *ngIf="canCompleteTask()" adf-cloud-complete-task
<button mat-button color="primary" *ngIf="canClaimTask()" adf-cloud-claim-task [appName]="appName" [taskId]="taskId" (success)="onCompletedTask()">{{ 'ADF_TASK_LIST.DETAILS.BUTTON.COMPLETE' | translate }}</button>
(success)="onClaimTask()">{{ 'ADF_CLOUD_TASK_HEADER.BUTTON.CLAIM' | translate }}</button>
<button mat-button color="primary" *ngIf="canClaimTask()" adf-cloud-claim-task
<button mat-button color="primary" *ngIf="canUnClaimTask()" adf-cloud-unclaim-task [appName]="appName" [taskId]="taskId" (success)="onClaimTask()">{{ 'ADF_TASK_LIST.DETAILS.BUTTON.CLAIM' | translate }}</button>
(success)="onUnclaimTask()">{{ 'ADF_CLOUD_TASK_HEADER.BUTTON.RELEASE' | translate }}</button>
<button mat-button color="primary" *ngIf="canUnClaimTask()" adf-cloud-unclaim-task
(success)="onUnclaimTask()">{{ 'ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM' | translate }}</button>
</div>
<adf-cloud-form *ngIf="hasTaskForm()" fxFlex="100%"
[appName]="appName"
[taskId]="taskId"
(formCompleted)="onTaskCompleted()"
(formSaved)="onFormSaved()">
</adf-cloud-form>
</div>
<adf-cloud-task-header fxFlex
[appName]="appName"
[taskId]="taskId"
[readOnly]="readOnly">
</adf-cloud-task-header>
</div> </div>
</div>
<adf-cloud-task-header class="adf-demop-card-container" [appName]="appName" [taskId]="taskId" [readOnly]="readOnly">
</adf-cloud-task-header>
</div>

View File

@@ -17,7 +17,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TaskDetailsCloudModel, TaskCloudService } from '@alfresco/adf-process-services-cloud'; import { TaskDetailsCloudModel, TaskCloudService, UploadCloudWidgetComponent } from '@alfresco/adf-process-services-cloud';
import { NotificationService, FormRenderingService } from '@alfresco/adf-core';
@Component({ @Component({
templateUrl: './task-details-cloud-demo.component.html', templateUrl: './task-details-cloud-demo.component.html',
@@ -33,7 +34,9 @@ export class TaskDetailsCloudDemoComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private taskCloudService: TaskCloudService private formRenderingService: FormRenderingService,
private taskCloudService: TaskCloudService,
private notificationService: NotificationService
) { ) {
this.route.params.subscribe((params) => { this.route.params.subscribe((params) => {
this.taskId = params.taskId; this.taskId = params.taskId;
@@ -41,6 +44,8 @@ export class TaskDetailsCloudDemoComponent implements OnInit {
this.route.parent.params.subscribe((params) => { this.route.parent.params.subscribe((params) => {
this.appName = params.appName; this.appName = params.appName;
}); });
this.formRenderingService.setComponentTypeResolver('upload', () => UploadCloudWidgetComponent, true);
} }
ngOnInit() { ngOnInit() {
@@ -59,7 +64,7 @@ export class TaskDetailsCloudDemoComponent implements OnInit {
} }
canCompleteTask(): boolean { canCompleteTask(): boolean {
return this.taskDetails && this.taskCloudService.canCompleteTask(this.taskDetails); return this.taskDetails && !this.taskDetails.formKey && this.taskCloudService.canCompleteTask(this.taskDetails);
} }
canClaimTask(): boolean { canClaimTask(): boolean {
@@ -70,6 +75,10 @@ export class TaskDetailsCloudDemoComponent implements OnInit {
return this.taskDetails && this.taskCloudService.canUnclaimTask(this.taskDetails); return this.taskDetails && this.taskCloudService.canUnclaimTask(this.taskDetails);
} }
hasTaskForm(): boolean {
return this.taskDetails && this.taskDetails.formKey;
}
goBack() { goBack() {
this.router.navigate([`/cloud/${this.appName}/`]); this.router.navigate([`/cloud/${this.appName}/`]);
} }
@@ -85,4 +94,12 @@ export class TaskDetailsCloudDemoComponent implements OnInit {
onClaimTask() { onClaimTask() {
this.goBack(); this.goBack();
} }
onTaskCompleted() {
this.goBack();
}
onFormSaved() {
this.notificationService.openSnackMessage('Task has been saved successfully');
}
} }

View File

@@ -16,7 +16,8 @@
*/ */
import { Component, ViewChild } from '@angular/core'; import { Component, ViewChild } from '@angular/core';
import { FormComponent, FormModel, FormService, LogService, FormOutcomeEvent } from '@alfresco/adf-core'; import { FormModel, FormService, LogService, FormOutcomeEvent } from '@alfresco/adf-core';
import { FormComponent } from '@alfresco/adf-process-services';
@Component({ @Component({
selector: 'app-form-list', selector: 'app-form-list',

View File

@@ -74,7 +74,7 @@ export class InMemoryFormService extends FormService {
if (!json.fields) { if (!json.fields) {
form.outcomes = [ form.outcomes = [
new FormOutcomeModel(form, { new FormOutcomeModel(form, {
id: '$custom', id: '$save',
name: FormOutcomeModel.SAVE_ACTION, name: FormOutcomeModel.SAVE_ACTION,
isSystem: true isSystem: true
}) })

View File

@@ -0,0 +1,665 @@
{
"formRepresentation": {
"id": "form-with-all-fields",
"name": "Form with all fields",
"description": "",
"version": 0,
"formDefinition": {
"tabs": [],
"fields": [
{
"fieldType": "ContainerRepresentation",
"id": "26b10e64-0403-4686-a75b-0d45279ce3a8",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "text1",
"name": "Text1",
"type": "text",
"value": null,
"required": false,
"readOnly": false,
"overrideId": false,
"colspan": 1,
"placeholder": null,
"minLength": 0,
"maxLength": 0,
"minValue": null,
"maxValue": null,
"regexPattern": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
],
"2": [
{
"fieldType": "FormFieldRepresentation",
"id": "text2",
"name": "Text2",
"type": "text",
"value": null,
"required": false,
"readOnly": false,
"overrideId": false,
"colspan": 1,
"placeholder": null,
"minLength": 0,
"maxLength": 0,
"minValue": null,
"maxValue": null,
"regexPattern": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
]
}
},
{
"fieldType": "ContainerRepresentation",
"id": "69c1390a-8d8d-423c-8efb-8e43401efa42",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "multilinetext1",
"name": "Multiline text1",
"type": "multi-line-text",
"overrideId": false,
"colspan": 1,
"placeholder": null,
"minLength": 0,
"maxLength": 0,
"regexPattern": null,
"required": false,
"readOnly": false,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
],
"2": [
{
"fieldType": "FormFieldRepresentation",
"id": "multilinetext2",
"name": "Multiline text2",
"type": "multi-line-text",
"overrideId": false,
"colspan": 1,
"placeholder": null,
"minLength": 0,
"maxLength": 0,
"regexPattern": null,
"required": false,
"readOnly": false,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
]
}
},
{
"fieldType": "ContainerRepresentation",
"id": "df046463-2d65-4388-9ee1-0e1517985215",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "number1",
"overrideId": false,
"name": "Number1",
"type": "integer",
"colspan": 1,
"placeholder": null,
"readOnly": false,
"minValue": null,
"maxValue": null,
"required": false,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
],
"2": [
{
"fieldType": "FormFieldRepresentation",
"id": "number2",
"overrideId": false,
"name": "Number2",
"type": "integer",
"colspan": 1,
"placeholder": null,
"readOnly": false,
"minValue": null,
"maxValue": null,
"required": false,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
]
}
},
{
"fieldType": "ContainerRepresentation",
"id": "9672cc7b-1959-49c9-96be-3816e57bdfc1",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "checkbox1",
"name": "Checkbox1",
"type": "boolean",
"required": false,
"readOnly": false,
"colspan": 1,
"overrideId": false,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
],
"2": [
{
"fieldType": "FormFieldRepresentation",
"id": "checkbox2",
"name": "Checkbox2",
"type": "boolean",
"required": false,
"readOnly": false,
"colspan": 1,
"overrideId": false,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
]
}
},
{
"fieldType": "ContainerRepresentation",
"id": "054d193e-a899-4494-9a3e-b489315b7d57",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "dropdown1",
"name": "Dropdown1",
"type": "dropdown",
"value": null,
"required": false,
"readOnly": false,
"overrideId": false,
"colspan": 1,
"placeholder": null,
"optionType": "manual",
"options": [],
"endpoint": null,
"requestHeaders": null,
"restUrl": null,
"restResponsePath": null,
"restIdProperty": null,
"restLabelProperty": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
],
"2": [
{
"fieldType": "FormFieldRepresentation",
"id": "dropdown2",
"name": "Dropdown2",
"type": "dropdown",
"value": null,
"required": false,
"readOnly": false,
"overrideId": false,
"colspan": 1,
"placeholder": null,
"optionType": "manual",
"options": [],
"endpoint": null,
"requestHeaders": null,
"restUrl": null,
"restResponsePath": null,
"restIdProperty": null,
"restLabelProperty": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
]
}
},
{
"fieldType": "ContainerRepresentation",
"id": "1f8f0b66-e022-4667-91b4-bbbf2ddc36fb",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "amount1",
"name": "Amount1",
"type": "amount",
"value": null,
"required": false,
"readOnly": false,
"overrideId": false,
"colspan": 1,
"placeholder": "123",
"minValue": null,
"maxValue": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
},
"enableFractions": false,
"currency": "$"
}
],
"2": [
{
"fieldType": "FormFieldRepresentation",
"id": "amount2",
"name": "Amount2",
"type": "amount",
"value": null,
"required": false,
"readOnly": false,
"overrideId": false,
"colspan": 1,
"placeholder": "123",
"minValue": null,
"maxValue": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
},
"enableFractions": false,
"currency": "$"
}
]
}
},
{
"fieldType": "ContainerRepresentation",
"id": "541a368b-67ee-4a7c-ae7e-232c050b9e24",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "date1",
"name": "Date1",
"type": "date",
"overrideId": false,
"required": false,
"readOnly": false,
"colspan": 1,
"placeholder": null,
"minValue": null,
"maxValue": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
},
"dateDisplayFormat": "D-M-YYYY"
}
],
"2": [
{
"fieldType": "FormFieldRepresentation",
"id": "date2",
"name": "Date2",
"type": "date",
"overrideId": false,
"required": false,
"readOnly": false,
"colspan": 1,
"placeholder": null,
"minValue": null,
"maxValue": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
},
"dateDisplayFormat": "D-M-YYYY"
}
]
}
},
{
"fieldType": "ContainerRepresentation",
"id": "e79cb7e2-3dc1-4c79-8158-28662c28a9f3",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "radiobuttons1",
"name": "Radio buttons1",
"type": "radio-buttons",
"value": null,
"required": false,
"readOnly": false,
"overrideId": false,
"colspan": 1,
"placeholder": null,
"optionType": "manual",
"options": [
{
"id": "option_1",
"name": "Option 1"
},
{
"id": "option_2",
"name": "Option 2"
}
],
"endpoint": null,
"requestHeaders": null,
"restUrl": null,
"restResponsePath": null,
"restIdProperty": null,
"restLabelProperty": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
],
"2": [
{
"fieldType": "FormFieldRepresentation",
"id": "radiobuttons2",
"name": "Radio buttons2",
"type": "radio-buttons",
"value": null,
"required": false,
"readOnly": false,
"overrideId": false,
"colspan": 1,
"placeholder": null,
"optionType": "manual",
"options": [
{
"id": "option_1",
"name": "Option 1"
},
{
"id": "option_2",
"name": "Option 2"
}
],
"endpoint": null,
"requestHeaders": null,
"restUrl": null,
"restResponsePath": null,
"restIdProperty": null,
"restLabelProperty": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
]
}
},
{
"fieldType": "ContainerRepresentation",
"id": "7c01ed35-be86-4be7-9c28-ed640a5a2ae1",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "AttachFileFieldRepresentation",
"id": "attachfile1",
"name": "Attach file1",
"type": "upload",
"value": null,
"required": false,
"readOnly": false,
"overrideId": false,
"colspan": 1,
"placeholder": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2,
"fileSource": {
"serviceId": "all-file-sources",
"name": "All file sources"
},
"multiple": false,
"link": false
}
}
],
"2": [
{
"fieldType": "AttachFileFieldRepresentation",
"id": "attachfile2",
"name": "Attach file2",
"type": "upload",
"value": null,
"required": false,
"readOnly": false,
"overrideId": false,
"colspan": 1,
"placeholder": null,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2,
"fileSource": {
"serviceId": "all-file-sources",
"name": "All file sources"
},
"multiple": false,
"link": false
}
}
]
}
},
{
"fieldType": "ContainerRepresentation",
"id": "07b13b96-d469-4a1e-8a9a-9bb957c68869",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "displayvalue1",
"name": "Display value1",
"type": "readonly",
"value": "No field selected",
"readOnly": false,
"overrideId": false,
"colspan": 1,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2,
"field": {
"id": "displayvalue",
"name": "Display value",
"type": "text"
}
}
}
],
"2": [
{
"fieldType": "FormFieldRepresentation",
"id": "displayvalue2",
"name": "Display value2",
"type": "readonly",
"value": "No field selected",
"readOnly": false,
"overrideId": false,
"colspan": 1,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2,
"field": {
"id": "displayvalue",
"name": "Display value",
"type": "text"
}
}
}
]
}
},
{
"fieldType": "ContainerRepresentation",
"id": "1576ef25-c842-494c-ab84-265a1e3bf68d",
"name": "Label",
"type": "container",
"tab": null,
"numberOfColumns": 2,
"fields": {
"1": [
{
"fieldType": "FormFieldRepresentation",
"id": "displaytext1",
"name": "Display text1",
"type": "readonly-text",
"value": "Display text as part of the form",
"readOnly": false,
"overrideId": false,
"colspan": 1,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
],
"2": [
{
"fieldType": "FormFieldRepresentation",
"id": "displaytext2",
"name": "Display text2",
"type": "readonly-text",
"value": "Display text as part of the form",
"readOnly": false,
"overrideId": false,
"colspan": 1,
"visibilityCondition": null,
"params": {
"existingColspan": 1,
"maxColspan": 2
}
}
]
}
}
],
"outcomes": [],
"javascriptEvents": [],
"className": "",
"style": "",
"customFieldTemplates": {},
"metadata": {},
"variables": [
{
"name": "FormVarStr",
"type": "string",
"value": ""
},
{
"name": "FormVarInt",
"type": "integer",
"value": ""
},
{
"name": "FormVarBool",
"type": "boolean",
"value": ""
},
{
"name": "FormVarDate",
"type": "date",
"value": ""
},
{
"name": "NewVar",
"type": "string",
"value": ""
}
],
"customFieldsValueInfo": {},
"gridsterForm": false
}
},
"processScopeIdentifiers": []
}

View File

@@ -0,0 +1,262 @@
---
Title: Form component
Added: v3.2.0
Status: Active
Last reviewed: 2019-04-01
---
# [Form cloud component](../../../lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts "Defined in form-cloud.component.ts")
Shows a [`form`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts) from Process Services
## Contents
- [Basic Usage](#basic-usage)
- [Empty form template](#empty-form-template)
- [Class members](#class-members)
- [Properties](#properties)
- [Events](#events)
- [Details](#details)
- [Displaying a form](#displaying-a-form)
- [Controlling outcome execution behaviour](#controlling-outcome-execution-behaviour)
- [Field Validators](#field-validators)
- [Common scenarios](#common-scenarios)
- [See also](#see-also)
## Basic Usage
```html
<adf-cloud-form
[appName]="appName"
[taskId]="taskId">
</adf-cloud-form>
```
### Empty form template
The template defined inside `empty-form` will be shown when no form definition is found:
```html
<adf-cloud-form .... >
<div empty-form >
<h2>Empty form</h2>
</div>
</adf-cloud-form>
```
## Class members
### Properties
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| appName | `string` | | App id to fetch corresponding form and values. |
| taskId | `string` | | Task id to fetch corresponding form and values. |
| form | [`FormCloudModel`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts) | | Underlying [form model](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts) instance. |
| formId | `string` | | The id of the form definition to load and display with custom values. |
| data | [`TaskVariableCloud[]`](../../../lib/process-services-cloud/src/lib/form/models/task-variable.model.ts) | | Custom form values map to be used with the rendered form. |
| disableCompleteButton | `boolean` | false | If true then the `Complete` outcome button is shown but it will be disabled. |
| disableStartProcessButton | `boolean` | false | If true then the `Start Process` outcome button is shown but it will be disabled. |
| fieldValidators | [`FormFieldValidator`](../../../lib/core/form/components/widgets/core/form-field-validator.ts)`[]` | \[] | Contains a list of form field validator instances. |
| readOnly | `boolean` | false | Toggle readonly state of the form. Forces all form widgets to render as readonly if enabled. |
| showCompleteButton | `boolean` | true | Toggle rendering of the `Complete` outcome button. |
| showDebugButton | `boolean` | false | Toggle debug options. |
| showRefreshButton | `boolean` | true | Toggle rendering of the `Refresh` button. |
| showSaveButton | `boolean` | true | Toggle rendering of the `Save` outcome button. |
| showTitle | `boolean` | true | Toggle rendering of the form title. |
| showValidationIcon | `boolean` | true | Toggle rendering of the validation icon next to the form title. |
### Events
| Name | Type | Description |
| ---- | ---- | ----------- |
| error | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when any error occurs. |
| executeOutcome | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormOutcomeEvent`](../../../lib/core/form/components/widgets/core/form-outcome-event.model.ts)`>` | Emitted when any outcome is executed. Default behaviour can be prevented via `event.preventDefault()`. |
| formCompleted | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormCloudModel`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts)`>` | Emitted when the form is submitted with the `Complete` outcome. |
| formDataRefreshed | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormCloudModel`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts)`>` | Emitted when form values are refreshed due to a data property change. |
| 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)`<`[`FormCloudModel`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts)`>` | Emitted when the form is loaded or reloaded. |
| formSaved | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormCloudModel`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts)`>` | Emitted when the form is submitted with the `Save` or custom outcomes. |
## Details
All `formXXX` events receive a [`FormCloudModel`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts) instance as their argument:
**MyView.component.html**
```html
<adf-cloud-form
[appName]="appName"
[taskId]="selectedTask?.id"
(formSaved)="onFormSaved($event)">
</adf-cloud-form>
```
**MyView.component.ts**
```ts
onFormSaved(form: FormCloudModel) {
console.log(form);
}
```
### Displaying a form
There are various ways to display a form. The common scenarios are detailed below.
#### Displaying a form instance by task id
```html
<adf-cloud-form
[appName]="appName"
[taskId]="selectedTask?.id">
</adf-cloud-form>
```
For an existing Task both the form and its values will be fetched and displayed.
#### Displaying a form definition by form id
```html
<adf-cloud-form
[appName]="appName"
[formId]="selectedFormDefinition?.id"
[data]="customData">
</adf-cloud-form>
```
In this case, only the form definition will be fetched.
### Controlling outcome execution behaviour
In unusual circumstances, you may need to take complete control of form outcome execution.
You can do this by implementing the `executeOutcome` event, which is emitted for both system
outcomes and custom ones.
Note that by default, the code in your `executeOutcome` handler is executed _before_ the default
behavior but you can switch the default behavior off using `event.preventDefault()`.
You might want to do this, for example, to provide custom form validation or to show a summary
of the form validation before it is submitted.
**MyView.component.html**
```html
<adf-cloud-form
[appName]="appName"
[taskId]="selectedTask?.id"
executeOutcome="validateForm($event)">
</adf-cloud-form>
```
**MyView.component.ts**
```ts
import { FormOutcomeEvent } from '@alfresco/adf-core';
export class MyView {
validateForm(event: FormOutcomeEvent) {
let outcome = event.outcome;
// you can also get additional properties of outcomes
// if you defined them within outcome definition
if (outcome) {
let form = outcome.form;
if (form) {
// check/update the form here
event.preventDefault();
}
}
}
}
```
There are two other functions that can be very useful when you need to control form outcomes:
- `saveTaskForm()` - Saves the current form
- `completeTaskForm(outcome?: string)` Saves and completes the form with a given outcome name
### Field Validators
You can supply a set of validator objects to the form using the `fieldValidators`
property. Each validator implements a check for a particular type of data (eg, a
date validator might check that the date in the field falls between 1980 and 2017).
ADF supplies a standard set of validators that handle most common cases but you can
also implement your own custom validators to replace or extend the set. See the
[Form Field Validator](../../core/interfaces/form-field-validator.interface.md) interface for full details and examples.
### Common scenarios
#### Rendering a form using form definition JSON
See the [demo-form](../../docassets/demo-cloud.form.json) file for an example of form definition JSON.
The component below (with the JSON assigned to the `formDefinitionJSON` property), shows how a
form definition is rendered:
```ts
@Component({
selector: 'sample-form',
template: `<div class="form-container">
<adf-cloud-form
[form]="form">
</adf-cloud-form>
</div>`
})
export class SampleFormComponent implements OnInit {
form: FormCloudModel;
formDefinitionJSON: any;
constructor(private formService: FormService) {
}
ngOnInit() {
this.form = this.formService.parseForm(this.formDefinitionJSON);
}
}
```
#### Customizing the styles of form outcome buttons
You can use normal CSS selectors to style the outcome buttons of your form.
Every outcome has an CSS id value following a simple pattern:
adf-cloud-form-OUTCOME_NAME
In the CSS, you can target any outcome ID and change the style as in this example:
```css
#adf-cloud-form-complete {
background-color: blue !important;
color: white;
}
#adf-cloud-form-save {
background-color: green !important;
color: white;
}
#adf-cloud-form-customoutcome {
background-color: yellow !important;
color: white;
}
```
![](../../docassets/images/form-style-sample.png)
## See also
- [Form Field Validator interface](../../core/interfaces/form-field-validator.interface.md)
- [Extensibility](../../user-guide/extensibility.md)
- [Form rendering service](../../core/services/form-rendering.service.md)
- [Form field model](../../core/models/form-field.model.md)
- [Form service](../services/form-cloud.service.md)

View File

@@ -0,0 +1,67 @@
---
Title: Form service
Title: Form cloud service
Added: v3.2.0
Status: Active
Last reviewed: 2019-04-02
---
# [Form cloud service](../../../lib/process-services-cloud/src/lib/form/services/form-cloud.service.ts "Defined in form-cloud.service.ts")
Implements Process Services form methods
## Basic Usage
```ts
import { FormService } from '@alfresco/adf-core';
@Component(...)
class MyComponent {
constructor(formService: FormService) {
}
```
### Methods
- `parseForm(json: any, data?:`[`TaskVariableCloud,`](../../../lib/process-services-cloud/src/lib/form/models/task-variable-cloud.model.ts)`readOnly: boolean = false):`[`FormModel`](../../../lib/core/form/components/widgets/core/form.model.ts)
Parses JSON data to create a corresponding [`Form`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts) model.
- `json` - JSON to create the form
- `data` - (Optional) [`Values`](../../../lib/process-services-cloud/src/lib/form/models/task-variable-cloud.model.ts) for the form fields
- `readOnly` - Should the form fields be read-only?
- `saveTaskForm(appName: string, taskId: string, formId: string, formValues: FormValues):`[`Observable`](http://reactivex.io/documentation/observable.html)`<any>`
Saves task [`form`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts).
- `appName` - App Name
- `taskId` - Task Id
- `formId` - Form Id
- `formValues` - [`Form Values`](../../../lib/core/form/components/widgets/core/form-values.ts)
- `completeTaskForm(appName: string, taskId: string, formId: string, formValues: FormValues, outcome: string):`[`Observable`](http://reactivex.io/documentation/observable.html)`<any>`
Completes task [`form`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts)
- `appName` - App Name
- `taskId` - Task Id
- `formId` - Form Id
- `formValues` - [`Form Values`](../../../lib/core/form/components/widgets/core/form-values.ts)
- `outcome` - (Optional) [`Form`](../../../lib/process-services-cloud/src/lib/form/models/form-cloud.model.ts) Outcome
- `getTaskForm(appName: string, taskId: string):`[`Observable`](http://reactivex.io/documentation/observable.html)`<any>`
Get form defintion of a task
- `appName` - App Name
- `taskId` - Task Id
- `getForm(appName: string, formId: string):`[`Observable`](http://reactivex.io/documentation/observable.html)`<any>`
Get a form definition
- `appName` - App Name
- `formId` - Form Id
- `getTask(appName: string, taskId: string):`[`Observable`](http://reactivex.io/documentation/observable.html)<[`TaskDetailsCloudModel`](../../../lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts)>
Gets details of a task.
- `appName` - App Name
- `taskId` - Task Id
- `getTaskVariables(appName: string, taskId: string):`[`Observable`](http://reactivex.io/documentation/observable.html)<[`TaskVariableCloud`](../../../lib/process-services-cloud/src/lib/form/models/task-variable-cloud.model.ts)[]>
Gets variables of a task.
- `appName` - App Name
- `taskId` - Task Id

View File

@@ -5,9 +5,9 @@ Status: Active
Last reviewed: 2019-01-16 Last reviewed: 2019-01-16
--- ---
# [Form component](../../../lib/core/form/components/form.component.ts "Defined in form.component.ts") # [Form component](../../../lib/process-services/form/form.component.ts "Defined in form.component.ts")
Shows a [`Form`](../../../lib/process-services/task-list/models/form.model.ts) from APS Shows a [`Form`](../../../lib/core/form/components/widgets/core/form.model.ts) from APS
(See it live: [Form Quickstart](https://embed.plnkr.co/YSLXTqb3DtMhVJSqXKkE/)) (See it live: [Form Quickstart](https://embed.plnkr.co/YSLXTqb3DtMhVJSqXKkE/))
@@ -279,7 +279,7 @@ could use this, say, to provide two alternative ways of entering the same inform
up default values that can be edited. up default values that can be edited.
You can implement this in ADF using the `formFieldValueChanged` event of the You can implement this in ADF using the `formFieldValueChanged` event of the
[Form service](../services/form.service.md). For example, if you had a form with a dropdown widget (id: `type`) [Form service](../../core/services/form.service.md). For example, if you had a form with a dropdown widget (id: `type`)
and a multiline text (id:`description`), you could synchronize their values as follows: and a multiline text (id:`description`), you could synchronize their values as follows:
```ts ```ts
@@ -305,7 +305,7 @@ The result should look like the following:
#### Responding to all form events #### Responding to all form events
Subscribe to the `formEvents` event of the [Form service](../services/form.service.md) to get notification Subscribe to the `formEvents` event of the [Form service](../../core/services/form.service.md) to get notification
of all form events: of all form events:
```ts ```ts
@@ -361,8 +361,8 @@ Also, don't forget to set the `providers` property to `ALL` in the `app.config.j
## See also ## See also
- [Form Field Validator interface](../interfaces/form-field-validator.interface.md) - [Form Field Validator interface](../../core/interfaces/form-field-validator.interface.md)
- [Extensibility](../../user-guide/extensibility.md) - [Extensibility](../../user-guide/extensibility.md)
- [Form rendering service](../services/form-rendering.service.md) - [Form rendering service](../../core/services/form-rendering.service.md)
- [Form field model](../models/form-field.model.md) - [Form field model](../../core/models/form-field.model.md)
- [Form service](../services/form.service.md) - [Form service](../../core/services/form.service.md)

View File

@@ -35,7 +35,7 @@ import { HostSettingsModule } from './settings/host-settings.module';
import { ToolbarModule } from './toolbar/toolbar.module'; import { ToolbarModule } from './toolbar/toolbar.module';
import { UserInfoModule } from './userinfo/userinfo.module'; import { UserInfoModule } from './userinfo/userinfo.module';
import { ViewerModule } from './viewer/viewer.module'; import { ViewerModule } from './viewer/viewer.module';
import { FormModule } from './form/form.module'; import { FormBaseModule } from './form/form-base.module';
import { SidenavLayoutModule } from './layout/layout.module'; import { SidenavLayoutModule } from './layout/layout.module';
import { CommentsModule } from './comments/comments.module'; import { CommentsModule } from './comments/comments.module';
import { ButtonsMenuModule } from './buttons-menu/buttons-menu.module'; import { ButtonsMenuModule } from './buttons-menu/buttons-menu.module';
@@ -75,7 +75,7 @@ import { ExtensionsModule } from '@alfresco/adf-extensions';
ToolbarModule, ToolbarModule,
ContextMenuModule, ContextMenuModule,
CardViewModule, CardViewModule,
FormModule, FormBaseModule,
CommentsModule, CommentsModule,
LoginModule, LoginModule,
LanguageMenuModule, LanguageMenuModule,
@@ -106,7 +106,7 @@ import { ExtensionsModule } from '@alfresco/adf-extensions';
ToolbarModule, ToolbarModule,
ContextMenuModule, ContextMenuModule,
CardViewModule, CardViewModule,
FormModule, FormBaseModule,
CommentsModule, CommentsModule,
LoginModule, LoginModule,
LanguageMenuModule, LanguageMenuModule,

View File

@@ -0,0 +1,213 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { FormBaseModel } from './form-base.model';
import { FormOutcomeModel, FormFieldValidator, FormFieldModel, FormOutcomeEvent } from './widgets';
import { EventEmitter, Input, Output } from '@angular/core';
export abstract class FormBaseComponent {
static SAVE_OUTCOME_ID: string = '$save';
static COMPLETE_OUTCOME_ID: string = '$complete';
static START_PROCESS_OUTCOME_ID: string = '$startProcess';
static CUSTOM_OUTCOME_ID: string = '$custom';
static COMPLETE_BUTTON_COLOR: string = 'primary';
static COMPLETE_OUTCOME_NAME: string = 'COMPLETE';
/** Path of the folder where the metadata will be stored. */
@Input()
path: string;
/** Name to assign to the new node where the metadata are stored. */
@Input()
nameNode: string;
/** Toggle rendering of the form title. */
@Input()
showTitle: boolean = true;
/** Toggle rendering of the `Complete` outcome button. */
@Input()
showCompleteButton: boolean = true;
/** If true then the `Complete` outcome button is shown but it will be disabled. */
@Input()
disableCompleteButton: boolean = false;
/** If true then the `Start Process` outcome button is shown but it will be disabled. */
@Input()
disableStartProcessButton: boolean = false;
/** Toggle rendering of the `Save` outcome button. */
@Input()
showSaveButton: boolean = true;
/** Toggle readonly state of the form. Forces all form widgets to render as readonly if enabled. */
@Input()
readOnly: boolean = false;
/** Toggle rendering of the `Refresh` button. */
@Input()
showRefreshButton: boolean = true;
/** Toggle rendering of the validation icon next to the form title. */
@Input()
showValidationIcon: boolean = true;
/** Contains a list of form field validator instances. */
@Input()
fieldValidators: FormFieldValidator[] = [];
/** Emitted when the supplied form values have a validation error. */
@Output()
formError: EventEmitter<FormFieldModel[]> = new EventEmitter<FormFieldModel[]>();
/** Emitted when any outcome is executed. Default behaviour can be prevented
* via `event.preventDefault()`.
*/
@Output()
executeOutcome: EventEmitter<FormOutcomeEvent> = new EventEmitter<FormOutcomeEvent>();
/**
* Emitted when any error occurs.
*/
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
form: FormBaseModel;
getParsedFormDefinition(): FormBaseComponent {
return this;
}
hasForm(): boolean {
return this.form ? true : false;
}
isTitleEnabled(): boolean {
let titleEnabled = false;
if (this.showTitle && this.form) {
titleEnabled = true;
}
return titleEnabled;
}
getColorForOutcome(outcomeName: string): string {
return outcomeName === FormBaseComponent.COMPLETE_OUTCOME_NAME ? FormBaseComponent.COMPLETE_BUTTON_COLOR : '';
}
isOutcomeButtonEnabled(outcome: FormOutcomeModel): boolean {
if (this.form.readOnly) {
return false;
}
if (outcome) {
// Make 'Save' button always available
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return true;
}
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return this.disableCompleteButton ? false : this.form.isValid;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return this.disableStartProcessButton ? false : this.form.isValid;
}
return this.form.isValid;
}
return false;
}
isOutcomeButtonVisible(outcome: FormOutcomeModel, isFormReadOnly: boolean): boolean {
if (outcome && outcome.name) {
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return this.showCompleteButton;
}
if (isFormReadOnly) {
return outcome.isSelected;
}
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return this.showSaveButton;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return false;
}
return true;
}
return false;
}
/**
* Invoked when user clicks outcome button.
* @param outcome Form outcome model
*/
onOutcomeClicked(outcome: FormOutcomeModel): boolean {
if (!this.readOnly && outcome && this.form) {
if (!this.onExecuteOutcome(outcome)) {
return false;
}
if (outcome.isSystem) {
if (outcome.id === FormBaseComponent.SAVE_OUTCOME_ID) {
this.saveTaskForm();
return true;
}
if (outcome.id === FormBaseComponent.COMPLETE_OUTCOME_ID) {
this.completeTaskForm();
return true;
}
if (outcome.id === FormBaseComponent.START_PROCESS_OUTCOME_ID) {
this.completeTaskForm();
return true;
}
if (outcome.id === FormBaseComponent.CUSTOM_OUTCOME_ID) {
this.onTaskSaved(this.form);
this.storeFormAsMetadata();
return true;
}
} else {
// Note: Activiti is using NAME field rather than ID for outcomes
if (outcome.name) {
this.onTaskSaved(this.form);
this.completeTaskForm(outcome.name);
return true;
}
}
}
return false;
}
handleError(err: any): any {
this.error.emit(err);
}
abstract onRefreshClicked();
abstract saveTaskForm();
abstract completeTaskForm(outcome?: string);
protected abstract onTaskSaved(form: FormBaseModel);
protected abstract storeFormAsMetadata();
protected abstract onExecuteOutcome(outcome: FormOutcomeModel);
}

View File

@@ -0,0 +1,84 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { FormValues } from './widgets/core/form-values';
import { TabModel } from './widgets/core/tab.model';
import { FormWidgetModel } from './widgets/core/form-widget.model';
import { FormOutcomeModel } from './widgets/core/form-outcome.model';
import { FormFieldModel } from './widgets/core/form-field.model';
import { ContainerModel } from './widgets/core/container.model';
export abstract class FormBaseModel {
static UNSET_TASK_NAME: string = 'Nameless task';
static SAVE_OUTCOME: string = '$save';
static COMPLETE_OUTCOME: string = '$complete';
static START_PROCESS_OUTCOME: string = '$startProcess';
json: any;
isValid: boolean;
values: FormValues = {};
tabs: TabModel[] = [];
fields: FormWidgetModel[] = [];
outcomes: FormOutcomeModel[] = [];
className: string;
readOnly: boolean = false;
taskName;
hasTabs(): boolean {
return this.tabs && this.tabs.length > 0;
}
hasFields(): boolean {
return this.fields && this.fields.length > 0;
}
hasOutcomes(): boolean {
return this.outcomes && this.outcomes.length > 0;
}
getFieldById(fieldId: string): FormFieldModel {
return this.getFormFields().find((field) => field.id === fieldId);
}
// TODO: consider evaluating and caching once the form is loaded
getFormFields(): FormFieldModel[] {
const formFieldModel: FormFieldModel[] = [];
for (let i = 0; i < this.fields.length; i++) {
const field = this.fields[i];
if (field instanceof ContainerModel) {
const container = <ContainerModel> field;
formFieldModel.push(container.field);
container.field.columns.forEach((column) => {
formFieldModel.push(...column.fields);
});
}
}
return formFieldModel;
}
abstract validateForm();
abstract validateField(field: FormFieldModel);
abstract onFormFieldChanged(field: FormFieldModel);
abstract markAsInvalid();
}

View File

@@ -0,0 +1,25 @@
<div class="{{formDefinition.className}}" [ngClass]="{'adf-readonly-form': formDefinition.readOnly }">
<div *ngIf="formDefinition.hasTabs()">
<tabs-widget [tabs]="formDefinition.tabs"></tabs-widget>
</div>
<div *ngIf="!formDefinition.hasTabs() && formDefinition.hasFields()">
<div *ngFor="let field of formDefinition.fields">
<adf-form-field [field]="field.field"></adf-form-field>
</div>
</div>
</div>
<!--
For debugging and data visualisation purposes,
will be removed during future revisions
-->
<div *ngIf="showDebugButton" class="adf-form-debug-container">
<mat-slide-toggle [(ngModel)]="debugMode">Debug mode</mat-slide-toggle>
<div *ngIf="debugMode">
<h4>Values</h4>
<pre>{{formDefinition.values | json}}</pre>
<h4>Form</h4>
<pre>{{formDefinition.json | json}}</pre>
</div>
</div>

View File

@@ -1,4 +1,4 @@
@mixin adf-form-component-theme($theme) { @mixin adf-form-renderer-theme($theme) {
$config: mat-typography-config(); $config: mat-typography-config();
$warn: map-get($theme, warn); $warn: map-get($theme, warn);

View File

@@ -0,0 +1,38 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { Component, ViewEncapsulation, Input } from '@angular/core';
import { FormBaseModel } from './form-base.model';
@Component({
selector: 'adf-form-renderer',
templateUrl: './form-renderer.component.html',
styleUrls: ['./form-renderer.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class FormRendererComponent {
/** Toggle debug options. */
@Input()
showDebugButton: boolean = false;
@Input()
formDefinition: FormBaseModel;
debugMode: boolean;
}

View File

@@ -1,64 +0,0 @@
<div *ngIf="!hasForm()">
<ng-content select="[empty-form]">
</ng-content>
</div>
<div *ngIf="hasForm()" class="{{form.className}} adf-form-container" [ngClass]="{'adf-readonly-form': readOnly }">
<mat-card>
<mat-card-header>
<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="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">{{form.taskName}}</span>
</h4>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngIf="form.hasTabs()">
<tabs-widget [tabs]="form.tabs"></tabs-widget>
</div>
<div *ngIf="!form.hasTabs() && form.hasFields()">
<div *ngFor="let field of form.fields">
<adf-form-field [field]="field.field"></adf-form-field>
</div>
</div>
</mat-card-content>
<mat-card-actions *ngIf="form.hasOutcomes()" class="adf-form-mat-card-actions">
<!--[class.mdl-button--colored]="!outcome.isSystem"-->
<button [id]="'adf-form-'+ outcome.name | formatSpace" *ngFor="let outcome of form.outcomes"
[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>
</mat-card-actions>
</mat-card>
</div>
<!--
For debugging and data visualisation purposes,
will be removed during future revisions
-->
<div *ngIf="showDebugButton" class="adf-form-debug-container">
<mat-slide-toggle [(ngModel)]="debugMode">Debug mode</mat-slide-toggle>
<div *ngIf="debugMode && hasForm()">
<h4>Values</h4>
<pre>{{form.values | json}}</pre>
<h4>Form</h4>
<pre>{{form.json | json}}</pre>
</div>
</div>

View File

@@ -27,7 +27,7 @@ export abstract class FormWidgetModel {
readonly type: string; readonly type: string;
readonly tab: string; readonly tab: string;
readonly form: FormModel; readonly form: any;
readonly json: any; readonly json: any;
constructor(form: FormModel, json: any) { constructor(form: FormModel, json: any) {

View File

@@ -34,13 +34,9 @@ import {
FORM_FIELD_VALIDATORS, FORM_FIELD_VALIDATORS,
FormFieldValidator FormFieldValidator
} from './form-field-validator'; } from './form-field-validator';
import { FormBaseModel } from '../../form-base.model';
export class FormModel { export class FormModel extends FormBaseModel {
static UNSET_TASK_NAME: string = 'Nameless task';
static SAVE_OUTCOME: string = '$save';
static COMPLETE_OUTCOME: string = '$complete';
static START_PROCESS_OUTCOME: string = '$startProcess';
readonly id: number; readonly id: number;
readonly name: string; readonly name: string;
@@ -53,34 +49,14 @@ export class FormModel {
return this._isValid; return this._isValid;
} }
className: string;
readOnly: boolean = false;
tabs: TabModel[] = [];
/** Stores root containers */
fields: FormWidgetModel[] = [];
outcomes: FormOutcomeModel[] = [];
customFieldTemplates: FormFieldTemplates = {}; customFieldTemplates: FormFieldTemplates = {};
fieldValidators: FormFieldValidator[] = [...FORM_FIELD_VALIDATORS]; fieldValidators: FormFieldValidator[] = [...FORM_FIELD_VALIDATORS];
readonly selectedOutcome: string; readonly selectedOutcome: string;
values: FormValues = {};
processVariables: any; processVariables: any;
readonly json: any;
hasTabs(): boolean {
return this.tabs && this.tabs.length > 0;
}
hasFields(): boolean {
return this.fields && this.fields.length > 0;
}
hasOutcomes(): boolean {
return this.outcomes && this.outcomes.length > 0;
}
constructor(json?: any, formValues?: FormValues, readOnly: boolean = false, protected formService?: FormService) { constructor(json?: any, formValues?: FormValues, readOnly: boolean = false, protected formService?: FormService) {
super();
this.readOnly = readOnly; this.readOnly = readOnly;
if (json) { if (json) {
@@ -156,30 +132,6 @@ export class FormModel {
} }
} }
getFieldById(fieldId: string): FormFieldModel {
return this.getFormFields().find((field) => field.id === fieldId);
}
// TODO: consider evaluating and caching once the form is loaded
getFormFields(): FormFieldModel[] {
const formFieldModel: FormFieldModel[] = [];
for (let i = 0; i < this.fields.length; i++) {
const field = this.fields[i];
if (field instanceof ContainerModel) {
const container = <ContainerModel> field;
formFieldModel.push(container.field);
container.field.columns.forEach((column) => {
formFieldModel.push(...column.fields);
});
}
}
return formFieldModel;
}
markAsInvalid() { markAsInvalid() {
this._isValid = false; this._isValid = false;
} }

View File

@@ -32,11 +32,10 @@ import { StartFormCustomButtonDirective } from './components/form-custom-button.
import { FormFieldComponent } from './components/form-field/form-field.component'; import { FormFieldComponent } from './components/form-field/form-field.component';
import { FormListComponent } from './components/form-list.component'; import { FormListComponent } from './components/form-list.component';
import { FormComponent } from './components/form.component';
import { StartFormComponent } from './components/start-form.component';
import { ContentWidgetComponent } from './components/widgets/content/content.widget'; import { ContentWidgetComponent } from './components/widgets/content/content.widget';
import { WidgetComponent } from './components/widgets/widget.component'; import { WidgetComponent } from './components/widgets/widget.component';
import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
import { FormRendererComponent } from './components/form-renderer.component';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -55,9 +54,8 @@ import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimep
declarations: [ declarations: [
ContentWidgetComponent, ContentWidgetComponent,
FormFieldComponent, FormFieldComponent,
FormComponent,
FormListComponent, FormListComponent,
StartFormComponent, FormRendererComponent,
StartFormCustomButtonDirective, StartFormCustomButtonDirective,
...WIDGET_DIRECTIVES, ...WIDGET_DIRECTIVES,
...MASK_DIRECTIVE, ...MASK_DIRECTIVE,
@@ -69,12 +67,11 @@ import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimep
exports: [ exports: [
ContentWidgetComponent, ContentWidgetComponent,
FormFieldComponent, FormFieldComponent,
FormComponent,
FormListComponent, FormListComponent,
StartFormComponent, FormRendererComponent,
StartFormCustomButtonDirective, StartFormCustomButtonDirective,
...WIDGET_DIRECTIVES ...WIDGET_DIRECTIVES
] ]
}) })
export class FormModule { export class FormBaseModule {
} }

View File

@@ -15,10 +15,10 @@
* limitations under the License. * limitations under the License.
*/ */
export * from './components/form.component'; export * from './components/form-base.component';
export * from './components/form-list.component'; export * from './components/form-list.component';
export * from './components/widgets/content/content.widget'; export * from './components/widgets/content/content.widget';
export * from './components/start-form.component'; export * from './components/form-renderer.component';
export * from './components/widgets/index'; export * from './components/widgets/index';
export * from './components/widgets/dynamic-table/dynamic-table-row.model'; export * from './components/widgets/dynamic-table/dynamic-table-row.model';
@@ -32,4 +32,4 @@ export * from './services/widget-visibility.service';
export * from './events/index'; export * from './events/index';
export * from './form.module'; export * from './form-base.module';

View File

@@ -106,7 +106,7 @@ export class FormService {
if (!json.fields) { if (!json.fields) {
form.outcomes = [ form.outcomes = [
new FormOutcomeModel(form, { new FormOutcomeModel(form, {
id: '$custom', id: '$save',
name: FormOutcomeModel.SAVE_ACTION, name: FormOutcomeModel.SAVE_ACTION,
isSystem: true isSystem: true
}) })

View File

@@ -26,6 +26,9 @@
"AT_LEAST_LONG": "Enter at least {{ minLength }} characters", "AT_LEAST_LONG": "Enter at least {{ minLength }} characters",
"NO_LONGER_THAN": "Enter no more than {{ maxLength }} characters" "NO_LONGER_THAN": "Enter no more than {{ maxLength }} characters"
} }
},
"FORM_RENDERER": {
"NAMELESS_TASK": "Nameless task"
} }
}, },
"CORE": { "CORE": {

View File

@@ -19,7 +19,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Pagination } from '@alfresco/js-api'; import { Pagination } from '@alfresco/js-api';
import { PaginationComponent } from './pagination.component'; import { PaginationComponent } from './pagination.component';
import { PaginatedComponent } from './public-api'; import { PaginatedComponent } from './paginated-component.interface';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { setupTestBed } from '../testing/setupTestBed'; import { setupTestBed } from '../testing/setupTestBed';
import { CoreTestingModule } from '../testing/core.testing.module'; import { CoreTestingModule } from '../testing/core.testing.module';

View File

@@ -20,7 +20,7 @@
@import '../viewer/components/pdfViewer-thumbnails.component'; @import '../viewer/components/pdfViewer-thumbnails.component';
@import '../viewer/components/txtViewer.component'; @import '../viewer/components/txtViewer.component';
@import '../viewer/components/imgViewer.component'; @import '../viewer/components/imgViewer.component';
@import '../form/components/form.component'; @import '../form/components/form-renderer.component';
@import '../layout/components/sidebar-action/sidebar-action-menu.component'; @import '../layout/components/sidebar-action/sidebar-action-menu.component';
@import '../layout/components/header/header.component'; @import '../layout/components/header/header.component';
@import '../comments/comment-list.component'; @import '../comments/comment-list.component';
@@ -54,7 +54,7 @@
@include adf-pdf-thumbnails-theme($theme); @include adf-pdf-thumbnails-theme($theme);
@include adf-image-viewer-theme($theme); @include adf-image-viewer-theme($theme);
@include adf-text-viewer-theme($theme); @include adf-text-viewer-theme($theme);
@include adf-form-component-theme($theme); @include adf-form-renderer-theme($theme);
@include adf-sidebar-action-menu-theme($theme); @include adf-sidebar-action-menu-theme($theme);
@include adf-task-list-comment-list-theme($theme); @include adf-task-list-comment-list-theme($theme);
@include adf-task-list-comment-theme($theme); @include adf-task-list-comment-theme($theme);

View File

@@ -0,0 +1,46 @@
<div *ngIf="!hasForm()">
<ng-content select="[empty-form]">
</ng-content>
</div>
<div *ngIf="hasForm()" class="adf-form-container">
<mat-card>
<mat-card-header>
<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="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">
{{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">
<button [id]="'adf-form-'+ outcome.name | formatSpace" *ngFor="let outcome of form.outcomes"
[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>
</mat-card-actions>
</mat-card>
</div>

View File

@@ -0,0 +1,753 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { SimpleChange } from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { FormFieldModel, FormFieldTypes, FormOutcomeEvent, FormOutcomeModel, LogService, WidgetVisibilityService } from '@alfresco/adf-core';
import { FormCloudService } from '../services/form-cloud.service';
import { FormCloudComponent } from './form-cloud.component';
import { FormCloud } from '../models/form-cloud.model';
import { cloudFormMock } from '../mocks/cloud-form.mock';
describe('FormCloudComponent', () => {
let formService: FormCloudService;
let formComponent: FormCloudComponent;
let visibilityService: WidgetVisibilityService;
let logService: LogService;
beforeEach(() => {
logService = new LogService(null);
visibilityService = new WidgetVisibilityService(null, logService);
spyOn(visibilityService, 'refreshVisibility').and.stub();
formService = new FormCloudService(null, null, logService);
formComponent = new FormCloudComponent(formService, visibilityService);
});
it('should check form', () => {
expect(formComponent.hasForm()).toBeFalsy();
formComponent.form = new FormCloud();
expect(formComponent.hasForm()).toBeTruthy();
});
it('should allow title if showTitle is true', () => {
const formModel = new FormCloud();
formComponent.form = formModel;
expect(formComponent.showTitle).toBeTruthy();
expect(formComponent.isTitleEnabled()).toBeTruthy();
});
it('should not allow title if showTitle is false', () => {
const formModel = new FormCloud();
formComponent.form = formModel;
formComponent.showTitle = false;
expect(formComponent.isTitleEnabled()).toBeFalsy();
});
it('should return primary color for complete button', () => {
expect(formComponent.getColorForOutcome('COMPLETE')).toBe('primary');
});
it('should not enable outcome button when model missing', () => {
expect(formComponent.isOutcomeButtonVisible(null, false)).toBeFalsy();
});
it('should enable custom outcome buttons', () => {
const formModel = new FormCloud();
formComponent.form = formModel;
const outcome = new FormOutcomeModel(<any> formModel, { id: 'action1', name: 'Action 1' });
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
});
it('should allow controlling [complete] button visibility', () => {
const formModel = new FormCloud();
formComponent.form = formModel;
const outcome = new FormOutcomeModel(<any> formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION });
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
formComponent.showSaveButton = false;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeFalsy();
});
it('should show only [complete] button with readOnly form ', () => {
const formModel = new FormCloud();
formModel.readOnly = true;
formComponent.form = formModel;
const outcome = new FormOutcomeModel(<any> formModel, { id: '$complete', name: FormOutcomeModel.COMPLETE_ACTION });
formComponent.showCompleteButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
});
it('should not show [save] button with readOnly form ', () => {
const formModel = new FormCloud();
formModel.readOnly = true;
formComponent.form = formModel;
const outcome = new FormOutcomeModel(<any> formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION });
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeFalsy();
});
it('should show [custom-outcome] button with readOnly form and selected custom-outcome', () => {
const formModel = new FormCloud({formRepresentation: {formDefinition: {selectedOutcome: 'custom-outcome'}}});
formModel.readOnly = true;
formComponent.form = formModel;
let outcome = new FormOutcomeModel(<any> formModel, { id: '$customoutome', name: 'custom-outcome' });
formComponent.showCompleteButton = true;
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
outcome = new FormOutcomeModel(<any> formModel, { id: '$customoutome2', name: 'custom-outcome2' });
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeFalsy();
});
it('should allow controlling [save] button visibility', () => {
const formModel = new FormCloud();
formModel.readOnly = false;
formComponent.form = formModel;
const outcome = new FormOutcomeModel(<any> formModel, { id: '$save', name: FormOutcomeModel.COMPLETE_ACTION });
formComponent.showCompleteButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
formComponent.showCompleteButton = false;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeFalsy();
});
it('should load form on refresh', () => {
spyOn(formComponent, 'loadForm').and.stub();
formComponent.onRefreshClicked();
expect(formComponent.loadForm).toHaveBeenCalled();
});
it('should get task variables if a task form is rendered', () => {
spyOn(formService, 'getTaskForm').and.callFake((currentTaskId) => {
return new Observable((observer) => {
observer.next({ formRepresentation: { taskId: currentTaskId }});
observer.complete();
});
});
spyOn(formService, 'getTaskVariables').and.returnValue(of({}));
spyOn(formService, 'getTask').and.callFake((currentTaskId) => {
return new Observable((observer) => {
observer.next({ formRepresentation: { taskId: currentTaskId }});
observer.complete();
});
});
const taskId = '123';
const appName = 'test-app';
formComponent.appName = appName;
formComponent.taskId = taskId;
formComponent.loadForm();
expect(formService.getTaskVariables).toHaveBeenCalledWith(appName, taskId);
});
it('should not get task variables and form if task id is not specified', () => {
spyOn(formService, 'getTaskForm').and.callFake((currentTaskId) => {
return new Observable((observer) => {
observer.next({ taskId: currentTaskId });
observer.complete();
});
});
spyOn(formService, 'getTaskVariables').and.returnValue(of({}));
formComponent.appName = 'test-app';
formComponent.taskId = null;
formComponent.loadForm();
expect(formService.getTaskForm).not.toHaveBeenCalled();
expect(formService.getTaskVariables).not.toHaveBeenCalled();
});
it('should get form definition by form id on load', () => {
spyOn(formComponent, 'getFormById').and.stub();
const formId = '123';
const appName = 'test-app';
formComponent.appName = appName;
formComponent.formId = formId;
formComponent.loadForm();
expect(formComponent.getFormById).toHaveBeenCalledWith(appName, formId);
});
it('should refresh visibility when the form is loaded', () => {
spyOn(formService, 'getForm').and.returnValue(of({formRepresentation: {formDefinition: {}}}));
const formId = '123';
const appName = 'test-app';
formComponent.appName = appName;
formComponent.formId = formId;
formComponent.loadForm();
expect(formService.getForm).toHaveBeenCalledWith(appName, formId);
expect(visibilityService.refreshVisibility).toHaveBeenCalled();
});
it('should reload form by task id on binding changes', () => {
spyOn(formComponent, 'getFormByTaskId').and.stub();
const taskId = '<task id>';
const appName = 'test-app';
formComponent.appName = appName;
const change = new SimpleChange(null, taskId, true);
formComponent.ngOnChanges({ 'taskId': change });
expect(formComponent.getFormByTaskId).toHaveBeenCalledWith(appName, taskId);
});
it('should reload form definition by form id on binding changes', () => {
spyOn(formComponent, 'getFormById').and.stub();
const formId = '123';
const appName = 'test-app';
formComponent.appName = appName;
const change = new SimpleChange(null, formId, true);
formComponent.ngOnChanges({ 'formId': change });
expect(formComponent.getFormById).toHaveBeenCalledWith(appName, formId);
});
it('should not get form on load', () => {
spyOn(formComponent, 'getFormByTaskId').and.stub();
spyOn(formComponent, 'getFormById').and.stub();
formComponent.taskId = null;
formComponent.formId = null;
formComponent.loadForm();
expect(formComponent.getFormByTaskId).not.toHaveBeenCalled();
expect(formComponent.getFormById).not.toHaveBeenCalled();
});
it('should not reload form on unrelated binding changes', () => {
spyOn(formComponent, 'getFormByTaskId').and.stub();
spyOn(formComponent, 'getFormById').and.stub();
formComponent.ngOnChanges({ 'tag': new SimpleChange(null, 'hello world', false) });
expect(formComponent.getFormByTaskId).not.toHaveBeenCalled();
expect(formComponent.getFormById).not.toHaveBeenCalled();
});
it('should complete form on custom outcome click', () => {
const formModel = new FormCloud();
const outcomeName = 'Custom Action';
const outcome = new FormOutcomeModel(<any> formModel, { id: 'custom1', name: outcomeName });
let saved = false;
formComponent.form = formModel;
formComponent.formSaved.subscribe((v) => saved = true);
spyOn(formComponent, 'completeTaskForm').and.stub();
const result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeTruthy();
expect(saved).toBeTruthy();
expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcomeName);
});
it('should save form on [save] outcome click', () => {
const formModel = new FormCloud();
const outcome = new FormOutcomeModel(<any> formModel, {
id: FormCloudComponent.SAVE_OUTCOME_ID,
name: 'Save',
isSystem: true
});
formComponent.form = formModel;
spyOn(formComponent, 'saveTaskForm').and.stub();
const result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeTruthy();
expect(formComponent.saveTaskForm).toHaveBeenCalled();
});
it('should complete form on [complete] outcome click', () => {
const formModel = new FormCloud();
const outcome = new FormOutcomeModel(<any> formModel, {
id: FormCloudComponent.COMPLETE_OUTCOME_ID,
name: 'Complete',
isSystem: true
});
formComponent.form = formModel;
spyOn(formComponent, 'completeTaskForm').and.stub();
const result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeTruthy();
expect(formComponent.completeTaskForm).toHaveBeenCalled();
});
it('should emit form saved event on custom outcome click', () => {
const formModel = new FormCloud();
const outcome = new FormOutcomeModel(<any> formModel, {
id: FormCloudComponent.CUSTOM_OUTCOME_ID,
name: 'Custom',
isSystem: true
});
let saved = false;
formComponent.form = formModel;
formComponent.formSaved.subscribe((v) => saved = true);
const result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeTruthy();
expect(saved).toBeTruthy();
});
it('should do nothing when clicking outcome for readonly form', () => {
const formModel = new FormCloud();
const outcomeName = 'Custom Action';
const outcome = new FormOutcomeModel(<any> formModel, { id: 'custom1', name: outcomeName });
formComponent.form = formModel;
spyOn(formComponent, 'completeTaskForm').and.stub();
expect(formComponent.onOutcomeClicked(outcome)).toBeTruthy();
formComponent.readOnly = true;
expect(formComponent.onOutcomeClicked(outcome)).toBeFalsy();
});
it('should require outcome model when clicking outcome', () => {
formComponent.form = new FormCloud();
formComponent.readOnly = false;
expect(formComponent.onOutcomeClicked(null)).toBeFalsy();
});
it('should require loaded form when clicking outcome', () => {
const formModel = new FormCloud();
const outcomeName = 'Custom Action';
const outcome = new FormOutcomeModel(<any> formModel, { id: 'custom1', name: outcomeName });
formComponent.readOnly = false;
formComponent.form = null;
expect(formComponent.onOutcomeClicked(outcome)).toBeFalsy();
});
it('should not execute unknown system outcome', () => {
const formModel = new FormCloud();
const outcome = new FormOutcomeModel(<any> formModel, { id: 'unknown', name: 'Unknown', isSystem: true });
formComponent.form = formModel;
expect(formComponent.onOutcomeClicked(outcome)).toBeFalsy();
});
it('should require custom action name to complete form', () => {
const formModel = new FormCloud();
let outcome = new FormOutcomeModel(<any> formModel, { id: 'custom' });
formComponent.form = formModel;
expect(formComponent.onOutcomeClicked(outcome)).toBeFalsy();
outcome = new FormOutcomeModel(<any> formModel, { id: 'custom', name: 'Custom' });
spyOn(formComponent, 'completeTaskForm').and.stub();
expect(formComponent.onOutcomeClicked(outcome)).toBeTruthy();
});
it('should fetch and parse form by task id', (done) => {
const appName = 'test-app';
const taskId = '456';
spyOn(formService, 'getTask').and.returnValue(of({}));
spyOn(formService, 'getTaskVariables').and.returnValue(of({}));
spyOn(formService, 'getTaskForm').and.returnValue(of({formRepresentation: {taskId: taskId, formDefinition: {selectedOutcome: 'custom-outcome'}}}));
formComponent.formLoaded.subscribe(() => {
expect(formService.getTaskForm).toHaveBeenCalledWith(appName, taskId);
expect(formComponent.form).toBeDefined();
expect(formComponent.form.taskId).toBe(taskId);
done();
});
formComponent.appName = appName;
formComponent.taskId = taskId;
formComponent.loadForm();
});
it('should handle error when getting form by task id', (done) => {
const error = 'Some error';
spyOn(formService, 'getTask').and.returnValue(of({}));
spyOn(formService, 'getTaskVariables').and.returnValue(of({}));
spyOn(formComponent, 'handleError').and.stub();
spyOn(formService, 'getTaskForm').and.callFake(() => {
return throwError(error);
});
formComponent.getFormByTaskId('test-app', '123').then((_) => {
expect(formComponent.handleError).toHaveBeenCalledWith(error);
done();
});
});
it('should fetch and parse form definition by id', (done) => {
spyOn(formService, 'getForm').and.callFake((currentAppName, currentFormId) => {
return new Observable((observer) => {
observer.next({ formRepresentation: {id: currentFormId, formDefinition: {}}});
observer.complete();
});
});
const appName = 'test-app';
const formId = '456';
formComponent.formLoaded.subscribe(() => {
expect(formComponent.form).toBeDefined();
expect(formComponent.form.id).toBe(formId);
done();
});
formComponent.appName = appName;
formComponent.formId = formId;
formComponent.loadForm();
});
it('should handle error when getting form by definition id', () => {
const error = 'Some error';
spyOn(formComponent, 'handleError').and.stub();
spyOn(formService, 'getForm').and.callFake(() => throwError(error));
formComponent.getFormById('test-app', '123');
expect(formComponent.handleError).toHaveBeenCalledWith(error);
});
it('should save task form and raise corresponding event', () => {
spyOn(formService, 'saveTaskForm').and.callFake(() => {
return new Observable((observer) => {
observer.next();
observer.complete();
});
});
let saved = false;
let savedForm = null;
formComponent.formSaved.subscribe((form) => {
saved = true;
savedForm = form;
});
const taskId = '123-223';
const appName = 'test-app';
const formModel = new FormCloud({
formRepresentation: {
id: '23',
taskId: taskId,
formDefinition: {
fields: [
{ id: 'field1' },
{ id: 'field2' }
]
}
}
});
formComponent.form = formModel;
formComponent.taskId = taskId;
formComponent.appName = appName;
formComponent.saveTaskForm();
expect(formService.saveTaskForm).toHaveBeenCalledWith(appName, formModel.taskId, formModel.id, formModel.values);
expect(saved).toBeTruthy();
expect(savedForm).toEqual(formModel);
});
it('should handle error during form save', () => {
const error = 'Error';
spyOn(formService, 'saveTaskForm').and.callFake(() => throwError(error));
spyOn(formComponent, 'handleError').and.stub();
const taskId = '123-223';
const appName = 'test-app';
const formModel = new FormCloud({
formRepresentation: {
id: '23',
taskId: taskId,
formDefinition: {
fields: [
{ id: 'field1' },
{ id: 'field2' }
]
}
}
});
formComponent.form = formModel;
formComponent.taskId = taskId;
formComponent.appName = appName;
formComponent.saveTaskForm();
expect(formComponent.handleError).toHaveBeenCalledWith(error);
});
it('should require form with appName and taskId to save', () => {
spyOn(formService, 'saveTaskForm').and.stub();
formComponent.form = null;
formComponent.saveTaskForm();
formComponent.form = new FormCloud();
formComponent.appName = 'test-app';
formComponent.saveTaskForm();
formComponent.appName = null;
formComponent.taskId = '123';
formComponent.saveTaskForm();
expect(formService.saveTaskForm).not.toHaveBeenCalled();
});
it('should require form with appName and taskId to complete', () => {
spyOn(formService, 'completeTaskForm').and.stub();
formComponent.form = null;
formComponent.completeTaskForm('save');
formComponent.form = new FormCloud();
formComponent.appName = 'test-app';
formComponent.completeTaskForm('complete');
formComponent.appName = null;
formComponent.taskId = '123';
formComponent.completeTaskForm('complete');
expect(formService.completeTaskForm).not.toHaveBeenCalled();
});
it('should complete form and raise corresponding event', () => {
spyOn(formService, 'completeTaskForm').and.callFake(() => {
return new Observable((observer) => {
observer.next();
observer.complete();
});
});
const outcome = 'complete';
let completed = false;
formComponent.formCompleted.subscribe(() => completed = true);
const taskId = '123-223';
const appName = 'test-app';
const formModel = new FormCloud({
formRepresentation: {
id: '23',
taskId: taskId,
formDefinition: {
fields: [
{ id: 'field1' },
{ id: 'field2' }
]
}
}
});
formComponent.form = formModel;
formComponent.taskId = taskId;
formComponent.appName = appName;
formComponent.completeTaskForm(outcome);
expect(formService.completeTaskForm).toHaveBeenCalledWith(appName, formModel.taskId, formModel.id, formModel.values, outcome);
expect(completed).toBeTruthy();
});
it('should require json to parse form', () => {
expect(formComponent.parseForm(null)).toBeNull();
});
it('should parse form from json', () => {
const form = formComponent.parseForm({
formRepresentation: {
id: '1',
formDefinition: {
fields: [
{ id: 'field1', type: FormFieldTypes.CONTAINER }
]
}
}
});
expect(form).toBeDefined();
expect(form.id).toBe('1');
expect(form.fields.length).toBe(1);
expect(form.fields[0].id).toBe('field1');
});
it('should provide outcomes for form definition', () => {
spyOn(formComponent, 'getFormDefinitionOutcomes').and.callThrough();
const form = formComponent.parseForm({ formRepresentation: { id: 1, formDefinition: {}}});
expect(formComponent.getFormDefinitionOutcomes).toHaveBeenCalledWith(form);
});
it('should prevent default outcome execution', () => {
const outcome = new FormOutcomeModel(<any> new FormCloud(), {
id: FormCloudComponent.CUSTOM_OUTCOME_ID,
name: 'Custom'
});
formComponent.form = new FormCloud();
formComponent.executeOutcome.subscribe((event: FormOutcomeEvent) => {
expect(event.outcome).toBe(outcome);
event.preventDefault();
expect(event.defaultPrevented).toBeTruthy();
});
const result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeFalsy();
});
it('should not prevent default outcome execution', () => {
const outcome = new FormOutcomeModel(<any> new FormCloud(), {
id: FormCloudComponent.CUSTOM_OUTCOME_ID,
name: 'Custom'
});
formComponent.form = new FormCloud();
formComponent.executeOutcome.subscribe((event: FormOutcomeEvent) => {
expect(event.outcome).toBe(outcome);
expect(event.defaultPrevented).toBeFalsy();
});
spyOn(formComponent, 'completeTaskForm').and.callThrough();
const result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeTruthy();
expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcome.name);
});
it('should check visibility only if field with form provided', () => {
formComponent.checkVisibility(null);
expect(visibilityService.refreshVisibility).not.toHaveBeenCalled();
let field = new FormFieldModel(null);
formComponent.checkVisibility(field);
expect(visibilityService.refreshVisibility).not.toHaveBeenCalled();
field = new FormFieldModel(<any> new FormCloud());
formComponent.checkVisibility(field);
expect(visibilityService.refreshVisibility).toHaveBeenCalledWith(field.form);
});
it('should disable outcome buttons for readonly form', () => {
const formModel = new FormCloud();
formModel.readOnly = true;
formComponent.form = formModel;
const outcome = new FormOutcomeModel(<any> new FormCloud(), {
id: FormCloudComponent.CUSTOM_OUTCOME_ID,
name: 'Custom'
});
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeFalsy();
});
it('should require outcome to eval button state', () => {
formComponent.form = new FormCloud();
expect(formComponent.isOutcomeButtonEnabled(null)).toBeFalsy();
});
it('should disable complete outcome button when disableCompleteButton is true', () => {
const formModel = new FormCloud();
formComponent.form = formModel;
formComponent.disableCompleteButton = true;
expect(formModel.isValid).toBeTruthy();
const completeOutcome = formComponent.form.outcomes.find((outcome) => outcome.name === FormOutcomeModel.COMPLETE_ACTION);
expect(formComponent.isOutcomeButtonEnabled(completeOutcome)).toBeFalsy();
});
it('should disable start process outcome button when disableStartProcessButton is true', () => {
const formModel = new FormCloud();
formComponent.form = formModel;
formComponent.disableStartProcessButton = true;
expect(formModel.isValid).toBeTruthy();
const startProcessOutcome = formComponent.form.outcomes.find((outcome) => outcome.name === FormOutcomeModel.START_PROCESS_ACTION);
expect(formComponent.isOutcomeButtonEnabled(startProcessOutcome)).toBeFalsy();
});
it('should raise [executeOutcome] event for formService', (done) => {
formComponent.executeOutcome.subscribe(() => {
done();
});
const outcome = new FormOutcomeModel(<any> new FormCloud(), {
id: FormCloudComponent.CUSTOM_OUTCOME_ID,
name: 'Custom'
});
formComponent.form = new FormCloud();
formComponent.onOutcomeClicked(outcome);
});
it('should refresh form values when data is changed', () => {
formComponent.form = new FormCloud(JSON.parse(JSON.stringify(cloudFormMock)));
let formFields = formComponent.form.getFormFields();
let labelField = formFields.find((field) => field.id === 'text1');
let radioField = formFields.find((field) => field.id === 'number1');
expect(labelField.value).toBeNull();
expect(radioField.value).toBeUndefined();
const formValues: any[] = [{name: 'text1', value: 'test'}, {name: 'number1', value: 23}];
const change = new SimpleChange(null, formValues, false);
formComponent.data = formValues;
formComponent.ngOnChanges({ 'data': change });
formFields = formComponent.form.getFormFields();
labelField = formFields.find((field) => field.id === 'text1');
radioField = formFields.find((field) => field.id === 'number1');
expect(labelField.value).toBe('test');
expect(radioField.value).toBe(23);
});
it('should refresh radio buttons value when id is given to data', () => {
formComponent.form = new FormCloud(JSON.parse(JSON.stringify(cloudFormMock)));
let formFields = formComponent.form.getFormFields();
let radioFieldById = formFields.find((field) => field.id === 'radiobuttons1');
const formValues: any[] = [{name: 'radiobuttons1', value: 'option_2'}];
const change = new SimpleChange(null, formValues, false);
formComponent.data = formValues;
formComponent.ngOnChanges({ 'data': change });
formFields = formComponent.form.getFormFields();
radioFieldById = formFields.find((field) => field.id === 'radiobuttons1');
expect(radioFieldById.value).toBe('option_2');
});
});

View File

@@ -0,0 +1,296 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 {
Component, EventEmitter, Input, OnChanges,
Output, SimpleChanges
} from '@angular/core';
import { Observable, of, forkJoin } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { Subscription } from 'rxjs';
import { FormBaseComponent, FormFieldModel, FormOutcomeEvent, FormOutcomeModel, WidgetVisibilityService } from '@alfresco/adf-core';
import { FormCloudService } from '../services/form-cloud.service';
import { FormCloud } from '../models/form-cloud.model';
import { TaskVariableCloud } from '../models/task-variable-cloud.model';
@Component({
selector: 'adf-cloud-form',
templateUrl: './form-cloud.component.html'
})
export class FormCloudComponent extends FormBaseComponent implements OnChanges {
/** App id to fetch corresponding form and values. */
@Input()
appName: string;
/** Task id to fetch corresponding form and values. */
@Input()
formId: string;
/** Underlying form model instance. */
@Input()
form: FormCloud;
/** Task id to fetch corresponding form and values. */
@Input()
taskId: string;
/** Custom form values map to be used with the rendered form. */
@Input()
data: TaskVariableCloud[];
/** Emitted when the form is submitted with the `Save` or custom outcomes. */
@Output()
formSaved: EventEmitter<FormCloud> = new EventEmitter<FormCloud>();
/** Emitted when the form is submitted with the `Complete` outcome. */
@Output()
formCompleted: EventEmitter<FormCloud> = new EventEmitter<FormCloud>();
/** Emitted when the form is loaded or reloaded. */
@Output()
formLoaded: EventEmitter<FormCloud> = new EventEmitter<FormCloud>();
/** Emitted when form values are refreshed due to a data property change. */
@Output()
formDataRefreshed: EventEmitter<FormCloud> = new EventEmitter<FormCloud>();
protected subscriptions: Subscription[] = [];
nodeId: string;
constructor(protected formService: FormCloudService,
protected visibilityService: WidgetVisibilityService) {
super();
}
ngOnChanges(changes: SimpleChanges) {
const appName = changes['appName'];
if (appName && appName.currentValue) {
if (this.taskId) {
this.getFormDefinitionWithFolderTask(this.appName, this.taskId);
} else if (this.formId) {
this.getFormById(appName.currentValue, this.formId);
}
return;
}
const formId = changes['formId'];
if (formId && formId.currentValue && this.appName) {
this.getFormById(this.appName, formId.currentValue);
return;
}
const taskId = changes['taskId'];
if (taskId && taskId.currentValue && this.appName) {
this.getFormByTaskId(this.appName, taskId.currentValue);
return;
}
const data = changes['data'];
if (data && data.currentValue) {
this.refreshFormData();
return;
}
}
/**
* Invoked when user clicks form refresh button.
*/
onRefreshClicked() {
this.loadForm();
}
loadForm() {
if (this.appName && this.taskId) {
this.getFormByTaskId(this.appName, this.taskId);
} else if (this.appName && this.formId) {
this.getFormById(this.appName, this.formId);
}
}
findProcessVariablesByTaskId(appName: string, taskId: string): Observable<any> {
return this.formService.getTask(appName, taskId).pipe(
switchMap((task: any) => {
if (this.isAProcessTask(task)) {
return this.formService.getTaskVariables(appName, taskId);
} else {
return of({});
}
})
);
}
isAProcessTask(taskRepresentation) {
return taskRepresentation.processDefinitionId && taskRepresentation.processDefinitionDeploymentId !== 'null';
}
getFormByTaskId(appName, taskId: string): Promise<FormCloud> {
return new Promise<FormCloud>((resolve, reject) => {
forkJoin(this.formService.getTaskForm(appName, taskId),
this.formService.getTaskVariables(appName, taskId))
.subscribe(
(data) => {
this.data = data[1];
const parsedForm = this.parseForm(data[0]);
this.visibilityService.refreshVisibility(<any> parsedForm);
parsedForm.validateForm();
this.form = parsedForm;
this.form.nodeId = this.nodeId;
this.onFormLoaded(this.form);
resolve(this.form);
},
(error) => {
this.handleError(error);
// reject(error);
resolve(null);
}
);
});
}
async getFormDefinitionWithFolderTask(appName: string, taskId: string) {
await this.getFolderTask(appName, taskId);
await this.getFormByTaskId(appName, taskId);
}
async getFolderTask(appName: string, taskId: string) {
this.nodeId = await this.formService.getProcessStorageFolderTask(appName, taskId).toPromise();
}
getFormById(appName: string, formId: string) {
this.formService
.getForm(appName, formId)
.subscribe(
(form) => {
const parsedForm = this.parseForm(form);
this.visibilityService.refreshVisibility(<any> parsedForm);
parsedForm.validateForm();
this.form = parsedForm;
this.form.nodeId = this.nodeId;
this.onFormLoaded(this.form);
},
(error) => {
this.handleError(error);
}
);
}
saveTaskForm() {
if (this.form && this.appName && this.taskId) {
this.formService
.saveTaskForm(this.appName, this.taskId, this.form.id, this.form.values)
.subscribe(
() => {
this.onTaskSaved(this.form);
},
(error) => this.onTaskSavedError(this.form, error)
);
}
}
completeTaskForm(outcome?: string) {
if (this.form && this.appName && this.taskId) {
this.formService
.completeTaskForm(this.appName, this.taskId, this.form.id, this.form.values, outcome)
.subscribe(
() => {
this.onTaskCompleted(this.form);
},
(error) => this.onTaskCompletedError(this.form, error)
);
}
}
parseForm(json: any): FormCloud {
if (json) {
const form = new FormCloud(json, this.data, this.readOnly, this.formService);
if (!json.formRepresentation.formDefinition || !json.formRepresentation.formDefinition.fields) {
form.outcomes = this.getFormDefinitionOutcomes(form);
}
if (this.fieldValidators && this.fieldValidators.length > 0) {
form.fieldValidators = this.fieldValidators;
}
return form;
}
return null;
}
/**
* Get custom set of outcomes for a Form Definition.
* @param form Form definition model.
*/
getFormDefinitionOutcomes(form: FormCloud): FormOutcomeModel[] {
return [
new FormOutcomeModel(<any> form, { id: '$save', name: FormOutcomeModel.SAVE_ACTION, isSystem: true })
];
}
checkVisibility(field: FormFieldModel) {
if (field && field.form) {
this.visibilityService.refreshVisibility(field.form);
}
}
private refreshFormData() {
this.form = this.parseForm(this.form.json);
this.onFormLoaded(this.form);
this.onFormDataRefreshed(this.form);
}
protected onFormLoaded(form: FormCloud) {
this.formLoaded.emit(form);
}
protected onFormDataRefreshed(form: FormCloud) {
this.formDataRefreshed.emit(form);
}
protected onTaskSaved(form: FormCloud) {
this.formSaved.emit(form);
}
protected onTaskSavedError(form: FormCloud, error: any) {
this.handleError(error);
}
protected onTaskCompleted(form: FormCloud) {
this.formCompleted.emit(form);
}
protected onTaskCompletedError(form: FormCloud, error: any) {
this.handleError(error);
}
protected onExecuteOutcome(outcome: FormOutcomeModel): boolean {
const args = new FormOutcomeEvent(outcome);
if (args.defaultPrevented) {
return false;
}
this.executeOutcome.emit(args);
if (args.defaultPrevented) {
return false;
}
return true;
}
protected storeFormAsMetadata() {
}
}

View File

@@ -0,0 +1,40 @@
<div class="adf-upload-widget {{field.className}}"
[class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name}}<span *ngIf="isRequired()">*</span></label>
<div class="adf-upload-widget-container">
<div>
<mat-list *ngIf="hasFile">
<mat-list-item class="adf-upload-files-row" *ngFor="let file of field.value">
<img mat-list-icon class="adf-upload-widget__icon"
[id]="'file-'+file.id+'-icon'"
[src]="getIcon(file.mimeType)"
[alt]="mimeTypeIcon"
(click)="fileClicked(file)"
(keyup.enter)="fileClicked(file)"
role="button"
tabindex="0"/>
<span matLine id="{{'file-'+file.id}}" (click)="fileClicked(file)" (keyup.enter)="fileClicked(file)"
role="button" tabindex="0" class="adf-file">{{file.name}}</span>
<button *ngIf="!field.readOnly" mat-icon-button [id]="'file-'+file.id+'-remove'"
(click)="removeFile(file);" (keyup.enter)="removeFile(file);">
<mat-icon class="mat-24">highlight_off</mat-icon>
</button>
</mat-list-item>
</mat-list>
</div>
<div class="button-row" *ngIf="(!hasFile || multipleOption) && !field.readOnly">
<a mat-raised-button color="primary">
{{ 'FORM.FIELD.UPLOAD' | translate }}<mat-icon>file_upload</mat-icon>
<input #uploadFiles
[multiple]="multipleOption"
type="file"
[id]="field.id"
(change)="onFileChanged($event)"/>
</a>
</div>
</div>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -0,0 +1,135 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* tslint:disable:component-selector */
import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Observable, from } from 'rxjs';
import { mergeMap, map } from 'rxjs/operators';
import { WidgetComponent, baseHost, LogService, FormService, ThumbnailService } from '@alfresco/adf-core';
import { FormCloudService } from '../services/form-cloud.service';
@Component({
selector: 'upload-cloud-widget',
templateUrl: './upload-cloud.widget.html',
styleUrls: ['./upload-cloud.widget.scss'],
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class UploadCloudWidgetComponent extends WidgetComponent implements OnInit {
hasFile: boolean;
displayText: string;
multipleOption: string = '';
mimeTypeIcon: string;
@ViewChild('uploadFiles')
fileInput: ElementRef;
constructor(public formService: FormService,
private thumbnailService: ThumbnailService,
private formCloudService: FormCloudService,
private logService: LogService) {
super(formService);
}
ngOnInit() {
if (this.field &&
this.field.value &&
this.field.value.length > 0) {
this.hasFile = true;
}
this.getMultipleFileParam();
}
removeFile(file: any) {
if (this.field) {
this.removeElementFromList(file);
}
}
onFileChanged(event: any) {
const files = event.target.files;
let filesSaved = [];
if (this.field.json.value) {
filesSaved = [...this.field.json.value];
}
if (files && files.length > 0) {
from(files)
.pipe(mergeMap((file) => this.uploadRawContent(file)))
.subscribe(
(res) => filesSaved.push(res),
(error) => this.logService.error(`Error uploading file. See console output for more details. ${error}` ),
() => {
this.field.form.values[this.field.id] = filesSaved;
this.hasFile = true;
}
);
}
}
getIcon(mimeType) {
return this.thumbnailService.getMimeTypeIcon(mimeType);
}
private uploadRawContent(file): Observable<any> {
return this.formCloudService.createTemporaryRawRelatedContent(file, this.field.form.nodeId)
.pipe(
map((response: any) => {
this.logService.info(response);
return { nodeId : response.id};
})
);
}
getMultipleFileParam() {
if (this.field &&
this.field.params &&
this.field.params.multiple) {
this.multipleOption = this.field.params.multiple ? 'multiple' : '';
}
}
private removeElementFromList(file) {
const index = this.field.value.indexOf(file);
// remove from content too
if (index !== -1) {
this.field.value.splice(index, 1);
this.field.json.value = this.field.value;
this.field.updateForm();
}
this.hasFile = this.field.value.length > 0;
this.resetFormValueWithNoFiles();
}
private resetFormValueWithNoFiles() {
if (this.field.value.length === 0) {
this.field.value = [];
this.field.json.value = [];
}
}
fileClicked(contentLinkModel: any): void {
}
}

View File

@@ -0,0 +1,47 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { TemplateModule, FormBaseModule, PipeModule, CoreModule } from '@alfresco/adf-core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FormCloudComponent } from './components/form-cloud.component';
import { UploadCloudWidgetComponent } from './components/upload-cloud.widget';
import { MaterialModule } from '../material.module';
@NgModule({
imports: [
CommonModule,
PipeModule,
TemplateModule,
FlexLayoutModule,
MaterialModule,
FormsModule,
ReactiveFormsModule,
FormBaseModule,
CoreModule
],
declarations: [FormCloudComponent, UploadCloudWidgetComponent],
entryComponents: [
UploadCloudWidgetComponent
],
exports: [
FormCloudComponent, UploadCloudWidgetComponent
]
})
export class FormCloudModule { }

View File

@@ -0,0 +1,686 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
export const cloudFormMock = {
'formRepresentation': {
'id': 'form-b661635a-dc3e-4557-914a-3498ed47189c',
'name': 'form-with-all-fields',
'description': '',
'version': 0,
'formDefinition': {
'tabs': [],
'fields': [
{
'fieldType': 'ContainerRepresentation',
'id': '26b10e64-0403-4686-a75b-0d45279ce3a8',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'text1',
'name': 'Text1',
'type': 'text',
'value': null,
'required': false,
'readOnly': true,
'overrideId': false,
'colspan': 1,
'placeholder': null,
'minLength': 0,
'maxLength': 0,
'minValue': null,
'maxValue': null,
'regexPattern': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
],
'2': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'text2',
'name': 'Text2',
'type': 'text',
'value': null,
'required': false,
'readOnly': true,
'overrideId': false,
'colspan': 1,
'placeholder': null,
'minLength': 0,
'maxLength': 0,
'minValue': null,
'maxValue': null,
'regexPattern': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
]
}
},
{
'fieldType': 'ContainerRepresentation',
'id': '69c1390a-8d8d-423c-8efb-8e43401efa42',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'multilinetext1',
'name': 'Multiline text1',
'type': 'multi-line-text',
'overrideId': false,
'colspan': 1,
'placeholder': null,
'minLength': 0,
'maxLength': 0,
'regexPattern': null,
'required': false,
'readOnly': true,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
],
'2': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'multilinetext2',
'name': 'Multiline text2',
'type': 'multi-line-text',
'overrideId': false,
'colspan': 1,
'placeholder': null,
'minLength': 0,
'maxLength': 0,
'regexPattern': null,
'required': false,
'readOnly': true,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
]
}
},
{
'fieldType': 'ContainerRepresentation',
'id': 'df046463-2d65-4388-9ee1-0e1517985215',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'number1',
'overrideId': false,
'name': 'Number1',
'type': 'integer',
'colspan': 1,
'placeholder': null,
'readOnly': true,
'minValue': null,
'maxValue': null,
'required': false,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
],
'2': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'number2',
'overrideId': false,
'name': 'Number2',
'type': 'integer',
'colspan': 1,
'placeholder': null,
'readOnly': true,
'minValue': null,
'maxValue': null,
'required': false,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
]
}
},
{
'fieldType': 'ContainerRepresentation',
'id': '9672cc7b-1959-49c9-96be-3816e57bdfc1',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'checkbox1',
'name': 'Checkbox1',
'type': 'boolean',
'required': false,
'readOnly': true,
'colspan': 1,
'overrideId': false,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
],
'2': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'checkbox2',
'name': 'Checkbox2',
'type': 'boolean',
'required': false,
'readOnly': true,
'colspan': 1,
'overrideId': false,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
]
}
},
{
'fieldType': 'ContainerRepresentation',
'id': '054d193e-a899-4494-9a3e-b489315b7d57',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'dropdown1',
'name': 'Dropdown1',
'type': 'dropdown',
'value': null,
'required': false,
'readOnly': true,
'overrideId': false,
'colspan': 1,
'placeholder': null,
'optionType': 'manual',
'options': [],
'endpoint': null,
'requestHeaders': null,
'restUrl': null,
'restResponsePath': null,
'restIdProperty': null,
'restLabelProperty': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
],
'2': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'dropdown2',
'name': 'Dropdown2',
'type': 'dropdown',
'value': null,
'required': false,
'readOnly': true,
'overrideId': false,
'colspan': 1,
'placeholder': null,
'optionType': 'manual',
'options': [],
'endpoint': null,
'requestHeaders': null,
'restUrl': null,
'restResponsePath': null,
'restIdProperty': null,
'restLabelProperty': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
]
}
},
{
'fieldType': 'ContainerRepresentation',
'id': '1f8f0b66-e022-4667-91b4-bbbf2ddc36fb',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'amount1',
'name': 'Amount1',
'type': 'amount',
'value': null,
'required': false,
'readOnly': true,
'overrideId': false,
'colspan': 1,
'placeholder': '123',
'minValue': null,
'maxValue': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
},
'enableFractions': false,
'currency': '$'
}
],
'2': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'amount2',
'name': 'Amount2',
'type': 'amount',
'value': null,
'required': false,
'readOnly': true,
'overrideId': false,
'colspan': 1,
'placeholder': '123',
'minValue': null,
'maxValue': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
},
'enableFractions': false,
'currency': '$'
}
]
}
},
{
'fieldType': 'ContainerRepresentation',
'id': '541a368b-67ee-4a7c-ae7e-232c050b9e24',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'date1',
'name': 'Date1',
'type': 'date',
'overrideId': false,
'required': false,
'readOnly': true,
'colspan': 1,
'placeholder': null,
'minValue': null,
'maxValue': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
},
'dateDisplayFormat': 'D-M-YYYY'
}
],
'2': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'date2',
'name': 'Date2',
'type': 'date',
'overrideId': false,
'required': false,
'readOnly': true,
'colspan': 1,
'placeholder': null,
'minValue': null,
'maxValue': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
},
'dateDisplayFormat': 'D-M-YYYY'
}
]
}
},
{
'fieldType': 'ContainerRepresentation',
'id': 'e79cb7e2-3dc1-4c79-8158-28662c28a9f3',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'radiobuttons1',
'name': 'Radio buttons1',
'type': 'radio-buttons',
'value': null,
'required': false,
'readOnly': true,
'overrideId': false,
'colspan': 1,
'placeholder': null,
'optionType': 'manual',
'options': [
{
'id': 'option_1',
'name': 'Option 1'
},
{
'id': 'option_2',
'name': 'Option 2'
}
],
'endpoint': null,
'requestHeaders': null,
'restUrl': null,
'restResponsePath': null,
'restIdProperty': null,
'restLabelProperty': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
],
'2': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'radiobuttons2',
'name': 'Radio buttons2',
'type': 'radio-buttons',
'value': null,
'required': false,
'readOnly': true,
'overrideId': false,
'colspan': 1,
'placeholder': null,
'optionType': 'manual',
'options': [
{
'id': 'option_1',
'name': 'Option 1'
},
{
'id': 'option_2',
'name': 'Option 2'
}
],
'endpoint': null,
'requestHeaders': null,
'restUrl': null,
'restResponsePath': null,
'restIdProperty': null,
'restLabelProperty': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
]
}
},
{
'fieldType': 'ContainerRepresentation',
'id': '7c01ed35-be86-4be7-9c28-ed640a5a2ae1',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'AttachFileFieldRepresentation',
'id': 'attachfile1',
'name': 'Attach file1',
'type': 'upload',
'value': null,
'required': false,
'readOnly': true,
'overrideId': false,
'colspan': 1,
'placeholder': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2,
'fileSource': {
'serviceId': 'all-file-sources',
'name': 'All file sources'
},
'multiple': false,
'link': false
}
}
],
'2': [
{
'fieldType': 'AttachFileFieldRepresentation',
'id': 'attachfile2',
'name': 'Attach file2',
'type': 'upload',
'value': null,
'required': false,
'readOnly': true,
'overrideId': false,
'colspan': 1,
'placeholder': null,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2,
'fileSource': {
'serviceId': 'all-file-sources',
'name': 'All file sources'
},
'multiple': false,
'link': false
}
}
]
}
},
{
'fieldType': 'ContainerRepresentation',
'id': '07b13b96-d469-4a1e-8a9a-9bb957c68869',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'displayvalue1',
'name': 'Display value1',
'type': 'readonly',
'value': 'No field selected',
'readOnly': true,
'required': false,
'overrideId': false,
'colspan': 1,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2,
'field': {
'id': 'displayvalue',
'name': 'Display value',
'type': 'text'
}
}
}
],
'2': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'displayvalue2',
'name': 'Display value2',
'type': 'readonly',
'value': 'No field selected',
'readOnly': true,
'required': false,
'overrideId': false,
'colspan': 1,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2,
'field': {
'id': 'displayvalue',
'name': 'Display value',
'type': 'text'
}
}
}
]
}
},
{
'fieldType': 'ContainerRepresentation',
'id': '1576ef25-c842-494c-ab84-265a1e3bf68d',
'name': 'Label',
'type': 'container',
'tab': null,
'numberOfColumns': 2,
'fields': {
'1': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'displaytext1',
'name': 'Display text1',
'type': 'readonly-text',
'value': 'Display text as part of the form',
'readOnly': true,
'required': false,
'overrideId': false,
'colspan': 1,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
],
'2': [
{
'fieldType': 'FormFieldRepresentation',
'id': 'displaytext2',
'name': 'Display text2',
'type': 'readonly-text',
'value': 'Display text as part of the form',
'readOnly': true,
'required': false,
'overrideId': false,
'colspan': 1,
'visibilityCondition': null,
'params': {
'existingColspan': 1,
'maxColspan': 2
}
}
]
}
}
],
'outcomes': [],
'javascriptEvents': [],
'className': '',
'style': '',
'customFieldTemplates': {},
'metadata': {},
'variables': [
{
'name': 'FormVarStr',
'type': 'string',
'value': ''
},
{
'name': 'FormVarInt',
'type': 'integer',
'value': ''
},
{
'name': 'FormVarBool',
'type': 'boolean',
'value': ''
},
{
'name': 'FormVarDate',
'type': 'date',
'value': ''
},
{
'name': 'NewVar',
'type': 'string',
'value': ''
}
],
'customFieldsValueInfo': {},
'gridsterForm': false
}
},
'processScopeIdentifiers': []
};

View File

@@ -0,0 +1,233 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { FormCloudService } from '../services/form-cloud.service';
import { FormCloud } from './form-cloud.model';
import { TabModel, FormFieldModel, ContainerModel, FormOutcomeModel, FormFieldTypes } from '@alfresco/adf-core';
describe('FormCloud', () => {
let formCloudService: FormCloudService;
beforeEach(() => {
formCloudService = new FormCloudService(null, null, null);
});
it('should store original json', () => {
const json = {formRepresentation: {formDefinition: {}}};
const form = new FormCloud(json);
expect(form.json).toBe(json);
});
it('should setup properties with json', () => {
const json = {formRepresentation: {
id: '<id>',
name: '<name>',
taskId: '<task-id>',
taskName: '<task-name>'
}};
const form = new FormCloud(json);
Object.keys(json).forEach((key) => {
expect(form[key]).toEqual(form[key]);
});
});
it('should take form name when task name is missing', () => {
const json = {formRepresentation: {
id: '<id>',
name: '<name>',
formDefinition: {}
}};
const form = new FormCloud(json);
expect(form.taskName).toBe(json.formRepresentation.name);
});
it('should set readonly state from params', () => {
const form = new FormCloud({}, null, true);
expect(form.readOnly).toBeTruthy();
});
it('should check tabs', () => {
const form = new FormCloud();
form.tabs = null;
expect(form.hasTabs()).toBeFalsy();
form.tabs = [];
expect(form.hasTabs()).toBeFalsy();
form.tabs = [new TabModel(null)];
expect(form.hasTabs()).toBeTruthy();
});
it('should check fields', () => {
const form = new FormCloud();
form.fields = null;
expect(form.hasFields()).toBeFalsy();
form.fields = [];
expect(form.hasFields()).toBeFalsy();
const field = new FormFieldModel(<any> form);
form.fields = [new ContainerModel(field)];
expect(form.hasFields()).toBeTruthy();
});
it('should check outcomes', () => {
const form = new FormCloud();
form.outcomes = null;
expect(form.hasOutcomes()).toBeFalsy();
form.outcomes = [];
expect(form.hasOutcomes()).toBeFalsy();
form.outcomes = [new FormOutcomeModel(null)];
expect(form.hasOutcomes()).toBeTruthy();
});
it('should parse tabs', () => {
const json = {formRepresentation: {formDefinition: {
tabs: [
{ id: 'tab1' },
{ id: 'tab2' }
]
}}};
const form = new FormCloud(json);
expect(form.tabs.length).toBe(2);
expect(form.tabs[0].id).toBe('tab1');
expect(form.tabs[1].id).toBe('tab2');
});
it('should parse fields', () => {
const json = {formRepresentation: {formDefinition: {
fields: [
{
id: 'field1',
type: FormFieldTypes.CONTAINER
},
{
id: 'field2',
type: FormFieldTypes.CONTAINER
}
]
}}};
const form = new FormCloud(json);
expect(form.fields.length).toBe(2);
expect(form.fields[0].id).toBe('field1');
expect(form.fields[1].id).toBe('field2');
});
it('should convert missing fields to empty collection', () => {
const json = {formRepresentation: {formDefinition: {
fields: null
}}};
const form = new FormCloud(json);
expect(form.fields).toBeDefined();
expect(form.fields.length).toBe(0);
});
it('should put fields into corresponding tabs', () => {
const json = {formRepresentation: {formDefinition: {
tabs: [
{ id: 'tab1' },
{ id: 'tab2' }
],
fields: [
{ id: 'field1', tab: 'tab1', type: FormFieldTypes.CONTAINER },
{ id: 'field2', tab: 'tab2', type: FormFieldTypes.CONTAINER },
{ id: 'field3', tab: 'tab1', type: FormFieldTypes.DYNAMIC_TABLE },
{ id: 'field4', tab: 'missing-tab', type: FormFieldTypes.DYNAMIC_TABLE }
]
}}};
const form = new FormCloud(json);
expect(form.tabs.length).toBe(2);
expect(form.fields.length).toBe(4);
const tab1 = form.tabs[0];
expect(tab1.fields.length).toBe(2);
expect(tab1.fields[0].id).toBe('field1');
expect(tab1.fields[1].id).toBe('field3');
const tab2 = form.tabs[1];
expect(tab2.fields.length).toBe(1);
expect(tab2.fields[0].id).toBe('field2');
});
it('should create standard form outcomes', () => {
const json = {formRepresentation: {formDefinition: {
fields: [
{ id: 'container1' }
]
}}};
const form = new FormCloud(json);
expect(form.outcomes.length).toBe(3);
expect(form.outcomes[0].id).toBe(FormCloud.SAVE_OUTCOME);
expect(form.outcomes[0].isSystem).toBeTruthy();
expect(form.outcomes[1].id).toBe(FormCloud.COMPLETE_OUTCOME);
expect(form.outcomes[1].isSystem).toBeTruthy();
expect(form.outcomes[2].id).toBe(FormCloud.START_PROCESS_OUTCOME);
expect(form.outcomes[2].isSystem).toBeTruthy();
});
it('should create outcomes only when fields available', () => {
const json = {formRepresentation: {formDefinition: {
fields: null
}}};
const form = new FormCloud(json);
expect(form.outcomes.length).toBe(0);
});
it('should use custom form outcomes', () => {
const json = {formRepresentation: {formDefinition: {
fields: [
{ id: 'container1' }
]},
outcomes: [
{ id: 'custom-1', name: 'custom 1' }
]
}};
const form = new FormCloud(json);
expect(form.outcomes.length).toBe(2);
expect(form.outcomes[0].id).toBe(FormCloud.SAVE_OUTCOME);
expect(form.outcomes[0].isSystem).toBeTruthy();
expect(form.outcomes[1].id).toBe('custom-1');
expect(form.outcomes[1].isSystem).toBeFalsy();
});
it('should get field by id', () => {
const form = new FormCloud({}, null, false, formCloudService);
const field: any = { id: 'field1' };
spyOn(form, 'getFormFields').and.returnValue([field]);
const result = form.getFieldById('field1');
expect(result).toBe(field);
});
});

View File

@@ -0,0 +1,247 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 {
TabModel, FormWidgetModel, FormOutcomeModel, FormValues,
FormWidgetModelCache, FormFieldModel, ContainerModel, FormFieldTypes,
ValidateFormFieldEvent, FormFieldValidator, FormFieldTemplates } from '@alfresco/adf-core';
import { FormCloudService } from '../services/form-cloud.service';
import { TaskVariableCloud } from './task-variable-cloud.model';
export class FormCloud {
static SAVE_OUTCOME: string = '$save';
static COMPLETE_OUTCOME: string = '$complete';
static START_PROCESS_OUTCOME: string = '$startProcess';
readonly id: string;
nodeId: string;
readonly name: string;
readonly taskId: string;
readonly taskName: string;
private _isValid: boolean = true;
get isValid(): boolean {
return this._isValid;
}
readonly selectedOutcome: string;
readonly json: any;
readOnly: boolean;
processDefinitionId: any;
className: string;
values: FormValues = {};
tabs: TabModel[] = [];
fields: FormWidgetModel[] = [];
outcomes: FormOutcomeModel[] = [];
customFieldTemplates: FormFieldTemplates = {};
fieldValidators: FormFieldValidator[] = [];
constructor(json?: any, formData?: TaskVariableCloud[], readOnly: boolean = false, protected formService?: FormCloudService) {
this.readOnly = readOnly;
if (json && json.formRepresentation && json.formRepresentation.formDefinition) {
this.json = json;
this.id = json.formRepresentation.id;
this.name = json.formRepresentation.name;
this.taskId = json.formRepresentation.taskId;
this.taskName = json.formRepresentation.taskName || json.formRepresentation.name;
this.processDefinitionId = json.formRepresentation.processDefinitionId;
this.customFieldTemplates = json.formRepresentation.formDefinition.customFieldTemplates || {};
this.selectedOutcome = json.formRepresentation.formDefinition.selectedOutcome || {};
this.className = json.formRepresentation.formDefinition.className || '';
const tabCache: FormWidgetModelCache<TabModel> = {};
this.tabs = (json.formRepresentation.formDefinition.tabs || []).map((t) => {
const model = new TabModel(<any> this, t);
tabCache[model.id] = model;
return model;
});
this.fields = this.parseRootFields(json);
if (formData) {
this.loadData(formData);
}
for (let i = 0; i < this.fields.length; i++) {
const field = this.fields[i];
if (field.tab) {
const tab = tabCache[field.tab];
if (tab) {
tab.fields.push(field);
}
}
}
if (json.formRepresentation.formDefinition.fields) {
const saveOutcome = new FormOutcomeModel(<any> this, {
id: FormCloud.SAVE_OUTCOME,
name: 'SAVE',
isSystem: true
});
const completeOutcome = new FormOutcomeModel(<any> this, {
id: FormCloud.COMPLETE_OUTCOME,
name: 'COMPLETE',
isSystem: true
});
const startProcessOutcome = new FormOutcomeModel(<any> this, {
id: FormCloud.START_PROCESS_OUTCOME,
name: 'START PROCESS',
isSystem: true
});
const customOutcomes = (json.formRepresentation.outcomes || []).map((obj) => new FormOutcomeModel(<any> this, obj));
this.outcomes = [saveOutcome].concat(
customOutcomes.length > 0 ? customOutcomes : [completeOutcome, startProcessOutcome]
);
}
}
this.validateForm();
}
hasTabs(): boolean {
return this.tabs && this.tabs.length > 0;
}
hasFields(): boolean {
return this.fields && this.fields.length > 0;
}
hasOutcomes(): boolean {
return this.outcomes && this.outcomes.length > 0;
}
getFieldById(fieldId: string): FormFieldModel {
return this.getFormFields().find((field) => field.id === fieldId);
}
onFormFieldChanged(field: FormFieldModel) {
this.validateField(field);
}
getFormFields(): FormFieldModel[] {
const formFields: FormFieldModel[] = [];
for (let i = 0; i < this.fields.length; i++) {
const field = this.fields[i];
if (field instanceof ContainerModel) {
const container = <ContainerModel> field;
formFields.push(container.field);
container.field.columns.forEach((column) => {
formFields.push(...column.fields);
});
}
}
return formFields;
}
markAsInvalid() {
this._isValid = false;
}
validateForm() {
const errorsField: FormFieldModel[] = [];
const fields = this.getFormFields();
for (let i = 0; i < fields.length; i++) {
if (!fields[i].validate()) {
errorsField.push(fields[i]);
}
}
this._isValid = errorsField.length > 0 ? false : true;
}
/**
* Validates a specific form field, triggers form validation.
*
* @param field Form field to validate.
* @memberof FormCloud
*/
validateField(field: FormFieldModel) {
if (!field) {
return;
}
const validateFieldEvent = new ValidateFormFieldEvent(<any> this, field);
if (!validateFieldEvent.isValid) {
this._isValid = false;
return;
}
if (validateFieldEvent.defaultPrevented) {
return;
}
if (!field.validate()) {
this._isValid = false;
}
this.validateForm();
}
// Activiti supports 3 types of root fields: container|group|dynamic-table
private parseRootFields(json: any): FormWidgetModel[] {
let fields = [];
if (json.formRepresentation.fields) {
fields = json.formRepresentation.fields;
} else if (json.formRepresentation.formDefinition && json.formRepresentation.formDefinition.fields) {
fields = json.formRepresentation.formDefinition.fields;
}
const formWidgetModel: FormWidgetModel[] = [];
for (const field of fields) {
if (field.type === FormFieldTypes.DISPLAY_VALUE) {
// workaround for dynamic table on a completed/readonly form
if (field.params) {
const originalField = field.params['field'];
if (originalField.type === FormFieldTypes.DYNAMIC_TABLE) {
formWidgetModel.push(new ContainerModel(new FormFieldModel(<any> this, field)));
}
}
} else {
formWidgetModel.push(new ContainerModel(new FormFieldModel(<any> this, field)));
}
}
return formWidgetModel;
}
// Loads external data and overrides field values
// Typically used when form definition and form data coming from different sources
private loadData(formData: TaskVariableCloud[]) {
for (const field of this.getFormFields()) {
const fieldValue = formData.find((value) => { return value.name === field.id; });
if (fieldValue) {
field.json.value = fieldValue.value;
field.value = field.parseValue(field.json);
}
}
}
}

View File

@@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
export class TaskVariableCloud {
name: string;
value: any;
constructor(obj) {
this.name = obj.name || null;
this.value = obj.value || null;
}
}

View File

@@ -0,0 +1,22 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
export * from './models/form-cloud.model';
export * from './models/task-variable-cloud.model';
export * from './components/form-cloud.component';
export * from './components/upload-cloud.widget';
export * from './services/form-cloud.service';

View File

@@ -0,0 +1,162 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { FormCloudService } from './form-cloud.service';
import { AlfrescoApiService, CoreModule, setupTestBed, AppConfigService, AppConfigServiceMock } from '@alfresco/adf-core';
import { of } from 'rxjs';
declare let jasmine: any;
const responseBody = {
entry:
{ id: 'id', name: 'name', formKey: 'form-key' }
};
const alfrescoApiServiceStub = {
getInstance() { },
load() { }
};
const oauth2Auth = jasmine.createSpyObj('oauth2Auth', ['callCustomApi']);
describe('Form Cloud service', () => {
let service: FormCloudService;
let apiService: AlfrescoApiService;
const appName = 'app-name';
const taskId = 'task-id';
setupTestBed({
imports: [
NoopAnimationsModule,
CoreModule.forRoot()
],
providers: [
FormCloudService,
{ provide: AlfrescoApiService, useValue: alfrescoApiServiceStub },
{ provide: AppConfigService, useClass: AppConfigServiceMock }
]
});
beforeEach(() => {
service = TestBed.get(FormCloudService);
apiService = TestBed.get(AlfrescoApiService);
spyOn(apiService, 'getInstance').and.returnValue({ oauth2Auth: oauth2Auth });
});
describe('Form tests', () => {
it('should fetch and parse form', (done) => {
const formId = 'form-id';
oauth2Auth.callCustomApi.and.returnValue(Promise.resolve({ formRepresentation: { id: formId, name: 'task-form', taskId: 'task-id' } }));
service.getForm(appName, formId).subscribe((result) => {
expect(result).toBeDefined();
expect(result.formRepresentation.id).toBe(formId);
expect(result.formRepresentation.name).toBe('task-form');
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[0].endsWith(`${appName}/form/v1/forms/${formId}`)).toBeTruthy();
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[1]).toBe('GET');
done();
});
});
it('should parse valid form json ', () => {
const formId = 'form-id';
const json = { formRepresentation: { id: formId, name: 'task-form', taskId: 'task-id', formDefinition: {} } };
const result = service.parseForm(json);
expect(result).toBeDefined();
expect(result.id).toBe(formId);
expect(result.name).toBe('task-form');
});
});
describe('Task tests', () => {
it('should fetch and parse task', (done) => {
oauth2Auth.callCustomApi.and.returnValue(Promise.resolve(responseBody));
service.getTask(appName, taskId).subscribe((result) => {
expect(result).toBeDefined();
expect(result.id).toBe(responseBody.entry.id);
expect(result.name).toBe(responseBody.entry.name);
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[0].endsWith(`${appName}/rb/v1/tasks/${taskId}`)).toBeTruthy();
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[1]).toBe('GET');
done();
});
});
it('should fetch task variables', (done) => {
oauth2Auth.callCustomApi.and.returnValue(Promise.resolve({ content: { name: 'abc' } }));
service.getTaskVariables(appName, taskId).subscribe((result: any) => {
expect(result).toBeDefined();
expect(result.name).toBe('abc');
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[0].endsWith(`${appName}/rb/v1/tasks/${taskId}/variables`)).toBeTruthy();
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[1]).toBe('GET');
done();
});
});
it('should fetch task form', (done) => {
spyOn(service, 'getTask').and.returnValue(of(responseBody.entry));
spyOn(service, 'getForm').and.returnValue(of({ formRepresentation: { name: 'task-form' } }));
service.getTaskForm(appName, taskId).subscribe((result) => {
expect(result).toBeDefined();
expect(result.formRepresentation.name).toBe('task-form');
expect(result.formRepresentation.taskId).toBe(responseBody.entry.id);
expect(result.formRepresentation.taskName).toBe(responseBody.entry.name);
done();
});
});
it('should save task form', (done) => {
oauth2Auth.callCustomApi.and.returnValue(Promise.resolve(responseBody));
const formId = 'form-id';
service.saveTaskForm(appName, taskId, formId, {}).subscribe((result: any) => {
expect(result).toBeDefined();
expect(result.id).toBe('id');
expect(result.name).toBe('name');
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[0].endsWith(`${appName}/form/v1/forms/${formId}/save`)).toBeTruthy();
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[1]).toBe('POST');
done();
});
});
it('should complete task form', (done) => {
oauth2Auth.callCustomApi.and.returnValue(Promise.resolve(responseBody));
const formId = 'form-id';
service.completeTaskForm(appName, taskId, formId, {}, '').subscribe((result: any) => {
expect(result).toBeDefined();
expect(result.id).toBe('id');
expect(result.name).toBe('name');
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[0].endsWith(`${appName}/form/v1/forms/${formId}/submit`)).toBeTruthy();
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[1]).toBe('POST');
done();
});
});
});
});

View File

@@ -0,0 +1,232 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { AlfrescoApiService, LogService, FormValues, AppConfigService, FormOutcomeModel } from '@alfresco/adf-core';
import { throwError, Observable, from } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
import { TaskDetailsCloudModel } from '../../task/start-task/models/task-details-cloud.model';
import { SaveFormRepresentation, CompleteFormRepresentation } from '@alfresco/js-api';
import { FormCloud } from '../models/form-cloud.model';
import { TaskVariableCloud } from '../models/task-variable-cloud.model';
@Injectable({
providedIn: 'root'
})
export class FormCloudService {
contentTypes = ['application/json']; accepts = ['application/json']; returnType = Object;
constructor(
private apiService: AlfrescoApiService,
private appConfigService: AppConfigService,
private logService: LogService
) {}
getTaskForm(appName: string, taskId: string): Observable<any> {
return this.getTask(appName, taskId).pipe(
switchMap((task: TaskDetailsCloudModel) => {
return this.getForm(appName, task.formKey).pipe(
map((form: any) => {
form.formRepresentation.taskId = task.id;
form.formRepresentation.taskName = task.name;
form.formRepresentation.processDefinitionId = task.processDefinitionId;
form.formRepresentation.processInstanceId = task.processInstanceId;
return form;
})
);
})
);
}
saveTaskForm(appName: string, taskId: string, formId: string, formValues: FormValues): Observable<TaskDetailsCloudModel> {
const apiUrl = this.buildSaveFormUrl(appName, formId);
const saveFormRepresentation = <SaveFormRepresentation> { values: formValues, taskId: taskId };
return from(this.apiService
.getInstance()
.oauth2Auth.callCustomApi(apiUrl, 'POST',
null, null, null,
null, saveFormRepresentation,
this.contentTypes, this.accepts,
this.returnType, null, null)
).pipe(
map((res: any) => {
return new TaskDetailsCloudModel(res.entry);
}),
catchError((err) => this.handleError(err))
);
}
createTemporaryRawRelatedContent(file, nodeId): Observable<any> {
const apiUrl = this.buildUploadUrl(nodeId);
return from(this.apiService
.getInstance()
.oauth2Auth.callCustomApi(apiUrl, 'POST',
null, null, null,
{ filedata: file, nodeType: 'cm:content' }, null,
['multipart/form-data'], this.accepts,
this.returnType, null, null)
).pipe(
map((res: any) => {
return (res.entry);
}),
catchError((err) => this.handleError(err))
);
}
completeTaskForm(appName: string, taskId: string, formId: string, formValues: FormValues, outcome: string): Observable<TaskDetailsCloudModel> {
const apiUrl = this.buildSubmitFormUrl(appName, formId);
const completeFormRepresentation: any = <CompleteFormRepresentation> { values: formValues, taskId: taskId };
if (outcome) {
completeFormRepresentation.outcome = outcome;
}
return from(this.apiService
.getInstance()
.oauth2Auth.callCustomApi(apiUrl, 'POST',
null, null, null,
null, completeFormRepresentation,
this.contentTypes, this.accepts,
this.returnType, null, null)
).pipe(
map((res: any) => {
return new TaskDetailsCloudModel(res.entry);
}),
catchError((err) => this.handleError(err))
);
}
getTask(appName: string, taskId: string): Observable<TaskDetailsCloudModel> {
const apiUrl = this.buildGetTaskUrl(appName, taskId);
return from(this.apiService
.getInstance()
.oauth2Auth.callCustomApi(apiUrl, 'GET',
null, null, null,
null, null,
this.contentTypes, this.accepts,
this.returnType, null, null)
).pipe(
map((res: any) => {
return new TaskDetailsCloudModel(res.entry);
}),
catchError((err) => this.handleError(err))
);
}
getProcessStorageFolderTask(appName: string, taskId: string): Observable<any> {
const apiUrl = this.buildFolderTask(appName, taskId);
return from(this.apiService
.getInstance()
.oauth2Auth.callCustomApi(apiUrl, 'GET',
null, null, null,
null, null,
this.contentTypes, this.accepts,
this.returnType, null, null)
).pipe(
map((res: any) => {
return res.nodeId;
}),
catchError((err) => this.handleError(err))
);
}
getTaskVariables(appName: string, taskId: string): Observable<TaskVariableCloud[]> {
const apiUrl = this.buildGetTaskVariablesUrl(appName, taskId);
return from(this.apiService
.getInstance()
.oauth2Auth.callCustomApi(apiUrl, 'GET',
null, null, null,
null, null,
this.contentTypes, this.accepts,
this.returnType, null, null)
).pipe(
map((res: any) => {
return <TaskVariableCloud[]> res.content;
}),
catchError((err) => this.handleError(err))
);
}
getForm(appName: string, taskId: string): Observable<any> {
const apiUrl = this.buildGetFormUrl(appName, taskId);
const bodyParam = {}, pathParams = {}, queryParams = {}, headerParams = {},
formParams = {};
return from(
this.apiService
.getInstance()
.oauth2Auth.callCustomApi(
apiUrl, 'GET', pathParams, queryParams,
headerParams, formParams, bodyParam,
this.contentTypes, this.accepts, this.returnType, null, null)
).pipe(
catchError((err) => this.handleError(err))
);
}
parseForm(json: any, data?: TaskVariableCloud[], readOnly: boolean = false): FormCloud {
if (json) {
const form = new FormCloud(json, data, readOnly, this);
if (!json.fields) {
form.outcomes = [
new FormOutcomeModel(<any> form, {
id: '$save',
name: FormOutcomeModel.SAVE_ACTION,
isSystem: true
})
];
}
return form;
}
return null;
}
private buildGetTaskUrl(appName: string, taskId: string): string {
return `${this.appConfigService.get('bpmHost')}/${appName}/rb/v1/tasks/${taskId}`;
}
private buildGetFormUrl(appName: string, formId: string): string {
return `${this.appConfigService.get('bpmHost')}/${appName}/form/v1/forms/${formId}`;
}
private buildSaveFormUrl(appName: string, formId: string): string {
return `${this.appConfigService.get('bpmHost')}/${appName}/form/v1/forms/${formId}/save`;
}
private buildUploadUrl(nodeId: string): string {
return `${this.appConfigService.get('ecmHost')}/alfresco/api/-default-/public/alfresco/versions/1/nodes/${nodeId}/children`;
}
private buildSubmitFormUrl(appName: string, formId: string): string {
return `${this.appConfigService.get('bpmHost')}/${appName}/form/v1/forms/${formId}/submit`;
}
private buildGetTaskVariablesUrl(appName: string, taskId: string): string {
return `${this.appConfigService.get('bpmHost')}/${appName}/rb/v1/tasks/${taskId}/variables`;
}
private buildFolderTask(appName: string, taskId: string): string {
return `${this.appConfigService.get('bpmHost')}/${appName}/process-storage/v1/folders/tasks/${taskId}`;
}
private handleError(error: any) {
this.logService.error(error);
return throwError(error || 'Server error');
}
}

View File

@@ -20,7 +20,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout'; import { FlexLayoutModule } from '@angular/flex-layout';
import { TemplateModule, FormModule, PipeModule, CoreModule } from '@alfresco/adf-core'; import { TemplateModule, PipeModule, CoreModule } from '@alfresco/adf-core';
import { MaterialModule } from '../material.module'; import { MaterialModule } from '../material.module';
import { GroupCloudComponent } from './components/group-cloud.component'; import { GroupCloudComponent } from './components/group-cloud.component';
import { InitialGroupNamePipe } from './pipe/group-initial.pipe'; import { InitialGroupNamePipe } from './pipe/group-initial.pipe';
@@ -34,7 +34,6 @@ import { InitialGroupNamePipe } from './pipe/group-initial.pipe';
MaterialModule, MaterialModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
FormModule,
CoreModule CoreModule
], ],
declarations: [GroupCloudComponent, InitialGroupNamePipe], declarations: [GroupCloudComponent, InitialGroupNamePipe],

View File

@@ -21,6 +21,7 @@ import { AppListCloudModule } from './app/app-list-cloud.module';
import { TaskCloudModule } from './task/task-cloud.module'; import { TaskCloudModule } from './task/task-cloud.module';
import { ProcessCloudModule } from './process/process-cloud.module'; import { ProcessCloudModule } from './process/process-cloud.module';
import { GroupCloudModule } from './group/group-cloud.module'; import { GroupCloudModule } from './group/group-cloud.module';
import { FormCloudModule } from './form/form-cloud.module';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -28,7 +29,8 @@ import { GroupCloudModule } from './group/group-cloud.module';
AppListCloudModule, AppListCloudModule,
ProcessCloudModule, ProcessCloudModule,
TaskCloudModule, TaskCloudModule,
GroupCloudModule GroupCloudModule,
FormCloudModule
], ],
providers: [ providers: [
{ {
@@ -44,7 +46,8 @@ import { GroupCloudModule } from './group/group-cloud.module';
AppListCloudModule, AppListCloudModule,
ProcessCloudModule, ProcessCloudModule,
TaskCloudModule, TaskCloudModule,
GroupCloudModule GroupCloudModule,
FormCloudModule
] ]
}) })
export class ProcessServicesCloudModule { } export class ProcessServicesCloudModule { }

View File

@@ -20,7 +20,7 @@ import { ProcessFiltersCloudModule } from './process-filters/process-filters-clo
import { ProcessListCloudModule } from './process-list/process-list-cloud.module'; import { ProcessListCloudModule } from './process-list/process-list-cloud.module';
import { StartProcessCloudModule } from './start-process/start-process-cloud.module'; import { StartProcessCloudModule } from './start-process/start-process-cloud.module';
import { CoreModule } from '@alfresco/adf-core'; import { CoreModule } from '@alfresco/adf-core';
import { ProcessHeaderCloudModule } from './process-header/public-api'; import { ProcessHeaderCloudModule } from './process-header/process-header-cloud.module';
@NgModule({ @NgModule({
imports: [ imports: [

View File

@@ -17,7 +17,7 @@
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { CardViewItem, CardViewTextItemModel, TranslationService, AppConfigService, CardViewDateItemModel, CardViewBaseItemModel } from '@alfresco/adf-core'; import { CardViewItem, CardViewTextItemModel, TranslationService, AppConfigService, CardViewDateItemModel, CardViewBaseItemModel } from '@alfresco/adf-core';
import { ProcessInstanceCloud } from '../../start-process/public-api'; import { ProcessInstanceCloud } from '../../start-process/models/process-instance-cloud.model';
import { ProcessHeaderCloudService } from '../services/process-header-cloud.service'; import { ProcessHeaderCloudService } from '../services/process-header-cloud.service';
@Component({ @Component({

View File

@@ -19,7 +19,7 @@ import { AlfrescoApiService, LogService, AppConfigService } from '@alfresco/adf-
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, from, throwError } from 'rxjs'; import { Observable, from, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { ProcessInstanceCloud } from '../../start-process/public-api'; import { ProcessInstanceCloud } from '../../start-process/models/process-instance-cloud.model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'

View File

@@ -31,7 +31,7 @@ import {
UserPreferenceValues UserPreferenceValues
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { PeopleCloudComponent } from './people-cloud/people-cloud.component'; import { PeopleCloudComponent } from './people-cloud/people-cloud.component';
import { GroupCloudComponent } from '../../../../lib/group/public-api'; import { GroupCloudComponent } from '../../../../lib/group/components/group-cloud.component';
@Component({ @Component({
selector: 'adf-cloud-start-task', selector: 'adf-cloud-start-task',

View File

@@ -19,7 +19,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout'; import { FlexLayoutModule } from '@angular/flex-layout';
import { MaterialModule } from '../../material.module'; import { MaterialModule } from '../../material.module';
import { TemplateModule, FormModule, PipeModule, CoreModule } from '@alfresco/adf-core'; import { TemplateModule, PipeModule, CoreModule } from '@alfresco/adf-core';
import { StartTaskCloudComponent } from './components/start-task-cloud.component'; import { StartTaskCloudComponent } from './components/start-task-cloud.component';
import { StartTaskCloudService } from './services/start-task-cloud.service'; import { StartTaskCloudService } from './services/start-task-cloud.service';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -36,7 +36,6 @@ import { GroupCloudModule } from '../../group/group-cloud.module';
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
GroupCloudModule, GroupCloudModule,
FormModule,
GroupCloudModule, GroupCloudModule,
CoreModule CoreModule
], ],

View File

@@ -21,7 +21,7 @@ import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout'; import { FlexLayoutModule } from '@angular/flex-layout';
import { MaterialModule } from '../../../material.module'; import { MaterialModule } from '../../../material.module';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TemplateModule, TranslateLoaderService, FormModule, PipeModule } from '@alfresco/adf-core'; import { TemplateModule, TranslateLoaderService, PipeModule } from '@alfresco/adf-core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { StartTaskCloudModule } from '../start-task-cloud.module'; import { StartTaskCloudModule } from '../start-task-cloud.module';
@@ -41,7 +41,6 @@ import { StartTaskCloudModule } from '../start-task-cloud.module';
MaterialModule, MaterialModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
FormModule,
PipeModule, PipeModule,
StartTaskCloudModule StartTaskCloudModule
] ]

View File

@@ -22,3 +22,4 @@ export * from './lib/process/public-api';
export * from './lib/task/public-api'; export * from './lib/task/public-api';
export * from './lib/group/public-api'; export * from './lib/group/public-api';
export * from './lib/services/public-api'; export * from './lib/services/public-api';
export * from './lib/form/public-api';

View File

@@ -0,0 +1,46 @@
<div *ngIf="!hasForm()">
<ng-content select="[empty-form]">
</ng-content>
</div>
<div *ngIf="hasForm()" class="adf-form-container">
<mat-card>
<mat-card-header>
<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="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">
{{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">
<button [id]="'adf-form-'+ outcome.name | formatSpace" *ngFor="let outcome of form.outcomes"
[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>
</mat-card-actions>
</mat-card>
</div>

View File

@@ -16,15 +16,11 @@
*/ */
import { SimpleChange } from '@angular/core'; import { SimpleChange } from '@angular/core';
import { LogService } from '../../services/log.service';
import { Observable, of, throwError } from 'rxjs'; import { Observable, of, throwError } from 'rxjs';
import { fakeForm } from '../../mock'; import { FormFieldModel, FormFieldTypes, FormModel, FormOutcomeEvent, FormOutcomeModel,
import { FormService } from './../services/form.service'; FormService, WidgetVisibilityService, NodeService, LogService, ContainerModel, fakeForm, FormRenderingService } from '@alfresco/adf-core';
import { NodeService } from './../services/node.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { FormComponent } from './form.component'; import { FormComponent } from './form.component';
import { FormFieldModel, FormFieldTypes, FormModel, FormOutcomeEvent, FormOutcomeModel } from './widgets/index';
import { ContainerModel } from './widgets/core/container.model';
describe('FormComponent', () => { describe('FormComponent', () => {
@@ -33,6 +29,7 @@ describe('FormComponent', () => {
let visibilityService: WidgetVisibilityService; let visibilityService: WidgetVisibilityService;
let nodeService: NodeService; let nodeService: NodeService;
let logService: LogService; let logService: LogService;
let formRenderingService: FormRenderingService;
beforeEach(() => { beforeEach(() => {
logService = new LogService(null); logService = new LogService(null);
@@ -40,7 +37,8 @@ describe('FormComponent', () => {
spyOn(visibilityService, 'refreshVisibility').and.stub(); spyOn(visibilityService, 'refreshVisibility').and.stub();
formService = new FormService(null, null, logService); formService = new FormService(null, null, logService);
nodeService = new NodeService(null); nodeService = new NodeService(null);
formComponent = new FormComponent(formService, visibilityService, null, nodeService); formRenderingService = new FormRenderingService();
formComponent = new FormComponent(formService, visibilityService, null, nodeService, formRenderingService);
}); });
it('should check form', () => { it('should check form', () => {
@@ -57,13 +55,7 @@ describe('FormComponent', () => {
expect(formModel.taskName).toBe(FormModel.UNSET_TASK_NAME); expect(formModel.taskName).toBe(FormModel.UNSET_TASK_NAME);
expect(formComponent.isTitleEnabled()).toBeTruthy(); expect(formComponent.isTitleEnabled()).toBeTruthy();
// override property as it's the readonly one formComponent.form = null;
Object.defineProperty(formModel, 'taskName', {
enumerable: false,
configurable: false,
writable: false,
value: null
});
expect(formComponent.isTitleEnabled()).toBeFalsy(); expect(formComponent.isTitleEnabled()).toBeFalsy();
}); });

View File

@@ -15,40 +15,24 @@
* limitations under the License. * limitations under the License.
*/ */
/* tslint:disable */
import { import {
Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Component, EventEmitter, Input, Output, ViewEncapsulation, SimpleChanges, OnInit, OnDestroy, OnChanges
Output, SimpleChanges, ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { FormErrorEvent, FormEvent } from './../events/index'; import { AttachFileWidgetComponent, AttachFolderWidgetComponent } from '../content-widget';
import { EcmModelService } from './../services/ecm-model.service'; import { EcmModelService, NodeService, WidgetVisibilityService,
import { FormService } from './../services/form.service'; FormService, FormRenderingService, FormBaseComponent, FormOutcomeModel,
import { NodeService } from './../services/node.service'; ValidateFormEvent, FormEvent, FormErrorEvent, FormFieldModel,
import { ContentLinkModel } from './widgets/core/content-link.model'; FormModel, FormOutcomeEvent, FormValues, ContentLinkModel } from '@alfresco/adf-core';
import {
FormFieldModel, FormModel, FormOutcomeEvent, FormOutcomeModel, import { Observable, of, Subscription } from 'rxjs';
FormValues, FormFieldValidator
} from './widgets/core/index';
import { Observable, of } from 'rxjs';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { ValidateFormEvent } from './../events/validate-form.event';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'adf-form', selector: 'adf-form',
templateUrl: './form.component.html', templateUrl: './form.component.html',
styleUrls: ['./form.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class FormComponent implements OnInit, OnChanges, OnDestroy { export class FormComponent extends FormBaseComponent implements OnInit, OnDestroy, OnChanges {
static SAVE_OUTCOME_ID: string = '$save';
static COMPLETE_OUTCOME_ID: string = '$complete';
static START_PROCESS_OUTCOME_ID: string = '$startProcess';
static CUSTOM_OUTCOME_ID: string = '$custom';
static COMPLETE_BUTTON_COLOR: string = 'primary';
static COMPLETE_OUTCOME_NAME: string = 'COMPLETE';
/** Underlying form model instance. */ /** Underlying form model instance. */
@Input() @Input()
@@ -78,54 +62,6 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
@Input() @Input()
data: FormValues; data: FormValues;
/** Path of the folder where the metadata will be stored. */
@Input()
path: string;
/** Name to assign to the new node where the metadata are stored. */
@Input()
nameNode: string;
/** Toggle rendering of the form title. */
@Input()
showTitle: boolean = true;
/** Toggle rendering of the `Complete` outcome button. */
@Input()
showCompleteButton: boolean = true;
/** If true then the `Complete` outcome button is shown but it will be disabled. */
@Input()
disableCompleteButton: boolean = false;
/** If true then the `Start Process` outcome button is shown but it will be disabled. */
@Input()
disableStartProcessButton: boolean = false;
/** Toggle rendering of the `Save` outcome button. */
@Input()
showSaveButton: boolean = true;
/** Toggle debug options. */
@Input()
showDebugButton: boolean = false;
/** Toggle readonly state of the form. Forces all form widgets to render as readonly if enabled. */
@Input()
readOnly: boolean = false;
/** Toggle rendering of the `Refresh` button. */
@Input()
showRefreshButton: boolean = true;
/** Toggle rendering of the validation icon next to the form title. */
@Input()
showValidationIcon: boolean = true;
/** Contains a list of form field validator instances. */
@Input()
fieldValidators: FormFieldValidator[] = [];
/** 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: EventEmitter<FormModel> = new EventEmitter<FormModel>(); formSaved: EventEmitter<FormModel> = new EventEmitter<FormModel>();
@@ -146,87 +82,18 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
@Output() @Output()
formDataRefreshed: EventEmitter<FormModel> = new EventEmitter<FormModel>(); formDataRefreshed: EventEmitter<FormModel> = new EventEmitter<FormModel>();
/** Emitted when the supplied form values have a validation error.*/
@Output()
formError: EventEmitter<FormFieldModel[]> = new EventEmitter<FormFieldModel[]>();
/** Emitted when any outcome is executed. Default behaviour can be prevented
* via `event.preventDefault()`.
*/
@Output()
executeOutcome: EventEmitter<FormOutcomeEvent> = new EventEmitter<FormOutcomeEvent>();
/**
* Emitted when any error occurs.
*/
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
debugMode: boolean = false; debugMode: boolean = false;
protected subscriptions: Subscription[] = []; protected subscriptions: Subscription[] = [];
constructor(protected formService: FormService, constructor(protected formService: FormService,
protected visibilityService: WidgetVisibilityService, protected visibilityService: WidgetVisibilityService,
private ecmModelService: EcmModelService, protected ecmModelService: EcmModelService,
private nodeService: NodeService) { protected nodeService: NodeService,
} protected formRenderingService: FormRenderingService) {
super();
hasForm(): boolean { this.formRenderingService.setComponentTypeResolver('upload', () => AttachFileWidgetComponent, true);
return this.form ? true : false; this.formRenderingService.setComponentTypeResolver('select-folder', () => AttachFolderWidgetComponent, true);
}
isTitleEnabled(): boolean {
if (this.showTitle) {
if (this.form && this.form.taskName) {
return true;
}
}
return false;
}
getColorForOutcome(outcomeName: string): string {
return outcomeName === FormComponent.COMPLETE_OUTCOME_NAME ? FormComponent.COMPLETE_BUTTON_COLOR : '';
}
isOutcomeButtonEnabled(outcome: FormOutcomeModel): boolean {
if (this.form.readOnly) {
return false;
}
if (outcome) {
// Make 'Save' button always available
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return true;
}
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return this.disableCompleteButton ? false : this.form.isValid;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return this.disableStartProcessButton ? false : this.form.isValid;
}
return this.form.isValid;
}
return false;
}
isOutcomeButtonVisible(outcome: FormOutcomeModel, isFormReadOnly: boolean): boolean {
if (outcome && outcome.name) {
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return this.showCompleteButton;
}
if (isFormReadOnly) {
return outcome.isSelected;
}
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return this.showSaveButton;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return false;
}
return true;
}
return false;
} }
ngOnInit() { ngOnInit() {
@@ -243,87 +110,42 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
} }
ngOnDestroy() { ngOnDestroy() {
this.subscriptions.forEach(subscription => subscription.unsubscribe()); this.subscriptions.forEach((subscription) => subscription.unsubscribe());
this.subscriptions = []; this.subscriptions = [];
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
let taskId = changes['taskId']; const taskId = changes['taskId'];
if (taskId && taskId.currentValue) { if (taskId && taskId.currentValue) {
this.getFormByTaskId(taskId.currentValue); this.getFormByTaskId(taskId.currentValue);
return; return;
} }
let formId = changes['formId']; const formId = changes['formId'];
if (formId && formId.currentValue) { if (formId && formId.currentValue) {
this.getFormDefinitionByFormId(formId.currentValue); this.getFormDefinitionByFormId(formId.currentValue);
return; return;
} }
let formName = changes['formName']; const formName = changes['formName'];
if (formName && formName.currentValue) { if (formName && formName.currentValue) {
this.getFormDefinitionByFormName(formName.currentValue); this.getFormDefinitionByFormName(formName.currentValue);
return; return;
} }
let nodeId = changes['nodeId']; const nodeId = changes['nodeId'];
if (nodeId && nodeId.currentValue) { if (nodeId && nodeId.currentValue) {
this.loadFormForEcmNode(nodeId.currentValue); this.loadFormForEcmNode(nodeId.currentValue);
return; return;
} }
let data = changes['data']; const data = changes['data'];
if (data && data.currentValue) { if (data && data.currentValue) {
this.refreshFormData(); this.refreshFormData();
return; return;
} }
} }
/**
* Invoked when user clicks outcome button.
* @param outcome Form outcome model
*/
onOutcomeClicked(outcome: FormOutcomeModel): boolean {
if (!this.readOnly && outcome && this.form) {
if (!this.onExecuteOutcome(outcome)) {
return false;
}
if (outcome.isSystem) {
if (outcome.id === FormComponent.SAVE_OUTCOME_ID) {
this.saveTaskForm();
return true;
}
if (outcome.id === FormComponent.COMPLETE_OUTCOME_ID) {
this.completeTaskForm();
return true;
}
if (outcome.id === FormComponent.START_PROCESS_OUTCOME_ID) {
this.completeTaskForm();
return true;
}
if (outcome.id === FormComponent.CUSTOM_OUTCOME_ID) {
this.onTaskSaved(this.form);
this.storeFormAsMetadata();
return true;
}
} else {
// Note: Activiti is using NAME field rather than ID for outcomes
if (outcome.name) {
this.onTaskSaved(this.form);
this.completeTaskForm(outcome.name);
return true;
}
}
}
return false;
}
/** /**
* Invoked when user clicks form refresh button. * Invoked when user clicks form refresh button.
*/ */
@@ -370,7 +192,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.formService this.formService
.getTaskForm(taskId) .getTaskForm(taskId)
.subscribe( .subscribe(
form => { (form) => {
const parsedForm = this.parseForm(form); const parsedForm = this.parseForm(form);
this.visibilityService.refreshVisibility(parsedForm); this.visibilityService.refreshVisibility(parsedForm);
parsedForm.validateForm(); parsedForm.validateForm();
@@ -378,7 +200,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.onFormLoaded(this.form); this.onFormLoaded(this.form);
resolve(this.form); resolve(this.form);
}, },
error => { (error) => {
this.handleError(error); this.handleError(error);
// reject(error); // reject(error);
resolve(null); resolve(null);
@@ -392,7 +214,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.formService this.formService
.getFormDefinitionById(formId) .getFormDefinitionById(formId)
.subscribe( .subscribe(
form => { (form) => {
this.formName = form.name; this.formName = form.name;
this.form = this.parseForm(form); this.form = this.parseForm(form);
this.visibilityService.refreshVisibility(this.form); this.visibilityService.refreshVisibility(this.form);
@@ -409,9 +231,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.formService this.formService
.getFormDefinitionByName(formName) .getFormDefinitionByName(formName)
.subscribe( .subscribe(
id => { (id) => {
this.formService.getFormDefinitionById(id).subscribe( this.formService.getFormDefinitionById(id).subscribe(
form => { (form) => {
this.form = this.parseForm(form); this.form = this.parseForm(form);
this.visibilityService.refreshVisibility(this.form); this.visibilityService.refreshVisibility(this.form);
this.form.validateForm(); this.form.validateForm();
@@ -437,7 +259,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.onTaskSaved(this.form); this.onTaskSaved(this.form);
this.storeFormAsMetadata(); this.storeFormAsMetadata();
}, },
error => this.onTaskSavedError(this.form, error) (error) => this.onTaskSavedError(this.form, error)
); );
} }
} }
@@ -451,7 +273,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.onTaskCompleted(this.form); this.onTaskCompleted(this.form);
this.storeFormAsMetadata(); this.storeFormAsMetadata();
}, },
error => this.onTaskCompletedError(this.form, error) (error) => this.onTaskCompletedError(this.form, error)
); );
} }
} }
@@ -462,7 +284,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
parseForm(json: any): FormModel { parseForm(json: any): FormModel {
if (json) { if (json) {
let form = new FormModel(json, this.data, this.readOnly, this.formService); const form = new FormModel(json, this.data, this.readOnly, this.formService);
if (!json.fields) { if (!json.fields) {
form.outcomes = this.getFormDefinitionOutcomes(form); form.outcomes = this.getFormDefinitionOutcomes(form);
} }
@@ -480,7 +302,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
*/ */
getFormDefinitionOutcomes(form: FormModel): FormOutcomeModel[] { getFormDefinitionOutcomes(form: FormModel): FormOutcomeModel[] {
return [ return [
new FormOutcomeModel(form, { id: '$custom', name: FormOutcomeModel.SAVE_ACTION, isSystem: true }) new FormOutcomeModel(form, { id: '$save', name: FormOutcomeModel.SAVE_ACTION, isSystem: true })
]; ];
} }
@@ -497,7 +319,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
} }
private loadFormForEcmNode(nodeId: string): void { private loadFormForEcmNode(nodeId: string): void {
this.nodeService.getNodeMetadata(nodeId).subscribe(data => { this.nodeService.getNodeMetadata(nodeId).subscribe((data) => {
this.data = data.metadata; this.data = data.metadata;
this.loadFormFromActiviti(data.nodeType); this.loadFormFromActiviti(data.nodeType);
}, },
@@ -506,9 +328,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
loadFormFromActiviti(nodeType: string): any { loadFormFromActiviti(nodeType: string): any {
this.formService.searchFrom(nodeType).subscribe( this.formService.searchFrom(nodeType).subscribe(
form => { (form) => {
if (!form) { if (!form) {
this.formService.createFormFromANode(nodeType).subscribe(formMetadata => { this.formService.createFormFromANode(nodeType).subscribe((formMetadata) => {
this.loadFormFromFormId(formMetadata.id); this.loadFormFromFormId(formMetadata.id);
}); });
} else { } else {
@@ -526,9 +348,9 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
this.loadForm(); this.loadForm();
} }
private storeFormAsMetadata() { protected storeFormAsMetadata() {
if (this.saveMetadata) { if (this.saveMetadata) {
this.ecmModelService.createEcmTypeForActivitiForm(this.formName, this.form).subscribe(type => { this.ecmModelService.createEcmTypeForActivitiForm(this.formName, this.form).subscribe((type) => {
this.nodeService.createNodeMetadata(type.nodeType || type.entry.prefixedName, EcmModelService.MODEL_NAMESPACE, this.form.values, this.path, this.nameNode); this.nodeService.createNodeMetadata(type.nodeType || type.entry.prefixedName, EcmModelService.MODEL_NAMESPACE, this.form.values, this.path, this.nameNode);
}, },
(error) => { (error) => {
@@ -569,7 +391,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
} }
protected onExecuteOutcome(outcome: FormOutcomeModel): boolean { protected onExecuteOutcome(outcome: FormOutcomeModel): boolean {
let args = new FormOutcomeEvent(outcome); const args = new FormOutcomeEvent(outcome);
this.formService.executeOutcome.next(args); this.formService.executeOutcome.next(args);
if (args.defaultPrevented) { if (args.defaultPrevented) {
@@ -583,4 +405,5 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
return true; return true;
} }
} }

View File

@@ -22,13 +22,11 @@ import { of } from 'rxjs';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { formDefinitionDropdownField, formDefinitionTwoTextFields, formDefinitionRequiredField } from '../../mock'; import { formDefinitionDropdownField, formDefinitionTwoTextFields,
import { formReadonlyTwoTextFields } from '../../mock'; formDefinitionRequiredField, FormService, setupTestBed, CoreModule,
import { formDefVisibilitiFieldDependsOnNextOne, formDefVisibilitiFieldDependsOnPreviousOne } from '../../mock'; formDefVisibilitiFieldDependsOnNextOne, formDefVisibilitiFieldDependsOnPreviousOne,
import { FormService } from './../services/form.service'; formReadonlyTwoTextFields } from '@alfresco/adf-core';
import { FormComponent } from './form.component'; import { FormComponent } from './form.component';
import { setupTestBed } from '../../testing/setupTestBed';
import { CoreModule } from '../../core.module';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
/** Duration of the select opening animation. */ /** Duration of the select opening animation. */
@@ -54,6 +52,9 @@ describe('FormComponent UI and visibility', () => {
NoopAnimationsModule, NoopAnimationsModule,
CoreModule.forRoot() CoreModule.forRoot()
], ],
declarations: [
FormComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}); });

View File

@@ -0,0 +1,38 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { NgModule } from '@angular/core';
import { MaterialModule } from '../material.module';
import { CoreModule } from '@alfresco/adf-core';
import { FormComponent } from './form.component';
import { StartFormComponent } from './start-form.component';
@NgModule({
imports: [
CoreModule.forChild(),
MaterialModule
],
declarations: [
FormComponent,
StartFormComponent
],
exports: [
FormComponent,
StartFormComponent
]
})
export class FormModule {}

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
export * from './public-api';

View File

@@ -0,0 +1,20 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
export * from './form.component';
export * from './start-form.component';
export * from './form.module';

View File

@@ -0,0 +1,125 @@
@mixin adf-start-form-component-theme($theme) {
$config: mat-typography-config();
$warn: map-get($theme, warn);
$accent: map-get($theme, accent);
.adf {
&-form-container {
max-width: 100% !important;
max-height: 100% !important;
& .mat-card {
padding: 16px 24px;
overflow: hidden;
}
& .mat-card-header-text {
margin: 0 !important;
}
& .mat-tab-body-content {
overflow: hidden;
}
& .mat-tab-label {
font-size: mat-font-size($config, subheading-2);
line-height: mat-line-height($config, headline);
letter-spacing: -0.4px;
text-align: left;
color: rgba(0, 0, 0, 0.54);
text-transform: uppercase;
}
& .mat-ink-bar {
height: 4px;
}
& .mat-form-field-wrapper {
margin: 0 12px 0 0;
}
}
&-form-title {
font-size: mat-font-size($alfresco-typography, title);
}
&-form-debug-container {
padding: 10px;
}
&-form-debug-container .adf-debug-toggle-text {
padding-left: 15px;
cursor: pointer;
}
&-form-debug-container .adf-debug-toggle-text:hover {
font-weight: bold;
}
&-form-reload-button {
position: absolute;
right: 12px;
top: 30px;
}
&-form-validation-button {
position: absolute;
right: 50px;
top: 39px;
color: mat-color($accent);
& .adf-invalid-color {
color: mat-color($warn);
}
}
&-form-hide-button {
display: none !important;
}
&-task-title {
text-align: center;
}
&-label {
width: 32px;
height: 16px;
font-size: mat-font-size($config, caption);
line-height: mat-line-height($config, headline);
text-align: left;
white-space: nowrap;
}
&-form-mat-card-actions {
float: right;
padding-bottom: 25px !important;
padding-right: 25px !important;
& .mat-button {
height: 36px;
border-radius: 5px;
}
& .mat-button-wrapper {
width: 58px;
height: 20px;
opacity: 0.54;
font-size: mat-font-size($config, body-2);
font-weight: bold;
}
}
}
form-field {
width: 100%;
.mat-input-element {
font-size: mat-font-size($config, body-2);
padding-top: 8px;
line-height: normal;
}
}
}

View File

@@ -18,15 +18,11 @@
import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs'; import { of, throwError } from 'rxjs';
import { startFormDateWidgetMock, startFormDropdownDefinitionMock, startFormTextDefinitionMock, startMockForm, startMockFormWithTab } from '../../mock'; import { startFormDateWidgetMock, startFormDropdownDefinitionMock, startFormTextDefinitionMock, startMockForm, startMockFormWithTab } from '../../core/mock';
import { startFormAmountWidgetMock, startFormNumberWidgetMock, startFormRadioButtonWidgetMock } from '../../mock'; import { startFormAmountWidgetMock, startFormNumberWidgetMock, startFormRadioButtonWidgetMock } from '../../core/mock';
import { FormService } from './../services/form.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { StartFormComponent } from './start-form.component'; import { StartFormComponent } from './start-form.component';
import { FormModel, FormOutcomeModel } from './widgets/index';
import { setupTestBed } from '../../testing/setupTestBed';
import { CoreModule } from '../../core.module';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { FormService, WidgetVisibilityService, setupTestBed, CoreModule, FormModel, FormOutcomeModel } from '@alfresco/adf-core';
describe('StartFormComponent', () => { describe('StartFormComponent', () => {
@@ -44,6 +40,9 @@ describe('StartFormComponent', () => {
NoopAnimationsModule, NoopAnimationsModule,
CoreModule.forRoot() CoreModule.forRoot()
], ],
declarations: [
StartFormComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}); });

View File

@@ -28,17 +28,13 @@ import {
ViewEncapsulation, ViewEncapsulation,
OnDestroy OnDestroy
} from '@angular/core'; } from '@angular/core';
import { FormService } from './../services/form.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { FormComponent } from './form.component'; import { FormComponent } from './form.component';
import { ContentLinkModel } from './widgets/core/content-link.model'; import { ContentLinkModel, FormService, WidgetVisibilityService, FormRenderingService, ValidateFormEvent, FormOutcomeModel } from '@alfresco/adf-core';
import { FormOutcomeModel } from './widgets/core/index';
import { ValidateFormEvent } from './../events/validate-form.event';
@Component({ @Component({
selector: 'adf-start-form', selector: 'adf-start-form',
templateUrl: './start-form.component.html', templateUrl: './start-form.component.html',
styleUrls: ['./form.component.scss'], styleUrls: ['./start-form.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class StartFormComponent extends FormComponent implements OnChanges, OnInit, OnDestroy { export class StartFormComponent extends FormComponent implements OnChanges, OnInit, OnDestroy {
@@ -75,8 +71,9 @@ export class StartFormComponent extends FormComponent implements OnChanges, OnIn
outcomesContainer: ElementRef = null; outcomesContainer: ElementRef = null;
constructor(formService: FormService, constructor(formService: FormService,
visibilityService: WidgetVisibilityService) { visibilityService: WidgetVisibilityService,
super(formService, visibilityService, null, null); formRenderingService: FormRenderingService) {
super(formService, visibilityService, null, null, formRenderingService);
this.showTitle = false; this.showTitle = false;
} }

View File

@@ -22,5 +22,6 @@ export * from './attachment/index';
export * from './process-comments/index'; export * from './process-comments/index';
export * from './people/index'; export * from './people/index';
export * from './content-widget/index'; export * from './content-widget/index';
export * from './form/index';
export * from './process.module'; export * from './process.module';

View File

@@ -20,7 +20,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { FormModule, setupTestBed } from '@alfresco/adf-core'; import { setupTestBed } from '@alfresco/adf-core';
import { TaskListModule } from '../../task-list/task-list.module'; import { TaskListModule } from '../../task-list/task-list.module';
import { ProcessInstance } from '../models/process-instance.model'; import { ProcessInstance } from '../models/process-instance.model';
@@ -28,6 +28,7 @@ import { exampleProcess, exampleProcessNoName } from './../../mock';
import { ProcessService } from './../services/process.service'; import { ProcessService } from './../services/process.service';
import { ProcessInstanceDetailsComponent } from './process-instance-details.component'; import { ProcessInstanceDetailsComponent } from './process-instance-details.component';
import { ProcessTestingModule } from '../../testing/process.testing.module'; import { ProcessTestingModule } from '../../testing/process.testing.module';
import { FormModule } from '../../form';
describe('ProcessInstanceDetailsComponent', () => { describe('ProcessInstanceDetailsComponent', () => {

View File

@@ -21,17 +21,17 @@ import {
} from '@angular/core'; } from '@angular/core';
import { import {
ActivitiContentService, AppConfigService, AppConfigValues, ActivitiContentService, AppConfigService, AppConfigValues,
StartFormComponent, FormRenderingService, FormValues FormValues
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { ProcessInstanceVariable } from '../models/process-instance-variable.model'; import { ProcessInstanceVariable } from '../models/process-instance-variable.model';
import { ProcessDefinitionRepresentation } from './../models/process-definition.model'; import { ProcessDefinitionRepresentation } from './../models/process-definition.model';
import { ProcessInstance } from './../models/process-instance.model'; import { ProcessInstance } from './../models/process-instance.model';
import { ProcessService } from './../services/process.service'; import { ProcessService } from './../services/process.service';
import { AttachFileWidgetComponent, AttachFolderWidgetComponent } from '../../content-widget';
import { FormControl, Validators, AbstractControl } from '@angular/forms'; import { FormControl, Validators, AbstractControl } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { MatAutocompleteTrigger } from '@angular/material'; import { MatAutocompleteTrigger } from '@angular/material';
import { StartFormComponent } from '../../form';
@Component({ @Component({
selector: 'adf-start-process', selector: 'adf-start-process',
@@ -102,12 +102,9 @@ export class StartProcessInstanceComponent implements OnChanges, OnInit {
maxProcessNameLength: number = this.MAX_LENGTH; maxProcessNameLength: number = this.MAX_LENGTH;
constructor(private activitiProcess: ProcessService, constructor(private activitiProcess: ProcessService,
private formRenderingService: FormRenderingService,
private activitiContentService: ActivitiContentService, private activitiContentService: ActivitiContentService,
private appConfig: AppConfigService) { private appConfig: AppConfigService) {
this.formRenderingService.setComponentTypeResolver('upload', () => AttachFileWidgetComponent, true); }
this.formRenderingService.setComponentTypeResolver('select-folder', () => AttachFolderWidgetComponent, true);
}
ngOnInit() { ngOnInit() {
this.processNameInput = new FormControl(this.name, [Validators.required, Validators.maxLength(this.maxProcessNameLength)]); this.processNameInput = new FormControl(this.name, [Validators.required, Validators.maxLength(this.maxProcessNameLength)]);

View File

@@ -33,6 +33,7 @@ import { ProcessInstanceHeaderComponent } from './components/process-instance-he
import { ProcessInstanceTasksComponent } from './components/process-instance-tasks.component'; import { ProcessInstanceTasksComponent } from './components/process-instance-tasks.component';
import { ProcessInstanceListComponent } from './components/process-list.component'; import { ProcessInstanceListComponent } from './components/process-list.component';
import { StartProcessInstanceComponent } from './components/start-process.component'; import { StartProcessInstanceComponent } from './components/start-process.component';
import { FormModule } from '../form/form.module';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -45,7 +46,8 @@ import { StartProcessInstanceComponent } from './components/start-process.compon
TaskListModule, TaskListModule,
PeopleModule, PeopleModule,
ContentWidgetModule, ContentWidgetModule,
ProcessCommentsModule ProcessCommentsModule,
FormModule
], ],
declarations: [ declarations: [
ProcessInstanceListComponent, ProcessInstanceListComponent,

View File

@@ -28,6 +28,7 @@ import { AppsListModule } from './app-list/apps-list.module';
import { ProcessCommentsModule } from './process-comments/process-comments.module'; import { ProcessCommentsModule } from './process-comments/process-comments.module';
import { AttachmentModule } from './attachment/attachment.module'; import { AttachmentModule } from './attachment/attachment.module';
import { PeopleModule } from './people/people.module'; import { PeopleModule } from './people/people.module';
import { FormModule } from './form/form.module';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -41,7 +42,8 @@ import { PeopleModule } from './people/people.module';
TaskListModule, TaskListModule,
AppsListModule, AppsListModule,
AttachmentModule, AttachmentModule,
PeopleModule PeopleModule,
FormModule
], ],
providers: [ providers: [
{ {
@@ -62,7 +64,8 @@ import { PeopleModule } from './people/people.module';
TaskListModule, TaskListModule,
AppsListModule, AppsListModule,
AttachmentModule, AttachmentModule,
PeopleModule PeopleModule,
FormModule
] ]
}) })
export class ProcessModule { export class ProcessModule {

View File

@@ -9,6 +9,7 @@
@import '../task-list/components/task-standalone.component'; @import '../task-list/components/task-standalone.component';
@import '../app-list/apps-list.component'; @import '../app-list/apps-list.component';
@import '../content-widget/attach-file-widget-dialog.component'; @import '../content-widget/attach-file-widget-dialog.component';
@import '../form/start-form.component';
@mixin adf-process-services-theme($theme) { @mixin adf-process-services-theme($theme) {
@include adf-process-filters-theme($theme); @include adf-process-filters-theme($theme);
@@ -22,4 +23,5 @@
@include adf-apps-theme($theme); @include adf-apps-theme($theme);
@include adf-task-standalone-component-theme($theme); @include adf-task-standalone-component-theme($theme);
@include adf-attach-file-widget-dialog-component-theme($theme); @include adf-attach-file-widget-dialog-component-theme($theme);
} @include adf-start-form-component-theme($theme);
}

View File

@@ -17,7 +17,7 @@
import { NoTaskDetailsTemplateDirective } from './no-task-detail-template.directive'; import { NoTaskDetailsTemplateDirective } from './no-task-detail-template.directive';
import { TaskDetailsComponent } from './task-details.component'; import { TaskDetailsComponent } from './task-details.component';
import { FormRenderingService, AuthenticationService } from '@alfresco/adf-core'; import { AuthenticationService } from '@alfresco/adf-core';
import { of } from 'rxjs'; import { of } from 'rxjs';
describe('NoTaskDetailsTemplateDirective', () => { describe('NoTaskDetailsTemplateDirective', () => {
@@ -29,7 +29,7 @@ describe('NoTaskDetailsTemplateDirective', () => {
beforeEach(() => { beforeEach(() => {
authService = new AuthenticationService(null, null, null, null); authService = new AuthenticationService(null, null, null, null);
spyOn(authService, 'getBpmLoggedUser').and.returnValue(of({ email: 'fake-email'})); spyOn(authService, 'getBpmLoggedUser').and.returnValue(of({ email: 'fake-email'}));
detailsComponent = new TaskDetailsComponent(null, authService, null, new FormRenderingService(), null, null, null); detailsComponent = new TaskDetailsComponent(null, authService, null, null, null, null);
component = new NoTaskDetailsTemplateDirective(detailsComponent); component = new NoTaskDetailsTemplateDirective(detailsComponent);
}); });

View File

@@ -23,7 +23,6 @@
<div class="adf-task-details-core-form"> <div class="adf-task-details-core-form">
<div *ngIf="isAssigned()"> <div *ngIf="isAssigned()">
<adf-form *ngIf="isFormComponentVisible()" #activitiForm <adf-form *ngIf="isFormComponentVisible()" #activitiForm
[showDebugButton]="debugMode"
[taskId]="taskDetails.id" [taskId]="taskDetails.id"
[showTitle]="showFormTitle" [showTitle]="showFormTitle"
[showRefreshButton]="showFormRefreshButton" [showRefreshButton]="showFormRefreshButton"

View File

@@ -22,7 +22,6 @@ import {
ClickNotification, ClickNotification,
LogService, LogService,
UpdateNotification, UpdateNotification,
FormRenderingService,
CommentsComponent CommentsComponent
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { import {
@@ -42,7 +41,6 @@ import { ContentLinkModel, FormFieldValidator, FormModel, FormOutcomeEvent } fro
import { TaskQueryRequestRepresentationModel } from '../models/filter.model'; import { TaskQueryRequestRepresentationModel } from '../models/filter.model';
import { TaskDetailsModel } from '../models/task-details.model'; import { TaskDetailsModel } from '../models/task-details.model';
import { TaskListService } from './../services/tasklist.service'; import { TaskListService } from './../services/tasklist.service';
import { AttachFileWidgetComponent, AttachFolderWidgetComponent } from '../../content-widget';
import { UserRepresentation } from '@alfresco/js-api'; import { UserRepresentation } from '@alfresco/js-api';
import { share } from 'rxjs/operators'; import { share } from 'rxjs/operators';
@@ -188,13 +186,10 @@ export class TaskDetailsComponent implements OnInit, OnChanges {
constructor(private taskListService: TaskListService, constructor(private taskListService: TaskListService,
private authService: AuthenticationService, private authService: AuthenticationService,
private peopleProcessService: PeopleProcessService, private peopleProcessService: PeopleProcessService,
private formRenderingService: FormRenderingService,
private logService: LogService, private logService: LogService,
private cardViewUpdateService: CardViewUpdateService, private cardViewUpdateService: CardViewUpdateService,
private dialog: MatDialog) { private dialog: MatDialog) {
this.formRenderingService.setComponentTypeResolver('select-folder', () => AttachFolderWidgetComponent, true);
this.formRenderingService.setComponentTypeResolver('upload', () => AttachFileWidgetComponent, true);
this.peopleSearch = new Observable<UserProcessModel[]>((observer) => this.peopleSearchObserver = observer) this.peopleSearch = new Observable<UserProcessModel[]>((observer) => this.peopleSearchObserver = observer)
.pipe(share()); .pipe(share());
this.authService.getBpmLoggedUser().subscribe((user: UserRepresentation) => { this.authService.getBpmLoggedUser().subscribe((user: UserRepresentation) => {

View File

@@ -36,6 +36,7 @@ import { TaskHeaderComponent } from './components/task-header.component';
import { TaskListComponent } from './components/task-list.component'; import { TaskListComponent } from './components/task-list.component';
import { TaskStandaloneComponent } from './components/task-standalone.component'; import { TaskStandaloneComponent } from './components/task-standalone.component';
import { AttachFormComponent } from './components/attach-form.component'; import { AttachFormComponent } from './components/attach-form.component';
import { FormModule } from '../form/form.module';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -43,6 +44,7 @@ import { AttachFormComponent } from './components/attach-form.component';
FlexLayoutModule, FlexLayoutModule,
MaterialModule, MaterialModule,
FormsModule, FormsModule,
FormModule,
ReactiveFormsModule, ReactiveFormsModule,
CoreModule.forChild(), CoreModule.forChild(),
PeopleModule, PeopleModule,

View File

@@ -58,6 +58,7 @@ export class ProcessInstancesService {
async completeProcessInstance(processInstanceId, appName) { async completeProcessInstance(processInstanceId, appName) {
const path = '/' + appName + '/rb/v1/process-instances/' + processInstanceId + '/complete'; const path = '/' + appName + '/rb/v1/process-instances/' + processInstanceId + '/complete';
const method = 'POST'; const method = 'POST';
const queryParams = {}, postBody = {}; const queryParams = {}, postBody = {};