Merge pull request #1161 from Alfresco/development

0.5.0 release
This commit is contained in:
Eugenio Romano
2016-11-25 22:47:49 +00:00
committed by GitHub
237 changed files with 6661 additions and 2187 deletions
+18 -19
View File
@@ -1,39 +1,38 @@
**Please check if the PR fulfills these requirements**
- [ ] The commit message follows our [guidelines](https://github.com/Alfresco/alfresco-ng2-components/wiki/Commit-format)
- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)
<!--
Before submitting your PR, please check that your code follows our contribution guidelines:
https://github.com/Alfresco/alfresco-ng2-components/wiki/Code-contribution-acceptance-criteria
Check also that your commit messages follow our commit message format guidelines:
https://github.com/Alfresco/alfresco-ng2-components/wiki/Commit-format
-->
**Type of contribution:** (check one with "x")
**What kind of change does this PR introduce?** (check one with "x")
```
[ ] Bugfix
[ ] Feature
[ ] Code formatting
[ ] Code Refactoring
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] Documentation
[ ] Other... Please describe:
```
**This PR adds the following feature:**
<!-- you can ignore this line in the case of a bugfix -->
**Current behavior:**
**What is the current behavior?** (You can also link to an open issue here)
**New behavior:**
**This PR fixes the following issue:**
<!-- link to the open issue, ignore this if there are no issues open -->
**This PR introduces a breaking change:** (check one with "x")
**What is the new behavior?**
**Does this PR introduce a breaking change?** (check one with "x")
```
- [ ] Yes
- [ ] No
[ ] Yes
[ ] No
```
<!-- Please describe the reason to introduce a breaking change, and what exactly breaks -->
**More information:**
If this PR contains a breaking change, please describe the impact and migration path for existing applications: ...
**Other information**:
+15 -17
View File
@@ -1,22 +1,17 @@
language: node_js
dist: trusty
sudo: required
sudo: false
node_js:
- "5"
addons:
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
before_install:
- export CHROME_BIN=/usr/bin/google-chrome
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
env:
matrix:
- MODULE=ng2-alfresco-core
@@ -37,31 +32,34 @@ env:
before_script:
- if ([ "$MODULE" != "ng2-alfresco-core" ]); then
(cd ng2-components/ng2-alfresco-core; npm install; npm link);
(cd ng2-components/ng2-alfresco-core; if [ ! -d "dist" ]; then npm install; fi; npm link);
fi
- if ([ "$MODULE" == "ng2-alfresco-documentlist" ] || [ "$MODULE" == "ng2-alfresco-webscript" ] || [ "$MODULE" == "ng2-activiti-processlist" ] || [ "$MODULE" == "ng2-activiti-tasklist" ]); then
(cd ng2-components/ng2-alfresco-datatable; npm link ng2-alfresco-core; npm install; npm link);
(cd ng2-components/ng2-alfresco-datatable; npm link ng2-alfresco-core; if [ ! -d "dist" ]; then npm install; fi; npm link);
fi
- if ([ "$MODULE" == "ng2-activiti-tasklist" ] || [ "$MODULE" == "ng2-activiti-processlist" ]); then
(cd ng2-components/ng2-activiti-form; npm link ng2-alfresco-core; npm install; npm link);
(cd ng2-components/ng2-activiti-form; npm link ng2-alfresco-core; if [ ! -d "dist" ]; then npm install; fi; npm link);
fi
- if ([ "$MODULE" == "ng2-activiti-processlist" ]); then
(cd ng2-components/ng2-activiti-tasklist; npm link ng2-alfresco-core; npm link ng2-alfresco-datatable; npm link ng2-activiti-form; npm install; npm link);
(cd ng2-components/ng2-activiti-tasklist; npm link ng2-alfresco-core; npm link ng2-alfresco-datatable; npm link ng2-activiti-form; if [ ! -d "dist" ]; then npm install; fi; npm link);
fi
- if ([ "$MODULE" == "ng2-activiti-analytics" ]); then
(cd ng2-components/ng2-activiti-diagrams; npm link ng2-alfresco-core; npm install; npm link);
(cd ng2-components/ng2-activiti-diagrams; npm link ng2-alfresco-core; if [ ! -d "dist" ]; then npm install; fi; npm link);
fi
- cd ng2-components/$MODULE;
- npm run travis
- npm run travis;
- npm install;
- ls -ltrh ./node_modules/
script: npm run test
# Send coverage data to Coveralls
after_success:
- bash <(curl -s https://codecov.io/bash)
- if ([ "$TRAVIS_BRANCH" == "development" ] || [ "$TRAVIS_BRANCH" == "master" ]); then
bash <(curl -s https://codecov.io/bash)
fi
cache:
directories:
- ./node_modules/material-design-lite
- ./node_modules/material-design-icons
- demo-shell-ng2/node_modules
- ng2-components/ng2-activiti-form/node_modules
- ng2-components/ng2-activiti-processlist/node_modules
+25 -11
View File
@@ -6,6 +6,7 @@ The [Angular 2](https://angular.io/) based application development framework req
- [Download and install Activiti](https://www.alfresco.com/products/bpm/alfresco-activiti/trial)
- [Node.js](https://nodejs.org/en/) JavaScript runtime.
- [npm](https://www.npmjs.com/) package manager for JavaScript.
- local nginx proxy to avoid cross-origin browser restrictions (see below)
- (If you use ECM and BPM together) Make sure your user has the same username and password in both system
*Note: Default username for activiti is "admin@app.activiti.com" and "admin" for Alfresco, and also the default password are different. Change them to be equal.*
@@ -25,26 +26,39 @@ $ node -v
v5.12.0
```
## Configure Nginx
## Installing nginx
To correctly configure Nginx use the following file [nginx.conf](/nginx.conf).
This will put Activiti, Alfresco and the app dev framework under the same domain.
Most Linux distributions will come with nginx available to install via your
package manager and on Mac OS you can use [Homebrew](http://brew.sh/).
If you want to install manually however you can follow the instructions on the
[download page](http://nginx.org/en/download.html). See also the specific information for
[windows users](http://nginx.org/en/docs/windows.html).
### Start nginx
Start nginx using the supplied configuration in [nginx.conf](nginx.conf)
nginx -c nginx.conf
### Review nginx configuration
To correctly configure nginx use the following file [nginx.conf](/nginx.conf).
This will host Activiti, Alfresco and the app dev framework under the same origin.
* ECM : http://localhost:8888/alfresco/
* BPM : http://localhost:8888/activiti/
To make everything work, you have to change the address of the ECM and BPM. In the demo app you can do that clicking on the top left menu and changing the bottom left options: ECM host and BPM host.
To make everything work, you have to change the address of the ECM and BPM. In the demo app you can do that clicking on the top right settings menu and changing the bottom left options: *ECM host* and *BPM host*.
This configuration assumes few things:
* Port mapping:
* Nginx entry point: 8888
* Demo Shell: 3000
* Alfresco: 8080
* Activiti: 9999
* nginx entry point: 0.0.0.0:8888
* Demo Shell: locathost:3000
* Alfresco: locathost:8080
* Activiti: locathost:9999
All those values can be modified at their respective `location` directive on the [nginx.conf](/nginx.conf) file.
It also need to be compiled with the [Headers More](https://www.nginx.com/resources/wiki/modules/headers_more/) module , which add more control over sending headers to the backend.
If you want to know more on how to install and configure Nginx to work with the Application Development Framework can be found [here](https://community.alfresco.com/community/application-development-framework/blog/2016/09/28/adf-development-set-up-with-nginx-proxy)
If you want to know more on how to install and configure nginx to work with the Application Development Framework can be found [here](https://community.alfresco.com/community/application-development-framework/blog/2016/09/28/adf-development-set-up-with-nginx-proxy)
+19
View File
@@ -36,6 +36,7 @@ install:
- IF %COMPONENT_NAME% EQU ng2-activiti-tasklist (cd ng2-components/ng2-alfresco-datatable && npm link ng2-alfresco-core && npm install && npm link && cd ../../)
- IF %COMPONENT_NAME% EQU ng2-activiti-tasklist (cd ng2-components/ng2-activiti-form && npm link ng2-alfresco-core && npm install && npm link && cd ../../)
- IF %COMPONENT_NAME% EQU ng2-alfresco-webscript (cd ng2-components/ng2-alfresco-datatable && npm link ng2-alfresco-core && npm install && npm link && cd ../../)
- IF %COMPONENT_NAME% EQU ng2-activiti-analytics (cd ng2-components/ng2-activiti-diagrams && npm link link ng2-alfresco-core && npm install && npm link && cd ../../)
- cd ng2-components/%COMPONENT_NAME%
- IF %COMPONENT_NAME% NEQ ng2-alfresco-core (npm link ng2-alfresco-core)
- IF %COMPONENT_NAME% EQU ng2-alfresco-documentlist (npm link ng2-alfresco-datatable)
@@ -55,3 +56,21 @@ test_script:
# Don't actually build.
build: off
cache:
- node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-activiti-form\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-activiti-processlist\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-activiti-tasklist\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-alfresco-core\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-alfresco-datatable\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-alfresco-documentlist\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-alfresco-login\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-alfresco-search\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-alfresco-upload\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-alfresco-viewer\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-alfresco-webscript\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-alfresco-tag\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-activiti-analytics\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-alfresco-userinfo\node_modules
- C:\projects\alfresco-ng2-components\ng2-components\ng2-activiti-diagrams\node_modules
+1 -1
View File
@@ -9,7 +9,7 @@
}
.user-profile {
margin-right: 50px;
margin-right: 10px;
}
.mdl-button-padding {
+9 -7
View File
@@ -4,10 +4,9 @@
<header class="mdl-layout__header">
<div class="mdl-button-padding mdl-layout__header-row">
<div *ngIf="isLoggedIn()">
<ng2-alfresco-userinfo class="user-profile" [menuOpenType]="left">
</ng2-alfresco-userinfo>
</div>
<ng2-alfresco-userinfo class="user-profile" [menuOpenType]="left">
</ng2-alfresco-userinfo>
<!-- Title -->
@@ -20,7 +19,8 @@
<!-- Navigation. We hide it in small screens. -->
<nav class="mdl-navigation mdl-layout--large-screen-only">
<a class="mdl-navigation__link" data-automation-id="files" href="" routerLink="/files">DocumentList</a>
<a class="mdl-navigation__link" data-automation-id="files" href=""
routerLink="/files">DocumentList</a>
<a class="mdl-navigation__link" data-automation-id="datatable" href="" routerLink="/datatable">DataTable</a>
<a class="mdl-navigation__link" data-automation-id="uploader" href="" routerLink="/uploader">Uploader</a>
<a class="mdl-navigation__link" data-automation-id="activiti" href="" routerLink="/activiti">Activiti</a>
@@ -66,10 +66,12 @@
<span class="mdl-layout-title">Components List</span>
<nav class="mdl-navigation">
<a class="mdl-navigation__link" href="" routerLink="/files" (click)="hideDrawer()">DocumentList Demo</a>
<a class="mdl-navigation__link" href="" routerLink="/datatable" (click)="hideDrawer()">DataTable Demo</a>
<a class="mdl-navigation__link" href="" routerLink="/datatable" (click)="hideDrawer()">DataTable
Demo</a>
<a class="mdl-navigation__link" href="" routerLink="/uploader" (click)="hideDrawer()">Uploader Demo</a>
<a class="mdl-navigation__link" href="" routerLink="/login" (click)="hideDrawer()">Login Demo</a>
<a class="mdl-navigation__link" href="" routerLink="/activiti" (click)="hideDrawer()">Activiti Components Demo</a>
<a class="mdl-navigation__link" href="" routerLink="/activiti" (click)="hideDrawer()">Activiti
Components Demo</a>
<a class="mdl-navigation__link" href="" routerLink="/webscript" (click)="hideDrawer()">Webscript</a>
<a class="mdl-navigation__link" href="" routerLink="/tag" (click)="hideDrawer()">Tag</a>
<a class="mdl-navigation__link" href="" routerLink="/settings" (click)="hideDrawer()">Settings</a>
+2 -1
View File
@@ -46,7 +46,8 @@ export class AppComponent {
this.setProvider();
if (translate) {
translate.addTranslationFolder();
translate.addTranslationFolder('custom', 'custom-translation/');
translate.addTranslationFolder('ng2-alfresco-login', 'custom-translation/alfresco-login');
}
}
+3 -1
View File
@@ -35,6 +35,7 @@ import { AnalyticsModule } from 'ng2-activiti-analytics';
import { AppComponent } from './app.component';
import { routing } from './app.routes';
import { CustomEditorsModule } from './components/activiti/custom-editor/custom-editor.component';
import {
DataTableDemoComponent,
@@ -68,7 +69,8 @@ import {
ActivitiTaskListModule.forRoot(),
ActivitiProcessListModule.forRoot(),
UserInfoComponentModule.forRoot(),
AnalyticsModule.forRoot()
AnalyticsModule.forRoot(),
CustomEditorsModule
],
declarations: [
AppComponent,
@@ -12,3 +12,7 @@
padding: 10px 10px 10px 10px;
border: solid 2px rgb(31,188,210);
}
.mdl-layout__header {
z-index: 1;
}
@@ -36,7 +36,13 @@
</div>
<div class="mdl-cell mdl-cell--3-col task-column mdl-shadow--2dp">
<span>Task List</span>
<activiti-tasklist [taskFilter]="taskFilter" [data]="dataTasks"
<activiti-tasklist *ngIf="taskFilter?.hasFilter()" [appId]="taskFilter.appId"
[processDefinitionKey]="taskFilter.filter.processDefinitionKey"
[name]="taskFilter.filter.name"
[assignment]="taskFilter.filter.assignment"
[state]="taskFilter.filter.state"
[sort]="taskFilter.filter.sort"
[data]="dataTasks"
(rowClick)="onTaskRowClick($event)" (onSuccess)="onSuccessTaskList($event)"
#activititasklist></activiti-tasklist>
</div>
@@ -58,21 +64,30 @@
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--2-col task-column">
<span>Process Filters</span>
<activiti-start-process-instance [appId]="appId" (start)="onStartProcessInstance($event)"></activiti-start-process-instance>
<button type="button" (click)="navigateStartProcess()" class="mdl-button" data-automation-id="btn-start-process">Start Process</button>
<activiti-process-instance-filters [appId]="appId"
(filterClick)="onProcessFilterClick($event)" (onSuccess)="onSuccessProcessFilterList($event)"
#activitiprocessfilter></activiti-process-instance-filters>
</div>
<div class="mdl-cell mdl-cell--3-col task-column">
<span>Process List</span>
<activiti-process-instance-list [filter]="processFilter" [data]="dataProcesses"
<activiti-process-instance-list *ngIf="processFilter?.hasFilter()" [appId]="processFilter.appId"
[processDefinitionKey]="processFilter.filter.processDefinitionKey"
[name]="processFilter.filter.name"
[state]="processFilter.filter.state"
[sort]="processFilter.filter.sort"
[data]="dataProcesses"
(rowClick)="onProcessRowClick($event)" (onSuccess)="onSuccessProcessList($event)"
#activitiprocesslist></activiti-process-instance-list>
</div>
<div class="mdl-cell mdl-cell--7-col task-column">
<div class="mdl-cell mdl-cell--7-col task-column" *ngIf="!isStartProcessMode()">
<span>Process Details</span>
<activiti-process-instance-details [processInstanceId]="currentProcessInstanceId" (activitiprocesslist)="taskFormCompleted()" (processCancelled)="processCancelled()" #activitiprocessdetails></activiti-process-instance-details>
</div>
<div class="mdl-cell mdl-cell--7-col task-column" *ngIf="isStartProcessMode()">
<span>Start Process</span>
<activiti-start-process [appId]="appId" (start)="onStartProcessInstance($event)"></activiti-start-process>
</div>
</div>
</div>
</div>
@@ -22,7 +22,11 @@ import {
ActivitiApps,
ActivitiTaskList
} from 'ng2-activiti-tasklist';
import { ActivitiProcessInstanceListComponent } from 'ng2-activiti-processlist';
import {
ActivitiProcessInstanceListComponent,
ActivitiStartProcessInstance,
ProcessInstance
} from 'ng2-activiti-processlist';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Rx';
import {
@@ -30,9 +34,14 @@ import {
DataSorting
} from 'ng2-alfresco-datatable';
import { FormRenderingService } from 'ng2-activiti-form';
import { /*CustomEditorComponent*/ CustomStencil01 } from './custom-editor/custom-editor.component';
declare let __moduleName: string;
declare var componentHandler;
const currentProcessIdNew = '__NEW__';
@Component({
moduleId: __moduleName,
selector: 'activiti-demo',
@@ -62,6 +71,9 @@ export class ActivitiDemoComponent implements AfterViewChecked {
@ViewChild('activitiprocessdetails')
activitiprocessdetails: any;
@ViewChild(ActivitiStartProcessInstance)
activitiStartProcess: ActivitiStartProcessInstance;
@ViewChild('tabmain')
tabMain: any;
@@ -78,16 +90,16 @@ export class ActivitiDemoComponent implements AfterViewChecked {
taskSchemaColumns: any [] = [];
processSchemaColumns: any [] = [];
taskFilter: any;
taskFilter: FilterRepresentationModel;
report: any;
processFilter: any;
processFilter: FilterRepresentationModel;
sub: Subscription;
dataTasks: ObjectDataTableAdapter;
dataProcesses: ObjectDataTableAdapter;
constructor(private route: ActivatedRoute) {
constructor(private route: ActivatedRoute, private formRenderingService: FormRenderingService) {
this.dataTasks = new ObjectDataTableAdapter(
[],
[
@@ -100,11 +112,16 @@ export class ActivitiDemoComponent implements AfterViewChecked {
this.dataProcesses = new ObjectDataTableAdapter(
[],
[
{type: 'text', key: 'name', title: 'Name', cssClass: 'full-width name-column', sortable: true},
{type: 'text', key: 'started', title: 'Started', cssClass: 'hidden', sortable: true}
{type: 'text', key: 'name', title: 'Name', cssClass: 'full-width name-column'},
{type: 'text', key: 'started', title: 'Started', cssClass: 'hidden'}
]
);
this.dataProcesses.setSorting(new DataSorting('started', 'desc'));
// Uncomment this line to replace all 'text' field editors with custom component
// formRenderingService.setComponentTypeResolver('text', () => CustomEditorComponent, true);
// Uncomment this line to map 'custom_stencil_01' to local editor component
formRenderingService.setComponentTypeResolver('custom_stencil_01', () => CustomStencil01, true);
}
ngOnInit() {
@@ -154,10 +171,10 @@ export class ActivitiDemoComponent implements AfterViewChecked {
}
onSuccessTaskList(event: FilterRepresentationModel) {
this.currentTaskId = this.activititasklist.getCurrentTaskId();
this.currentTaskId = this.activititasklist.getCurrentId();
}
onProcessFilterClick(event: any) {
onProcessFilterClick(event: FilterRepresentationModel) {
this.processFilter = event;
}
@@ -166,7 +183,7 @@ export class ActivitiDemoComponent implements AfterViewChecked {
}
onSuccessProcessList(event: any) {
this.currentProcessInstanceId = this.activitiprocesslist.getCurrentProcessId();
this.currentProcessInstanceId = this.activitiprocesslist.getCurrentId();
}
onTaskRowClick(taskId) {
@@ -177,8 +194,17 @@ export class ActivitiDemoComponent implements AfterViewChecked {
this.currentProcessInstanceId = processInstanceId;
}
onStartProcessInstance() {
this.activitiprocesslist.reload();
navigateStartProcess() {
this.currentProcessInstanceId = currentProcessIdNew;
}
onStartProcessInstance(instance: ProcessInstance) {
this.currentProcessInstanceId = instance.id;
this.activitiStartProcess.reset();
}
isStartProcessMode() {
return this.currentProcessInstanceId === currentProcessIdNew;
}
processCancelled(data: any) {
@@ -195,7 +221,7 @@ export class ActivitiDemoComponent implements AfterViewChecked {
}
onFormCompleted(form) {
this.activititasklist.load(this.taskFilter);
this.activititasklist.reload();
this.currentTaskId = null;
}
@@ -0,0 +1,23 @@
import { NgModule, Component } from '@angular/core';
import { WidgetComponent } from 'ng2-activiti-form';
@Component({
selector: 'custom-editor',
template: `
<div style="color: red">Look, I'm a custom editor!</div>
`
})
export class CustomEditorComponent extends WidgetComponent {}
@Component({
selector: 'custom-stencil-01',
template: `<div style="color: green">ADF version of custom Activiti stencil</div>`
})
export class CustomStencil01 extends WidgetComponent {}
@NgModule({
declarations: [ CustomEditorComponent, CustomStencil01 ],
exports: [ CustomEditorComponent, CustomStencil01 ],
entryComponents: [ CustomEditorComponent, CustomStencil01 ]
})
export class CustomEditorsModule {}
@@ -73,7 +73,6 @@ export class LoginDemoComponent implements OnInit {
}
onLogin($event) {
console.log($event);
this.router.navigate(['/home']);
}
@@ -0,0 +1,26 @@
{
"LOGIN": {
"LOGO": "Alfresco",
"LABEL": {
"LOGIN": "Login",
"USERNAME": "Username",
"PASSWORD": "Password",
"REMEMBER": "Remember"
},
"MESSAGES": {
"USERNAME-REQUIRED": "Required",
"USERNAME-MIN": "Your username needs to be at least 4 characters.",
"PASSWORD-REQUIRED": "Enter your password to sign in",
"LOGIN-ERROR-CREDENTIALS": "You have entered an invalid username or password",
"LOGIN-ERROR-PROVIDERS": "Providers cannot be undefined",
"LOGIN-SUCCESS": "Login successful"
},
"BUTTON": {
"LOGIN": "SIGN IN"
},
"ACTION": {
"HELP": "NEED HELP?",
"REGISTER": "REGISTER"
}
}
}
@@ -0,0 +1,25 @@
{
"LOGIN": {
"LABEL": {
"LOGIN": "Autenticazione",
"USERNAME": "Nome utente",
"PASSWORD": "Password",
"REMEMBER": "Ricordami"
},
"MESSAGES": {
"USERNAME-REQUIRED": "Campo obbligatorio",
"USERNAME-MIN": "Inserire un nome utente di minimo 4 caratteri.",
"PASSWORD-REQUIRED": "Campo obbligatorio",
"LOGIN-ERROR-CREDENTIALS": "Nome utente o password non validi",
"LOGIN-ERROR-PROVIDERS": "Providers non può essere nullo",
"LOGIN-SUCCESS": "Login effettuata con successo"
},
"BUTTON": {
"LOGIN": "Accedi"
},
"ACTION": {
"HELP": "BISOGNO DI AIUTO?",
"REGISTER": "REGISTRATI"
}
}
}
@@ -0,0 +1,24 @@
{
"title": "Welcome",
"DOCUMENT_LIST": {
"COLUMNS": {
"DISPLAY_NAME": "Display name",
"CREATED_BY": "Created by",
"CREATED_ON": "Created on"
},
"ACTIONS": {
"FOLDER": {
"SYSTEM_1": "System folder action 1",
"CUSTOM": "Custom folder action",
"DELETE": "Delete"
},
"DOCUMENT": {
"DOWNLOAD": "Download",
"DELETE": "Delete",
"SYSTEM_2": "System document action 2",
"CUSTOM": "Custom action"
}
}
}
}
@@ -18,24 +18,8 @@
"CUSTOM": "Custom action"
}
}
},
"DATATABLE": {
"COLUMNS": {
"DISPLAY_NAME": "Display name",
"CREATED_BY": "Created by",
"CREATED_ON": "Created on"
},
"ACTIONS": {
"FOLDER": {
"SYSTEM_1": "System folder action 1",
"CUSTOM": "Custom folder action"
},
"DOCUMENT": {
"DOWNLOAD": "Download",
"SYSTEM_2": "System document action 2",
"CUSTOM": "Custom action"
}
}
}
}
-67
View File
@@ -1,67 +0,0 @@
{
"title": "Welcome",
"DOCUMENT_LIST": {
"COLUMNS": {
"DISPLAY_NAME": "Display name",
"CREATED_BY": "Created by",
"CREATED_ON": "Created on"
},
"ACTIONS": {
"FOLDER": {
"SYSTEM_1": "System folder action 1",
"CUSTOM": "Custom folder action",
"DELETE": "Delete"
},
"DOCUMENT": {
"DOWNLOAD": "Download",
"DELETE": "Delete",
"SYSTEM_2": "System document action 2",
"CUSTOM": "Custom action"
}
}
},
"DATATABLE": {
"COLUMNS": {
"DISPLAY_NAME": "Display name",
"CREATED_BY": "Created by",
"CREATED_ON": "Created on"
},
"ACTIONS": {
"FOLDER": {
"SYSTEM_1": "System folder action 1",
"CUSTOM": "Custom folder action"
},
"DOCUMENT": {
"DOWNLOAD": "Download",
"SYSTEM_2": "System document action 2",
"CUSTOM": "Custom action"
}
}
},
"LOGIN": {
"LOGO": "Alfresco",
"LABEL": {
"LOGIN": "Login",
"USERNAME": "Username",
"PASSWORD": "Password",
"REMEMBER": "Remember"
},
"MESSAGES": {
"USERNAME-REQUIRED": "Required",
"USERNAME-MIN": "Your username needs to be at least 4 characters.",
"PASSWORD-REQUIRED": "Enter your password to sign in",
"LOGIN-ERROR": "You have entered an invalid username or password",
"LOGIN-SUCCESS": "Login successful"
},
"BUTTON": {
"LOGIN": "Login"
},
"ACTION": {
"HELP": "NEED HELP?",
"REGISTER": "REGISTER"
}
}
}
+5
View File
@@ -48,6 +48,11 @@
<script src="node_modules/md-date-time-picker/dist/js/mdDateTimePicker.min.js"></script>
<script src="node_modules/md-date-time-picker/dist/js/draggabilly.pkgd.min.js"></script>
<!-- Stencils integration -->
<script src="node_modules/ng2-activiti-form/stencils/runtime.ng1.js"></script>
<script src="node_modules/ng2-activiti-form/stencils/runtime.adf.js"></script>
<script src="http://localhost:9999/activiti-app/app/rest/script-files/controllers"></script>
<!-- 2. Configure SystemJS -->
<script src="systemjs.config.js"></script>
<script>
+16 -16
View File
@@ -1,7 +1,7 @@
{
"name": "Alfresco-Angular2-Demo",
"description": "Demo shell for Alfresco Angular2 components",
"version": "0.4.0",
"version": "0.5.0",
"author": "Alfresco Software, Ltd.",
"scripts": {
"clean": "npm install rimraf && rimraf dist node_modules typings",
@@ -81,21 +81,21 @@
"ng2-charts": "1.1.0",
"raphael": "^2.2.6",
"md-date-time-picker": "^2.2.0",
"alfresco-js-api": "^0.4.0",
"ng2-activiti-analytics": "0.4.0",
"ng2-alfresco-core": "0.4.0",
"ng2-alfresco-datatable": "0.4.0",
"ng2-alfresco-documentlist": "0.4.0",
"ng2-alfresco-login": "0.4.0",
"ng2-alfresco-search": "0.4.0",
"ng2-alfresco-upload": "0.4.0",
"ng2-alfresco-viewer": "0.4.0",
"ng2-activiti-form": "0.4.0",
"ng2-activiti-tasklist": "0.4.0",
"ng2-alfresco-userinfo": "0.4.0",
"ng2-activiti-processlist": "0.4.0",
"ng2-alfresco-webscript": "0.4.0",
"ng2-alfresco-tag": "0.4.0",
"alfresco-js-api": "^0.5.0",
"ng2-activiti-analytics": "0.5.0",
"ng2-alfresco-core": "0.5.0",
"ng2-alfresco-datatable": "0.5.0",
"ng2-alfresco-documentlist": "0.5.0",
"ng2-alfresco-login": "0.5.0",
"ng2-alfresco-search": "0.5.0",
"ng2-alfresco-upload": "0.5.0",
"ng2-alfresco-viewer": "0.5.0",
"ng2-activiti-form": "0.5.0",
"ng2-activiti-tasklist": "0.5.0",
"ng2-alfresco-userinfo": "0.5.0",
"ng2-activiti-processlist": "0.5.0",
"ng2-alfresco-webscript": "0.5.0",
"ng2-alfresco-tag": "0.5.0",
"dialog-polyfill": "^0.4.3",
"element.scrollintoviewifneeded-polyfill": "^1.0.1"
},
@@ -56,10 +56,10 @@
"raphael": "^2.2.6",
"ng2-translate": "2.5.0",
"alfresco-js-api": "^0.4.0",
"ng2-alfresco-core": "0.4.0",
"ng2-activiti-diagrams": "0.4.0",
"ng2-activiti-analytics": "^0.4.0"
"alfresco-js-api": "^0.5.0",
"ng2-alfresco-core": "0.5.0",
"ng2-activiti-diagrams": "0.5.0",
"ng2-activiti-analytics": "^0.5.0"
},
"devDependencies": {
"@types/node": "^6.0.42",
@@ -1,7 +1,7 @@
{
"name": "ng2-activiti-analytics",
"description": "Activiti Angular2 Analytics Component",
"version": "0.4.0",
"version": "0.5.0",
"author": "Alfresco Software, Ltd.",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
@@ -54,22 +54,16 @@
"systemjs": "0.19.27",
"zone.js": "^0.6.23",
"intl": "1.2.4",
"dialog-polyfill": "^0.4.3",
"element.scrollintoviewifneeded-polyfill": "^1.0.1",
"material-design-icons": "2.2.3",
"material-design-lite": "1.2.1",
"chart.js": "^2.1.4",
"md-date-time-picker": "^2.2.0",
"ng2-charts": "1.1.0",
"moment": "2.15.1",
"raphael": "^2.2.6",
"alfresco-js-api": "^0.4.0",
"alfresco-js-api": "^0.5.0",
"ng2-translate": "2.5.0",
"ng2-alfresco-core": "0.4.0",
"ng2-activiti-diagrams": "0.4.0"
"ng2-alfresco-core": "0.5.0",
"ng2-activiti-diagrams": "0.5.0"
},
"devDependencies": {
"@types/node": "^6.0.42",
@@ -45,7 +45,7 @@ export class AnalyticsReportHeatMapComponent implements OnInit {
private analyticsService: AnalyticsService,
private formBuilder: FormBuilder) {
if (translate) {
translate.addTranslationFolder('node_modules/ng2-activiti-analytics/src');
translate.addTranslationFolder('ng2-activiti-analytics', 'node_modules/ng2-activiti-analytics/dist/src');
}
}
@@ -68,7 +68,7 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges {
private analyticsService: AnalyticsService,
private formBuilder: FormBuilder ) {
if (translate) {
translate.addTranslationFolder('node_modules/ng2-activiti-analytics/src');
translate.addTranslationFolder('ng2-activiti-analytics', 'node_modules/ng2-activiti-analytics/dist/src');
}
}
@@ -69,7 +69,7 @@ export class AnalyticsComponent implements OnChanges {
private analyticsService: AnalyticsService) {
console.log('AnalyticsComponent');
if (translate) {
translate.addTranslationFolder('node_modules/ng2-activiti-analytics/src');
translate.addTranslationFolder('ng2-activiti-analytics', 'node_modules/ng2-activiti-analytics/dist/src');
}
}
@@ -52,9 +52,9 @@
"raphael": "^2.2.6",
"ng2-translate": "2.5.0",
"alfresco-js-api": "^0.4.0",
"ng2-alfresco-core": "0.4.0",
"ng2-activiti-diagrams": "^0.4.0"
"alfresco-js-api": "^0.5.0",
"ng2-alfresco-core": "0.5.0",
"ng2-activiti-diagrams": "^0.5.0"
},
"devDependencies": {
"@types/node": "^6.0.42",
@@ -1,7 +1,7 @@
{
"name": "ng2-activiti-diagrams",
"description": "Activiti Angular2 Diagrams Component",
"version": "0.4.0",
"version": "0.5.0",
"author": "Alfresco Software, Ltd.",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
@@ -50,17 +50,11 @@
"systemjs": "0.19.27",
"zone.js": "^0.6.23",
"intl": "1.2.4",
"dialog-polyfill": "^0.4.3",
"element.scrollintoviewifneeded-polyfill": "^1.0.1",
"material-design-icons": "2.2.3",
"material-design-lite": "1.2.1",
"raphael": "^2.2.6",
"ng2-translate": "2.5.0",
"alfresco-js-api": "^0.4.0",
"ng2-alfresco-core": "0.4.0"
"alfresco-js-api": "^0.5.0",
"ng2-alfresco-core": "0.5.0"
},
"devDependencies": {
"@types/node": "^6.0.42",
@@ -54,7 +54,7 @@ export class DiagramComponent {
private raphaelService: RaphaelService,
private diagramsService: DiagramsService) {
if (translate) {
translate.addTranslationFolder('node_modules/ng2-activiti-analytics/src');
translate.addTranslationFolder('ng2-activiti-analytics', 'node_modules/ng2-activiti-analytics/dist/src');
}
this.elementRef = elementRef;
}
+18 -9
View File
@@ -110,7 +110,6 @@ Usage example of this component :
**main.ts**
```ts
import { NgModule, Component } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
@@ -150,8 +149,6 @@ export class AppModule {
}
platformBrowserDynamic().bootstrapModule(AppModule);
```
### Display form instance by task id
@@ -173,7 +170,7 @@ For an existing Task both form and values will be fetched and displayed.
</activiti-form>
```
Only form definition will be fetched
Only form definition will be fetched.
### Display form definition by form name
@@ -184,8 +181,11 @@ Only form definition will be fetched
</activiti-form>
```
### Display form definition by ECM nodeId, in this case the metadata of the node are showed in an activiti Form. If there are no form
definied in activiti for the type of the node, a new form will be automaticaly created in activiti.
### Display form definition by ECM nodeId
In this case the metadata of the node are showed in an activiti Form.
If there is no form definied in activiti for the type of the node,
a new form will be automaticaly created in Activiti.
```html
<activiti-form
@@ -193,7 +193,9 @@ definied in activiti for the type of the node, a new form will be automaticaly c
</activiti-form>
```
### Display form definition by form name, and store the form field as metadata. The param nameNode is optional.
### Display form definition by form name, and store the form field as metadata.
The param nameNode is optional.
```html
<activiti-form
@@ -204,8 +206,10 @@ definied in activiti for the type of the node, a new form will be automaticaly c
</activiti-form>
```
### Display form definition by ECM nodeId, in this case the metadata of the node are showed in an activiti Form, and store the form field
as metadata. The param nameNode is optional.
### Display form definition by ECM nodeId
In this case the metadata of the node are showed in an activiti Form,
and store the form field as metadata. The param nameNode is optional.
```html
<activiti-form
@@ -324,6 +328,11 @@ There are two additional functions that can be of a great value when controlling
**Please note that if `event.preventDefault()` is not called then default outcome behaviour
will also be executed after your custom code.**
## See also
- [Form Stencils with Angular 2](docs/stencils.md)
- [Form Extensibility and Customisation](docs/extensibility.md).
## Build from sources
Alternatively you can build component from sources with the following commands:
@@ -53,9 +53,9 @@
"md-date-time-picker": "^2.2.0",
"ng2-translate": "2.5.0",
"alfresco-js-api": "^0.4.0",
"ng2-alfresco-core": "0.4.0",
"ng2-activiti-form": "^0.4.0"
"alfresco-js-api": "^0.5.0",
"ng2-alfresco-core": "0.5.0",
"ng2-activiti-form": "^0.5.0"
},
"devDependencies": {
"@types/node": "^6.0.42",
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@@ -0,0 +1,261 @@
# Form Extensibility and Customisation
_Note: it is assumed you are familiar with Alfresco Activiti form definition structure._
- How components and widgets are rendred on a Form
- Replacing default form widgets with custom components
- Replacing custom stencils with custom components
## How components and widgets are rendred on a Form
All form field editors (aka widgets) on a Form are rendered by means of `FormFieldComponent`
that takes an instance of a `FormFieldModel`:
```html
<form-field [field]="field"></form-field>
```
This component depends on `FormRenderingService` service to map `FormFieldModel` to UI component
based on field type or metadata information.
### Component type resolvers
`FormRenderingService` maps field types to corresponding instances exposing `ComponentTypeResolver` interface:
```ts
export interface ComponentTypeResolver {
(field: FormFieldModel): Type<{}>;
}
```
Typically a `ComponentTypeResolver` is a function that takes `FormFieldModel` and returns corresponding component type.
It can be either a predefined component type or a dynamically evaluated based on field properties and metadata.
#### Static component mapping
You can (re)map fields like following:
```ts
let customResolver: ComponentTypeResolver = () => CustomWidgetComponent;
formRenderingService.setComponentTypeResolver('text', customResolver, true);
```
or simply:
```ts
formRenderingService.setComponentTypeResolver('text', () => CustomWidgetComponent, true);
```
#### Dynamic component mapping
Alternatively your resolver may return different component types based on `FormFieldModel` state and condition:
```ts
let customResolver: ComponentTypeResolver = (field: FormFieldModel): Type<{}> => {
if (field) {
let params = field.params;
}
return UnknownWidget;
};
formRenderingService.setComponentTypeResolver('text', customResolver, true);
```
### Default component mappings
Stencil Name | Field Type | Component Type |
| --- | --- | --- |
| Text | text | TextWidget |
| Number | integer | NumberWidget |
| Multi-line text | multi-line-text | MultilineTextWidget |
| Checkbox | boolean | CheckboxWidget |
| Dropdown | dropdown | DropdownWidget |
| Date | date | DateWidget |
| Amount | amount | AmountWidget |
| Radio buttons | radio-buttons | RadioButtonsWidget |
| Hyperlink | hyperlink | HyperlinkWidget |
| Display value | readonly | DisplayValueWidget |
| Display text | readonly-text | DisplayTextWidget |
| Typeahead | typeahead | TypeaheadWidget |
| People | people | PeopleWidget |
| Group of people | functional-group | FunctionalGroupWidget |
| Dynamic table | dynamic-table | DynamicTableWidget |
| N/A | container | ContainerWidget (layout component) |
| Header | group | ContainerWidget |
| Attach | upload | AttachWidget or UploadWidget (based on metadata) |
| N/A | N/A | UnknownWidget |
## Replacing default form widgets with custom components
This is a short walkthrough on replacing a standard `Text` widget with a custom component for all Alfresco Activiti forms
rendered within `<activiti-form>` component.
First let's create a simple Alfresco Activiti form with `Text` widgets:
![default text widget](assets/text-default-widget.png)
Every custom widget must inherit `WidgetComponent` class in order to function properly:
```ts
import { Component } from '@angular/core';
import { WidgetComponent } from 'ng2-activiti-form';
@Component({
selector: 'custom-editor',
template: `
<div style="color: red">Look, I'm a custom editor!</div>
`
})
export class CustomEditorComponent extends WidgetComponent {}
```
Now you will need adding it to the application module or any custom module that is imported into the application one:
```ts
import { NgModule } from '@angular/core';
import { CustomEditorComponent } from './custom-editor.component';
@NgModule({
declarations: [ CustomEditorComponent ],
exports: [ CustomEditorComponent ],
entryComponents: [ CustomEditorComponent ]
})
export class CustomEditorsModule {}
```
Every custom widget should be added into all three module collections: `declarations`, `exports` and `entryComponents`.
If you decided storing custom widgets in a separate dedicated module (and optionally as separate redistributable library)
don't forget importing it into your main application one:
```ts
@NgModule({
imports: [
// ...
CustomEditorsModule
// ...
],
providers: [],
bootstrap: [ AppComponent ]
})
export class AppModule {}
```
Now you can import `FormRenderingService` in any of your Views and override default mapping similar to the following:
```ts
import { Component } from '@angular/core';
import { CustomEditorComponent } from './custom-editor.component';
@Component({...})
export class MyView {
constructor(formRenderingService: FormRenderingService) {
formRenderingService.setComponentTypeResolver('text', () => CustomEditorComponent, true);
}
}
```
At runtime it should look similar to the following:
![custom text widget](assets/text-custom-widget.png)
## Replacing custom stencils with custom components
This is a short walkthrough on rendering custom Alfresco Activiti stencils by means of custom Angular 2 components.
### Creating custom stencil
First let's create a basic stencil and call it `Custom Stencil 01`:
![custom stencil](assets/activiti-stencil-01.png)
_Note the `internal identifier` value as it will become a `field type` value when corresponding form is rendered._
Next put some simple html layout for `Form runtime template` and `Form editor template` fields:
```html
<div style="color: blue">Custom activiti stencil</div>
```
Now you are ready to design a test form based on your custom stencil:
![custom stencil form](assets/activiti-stencil-02.png)
Once wired with a new task it should look like the following within Alfresco Activiti web application:
![custom stencil task](assets/activiti-stencil-03.png)
### Creating custom widget
If you load previously created task into ADF `<actiiti-form>` component you will see something like the following:
![adf stencil](assets/adf-stencil-01.png)
Let's create an Angular 2 component to render missing content:
```ts
import { Component } from '@angular/core';
import { WidgetComponent } from 'ng2-activiti-form';
@Component({
selector: 'custom-stencil-01',
template: `<div style="color: green">ADF version of custom Activiti stencil</div>`
})
export class CustomStencil01 extends WidgetComponent {}
```
Put it inside custom module:
```ts
import { NgModule } from '@angular/core';
import { CustomStencil01 } from './custom-stencil-01.component';
@NgModule({
declarations: [ CustomStencil01 ],
exports: [ CustomStencil01 ],
entryComponents: [ CustomStencil01 ]
})
export class CustomEditorsModule {}
```
And import into your Application Module
```ts
@NgModule({
imports: [
// ...
CustomEditorsModule
// ...
],
providers: [],
bootstrap: [ AppComponent ]
})
export class AppModule {}
```
Now you can import `FormRenderingService` in any of your Views and provide new mapping:
```ts
import { Component } from '@angular/core';
import { CustomStencil01 } from './custom-stencil-01.component';
@Component({...})
export class MyView {
constructor(formRenderingService: FormRenderingService) {
formRenderingService.setComponentTypeResolver('custom_stencil_01', () => CustomStencil01, true);
}
}
```
At the runtime you should now see your custom Angular 2 component rendered in place of the stencils:
![adf stencil runtime](assets/adf-stencil-02.png)
@@ -0,0 +1,144 @@
# Form Stencils with Angular 2
Form component provides basic support for custom stencils created with Activiti stencil editor.
## Installing
Configuring support for stencils requires the following configuration for your `index.html` file:
```html
<!-- Stencils integration -->
<script src="node_modules/ng2-activiti-form/stencils/runtime.ng1.js"></script>
<script src="node_modules/ng2-activiti-form/stencils/runtime.adf.js"></script>
<script src="http://<activiti-app-root>/app/rest/script-files/controllers"></script>
```
Where `<activiti-app-root>` should be replaced with a valid url pointing to your Activiti installation, for example:
```html
<script src="http://localhost:9999/activiti-app/app/rest/script-files/controllers"></script>
```
- `/app/rest/script-files/controllers`
provides all stencil controllers stored within Activiti
- `runtime.ng1.js`
provides a compatibility layer for controllers created with Angular 1
(this is to avoid runtime errors when loading Angular 1 code into `<activiti-form>` component)
- `runtime.adf.js`
provides API for stencil management and registration,
i.e. mapping html templates with corresponding controller classes
## Creating new stencil
Create a new stencil and add a new item called `ng2 component 01`.
The internal identifier in this case should be `ng2_component_01`.
This value will be used as field type when form gets rendered.
## Form runtime template
This should be a valid Angular 2 component template that you want to render in `<activiti-form>` component:
```html
<div>
<div>Angular2 Component</div>
<div>Created by: {{name}}</div>
</div>
```
## Form editor template
This can be any html layout to be rendered as a component placeholder in Activiti Form Designer.
```html
<div>
<div style="color: blue">
Angular2 Component 01
</div>
</div>
```
## Custom component controller
This field should contain JavaScript code for Angular 2 component class.
_Note: If you are using TypeScript then you should be putting transpiled JavaScript code here,
you can try official [TypeScript playground](http://www.typescriptlang.org/play/)
to see how TS code gets transpiled into JS._
### JavaScript code
```js
var SampleClass1 = (function () {
function SampleClass1() {
this.name = 'Denys';
console.log('ng2_component_01 ctor');
}
SampleClass1.prototype.ngOnInit = function () {
console.log('OnInit called');
};
return SampleClass1;
}());
```
### TypeScript code
The TypeScript version of the code above is:
```ts
import { OnInit } from '@angular/core';
class SampleClass1 implements OnInit {
constructor() {
console.log('ctor called');
}
ngOnInit() {
console.log('OnInit called');
}
}
```
### Mapping template with component class
In order to map **form runtime template** with the corresponding component class
you will need registering both parts with `adf.registerComponent(identifier, class)` api:
```js
if (adf) {
adf.registerComponent('ng2_component_01', SampleClass1);
}
```
### Final result
```js
var SampleClass1 = (function () {
function SampleClass1() {
this.name = 'Denys';
console.log('ng2_component_01 ctor');
}
SampleClass1.prototype.ngOnInit = function () {
console.log('OnInit called');
};
return SampleClass1;
}());
if (adf) {
adf.registerComponent('ng2_component_01', SampleClass1);
}
```
## Runtime result
When rendered on the form this stencil item should look like the following:
```html
Angular2 Component
Created by: Denys
```
ADF Form component will automatically assemble and compile a valid Angular 2 component on the fly.
+9 -1
View File
@@ -19,12 +19,14 @@ import { NgModule, ModuleWithProviders } from '@angular/core';
import { CoreModule } from 'ng2-alfresco-core';
import { ActivitiForm } from './src/components/activiti-form.component';
import { FormFieldComponent } from './src/components/form-field/form-field.component';
import { ActivitiStartForm } from './src/components/activiti-start-form.component';
import { FormService } from './src/services/form.service';
import { EcmModelService } from './src/services/ecm-model.service';
import { NodeService } from './src/services/node.service';
import { WidgetVisibilityService } from './src/services/widget-visibility.service';
import { ActivitiAlfrescoContentService } from './src/services/activiti-alfresco.service';
import { FormRenderingService } from './src/services/form-rendering.service';
import { HttpModule } from '@angular/http';
import { WIDGET_DIRECTIVES } from './src/components/widgets/index';
@@ -34,10 +36,12 @@ export * from './src/services/form.service';
export * from './src/components/widgets/index';
export * from './src/services/ecm-model.service';
export * from './src/services/node.service';
export * from './src/services/form-rendering.service';
export const ACTIVITI_FORM_DIRECTIVES: any[] = [
ActivitiForm,
ActivitiStartForm,
FormFieldComponent,
...WIDGET_DIRECTIVES
];
@@ -46,7 +50,8 @@ export const ACTIVITI_FORM_PROVIDERS: any[] = [
EcmModelService,
NodeService,
WidgetVisibilityService,
ActivitiAlfrescoContentService
ActivitiAlfrescoContentService,
FormRenderingService
];
@NgModule({
@@ -57,6 +62,9 @@ export const ACTIVITI_FORM_PROVIDERS: any[] = [
declarations: [
...ACTIVITI_FORM_DIRECTIVES
],
entryComponents: [
...WIDGET_DIRECTIVES
],
providers: [
...ACTIVITI_FORM_PROVIDERS
],
@@ -1,7 +1,7 @@
{
"name": "ng2-activiti-form",
"description": "Alfresco Activiti Form Component for Angular 2",
"version": "0.4.0",
"version": "0.5.0",
"author": "Alfresco Software, Ltd.",
"scripts": {
"clean": "npm install rimraf && rimraf dist node_modules typings",
@@ -58,18 +58,12 @@
"systemjs": "0.19.27",
"zone.js": "^0.6.23",
"intl": "1.2.4",
"dialog-polyfill": "^0.4.3",
"element.scrollintoviewifneeded-polyfill": "^1.0.1",
"material-design-icons": "2.2.3",
"material-design-lite": "1.2.1",
"moment": "2.15.1",
"md-date-time-picker": "^2.2.0",
"ng2-translate": "2.5.0",
"alfresco-js-api": "^0.4.0",
"ng2-alfresco-core": "0.4.0"
"alfresco-js-api": "^0.5.0",
"ng2-alfresco-core": "0.5.0"
},
"devDependencies": {
"@types/node": "^6.0.42",
@@ -15,23 +15,7 @@
<div *ngIf="!form.hasTabs() && form.hasFields()">
<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>
<form-field [field]="field.field"></form-field>
</div>
</div>
</div>
@@ -12,23 +12,7 @@
<div *ngIf="!form.hasTabs() && form.hasFields()">
<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>
<form-field [field]="field.field"></form-field>
</div>
</div>
</div>
@@ -20,6 +20,7 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { Observable } from 'rxjs/Rx';
import { ActivitiStartForm } from './activiti-start-form.component';
import { FormFieldComponent } from './form-field/form-field.component';
import { WIDGET_DIRECTIVES } from './widgets/index';
import { FormService } from './../services/form.service';
import { EcmModelService } from './../services/ecm-model.service';
@@ -43,6 +44,7 @@ describe('ActivitiStartForm', () => {
imports: [ CoreModule ],
declarations: [
ActivitiStartForm,
FormFieldComponent,
...WIDGET_DIRECTIVES
],
providers: [
@@ -83,7 +83,7 @@ export class ActivitiStartForm extends ActivitiForm implements OnInit, AfterView
}
if (this.translate) {
this.translate.addTranslationFolder('node_modules/ng2-activiti-form/src');
this.translate.addTranslationFolder('ng2-activiti-form', 'node_modules/ng2-activiti-form/dist/src');
}
}
@@ -0,0 +1,100 @@
/*!
* @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 { CoreModule } from 'ng2-alfresco-core';
import { ActivitiFormModule } from './../../../index';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { FormFieldComponent } from './form-field.component';
import { FormRenderingService } from './../../services/form-rendering.service';
import { FormModel, FormFieldModel, FormFieldTypes } from './../widgets/core/index';
import { TextWidget, CheckboxWidget } from './../widgets/index';
describe('FormFieldComponent', () => {
let fixture: ComponentFixture<FormFieldComponent>;
let component: FormFieldComponent;
let componentHandler: any;
let form: FormModel;
let formRenderingService: FormRenderingService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ CoreModule, ActivitiFormModule ]
})
.compileComponents();
}));
beforeEach(() => {
componentHandler = jasmine.createSpyObj('componentHandler', [
'upgradeAllRegistered',
'upgradeElement'
]);
window['componentHandler'] = componentHandler;
fixture = TestBed.createComponent(FormFieldComponent);
component = fixture.componentInstance;
formRenderingService = fixture.debugElement.injector.get(FormRenderingService);
form = new FormModel();
});
it('should create default component instance', () => {
let field = new FormFieldModel(form, {
type: FormFieldTypes.TEXT
});
component.field = field;
fixture.detectChanges();
expect(component.componentRef).toBeDefined();
expect(component.componentRef.componentType).toBe(TextWidget);
});
it('should create custom component instance', () => {
let field = new FormFieldModel(form, {
type: FormFieldTypes.TEXT
});
formRenderingService.setComponentTypeResolver(FormFieldTypes.TEXT, () => CheckboxWidget, true);
component.field = field;
fixture.detectChanges();
expect(component.componentRef).toBeDefined();
expect(component.componentRef.componentType).toBe(CheckboxWidget);
});
it('should require field to create component', () => {
component.field = null;
fixture.detectChanges();
expect(component.componentRef).toBeUndefined();
});
it('should require component type to be resolved', () => {
let field = new FormFieldModel(form, {
type: FormFieldTypes.TEXT
});
spyOn(formRenderingService, 'resolveComponentType').and.returnValue(null);
component.field = field;
fixture.detectChanges();
expect(formRenderingService.resolveComponentType).toHaveBeenCalled();
expect(component.componentRef).toBeUndefined();
});
});
@@ -0,0 +1,133 @@
/*!
* @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, OnDestroy,
ViewChild,
ViewContainerRef,
Input,
ComponentRef,
ComponentFactoryResolver,
ComponentFactory,
Compiler,
NgModule,
ModuleWithComponentFactories
} from '@angular/core';
import { CoreModule } from 'ng2-alfresco-core';
import { WidgetVisibilityService } from './../../services/widget-visibility.service';
import { FormRenderingService } from './../../services/form-rendering.service';
import { WidgetComponent } from './../widgets/widget.component';
import { FormFieldModel } from './../widgets/core/index';
declare var adf: any;
@Component({
selector: 'form-field',
template: `
<div [hidden]="!field?.isVisible">
<div #container></div>
</div>
`
})
export class FormFieldComponent implements OnInit, OnDestroy {
@ViewChild('container', { read: ViewContainerRef })
container: ViewContainerRef;
@Input()
field: FormFieldModel = null;
componentRef: ComponentRef<{}>;
constructor(
private formRenderingService: FormRenderingService,
private componentFactoryResolver: ComponentFactoryResolver,
private visibilityService: WidgetVisibilityService,
private compiler: Compiler) {
}
ngOnInit() {
if (this.field) {
let customTemplate = this.field.form.customFieldTemplates[this.field.type];
if (customTemplate && this.hasController(this.field.type)) {
let factory = this.getComponentFactorySync(this.field.type, customTemplate);
this.componentRef = this.container.createComponent(factory);
let instance: any = this.componentRef.instance;
if (instance) {
instance.field = this.field;
}
} else {
let componentType = this.formRenderingService.resolveComponentType(this.field);
if (componentType) {
let factory = this.componentFactoryResolver.resolveComponentFactory(componentType);
this.componentRef = this.container.createComponent(factory);
let instance = <WidgetComponent>this.componentRef.instance;
instance.field = this.field;
instance.fieldChanged.subscribe(field => {
console.log('WidgetComponent.fieldChanged was used only to trigger visibility engine, components should do that internally if needed');
if (field && field.form) {
this.visibilityService.refreshVisibility(field.form);
}
});
}
}
}
}
ngOnDestroy() {
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
}
private hasController(type: string): boolean {
return (adf && adf.components && adf.components[type]);
}
private getComponentFactorySync(type: string, template: string): ComponentFactory<any> {
let componentInfo = adf.components[type];
if (componentInfo.factory) {
return componentInfo.factory;
}
let metadata = {
selector: `runtime-component-${type}`,
template: template
};
let factory = this.createComponentFactorySync(this.compiler, metadata, componentInfo.class);
componentInfo.factory = factory;
return factory;
}
private createComponentFactorySync(compiler: Compiler, metadata: Component, componentClass: any): ComponentFactory<any> {
const cmpClass = componentClass || class RuntimeComponent { };
const decoratedCmp = Component(metadata)(cmpClass);
@NgModule({ imports: [CoreModule], declarations: [decoratedCmp] })
class RuntimeComponentModule { }
let module: ModuleWithComponentFactories<any> = compiler.compileModuleAndAllComponentsSync(RuntimeComponentModule);
return module.componentFactories.find(x => x.componentType === decoratedCmp);
}
}
@@ -1,13 +1,13 @@
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label amount-widget"
<div class="mdl-textfield mdl-js-textfield amount-widget"
[class.amount-widget__invalid]="!field.isValid">
<label [attr.for]="field.id">{{field.name}}, {{currency}}</label>
<input class="mdl-textfield__input"
type="text"
pattern="-?[0-9]*(\.[0-9]+)?"
[attr.id]="field.id"
[attr.required]="isRequired()"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"
[disabled]="field.readOnly">
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}, {{currency}}</label>
[disabled]="field.readOnly"
placeholder="{{field.placeholder}}">
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>
@@ -23,7 +23,7 @@ describe('AmountWidget', () => {
let widget: AmountWidget;
beforeEach(() => {
widget = new AmountWidget(null);
widget = new AmountWidget();
});
it('should setup currentcy from field', () => {
@@ -15,8 +15,8 @@
* limitations under the License.
*/
import { Component, ElementRef, OnInit } from '@angular/core';
import { TextFieldWidgetComponent } from './../textfield-widget.component';
import { Component, OnInit } from '@angular/core';
import { WidgetComponent } from './../widget.component';
@Component({
moduleId: module.id,
@@ -24,16 +24,12 @@ import { TextFieldWidgetComponent } from './../textfield-widget.component';
templateUrl: './amount.widget.html',
styleUrls: ['./amount.widget.css']
})
export class AmountWidget extends TextFieldWidgetComponent implements OnInit {
export class AmountWidget extends WidgetComponent implements OnInit {
static DEFAULT_CURRENCY: string = '$';
currency: string = AmountWidget.DEFAULT_CURRENCY;
constructor(elementRef: ElementRef) {
super(elementRef);
}
ngOnInit() {
if (this.field && this.field.currency) {
this.currency = this.field.currency;
@@ -2,7 +2,7 @@
<label [attr.for]="field.id">{{field.name}}</label>
<div>
<span *ngIf="hasFile" class="attach-widget__file">{{getLinkedFileName()}}</span>
<span *ngIf="hasFile()" class="attach-widget__file mdl-chip"><span class="mdl-chip__text">{{getLinkedFileName()}}</span></span>
<button #browseFile (click)="showDialog();" class="mdl-button mdl-js-button mdl-js-ripple-effect attach-widget__browser">
<i class="material-icons">image</i>
Browse {{selectedFolderSiteName}}
@@ -42,24 +42,23 @@ describe('AttachWidget', () => {
});
it('should require field value to check file', () => {
widget.hasFile = false;
widget.field = null;
widget.ngOnInit();
expect(widget.hasFile).toBeFalsy();
expect(widget.hasFile()).toBeFalsy();
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
value: null
});
widget.ngOnInit();
expect(widget.hasFile).toBeFalsy();
expect(widget.hasFile()).toBeFalsy();
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
value: [{ name: 'file' }]
});
widget.ngOnInit();
expect(widget.hasFile).toBeTruthy();
expect(widget.hasFile()).toBeTruthy();
});
it('should setup with form field', () => {
@@ -116,19 +115,20 @@ describe('AttachWidget', () => {
expect(widget.selectedFile).toBe(node);
expect(widget.field.value).toEqual([link]);
expect(widget.field.json.value).toEqual([link]);
expect(widget.hasFile()).toBeTruthy();
});
it('should reset', () => {
widget.hasFile = true;
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
value: [{ name: 'filename' }]
});
widget.reset();
expect(widget.hasFile).toBeFalsy();
expect(widget.hasFile()).toBeFalsy();
expect(widget.field.value).toBeNull();
expect(widget.field.json.value).toBeNull();
expect(widget.hasFile()).toBeFalsy();
});
it('should close dialog on cancel', () => {
@@ -35,7 +35,6 @@ export class AttachWidget extends WidgetComponent implements OnInit {
selectedFolderSiteName: string;
selectedFolderAccountId: string;
fileName: string;
hasFile: boolean;
selectedFolderNodes: [ExternalContent];
selectedFile: ExternalContent;
@@ -54,10 +53,6 @@ export class AttachWidget extends WidgetComponent implements OnInit {
ngOnInit() {
if (this.field) {
if (this.field.value) {
this.hasFile = true;
}
let params = this.field.params;
if (params &&
@@ -149,7 +144,10 @@ export class AttachWidget extends WidgetComponent implements OnInit {
reset() {
this.field.value = null;
this.field.json.value = null;
this.hasFile = false;
}
hasFile(): boolean {
return this.field && this.field.value;
}
}
@@ -5,7 +5,7 @@
class="mdl-checkbox__input"
[checked]="field.value"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"
(ngModelChange)="onChange()"
[disabled]="field.readOnly">
<span class="mdl-checkbox__label">{{field.name}}</span>
</label>
@@ -17,6 +17,7 @@
import { Component } from '@angular/core';
import { WidgetComponent } from './../widget.component';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
@Component({
moduleId: module.id,
@@ -25,4 +26,12 @@ import { WidgetComponent } from './../widget.component';
})
export class CheckboxWidget extends WidgetComponent {
constructor(private visibilityService: WidgetVisibilityService) {
super();
}
onChange() {
this.visibilityService.refreshVisibility(this.field.form);
}
}
@@ -15,9 +15,9 @@
* limitations under the License.
*/
import { ContainerColumnModel } from './container-column.model';
import { FormModel } from './form.model';
import { FormFieldModel } from './form-field.model';
import { ContainerColumnModel } from './../core/container-column.model';
import { FormModel } from './../core/form.model';
import { FormFieldModel } from './../core/form-field.model';
describe('ContainerColumnModel', () => {
@@ -15,57 +15,7 @@
<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" [hidden]="!field.isVisible">
<div *ngSwitchCase="'integer'">
<number-widget [field]="field" (fieldChanged)="fieldChanged($event);"></number-widget>
</div>
<div *ngSwitchCase="'text'">
<text-widget [field]="field" (fieldChanged)="fieldChanged($event);"></text-widget>
</div>
<div *ngSwitchCase="'multi-line-text'">
<multiline-text-widget [field]="field" (fieldChanged)="fieldChanged($event);"></multiline-text-widget>
</div>
<div *ngSwitchCase="'boolean'">
<checkbox-widget [field]="field" (fieldChanged)="fieldChanged($event);"></checkbox-widget>
</div>
<div *ngSwitchCase="'dropdown'">
<dropdown-widget [field]="field" (fieldChanged)="fieldChanged($event);"></dropdown-widget>
</div>
<div *ngSwitchCase="'hyperlink'">
<hyperlink-widget [field]="field" (fieldChanged)="fieldChanged($event);"></hyperlink-widget>
</div>
<div *ngSwitchCase="'radio-buttons'">
<radio-buttons-widget [field]="field" (fieldChanged)="fieldChanged($event);"></radio-buttons-widget>
</div>
<div *ngSwitchCase="'readonly'">
<display-value-widget [field]="field" (fieldChanged)="fieldChanged($event);"></display-value-widget>
</div>
<div *ngSwitchCase="'readonly-text'">
<display-text-widget [field]="field" (fieldChanged)="fieldChanged($event);"></display-text-widget>
</div>
<div *ngSwitchCase="'upload'">
<upload-widget *ngIf="!field.params.link" [field]="field" (fieldChanged)="fieldChanged($event);"></upload-widget>
<attach-widget *ngIf="field.params.link" [field]="field" (fieldChanged)="fieldChanged($event);"></attach-widget>
</div>
<div *ngSwitchCase="'typeahead'">
<typeahead-widget [field]="field" (fieldChanged)="fieldChanged($event);"></typeahead-widget>
</div>
<div *ngSwitchCase="'functional-group'">
<functional-group-widget [field]="field" (fieldChanged)="fieldChanged($event);"></functional-group-widget>
</div>
<div *ngSwitchCase="'people'">
<people-widget [field]="field" (fieldChanged)="fieldChanged($event);"></people-widget>
</div>
<div *ngSwitchCase="'date'">
<date-widget [field]="field" (fieldChanged)="fieldChanged($event);"></date-widget>
</div>
<div *ngSwitchCase="'amount'">
<amount-widget [field]="field" (fieldChanged)="fieldChanged($event);"></amount-widget>
</div>
<div *ngSwitchDefault>
<span>UNKNOWN WIDGET TYPE: {{field.type}}</span>
</div>
</div>
<form-field [field]="field"></form-field>
</div>
</div>
</div>
@@ -0,0 +1,124 @@
/*!
* @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 { ContainerWidgetModel } from './container.widget.model';
import { FormModel } from './../core/form.model';
import { FormFieldModel } from './../core/form-field.model';
import { FormFieldTypes } from './../core/form-field-types';
describe('ContainerWidgetModel', () => {
it('should store the form reference', () => {
let form = new FormModel();
let field = new FormFieldModel(form);
let model = new ContainerWidgetModel(field);
expect(model.form).toBe(form);
});
it('should be expanded by default', () => {
let container = new ContainerWidgetModel(null);
expect(container.isExpanded).toBeTruthy();
});
it('should wrap fields into columns on setup', () => {
let form = new FormModel();
let json = {
fieldType: '<type>',
id: '<id>',
name: '<name>',
type: '<type>',
tab: '<tab>',
numberOfColumns: 3,
params: {},
visibilityCondition: {},
fields: {
'1': [
{ id: 'field-1' },
{ id: 'field-3' }
],
'2': [
{ id: 'field-2' }
],
'3': null
}
};
let field = new FormFieldModel(form, json);
let container = new ContainerWidgetModel(field);
expect(container.columns.length).toBe(3);
let col1 = container.columns[0];
expect(col1.fields.length).toBe(2);
expect(col1.fields[0].id).toBe('field-1');
expect(col1.fields[1].id).toBe('field-3');
let col2 = container.columns[1];
expect(col2.fields.length).toBe(1);
expect(col2.fields[0].id).toBe('field-2');
let col3 = container.columns[2];
expect(col3.fields.length).toBe(0);
});
it('should allow collapsing only when of a group type', () => {
let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.CONTAINER,
params: {
allowCollapse: true
}
}));
expect(container.isCollapsible()).toBeFalsy();
container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true
}
}));
expect(container.isCollapsible()).toBeTruthy();
});
it('should allow collapsing only when explicitly defined in params', () => {
let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {}
}));
expect(container.isCollapsible()).toBeFalsy();
container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true
}
}));
expect(container.isCollapsible()).toBeTruthy();
});
it('should be collapsed by default', () => {
let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true,
collapseByDefault: true
}
}));
expect(container.isCollapsedByDefault()).toBeTruthy();
});
});
@@ -0,0 +1,60 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ContainerModel } from './../core/container.model';
import { ContainerColumnModel } from './../core/container-column.model';
import { FormFieldTypes } from './../core/form-field-types';
import { FormFieldModel } from './../core/form-field.model';
export class ContainerWidgetModel extends ContainerModel {
columns: ContainerColumnModel[] = [];
isExpanded: boolean = true;
isGroup(): boolean {
return this.type === FormFieldTypes.GROUP;
}
isCollapsible(): boolean {
let allowCollapse = false;
if (this.isGroup() && this.field.params['allowCollapse']) {
allowCollapse = <boolean> this.field.params['allowCollapse'];
}
return allowCollapse;
}
isCollapsedByDefault(): boolean {
let collapseByDefault = false;
if (this.isCollapsible() && this.field.params['collapseByDefault']) {
collapseByDefault = <boolean> this.field.params['collapseByDefault'];
}
return collapseByDefault;
}
constructor(field: FormFieldModel) {
super(field);
if (this.field) {
this.columns = this.field.columns || [];
this.isExpanded = !this.isCollapsedByDefault();
}
}
}
@@ -16,13 +16,14 @@
*/
import { ContainerWidget } from './container.widget';
import { ContainerWidgetModel } from './container.widget.model';
import { FormModel } from './../core/form.model';
import { ContainerModel } from './../core/container.model';
import { FormFieldTypes } from './../core/form-field-types';
import { FormFieldModel } from './../core/form-field.model';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { CoreModule } from 'ng2-alfresco-core';
import { WIDGET_DIRECTIVES } from '../index';
import { FormFieldComponent } from './../../form-field/form-field.component';
import { fakeFormJson } from '../../../services/assets/widget-visibility.service.mock';
describe('ContainerWidget', () => {
@@ -37,6 +38,15 @@ describe('ContainerWidget', () => {
window['componentHandler'] = componentHandler;
});
it('should wrap field with model instance', () => {
let field = new FormFieldModel(null);
let container = new ContainerWidget();
container.field = field;
container.ngOnInit();
expect(container.content).toBeDefined();
expect(container.content.field).toBe(field);
});
it('should upgrade MDL content on view init', () => {
let container = new ContainerWidget();
container.ngAfterViewInit();
@@ -52,12 +62,12 @@ describe('ContainerWidget', () => {
});
it('should toggle underlying group container', () => {
let container = new ContainerModel(new FormModel(), {
let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true
}
});
}));
let widget = new ContainerWidget();
widget.content = container;
@@ -70,9 +80,9 @@ describe('ContainerWidget', () => {
});
it('should toggle only collapsible container', () => {
let container = new ContainerModel(new FormModel(), {
let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.GROUP
});
}));
let widget = new ContainerWidget();
widget.content = container;
@@ -83,12 +93,12 @@ describe('ContainerWidget', () => {
});
it('should toggle only group container', () => {
let container = new ContainerModel(new FormModel(), {
let container = new ContainerWidgetModel(new FormFieldModel(new FormModel(), {
type: FormFieldTypes.CONTAINER,
params: {
allowCollapse: true
}
});
}));
let widget = new ContainerWidget();
widget.content = container;
@@ -102,27 +112,27 @@ describe('ContainerWidget', () => {
let widget = new ContainerWidget();
let fakeForm = new FormModel();
let fakeField = new FormFieldModel(fakeForm, {id: 'fakeField', value: 'fakeValue'});
widget.formValueChanged.subscribe(field => {
widget.fieldChanged.subscribe(field => {
expect(field).not.toBe(null);
expect(field.id).toBe('fakeField');
expect(field.value).toBe('fakeValue');
done();
});
widget.fieldChanged(fakeField);
widget.onFieldChanged(fakeField);
});
describe('when template is ready', () => {
let containerWidgetComponent: ContainerWidget;
let fixture: ComponentFixture<ContainerWidget>;
let element: HTMLElement;
let fakeContainerVisible: ContainerModel;
let fakeContainerInvisible: ContainerModel;
let fakeContainerVisible: ContainerWidgetModel;
let fakeContainerInvisible: ContainerWidgetModel;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CoreModule],
declarations: [WIDGET_DIRECTIVES]
declarations: [FormFieldComponent, WIDGET_DIRECTIVES]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(ContainerWidget);
containerWidgetComponent = fixture.componentInstance;
@@ -133,18 +143,18 @@ describe('ContainerWidget', () => {
beforeEach(() => {
componentHandler = jasmine.createSpyObj('componentHandler', ['upgradeAllRegistered', 'upgradeElement']);
window['componentHandler'] = componentHandler;
fakeContainerVisible = new ContainerModel(new FormModel(fakeFormJson), {
fakeContainerVisible = new ContainerWidgetModel(new FormFieldModel(new FormModel(fakeFormJson), {
fieldType: FormFieldTypes.GROUP,
id: 'fake-cont-id-1',
name: 'fake-cont-1-name',
type: FormFieldTypes.GROUP
});
fakeContainerInvisible = new ContainerModel(new FormModel(fakeFormJson), {
}));
fakeContainerInvisible = new ContainerWidgetModel(new FormFieldModel(new FormModel(fakeFormJson), {
fieldType: FormFieldTypes.GROUP,
id: 'fake-cont-id-2',
name: 'fake-cont-2-name',
type: FormFieldTypes.GROUP
});
}));
fakeContainerVisible.field.isVisible = true;
fakeContainerInvisible.field.isVisible = false;
});
@@ -179,7 +189,7 @@ describe('ContainerWidget', () => {
it('should hide header when it becomes not visible', async(() => {
containerWidgetComponent.content = fakeContainerVisible;
fixture.detectChanges();
containerWidgetComponent.formValueChanged.subscribe((res) => {
containerWidgetComponent.fieldChanged.subscribe((res) => {
containerWidgetComponent.content.field.isVisible = false;
fixture.detectChanges();
fixture.whenStable()
@@ -188,12 +198,12 @@ describe('ContainerWidget', () => {
expect(element.querySelector('#container-header-label')).toBeNull();
});
});
containerWidgetComponent.fieldChanged(null);
containerWidgetComponent.onFieldChanged(null);
}));
it('should show header when it becomes visible', async(() => {
containerWidgetComponent.content = fakeContainerInvisible;
containerWidgetComponent.formValueChanged.subscribe((res) => {
containerWidgetComponent.fieldChanged.subscribe((res) => {
containerWidgetComponent.content.field.isVisible = true;
fixture.detectChanges();
fixture.whenStable()
@@ -204,7 +214,7 @@ describe('ContainerWidget', () => {
expect(element.querySelector('#container-header-label').innerHTML).toContain('fake-cont-2-name');
});
});
containerWidgetComponent.fieldChanged(null);
containerWidgetComponent.onFieldChanged(null);
}));
});
@@ -15,8 +15,9 @@
* limitations under the License.
*/
import { Component, Input, AfterViewInit, Output, EventEmitter } from '@angular/core';
import { ContainerModel, FormFieldModel } from './../core/index';
import { Component, AfterViewInit, OnInit } from '@angular/core';
import { ContainerWidgetModel } from './container.widget.model';
import { WidgetComponent } from './../widget.component';
@Component({
moduleId: module.id,
@@ -24,13 +25,9 @@ import { ContainerModel, FormFieldModel } from './../core/index';
templateUrl: './container.widget.html',
styleUrls: ['./container.widget.css']
})
export class ContainerWidget implements AfterViewInit {
export class ContainerWidget extends WidgetComponent implements OnInit, AfterViewInit {
@Input()
content: ContainerModel;
@Output()
formValueChanged: EventEmitter<FormFieldModel> = new EventEmitter<FormFieldModel>();
content: ContainerWidgetModel;
onExpanderClicked() {
if (this.content && this.content.isCollapsible()) {
@@ -38,6 +35,12 @@ export class ContainerWidget implements AfterViewInit {
}
}
ngOnInit() {
if (this.field) {
this.content = new ContainerWidgetModel(this.field);
}
}
ngAfterViewInit() {
this.setupMaterialComponents();
}
@@ -50,9 +53,4 @@ export class ContainerWidget implements AfterViewInit {
}
return false;
}
fieldChanged(field: FormFieldModel) {
this.formValueChanged.emit(field);
}
}
@@ -17,129 +17,14 @@
import { ContainerModel } from './container.model';
import { FormModel } from './form.model';
import { FormFieldTypes } from './form-field-types';
import { FormFieldModel } from './form-field.model';
describe('ContainerModel', () => {
it('should store the form reference', () => {
let form = new FormModel();
let model = new ContainerModel(form);
let model = new ContainerModel(new FormFieldModel(form));
expect(model.form).toBe(form);
});
it('should store original json', () => {
let json = {};
let model = new ContainerModel(null, json);
expect(model.json).toBe(json);
});
it('should have 1 column layout by default', () => {
let container = new ContainerModel(null, null);
expect(container.numberOfColumns).toBe(1);
});
it('should be expanded by default', () => {
let container = new ContainerModel(null, null);
expect(container.isExpanded).toBeTruthy();
});
it('should setup with json config', () => {
let json = {
fieldType: '<type>',
id: '<id>',
name: '<name>',
type: '<type>',
tab: '<tab>',
numberOfColumns: 2,
params: {}
};
let container = new ContainerModel(null, json);
Object.keys(json).forEach(key => {
expect(container[key]).toEqual(json[key]);
});
});
it('should wrap fields into columns on setup', () => {
let form = new FormModel();
let json = {
fieldType: '<type>',
id: '<id>',
name: '<name>',
type: '<type>',
tab: '<tab>',
numberOfColumns: 3,
params: {},
visibilityCondition: {},
fields: {
'1': [
{ id: 'field-1' },
{ id: 'field-3' }
],
'2': [
{ id: 'field-2' }
],
'3': null
}
};
let container = new ContainerModel(form, json);
expect(container.columns.length).toBe(3);
let col1 = container.columns[0];
expect(col1.fields.length).toBe(2);
expect(col1.fields[0].id).toBe('field-1');
expect(col1.fields[1].id).toBe('field-3');
let col2 = container.columns[1];
expect(col2.fields.length).toBe(1);
expect(col2.fields[0].id).toBe('field-2');
let col3 = container.columns[2];
expect(col3.fields.length).toBe(0);
});
it('should allow collapsing only when of a group type', () => {
let container = new ContainerModel(new FormModel(), {
type: FormFieldTypes.CONTAINER,
params: {
allowCollapse: true
}
});
expect(container.isCollapsible()).toBeFalsy();
container = new ContainerModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true
}
});
expect(container.isCollapsible()).toBeTruthy();
});
it('should allow collapsing only when explicitly defined in params', () => {
let container = new ContainerModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {}
});
expect(container.isCollapsible()).toBeFalsy();
container = new ContainerModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true
}
});
expect(container.isCollapsible()).toBeTruthy();
});
it('should be collapsed by default', () => {
let container = new ContainerModel(new FormModel(), {
type: FormFieldTypes.GROUP,
params: {
allowCollapse: true,
collapseByDefault: true
}
});
expect(container.isCollapsedByDefault()).toBeTruthy();
});
});
@@ -16,95 +16,21 @@
*/
import { FormWidgetModel } from './form-widget.model';
import { FormFieldMetadata } from './form-field-metadata';
import { ContainerColumnModel } from './container-column.model';
import { FormFieldTypes } from './form-field-types';
import { FormModel } from './form.model';
import { FormFieldModel } from './form-field.model';
export class ContainerModel extends FormWidgetModel {
field: FormFieldModel;
numberOfColumns: number = 1;
params: FormFieldMetadata = {};
columns: ContainerColumnModel[] = [];
isExpanded: boolean = true;
get isVisible(): boolean {
return this.field.isVisible;
}
isGroup(): boolean {
return this.type === FormFieldTypes.GROUP;
}
isCollapsible(): boolean {
let allowCollapse = false;
if (this.isGroup() && this.params['allowCollapse']) {
allowCollapse = <boolean> this.params['allowCollapse'];
}
return allowCollapse;
}
isCollapsedByDefault(): boolean {
let collapseByDefault = false;
if (this.isCollapsible() && this.params['collapseByDefault']) {
collapseByDefault = <boolean> this.params['collapseByDefault'];
}
return collapseByDefault;
}
constructor(form: FormModel, json?: any) {
super(form, json);
if (json) {
this.field = new FormFieldModel(form, json);
this.numberOfColumns = <number> json.numberOfColumns;
this.params = <FormFieldMetadata> json.params || {};
let columnSize: number = 12;
if (this.numberOfColumns > 1) {
columnSize = 12 / this.numberOfColumns;
}
for (let i = 0; i < this.numberOfColumns; i++) {
let col = new ContainerColumnModel();
col.size = columnSize;
this.columns.push(col);
}
if (json.fields) {
Object.keys(json.fields).map(key => {
let fields = (json.fields[key] || []).map(f => new FormFieldModel(form, f));
let col = this.columns[parseInt(key, 10) - 1];
col.fields = fields;
});
}
this.isExpanded = !this.isCollapsedByDefault();
constructor(field: FormFieldModel) {
if (field) {
super(field.form, field.json);
this.field = field;
}
}
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;
}
}
@@ -1,48 +0,0 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 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;
}
@@ -15,10 +15,6 @@
* limitations under the License.
*/
export interface DynamicTableRow {
isNew: boolean;
selected: boolean;
value: any;
export interface FormFieldTemplates {
[key: string]: string;
}
@@ -158,6 +158,9 @@ describe('FormFieldValidator', () => {
expect(NumberFieldValidator.isNumber('1')).toBeTruthy();
expect(NumberFieldValidator.isNumber('1.0')).toBeTruthy();
expect(NumberFieldValidator.isNumber('-1')).toBeTruthy();
expect(NumberFieldValidator.isNumber(1)).toBeTruthy();
expect(NumberFieldValidator.isNumber(0)).toBeTruthy();
expect(NumberFieldValidator.isNumber(-1)).toBeTruthy();
});
it('should not verify number', () => {
@@ -177,6 +180,24 @@ describe('FormFieldValidator', () => {
expect(validator.validate(field)).toBeTruthy();
});
it('should allow number value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: 44
});
expect(validator.validate(field)).toBeTruthy();
});
it('should allow zero number value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: 0
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for wrong number value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
@@ -103,11 +103,18 @@ export class NumberFieldValidator implements FormFieldValidator {
if (this.isSupported(field)) {
if (field.value === null ||
field.value === undefined ||
field.value === '' ||
NumberFieldValidator.isNumber(field.value)) {
field.value === '') {
return true;
}
field.validationSummary = 'Input must be a number';
let valueStr = '' + field.value;
let pattern = new RegExp(/^-?\d+$/);
if (field.enableFractions) {
pattern = new RegExp(/^-?[0-9]+(\.[0-9]{1,2})?$/);
}
if (valueStr.match(pattern)) {
return true;
}
field.validationSummary = 'Incorrect number format';
return false;
}
return true;
@@ -163,8 +163,8 @@ describe('FormFieldModel', () => {
id: 'dropdown-2',
type: FormFieldTypes.DROPDOWN,
options: [
{ id: 'opt1', value: 'Option 1' },
{ id: 'opt2', value: 'Option 2' }
{ id: 'opt1', name: 'Option 1' },
{ id: 'opt2', name: 'Option 2' }
]
});
@@ -20,6 +20,7 @@ import { FormFieldOption } from './form-field-option';
import { FormFieldTypes } from './form-field-types';
import { FormFieldMetadata } from './form-field-metadata';
import { FormModel } from './form.model';
import { ContainerColumnModel } from './container-column.model';
import { WidgetVisibilityModel } from '../../../models/widget-visibility.model';
import {
FormFieldValidator,
@@ -37,6 +38,7 @@ import {
declare var moment: any;
// Maps to FormFieldRepresentation
export class FormFieldModel extends FormWidgetModel {
private _value: string;
@@ -52,6 +54,7 @@ export class FormFieldModel extends FormWidgetModel {
overrideId: boolean;
tab: string;
colspan: number = 1;
placeholder: string = null;
minLength: number = 0;
maxLength: number = 0;
minValue: string;
@@ -73,7 +76,12 @@ export class FormFieldModel extends FormWidgetModel {
enableFractions: boolean = false;
currency: string = null;
// advanced members
// container model members
numberOfColumns: number = 1;
fields: FormFieldModel[] = [];
columns: ContainerColumnModel[] = [];
// util members
emptyOption: FormFieldOption;
validationSummary: string;
validators: FormFieldValidator[] = [];
@@ -149,6 +157,34 @@ export class FormFieldModel extends FormWidgetModel {
this.enableFractions = <boolean>json.enableFractions;
this.currency = json.currency;
this._value = this.parseValue(json);
if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') {
this.placeholder = json.placeholder;
}
// <container>
this.numberOfColumns = <number> json.numberOfColumns;
let columnSize: number = 12;
if (this.numberOfColumns > 1) {
columnSize = 12 / this.numberOfColumns;
}
for (let i = 0; i < this.numberOfColumns; i++) {
let col = new ContainerColumnModel();
col.size = columnSize;
this.columns.push(col);
}
if (json.fields) {
Object.keys(json.fields).map(key => {
let fields = (json.fields[key] || []).map(f => new FormFieldModel(form, f));
let col = this.columns[parseInt(key, 10) - 1];
col.fields = fields;
this.fields.push(...fields);
});
}
// </container>
}
if (this.hasEmptyValue && this.options && this.options.length > 0) {
@@ -19,7 +19,7 @@ import { FormModel } from './form.model';
import { TabModel } from './tab.model';
import { ContainerModel } from './container.model';
import { FormOutcomeModel } from './form-outcome.model';
import { FormValues } from './form-values';
// import { FormValues } from './form-values';
import { FormFieldTypes } from './form-field-types';
describe('FormModel', () => {
@@ -197,6 +197,7 @@ describe('FormModel', () => {
expect(tab2.fields[0].id).toBe('field2');
});
/*
it('should apply external data', () => {
let data: FormValues = {
field1: 'one',
@@ -259,6 +260,7 @@ describe('FormModel', () => {
expect(field3.id).toBe('field3');
expect(field3.value).toBe('original-value');
});
*/
it('should create standard form outcomes', () => {
let json = {
@@ -22,7 +22,7 @@ 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';
import { FormFieldTemplates } from './form-field-templates';
export class FormModel {
@@ -43,8 +43,10 @@ export class FormModel {
readOnly: boolean = false;
tabs: TabModel[] = [];
/** Stores root containers */
fields: FormWidgetModel[] = [];
outcomes: FormOutcomeModel[] = [];
customFieldTemplates: FormFieldTemplates = {};
values: FormValues = {};
@@ -73,6 +75,7 @@ export class FormModel {
this.taskId = json.taskId;
this.taskName = json.taskName || json.name || FormModel.UNSET_TASK_NAME;
this.processDefinitionId = json.processDefinitionId;
this.customFieldTemplates = json.customFieldTemplates || {};
let tabCache: FormWidgetModelCache<TabModel> = {};
@@ -122,14 +125,10 @@ export class FormModel {
for (let i = 0; i < this.fields.length; i++) {
let field = this.fields[i];
if (field.type === FormFieldTypes.CONTAINER || field.type === FormFieldTypes.GROUP) {
if (field instanceof ContainerModel) {
let container = <ContainerModel> field;
result.push(...container.getFormFields());
}
if (field.type === FormFieldTypes.DYNAMIC_TABLE) {
let dynamicTable = <DynamicTableModel> field;
result.push(dynamicTable.field);
result.push(container.field);
result.push(...container.field.fields);
}
}
@@ -158,7 +157,7 @@ export class FormModel {
this.validateForm();
}
// Activiti supports 2 types of root fields: 'container' and 'dynamic-table'.
// Activiti supports 3 types of root fields: container|group|dynamic-table
private parseRootFields(json: any): FormWidgetModel[] {
let fields = [];
@@ -171,18 +170,16 @@ export class FormModel {
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) {
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));
result.push(new ContainerModel(new FormFieldModel(this, field)));
}
}
} else {
result.push(new ContainerModel(new FormFieldModel(this, field)));
}
}
@@ -19,15 +19,13 @@ export * from './form-field-metadata';
export * from './form-values';
export * from './form-field-types';
export * from './form-field-option';
export * from './form-field-templates';
export * from './form-widget.model';
export * from './form-field.model';
export * from './form.model';
export * from './container-column.model';
export * from './container.model';
export * from './container-column.model';
export * from './tab.model';
export * from './form-outcome.model';
export * from './form-outcome-event.model';
export * from './form-field-validator';
export * from './dynamic-table.model';
export * from './dynamic-table-column';
export * from './dynamic-table-row';
@@ -53,7 +53,7 @@ describe('TabModel', () => {
model.fields = [];
expect(model.hasContent()).toBeFalsy();
model.fields = [new ContainerModel(null, null)];
model.fields = [new ContainerModel(null)];
expect(model.hasContent()).toBeTruthy();
});
@@ -1,7 +1,8 @@
<div class="mdl-grid" *ngIf="field?.isVisible" id="data-widget">
<div class="mdl-cell mdl-cell--11-col">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label date-widget"
<div class="mdl-textfield mdl-js-textfield date-widget"
[class.date-widget__invalid]="!field.isValid">
<label [attr.for]="field.id">{{field.name}}</label>
<input class="mdl-textfield__input"
type="text"
[attr.id]="field.id"
@@ -9,8 +10,8 @@
[(ngModel)]="field.value"
(ngModelChange)="onDateChanged()"
(onOk)="onDateSelected()"
[disabled]="field.readOnly">
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}} (d-M-yyyy)</label>
[disabled]="field.readOnly"
placeholder="{{field.placeholder}}">
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>
</div>
@@ -16,7 +16,7 @@
*/
import { Component, ElementRef, OnInit, AfterViewChecked } from '@angular/core';
import { TextFieldWidgetComponent } from './../textfield-widget.component';
import { WidgetComponent } from './../widget.component';
@Component({
moduleId: module.id,
@@ -24,14 +24,14 @@ import { TextFieldWidgetComponent } from './../textfield-widget.component';
templateUrl: './date.widget.html',
styleUrls: ['./date.widget.css']
})
export class DateWidget extends TextFieldWidgetComponent implements OnInit, AfterViewChecked {
export class DateWidget extends WidgetComponent implements OnInit, AfterViewChecked {
DATE_FORMAT: string = 'D-M-YYYY';
datePicker: any;
constructor(elementRef: ElementRef) {
super(elementRef);
constructor(private elementRef: ElementRef) {
super();
}
ngOnInit() {
@@ -69,7 +69,11 @@ export class DateWidget extends TextFieldWidgetComponent implements OnInit, Afte
onDateChanged() {
if (this.field.value) {
this.datePicker.time = moment(this.field.value, this.DATE_FORMAT);
let value = moment(this.field.value, this.DATE_FORMAT);
if (!value.isValid()) {
value = moment();
}
this.datePicker.time = value;
}
this.checkVisibility(this.field);
}
@@ -6,6 +6,12 @@
padding: 8px;
}
.display-value-widget__dynamic-table .is-disabled {
background-color: transparent;
border-bottom: 1px dotted rgba(0,0,0,.12);
color: rgba(0,0,0,.26);
}
.display-value-widget__dynamic-table table {
width: 100%;
}
@@ -1,8 +1,9 @@
<div [ngSwitch]="fieldType" class="display-value-widget">
<div *ngSwitchCase="'boolean'">
<label class="mdl-checkbox mdl-js-checkbox" [attr.for]="field.id">
<label class="mdl-checkbox mdl-js-checkbox" [attr.for]="field.id" *ngIf="field?.isVisible">
<input type="checkbox"
[attr.id]="field.id"
[checked]="field.value"
[(ngModel)]="value"
class="mdl-checkbox__input"
disabled>
@@ -10,41 +11,46 @@
</label>
</div>
<div *ngSwitchCase="'text'"
class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label text-widget">
<input class="mdl-textfield__input"
class="mdl-textfield mdl-js-textfield text-widget">
<label *ngIf="field?.isVisible" [attr.for]="field.id">{{field.name}}</label>
<input
*ngIf="field?.isVisible"
class="mdl-textfield__input"
type="text"
[attr.id]="field.id"
[value]="value"
disabled>
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
</div>
<div *ngSwitchCase="'multi-line-text'"
class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label multiline-text-widget">
class="mdl-textfield mdl-js-textfield multiline-text-widget">
<textarea class="mdl-textfield__input"
type="text"
rows= "3"
rows="3"
[value]="value"
[attr.id]="field.id"
disabled>
disabled
*ngIf="field?.isVisible">
</textarea>
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
<label *ngIf="field?.isVisible" class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
</div>
<div *ngSwitchCase="'hyperlink'" class="hyperlink-widget">
<div>
<span>{{field.name}}</span>
</div>
<div>
<a [href]="linkUrl" target="_blank" rel="nofollow">{{linkText}}</a>
<div *ngIf="field?.isVisible">
<div>
<span>{{field.name}}</span>
</div>
<div>
<a [href]="linkUrl" target="_blank" rel="nofollow">{{linkText}}</a>
</div>
</div>
</div>
<div *ngSwitchCase="'dynamic-table'">
<div class="display-value-widget__dynamic-table">
<div class="display-value-widget__dynamic-table" *ngIf="field?.isVisible">
<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">
class="mdl-data-table__cell--non-numeric is-disabled">
{{column.name}}
</th>
</tr>
@@ -52,7 +58,7 @@
<tbody>
<tr *ngFor="let row of rows">
<td *ngFor="let column of visibleColumns"
class="mdl-data-table__cell--non-numeric">
class="mdl-data-table__cell--non-numeric is-disabled">
{{ getCellValue(row, column) }}
</td>
</tr>
@@ -61,7 +67,7 @@
</div>
</div>
<div *ngSwitchCase="'upload'">
<div class="upload-widget">
<div class="upload-widget" *ngIf="field?.isVisible">
<label class="upload-widget__label" [attr.for]="field.id">{{field.name}}</label>
<div>
<i *ngIf="hasFile" class="material-icons upload-widget__icon">attachment</i>
@@ -70,14 +76,15 @@
</div>
</div>
<div *ngSwitchDefault
class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label text-widget">
<input class="mdl-textfield__input"
type="text"
[attr.id]="field.id"
[value]="value"
(ngModelChange)="checkVisibility(field)"
disabled>
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
class="mdl-textfield mdl-js-textfield text-widget is-disabled is-dirty is-upgraded">
<label *ngIf="field?.isVisible" [attr.for]="field.id">{{field.name}}</label>
<input
*ngIf="field?.isVisible"
class="mdl-textfield__input"
type="text"
[attr.id]="field.id"
[value]="value"
disabled>
</div>
</div>
@@ -15,23 +15,28 @@
* limitations under the License.
*/
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { CoreModule } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx';
import { DisplayValueWidget } from './display-value.widget';
import { FormService } from '../../../services/form.service';
import { EcmModelService } from '../../../services/ecm-model.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';
import { DynamicTableColumn, DynamicTableRow } from './../dynamic-table/dynamic-table.widget.model';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
describe('DisplayValueWidget', () => {
let widget: DisplayValueWidget;
let formService: FormService;
let visibilityService: WidgetVisibilityService;
beforeEach(() => {
formService = new FormService(null, null);
widget = new DisplayValueWidget(formService);
visibilityService = new WidgetVisibilityService(null, null, null);
widget = new DisplayValueWidget(formService, visibilityService);
});
it('should require field to setup default value', () => {
@@ -670,4 +675,79 @@ describe('DisplayValueWidget', () => {
expect(widget.getCellValue(row, column)).toBe(expected);
});
describe('UI check', () => {
let widgetUI: DisplayValueWidget;
let fixture: ComponentFixture<DisplayValueWidget>;
let element: HTMLElement;
let componentHandler;
beforeEach(async(() => {
componentHandler = jasmine.createSpyObj('componentHandler', ['upgradeAllRegistered', 'upgradeElement']);
window['componentHandler'] = componentHandler;
TestBed.configureTestingModule({
imports: [CoreModule],
declarations: [DisplayValueWidget],
providers: [
EcmModelService,
FormService,
WidgetVisibilityService
]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(DisplayValueWidget);
widgetUI = fixture.componentInstance;
element = fixture.nativeElement;
});
}));
beforeEach(() => {
spyOn(widgetUI, 'setupMaterialTextField').and.stub();
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
it('should show the checkbox on when [BOOLEAN] field is true', async(() => {
widgetUI.field = new FormFieldModel(null, {
id: 'fake-checkbox-id',
type: FormFieldTypes.DISPLAY_VALUE,
value: 'true',
params: {
field: {
type: FormFieldTypes.BOOLEAN
}
}
});
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let elWidget: any = element.querySelector('#fake-checkbox-id');
expect(elWidget).toBeDefined();
expect(elWidget.checked).toBeTruthy();
});
}));
it('should show the checkbox off when [BOOLEAN] field is false', async(() => {
widgetUI.field = new FormFieldModel(null, {
id: 'fake-checkbox-id',
type: FormFieldTypes.DISPLAY_VALUE,
value: 'false',
params: {
field: {
type: FormFieldTypes.BOOLEAN
}
}
});
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let elWidget: any = element.querySelector('#fake-checkbox-id');
expect(elWidget).toBeDefined();
expect(elWidget.checked).toBeFalsy();
});
}));
});
});
@@ -15,13 +15,13 @@
* limitations under the License.
*/
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, AfterViewChecked } from '@angular/core';
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';
import { DynamicTableColumn, DynamicTableRow } from './../dynamic-table/dynamic-table.widget.model';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
@Component({
moduleId: module.id,
@@ -29,7 +29,7 @@ import { DynamicTableRow } from './../core/dynamic-table-row';
templateUrl: './display-value.widget.html',
styleUrls: ['./display-value.widget.css']
})
export class DisplayValueWidget extends WidgetComponent implements OnInit {
export class DisplayValueWidget extends WidgetComponent implements OnInit, AfterViewChecked {
value: any;
fieldType: string;
@@ -46,14 +46,15 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
// upload/attach
hasFile: boolean = false;
constructor(private formService: FormService) {
constructor(private formService: FormService,
private visibilityService: WidgetVisibilityService) {
super();
}
ngOnInit() {
if (this.field) {
this.value = this.field.value;
this.visibilityService.refreshEntityVisibility(this.field);
if (this.field.params) {
let originalField = this.field.params['field'];
if (originalField && originalField.type) {
@@ -124,7 +125,7 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
this.visibleColumns = this.columns.filter(col => col.visible);
}
if (json.value) {
this.rows = json.value.map(obj => <DynamicTableRow> {selected: false, value: obj});
this.rows = json.value.map(obj => <DynamicTableRow> { selected: false, value: obj });
}
break;
default:
@@ -136,6 +137,10 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
}
}
ngAfterViewChecked() {
this.visibilityService.refreshVisibility(this.field.form);
}
loadRadioButtonValue() {
let options = this.field.options || [];
let toSelect = options.find(item => item.id === this.field.value);
@@ -147,10 +152,10 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
}
loadRestFieldValue() {
if (this.field.form.processDefinitionId) {
this.getValuesByProcessDefinitionId();
} else {
if (this.field.form.taskId) {
this.getValuesByTaskId();
} else {
this.getValuesByProcessDefinitionId();
}
}
@@ -164,6 +169,7 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
(result: FormFieldOption[]) => {
let options = result || [];
let toSelect = options.find(item => item.id === this.field.value);
this.field.options = options;
if (toSelect) {
this.value = toSelect.name;
} else {
@@ -184,6 +190,7 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
(result: FormFieldOption[]) => {
let options = result || [];
let toSelect = options.find(item => item.id === this.field.value);
this.field.options = options;
if (toSelect) {
this.value = toSelect.name;
} else {
@@ -1,11 +1,11 @@
<div class="dropdown-widget"
[class.dropdown-widget__invalid]="!field.isValid">
[class.dropdown-widget__invalid]="!field.isValid" *ngIf="field?.isVisible">
<label class="dropdown-widget__label" [attr.for]="field.id">{{field.name}}</label>
<select class="dropdown-widget__select"
[attr.id]="field.id"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)">
<option *ngFor="let opt of field.options" [value]="opt.id">{{opt.name}}</option>
<option *ngFor="let opt of field.options" [value]="getOptionValue(opt, field.value)" [id]="opt.id">{{opt.name}}</option>
</select>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>
@@ -21,15 +21,21 @@ import { DropdownWidget } from './dropdown.widget';
import { FormModel } from './../core/form.model';
import { FormFieldModel } from './../core/form-field.model';
import { FormFieldOption } from './../core/form-field-option';
import { CoreModule } from 'ng2-alfresco-core';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { EcmModelService } from '../../../services/ecm-model.service';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
describe('DropdownWidget', () => {
let formService: FormService;
let widget: DropdownWidget;
let visibilityService: WidgetVisibilityService;
beforeEach(() => {
formService = new FormService(null, null);
widget = new DropdownWidget(formService);
visibilityService = new WidgetVisibilityService(null, null, null);
widget = new DropdownWidget(formService, visibilityService);
widget.field = new FormFieldModel(new FormModel());
});
@@ -98,4 +104,161 @@ describe('DropdownWidget', () => {
expect(widget.field.options[0]).toBe(emptyOption);
expect(widget.field.options[1]).toBe(restFieldValue);
});
describe('when template is ready', () => {
let dropDownWidget: DropdownWidget;
let fixture: ComponentFixture<DropdownWidget>;
let element: HTMLElement;
let componentHandler;
let stubFormService;
let fakeOptionList: FormFieldOption[] = [{
id: 'opt_1',
name: 'option_1'
}, {
id: 'opt_2',
name: 'option_2'
}, { id: 'opt_3', name: 'option_3' }];
beforeEach(async(() => {
componentHandler = jasmine.createSpyObj('componentHandler', ['upgradeAllRegistered', 'upgradeElement']);
window['componentHandler'] = componentHandler;
TestBed.configureTestingModule({
imports: [CoreModule],
declarations: [DropdownWidget],
providers: [FormService, EcmModelService, WidgetVisibilityService]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(DropdownWidget);
dropDownWidget = fixture.componentInstance;
element = fixture.nativeElement;
});
}));
describe('and dropdown is populated via taskId', () => {
beforeEach(async(() => {
stubFormService = fixture.debugElement.injector.get(FormService);
visibilityService = fixture.debugElement.injector.get(WidgetVisibilityService);
spyOn(visibilityService, 'refreshVisibility').and.stub();
spyOn(stubFormService, 'getRestFieldValues').and.returnValue(Observable.of(fakeOptionList));
dropDownWidget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
dropDownWidget.field.emptyOption = { id: 'empty', name: 'Choose one...' };
dropDownWidget.field.isVisible = true;
fixture.detectChanges();
}));
it('should show visible dropdown widget', async(() => {
expect(element.querySelector('#dropdown-id')).toBeDefined();
expect(element.querySelector('#dropdown-id')).not.toBeNull();
expect(element.querySelector('#opt_1')).not.toBeNull();
expect(element.querySelector('#opt_2')).not.toBeNull();
expect(element.querySelector('#opt_3')).not.toBeNull();
}));
it('should select the default value when an option is chosen as default', async(() => {
dropDownWidget.field.value = 'option_2';
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let dropDownElement: HTMLSelectElement = <HTMLSelectElement> element.querySelector('#dropdown-id');
expect(dropDownElement).not.toBeNull();
expect(element.querySelector('#opt_2')).not.toBeNull();
expect(dropDownElement.value).toBe('option_2');
expect(dropDownElement.selectedOptions[0].textContent).toBe('option_2');
});
}));
it('should select the empty value when no default is chosen', async(() => {
dropDownWidget.field.value = 'empty';
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let dropDownElement: HTMLSelectElement = <HTMLSelectElement> element.querySelector('#dropdown-id');
expect(dropDownElement).not.toBeNull();
expect(dropDownElement.value).toBe('empty');
expect(dropDownElement.selectedOptions[0].textContent).toBe('Choose one...');
});
}));
it('should be not visibile when isVisible is false', async(() => {
dropDownWidget.field.isVisible = false;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let dropDownElement: HTMLSelectElement = <HTMLSelectElement> element.querySelector('#dropdown-id');
expect(dropDownElement).toBeNull();
});
}));
it('should became visibile when isVisible is true', async(() => {
dropDownWidget.field.isVisible = false;
fixture.detectChanges();
expect(element.querySelector('#dropdown-id')).toBeNull();
dropDownWidget.field.isVisible = true;
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#dropdown-id')).not.toBeNull();
});
}));
});
describe('and dropdown is populated via processDefinitionId', () => {
beforeEach(async(() => {
stubFormService = fixture.debugElement.injector.get(FormService);
visibilityService = fixture.debugElement.injector.get(WidgetVisibilityService);
spyOn(visibilityService, 'refreshVisibility').and.stub();
spyOn(stubFormService, 'getRestFieldValuesByProcessId').and.returnValue(Observable.of(fakeOptionList));
dropDownWidget.field = new FormFieldModel(new FormModel({ processDefinitionId: 'fake-process-id' }), {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
dropDownWidget.field.emptyOption = { id: 'empty', name: 'Choose one...' };
dropDownWidget.field.isVisible = true;
fixture.detectChanges();
}));
it('should show visible dropdown widget', async(() => {
expect(element.querySelector('#dropdown-id')).toBeDefined();
expect(element.querySelector('#dropdown-id')).not.toBeNull();
expect(element.querySelector('#opt_1')).not.toBeNull();
expect(element.querySelector('#opt_2')).not.toBeNull();
expect(element.querySelector('#opt_3')).not.toBeNull();
}));
it('should select the default value when an option is chosen as default', async(() => {
dropDownWidget.field.value = 'option_2';
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let dropDownElement: HTMLSelectElement = <HTMLSelectElement> element.querySelector('#dropdown-id');
expect(dropDownElement).not.toBeNull();
expect(element.querySelector('#opt_2')).not.toBeNull();
expect(dropDownElement.value).toBe('option_2');
expect(dropDownElement.selectedOptions[0].textContent).toBe('option_2');
});
}));
it('should select the empty value when no default is chosen', async(() => {
dropDownWidget.field.value = 'empty';
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let dropDownElement: HTMLSelectElement = <HTMLSelectElement> element.querySelector('#dropdown-id');
expect(dropDownElement).not.toBeNull();
expect(dropDownElement.value).toBe('empty');
expect(dropDownElement.selectedOptions[0].textContent).toBe('Choose one...');
});
}));
});
});
});
@@ -19,6 +19,7 @@ import { Component, OnInit } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { WidgetComponent } from './../widget.component';
import { FormFieldOption } from './../core/form-field-option';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
@Component({
moduleId: module.id,
@@ -28,16 +29,17 @@ import { FormFieldOption } from './../core/form-field-option';
})
export class DropdownWidget extends WidgetComponent implements OnInit {
constructor(private formService: FormService) {
constructor(private formService: FormService,
private visibilityService: WidgetVisibilityService) {
super();
}
ngOnInit() {
if (this.field && this.field.restUrl) {
if (this.field.form.processDefinitionId) {
this.getValuesByProcessDefinitionId();
} else {
if (this.field.form.taskId) {
this.getValuesByTaskId();
} else {
this.getValuesByProcessDefinitionId();
}
}
}
@@ -80,6 +82,20 @@ export class DropdownWidget extends WidgetComponent implements OnInit {
);
}
getOptionValue(option: FormFieldOption, fieldValue: string): string {
let optionValue: string = '';
if (option.id === 'empty' || option.name !== fieldValue) {
optionValue = option.id;
} else {
optionValue = option.name;
}
return optionValue;
}
checkVisibility() {
this.visibilityService.refreshVisibility(this.field.form);
}
handleError(error: any) {
console.error(error);
}
@@ -1,5 +1,5 @@
<div class="dynamic-table-widget"
[class.dynamic-table-widget__invalid]="!isValid()">
[class.dynamic-table-widget__invalid]="!isValid()" *ngIf="field?.isVisible">
<div class="dynamic-table-widget__label">{{content.name}}</div>
<div *ngIf="!editMode">
@@ -15,11 +15,9 @@
* 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';
import { FormWidgetModel } from './../core/form-widget.model';
import { FormModel } from './../core/form.model';
import { FormFieldModel } from './../core/form-field.model';
export class DynamicTableModel extends FormWidgetModel {
@@ -285,3 +283,41 @@ export class NumberCellValidator implements CellValidator {
return true;
}
}
// 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;
}
export interface DynamicTableRow {
isNew: boolean;
selected: boolean;
value: any;
}
@@ -16,16 +16,20 @@
*/
import { DynamicTableWidget } from './dynamic-table.widget';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn, FormModel, FormFieldTypes } from './../core/index';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './dynamic-table.widget.model';
import { FormModel, FormFieldTypes } from './../core/index';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
describe('DynamicTableWidget', () => {
let widget: DynamicTableWidget;
let table: DynamicTableModel;
let visibilityService: WidgetVisibilityService;
beforeEach(() => {
table = new DynamicTableModel(null);
widget = new DynamicTableWidget(null);
visibilityService = new WidgetVisibilityService(null, null, null);
widget = new DynamicTableWidget(null, visibilityService);
widget.content = table;
});
@@ -91,8 +95,8 @@ describe('DynamicTableWidget', () => {
});
it('should move selection down', () => {
let row1 = <DynamicTableRow> { };
let row2 = <DynamicTableRow> { };
let row1 = <DynamicTableRow> {};
let row2 = <DynamicTableRow> {};
widget.content.rows.push(...[row1, row2]);
widget.content.selectedRow = row1;
@@ -247,5 +251,4 @@ describe('DynamicTableWidget', () => {
let actual = widget.getCellValue(row, column);
expect(actual).toBe('GBP 100');
});
});
@@ -15,9 +15,10 @@
* limitations under the License.
*/
import { Component, Input, ElementRef } from '@angular/core';
import { Component, ElementRef, OnInit } from '@angular/core';
import { WidgetComponent } from './../widget.component';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './../core/index';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './dynamic-table.widget.model';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
@Component({
moduleId: module.id,
@@ -25,20 +26,27 @@ import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './../cor
templateUrl: './dynamic-table.widget.html',
styleUrls: ['./dynamic-table.widget.css']
})
export class DynamicTableWidget extends WidgetComponent {
export class DynamicTableWidget extends WidgetComponent implements OnInit {
ERROR_MODEL_NOT_FOUND = 'Table model not found';
@Input()
content: DynamicTableModel;
editMode: boolean = false;
editRow: DynamicTableRow = null;
constructor(private elementRef: ElementRef) {
constructor(private elementRef: ElementRef,
private visibilityService: WidgetVisibilityService) {
super();
}
ngOnInit() {
if (this.field) {
this.content = new DynamicTableModel(this.field.form, this.field.json);
this.visibilityService.refreshVisibility(this.field.form);
}
}
isValid() {
let result = true;
@@ -16,7 +16,7 @@
*/
import { BooleanEditorComponent } from './boolean.editor';
import { DynamicTableRow, DynamicTableColumn } from './../../../core/index';
import { DynamicTableRow, DynamicTableColumn } from './../../dynamic-table.widget.model';
describe('BooleanEditorComponent', () => {
@@ -17,7 +17,7 @@
import { Component } from '@angular/core';
import { CellEditorComponent } from './../cell.editor';
import { DynamicTableRow, DynamicTableColumn } from './../../../core/index';
import { DynamicTableRow, DynamicTableColumn } from './../../dynamic-table.widget.model';
@Component({
moduleId: module.id,
@@ -16,7 +16,7 @@
*/
import { Input } from '@angular/core';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './../../core/index';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './../dynamic-table.widget.model';
export abstract class CellEditorComponent {
@@ -17,7 +17,7 @@
import { ElementRef } from '@angular/core';
import { DateEditorComponent } from './date.editor';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './../../../core/index';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './../../dynamic-table.widget.model';
describe('DateEditorComponent', () => {
@@ -3,12 +3,13 @@
<div>
<select
class="dropdown-editor__select"
[attr.id]="column.id"
[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>
<option *ngFor="let opt of options" [value]="opt.name" [id]="opt.id">{{opt.name}}</option>
</select>
</div>
</div>
@@ -21,11 +21,13 @@ import {
DynamicTableModel,
DynamicTableRow,
DynamicTableColumn,
DynamicTableColumnOption,
FormFieldModel,
FormModel
} from './../../../core/index';
DynamicTableColumnOption
} from './../../dynamic-table.widget.model';
import { FormFieldModel, FormModel } from './../../../core/index';
import { FormService } from './../../../../../services/form.service';
import { CoreModule } from 'ng2-alfresco-core';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { EcmModelService } from '../../../../../services/ecm-model.service';
describe('DropdownEditorComponent', () => {
@@ -48,8 +50,8 @@ describe('DropdownEditorComponent', () => {
]
};
table = new DynamicTableModel(null);
form = new FormModel({ taskId: '<task-id>' });
table = new DynamicTableModel(form, null);
table.field = new FormFieldModel(form, { id: '<field-id>' });
table.rows.push(row);
table.columns.push(column);
@@ -132,7 +134,7 @@ describe('DropdownEditorComponent', () => {
expect(component.value).toBe(row.value[column.id]);
});
it('should handle REST error', () => {
it('should handle REST error gettig options with task id', () => {
column.optionType = 'rest';
const error = 'error';
@@ -145,10 +147,155 @@ describe('DropdownEditorComponent', () => {
expect(component.handleError).toHaveBeenCalledWith(error);
});
it('should handle REST error getting option with processDefinitionId', () => {
column.optionType = 'rest';
let procForm = new FormModel ({processDefinitionId: '<process-definition-id>'});
let procTable = new DynamicTableModel(procForm, null);
procTable.field = new FormFieldModel(form, { id: '<field-id>' });
component.table = procTable;
const error = 'error';
spyOn(formService, 'getRestFieldValuesColumnByProcessId').and.returnValue(
Observable.throw(error)
);
spyOn(component, 'handleError').and.stub();
component.ngOnInit();
expect(component.handleError).toHaveBeenCalledWith(error);
});
it('should update row on value change', () => {
let event = { target: { value: 'two' } };
component.onValueChanged(row, column, event);
expect(row.value[column.id]).toBe(column.options[1]);
});
describe('when template is ready', () => {
let dropDownEditorComponent: DropdownEditorComponent;
let fixture: ComponentFixture<DropdownEditorComponent>;
let element: HTMLElement;
let componentHandler;
let stubFormService;
let fakeOptionList: DynamicTableColumnOption[] = [{
id: 'opt_1',
name: 'option_1'
}, {
id: 'opt_2',
name: 'option_2'
}, { id: 'opt_3', name: 'option_3' }];
let dynamicTable: DynamicTableModel;
beforeEach(async(() => {
componentHandler = jasmine.createSpyObj('componentHandler', ['upgradeAllRegistered', 'upgradeElement']);
window['componentHandler'] = componentHandler;
TestBed.configureTestingModule({
imports: [CoreModule],
declarations: [DropdownEditorComponent],
providers: [FormService, EcmModelService]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(DropdownEditorComponent);
dropDownEditorComponent = fixture.componentInstance;
element = fixture.nativeElement;
});
}));
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('and dropdown is populated via taskId', () => {
beforeEach(async(() => {
stubFormService = fixture.debugElement.injector.get(FormService);
spyOn(stubFormService, 'getRestFieldValuesColumn').and.returnValue(Observable.of(fakeOptionList));
row = <DynamicTableRow> { value: { dropdown: 'one' } };
column = <DynamicTableColumn> {
id: 'column-id',
optionType: 'rest',
options: [
<DynamicTableColumnOption> { id: '1', name: 'one' },
<DynamicTableColumnOption> { id: '2', name: 'two' }
]
};
form = new FormModel({ taskId: '<task-id>' });
dynamicTable = new DynamicTableModel(form, null);
dynamicTable.field = new FormFieldModel(form, { id: '<field-id>' });
dynamicTable.rows.push(row);
dynamicTable.columns.push(column);
dropDownEditorComponent.table = dynamicTable;
dropDownEditorComponent.column = column;
dropDownEditorComponent.row = row;
dropDownEditorComponent.table.field = new FormFieldModel(form, {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
dropDownEditorComponent.table.field.isVisible = true;
fixture.detectChanges();
}));
it('should show visible dropdown widget', async(() => {
expect(element.querySelector('#column-id')).toBeDefined();
expect(element.querySelector('#column-id')).not.toBeNull();
expect(element.querySelector('#opt_1')).not.toBeNull();
expect(element.querySelector('#opt_2')).not.toBeNull();
expect(element.querySelector('#opt_3')).not.toBeNull();
}));
});
describe('and dropdown is populated via processDefinitionId', () => {
beforeEach(async(() => {
stubFormService = fixture.debugElement.injector.get(FormService);
spyOn(stubFormService, 'getRestFieldValuesColumnByProcessId').and.returnValue(Observable.of(fakeOptionList));
row = <DynamicTableRow> { value: { dropdown: 'one' } };
column = <DynamicTableColumn> {
id: 'column-id',
optionType: 'rest',
options: [
<DynamicTableColumnOption> { id: '1', name: 'one' },
<DynamicTableColumnOption> { id: '2', name: 'two' }
]
};
form = new FormModel({ processDefinitionId: '<proc-id>' });
dynamicTable = new DynamicTableModel(form, null);
dynamicTable.field = new FormFieldModel(form, { id: '<field-id>' });
dynamicTable.rows.push(row);
dynamicTable.columns.push(column);
dropDownEditorComponent.table = dynamicTable;
dropDownEditorComponent.column = column;
dropDownEditorComponent.row = row;
dropDownEditorComponent.table.field = new FormFieldModel(form, {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
dropDownEditorComponent.table.field.isVisible = true;
fixture.detectChanges();
}));
it('should show visible dropdown widget', async(() => {
expect(element.querySelector('#column-id')).toBeDefined();
expect(element.querySelector('#column-id')).not.toBeNull();
expect(element.querySelector('#opt_1')).not.toBeNull();
expect(element.querySelector('#opt_2')).not.toBeNull();
expect(element.querySelector('#opt_3')).not.toBeNull();
}));
it('should show visible dropdown widget', async(() => {
expect(element.querySelector('#column-id')).toBeDefined();
expect(element.querySelector('#column-id')).not.toBeNull();
expect(element.querySelector('#opt_1')).not.toBeNull();
expect(element.querySelector('#opt_2')).not.toBeNull();
expect(element.querySelector('#opt_3')).not.toBeNull();
}));
});
});
});
@@ -17,7 +17,7 @@
import { Component, OnInit } from '@angular/core';
import { CellEditorComponent } from './../cell.editor';
import { DynamicTableRow, DynamicTableColumn, DynamicTableColumnOption } from './../../../core/index';
import { DynamicTableRow, DynamicTableColumn, DynamicTableColumnOption } from './../../dynamic-table.widget.model';
import { FormService } from './../../../../../services/form.service';
@Component({
@@ -39,10 +39,10 @@ export class DropdownEditorComponent extends CellEditorComponent implements OnIn
let field = this.table.field;
if (field) {
if (this.column.optionType === 'rest') {
if (this.table.form && this.table.form.processDefinitionId) {
this.getValuesByProcessDefinitionId(field);
} else {
if (this.table.form && this.table.form.taskId) {
this.getValuesByTaskId(field);
} else {
this.getValuesByProcessDefinitionId(field);
}
} else {
this.options = this.column.options || [];
@@ -16,7 +16,7 @@
*/
import { RowEditorComponent } from './row.editor';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn, DynamicRowValidationSummary } from './../../core/index';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn, DynamicRowValidationSummary } from './../dynamic-table.widget.model';
describe('RowEditorComponent', () => {
@@ -16,7 +16,7 @@
*/
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn, DynamicRowValidationSummary } from './../../core/index';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn, DynamicRowValidationSummary } from './../dynamic-table.widget.model';
@Component({
moduleId: module.id,
@@ -16,7 +16,7 @@
*/
import { TextEditorComponent } from './text.editor';
import { DynamicTableRow, DynamicTableColumn } from './../../../core/index';
import { DynamicTableRow, DynamicTableColumn } from './../../dynamic-table.widget.model';
describe('TextEditorComponent', () => {
@@ -17,7 +17,7 @@
import { Component, OnInit } from '@angular/core';
import { CellEditorComponent } from './../cell.editor';
import { DynamicTableRow, DynamicTableColumn } from './../../../core/index';
import { DynamicTableRow, DynamicTableColumn } from './../../dynamic-table.widget.model';
@Component({
moduleId: module.id,
@@ -1,5 +1,6 @@
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label functional-group-widget"
<div class="mdl-textfield mdl-js-textfield functional-group-widget"
[class.functional-group-widget__invalid]="!field.isValid">
<label [attr.for]="field.id">{{field.name}}</label>
<input class="mdl-textfield__input"
type="text"
[attr.id]="field.id"
@@ -7,8 +8,8 @@
(ngModelChange)="checkVisibility(field)"
(keyup)="onKeyUp($event)"
(blur)="onBlur()"
[disabled]="field.readOnly">
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
[disabled]="field.readOnly"
placeholder="{{field.placeholder}}">
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>
<div class="functional-group-widget--autocomplete mdl-shadow--2dp" *ngIf="popupVisible && groups.length > 0">
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import { UnknownWidget } from './unknown/unknown.widget';
import { TabsWidget } from './tabs/tabs.widget';
import { ContainerWidget } from './container/container.widget';
@@ -50,6 +51,7 @@ export * from './tabs/tabs.widget';
export * from './container/container.widget';
// primitives
export * from './unknown/unknown.widget';
export * from './text/text.widget';
export * from './number/number.widget';
export * from './checkbox/checkbox.widget';
@@ -76,6 +78,7 @@ export * from './dynamic-table/editors/boolean/boolean.editor';
export * from './dynamic-table/editors/text/text.editor';
export const WIDGET_DIRECTIVES: any[] = [
UnknownWidget,
TabsWidget,
ContainerWidget,
TextWidget,
@@ -1,5 +1,6 @@
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label multiline-text-widget"
<div class="mdl-textfield mdl-js-textfield multiline-text-widget"
[class.multiline-text-widget__invalid]="!field.isValid">
<label [attr.for]="field.id">{{field.name}}</label>
<textarea class="mdl-textfield__input"
type="text"
rows= "3"
@@ -7,9 +8,9 @@
[attr.required]="isRequired()"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"
[disabled]="field.readOnly">
[disabled]="field.readOnly"
placeholder="{{field.placeholder}}">
</textarea>
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>

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