diff --git a/demo-shell-ng2/app/components/activiti/activiti-demo.component.ts b/demo-shell-ng2/app/components/activiti/activiti-demo.component.ts index f14d1565ad..9775ac02b8 100644 --- a/demo-shell-ng2/app/components/activiti/activiti-demo.component.ts +++ b/demo-shell-ng2/app/components/activiti/activiti-demo.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Component, OnInit, AfterViewChecked, ViewChild } from '@angular/core'; +import { Component, AfterViewChecked, ViewChild } from '@angular/core'; import { ALFRESCO_TASKLIST_DIRECTIVES } from 'ng2-activiti-tasklist'; import { ActivitiForm } from 'ng2-activiti-form'; @@ -29,7 +29,7 @@ declare var componentHandler; styleUrls: ['./activiti-demo.component.css'], directives: [ALFRESCO_TASKLIST_DIRECTIVES, ActivitiForm] }) -export class ActivitiDemoComponent implements OnInit, AfterViewChecked { +export class ActivitiDemoComponent implements AfterViewChecked { currentChoice: string = 'task-list'; diff --git a/ng2-components/ng2-activiti-form/README.md b/ng2-components/ng2-activiti-form/README.md index 77aab3526e..5f025df8a4 100644 --- a/ng2-components/ng2-activiti-form/README.md +++ b/ng2-components/ng2-activiti-form/README.md @@ -40,10 +40,88 @@ Also make sure you include these dependencies in your `index.html` file: ``` -## Basic usage example +## Basic usage examples + +### Display form instance by task id ```html - + + +``` + +For an existing Task both form and values will be fetched and displayed. + +### Display form definition by form id + +```html + + +``` + +Only form definition will be fetched + +### Display form definition by form name + +```html + + +``` + +## Configuration + +### Properties + +The recommended set of properties can be found in the following table: + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| taskId | string | | Task id to fetch corresponding form and values. | +| formId | string | | The id of the form definition to load and display with custom values. | +| formName | string | | Name of hte form definition to load and display with custom values. | +| data | `FormValues` | | Custom form values map to be used with the rendered form. | +| showTitle | boolean | true | Toggle rendering of the form title. | +| showCompleteButton | boolean | true | Toggle rendering of the `Complete` outcome button. | +| showSaveButton | boolean | true | Toggle rendering of the `Save` outcome button. | +| readOnly | boolean | false | Toggle readonly state of the form. Enforces all form widgets render readonly if enabled. | +| showRefreshButton | boolean | true | Toggle rendering of the `Refresh` button. | + +#### Advanced properties + + The following properties are for complex customisation purposes: + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| form | `FormModel` | | Underlying form model instance. | +| debugMode | boolean | false | Toggle debug mode, allows displaying additional data for development and debugging purposes. | + +### Events + +| Name | Description | +| --- | --- | +| formLoaded | Invoked when form is loaded or reloaded. | +| formSaved | Invoked when form is submitted with `Save` or custom outcomes. | +| formCompleted | Invoked when form is submitted with `Complete` outcome. | + +All `form*` events recieve an instance of the `FormModel` as event argument for ease of development: + +**MyView.component.html** +```html + + +``` + +**MyView.component.ts** +```ts +onFormSaved(form: FormModel) { + console.log(form); +} ``` ## Build from sources diff --git a/ng2-components/ng2-activiti-form/src/components/activiti-form.component.spec.ts b/ng2-components/ng2-activiti-form/src/components/activiti-form.component.spec.ts index 004bda4f5f..a9b1ce5216 100644 --- a/ng2-components/ng2-activiti-form/src/components/activiti-form.component.spec.ts +++ b/ng2-components/ng2-activiti-form/src/components/activiti-form.component.spec.ts @@ -16,11 +16,551 @@ */ import { it, describe, expect } from '@angular/core/testing'; +import { Observable } from 'rxjs/Rx'; +import { SimpleChange } from '@angular/core'; +import { ActivitiForm } from './activiti-form.component'; +import { FormModel, FormOutcomeModel } from './widgets/index'; +import { FormService } from './../services/form.service'; describe('ActivitiForm', () => { - it('test placeholder', () => { - expect(true).toBeTruthy(); + let componentHandler: any; + let formService: FormService; + let formComponent: ActivitiForm; + + beforeEach(() => { + componentHandler = jasmine.createSpyObj('componentHandler', [ + 'upgradeAllRegistered' + ]); + window['componentHandler'] = componentHandler; + + formService = new FormService(null, null, null); + formComponent = new ActivitiForm(formService); }); + it('should upgrade MDL content on view checked', () => { + formComponent.ngAfterViewChecked(); + expect(componentHandler.upgradeAllRegistered).toHaveBeenCalled(); + }); + + it('should setup MDL content only if component handler available', () => { + expect(formComponent.setupMaterialComponents()).toBeTruthy(); + + window['componentHandler'] = null; + expect(formComponent.setupMaterialComponents()).toBeFalsy(); + }); + + it('should start loading form on init', () => { + spyOn(formComponent, 'loadForm').and.stub(); + formComponent.ngOnInit(); + expect(formComponent.loadForm).toHaveBeenCalled(); + }); + + it('should check form', () => { + expect(formComponent.hasForm()).toBeFalsy(); + formComponent.form = new FormModel(); + expect(formComponent.hasForm()).toBeTruthy(); + }); + + it('should allow title if task name available', () => { + let formModel = new FormModel(); + formComponent.form = formModel; + + expect(formComponent.showTitle).toBeTruthy(); + expect(formModel.taskName).toBe(FormModel.UNSET_TASK_NAME); + expect(formComponent.isTitleEnabled()).toBeTruthy(); + + // override property as it's the readonly one + Object.defineProperty(formModel, 'taskName', { + enumerable: false, + configurable: false, + writable: false, + value: null + }); + + expect(formComponent.isTitleEnabled()).toBeFalsy(); + }); + + it('should not allow title', () => { + let formModel = new FormModel(); + + formComponent.form = formModel; + formComponent.showTitle = false; + + expect(formModel.taskName).toBe(FormModel.UNSET_TASK_NAME); + expect(formComponent.isTitleEnabled()).toBeFalsy(); + }); + + it('should not enable outcome button when model missing', () => { + expect(formComponent.isOutcomeButtonEnabled(null)).toBeFalsy(); + }); + + it('should enable custom outcome buttons', () => { + let formModel = new FormModel(); + let outcome = new FormOutcomeModel(formModel, { id: 'action1', name: 'Action 1' }); + expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeTruthy(); + }); + + + it('should allow controlling [complete] button visibility', () => { + let formModel = new FormModel(); + let outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION }); + + formComponent.showSaveButton = true; + expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeTruthy(); + + formComponent.showSaveButton = false; + expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeFalsy(); + }); + + it('should allow controlling [save] button visibility', () => { + let formModel = new FormModel(); + let outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.COMPLETE_ACTION }); + + formComponent.showCompleteButton = true; + expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeTruthy(); + + formComponent.showCompleteButton = false; + expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeFalsy(); + }); + + it('should load form on refresh', () => { + spyOn(formComponent, 'loadForm').and.stub(); + + formComponent.onRefreshClicked(); + expect(formComponent.loadForm).toHaveBeenCalled(); + }); + + it('should get form by task id on load', () => { + spyOn(formComponent, 'getFormByTaskId').and.stub(); + const taskId = '123'; + + formComponent.taskId = taskId; + formComponent.loadForm(); + + expect(formComponent.getFormByTaskId).toHaveBeenCalledWith(taskId); + }); + + it('should get form definition by form id on load', () => { + spyOn(formComponent, 'getFormDefinitionByFormId').and.stub(); + const formId = '123'; + + formComponent.formId = formId; + formComponent.loadForm(); + + expect(formComponent.getFormDefinitionByFormId).toHaveBeenCalledWith(formId); + }); + + it('should get form definition by form name on load', () => { + spyOn(formComponent, 'getFormDefinitionByFormName').and.stub(); + const formName = '
'; + + formComponent.formName = formName; + formComponent.loadForm(); + + expect(formComponent.getFormDefinitionByFormName).toHaveBeenCalledWith(formName); + }); + + it('should reload form by task id on binding changes', () => { + spyOn(formComponent, 'getFormByTaskId').and.stub(); + const taskId = ''; + + let change = new SimpleChange(null, taskId); + formComponent.ngOnChanges({ 'taskId': change }); + + expect(formComponent.getFormByTaskId).toHaveBeenCalledWith(taskId); + }); + + it('should reload form definition by form id on binding changes', () => { + spyOn(formComponent, 'getFormDefinitionByFormId').and.stub(); + const formId = '123'; + + let change = new SimpleChange(null, formId); + formComponent.ngOnChanges({ 'formId': change }); + + expect(formComponent.getFormDefinitionByFormId).toHaveBeenCalledWith(formId); + }); + + it('should reload form definition by name on binding changes', () => { + spyOn(formComponent, 'getFormDefinitionByFormName').and.stub(); + const formName = ''; + + let change = new SimpleChange(null, formName); + formComponent.ngOnChanges({ 'formName': change }); + + expect(formComponent.getFormDefinitionByFormName).toHaveBeenCalledWith(formName); + }); + + it('should not get form on load', () => { + spyOn(formComponent, 'getFormByTaskId').and.stub(); + spyOn(formComponent, 'getFormDefinitionByFormId').and.stub(); + spyOn(formComponent, 'getFormDefinitionByFormName').and.stub(); + + formComponent.taskId = null; + formComponent.formId = null; + formComponent.formName = null; + formComponent.loadForm(); + + expect(formComponent.getFormByTaskId).not.toHaveBeenCalled(); + expect(formComponent.getFormDefinitionByFormId).not.toHaveBeenCalled(); + expect(formComponent.getFormDefinitionByFormName).not.toHaveBeenCalled(); + }); + + it('should not reload form on binding changes', () => { + spyOn(formComponent, 'getFormByTaskId').and.stub(); + spyOn(formComponent, 'getFormDefinitionByFormId').and.stub(); + spyOn(formComponent, 'getFormDefinitionByFormName').and.stub(); + + formComponent.ngOnChanges({ 'tag': new SimpleChange(null, 'hello world')}); + + expect(formComponent.getFormByTaskId).not.toHaveBeenCalled(); + expect(formComponent.getFormDefinitionByFormId).not.toHaveBeenCalled(); + expect(formComponent.getFormDefinitionByFormName).not.toHaveBeenCalled(); + }); + + it('should complete form on custom outcome click', () => { + let formModel = new FormModel(); + let outcomeName = 'Custom Action'; + let outcome = new FormOutcomeModel(formModel, { id: 'custom1', name: outcomeName }); + + let saved = false; + formComponent.form = formModel; + formComponent.formSaved.subscribe(v => saved = true); + spyOn(formComponent, 'completeTaskForm').and.stub(); + + let result = formComponent.onOutcomeClicked(outcome); + expect(result).toBeTruthy(); + expect(saved).toBeTruthy(); + expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcomeName); + }); + + it('should save form on [save] outcome click', () => { + let formModel = new FormModel(); + let outcome = new FormOutcomeModel(formModel, { + id: ActivitiForm.SAVE_OUTCOME_ID, + name: 'Save', + isSystem: true + }); + + formComponent.form = formModel; + spyOn(formComponent, 'saveTaskForm').and.stub(); + + let result = formComponent.onOutcomeClicked(outcome); + expect(result).toBeTruthy(); + expect(formComponent.saveTaskForm).toHaveBeenCalled(); + }); + + it('should complete form on [complete] outcome click', () => { + let formModel = new FormModel(); + let outcome = new FormOutcomeModel(formModel, { + id: ActivitiForm.COMPLETE_OUTCOME_ID, + name: 'Complete', + isSystem: true + }); + + formComponent.form = formModel; + spyOn(formComponent, 'completeTaskForm').and.stub(); + + let result = formComponent.onOutcomeClicked(outcome); + expect(result).toBeTruthy(); + expect(formComponent.completeTaskForm).toHaveBeenCalled(); + }); + + it('should emit form saved event on custom outcome click', () => { + let formModel = new FormModel(); + let outcome = new FormOutcomeModel(formModel, { + id: ActivitiForm.CUSTOM_OUTCOME_ID, + name: 'Custom', + isSystem: true + }); + + let saved = false; + formComponent.form = formModel; + formComponent.formSaved.subscribe(v => saved = true); + + let result = formComponent.onOutcomeClicked(outcome); + expect(result).toBeTruthy(); + expect(saved).toBeTruthy(); + }); + + it('should do nothing when clicking outcome for readonly form', () => { + let formModel = new FormModel(); + const outcomeName = 'Custom Action'; + let outcome = new FormOutcomeModel(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 FormModel(); + formComponent.readOnly = false; + expect(formComponent.onOutcomeClicked(null)).toBeFalsy(); + }); + + it('should require loaded form when clicking outcome', () => { + let formModel = new FormModel(); + const outcomeName = 'Custom Action'; + let outcome = new FormOutcomeModel(formModel, { id: 'custom1', name: outcomeName }); + + formComponent.readOnly = false; + formComponent.form = null; + expect(formComponent.onOutcomeClicked(outcome)).toBeFalsy(); + }); + + it('should not execute unknown system outcome', () => { + let formModel = new FormModel(); + let outcome = new FormOutcomeModel(formModel, { id: 'unknown', name: 'Unknown', isSystem: true }); + + formComponent.form = formModel; + expect(formComponent.onOutcomeClicked(outcome)).toBeFalsy(); + }); + + it('should require custom action name to complete form', () => { + let formModel = new FormModel(); + let outcome = new FormOutcomeModel(formModel, { id: 'custom' }); + + formComponent.form = formModel; + expect(formComponent.onOutcomeClicked(outcome)).toBeFalsy(); + + outcome = new FormOutcomeModel(formModel, { id: 'custom', name: 'Custom' }); + spyOn(formComponent, 'completeTaskForm').and.stub(); + expect(formComponent.onOutcomeClicked(outcome)).toBeTruthy(); + }); + + it('should fetch and parse form by task id', () => { + spyOn(formService, 'getTaskForm').and.callFake((taskId) => { + return Observable.create(observer => { + observer.next({ taskId: taskId }); + observer.complete(); + }); + }); + + const taskId = '456'; + let loaded = false; + formComponent.formLoaded.subscribe(() => loaded = true); + + expect(formComponent.form).toBeUndefined(); + formComponent.getFormByTaskId(taskId); + + expect(loaded).toBeTruthy(); + expect(formService.getTaskForm).toHaveBeenCalledWith(taskId); + expect(formComponent.form).toBeDefined(); + expect(formComponent.form.taskId).toBe(taskId); + }); + + it('should handle error when getting form by task id', () => { + const error = 'Some error'; + + spyOn(formComponent, 'handleError').and.stub(); + spyOn(formService, 'getTaskForm').and.callFake((taskId) => { + return Observable.throw(error); + }); + + formComponent.getFormByTaskId('123'); + expect(formComponent.handleError).toHaveBeenCalledWith(error); + }); + + it('should apply readonly state when getting form by task id', () => { + spyOn(formService, 'getTaskForm').and.callFake((taskId) => { + return Observable.create(observer => { + observer.next({ taskId: taskId }); + observer.complete(); + }); + }); + + formComponent.readOnly = true; + formComponent.getFormByTaskId('123'); + + expect(formComponent.form).toBeDefined(); + expect(formComponent.form.readOnly).toBe(true); + }); + + it('should fetch and parse form definition by id', () => { + spyOn(formService, 'getFormDefinitionById').and.callFake((formId) => { + return Observable.create(observer => { + observer.next({ id: formId }); + observer.complete(); + }); + }); + + const formId = '456'; + let loaded = false; + formComponent.formLoaded.subscribe(() => loaded = true); + + expect(formComponent.form).toBeUndefined(); + formComponent.getFormDefinitionByFormId(formId); + + expect(loaded).toBeTruthy(); + expect(formService.getFormDefinitionById).toHaveBeenCalledWith(formId); + expect(formComponent.form).toBeDefined(); + expect(formComponent.form.id).toBe(formId); + }); + + it('should handle error when getting form by definition id', () => { + const error = 'Some error'; + + spyOn(formComponent, 'handleError').and.stub(); + spyOn(formService, 'getFormDefinitionById').and.callFake(() => Observable.throw(error)); + + formComponent.getFormDefinitionByFormId('123'); + expect(formService.getFormDefinitionById).toHaveBeenCalledWith('123'); + expect(formComponent.handleError).toHaveBeenCalledWith(error); + }); + + it('should fetch and parse form definition by form name', () => { + spyOn(formService, 'getFormDefinitionByName').and.callFake((formName) => { + return Observable.create(observer => { + observer.next(formName); + observer.complete(); + }); + }); + + spyOn(formService, 'getFormDefinitionById').and.callFake((formName) => { + return Observable.create(observer => { + observer.next({ name: formName }); + observer.complete(); + }); + }); + + const formName = ''; + let loaded = false; + formComponent.formLoaded.subscribe(() => loaded = true); + + expect(formComponent.form).toBeUndefined(); + formComponent.getFormDefinitionByFormName(formName); + + expect(loaded).toBeTruthy(); + expect(formService.getFormDefinitionByName).toHaveBeenCalledWith(formName); + expect(formComponent.form).toBeDefined(); + expect(formComponent.form.name).toBe(formName); + }); + + it('should save task form and raise corresponding event', () => { + spyOn(formService, 'saveTaskForm').and.callFake(() => { + return Observable.create(observer => { + observer.next(); + observer.complete(); + }); + }); + + let saved = false; + let savedForm = null; + formComponent.formSaved.subscribe(form => { + saved = true; + savedForm = form; + }); + + let formModel = new FormModel({ + taskId: '123', + fields: [ + { id: 'field1' }, + { id: 'field2' } + ] + }); + formComponent.form = formModel; + formComponent.saveTaskForm(); + + expect(formService.saveTaskForm).toHaveBeenCalledWith(formModel.taskId, formModel.values); + expect(saved).toBeTruthy(); + expect(savedForm).toEqual(formModel); + }); + + it('should handle error during form save', () => { + const error = 'Error'; + spyOn(formService, 'saveTaskForm').and.callFake(() => Observable.throw(error)); + spyOn(formComponent, 'handleError').and.stub(); + + formComponent.form = new FormModel({ taskId: '123' }); + formComponent.saveTaskForm(); + + expect(formComponent.handleError).toHaveBeenCalledWith(error); + }); + + it('should require form with task id to save', () => { + spyOn(formService, 'saveTaskForm').and.stub(); + + formComponent.form = null; + formComponent.saveTaskForm(); + + formComponent.form = new FormModel(); + formComponent.saveTaskForm(); + + expect(formService.saveTaskForm).not.toHaveBeenCalled(); + }); + + it('should require form with task id to complete', () => { + spyOn(formService, 'completeTaskForm').and.stub(); + + formComponent.form = null; + formComponent.completeTaskForm('save'); + + formComponent.form = new FormModel(); + formComponent.completeTaskForm('complete'); + + expect(formService.completeTaskForm).not.toHaveBeenCalled(); + }); + + it('should log error to console by default', () => { + const error = 'Error'; + spyOn(console, 'log').and.stub(); + formComponent.handleError(error); + expect(console.log).toHaveBeenCalledWith(error); + }); + + it('should complete form form and raise corresponding event', () => { + spyOn(formService, 'completeTaskForm').and.callFake(() => { + return Observable.create(observer => { + observer.next(); + observer.complete(); + }); + }); + + const outcome = 'complete'; + let completed = false; + formComponent.formCompleted.subscribe(() => completed = true); + + let formModel = new FormModel({ + taskId: '123', + fields: [ + { id: 'field1' }, + { id: 'field2' } + ] + }); + + formComponent.form = formModel; + formComponent.completeTaskForm(outcome); + + expect(formService.completeTaskForm).toHaveBeenCalledWith(formModel.taskId, formModel.values, outcome); + expect(completed).toBeTruthy(); + }); + + it('should require json to parse form', () => { + expect(formComponent.parseForm(null)).toBeNull(); + }); + + it('should parse form from json', () => { + let form = formComponent.parseForm({ + id: '', + fields: [ + { id: 'field1' } + ] + }); + + expect(form).toBeDefined(); + expect(form.id).toBe(''); + 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(); + + let form = formComponent.parseForm({ id: '' }); + expect(formComponent.getFormDefinitionOutcomes).toHaveBeenCalledWith(form); + }); }); diff --git a/ng2-components/ng2-activiti-form/src/components/activiti-form.component.ts b/ng2-components/ng2-activiti-form/src/components/activiti-form.component.ts index b092874ad9..c01bd4ca7f 100644 --- a/ng2-components/ng2-activiti-form/src/components/activiti-form.component.ts +++ b/ng2-components/ng2-activiti-form/src/components/activiti-form.component.ts @@ -18,7 +18,7 @@ import { Component, OnInit, AfterViewChecked, OnChanges, - SimpleChange, + SimpleChanges, Input, Output, EventEmitter @@ -26,7 +26,7 @@ import { import { MATERIAL_DESIGN_DIRECTIVES } from 'ng2-alfresco-core'; import { FormService } from './../services/form.service'; -import { FormModel, FormOutcomeModel } from './widgets/widget.model'; +import { FormModel, FormOutcomeModel, FormValues } from './widgets/core/index'; import { TabsWidget } from './widgets/tabs/tabs.widget'; import { ContainerWidget } from './widgets/container/container.widget'; @@ -39,12 +39,12 @@ declare var componentHandler; * ActivitiForm can show 3 forms searching by 3 type of params: * 1) Form attached to a task passing the {taskId}. * 2) Form that are only defined with the {formId} (in this case you receive only the form definition and the form will not be - * attached to any process, usefull in case you want to use Activitiform as form designer), in this case you can pass also other 2 + * attached to any process, useful in case you want to use ActivitiForm as form designer), in this case you can pass also other 2 * parameters: * - {saveOption} as parameter to tell what is the function to call on the save action. * - {data} to fill the form field with some data, the id of the form must to match the name of the field of the provided data object. * 3) Form that are only defined with the {formName} (in this case you receive only the form definition and the form will not be - * attached to any process, usefull in case you want to use Activitiform as form designer), + * attached to any process, useful in case you want to use ActivitiForm as form designer), * in this case you can pass also other 2 parameters: * - {saveOption} as parameter to tell what is the function to call on the save action. * - {data} to fill the form field with some data, the id of the form must to match the name of the field of the provided data object. @@ -74,6 +74,10 @@ declare var componentHandler; }) export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges { + static SAVE_OUTCOME_ID: string = '$save'; + static COMPLETE_OUTCOME_ID: string = '$complete'; + static CUSTOM_OUTCOME_ID: string = '$custom'; + @Input() taskId: string; @@ -84,7 +88,7 @@ export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges { formName: string; @Input() - data: any; + data: FormValues; @Input() showTitle: boolean = true; @@ -102,13 +106,13 @@ export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges { showRefreshButton: boolean = true; @Output() - formSaved = new EventEmitter(); + formSaved: EventEmitter = new EventEmitter(); @Output() - formCompleted = new EventEmitter(); + formCompleted: EventEmitter = new EventEmitter(); @Output() - formLoaded = new EventEmitter(); + formLoaded: EventEmitter = new EventEmitter(); form: FormModel; @@ -122,155 +126,212 @@ export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges { } isTitleEnabled(): boolean { - return this.form.taskName && this.showTitle; + if (this.showTitle) { + if (this.form && this.form.taskName) { + return true; + } + } + return false; } - isOutcomeButtonEnabled(outcome: any): boolean { - if (outcome.name === 'Complete') { - return this.showCompleteButton; + isOutcomeButtonEnabled(outcome: FormOutcomeModel): boolean { + if (outcome && outcome.name) { + if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) { + return this.showCompleteButton; + } + if (outcome.name === FormOutcomeModel.SAVE_ACTION) { + return this.showSaveButton; + } + return true; } - if (outcome.name === 'Save') { - return this.showSaveButton; - } - return true; + return false; } ngOnInit() { - if (this.taskId) { - this.loadForm(this.taskId); - } - if (this.formId) { - this.getFormDefinitionById(); - } - if (this.formName) { - this.getFormDefinitionByName(); - } + this.loadForm(); } ngAfterViewChecked() { - // workaround for MDL issues with dynamic components - if (componentHandler) { - componentHandler.upgradeAllRegistered(); - } + this.setupMaterialComponents(); } - ngOnChanges(changes: {[propertyName: string]: SimpleChange}) { + ngOnChanges(changes: SimpleChanges) { let taskId = changes['taskId']; if (taskId && taskId.currentValue) { - this.loadForm(taskId.currentValue); + this.getFormByTaskId(taskId.currentValue); + return; } let formId = changes['formId']; if (formId && formId.currentValue) { - this.getFormDefinitionById(); + this.getFormDefinitionByFormId(formId.currentValue); + return; } let formName = changes['formName']; if (formName && formName.currentValue) { - this.getFormDefinitionByName(); + this.getFormDefinitionByFormName(formName.currentValue); + return; } } - - onOutcomeClicked(outcome: FormOutcomeModel, event?: Event) { - if (!this.readOnly && outcome) { + /** + * Invoked when user clicks outcome button. + * @param outcome Form outcome model + * @returns {boolean} True if outcome action was executed, otherwise false. + */ + onOutcomeClicked(outcome: FormOutcomeModel): boolean { + if (!this.readOnly && outcome && this.form) { if (outcome.isSystem) { - if (outcome.id === '$save') { - return this.saveTaskForm(); + if (outcome.id === ActivitiForm.SAVE_OUTCOME_ID) { + this.saveTaskForm(); + return true; } - if (outcome.id === '$complete') { - return this.completeTaskForm(); + if (outcome.id === ActivitiForm.COMPLETE_OUTCOME_ID) { + this.completeTaskForm(); + return true; } - if (outcome.id === '$custom') { - this.formSaved.emit(this.form.values); + if (outcome.id === ActivitiForm.CUSTOM_OUTCOME_ID) { + this.formSaved.emit(this.form); + return true; } } else { // Note: Activiti is using NAME field rather than ID for outcomes if (outcome.name) { - this.formSaved.emit(this.form.values); - return this.completeTaskForm(outcome.name); + this.formSaved.emit(this.form); + this.completeTaskForm(outcome.name); + return true; } } } + + return false; } + /** + * Invoked when user clicks form refresh button. + */ onRefreshClicked() { + this.loadForm(); + } + + loadForm() { if (this.taskId) { - this.loadForm(this.taskId); + this.getFormByTaskId(this.taskId); + return; } + if (this.formId) { - this.getFormDefinitionById(); + this.getFormDefinitionByFormId(this.formId); + return; } + if (this.formName) { - this.getFormDefinitionByName(); + this.getFormDefinitionByFormName(this.formName); + return; } } - private loadForm(taskId: string) { + setupMaterialComponents(): boolean { + // workaround for MDL issues with dynamic components + if (componentHandler) { + componentHandler.upgradeAllRegistered(); + return true; + } + return false; + } + + getFormByTaskId(taskId: string) { let data = this.data; this.formService .getTaskForm(taskId) .subscribe( form => { - this.form = new FormModel(form, data, null, this.readOnly); - this.formLoaded.emit(this.form.values); + this.form = new FormModel(form, data, this.readOnly); + this.formLoaded.emit(this.form); }, - err => console.log(err) + this.handleError ); } - private getFormDefinitionById() { + getFormDefinitionByFormId(formId: string) { this.formService - .getFormDefinitionById(this.formId) + .getFormDefinitionById(formId) .subscribe( form => { - console.log('Get Form By definition Id', form); - this.form = new FormModel(form, this.data, this.formSaved, this.readOnly); - this.formLoaded.emit(this.form.values); + // console.log('Get Form By definition Id', form); + this.form = this.parseForm(form); + this.formLoaded.emit(this.form); }, - err => console.log(err) + this.handleError ); } - private getFormDefinitionByName() { + getFormDefinitionByFormName(formName: string) { this.formService - .getFormDefinitionByName(this.formName) + .getFormDefinitionByName(formName) .subscribe( id => { this.formService.getFormDefinitionById(id).subscribe( form => { - console.log('Get Form By Form definition Name', form); - this.form = new FormModel(form, this.data, this.formSaved, this.readOnly); - this.formLoaded.emit(this.form.values); + // console.log('Get Form By Form definition Name', form); + this.form = this.parseForm(form); + this.formLoaded.emit(this.form); }, - err => console.log(err) + this.handleError ); }, - err => console.log(err) + this.handleError ); } - private saveTaskForm() { - this.formService.saveTaskForm(this.form.taskId, this.form.values).subscribe( - (response) => { - console.log('Saved task', response); - this.formSaved.emit(this.form.values); - }, - (err) => console.log(err) - ); + saveTaskForm() { + if (this.form && this.form.taskId) { + this.formService + .saveTaskForm(this.form.taskId, this.form.values) + .subscribe( + () => this.formSaved.emit(this.form), + this.handleError + ); + } } - private completeTaskForm(outcome?: string) { - this.formService - .completeTaskForm(this.form.taskId, this.form.values, outcome) - .subscribe( - (response) => { - console.log('Completed task', response); - this.formCompleted.emit(this.form.values); - }, - (err) => console.log(err) - ); + completeTaskForm(outcome?: string) { + if (this.form && this.form.taskId) { + this.formService + .completeTaskForm(this.form.taskId, this.form.values, outcome) + .subscribe( + () => this.formCompleted.emit(this.form), + this.handleError + ); + } + } + + handleError(err: any) { + console.log(err); + } + + parseForm(json: any): FormModel { + if (json) { + let form = new FormModel(json, this.data, this.readOnly); + if (!json.fields) { + form.outcomes = this.getFormDefinitionOutcomes(form); + } + return form; + } + return null; + } + + /** + * Get custom set of outcomes for a Form Definition. + * @param form Form definition model. + * @returns {FormOutcomeModel[]} Outcomes for a given form definition. + */ + getFormDefinitionOutcomes(form: FormModel): FormOutcomeModel[] { + return [ + new FormOutcomeModel(form, { id: '$custom', name: FormOutcomeModel.SAVE_ACTION, isSystem: true }) + ]; } } diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/checkbox/checkbox.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/checkbox/checkbox.widget.html index cee9bd4148..a417d59cb3 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/checkbox/checkbox.widget.html +++ b/ng2-components/ng2-activiti-form/src/components/widgets/checkbox/checkbox.widget.html @@ -1,4 +1,8 @@ diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.spec.ts new file mode 100644 index 0000000000..54b43944fa --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.spec.ts @@ -0,0 +1,97 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect, beforeEach } from '@angular/core/testing'; +import { ContainerWidget } from './container.widget'; +import { FormModel } from './../core/form.model'; +import { ContainerModel } from './../core/container.model'; +import { FormFieldTypes } from './../core/form-field-types'; + +describe('ContainerWidget', () => { + + let componentHandler; + + beforeEach(() => { + componentHandler = jasmine.createSpyObj('componentHandler', [ + 'upgradeAllRegistered' + ]); + + window['componentHandler'] = componentHandler; + }); + + it('should upgrade MDL content on view init', () => { + let container = new ContainerWidget(); + container.ngAfterViewInit(); + expect(componentHandler.upgradeAllRegistered).toHaveBeenCalled(); + }); + + it('should setup MDL content only if component handler available', () => { + let container = new ContainerWidget(); + expect(container.setupMaterialComponents()).toBeTruthy(); + + window['componentHandler'] = null; + expect(container.setupMaterialComponents()).toBeFalsy(); + }); + + it('should toggle underlying group container', () => { + let container = new ContainerModel(new FormModel(), { + type: FormFieldTypes.GROUP, + params: { + allowCollapse: true + } + }); + + let widget = new ContainerWidget(); + widget.content = container; + + expect(container.isExpanded).toBeTruthy(); + widget.onExpanderClicked(); + expect(container.isExpanded).toBeFalsy(); + widget.onExpanderClicked(); + expect(container.isExpanded).toBeTruthy(); + }); + + it('should toggle only collapsible container', () => { + let container = new ContainerModel(new FormModel(), { + type: FormFieldTypes.GROUP + }); + + let widget = new ContainerWidget(); + widget.content = container; + + expect(container.isExpanded).toBeTruthy(); + widget.onExpanderClicked(); + expect(container.isExpanded).toBeTruthy(); + }); + + it('should toggle only group container', () => { + let container = new ContainerModel(new FormModel(), { + type: FormFieldTypes.CONTAINER, + params: { + allowCollapse: true + } + }); + + let widget = new ContainerWidget(); + widget.content = container; + + expect(container.isExpanded).toBeTruthy(); + widget.onExpanderClicked(); + expect(container.isExpanded).toBeTruthy(); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.ts b/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.ts index 2ee4b5da01..50e14bc604 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.ts @@ -16,7 +16,7 @@ */ import { Component, Input, AfterViewInit } from '@angular/core'; -import { ContainerModel } from './../widget.model'; +import { ContainerModel } from './../core/index'; import { MATERIAL_DESIGN_DIRECTIVES } from 'ng2-alfresco-core'; import { PRIMITIVE_WIDGET_DIRECTIVES } from './../index'; @@ -46,10 +46,16 @@ export class ContainerWidget implements AfterViewInit { } ngAfterViewInit() { + this.setupMaterialComponents(); + } + + setupMaterialComponents(): boolean { // workaround for MDL issues with dynamic components if (componentHandler) { componentHandler.upgradeAllRegistered(); + return true; } + return false; } } diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/container-column.model.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/container-column.model.spec.ts new file mode 100644 index 0000000000..5296d54288 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/container-column.model.spec.ts @@ -0,0 +1,42 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect } from '@angular/core/testing'; +import { ContainerColumnModel } from './container-column.model'; +import { FormFieldModel } from './form-field.model'; + +describe('ContainerColumnModel', () => { + + it('should have max size by default', () => { + let column = new ContainerColumnModel(); + expect(column.size).toBe(12); + }); + + it('should check fields', () => { + let column = new ContainerColumnModel(); + + column.fields = null; + expect(column.hasFields()).toBeFalsy(); + + column.fields = []; + expect(column.hasFields()).toBeFalsy(); + + column.fields = [new FormFieldModel(null, null)]; + expect(column.hasFields()).toBeTruthy(); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/container-column.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/container-column.model.ts new file mode 100644 index 0000000000..94216ed655 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/container-column.model.ts @@ -0,0 +1,28 @@ +/*! + * @license + * Copyright 2016 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 { FormFieldModel } from './form-field.model'; + +export class ContainerColumnModel { + + size: number = 12; + fields: FormFieldModel[] = []; + + hasFields(): boolean { + return this.fields && this.fields.length > 0; + } +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/container.model.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/container.model.spec.ts new file mode 100644 index 0000000000..3f007d4869 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/container.model.spec.ts @@ -0,0 +1,140 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect } from '@angular/core/testing'; +import { ContainerModel } from './container.model'; +import { FormModel } from './form.model'; +import { FormFieldTypes } from './form-field-types'; + +describe('ContainerModel', () => { + + it('should store the form reference', () => { + let form = new FormModel(); + let model = new ContainerModel(form); + expect(model.form).toBe(form); + }); + + it('should store original json', () => { + let json = {}; + let model = new ContainerModel(null, json); + expect(model.json).toBe(json); + }); + + it('should have 1 column layout by default', () => { + let container = new ContainerModel(null, null); + expect(container.numberOfColumns).toBe(1); + }); + + it('should be expanded by default', () => { + let container = new ContainerModel(null, null); + expect(container.isExpanded).toBeTruthy(); + }); + + it('should setup with json config', () => { + let json = { + fieldType: '', + id: '', + name: '', + type: '', + tab: '', + numberOfColumns: 2, + params: {} + }; + let container = new ContainerModel(null, json); + Object.keys(json).forEach(key => { + expect(container[key]).toEqual(json[key]); + }); + }); + + it('should wrap fields into columns on setup', () => { + let form = new FormModel(); + let json = { + fieldType: '', + id: '', + name: '', + type: '', + tab: '', + numberOfColumns: 3, + params: {}, + fields: { + '1': [ + { id: 'field-1' }, + { id: 'field-3' } + ], + '2': [ + { id: 'field-2' } + ], + '3': null + } + }; + let container = new ContainerModel(form, json); + expect(container.columns.length).toBe(3); + + let col1 = container.columns[0]; + expect(col1.fields.length).toBe(2); + expect(col1.fields[0].id).toBe('field-1'); + expect(col1.fields[1].id).toBe('field-3'); + + let col2 = container.columns[1]; + expect(col2.fields.length).toBe(1); + expect(col2.fields[0].id).toBe('field-2'); + + let col3 = container.columns[2]; + expect(col3.fields.length).toBe(0); + }); + + it('should allow collapsing only when of a group type', () => { + let container = new ContainerModel(new FormModel(), { + type: FormFieldTypes.CONTAINER, + params: { + allowCollapse: true + } + }); + + expect(container.isCollapsible()).toBeFalsy(); + container.type = FormFieldTypes.GROUP; + expect(container.isCollapsible()).toBeTruthy(); + }); + + it('should allow collapsing only when explicitly defined in params', () => { + let container = new ContainerModel(new FormModel(), { + type: FormFieldTypes.GROUP, + params: {} + }); + expect(container.isCollapsible()).toBeFalsy(); + + container = new ContainerModel(new FormModel(), { + type: FormFieldTypes.GROUP, + params: { + allowCollapse: true + } + }); + expect(container.isCollapsible()).toBeTruthy(); + }); + + it('should be collapsed by default', () => { + let container = new ContainerModel(new FormModel(), { + type: FormFieldTypes.GROUP, + params: { + allowCollapse: true, + collapseByDefault: true + } + }); + expect(container.isCollapsedByDefault()).toBeTruthy(); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/container.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/container.model.ts new file mode 100644 index 0000000000..dfeaf231b9 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/container.model.ts @@ -0,0 +1,97 @@ +/*! + * @license + * Copyright 2016 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 { FormWidgetModel } from './form-widget.model'; +import { FormFieldMetadata } from './form-field-metadata'; +import { ContainerColumnModel } from './container-column.model'; +import { FormFieldTypes } from './form-field-types'; +import { FormModel } from './form.model'; +import { FormFieldModel } from './form-field.model'; + +// TODO: inherit FormFieldModel +export class ContainerModel extends FormWidgetModel { + + fieldType: string; + id: string; + name: string; + type: string; + tab: string; + numberOfColumns: number = 1; + params: FormFieldMetadata = {}; + + columns: ContainerColumnModel[] = []; + isExpanded: boolean = true; + + isGroup(): boolean { + return this.type === FormFieldTypes.GROUP; + } + + isCollapsible(): boolean { + let allowCollapse = false; + + if (this.isGroup() && this.params['allowCollapse']) { + allowCollapse = this.params['allowCollapse']; + } + + return allowCollapse; + } + + isCollapsedByDefault(): boolean { + let collapseByDefault = false; + + if (this.isCollapsible() && this.params['collapseByDefault']) { + collapseByDefault = this.params['collapseByDefault']; + } + + return collapseByDefault; + } + + constructor(form: FormModel, json?: any) { + super(form, json); + + if (json) { + this.fieldType = json.fieldType; + this.id = json.id; + this.name = json.name; + this.type = json.type; + this.tab = json.tab; + this.numberOfColumns = json.numberOfColumns; + this.params = json.params || {}; + + let columnSize: number = 12; + if (this.numberOfColumns > 1) { + columnSize = 12 / this.numberOfColumns; + } + + for (let i = 0; i < this.numberOfColumns; i++) { + let col = new ContainerColumnModel(); + col.size = columnSize; + this.columns.push(col); + } + + if (json.fields) { + Object.keys(json.fields).map(key => { + let fields = (json.fields[key] || []).map(f => new FormFieldModel(form, f)); + let col = this.columns[parseInt(key, 10) - 1]; + col.fields = fields; + }); + } + + this.isExpanded = !this.isCollapsedByDefault(); + } + } +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-metadata.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-metadata.ts new file mode 100644 index 0000000000..02378e3b41 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-metadata.ts @@ -0,0 +1,20 @@ +/*! + * @license + * Copyright 2016 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 interface FormFieldMetadata { + [key: string]: any; +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-option.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-option.ts new file mode 100644 index 0000000000..81b39fc6c1 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-option.ts @@ -0,0 +1,21 @@ +/*! + * @license + * Copyright 2016 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 interface FormFieldOption { + id: string; + name: string; +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-types.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-types.ts new file mode 100644 index 0000000000..e01b261033 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-types.ts @@ -0,0 +1,36 @@ +/*! + * @license + * Copyright 2016 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 FormFieldTypes { + static CONTAINER: string = 'container'; + static GROUP: string = 'group'; + static DROPDOWN: string = 'dropdown'; + static HYPERLINK: string = 'hyperlink'; + static RADIO_BUTTONS: string = 'radio-buttons'; + static DISPLAY_VALUE: string = 'readonly'; + static READONLY_TEXT: string = 'readonly-text'; + + static READONLY_TYPES: string[] = [ + FormFieldTypes.HYPERLINK, + FormFieldTypes.DISPLAY_VALUE, + FormFieldTypes.READONLY_TEXT + ]; + + static isReadOnlyType(type: string) { + return FormFieldTypes.READONLY_TYPES.indexOf(type) > -1; + } +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.spec.ts new file mode 100644 index 0000000000..aa987b5e2a --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.spec.ts @@ -0,0 +1,243 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect } from '@angular/core/testing'; +import { FormFieldModel } from './form-field.model'; +import { FormFieldTypes } from './form-field-types'; +import { FormModel } from './form.model'; + + +describe('FormFieldModel', () => { + + it('should store the form reference', () => { + let form = new FormModel(); + let model = new FormFieldModel(form); + expect(model.form).toBe(form); + }); + + it('should store original json', () => { + let json = {}; + let model = new FormFieldModel(new FormModel(), json); + expect(model.json).toBe(json); + }); + + it('should setup with json config', () => { + let json = { + fieldType: '', + id: '', + name: '', + type: '', + required: true, + readOnly: true, + overrideId: true, + tab: '', + restUrl: '', + restResponsePath: '', + restIdProperty: '', + restLabelProperty: '', + colspan: 1, + options: [], + hasEmptyValue: true, + className: '', + optionType: '', + params: {}, + hyperlinkUrl: '', + displayText: '', + value: '' + }; + let field = new FormFieldModel(new FormModel(), json); + Object.keys(json).forEach(key => { + expect(field[key]).toBe(json[key]); + }); + }); + + it('should setup empty options collection', () => { + let field = new FormFieldModel(new FormModel(), null); + expect(field.options).toBeDefined(); + expect(field.options.length).toBe(0); + + field = new FormFieldModel(new FormModel(), { options: null }); + expect(field.options).toBeDefined(); + expect(field.options.length).toBe(0); + }); + + it('should setup empty params', () => { + let field = new FormFieldModel(new FormModel(), null); + expect(field.params).toEqual({}); + + field = new FormFieldModel(new FormModel(), { params: null }); + expect(field.params).toEqual({}); + }); + + it('should update form on every value change', () => { + let form = new FormModel(); + let field = new FormFieldModel(form, { id: 'field1' }); + let value = 10; + + spyOn(field, 'updateForm').and.callThrough(); + field.value = value; + + expect(field.value).toBe(value); + expect(field.updateForm).toHaveBeenCalled(); + expect(form.values['field1']).toBe(value); + }); + + it('should get form readonly state', () => { + let form = new FormModel(); + let field = new FormFieldModel(form, null); + + expect(field.readOnly).toBeFalsy(); + form.readOnly = true; + expect(field.readOnly).toBeTruthy(); + }); + + it('should take own readonly state if form is writable', () => { + let form = new FormModel(); + let field = new FormFieldModel(form, { readOnly: true }); + + expect(form.readOnly).toBeFalsy(); + expect(field.readOnly).toBeTruthy(); + }); + + it('should parse and convert empty dropdown value', () => { + let field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DROPDOWN, + value: '' + }); + + expect(field.value).toBe('empty'); + }); + + it('should parse and leave dropdown value as is', () => { + let field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DROPDOWN, + options: [], + value: 'deferred' + }); + + expect(field.value).toBe('deferred'); + }); + + it('should parse and resolve radio button value', () => { + let field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.RADIO_BUTTONS, + options: [ + { id: 'opt1', value: 'Option 1' }, + { id: 'opt2', value: 'Option 2' } + ], + value: 'opt2' + }); + + expect(field.value).toBe('opt2'); + }); + + it('should parse and fall back to first radio button value', () => { + let field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.RADIO_BUTTONS, + options: [ + { id: 'opt1', value: 'Option 1' }, + { id: 'opt2', value: 'Option 2' } + ], + value: 'opt3' + }); + + expect(field.value).toBe('opt1'); + }); + + it('should parse and leave radio button value as is', () => { + let field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.RADIO_BUTTONS, + options: [], + value: 'deferred-radio' + }); + expect(field.value).toBe('deferred-radio'); + }); + + it('should update form with empty dropdown value', () => { + let form = new FormModel(); + let field = new FormFieldModel(form, { + id: 'dropdown-1', + type: FormFieldTypes.DROPDOWN + }); + + field.value = 'empty'; + expect(form.values['dropdown-1']).toEqual({}); + + field.value = ''; + expect(form.values['dropdown-1']).toEqual({}); + }); + + it('should update form with dropdown value', () => { + let form = new FormModel(); + let field = new FormFieldModel(form, { + id: 'dropdown-2', + type: FormFieldTypes.DROPDOWN, + options: [ + { id: 'opt1', value: 'Option 1' }, + { id: 'opt2', value: 'Option 2' } + ] + }); + + field.value = 'opt2'; + expect(form.values['dropdown-2']).toEqual(field.options[1]); + }); + + it('should update form with radio button value', () => { + let form = new FormModel(); + let field = new FormFieldModel(form, { + id: 'radio-1', + type: FormFieldTypes.RADIO_BUTTONS, + options: [ + { id: 'opt1', value: 'Option 1' }, + { id: 'opt2', value: 'Option 2' } + ] + }); + + field.value = 'opt2'; + expect(form.values['radio-1']).toEqual(field.options[1]); + }); + + it('should update form with the first radio button value', () => { + let form = new FormModel(); + let field = new FormFieldModel(form, { + id: 'radio-2', + type: FormFieldTypes.RADIO_BUTTONS, + options: [ + { id: 'opt1', value: 'Option 1' }, + { id: 'opt2', value: 'Option 2' } + ] + }); + + field.value = 'missing'; + expect(form.values['radio-2']).toEqual(field.options[0]); + }); + + it('should not update form with display-only field value', () => { + let form = new FormModel(); + + FormFieldTypes.READONLY_TYPES.forEach(typeName => { + let field = new FormFieldModel(form, { + id: typeName, + type: typeName + }); + + field.value = ''; + expect(form.values[field.id]).toBeUndefined(); + }); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts new file mode 100644 index 0000000000..988208b2b9 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts @@ -0,0 +1,158 @@ +/*! + * @license + * Copyright 2016 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 { FormWidgetModel } from './form-widget.model'; +import { FormFieldOption } from './form-field-option'; +import { FormFieldTypes } from './form-field-types'; +import { FormFieldMetadata } from './form-field-metadata'; +import { FormModel } from './form.model'; + +export class FormFieldModel extends FormWidgetModel { + + private _value: string; + private _readOnly: boolean = false; + + fieldType: string; + id: string; + name: string; + type: string; + required: boolean; + overrideId: boolean; + tab: string; + colspan: number = 1; + options: FormFieldOption[] = []; + restUrl: string; + restResponsePath: string; + restIdProperty: string; + restLabelProperty: string; + hasEmptyValue: boolean; + className: string; + optionType: string; + params: FormFieldMetadata = {}; + hyperlinkUrl: string; + displayText: string; + + get value(): any { + return this._value; + } + + set value(v: any) { + this._value = v; + this.updateForm(); + } + + get readOnly(): boolean { + if (this.form && this.form.readOnly) { + return true; + } + return this._readOnly; + } + + constructor(form: FormModel, json?: any) { + super(form, json); + + if (json) { + this.fieldType = json.fieldType; + this.id = json.id; + this.name = json.name; + this.type = json.type; + this.required = json.required; + this._readOnly = json.readOnly; + this.overrideId = json.overrideId; + this.tab = json.tab; + this.restUrl = json.restUrl; + this.restResponsePath = json.restResponsePath; + this.restIdProperty = json.restIdProperty; + this.restLabelProperty = json.restLabelProperty; + this.colspan = json.colspan; + this.options = json.options || []; + this.hasEmptyValue = json.hasEmptyValue; + this.className = json.className; + this.optionType = json.optionType; + this.params = json.params || {}; + this.hyperlinkUrl = json.hyperlinkUrl; + this.displayText = json.displayText; + + this._value = this.parseValue(json); + this.updateForm(); + } + } + + parseValue(json: any): any { + let value = json.value; + + /* + This is needed due to Activiti issue related to reading dropdown values as value string + but saving back as object: { id: , name: } + */ + // TODO: needs review + if (json.type === FormFieldTypes.DROPDOWN) { + if (value === '') { + value = 'empty'; + } + } + + /* + This is needed due to Activiti issue related to reading radio button values as value string + but saving back as object: { id: , name: } + */ + if (json.type === FormFieldTypes.RADIO_BUTTONS) { + // Activiti has a bug with default radio button value, + // so try resolving current one with a fallback to first entry + let entry: FormFieldOption[] = this.options.filter(opt => opt.id === value); + if (entry.length > 0) { + value = entry[0].id; + } else if (this.options.length > 0) { + value = this.options[0].id; + } + } + + return value; + } + + updateForm() { + if (this.type === FormFieldTypes.DROPDOWN) { + /* + This is needed due to Activiti reading dropdown values as string + but saving back as object: { id: , name: } + */ + if (this.value === 'empty' || this.value === '') { + this.form.values[this.id] = {}; + } else { + let entry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value); + if (entry.length > 0) { + this.form.values[this.id] = entry[0]; + } + } + } else if (this.type === FormFieldTypes.RADIO_BUTTONS) { + /* + This is needed due to Activiti issue related to reading radio button values as value string + but saving back as object: { id: , name: } + */ + let entry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value); + if (entry.length > 0) { + this.form.values[this.id] = entry[0]; + } else if (this.options.length > 0) { + this.form.values[this.id] = this.options[0]; + } + } else { + if (!FormFieldTypes.isReadOnlyType(this.type)) { + this.form.values[this.id] = this.value; + } + } + } +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-outcome.model.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-outcome.model.spec.ts new file mode 100644 index 0000000000..88dd417cc6 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-outcome.model.spec.ts @@ -0,0 +1,46 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect } from '@angular/core/testing'; +import { FormModel } from './form.model'; +import { FormOutcomeModel } from './form-outcome.model'; + +describe('FormOutcomeModel', () => { + + it('should setup with json config', () => { + let json = { + id: '', + name: '' + }; + let model = new FormOutcomeModel(null, json); + expect(model.id).toBe(json.id); + expect(model.name).toBe(json.name); + }); + + it('should store the form reference', () => { + let form = new FormModel(); + let model = new FormOutcomeModel(form); + expect(model.form).toBe(form); + }); + + it('should store original json', () => { + let json = {}; + let model = new FormOutcomeModel(null, json); + expect(model.json).toBe(json); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-outcome.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-outcome.model.ts new file mode 100644 index 0000000000..f0eb0abb05 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-outcome.model.ts @@ -0,0 +1,48 @@ +/*! + * @license + * Copyright 2016 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 { FormWidgetModel } from './form-widget.model'; +import { FormModel } from './form.model'; + +export class FormOutcomeModel extends FormWidgetModel { + + static SAVE_ACTION: string = 'Save'; // Activiti 'Save' action name + static COMPLETE_ACTION: string = 'Complete'; // Activiti 'Complete' action name + + private _id: string; + private _name: string; + + isSystem: boolean = false; + + get id() { + return this._id; + } + + get name() { + return this._name; + } + + constructor(form: FormModel, json?: any) { + super(form, json); + + if (json) { + this._id = json.id; + this._name = json.name; + this.isSystem = json.isSystem ? true : false; + } + } +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-values.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-values.ts new file mode 100644 index 0000000000..cfd32ee6a4 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-values.ts @@ -0,0 +1,21 @@ +/*! + * @license + * Copyright 2016 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 { FormFieldMetadata } from './form-field-metadata'; + +export interface FormValues extends FormFieldMetadata { +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-widget.model.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-widget.model.spec.ts new file mode 100644 index 0000000000..0481b59432 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-widget.model.spec.ts @@ -0,0 +1,36 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect } from '@angular/core/testing'; +import { FormModel } from './form.model'; +import { FormWidgetModel } from './form-widget.model'; + +describe('FormWidgetModel', () => { + + it('should store the form reference', () => { + let form = new FormModel(); + let model = new FormWidgetModel(form, null); + expect(model.form).toBe(form); + }); + + it('should store original json', () => { + let json = {}; + let model = new FormWidgetModel(null, json); + expect(model.json).toBe(json); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-widget.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-widget.model.ts new file mode 100644 index 0000000000..39415c2547 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-widget.model.ts @@ -0,0 +1,41 @@ +/*! + * @license + * Copyright 2016 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 { FormModel } from './form.model'; + +export class FormWidgetModel { + + private _form: FormModel; + private _json: any; + + get form(): FormModel { + return this._form; + } + + get json(): any { + return this._json; + } + + constructor(form: FormModel, json: any) { + this._form = form; + this._json = json; + } +} + +export interface FormWidgetModelCache { + [key: string]: T; +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.spec.ts new file mode 100644 index 0000000000..1f5d36304a --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.spec.ts @@ -0,0 +1,295 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect } from '@angular/core/testing'; +import { FormModel } from './form.model'; +import { TabModel } from './tab.model'; +import { ContainerModel } from './container.model'; +import { FormOutcomeModel } from './form-outcome.model'; +import { FormValues } from './form-values'; + +describe('FormModel', () => { + + it('should store original json', () => { + let json = {}; + let form = new FormModel(json); + expect(form.json).toBe(json); + }); + + it('should setup properties with json', () => { + let json = { + id: '', + name: '', + taskId: '', + taskName: '' + }; + let form = new FormModel(json); + + Object.keys(json).forEach(key => { + expect(form[key]).toEqual(form[key]); + }); + }); + + it('should take form name when task name is missing', () => { + let json = { + id: '', + name: '' + }; + let form = new FormModel(json); + expect(form.taskName).toBe(json.name); + }); + + it('should use fallback value for task name', () => { + let form = new FormModel({}); + expect(form.taskName).toBe(FormModel.UNSET_TASK_NAME); + }); + + it('should set readonly state from params', () => { + let form = new FormModel({}, null, true); + expect(form.readOnly).toBeTruthy(); + }); + + it('should check tabs', () => { + let form = new FormModel(); + + 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', () => { + let form = new FormModel(); + + form.fields = null; + expect(form.hasFields()).toBeFalsy(); + + form.fields = []; + expect(form.hasFields()).toBeFalsy(); + + form.fields = [new ContainerModel(null)]; + expect(form.hasFields()).toBeTruthy(); + }); + + it('should check outcomes', () => { + let form = new FormModel(); + + 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', () => { + let json = { + tabs: [ + { id: 'tab1' }, + { id: 'tab2' } + ] + }; + + let form = new FormModel(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', () => { + let json = { + fields: [ + { id: 'field1' }, + { id: 'field2' } + ] + }; + + let form = new FormModel(json); + expect(form.fields.length).toBe(2); + expect(form.fields[0].id).toBe('field1'); + expect(form.fields[1].id).toBe('field2'); + }); + + it('should parse fields from the definition', () => { + let json = { + fields: null, + formDefinition: { + fields: [ + { id: 'field1' }, + { id: 'field2' } + ] + } + }; + + let form = new FormModel(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', () => { + let json = { + fields: null + }; + + let form = new FormModel(json); + expect(form.fields).toBeDefined(); + expect(form.fields.length).toBe(0); + }); + + it('should put fields into corresponding tabs', () => { + let json = { + tabs: [ + { id: 'tab1' }, + { id: 'tab2' } + ], + fields: [ + { id: 'field1', tab: 'tab1' }, + { id: 'field2', tab: 'tab2' }, + { id: 'field3', tab: 'tab1' }, + { id: 'field4', tab: 'missing-tab' } + ] + }; + + let form = new FormModel(json); + expect(form.tabs.length).toBe(2); + expect(form.fields.length).toBe(4); + + let tab1 = form.tabs[0]; + expect(tab1.fields.length).toBe(2); + expect(tab1.fields[0].id).toBe('field1'); + expect(tab1.fields[1].id).toBe('field3'); + + let tab2 = form.tabs[1]; + expect(tab2.fields.length).toBe(1); + expect(tab2.fields[0].id).toBe('field2'); + }); + + it('should apply external data', () => { + let data: FormValues = { + field1: 'one', + field2: 'two' + }; + + let json = { + fields: [ + { + fieldType: 'ContainerRepresentation', + id: 'container1', + type: 'container', + numberOfColumns: 2, + fields: { + '1': [ + { + fieldType: 'FormFieldRepresentation', + type: 'text', + id: 'field1' + } + ], + '2': [ + { + fieldType: 'FormFieldRepresentation', + type: 'text', + id: 'field2' + }, + { + fieldType: 'FormFieldRepresentation', + type: 'text', + id: 'field3', + value: 'original-value' + } + ] + } + } + ] + }; + + let form = new FormModel(json, data); + expect(form.fields.length).toBe(1); + + let container = form.fields[0]; + expect(container.columns.length).toBe(2); + + let column1 = container.columns[0]; + let column2 = container.columns[1]; + expect(column1.fields.length).toBe(1); + expect(column2.fields.length).toBe(2); + + let field1 = column1.fields[0]; + expect(field1.id).toBe('field1'); + expect(field1.value).toBe('one'); + + let field2 = column2.fields[0]; + expect(field2.id).toBe('field2'); + expect(field2.value).toBe('two'); + + let field3 = column2.fields[1]; + expect(field3.id).toBe('field3'); + expect(field3.value).toBe('original-value'); + }); + + it('should create standard form outcomes', () => { + let json = { + fields: [ + { id: 'container1' } + ] + }; + + let form = new FormModel(json); + expect(form.outcomes.length).toBe(2); + + expect(form.outcomes[0].id).toBe(FormModel.SAVE_OUTCOME); + expect(form.outcomes[0].isSystem).toBeTruthy(); + + expect(form.outcomes[1].id).toBe(FormModel.COMPLETE_OUTCOME); + expect(form.outcomes[1].isSystem).toBeTruthy(); + }); + + it('should create outcomes only when fields available', () => { + let json = { + fields: null + }; + let form = new FormModel(json); + expect(form.outcomes.length).toBe(0); + }); + + it('should use custom form outcomes', () => { + let json = { + fields: [ + { id: 'container1' } + ], + outcomes: [ + { id: 'custom-1', name: 'custom 1' } + ] + }; + + let form = new FormModel(json); + expect(form.outcomes.length).toBe(2); + + expect(form.outcomes[0].id).toBe(FormModel.SAVE_OUTCOME); + expect(form.outcomes[0].isSystem).toBeTruthy(); + + expect(form.outcomes[1].id).toBe('custom-1'); + expect(form.outcomes[1].isSystem).toBeFalsy(); + }); +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts new file mode 100644 index 0000000000..00f72226f5 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts @@ -0,0 +1,151 @@ +/*! + * @license + * Copyright 2016 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 { FormWidgetModelCache } from './form-widget.model'; +import { FormValues } from './form-values'; +import { ContainerModel } from './container.model'; +import { TabModel } from './tab.model'; +import { FormOutcomeModel } from './form-outcome.model'; + +export class FormModel { + + static UNSET_TASK_NAME: string = 'Nameless task'; + static SAVE_OUTCOME: string = '$save'; + static COMPLETE_OUTCOME: string = '$complete'; + + private _id: string; + private _name: string; + private _taskId: string; + private _taskName: string = FormModel.UNSET_TASK_NAME; + + get id(): string { + return this._id; + } + + get name(): string { + return this._name; + } + + get taskId(): string { + return this._taskId; + } + + get taskName(): string { + return this._taskName; + } + + readOnly: boolean = false; + tabs: TabModel[] = []; + fields: ContainerModel[] = []; + outcomes: FormOutcomeModel[] = []; + + values: FormValues = {}; + + private _json: any; + + get json() { + return this._json; + } + + 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, data?: FormValues, readOnly: boolean = false) { + this.readOnly = readOnly; + if (json) { + this._json = json; + + this._id = json.id; + this._name = json.name; + this._taskId = json.taskId; + this._taskName = json.taskName || json.name || FormModel.UNSET_TASK_NAME; + + let tabCache: FormWidgetModelCache = {}; + + this.tabs = (json.tabs || []).map(t => { + let model = new TabModel(this, t); + tabCache[model.id] = model; + return model; + }); + + this.fields = this.parseContainerFields(json); + + if (data) { + this.loadData(data); + } + + for (let i = 0; i < this.fields.length; i++) { + let field = this.fields[i]; + if (field.tab) { + let tab = tabCache[field.tab]; + if (tab) { + tab.fields.push(new ContainerModel(this, field.json)); + } + } + } + if (json.fields) { + let saveOutcome = new FormOutcomeModel(this, { id: FormModel.SAVE_OUTCOME, name: 'Save', isSystem: true }); + let completeOutcome = new FormOutcomeModel(this, {id: FormModel.COMPLETE_OUTCOME, name: 'Complete', isSystem: true }); + + let customOutcomes = (json.outcomes || []).map(obj => new FormOutcomeModel(this, obj)); + + this.outcomes = [saveOutcome].concat( + customOutcomes.length > 0 ? customOutcomes : [completeOutcome] + ); + } + } + } + + private parseContainerFields(json: any): ContainerModel[] { + let fields = []; + + if (json.fields) { + fields = json.fields; + } else if (json.formDefinition && json.formDefinition.fields) { + fields = json.formDefinition.fields; + } + + return fields.map(obj => new ContainerModel(this, obj)); + } + + // Loads external data and overrides field values + // Typically used when form definition and form data coming from different sources + private loadData(data: FormValues) { + for (let i = 0; i < this.fields.length; i++) { + let container = this.fields[i]; + for (let i = 0; i < container.columns.length; i++) { + let column = container.columns[i]; + for (let i = 0; i < column.fields.length; i++) { + let field = column.fields[i]; + if (data[field.id]) { + field.json.value = data[field.id]; + field.value = data[field.id]; + } + } + } + } + } +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/index.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/index.ts new file mode 100644 index 0000000000..ce68bdba24 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/index.ts @@ -0,0 +1,28 @@ +/*! + * @license + * Copyright 2016 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-field-metadata'; +export * from './form-values'; +export * from './form-field-types'; +export * from './form-field-option'; +export * from './form-widget.model'; +export * from './form-field.model'; +export * from './form.model'; +export * from './container-column.model'; +export * from './container.model'; +export * from './tab.model'; +export * from './form-outcome.model'; diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/tab.model.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/tab.model.spec.ts new file mode 100644 index 0000000000..6d060bc61f --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/tab.model.spec.ts @@ -0,0 +1,70 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect } from '@angular/core/testing'; +import { FormModel } from './form.model'; +import { TabModel } from './tab.model'; +import { ContainerModel } from './container.model'; + +describe('TabModel', () => { + + it('should setup with json config', () => { + let json = { + id: '', + title: '', + visibilityCondition: '<condition>' + }; + + let model = new TabModel(null, json); + expect(model.id).toBe(json.id); + expect(model.title).toBe(json.title); + expect(model.visibilityCondition).toBe(json.visibilityCondition); + }); + + it('should not setup with json config', () => { + let model = new TabModel(null, null); + expect(model.id).toBeUndefined(); + expect(model.title).toBeUndefined(); + expect(model.visibilityCondition).toBeUndefined(); + }); + + it('should evaluate content based on fields', () => { + let model = new TabModel(null, null); + + model.fields = null; + expect(model.hasContent()).toBeFalsy(); + + model.fields = []; + expect(model.hasContent()).toBeFalsy(); + + model.fields = [new ContainerModel(null, null)]; + expect(model.hasContent()).toBeTruthy(); + }); + + it('should store the form reference', () => { + let form = new FormModel(); + let model = new TabModel(form); + expect(model.form).toBe(form); + }); + + it('should store original json', () => { + let json = {}; + let model = new TabModel(null, json); + expect(model.json).toBe(json); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/tab.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/tab.model.ts new file mode 100644 index 0000000000..e1932ba1f6 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/tab.model.ts @@ -0,0 +1,43 @@ +/*! + * @license + * Copyright 2016 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 { FormWidgetModel } from './form-widget.model'; +import { ContainerModel } from './container.model'; +import { FormModel } from './form.model'; + +export class TabModel extends FormWidgetModel { + + id: string; + title: string; + visibilityCondition: any; + + fields: ContainerModel[] = []; + + hasContent(): boolean { + return this.fields && this.fields.length > 0; + } + + constructor(form: FormModel, json?: any) { + super(form, json); + + if (json) { + this.id = json.id; + this.title = json.title; + this.visibilityCondition = json.visibilityCondition; + } + } +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/dropdown/dropdown.widget.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/dropdown/dropdown.widget.spec.ts new file mode 100644 index 0000000000..2fdaf83374 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/dropdown/dropdown.widget.spec.ts @@ -0,0 +1,212 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect, beforeEach } from '@angular/core/testing'; +import { Http, RequestOptionsArgs, Response, ResponseOptions } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; +import { DropdownWidget } from './dropdown.widget'; +import { FormModel } from './../core/form.model'; +import { FormFieldModel } from './../core/form-field.model'; + +describe('DropdownWidget', () => { + + let http: Http; + let widget: DropdownWidget; + + beforeEach(() => { + http = <Http> { + get(url: string, options?: RequestOptionsArgs): Observable<Response> { + return null; + } + }; + widget = new DropdownWidget(http); + }); + + it('should fetch and parse REST data on init', () => { + + let data = [ + { uid: '1', text: 'One' }, + { uid: '2', text: 'Two' } + ]; + + spyOn(http, 'get').and.callFake((url) => { + return Observable.create(observer => { + let options = new ResponseOptions({ + body: data, + url: url + }); + let response = new Response(options); + observer.next(response); + observer.complete(); + }); + }); + + let field = new FormFieldModel(new FormModel(), { + optionType: 'rest', + restUrl: 'http://<address>', + restIdProperty: 'uid', + restLabelProperty: 'text' + }); + + widget.field = field; + widget.ngOnInit(); + + + expect((<any>http.get).calls.argsFor(0)).toEqual([field.restUrl]); + expect(field.options.length).toBe(2); + + expect(field.options[0].id).toBe(data[0].uid); + expect(field.options[0].name).toBe(data[0].text); + + expect(field.options[1].id).toBe(data[1].uid); + expect(field.options[1].name).toBe(data[1].text); + }); + + it('should require REST settings to fetch data', () => { + let form = new FormModel(); + spyOn(http, 'get').and.stub(); + + // 1) Null field + widget.field = null; + widget.ngOnInit(); + expect(http.get).not.toHaveBeenCalled(); + + // 2) Missing [optionType] + widget.field = new FormFieldModel(form, { + optionType: null, + restUrl: 'http://<address>', + restIdProperty: 'uid', + restLabelProperty: 'text' + }); + widget.ngOnInit(); + expect(http.get).not.toHaveBeenCalled(); + + // 3) Missing [restUrl] + widget.field = new FormFieldModel(form, { + optionType: 'rest', + restUrl: null, + restIdProperty: 'uid', + restLabelProperty: 'text' + }); + widget.ngOnInit(); + expect(http.get).not.toHaveBeenCalled(); + + // 4) Missing [restIdProperty] + widget.field = new FormFieldModel(form, { + optionType: 'rest', + restUrl: 'http://<address>', + restIdProperty: null, + restLabelProperty: 'text' + }); + widget.ngOnInit(); + expect(http.get).not.toHaveBeenCalled(); + + // 4) Missing [restLabelProperty] + widget.field = new FormFieldModel(form, { + optionType: 'rest', + restUrl: 'http://<address>', + restIdProperty: null, + restLabelProperty: null + }); + widget.ngOnInit(); + expect(http.get).not.toHaveBeenCalled(); + }); + + it('should parse only array response', () => { + expect(widget.loadFromJson([])).toBeFalsy(); + + widget.field = new FormFieldModel(new FormModel()); + expect(widget.loadFromJson([])).toBeTruthy(); + + expect(widget.loadFromJson(null)).toBeFalsy(); + expect(widget.loadFromJson({})).toBeFalsy(); + }); + + it('should bind to nested properties', () => { + let data = [ + { uid: { value: 1 }, name: { fullName: 'John Doe' } } + ]; + + spyOn(http, 'get').and.callFake((url) => { + return Observable.create(observer => { + let options = new ResponseOptions({ + body: data, + url: url + }); + let response = new Response(options); + observer.next(response); + observer.complete(); + }); + }); + + let field = new FormFieldModel(new FormModel(), { + optionType: 'rest', + restUrl: 'http://<address>', + restIdProperty: 'uid.value', + restLabelProperty: 'name.fullName' + }); + + widget.field = field; + widget.ngOnInit(); + + expect(field.options.length).toBe(1); + expect(field.options[0].id).toBe(data[0].uid.value.toString()); + expect(field.options[0].name).toBe(data[0].name.fullName); + }); + + it('should update form upon loading REST data', () => { + let field = new FormFieldModel(new FormModel()); + widget.field = field; + + spyOn(field, 'updateForm').and.stub(); + + expect(widget.loadFromJson([])).toBeTruthy(); + expect(field.updateForm).toHaveBeenCalled(); + }); + + it('should handle error with generic message', () => { + spyOn(console, 'error').and.stub(); + + widget.handleError(null); + expect(console.error).toHaveBeenCalledWith(DropdownWidget.UNKNOWN_ERROR_MESSAGE); + }); + + it('should handle error with error message', () => { + spyOn(console, 'error').and.stub(); + + const message = '<error>'; + widget.handleError({ message: message }); + + expect(console.error).toHaveBeenCalledWith(message); + }); + + it('should handle error with detailed message', () => { + spyOn(console, 'error').and.stub(); + widget.handleError({ + status: '400', + statusText: 'Bad request' + }); + expect(console.error).toHaveBeenCalledWith('400 - Bad request'); + }); + + it('should handle error with generic message', () => { + spyOn(console, 'error').and.stub(); + widget.handleError({}); + expect(console.error).toHaveBeenCalledWith(DropdownWidget.GENERIC_ERROR_MESSAGE); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/dropdown/dropdown.widget.ts b/ng2-components/ng2-activiti-form/src/components/widgets/dropdown/dropdown.widget.ts index c5d61526e5..5e47fd41b9 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/dropdown/dropdown.widget.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/dropdown/dropdown.widget.ts @@ -16,7 +16,6 @@ */ import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs/Rx'; import { Http } from '@angular/http'; import { ObjectUtils } from 'ng2-alfresco-core'; import { WidgetComponent } from './../widget.component'; @@ -32,6 +31,9 @@ declare var componentHandler; }) export class DropdownWidget extends WidgetComponent implements OnInit { + static UNKNOWN_ERROR_MESSAGE: string = 'Unknown error'; + static GENERIC_ERROR_MESSAGE: string = 'Server error'; + constructor(private http: Http) { super(); } @@ -51,13 +53,12 @@ export class DropdownWidget extends WidgetComponent implements OnInit { }, this.handleError ); - } } // TODO: support 'restResponsePath' - private loadFromJson(json: any) { - if (json instanceof Array) { + loadFromJson(json: any): boolean { + if (this.field && json && json instanceof Array) { let options = json.map(obj => { return { id: ObjectUtils.getValue(obj, this.field.restIdProperty).toString(), @@ -66,16 +67,19 @@ export class DropdownWidget extends WidgetComponent implements OnInit { }); this.field.options = options; this.field.updateForm(); + return true; } + return false; } - private handleError (error: any) { - // In a real world app, we might use a remote logging infrastructure - // We'd also dig deeper into the error to get a better message - let errMsg = (error.message) ? error.message : - error.status ? `${error.status} - ${error.statusText}` : 'Server error'; - console.error(errMsg); // log to console instead - return Observable.throw(errMsg); + + handleError(error: any) { + let errMsg = DropdownWidget.UNKNOWN_ERROR_MESSAGE; + if (error) { + errMsg = (error.message) ? error.message : + error.status ? `${error.status} - ${error.statusText}` : DropdownWidget.GENERIC_ERROR_MESSAGE; + } + console.error(errMsg); } } diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/hyperlink/hyperlink.widget.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/hyperlink/hyperlink.widget.spec.ts new file mode 100644 index 0000000000..9b6be175ae --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/hyperlink/hyperlink.widget.spec.ts @@ -0,0 +1,97 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect, beforeEach } from '@angular/core/testing'; +import { HyperlinkWidget } from './hyperlink.widget'; +import { FormModel } from './../core/form.model'; +import { FormFieldModel } from './../core/form-field.model'; + +describe('HyperlinkWidget', () => { + + let widget: HyperlinkWidget; + + beforeEach(() => { + widget = new HyperlinkWidget(); + }); + + it('should get link text from field display text', () => { + const text = 'hello world'; + + widget.field = new FormFieldModel(new FormModel(), { + displayText: text + }); + + expect(widget.linkText).toBe(text); + }); + + it('should get link text from field url', () => { + const url = 'http://<address>'; + + widget.field = new FormFieldModel(new FormModel(), { + displayText: null, + hyperlinkUrl: url + }); + + expect(widget.linkText).toBe(url); + }); + + it('should require field to get link text', () => { + widget.field = null; + expect(widget.linkText).toBeNull(); + }); + + it('should not return link text', () => { + widget.field = new FormFieldModel(new FormModel(), { + displayText: null, + hyperlinkUrl: null + }); + + expect(widget.linkText).toBeNull(); + }); + + it('should return default url for missing field', () => { + widget.field = null; + expect(widget.linkUrl).toBe(HyperlinkWidget.DEFAULT_URL); + }); + + it('should return default url for missing field property', () => { + widget.field = new FormFieldModel(new FormModel(), { + hyperlinkUrl: null + }); + + expect(widget.linkUrl).toBe(HyperlinkWidget.DEFAULT_URL); + }); + + it('should prepend url with scheme', () => { + const url = 'www.alfresco.com'; + widget.field = new FormFieldModel(new FormModel(), { + hyperlinkUrl: url + }); + + expect(widget.linkUrl).toBe(`${HyperlinkWidget.DEFAULT_URL_SCHEME}${url}`); + }); + + it('should not prepend url with scheme', () => { + const url = 'https://<secure/address>'; + widget.field = new FormFieldModel(new FormModel(), { + hyperlinkUrl: url + }); + + expect(widget.linkUrl).toBe(url); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/hyperlink/hyperlink.widget.ts b/ng2-components/ng2-activiti-form/src/components/widgets/hyperlink/hyperlink.widget.ts index 5f7139efed..dd9c7e3fd2 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/hyperlink/hyperlink.widget.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/hyperlink/hyperlink.widget.ts @@ -29,21 +29,27 @@ declare var componentHandler; }) export class HyperlinkWidget extends WidgetComponent { + static DEFAULT_URL: string = '#'; + static DEFAULT_URL_SCHEME: string = 'http://'; + get linkUrl(): string { - let url = '#'; + let url = HyperlinkWidget.DEFAULT_URL; if (this.field && this.field.hyperlinkUrl) { url = this.field.hyperlinkUrl; if (!/^https?:\/\//i.test(url)) { - url = 'http://' + url; + url = HyperlinkWidget.DEFAULT_URL_SCHEME + url; } } return url; } - get linkText() { - return this.field.displayText || this.field.hyperlinkUrl; + get linkText(): string { + if (this.field) { + return this.field.displayText || this.field.hyperlinkUrl; + } + return null; } } diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/index.ts b/ng2-components/ng2-activiti-form/src/components/widgets/index.ts index f2c0a1e6f1..9e5d480583 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/index.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/index.ts @@ -30,9 +30,7 @@ import { DisplayTextWidget } from './display-text/display-text.widget'; // core export * from './widget.component'; - -// model -export * from './widget.model'; +export * from './core/index'; // containers export * from './tabs/tabs.widget'; diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/multiline-text/multiline-text.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/multiline-text/multiline-text.widget.html index 21e1310d50..d33c9d518f 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/multiline-text/multiline-text.widget.html +++ b/ng2-components/ng2-activiti-form/src/components/widgets/multiline-text/multiline-text.widget.html @@ -1,6 +1,10 @@ <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label multiline-text-widget"> - <textarea class="mdl-textfield__input" type="text" rows= "3" - [attr.id]="field.id" [(ngModel)]="field.value"> + <textarea class="mdl-textfield__input" + type="text" + rows= "3" + [attr.id]="field.id" + [(ngModel)]="field.value" + [disabled]="field.readOnly"> </textarea> <label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label> </div> diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/number/number.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/number/number.widget.html index 879ad88dd7..37b0be6875 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/number/number.widget.html +++ b/ng2-components/ng2-activiti-form/src/components/widgets/number/number.widget.html @@ -1,6 +1,10 @@ <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label number-widget"> - <input class="mdl-textfield__input" type="text" pattern="-?[0-9]*(\.[0-9]+)?" - [attr.id]="field.id" [(ngModel)]="field.value"> + <input class="mdl-textfield__input" + type="text" + pattern="-?[0-9]*(\.[0-9]+)?" + [attr.id]="field.id" + [(ngModel)]="field.value" + [disabled]="field.readOnly"> <label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label> <span class="mdl-textfield__error">Input is not a number!</span> </div> diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/radio-buttons/radio-buttons.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/radio-buttons/radio-buttons.widget.html index cc7fbd8cb2..a832a925f2 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/radio-buttons/radio-buttons.widget.html +++ b/ng2-components/ng2-activiti-form/src/components/widgets/radio-buttons/radio-buttons.widget.html @@ -2,11 +2,12 @@ <div *ngFor="let opt of field.options"> <label [attr.for]="opt.id" class="mdl-radio mdl-js-radio"> <input type="radio" + class="mdl-radio__button" [checked]="field.value === opt.id" [attr.id]="opt.id" [attr.name]="field.id" [attr.value]="opt.id" - class="mdl-radio__button" + [disabled]="field.readOnly" (click)="field.value = opt.id"> <span class="mdl-radio__label">{{opt.name}}</span> </label> diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/tabs/tabs.widget.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/tabs/tabs.widget.spec.ts new file mode 100644 index 0000000000..2ffc23f974 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/tabs/tabs.widget.spec.ts @@ -0,0 +1,60 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect, beforeEach } from '@angular/core/testing'; +import { TabsWidget } from './tabs.widget'; +import { TabModel } from './../core/tab.model'; + +describe('TabsWidget', () => { + + let componentHandler; + let widget: TabsWidget; + + beforeEach(() => { + widget = new TabsWidget(); + + componentHandler = jasmine.createSpyObj('componentHandler', [ + 'upgradeAllRegistered' + ]); + + window['componentHandler'] = componentHandler; + }); + + it('should check tabs', () => { + widget.tabs = null; + expect(widget.hasTabs()).toBeFalsy(); + + widget.tabs = []; + expect(widget.hasTabs()).toBeFalsy(); + + widget.tabs = [new TabModel(null)]; + expect(widget.hasTabs()).toBeTruthy(); + }); + + it('should upgrade MDL content on view init', () => { + widget.ngAfterViewInit(); + expect(componentHandler.upgradeAllRegistered).toHaveBeenCalled(); + }); + + it('should setup MDL content only if component handler available', () => { + expect(widget.setupMaterialComponents()).toBeTruthy(); + + window['componentHandler'] = null; + expect(widget.setupMaterialComponents()).toBeFalsy(); + }); + +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/tabs/tabs.widget.ts b/ng2-components/ng2-activiti-form/src/components/widgets/tabs/tabs.widget.ts index 75a891ee87..8c4d9de949 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/tabs/tabs.widget.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/tabs/tabs.widget.ts @@ -17,7 +17,7 @@ import { Component, Input, AfterViewInit } from '@angular/core'; import { MATERIAL_DESIGN_DIRECTIVES } from 'ng2-alfresco-core'; -import { TabModel } from './../widget.model'; +import { TabModel } from './../core/index'; import { ContainerWidget } from './../container/container.widget'; declare let __moduleName: string; @@ -39,9 +39,15 @@ export class TabsWidget implements AfterViewInit { } ngAfterViewInit() { + this.setupMaterialComponents(); + } + + setupMaterialComponents(): boolean { // workaround for MDL issues with dynamic components if (componentHandler) { componentHandler.upgradeAllRegistered(); + return true; } + return false; } } diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/text/text.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/text/text.widget.html index 65d13b4eeb..5d6d2e5901 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/text/text.widget.html +++ b/ng2-components/ng2-activiti-form/src/components/widgets/text/text.widget.html @@ -1,4 +1,8 @@ <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label text-widget"> - <input class="mdl-textfield__input" type="text" [attr.id]="field.id" [(ngModel)]="field.value"> + <input class="mdl-textfield__input" + type="text" + [attr.id]="field.id" + [(ngModel)]="field.value" + [disabled]="field.readOnly"> <label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label> </div> diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/widget.component.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/widget.component.spec.ts new file mode 100644 index 0000000000..d57b2e54f9 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/widget.component.spec.ts @@ -0,0 +1,55 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect, beforeEach } from '@angular/core/testing'; +import { WidgetComponent } from './widget.component'; +import { FormFieldModel } from './core/form-field.model'; + +describe('WidgetComponent', () => { + + let componentHandler; + + beforeEach(() => { + componentHandler = jasmine.createSpyObj('componentHandler', [ + 'upgradeAllRegistered' + ]); + + window['componentHandler'] = componentHandler; + }); + + it('should upgrade MDL content on view init', () => { + let component = new WidgetComponent(); + component.ngAfterViewInit(); + expect(componentHandler.upgradeAllRegistered).toHaveBeenCalled(); + }); + + it('should setup MDL content only if component handler available', () => { + let component = new WidgetComponent(); + expect(component.setupMaterialComponents()).toBeTruthy(); + + window['componentHandler'] = null; + expect(component.setupMaterialComponents()).toBeFalsy(); + }); + + it('should check field', () => { + let component = new WidgetComponent(); + + expect(component.hasField()).toBeFalsy(); + component.field = new FormFieldModel(null); + expect(component.hasField()).toBeTruthy(); + }); +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/widget.component.ts b/ng2-components/ng2-activiti-form/src/components/widgets/widget.component.ts index b698afeb3a..ec08a747cc 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/widget.component.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/widget.component.ts @@ -16,7 +16,7 @@ */ import { Input, AfterViewInit } from '@angular/core'; -import { FormFieldModel } from './widget.model'; +import { FormFieldModel } from './core/index'; declare let __moduleName: string; declare var componentHandler; @@ -34,10 +34,16 @@ export class WidgetComponent implements AfterViewInit { } ngAfterViewInit() { + this.setupMaterialComponents(); + } + + setupMaterialComponents(): boolean { // workaround for MDL issues with dynamic components if (componentHandler) { componentHandler.upgradeAllRegistered(); + return true; } + return false; } } diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/widget.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/widget.model.ts deleted file mode 100644 index 89d866e6fa..0000000000 --- a/ng2-components/ng2-activiti-form/src/components/widgets/widget.model.ts +++ /dev/null @@ -1,463 +0,0 @@ -/*! - * @license - * Copyright 2016 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 interface FormFieldMetadata { - [key: string]: any; -} - -export interface FormValues extends FormFieldMetadata { -} - -export class FormFieldTypes { - static CONTAINER: string = 'container'; - static GROUP: string = 'group'; - static DROPDOWN: string = 'dropdown'; - static HYPERLINK: string = 'hyperlink'; - static RADIO_BUTTONS: string = 'radio-buttons'; - static DISPLAY_VALUE: string = 'readonly'; - static READONLY_TEXT: string = 'readonly-text'; -} - -export class FormWidgetModel { - - private _form: FormModel; - private _json: any; - - get form(): FormModel { - return this._form; - } - - get json(): any { - return this._json; - } - - constructor(form: FormModel, json: any) { - this._form = form; - this._json = json; - } -} - -export interface FormFieldOption { - id: string; - name: string; -} - -export class FormFieldModel extends FormWidgetModel { - - private _value: string; - - fieldType: string; - id: string; - name: string; - type: string; - required: boolean; - readOnly: boolean; - overrideId: boolean; - tab: string; - colspan: number = 1; - options: FormFieldOption[] = []; - restUrl: string; - restResponsePath: string; - restIdProperty: string; - restLabelProperty: string; - hasEmptyValue: boolean; - className: string; - optionType: string; - params: FormFieldMetadata = {}; - hyperlinkUrl: string; - displayText: string; - - get value(): any { - return this._value; - } - - set value(v: any) { - this._value = v; - this.updateForm(); - } - - constructor(form: FormModel, json?: any) { - super(form, json); - - if (json) { - this.fieldType = json.fieldType; - this.id = json.id; - this.name = json.name; - this.type = json.type; - this.required = <boolean> json.required; - this.readOnly = <boolean> json.readOnly; - this.overrideId = <boolean> json.overrideId; - this.tab = json.tab; - this.restUrl = json.restUrl; - this.restResponsePath = json.restResponsePath; - this.restIdProperty = json.restIdProperty; - this.restLabelProperty = json.restLabelProperty; - this.colspan = <number> json.colspan; - this.options = <FormFieldOption[]> json.options || []; - this.hasEmptyValue = <boolean> json.hasEmptyValue; - this.className = json.className; - this.optionType = json.optionType; - this.params = <FormFieldMetadata> json.params || {}; - this.hyperlinkUrl = json.hyperlinkUrl; - this.displayText = json.displayText; - - this._value = this.parseValue(json); - this.updateForm(); - } - } - - private parseValue(json: any): any { - let value = json.value; - - /* - This is needed due to Activiti issue related to reading dropdown values as value string - but saving back as object: { id: <id>, name: <name> } - */ - // TODO: needs review - if (json.type === FormFieldTypes.DROPDOWN) { - if (value === '') { - value = 'empty'; - } - } - - /* - This is needed due to Activiti issue related to reading radio button values as value string - but saving back as object: { id: <id>, name: <name> } - */ - if (json.type === FormFieldTypes.RADIO_BUTTONS) { - // Activiti has a bug with default radio button value, - // so try resolving current one with a fallback to first entry - let entry: FormFieldOption[] = this.options.filter(opt => opt.id === value); - if (entry.length > 0) { - value = entry[0].id; - } else if (this.options.length > 0) { - value = this.options[0].id; - } - } - - return value; - } - - updateForm() { - if (this.type === FormFieldTypes.DROPDOWN) { - /* - This is needed due to Activiti reading dropdown values as string - but saving back as object: { id: <id>, name: <name> } - */ - if (this.value === 'empty' || this.value === '') { - this.form.values[this.id] = {}; - } else { - let entry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value); - if (entry.length > 0) { - this.form.values[this.id] = entry[0]; - } - } - } else if (this.type === FormFieldTypes.RADIO_BUTTONS) { - /* - This is needed due to Activiti issue related to reading radio button values as value string - but saving back as object: { id: <id>, name: <name> } - */ - let entry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value); - if (entry.length > 0) { - this.form.values[this.id] = entry[0]; - } else if (this.options.length > 0) { - this.form.values[this.id] = this.options[0].id; - } - } else { - if (!this.isIngonreType()) { - this.form.values[this.id] = this.value; - } - } - } - - private isIngonreType(): boolean { - if (this.type === FormFieldTypes.READONLY_TEXT) { - return true; - } else { - return false; - } - } -} - -export class ContainerColumnModel { - - size: number = 12; - fields: FormFieldModel[] = []; - - hasFields(): boolean { - return this.fields && this.fields.length > 0; - } -} - -// TODO: inherit FormFieldModel -export class ContainerModel extends FormWidgetModel { - - fieldType: string; - id: string; - name: string; - type: string; - tab: string; - numberOfColumns: number = 1; - params: FormFieldMetadata = {}; - - columns: ContainerColumnModel[] = []; - isExpanded: boolean = true; - - isGroup(): boolean { - return this.type === FormFieldTypes.GROUP; - } - - isCollapsible(): boolean { - let allowCollapse = false; - - if (this.isGroup() && this.params['allowCollapse']) { - allowCollapse = <boolean> this.params['allowCollapse']; - } - - return allowCollapse; - } - - isCollapsedByDefault(): boolean { - let collapseByDefault = false; - - if (this.isCollapsible() && this.params['collapseByDefault']) { - collapseByDefault = <boolean> this.params['collapseByDefault']; - } - - return collapseByDefault; - } - - constructor(form: FormModel, json?: any) { - super(form, json); - - if (json) { - this.fieldType = json.fieldType; - this.id = json.id; - this.name = json.name; - this.type = json.type; - this.tab = json.tab; - this.numberOfColumns = <number> json.numberOfColumns; - this.params = <FormFieldMetadata> json.params || {}; - - let columnSize: number = 12; - if (this.numberOfColumns > 1) { - columnSize = 12 / this.numberOfColumns; - } - - for (let i = 0; i < this.numberOfColumns; i++) { - let col = new ContainerColumnModel(); - col.size = columnSize; - this.columns.push(col); - } - - Object.keys(json.fields).map(key => { - let fields = (json.fields[key] || []).map(f => new FormFieldModel(form, f)); - let col = this.columns[parseInt(key, 10) - 1]; - col.fields = fields; - }); - - this.isExpanded = !this.isCollapsedByDefault(); - } - } -} - -export class TabModel extends FormWidgetModel { - - id: string; - title: string; - visibilityCondition: any; - - fields: ContainerModel[] = []; - - hasContent(): boolean { - return this.fields && this.fields.length > 0; - } - - constructor(form: FormModel, json?: any) { - super(form, json); - - if (json) { - this.id = json.id; - this.title = json.title; - this.visibilityCondition = json.visibilityCondition; - } - } -} - -export interface WidgetModelCache<T extends FormWidgetModel> { - [key: string]: T; -} - -export class FormOutcomeModel extends FormWidgetModel { - - private _id: string; - private _name: string; - - isSystem: boolean = false; - - get id() { - return this._id; - } - - get name() { - return this._name; - } - - constructor(form: FormModel, json?: any) { - super(form, json); - - if (json) { - this._id = json.id; - this._name = json.name; - } - } -} - -export class FormModel { - - private UNSET_TASK_NAME: string = 'Nameless task'; - - private _id: string; - private _name: string; - private _taskId: string; - private _taskName: string = this.UNSET_TASK_NAME; - - get id(): string { - return this._id; - } - - get name(): string { - return this._name; - } - - get taskId(): string { - return this._taskId; - } - - get taskName(): string { - return this._taskName; - } - - tabs: TabModel[] = []; - fields: ContainerModel[] = []; - outcomes: FormOutcomeModel[] = []; - - values: FormValues = {}; - - private _json: any; - - get json() { - return this._json; - } - - 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, data?: any, saveOption?: any, readOnly: boolean = false) { - if (json) { - this._json = json; - - this._id = json.id; - this._name = json.name; - this._taskId = json.taskId; - this._taskName = json.taskName || json.name || this.UNSET_TASK_NAME; - - let tabCache: WidgetModelCache<TabModel> = {}; - - // this.tabs = (json.tabs || []).map(t => new TabModel(this, t)); - this.tabs = (json.tabs || []).map(t => { - let model = new TabModel(this, t); - tabCache[model.id] = model; - return model; - }); - - this.fields = (json.fields || json.formDefinition.fields || []).map(obj => new ContainerModel(this, obj)); - - if (data) { - this.updateFormValueWithProvaidedDataModel(data); - } - - for (let i = 0; i < this.fields.length; i++) { - let field = this.fields[i]; - if (field.tab) { - let tab = tabCache[field.tab]; - if (tab) { - tab.fields.push(new ContainerModel(this, field.json)); - } - } - } - if (this.isATaskForm()) { - let saveOutcome = new FormOutcomeModel(this, {id: '$save', name: 'Save'}); - saveOutcome.isSystem = true; - - let completeOutcome = new FormOutcomeModel(this, {id: '$complete', name: 'Complete'}); - completeOutcome.isSystem = true; - - let customOutcomes = (json.outcomes || []).map(obj => new FormOutcomeModel(this, obj)); - - this.outcomes = [saveOutcome].concat( - customOutcomes.length > 0 ? customOutcomes : [completeOutcome] - ); - } else { - if (saveOption && saveOption.observers.length > 0) { - let saveOutcome = new FormOutcomeModel(this, {id: '$custom', name: 'Save'}); - saveOutcome.isSystem = true; - - this.outcomes = [saveOutcome]; - } - } - } - } - - private updateFormValueWithProvaidedDataModel(data: any) { - for (let i = 0; i < this.fields.length; i++) { - let containerModel = this.fields[i]; - if (containerModel) { - for (let i = 0; i < containerModel.columns.length; i++) { - let containerModelColumn = containerModel.columns[i]; - if (containerModelColumn) { - for (let i = 0; i < containerModelColumn.fields.length; i++) { - let formField = containerModelColumn.fields[i]; - if (data[formField.id]) { - formField.value = data[formField.id]; - formField.json.value = data[formField.id]; - } - } - } - } - } - - } - } - - /** - * Check if the form is associated to a task or if is only the form definition - * @returns {boolean} - */ - private isATaskForm(): boolean { - return this._json.fields ? true : false; - } -} diff --git a/ng2-components/ng2-activiti-form/src/services/form.service.spec.ts b/ng2-components/ng2-activiti-form/src/services/form.service.spec.ts new file mode 100644 index 0000000000..57cbb1ed40 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/services/form.service.spec.ts @@ -0,0 +1,320 @@ +/*! + * @license + * Copyright 2016 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 { it, describe, expect, beforeEach } from '@angular/core/testing'; +import { Http, RequestOptionsArgs, Response, ResponseOptions } from '@angular/http'; +import { Observable } from 'rxjs/Rx'; +import { AlfrescoAuthenticationService, AlfrescoSettingsService } from 'ng2-alfresco-core'; + +import { FormService } from './form.service'; +import { FormValues } from './../components/widgets/core/index'; + +describe('FormService', () => { + + let http: Http; + let responseBody: any; + let formService: FormService; + let authService: AlfrescoAuthenticationService; + let settingsService: AlfrescoSettingsService; + + let createResponse = (url, body): Observable<Response> => { + return Observable.create(observer => { + let response = new Response(new ResponseOptions({ + url: url, + body: body + })); + observer.next(response); + observer.complete(); + }); + }; + + beforeEach(() => { + + http = <Http> { + get(url: string, options?: RequestOptionsArgs): Observable<Response> { + return createResponse(url, responseBody); + }, + post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> { + return createResponse(url, responseBody); + } + }; + + settingsService = new AlfrescoSettingsService(); + settingsService.setProviders([]); + + authService = new AlfrescoAuthenticationService(settingsService, null); + formService = new FormService(http, authService, settingsService); + }); + + it('should resolve host address via settings service', () => { + const url = '<url>'; + settingsService.bpmHost = url; + expect(formService.getHostAddress()).toBe(url); + }); + + it('should fetch and parse process definitions', (done) => { + spyOn(http, 'get').and.callThrough(); + + responseBody = { + data: [ + { id: '1' }, + { id: '2' } + ] + }; + + formService.getProcessDefinitions().subscribe(result => { + expect(http.get).toHaveBeenCalled(); + + let args: any[] = (<any>http).get.calls.argsFor(0); + expect(args[0].endsWith('/process-definitions')).toBeTruthy(); + + expect(result).toEqual(responseBody.data); + done(); + }); + }); + + it('should fetch and parse tasks', (done) => { + spyOn(http, 'post').and.callThrough(); + + responseBody = { + data: [ + { id: '1' }, + { id: '2' } + ] + }; + + formService.getTasks().subscribe(result => { + expect(http.post).toHaveBeenCalled(); + + let args: any[] = (<any>http).post.calls.argsFor(0); + expect(args[0].endsWith('/tasks/query')).toBeTruthy(); + + expect(result).toEqual(responseBody.data); + done(); + }); + }); + + it('should fetch and parse the task by id', (done) => { + spyOn(http, 'get').and.callThrough(); + + responseBody = { + id: '1' + }; + + formService.getTask('1').subscribe(result => { + expect(http.get).toHaveBeenCalled(); + + let args: any[] = (<any>http).get.calls.argsFor(0); + expect(args[0].endsWith('/tasks/1')).toBeTruthy(); + + expect(result).toEqual(responseBody); + done(); + }); + }); + + it('should save task form', (done) => { + spyOn(http, 'post').and.callThrough(); + + let values = <FormValues> { + field1: 'one', + field2: 'two' + }; + + formService.saveTaskForm('1', values).subscribe(() => { + expect(http.post).toHaveBeenCalled(); + + let args: any[] = (<any>http).post.calls.argsFor(0); + expect(args[0].endsWith('/task-forms/1/save-form')).toBeTruthy(); + expect(args[1]).toEqual(JSON.stringify({ values: values })); + + done(); + }); + }); + + it('should complete task form', (done) => { + spyOn(http, 'post').and.callThrough(); + + let values = <FormValues> { + field1: 'one', + field2: 'two' + }; + + formService.completeTaskForm('1', values).subscribe(() => { + expect(http.post).toHaveBeenCalled(); + + let args: any[] = (<any>http).post.calls.argsFor(0); + expect(args[0].endsWith('/task-forms/1')).toBeTruthy(); + expect(args[1]).toEqual(JSON.stringify({ values: values })); + + done(); + }); + }); + + it('should complete task form with a specific outcome', (done) => { + spyOn(http, 'post').and.callThrough(); + + let values = <FormValues> { + field1: 'one', + field2: 'two' + }; + + formService.completeTaskForm('1', values, 'custom').subscribe(() => { + expect(http.post).toHaveBeenCalled(); + + let args: any[] = (<any>http).post.calls.argsFor(0); + expect(args[0].endsWith('/task-forms/1')).toBeTruthy(); + expect(args[1]).toEqual(JSON.stringify({ values: values, outcome: 'custom' })); + + done(); + }); + }); + + it('should get task form by id', (done) => { + spyOn(http, 'get').and.callThrough(); + + responseBody = { id: '1' }; + + formService.getTaskForm('1').subscribe(result => { + expect(http.get).toHaveBeenCalled(); + + let args: any[] = (<any>http).get.calls.argsFor(0); + expect(args[0].endsWith('/task-forms/1')).toBeTruthy(); + + expect(result).toEqual(responseBody); + done(); + }); + }); + + it('should get form definition by id', (done) => { + spyOn(http, 'get').and.callThrough(); + + responseBody = { id: '1' }; + + formService.getFormDefinitionById('1').subscribe(result => { + expect(http.get).toHaveBeenCalled(); + + let args: any[] = (<any>http).get.calls.argsFor(0); + expect(args[0].endsWith('/form-models/1')).toBeTruthy(); + + expect(result).toEqual(responseBody); + done(); + }); + }); + + it('should get form definition id by name', (done) => { + spyOn(http, 'get').and.callThrough(); + + const formName = 'form1'; + const formId = 1; + responseBody = { + data: [ + { id: formId } + ] + }; + + formService.getFormDefinitionByName(formName).subscribe(result => { + expect(http.get).toHaveBeenCalled(); + + let args: any[] = (<any>http).get.calls.argsFor(0); + expect(args[0].endsWith(`models?filter=myReusableForms&filterText=${formName}&modelType=2`)).toBeTruthy(); + + expect(result).toEqual(formId); + done(); + }); + }); + + it('should not get form id from response', () => { + let response = new Response(new ResponseOptions({ body: null })); + expect(formService.getFormId(response)).toBeNull(); + + response = new Response(new ResponseOptions({ body: {} })); + expect(formService.getFormId(response)).toBeNull(); + + response = new Response(new ResponseOptions({ body: { data: null } })); + expect(formService.getFormId(response)).toBeNull(); + + response = new Response(new ResponseOptions({ body: { data: [] } })); + expect(formService.getFormId(response)).toBeNull(); + + expect(formService.getFormId(null)).toBeNull(); + }); + + it('should convert response to json object', () => { + let data = { id: 1 }; + let response = new Response(new ResponseOptions({ body: data })); + expect(formService.toJson(response)).toEqual(data); + }); + + it('should fallback to empty json object', () => { + let response = new Response(new ResponseOptions({ body: null })); + expect(formService.toJson(response)).toEqual({}); + + expect(formService.toJson(null)).toEqual({}); + }); + + it('should convert response to json array', () => { + let payload = { + data: [ + { id: 1 } + ] + }; + + let response = new Response(new ResponseOptions({ body: JSON.stringify(payload) })); + expect(formService.toJsonArray(response)).toEqual(payload.data); + }); + + it('should fallback to empty json array', () => { + expect(formService.toJsonArray(null)).toEqual([]); + + let response = new Response(new ResponseOptions({ body: {} })); + expect(formService.toJsonArray(response)).toEqual([]); + + response = new Response(new ResponseOptions({ body: { data: null } })); + expect(formService.toJsonArray(response)).toEqual([]); + }); + + it('should handle error with generic message', () => { + spyOn(console, 'error').and.stub(); + + formService.handleError(null); + expect(console.error).toHaveBeenCalledWith(FormService.UNKNOWN_ERROR_MESSAGE); + }); + + it('should handle error with error message', () => { + spyOn(console, 'error').and.stub(); + + const message = '<error>'; + formService.handleError({ message: message }); + + expect(console.error).toHaveBeenCalledWith(message); + }); + + it('should handle error with detailed message', () => { + spyOn(console, 'error').and.stub(); + formService.handleError({ + status: '400', + statusText: 'Bad request' + }); + expect(console.error).toHaveBeenCalledWith('400 - Bad request'); + }); + + it('should handle error with generic message', () => { + spyOn(console, 'error').and.stub(); + formService.handleError({}); + expect(console.error).toHaveBeenCalledWith(FormService.GENERIC_ERROR_MESSAGE); + }); +}); diff --git a/ng2-components/ng2-activiti-form/src/services/form.service.ts b/ng2-components/ng2-activiti-form/src/services/form.service.ts index 4eb6228416..dc8092ece2 100644 --- a/ng2-components/ng2-activiti-form/src/services/form.service.ts +++ b/ng2-components/ng2-activiti-form/src/services/form.service.ts @@ -19,19 +19,26 @@ import { Injectable } from '@angular/core'; import { Response, Http, Headers, RequestOptions } from '@angular/http'; import { Observable } from 'rxjs/Rx'; import { AlfrescoAuthenticationService } from 'ng2-alfresco-core'; -import { FormValues } from './../components/widgets/widget.model'; +import { FormValues } from './../components/widgets/core/index'; import { AlfrescoSettingsService } from 'ng2-alfresco-core'; @Injectable() export class FormService { + static UNKNOWN_ERROR_MESSAGE: string = 'Unknown error'; + static GENERIC_ERROR_MESSAGE: string = 'Server error'; + constructor(private http: Http, private authService: AlfrescoAuthenticationService, private alfrescoSettingsService: AlfrescoSettingsService) { } + getHostAddress(): string { + return this.alfrescoSettingsService.bpmHost; + } + getProcessDefinitions(): Observable<any> { - let url = `${this.alfrescoSettingsService.bpmHost}/activiti-app/api/enterprise/process-definitions`; + let url = `${this.getHostAddress()}/activiti-app/api/enterprise/process-definitions`; let options = this.getRequestOptions(); return this.http .get(url, options) @@ -40,7 +47,7 @@ export class FormService { } getTasks(): Observable<any> { - let url = `${this.alfrescoSettingsService.bpmHost}/activiti-app/api/enterprise/tasks/query`; + let url = `${this.getHostAddress()}/activiti-app/api/enterprise/tasks/query`; let body = JSON.stringify({}); let options = this.getRequestOptions(); @@ -51,7 +58,7 @@ export class FormService { } getTask(id: string): Observable<any> { - let url = `${this.alfrescoSettingsService.bpmHost}/activiti-app/api/enterprise/tasks/${id}`; + let url = `${this.getHostAddress()}/activiti-app/api/enterprise/tasks/${id}`; let options = this.getRequestOptions(); return this.http @@ -61,10 +68,8 @@ export class FormService { } saveTaskForm(id: string, formValues: FormValues): Observable<Response> { - let url = `${this.alfrescoSettingsService.bpmHost}/activiti-app/api/enterprise/task-forms/${id}/save-form`; - let body = JSON.stringify({ - values: formValues - }); + let url = `${this.getHostAddress()}/activiti-app/api/enterprise/task-forms/${id}/save-form`; + let body = JSON.stringify({ values: formValues }); let options = this.getRequestOptions(); return this.http @@ -72,9 +77,16 @@ export class FormService { .catch(this.handleError); } + /** + * Complete Task Form + * @param id Task Id + * @param formValues Form Values + * @param outcome Form Outcome + * @returns {any} + */ completeTaskForm(id: string, formValues: FormValues, outcome?: string): Observable<Response> { - let url = `${this.alfrescoSettingsService.bpmHost}/activiti-app/api/enterprise/task-forms/${id}`; - let data: any = {values: formValues}; + let url = `${this.getHostAddress()}/activiti-app/api/enterprise/task-forms/${id}`; + let data: any = { values: formValues }; if (outcome) { data.outcome = outcome; } @@ -87,7 +99,7 @@ export class FormService { } getTaskForm(id: string): Observable<any> { - let url = `${this.alfrescoSettingsService.bpmHost}/activiti-app/api/enterprise/task-forms/${id}`; + let url = `${this.getHostAddress()}/activiti-app/api/enterprise/task-forms/${id}`; let options = this.getRequestOptions(); return this.http @@ -97,7 +109,7 @@ export class FormService { } getFormDefinitionById(id: string): Observable<any> { - let url = `${this.alfrescoSettingsService.bpmHost}/activiti-app/app/rest/form-models/${id}`; + let url = `${this.getHostAddress()}/activiti-app/app/rest/form-models/${id}`; let options = this.getRequestOptions(); return this.http @@ -106,9 +118,13 @@ export class FormService { .catch(this.handleError); } + /** + * Returns form definition ID by a given name. + * @param name + * @returns {Promise<T>|Promise<ErrorObservable>} + */ getFormDefinitionByName(name: string): Observable<any> { - let url = `${this.alfrescoSettingsService.bpmHost}` + - `/activiti-app/app/rest/models?filter=myReusableForms&filterText=${name}&modelType=2`; + let url = `${this.getHostAddress()}/activiti-app/app/rest/models?filter=myReusableForms&filterText=${name}&modelType=2`; let options = this.getRequestOptions(); return this.http @@ -130,27 +146,42 @@ export class FormService { return new RequestOptions({headers: headers}); } - private getFormId(res: Response) { - let body = res.json(); - return body.data[0].id || {}; + getFormId(res: Response) { + let result = null; + + if (res) { + let body = res.json(); + if (body && body.data && body.data.length > 0) { + result = body.data[0].id; + } + } + + return result; } - private toJson(res: Response) { - let body = res.json(); - return body || {}; + toJson(res: Response) { + if (res) { + let body = res.json(); + return body || {}; + } + return {}; } - private toJsonArray(res: Response) { - let body = res.json(); - return body.data || []; + toJsonArray(res: Response) { + if (res) { + let body = res.json(); + return body.data || []; + } + return []; } - private handleError(error: any) { - // In a real world app, we might use a remote logging infrastructure - // We'd also dig deeper into the error to get a better message - let errMsg = (error.message) ? error.message : - error.status ? `${error.status} - ${error.statusText}` : 'Server error'; - console.error(errMsg); // log to console instead + handleError(error: any): Observable<any> { + let errMsg = FormService.UNKNOWN_ERROR_MESSAGE; + if (error) { + errMsg = (error.message) ? error.message : + error.status ? `${error.status} - ${error.statusText}` : FormService.GENERIC_ERROR_MESSAGE; + } + console.error(errMsg); return Observable.throw(errMsg); } diff --git a/ng2-components/ng2-alfresco-core/src/services/AlfrescoAuthenticationService.service.ts b/ng2-components/ng2-alfresco-core/src/services/AlfrescoAuthenticationService.service.ts index 25cec2f692..cb66572d1a 100644 --- a/ng2-components/ng2-alfresco-core/src/services/AlfrescoAuthenticationService.service.ts +++ b/ng2-components/ng2-alfresco-core/src/services/AlfrescoAuthenticationService.service.ts @@ -35,13 +35,15 @@ export class AlfrescoAuthenticationService extends AlfrescoAuthenticationBase { /** * Constructor - * @param alfrescoSetting + * @param settingsService * @param http */ - constructor(alfrescoSetting: AlfrescoSettingsService, + constructor(settingsService: AlfrescoSettingsService, http: Http) { - super(alfrescoSetting, http); - this.createProviderInstance(alfrescoSetting.getProviders()); + super(settingsService, http); + if (settingsService) { + this.createProviderInstance(settingsService.getProviders()); + } } /**