Merge pull request #923 from Alfresco/dev-denys-637

Dynamic Table widget for BPM Form renderer
This commit is contained in:
Eugenio Romano 2016-10-28 13:54:16 +01:00 committed by GitHub
commit cf8ada62c0
56 changed files with 2178 additions and 160 deletions

View File

@ -244,22 +244,25 @@ will also be executed after your custom code.**
## Supported form widgets
Form renderer provides support for all basic widgets:
- [x] Tabs
- [x] Text
- [x] Multiline Text
- [x] Number
- [x] Checkbox
- [ ] Date
- [x] Date
- Dropdown
* [x] Manual
* [x] REST service
* [ ] Data source
- [x] Typeahead
- [ ] Amount
- [x] Radio buttons
- [x] Amount
- Radio buttons
* [x] Manual
* [x] REST service
- [x] People
- [x] Group of People
- [ ] Dynamic Table
- [x] Dynamic Table
- [x] Hyperlink
- Header
* [x] Plain header

View File

@ -14,7 +14,25 @@
</div>
<div *ngIf="!form.hasTabs() && form.hasFields()">
<container-widget *ngFor="let field of form.fields" [content]="field" (formValueChanged)="checkVisibility($event);"></container-widget>
<div *ngFor="let field of form.fields">
<div [ngSwitch]="field.type">
<div *ngSwitchCase="'container'">
<container-widget [content]="field" (formValueChanged)="checkVisibility($event)"></container-widget>
</div>
<div *ngSwitchCase="'group'">
<container-widget [content]="field" (formValueChanged)="checkVisibility($event)"></container-widget>
</div>
<div *ngSwitchCase="'dynamic-table'">
<dynamic-table-widget [content]="field"></dynamic-table-widget>
</div>
<div *ngSwitchCase="'readonly'">
<display-value-widget [field]="field.field" (fieldChanged)="checkVisibility($event)"></display-value-widget>
</div>
<div *ngSwitchDefault>
<span>UNKNOWN WIDGET TYPE: {{field.type}}</span>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="form.hasOutcomes()" class="mdl-card__actions mdl-card--border">

View File

@ -18,7 +18,7 @@
import { Observable } from 'rxjs/Rx';
import { SimpleChange } from '@angular/core';
import { ActivitiForm } from './activiti-form.component';
import { FormModel, FormOutcomeModel, FormFieldModel, FormOutcomeEvent } from './widgets/index';
import { FormModel, FormOutcomeModel, FormFieldModel, FormOutcomeEvent, FormFieldTypes } from './widgets/index';
import { FormService } from './../services/form.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { NodeService } from './../services/node.service';
@ -554,7 +554,7 @@ describe('ActivitiForm', () => {
let form = formComponent.parseForm({
id: '<id>',
fields: [
{ id: 'field1' }
{ id: 'field1', type: FormFieldTypes.CONTAINER }
]
});

View File

@ -3,6 +3,7 @@
[attr.id]="field.id"
[attr.required]="isRequired()"
class="mdl-checkbox__input"
[checked]="field.value"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"
[disabled]="field.readOnly">

View File

@ -11,7 +11,7 @@
<span (click)="onExpanderClicked()" id="container-header-label">{{content.name}}</span>
</h4>
</div>
<div class="mdl-grid" *ngIf="content?.isExpanded">
<div class="mdl-grid" *ngIf="content?.isVisible && content?.isExpanded">
<div *ngFor="let col of content.columns" class="mdl-cell mdl-cell--{{col.size}}-col">
<div class="mdl-grid" *ngIf="col.hasFields()">
<div *ngFor="let field of col.fields" class="mdl-cell mdl-cell--12-col">

View File

@ -145,8 +145,8 @@ describe('ContainerWidget', () => {
name: 'fake-cont-2-name',
type: FormFieldTypes.GROUP
});
fakeContainerVisible.isVisible = true;
fakeContainerInvisible.isVisible = false;
fakeContainerVisible.field.isVisible = true;
fakeContainerInvisible.field.isVisible = false;
});
afterEach(() => {
@ -180,7 +180,7 @@ describe('ContainerWidget', () => {
containerWidgetComponent.content = fakeContainerVisible;
fixture.detectChanges();
containerWidgetComponent.formValueChanged.subscribe((res) => {
containerWidgetComponent.content.isVisible = false;
containerWidgetComponent.content.field.isVisible = false;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
@ -194,7 +194,7 @@ describe('ContainerWidget', () => {
it('should show header when it becomes visible', async(() => {
containerWidgetComponent.content = fakeContainerInvisible;
containerWidgetComponent.formValueChanged.subscribe((res) => {
containerWidgetComponent.content.isVisible = true;
containerWidgetComponent.content.field.isVisible = true;
fixture.detectChanges();
fixture.whenStable()
.then(() => {

View File

@ -51,8 +51,7 @@ describe('ContainerModel', () => {
type: '<type>',
tab: '<tab>',
numberOfColumns: 2,
params: {},
visibilityCondition: {}
params: {}
};
let container = new ContainerModel(null, json);
Object.keys(json).forEach(key => {
@ -107,7 +106,12 @@ describe('ContainerModel', () => {
});
expect(container.isCollapsible()).toBeFalsy();
container.type = FormFieldTypes.GROUP;
container = new ContainerModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true
}
});
expect(container.isCollapsible()).toBeTruthy();
});

View File

@ -21,24 +21,20 @@ import { ContainerColumnModel } from './container-column.model';
import { FormFieldTypes } from './form-field-types';
import { FormModel } from './form.model';
import { FormFieldModel } from './form-field.model';
import { WidgetVisibilityModel } from '../../../models/widget-visibility.model';
// TODO: inherit FormFieldModel
export class ContainerModel extends FormWidgetModel {
fieldType: string;
id: string;
name: string;
type: string;
tab: string;
field: FormFieldModel;
numberOfColumns: number = 1;
params: FormFieldMetadata = {};
isVisible: boolean = true;
visibilityCondition: WidgetVisibilityModel = null;
columns: ContainerColumnModel[] = [];
isExpanded: boolean = true;
get isVisible(): boolean {
return this.field.isVisible;
}
isGroup(): boolean {
return this.type === FormFieldTypes.GROUP;
}
@ -67,14 +63,9 @@ export class ContainerModel extends FormWidgetModel {
super(form, json);
if (json) {
this.fieldType = json.fieldType;
this.id = json.id;
this.name = json.name;
this.type = json.type;
this.tab = json.tab;
this.field = new FormFieldModel(form, json);
this.numberOfColumns = <number> json.numberOfColumns;
this.params = <FormFieldMetadata> json.params || {};
this.visibilityCondition = <WidgetVisibilityModel> json.visibilityCondition;
let columnSize: number = 12;
if (this.numberOfColumns > 1) {
@ -98,4 +89,22 @@ export class ContainerModel extends FormWidgetModel {
this.isExpanded = !this.isCollapsedByDefault();
}
}
getFormFields(): FormFieldModel[] {
let result: FormFieldModel[] = [];
if (this.field) {
result.push(this.field);
}
for (let j = 0; j < this.columns.length; j++) {
let column = this.columns[j];
for (let k = 0; k < column.fields.length; k++) {
let field = column.fields[k];
result.push(field);
}
}
return result;
}
}

View File

@ -0,0 +1,48 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 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;
}
// maps to: com.activiti.model.editor.form.OptionRepresentation
export interface DynamicTableColumnOption {
id: string;
name: 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.
*/
export interface DynamicTableRow {
isNew: boolean;
selected: boolean;
value: any;
}

View File

@ -0,0 +1,250 @@
/*!
* @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';
import { FormFieldModel } from './form-field.model';
import { DynamicTableColumn } from './dynamic-table-column';
import { DynamicTableRow } from './dynamic-table-row';
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(form: FormModel, json?: any) {
super(form, json);
if (json) {
this.field = new FormFieldModel(form, json);
if (json.columnDefinitions) {
this.columns = json.columnDefinitions.map(obj => <DynamicTableColumn> obj);
this.visibleColumns = this.columns.filter(col => col.visible);
}
if (json.value) {
this.rows = json.value.map(obj => <DynamicTableRow> { selected: false, value: obj });
}
}
this._validators = [
new RequiredCellValidator(),
new NumberCellValidator()
];
}
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 {
let summary = <DynamicRowValidationSummary> {
isValid: true,
text: null
};
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 || '';
}
}
export interface DynamicRowValidationSummary {
isValid: boolean;
text: string;
}
export interface CellValidator {
isSupported(column: DynamicTableColumn): boolean;
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean;
}
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;
}
}
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

@ -18,6 +18,7 @@
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';

View File

@ -23,25 +23,12 @@ export class FormOutcomeModel extends FormWidgetModel {
static SAVE_ACTION: string = 'Save'; // Activiti 'Save' action name
static COMPLETE_ACTION: string = 'Complete'; // Activiti 'Complete' action name
private _id: string;
private _name: string;
isSystem: boolean = false;
get id() {
return this._id;
}
get name() {
return this._name;
}
constructor(form: FormModel, json?: any) {
super(form, json);
if (json) {
this._id = json.id;
this._name = json.name;
this.isSystem = json.isSystem ? true : false;
}
}

View File

@ -20,15 +20,21 @@ import { FormWidgetModel } from './form-widget.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 FormWidgetModel(form, null);
let model = new FormWidgetModelMock(form, null);
expect(model.form).toBe(form);
});
it('should store original json', () => {
let json = {};
let model = new FormWidgetModel(null, json);
let model = new FormWidgetModelMock(null, json);
expect(model.json).toBe(json);
});

View File

@ -17,22 +17,28 @@
import { FormModel } from './form.model';
export class FormWidgetModel {
export abstract class FormWidgetModel {
private _form: FormModel;
private _json: any;
readonly fieldType: string;
readonly id: string;
readonly name: string;
readonly type: string;
readonly tab: string;
get form(): FormModel {
return this._form;
}
get json(): any {
return this._json;
}
readonly form: FormModel;
readonly json: any;
constructor(form: FormModel, json: any) {
this._form = form;
this._json = json;
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;
}
}
}

View File

@ -20,6 +20,7 @@ import { TabModel } from './tab.model';
import { ContainerModel } from './container.model';
import { FormOutcomeModel } from './form-outcome.model';
import { FormValues } from './form-values';
import { FormFieldTypes } from './form-field-types';
describe('FormModel', () => {
@ -118,8 +119,14 @@ describe('FormModel', () => {
it('should parse fields', () => {
let json = {
fields: [
{ id: 'field1' },
{ id: 'field2' }
{
id: 'field1',
type: FormFieldTypes.CONTAINER
},
{
id: 'field2',
type: FormFieldTypes.CONTAINER
}
]
};
@ -134,8 +141,14 @@ describe('FormModel', () => {
fields: null,
formDefinition: {
fields: [
{ id: 'field1' },
{ id: 'field2' }
{
id: 'field1',
type: FormFieldTypes.CONTAINER
},
{
id: 'field2',
type: FormFieldTypes.CONTAINER
}
]
}
};
@ -163,10 +176,10 @@ describe('FormModel', () => {
{ id: 'tab2' }
],
fields: [
{ id: 'field1', tab: 'tab1' },
{ id: 'field2', tab: 'tab2' },
{ id: 'field3', tab: 'tab1' },
{ id: 'field4', tab: 'missing-tab' }
{ 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 }
]
};
@ -226,7 +239,7 @@ describe('FormModel', () => {
let form = new FormModel(json, data);
expect(form.fields.length).toBe(1);
let container = form.fields[0];
let container = <ContainerModel> form.fields[0];
expect(container.columns.length).toBe(2);
let column1 = container.columns[0];

View File

@ -15,12 +15,14 @@
* limitations under the License.
*/
import { FormWidgetModelCache } from './form-widget.model';
import { FormWidgetModel, FormWidgetModelCache } from './form-widget.model';
import { FormValues } from './form-values';
import { ContainerModel } from './container.model';
import { TabModel } from './tab.model';
import { FormOutcomeModel } from './form-outcome.model';
import { FormFieldModel } from './form-field.model';
import { FormFieldTypes } from './form-field-types';
import { DynamicTableModel } from './dynamic-table.model';
export class FormModel {
@ -28,44 +30,25 @@ export class FormModel {
static SAVE_OUTCOME: string = '$save';
static COMPLETE_OUTCOME: string = '$complete';
private _id: string;
private _name: string;
private _taskId: string;
private _taskName: string = FormModel.UNSET_TASK_NAME;
readonly id: string;
readonly name: string;
readonly taskId: string;
readonly taskName: string = FormModel.UNSET_TASK_NAME;
private _isValid: boolean = true;
get id(): string {
return this._id;
}
get name(): string {
return this._name;
}
get taskId(): string {
return this._taskId;
}
get taskName(): string {
return this._taskName;
}
get isValid(): boolean {
return this._isValid;
}
readOnly: boolean = false;
tabs: TabModel[] = [];
fields: ContainerModel[] = [];
fields: FormWidgetModel[] = [];
outcomes: FormOutcomeModel[] = [];
values: FormValues = {};
private _json: any;
get json() {
return this._json;
}
readonly json: any;
hasTabs(): boolean {
return this.tabs && this.tabs.length > 0;
@ -81,13 +64,14 @@ export class FormModel {
constructor(json?: any, data?: FormValues, readOnly: boolean = false) {
this.readOnly = readOnly;
if (json) {
this._json = json;
this._id = json.id;
this._name = json.name;
this._taskId = json.taskId;
this._taskName = json.taskName || json.name || FormModel.UNSET_TASK_NAME;
if (json) {
this.json = json;
this.id = json.id;
this.name = json.name;
this.taskId = json.taskId;
this.taskName = json.taskName || json.name || FormModel.UNSET_TASK_NAME;
let tabCache: FormWidgetModelCache<TabModel> = {};
@ -97,7 +81,7 @@ export class FormModel {
return model;
});
this.fields = this.parseContainerFields(json);
this.fields = this.parseRootFields(json);
if (data) {
this.loadData(data);
@ -108,7 +92,6 @@ export class FormModel {
if (field.tab) {
let tab = tabCache[field.tab];
if (tab) {
// tab.fields.push(new ContainerModel(this, field.json));
tab.fields.push(field);
}
}
@ -131,18 +114,21 @@ export class FormModel {
this.validateField(field);
}
// TODO: evaluate and cache once the form is loaded
private getFormFields(): FormFieldModel[] {
// 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 container = this.fields[i];
for (let j = 0; j < container.columns.length; j++) {
let column = container.columns[j];
for (let k = 0; k < column.fields.length; k++) {
let field = column.fields[k];
result.push(field);
let field = this.fields[i];
if (field.type === FormFieldTypes.CONTAINER || field.type === FormFieldTypes.GROUP) {
let container = <ContainerModel> field;
result.push(...container.getFormFields());
}
if (field.type === FormFieldTypes.DYNAMIC_TABLE) {
let dynamicTable = <DynamicTableModel> field;
result.push(dynamicTable.field);
}
}
@ -171,7 +157,8 @@ export class FormModel {
this.validateForm();
}
private parseContainerFields(json: any): ContainerModel[] {
// Activiti supports 2 types of root fields: 'container' and 'dynamic-table'.
private parseRootFields(json: any): FormWidgetModel[] {
let fields = [];
if (json.fields) {
@ -180,18 +167,31 @@ export class FormModel {
fields = json.formDefinition.fields;
}
return fields.map(obj => new ContainerModel(this, obj));
let result: FormWidgetModel[] = [];
for (let field of fields) {
if (field.type === FormFieldTypes.CONTAINER || field.type === FormFieldTypes.GROUP ) {
result.push(new ContainerModel(this, field));
} else if (field.type === FormFieldTypes.DYNAMIC_TABLE) {
result.push(new DynamicTableModel(this, field));
} else 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 DynamicTableModel(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 i = 0; i < this.fields.length; i++) {
let container = this.fields[i];
for (let i = 0; i < container.columns.length; i++) {
let column = container.columns[i];
for (let i = 0; i < column.fields.length; i++) {
let field = column.fields[i];
for (let field of this.getFormFields()) {
if (data[field.id]) {
field.json.value = data[field.id];
field.value = data[field.id];
@ -199,5 +199,3 @@ export class FormModel {
}
}
}
}
}

View File

@ -28,3 +28,6 @@ export * from './tab.model';
export * from './form-outcome.model';
export * from './form-outcome-event.model';
export * from './form-field-validator';
export * from './dynamic-table.model';
export * from './dynamic-table-column';
export * from './dynamic-table-row';

View File

@ -16,18 +16,16 @@
*/
import { FormWidgetModel } from './form-widget.model';
import { ContainerModel } from './container.model';
import { FormModel } from './form.model';
import { WidgetVisibilityModel } from '../../../models/widget-visibility.model';
export class TabModel extends FormWidgetModel {
id: string;
title: string;
isVisible: boolean = true;
visibilityCondition: WidgetVisibilityModel;
fields: ContainerModel[] = [];
fields: FormWidgetModel[] = [];
hasContent(): boolean {
return this.fields && this.fields.length > 0;
@ -37,7 +35,6 @@ export class TabModel extends FormWidgetModel {
super(form, json);
if (json) {
this.id = json.id;
this.title = json.title;
this.visibilityCondition = <WidgetVisibilityModel> json.visibilityCondition;
}

View File

@ -1,3 +1,11 @@
.display-value-widget {
width: 100%;
}
.display-value-widget__dynamic-table {
padding: 8px;
}
.display-value-widget__dynamic-table table {
width: 100%;
}

View File

@ -23,6 +23,7 @@
<textarea class="mdl-textfield__input"
type="text"
rows= "3"
[value]="value"
[attr.id]="field.id"
[(ngModel)]="value"
[disabled]="field.readOnly">
@ -38,6 +39,29 @@
<a [href]="linkUrl" target="_blank" rel="nofollow">{{linkText}}</a>
</div>
</div>
<div *ngSwitchCase="'dynamic-table'">
<div class="display-value-widget__dynamic-table">
<div>{{field.name}}</div>
<table class="mdl-data-table mdl-js-data-table">
<thead>
<tr>
<th *ngFor="let column of visibleColumns"
class="mdl-data-table__cell--non-numeric">
{{column.name}}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of rows">
<td *ngFor="let column of visibleColumns"
class="mdl-data-table__cell--non-numeric">
{{ getCellValue(row, column) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div *ngSwitchDefault
class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label text-widget">
<input class="mdl-textfield__input"

View File

@ -21,6 +21,8 @@ import { FormService } from '../../../services/form.service';
import { FormFieldModel } from './../core/form-field.model';
import { FormFieldTypes } from '../core/form-field-types';
import { FormModel } from '../core/form.model';
import { DynamicTableRow } from './../core/dynamic-table-row';
import { DynamicTableColumn } from './../core/dynamic-table-column';
describe('DisplayValueWidget', () => {
@ -539,4 +541,104 @@ describe('DisplayValueWidget', () => {
expect(widget.value).toBe(value);
});
it('should setup [DYNAMIC_TABLE] field', () => {
let columns = [{ id: '1', visible: false }, { id: '2', visible: true }];
let rows = [{}, {}];
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.DISPLAY_VALUE,
params: {
field: {
type: FormFieldTypes.DYNAMIC_TABLE
}
},
columnDefinitions: columns,
value: rows
});
widget.ngOnInit();
expect(widget.columns.length).toBe(2);
expect(widget.columns[0].id).toBe(columns[0].id);
expect(widget.columns[1].id).toBe(columns[1].id);
expect(widget.visibleColumns.length).toBe(1);
expect(widget.visibleColumns[0].id).toBe(columns[1].id);
expect(widget.rows.length).toBe(2);
});
it('should setup [DYNAMIC_TABLE] field with empty schema', () => {
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.DISPLAY_VALUE,
params: {
field: {
type: FormFieldTypes.DYNAMIC_TABLE
}
},
columnDefinitions: null,
value: null
});
widget.ngOnInit();
expect(widget.value).toBeNull();
expect(widget.columns).toEqual([]);
expect(widget.rows).toEqual([]);
});
it('should retrieve default 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 retrieve dropdown cell value', () => {
const value = { id: '1', name: 'one' };
let row = <DynamicTableRow> { value: { key: value } };
let column = <DynamicTableColumn> { id: 'key', type: 'Dropdown' };
expect(widget.getCellValue(row, column)).toBe(value.name);
});
it('should fallback to empty cell value for dropdown', () => {
let row = <DynamicTableRow> { value: {} };
let column = <DynamicTableColumn> { id: 'key', type: 'Dropdown' };
expect(widget.getCellValue(row, column)).toBe('');
});
it('should retrieve boolean cell value', () => {
let row1 = <DynamicTableRow> { value: { key: true } };
let row2 = <DynamicTableRow> { value: { key: 'positive' } };
let row3 = <DynamicTableRow> { value: { key: null } };
let column = <DynamicTableColumn> { id: 'key', type: 'Boolean' };
expect(widget.getCellValue(row1, column)).toBe(true);
expect(widget.getCellValue(row2, column)).toBe(true);
expect(widget.getCellValue(row3, column)).toBe(false);
});
it('should retrieve date cell value', () => {
const value = '2016-10-04T00:00:00.000Z';
let row = <DynamicTableRow> { value: { key: value } };
let column = <DynamicTableColumn> { id: 'key', type: 'Date' };
expect(widget.getCellValue(row, column)).toBe('04-10-2016');
});
it('should fallback to empty cell value for date', () => {
let row = <DynamicTableRow> { value: {} };
let column = <DynamicTableColumn> { id: 'key', type: 'Date' };
expect(widget.getCellValue(row, column)).toBe('');
});
it('should retrieve empty text cell value', () => {
let row = <DynamicTableRow> { value: {} };
let column = <DynamicTableColumn> { id: 'key' };
expect(widget.getCellValue(row, column)).toBe('');
});
});

View File

@ -20,6 +20,8 @@ import { WidgetComponent } from './../widget.component';
import { FormFieldTypes } from '../core/form-field-types';
import { FormService } from '../../../services/form.service';
import { FormFieldOption } from './../core/form-field-option';
import { DynamicTableColumn } from './../core/dynamic-table-column';
import { DynamicTableRow } from './../core/dynamic-table-row';
@Component({
moduleId: module.id,
@ -31,9 +33,16 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
value: any;
fieldType: string;
// hyperlink
linkUrl: string;
linkText: string;
// dynamic table
rows: DynamicTableRow[] = [];
columns: DynamicTableColumn[] = [];
visibleColumns: DynamicTableColumn[] = [];
constructor(private formService: FormService) {
super();
}
@ -103,6 +112,16 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
this.linkUrl = this.getHyperlinkUrl(this.field);
this.linkText = this.getHyperlinkText(this.field);
break;
case FormFieldTypes.DYNAMIC_TABLE:
let json = this.field.json;
if (json.columnDefinitions) {
this.columns = json.columnDefinitions.map(obj => <DynamicTableColumn> obj);
this.visibleColumns = this.columns.filter(col => col.visible);
}
if (json.value) {
this.rows = json.value.map(obj => <DynamicTableRow> { selected: false, value: obj });
}
break;
default:
this.value = this.field.value;
break;
@ -141,4 +160,26 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
}
);
}
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 || '';
}
}

View File

@ -0,0 +1,28 @@
.dynamic-table-widget {
padding: 8px;
}
.dynamic-table-widget__row-selected,
.dynamic-table-widget__row-selected:hover {
background-color: #eef !important;
}
.dynamic-table-widget__table {
width: 100%;
}
.dynamic-table-widget__invalid .mdl-textfield__input {
border-color: #d50000;
}
.dynamic-table-widget__invalid .mdl-textfield__label {
color: #d50000;
}
.dynamic-table-widget__invalid .mdl-textfield__label:after {
background-color: #d50000;
}
.dynamic-table-widget__invalid .mdl-textfield__error {
visibility: visible !important;
}

View File

@ -0,0 +1,62 @@
<div class="dynamic-table-widget">
<div>{{content.name}}</div>
<div *ngIf="!editMode">
<table class="mdl-data-table mdl-js-data-table dynamic-table-widget__table">
<thead>
<tr>
<th *ngFor="let column of content.visibleColumns"
class="mdl-data-table__cell--non-numeric">
{{column.name}}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of content.rows"
[class.dynamic-table-widget__row-selected]="row.selected">
<td *ngFor="let column of content.visibleColumns"
class="mdl-data-table__cell--non-numeric"
(click)="onRowClicked(row)">
{{ getCellValue(row, column) }}
</td>
</tr>
</tbody>
</table>
<div>
<button class="mdl-button mdl-js-button mdl-button--icon"
[disabled]="!hasSelection()"
(click)="moveSelectionUp()">
<i class="material-icons">arrow_upward</i>
</button>
<button class="mdl-button mdl-js-button mdl-button--icon"
[disabled]="!hasSelection()"
(click)="moveSelectionDown()">
<i class="material-icons">arrow_downward</i>
</button>
<button class="mdl-button mdl-js-button mdl-button--icon"
(click)="addNewRow()">
<i class="material-icons">add_circle_outline</i>
</button>
<button class="mdl-button mdl-js-button mdl-button--icon"
[disabled]="!hasSelection()"
(click)="deleteSelection()">
<i class="material-icons">remove_circle_outline</i>
</button>
<button class="mdl-button mdl-js-button mdl-button--icon"
[disabled]="!hasSelection()"
(click)="editSelection()">
<i class="material-icons">edit</i>
</button>
</div>
</div>
<row-editor *ngIf="editMode"
[table]="content"
[row]="editRow"
[column]="column"
(save)="onSaveChanges()"
(cancel)="onCancelChanges()">
</row-editor>
</div>

View File

@ -0,0 +1,209 @@
/*!
* @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 { DynamicTableWidget } from './dynamic-table.widget';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './../core/index';
describe('DynamicTableWidget', () => {
let widget: DynamicTableWidget;
let table: DynamicTableModel;
beforeEach(() => {
table = new DynamicTableModel(null);
widget = new DynamicTableWidget(null);
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(console, 'log').and.stub();
widget.editMode = true;
widget.content = null;
widget.onSaveChanges();
expect(widget.editMode).toBeFalsy();
expect(console.log).toHaveBeenCalledWith(widget.ERROR_MODEL_NOT_FOUND);
});
it('should cancel changes', () => {
widget.editMode = true;
widget.editRow = <DynamicTableRow> {};
widget.onCancelChanges();
expect(widget.editMode).toBeFalsy();
expect(widget.editRow).toBeNull();
});
});

View File

@ -0,0 +1,147 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, ElementRef } from '@angular/core';
import { WidgetComponent } from './../widget.component';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './../core/index';
@Component({
moduleId: module.id,
selector: 'dynamic-table-widget',
templateUrl: './dynamic-table.widget.html',
styleUrls: ['./dynamic-table.widget.css']
})
export class DynamicTableWidget extends WidgetComponent {
ERROR_MODEL_NOT_FOUND = 'Table model not found';
@Input()
content: DynamicTableModel;
editMode: boolean = false;
editRow: DynamicTableRow = null;
constructor(private elementRef: ElementRef) {
super();
}
onRowClicked(row: DynamicTableRow) {
if (this.content) {
this.content.selectedRow = row;
}
}
hasSelection(): boolean {
return !!(this.content && this.content.selectedRow);
}
moveSelectionUp(): boolean {
if (this.content) {
this.content.moveRow(this.content.selectedRow, -1);
return true;
}
return false;
}
moveSelectionDown(): boolean {
if (this.content) {
this.content.moveRow(this.content.selectedRow, 1);
return true;
}
return false;
}
deleteSelection(): boolean {
if (this.content) {
this.content.deleteRow(this.content.selectedRow);
return true;
}
return false;
}
addNewRow(): boolean {
if (this.content) {
this.editRow = <DynamicTableRow> {
isNew: true,
selected: false,
value: {}
};
this.editMode = true;
return true;
}
return false;
}
editSelection(): boolean {
if (this.content) {
this.editRow = this.copyRow(this.content.selectedRow);
this.editMode = true;
return true;
}
return false;
}
getCellValue(row: DynamicTableRow, column: DynamicTableColumn): any {
if (this.content) {
return this.content.getCellValue(row, column);
}
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.handleError(this.ERROR_MODEL_NOT_FOUND);
}
this.editMode = false;
}
onCancelChanges() {
this.editMode = false;
this.editRow = null;
}
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 class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" [attr.for]="column.id">
<input
class="mdl-checkbox__input"
type="checkbox"
[attr.id]="column.id"
[checked]="table.getCellValue(row, column)"
[required]="column.required"
[disabled]="!column.editable"
(change)="onValueChanged(row, column, $event)">
<span class="mdl-checkbox__label">{{column.name}}</span>
</label>

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.
*/
import { BooleanEditorComponent } from './boolean.editor';
import { DynamicTableRow, DynamicTableColumn } from './../../../core/index';
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 = { srcElement: { checked: true } };
component.onValueChanged(row, column, event);
expect(row.value[column.id]).toBeTruthy();
});
});

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.
*/
import { Component } from '@angular/core';
import { CellEditorComponent } from './../cell.editor';
import { DynamicTableRow, DynamicTableColumn } from './../../../core/index';
@Component({
moduleId: module.id,
selector: 'alf-boolean-editor',
templateUrl: './boolean.editor.html',
styleUrls: ['./boolean.editor.css']
})
export class BooleanEditorComponent extends CellEditorComponent {
onValueChanged(row: DynamicTableRow, column: DynamicTableColumn, event: any) {
let value: boolean = (<HTMLInputElement>event.srcElement).checked;
row.value[column.id] = value;
}
}

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 { CellEditorComponent } from './cell.editor';
describe('CellEditorComponent', () => {
class CustomEditor extends CellEditorComponent {
onError(error: any) {
this.handleError(error);
}
}
let component: CustomEditor;
beforeEach(() => {
component = new CustomEditor();
});
it('should handle error', () => {
const error = 'error';
spyOn(console, 'error').and.stub();
component.onError(error);
expect(console.error).toHaveBeenCalledWith(error);
});
});

View File

@ -0,0 +1,36 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Input } from '@angular/core';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './../../core/index';
export abstract class CellEditorComponent {
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
handleError(error: any) {
console.error(error);
}
}

View File

@ -0,0 +1,7 @@
.date-editor {
width: 100%;
}
.date-editor--button {
margin-top: 15px;
}

View File

@ -0,0 +1,23 @@
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--11-col">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label date-editor">
<input id="dateInput"
class="mdl-textfield__input"
type="text"
[value]="table.getCellValue(row, column)"
[attr.id]="column.id"
[readonly]="true"
[required]="column.required"
[disabled]="!column.editable"
(onOk)="onDateSelected($event)">
<label class="mdl-textfield__label" [attr.for]="column.id">{{column.name}} (d-M-yyyy)</label>
</div>
</div>
<div class="mdl-cell mdl-cell--1-col">
<button
class="mdl-button mdl-js-button mdl-button--icon date-editor--button"
(click)="datePicker.toggle()">
<i class="material-icons">date_range</i>
</button>
</div>
</div>

View File

@ -0,0 +1,138 @@
/*!
* @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 { ElementRef } from '@angular/core';
import { DateEditorComponent } from './date.editor';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn/*, DynamicRowValidationSummary*/ } from './../../../core/index';
describe('DateEditorComponent', () => {
let nativeElement: any;
let elementRef: ElementRef;
let component: DateEditorComponent;
let row: DynamicTableRow;
let column: DynamicTableColumn;
let table: DynamicTableModel;
beforeEach(() => {
nativeElement = {
querySelector: function () { return null; }
};
row = <DynamicTableRow> { value: { date: '1879-03-14T00:00:00.000Z' } };
column = <DynamicTableColumn> { id: 'date', type: 'Date' };
table = new DynamicTableModel(null);
table.rows.push(row);
table.columns.push(column);
elementRef = new ElementRef(nativeElement);
component = new DateEditorComponent(elementRef);
component.table = table;
component.row = row;
component.column = column;
});
it('should setup date picker on init', () => {
let trigger = {};
spyOn(nativeElement, 'querySelector').and.returnValue(trigger);
component.ngOnInit();
let settings = component.settings;
expect(settings.type).toBe('date');
expect(settings.future.year()).toBe(moment().year() + 21);
expect(settings.init.isSame(moment('14-03-1879', component.DATE_FORMAT))).toBeTruthy();
expect(component.datePicker.trigger).toBe(trigger);
});
it('should require cell value to setup initial date', () => {
row.value[column.id] = null;
component.ngOnInit();
expect(component.settings.init).toBeUndefined();
});
it('should require dom element to setup trigger', () => {
component = new DateEditorComponent(null);
component.table = table;
component.row = row;
component.column = column;
component.ngOnInit();
expect(component.datePicker.trigger).toBeFalsy();
});
it('should update fow value on change', () => {
component.ngOnInit();
component.datePicker.time = moment('14-03-1879', 'DD-MM-YYYY');
component.onDateSelected(null);
expect(row.value[column.id]).toBe('1879-03-14T00:00:00.000Z');
});
it('should update material textfield on date selected', () => {
component.ngOnInit();
component.datePicker.time = moment('14-03-1879', 'DD-MM-YYYY');
spyOn(component, 'updateMaterialTextField').and.stub();
component.onDateSelected(null);
expect(component.updateMaterialTextField).toHaveBeenCalled();
});
it('should require dom element to update material textfield on change', () => {
component = new DateEditorComponent(null);
component.table = table;
component.row = row;
component.column = column;
component.ngOnInit();
component.datePicker.time = moment('14-03-1879', 'DD-MM-YYYY');
spyOn(component, 'updateMaterialTextField').and.stub();
component.onDateSelected(null);
expect(component.updateMaterialTextField).not.toHaveBeenCalled();
});
it('should require dom element to update material textfield', () => {
let result = component.updateMaterialTextField(null, 'value');
expect(result).toBeFalsy();
});
it('should require native dom element to update material textfield', () => {
elementRef.nativeElement = null;
let result = component.updateMaterialTextField(elementRef, 'value');
expect(result).toBeFalsy();
});
it('should require input element to update material textfield', () => {
spyOn(nativeElement, 'querySelector').and.returnValue(null);
let result = component.updateMaterialTextField(elementRef, 'value');
expect(result).toBeFalsy();
});
it('should update material textfield with new value', () => {
let called = false;
const value = '<value>';
spyOn(nativeElement, 'querySelector').and.returnValue({
MaterialTextfield: {
change: function (val) {
called = true;
expect(val).toBe(value);
}
}
});
component.updateMaterialTextField(elementRef, value);
expect(called).toBeTruthy();
});
});

View File

@ -0,0 +1,79 @@
/*!
* @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, ElementRef } from '@angular/core';
import { CellEditorComponent } from './../cell.editor';
@Component({
moduleId: module.id,
selector: 'alf-date-editor',
templateUrl: './date.editor.html',
styleUrls: ['./date.editor.css']
})
export class DateEditorComponent extends CellEditorComponent implements OnInit {
DATE_FORMAT: string = 'DD-MM-YYYY';
datePicker: any;
settings: any;
constructor(private elementRef: ElementRef) {
super();
}
ngOnInit() {
this.settings = {
type: 'date',
future: moment().add(21, 'years')
};
let value = this.table.getCellValue(this.row, this.column);
if (value) {
this.settings.init = moment(value, this.DATE_FORMAT);
}
this.datePicker = new mdDateTimePicker.default(this.settings);
if (this.elementRef) {
this.datePicker.trigger = this.elementRef.nativeElement.querySelector('#dateInput');
}
}
onDateSelected(event: CustomEvent) {
let newValue = this.datePicker.time.format('YYYY-MM-DD');
this.row.value[this.column.id] = newValue + 'T00:00:00.000Z';
this.table.flushValue();
if (this.elementRef) {
this.updateMaterialTextField(this.elementRef, newValue);
}
}
updateMaterialTextField(elementRef: ElementRef, value: string): boolean {
if (elementRef) {
let el = elementRef.nativeElement;
if (el) {
let container = el.querySelector('.mdl-textfield');
if (container) {
container.MaterialTextfield.change(value);
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,3 @@
.dropdown-editor__select {
width: 100%;
}

View File

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

View File

@ -0,0 +1,154 @@
/*!
* @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 { Observable } from 'rxjs/Rx';
import { DropdownEditorComponent } from './dropdown.editor';
import {
DynamicTableModel,
DynamicTableRow,
DynamicTableColumn,
DynamicTableColumnOption,
FormFieldModel,
FormModel
} from './../../../core/index';
import { FormService } from './../../../../../services/form.service';
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);
row = <DynamicTableRow> { value: { dropdown: 'one' } };
column = <DynamicTableColumn> {
id: 'dropdown',
options: [
<DynamicTableColumnOption> { id: '1', name: 'one' },
<DynamicTableColumnOption> { id: '2', name: 'two' }
]
};
table = new DynamicTableModel(null);
form = new FormModel({ taskId: '<task-id>' });
table.field = new FormFieldModel(form, { id: '<field-id>' });
table.rows.push(row);
table.columns.push(column);
component = new DropdownEditorComponent(formService);
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', () => {
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 update row on value change', () => {
let event = { srcElement: { value: 'two' } };
component.onValueChanged(row, column, event);
expect(row.value[column.id]).toBe(column.options[1]);
});
});

View File

@ -0,0 +1,68 @@
/*!
* @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 } from '@angular/core';
import { CellEditorComponent } from './../cell.editor';
import { DynamicTableRow, DynamicTableColumn, DynamicTableColumnOption } from './../../../core/index';
import { FormService } from './../../../../../services/form.service';
@Component({
moduleId: module.id,
selector: 'alf-dropdown-editor',
templateUrl: './dropdown.editor.html',
styleUrls: ['./dropdown.editor.css']
})
export class DropdownEditorComponent extends CellEditorComponent implements OnInit {
value: any = null;
options: DynamicTableColumnOption[] = [];
constructor(private formService: FormService) {
super();
}
ngOnInit() {
let field = this.table.field;
if (field) {
if (this.column.optionType === 'rest') {
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)
);
} else {
this.options = this.column.options || [];
this.value = this.table.getCellValue(this.row, this.column);
}
}
}
onValueChanged(row: DynamicTableRow, column: DynamicTableColumn, event: any) {
let value: any = (<HTMLInputElement>event.srcElement).value;
value = column.options.find(opt => opt.name === value);
row.value[column.id] = value;
}
}

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,45 @@
<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'">
<alf-dropdown-editor
[table]="table"
[row]="row"
[column]="column">
</alf-dropdown-editor>
</div>
<div *ngSwitchCase="'Date'">
<alf-date-editor
[table]="table"
[row]="row"
[column]="column">
</alf-date-editor>
</div>
<div *ngSwitchCase="'Boolean'">
<alf-boolean-editor
[table]="table"
[row]="row"
[column]="column">
</alf-boolean-editor>
</div>
<div *ngSwitchDefault>
<alf-text-editor
[table]="table"
[row]="row"
[column]="column">
</alf-text-editor>
</div>
</div>
</div>
<div class="row-editor__validation-summary" *ngIf="!validationSummary.isValid">{{validationSummary.text}}</div>
<div>
<button
class="mdl-button mdl-js-button mdl-js-ripple-effect"
(click)="onCancelChanges()">Cancel</button>
<button
class="mdl-button mdl-js-button mdl-js-ripple-effect"
(click)="onSaveChanges()">Save</button>
</div>
</div>

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.
*/
import { RowEditorComponent } from './row.editor';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn, DynamicRowValidationSummary } from './../../core/index';
describe('RowEditorComponent', () => {
let component: RowEditorComponent;
beforeEach(() => {
component = new RowEditorComponent();
component.table = new DynamicTableModel(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,73 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn, DynamicRowValidationSummary } from './../../core/index';
@Component({
moduleId: module.id,
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,3 @@
.text-editor {
width: 100%;
}

View File

@ -0,0 +1,11 @@
<div alfresco-mdl-textfield class="text-editor">
<input
class="mdl-textfield__input"
type="text"
[value]="table.getCellValue(row, column)"
(keyup)="onValueChanged(row, column, $event)"
[required]="column.required"
[disabled]="!column.editable"
[attr.id]="column.id">
<label class="mdl-textfield__label" [attr.for]="column.id">{{column.name}}</label>
</div>

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 { TextEditorComponent } from './text.editor';
import { DynamicTableRow, DynamicTableColumn } from './../../../core/index';
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 = { srcElement: { value } };
editor.onValueChanged(row, column, event);
expect(row.value[column.id]).toBe(value);
});
});

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.
*/
import { Component } from '@angular/core';
import { CellEditorComponent } from './../cell.editor';
import { DynamicTableRow, DynamicTableColumn } from './../../../core/index';
@Component({
moduleId: module.id,
selector: 'alf-text-editor',
templateUrl: './text.editor.html',
styleUrls: ['./text.editor.css']
})
export class TextEditorComponent extends CellEditorComponent {
onValueChanged(row: DynamicTableRow, column: DynamicTableColumn, event: any) {
let value: any = (<HTMLInputElement>event.srcElement).value;
row.value[column.id] = value;
}
}

View File

@ -34,6 +34,12 @@ import { FunctionalGroupWidget } from './functional-group/functional-group.widge
import { PeopleWidget } from './people/people.widget';
import { DateWidget } from './date/date.widget';
import { AmountWidget } from './amount/amount.widget';
import { DynamicTableWidget } from './dynamic-table/dynamic-table.widget';
import { DateEditorComponent } from './dynamic-table/editors/date/date.editor';
import { DropdownEditorComponent } from './dynamic-table/editors/dropdown/dropdown.editor';
import { BooleanEditorComponent } from './dynamic-table/editors/boolean/boolean.editor';
import { TextEditorComponent } from './dynamic-table/editors/text/text.editor';
import { RowEditorComponent } from './dynamic-table/editors/row.editor';
// core
export * from './widget.component';
@ -60,6 +66,14 @@ export * from './functional-group/functional-group.widget';
export * from './people/people.widget';
export * from './date/date.widget';
export * from './amount/amount.widget';
export * from './dynamic-table/dynamic-table.widget';
// editors (dynamic table)
export * from './dynamic-table/editors/row.editor';
export * from './dynamic-table/editors/date/date.editor';
export * from './dynamic-table/editors/dropdown/dropdown.editor';
export * from './dynamic-table/editors/boolean/boolean.editor';
export * from './dynamic-table/editors/text/text.editor';
export const WIDGET_DIRECTIVES: any[] = [
TabsWidget,
@ -79,5 +93,12 @@ export const WIDGET_DIRECTIVES: any[] = [
FunctionalGroupWidget,
PeopleWidget,
DateWidget,
AmountWidget
AmountWidget,
DynamicTableWidget,
DateEditorComponent,
DropdownEditorComponent,
BooleanEditorComponent,
TextEditorComponent,
RowEditorComponent
];

View File

@ -12,10 +12,22 @@
class="mdl-tabs__panel"
[class.is-active]="isFirst"
[attr.id]="tab.id">
<container-widget
*ngFor="let field of tab.fields"
[content]="field" (formValueChanged)="tabChanged($event);">
</container-widget>
<div *ngFor="let field of tab.fields">
<div [ngSwitch]="field.type">
<div *ngSwitchCase="'container'">
<container-widget [content]="field" (formValueChanged)="tabChanged($event);"></container-widget>
</div>
<div *ngSwitchCase="'group'">
<container-widget [content]="field" (formValueChanged)="tabChanged($event);"></container-widget>
</div>
<div *ngSwitchCase="'dynamic-table'">
<dynamic-table-widget [content]="field"></dynamic-table-widget>
</div>
<div *ngSwitchDefault>
<span>UNKNOWN WIDGET TYPE: {{field.type}}</span>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -208,7 +208,12 @@ export class FormService {
getRestFieldValues(taskId: string, field: string): Observable<any> {
let alfrescoApi = this.apiService.getInstance();
return Observable.fromPromise(alfrescoApi.activiti.taskFormsApi.getRestFieldValues(taskId, field));
return Observable.fromPromise(alfrescoApi.activiti.taskApi.getRestFieldValues(taskId, field));
}
getRestFieldValuesColumn(taskId: string, field: string, column?: string): Observable<any> {
let alfrescoApi = this.apiService.getInstance();
return Observable.fromPromise(alfrescoApi.activiti.taskApi.getRestFieldValuesColumn(taskId, field, column));
}
// TODO: uses private webApp api

View File

@ -27,7 +27,7 @@ import { WidgetVisibilityService } from './widget-visibility.service';
import { AlfrescoSettingsService, AlfrescoAuthenticationService, AlfrescoApiService } from 'ng2-alfresco-core';
import { TaskProcessVariableModel } from '../models/task-process-variable.model';
import { WidgetVisibilityModel } from '../models/widget-visibility.model';
import { FormModel, FormFieldModel, TabModel, ContainerModel } from '../components/widgets/core/index';
import { FormModel, FormFieldModel, TabModel, ContainerModel, FormFieldTypes } from '../components/widgets/core/index';
declare let jasmine: any;
@ -620,13 +620,18 @@ describe('WidgetVisibilityService', () => {
visibilityObjTest.leftFormFieldId = 'FIELD_TEST';
visibilityObjTest.operator = '!=';
visibilityObjTest.rightFormFieldId = 'RIGHT_FORM_FIELD_ID';
fakeFormWithField.fields[0].columns[0].fields[0].visibilityCondition = visibilityObjTest;
let container = <ContainerModel> fakeFormWithField.fields[0];
let column0 = container.columns[0];
let column1 = container.columns[1];
column0.fields[0].visibilityCondition = visibilityObjTest;
service.refreshVisibility(fakeFormWithField);
expect(fakeFormWithField.fields[0].columns[0].fields[0].isVisible).toBeFalsy();
expect(fakeFormWithField.fields[0].columns[0].fields[1].isVisible).toBeTruthy();
expect(fakeFormWithField.fields[0].columns[0].fields[2].isVisible).toBeTruthy();
expect(fakeFormWithField.fields[0].columns[1].fields[0].isVisible).toBeTruthy();
expect(column0.fields[0].isVisible).toBeFalsy();
expect(column0.fields[1].isVisible).toBeTruthy();
expect(column0.fields[2].isVisible).toBeTruthy();
expect(column1.fields[0].isVisible).toBeTruthy();
});
it('should refresh the visibility for tab in forms', () => {
@ -652,20 +657,21 @@ describe('WidgetVisibilityService', () => {
expect(tab.isVisible).toBeFalsy();
});
it('should refresh the visibility for container in forms', () => {
xit('should refresh the visibility for container in forms', () => {
visibilityObjTest.leftFormFieldId = 'FIELD_TEST';
visibilityObjTest.operator = '!=';
visibilityObjTest.rightFormFieldId = 'RIGHT_FORM_FIELD_ID';
visibilityObjTest.rightFormFieldId = 'LEFT_FORM_FIELD_ID';
let contModel = new ContainerModel(fakeFormWithField, {
id: 'fake-container-id',
type: FormFieldTypes.GROUP,
name: 'fake-container-name',
isVisible: true
isVisible: true,
visibilityCondition: visibilityObjTest
});
contModel.visibilityCondition = visibilityObjTest;
fakeFormWithField.fields[0].visibilityCondition = visibilityObjTest;
service.refreshVisibility(fakeFormWithField);
expect(fakeFormWithField.fields[0].isVisible).toBeFalsy();
fakeFormWithField.fields.push(contModel);
service.refreshVisibility(fakeFormWithField);
expect(contModel.isVisible).toBeFalsy();
});
it('should refresh the visibility for single container', () => {
@ -674,12 +680,12 @@ describe('WidgetVisibilityService', () => {
visibilityObjTest.rightFormFieldId = 'RIGHT_FORM_FIELD_ID';
let contModel = new ContainerModel(fakeFormWithField, {
id: 'fake-container-id',
type: FormFieldTypes.GROUP,
name: 'fake-container-name',
isVisible: true
isVisible: true,
visibilityCondition: visibilityObjTest
});
contModel.visibilityCondition = visibilityObjTest;
service.refreshEntityVisibility(contModel);
service.refreshEntityVisibility(contModel.field);
expect(contModel.isVisible).toBeFalsy();
});
});

View File

@ -19,7 +19,7 @@ import { Injectable } from '@angular/core';
import { Response, Http, Headers, RequestOptions } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { AlfrescoSettingsService, AlfrescoAuthenticationService } from 'ng2-alfresco-core';
import { FormModel, FormFieldModel, TabModel, ContainerModel } from '../components/widgets/core/index';
import { FormModel, FormFieldModel, TabModel } from '../components/widgets/core/index';
import { WidgetVisibilityModel } from '../models/widget-visibility.model';
import { TaskProcessVariableModel } from '../models/task-process-variable.model';
@ -37,16 +37,13 @@ export class WidgetVisibilityService {
if (form && form.tabs && form.tabs.length > 0) {
form.tabs.map(tabModel => this.refreshEntityVisibility(tabModel));
}
if (form && form.fields.length > 0) {
form.fields.map(contModel => {
this.refreshEntityVisibility(contModel);
contModel.columns.map(contColModel =>
contColModel.fields.map(field => this.refreshEntityVisibility(field)));
});
if (form) {
form.getFormFields().map(field => this.refreshEntityVisibility(field));
}
}
refreshEntityVisibility(element: FormFieldModel | ContainerModel | TabModel) {
refreshEntityVisibility(element: FormFieldModel | TabModel) {
element.isVisible = this.evaluateVisibility(element.form, element.visibilityCondition);
}

View File

@ -19,15 +19,18 @@ import { MDL } from './MaterialDesignLiteUpgradeElement';
import { AlfrescoMdlButtonDirective } from './mdl-button.directive';
import { AlfrescoMdlMenuDirective } from './mdl-menu.directive';
import { AlfrescoMdlTabsDirective } from './mdl-tabs.directive';
import { AlfrescoMdlTextFieldDirective } from './mdl-textfield.directive';
export * from './MaterialDesignLiteUpgradeElement';
export * from './mdl-button.directive';
export * from './mdl-menu.directive';
export * from './mdl-tabs.directive';
export * from './mdl-textfield.directive';
export const MATERIAL_DESIGN_DIRECTIVES: [any] = [
MDL,
AlfrescoMdlButtonDirective,
AlfrescoMdlMenuDirective,
AlfrescoMdlTabsDirective
AlfrescoMdlTabsDirective,
AlfrescoMdlTextFieldDirective
];

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.
*/
import { Directive, ElementRef, AfterViewInit } from '@angular/core';
declare var componentHandler;
@Directive({
selector: '[alfresco-mdl-textfield]'
})
export class AlfrescoMdlTextFieldDirective implements AfterViewInit {
constructor(private element: ElementRef) {}
ngAfterViewInit() {
if (componentHandler) {
let el = this.element.nativeElement;
el.classList.add('mdl-textfield');
el.classList.add('mdl-js-textfield');
el.classList.add('mdl-textfield--floating-label');
componentHandler.upgradeElement(el, 'MaterialTextfield');
}
}
}