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,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();
}));
});
});
});

View File

@@ -0,0 +1,110 @@
/*!
* @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, Input, OnInit } from '@angular/core';
import { FormService } from './../../../../../services/form.service';
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';
@Component({
selector: 'adf-dropdown-editor',
templateUrl: './dropdown.editor.html',
styleUrls: ['./dropdown.editor.scss']
})
export class DropdownEditorComponent implements OnInit {
value: any = null;
options: DynamicTableColumnOption[] = [];
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
constructor(public formService: FormService,
private logService: LogService) {
}
ngOnInit() {
let field = this.table.field;
if (field) {
if (this.column.optionType === 'rest') {
if (this.table.form && this.table.form.taskId) {
this.getValuesByTaskId(field);
} else {
this.getValuesByProcessDefinitionId(field);
}
} else {
this.options = this.column.options || [];
this.value = this.table.getCellValue(this.row, this.column);
}
}
}
getValuesByTaskId(field) {
this.formService
.getRestFieldValuesColumn(
field.form.taskId,
field.id,
this.column.id
)
.subscribe(
(result: DynamicTableColumnOption[]) => {
this.column.options = result || [];
this.options = this.column.options;
this.value = this.table.getCellValue(this.row, this.column);
},
err => this.handleError(err)
);
}
getValuesByProcessDefinitionId(field) {
this.formService
.getRestFieldValuesColumnByProcessId(
field.form.processDefinitionId,
field.id,
this.column.id
)
.subscribe(
(result: DynamicTableColumnOption[]) => {
this.column.options = result || [];
this.options = this.column.options;
this.value = this.table.getCellValue(this.row, this.column);
},
err => this.handleError(err)
);
}
onValueChanged(row: DynamicTableRow, column: DynamicTableColumn, event: any) {
let value: any = (<HTMLInputElement> event).value;
value = column.options.find(opt => opt.name === value);
row.value[column.id] = value;
}
handleError(error: any) {
this.logService.error(error);
}
}

View File

@@ -0,0 +1,16 @@
.row-editor {
padding: 8px;
}
.row-editor__validation-summary {
visibility: hidden;
}
.row-editor__invalid .row-editor__validation-summary {
padding-left: 16px;
padding-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
color: #d50000;
visibility: visible;
}

View File

@@ -0,0 +1,41 @@
<div class="row-editor mdl-shadow--2dp"
[class.row-editor__invalid]="!validationSummary.isValid">
<div class="mdl-grid" *ngFor="let column of table.columns">
<div class="mdl-cell mdl-cell--6-col" [ngSwitch]="column.type">
<div *ngSwitchCase="'Dropdown'">
<adf-dropdown-editor
[table]="table"
[row]="row"
[column]="column">
</adf-dropdown-editor>
</div>
<div *ngSwitchCase="'Date'">
<adf-date-editor
[table]="table"
[row]="row"
[column]="column">
</adf-date-editor>
</div>
<div *ngSwitchCase="'Boolean'">
<adf-boolean-editor
[table]="table"
[row]="row"
[column]="column">
</adf-boolean-editor>
</div>
<div *ngSwitchDefault>
<adf-text-editor
[table]="table"
[row]="row"
[column]="column">
</adf-text-editor>
</div>
</div>
</div>
<error-widget [error]="validationSummary.text"></error-widget>
<div>
<button mat-button (click)="onCancelChanges()">Cancel</button>
<button mat-button (click)="onSaveChanges()">Save</button>
</div>
</div>

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 { FormFieldModel, FormModel } from '../../core';
import { FormService } from './../../../../services/form.service';
import { DynamicRowValidationSummary } from './../dynamic-row-validation-summary.model';
import { DynamicTableColumn } from './../dynamic-table-column.model';
import { DynamicTableRow } from './../dynamic-table-row.model';
import { DynamicTableModel } from './../dynamic-table.widget.model';
import { RowEditorComponent } from './row.editor';
describe('RowEditorComponent', () => {
let component: RowEditorComponent;
beforeEach(() => {
component = new RowEditorComponent();
const field = new FormFieldModel(new FormModel());
component.table = new DynamicTableModel(field, new FormService(null, null, null));
component.row = <DynamicTableRow> {};
component.column = <DynamicTableColumn> {};
});
it('should be valid upon init', () => {
expect(component.validationSummary.isValid).toBeTruthy();
expect(component.validationSummary.text).toBeNull();
});
it('should emit [cancel] event', (done) => {
component.cancel.subscribe(e => {
expect(e.table).toBe(component.table);
expect(e.row).toBe(component.row);
expect(e.column).toBe(component.column);
done();
});
component.onCancelChanges();
});
it('should validate row on save', () => {
spyOn(component.table, 'validateRow').and.callThrough();
component.onSaveChanges();
expect(component.table.validateRow).toHaveBeenCalledWith(component.row);
});
it('should emit [save] event', (done) => {
spyOn(component.table, 'validateRow').and.returnValue(
<DynamicRowValidationSummary> {isValid: true, text: null}
);
component.save.subscribe(e => {
expect(e.table).toBe(component.table);
expect(e.row).toBe(component.row);
expect(e.column).toBe(component.column);
done();
});
component.onSaveChanges();
});
it('should not emit [save] event for invalid row', () => {
spyOn(component.table, 'validateRow').and.returnValue(
<DynamicRowValidationSummary> {isValid: false, text: 'error'}
);
let raised = false;
component.save.subscribe(e => raised = true);
component.onSaveChanges();
expect(raised).toBeFalsy();
});
});

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.
*/
/* tslint:disable:component-selector */
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DynamicRowValidationSummary } from './../dynamic-row-validation-summary.model';
import { DynamicTableColumn } from './../dynamic-table-column.model';
import { DynamicTableRow } from './../dynamic-table-row.model';
import { DynamicTableModel } from './../dynamic-table.widget.model';
@Component({
selector: 'row-editor',
templateUrl: './row.editor.html',
styleUrls: ['./row.editor.css']
})
export class RowEditorComponent {
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
@Output()
save: EventEmitter<any> = new EventEmitter<any>();
@Output()
cancel: EventEmitter<any> = new EventEmitter<any>();
validationSummary: DynamicRowValidationSummary = <DynamicRowValidationSummary> { isValid: true, text: null };
onCancelChanges() {
this.cancel.emit({
table: this.table,
row: this.row,
column: this.column
});
}
onSaveChanges() {
this.validate();
if (this.isValid()) {
this.save.emit({
table: this.table,
row: this.row,
column: this.column
});
}
}
private isValid(): boolean {
return this.validationSummary && this.validationSummary.isValid;
}
private validate() {
this.validationSummary = this.table.validateRow(this.row);
}
}

View File

@@ -0,0 +1,12 @@
<div class="adf-text-editor">
<mat-form-field>
<label [attr.for]="column.id">{{displayName}}</label>
<input matInput
type="text"
[value]="table.getCellValue(row, column)"
(keyup)="onValueChanged(row, column, $event)"
[required]="column.required"
[disabled]="!column.editable"
[id]="column.id">
</mat-form-field>
</div>

View File

@@ -0,0 +1,6 @@
.adf {
&-text-editor {
width: 100%;
}
}

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 { DynamicTableColumn } from './../../dynamic-table-column.model';
import { DynamicTableRow } from './../../dynamic-table-row.model';
import { TextEditorComponent } from './text.editor';
describe('TextEditorComponent', () => {
let editor: TextEditorComponent;
beforeEach(() => {
editor = new TextEditorComponent();
});
it('should update row value on change', () => {
let row = <DynamicTableRow> { value: {} };
let column = <DynamicTableColumn> { id: 'key' };
const value = '<value>';
let event = { target: { value } };
editor.onValueChanged(row, column, event);
expect(row.value[column.id]).toBe(value);
});
});

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 { Component, Input, OnInit } 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-text-editor',
templateUrl: './text.editor.html',
styleUrls: ['./text.editor.scss']
})
export class TextEditorComponent implements OnInit {
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
displayName: string;
ngOnInit() {
this.displayName = this.table.getDisplayText(this.column);
}
onValueChanged(row: DynamicTableRow, column: DynamicTableColumn, event: any) {
let value: any = (<HTMLInputElement> event.target).value;
row.value[column.id] = value;
}
}

View File

@@ -0,0 +1,63 @@
/*!
* @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 { 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 NumberCellValidator implements CellValidator {
private supportedTypes: string[] = [
'Number',
'Amount'
];
isSupported(column: DynamicTableColumn): boolean {
return column && column.required && this.supportedTypes.indexOf(column.type) > -1;
}
isNumber(value: any): boolean {
if (value === null || value === undefined || value === '') {
return false;
}
return !isNaN(+value);
}
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean {
if (this.isSupported(column)) {
let value = row.value[column.id];
if (value === null ||
value === undefined ||
value === '' ||
this.isNumber(value)) {
return true;
}
if (summary) {
summary.isValid = false;
summary.text = `Field '${column.name}' must be a number.`;
}
return false;
}
return true;
}
}

View File

@@ -0,0 +1,55 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* tslint:disable:component-selector */
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 RequiredCellValidator implements CellValidator {
private supportedTypes: string[] = [
'String',
'Number',
'Amount',
'Date',
'Dropdown'
];
isSupported(column: DynamicTableColumn): boolean {
return column && column.required && this.supportedTypes.indexOf(column.type) > -1;
}
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean {
if (this.isSupported(column)) {
let value = row.value[column.id];
if (column.required) {
if (value === null || value === undefined || value === '') {
if (summary) {
summary.isValid = false;
summary.text = `Field '${column.name}' is required.`;
}
return false;
}
}
}
return true;
}
}

View File

@@ -0,0 +1,9 @@
<div class="adf-error-text-container">
<div *ngIf="error.isActive()" [@transitionMessages]="_subscriptAnimationState">
<div class="adf-error-text">{{error.message | translate:translateParameters}}</div>
<mat-icon class="adf-error-icon">warning</mat-icon>
</div>
<div *ngIf="required" [@transitionMessages]="_subscriptAnimationState">
<div class="adf-error-text">{{required}}</div>
</div>
</div>

View File

@@ -0,0 +1,4 @@
.adf-error-text{
width: 85%;
}

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.
*/
/* tslint:disable:component-selector */
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { ErrorMessageModel } from '../core/index';
import { FormService } from './../../../services/form.service';
import { baseHost , WidgetComponent } from './../widget.component';
@Component({
selector: 'error-widget',
templateUrl: './error.component.html',
styleUrls: ['./error.component.scss'],
animations: [
trigger('transitionMessages', [
state('enter', style({opacity: 1, transform: 'translateY(0%)'})),
transition('void => enter', [
style({opacity: 0, transform: 'translateY(-100%)'}),
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')
])
])
],
host: baseHost,
encapsulation: ViewEncapsulation.None
})
export class ErrorWidgetComponent extends WidgetComponent implements OnChanges {
@Input()
error: ErrorMessageModel;
@Input()
required: string;
translateParameters: any = null;
_subscriptAnimationState: string = '';
constructor(public formService: FormService) {
super(formService);
}
ngOnChanges(changes: SimpleChanges) {
if (changes['required']) {
this.required = changes.required.currentValue;
this._subscriptAnimationState = 'enter';
}
if (changes['error']) {
if (changes.error.currentValue.isActive()) {
this.error = changes.error.currentValue;
this.translateParameters = this.error.getAttributesAsJsonObj();
this._subscriptAnimationState = 'enter';
}
}
}
}

View File

@@ -0,0 +1,71 @@
@import './hyperlink/hyperlink.widget';
@import './container/container.widget';
@import './people/people.widget';
@mixin adf-form-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
@include mat-hyperlink-widget-theme($theme);
ul > li > form-field > .adf-focus {
.adf-label {
color: mat-color($primary);
}
}
.adf {
&-error-text-container {
height: 20px;
margin-top: -12px;
}
&-error-text {
padding: 1px;
height: 16px;
font-size: 12px;
line-height: 1.33;
float: left;
color: mat-color($warn);
}
&-error-icon {
float: right;
font-size: 17px;
color: mat-color($warn);
}
&-label {
color: rgb(186, 186, 186);;
}
&-invalid {
.mat-input-underline {
background-color: #f44336 !important;
}
.adf-file {
border-color: mat-color($warn);
}
.mat-input-prefix {
color: mat-color($warn);
}
.adf-input {
border-color: mat-color($warn);
}
.adf-label {
color: mat-color($warn);
&:after {
background-color: mat-color($warn);
}
}
}
}
}

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