New packages org (#2639)

New packages org
This commit is contained in:
Eugenio Romano
2017-11-16 14:12:52 +00:00
committed by GitHub
parent 6a24c6ef75
commit a52bb5600a
1984 changed files with 17179 additions and 40423 deletions

View File

@@ -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.
*/
import { Directive } from '@angular/core';
@Directive({ selector: '[form-custom-button]' }) export class StartFormCustomButtonDirective {}

View File

@@ -0,0 +1,101 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MaterialModule } from '../../../material.module';
import { ErrorWidgetComponent } from '../widgets/error/error.component';
import { FormRenderingService } from './../../services/form-rendering.service';
import { WidgetVisibilityService } from './../../services/widget-visibility.service';
import { CheckboxWidgetComponent } from './../widgets/checkbox/checkbox.widget';
import { FormFieldModel, FormFieldTypes, FormModel } from './../widgets/core/index';
import { InputMaskDirective } from './../widgets/text/text-mask.component';
import { TextWidgetComponent } from './../widgets/text/text.widget';
import { FormFieldComponent } from './form-field.component';
describe('FormFieldComponent', () => {
let fixture: ComponentFixture<FormFieldComponent>;
let component: FormFieldComponent;
let form: FormModel;
let formRenderingService: FormRenderingService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule
],
declarations: [
FormFieldComponent,
TextWidgetComponent,
CheckboxWidgetComponent,
InputMaskDirective,
ErrorWidgetComponent],
providers: [
FormRenderingService,
WidgetVisibilityService
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FormFieldComponent);
component = fixture.componentInstance;
formRenderingService = fixture.debugElement.injector.get(FormRenderingService);
form = new FormModel();
});
xit('should create default component instance', () => {
let field = new FormFieldModel(form, {
type: FormFieldTypes.TEXT
});
component.field = field;
fixture.detectChanges();
expect(component.componentRef).toBeDefined();
expect(component.componentRef.componentType).toBe(TextWidgetComponent);
});
xit('should create custom component instance', () => {
let field = new FormFieldModel(form, {
type: FormFieldTypes.TEXT
});
formRenderingService.setComponentTypeResolver(FormFieldTypes.TEXT, () => CheckboxWidgetComponent, true);
component.field = field;
fixture.detectChanges();
expect(component.componentRef).toBeDefined();
expect(component.componentRef.componentType).toBe(CheckboxWidgetComponent);
});
it('should require component type to be resolved', () => {
let field = new FormFieldModel(form, {
type: FormFieldTypes.TEXT
});
spyOn(formRenderingService, 'resolveComponentType').and.returnValue(null);
component.field = field;
fixture.detectChanges();
expect(formRenderingService.resolveComponentType).toHaveBeenCalled();
expect(component.componentRef).toBeUndefined();
});
});

View File

@@ -0,0 +1,152 @@
/*!
* @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 {
Compiler,
Component, ComponentFactory,
ComponentFactoryResolver,
ComponentRef,
Input,
ModuleWithComponentFactories,
NgModule,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { FormRenderingService } from './../../services/form-rendering.service';
import { WidgetVisibilityService } from './../../services/widget-visibility.service';
import { FormFieldModel } from './../widgets/core/index';
import { WidgetComponent } from './../widgets/widget.component';
declare var adf: any;
@Component({
selector: 'adf-form-field, form-field',
template: `
<div [hidden]="!field?.isVisible"
[class.adf-focus]="focus"
(focusin)="focusToggle()"
(focusout)="focusToggle()">
<div #container></div>
</div>
`,
encapsulation: ViewEncapsulation.None
})
export class FormFieldComponent implements OnInit, OnDestroy {
@ViewChild('container', { read: ViewContainerRef })
container: ViewContainerRef;
@Input()
field: FormFieldModel = null;
componentRef: ComponentRef<{}>;
focus: boolean = false;
constructor(private formRenderingService: FormRenderingService,
private componentFactoryResolver: ComponentFactoryResolver,
private visibilityService: WidgetVisibilityService,
private compiler: Compiler) {
}
ngOnInit() {
let originalField = this.getField();
if (originalField) {
let customTemplate = this.field.form.customFieldTemplates[originalField.type];
if (customTemplate && this.hasController(originalField.type)) {
let factory = this.getComponentFactorySync(originalField.type, customTemplate);
this.componentRef = this.container.createComponent(factory);
let instance: any = this.componentRef.instance;
if (instance) {
instance.field = originalField;
}
} else {
let componentType = this.formRenderingService.resolveComponentType(originalField);
if (componentType) {
let factory = this.componentFactoryResolver.resolveComponentFactory(componentType);
this.componentRef = this.container.createComponent(factory);
let instance = <WidgetComponent> this.componentRef.instance;
instance.field = this.field;
instance.fieldChanged.subscribe(field => {
if (field && this.field.form) {
this.visibilityService.refreshVisibility(this.field.form);
}
});
}
}
}
}
ngOnDestroy() {
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
}
private getField(): FormFieldModel {
if (this.field && this.field.params) {
const wrappedField = this.field.params.field;
if (wrappedField && wrappedField.type) {
return wrappedField;
}
}
return this.field;
}
private hasController(type: string): boolean {
return (adf && adf.components && adf.components[type]);
}
private getComponentFactorySync(type: string, template: string): ComponentFactory<any> {
let componentInfo = adf.components[type];
if (componentInfo.factory) {
return componentInfo.factory;
}
let metadata = {
selector: `runtime-component-${type}`,
template: template
};
let factory = this.createComponentFactorySync(this.compiler, metadata, componentInfo.class);
componentInfo.factory = factory;
return factory;
}
private createComponentFactorySync(compiler: Compiler, metadata: Component, componentClass: any): ComponentFactory<any> {
const cmpClass = componentClass || class RuntimeComponent {
};
const decoratedCmp = Component(metadata)(cmpClass);
@NgModule({ imports: [], declarations: [decoratedCmp] })
class RuntimeComponentModule {
}
let module: ModuleWithComponentFactories<any> = compiler.compileModuleAndAllComponentsSync(RuntimeComponentModule);
return module.componentFactories.find(x => x.componentType === decoratedCmp);
}
focusToggle() {
this.focus = !this.focus;
}
}

View File

@@ -0,0 +1,8 @@
<adf-datatable *ngIf="!isEmpty()"
[rows]="forms">
<data-columns>
<data-column key="name" type="text" title="Name" class="ellipsis-cell" [sortable]="true"></data-column>
<data-column key="lastUpdatedByFullName" type="text" title="User" class="ellipsis-cell" [sortable]="true"></data-column>
<data-column key="lastUpdated" type="date" format="shortDate" title="Date"></data-column>
</data-columns>
</adf-datatable>

View File

@@ -0,0 +1,82 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslationService } from '../../index';
import { DataTableModule } from '../../datatable';
import { DataColumnModule } from '../../data-column';
import { Observable } from 'rxjs/Rx';
import { EcmModelService } from '../services/ecm-model.service';
import { FormService } from '../services/form.service';
import { FormListComponent } from './form-list.component';
import { MaterialModule } from '../../material.module';
describe('TaskAttachmentList', () => {
let component: FormListComponent;
let fixture: ComponentFixture<FormListComponent>;
let service: FormService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DataColumnModule,
DataTableModule,
MaterialModule
],
declarations: [
FormListComponent
],
providers: [
FormService,
EcmModelService
]
}).compileComponents();
let translateService = TestBed.get(TranslationService);
spyOn(translateService, 'addTranslationFolder').and.stub();
spyOn(translateService, 'get').and.callFake((key) => {
return Observable.of(key);
});
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(FormListComponent);
component = fixture.componentInstance;
service = TestBed.get(FormService);
}));
it('should show the forms as a list', async(() => {
spyOn(service, 'getForms').and.returnValue(Observable.of([
{ name: 'FakeName-1', lastUpdatedByFullName: 'FakeUser-1', lastUpdated: '2017-01-02' },
{ name: 'FakeName-2', lastUpdatedByFullName: 'FakeUser-2', lastUpdated: '2017-01-03' }
]));
component.ngOnChanges({});
fixture.whenStable()
.then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('adf-datatable tbody tr')).length).toBe(2);
});
}));
});

View File

@@ -0,0 +1,49 @@
/*!
* @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 { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { FormService } from './../services/form.service';
@Component({
selector: 'adf-form-list',
templateUrl: './form-list.component.html',
styleUrls: ['./form-list.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class FormListComponent implements OnChanges {
@Input()
forms: any [] = [];
constructor(protected formService: FormService) {
}
ngOnChanges(changes: SimpleChanges) {
this.getForms();
}
isEmpty(): boolean {
return this.forms && this.forms.length === 0;
}
getForms() {
this.formService.getForms().subscribe((forms) => {
this.forms.push(...forms);
});
}
}

View File

@@ -0,0 +1,57 @@
<div *ngIf="!hasForm()">
<ng-content select="[empty-form]">
</ng-content>
</div>
<div *ngIf="hasForm()" class="{{form.className}}">
<mat-card>
<mat-card-header>
<mat-card-title>
<h4 *ngIf="isTitleEnabled()">
<div *ngIf="showRefreshButton" class="adf-form-reload-button">
<button mat-icon-button (click)="onRefreshClicked()">
<mat-icon>refresh</mat-icon>
</button>
</div>
<mat-icon>{{ form.isValid ? 'event_available' : 'event_busy' }}</mat-icon>
<span>{{form.taskName}}</span>
</h4>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngIf="form.hasTabs()">
<tabs-widget [tabs]="form.tabs" (formTabChanged)="checkVisibility($event);"></tabs-widget>
</div>
<div *ngIf="!form.hasTabs() && form.hasFields()">
<div *ngFor="let field of form.fields">
<form-field [field]="field.field"></form-field>
</div>
</div>
</mat-card-content>
<mat-card-actions *ngIf="form.hasOutcomes()">
<!--[class.mdl-button--colored]="!outcome.isSystem"-->
<button *ngFor="let outcome of form.outcomes"
mat-button
[disabled]="!isOutcomeButtonEnabled(outcome)"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"
(click)="onOutcomeClicked(outcome, $event)">
{{outcome.name | uppercase}}
</button>
</mat-card-actions>
</mat-card>
</div>
<!--
For debugging and data visualisation purposes,
will be removed during future revisions
-->
<div *ngIf="showDebugButton" class="adf-form-debug-container">
<mat-slide-toggle [(ngModel)]="debugMode">Debug mode</mat-slide-toggle>
<div *ngIf="debugMode && hasForm()">
<h4>Values</h4>
<pre>{{form.values | json}}</pre>
<h4>Form</h4>
<pre>{{form.json | json}}</pre>
</div>
</div>

View File

@@ -0,0 +1,44 @@
.adf {
&-form-container {
width: 100%;
min-height: 100px;
overflow: visible;
}
&-form-debug-container {
padding: 10px;
}
&-form-debug-container .debug-toggle-text {
padding-left: 15px;
cursor: pointer;
}
&-form-debug-container .debug-toggle-text:hover {
font-weight: bold;
}
&-form-reload-button {
position: absolute;
right: 0;
top: 0;
}
&-form-hide-button {
display: none;
}
&-task-title {
text-align: center
}
}
form-field {
width: 100%;
.mat-input-element {
font-size: 14px;
padding-top: 8px;
line-height: normal;
}
}

View File

@@ -0,0 +1,828 @@
/*!
* @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 { SimpleChange } from '@angular/core';
import { LogService } from '../../services';
import { Observable } from 'rxjs/Rx';
import { fakeForm } from '../../mock';
import { FormService } from './../services/form.service';
import { NodeService } from './../services/node.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { FormComponent } from './form.component';
import { FormFieldModel, FormFieldTypes, FormModel, FormOutcomeEvent, FormOutcomeModel } from './widgets/index';
describe('FormComponent', () => {
let formService: FormService;
let formComponent: FormComponent;
let visibilityService: WidgetVisibilityService;
let nodeService: NodeService;
let logService: LogService;
beforeEach(() => {
logService = new LogService(null);
visibilityService = new WidgetVisibilityService(null, logService);
spyOn(visibilityService, 'refreshVisibility').and.stub();
formService = new FormService(null, null, logService);
nodeService = new NodeService(null);
formComponent = new FormComponent(formService, visibilityService, null, nodeService);
});
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.isOutcomeButtonVisible(null, false)).toBeFalsy();
});
it('should enable custom outcome buttons', () => {
let formModel = new FormModel();
formComponent.form = formModel;
let outcome = new FormOutcomeModel(formModel, { id: 'action1', name: 'Action 1' });
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
});
it('should allow controlling [complete] button visibility', () => {
let formModel = new FormModel();
formComponent.form = formModel;
let outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION });
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
formComponent.showSaveButton = false;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeFalsy();
});
it('should show only [complete] button with readOnly form ', () => {
let formModel = new FormModel();
formModel.readOnly = true;
formComponent.form = formModel;
let outcome = new FormOutcomeModel(formModel, { id: '$complete', name: FormOutcomeModel.COMPLETE_ACTION });
formComponent.showCompleteButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
});
it('should not show [save] button with readOnly form ', () => {
let formModel = new FormModel();
formModel.readOnly = true;
formComponent.form = formModel;
let outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION });
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeFalsy();
});
it('should show [custom-outcome] button with readOnly form and selected custom-outcome', () => {
let formModel = new FormModel({ selectedOutcome: 'custom-outcome' });
formModel.readOnly = true;
formComponent.form = formModel;
let outcome = new FormOutcomeModel(formModel, { id: '$customoutome', name: 'custom-outcome' });
formComponent.showCompleteButton = true;
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
outcome = new FormOutcomeModel(formModel, { id: '$customoutome2', name: 'custom-outcome2' });
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeFalsy();
});
it('should allow controlling [save] button visibility', () => {
let formModel = new FormModel();
formModel.readOnly = false;
formComponent.form = formModel;
let outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.COMPLETE_ACTION });
formComponent.showCompleteButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
formComponent.showCompleteButton = false;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeFalsy();
});
it('should load form on refresh', () => {
spyOn(formComponent, 'loadForm').and.stub();
formComponent.onRefreshClicked();
expect(formComponent.loadForm).toHaveBeenCalled();
});
it('should get 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 process variable if is a process task', () => {
spyOn(formService, 'getTaskForm').and.callFake((currentTaskId) => {
return Observable.create(observer => {
observer.next({ taskId: currentTaskId });
observer.complete();
});
});
spyOn(visibilityService, 'getTaskProcessVariable').and.returnValue(Observable.of({}));
spyOn(formService, 'getTask').and.callFake((currentTaskId) => {
return Observable.create(observer => {
observer.next({ taskId: currentTaskId, processDefinitionId: '10201' });
observer.complete();
});
});
const taskId = '123';
formComponent.taskId = taskId;
formComponent.loadForm();
expect(visibilityService.getTaskProcessVariable).toHaveBeenCalledWith(taskId);
});
it('should not get process variable if is not a process task', () => {
spyOn(formService, 'getTaskForm').and.callFake((currentTaskId) => {
return Observable.create(observer => {
observer.next({ taskId: currentTaskId });
observer.complete();
});
});
spyOn(visibilityService, 'getTaskProcessVariable').and.returnValue(Observable.of({}));
spyOn(formService, 'getTask').and.callFake((currentTaskId) => {
return Observable.create(observer => {
observer.next({ taskId: currentTaskId, processDefinitionId: 'null' });
observer.complete();
});
});
const taskId = '123';
formComponent.taskId = taskId;
formComponent.loadForm();
expect(visibilityService.getTaskProcessVariable).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 = '<form>';
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 = '<task id>';
let change = new SimpleChange(null, taskId, true);
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, true);
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 = '<form>';
let change = new SimpleChange(null, formName, true);
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', true) });
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: FormComponent.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: FormComponent.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: FormComponent.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', (done) => {
spyOn(formService, 'getTask').and.returnValue(Observable.of({}));
spyOn(formService, 'getTaskForm').and.callFake((currentTaskId) => {
return Observable.create(observer => {
observer.next({ taskId: currentTaskId });
observer.complete();
});
});
const taskId = '456';
formComponent.formLoaded.subscribe(() => {
expect(formService.getTaskForm).toHaveBeenCalledWith(taskId);
expect(formComponent.form).toBeDefined();
expect(formComponent.form.taskId).toBe(taskId);
done();
});
expect(formComponent.form).toBeUndefined();
formComponent.getFormByTaskId(taskId);
});
it('should handle error when getting form by task id', (done) => {
const error = 'Some error';
spyOn(formService, 'getTask').and.returnValue(Observable.of({}));
spyOn(formComponent, 'handleError').and.stub();
spyOn(formService, 'getTaskForm').and.callFake((taskId) => {
return Observable.throw(error);
});
formComponent.getFormByTaskId('123').then(_ => {
expect(formComponent.handleError).toHaveBeenCalledWith(error);
done();
});
});
it('should apply readonly state when getting form by task id', (done) => {
spyOn(formService, 'getTask').and.returnValue(Observable.of({}));
spyOn(formService, 'getTaskForm').and.callFake((taskId) => {
return Observable.create(observer => {
observer.next({ taskId: taskId });
observer.complete();
});
});
formComponent.readOnly = true;
formComponent.getFormByTaskId('123').then(_ => {
expect(formComponent.form).toBeDefined();
expect(formComponent.form.readOnly).toBe(true);
done();
});
});
it('should fetch and parse form definition by id', () => {
spyOn(formService, 'getFormDefinitionById').and.callFake((currentFormId) => {
return Observable.create(observer => {
observer.next({ id: currentFormId });
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((currentFormName) => {
return Observable.create(observer => {
observer.next(currentFormName);
observer.complete();
});
});
spyOn(formService, 'getFormDefinitionById').and.callFake((currentFormName) => {
return Observable.create(observer => {
observer.next({ name: currentFormName });
observer.complete();
});
});
const formName = '<form>';
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 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: '<id>',
fields: [
{ id: 'field1', type: FormFieldTypes.CONTAINER }
]
});
expect(form).toBeDefined();
expect(form.id).toBe('<id>');
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: '<id>' });
expect(formComponent.getFormDefinitionOutcomes).toHaveBeenCalledWith(form);
});
it('should prevent default outcome execution', () => {
let outcome = new FormOutcomeModel(new FormModel(), {
id: FormComponent.CUSTOM_OUTCOME_ID,
name: 'Custom'
});
formComponent.form = new FormModel();
formComponent.executeOutcome.subscribe((event: FormOutcomeEvent) => {
expect(event.outcome).toBe(outcome);
event.preventDefault();
expect(event.defaultPrevented).toBeTruthy();
});
let result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeFalsy();
});
it('should not prevent default outcome execution', () => {
let outcome = new FormOutcomeModel(new FormModel(), {
id: FormComponent.CUSTOM_OUTCOME_ID,
name: 'Custom'
});
formComponent.form = new FormModel();
formComponent.executeOutcome.subscribe((event: FormOutcomeEvent) => {
expect(event.outcome).toBe(outcome);
expect(event.defaultPrevented).toBeFalsy();
});
spyOn(formComponent, 'completeTaskForm').and.callThrough();
let result = formComponent.onOutcomeClicked(outcome);
expect(result).toBeTruthy();
expect(formComponent.completeTaskForm).toHaveBeenCalledWith(outcome.name);
});
it('should check visibility only if field with form provided', () => {
formComponent.checkVisibility(null);
expect(visibilityService.refreshVisibility).not.toHaveBeenCalled();
let field = new FormFieldModel(null);
formComponent.checkVisibility(field);
expect(visibilityService.refreshVisibility).not.toHaveBeenCalled();
field = new FormFieldModel(new FormModel());
formComponent.checkVisibility(field);
expect(visibilityService.refreshVisibility).toHaveBeenCalledWith(field.form);
});
it('should load form for ecm node', () => {
let metadata = {};
spyOn(nodeService, 'getNodeMetadata').and.returnValue(
Observable.create(observer => {
observer.next({ metadata: metadata });
observer.complete();
})
);
spyOn(formComponent, 'loadFormFromActiviti').and.stub();
const nodeId = '<id>';
let change = new SimpleChange(null, nodeId, false);
formComponent.ngOnChanges({'nodeId' : change});
expect(nodeService.getNodeMetadata).toHaveBeenCalledWith(nodeId);
expect(formComponent.loadFormFromActiviti).toHaveBeenCalled();
expect(formComponent.data).toBe(metadata);
});
it('should disable outcome buttons for readonly form', () => {
let formModel = new FormModel();
formModel.readOnly = true;
formComponent.form = formModel;
let outcome = new FormOutcomeModel(new FormModel(), {
id: FormComponent.CUSTOM_OUTCOME_ID,
name: 'Custom'
});
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeFalsy();
});
it('should require outcome to eval button state', () => {
formComponent.form = new FormModel();
expect(formComponent.isOutcomeButtonEnabled(null)).toBeFalsy();
});
it('should always enable save outcome for writeable form', () => {
let formModel = new FormModel();
let field = new FormFieldModel(formModel, {
type: 'text',
value: null,
required: true
});
formComponent.form = formModel;
formModel.onFormFieldChanged(field);
expect(formModel.isValid).toBeFalsy();
let outcome = new FormOutcomeModel(new FormModel(), {
id: FormComponent.SAVE_OUTCOME_ID,
name: FormOutcomeModel.SAVE_ACTION
});
formComponent.readOnly = true;
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeTruthy();
});
it('should disable oucome buttons for invalid form', () => {
let formModel = new FormModel();
let field = new FormFieldModel(formModel, {
type: 'text',
value: null,
required: true
});
formComponent.form = formModel;
formModel.onFormFieldChanged(field);
expect(formModel.isValid).toBeFalsy();
let outcome = new FormOutcomeModel(new FormModel(), {
id: FormComponent.CUSTOM_OUTCOME_ID,
name: 'Custom'
});
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeFalsy();
});
it('should disable complete outcome button when disableCompleteButton is true', () => {
let formModel = new FormModel();
formComponent.form = formModel;
formComponent.disableCompleteButton = true;
expect(formModel.isValid).toBeTruthy();
let completeOutcome = formComponent.form.outcomes.find(outcome => outcome.name === FormOutcomeModel.COMPLETE_ACTION);
expect(formComponent.isOutcomeButtonEnabled(completeOutcome)).toBeFalsy();
});
it('should disable start process outcome button when disableStartProcessButton is true', () => {
let formModel = new FormModel();
formComponent.form = formModel;
formComponent.disableStartProcessButton = true;
expect(formModel.isValid).toBeTruthy();
let startProcessOutcome = formComponent.form.outcomes.find(outcome => outcome.name === FormOutcomeModel.START_PROCESS_ACTION);
expect(formComponent.isOutcomeButtonEnabled(startProcessOutcome)).toBeFalsy();
});
it('should raise [executeOutcome] event for formService', (done) => {
formService.executeOutcome.subscribe(() => {
done();
});
let outcome = new FormOutcomeModel(new FormModel(), {
id: FormComponent.CUSTOM_OUTCOME_ID,
name: 'Custom'
});
formComponent.form = new FormModel();
formComponent.onOutcomeClicked(outcome);
});
it('should refresh form values when data is changed', () => {
formComponent.form = new FormModel(fakeForm);
let formFields = formComponent.form.getFormFields();
let labelField = formFields.find(field => field.id === 'label');
let radioField = formFields.find(field => field.id === 'raduio');
expect(labelField.value).toBe('empty');
expect(radioField.value).toBeNull();
let formValues: any = {};
formValues.label = {
id: 'option_1',
name: 'test1'
};
formValues.raduio = { id: 'option_1', name: 'Option 1' };
let change = new SimpleChange(null, formValues, false);
formComponent.data = formValues;
formComponent.ngOnChanges({ 'data': change });
formFields = formComponent.form.getFormFields();
labelField = formFields.find(field => field.id === 'label');
radioField = formFields.find(field => field.id === 'raduio');
expect(labelField.value).toBe('option_1');
expect(radioField.value).toBe('option_1');
});
});

View File

@@ -0,0 +1,519 @@
/*!
* @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.
*/
/* tslint:disable */
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { FormErrorEvent, FormEvent } from './../events/index';
import { EcmModelService } from './../services/ecm-model.service';
import { FormService } from './../services/form.service';
import { NodeService } from './../services/node.service';
import { ContentLinkModel } from './widgets/core/content-link.model';
import { FormFieldModel, FormModel, FormOutcomeEvent, FormOutcomeModel, FormValues, FormFieldValidator } from './widgets/core/index';
import { Observable } from 'rxjs/Rx';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
@Component({
selector: 'adf-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class FormComponent implements OnInit, OnChanges {
static SAVE_OUTCOME_ID: string = '$save';
static COMPLETE_OUTCOME_ID: string = '$complete';
static START_PROCESS_OUTCOME_ID: string = '$startProcess';
static CUSTOM_OUTCOME_ID: string = '$custom';
@Input()
form: FormModel;
@Input()
taskId: string;
@Input()
nodeId: string;
@Input()
formId: string;
@Input()
formName: string;
@Input()
saveMetadata: boolean = false;
@Input()
data: FormValues;
@Input()
path: string;
@Input()
nameNode: string;
@Input()
showTitle: boolean = true;
@Input()
showCompleteButton: boolean = true;
@Input()
disableCompleteButton: boolean = false;
@Input()
disableStartProcessButton: boolean = false;
@Input()
showSaveButton: boolean = true;
@Input()
showDebugButton: boolean = false;
@Input()
readOnly: boolean = false;
@Input()
showRefreshButton: boolean = true;
@Input()
showValidationIcon: boolean = true;
@Input()
fieldValidators: FormFieldValidator[] = [];
@Output()
formSaved: EventEmitter<FormModel> = new EventEmitter<FormModel>();
@Output()
formCompleted: EventEmitter<FormModel> = new EventEmitter<FormModel>();
@Output()
formContentClicked: EventEmitter<ContentLinkModel> = new EventEmitter<ContentLinkModel>();
@Output()
formLoaded: EventEmitter<FormModel> = new EventEmitter<FormModel>();
@Output()
formDataRefreshed: EventEmitter<FormModel> = new EventEmitter<FormModel>();
@Output()
executeOutcome: EventEmitter<FormOutcomeEvent> = new EventEmitter<FormOutcomeEvent>();
@Output()
onError: EventEmitter<any> = new EventEmitter<any>();
debugMode: boolean = false;
constructor(protected formService: FormService,
protected visibilityService: WidgetVisibilityService,
private ecmModelService: EcmModelService,
private nodeService: NodeService) {
}
hasForm(): boolean {
return this.form ? true : false;
}
isTitleEnabled(): boolean {
if (this.showTitle) {
if (this.form && this.form.taskName) {
return true;
}
}
return false;
}
isOutcomeButtonEnabled(outcome: FormOutcomeModel): boolean {
if (this.form.readOnly) {
return false;
}
if (outcome) {
// Make 'Save' button always available
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return true;
}
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return this.disableCompleteButton ? false : this.form.isValid;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return this.disableStartProcessButton ? false : this.form.isValid;
}
return this.form.isValid;
}
return false;
}
isOutcomeButtonVisible(outcome: FormOutcomeModel, isFormReadOnly: boolean): boolean {
if (outcome && outcome.name) {
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return this.showCompleteButton;
}
if (isFormReadOnly) {
return outcome.isSelected;
}
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return this.showSaveButton;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return false;
}
return true;
}
return false;
}
ngOnInit() {
this.formService.formContentClicked.subscribe((content: ContentLinkModel) => {
this.formContentClicked.emit(content);
});
}
ngOnChanges(changes: SimpleChanges) {
let taskId = changes['taskId'];
if (taskId && taskId.currentValue) {
this.getFormByTaskId(taskId.currentValue);
return;
}
let formId = changes['formId'];
if (formId && formId.currentValue) {
this.getFormDefinitionByFormId(formId.currentValue);
return;
}
let formName = changes['formName'];
if (formName && formName.currentValue) {
this.getFormDefinitionByFormName(formName.currentValue);
return;
}
let nodeId = changes['nodeId'];
if (nodeId && nodeId.currentValue) {
this.loadFormForEcmNode(nodeId.currentValue);
return;
}
let data = changes['data'];
if (data && data.currentValue) {
this.refreshFormData();
return;
}
}
/**
* 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 (!this.onExecuteOutcome(outcome)) {
return false;
}
if (outcome.isSystem) {
if (outcome.id === FormComponent.SAVE_OUTCOME_ID) {
this.saveTaskForm();
return true;
}
if (outcome.id === FormComponent.COMPLETE_OUTCOME_ID) {
this.completeTaskForm();
return true;
}
if (outcome.id === FormComponent.START_PROCESS_OUTCOME_ID) {
this.completeTaskForm();
return true;
}
if (outcome.id === FormComponent.CUSTOM_OUTCOME_ID) {
this.onTaskSaved(this.form);
this.storeFormAsMetadata();
return true;
}
} else {
// Note: Activiti is using NAME field rather than ID for outcomes
if (outcome.name) {
this.onTaskSaved(this.form);
this.completeTaskForm(outcome.name);
return true;
}
}
}
return false;
}
/**
* Invoked when user clicks form refresh button.
*/
onRefreshClicked() {
this.loadForm();
}
loadForm() {
if (this.taskId) {
this.getFormByTaskId(this.taskId);
return;
}
if (this.formId) {
this.getFormDefinitionByFormId(this.formId);
return;
}
if (this.formName) {
this.getFormDefinitionByFormName(this.formName);
return;
}
}
findProcessVariablesByTaskId(taskId: string): Observable<any> {
return this.formService.getTask(taskId).
switchMap((task: any) => {
if (this.isAProcessTask(task)) {
return this.visibilityService.getTaskProcessVariable(taskId);
} else {
return Observable.of({});
}
});
}
isAProcessTask(taskRepresentation) {
return taskRepresentation.processDefinitionId && taskRepresentation.processDefinitionDeploymentId !== 'null';
}
getFormByTaskId(taskId: string): Promise<FormModel> {
return new Promise<FormModel>((resolve, reject) => {
this.findProcessVariablesByTaskId(taskId).subscribe( (processVariables) => {
this.formService
.getTaskForm(taskId)
.subscribe(
form => {
const parsedForm = this.parseForm(form);
this.visibilityService.refreshVisibility(parsedForm);
this.form = parsedForm;
this.onFormLoaded(this.form);
resolve(this.form);
},
error => {
this.handleError(error);
// reject(error);
resolve(null);
}
);
});
});
}
getFormDefinitionByFormId(formId: string) {
this.formService
.getFormDefinitionById(formId)
.subscribe(
form => {
this.formName = form.name;
this.form = this.parseForm(form);
this.onFormLoaded(this.form);
},
(error) => {
this.handleError(error);
}
);
}
getFormDefinitionByFormName(formName: string) {
this.formService
.getFormDefinitionByName(formName)
.subscribe(
id => {
this.formService.getFormDefinitionById(id).subscribe(
form => {
this.form = this.parseForm(form);
this.onFormLoaded(this.form);
},
(error) => {
this.handleError(error);
}
);
},
(error) => {
this.handleError(error);
}
);
}
saveTaskForm() {
if (this.form && this.form.taskId) {
this.formService
.saveTaskForm(this.form.taskId, this.form.values)
.subscribe(
() => {
this.onTaskSaved(this.form);
this.storeFormAsMetadata();
},
error => this.onTaskSavedError(this.form, error)
);
}
}
completeTaskForm(outcome?: string) {
if (this.form && this.form.taskId) {
this.formService
.completeTaskForm(this.form.taskId, this.form.values, outcome)
.subscribe(
() => {
this.onTaskCompleted(this.form);
this.storeFormAsMetadata();
},
error => this.onTaskCompletedError(this.form, error)
);
}
}
handleError(err: any): any {
this.onError.emit(err);
}
parseForm(json: any): FormModel {
if (json) {
let form = new FormModel(json, this.data, this.readOnly, this.formService);
if (!json.fields) {
form.outcomes = this.getFormDefinitionOutcomes(form);
}
if (this.fieldValidators && this.fieldValidators.length > 0) {
form.fieldValidators = this.fieldValidators;
}
return form;
}
return null;
}
/**
* Get custom set of outcomes for a Form Definition.
* @param form Form definition model.
* @returns {FormOutcomeModel[]} Outcomes for a given form definition.
*/
getFormDefinitionOutcomes(form: FormModel): FormOutcomeModel[] {
return [
new FormOutcomeModel(form, { id: '$custom', name: FormOutcomeModel.SAVE_ACTION, isSystem: true })
];
}
checkVisibility(field: FormFieldModel) {
if (field && field.form) {
this.visibilityService.refreshVisibility(field.form);
}
}
private refreshFormData() {
this.form = this.parseForm(this.form.json);
this.onFormLoaded(this.form);
this.onFormDataRefreshed(this.form);
}
private loadFormForEcmNode(nodeId: string): void {
this.nodeService.getNodeMetadata(nodeId).subscribe(data => {
this.data = data.metadata;
this.loadFormFromActiviti(data.nodeType);
},
this.handleError);
}
loadFormFromActiviti(nodeType: string): any {
this.formService.searchFrom(nodeType).subscribe(
form => {
if (!form) {
this.formService.createFormFromANode(nodeType).subscribe(formMetadata => {
this.loadFormFromFormId(formMetadata.id);
});
} else {
this.loadFormFromFormId(form.id);
}
},
(error) => {
this.handleError(error);
}
);
}
private loadFormFromFormId(formId: string) {
this.formId = formId;
this.loadForm();
}
private storeFormAsMetadata() {
if (this.saveMetadata) {
this.ecmModelService.createEcmTypeForActivitiForm(this.formName, this.form).subscribe(type => {
this.nodeService.createNodeMetadata(type.nodeType || type.entry.prefixedName, EcmModelService.MODEL_NAMESPACE, this.form.values, this.path, this.nameNode);
},
(error) => {
this.handleError(error);
}
);
}
}
protected onFormLoaded(form: FormModel) {
this.formLoaded.emit(form);
this.formService.formLoaded.next(new FormEvent(form));
}
protected onFormDataRefreshed(form: FormModel) {
this.formDataRefreshed.emit(form);
this.formService.formDataRefreshed.next(new FormEvent(form));
}
protected onTaskSaved(form: FormModel) {
this.formSaved.emit(form);
this.formService.taskSaved.next(new FormEvent(form));
}
protected onTaskSavedError(form: FormModel, error: any) {
this.handleError(error);
this.formService.taskSavedError.next(new FormErrorEvent(form, error));
}
protected onTaskCompleted(form: FormModel) {
this.formCompleted.emit(form);
this.formService.taskCompleted.next(new FormEvent(form));
}
protected onTaskCompletedError(form: FormModel, error: any) {
this.handleError(error);
this.formService.taskCompletedError.next(new FormErrorEvent(form, error));
}
protected onExecuteOutcome(outcome: FormOutcomeModel): boolean {
let args = new FormOutcomeEvent(outcome);
this.formService.executeOutcome.next(args);
if (args.defaultPrevented) {
return false;
}
this.executeOutcome.emit(args);
if (args.defaultPrevented) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,203 @@
/*!
* @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 { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { DebugElement, SimpleChange } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { FormModule } from '../../index';
import { formDefinitionDropdownField, formDefinitionTwoTextFields } from '../../mock';
import { formReadonlyTwoTextFields } from '../../mock';
import { formDefVisibilitiFieldDependsOnNextOne, formDefVisibilitiFieldDependsOnPreviousOne } from '../../mock';
import { FormService } from './../services/form.service';
import { FormComponent } from './form.component';
/** Duration of the select opening animation. */
const SELECT_OPEN_ANIMATION = 200;
/** Duration of the select closing animation and the timeout interval for the backdrop. */
const SELECT_CLOSE_ANIMATION = 500;
describe('FormComponent UI and visibiltiy', () => {
let debugElement: DebugElement;
let element: HTMLElement;
let component: FormComponent;
let service: FormService;
let fixture: ComponentFixture<FormComponent>;
let formDefinitionSpy: jasmine.Spy;
let taskSpy: jasmine.Spy;
function openSelect() {
let trigger: HTMLElement;
trigger = fixture.debugElement.query(By.css('[class="mat-select-trigger"]')).nativeElement;
trigger.click();
fixture.detectChanges();
}
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FormComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
debugElement = fixture.debugElement;
service = fixture.debugElement.injector.get(FormService);
});
it('should create instance of FormComponent', () => {
expect(fixture.componentInstance instanceof FormComponent).toBe(true, 'should create FormComponent');
});
describe('form definition', () => {
it('should display two text fields form definition', () => {
taskSpy = spyOn(service, 'getTask').and.returnValue(Observable.of({}));
formDefinitionSpy = spyOn(service, 'getTaskForm').and.returnValue(Observable.of(formDefinitionTwoTextFields));
let change = new SimpleChange(null, 1, true);
component.ngOnChanges({ 'taskId': change });
fixture.detectChanges();
let firstNameEl = fixture.debugElement.query(By.css('#firstname'));
expect(firstNameEl).not.toBeNull();
expect(firstNameEl).toBeDefined();
let lastNameEl = fixture.debugElement.query(By.css('#lastname'));
expect(lastNameEl).not.toBeNull();
expect(lastNameEl).toBeDefined();
});
it('should display dropdown field', fakeAsync(() => {
taskSpy = spyOn(service, 'getTask').and.returnValue(Observable.of({}));
formDefinitionSpy = spyOn(service, 'getTaskForm').and.returnValue(Observable.of(formDefinitionDropdownField));
let change = new SimpleChange(null, 1, true);
component.ngOnChanges({ 'taskId': change });
fixture.detectChanges();
openSelect();
tick(SELECT_OPEN_ANIMATION);
const dropdown = fixture.debugElement.queryAll(By.css('#country'));
expect(dropdown).toBeDefined();
expect(dropdown).not.toBeNull();
const optOne = fixture.debugElement.queryAll(By.css('[id="mat-option-1"]'));
const optTwo = fixture.debugElement.queryAll(By.css('[id="mat-option-2"]'));
const optThree = fixture.debugElement.queryAll(By.css('[id="mat-option-3"]'));
expect(optOne[0].nativeElement.innerText).toEqual('united kingdom');
expect(optTwo[0].nativeElement.innerText).toEqual('italy');
expect(optThree[0].nativeElement.innerText).toEqual('france');
optTwo[0].nativeElement.click();
fixture.detectChanges();
expect(dropdown[0].nativeElement.innerText.trim()).toEqual('italy');
tick(SELECT_CLOSE_ANIMATION);
}));
describe('Visibility conditions', () => {
it('should hide the field based on the next one', () => {
taskSpy = spyOn(service, 'getTask').and.returnValue(Observable.of({}));
formDefinitionSpy = spyOn(service, 'getTaskForm').and.returnValue(Observable.of(formDefVisibilitiFieldDependsOnNextOne));
let change = new SimpleChange(null, 1, true);
component.ngOnChanges({ 'taskId': change });
fixture.detectChanges();
let firstEl = fixture.debugElement.query(By.css('#country'));
expect(firstEl).toBeNull();
let secondEl = fixture.debugElement.query(By.css('#name'));
expect(secondEl).not.toBeNull();
expect(secondEl).toBeDefined();
});
it('should hide the field based on the previous one', () => {
taskSpy = spyOn(service, 'getTask').and.returnValue(Observable.of({}));
formDefinitionSpy = spyOn(service, 'getTaskForm').and.returnValue(Observable.of(formDefVisibilitiFieldDependsOnPreviousOne));
let change = new SimpleChange(null, 1, true);
component.ngOnChanges({ 'taskId': change });
fixture.detectChanges();
let firstEl = fixture.debugElement.query(By.css('#name'));
expect(firstEl).not.toBeNull();
expect(firstEl).toBeDefined();
let secondEl = fixture.debugElement.query(By.css('#country'));
expect(secondEl).toBeNull();
});
it('should show the hidden field when the visibility condition change to true', () => {
taskSpy = spyOn(service, 'getTask').and.returnValue(Observable.of({}));
formDefinitionSpy = spyOn(service, 'getTaskForm').and.returnValue(Observable.of(formDefVisibilitiFieldDependsOnNextOne));
let change = new SimpleChange(null, 1, true);
component.ngOnChanges({ 'taskId': change });
fixture.detectChanges();
let firstEl = fixture.debugElement.query(By.css('#country'));
expect(firstEl).toBeNull();
const secondEl = fixture.debugElement.query(By.css('#name'));
expect(secondEl).not.toBeNull();
let el = secondEl.nativeElement;
el.value = 'italy';
el.dispatchEvent(new Event('input'));
fixture.detectChanges();
firstEl = fixture.debugElement.query(By.css('#country'));
expect(firstEl).not.toBeNull();
});
});
describe('Readonly Form', () => {
it('should display two text fields readonly', () => {
taskSpy = spyOn(service, 'getTask').and.returnValue(Observable.of({}));
formDefinitionSpy = spyOn(service, 'getTaskForm').and.returnValue(Observable.of(formReadonlyTwoTextFields));
let change = new SimpleChange(null, 1, true);
component.ngOnChanges({ 'taskId': change });
fixture.detectChanges();
let firstNameEl = fixture.debugElement.query(By.css('#firstname'));
expect(firstNameEl).not.toBeNull();
expect(firstNameEl).toBeDefined();
expect(firstNameEl.nativeElement.value).toEqual('fakeFirstName');
let lastNameEl = fixture.debugElement.query(By.css('#lastname'));
expect(lastNameEl).not.toBeNull();
expect(lastNameEl).toBeDefined();
expect(lastNameEl.nativeElement.value).toEqual('fakeLastName');
});
});
});
});

View File

@@ -0,0 +1,37 @@
<div class="adf-start-form-container" *ngIf="hasForm()">
<mat-card>
<mat-card-header>
<mat-card-title>
<h2 *ngIf="isTitleEnabled()" class="mdl-card__title-text">{{form.taskName}}</h2>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngIf="form.hasTabs()">
<tabs-widget [tabs]="form.tabs" (formTabChanged)="checkVisibility($event);"></tabs-widget>
</div>
<div *ngIf="!form.hasTabs() && form.hasFields()">
<div *ngFor="let field of form.fields">
<form-field [field]="field.field"></form-field>
</div>
</div>
</mat-card-content>
<mat-card-content class="adf-start-form-actions" *ngIf="showOutcomeButtons && form.hasOutcomes()" #outcomesContainer>
<ng-content select="[form-custom-button]"></ng-content>
<button *ngFor="let outcome of form.outcomes"
mat-button
[disabled]="!isOutcomeButtonEnabled(outcome)"
[class.mdl-button--colored]="!outcome.isSystem"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"
(click)="onOutcomeClicked(outcome, $event)">
{{outcome.name}}
</button>
</mat-card-content>
<mat-card-actions *ngIf="showRefreshButton">
<button mat-button
(click)="onRefreshClicked()">
<mat-icon>refresh</mat-icon>
</button>
</mat-card-actions>
</mat-card>
</div>

View File

@@ -0,0 +1,452 @@
/*!
* @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 { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Rx';
import { EntryComponentMockModule } from '../../mock/form/entry-module.mock';
import { startFormDateWidgetMock, startFormDropdownDefinitionMock, startFormTextDefinitionMock, startMockForm, startMockFormWithTab } from '../../mock';
import { startFormAmountWidgetMock, startFormNumberWidgetMock, startFormRadioButtonWidgetMock } from '../../mock';
import { EcmModelService } from './../services/ecm-model.service';
import { FormRenderingService } from './../services/form-rendering.service';
import { FormService } from './../services/form.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { FormFieldComponent } from './form-field/form-field.component';
import { MaterialModule } from '../../material.module';
import { StartFormComponent } from './start-form.component';
import { ContentWidgetComponent } from './widgets/content/content.widget';
import { MASK_DIRECTIVE } from './widgets/index';
import { WIDGET_DIRECTIVES } from './widgets/index';
import { FormModel, FormOutcomeModel } from './widgets/index';
describe('ActivitiStartForm', () => {
let formService: FormService;
let component: StartFormComponent;
let element: HTMLElement;
let fixture: ComponentFixture<StartFormComponent>;
let getStartFormSpy: jasmine.Spy;
const exampleId1 = 'my:process1';
const exampleId2 = 'my:process2';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule,
EntryComponentMockModule
],
declarations: [
StartFormComponent,
FormFieldComponent,
ContentWidgetComponent,
...WIDGET_DIRECTIVES,
...MASK_DIRECTIVE
],
providers: [
EcmModelService,
FormService,
FormRenderingService,
WidgetVisibilityService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StartFormComponent);
component = fixture.componentInstance;
formService = fixture.debugElement.injector.get(FormService);
element = fixture.nativeElement;
getStartFormSpy = spyOn(formService, 'getStartFormDefinition').and.returnValue(Observable.of({
processDefinitionName: 'my:process'
}));
});
it('should load start form on change if processDefinitionId defined', () => {
component.processDefinitionId = exampleId1;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
expect(formService.getStartFormDefinition).toHaveBeenCalled();
});
it('should load start form when processDefinitionId changed', () => {
component.processDefinitionId = exampleId1;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
expect(formService.getStartFormDefinition).toHaveBeenCalled();
});
it('should not load start form when changes notified but no change to processDefinitionId', () => {
component.processDefinitionId = exampleId1;
component.ngOnChanges({ otherProp: new SimpleChange(exampleId1, exampleId2, true) });
expect(formService.getStartFormDefinition).not.toHaveBeenCalled();
});
it('should consume errors encountered when loading start form', () => {
getStartFormSpy.and.returnValue(Observable.throw({}));
component.processDefinitionId = exampleId1;
component.ngOnInit();
});
it('should show outcome buttons by default', () => {
getStartFormSpy.and.returnValue(Observable.of({
id: '1',
processDefinitionName: 'my:process',
outcomes: [{
id: 'approve',
name: 'Approve'
}]
}));
component.processDefinitionId = exampleId1;
component.ngOnInit();
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
expect(component.outcomesContainer).toBeTruthy();
});
it('should show outcome buttons if showOutcomeButtons is true', () => {
getStartFormSpy.and.returnValue(Observable.of({
id: '1',
processDefinitionName: 'my:process',
outcomes: [{
id: 'approve',
name: 'Approve'
}]
}));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
expect(component.outcomesContainer).toBeTruthy();
});
it('should fetch start form detasils by processDefinitionId ', () => {
getStartFormSpy.and.returnValue(Observable.of(startMockForm));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
expect(component.outcomesContainer).toBeTruthy();
expect(getStartFormSpy).toHaveBeenCalled();
});
describe('Disply widgets', () => {
it('should be able to display a textWidget from a process definition', () => {
getStartFormSpy.and.returnValue(Observable.of(startFormTextDefinitionMock));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const formFields = component.form.getFormFields();
const labelField = formFields.find(field => field.id === 'mocktext');
const textWidget = fixture.debugElement.nativeElement.querySelector('text-widget');
const textWidgetLabel = fixture.debugElement.nativeElement.querySelector('.adf-label');
expect(labelField.type).toBe('text');
expect(textWidget).toBeDefined();
expect(textWidgetLabel.innerText).toBe('mockText');
});
});
it('should be able to display a radioButtonWidget from a process definition', () => {
getStartFormSpy.and.returnValue(Observable.of(startFormRadioButtonWidgetMock));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const formFields = component.form.getFormFields();
const labelField = formFields.find(field => field.id === 'radio-but');
const radioButtonWidget = fixture.debugElement.nativeElement.querySelector('radio-buttons-widget');
const radioButtonWidgetLabel = fixture.debugElement.nativeElement.querySelector('.adf-input');
expect(labelField.type).toBe('radio-buttons');
expect(radioButtonWidget).toBeDefined();
expect(radioButtonWidgetLabel.innerText).toBe('radio-buttons');
});
});
it('should be able to display a amountWidget from a process definition', () => {
getStartFormSpy.and.returnValue(Observable.of(startFormAmountWidgetMock));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const formFields = component.form.getFormFields();
const labelField = formFields.find(field => field.id === 'amount');
const amountWidget = fixture.debugElement.nativeElement.querySelector('amount-widget');
const amountWidgetLabel = fixture.debugElement.nativeElement.querySelector('.adf-input');
expect(labelField.type).toBe('amount');
expect(amountWidget).toBeDefined();
expect(amountWidgetLabel.innerText).toBe('amount');
});
});
it('should be able to display a numberWidget from a process definition', () => {
getStartFormSpy.and.returnValue(Observable.of(startFormNumberWidgetMock));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const formFields = component.form.getFormFields();
const labelField = formFields.find(field => field.id === 'number');
const numberWidget = fixture.debugElement.nativeElement.querySelector('number-widget');
const numberWidgetLabel = fixture.debugElement.nativeElement.querySelector('.adf-input');
expect(labelField.type).toBe('text');
expect(numberWidget).toBeDefined();
expect(numberWidgetLabel.innerText).toBe('number');
});
});
it('should be able to display a dropDown Widget from a process definition', () => {
getStartFormSpy.and.returnValue(Observable.of(startFormDropdownDefinitionMock));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const formFields = component.form.getFormFields();
const labelField = formFields.find(field => field.id === 'mockTypeDropDown');
const dropDownWidget = fixture.debugElement.nativeElement.querySelector('dropdown-widget');
const selectElement = fixture.debugElement.nativeElement.querySelector('.adf-dropdown-widget>mat-select .mat-select-trigger');
selectElement.click();
expect(selectElement).toBeDefined();
expect(dropDownWidget).toBeDefined();
expect(selectElement.innerText).toBe('Choose one...');
expect(labelField.type).toBe('dropdown');
expect(labelField.options[0].name).toBe('Chooseone...');
expect(labelField.options[1].name).toBe('Option-1');
expect(labelField.options[2].name).toBe('Option-2');
});
});
it('should be able to display a date Widget from a process definition', () => {
getStartFormSpy.and.returnValue(Observable.of(startFormDateWidgetMock));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const formFields = component.form.getFormFields();
const labelField = formFields.find(field => field.id === 'date');
const dateWidget = fixture.debugElement.nativeElement.querySelector('dropdown-widget');
const dateLabelElement = fixture.debugElement.nativeElement.querySelector('#data-widget .mat-input-infix> .adf-label');
expect(dateWidget).toBeDefined();
expect(labelField.type).toBe('date');
expect(dateLabelElement.innerText).toBe('date (D-M-YYYY)');
});
});
it('should fetch and define form fields with proper type', () => {
getStartFormSpy.and.returnValue(Observable.of(startMockForm));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
const formFields = component.form.getFormFields();
const labelField = formFields.find(field => field.id === 'billdate');
expect(labelField.type).toBe('date');
const formFields1 = component.form.getFormFields();
const labelField1 = formFields1.find(field => field.id === 'claimtype');
expect(labelField1.type).toBe('dropdown');
});
it('should show dropdown options', () => {
getStartFormSpy.and.returnValue(Observable.of(startMockForm));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
fixture.whenStable().then(() => {
const formFields = component.form.getFormFields();
const labelField = formFields.find(field => field.id === 'claimtype');
expect(labelField.type).toBe('dropdown');
expect(labelField.options[0].name).toBe('Chooseone...');
expect(labelField.options[1].name).toBe('Cashless');
expect(labelField.options[2].name).toBe('Reimbursement');
});
});
it('should disply start form with fields ', async(() => {
getStartFormSpy.and.returnValue(Observable.of(startMockForm));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.whenStable().then(() => {
fixture.detectChanges();
const formFieldsWidget = fixture.debugElement.nativeElement.querySelector('form-field');
const inputElement = fixture.debugElement.nativeElement.querySelector('.adf-input');
const inputLabelElement = fixture.debugElement.nativeElement.querySelector('.mat-input-infix > .adf-label');
const dateElement = fixture.debugElement.nativeElement.querySelector('#billdate');
const dateLabelElement = fixture.debugElement.nativeElement.querySelector('#data-widget .mat-input-infix> .adf-label');
const selectElement = fixture.debugElement.nativeElement.querySelector('#claimtype');
const selectLabelElement = fixture.debugElement.nativeElement.querySelector('.adf-dropdown-widget > .adf-label');
expect(formFieldsWidget).toBeDefined();
expect(inputElement).toBeDefined();
expect(dateElement).toBeDefined();
expect(selectElement).toBeDefined();
expect(inputLabelElement.innerText).toBe('ClientName*');
expect(dateLabelElement.innerText).toBe('BillDate (D-M-YYYY)');
expect(selectLabelElement.innerText).toBe('ClaimType');
});
}));
it('should refresh start form on click of refresh button ', async(() => {
getStartFormSpy.and.returnValue(Observable.of(startMockForm));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.showRefreshButton = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.detectChanges();
fixture.whenStable().then(() => {
const refreshElement = fixture.debugElement.nativeElement.querySelector('.mat-card-actions>button');
refreshElement.click();
fixture.detectChanges();
const selectElement = fixture.debugElement.nativeElement.querySelector('#claimtype');
const selectLabelElement = fixture.debugElement.nativeElement.querySelector('.adf-dropdown-widget > .adf-label');
expect(refreshElement).toBeDefined();
expect(selectElement).toBeDefined();
expect(selectLabelElement.innerText).toBe('ClaimType');
});
}));
it('should difine custom-tabs ', async(() => {
getStartFormSpy.and.returnValue(Observable.of(startMockFormWithTab));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.showRefreshButton = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.whenStable().then(() => {
fixture.detectChanges();
const formTabs = component.form.tabs;
const tabField1 = formTabs.find(tab => tab.id === 'form1');
const tabField2 = formTabs.find(tab => tab.id === 'form2');
const tabsWidgetElement = fixture.debugElement.nativeElement.querySelector('tabs-widget');
expect(tabField1.name).toBe('Tab 1');
expect(tabField2.name).toBe('Tab 2');
expect(tabsWidgetElement).toBeDefined();
});
}));
it('should difine title and [custom-action-buttons]', async(() => {
getStartFormSpy.and.returnValue(Observable.of(startMockFormWithTab));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.showRefreshButton = true;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
fixture.whenStable().then(() => {
fixture.detectChanges();
const titleIcon = fixture.debugElement.nativeElement.querySelector('mat-card-title>mat-icon');
const titleElement = fixture.debugElement.nativeElement.querySelector('mat-card-title>h2');
const actionButtons = fixture.debugElement.nativeElement.querySelectorAll('.mat-button');
expect(titleIcon).toBeDefined();
expect(titleElement).toBeDefined();
expect(actionButtons.length).toBe(4);
expect(actionButtons[0].innerText).toBe('Save');
expect(actionButtons[0].disabled).toBeFalsy();
expect(actionButtons[1].innerText).toBe('Approve');
expect(actionButtons[1].disabled).toBeTruthy();
expect(actionButtons[2].innerText).toBe('Complete');
expect(actionButtons[2].disabled).toBeTruthy();
});
}));
});
describe('OutCome Actions', () => {
it('should not enable outcome button when model missing', () => {
expect(component.isOutcomeButtonVisible(null, false)).toBeFalsy();
});
it('should enable custom outcome buttons', () => {
const formModel = new FormModel();
component.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: 'action1', name: 'Action 1' });
expect(component.isOutcomeButtonVisible(outcome, component.form.readOnly)).toBeTruthy();
});
it('should allow controlling [complete] button visibility', () => {
const formModel = new FormModel();
component.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION });
component.showSaveButton = true;
expect(component.isOutcomeButtonVisible(outcome, component.form.readOnly)).toBeTruthy();
component.showSaveButton = false;
expect(component.isOutcomeButtonVisible(outcome, component.form.readOnly)).toBeFalsy();
});
it('should show only [complete] button with readOnly form ', () => {
const formModel = new FormModel();
formModel.readOnly = true;
component.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: '$complete', name: FormOutcomeModel.COMPLETE_ACTION });
component.showCompleteButton = true;
expect(component.isOutcomeButtonVisible(outcome, component.form.readOnly)).toBeTruthy();
});
it('should not show [save] button with readOnly form ', () => {
const formModel = new FormModel();
formModel.readOnly = true;
component.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION });
component.showSaveButton = true;
expect(component.isOutcomeButtonVisible(outcome, component.form.readOnly)).toBeFalsy();
});
it('should show [custom-outcome] button with readOnly form and selected custom-outcome', () => {
const formModel = new FormModel({ selectedOutcome: 'custom-outcome' });
formModel.readOnly = true;
component.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: '$customoutome', name: 'custom-outcome' });
component.showCompleteButton = true;
component.showSaveButton = true;
expect(component.isOutcomeButtonVisible(outcome, component.form.readOnly)).toBeTruthy();
const outcome1 = new FormOutcomeModel(formModel, { id: '$customoutome2', name: 'custom-outcome2' });
expect(component.isOutcomeButtonVisible(outcome1, component.form.readOnly)).toBeFalsy();
});
it('should allow controlling [save] button visibility', () => {
const formModel = new FormModel();
formModel.readOnly = false;
component.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.COMPLETE_ACTION });
component.showCompleteButton = true;
expect(component.isOutcomeButtonVisible(outcome, component.form.readOnly)).toBeTruthy();
component.showCompleteButton = false;
expect(component.isOutcomeButtonVisible(outcome, component.form.readOnly)).toBeFalsy();
});
});
});

View File

@@ -0,0 +1,168 @@
/*!
* @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 { LogService } from '../../services';
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core';
import { FormService } from './../services/form.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { FormComponent } from './form.component';
import { ContentLinkModel } from './widgets/core/content-link.model';
import { FormOutcomeModel } from './widgets/core/index';
/**
* Displays the start form for a named process definition, which can be used to retrieve values to start a new process.
*
* After the form has been completed the form values are available from the attribute component.form.values and
* component.form.isValid (boolean) can be used to check the if the form is valid or not. Both of these properties are
* updated as the user types into the form.
*
* @Input
* {processDefinitionId} string: The process definition ID
* {showOutcomeButtons} boolean: Whether form outcome buttons should be shown, this is now always active to show form outcomes
* @Output
* {formLoaded} EventEmitter - This event is fired when the form is loaded, it pass all the value in the form.
* {formSaved} EventEmitter - This event is fired when the form is saved, it pass all the value in the form.
* {formCompleted} EventEmitter - This event is fired when the form is completed, it pass all the value in the form.
*
* @returns {FormComponent} .
*/
@Component({
selector: 'adf-start-form',
templateUrl: './start-form.component.html',
styleUrls: ['./form.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class StartFormComponent extends FormComponent implements OnChanges, OnInit {
@Input()
processDefinitionId: string;
@Input()
processId: string;
@Input()
showOutcomeButtons: boolean = true;
@Input()
showRefreshButton: boolean = true;
@Input()
readOnlyForm: boolean = false;
@Output()
outcomeClick: EventEmitter<any> = new EventEmitter<any>();
@Output()
formContentClicked: EventEmitter<ContentLinkModel> = new EventEmitter<ContentLinkModel>();
@ViewChild('outcomesContainer', {})
outcomesContainer: ElementRef = null;
constructor(formService: FormService,
visibilityService: WidgetVisibilityService,
logService: LogService) {
super(formService, visibilityService, null, null);
this.showTitle = false;
}
ngOnInit() {
this.formService.formContentClicked.subscribe((content: ContentLinkModel) => {
this.formContentClicked.emit(content);
});
}
ngOnChanges(changes: SimpleChanges) {
let processDefinitionId = changes['processDefinitionId'];
if (processDefinitionId && processDefinitionId.currentValue) {
this.visibilityService.cleanProcessVariable();
this.getStartFormDefinition(processDefinitionId.currentValue);
return;
}
let processId = changes['processId'];
if (processId && processId.currentValue) {
this.visibilityService.cleanProcessVariable();
this.loadStartForm(processId.currentValue);
return;
}
}
loadStartForm(processId: string) {
this.formService.getProcessIntance(processId)
.subscribe((intance: any) => {
this.formService
.getStartFormInstance(processId)
.subscribe(
form => {
this.formName = form.name;
if (intance.variables) {
form.processVariables = intance.variables;
}
this.form = this.parseForm(form);
this.form.readOnly = this.readOnlyForm;
this.onFormLoaded(this.form);
},
error => this.handleError(error)
);
});
}
getStartFormDefinition(processId: string) {
this.formService
.getStartFormDefinition(processId)
.subscribe(
form => {
this.formName = form.processDefinitionName;
this.form = this.parseForm(form);
this.form.readOnly = this.readOnlyForm;
this.onFormLoaded(this.form);
},
error => this.handleError(error)
);
}
/** @override */
isOutcomeButtonVisible(outcome: FormOutcomeModel, isFormReadOnly: boolean): boolean {
if (outcome && outcome.isSystem && ( outcome.name === FormOutcomeModel.SAVE_ACTION ||
outcome.name === FormOutcomeModel.COMPLETE_ACTION )) {
return false;
} else if (outcome && outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return true;
}
return super.isOutcomeButtonVisible(outcome, isFormReadOnly);
}
/** @override */
saveTaskForm() {
// do nothing
}
/** @override */
onRefreshClicked() {
if (this.processDefinitionId) {
this.visibilityService.cleanProcessVariable();
this.getStartFormDefinition(this.processDefinitionId);
} else if (this.processId) {
this.visibilityService.cleanProcessVariable();
this.loadStartForm(this.processId);
}
}
completeTaskForm(outcome?: string) {
this.outcomeClick.emit(outcome);
}
}

View File

@@ -0,0 +1,19 @@
<div class="adf-amount-widget__container adf-amount-widget {{field.className}}" [class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly">
<mat-form-field floatPlaceholder="never" class="adf-amount-widget__input">
<label class="adf-label" [attr.for]="field.id">{{field.name}}<span *ngIf="isRequired()">*</span></label>
<span matPrefix class="adf-amount-widget__prefix-spacing"> {{currency }}</span>
<input matInput
class="adf-amount-widget"
type="text"
[id]="field.id"
[required]="isRequired()"
[value]="field.value"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"
[disabled]="field.readOnly"
placeholder="{{field.placeholder}}">
</mat-form-field>
<error-widget [error]="field.validationSummary" ></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -0,0 +1,45 @@
@import '../form';
.adf {
&-amount-widget {
width: 100%;
vertical-align: baseline !important;
.mat-input-element {
margin-left: 11px;
}
.mat-input-prefix {
position: absolute;
margin-top: 42px;
}
.mat-input-placeholder {
margin-top: 5px;
}
}
&-amount-widget__container {
max-width: 100%;
.mat-input-placeholder-wrapper {
top: -6px !important;
}
.mat-input-placeholder-wrapper {
top: 0 !important;
left: 13px;
}
}
&-amount-widget__input .mat-focused {
transition: none;
}
&-amount-widget__prefix-spacing {
padding-right: 5px;
}
}

View File

@@ -0,0 +1,71 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MaterialModule } from '../../../../material.module';
import { ActivitiContentService } from '../../../services/activiti-alfresco.service';
import { ErrorWidgetComponent } from '../error/error.component';
import { EcmModelService } from './../../../services/ecm-model.service';
import { FormService } from './../../../services/form.service';
import { FormFieldModel } from './../core/form-field.model';
import { AmountWidgetComponent } from './amount.widget';
describe('AmountWidgetComponent', () => {
let widget: AmountWidgetComponent;
let fixture: ComponentFixture<AmountWidgetComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule
],
declarations: [
AmountWidgetComponent,
ErrorWidgetComponent
],
providers: [
FormService,
EcmModelService,
ActivitiContentService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AmountWidgetComponent);
widget = fixture.componentInstance;
});
it('should setup currentcy from field', () => {
const currency = 'UAH';
widget.field = new FormFieldModel(null, {
currency: currency
});
widget.ngOnInit();
expect(widget.currency).toBe(currency);
});
it('should setup default currency', () => {
widget.field = null;
widget.ngOnInit();
expect(widget.currency).toBe(AmountWidgetComponent.DEFAULT_CURRENCY);
});
});

View File

@@ -0,0 +1,47 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from './../../../services/form.service';
import { baseHost , WidgetComponent } from './../widget.component';
@Component({
selector: 'amount-widget',
templateUrl: './amount.widget.html',
styleUrls: ['./amount.widget.scss'],
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class AmountWidgetComponent extends WidgetComponent implements OnInit {
static DEFAULT_CURRENCY: string = '$';
currency: string = AmountWidgetComponent.DEFAULT_CURRENCY;
constructor(public formService: FormService) {
super(formService);
}
ngOnInit() {
if (this.field && this.field.currency) {
this.currency = this.field.currency;
}
}
}

View File

@@ -0,0 +1,15 @@
.attach-widget {
width:100%
}
.attach-widget__icon {
float: left;
}
.attach-widget__file {
margin-top: 4px;
}
.attach-widget__reset {
margin-top: 4px;
}

View File

@@ -0,0 +1,32 @@
<div class="attach-widget {{field.className}}">
<label [attr.for]="field.id">{{field.name}}<span *ngIf="isRequired()">*</span></label>
<div>
<span *ngIf="hasFile()" class="attach-widget__file mdl-chip"><span class="mdl-chip__text">{{getLinkedFileName()}}</span></span>
<button #browseFile [disabled]="field.readOnly" (click)="showDialog();" class="mdl-button mdl-jsm-button mdl-js-ripple-effect attach-widget__browser">
<mat-icon>image</mat-icon>
Browse {{selectedFolderSiteName}}
</button>
<button *ngIf="hasFile" [disabled]="field.readOnly" (click)="reset(file);" class="mdl-button mdl-js-button mdl-js-ripple-effect attach-widget__reset">Clear</button>
</div>
</div>
<dialog class="mdl-dialog" #dialog>
<h4 class="mdl-dialog__title">Select content</h4>
<div class="mdl-dialog__content">
<ul class='mdl-list'>
<li class="mdl-list__item" *ngFor="let node of selectedFolderNodes">
<span class="mdl-list__item-primary-content" *ngIf="node.folder">
<mat-icon class="mdl-list__item-icon">folder</mat-icon>
<a (click)="selectFolder(node, $event)">{{node.title}}</a>
</span>
<span class="mdl-list__item-primary-content" *ngIf="!node.folder">
<mat-icon class="mdl-list__item-icon">description</mat-icon>
<a (click)="selectFile(node, $event)">{{node.title}}</a>
</span>
</li>
</ul>
</div>
<div class="mdl-dialog__actions">
<button type="button" (click)="cancel()" class="mdl-button close">Cancel</button>
</div>
</dialog>

View File

@@ -0,0 +1,304 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Rx';
import { ActivitiContentService } from '../../../services/activiti-alfresco.service';
import { MaterialModule } from '../../../../material.module';
import { ExternalContent } from '../core/external-content';
import { ExternalContentLink } from '../core/external-content-link';
import { FormFieldTypes } from '../core/form-field-types';
import { ErrorWidgetComponent } from '../error/error.component';
import { EcmModelService } from './../../../services/ecm-model.service';
import { FormService } from './../../../services/form.service';
import { FormFieldModel } from './../core/form-field.model';
import { FormModel } from './../core/form.model';
import { AttachWidgetComponent } from './attach.widget';
describe('AttachWidgetComponent', () => {
let widget: AttachWidgetComponent;
let fixture: ComponentFixture<AttachWidgetComponent>;
let element: HTMLElement;
let contentService: ActivitiContentService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule
],
declarations: [
AttachWidgetComponent,
ErrorWidgetComponent
],
providers: [
FormService,
EcmModelService,
ActivitiContentService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AttachWidgetComponent);
contentService = TestBed.get(ActivitiContentService);
element = fixture.nativeElement;
widget = fixture.componentInstance;
});
it('should require field value to check file', () => {
widget.field = null;
widget.ngOnInit();
expect(widget.hasFile()).toBeFalsy();
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
value: null
});
widget.ngOnInit();
expect(widget.hasFile()).toBeFalsy();
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
value: [{ name: 'file' }]
});
widget.ngOnInit();
expect(widget.hasFile()).toBeTruthy();
});
it('should setup with form field', () => {
let nodes: any = [{}];
spyOn(contentService, 'getAlfrescoNodes').and.returnValue(
Observable.create(observer => {
observer.next(nodes);
observer.complete();
})
);
let config = {
siteId: '<id>',
site: '<site>',
pathId: '<pathId>',
accountId: '<accountId>'
};
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
params: {
fileSource: {
selectedFolder: config
}
}
});
widget.ngOnInit();
expect(widget.selectedFolderSiteId).toBe(config.siteId);
expect(widget.selectedFolderSiteName).toBe(config.site);
expect(widget.selectedFolderPathId).toBe(config.pathId);
expect(widget.selectedFolderAccountId).toBe(config.accountId);
expect(widget.selectedFolderNodes).toEqual(nodes);
});
xit('should link file on select', () => {
let link = <ExternalContentLink> {};
spyOn(contentService, 'linkAlfrescoNode').and.returnValue(
Observable.create(observer => {
observer.next(link);
observer.complete();
})
);
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD
});
widget.ngOnInit();
let node = <ExternalContent> {};
widget.selectFile(node, null);
expect(contentService.linkAlfrescoNode).toHaveBeenCalled();
expect(widget.selectedFile).toBe(node);
expect(widget.field.value).toEqual([link]);
expect(widget.field.json.value).toEqual([link]);
expect(widget.hasFile()).toBeTruthy();
});
it('should reset', () => {
widget.field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD,
value: [{ name: 'filename' }]
});
widget.reset();
expect(widget.hasFile()).toBeFalsy();
expect(widget.field.value).toBeNull();
expect(widget.field.json.value).toBeNull();
expect(widget.hasFile()).toBeFalsy();
});
it('should close dialog on cancel', () => {
let closed = false;
widget.dialog = {
nativeElement: {
close: function () {
closed = true;
}
}
};
widget.cancel();
expect(closed).toBeTruthy();
});
xit('should show modal dialog', () => {
spyOn(contentService, 'getAlfrescoNodes').and.returnValue(
Observable.create(observer => {
observer.next([]);
observer.complete();
})
);
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
params: {
fileSource: {
selectedFolder: {}
}
}
});
let modalShown = false;
widget.dialog = {
nativeElement: {
showModal: function () {
modalShown = true;
}
}
};
widget.showDialog();
expect(modalShown).toBeTruthy();
});
it('should select folder and load nodes', () => {
let nodes: any = [{}];
spyOn(contentService, 'getAlfrescoNodes').and.returnValue(
Observable.create(observer => {
observer.next(nodes);
observer.complete();
})
);
let node = <ExternalContent> { id: '<id>' };
widget.selectFolder(node, null);
expect(widget.selectedFolderPathId).toBe(node.id);
expect(widget.selectedFolderNodes).toEqual(nodes);
});
it('should get linked file name via local variable', () => {
widget.fileName = '<fileName>';
widget.selectedFile = null;
widget.field = null;
expect(widget.getLinkedFileName()).toBe(widget.fileName);
});
it('should get linked file name via selected file', () => {
widget.fileName = null;
widget.selectedFile = <ExternalContent> { title: '<title>' };
widget.field = null;
expect(widget.getLinkedFileName()).toBe(widget.selectedFile.title);
});
it('should get linked file name via form field', () => {
widget.fileName = null;
widget.selectedFile = null;
let name = '<file>';
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
value: [{ name: name }]
});
expect(widget.getLinkedFileName()).toBe(name);
});
it('should require form field to setup file browser', () => {
widget.field = null;
widget.setupFileBrowser();
expect(widget.selectedFolderPathId).toBeUndefined();
expect(widget.selectedFolderAccountId).toBeUndefined();
const pathId = '<pathId>';
const accountId = '<accountId>';
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
params: {
fileSource: {
selectedFolder: {
pathId: pathId,
accountId: accountId
}
}
}
});
widget.setupFileBrowser();
expect(widget.selectedFolderPathId).toBe(pathId);
expect(widget.selectedFolderAccountId).toBe(accountId);
});
it('should get external content nodes', () => {
let nodes: any = [{}];
spyOn(contentService, 'getAlfrescoNodes').and.returnValue(
Observable.create(observer => {
observer.next(nodes);
observer.complete();
})
);
const accountId = '<accountId>';
const pathId = '<pathId>';
widget.selectedFolderAccountId = accountId;
widget.selectedFolderPathId = pathId;
widget.getExternalContentNodes();
expect(contentService.getAlfrescoNodes).toHaveBeenCalledWith(accountId, pathId);
expect(widget.selectedFolderNodes).toEqual(nodes);
});
it('should handle error', (done) => {
let error = 'error';
spyOn(contentService, 'getAlfrescoNodes').and.returnValue(
Observable.throw(error)
);
widget.error.subscribe(() => {
done();
});
widget.getExternalContentNodes();
});
it('should require configured dialog to show modal', () => {
widget.dialog = null;
spyOn(widget, 'setupFileBrowser').and.stub();
spyOn(widget, 'getExternalContentNodes').and.stub();
expect(widget.showDialog()).toBeFalsy();
});
});

View File

@@ -0,0 +1,156 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { Component, EventEmitter, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core';
import { ActivitiContentService } from '../../../services/activiti-alfresco.service';
import { ExternalContent } from '../core/external-content';
import { ExternalContentLink } from '../core/external-content-link';
import { FormFieldModel } from '../core/form-field.model';
import { FormService } from './../../../services/form.service';
import { baseHost , WidgetComponent } from './../widget.component';
@Component({
selector: 'attach-widget',
templateUrl: './attach.widget.html',
styleUrls: ['./attach.widget.css'],
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class AttachWidgetComponent extends WidgetComponent implements OnInit {
selectedFolderPathId: string;
selectedFolderSiteId: string;
selectedFolderSiteName: string;
selectedFolderAccountId: string;
fileName: string;
selectedFolderNodes: [ExternalContent];
selectedFile: ExternalContent;
@Output()
fieldChanged: EventEmitter<FormFieldModel> = new EventEmitter<FormFieldModel>();
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('dialog')
dialog: any;
constructor(public formService: FormService,
private contentService: ActivitiContentService) {
super(formService);
}
ngOnInit() {
if (this.field) {
let params = this.field.params;
if (params &&
params.fileSource &&
params.fileSource.selectedFolder) {
this.selectedFolderSiteId = params.fileSource.selectedFolder.siteId;
this.selectedFolderSiteName = params.fileSource.selectedFolder.site;
this.setupFileBrowser();
this.getExternalContentNodes();
}
}
}
setupFileBrowser() {
if (this.field) {
let params = this.field.params;
this.selectedFolderPathId = params.fileSource.selectedFolder.pathId;
this.selectedFolderAccountId = params.fileSource.selectedFolder.accountId;
}
}
getLinkedFileName(): string {
let result = this.fileName;
if (this.selectedFile &&
this.selectedFile.title) {
result = this.selectedFile.title;
}
if (this.field &&
this.field.value &&
this.field.value.length > 0 &&
this.field.value[0].name) {
result = this.field.value[0].name;
}
return result;
}
getExternalContentNodes() {
this.contentService.getAlfrescoNodes(this.selectedFolderAccountId, this.selectedFolderPathId)
.subscribe(
nodes => this.selectedFolderNodes = nodes,
(err) => {
this.error.emit(err);
}
);
}
selectFile(node: ExternalContent, $event: any) {
this.contentService.linkAlfrescoNode(this.selectedFolderAccountId, node, this.selectedFolderSiteId).subscribe(
(link: ExternalContentLink) => {
this.selectedFile = node;
this.field.value = [link];
this.field.json.value = [link];
this.closeDialog();
this.fieldChanged.emit(this.field);
}
);
}
selectFolder(node: ExternalContent, $event: any) {
this.selectedFolderPathId = node.id;
this.getExternalContentNodes();
}
showDialog(): boolean {
this.setupFileBrowser();
this.getExternalContentNodes();
if (this.dialog) {
// todo: show dialog
return true;
}
return false;
}
private closeDialog() {
if (this.dialog) {
this.dialog.nativeElement.close();
}
}
cancel() {
this.closeDialog();
}
reset() {
this.field.value = null;
this.field.json.value = null;
}
hasFile(): boolean {
return this.field && this.field.value;
}
}

View File

@@ -0,0 +1,12 @@
<div [ngClass]="field.className">
<mat-checkbox
[id]="field.id"
color="primary"
[required]="field.required"
[disabled]="field.readOnly || readOnly"
[(ngModel)]="field.value"
(change)="onChange()">
{{field.name}}
<span *ngIf="field.required">*</span>
</mat-checkbox>
</div>

View File

@@ -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.
*/
/* tslint:disable:component-selector */
import { Component, ViewEncapsulation } from '@angular/core';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { FormService } from './../../../services/form.service';
import { baseHost , WidgetComponent } from './../widget.component';
@Component({
selector: 'checkbox-widget',
templateUrl: './checkbox.widget.html',
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class CheckboxWidgetComponent extends WidgetComponent {
constructor(private visibilityService: WidgetVisibilityService, public formService: FormService) {
super(formService);
}
onChange() {
this.visibilityService.refreshVisibility(this.field.form);
}
}

View File

@@ -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 { ContainerColumnModel } from './../core/container-column.model';
import { FormFieldModel } from './../core/form-field.model';
import { FormModel } from './../core/form.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(new FormModel(), null)];
expect(column.hasFields()).toBeTruthy();
});
});

View File

@@ -0,0 +1,20 @@
<div [ngClass]="{'hidden':!(content?.isGroup() && content?.isVisible)}" class="container-widget__header">
<h4 class="container-widget__header-text" id="container-header"
[class.collapsible]="content?.isCollapsible()">
<button *ngIf="content?.isCollapsible()"
mat-icon-button
class="mdl-button--icon"
(click)="onExpanderClicked()">
<mat-icon>{{ content?.isExpanded ? 'expand_less' : 'expand_more' }}</mat-icon>
</button>
<span (click)="onExpanderClicked()" id="container-header-label">{{content.name}}</span>
</h4>
</div>
<section class="grid-list" [ngClass]="{'hidden':!(content?.isVisible && content?.isExpanded)}">
<div class="grid-list-item" *ngFor="let field of fields" [style.width]="getColumnWith(field)">
<form-field *ngIf="field" [field]="field"></form-field>
</div>
</section>

View File

@@ -0,0 +1,77 @@
/*!
* @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 { FormFieldTypes } from './../core/form-field-types';
import { FormFieldModel } from './../core/form-field.model';
import { FormModel } from './../core/form.model';
import { ContainerWidgetComponentModel } from './container.widget.model';
describe('ContainerWidgetComponentModel', () => {
it('should store the form reference', () => {
let form = new FormModel();
let field = new FormFieldModel(form);
let model = new ContainerWidgetComponentModel(field);
expect(model.form).toBe(form);
});
it('should allow collapsing only when of a group type', () => {
let container = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.CONTAINER,
params: {
allowCollapse: true
}
}));
expect(container.isCollapsible()).toBeFalsy();
container = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true
}
}));
expect(container.isCollapsible()).toBeTruthy();
});
it('should allow collapsing only when explicitly defined in params', () => {
let container = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {}
}));
expect(container.isCollapsible()).toBeFalsy();
container = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true
}
}));
expect(container.isCollapsible()).toBeTruthy();
});
it('should be collapsed by default', () => {
let container = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true,
collapseByDefault: true
}
}));
expect(container.isCollapsedByDefault()).toBeTruthy();
});
});

View File

@@ -0,0 +1,66 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { ContainerColumnModel } from './../core/container-column.model';
import { ContainerModel } from './../core/container.model';
import { FormFieldTypes } from './../core/form-field-types';
import { FormFieldModel } from './../core/form-field.model';
export class ContainerWidgetComponentModel extends ContainerModel {
columns: ContainerColumnModel[] = [];
isExpanded: boolean = true;
rowspan: number = 1;
colspan: number = 1;
isGroup(): boolean {
return this.type === FormFieldTypes.GROUP;
}
isCollapsible(): boolean {
let allowCollapse = false;
if (this.isGroup() && this.field.params['allowCollapse']) {
allowCollapse = <boolean> this.field.params['allowCollapse'];
}
return allowCollapse;
}
isCollapsedByDefault(): boolean {
let collapseByDefault = false;
if (this.isCollapsible() && this.field.params['collapseByDefault']) {
collapseByDefault = <boolean> this.field.params['collapseByDefault'];
}
return collapseByDefault;
}
constructor(field: FormFieldModel) {
super(field);
if (this.field) {
this.columns = this.field.columns || [];
this.isExpanded = !this.isCollapsedByDefault();
this.colspan = field.colspan;
this.rowspan = field.rowspan;
}
}
}

View File

@@ -0,0 +1,94 @@
@mixin adf-form-container-widget-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
.hidden{
display: none;
}
.adf {
&-field-list {
padding: 0;
list-style-type: none;
width: 100%;
height: 100%;
}
.container-widget__header-text {
border-bottom: 1px solid rgba(0, 0, 0, 0.87);
margin-left: 10px;
margin-right: 10px;
cursor: default;
user-select: none;
-webkit-user-select: none;
/* Chrome/Safari/Opera */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* IE/Edge */
-webkit-touch-callout: none;
/* iOS Safari */
&.collapsible {
cursor: pointer;
}
}
}
container-widget {
.grid-list {
display: flex;
flex-wrap: wrap;
margin-left: -1%;
margin-right: -1%;
}
.grid-list-item {
flex-grow: 1;
box-sizing: border-box;
padding-left: 1%;
padding-right: 1%;
}
.mat-form-field {
width: 100%;
}
mat-form-field {
width: 100%;
}
.mat-input-placeholder-wrapper {
top: 5px !important;
}
.mat-input-placeholder {
top: 1.8em !important;
}
.mat-focused {
.mat-input-placeholder-wrapper {
display: none;
}
label {
transform: scaleX(1);
transition: transform 150ms linear,
background-color $swift-ease-in-duration $swift-ease-in-timing-function;
color: mat-color($primary);
}
.mat-input-prefix {
color: mat-color($primary);
}
}
.mat-grid-tile {
overflow: visible;
}
}
}

View File

@@ -0,0 +1,258 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivitiContentService } from '../../../services/activiti-alfresco.service';
import { fakeFormJson } from '../../../../mock';
import { MaterialModule } from '../../../../material.module';
import { WIDGET_DIRECTIVES } from '../index';
import { MASK_DIRECTIVE } from '../index';
import { EcmModelService } from './../../../services/ecm-model.service';
import { FormService } from './../../../services/form.service';
import { FormFieldComponent } from './../../form-field/form-field.component';
import { ContentWidgetComponent } from './../content/content.widget';
import { ContainerColumnModel } from './../core/container-column.model';
import { FormFieldTypes } from './../core/form-field-types';
import { FormFieldModel } from './../core/form-field.model';
import { FormModel } from './../core/form.model';
import { ContainerWidgetComponent } from './container.widget';
import { ContainerWidgetComponentModel } from './container.widget.model';
describe('ContainerWidgetComponent', () => {
let widget: ContainerWidgetComponent;
let fixture: ComponentFixture<ContainerWidgetComponent>;
let element: HTMLElement;
let contentService: ActivitiContentService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule
],
declarations: [FormFieldComponent, ContentWidgetComponent, WIDGET_DIRECTIVES, MASK_DIRECTIVE],
providers: [
FormService,
EcmModelService,
ActivitiContentService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContainerWidgetComponent);
contentService = TestBed.get(ActivitiContentService);
element = fixture.nativeElement;
widget = fixture.componentInstance;
});
it('should wrap field with model instance', () => {
let field = new FormFieldModel(null);
widget.field = field;
widget.ngOnInit();
expect(widget.content).toBeDefined();
expect(widget.content.field).toBe(field);
});
it('should toggle underlying group container', () => {
let container = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true
}
}));
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 ContainerWidgetComponentModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP
}));
widget.content = container;
expect(container.isExpanded).toBeTruthy();
widget.onExpanderClicked();
expect(container.isExpanded).toBeTruthy();
});
it('should toggle only group container', () => {
let container = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.CONTAINER,
params: {
allowCollapse: true
}
}));
widget.content = container;
expect(container.isExpanded).toBeTruthy();
widget.onExpanderClicked();
expect(container.isExpanded).toBeTruthy();
});
it('should send an event when a value is changed in the form', (done) => {
let fakeForm = new FormModel();
let fakeField = new FormFieldModel(fakeForm, {id: 'fakeField', value: 'fakeValue'});
widget.fieldChanged.subscribe(field => {
expect(field).not.toBe(null);
expect(field.id).toBe('fakeField');
expect(field.value).toBe('fakeValue');
done();
});
widget.onFieldChanged(fakeField);
});
describe('when template is ready', () => {
let fakeContainerVisible;
let fakeContainerInvisible;
beforeEach(() => {
fakeContainerVisible = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel(fakeFormJson), {
fieldType: FormFieldTypes.GROUP,
id: 'fake-cont-id-1',
name: 'fake-cont-1-name',
type: FormFieldTypes.GROUP
}));
fakeContainerInvisible = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel(fakeFormJson), {
fieldType: FormFieldTypes.GROUP,
id: 'fake-cont-id-2',
name: 'fake-cont-2-name',
type: FormFieldTypes.GROUP
}));
fakeContainerVisible.field.isVisible = true;
fakeContainerInvisible.field.isVisible = false;
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
it('should show the container header when it is visible', () => {
widget.content = fakeContainerVisible;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('.container-widget__header').classList.contains('hidden')).toBe(false);
expect(element.querySelector('#container-header-label')).toBeDefined();
expect(element.querySelector('#container-header-label').innerHTML).toContain('fake-cont-1-name');
});
});
it('should not show the container header when it is not visible', () => {
widget.content = fakeContainerInvisible;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('.container-widget__header').classList.contains('hidden')).toBe(true);
});
});
it('should hide header when it becomes not visible', async(() => {
widget.content = fakeContainerVisible;
fixture.detectChanges();
widget.fieldChanged.subscribe((res) => {
widget.content.field.isVisible = false;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('.container-widget__header').classList.contains('hidden')).toBe(true);
});
});
widget.onFieldChanged(null);
}));
it('should show header when it becomes visible', async(() => {
widget.content = fakeContainerInvisible;
widget.fieldChanged.subscribe((res) => {
widget.content.field.isVisible = true;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#container-header')).toBeDefined();
expect(element.querySelector('#container-header')).not.toBeNull();
expect(element.querySelector('#container-header-label')).toBeDefined();
expect(element.querySelector('#container-header-label').innerHTML).toContain('fake-cont-2-name');
});
});
widget.onFieldChanged(null);
}));
});
describe('fields', () => {
it('should serializes the content fields', () => {
const field1 = <FormFieldModel> {id: '1'},
field2 = <FormFieldModel> {id: '2'},
field3 = <FormFieldModel> {id: '3'},
field4 = <FormFieldModel> {id: '4'},
field5 = <FormFieldModel> {id: '5'},
field6 = <FormFieldModel> {id: '6'};
let container = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel()));
container.columns = [
<ContainerColumnModel> { fields: [
field1,
field2,
field3
] },
<ContainerColumnModel> { fields: [
field4,
field5
] },
<ContainerColumnModel> { fields: [
field6
] }
];
widget.content = container;
expect(widget.fields[0].id).toEqual('1');
expect(widget.fields[1].id).toEqual('4');
expect(widget.fields[2].id).toEqual('6');
expect(widget.fields[3].id).toEqual('2');
expect(widget.fields[4].id).toEqual('5');
expect(widget.fields[5]).toEqual(undefined);
expect(widget.fields[6].id).toEqual('3');
expect(widget.fields[7]).toEqual(undefined);
expect(widget.fields[8]).toEqual(undefined);
});
});
describe('getColumnWith', () => {
it('should calculate the column width based on the numberOfColumns and current field\'s colspan property', () => {
let container = new ContainerWidgetComponentModel(new FormFieldModel(new FormModel(), { numberOfColumns: 4 }));
widget.content = container;
expect(widget.getColumnWith(undefined)).toBe('25%');
expect(widget.getColumnWith(<FormFieldModel> { colspan: 1 })).toBe('25%');
expect(widget.getColumnWith(<FormFieldModel> { colspan: 3 })).toBe('75%');
});
});
});

View File

@@ -0,0 +1,87 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { AfterViewInit, Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from './../../../services/form.service';
import { FormFieldModel } from './../core/form-field.model';
import { baseHost , WidgetComponent } from './../widget.component';
import { ContainerWidgetComponentModel } from './container.widget.model';
@Component({
selector: 'container-widget',
templateUrl: './container.widget.html',
styleUrls: ['./container.widget.scss'],
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class ContainerWidgetComponent extends WidgetComponent implements OnInit, AfterViewInit {
content: ContainerWidgetComponentModel;
constructor(public formService: FormService) {
super(formService);
}
onExpanderClicked() {
if (this.content && this.content.isCollapsible()) {
this.content.isExpanded = !this.content.isExpanded;
}
}
ngOnInit() {
if (this.field) {
this.content = new ContainerWidgetComponentModel(this.field);
}
}
/**
* Serializes column fields
*/
get fields(): FormFieldModel[] {
const fields = [];
let rowContainsElement = true,
rowIndex = 0;
while (rowContainsElement) {
rowContainsElement = false;
for (let i = 0; i < this.content.columns.length; i++ ) {
let field = this.content.columns[i].fields[rowIndex];
if (field) {
rowContainsElement = true;
}
fields.push(field);
}
rowIndex++;
}
return fields;
}
/**
* Calculate the column width based on the numberOfColumns and current field's colspan property
*
* @param field
*/
getColumnWith(field: FormFieldModel): string {
const colspan = field ? field.colspan : 1;
return (100 / this.content.json.numberOfColumns) * colspan + '%';
}
}

View File

@@ -0,0 +1,22 @@
<mat-card class="adf-content-container" *ngIf="content">
<mat-card-content *ngIf="showDocumentContent">
<div *ngIf="content.isThumbnailSupported()" >
<img id="thumbnailPreview" class="adf-img-upload-widget" [src]="content.thumbnailUrl" alt="{{content.name}}">
</div>
<div *ngIf="!content.isThumbnailSupported()">
<mat-icon>image</mat-icon>
<div id="unsupported-thumbnail" class="adf-content-widget-preview-text">{{ 'FORM.PREVIEW.IMAGE_NOT_AVAILABLE' | translate }}
</div>
</div>
<div class="mdl-card__supporting-text upload-widget__content-text">{{content.name}}</div>
</mat-card-content>
<mat-card-actions>
<button mat-icon-button id="view" (click)="openViewer(content)">
<mat-icon class="mat-24">zoom_in</mat-icon>
</button>
<button mat-icon-button id="download" (click)="download(content)">
<mat-icon class="mat-24">file_download</mat-icon>
</button>
</mat-card-actions>
</mat-card>

View File

@@ -0,0 +1,15 @@
.adf {
&-img-upload-widget {
width: 100%;
height: 100%;
border: 1px solid rgba(117, 117, 117, 0.57);
box-shadow: 1px 1px 2px #dddddd;
background-color: #ffffff;
}
&-content-widget-preview-text {
word-wrap: break-word;
word-break: break-all;
text-align: center;
}
}

View File

@@ -0,0 +1,310 @@
/*!
* @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 { DebugElement, SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MaterialModule } from '../../../../material.module';
import { By } from '@angular/platform-browser';
import { TranslationService, ContentService } from '../../../../services';
import { Observable } from 'rxjs/Rx';
import { EcmModelService } from '../../../services/ecm-model.service';
import { FormService } from '../../../services/form.service';
import { ProcessContentService } from '../../../services/process-content.service';
import { ContentLinkModel } from '../index';
import { ContentWidgetComponent } from './content.widget';
declare let jasmine: any;
describe('ContentWidgetComponent', () => {
let component: ContentWidgetComponent;
let fixture: ComponentFixture<ContentWidgetComponent>;
let debug: DebugElement;
let element: HTMLElement;
let serviceForm: FormService;
let processContentService: ProcessContentService;
let serviceContent: ContentService;
function createFakeImageBlob() {
let data = atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
return new Blob([data], {type: 'image/png'});
}
function createFakePdfBlob(): Blob {
let pdfData = atob(
'JVBERi0xLjcKCjEgMCBvYmogICUgZW50cnkgcG9pbnQKPDwKICAvVHlwZSAvQ2F0YWxvZwog' +
'IC9QYWdlcyAyIDAgUgo+PgplbmRvYmoKCjIgMCBvYmoKPDwKICAvVHlwZSAvUGFnZXMKICAv' +
'TWVkaWFCb3ggWyAwIDAgMjAwIDIwMCBdCiAgL0NvdW50IDEKICAvS2lkcyBbIDMgMCBSIF0K' +
'Pj4KZW5kb2JqCgozIDAgb2JqCjw8CiAgL1R5cGUgL1BhZ2UKICAvUGFyZW50IDIgMCBSCiAg' +
'L1Jlc291cmNlcyA8PAogICAgL0ZvbnQgPDwKICAgICAgL0YxIDQgMCBSIAogICAgPj4KICA+' +
'PgogIC9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKCjQgMCBvYmoKPDwKICAvVHlwZSAvRm9u' +
'dAogIC9TdWJ0eXBlIC9UeXBlMQogIC9CYXNlRm9udCAvVGltZXMtUm9tYW4KPj4KZW5kb2Jq' +
'Cgo1IDAgb2JqICAlIHBhZ2UgY29udGVudAo8PAogIC9MZW5ndGggNDQKPj4Kc3RyZWFtCkJU' +
'CjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8sIHdvcmxkISkgVGoKRVQKZW5kc3RyZWFtCmVu' +
'ZG9iagoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDEwIDAwMDAwIG4g' +
'CjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE3MyAwMDAwMCBuIAowMDAwMDAwMzAxIDAw' +
'MDAwIG4gCjAwMDAwMDAzODAgMDAwMDAgbiAKdHJhaWxlcgo8PAogIC9TaXplIDYKICAvUm9v' +
'dCAxIDAgUgo+PgpzdGFydHhyZWYKNDkyCiUlRU9G');
return new Blob([pdfData], {type: 'application/pdf'});
}
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule
],
declarations: [
ContentWidgetComponent
],
providers: [
FormService,
EcmModelService,
ContentService,
ProcessContentService
]
}).compileComponents();
serviceForm = TestBed.get(FormService);
serviceContent = TestBed.get(ContentService);
processContentService = TestBed.get(ProcessContentService);
let translateService = TestBed.get(TranslationService);
spyOn(translateService, 'addTranslationFolder').and.stub();
spyOn(translateService, 'get').and.callFake((key) => {
return Observable.of(key);
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentWidgetComponent);
component = fixture.componentInstance;
debug = fixture.debugElement;
element = fixture.nativeElement;
fixture.detectChanges();
});
describe('Rendering tests', () => {
beforeEach(() => {
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
it('should display content thumbnail', () => {
component.showDocumentContent = true;
component.content = new ContentLinkModel();
fixture.detectChanges();
let content = fixture.debugElement.query(By.css('div.upload-widget__content-thumbnail'));
expect(content).toBeDefined();
});
it('should load the thumbnail preview of the png image', (done) => {
let blob = createFakeImageBlob();
spyOn(processContentService, 'getFileRawContent').and.returnValue(Observable.of(blob));
component.thumbnailLoaded.subscribe((res) => {
fixture.detectChanges();
expect(res).toBeDefined();
expect(res.changingThisBreaksApplicationSecurity).toBeDefined();
expect(res.changingThisBreaksApplicationSecurity).toContain('blob');
fixture.whenStable()
.then(() => {
let thumbnailPreview: any = element.querySelector('#thumbnailPreview');
expect(thumbnailPreview.src).toContain('blob');
});
done();
});
let contentId = 1;
let change = new SimpleChange(null, contentId, true);
component.ngOnChanges({ 'id': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: {
id: 4004,
name: 'Useful expressions - Email_English.png',
created: 1490354907883,
createdBy: {
id: 2,
firstName: 'dasdas', 'lastName': 'dasads', 'email': 'administrator@admin.com'
},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'image/png',
simpleType: 'image',
previewStatus: 'unsupported',
thumbnailStatus: 'unsupported'
}
});
});
it('should load the thumbnail preview of a pdf', (done) => {
let blob = createFakePdfBlob();
spyOn(processContentService, 'getContentThumbnailUrl').and.returnValue(Observable.of(blob));
component.thumbnailLoaded.subscribe((res) => {
fixture.detectChanges();
expect(res).toBeDefined();
expect(res.changingThisBreaksApplicationSecurity).toBeDefined();
expect(res.changingThisBreaksApplicationSecurity).toContain('blob');
fixture.whenStable()
.then(() => {
let thumbnailPreview: any = element.querySelector('#thumbnailPreview');
expect(thumbnailPreview.src).toContain('blob');
});
done();
});
let contentId = 1;
let change = new SimpleChange(null, contentId, true);
component.ngOnChanges({'id': change});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: {
id: 4004,
name: 'FakeBlob.pdf',
created: 1490354907883,
createdBy: {
id: 2,
firstName: 'dasdas', 'lastName': 'dasads', 'email': 'administrator@admin.com'
},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'application/pdf',
simpleType: 'pdf',
previewStatus: 'created',
thumbnailStatus: 'created'
}
});
});
it('should show unsupported preview with unsupported file', (done) => {
let contentId = 1;
let change = new SimpleChange(null, contentId, true);
component.ngOnChanges({'id': change});
component.contentLoaded.subscribe((res) => {
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let thumbnailPreview: any = element.querySelector('#unsupported-thumbnail');
expect(thumbnailPreview).toBeDefined();
expect(element.querySelector('div.upload-widget__content-text').innerHTML).toEqual('FakeBlob.zip');
});
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: {
id: 4004,
name: 'FakeBlob.zip',
created: 1490354907883,
createdBy: {
id: 2,
firstName: 'dasdas', 'lastName': 'dasads', 'email': 'administrator@admin.com'
},
relatedContent: false,
contentAvailable: false,
link: false,
mimeType: 'application/zip',
simpleType: 'zip',
previewStatus: 'unsupported',
thumbnailStatus: 'unsupported'
}
});
});
it('should open the viewer when the view button is clicked', (done) => {
let blob = createFakePdfBlob();
spyOn(processContentService, 'getFileRawContent').and.returnValue(Observable.of(blob));
component.content = new ContentLinkModel({
id: 4004,
name: 'FakeBlob.pdf',
created: 1490354907883,
createdBy: {
id: 2,
firstName: 'dasdas', 'lastName': 'dasads', 'email': 'administrator@admin.com'
},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'application/pdf',
simpleType: 'pdf',
previewStatus: 'created',
thumbnailStatus: 'created'
});
component.contentClick.subscribe((content) => {
expect(content.contentBlob).toBe(blob);
expect(content.mimeType).toBe('application/pdf');
expect(content.name).toBe('FakeBlob.pdf');
done();
});
fixture.detectChanges();
let viewButton: any = element.querySelector('#view');
viewButton.click();
});
it('should download the pdf when the download button is clicked', () => {
let blob = createFakePdfBlob();
spyOn(processContentService, 'getFileRawContent').and.returnValue(Observable.of(blob));
spyOn(serviceContent, 'downloadBlob').and.callThrough();
component.content = new ContentLinkModel({
id: 4004,
name: 'FakeBlob.pdf',
created: 1490354907883,
createdBy: {
id: 2,
firstName: 'dasdas', 'lastName': 'dasads', 'email': 'administrator@admin.com'
},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'application/pdf',
simpleType: 'pdf',
previewStatus: 'created',
thumbnailStatus: 'created'
});
fixture.detectChanges();
let downloadButton: any = element.querySelector('#download');
downloadButton.click();
fixture.whenStable()
.then(() => {
expect(serviceContent.downloadBlob).toHaveBeenCalledWith(blob, 'FakeBlob.pdf');
});
});
});
});

View File

@@ -0,0 +1,131 @@
/*!
* @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 { ContentService, LogService } from '../../../../services';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { ProcessContentService } from '../../../services/process-content.service';
import { ContentLinkModel } from '../core/content-link.model';
import { FormService } from './../../../services/form.service';
@Component({
selector: 'adf-content',
templateUrl: './content.widget.html',
styleUrls: ['./content.widget.scss'],
encapsulation: ViewEncapsulation.None
})
export class ContentWidgetComponent implements OnChanges {
@Input()
id: string;
@Input()
showDocumentContent: boolean = true;
@Output()
contentClick = new EventEmitter();
@Output()
thumbnailLoaded: EventEmitter<any> = new EventEmitter<any>();
@Output()
contentLoaded: EventEmitter<any> = new EventEmitter<any>();
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
content: ContentLinkModel;
constructor(protected formService: FormService,
private logService: LogService,
private contentService: ContentService,
private processContentService: ProcessContentService) {
}
ngOnChanges(changes: SimpleChanges) {
const contentId = changes['id'];
if (contentId && contentId.currentValue) {
this.loadContent(contentId.currentValue);
}
}
loadContent(id: number) {
this.processContentService
.getFileContent(id)
.subscribe(
(response: ContentLinkModel) => {
this.content = new ContentLinkModel(response);
this.contentLoaded.emit(this.content);
this.loadThumbnailUrl(this.content);
},
(error) => {
this.error.emit(error);
}
);
}
loadThumbnailUrl(content: ContentLinkModel) {
if (this.content.isThumbnailSupported()) {
let observable: Observable<any>;
if (this.content.isTypeImage()) {
observable = this.processContentService.getFileRawContent(content.id);
} else {
observable = this.processContentService.getContentThumbnailUrl(content.id);
}
if (observable) {
observable.subscribe(
(response: Blob) => {
this.content.thumbnailUrl = this.contentService.createTrustedUrl(response);
this.thumbnailLoaded.emit(this.content.thumbnailUrl);
},
(error) => {
this.error.emit(error);
}
);
}
}
}
openViewer(content: ContentLinkModel): void {
this.processContentService.getFileRawContent(content.id).subscribe(
(blob: Blob) => {
content.contentBlob = blob;
this.contentClick.emit(content);
this.logService.info('Content clicked' + content.id);
this.formService.formContentClicked.next(content);
},
(error) => {
this.error.emit(error);
}
);
}
/**
* Invoke content download.
*/
download(content: ContentLinkModel): void {
this.processContentService.getFileRawContent(content.id).subscribe(
(blob: Blob) => this.contentService.downloadBlob(blob, content.name),
(error) => {
this.error.emit(error);
}
);
}
}

View File

@@ -0,0 +1,32 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { FormFieldModel } from './form-field.model';
export class ContainerColumnModel {
size: number = 12;
fields: FormFieldModel[] = [];
colspan: number = 1;
rowspan: number = 1;
hasFields(): boolean {
return this.fields && this.fields.length > 0;
}
}

View File

@@ -0,0 +1,30 @@
/*!
* @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 { ContainerModel } from './container.model';
import { FormFieldModel } from './form-field.model';
import { FormModel } from './form.model';
describe('ContainerModel', () => {
it('should store the form reference', () => {
let form = new FormModel();
let model = new ContainerModel(new FormFieldModel(form));
expect(model.form).toBe(form);
});
});

View File

@@ -0,0 +1,39 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { FormFieldModel } from './form-field.model';
import { FormWidgetModel } from './form-widget.model';
export class ContainerModel extends FormWidgetModel {
field: FormFieldModel;
get isVisible(): boolean {
return this.field.isVisible;
}
constructor(field: FormFieldModel) {
super(field.form, field.json);
if (field) {
this.field = field;
}
}
}

View File

@@ -0,0 +1,76 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { RelatedContentRepresentation } from 'alfresco-js-api';
export class ContentLinkModel implements RelatedContentRepresentation {
contentAvailable: boolean;
created: Date;
createdBy: any;
id: number;
link: boolean;
mimeType: string;
name: string;
previewStatus: string;
relatedContent: boolean;
simpleType: string;
thumbnailUrl: string;
contentRawUrl: string;
contentBlob: Blob;
thumbnailStatus: string;
constructor(obj?: any) {
this.contentAvailable = obj && obj.contentAvailable;
this.created = obj && obj.created;
this.createdBy = obj && obj.createdBy || {};
this.id = obj && obj.id;
this.link = obj && obj.link;
this.mimeType = obj && obj.mimeType;
this.name = obj && obj.name;
this.previewStatus = obj && obj.previewStatus;
this.relatedContent = obj && obj.relatedContent;
this.simpleType = obj && obj.simpleType;
this.thumbnailStatus = obj && obj.thumbnailStatus;
}
hasPreviewStatus(): boolean {
return this.previewStatus === 'supported' ? true : false;
}
isTypeImage(): boolean {
return this.simpleType === 'image' ? true : false;
}
isTypePdf(): boolean {
return this.simpleType === 'pdf' ? true : false;
}
isTypeDoc(): boolean {
return this.simpleType === 'word' || this.simpleType === 'content' ? true : false;
}
isThumbnailReady(): boolean {
return this.thumbnailStatus === 'created';
}
isThumbnailSupported(): boolean {
return this.isTypeImage() || ((this.isTypePdf() || this.isTypeDoc()) && this.isThumbnailReady());
}
}

View File

@@ -0,0 +1,45 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export class ErrorMessageModel {
message: string = '';
attributes: Map<string, string> = null;
constructor(obj?: any) {
this.message = obj && obj.message ? obj.message : '';
this.attributes = new Map();
}
isActive() {
return this.message ? true : false;
}
getAttributesAsJsonObj() {
let result = {};
if (this.attributes.size > 0) {
let obj = Object.create(null);
this.attributes.forEach((value, key) => {
obj[key] = value;
});
result = JSON.stringify(obj);
}
return result;
}
}

View File

@@ -0,0 +1,34 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export interface ExternalContentLink {
contentAvailable: boolean;
created: string;
createdBy: any;
id: number;
link: boolean;
mimeType: string;
name: string;
previewStatus: string;
relatedContent: boolean;
simpleType: string;
source: string;
sourceId: string;
thumbnailStatus: string;
}

View File

@@ -0,0 +1,25 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export interface ExternalContent {
folder: boolean;
id: string;
simpleType: string;
title: string;
}

View File

@@ -0,0 +1,27 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { FormFieldSelectedFolder } from './form-field-selected-folder';
export interface FormFieldFileSource {
metadataAllowed: boolean;
name: string;
selectedFolder: FormFieldSelectedFolder;
serviceId: string;
}

View File

@@ -0,0 +1,26 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { FormFieldFileSource } from './form-field-file-source';
export interface FormFieldMetadata {
[key: string]: any;
fileSource?: FormFieldFileSource;
link?: boolean;
}

View File

@@ -0,0 +1,23 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export interface FormFieldOption {
id: string;
name: string;
}

View File

@@ -0,0 +1,27 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export interface FormFieldSelectedFolder {
accountId: string;
folderTree: [any];
path: string;
pathId: string;
site: string;
siteId: string;
}

View File

@@ -0,0 +1,22 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export interface FormFieldTemplates {
[key: string]: string;
}

View File

@@ -0,0 +1,54 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export class FormFieldTypes {
static CONTAINER: string = 'container';
static GROUP: string = 'group';
static DYNAMIC_TABLE: string = 'dynamic-table';
static TEXT: string = 'text';
static MULTILINE_TEXT: string = 'multi-line-text';
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 UPLOAD: string = 'upload';
static TYPEAHEAD: string = 'typeahead';
static FUNCTIONAL_GROUP: string = 'functional-group';
static PEOPLE: string = 'people';
static BOOLEAN: string = 'boolean';
static NUMBER: string = 'integer';
static DATE: string = 'date';
static AMOUNT: string = 'amount';
static DOCUMENT: string = 'document';
static READONLY_TYPES: string[] = [
FormFieldTypes.HYPERLINK,
FormFieldTypes.DISPLAY_VALUE,
FormFieldTypes.READONLY_TEXT
];
static isReadOnlyType(type: string) {
return FormFieldTypes.READONLY_TYPES.indexOf(type) > -1;
}
static isContainerType(type: string) {
return type === FormFieldTypes.CONTAINER || type === FormFieldTypes.GROUP;
}
}

View File

@@ -0,0 +1,591 @@
/*!
* @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 { ErrorMessageModel } from './error-message.model';
import { FormFieldOption } from './form-field-option';
import { FormFieldTypes } from './form-field-types';
import {
FixedValueFieldValidator,
MaxLengthFieldValidator,
MaxValueFieldValidator,
MinLengthFieldValidator,
MinValueFieldValidator,
NumberFieldValidator,
RegExFieldValidator,
RequiredFieldValidator
} from './form-field-validator';
import { FormFieldModel } from './form-field.model';
import { FormModel } from './form.model';
describe('FormFieldValidator', () => {
describe('RequiredFieldValidator', () => {
let validator: RequiredFieldValidator;
beforeEach(() => {
validator = new RequiredFieldValidator();
});
it('should require [required] setting', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: '<value>'
});
field.required = false;
expect(validator.isSupported(field)).toBeFalsy();
expect(validator.validate(field)).toBeTruthy();
field.required = true;
expect(validator.isSupported(field)).toBeTruthy();
expect(validator.validate(field)).toBeTruthy();
});
it('should skip unsupported type', () => {
let field = new FormFieldModel(new FormModel(), { type: 'wrong-type' });
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for dropdown with empty value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
value: '<empty>',
hasEmptyValue: true,
required: true
});
field.emptyOption = <FormFieldOption> { id: '<empty>' };
expect(validator.validate(field)).toBeFalsy();
field.value = '<non-empty>';
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for radio buttons', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
required: true,
value: 'one',
options: [{ id: 'two', name: 'two' }]
});
expect(validator.validate(field)).toBeFalsy();
});
it('should succeed for radio buttons', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
required: true,
value: 'two',
options: [{ id: 'two', name: 'two' }]
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for upload', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD,
value: null,
required: true
});
field.value = null;
expect(validator.validate(field)).toBeFalsy();
field.value = [];
expect(validator.validate(field)).toBeFalsy();
});
it('should succeed for upload', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD,
value: [{}],
required: true
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for text', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: null,
required: true
});
field.value = null;
expect(validator.validate(field)).toBeFalsy();
field.value = '';
expect(validator.validate(field)).toBeFalsy();
});
it('should succeed for date', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: '2016-12-31',
required: true
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for date', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: null,
required: true
});
field.value = null;
expect(validator.validate(field)).toBeFalsy();
field.value = '';
expect(validator.validate(field)).toBeFalsy();
});
it('should succeed for text', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: '<value>',
required: true
});
expect(validator.validate(field)).toBeTruthy();
});
});
describe('NumberFieldValidator', () => {
let validator: NumberFieldValidator;
beforeEach(() => {
validator = new NumberFieldValidator();
});
it('should verify number', () => {
expect(NumberFieldValidator.isNumber('1')).toBeTruthy();
expect(NumberFieldValidator.isNumber('1.0')).toBeTruthy();
expect(NumberFieldValidator.isNumber('-1')).toBeTruthy();
expect(NumberFieldValidator.isNumber(1)).toBeTruthy();
expect(NumberFieldValidator.isNumber(0)).toBeTruthy();
expect(NumberFieldValidator.isNumber(-1)).toBeTruthy();
});
it('should not verify number', () => {
expect(NumberFieldValidator.isNumber(null)).toBeFalsy();
expect(NumberFieldValidator.isNumber(undefined)).toBeFalsy();
expect(NumberFieldValidator.isNumber('')).toBeFalsy();
expect(NumberFieldValidator.isNumber('one')).toBeFalsy();
expect(NumberFieldValidator.isNumber('1q')).toBeFalsy();
});
it('should allow empty number value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: null
});
expect(validator.validate(field)).toBeTruthy();
});
it('should allow number value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: 44
});
expect(validator.validate(field)).toBeTruthy();
});
it('should allow zero number value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: 0
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for wrong number value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: '<value>'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBeFalsy();
expect(field.validationSummary).not.toBeNull();
});
});
describe('MinLengthFieldValidator', () => {
let validator: MinLengthFieldValidator;
beforeEach(() => {
validator = new MinLengthFieldValidator();
});
it('should require minLength defined', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.isSupported(field)).toBeFalsy();
field.minLength = 10;
expect(validator.isSupported(field)).toBeTruthy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
minLength: 10,
value: null
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed text validation', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
minLength: 3,
value: '1234'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail text validation', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
minLength: 3,
value: '12'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBeFalsy();
expect(field.validationSummary).not.toBeNull();
});
});
describe('MaxLengthFieldValidator', () => {
let validator: MaxLengthFieldValidator;
beforeEach(() => {
validator = new MaxLengthFieldValidator();
});
it('should require maxLength defined', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.isSupported(field)).toBeFalsy();
field.maxLength = 10;
expect(validator.isSupported(field)).toBeTruthy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
maxLength: 10,
value: null
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed text validation', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
maxLength: 3,
value: '123'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail text validation', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
maxLength: 3,
value: '1234'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBeFalsy();
expect(field.validationSummary).not.toBeNull();
});
});
describe('MinValueFieldValidator', () => {
let validator: MinValueFieldValidator;
beforeEach(() => {
validator = new MinValueFieldValidator();
});
it('should require minValue defined', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER
});
expect(validator.isSupported(field)).toBeFalsy();
field.minValue = '1';
expect(validator.isSupported(field)).toBeTruthy();
});
it('should support numeric widgets only', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
minValue: '1'
});
expect(validator.isSupported(field)).toBeTruthy();
field.type = FormFieldTypes.TEXT;
expect(validator.isSupported(field)).toBeFalsy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: null,
minValue: '1'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed for unsupported types', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed validating value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: '10',
minValue: '10'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail validating value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: '9',
minValue: '10'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBeFalsy();
expect(field.validationSummary).not.toBeNull();
});
});
describe('MaxValueFieldValidator', () => {
let validator: MaxValueFieldValidator;
beforeEach(() => {
validator = new MaxValueFieldValidator();
});
it('should require maxValue defined', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER
});
expect(validator.isSupported(field)).toBeFalsy();
field.maxValue = '1';
expect(validator.isSupported(field)).toBeTruthy();
});
it('should support numeric widgets only', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
maxValue: '1'
});
expect(validator.isSupported(field)).toBeTruthy();
field.type = FormFieldTypes.TEXT;
expect(validator.isSupported(field)).toBeFalsy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: null,
maxValue: '1'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed for unsupported types', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed validating value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: '10',
maxValue: '10'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail validating value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: '11',
maxValue: '10'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBeFalsy();
expect(field.validationSummary).not.toBeNull();
});
});
describe('RegExFieldValidator', () => {
let validator: RegExFieldValidator;
beforeEach(() => {
validator = new RegExFieldValidator();
});
it('should require regex pattern to be defined', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.isSupported(field)).toBeFalsy();
field.regexPattern = '<pattern>';
expect(validator.isSupported(field)).toBeTruthy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: null,
regexPattern: 'pattern'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed validating regex', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: 'pattern',
regexPattern: 'pattern'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail validating regex', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: 'some value',
regexPattern: 'pattern'
});
expect(validator.validate(field)).toBeFalsy();
});
});
describe('FixedValueFieldValidator', () => {
let validator: FixedValueFieldValidator;
beforeEach(() => {
validator = new FixedValueFieldValidator();
});
it('should support only typeahead field', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.isSupported(field)).toBeFalsy();
field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TYPEAHEAD
});
expect(validator.isSupported(field)).toBeTruthy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TYPEAHEAD,
value: null,
regexPattern: 'pattern'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed for a valid input value in options', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TYPEAHEAD,
value: '1',
options: [{id: '1', name: 'Leanne Graham'}, {id: '2', name: 'Ervin Howell'}]
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for an invalid input value in options', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TYPEAHEAD,
value: 'Lean',
options: [{id: '1', name: 'Leanne Graham'}, {id: '2', name: 'Ervin Howell'}]
});
expect(validator.validate(field)).toBeFalsy();
});
});
});

View File

@@ -0,0 +1,432 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import * as moment from 'moment';
import { FormFieldTypes } from './form-field-types';
import { FormFieldModel } from './form-field.model';
export interface FormFieldValidator {
isSupported(field: FormFieldModel): boolean;
validate(field: FormFieldModel): boolean;
}
export class RequiredFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.TEXT,
FormFieldTypes.MULTILINE_TEXT,
FormFieldTypes.NUMBER,
FormFieldTypes.TYPEAHEAD,
FormFieldTypes.DROPDOWN,
FormFieldTypes.PEOPLE,
FormFieldTypes.FUNCTIONAL_GROUP,
FormFieldTypes.RADIO_BUTTONS,
FormFieldTypes.UPLOAD,
FormFieldTypes.AMOUNT,
FormFieldTypes.DYNAMIC_TABLE,
FormFieldTypes.DATE
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
field.required;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field)) {
if (field.type === FormFieldTypes.DROPDOWN) {
if (field.hasEmptyValue && field.emptyOption) {
if (field.value === field.emptyOption.id) {
return false;
}
}
}
if (field.type === FormFieldTypes.RADIO_BUTTONS) {
let option = field.options.find(opt => opt.id === field.value);
return !!option;
}
if (field.type === FormFieldTypes.UPLOAD) {
return field.value && field.value.length > 0;
}
if (field.type === FormFieldTypes.DYNAMIC_TABLE) {
return field.value && field.value instanceof Array && field.value.length > 0;
}
if (field.value === null || field.value === undefined || field.value === '') {
return false;
}
}
return true;
}
}
export class NumberFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.NUMBER,
FormFieldTypes.AMOUNT
];
static isNumber(value: any): boolean {
if (value === null || value === undefined || value === '') {
return false;
}
return !isNaN(+value);
}
isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field)) {
if (field.value === null ||
field.value === undefined ||
field.value === '') {
return true;
}
let valueStr = '' + field.value;
let pattern = new RegExp(/^-?\d+$/);
if (field.enableFractions) {
pattern = new RegExp(/^-?[0-9]+(\.[0-9]{1,2})?$/);
}
if (valueStr.match(pattern)) {
return true;
}
field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_NUMBER';
return false;
}
return true;
}
}
export class DateFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.DATE
];
// Validates that the input string is a valid date formatted as <dateFormat> (default D-M-YYYY)
static isValidDate(inputDate: string, dateFormat: string = 'D-M-YYYY'): boolean {
if (inputDate) {
let d = moment(inputDate, dateFormat, true);
return d.isValid();
}
return false;
}
isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
if (DateFieldValidator.isValidDate(field.value, field.dateDisplayFormat)) {
return true;
}
field.validationSummary.message = field.dateDisplayFormat;
return false;
}
return true;
}
}
export class MinDateFieldValidator implements FormFieldValidator {
MIN_DATE_FORMAT = 'DD-MM-YYYY';
private supportedTypes = [
FormFieldTypes.DATE
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 && !!field.minValue;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
const dateFormat = field.dateDisplayFormat;
if (!DateFieldValidator.isValidDate(field.value, dateFormat)) {
field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DATE';
return false;
}
// remove time and timezone info
let d;
if (typeof field.value === 'string') {
d = moment(field.value.split('T')[0], dateFormat);
} else {
d = field.value;
}
let min = moment(field.minValue, this.MIN_DATE_FORMAT);
if (d.isBefore(min)) {
field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_LESS_THAN`;
field.validationSummary.attributes.set('minValue', field.minValue.toLocaleString());
return false;
}
}
return true;
}
}
export class MaxDateFieldValidator implements FormFieldValidator {
MAX_DATE_FORMAT = 'DD-MM-YYYY';
private supportedTypes = [
FormFieldTypes.DATE
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 && !!field.maxValue;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
const dateFormat = field.dateDisplayFormat;
if (!DateFieldValidator.isValidDate(field.value, dateFormat)) {
field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DATE';
return false;
}
// remove time and timezone info
let d;
if (typeof field.value === 'string') {
d = moment(field.value.split('T')[0], dateFormat);
} else {
d = field.value;
}
let max = moment(field.maxValue, this.MAX_DATE_FORMAT);
if (d.isAfter(max)) {
field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`;
field.validationSummary.attributes.set('maxValue', field.maxValue.toLocaleString());
return false;
}
}
return true;
}
}
export class MinLengthFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.TEXT,
FormFieldTypes.MULTILINE_TEXT
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
field.minLength > 0;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
if (field.value.length >= field.minLength) {
return true;
}
field.validationSummary.message = `FORM.FIELD.VALIDATOR.AT_LEAST_LONG`;
field.validationSummary.attributes.set('minLength', field.minLength.toLocaleString());
return false;
}
return true;
}
}
export class MaxLengthFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.TEXT,
FormFieldTypes.MULTILINE_TEXT
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
field.maxLength > 0;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
if (field.value.length <= field.maxLength) {
return true;
}
field.validationSummary.message = `FORM.FIELD.VALIDATOR.NO_LONGER_THAN`;
field.validationSummary.attributes.set('maxLength', field.maxLength.toLocaleString());
return false;
}
return true;
}
}
export class MinValueFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.NUMBER,
FormFieldTypes.AMOUNT
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
NumberFieldValidator.isNumber(field.minValue);
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
let value: number = +field.value;
let minValue: number = +field.minValue;
if (value >= minValue) {
return true;
}
field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_LESS_THAN`;
field.validationSummary.attributes.set('minValue', field.minValue.toLocaleString());
return false;
}
return true;
}
}
export class MaxValueFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.NUMBER,
FormFieldTypes.AMOUNT
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
NumberFieldValidator.isNumber(field.maxValue);
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
let value: number = +field.value;
let maxValue: number = +field.maxValue;
if (value <= maxValue) {
return true;
}
field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`;
field.validationSummary.attributes.set('maxValue', field.maxValue.toLocaleString());
return false;
}
return true;
}
}
export class RegExFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.TEXT,
FormFieldTypes.MULTILINE_TEXT
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 && !!field.regexPattern;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
if (field.value.length > 0 && field.value.match(new RegExp('^' + field.regexPattern + '$'))) {
return true;
}
field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_VALUE';
return false;
}
return true;
}
}
export class FixedValueFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.TYPEAHEAD
];
isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1;
}
hasValidNameOrValidId(field: FormFieldModel): boolean {
return this.hasValidName(field) || this.hasValidId(field);
}
hasValidName(field: FormFieldModel) {
return field.options.find(item => item.name && item.name.toLocaleLowerCase() === field.value.toLocaleLowerCase()) ? true : false;
}
hasValidId(field: FormFieldModel) {
return field.options[field.value - 1] ? true : false;
}
hasStringValue(field: FormFieldModel) {
return field.value && typeof field.value === 'string';
}
hasOptions(field: FormFieldModel) {
return field.options && field.options.length > 0;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field)) {
if (this.hasStringValue(field) && this.hasOptions(field) && !this.hasValidNameOrValidId(field)) {
field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_VALUE';
return false;
}
}
return true;
}
}
export const FORM_FIELD_VALIDATORS = [
new RequiredFieldValidator(),
new NumberFieldValidator(),
new MinLengthFieldValidator(),
new MaxLengthFieldValidator(),
new MinValueFieldValidator(),
new MaxValueFieldValidator(),
new RegExFieldValidator(),
new DateFieldValidator(),
new MinDateFieldValidator(),
new MaxDateFieldValidator(),
new FixedValueFieldValidator()
];

View File

@@ -0,0 +1,378 @@
/*!
* @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 { FormFieldTypes } from './form-field-types';
import { FormFieldModel } from './form-field.model';
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: '<fieldType>',
id: '<id>',
name: '<name>',
type: '<type>',
required: true,
readOnly: true,
overrideId: true,
tab: '<tab>',
restUrl: '<rest-url>',
restResponsePath: '<rest-path>',
restIdProperty: '<rest-id>',
restLabelProperty: '<rest-label>',
colspan: 1,
options: [],
hasEmptyValue: true,
className: '<class>',
optionType: '<type>',
params: {},
hyperlinkUrl: '<url>',
displayText: '<text>',
value: '<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 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 the date with the default format (D-M-YYYY) if the display format is missing', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
fieldType: 'FormFieldRepresentation',
id: 'mmddyyyy',
name: 'MM-DD-YYYY',
type: 'date',
value: '2017-04-28T00:00:00.000+0000',
required: false,
readOnly: false,
params: {
field: {
id: 'mmddyyyy',
name: 'MM-DD-YYYY',
type: 'date',
value: null,
required: false,
readOnly: false
}
}
});
expect(field.value).toBe('28-4-2017');
expect(form.values['mmddyyyy']).toEqual('2017-04-28T00:00:00.000Z');
});
it('should parse the date with the format MM-DD-YYYY', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
fieldType: 'FormFieldRepresentation',
id: 'mmddyyyy',
name: 'MM-DD-YYYY',
type: 'date',
value: '2017-04-28T00:00:00.000+0000',
required: false,
readOnly: false,
params: {
field: {
id: 'mmddyyyy',
name: 'MM-DD-YYYY',
type: 'date',
value: null,
required: false,
readOnly: false
}
},
dateDisplayFormat: 'MM-DD-YYYY'
});
expect(field.value).toBe('04-28-2017');
expect(form.values['mmddyyyy']).toEqual('2017-04-28T00:00:00.000Z');
});
it('should parse the date with the format MM-YY-DD', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
fieldType: 'FormFieldRepresentation',
id: 'mmyydd',
name: 'MM-YY-DD',
type: 'date',
value: '2017-04-28T00:00:00.000+0000',
required: false,
readOnly: false,
params: {
field: {
id: 'mmyydd',
name: 'MM-YY-DD',
type: 'date',
value: null,
required: false,
readOnly: false
}
},
dateDisplayFormat: 'MM-YY-DD'
});
expect(field.value).toBe('04-17-28');
expect(form.values['mmyydd']).toEqual('2017-04-28T00:00:00.000Z');
});
it('should parse the date with the format DD-MM-YYYY', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
fieldType: 'FormFieldRepresentation',
id: 'ddmmyyy',
name: 'DD-MM-YYYY',
type: 'date',
value: '2017-04-28T00:00:00.000+0000',
required: false,
readOnly: false,
params: {
field: {
id: 'ddmmyyy',
name: 'DD-MM-YYYY',
type: 'date',
value: null,
required: false,
readOnly: false
}
},
dateDisplayFormat: 'DD-MM-YYYY'
});
expect(field.value).toBe('28-04-2017');
expect(form.values['ddmmyyy']).toEqual('2017-04-28T00:00:00.000Z');
});
it('should return the label of selected dropdown value ', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
options: [
{id: 'fake-option-1', name: 'fake label 1'},
{id: 'fake-option-2', name: 'fake label 2'},
{id: 'fake-option-3', name: 'fake label 3'}
],
value: 'fake-option-2'
});
expect(field.getOptionName()).toBe('fake label 2');
});
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 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', name: 'Option 1'},
{id: 'opt2', name: '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 = '<some value>';
expect(form.values[field.id]).toBeUndefined();
});
});
it('should be able to check if the field has options available', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
id: 'dropdown-happy',
type: FormFieldTypes.DROPDOWN,
options: [
{id: 'opt1', name: 'Option 1'},
{id: 'opt2', name: 'Option 2'}
]
});
expect(field.hasOptions()).toBeTruthy();
});
it('should return false if field has no options', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
id: 'dropdown-sad',
type: FormFieldTypes.DROPDOWN
});
expect(field.hasOptions()).toBeFalsy();
});
it('should calculate the columns in case of container type', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
type: FormFieldTypes.CONTAINER,
numberOfColumns: 888
});
expect(field.numberOfColumns).toBe(888);
});
it('should calculate the columns in case of group type', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
type: FormFieldTypes.GROUP,
numberOfColumns: 999
});
expect(field.numberOfColumns).toBe(999);
});
});

View File

@@ -0,0 +1,420 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import * as moment from 'moment';
import { WidgetVisibilityModel } from '../../../models/widget-visibility.model';
import { ContainerColumnModel } from './container-column.model';
import { ErrorMessageModel } from './error-message.model';
import { FormFieldMetadata } from './form-field-metadata';
import { FormFieldOption } from './form-field-option';
import { FormFieldTypes } from './form-field-types';
import { NumberFieldValidator } from './form-field-validator';
import { FormWidgetModel } from './form-widget.model';
import { FormModel } from './form.model';
// Maps to FormFieldRepresentation
export class FormFieldModel extends FormWidgetModel {
private _value: string;
private _readOnly: boolean = false;
private _isValid: boolean = true;
private _required: boolean = false;
readonly defaultDateFormat: string = 'D-M-YYYY';
// model members
fieldType: string;
id: string;
name: string;
type: string;
overrideId: boolean;
tab: string;
rowspan: number = 1;
colspan: number = 1;
placeholder: string = null;
minLength: number = 0;
maxLength: number = 0;
minValue: string;
maxValue: string;
regexPattern: string;
options: FormFieldOption[] = [];
restUrl: string;
restResponsePath: string;
restIdProperty: string;
restLabelProperty: string;
hasEmptyValue: boolean;
className: string;
optionType: string;
params: FormFieldMetadata = {};
hyperlinkUrl: string;
displayText: string;
isVisible: boolean = true;
visibilityCondition: WidgetVisibilityModel = null;
enableFractions: boolean = false;
currency: string = null;
dateDisplayFormat: string = this.dateDisplayFormat || this.defaultDateFormat;
// container model members
numberOfColumns: number = 1;
fields: FormFieldModel[] = [];
columns: ContainerColumnModel[] = [];
// util members
emptyOption: FormFieldOption;
validationSummary: ErrorMessageModel;
get value(): any {
return this._value;
}
set value(v: any) {
this._value = v;
this.validate();
this.updateForm();
}
get readOnly(): boolean {
if (this.form && this.form.readOnly) {
return true;
}
return this._readOnly;
}
set readOnly(readOnly: boolean) {
this._readOnly = readOnly;
this.updateForm();
}
get required(): boolean {
return this._required;
}
set required(value: boolean) {
this._required = value;
this.updateForm();
}
get isValid(): boolean {
return this._isValid;
}
markAsInvalid() {
this._isValid = false;
}
validate(): boolean {
this.validationSummary = new ErrorMessageModel();
let validators = this.form.fieldValidators || [];
for (let validator of validators) {
if (!validator.validate(this)) {
this._isValid = false;
return this._isValid;
}
}
this._isValid = true;
return this._isValid;
}
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 || json.type === '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.minLength = <number> json.minLength || 0;
this.maxLength = <number> json.maxLength || 0;
this.minValue = json.minValue;
this.maxValue = json.maxValue;
this.regexPattern = json.regexPattern;
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.visibilityCondition = <WidgetVisibilityModel> json.visibilityCondition;
this.enableFractions = <boolean> json.enableFractions;
this.currency = json.currency;
this.dateDisplayFormat = json.dateDisplayFormat || this.defaultDateFormat;
this._value = this.parseValue(json);
this.validationSummary = new ErrorMessageModel();
if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') {
this.placeholder = json.placeholder;
}
if (FormFieldTypes.isReadOnlyType(json.type)) {
if (json.params && json.params.field) {
if (form.processVariables) {
const processVariable = this.getProcessVariableValue(json.params.field, form);
if (processVariable) {
this.value = processVariable;
}
} else if (json.params.field.responseVariable) {
const formVariable = this.getVariablesValue(json.params.field.name, form);
if (formVariable) {
this.value = formVariable;
}
}
}
}
if (FormFieldTypes.isContainerType(json.type)) {
this.containerFactory(json, form);
}
}
if (this.hasEmptyValue && this.options && this.options.length > 0) {
this.emptyOption = this.options[0];
}
this.updateForm();
}
private isTypeaHeadFieldType(type: string): boolean {
return type === 'typeahead' ? true : false;
}
private getFieldNameWithLabel(name: string): string {
return name += '_LABEL';
}
private getProcessVariableValue(field: any, form: FormModel) {
let fieldName = field.name;
if (this.isTypeaHeadFieldType(field.type)) {
fieldName = this.getFieldNameWithLabel(field.id);
}
return this.findProcessVariableValue(fieldName, form);
}
private getVariablesValue(variableName: string, form: FormModel) {
let variable = form.json.variables.find((currentVariable) => {
return currentVariable.name === variableName;
});
if (variable) {
if (variable.type === 'boolean') {
return JSON.parse(variable.value);
}
return variable.value;
}
return null;
}
private findProcessVariableValue(variableName: string, form: FormModel) {
if (form.processVariables) {
const variable = form.processVariables.find((currentVariable) => {
return currentVariable.name === variableName;
});
if (variable) {
return variable.type === 'boolean' ? JSON.parse(variable.value) : variable.value;
}
}
return undefined;
}
private containerFactory(json: any, form: FormModel): void {
this.numberOfColumns = <number> json.numberOfColumns || 1;
this.fields = json.fields;
this.rowspan = 1;
this.colspan = 1;
if (json.fields) {
for (let currentField in json.fields) {
if (json.fields.hasOwnProperty(currentField)) {
let col = new ContainerColumnModel();
let fields: FormFieldModel[] = (json.fields[currentField] || []).map(f => new FormFieldModel(form, f));
col.fields = fields;
col.rowspan = json.fields[currentField].length;
col.fields.forEach((colFields: any) => {
this.colspan = colFields.colspan > this.colspan ? colFields.colspan : this.colspan;
});
this.rowspan = this.rowspan < col.rowspan ? col.rowspan : this.rowspan;
this.columns.push(col);
}
}
}
}
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> }
*/
if (json.type === FormFieldTypes.DROPDOWN) {
if (json.hasEmptyValue && json.options) {
let options = <FormFieldOption[]> json.options || [];
if (options.length > 0) {
let emptyOption = json.options[0];
if (value === '' || value === emptyOption.id || value === emptyOption.name) {
value = emptyOption.id;
}
}
}
}
/*
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 where initial selection passed as `name` value
// so try resolving current one with a fallback to first entry via name or id
// TODO: needs to be reported and fixed at Activiti side
let entry: FormFieldOption[] = this.options.filter(opt => opt.id === value || opt.name === value);
if (entry.length > 0) {
value = entry[0].id;
}
}
/*
This is needed due to Activiti displaying/editing dates in d-M-YYYY format
but storing on server in ISO8601 format (i.e. 2013-02-04T22:44:30.652Z)
*/
if (json.type === FormFieldTypes.DATE) {
if (value) {
let dateValue;
if (NumberFieldValidator.isNumber(value)) {
dateValue = moment(value);
} else {
dateValue = moment(value.split('T')[0], 'YYYY-M-D');
}
if (dateValue && dateValue.isValid()) {
value = dateValue.format(this.dateDisplayFormat);
}
}
}
return value;
}
updateForm() {
if (!this.form) {
return;
}
switch (this.type) {
case 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];
}
}
break;
case 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 rbEntry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value);
if (rbEntry.length > 0) {
this.form.values[this.id] = rbEntry[0];
}
break;
case FormFieldTypes.UPLOAD:
if (this.value && this.value.length > 0) {
this.form.values[this.id] = this.value.map(elem => elem.id).join(',');
} else {
this.form.values[this.id] = null;
}
break;
case FormFieldTypes.TYPEAHEAD:
let taEntry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value);
if (taEntry.length > 0) {
this.form.values[this.id] = taEntry[0];
} else if (this.options.length > 0) {
this.form.values[this.id] = null;
}
break;
case FormFieldTypes.DATE:
let dateValue = moment(this.value, this.dateDisplayFormat, true);
if (dateValue && dateValue.isValid()) {
this.form.values[this.id] = `${dateValue.format('YYYY-MM-DD')}T00:00:00.000Z`;
} else {
this.form.values[this.id] = null;
this._value = this.value;
}
break;
case FormFieldTypes.NUMBER:
this.form.values[this.id] = parseInt(this.value, 10);
break;
case FormFieldTypes.AMOUNT:
this.form.values[this.id] = this.enableFractions ? parseFloat(this.value) : parseInt(this.value, 10);
break;
default:
if (!FormFieldTypes.isReadOnlyType(this.type) && !this.isInvalidFieldType(this.type)) {
this.form.values[this.id] = this.value;
}
}
this.form.onFormFieldChanged(this);
}
/**
* Skip the invalid field type
* @param type
*/
isInvalidFieldType(type: string) {
if (type === 'container') {
return true;
} else {
return false;
}
}
getOptionName(): string {
let option: FormFieldOption = this.options.find(opt => opt.id === this.value);
return option ? option.name : null;
}
hasOptions() {
return this.options && this.options.length > 0;
}
}

View File

@@ -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.
*/
/* tslint:disable:component-selector */
import { FormOutcomeModel } from './form-outcome.model';
export class FormOutcomeEvent {
private _outcome: FormOutcomeModel;
private _defaultPrevented: boolean = false;
get outcome(): FormOutcomeModel {
return this._outcome;
}
get defaultPrevented() {
return this._defaultPrevented;
}
constructor(outcome: FormOutcomeModel) {
this._outcome = outcome;
}
preventDefault() {
this._defaultPrevented = true;
}
}

View File

@@ -0,0 +1,45 @@
/*!
* @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 { FormOutcomeModel } from './form-outcome.model';
import { FormModel } from './form.model';
describe('FormOutcomeModel', () => {
it('should setup with json config', () => {
let json = {
id: '<id>',
name: '<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);
});
});

View File

@@ -0,0 +1,40 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
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
static START_PROCESS_ACTION: string = 'Start Process'; // Activiti 'Start Process' action name
isSystem: boolean = false;
isSelected: boolean = false;
constructor(form: FormModel, json?: any) {
super(form, json);
if (json) {
this.isSystem = json.isSystem ? true : false;
this.isSelected = form && json.name === form.selectedOutcome ? true : false;
}
}
}

View File

@@ -0,0 +1,24 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
/* tslint:disable */
import { FormFieldMetadata } from './form-field-metadata';
export interface FormValues extends FormFieldMetadata {
}

View File

@@ -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 { FormWidgetModel } from './form-widget.model';
import { FormModel } from './form.model';
describe('FormWidgetModel', () => {
class FormWidgetModelMock extends FormWidgetModel {
constructor(form: FormModel, json: any) {
super(form, json);
}
}
it('should store the form reference', () => {
let form = new FormModel();
let model = new FormWidgetModelMock(form, null);
expect(model.form).toBe(form);
});
it('should store original json', () => {
let json = {};
let model = new FormWidgetModelMock(null, json);
expect(model.json).toBe(json);
});
});

View File

@@ -0,0 +1,49 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { FormModel } from './form.model';
export abstract class FormWidgetModel {
readonly fieldType: string;
readonly id: string;
readonly name: string;
readonly type: string;
readonly tab: string;
readonly form: FormModel;
readonly json: any;
constructor(form: FormModel, json: any) {
this.form = form;
this.json = json;
if (json) {
this.fieldType = json.fieldType;
this.id = json.id;
this.name = json.name;
this.type = json.type;
this.tab = json.tab;
}
}
}
export interface FormWidgetModelCache<T extends FormWidgetModel> {
[key: string]: T;
}

View File

@@ -0,0 +1,450 @@
/*!
* @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 { ValidateFormFieldEvent } from './../../../events/validate-form-field.event';
import { ValidateFormEvent } from './../../../events/validate-form.event';
import { FormService } from './../../../services/form.service';
import { ContainerModel } from './container.model';
import { FormFieldTypes } from './form-field-types';
import { FORM_FIELD_VALIDATORS, FormFieldValidator } from './form-field-validator';
import { FormFieldModel } from './form-field.model';
import { FormOutcomeModel } from './form-outcome.model';
import { FormModel } from './form.model';
import { TabModel } from './tab.model';
describe('FormModel', () => {
let formService: FormService;
beforeEach(() => {
formService = new FormService(null, null, null);
});
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: '<id>',
name: '<name>',
taskId: '<task-id>',
taskName: '<task-name>'
};
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: '<id>',
name: '<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();
let field = new FormFieldModel(form);
form.fields = [new ContainerModel(field)];
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',
type: FormFieldTypes.CONTAINER
},
{
id: 'field2',
type: FormFieldTypes.CONTAINER
}
]
};
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',
type: FormFieldTypes.CONTAINER
},
{
id: 'field2',
type: FormFieldTypes.CONTAINER
}
]
}
};
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', type: FormFieldTypes.CONTAINER },
{ id: 'field2', tab: 'tab2', type: FormFieldTypes.CONTAINER },
{ id: 'field3', tab: 'tab1', type: FormFieldTypes.DYNAMIC_TABLE },
{ id: 'field4', tab: 'missing-tab', type: FormFieldTypes.DYNAMIC_TABLE }
]
};
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 create standard form outcomes', () => {
let json = {
fields: [
{ id: 'container1' }
]
};
let form = new FormModel(json);
expect(form.outcomes.length).toBe(3);
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();
expect(form.outcomes[2].id).toBe(FormModel.START_PROCESS_OUTCOME);
expect(form.outcomes[2].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();
});
it('should raise validation event when validating form', (done) => {
const form = new FormModel({}, null, false, formService);
formService.validateForm.subscribe(() => done());
form.validateForm();
});
it('should raise validation event when validating field', (done) => {
const form = new FormModel({}, null, false, formService);
const field = jasmine.createSpyObj('FormFieldModel', ['validate']);
formService.validateFormField.subscribe(() => done());
form.validateField(field);
});
it('should skip form validation when default behaviour prevented', () => {
const form = new FormModel({}, null, false, formService);
let prevented = false;
formService.validateForm.subscribe((event: ValidateFormEvent) => {
event.isValid = false;
event.preventDefault();
prevented = true;
});
const field = jasmine.createSpyObj('FormFieldModel', ['validate']);
spyOn(form, 'getFormFields').and.returnValue([field]);
form.validateForm();
expect(prevented).toBeTruthy();
expect(form.isValid).toBeFalsy();
expect(field.validate).not.toHaveBeenCalled();
});
it('should skip field validation when default behaviour prevented', () => {
const form = new FormModel({}, null, false, formService);
let prevented = false;
formService.validateFormField.subscribe((event: ValidateFormFieldEvent) => {
event.isValid = false;
event.preventDefault();
prevented = true;
});
const field = jasmine.createSpyObj('FormFieldModel', ['validate']);
form.validateField(field);
expect(prevented).toBeTruthy();
expect(form.isValid).toBeFalsy();
expect(field.validate).not.toHaveBeenCalled();
});
it('should validate fields when form validation not prevented', () => {
const form = new FormModel({}, null, false, formService);
let validated = false;
formService.validateForm.subscribe((event: ValidateFormEvent) => {
validated = true;
});
const field = jasmine.createSpyObj('FormFieldModel', ['validate']);
spyOn(form, 'getFormFields').and.returnValue([field]);
form.validateForm();
expect(validated).toBeTruthy();
expect(field.validate).toHaveBeenCalled();
});
it('should validate field when field validation not prevented', () => {
const form = new FormModel({}, null, false, formService);
let validated = false;
formService.validateFormField.subscribe((event: ValidateFormFieldEvent) => {
validated = true;
});
const field = jasmine.createSpyObj('FormFieldModel', ['validate']);
form.validateField(field);
expect(validated).toBeTruthy();
expect(field.validate).toHaveBeenCalled();
});
it('should validate form when field validation not prevented', () => {
const form = new FormModel({}, null, false, formService);
spyOn(form, 'validateForm').and.stub();
let validated = false;
formService.validateFormField.subscribe((event: ValidateFormFieldEvent) => {
validated = true;
});
const field: any = {
validate() {
return true;
}
};
form.validateField(field);
expect(validated).toBeTruthy();
expect(form.validateForm).toHaveBeenCalled();
});
it('should not validate form when field validation prevented', () => {
const form = new FormModel({}, null, false, formService);
spyOn(form, 'validateForm').and.stub();
let prevented = false;
formService.validateFormField.subscribe((event: ValidateFormFieldEvent) => {
event.preventDefault();
prevented = true;
});
const field = jasmine.createSpyObj('FormFieldModel', ['validate']);
form.validateField(field);
expect(prevented).toBeTruthy();
expect(field.validate).not.toHaveBeenCalled();
expect(form.validateForm).not.toHaveBeenCalled();
});
it('should get field by id', () => {
const form = new FormModel({}, null, false, formService);
const field: any = { id: 'field1' };
spyOn(form, 'getFormFields').and.returnValue([field]);
const result = form.getFieldById('field1');
expect(result).toBe(field);
});
it('should use custom field validator', () => {
const form = new FormModel({}, null, false, formService);
const testField = new FormFieldModel(form, {
id: 'test-field-1'
});
spyOn(form, 'getFormFields').and.returnValue([testField]);
let validator = <FormFieldValidator> {
isSupported(field: FormFieldModel): boolean {
return true;
},
validate(field: FormFieldModel): boolean {
return true;
}
};
spyOn(validator, 'validate').and.callThrough();
form.fieldValidators = [validator];
form.validateForm();
expect(validator.validate).toHaveBeenCalledWith(testField);
});
it('should re-validate the field when required attribute changes', () => {
const form = new FormModel({}, null, false, formService);
const testField = new FormFieldModel(form, {
id: 'test-field-1',
required: false
});
spyOn(form, 'getFormFields').and.returnValue([testField]);
spyOn(form, 'onFormFieldChanged').and.callThrough();
spyOn(form, 'validateField').and.callThrough();
testField.required = true;
expect(testField.required).toBeTruthy();
expect(form.onFormFieldChanged).toHaveBeenCalledWith(testField);
expect(form.validateField).toHaveBeenCalledWith(testField);
});
it('should not change default validators export', () => {
const form = new FormModel({}, null, false, formService);
const defaultLength = FORM_FIELD_VALIDATORS.length;
expect(form.fieldValidators.length).toBe(defaultLength);
form.fieldValidators.push(<any> {});
expect(form.fieldValidators.length).toBe(defaultLength + 1);
expect(FORM_FIELD_VALIDATORS.length).toBe(defaultLength);
});
});

View File

@@ -0,0 +1,280 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { FormFieldEvent, ValidateFormEvent, ValidateFormFieldEvent } from './../../../events/index';
import { FormService } from './../../../services/form.service';
import { ContainerModel } from './container.model';
import { FormFieldTemplates } from './form-field-templates';
import { FormFieldTypes } from './form-field-types';
import { FormFieldModel } from './form-field.model';
import { FormOutcomeModel } from './form-outcome.model';
import { FormValues } from './form-values';
import { FormWidgetModel, FormWidgetModelCache } from './form-widget.model';
import { TabModel } from './tab.model';
import {
FORM_FIELD_VALIDATORS,
FormFieldValidator
} from './form-field-validator';
export class FormModel {
static UNSET_TASK_NAME: string = 'Nameless task';
static SAVE_OUTCOME: string = '$save';
static COMPLETE_OUTCOME: string = '$complete';
static START_PROCESS_OUTCOME: string = '$startProcess';
readonly id: string;
readonly name: string;
readonly taskId: string;
readonly taskName: string = FormModel.UNSET_TASK_NAME;
processDefinitionId: string;
private _isValid: boolean = true;
get isValid(): boolean {
return this._isValid;
}
className: string;
readOnly: boolean = false;
tabs: TabModel[] = [];
/** Stores root containers */
fields: FormWidgetModel[] = [];
outcomes: FormOutcomeModel[] = [];
customFieldTemplates: FormFieldTemplates = {};
fieldValidators: FormFieldValidator[] = [...FORM_FIELD_VALIDATORS];
readonly selectedOutcome: string;
values: FormValues = {};
processVariables: any;
readonly json: any;
hasTabs(): boolean {
return this.tabs && this.tabs.length > 0;
}
hasFields(): boolean {
return this.fields && this.fields.length > 0;
}
hasOutcomes(): boolean {
return this.outcomes && this.outcomes.length > 0;
}
constructor(json?: any, data?: FormValues, readOnly: boolean = false, protected formService?: FormService) {
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;
this.processDefinitionId = json.processDefinitionId;
this.customFieldTemplates = json.customFieldTemplates || {};
this.selectedOutcome = json.selectedOutcome || {};
this.className = json.className || '';
let tabCache: FormWidgetModelCache<TabModel> = {};
this.processVariables = json.processVariables;
this.tabs = (json.tabs || []).map(t => {
let model = new TabModel(this, t);
tabCache[model.id] = model;
return model;
});
this.fields = this.parseRootFields(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(field);
}
}
}
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 startProcessOutcome = new FormOutcomeModel(this, { id: FormModel.START_PROCESS_OUTCOME, name: 'Start Process', isSystem: true });
let customOutcomes = (json.outcomes || []).map(obj => new FormOutcomeModel(this, obj));
this.outcomes = [saveOutcome].concat(
customOutcomes.length > 0 ? customOutcomes : [completeOutcome, startProcessOutcome]
);
}
}
this.validateForm();
}
onFormFieldChanged(field: FormFieldModel) {
this.validateField(field);
if (this.formService) {
this.formService.formFieldValueChanged.next(new FormFieldEvent(this, field));
}
}
getFieldById(fieldId: string): FormFieldModel {
return this.getFormFields().find(field => field.id === fieldId);
}
// TODO: consider evaluating and caching once the form is loaded
getFormFields(): FormFieldModel[] {
let result: FormFieldModel[] = [];
for (let i = 0; i < this.fields.length; i++) {
let field = this.fields[i];
if (field instanceof ContainerModel) {
let container = <ContainerModel> field;
result.push(container.field);
container.field.columns.forEach((column) => {
result.push(...column.fields);
});
}
}
return result;
}
markAsInvalid() {
this._isValid = false;
}
/**
* Validates entire form and all form fields.
*
* @returns {void}
* @memberof FormModel
*/
validateForm(): void {
const validateFormEvent = new ValidateFormEvent(this);
if (this.formService) {
this.formService.validateForm.next(validateFormEvent);
}
this._isValid = validateFormEvent.isValid;
if (validateFormEvent.defaultPrevented) {
return;
}
if (validateFormEvent.isValid) {
let fields = this.getFormFields();
for (let i = 0; i < fields.length; i++) {
if (!fields[i].validate()) {
this._isValid = false;
return;
}
}
}
}
/**
* Validates a specific form field, triggers form validation.
*
* @param {FormFieldModel} field Form field to validate.
* @returns {void}
* @memberof FormModel
*/
validateField(field: FormFieldModel): void {
if (!field) {
return;
}
const validateFieldEvent = new ValidateFormFieldEvent(this, field);
if (this.formService) {
this.formService.validateFormField.next(validateFieldEvent);
}
if (!validateFieldEvent.isValid) {
this._isValid = false;
return;
}
if (validateFieldEvent.defaultPrevented) {
return;
}
if (!field.validate()) {
this._isValid = false;
return;
}
this.validateForm();
}
// Activiti supports 3 types of root fields: container|group|dynamic-table
private parseRootFields(json: any): FormWidgetModel[] {
let fields = [];
if (json.fields) {
fields = json.fields;
} else if (json.formDefinition && json.formDefinition.fields) {
fields = json.formDefinition.fields;
}
let result: FormWidgetModel[] = [];
for (let field of fields) {
if (field.type === FormFieldTypes.DISPLAY_VALUE) {
// workaround for dynamic table on a completed/readonly form
if (field.params) {
let originalField = field.params['field'];
if (originalField.type === FormFieldTypes.DYNAMIC_TABLE) {
result.push(new ContainerModel(new FormFieldModel(this, field)));
}
}
} else {
result.push(new ContainerModel(new FormFieldModel(this, field)));
}
}
return result;
}
// 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 field of this.getFormFields()) {
if (data[field.id]) {
field.json.value = data[field.id];
field.value = field.parseValue(field.json);
if (field.type === FormFieldTypes.DROPDOWN ||
field.type === FormFieldTypes.RADIO_BUTTONS) {
field.value = data[field.id].id;
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export class GroupModel {
externalId: string;
groups: any;
id: string;
name: string;
status: string;
constructor(json?: any) {
if (json) {
this.externalId = json.externalId;
this.groups = json.groups;
this.id = json.id;
this.name = json.name;
this.status = json.status;
}
}
}

View File

@@ -0,0 +1,35 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export * from './form-field-metadata';
export * from './form-values';
export * from './form-field-types';
export * from './form-field-option';
export * from './form-field-templates';
export * from './form-widget.model';
export * from './form-field.model';
export * from './form.model';
export * from './container.model';
export * from './container-column.model';
export * from './tab.model';
export * from './form-outcome.model';
export * from './form-outcome-event.model';
export * from './form-field-validator';
export * from './content-link.model';
export * from './error-message.model';

View File

@@ -0,0 +1,74 @@
/*!
* @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 { ContainerModel } from './container.model';
import { FormModel } from './form.model';
import { FormFieldModel } from './../core/form-field.model';
import { TabModel } from './tab.model';
describe('TabModel', () => {
it('should setup with json config', () => {
let json = {
id: '<id>',
title: '<title>',
visibilityCondition: '<condition>'
};
let model = new TabModel(null, json);
expect(model.id).toBe(json.id);
expect(model.title).toBe(json.title);
expect(model.isVisible).toBe(true);
});
it('should not setup with json config', () => {
let model = new TabModel(null, null);
expect(model.id).toBeUndefined();
expect(model.title).toBeUndefined();
expect(model.isVisible).toBeDefined();
expect(model.isVisible).toBe(true);
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();
let form = new FormModel();
let field = new FormFieldModel(form);
model.fields = [new ContainerModel(field)];
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);
});
});

View File

@@ -0,0 +1,44 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { WidgetVisibilityModel } from '../../../models/widget-visibility.model';
import { FormWidgetModel } from './form-widget.model';
import { FormModel } from './form.model';
export class TabModel extends FormWidgetModel {
title: string;
isVisible: boolean = true;
visibilityCondition: WidgetVisibilityModel;
fields: FormWidgetModel[] = [];
hasContent(): boolean {
return this.fields && this.fields.length > 0;
}
constructor(form: FormModel, json?: any) {
super(form, json);
if (json) {
this.title = json.title;
this.visibilityCondition = <WidgetVisibilityModel> json.visibilityCondition;
}
}
}

View File

@@ -0,0 +1,21 @@
<div class="{{field.className}}" *ngIf="field?.isVisible" id="data-widget" [class.adf-invalid]="!field.isValid || field.validationSummary.message">
<mat-form-field class="adf-date-widget">
<label class="adf-label" [attr.for]="field.id">{{field.name}} ({{field.dateDisplayFormat}})<span *ngIf="isRequired()">*</span></label>
<input matInput
[id]="field.id"
[matDatepicker]="datePicker"
[(ngModel)]="displayDate"
[required]="isRequired()"
[disabled]="field.readOnly"
[min]="minDate"
[max]="maxDate"
(focusout)="onDateChanged($event.srcElement.value)"
(dateChange)="onDateChanged($event)"
placeholder="{{field.placeholder}}">
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly" ></mat-datepicker-toggle>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<mat-datepicker #datePicker [touchUi]="true" [startAt]="startAt" ></mat-datepicker>
</div>

View File

@@ -0,0 +1,34 @@
@import '../form';
.adf {
&-date-widget {
.mat-input-suffix {
text-align: right;
position: absolute;
margin-top: 30px;
width: 100%;
}
&-date-widget-button {
position: relative;
float: right;
}
&-date-input {
padding-top: 5px;
padding-bottom: 5px;
}
&-grid-date-widget {
align-items: center;
padding: 0;
}
&-date-widget-button__cell {
margin-top: 0;
margin-bottom: 0;
}
}
}

View File

@@ -0,0 +1,232 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import * as moment from 'moment';
import { ActivitiContentService } from '../../../services/activiti-alfresco.service';
import { MaterialModule } from '../../../../material.module';
import { ErrorWidgetComponent } from '../error/error.component';
import { EcmModelService } from './../../../services/ecm-model.service';
import { FormService } from './../../../services/form.service';
import { FormFieldModel } from './../core/form-field.model';
import { FormModel } from './../core/form.model';
import { DateWidgetComponent } from './date.widget';
describe('DateWidgetComponent', () => {
let widget: DateWidgetComponent;
let fixture: ComponentFixture<DateWidgetComponent>;
let nativeElement: any;
let element: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule
],
declarations: [
DateWidgetComponent,
ErrorWidgetComponent
],
providers: [
FormService,
ActivitiContentService,
EcmModelService
]
}).compileComponents();
}));
beforeEach(() => {
nativeElement = {
querySelector: function () {
return null;
}
};
fixture = TestBed.createComponent(DateWidgetComponent);
element = fixture.nativeElement;
widget = fixture.componentInstance;
});
it('should setup min value for date picker', () => {
let minValue = '13-03-1982';
widget.field = new FormFieldModel(null, {
id: 'date-id',
name: 'date-name',
minValue: minValue
});
widget.ngOnInit();
let expected = moment(minValue, widget.field.dateDisplayFormat);
expect(widget.minDate.isSame(expected)).toBeTruthy();
});
it('should date field be present', () => {
let minValue = '13-03-1982';
widget.field = new FormFieldModel(null, {
minValue: minValue
});
widget.ngOnInit();
expect(element.querySelector('#dropdown-id')).toBeDefined();
});
it('should setup max value for date picker', () => {
let maxValue = '31-03-1982';
widget.field = new FormFieldModel(null, {
maxValue: maxValue
});
widget.ngOnInit();
let expected = moment(maxValue, widget.field.dateDisplayFormat);
expect(widget.maxDate.isSame(expected)).toBeTruthy();
});
it('should eval visibility on date changed', () => {
spyOn(widget, 'checkVisibility').and.callThrough();
let field = new FormFieldModel(new FormModel(), {
id: 'date-field-id',
name: 'date-name',
value: '9-9-9999',
type: 'date',
readOnly: 'false'
});
widget.field = field;
widget.onDateChanged({ value: moment('12/12/2012') });
expect(widget.checkVisibility).toHaveBeenCalledWith(field);
});
describe('template check', () => {
beforeEach(() => {
widget.field = new FormFieldModel(new FormModel(), {
id: 'date-field-id',
name: 'date-name',
value: '9-9-9999',
type: 'date',
readOnly: 'false'
});
widget.field.isVisible = true;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
it('should show visible date widget', async(() => {
fixture.whenStable()
.then(() => {
expect(element.querySelector('#date-field-id')).toBeDefined();
expect(element.querySelector('#date-field-id')).not.toBeNull();
let dateElement: any = element.querySelector('#date-field-id');
expect(dateElement.value).toContain('9-9-9999');
});
}));
it('should check correctly the min value with different formats', async(() => {
widget.field.value = '11-30-9999';
widget.field.dateDisplayFormat = 'MM-DD-YYYY';
widget.field.minValue = '30-12-9999';
widget.ngOnInit();
widget.field.validate();
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#date-field-id')).toBeDefined();
expect(element.querySelector('#date-field-id')).not.toBeNull();
let dateElement: any = element.querySelector('#date-field-id');
expect(dateElement.value).toContain('11-30-9999');
expect(element.querySelector('.adf-error-text').textContent).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
});
}));
it('should show the correct format type', async(() => {
widget.field.value = '12-30-9999';
widget.field.dateDisplayFormat = 'MM-DD-YYYY';
widget.ngOnInit();
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#date-field-id')).toBeDefined();
expect(element.querySelector('#date-field-id')).not.toBeNull();
let dateElement: any = element.querySelector('#date-field-id');
expect(dateElement.value).toContain('12-30-9999');
});
}));
it('should hide not visible date widget', async(() => {
widget.field.isVisible = false;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
fixture.detectChanges();
expect(element.querySelector('#data-widget')).toBeNull();
});
}));
it('should become visibile if the visibility change to true', async(() => {
widget.field.isVisible = false;
fixture.detectChanges();
widget.fieldChanged.subscribe((field) => {
field.isVisible = true;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#date-field-id')).toBeDefined();
expect(element.querySelector('#date-field-id')).not.toBeNull();
let dateElement: any = element.querySelector('#date-field-id');
expect(dateElement.value).toContain('9-9-9999');
});
});
widget.checkVisibility(widget.field);
}));
it('should be hided if the visibility change to false', async(() => {
widget.fieldChanged.subscribe((field) => {
field.isVisible = false;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#data-widget')).toBeNull();
});
});
widget.checkVisibility(widget.field);
}));
it('should disable date button when is readonly', async(() => {
widget.field.readOnly = false;
fixture.detectChanges();
let dateButton = <HTMLButtonElement> element.querySelector('button');
expect(dateButton.disabled).toBeFalsy();
widget.field.readOnly = true;
fixture.detectChanges();
dateButton = <HTMLButtonElement> element.querySelector('button');
expect(dateButton.disabled).toBeTruthy();
}));
});
});

View File

@@ -0,0 +1,82 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { UserPreferencesService } from '../../../../services';
import { MOMENT_DATE_FORMATS, MomentDateAdapter } from '../../../../utils';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material';
import * as moment from 'moment';
import { Moment } from 'moment';
import { FormService } from './../../../services/form.service';
import { baseHost, WidgetComponent } from './../widget.component';
@Component({
selector: 'date-widget',
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter },
{ provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS }],
templateUrl: './date.widget.html',
styleUrls: ['./date.widget.scss'],
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class DateWidgetComponent extends WidgetComponent implements OnInit {
minDate: Moment;
maxDate: Moment;
displayDate: Moment;
constructor(public formService: FormService,
private dateAdapter: DateAdapter<Moment>,
private preferences: UserPreferencesService) {
super(formService);
}
ngOnInit() {
this.preferences.locale$.subscribe((locale) => {
this.dateAdapter.setLocale(locale);
});
let momentDateAdapter = <MomentDateAdapter> this.dateAdapter;
momentDateAdapter.overrideDisplyaFormat = this.field.dateDisplayFormat;
if (this.field) {
if (this.field.minValue) {
this.minDate = moment(this.field.minValue, 'DD/MM/YYYY');
}
if (this.field.maxValue) {
this.maxDate = moment(this.field.maxValue, 'DD/MM/YYYY');
}
}
this.displayDate = moment(this.field.value, this.field.dateDisplayFormat);
}
onDateChanged(newDateValue) {
if (newDateValue && newDateValue.value) {
this.field.value = newDateValue.value.format(this.field.dateDisplayFormat);
} else if (newDateValue) {
this.field.value = newDateValue;
} else {
this.field.value = null;
}
this.checkVisibility(this.field);
}
}

View File

@@ -0,0 +1 @@
<div class="adf-display-text-widget {{field.className}}">{{field.value}}</div>

View File

@@ -0,0 +1,3 @@
.adf-display-text-widget {
white-space: pre-wrap;
}

View File

@@ -0,0 +1,37 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { Component, ViewEncapsulation } from '@angular/core';
import { FormService } from './../../../services/form.service';
import { baseHost , WidgetComponent } from './../widget.component';
@Component({
selector: 'display-text-widget',
templateUrl: './display-text.widget.html',
styleUrls: ['./display-text.widget.scss'],
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class DisplayTextWidgetComponentComponent extends WidgetComponent {
constructor(public formService: FormService) {
super(formService);
}
}

View File

@@ -0,0 +1,5 @@
<div class="adf-form-document-widget {{field.className}}">
<ng-container *ngIf="hasFile">
<adf-content [id]="fileId" [showDocumentContent]="true"></adf-content>
</ng-container>
</div>

View File

@@ -0,0 +1,50 @@
/*!
* @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 { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from './../../../services/form.service';
import { baseHost , WidgetComponent } from './../widget.component';
@Component({
selector: 'adf-form-document-widget',
templateUrl: 'document.widget.html',
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class DocumentWidgetComponent extends WidgetComponent implements OnInit {
fileId: string = null;
hasFile: boolean = false;
constructor(public formService: FormService) {
super(formService);
}
ngOnInit() {
if (this.field) {
const file = this.field.value;
if (file) {
this.fileId = file.id;
this.hasFile = true;
} else {
this.fileId = null;
this.hasFile = false;
}
}
}
}

View File

@@ -0,0 +1,20 @@
<div class="adf-dropdown-widget {{field.className}}"
[class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly" *ngIf="field?.isVisible">
<label class="adf-label" [attr.for]="field.id">{{field.name}}<span *ngIf="isRequired()">*</span></label>
<mat-form-field>
<mat-select class="adf-select"
[id]="field.id"
[(ngModel)]="field.value"
[disabled]="field.readOnly"
(ngModelChange)="checkVisibility(field)">
<mat-option *ngFor="let opt of field.options"
[value]="getOptionValue(opt, field.value)"
[id]="opt.id">{{opt.name}}
</mat-option>
<mat-option id="readonlyOption" *ngIf="isReadOnlyType()" [value]="field.value">{{field.value}}</mat-option>
</mat-select>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget class="adf-dropdown-required-message" *ngIf="isInvalidFieldRequired()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -0,0 +1,27 @@
@import '../form';
.adf {
&-dropdown-widget {
width: 100%;
margin-top: 13px;
.adf-select{
padding-top: 0 !important;
width: 100%;
}
.mat-select-value-text{
font-size: 14px;
}
&-select {
width: 100%;
}
&-dropdown-required-message .adf-error-text-container{
margin-top: 1px !important;
}
}
}

View File

@@ -0,0 +1,312 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Rx';
import { EcmModelService } from '../../../services/ecm-model.service';
import { FormService } from '../../../services/form.service';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { MaterialModule } from '../../../../material.module';
import { ErrorWidgetComponent } from '../error/error.component';
import { FormFieldOption } from './../core/form-field-option';
import { FormFieldModel } from './../core/form-field.model';
import { FormModel } from './../core/form.model';
import { DropdownWidgetComponent } from './dropdown.widget';
describe('DropdownWidgetComponent', () => {
function openSelect() {
const dropdown = fixture.debugElement.query(By.css('[class="mat-select-trigger"]'));
dropdown.triggerEventHandler('click', null);
fixture.detectChanges();
}
let formService: FormService;
let widget: DropdownWidgetComponent;
let visibilityService: WidgetVisibilityService;
let fixture: ComponentFixture<DropdownWidgetComponent>;
let element: HTMLElement;
let fakeOptionList: FormFieldOption[] = [
{id: 'opt_1', name: 'option_1'},
{id: 'opt_2', name: 'option_2'},
{id: 'opt_3', name: 'option_3'}];
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule
],
declarations: [DropdownWidgetComponent, ErrorWidgetComponent],
providers: [FormService, EcmModelService, WidgetVisibilityService]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(DropdownWidgetComponent);
widget = fixture.componentInstance;
element = fixture.nativeElement;
formService = TestBed.get(FormService);
visibilityService = TestBed.get(WidgetVisibilityService);
widget.field = new FormFieldModel(new FormModel());
});
}));
it('should require field with restUrl', () => {
spyOn(formService, 'getRestFieldValues').and.stub();
widget.field = null;
widget.ngOnInit();
expect(formService.getRestFieldValues).not.toHaveBeenCalled();
widget.field = new FormFieldModel(null, {restUrl: null});
widget.ngOnInit();
expect(formService.getRestFieldValues).not.toHaveBeenCalled();
});
it('should request field values from service', () => {
const taskId = '<form-id>';
const fieldId = '<field-id>';
let form = new FormModel({
taskId: taskId
});
widget.field = new FormFieldModel(form, {
id: fieldId,
restUrl: '<url>'
});
spyOn(formService, 'getRestFieldValues').and.returnValue(
Observable.create(observer => {
observer.next(null);
observer.complete();
})
);
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalledWith(taskId, fieldId);
});
it('should preserve empty option when loading fields', () => {
let restFieldValue: FormFieldOption = <FormFieldOption> {id: '1', name: 'Option1'};
spyOn(formService, 'getRestFieldValues').and.callFake(() => {
return Observable.create(observer => {
observer.next([restFieldValue]);
observer.complete();
});
});
let form = new FormModel({taskId: '<id>'});
let emptyOption: FormFieldOption = <FormFieldOption> {id: 'empty', name: 'Empty'};
widget.field = new FormFieldModel(form, {
id: '<id>',
restUrl: '/some/url/address',
hasEmptyValue: true,
options: [emptyOption]
});
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalled();
expect(widget.field.options.length).toBe(2);
expect(widget.field.options[0]).toBe(emptyOption);
expect(widget.field.options[1]).toBe(restFieldValue);
});
describe('when template is ready', () => {
describe('and dropdown is populated via taskId', () => {
beforeEach(async(() => {
spyOn(visibilityService, 'refreshVisibility').and.stub();
spyOn(formService, 'getRestFieldValues').and.callFake(() => {
return Observable.of(fakeOptionList);
});
widget.field = new FormFieldModel(new FormModel({taskId: 'fake-task-id'}), {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
widget.field.emptyOption = {id: 'empty', name: 'Choose one...'};
widget.field.isVisible = true;
fixture.detectChanges();
}));
it('should show visible dropdown widget', async(() => {
expect(element.querySelector('#dropdown-id')).toBeDefined();
expect(element.querySelector('#dropdown-id')).not.toBeNull();
openSelect();
const optOne = fixture.debugElement.queryAll(By.css('[id="mat-option-1"]'));
const optTwo = fixture.debugElement.queryAll(By.css('[id="mat-option-2"]'));
const optThree = fixture.debugElement.queryAll(By.css('[id="mat-option-3"]'));
expect(optOne).not.toBeNull();
expect(optTwo).not.toBeNull();
expect(optThree).not.toBeNull();
}));
it('should select the default value when an option is chosen as default', async(() => {
widget.field.value = 'option_2';
widget.ngOnInit();
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('option_2');
expect(dropDownElement.attributes['ng-reflect-model'].textContent).toBe('option_2');
});
}));
it('should select the empty value when no default is chosen', async(() => {
widget.field.value = 'empty';
widget.ngOnInit();
fixture.detectChanges();
openSelect();
fixture.whenStable()
.then(() => {
let dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('empty');
});
}));
it('should be not visible when isVisible is false', async(() => {
widget.field.isVisible = false;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let dropDownElement: HTMLSelectElement = <HTMLSelectElement> element.querySelector('#dropdown-id');
expect(dropDownElement).toBeNull();
});
}));
it('should became visible when isVisible is true', async(() => {
widget.field.isVisible = false;
fixture.detectChanges();
expect(element.querySelector('#dropdown-id')).toBeNull();
widget.field.isVisible = true;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#dropdown-id')).not.toBeNull();
});
}));
});
describe('and dropdown is populated via processDefinitionId', () => {
beforeEach(async(() => {
spyOn(visibilityService, 'refreshVisibility').and.stub();
spyOn(formService, 'getRestFieldValuesByProcessId').and.callFake(() => {
return Observable.of(fakeOptionList);
});
widget.field = new FormFieldModel(new FormModel({processDefinitionId: 'fake-process-id'}), {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
widget.field.emptyOption = {id: 'empty', name: 'Choose one...'};
widget.field.isVisible = true;
fixture.detectChanges();
}));
it('should show visible dropdown widget', async(() => {
expect(element.querySelector('#dropdown-id')).toBeDefined();
expect(element.querySelector('#dropdown-id')).not.toBeNull();
openSelect();
const optOne = fixture.debugElement.queryAll(By.css('[id="mat-option-1"]'));
const optTwo = fixture.debugElement.queryAll(By.css('[id="mat-option-2"]'));
const optThree = fixture.debugElement.queryAll(By.css('[id="mat-option-3"]'));
expect(optOne).not.toBeNull();
expect(optTwo).not.toBeNull();
expect(optThree).not.toBeNull();
}));
it('should select the default value when an option is chosen as default', async(() => {
widget.field.value = 'option_2';
widget.ngOnInit();
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('option_2');
expect(dropDownElement.attributes['ng-reflect-model'].textContent).toBe('option_2');
});
}));
it('should select the empty value when no default is chosen', async(() => {
widget.field.value = 'empty';
widget.ngOnInit();
fixture.detectChanges();
openSelect();
fixture.whenStable()
.then(() => {
let dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('empty');
});
}));
it('should be disabled when the field is readonly', async(() => {
widget.field = new FormFieldModel(new FormModel({processDefinitionId: 'fake-process-id'}), {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'true',
restUrl: 'fake-rest-url'
});
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let dropDownElement: HTMLSelectElement = <HTMLSelectElement> element.querySelector('#dropdown-id');
expect(dropDownElement).not.toBeNull();
expect(dropDownElement.getAttribute('aria-disabled')).toBe('true');
});
}));
it('should show the option value when the field is readonly', async(() => {
widget.field = new FormFieldModel(new FormModel({ processDefinitionId: 'fake-process-id' }), {
id: 'dropdown-id',
name: 'date-name',
type: 'readonly',
value: 'FakeValue',
readOnly: true,
params: { field: { name: 'date-name', type: 'dropdown' } }
});
openSelect();
fixture.whenStable()
.then(() => {
fixture.detectChanges();
expect(element.querySelector('#dropdown-id')).not.toBeNull();
const option = fixture.debugElement.query(By.css('.mat-option')).nativeElement;
expect(option.innerText).toEqual('FakeValue');
});
}));
});
});
});

View File

@@ -0,0 +1,112 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { LogService } from '../../../../services';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { FormFieldOption } from './../core/form-field-option';
import { baseHost , WidgetComponent } from './../widget.component';
@Component({
selector: 'dropdown-widget',
templateUrl: './dropdown.widget.html',
styleUrls: ['./dropdown.widget.scss'],
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class DropdownWidgetComponent extends WidgetComponent implements OnInit {
constructor(public formService: FormService,
private visibilityService: WidgetVisibilityService,
private logService: LogService) {
super(formService);
}
ngOnInit() {
if (this.field && this.field.restUrl) {
if (this.field.form.taskId) {
this.getValuesByTaskId();
} else {
this.getValuesByProcessDefinitionId();
}
}
}
getValuesByTaskId() {
this.formService
.getRestFieldValues(
this.field.form.taskId,
this.field.id
)
.subscribe(
(result: FormFieldOption[]) => {
let options = [];
if (this.field.emptyOption) {
options.push(this.field.emptyOption);
}
this.field.options = options.concat((result || []));
this.field.updateForm();
},
err => this.handleError(err)
);
}
getValuesByProcessDefinitionId() {
this.formService
.getRestFieldValuesByProcessId(
this.field.form.processDefinitionId,
this.field.id
)
.subscribe(
(result: FormFieldOption[]) => {
let options = [];
if (this.field.emptyOption) {
options.push(this.field.emptyOption);
}
this.field.options = options.concat((result || []));
this.field.updateForm();
},
err => this.handleError(err)
);
}
getOptionValue(option: FormFieldOption, fieldValue: string): string {
let optionValue: string = '';
if (option.id === 'empty' || option.name !== fieldValue) {
optionValue = option.id;
} else {
optionValue = option.name;
}
return optionValue;
}
checkVisibility() {
this.visibilityService.refreshVisibility(this.field.form);
}
handleError(error: any) {
this.logService.error(error);
}
isReadOnlyType(): boolean {
return this.field.type === 'readonly' ? true : false;
}
}

View File

@@ -0,0 +1,29 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
export interface CellValidator {
isSupported(column: DynamicTableColumn): boolean;
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean;
}

View File

@@ -0,0 +1,52 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import * as moment from 'moment';
import { CellValidator } from './cell-validator.model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
export class DateCellValidator implements CellValidator {
private supportedTypes: string[] = [
'Date'
];
isSupported(column: DynamicTableColumn): boolean {
return column && column.editable && this.supportedTypes.indexOf(column.type) > -1;
}
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean {
if (this.isSupported(column)) {
let value = row.value[column.id];
let dateValue = moment(value, 'D-M-YYYY');
if (!dateValue.isValid()) {
if (summary) {
summary.isValid = false;
summary.text = `Invalid '${column.name}' format.`;
}
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,25 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export interface DynamicRowValidationSummary {
isValid: boolean;
text: string;
}

View File

@@ -0,0 +1,24 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
// maps to: com.activiti.model.editor.form.OptionRepresentation
export interface DynamicTableColumnOption {
id: string;
name: string;
}

View File

@@ -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.
*/
/* tslint:disable:component-selector */
import { DynamicTableColumnOption } from './dynamic-table-column-option.model';
// maps to: com.activiti.model.editor.form.ColumnDefinitionRepresentation
export interface DynamicTableColumn {
id: string;
name: string;
type: string;
value: any;
optionType: string;
options: DynamicTableColumnOption[];
restResponsePath: string;
restUrl: string;
restIdProperty: string;
restLabelProperty: string;
amountCurrency: string;
amountEnableFractions: boolean;
required: boolean;
editable: boolean;
sortable: boolean;
visible: boolean;
// TODO: com.activiti.domain.idm.EndpointConfiguration.EndpointConfigurationRepresentation
endpoint: any;
// TODO: com.activiti.model.editor.form.RequestHeaderRepresentation
requestHeaders: any;
}

View File

@@ -0,0 +1,24 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
export interface DynamicTableRow {
isNew: boolean;
selected: boolean;
value: any;
}

View File

@@ -0,0 +1,66 @@
<div class="{{field.className}}"
[class.adf-invalid]="!isValid()" *ngIf="field?.isVisible">
<div class="adf-label">{{content.name}}<span *ngIf="isRequired()">*</span></div>
<div *ngIf="!editMode">
<div class="adf-table-container">
<table class="full-width adf-dynamic-table" id="dynamic-table-{{content.id}}">
<thead>
<tr>
<th *ngFor="let column of content.visibleColumns">
{{column.name}}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of content.rows; let idx = index" tabindex="0" id="{{content.id}}-row-{{idx}}"
[class.adf-dynamic-table-widget__row-selected]="row.selected" (keyup)="onKeyPressed($event, row)">
<td *ngFor="let column of content.visibleColumns"
(click)="onRowClicked(row)">
{{ getCellValue(row, column) }}
</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="!readOnly">
<button mat-button
[disabled]="!hasSelection()"
(click)="moveSelectionUp()">
<mat-icon>arrow_upward</mat-icon>
</button>
<button mat-button
[disabled]="!hasSelection()"
(click)="moveSelectionDown()">
<mat-icon>arrow_downward</mat-icon>
</button>
<button mat-button
[disabled]="field.readOnly"
id="{{content.id}}-add-row"
(click)="addNewRow()">
<mat-icon>add_circle_outline</mat-icon>
</button>
<button mat-button
[disabled]="!hasSelection()"
(click)="deleteSelection()">
<mat-icon>remove_circle_outline</mat-icon>
</button>
<button mat-button
[disabled]="!hasSelection()"
(click)="editSelection()">
<mat-icon>edit</mat-icon>
</button>
</div>
</div>
<row-editor *ngIf="editMode"
[table]="content"
[row]="editRow"
[column]="column"
(save)="onSaveChanges()"
(cancel)="onCancelChanges()">
</row-editor>
<error-widget [error]="field.validationSummary" ></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -0,0 +1,203 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import * as moment from 'moment';
import { ValidateDynamicTableRowEvent } from '../../../events/validate-dynamic-table-row.event';
import { FormService } from './../../../services/form.service';
import { FormFieldModel } from './../core/form-field.model';
import { FormWidgetModel } from './../core/form-widget.model';
import { CellValidator } from './cell-validator.model';
import { DateCellValidator } from './date-cell-validator-model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { NumberCellValidator } from './number-cell-validator.model';
import { RequiredCellValidator } from './required-cell-validator.model';
export class DynamicTableModel extends FormWidgetModel {
field: FormFieldModel;
columns: DynamicTableColumn[] = [];
visibleColumns: DynamicTableColumn[] = [];
rows: DynamicTableRow[] = [];
private _selectedRow: DynamicTableRow;
private _validators: CellValidator[] = [];
get selectedRow(): DynamicTableRow {
return this._selectedRow;
}
set selectedRow(value: DynamicTableRow) {
if (this._selectedRow && this._selectedRow === value) {
this._selectedRow.selected = false;
this._selectedRow = null;
return;
}
this.rows.forEach(row => row.selected = false);
this._selectedRow = value;
if (value) {
this._selectedRow.selected = true;
}
}
constructor(field: FormFieldModel, private formService: FormService) {
super(field.form, field.json);
this.field = field;
if (field.json) {
const columns = this.getColumns(field);
if (columns) {
this.columns = columns;
this.visibleColumns = this.columns.filter(col => col.visible);
}
if (field.json.value) {
this.rows = field.json.value.map(obj => <DynamicTableRow> {selected: false, value: obj});
}
}
this._validators = [
new RequiredCellValidator(),
new DateCellValidator(),
new NumberCellValidator()
];
}
private getColumns(field: FormFieldModel): DynamicTableColumn[] {
if (field && field.json) {
let definitions = field.json.columnDefinitions;
if (!definitions && field.json.params && field.json.params.field) {
definitions = field.json.params.field.columnDefinitions;
}
if (definitions) {
return definitions.map(obj => <DynamicTableColumn> obj);
}
}
return null;
}
flushValue() {
if (this.field) {
this.field.value = this.rows.map(r => r.value);
this.field.updateForm();
}
}
moveRow(row: DynamicTableRow, offset: number) {
let oldIndex = this.rows.indexOf(row);
if (oldIndex > -1) {
let newIndex = (oldIndex + offset);
if (newIndex < 0) {
newIndex = 0;
} else if (newIndex >= this.rows.length) {
newIndex = this.rows.length;
}
let arr = this.rows.slice();
arr.splice(oldIndex, 1);
arr.splice(newIndex, 0, row);
this.rows = arr;
this.flushValue();
}
}
deleteRow(row: DynamicTableRow) {
if (row) {
if (this.selectedRow === row) {
this.selectedRow = null;
}
let idx = this.rows.indexOf(row);
if (idx > -1) {
this.rows.splice(idx, 1);
this.flushValue();
}
}
}
addRow(row: DynamicTableRow) {
if (row) {
this.rows.push(row);
// this.selectedRow = row;
}
}
validateRow(row: DynamicTableRow): DynamicRowValidationSummary {
const summary = <DynamicRowValidationSummary> {
isValid: true,
text: null
};
const event = new ValidateDynamicTableRowEvent(this.form, this.field, row, summary);
this.formService.validateDynamicTableRow.next(event);
if (event.defaultPrevented || !summary.isValid) {
return summary;
}
if (row) {
for (let col of this.columns) {
for (let validator of this._validators) {
if (!validator.validate(row, col, summary)) {
return summary;
}
}
}
}
return summary;
}
getCellValue(row: DynamicTableRow, column: DynamicTableColumn): any {
let result = row.value[column.id];
if (column.type === 'Dropdown') {
if (result) {
return result.name;
}
}
if (column.type === 'Boolean') {
return result ? true : false;
}
if (column.type === 'Date') {
if (result) {
return moment(result.split('T')[0], 'YYYY-MM-DD').format('DD-MM-YYYY');
}
}
return result || '';
}
getDisplayText(column: DynamicTableColumn): string {
let result = column.name;
if (column.type === 'Amount') {
let currency = column.amountCurrency || '$';
result = `${column.name} (${currency})`;
}
return result;
}
}

View File

@@ -0,0 +1,161 @@
@import '../form';
@mixin mat-dynamic-table-theme($theme) {
$foreground: map-get($theme, foreground);
$dynamic-table-font-size: 14px !default;
$dynamic-table-header-font-size: 12px !default;
$dynamic-table-header-sort-icon-size: 16px !default;
$dynamic-table-header-color: mat-color($foreground, text) !default;
$dynamic-table-header-sorted-color: mat-color($foreground, text) !default;
$dynamic-table-header-sorted-icon-hover-color: mat-color($foreground, disabled-text) !default;
$dynamic-table-divider-color: mat-color($foreground, text, .07) !default;
$dynamic-table-hover-color: #eeeeee !default;
$dynamic-table-selection-color: #e0f7fa !default;
$dynamic-table-dividers: 1px solid $dynamic-table-divider-color !default;
$dynamic-table-row-height: 56px !default;
$dynamic-table-column-spacing: 36px !default;
$dynamic-table-column-padding: $dynamic-table-column-spacing / 2;
$dynamic-table-card-padding: 24px !default;
$dynamic-table-cell-top: $dynamic-table-card-padding / 2;
$dynamic-table-drag-border: 1px dashed rgb(68, 138, 255);
.adf {
&-dynamic-table {
width: 100%;
position: relative;
border: $dynamic-table-dividers;
white-space: nowrap;
font-size: $dynamic-table-font-size;
/* Firefox fixes */
border-collapse: unset;
border-spacing: 0;
thead {
padding-bottom: 3px;
}
tbody {
tr {
position: relative;
height: $dynamic-table-row-height;
@include material-animation-default(0.28s);
transition-property: background-color;
&:hover {
background-color: $dynamic-table-hover-color;
}
&.is-selected, &.is-selected:hover {
background-color: $dynamic-table-selection-color;
}
&:focus {
outline-offset: -1px;
outline-width: 1px;
outline-color: rgb(68, 138, 255);
outline-style: solid;
}
}
}
td, th {
padding: 0 $dynamic-table-column-padding 12px $dynamic-table-column-padding;
text-align: center;
&:first-of-type {
padding-left: 24px;
}
&:last-of-type {
padding-right: 24px;
}
}
td {
color: mat-color($foreground, text);
position: relative;
vertical-align: middle;
height: $dynamic-table-row-height;
border-top: $dynamic-table-dividers;
border-bottom: $dynamic-table-dividers;
padding-top: $dynamic-table-cell-top;
box-sizing: border-box;
@include no-select;
cursor: default;
}
th {
@include no-select;
cursor: pointer;
position: relative;
vertical-align: bottom;
text-overflow: ellipsis;
font-weight: bold;
line-height: 24px;
letter-spacing: 0;
height: $dynamic-table-row-height;
font-size: $dynamic-table-header-font-size;
color: $dynamic-table-header-color;
padding-bottom: 8px;
box-sizing: border-box;
&.sortable {
@include no-select;
&:hover {
cursor: pointer;
}
}
&.adf-dynamic-table__header--sorted-asc,
&.adf-dynamic-table__header--sorted-desc {
color: $dynamic-table-header-sorted-color;
&:before {
@include typo-icon;
font-size: $dynamic-table-header-sort-icon-size;
content: "\e5d8";
margin-right: 5px;
vertical-align: sub;
}
&:hover {
cursor: pointer;
&:before {
color: $dynamic-table-header-sorted-icon-hover-color;
}
}
}
&.adf-dynamic-table__header--sorted-desc:before {
content: "\e5db";
}
}
.adf-dynamic-table-cell {
text-align: left;
cursor: default;
&--text {
text-align: left;
}
&--number {
text-align: right;
}
&--image {
text-align: left;
img {
width: 24px;
height: 24px;
}
}
}
.full-width {
width: 100%;
}
}
}
}

View File

@@ -0,0 +1,390 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LogService } from '../../../../services';
import { ActivitiContentService } from '../../../services/activiti-alfresco.service';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { MaterialModule } from '../../../../material.module';
import { ErrorWidgetComponent } from '../error/error.component';
import { EcmModelService } from './../../../services/ecm-model.service';
import { FormService } from './../../../services/form.service';
import { FormFieldModel, FormFieldTypes, FormModel } from './../core/index';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { DynamicTableWidgetComponent } from './dynamic-table.widget';
import { DynamicTableModel } from './dynamic-table.widget.model';
import { BooleanEditorComponent } from './editors/boolean/boolean.editor';
import { DateEditorComponent } from './editors/date/date.editor';
import { DropdownEditorComponent } from './editors/dropdown/dropdown.editor';
import { RowEditorComponent } from './editors/row.editor';
import { TextEditorComponent } from './editors/text/text.editor';
let fakeFormField = {
id: 'fake-dynamic-table',
name: 'fake-label',
value: [{1: 1, 2: 2, 3: 4}],
required: false,
readOnly: false,
overrideId: false,
colspan: 1,
placeholder: null,
minLength: 0,
maxLength: 0,
params: {
existingColspan: 1,
maxColspan: 1
},
sizeX: 2,
sizeY: 2,
row: -1,
col: -1,
columnDefinitions: [
{
id: 1,
name: 1,
type: 'String',
visible: true
},
{
id: 2,
name: 2,
type: 'String',
visible: true
},
{
id: 3,
name: 3,
type: 'String',
visible: true
}
]
};
describe('DynamicTableWidgetComponent', () => {
let widget: DynamicTableWidgetComponent;
let fixture: ComponentFixture<DynamicTableWidgetComponent>;
let element: HTMLElement;
let table: DynamicTableModel;
let logService: LogService;
let formService: FormService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule
],
declarations: [DynamicTableWidgetComponent, RowEditorComponent,
DropdownEditorComponent, DateEditorComponent, BooleanEditorComponent,
TextEditorComponent, ErrorWidgetComponent],
providers: [
FormService,
LogService,
ActivitiContentService,
EcmModelService,
WidgetVisibilityService
]
}).compileComponents();
}));
beforeEach(() => {
const field = new FormFieldModel(new FormModel());
logService = TestBed.get(LogService);
formService = TestBed.get(FormService);
table = new DynamicTableModel(field, formService);
let changeDetectorSpy = jasmine.createSpyObj('cd', ['detectChanges']);
let nativeElementSpy = jasmine.createSpyObj('nativeElement', ['querySelector']);
changeDetectorSpy.nativeElement = nativeElementSpy;
let elementRefSpy = jasmine.createSpyObj('elementRef', ['']);
elementRefSpy.nativeElement = nativeElementSpy;
fixture = TestBed.createComponent(DynamicTableWidgetComponent);
element = fixture.nativeElement;
widget = fixture.componentInstance;
widget.content = table;
});
it('should select row on click', () => {
let row = <DynamicTableRow> {selected: false};
widget.onRowClicked(row);
expect(row.selected).toBeTruthy();
expect(widget.content.selectedRow).toBe(row);
});
it('should requre table to select clicked row', () => {
let row = <DynamicTableRow> {selected: false};
widget.content = null;
widget.onRowClicked(row);
expect(row.selected).toBeFalsy();
});
it('should reset selected row', () => {
let row = <DynamicTableRow> {selected: false};
widget.content.rows.push(row);
widget.content.selectedRow = row;
expect(widget.content.selectedRow).toBe(row);
expect(row.selected).toBeTruthy();
widget.onRowClicked(null);
expect(widget.content.selectedRow).toBeNull();
expect(row.selected).toBeFalsy();
});
it('should check selection', () => {
let row = <DynamicTableRow> {selected: false};
widget.content.rows.push(row);
widget.content.selectedRow = row;
expect(widget.hasSelection()).toBeTruthy();
widget.content.selectedRow = null;
expect(widget.hasSelection()).toBeFalsy();
widget.content = null;
expect(widget.hasSelection()).toBeFalsy();
});
it('should require table to move selection up', () => {
widget.content = null;
expect(widget.moveSelectionUp()).toBeFalsy();
});
it('should move selection up', () => {
let row1 = <DynamicTableRow> {};
let row2 = <DynamicTableRow> {};
widget.content.rows.push(...[row1, row2]);
widget.content.selectedRow = row2;
expect(widget.moveSelectionUp()).toBeTruthy();
expect(widget.content.rows.indexOf(row2)).toBe(0);
});
it('should require table to move selection down', () => {
widget.content = null;
expect(widget.moveSelectionDown()).toBeFalsy();
});
it('should move selection down', () => {
let row1 = <DynamicTableRow> {};
let row2 = <DynamicTableRow> {};
widget.content.rows.push(...[row1, row2]);
widget.content.selectedRow = row1;
expect(widget.moveSelectionDown()).toBeTruthy();
expect(widget.content.rows.indexOf(row1)).toBe(1);
});
it('should require table to delete selection', () => {
widget.content = null;
expect(widget.deleteSelection()).toBeFalsy();
});
it('should delete selected row', () => {
let row = <DynamicTableRow> {};
widget.content.rows.push(row);
widget.content.selectedRow = row;
widget.deleteSelection();
expect(widget.content.rows.length).toBe(0);
});
it('should require table to add new row', () => {
widget.content = null;
expect(widget.addNewRow()).toBeFalsy();
});
it('should start editing new row', () => {
expect(widget.editMode).toBeFalsy();
expect(widget.editRow).toBeNull();
expect(widget.addNewRow()).toBeTruthy();
expect(widget.editRow).not.toBeNull();
expect(widget.editMode).toBeTruthy();
});
it('should require table to edit selected row', () => {
widget.content = null;
expect(widget.editSelection()).toBeFalsy();
});
it('should start editing selected row', () => {
expect(widget.editMode).toBeFalsy();
expect(widget.editRow).toBeFalsy();
let row = <DynamicTableRow> {value: true};
widget.content.selectedRow = row;
expect(widget.editSelection()).toBeTruthy();
expect(widget.editMode).toBeTruthy();
expect(widget.editRow).not.toBeNull();
expect(widget.editRow.value).toEqual(row.value);
});
it('should copy row', () => {
let row = <DynamicTableRow> {value: {opt: {key: '1', value: 1}}};
let copy = widget.copyRow(row);
expect(copy.value).toEqual(row.value);
});
it('should require table to retrieve cell value', () => {
widget.content = null;
expect(widget.getCellValue(null, null)).toBeNull();
});
it('should retrieve cell value', () => {
const value = '<value>';
let row = <DynamicTableRow> {value: {key: value}};
let column = <DynamicTableColumn> {id: 'key'};
expect(widget.getCellValue(row, column)).toBe(value);
});
it('should save changes and add new row', () => {
let row = <DynamicTableRow> {isNew: true, value: {key: 'value'}};
widget.editMode = true;
widget.editRow = row;
widget.onSaveChanges();
expect(row.isNew).toBeFalsy();
expect(widget.content.selectedRow).toBeNull();
expect(widget.content.rows.length).toBe(1);
expect(widget.content.rows[0].value).toEqual(row.value);
});
it('should save changes and update row', () => {
let row = <DynamicTableRow> {isNew: false, value: {key: 'value'}};
widget.editMode = true;
widget.editRow = row;
widget.content.selectedRow = row;
widget.onSaveChanges();
expect(widget.content.selectedRow.value).toEqual(row.value);
});
it('should require table to save changes', () => {
spyOn(logService, 'error').and.stub();
widget.editMode = true;
widget.content = null;
widget.onSaveChanges();
expect(widget.editMode).toBeFalsy();
});
it('should cancel changes', () => {
widget.editMode = true;
widget.editRow = <DynamicTableRow> {};
widget.onCancelChanges();
expect(widget.editMode).toBeFalsy();
expect(widget.editRow).toBeNull();
});
it('should be valid by default', () => {
widget.content.field = null;
expect(widget.isValid()).toBeTruthy();
widget.content = null;
expect(widget.isValid()).toBeTruthy();
});
it('should take validation state from underlying field', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
type: FormFieldTypes.DYNAMIC_TABLE,
required: true,
value: null
});
widget.content = new DynamicTableModel(field, formService);
expect(widget.content.field.validate()).toBeFalsy();
expect(widget.isValid()).toBe(widget.content.field.isValid);
expect(widget.content.field.isValid).toBeFalsy();
widget.content.field.value = [{}];
expect(widget.content.field.validate()).toBeTruthy();
expect(widget.isValid()).toBe(widget.content.field.isValid);
expect(widget.content.field.isValid).toBeTruthy();
});
it('should prepend default currency for amount columns', () => {
let row = <DynamicTableRow> {value: {key: '100'}};
let column = <DynamicTableColumn> {id: 'key', type: 'Amount'};
let actual = widget.getCellValue(row, column);
expect(actual).toBe('$ 100');
});
it('should prepend custom currency for amount columns', () => {
let row = <DynamicTableRow> {value: {key: '100'}};
let column = <DynamicTableColumn> {id: 'key', type: 'Amount', amountCurrency: 'GBP'};
let actual = widget.getCellValue(row, column);
expect(actual).toBe('GBP 100');
});
describe('when template is ready', () => {
beforeEach(() => {
widget.field = new FormFieldModel(new FormModel({taskId: 'fake-task-id'}), fakeFormField);
widget.field.type = FormFieldTypes.DYNAMIC_TABLE;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
it('should select a row when press space bar', async(() => {
let rowElement = element.querySelector('#fake-dynamic-table-row-0');
expect(element.querySelector('#dynamic-table-fake-dynamic-table')).not.toBeNull();
expect(rowElement).not.toBeNull();
expect(rowElement.className).toBeFalsy();
let event: any = new Event('keyup');
event.keyCode = 32;
rowElement.dispatchEvent(event);
fixture.detectChanges();
fixture.whenStable().then(() => {
let selectedRow = element.querySelector('#fake-dynamic-table-row-0');
expect(selectedRow.className).toBe('adf-dynamic-table-widget__row-selected');
});
}));
it('should focus on add button when a new row is saved', async(() => {
let addNewRowButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#fake-dynamic-table-add-row');
expect(element.querySelector('#dynamic-table-fake-dynamic-table')).not.toBeNull();
expect(addNewRowButton).not.toBeNull();
widget.addNewRow();
widget.onSaveChanges();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(document.activeElement.id).toBe('fake-dynamic-table-add-row');
});
}));
});
});

View File

@@ -0,0 +1,207 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { LogService } from '../../../../services';
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { FormService } from './../../../services/form.service';
import { baseHost, WidgetComponent } from './../widget.component';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { DynamicTableModel } from './dynamic-table.widget.model';
@Component({
selector: 'dynamic-table-widget',
templateUrl: './dynamic-table.widget.html',
styleUrls: ['./dynamic-table.widget.scss'],
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class DynamicTableWidgetComponent extends WidgetComponent implements OnInit {
ERROR_MODEL_NOT_FOUND = 'Table model not found';
content: DynamicTableModel;
editMode: boolean = false;
editRow: DynamicTableRow = null;
private selectArrayCode = [32, 0, 13];
constructor(public formService: FormService,
public elementRef: ElementRef,
private visibilityService: WidgetVisibilityService,
private logService: LogService,
private cd: ChangeDetectorRef) {
super(formService);
}
ngOnInit() {
if (this.field) {
this.content = new DynamicTableModel(this.field, this.formService);
this.visibilityService.refreshVisibility(this.field.form);
}
}
forceFocusOnAddButton() {
if (this.content) {
this.cd.detectChanges();
let buttonAddRow = <HTMLButtonElement> this.elementRef.nativeElement.querySelector('#' + this.content.id + '-add-row');
if (this.isDynamicTableReady(buttonAddRow)) {
buttonAddRow.focus();
}
}
}
private isDynamicTableReady(buttonAddRow) {
return this.field && !this.editMode && buttonAddRow;
}
isValid() {
let result = true;
if (this.content && this.content.field) {
result = this.content.field.isValid;
}
return result;
}
onRowClicked(row: DynamicTableRow) {
if (this.content) {
this.content.selectedRow = row;
}
}
onKeyPressed($event: KeyboardEvent, row: DynamicTableRow) {
if (this.content && this.isEnterOrSpacePressed($event.keyCode)) {
this.content.selectedRow = row;
}
}
private isEnterOrSpacePressed(keycode) {
return this.selectArrayCode.indexOf(keycode) !== -1;
}
hasSelection(): boolean {
return !!(this.content && this.content.selectedRow);
}
moveSelectionUp(): boolean {
if (this.content && !this.readOnly) {
this.content.moveRow(this.content.selectedRow, -1);
return true;
}
return false;
}
moveSelectionDown(): boolean {
if (this.content && !this.readOnly) {
this.content.moveRow(this.content.selectedRow, 1);
return true;
}
return false;
}
deleteSelection(): boolean {
if (this.content && !this.readOnly) {
this.content.deleteRow(this.content.selectedRow);
return true;
}
return false;
}
addNewRow(): boolean {
if (this.content && !this.readOnly) {
this.editRow = <DynamicTableRow> {
isNew: true,
selected: false,
value: {}
};
this.editMode = true;
return true;
}
return false;
}
editSelection(): boolean {
if (this.content && !this.readOnly) {
this.editRow = this.copyRow(this.content.selectedRow);
this.editMode = true;
return true;
}
return false;
}
getCellValue(row: DynamicTableRow, column: DynamicTableColumn): any {
if (this.content) {
let result = this.content.getCellValue(row, column);
if (column.type === 'Amount') {
return (column.amountCurrency || '$') + ' ' + (result || 0);
}
return result;
}
return null;
}
onSaveChanges() {
if (this.content) {
if (this.editRow.isNew) {
let row = this.copyRow(this.editRow);
this.content.selectedRow = null;
this.content.addRow(row);
this.editRow.isNew = false;
} else {
this.content.selectedRow.value = this.copyObject(this.editRow.value);
}
this.content.flushValue();
} else {
this.logService.error(this.ERROR_MODEL_NOT_FOUND);
}
this.editMode = false;
this.forceFocusOnAddButton();
}
onCancelChanges() {
this.editMode = false;
this.editRow = null;
this.forceFocusOnAddButton();
}
copyRow(row: DynamicTableRow): DynamicTableRow {
return <DynamicTableRow> {
value: this.copyObject(row.value)
};
}
private copyObject(obj: any): any {
let result = obj;
if (typeof obj === 'object' && obj !== null && obj !== undefined) {
result = Object.assign({}, obj);
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object') {
result[key] = this.copyObject(obj[key]);
}
});
}
return result;
}
}

View File

@@ -0,0 +1,11 @@
<label [attr.for]="column.id">
<mat-checkbox
color="primary"
[id]="column.id"
[checked]="table.getCellValue(row, column)"
[required]="column.required"
[disabled]="!column.editable"
(change)="onValueChanged(row, column, $event)">
<span class="adf-checkbox-label">{{column.name}}</span>
</mat-checkbox>
</label>

View File

@@ -0,0 +1,11 @@
.adf {
&-checkbox-label {
position: relative;
cursor: pointer;
font-size: 16px;
line-height: 24px;
margin: 0;
}
}

View File

@@ -0,0 +1,40 @@
/*!
* @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 { DynamicTableColumn } from './../../dynamic-table-column.model';
import { DynamicTableRow } from './../../dynamic-table-row.model';
import { BooleanEditorComponent } from './boolean.editor';
describe('BooleanEditorComponent', () => {
let component: BooleanEditorComponent;
beforeEach(() => {
component = new BooleanEditorComponent();
});
it('should update row value on change', () => {
let row = <DynamicTableRow> { value: {} };
let column = <DynamicTableColumn> { id: 'key' };
let event = { checked: true } ;
component.onValueChanged(row, column, event);
expect(row.value[column.id]).toBeTruthy();
});
});

View File

@@ -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.
*/
/* tslint:disable:component-selector */
import { Component, Input } from '@angular/core';
import { DynamicTableColumn } from './../../dynamic-table-column.model';
import { DynamicTableRow } from './../../dynamic-table-row.model';
import { DynamicTableModel } from './../../dynamic-table.widget.model';
@Component({
selector: 'adf-boolean-editor',
templateUrl: './boolean.editor.html',
styleUrls: ['./boolean.editor.scss']
})
export class BooleanEditorComponent {
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
onValueChanged(row: DynamicTableRow, column: DynamicTableColumn, event: any) {
let value: boolean = (<HTMLInputElement> event).checked;
row.value[column.id] = value;
}
}

View File

@@ -0,0 +1,16 @@
<div>
<mat-form-field class="adf-date-editor">
<label [attr.for]="column.id">{{column.name}} (d-M-yyyy)</label>
<input matInput
id="dateInput"
type="text"
[matDatepicker]="datePicker"
[value]="value"
[id]="column.id"
[required]="column.required"
[disabled]="!column.editable"
(focusout)="onDateChanged($event.srcElement.value)">
<mat-datepicker-toggle *ngIf="column.editable" matSuffix [for]="datePicker" class="adf-date-editor-button" ></mat-datepicker-toggle>
</mat-form-field>
<mat-datepicker #datePicker (dateChange)="onDateChanged($event)" [touchUi]="true"></mat-datepicker>
</div>

View File

@@ -0,0 +1,10 @@
.adf {
&-date-editor {
width: 100%;
}
&-date-editor-button {
position: relative;
top: 25px;
}
}

View File

@@ -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 { DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import * as moment from 'moment';
import { MaterialModule } from '../../../../../../material.module';
import { FormFieldModel, FormModel } from '../../../index';
import { DynamicTableColumn } from './../../dynamic-table-column.model';
import { DynamicTableRow } from './../../dynamic-table-row.model';
import { DynamicTableModel } from './../../dynamic-table.widget.model';
import { DateEditorComponent } from './date.editor';
describe('DateEditorComponent', () => {
let debugElement: DebugElement;
let element: HTMLElement;
let component: DateEditorComponent;
let fixture: ComponentFixture<DateEditorComponent>;
let row: DynamicTableRow;
let column: DynamicTableColumn;
let table: DynamicTableModel;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
DateEditorComponent
],
imports: [
MaterialModule
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DateEditorComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
debugElement = fixture.debugElement;
row = <DynamicTableRow> { value: { date: '1879-03-14T00:00:00.000Z' } };
column = <DynamicTableColumn> { id: 'date', type: 'Date' };
const field = new FormFieldModel(new FormModel());
table = new DynamicTableModel(field, null);
table.rows.push(row);
table.columns.push(column);
component.table = table;
component.row = row;
component.column = column;
});
it('should create instance of DateEditorComponent', () => {
expect(fixture.componentInstance instanceof DateEditorComponent).toBe(true, 'should create DateEditorComponent');
});
it('should update fow value on change', () => {
component.ngOnInit();
let newDate = moment('14-03-1879', 'DD-MM-YYYY');
component.onDateChanged(newDate);
expect(row.value[column.id]).toBe('1879-03-14T00:00:00.000Z');
});
it('should update row value upon user input', () => {
const input = '14-03-2016';
component.ngOnInit();
component.onDateChanged(input);
let actual = row.value[column.id];
expect(actual).toBe('2016-03-14T00:00:00.000Z');
});
it('should flush value on user input', () => {
spyOn(table, 'flushValue').and.callThrough();
const input = '14-03-2016';
component.ngOnInit();
component.onDateChanged(input);
expect(table.flushValue).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,83 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { UserPreferencesService } from '../../../../../../services';
import { MOMENT_DATE_FORMATS, MomentDateAdapter } from '../../../../../../utils';
import { Component, Input, OnInit } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material';
import * as moment from 'moment';
import { Moment } from 'moment';
import { DynamicTableColumn } from './../../dynamic-table-column.model';
import { DynamicTableRow } from './../../dynamic-table-row.model';
import { DynamicTableModel } from './../../dynamic-table.widget.model';
@Component({
selector: 'adf-date-editor',
templateUrl: './date.editor.html',
providers: [
{provide: DateAdapter, useClass: MomentDateAdapter},
{provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS}],
styleUrls: ['./date.editor.scss']
})
export class DateEditorComponent implements OnInit {
DATE_FORMAT: string = 'DD-MM-YYYY';
value: any;
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
minDate: Moment;
maxDate: Moment;
constructor(private dateAdapter: DateAdapter<Moment>,
private preferences: UserPreferencesService) {
}
ngOnInit() {
this.preferences.locale$.subscribe((locale) => {
this.dateAdapter.setLocale(locale);
});
let momentDateAdapter = <MomentDateAdapter> this.dateAdapter;
momentDateAdapter.overrideDisplyaFormat = this.DATE_FORMAT;
this.value = moment(this.table.getCellValue(this.row, this.column), this.DATE_FORMAT);
}
onDateChanged(newDateValue) {
if (newDateValue) {
let momentDate = moment(newDateValue, this.DATE_FORMAT, true);
if (!momentDate.isValid()) {
this.row.value[this.column.id] = '';
} else {
this.row.value[this.column.id] = `${momentDate.format('YYYY-MM-DD')}T00:00:00.000Z`;
this.table.flushValue();
}
}
}
}

View File

@@ -0,0 +1,16 @@
<div class="dropdown-editor">
<label [attr.for]="column.id">{{column.name}}</label>
<mat-form-field>
<mat-select
floatPlaceholder="never"
class="adf-dropdown-editor-select"
[id]="column.id"
[(ngModel)]="value"
[required]="column.required"
[disabled]="!column.editable"
(change)="onValueChanged(row, column, $event)">
<mat-option></mat-option>
<mat-option *ngFor="let opt of options" [value]="opt.name" [id]="opt.id">{{opt.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>

View File

@@ -0,0 +1,5 @@
.adf {
&-dropdown-editor-select {
width: 100%;
}
}

View File

@@ -0,0 +1,308 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Rx';
import { EcmModelService } from '../../../../../services/ecm-model.service';
import { MaterialModule } from '../../../../../../material.module';
import { FormService } from './../../../../../services/form.service';
import { FormFieldModel, FormModel } from './../../../core/index';
import { DynamicTableColumnOption } from './../../dynamic-table-column-option.model';
import { DynamicTableColumn } from './../../dynamic-table-column.model';
import { DynamicTableRow } from './../../dynamic-table-row.model';
import { DynamicTableModel } from './../../dynamic-table.widget.model';
import { DropdownEditorComponent } from './dropdown.editor';
describe('DropdownEditorComponent', () => {
let component: DropdownEditorComponent;
let formService: FormService;
let form: FormModel;
let table: DynamicTableModel;
let column: DynamicTableColumn;
let row: DynamicTableRow;
beforeEach(() => {
formService = new FormService(null, null, null);
row = <DynamicTableRow> {value: {dropdown: 'one'}};
column = <DynamicTableColumn> {
id: 'dropdown',
options: [
<DynamicTableColumnOption> {id: '1', name: 'one'},
<DynamicTableColumnOption> {id: '2', name: 'two'}
]
};
form = new FormModel({taskId: '<task-id>'});
table = new DynamicTableModel(new FormFieldModel(form, {id: '<field-id>'}), formService);
table.rows.push(row);
table.columns.push(column);
component = new DropdownEditorComponent(formService, null);
component.table = table;
component.row = row;
component.column = column;
});
it('should require table field to setup', () => {
table.field = null;
component.ngOnInit();
expect(component.value).toBeNull();
expect(component.options).toEqual([]);
});
it('should setup with manual mode', () => {
row.value[column.id] = 'two';
component.ngOnInit();
expect(component.options).toEqual(column.options);
expect(component.value).toBe(row.value[column.id]);
});
it('should setup empty columns for manual mode', () => {
column.options = null;
component.ngOnInit();
expect(component.options).toEqual([]);
});
it('should setup with REST mode', () => {
column.optionType = 'rest';
row.value[column.id] = 'twelve';
let restResults = [
<DynamicTableColumnOption> {id: '11', name: 'eleven'},
<DynamicTableColumnOption> {id: '12', name: 'twelve'}
];
spyOn(formService, 'getRestFieldValuesColumn').and.returnValue(
Observable.create(observer => {
observer.next(restResults);
observer.complete();
})
);
component.ngOnInit();
expect(formService.getRestFieldValuesColumn).toHaveBeenCalledWith(
form.taskId,
table.field.id,
column.id
);
expect(column.options).toEqual(restResults);
expect(component.options).toEqual(restResults);
expect(component.value).toBe(row.value[column.id]);
});
it('should create empty options array on REST response', () => {
column.optionType = 'rest';
spyOn(formService, 'getRestFieldValuesColumn').and.returnValue(
Observable.create(observer => {
observer.next(null);
observer.complete();
})
);
component.ngOnInit();
expect(formService.getRestFieldValuesColumn).toHaveBeenCalledWith(
form.taskId,
table.field.id,
column.id
);
expect(column.options).toEqual([]);
expect(component.options).toEqual([]);
expect(component.value).toBe(row.value[column.id]);
});
it('should handle REST error gettig options with task id', () => {
column.optionType = 'rest';
const error = 'error';
spyOn(formService, 'getRestFieldValuesColumn').and.returnValue(
Observable.throw(error)
);
spyOn(component, 'handleError').and.stub();
component.ngOnInit();
expect(component.handleError).toHaveBeenCalledWith(error);
});
it('should handle REST error getting option with processDefinitionId', () => {
column.optionType = 'rest';
let procForm = new FormModel({processDefinitionId: '<process-definition-id>'});
let procTable = new DynamicTableModel(new FormFieldModel(procForm, {id: '<field-id>'}), formService);
component.table = procTable;
const error = 'error';
spyOn(formService, 'getRestFieldValuesColumnByProcessId').and.returnValue(
Observable.throw(error)
);
spyOn(component, 'handleError').and.stub();
component.ngOnInit();
expect(component.handleError).toHaveBeenCalledWith(error);
});
it('should update row on value change', () => {
let event = {value: 'two'};
component.onValueChanged(row, column, event);
expect(row.value[column.id]).toBe(column.options[1]);
});
describe('when template is ready', () => {
function openSelect() {
const dropdown = fixture.debugElement.query(By.css('[class="mat-select-trigger"]'));
dropdown.triggerEventHandler('click', null);
fixture.detectChanges();
}
let dropDownEditorComponent: DropdownEditorComponent;
let fixture: ComponentFixture<DropdownEditorComponent>;
let element: HTMLElement;
let stubFormService;
let fakeOptionList: DynamicTableColumnOption[] = [{
id: 'opt_1',
name: 'option_1'
}, {
id: 'opt_2',
name: 'option_2'
}, {id: 'opt_3', name: 'option_3'}];
let dynamicTable: DynamicTableModel;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MaterialModule],
declarations: [DropdownEditorComponent],
providers: [FormService, EcmModelService]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(DropdownEditorComponent);
dropDownEditorComponent = fixture.componentInstance;
element = fixture.nativeElement;
});
}));
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('and dropdown is populated via taskId', () => {
beforeEach(async(() => {
stubFormService = fixture.debugElement.injector.get(FormService);
spyOn(stubFormService, 'getRestFieldValuesColumn').and.returnValue(Observable.of(fakeOptionList));
row = <DynamicTableRow> {value: {dropdown: 'one'}};
column = <DynamicTableColumn> {
id: 'column-id',
optionType: 'rest',
options: [
<DynamicTableColumnOption> {id: '1', name: 'one'},
<DynamicTableColumnOption> {id: '2', name: 'two'}
]
};
form = new FormModel({taskId: '<task-id>'});
dynamicTable = new DynamicTableModel(new FormFieldModel(form, {id: '<field-id>'}), formService);
dynamicTable.rows.push(row);
dynamicTable.columns.push(column);
dropDownEditorComponent.table = dynamicTable;
dropDownEditorComponent.column = column;
dropDownEditorComponent.row = row;
dropDownEditorComponent.table.field = new FormFieldModel(form, {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
dropDownEditorComponent.table.field.isVisible = true;
fixture.detectChanges();
}));
it('should show visible dropdown widget', async(() => {
expect(element.querySelector('#column-id')).toBeDefined();
expect(element.querySelector('#column-id')).not.toBeNull();
openSelect();
const optOne = fixture.debugElement.queryAll(By.css('[id="mat-option-1"]'));
const optTwo = fixture.debugElement.queryAll(By.css('[id="mat-option-2"]'));
const optThree = fixture.debugElement.queryAll(By.css('[id="mat-option-3"]'));
expect(optOne).not.toBeNull();
expect(optTwo).not.toBeNull();
expect(optThree).not.toBeNull();
}));
});
describe('and dropdown is populated via processDefinitionId', () => {
beforeEach(async(() => {
stubFormService = fixture.debugElement.injector.get(FormService);
spyOn(stubFormService, 'getRestFieldValuesColumnByProcessId').and.returnValue(Observable.of(fakeOptionList));
row = <DynamicTableRow> {value: {dropdown: 'one'}};
column = <DynamicTableColumn> {
id: 'column-id',
optionType: 'rest',
options: [
<DynamicTableColumnOption> {id: '1', name: 'one'},
<DynamicTableColumnOption> {id: '2', name: 'two'}
]
};
form = new FormModel({processDefinitionId: '<proc-id>'});
dynamicTable = new DynamicTableModel(new FormFieldModel(form, {id: '<field-id>'}), formService);
dynamicTable.rows.push(row);
dynamicTable.columns.push(column);
dropDownEditorComponent.table = dynamicTable;
dropDownEditorComponent.column = column;
dropDownEditorComponent.row = row;
dropDownEditorComponent.table.field = new FormFieldModel(form, {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
dropDownEditorComponent.table.field.isVisible = true;
fixture.detectChanges();
}));
it('should show visible dropdown widget', async(() => {
expect(element.querySelector('#column-id')).toBeDefined();
expect(element.querySelector('#column-id')).not.toBeNull();
openSelect();
const optOne = fixture.debugElement.queryAll(By.css('[id="mat-option-1"]'));
const optTwo = fixture.debugElement.queryAll(By.css('[id="mat-option-2"]'));
const optThree = fixture.debugElement.queryAll(By.css('[id="mat-option-3"]'));
expect(optOne).not.toBeNull();
expect(optTwo).not.toBeNull();
expect(optThree).not.toBeNull();
}));
});
});
});

Some files were not shown because too many files have changed in this diff Show More