Merge pull request #477 from Alfresco/dev-denys-forms

Activiti Form widgets
This commit is contained in:
Mario Romano
2016-07-26 17:04:06 +01:00
committed by GitHub
21 changed files with 497 additions and 119 deletions

View File

@@ -25,6 +25,12 @@
{{outcome.name}}
</button>
</div>
<div class="mdl-card__menu">
<button (click)="onRefreshClicked()"
class="mdl-button mdl-button--icon mdl-js-button mdl-js-ripple-effect">
<i class="material-icons">refresh</i>
</button>
</div>
</div>
</div>
</div>

View File

@@ -95,6 +95,12 @@ export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges {
}
}
onRefreshClicked() {
if (this.taskId) {
this.loadForm(this.taskId);
}
}
private loadForm(taskId: string) {
this.formService
.getTaskForm(taskId)

View File

@@ -15,8 +15,8 @@
* limitations under the License.
*/
import { Component, Input, AfterViewInit } from '@angular/core';
import { FormFieldModel } from './../widget.model';
import { Component } from '@angular/core';
import { WidgetComponent } from './../widget.component';
declare let __moduleName: string;
declare var componentHandler;
@@ -26,20 +26,6 @@ declare var componentHandler;
selector: 'checkbox-widget',
templateUrl: './checkbox.widget.html'
})
export class CheckboxWidget implements AfterViewInit {
@Input()
field: FormFieldModel;
hasField() {
return this.field ? true : false;
}
ngAfterViewInit() {
// workaround for MDL issues with dynamic components
if (componentHandler) {
componentHandler.upgradeAllRegistered();
}
}
export class CheckboxWidget extends WidgetComponent {
}

View File

@@ -0,0 +1,20 @@
.container-widget {}
.container-widget__header {}
.container-widget__header-text {
border-bottom: 1px solid rgba(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 */
}
.container-widget__header-text.collapsible {
cursor: pointer;
}

View File

@@ -1,22 +1,45 @@
<div class="mdl-grid">
<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">
<div [ngSwitch]="field.type">
<div *ngSwitchCase="'integer'">
<number-widget [field]="field"></number-widget>
</div>
<div *ngSwitchCase="'text'">
<text-widget [field]="field"></text-widget>
</div>
<div *ngSwitchCase="'multi-line-text'">
<multiline-text-widget [field]="field"></multiline-text-widget>
</div>
<div *ngSwitchCase="'boolean'">
<checkbox-widget [field]="field"></checkbox-widget>
</div>
<div *ngSwitchDefault>
<span>UNKNOWN WIDGET TYPE: {{field.type}}</span>
<div class="container-widget">
<div *ngIf="content?.isGroup()" class="container-widget__header">
<h3 class="container-widget__header-text"
[class.collapsible]="content?.isCollapsible()">
<button *ngIf="content?.isCollapsible()"
alfresco-mdl-button
class="mdl-button--icon"
(click)="onExpanderClicked()">
<i class="material-icons">{{ content?.isExpanded ? 'expand_less' : 'expand_more' }}</i>
</button>
<span (click)="onExpanderClicked()">{{content.name}}</span>
</h3>
</div>
<div class="mdl-grid" *ngIf="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">
<div [ngSwitch]="field.type">
<div *ngSwitchCase="'integer'">
<number-widget [field]="field"></number-widget>
</div>
<div *ngSwitchCase="'text'">
<text-widget [field]="field"></text-widget>
</div>
<div *ngSwitchCase="'multi-line-text'">
<multiline-text-widget [field]="field"></multiline-text-widget>
</div>
<div *ngSwitchCase="'boolean'">
<checkbox-widget [field]="field"></checkbox-widget>
</div>
<div *ngSwitchCase="'dropdown'">
<dropdown-widget [field]="field"></dropdown-widget>
</div>
<div *ngSwitchCase="'hyperlink'">
<hyperlink-widget [field]="field"></hyperlink-widget>
</div>
<div *ngSwitchCase="'radio-buttons'">
<radio-buttons-widget [field]="field"></radio-buttons-widget>
</div>
<div *ngSwitchDefault>
<span>UNKNOWN WIDGET TYPE: {{field.type}}</span>
</div>
</div>
</div>
</div>

View File

@@ -18,10 +18,14 @@
import { Component, Input, AfterViewInit } from '@angular/core';
import { ContainerModel } from './../widget.model';
import { MATERIAL_DESIGN_DIRECTIVES } from 'ng2-alfresco-core';
import { TextWidget } from './../text/text.widget';
import { NumberWidget } from './../number/number.widget';
import { CheckboxWidget } from './../checkbox/checkbox.widget';
import { MultilineTextWidget } from './../multiline-text/multiline-text.widget';
import { DropdownWidget } from './../dropdown/dropdown.widget';
import { HyperlinkWidget } from './../hyperlink/hyperlink.widget';
import { RadioButtonsWidget } from './../radio-buttons/radio-buttons.widget';
declare let __moduleName: string;
declare var componentHandler;
@@ -30,13 +34,29 @@ declare var componentHandler;
moduleId: __moduleName,
selector: 'container-widget',
templateUrl: './container.widget.html',
directives: [TextWidget, NumberWidget, CheckboxWidget, MultilineTextWidget]
styleUrls: ['./container.widget.css'],
directives: [
MATERIAL_DESIGN_DIRECTIVES,
TextWidget,
NumberWidget,
CheckboxWidget,
MultilineTextWidget,
DropdownWidget,
HyperlinkWidget,
RadioButtonsWidget
]
})
export class ContainerWidget implements AfterViewInit {
@Input()
content: ContainerModel;
onExpanderClicked() {
if (this.content && this.content.isCollapsible()) {
this.content.isExpanded = !this.content.isExpanded;
}
}
ngAfterViewInit() {
// workaround for MDL issues with dynamic components
if (componentHandler) {

View File

@@ -0,0 +1,7 @@
.dropdown-widget {
width: 100%;
}
.dropdown-widget > select {
width: 100%;
}

View File

@@ -0,0 +1,5 @@
<div class="dropdown-widget">
<select [(ngModel)]="field.value">
<option *ngFor="let opt of field.options" [value]="opt.id">{{opt.name}}</option>
</select>
</div>

View File

@@ -0,0 +1,81 @@
/*!
* @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 { Observable } from 'rxjs/Rx';
import { Http } from '@angular/http';
import { ObjectUtils } from 'ng2-alfresco-core';
import { WidgetComponent } from './../widget.component';
declare let __moduleName: string;
declare var componentHandler;
@Component({
moduleId: __moduleName,
selector: 'dropdown-widget',
templateUrl: './dropdown.widget.html',
styleUrls: ['./dropdown.widget.css']
})
export class DropdownWidget extends WidgetComponent implements OnInit {
constructor(private http: Http) {
super();
}
ngOnInit() {
if (this.field &&
this.field.optionType === 'rest' &&
this.field.restUrl &&
this.field.restIdProperty &&
this.field.restLabelProperty) {
let url = `${this.field.restUrl}`;
this.http.get(url).subscribe(
response => {
let json: any = response.json();
this.loadFromJson(json);
},
this.handleError
);
}
}
// TODO: support 'restResponsePath'
private loadFromJson(json: any) {
if (json instanceof Array) {
let options = json.map(obj => {
return {
id: ObjectUtils.getValue(obj, this.field.restIdProperty).toString(),
name: ObjectUtils.getValue(obj, this.field.restLabelProperty).toString()
};
});
this.field.options = options;
this.field.updateForm();
}
}
private handleError (error: any) {
// In a real world app, we might use a remote logging infrastructure
// We'd also dig deeper into the error to get a better message
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);
}
}

View File

@@ -0,0 +1 @@
.hyperlink-widget {}

View File

@@ -0,0 +1,3 @@
<div class="hyperlink-widget">
<a [href]="linkUrl" target="_blank" rel="nofollow">{{linkText}}</a>
</div>

View File

@@ -0,0 +1,49 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component } from '@angular/core';
import { WidgetComponent } from './../widget.component';
declare let __moduleName: string;
declare var componentHandler;
@Component({
moduleId: __moduleName,
selector: 'hyperlink-widget',
templateUrl: './hyperlink.widget.html',
styleUrls: ['./hyperlink.widget.css']
})
export class HyperlinkWidget extends WidgetComponent {
get linkUrl(): string {
let url = '#';
if (this.field && this.field.hyperlinkUrl) {
url = this.field.hyperlinkUrl;
if (!/^https?:\/\//i.test(url)) {
url = 'http://' + url;
}
}
return url;
}
get linkText() {
return this.field.displayText || this.field.hyperlinkUrl;
}
}

View File

@@ -23,3 +23,6 @@ export * from './text/text.widget';
export * from './number/number.widget';
export * from './checkbox/checkbox.widget';
export * from './multiline-text/multiline-text.widget';
export * from './dropdown/dropdown.widget';
export * from './hyperlink/hyperlink.widget';
export * from './radio-buttons/radio-buttons.widget';

View File

@@ -15,8 +15,8 @@
* limitations under the License.
*/
import { Component, Input, AfterViewInit } from '@angular/core';
import { FormFieldModel } from './../widget.model';
import { Component } from '@angular/core';
import { WidgetComponent } from './../widget.component';
declare let __moduleName: string;
declare var componentHandler;
@@ -27,20 +27,6 @@ declare var componentHandler;
templateUrl: './multiline-text.widget.html',
styleUrls: ['./multiline-text.widget.css']
})
export class MultilineTextWidget implements AfterViewInit {
@Input()
field: FormFieldModel;
hasField() {
return this.field ? true : false;
}
ngAfterViewInit() {
// workaround for MDL issues with dynamic components
if (componentHandler) {
componentHandler.upgradeAllRegistered();
}
}
export class MultilineTextWidget extends WidgetComponent {
}

View File

@@ -15,8 +15,8 @@
* limitations under the License.
*/
import { Component, Input, AfterViewInit } from '@angular/core';
import { FormFieldModel } from './../widget.model';
import { Component } from '@angular/core';
import { WidgetComponent } from './../widget.component';
declare let __moduleName: string;
declare var componentHandler;
@@ -27,20 +27,6 @@ declare var componentHandler;
templateUrl: './number.widget.html',
styleUrls: ['./number.widget.css']
})
export class NumberWidget implements AfterViewInit {
@Input()
field: FormFieldModel;
hasField() {
return this.field ? true : false;
}
ngAfterViewInit() {
// workaround for MDL issues with dynamic components
if (componentHandler) {
componentHandler.upgradeAllRegistered();
}
}
export class NumberWidget extends WidgetComponent {
}

View File

@@ -0,0 +1 @@
.radio-buttons-widget {}

View File

@@ -0,0 +1,14 @@
<div class="radio-buttons-widget">
<div *ngFor="let opt of field.options">
<label [attr.for]="opt.id" class="mdl-radio mdl-js-radio">
<input type="radio"
[checked]="field.value === opt.id"
[attr.id]="opt.id"
[attr.name]="field.id"
[attr.value]="opt.id"
class="mdl-radio__button"
(click)="field.value = opt.id">
<span class="mdl-radio__label">{{opt.name}}</span>
</label>
</div>
</div>

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.
*/
import { Component } from '@angular/core';
import { WidgetComponent } from './../widget.component';
declare let __moduleName: string;
declare var componentHandler;
@Component({
moduleId: __moduleName,
selector: 'radio-buttons-widget',
templateUrl: './radio-buttons.widget.html',
styleUrls: ['./radio-buttons.widget.css']
})
export class RadioButtonsWidget extends WidgetComponent {
}

View File

@@ -15,8 +15,8 @@
* limitations under the License.
*/
import { Component, Input, AfterViewInit } from '@angular/core';
import { FormFieldModel } from './../widget.model';
import { Component } from '@angular/core';
import { WidgetComponent } from './../widget.component';
declare let __moduleName: string;
declare var componentHandler;
@@ -27,20 +27,6 @@ declare var componentHandler;
templateUrl: './text.widget.html',
styleUrls: ['./text.widget.css']
})
export class TextWidget implements AfterViewInit {
@Input()
field: FormFieldModel;
hasField() {
return this.field ? true : false;
}
ngAfterViewInit() {
// workaround for MDL issues with dynamic components
if (componentHandler) {
componentHandler.upgradeAllRegistered();
}
}
export class TextWidget extends WidgetComponent {
}

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.
*/
import { Input, AfterViewInit } from '@angular/core';
import { FormFieldModel } from './widget.model';
declare let __moduleName: string;
declare var componentHandler;
/**
* Base widget component.
*/
export class WidgetComponent implements AfterViewInit {
@Input()
field: FormFieldModel;
hasField() {
return this.field ? true : false;
}
ngAfterViewInit() {
// workaround for MDL issues with dynamic components
if (componentHandler) {
componentHandler.upgradeAllRegistered();
}
}
}

View File

@@ -15,11 +15,21 @@
* limitations under the License.
*/
export interface FormValues {
export interface FormFieldMetadata {
[key: string]: any;
}
export interface FormValues extends FormFieldMetadata {
}
export class FormFieldTypes {
static CONTAINER: string = 'container';
static GROUP: string = 'group';
static DROPDOWN: string = 'dropdown';
static HYPERLINK: string = 'hyperlink';
static RADIO_BUTTONS: string = 'radio-buttons';
}
export class FormWidgetModel {
private _form: FormModel;
@@ -39,25 +49,35 @@ export class FormWidgetModel {
}
}
export interface FormFieldOption {
id: string;
name: string;
}
export class FormFieldModel extends FormWidgetModel {
private _id: string;
private _name: string;
private _type: string;
private _value: string;
private _tab: string;
get id(): string {
return this._id;
}
get name(): string {
return this._name;
}
get type(): string {
return this._type;
}
fieldType: string;
id: string;
name: string;
type: string;
required: boolean;
readOnly: boolean;
overrideId: boolean;
tab: string;
colspan: number = 1;
options: FormFieldOption[] = [];
restUrl: string;
restResponsePath: string;
restIdProperty: string;
restLabelProperty: string;
hasEmptyValue: boolean;
className: string;
optionType: string;
params: FormFieldMetadata = {};
hyperlinkUrl: string;
displayText: string;
get value(): any {
return this._value;
@@ -65,28 +85,98 @@ export class FormFieldModel extends FormWidgetModel {
set value(v: any) {
this._value = v;
this.form.values[this._id] = v;
this.updateForm();
}
get tab(): string {
return this._tab;
}
colspan: number = 1;
constructor(form: FormModel, json?: any) {
super(form, json);
if (json) {
this._id = json.id;
this._name = json.name;
this._type = json.type;
this._value = json.value;
this._tab = json.tab;
this.fieldType = json.fieldType;
this.id = json.id;
this.name = json.name;
this.type = json.type;
this.required = <boolean> json.required;
this.readOnly = <boolean> json.readOnly;
this.overrideId = <boolean> json.overrideId;
this.tab = json.tab;
this.restUrl = json.restUrl;
this.restResponsePath = json.restResponsePath;
this.restIdProperty = json.restIdProperty;
this.restLabelProperty = json.restLabelProperty;
this.colspan = <number> json.colspan;
this.options = <FormFieldOption[]> json.options || [];
this.hasEmptyValue = <boolean> json.hasEmptyValue;
this.className = json.className;
this.optionType = json.optionType;
this.params = <FormFieldMetadata> json.params || {};
this.hyperlinkUrl = json.hyperlinkUrl;
this.displayText = json.displayText;
// update form values
form.values[json.id] = json.value;
this._value = this.parseValue(json);
this.updateForm();
}
}
private parseValue(json: any): any {
let value = json.value;
/*
This is needed due to Activiti issue related to reading dropdown values as value string
but saving back as object: { id: <id>, name: <name> }
*/
// TODO: needs review
if (json.type === FormFieldTypes.DROPDOWN) {
if (value === '') {
value = 'empty';
}
}
/*
This is needed due to Activiti issue related to reading radio button values as value string
but saving back as object: { id: <id>, name: <name> }
*/
if (json.type === FormFieldTypes.RADIO_BUTTONS) {
// Activiti has a bug with default radio button value,
// so try resolving current one with a fallback to first entry
let entry: FormFieldOption[] = this.options.filter(opt => opt.id === value);
if (entry.length > 0) {
value = entry[0].id;
} else if (this.options.length > 0) {
value = this.options[0].id;
}
}
return value;
}
updateForm() {
if (this.type === FormFieldTypes.DROPDOWN) {
/*
This is needed due to Activiti reading dropdown values as string
but saving back as object: { id: <id>, name: <name> }
*/
if (this.value === 'empty' || this.value === '') {
this.form.values[this.id] = {};
} else {
let entry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value);
if (entry.length > 0) {
this.form.values[this.id] = entry[0];
}
}
} else if (this.type === FormFieldTypes.RADIO_BUTTONS) {
/*
This is needed due to Activiti issue related to reading radio button values as value string
but saving back as object: { id: <id>, name: <name> }
*/
let entry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value);
if (entry.length > 0) {
this.form.values[this.id] = entry[0];
} else if (this.options.length > 0) {
this.form.values[this.id] = this.options[0].id;
}
} else {
this.form.values[this.id] = this.value;
}
}
}
@@ -101,6 +191,7 @@ export class ContainerColumnModel {
}
}
// TODO: inherit FormFieldModel
export class ContainerModel extends FormWidgetModel {
fieldType: string;
@@ -109,8 +200,34 @@ export class ContainerModel extends FormWidgetModel {
type: string;
tab: string;
numberOfColumns: number = 1;
params: FormFieldMetadata = {};
columns: ContainerColumnModel[] = [];
isExpanded: boolean = true;
isGroup(): boolean {
return this.type === FormFieldTypes.GROUP;
}
isCollapsible(): boolean {
let allowCollapse = false;
if (this.isGroup() && this.params['allowCollapse']) {
allowCollapse = <boolean> this.params['allowCollapse'];
}
return allowCollapse;
}
isCollapsedByDefault(): boolean {
let collapseByDefault = false;
if (this.isCollapsible() && this.params['collapseByDefault']) {
collapseByDefault = <boolean> this.params['collapseByDefault'];
}
return collapseByDefault;
}
constructor(form: FormModel, json?: any) {
super(form, json);
@@ -122,6 +239,7 @@ export class ContainerModel extends FormWidgetModel {
this.type = json.type;
this.tab = json.tab;
this.numberOfColumns = <number> json.numberOfColumns;
this.params = <FormFieldMetadata> json.params || {};
let columnSize: number = 12;
if (this.numberOfColumns > 1) {
@@ -139,6 +257,8 @@ export class ContainerModel extends FormWidgetModel {
let col = this.columns[parseInt(key, 10) - 1];
col.fields = fields;
});
this.isExpanded = !this.isCollapsedByDefault();
}
}
}