Merge pull request #1850 from Alfresco/development

1.4.0
This commit is contained in:
Mario Romano 2017-04-27 14:18:33 +01:00 committed by GitHub
commit bca027ddf0
192 changed files with 7059 additions and 2308 deletions

View File

@ -25,6 +25,7 @@ env:
- MODULE=ng2-activiti-diagrams
- MODULE=ng2-activiti-analytics
- MODULE=ng2-alfresco-userinfo
- MODULE=ng2-alfresco-social
before_script:
- if ([ "$MODULE" != "ng2-alfresco-core" ]); then

41
Jenkinsfile vendored
View File

@ -1,41 +0,0 @@
#!/usr/bin/groovy
@Library('github.com/fabric8io/fabric8-pipeline-library@v2.2.311')
def utils = new io.fabric8.Utils()
clientsNode{
def envStage = utils.environmentNamespace('staging')
def envProd = utils.environmentNamespace('production')
def newVersion = ''
git 'https://github.com/Alfresco/alfresco-ng2-components.git'
stage 'Canary release'
echo 'NOTE: running pipelines for the first time will take longer as build and base docker images are pulled onto the node'
if (!fileExists ('Dockerfile')) {
writeFile file: 'Dockerfile', text: 'FROM node:5.3-onbuild'
}
newVersion = performCanaryRelease {}
def rc = getKubernetesJson {
port = 8080
label = 'node'
icon = 'https://cdn.rawgit.com/fabric8io/fabric8/dc05040/website/src/images/logos/nodejs.svg'
version = newVersion
imageName = clusterImageName
}
stage 'Rollout Staging'
kubernetesApply(file: rc, environment: envStage)
stage 'Approve'
approve{
room = null
version = canaryVersion
console = fabric8Console
environment = envStage
}
stage 'Rollout Production'
kubernetesApply(file: rc, environment: envProd)
}

View File

@ -24,6 +24,7 @@ environment:
- COMPONENT_NAME: ng2-activiti-diagrams
- COMPONENT_NAME: ng2-activiti-analytics
- COMPONENT_NAME: ng2-alfresco-userinfo
- COMPONENT_NAME: ng2-alfresco-social
# Install scripts. (runs after repo cloning)
install:

View File

@ -58,6 +58,7 @@
<a class="mdl-navigation__link" href="" routerLink="/activiti" (click)="hideDrawer()">Process Services</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="/social" (click)="hideDrawer()">Social</a>
<a class="mdl-navigation__link" href="" routerLink="/about" (click)="hideDrawer()">About</a>
<a class="mdl-navigation__link" href="" routerLink="/settings" (click)="hideDrawer()">Settings</a>
</nav>

View File

@ -25,6 +25,7 @@ import { DataTableModule } from 'ng2-alfresco-datatable';
import { DocumentListModule } from 'ng2-alfresco-documentlist';
import { UploadModule } from 'ng2-alfresco-upload';
import { TagModule } from 'ng2-alfresco-tag';
import { SocialModule } from 'ng2-alfresco-social';
import { WebScriptModule } from 'ng2-alfresco-webscript';
import { ViewerModule } from 'ng2-alfresco-viewer';
import { ActivitiFormModule } from 'ng2-activiti-form';
@ -50,6 +51,7 @@ import {
FormViewer,
WebscriptComponent,
TagComponent,
SocialComponent,
AboutComponent,
FilesComponent,
FormNodeViewer,
@ -68,6 +70,7 @@ import {
DocumentListModule.forRoot(),
UploadModule.forRoot(),
TagModule.forRoot(),
SocialModule.forRoot(),
WebScriptModule.forRoot(),
ViewerModule.forRoot(),
ActivitiFormModule.forRoot(),
@ -91,6 +94,7 @@ import {
FormViewer,
WebscriptComponent,
TagComponent,
SocialComponent,
AboutComponent,
FilesComponent,
FormNodeViewer,

View File

@ -29,6 +29,7 @@ import {
ActivitiAppsView,
WebscriptComponent,
TagComponent,
SocialComponent,
AboutComponent,
FormViewer,
FormNodeViewer,
@ -117,6 +118,11 @@ export const appRoutes: Routes = [
component: TagComponent,
canActivate: [AuthGuardEcm]
},
{
path: 'social',
component: SocialComponent,
canActivate: [AuthGuardEcm]
},
{ path: 'about', component: AboutComponent },
{ path: 'settings', component: SettingComponent }
];

View File

@ -21,6 +21,6 @@
}
.list-buttons {
text-align: right;
text-align: left;
margin-bottom: 5px;
}
}

View File

@ -19,20 +19,25 @@
<div class="page-content">
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--2-col task-column mdl-shadow--2dp">
<activiti-filters
[appId]="appId"
(filterClick)="onTaskFilterClick($event)"
(onSuccess)="onSuccessTaskFilterList($event)"
#activitifilter>
</activiti-filters>
</div>
<div class="mdl-cell mdl-cell--3-col task-column mdl-shadow--2dp list-column">
<div class="list-buttons">
<activiti-start-task
[appId]="appId"
(onSuccess)="onStartTaskSuccess($event)">
</activiti-start-task>
</div>
<adf-accordion>
<adf-accordion-group [heading]="'Tasks'" [isSelected]="true" [isOpen]="true" [headingIcon]="'assignment'">
<activiti-filters
[appId]="appId"
[hasIcon]="false"
(filterClick)="onTaskFilterClick($event)"
(onSuccess)="onSuccessTaskFilterList($event)"
#activitifilter>
</activiti-filters>
</adf-accordion-group>
</adf-accordion>
</div>
<div class="mdl-cell mdl-cell--3-col task-column mdl-shadow--2dp list-column">
<activiti-tasklist
[appId]="taskFilter?.appId"
[processDefinitionKey]="taskFilter?.filter?.processDefinitionKey"
@ -58,10 +63,12 @@
</div>
<div class="mdl-cell mdl-cell--7-col task-column mdl-shadow--2dp">
<activiti-task-details #activitidetails
[debugMode]="true"
[taskId]="currentTaskId"
(formCompleted)="onFormCompleted($event)"
(formContentClicked)="onFormContentClick($event)"
(taskCreated)="onTaskCreated($event)">
(taskCreated)="onTaskCreated($event)"
(taskDeleted)="onTaskDeleted($event)">
</activiti-task-details>
</div>
</div>
@ -76,13 +83,6 @@
<div class="page-content">
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--2-col task-column mdl-shadow--2dp">
<activiti-process-instance-filters
[appId]="appId"
(filterClick)="onProcessFilterClick($event)"
(onSuccess)="onSuccessProcessFilterList($event)">
</activiti-process-instance-filters>
</div>
<div class="mdl-cell mdl-cell--3-col task-column list-column mdl-shadow--2dp" *ngIf="processFilter && !isStartProcessMode()">
<div class="list-buttons">
<button
md-raised-button
@ -92,6 +92,17 @@
<span>START PROCESS</span>
</button>
</div>
<adf-accordion>
<adf-accordion-group [heading]="'Processes'" [isSelected]="true" [isOpen]="true" [headingIcon]="'assessment'">
<activiti-process-instance-filters
[appId]="appId"
(filterClick)="onProcessFilterClick($event)"
(onSuccess)="onSuccessProcessFilterList($event)">
</activiti-process-instance-filters>
</adf-accordion-group>
</adf-accordion>
</div>
<div class="mdl-cell mdl-cell--3-col task-column list-column mdl-shadow--2dp" *ngIf="processFilter && !isStartProcessMode()">
<activiti-process-instance-list
*ngIf="processFilter?.hasFilter()" [appId]="processFilter.appId"
[processDefinitionKey]="processFilter.filter.processDefinitionKey"

View File

@ -264,6 +264,10 @@ export class ActivitiDemoComponent implements AfterViewInit {
this.taskList.reload();
}
onTaskDeleted(data: any) {
this.taskList.reload();
}
ngAfterViewInit() {
// workaround for MDL issues with dynamic components
if (componentHandler) {

View File

@ -15,8 +15,3 @@
.error-message--text {
color: #d50000;
}
.options-container {
width: 250px;
margin: 20px;
}

View File

@ -1,8 +1,7 @@
<div class="container">
<alfresco-upload-drag-area
[rootFolderId]="documentList.currentFolderId"
[versioning] = "versioning"
(onSuccess)="documentList.reload()">
[versioning] = "versioning">
<alfresco-document-list-breadcrumb
[target]="documentList"
[folderNode]="documentList.folderNode">
@ -21,19 +20,29 @@
[allowDropFiles]="true"
(error)="onNavigationError($event)"
(success)="resetError()"
(preview)="showFile($event)">
(preview)="showFile($event)"
(permissionError)="onPermissionsFailed($event)">
<data-columns>
<data-column key="$thumbnail" type="image" [sortable]="false"></data-column>
<data-column
title="{{'DOCUMENT_LIST.COLUMNS.DISPLAY_NAME' | translate}}"
key="name"
class="full-width ellipsis-cell">
<!-- Example of using custom column template -->
<!-- Example #1: using custom template with implicit access to data context -->
<!--
<template let-entry="$implicit">
<span>Hi! {{entry.data.getValue(entry.row, entry.col)}}</span>
</template>
-->
<!-- Example #2: using custom template with value access -->
<!--
<template let-value="value">
<span>Hi! {{value}}</span>
</template>
-->
</data-column>
<data-column
title="{{'DOCUMENT_LIST.COLUMNS.TAG' | translate}}"
@ -71,7 +80,10 @@
</content-action>
<content-action
target="folder"
permission="delete"
[disableWithNoPermission]="true"
title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}"
(permissionEvent)="onPermissionsFailed($event)"
handler="delete">
</content-action>
<!-- document actions -->
@ -92,6 +104,9 @@
</content-action>
<content-action
target="document"
permission="delete"
[disableWithNoPermission]="true"
(permissionEvent)="onPermissionsFailed($event)"
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DELETE' | translate}}"
handler="delete">
</content-action>
@ -107,64 +122,65 @@
<context-menu-holder></context-menu-holder>
<p class="options-container">
<label for="switch-multiple-file" class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input type="checkbox" id="switch-multiple-file" class="mdl-switch__input" (change)="toggleMultipleFileUpload()" >
<span class="mdl-switch__label">Multiple File Upload</span>
</label>
</p>
<div class="container">
<section>
<md-slide-toggle [(ngModel)]="multipleFileUpload">Multiple File Upload</md-slide-toggle>
</section>
<section>
<md-slide-toggle [(ngModel)]="folderUpload">Folder upload</md-slide-toggle>
</section>
<p class="options-container">
<label for="switch-folder-upload" class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input type="checkbox" id="switch-folder-upload" class="mdl-switch__input" (change)="toggleFolder()">
<span class="mdl-switch__label">Folder Upload</span>
</label>
</p>
<section>
<md-slide-toggle [(ngModel)]="acceptedFilesTypeShow">Custom extensions filter</md-slide-toggle>
</section>
<p class="options-container">
<label for="switch-accepted-file-type" class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input type="checkbox" id="switch-accepted-file-type" class="mdl-switch__input" (change)="toggleAcceptedFilesType()">
<span class="mdl-switch__label">Filter extension</span>
</label>
</p>
<section>
<md-slide-toggle [(ngModel)]="versioning">Enable versioning</md-slide-toggle>
</section>
<p style="width:250px;margin: 20px;">
<label for="switch-versioning" class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input type="checkbox" id="switch-versioning" class="mdl-switch__input" (change)="toggleVersioning()">
<span class="mdl-switch__label">Versioning</span>
</label>
</p>
<section>
<md-slide-toggle [(ngModel)]="disableWithNoPermission">Disable when user has no permissions</md-slide-toggle>
</section>
<h5>Upload</h5>
<br>
<div *ngIf="acceptedFilesTypeShow">
<label class="mdl-input__label">Extension accepted
<input type="text" data-automation-id="accepted-files-type" [(ngModel)]="acceptedFilesType">
</label>
<br/>
</div>
<div *ngIf="!acceptedFilesTypeShow">
<alfresco-upload-button data-automation-id="multiple-file-upload"
[rootFolderId]="documentList.currentFolderId"
[multipleFiles]="multipleFileUpload"
[uploadFolders]="folderUpload"
[versioning] = "versioning"
(onSuccess)="documentList.reload()">
<div class="mdl-spinner mdl-js-spinner is-active"></div>
</alfresco-upload-button>
</div>
<div *ngIf="acceptedFilesTypeShow">
<alfresco-upload-button data-automation-id="multiple-file-upload"
[rootFolderId]="documentList.currentFolderId"
[acceptedFilesType]="acceptedFilesType"
[multipleFiles]="multipleFileUpload"
[uploadFolders]="folderUpload"
[versioning] = "versioning"
(onSuccess)="documentList.reload()">
<div class="mdl-spinner mdl-js-spinner is-active"></div>
</alfresco-upload-button>
<h5>Upload</h5>
<section *ngIf="acceptedFilesTypeShow">
<md-input-container>
<input md-input placeholder="Extension accepted" [(ngModel)]="acceptedFilesType" data-automation-id="accepted-files-type">
</md-input-container>
</section>
<div *ngIf="!acceptedFilesTypeShow">
<alfresco-upload-button
#uploadButton
[disabled]="!enableUpload"
data-automation-id="multiple-file-upload"
[rootFolderId]="documentList.currentFolderId"
[multipleFiles]="multipleFileUpload"
[uploadFolders]="folderUpload"
[versioning]="versioning"
[disableWithNoPermission]="disableWithNoPermission"
(permissionEvent)="onUploadPermissionFailed($event)">
</alfresco-upload-button>
</div>
<div *ngIf="acceptedFilesTypeShow">
<alfresco-upload-button
#uploadButton
[disabled]="!enableUpload"
data-automation-id="multiple-file-upload"
[rootFolderId]="documentList.currentFolderId"
[acceptedFilesType]="acceptedFilesType"
[multipleFiles]="multipleFileUpload"
[uploadFolders]="folderUpload"
[versioning]="versioning"
[disableWithNoPermission]="disableWithNoPermission"
(permissionEvent)="onUploadPermissionFailed($event)">
</alfresco-upload-button>
</div>
<section>
<md-checkbox [(ngModel)]="enableUpload">Enable upload (demoing enabled/disabled state)</md-checkbox>
</section>
</div>
<file-uploading-dialog #fileDialog></file-uploading-dialog>
<div *ngIf="fileShowed">

View File

@ -15,39 +15,63 @@
* limitations under the License.
*/
import { Component, OnInit, Optional, ViewChild, ChangeDetectorRef } from '@angular/core';
import { Component, Input, OnInit, AfterViewInit, Optional, ViewChild, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { AlfrescoAuthenticationService, LogService } from 'ng2-alfresco-core';
import { AlfrescoAuthenticationService, LogService, NotificationService } from 'ng2-alfresco-core';
import { DocumentActionsService, DocumentListComponent, ContentActionHandler, DocumentActionModel, FolderActionModel } from 'ng2-alfresco-documentlist';
import { FormService } from 'ng2-activiti-form';
import { UploadButtonComponent, UploadDragAreaComponent } from 'ng2-alfresco-upload';
@Component({
selector: 'files-component',
templateUrl: './files.component.html',
styleUrls: ['./files.component.css']
})
export class FilesComponent implements OnInit {
export class FilesComponent implements OnInit, AfterViewInit {
// The identifier of a node. You can also use one of these well-known aliases: -my- | -shared- | -root-
currentFolderId: string = '-my-';
errorMessage: string = null;
fileNodeId: any;
fileShowed: boolean = false;
@Input()
multipleFileUpload: boolean = false;
@Input()
disableWithNoPermission: boolean = false;
@Input()
folderUpload: boolean = false;
@Input()
acceptedFilesTypeShow: boolean = false;
@Input()
versioning: boolean = false;
@Input()
acceptedFilesType: string = '.jpg,.pdf,.js';
@Input()
enableUpload: boolean = true;
@ViewChild(DocumentListComponent)
documentList: DocumentListComponent;
@ViewChild(UploadButtonComponent)
uploadButton: UploadButtonComponent;
@ViewChild(UploadDragAreaComponent)
uploadDragArea: UploadDragAreaComponent;
constructor(private documentActions: DocumentActionsService,
private authService: AlfrescoAuthenticationService,
private formService: FormService,
private logService: LogService,
private changeDetector: ChangeDetectorRef,
private router: Router,
private notificationService: NotificationService,
@Optional() private route: ActivatedRoute) {
documentActions.setHandler('my-handler', this.myDocumentActionHandler.bind(this));
}
@ -73,27 +97,12 @@ export class FilesComponent implements OnInit {
}
}
toggleMultipleFileUpload() {
this.multipleFileUpload = !this.multipleFileUpload;
return this.multipleFileUpload;
}
toggleFolder() {
this.multipleFileUpload = false;
this.folderUpload = !this.folderUpload;
return this.folderUpload;
}
toggleAcceptedFilesType() {
this.acceptedFilesTypeShow = !this.acceptedFilesTypeShow;
return this.acceptedFilesTypeShow;
}
toggleVersioning() {
this.versioning = !this.versioning;
return this.versioning;
}
ngOnInit() {
if (this.route) {
this.route.params.forEach((params: Params) => {
@ -113,6 +122,20 @@ export class FilesComponent implements OnInit {
}
}
ngAfterViewInit() {
this.uploadButton.onSuccess
.debounceTime(100)
.subscribe((event) => {
this.reload(event);
});
this.uploadDragArea.onSuccess
.debounceTime(100)
.subscribe((event) => {
this.reload(event);
});
}
viewActivitiForm(event?: any) {
this.router.navigate(['/activiti/tasksnode', event.value.entry.id]);
}
@ -146,4 +169,20 @@ export class FilesComponent implements OnInit {
window.alert(`Starting BPM process: ${processDefinition.id}`);
}.bind(this);
}
onPermissionsFailed(event: any) {
this.notificationService.openSnackMessage(`you don't have the ${event.permission} permission to ${event.action} the ${event.type} `, 4000);
}
onUploadPermissionFailed(event: any) {
this.notificationService.openSnackMessage(`you don't have the ${event.permission} permission to ${event.action} the ${event.type} `, 4000);
}
reload(event: any) {
if (event && event.value && event.value.entry && event.value.entry.parentId) {
if (this.documentList.currentFolderId === event.value.entry.parentId) {
this.documentList.reload();
}
}
}
}

View File

@ -24,6 +24,7 @@ export { ActivitiDemoComponent } from './activiti/activiti-demo.component';
export { FormViewer } from './activiti/form-viewer.component';
export { WebscriptComponent } from './webscript/webscript.component';
export { TagComponent } from './tag/tag.component';
export { SocialComponent } from './social/social.component';
export { AboutComponent } from './about/about.component';
export { FilesComponent } from './files/files.component';
export { FormNodeViewer } from './activiti/form-node-viewer.component';

View File

@ -29,3 +29,7 @@
.table-row {
display: table-row;
}
.adf-setting-input-padding{
padding-top: 0px !important;
}

View File

@ -10,18 +10,31 @@
Content Services host URL configuration
</div>
<nav class="mdl-navigation">
<div class="icon material-icons icon-margin">link</div>
<input type="text" class="mdl-textfield__input" id="ecmHost" data-automation-id="ecmHost"
tabindex="1" (change)="onChangeECMHost($event)" value="{{ecmHost}}"/>
<i class="icon material-icons icon-margin">link</i>
<div class="mdl-textfield mdl-js-textfield adf-setting-input-padding">
<input data-automation-id="ecmHost"
class="mdl-textfield__input" tabindex="1"
type="text" tabindex="1"
(change)="onChangeECMHost($event)"
pattern="^(http|https):\/\/.*" id="ecmHost" value="{{ecmHost}}">
<label class="mdl-textfield__label" for="ecmHost">ECM Host</label>
<span class="mdl-textfield__error">ECM host is not valid!</span>
</div>
</nav>
<div class="mdl-card__supporting-text">
Process Services host URL configuration
</div>
<nav class="mdl-navigation">
<div class="icon material-icons icon-margin">link</div>
<input type="text" class="mdl-textfield__input" id="bpmHost" data-automation-id="bpmHost"
tabindex="1" (change)="onChangeBPMHost($event)" value="{{bpmHost}}"/>
<i class="icon material-icons icon-margin">link</i>
<div class="mdl-textfield mdl-js-textfield adf-setting-input-padding">
<input class="mdl-textfield__input"
type="text"
(change)="onChangeBPMHost($event)"
tabindex="2" pattern="^(http|https):\/\/.*" id="bpmHost" value="{{bpmHost}}">
<label class="mdl-textfield__label" for="bpmHost">BPM Host</label>
<span class="mdl-textfield__error">BPM host is not valid!</span>
</div>
</nav>
</div>
<div class="mdl-card__actions mdl-card--border">

View File

@ -15,15 +15,17 @@
* limitations under the License.
*/
import { Component } from '@angular/core';
import { Component, AfterViewChecked } from '@angular/core';
import { AlfrescoSettingsService, StorageService, LogService } from 'ng2-alfresco-core';
declare var componentHandler: any;
@Component({
selector: 'alfresco-setting-demo',
templateUrl: './setting.component.html',
styleUrls: ['./setting.component.css']
})
export class SettingComponent {
export class SettingComponent implements AfterViewChecked {
ecmHost: string;
bpmHost: string;
@ -35,24 +37,39 @@ export class SettingComponent {
this.bpmHost = this.settingsService.bpmHost;
}
ngAfterViewChecked() {
// workaround for MDL issues with dynamic components
if (componentHandler) {
componentHandler.upgradeAllRegistered();
}
}
public onChangeECMHost(event: KeyboardEvent): void {
let value = (<HTMLInputElement>event.target).value.trim();
if (value) {
if (value && this.isValidUrl(value)) {
this.logService.info(`ECM host: ${value}`);
this.ecmHost = value;
this.settingsService.ecmHost = value;
this.storage.setItem(`ecmHost`, value);
} else {
console.error('Ecm address does not match the pattern');
}
}
public onChangeBPMHost(event: KeyboardEvent): void {
let value = (<HTMLInputElement>event.target).value.trim();
if (value) {
if (value && this.isValidUrl(value)) {
this.logService.info(`BPM host: ${value}`);
this.bpmHost = value;
this.settingsService.bpmHost = value;
this.storage.setItem(`bpmHost`, value);
} else {
console.error('Bpm address does not match the pattern');
}
}
isValidUrl(url: string) {
return /^(http|https):\/\/.*/.test(url);
}
}

View File

@ -0,0 +1,39 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component } from '@angular/core';
@Component({
selector: 'alfresco-social-demo',
template: `
<label for="nodeId"><b>Insert Node Id</b></label><br>
<input id="nodeId" type="text" size="48" [(ngModel)]="nodeId"><br>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--4-col">
Like component
<adf-like [nodeId]="nodeId"></adf-like></div>
<div class="mdl-cell mdl-cell--4-col">
Rating component
<adf-rating [nodeId]="nodeId"></adf-rating>
</div>
</div>
`
})
export class SocialComponent {
nodeId: string = '74cd8a96-8a21-47e5-9b3b-a1b3e296787d';
}

View File

@ -25,6 +25,7 @@ import 'ng2-alfresco-documentlist';
import 'ng2-alfresco-login';
import 'ng2-alfresco-search';
import 'ng2-alfresco-tag';
import 'ng2-alfresco-social';
import 'ng2-alfresco-upload';
import 'ng2-alfresco-viewer';
import 'ng2-alfresco-webscript';

View File

@ -1,11 +1,11 @@
{
"name": "Alfresco-Angular2-Demo",
"description": "Demo shell for Alfresco Angular2 components",
"version": "1.3.0",
"version": "1.4.0",
"author": "Alfresco Software, Ltd.",
"scripts": {
"clean": "npm install rimraf && npm run clean-build && rimraf dist node_modules typings dist",
"start": "npm run server-versions && webpack-dev-server --progress --max_old_space_size=4096 --max_new_space_size=4096",
"start": "npm run tslint && npm run server-versions && webpack-dev-server --progress --max_old_space_size=4096 --max_new_space_size=4096",
"start:dist": "wsrv -s dist/ -p 3000 -a 0.0.0.0",
"clean-build": "rimraf 'app/{,**/}**.js' 'app/{,**/}**.js.map' 'app/{,**/}**.d.ts'",
"test": "karma start",
@ -63,7 +63,7 @@
"@angular/platform-browser-dynamic": "2.2.2",
"@angular/router": "3.2.2",
"@angular/upgrade": "2.2.2",
"alfresco-js-api": "~1.3.0",
"alfresco-js-api": "~1.4.0",
"chart.js": "2.5.0",
"core-js": "2.4.1",
"dialog-polyfill": "0.4.7",
@ -75,20 +75,21 @@
"md-date-time-picker": "2.2.0",
"moment": "2.15.1",
"ng2-3d-editor": "0.0.15",
"ng2-activiti-analytics": "1.3.0",
"ng2-activiti-form": "1.3.0",
"ng2-activiti-processlist": "1.3.0",
"ng2-activiti-tasklist": "1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-alfresco-datatable": "1.3.0",
"ng2-alfresco-documentlist": "1.3.0",
"ng2-alfresco-login": "1.3.0",
"ng2-alfresco-search": "1.3.0",
"ng2-alfresco-tag": "1.3.0",
"ng2-alfresco-upload": "1.3.0",
"ng2-alfresco-userinfo": "1.3.0",
"ng2-alfresco-viewer": "1.3.0",
"ng2-alfresco-webscript": "1.3.0",
"ng2-activiti-analytics": "1.4.0",
"ng2-activiti-form": "1.4.0",
"ng2-activiti-processlist": "1.4.0",
"ng2-activiti-tasklist": "1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-alfresco-datatable": "1.4.0",
"ng2-alfresco-documentlist": "1.4.0",
"ng2-alfresco-login": "1.4.0",
"ng2-alfresco-search": "1.4.0",
"ng2-alfresco-tag": "1.4.0",
"ng2-alfresco-social": "1.3.0",
"ng2-alfresco-upload": "1.4.0",
"ng2-alfresco-userinfo": "1.4.0",
"ng2-alfresco-viewer": "1.4.0",
"ng2-alfresco-webscript": "1.4.0",
"ng2-charts": "1.5.0",
"ng2-translate": "2.5.0",
"pdfjs-dist": "1.5.404",

View File

@ -1,11 +0,0 @@
---
buildName: "adf2"
links:
Git: "https://github.com/Alfresco/alfresco-ng2-components.git"
Job: "http://192.168.64.3:31752/job/adf2"
Production: "http://10.0.0.114:80/kubernetes/pods?namespace=default-production"
Staging: "http://10.0.0.114:80/kubernetes/pods?namespace=default-staging"
environments:
Staging: "default-staging"
Production: "default-production"
useLocalFlow: true

View File

@ -59,10 +59,10 @@
"moment": "2.15.1",
"raphael": "^2.2.6",
"ng2-translate": "2.5.0",
"alfresco-js-api": "~1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-activiti-diagrams": "1.3.0",
"ng2-activiti-analytics": "1.3.0"
"alfresco-js-api": "~1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-activiti-diagrams": "1.4.0",
"ng2-activiti-analytics": "1.4.0"
},
"devDependencies": {
"@types/jasmine": "^2.2.33",

View File

@ -1,7 +1,7 @@
{
"name": "ng2-activiti-analytics",
"description": "Activiti Angular2 Analytics Component",
"version": "1.3.0",
"version": "1.4.0",
"author": "Alfresco Software, Ltd.",
"scripts": {
"clean": "npm install rimraf && npm run clean-build && rimraf dist node_modules typings",
@ -52,14 +52,14 @@
"@angular/platform-browser": "2.2.2",
"@angular/platform-browser-dynamic": "2.2.2",
"@angular/router": "3.2.2",
"alfresco-js-api": "~1.3.0",
"alfresco-js-api": "~1.4.0",
"chart.js": "2.5.0",
"core-js": "2.4.1",
"hammerjs": "2.0.8",
"md-date-time-picker": "2.2.0",
"moment": "2.15.1",
"ng2-activiti-diagrams": "1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-activiti-diagrams": "1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-charts": "1.5.0",
"ng2-translate": "2.5.0",
"raphael": "2.2.7",

View File

@ -428,6 +428,7 @@ describe('AnalyticsReportParametersComponent', () => {
});
describe('When the form is rendered correctly', () => {
let validForm: boolean = true;
let values: any = {
dateRange: {
startDate: '2016-09-01', endDate: '2016-10-05'
@ -468,11 +469,17 @@ describe('AnalyticsReportParametersComponent', () => {
fixture.whenStable().then(() => {
component.toggleParameters();
component.reportId = '1';
spyOn(component, 'isFormValid').and.returnValue(true);
spyOn(component, 'isFormValid').and.callFake(() => {
return validForm;
});
fixture.detectChanges();
});
}));
afterEach(() => {
validForm = true;
});
it('Should be able to change the report title', async(() => {
let title: HTMLElement = element.querySelector('h4');
title.click();
@ -567,6 +574,52 @@ describe('AnalyticsReportParametersComponent', () => {
contentType: 'json'
});
}));
it('Should hide export button if the form is not valid', async(() => {
let exportButton: HTMLButtonElement = <HTMLButtonElement>element.querySelector('#export-button');
expect(exportButton).toBeDefined();
expect(exportButton).not.toBeNull();
validForm = false;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
exportButton = <HTMLButtonElement>element.querySelector('#export-button');
expect(exportButton).toBeNull();
});
}));
it('Should hide save button if the form is not valid', async(() => {
let saveButton: HTMLButtonElement = <HTMLButtonElement>element.querySelector('#save-button');
expect(saveButton).toBeDefined();
expect(saveButton).not.toBeNull();
validForm = false;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
saveButton = <HTMLButtonElement>element.querySelector('#save-button');
expect(saveButton).toBeNull();
});
}));
it('Should show export and save button when the form became valid', async(() => {
validForm = false;
fixture.detectChanges();
let saveButton: HTMLButtonElement = <HTMLButtonElement>element.querySelector('#save-button');
let exportButton: HTMLButtonElement = <HTMLButtonElement>element.querySelector('#export-button');
expect(saveButton).toBeNull();
expect(exportButton).toBeNull();
validForm = true;
fixture.whenStable().then(() => {
fixture.detectChanges();
saveButton = <HTMLButtonElement>element.querySelector('#save-button');
exportButton = <HTMLButtonElement>element.querySelector('#export-button');
expect(saveButton).not.toBeNull();
expect(saveButton).toBeDefined();
expect(exportButton).not.toBeNull();
expect(exportButton).toBeDefined();
});
}));
});
});
});

View File

@ -25,9 +25,10 @@ import {
SimpleChanges,
OnDestroy,
AfterViewChecked,
AfterContentChecked,
ViewChild
} from '@angular/core';
import { FormGroup, FormBuilder, FormControl } from '@angular/forms';
import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms';
import * as moment from 'moment';
import { AlfrescoTranslationService, LogService, ContentService } from 'ng2-alfresco-core';
import { AnalyticsService } from '../services/analytics.service';
@ -47,7 +48,7 @@ declare let dialogPolyfill: any;
templateUrl: './analytics-report-parameters.component.html',
styleUrls: ['./analytics-report-parameters.component.css']
})
export class AnalyticsReportParametersComponent implements OnInit, OnChanges, OnDestroy, AfterViewChecked {
export class AnalyticsReportParametersComponent implements OnInit, OnChanges, OnDestroy, AfterViewChecked, AfterContentChecked {
public static FORMAT_DATE_ACTIVITI: string = 'YYYY-MM-DD';
@ -102,6 +103,7 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
private reportParamQuery: ReportQuery;
private reportName: string;
private hideParameters: boolean = true;
private formValidState: boolean = false;
constructor(private translateService: AlfrescoTranslationService,
private analyticsService: AnalyticsService,
@ -131,6 +133,9 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
ngOnChanges(changes: SimpleChanges) {
this.isEditable = false;
if (this.reportForm) {
this.reportForm.reset();
}
let reportId = changes['reportId'];
if (reportId && reportId.currentValue) {
this.getReportParams(reportId.currentValue);
@ -147,42 +152,42 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
parameters.forEach((param: ReportParameterDetailsModel) => {
switch (param.type) {
case 'dateRange' :
formBuilderGroup.dateRange = new FormGroup({});
formBuilderGroup.dateRange = new FormGroup({}, Validators.required);
break;
case 'processDefinition':
formBuilderGroup.processDefGroup = new FormGroup({
processDefinitionId: new FormControl()
});
processDefinitionId: new FormControl(null, Validators.required, null)
}, Validators.required);
break;
case 'duration':
formBuilderGroup.durationGroup = new FormGroup({
duration: new FormControl()
});
duration: new FormControl(null, Validators.required, null)
}, Validators.required);
break;
case 'dateInterval':
formBuilderGroup.dateIntervalGroup = new FormGroup({
dateRangeInterval: new FormControl()
});
dateRangeInterval: new FormControl(null, Validators.required, null)
}, Validators.required);
break;
case 'boolean':
formBuilderGroup.typeFilteringGroup = new FormGroup({
typeFiltering: new FormControl()
});
typeFiltering: new FormControl(null, Validators.required, null)
}, Validators.required);
break;
case 'task':
formBuilderGroup.taskGroup = new FormGroup({
taskName: new FormControl()
});
taskName: new FormControl(null, Validators.required, null)
}, Validators.required);
break;
case 'integer':
formBuilderGroup.processInstanceGroup = new FormGroup({
slowProcessInstanceInteger: new FormControl()
});
slowProcessInstanceInteger: new FormControl(null, Validators.required, null)
}, Validators.required);
break;
case 'status':
formBuilderGroup.statusGroup = new FormGroup({
status: new FormControl()
});
status: new FormControl(null, Validators.required, null)
}, Validators.required);
break;
default:
return;
@ -190,6 +195,7 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
});
this.reportForm = this.formBuilder.group(formBuilderGroup);
this.reportForm.valueChanges.subscribe(data => this.onValueChanged(data));
this.reportForm.statusChanges.subscribe(data => this.onStatusChanged(data));
}
public getReportParams(reportId: string) {
@ -243,6 +249,12 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
}
}
onStatusChanged(status: any) {
if (this.reportForm && !this.reportForm.pending && this.reportForm.dirty) {
this.formValidState = this.reportForm.valid;
}
}
public convertMomentDate(date: string) {
return moment(date, AnalyticsReportParametersComponent.FORMAT_DATE_ACTIVITI, true)
.format(AnalyticsReportParametersComponent.FORMAT_DATE_ACTIVITI) + 'T00:00:00.000Z';
@ -346,14 +358,14 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
this.reportName = '';
}
isFormValid() {
return this.reportForm && this.reportForm.valid && this.reportForm.dirty;
}
isSaveAction() {
return this.action === 'Save';
}
isFormValid() {
return this.reportForm && this.reportForm.dirty && this.reportForm.valid;
}
doExport(paramQuery: ReportQuery) {
this.analyticsService.exportReportToCsv(this.reportId, paramQuery).subscribe(
(data: any) => {
@ -375,12 +387,17 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
}
ngAfterViewChecked() {
// workaround for MDL issues with dynamic components
if (componentHandler) {
componentHandler.upgradeAllRegistered();
}
}
ngAfterContentChecked() {
if (this.reportForm && this.reportForm.valid) {
this.reportForm.markAsDirty();
}
}
toggleParameters() {
this.hideParameters = !this.hideParameters;
}

View File

@ -43,15 +43,15 @@
"@angular/material": "2.0.0-beta.1",
"@angular/router": "3.2.2",
"@angular/upgrade": "2.2.2",
"alfresco-js-api": "~1.3.0",
"alfresco-js-api": "~1.4.0",
"core-js": "2.4.1",
"dialog-polyfill": "0.4.7",
"element.scrollintoviewifneeded-polyfill": "1.0.1",
"intl": "1.2.4",
"material-design-icons": "2.2.3",
"material-design-lite": "1.2.1",
"ng2-activiti-diagrams": "1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-activiti-diagrams": "1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-translate": "2.5.0",
"raphael": "^2.2.6",
"reflect-metadata": "0.1.10",

View File

@ -1,7 +1,7 @@
{
"name": "ng2-activiti-diagrams",
"description": "Activiti Angular2 Diagrams Component",
"version": "1.3.0",
"version": "1.4.0",
"author": "Alfresco Software, Ltd.",
"scripts": {
"clean": "npm install rimraf && npm run clean-build && rimraf dist node_modules typings",
@ -48,10 +48,10 @@
"@angular/platform-browser": "2.2.2",
"@angular/platform-browser-dynamic": "2.2.2",
"@angular/router": "3.2.2",
"alfresco-js-api": "~1.3.0",
"alfresco-js-api": "~1.4.0",
"core-js": "2.4.1",
"hammerjs": "2.0.8",
"ng2-alfresco-core": "1.3.0",
"ng2-alfresco-core": "1.4.0",
"ng2-translate": "2.5.0",
"raphael": "^2.2.6",
"reflect-metadata": "0.1.10",

View File

@ -438,10 +438,33 @@ class MyComponent {
| getRestFieldValuesByProcessId | (processDefinitionId: string, field: string) | Observable\<any\> | |
| getRestFieldValuesColumnByProcessId | (processDefinitionId: string, field: string, column?: string) | Observable\<any\> | |
| getRestFieldValuesColumn | (taskId: string, field: string, column?: string) | Observable\<any\> | |
| getWorkflowGroups\* | (filter: string, groupId?: string) | Observable\<GroupModel[]\> | |
| getWorkflowUsers\* | (filter: string, groupId?: string) | Observable\<GroupUserModel[]\> | |
| getWorkflowGroups\ | (filter: string, groupId?: string) | Observable\<GroupModel[]\> | |
| getWorkflowUsers\ | (filter: string, groupId?: string) | Observable\<GroupUserModel[]\> | |
\* _Uses private Activiti WebApp api_
## Common scenarios
### Changing field value based on another field
Create a simple Form with a dropdown widget (id: `type`), and a multiline text (id: `description`).
```ts
formService.formFieldValueChanged.subscribe((e: FormFieldEvent) => {
if (e.field.id === 'type') {
const fields: FormFieldModel[] = e.form.getFormFields();
const description = fields.find(f => f.id === 'description');
if (description != null) {
console.log(description);
description.value = 'Type set to ' + e.field.value;
}
}
});
```
You subscribe to the `formFieldValueChanged` event and check whether event is raised for the `type` widget, then you search for a `description` widget and assign its value to some simple text.
The result should be as following:
![](docs/assets/form-service-sample-01.png)
## See also

View File

@ -56,9 +56,9 @@
"moment": "2.15.1",
"md-date-time-picker": "2.2.0",
"ng2-translate": "2.5.0",
"alfresco-js-api": "~1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-activiti-form": "1.3.0"
"alfresco-js-api": "~1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-activiti-form": "1.4.0"
},
"devDependencies": {
"@types/jasmine": "^2.2.33",

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,7 +1,7 @@
{
"name": "ng2-activiti-form",
"description": "Alfresco Activiti Form Component for Angular 2",
"version": "1.3.0",
"version": "1.4.0",
"author": "Alfresco Software, Ltd.",
"scripts": {
"clean": "npm install rimraf && npm run clean-build && rimraf dist node_modules typings",
@ -55,12 +55,12 @@
"@angular/platform-browser": "2.2.2",
"@angular/platform-browser-dynamic": "2.2.2",
"@angular/router": "3.2.2",
"alfresco-js-api": "~1.3.0",
"alfresco-js-api": "~1.4.0",
"core-js": "2.4.1",
"hammerjs": "2.0.8",
"md-date-time-picker": "2.2.0",
"moment": "2.15.1",
"ng2-alfresco-core": "1.3.0",
"ng2-alfresco-core": "1.4.0",
"ng2-translate": "2.5.0",
"reflect-metadata": "0.1.10",
"rxjs": "5.0.0-beta.12",

View File

@ -26,7 +26,6 @@ import { FormEvent, FormErrorEvent } from './../events/index';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
declare let dialogPolyfill: any;
declare var componentHandler: any;
/**
@ -118,7 +117,7 @@ export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges {
showSaveButton: boolean = true;
@Input()
showDebugButton: boolean = true;
showDebugButton: boolean = false;
@Input()
readOnly: boolean = false;

View File

@ -121,6 +121,109 @@ describe('FormFieldModel', () => {
expect(field.value).toBe('deferred');
});
it('should parse the date with the default format (D-M-YYYY) if the display format is missing', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
fieldType: 'FormFieldRepresentation',
id: 'mmddyyyy',
name: 'MM-DD-YYYY',
type: 'date',
value: '2017-04-28T00:00:00.000+0000',
required: false,
readOnly: false,
params: {
field: {
id: 'mmddyyyy',
name: 'MM-DD-YYYY',
type: 'date',
value: null,
required: false,
readOnly: false
}
}
});
expect(field.value).toBe('28-4-2017');
expect(form.values['mmddyyyy']).toEqual('2017-04-28T00:00:00.000Z');
});
it('should parse the date with the format MM-DD-YYYY', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
fieldType: 'FormFieldRepresentation',
id: 'mmddyyyy',
name: 'MM-DD-YYYY',
type: 'date',
value: '2017-04-28T00:00:00.000+0000',
required: false,
readOnly: false,
params: {
field: {
id: 'mmddyyyy',
name: 'MM-DD-YYYY',
type: 'date',
value: null,
required: false,
readOnly: false
}
},
dateDisplayFormat: 'MM-DD-YYYY'
});
expect(field.value).toBe('04-28-2017');
expect(form.values['mmddyyyy']).toEqual('2017-04-28T00:00:00.000Z');
});
it('should parse the date with the format MM-YY-DD', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
fieldType: 'FormFieldRepresentation',
id: 'mmyydd',
name: 'MM-YY-DD',
type: 'date',
value: '2017-04-28T00:00:00.000+0000',
required: false,
readOnly: false,
params: {
field: {
id: 'mmyydd',
name: 'MM-YY-DD',
type: 'date',
value: null,
required: false,
readOnly: false
}
},
dateDisplayFormat: 'MM-YY-DD'
});
expect(field.value).toBe('04-17-28');
expect(form.values['mmyydd']).toEqual('2017-04-28T00:00:00.000Z');
});
it('should parse the date with the format DD-MM-YYYY', () => {
let form = new FormModel();
let field = new FormFieldModel(form, {
fieldType: 'FormFieldRepresentation',
id: 'ddmmyyy',
name: 'DD-MM-YYYY',
type: 'date',
value: '2017-04-28T00:00:00.000+0000',
required: false,
readOnly: false,
params: {
field: {
id: 'ddmmyyy',
name: 'DD-MM-YYYY',
type: 'date',
value: null,
required: false,
readOnly: false
}
},
dateDisplayFormat: 'DD-MM-YYYY'
});
expect(field.value).toBe('28-04-2017');
expect(form.values['ddmmyyy']).toEqual('2017-04-28T00:00:00.000Z');
});
it('should return the label of selected dropdown value ', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,

View File

@ -76,7 +76,7 @@ export class FormFieldModel extends FormWidgetModel {
visibilityCondition: WidgetVisibilityModel = null;
enableFractions: boolean = false;
currency: string = null;
dateDisplayFormat: string = this.defaultDateFormat;
dateDisplayFormat: string = this.dateDisplayFormat || this.defaultDateFormat;
// container model members
numberOfColumns: number = 1;
@ -249,9 +249,14 @@ export class FormFieldModel extends FormWidgetModel {
*/
if (json.type === FormFieldTypes.DATE) {
if (value) {
let d = moment(value.split('T')[0], 'YYYY-M-D');
if (d.isValid()) {
value = d.format(this.dateDisplayFormat);
let dateValue;
if (NumberFieldValidator.isNumber(value)) {
dateValue = moment(value);
} else {
dateValue = moment(value.split('T')[0], 'YYYY-M-D');
}
if (dateValue && dateValue.isValid()) {
value = dateValue.format(this.dateDisplayFormat);
}
}
}
@ -307,9 +312,14 @@ export class FormFieldModel extends FormWidgetModel {
}
break;
case FormFieldTypes.DATE:
let d = moment(this.value, this.dateDisplayFormat);
if (d.isValid()) {
this.form.values[this.id] = `${d.format('YYYY-MM-DD')}T00:00:00.000Z`;
let dateValue;
if (NumberFieldValidator.isNumber(this.value)) {
dateValue = moment(this.value);
} else {
dateValue = moment(this.value, this.dateDisplayFormat);
}
if (dateValue && dateValue.isValid()) {
this.form.values[this.id] = `${dateValue.format('YYYY-MM-DD')}T00:00:00.000Z`;
} else {
this.form.values[this.id] = null;
}

View File

@ -42,30 +42,7 @@
</div>
</div>
<div *ngSwitchCase="'dynamic-table'">
<div class="display-value-widget__dynamic-table">
<div>{{field.name}}</div>
<div class="display-value-dynamic-table-widget__table-container">
<table class="mdl-data-table mdl-js-data-table">
<thead>
<tr>
<th *ngFor="let column of visibleColumns"
class="mdl-data-table__cell--non-numeric is-disabled">
{{column.name}}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of rows">
<td *ngFor="let column of visibleColumns"
class="mdl-data-table__cell--non-numeric is-disabled">
{{ getCellValue(row, column) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<dynamic-table-widget [field]="field" [readOnly]="!tableEditable"></dynamic-table-widget>
</div>
<div *ngSwitchCase="'upload'">
<div *ngIf="hasFile" class="mdl-grid">

View File

@ -15,6 +15,7 @@
* limitations under the License.
*/
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { CoreModule, LogServiceMock } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx';
@ -25,7 +26,6 @@ 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 { DynamicTableColumn, DynamicTableRow } from './../dynamic-table/dynamic-table.widget.model';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
describe('DisplayValueWidget', () => {
@ -441,6 +441,65 @@ describe('DisplayValueWidget', () => {
expect(widget.value).toBe('<invalid value>');
});
it('should show the [DATE] field with the default format (D-M-YYYY) if the display format is missing', () => {
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.DISPLAY_VALUE,
value: '1982-03-13T00:00:00.000Z',
params: {
field: {
type: FormFieldTypes.DATE
}
}
});
widget.ngOnInit();
expect(widget.value).toBe('13-3-1982');
});
it('should show the [DATE] field with the custom display format (MM-DD-YYYY)', () => {
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.DISPLAY_VALUE,
value: '1982-03-13T00:00:00.000Z',
dateDisplayFormat: 'MM-DD-YYYY',
params: {
field: {
type: FormFieldTypes.DATE
}
}
});
widget.ngOnInit();
expect(widget.value).toBe('03-13-1982');
});
it('should show the [DATE] field with the custom display format (MM-YY-DD)', () => {
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.DISPLAY_VALUE,
value: '1982-03-13T00:00:00.000Z',
dateDisplayFormat: 'MM-YY-DD',
params: {
field: {
type: FormFieldTypes.DATE
}
}
});
widget.ngOnInit();
expect(widget.value).toBe('03-82-13');
});
it('should show the [DATE] field with the custom display format (DD-MM-YYYY)', () => {
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.DISPLAY_VALUE,
value: '1982-03-13T00:00:00.000Z',
dateDisplayFormat: 'DD-MM-YYYY',
params: {
field: {
type: FormFieldTypes.DATE
}
}
});
widget.ngOnInit();
expect(widget.value).toBe('13-03-1982');
});
it('should not setup [DATE] field when missing value', () => {
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.DISPLAY_VALUE,
@ -549,135 +608,6 @@ describe('DisplayValueWidget', () => {
expect(widget.value).toBe(value);
});
it('should setup [DYNAMIC_TABLE] field', () => {
let columns = [{id: '1', visible: false}, {id: '2', visible: true}];
let rows = [{}, {}];
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.DISPLAY_VALUE,
params: {
field: {
type: FormFieldTypes.DYNAMIC_TABLE
}
},
columnDefinitions: columns,
value: rows
});
widget.ngOnInit();
expect(widget.columns.length).toBe(2);
expect(widget.columns[0].id).toBe(columns[0].id);
expect(widget.columns[1].id).toBe(columns[1].id);
expect(widget.visibleColumns.length).toBe(1);
expect(widget.visibleColumns[0].id).toBe(columns[1].id);
expect(widget.rows.length).toBe(2);
});
it('should setup [DYNAMIC_TABLE] field with empty schema', () => {
widget.field = new FormFieldModel(null, {
type: FormFieldTypes.DISPLAY_VALUE,
params: {
field: {
type: FormFieldTypes.DYNAMIC_TABLE
}
},
columnDefinitions: null,
value: null
});
widget.ngOnInit();
expect(widget.value).toBeNull();
expect(widget.columns).toEqual([]);
expect(widget.rows).toEqual([]);
});
it('should retrieve default cell value', () => {
const value = '<value>';
let row = <DynamicTableRow> {value: {key: value}};
let column = <DynamicTableColumn> {id: 'key'};
expect(widget.getCellValue(row, column)).toBe(value);
});
it('should retrieve dropdown cell value', () => {
const value = {id: '1', name: 'one'};
let row = <DynamicTableRow> {value: {key: value}};
let column = <DynamicTableColumn> {id: 'key', type: 'Dropdown'};
expect(widget.getCellValue(row, column)).toBe(value.name);
});
it('should fallback to empty cell value for dropdown', () => {
let row = <DynamicTableRow> {value: {}};
let column = <DynamicTableColumn> {id: 'key', type: 'Dropdown'};
expect(widget.getCellValue(row, column)).toBe('');
});
it('should retrieve boolean cell value', () => {
let row1 = <DynamicTableRow> {value: {key: true}};
let row2 = <DynamicTableRow> {value: {key: 'positive'}};
let row3 = <DynamicTableRow> {value: {key: null}};
let column = <DynamicTableColumn> {id: 'key', type: 'Boolean'};
expect(widget.getCellValue(row1, column)).toBe(true);
expect(widget.getCellValue(row2, column)).toBe(true);
expect(widget.getCellValue(row3, column)).toBe(false);
});
it('should retrieve date cell value', () => {
const value = '2016-10-04T00:00:00.000Z';
let row = <DynamicTableRow> {value: {key: value}};
let column = <DynamicTableColumn> {id: 'key', type: 'Date'};
expect(widget.getCellValue(row, column)).toBe('4-10-2016');
});
it('should fallback to empty cell value for date', () => {
let row = <DynamicTableRow> {value: {}};
let column = <DynamicTableColumn> {id: 'key', type: 'Date'};
expect(widget.getCellValue(row, column)).toBe('');
});
it('should retrieve empty text cell value', () => {
let row = <DynamicTableRow> {value: {}};
let column = <DynamicTableColumn> {id: 'key'};
expect(widget.getCellValue(row, column)).toBe('');
});
it('should prepend default amount currency', () => {
const value = '10';
let row = <DynamicTableRow> {value: {key: value}};
let column = <DynamicTableColumn> {id: 'key', type: 'Amount'};
const expected = `$ ${value}`;
expect(widget.getCellValue(row, column)).toBe(expected);
});
it('should prepend custom amount currency', () => {
const value = '10';
const currency = 'GBP';
let row = <DynamicTableRow> {value: {key: value}};
let column = <DynamicTableColumn> {id: 'key', type: 'Amount', amountCurrency: currency};
const expected = `${currency} ${value}`;
expect(widget.getCellValue(row, column)).toBe(expected);
});
it('should use zero for missing amount', () => {
const value = null;
const currency = 'GBP';
let row = <DynamicTableRow> {value: {key: value}};
let column = <DynamicTableColumn> {id: 'key', type: 'Amount', amountCurrency: currency};
const expected = `${currency} 0`;
expect(widget.getCellValue(row, column)).toBe(expected);
});
describe('UI check', () => {
let widgetUI: DisplayValueWidget;
let fixture: ComponentFixture<DisplayValueWidget>;
@ -689,12 +619,16 @@ describe('DisplayValueWidget', () => {
window['componentHandler'] = componentHandler;
TestBed.configureTestingModule({
imports: [CoreModule],
declarations: [DisplayValueWidget, ActivitiContent],
declarations: [
DisplayValueWidget,
ActivitiContent
],
providers: [
EcmModelService,
FormService,
WidgetVisibilityService
]
],
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(DisplayValueWidget);
widgetUI = fixture.componentInstance;

View File

@ -22,8 +22,8 @@ import { WidgetComponent } from './../widget.component';
import { FormFieldTypes } from '../core/form-field-types';
import { FormService } from '../../../services/form.service';
import { FormFieldOption } from './../core/form-field-option';
import { DynamicTableColumn, DynamicTableRow } from './../dynamic-table/dynamic-table.widget.model';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { NumberFieldValidator } from '../core/form-field-validator';
@Component({
moduleId: module.id,
@ -42,9 +42,7 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
linkText: string;
// dynamic table
rows: DynamicTableRow[] = [];
columns: DynamicTableColumn[] = [];
visibleColumns: DynamicTableColumn[] = [];
tableEditable = false;
// upload/attach
hasFile: boolean = false;
@ -64,6 +62,10 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
if (this.field.params['showDocumentContent'] !== undefined) {
this.showDocumentContent = !!this.field.params['showDocumentContent'];
}
if (this.field.params['tableEditable'] !== undefined) {
this.tableEditable = !!this.field.params['tableEditable'];
}
let originalField = this.field.params['field'];
if (originalField && originalField.type) {
this.fieldType = originalField.type;
@ -115,10 +117,15 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
break;
case FormFieldTypes.DATE:
if (this.value) {
let d = moment(this.value.split('T')[0], 'YYYY-M-D');
if (d.isValid()) {
const displayFormat = originalField['dateDisplayFormat'] || this.field.defaultDateFormat;
this.value = d.format(displayFormat);
let dateValue;
if (NumberFieldValidator.isNumber(this.value)) {
dateValue = moment(this.value);
} else {
dateValue = moment(this.value.split('T')[0], 'YYYY-M-D');
}
if (dateValue && dateValue.isValid()) {
const displayFormat = this.field.dateDisplayFormat || this.field.defaultDateFormat;
this.value = dateValue.format(displayFormat);
}
}
break;
@ -132,16 +139,6 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
this.linkUrl = this.getHyperlinkUrl(this.field);
this.linkText = this.getHyperlinkText(this.field);
break;
case FormFieldTypes.DYNAMIC_TABLE:
let json = this.field.json;
if (json.columnDefinitions) {
this.columns = json.columnDefinitions.map(obj => <DynamicTableColumn> obj);
this.visibleColumns = this.columns.filter(col => col.visible);
}
if (json.value) {
this.rows = json.value.map(obj => <DynamicTableRow> {selected: false, value: obj});
}
break;
default:
this.value = this.field.value;
break;
@ -216,31 +213,4 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
}
);
}
getCellValue(row: DynamicTableRow, column: DynamicTableColumn): any {
let result = row.value[column.id];
if (column.type === 'Dropdown') {
if (result) {
return result.name;
}
}
if (column.type === 'Boolean') {
return result ? true : false;
}
if (column.type === 'Date') {
if (result) {
return moment(result.split('T')[0], 'YYYY-MM-DD').format('D-M-YYYY');
}
}
if (column.type === 'Amount') {
return (column.amountCurrency || '$') + ' ' + (result || 0);
}
return result || '';
}
}

View File

@ -1,9 +1,9 @@
<div class="dynamic-table-widget {{field.className}}"
[class.dynamic-table-widget__invalid]="!isValid()" *ngIf="field?.isVisible">
<div class="dynamic-table-widget__label">{{content.name}}</div>
<div class="dynamic-table-widget__label">{{content.name}}</div>
<div *ngIf="!editMode">
<div class="dynamic-table-widget__table-container">
<div *ngIf="!editMode">
<div class="dynamic-table-widget__table-container">
<table class="mdl-data-table mdl-js-data-table dynamic-table-widget__table">
<thead>
<tr>
@ -24,9 +24,9 @@
</tr>
</tbody>
</table>
</div>
</div>
<div class="dynamic-table-widget__buttons">
<div class="dynamic-table-widget__buttons" *ngIf="!readOnly">
<button class="mdl-button mdl-js-button mdl-button--icon"
[disabled]="!hasSelection()"
(click)="moveSelectionUp()">

View File

@ -54,9 +54,9 @@ export class DynamicTableModel extends FormWidgetModel {
this.field = field;
if (field.json) {
if (field.json.columnDefinitions) {
this.columns = field.json.columnDefinitions.map(obj => <DynamicTableColumn> obj);
const columns = this.getColumns(field);
if (columns) {
this.columns = columns;
this.visibleColumns = this.columns.filter(col => col.visible);
}
@ -72,6 +72,20 @@ export class DynamicTableModel extends FormWidgetModel {
];
}
private getColumns(field: FormFieldModel): DynamicTableColumn[] {
if (field && field.json) {
let definitions = field.json.columnDefinitions;
if (!definitions && field.json.params && field.json.params.field) {
definitions = field.json.params.field.columnDefinitions;
}
if (definitions) {
return definitions.map(obj => <DynamicTableColumn> obj);
}
}
return null;
}
flushValue() {
if (this.field) {
this.field.value = this.rows.map(r => r.value);

View File

@ -15,11 +15,12 @@
* limitations under the License.
*/
import { Component, ElementRef, OnInit } from '@angular/core';
import { Component, ElementRef, OnInit, Input } from '@angular/core';
import { LogService } from 'ng2-alfresco-core';
import { WidgetComponent } from './../widget.component';
import { DynamicTableModel, DynamicTableRow, DynamicTableColumn } from './dynamic-table.widget.model';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { FormFieldModel } from '../core/form-field.model';
@Component({
moduleId: module.id,
@ -31,6 +32,12 @@ export class DynamicTableWidget extends WidgetComponent implements OnInit {
ERROR_MODEL_NOT_FOUND = 'Table model not found';
@Input()
field: FormFieldModel;
@Input()
readOnly: boolean = false;
content: DynamicTableModel;
editMode: boolean = false;
@ -70,7 +77,7 @@ export class DynamicTableWidget extends WidgetComponent implements OnInit {
}
moveSelectionUp(): boolean {
if (this.content) {
if (this.content && !this.readOnly) {
this.content.moveRow(this.content.selectedRow, -1);
return true;
}
@ -78,7 +85,7 @@ export class DynamicTableWidget extends WidgetComponent implements OnInit {
}
moveSelectionDown(): boolean {
if (this.content) {
if (this.content && !this.readOnly) {
this.content.moveRow(this.content.selectedRow, 1);
return true;
}
@ -86,7 +93,7 @@ export class DynamicTableWidget extends WidgetComponent implements OnInit {
}
deleteSelection(): boolean {
if (this.content) {
if (this.content && !this.readOnly) {
this.content.deleteRow(this.content.selectedRow);
return true;
}
@ -94,7 +101,7 @@ export class DynamicTableWidget extends WidgetComponent implements OnInit {
}
addNewRow(): boolean {
if (this.content) {
if (this.content && !this.readOnly) {
this.editRow = <DynamicTableRow> {
isNew: true,
selected: false,
@ -107,7 +114,7 @@ export class DynamicTableWidget extends WidgetComponent implements OnInit {
}
editSelection(): boolean {
if (this.content) {
if (this.content && !this.readOnly) {
this.editRow = this.copyRow(this.content.selectedRow);
this.editMode = true;
return true;

View File

@ -105,7 +105,7 @@ export class FormService {
return forms.data.find(formdata => formdata.name === name);
})
.catch(err => this.handleError(err)
);
);
}
/**
@ -158,7 +158,7 @@ export class FormService {
* @returns {Observable<any>}
*/
saveTaskForm(taskId: string, formValues: FormValues): Observable<any> {
let body = JSON.stringify({values: formValues});
let body = JSON.stringify({ values: formValues });
return Observable.fromPromise(this.apiService.getInstance().activiti.taskApi.saveTaskForm(taskId, body))
.catch(err => this.handleError(err));
@ -172,7 +172,7 @@ export class FormService {
* @returns {Observable<any>}
*/
completeTaskForm(taskId: string, formValues: FormValues, outcome?: string): Observable<any> {
let data: any = {values: formValues};
let data: any = { values: formValues };
if (outcome) {
data.outcome = outcome;
}
@ -294,67 +294,34 @@ export class FormService {
return Observable.fromPromise(alfrescoApi.activiti.taskApi.getRestFieldValuesColumn(taskId, field, column));
}
// TODO: uses private webApp api
getWorkflowGroups(filter: string, groupId?: string): Observable<GroupModel[]> {
return Observable.create(observer => {
let xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let json = JSON.parse(xhr.response);
let data: GroupModel[] = (json.data || []).map(item => <GroupModel> item);
observer.next(data);
observer.complete();
} else {
this.logService.error(xhr.response);
Observable.throw(new Error(xhr.response));
}
}
};
let host = this.apiService.getInstance().config.hostBpm;
let url = `${host}/activiti-app/app/rest/workflow-groups?filter=${filter}`;
if (groupId) {
url += `&groupId=${groupId}`;
}
xhr.open('GET', url, true);
xhr.setRequestHeader('Authorization', this.apiService.getInstance().getTicketBpm());
xhr.send();
});
getWorkflowUsers(filter: string, groupId?: string): Observable<GroupUserModel[]> {
let option: any = { filter: filter };
if (groupId) {
option.groupId = groupId;
}
return Observable.fromPromise(this.getWorkflowUserApi(option))
.map((response: any) => <GroupUserModel[]> response.data || [])
.catch(err => this.handleError(err));
}
getWorkflowUsers(filter: string, groupId?: string): Observable<GroupUserModel[]> {
return Observable.create(observer => {
private getWorkflowUserApi(options: any) {
let alfrescoApi = this.apiService.getInstance();
return alfrescoApi.activiti.usersWorkflowApi.getUsers(options);
}
let xhr: XMLHttpRequest = new XMLHttpRequest();
xhr.withCredentials = true;
getWorkflowGroups(filter: string, groupId?: string): Observable<GroupModel[]> {
let option: any = { filter: filter };
if (groupId) {
option.groupId = groupId;
}
return Observable.fromPromise(this.getWorkflowGroupsApi(option))
.map((response: any) => <GroupModel[]> response.data || [])
.catch(err => this.handleError(err));
}
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let json = JSON.parse(xhr.response);
let data: GroupUserModel[] = (json.data || []).map(item => <GroupUserModel> item);
observer.next(data);
observer.complete();
} else {
this.logService.error(xhr.response);
Observable.throw(new Error(xhr.response));
}
}
};
let host = this.apiService.getInstance().config.hostBpm;
let url = `${host}/activiti-app/app/rest/workflow-users?filter=${filter}`;
if (groupId) {
url += `&groupId=${groupId}`;
}
xhr.open('GET', url, true);
xhr.setRequestHeader('Authorization', this.apiService.getInstance().getTicketBpm());
xhr.send();
});
private getWorkflowGroupsApi(options: any) {
let alfrescoApi = this.apiService.getInstance();
return alfrescoApi.activiti.groupsApi.getGroups(options);
}
getFormId(res: any): string {

View File

@ -248,6 +248,25 @@ If both `appId` and `appName` are specified then `appName` will take precedence
| `onError` | Emitted when an error occurs |
| `ilterClick` | Emitted when the user selects a filter from the list |
### How to create an accordion menu with the processes filter
You can create an accordion menu using the AccordionComponent that wrap the activiti task filter.
The AccordionComponent is exposed by the alfresco-core.
```html
<adf-accordion>
<adf-accordion-group [heading]="'Processes'" [isSelected]="true" [headingIcon]="'assessment'">
<activiti-process-instance-filters
[appId]="appId"
(filterClick)="onProcessFilterClick($event)"
(onSuccess)="onSuccessProcessFilterList($event)">
</activiti-process-instance-filters>
</adf-accordion-group>
</adf-accordion>
```
![how-create-accordion-menu](docs/assets/how-to-create-accordion-menu.png)
### Start Process Button component
Displays a button which in turn displays a dialog when clicked, allowing the user

View File

@ -49,11 +49,11 @@
"moment": "2.15.1",
"md-date-time-picker": "2.2.0",
"ng2-translate": "2.5.0",
"alfresco-js-api": "~1.3.0",
"ng2-activiti-tasklist": "1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-alfresco-datatable": "1.3.0",
"ng2-activiti-processlist": "1.3.0"
"alfresco-js-api": "~1.4.0",
"ng2-activiti-tasklist": "1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-alfresco-datatable": "1.4.0",
"ng2-activiti-processlist": "1.4.0"
},
"devDependencies": {
"@types/jasmine": "^2.2.33",

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,7 +1,7 @@
{
"name": "ng2-activiti-processlist",
"description": "Show active processes from the Activiti Process Services suite",
"version": "1.3.0",
"version": "1.4.0",
"author": "Alfresco Software, Ltd.",
"scripts": {
"clean": "npm install rimraf && npm run clean-build && rimraf dist node_modules typings",
@ -54,15 +54,15 @@
"@angular/platform-browser": "2.2.2",
"@angular/platform-browser-dynamic": "2.2.2",
"@angular/router": "3.2.2",
"alfresco-js-api": "~1.3.0",
"alfresco-js-api": "~1.4.0",
"core-js": "2.4.1",
"hammerjs": "2.0.8",
"md-date-time-picker": "2.2.0",
"moment": "2.15.1",
"ng2-activiti-form": "1.3.0",
"ng2-activiti-tasklist": "1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-alfresco-datatable": "1.3.0",
"ng2-activiti-form": "1.4.0",
"ng2-activiti-tasklist": "1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-alfresco-datatable": "1.4.0",
"ng2-translate": "2.5.0",
"reflect-metadata": "0.1.10",
"rxjs": "5.0.0-beta.12",

View File

@ -18,17 +18,17 @@
import {
AppDefinitionRepresentationModel,
Comment,
FilterRepresentationModel,
TaskDetailsModel,
User
} from 'ng2-activiti-tasklist';
import { ProcessDefinitionRepresentation } from '../models/index';
import { ProcessDefinitionRepresentation, FilterProcessRepresentationModel } from '../models/index';
export var fakeFilters = {
size: 1, total: 1, start: 0,
data: [new FilterRepresentationModel({
data: [new FilterProcessRepresentationModel({
'name': 'Running',
'appId': '22',
'id': '333',
'recent': true,
'icon': 'glyphicon-random',
'filter': {'sort': 'created-desc', 'name': '', 'state': 'running'}

View File

@ -14,6 +14,10 @@
color: rgb(68,138,255);
}
.activiti-filters__entry:hover {
opacity: 0.8;
}
.activiti-filters__entry.active .activiti-filters__entry-icon {
color: rgb(68,138,255);
}
}

View File

@ -20,7 +20,7 @@ import { Observable } from 'rxjs/Rx';
import { LogServiceMock } from 'ng2-alfresco-core';
import { ActivitiProcessFilters } from './activiti-filters.component';
import { ActivitiProcessService } from '../services/activiti-process.service';
import { FilterRepresentationModel } from 'ng2-activiti-tasklist';
import { FilterProcessRepresentationModel } from '../models/filter-process.model';
describe('ActivitiFilters', () => {
@ -29,8 +29,8 @@ describe('ActivitiFilters', () => {
let logService: LogServiceMock;
let fakeGlobalFilter = [];
fakeGlobalFilter.push(new FilterRepresentationModel({name: 'FakeInvolvedTasks', filter: { state: 'open', assignment: 'fake-involved'}}));
fakeGlobalFilter.push(new FilterRepresentationModel({name: 'FakeMyTasks', filter: { state: 'open', assignment: 'fake-assignee'}}));
fakeGlobalFilter.push(new FilterProcessRepresentationModel({name: 'FakeInvolvedTasks', filter: { state: 'open', assignment: 'fake-involved'}}));
fakeGlobalFilter.push(new FilterProcessRepresentationModel({name: 'FakeMyTasks', filter: { state: 'open', assignment: 'fake-assignee'}}));
let fakeGlobalFilterPromise = new Promise(function (resolve, reject) {
resolve(fakeGlobalFilter);
@ -121,9 +121,9 @@ describe('ActivitiFilters', () => {
});
it('should emit an event when a filter is selected', (done) => {
let currentFilter = new FilterRepresentationModel({filter: { state: 'open', assignment: 'fake-involved'}});
let currentFilter = new FilterProcessRepresentationModel({filter: { state: 'open', assignment: 'fake-involved'}});
filterList.filterClick.subscribe((filter: FilterRepresentationModel) => {
filterList.filterClick.subscribe((filter: FilterProcessRepresentationModel) => {
expect(filter).toBeDefined();
expect(filter).toEqual(currentFilter);
expect(filterList.currentFilter).toEqual(currentFilter);
@ -164,7 +164,7 @@ describe('ActivitiFilters', () => {
});
it('should return the current filter after one is selected', () => {
let filter = new FilterRepresentationModel({name: 'FakeMyTasks', filter: { state: 'open', assignment: 'fake-assignee'}});
let filter = new FilterProcessRepresentationModel({name: 'FakeMyTasks', filter: { state: 'open', assignment: 'fake-assignee'}});
expect(filterList.currentFilter).toBeUndefined();
filterList.selectFilter(filter);
expect(filterList.getCurrentFilter()).toBe(filter);

View File

@ -160,7 +160,7 @@ export class ActivitiProcessFilters implements OnInit, OnChanges {
/**
* Return the current task
* @returns {FilterRepresentationModel}
* @returns {FilterProcessRepresentationModel}
*/
getCurrentFilter(): FilterProcessRepresentationModel {
return this.currentFilter;

View File

@ -23,7 +23,7 @@
* @returns {FilterProcessRepresentationModel} .
*/
export class FilterProcessRepresentationModel {
id: number;
id: string;
appId: string;
name: string;
recent: boolean;
@ -32,6 +32,7 @@ export class FilterProcessRepresentationModel {
index: number;
constructor(obj?: any) {
this.id = obj && obj.id || null;
this.appId = obj && obj.appId || null;
this.name = obj && obj.name || null;
this.recent = obj && obj.recent || false;

View File

@ -18,7 +18,6 @@
import { TestBed } from '@angular/core/testing';
import { async } from '@angular/core/testing';
import { CoreModule, AlfrescoApiService } from 'ng2-alfresco-core';
import { FilterRepresentationModel } from 'ng2-activiti-tasklist';
import { AlfrescoApi } from 'alfresco-js-api';
import {
fakeFilters,
@ -581,7 +580,7 @@ describe('ActivitiProcessService', () => {
createFilter = spyOn(alfrescoApi.activiti.userFiltersApi, 'createUserProcessInstanceFilter')
.and
.callFake((filter: FilterRepresentationModel) => Promise.resolve(filter));
.callFake((filter: FilterProcessRepresentationModel) => Promise.resolve(filter));
});
describe('get filters', () => {
@ -596,6 +595,32 @@ describe('ActivitiProcessService', () => {
expect(getFilters).toHaveBeenCalledWith({appId: 226});
});
it('should return the task filter by id', (done) => {
service.getProcessFilterById('333').subscribe(
(res: FilterProcessRepresentationModel) => {
expect(res).toBeDefined();
expect(res.id).toEqual('333');
expect(res.name).toEqual('Running');
expect(res.filter.sort).toEqual('created-desc');
expect(res.filter.state).toEqual('running');
done();
}
);
});
it('should return the task filter by name', (done) => {
service.getProcessFilterByName('Running').subscribe(
(res: FilterProcessRepresentationModel) => {
expect(res).toBeDefined();
expect(res.id).toEqual('333');
expect(res.name).toEqual('Running');
expect(res.filter.sort).toEqual('created-desc');
expect(res.filter.state).toEqual('running');
done();
}
);
});
it('should return the non-empty filter list that is returned by the API', async(() => {
service.getProcessFilters(null).subscribe(
(res) => {
@ -640,7 +665,7 @@ describe('ActivitiProcessService', () => {
});
it('should return the created filter', async(() => {
service.addFilter(filter).subscribe((createdFilter: FilterRepresentationModel) => {
service.addFilter(filter).subscribe((createdFilter: FilterProcessRepresentationModel) => {
expect(createdFilter).toBe(filter);
});
}));

View File

@ -70,10 +70,34 @@ export class ActivitiProcessService {
.catch(err => this.handleError(err));
}
/**
* Retrieve the process filter by id
* @param processId - string - The id of the filter
* @returns {Observable<FilterProcessRepresentationModel>}
*/
getProcessFilterById(processId: string, appId?: string): Observable<FilterProcessRepresentationModel> {
return Observable.fromPromise(this.callApiGetUserProcessInstanceFilters(appId))
.map((response: any) => {
return response.data.find(filter => filter.id === processId);
}).catch(err => this.handleError(err));
}
/**
* Retrieve the process filter by name
* @param processName - string - The name of the filter
* @returns {Observable<FilterProcessRepresentationModel>}
*/
getProcessFilterByName(processName: string, appId?: string): Observable<FilterProcessRepresentationModel> {
return Observable.fromPromise(this.callApiGetUserProcessInstanceFilters(appId))
.map((response: any) => {
return response.data.find(filter => filter.name === processName);
}).catch(err => this.handleError(err));
}
/**
* Create and return the default filters
* @param appId
* @returns {FilterRepresentationModel[]}
* @returns {FilterProcessRepresentationModel[]}
*/
public createDefaultFilters(appId: number): Observable<FilterProcessRepresentationModel[]> {
let runnintFilter = this.getRunningFilterInstance(appId);
@ -294,6 +318,14 @@ export class ActivitiProcessService {
return this.apiService.getInstance().activiti.userFiltersApi.createUserProcessInstanceFilter(filter);
}
callApiProcessFilters(appId?: string) {
if (appId) {
return this.apiService.getInstance().activiti.userFiltersApi.getUserProcessInstanceFilters({ appId: appId });
} else {
return this.apiService.getInstance().activiti.userFiltersApi.getUserProcessInstanceFilters();
}
}
private extractData(res: any) {
return res.data || {};
}

View File

@ -211,11 +211,12 @@ Here's the list of available properties you can define for a Data Column definit
### Properties
| Name | Description |
| --- | --- |
| --- | --- | --- | --- |
|`appId`| { string } The id of the app. |
|`processDefinitionKey`| { string } The processDefinitionKey of the process. |
|`assignment`| { string } The assignment of the process. <ul>Possible values are: <li>assignee : where the current user is the assignee</li> <li>candidate: where the current user is a task candidate </li><li>group_x: where the task is assigned to a group where the current user is a member of.</li> <li>no value: where the current user is involved</li> </ul> |
|`state`| { string } Define state of the processes. Possible values are: completed, active |
|`hasIcon` | boolean | true | Show/Hide the icon on the left . |
|`landingTaskId`| { string } Define which task id should be selected after the reloading. If the task id doesn't exist or nothing is passed it will select the first task |
|`sort`| { string } Define the sort of the processes. Possible values are : created-desc, created-asc, due-desc, due-asc |
| `data` | { DataTableAdapter } (optional) JSON object that represent the number and the type of the columns that you want show |
@ -224,10 +225,10 @@ Example:
```json
[
{type: 'text', key: 'id', title: 'Id'},
{type: 'text', key: 'name', title: 'Name', cssClass: 'full-width name-column', sortable: true},
{type: 'text', key: 'formKey', title: 'Form Key', sortable: true},
{type: 'text', key: 'created', title: 'Created', sortable: true}
{"type": "text", "key": "id", "title": "Id"},
{"type": "text", "key": "name", "title": "Name", "cssClass": "full-width name-column", "sortable": true},
{"type": "text", "key": "formKey", "title": "Form Key", "sortable": true},
{"type": "text", "key": "created", "title": "Created", "sortable": true}
]
```
@ -310,6 +311,31 @@ The component shows all the available apps.
| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `layoutType` | {string} | required | Define the layout of the apps. There are two possible values: GRID or LIST. |
| `filtersAppId` | {Object} | | Provide a way to filter the apps to show. |
### How filter the activiti apps
If you want show some specific apps you can specify them through the filtersAppId parameters
```html
<activiti-apps [filtersAppId]="'[{defaultAppId: 'tasks'}, {deploymentId: '15037'}, {name : 'my app name'}]'"></activiti-apps>
```
In this specific case only the Tasks app, the app with deploymentId 15037 and the app with "my app name" will be showed
![how-filter-apps](docs/assets/how-filter-apps.png)
You can use inside the filter one of the following property
```json
{
"defaultAppId": "string",
"deploymentId": "string",
"name": "string",
"id": "number",
"modelId": "number",
"tenantId": "number"
}
```
## Basic usage example Activiti Filter
@ -329,6 +355,27 @@ The component shows all the available filters.
No options
### How to create an accordion menu with the task filter
You can create an accordion menu using the AccordionComponent that wrap the activiti task filter.
The AccordionComponent is exposed by the alfresco-core.
```html
<adf-accordion>
<adf-accordion-group [heading]="'Tasks'" [isSelected]="true" [headingIcon]="'assignment'">
<activiti-filters
[appId]="appId"
[hasIcon]="false"
(filterClick)="onTaskFilterClick($event)"
(onSuccess)="onSuccessTaskFilterList($event)"
#activitifilter>
</activiti-filters>
</adf-accordion-group>
</adf-accordion>
```
![how-create-accordion-menu](docs/assets/how-to-create-accordion-menu.png)
## Basic usage example Activiti Checklist
The component shows the checklist task functionality.

View File

@ -43,10 +43,10 @@
"moment": "2.15.1",
"md-date-time-picker": "2.2.0",
"ng2-translate": "2.5.0",
"alfresco-js-api": "~1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-alfresco-datatable": "1.3.0",
"ng2-activiti-tasklist": "1.3.0"
"alfresco-js-api": "~1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-alfresco-datatable": "1.4.0",
"ng2-activiti-tasklist": "1.4.0"
},
"devDependencies": {
"@types/jasmine": "^2.2.33",

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -1,7 +1,7 @@
{
"name": "ng2-activiti-tasklist",
"description": "Activiti Angular2 Task List Component",
"version": "1.3.0",
"version": "1.4.0",
"author": "Alfresco Software, Ltd.",
"scripts": {
"clean": "npm install rimraf && npm run clean-build && rimraf dist node_modules typings",
@ -59,14 +59,14 @@
"@angular/platform-browser": "2.2.2",
"@angular/platform-browser-dynamic": "2.2.2",
"@angular/router": "3.2.2",
"alfresco-js-api": "~1.3.0",
"alfresco-js-api": "~1.4.0",
"core-js": "2.4.1",
"hammerjs": "2.0.8",
"md-date-time-picker": "2.2.0",
"moment": "2.15.1",
"ng2-activiti-form": "1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-alfresco-datatable": "1.3.0",
"ng2-activiti-form": "1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-alfresco-datatable": "1.4.0",
"ng2-translate": "2.5.0",
"reflect-metadata": "0.1.10",
"rxjs": "5.0.0-beta.12",

View File

@ -17,22 +17,6 @@
import { AppDefinitionRepresentationModel } from '../models/filter.model';
export var deployedApps = [new AppDefinitionRepresentationModel({
id: '1',
name: 'App1',
icon: 'icon1',
deploymentId: '1'
}), new AppDefinitionRepresentationModel({
id: '2',
name: 'App2',
icon: 'icon2',
deploymentId: '2'
}), new AppDefinitionRepresentationModel({
id: '3',
name: 'App3',
icon: 'icon3',
deploymentId: '3'
})];
export var nonDeployedApps = [new AppDefinitionRepresentationModel({
id: '1',
name: '1',
@ -46,6 +30,50 @@ export var nonDeployedApps = [new AppDefinitionRepresentationModel({
name: '3',
icon: 'icon3'
})];
export var deployedApps = [new AppDefinitionRepresentationModel({
id: 1,
name: 'App1',
icon: 'icon1',
deploymentId: '1',
defaultAppId: 'fake-app-1',
modelId: null,
tenantId: null
}), new AppDefinitionRepresentationModel({
id: 2,
name: 'App2',
icon: 'icon2',
deploymentId: '2',
modelId: null,
tenantId: null
}), new AppDefinitionRepresentationModel({
id: 3,
name: 'App3',
icon: 'icon3',
deploymentId: '3',
modelId: null,
tenantId: null
}), new AppDefinitionRepresentationModel({
id: 4,
name: 'App4',
icon: 'icon4',
deploymentId: '4',
modelId: 65,
tenantId: null
}), new AppDefinitionRepresentationModel({
id: 5,
name: 'App5',
icon: 'icon5',
deploymentId: '5',
modelId: 66,
tenantId: 9
}), new AppDefinitionRepresentationModel({
id: 6,
name: 'App6',
icon: 'icon6',
deploymentId: '6',
tenantId: 9,
modelId: 66
})];
export var defaultApp = [new AppDefinitionRepresentationModel({
defaultAppId: 'tasks'
})];

View File

@ -0,0 +1,226 @@
/*!
* @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 {
FilterRepresentationModel,
AppDefinitionRepresentationModel
} from '../models/filter.model';
export var fakeFilters = {
size: 2, total: 2, start: 0,
data: [
new AppDefinitionRepresentationModel(
{
id: '1', name: 'FakeInvolvedTasks', recent: false, icon: 'glyphicon-align-left',
filter: { sort: 'created-desc', name: '', state: 'open', assignment: 'fake-involved' }
}
),
{
id: '2', name: 'FakeMyTasks', recent: false, icon: 'glyphicon-align-left',
filter: { sort: 'created-desc', name: '', state: 'open', assignment: 'fake-assignee' }
}
]
};
export var fakeAppFilter = {
size: 1, total: 1, start: 0,
data: [
{
id: 1, name: 'FakeInvolvedTasks', recent: false, icon: 'glyphicon-align-left',
filter: { sort: 'created-desc', name: '', state: 'open', assignment: 'fake-involved' }
}
]
};
export var fakeApps = {
size: 2, total: 2, start: 0,
data: [
{
id: 1, defaultAppId: null, name: 'Sales-Fakes-App', description: 'desc-fake1', modelId: 22,
theme: 'theme-1-fake', icon: 'glyphicon-asterisk', 'deploymentId': '111', 'tenantId': null
},
{
id: 2, defaultAppId: null, name: 'health-care-Fake', description: 'desc-fake2', modelId: 33,
theme: 'theme-2-fake', icon: 'glyphicon-asterisk', 'deploymentId': '444', 'tenantId': null
}
]
};
export var fakeFilter = {
sort: 'created-desc', text: '', state: 'open', assignment: 'fake-assignee'
};
export var fakeFilterWithProcessDefinitionKey = {
sort: 'created-desc', text: '', state: 'open', assignment: 'fake-assignee', processDefinitionKey: '1'
};
export var fakeUser = { id: 1, email: 'fake-email@dom.com', firstName: 'firstName', lastName: 'lastName' };
export var fakeTaskList = {
size: 1, total: 1, start: 0,
data: [
{
id: '1', name: 'FakeNameTask', description: null, category: null,
assignee: fakeUser,
created: '2016-07-15T11:19:17.440+0000'
}
]
};
export var fakeTaskListDifferentProcessDefinitionKey = {
size: 1, total: 1, start: 0,
data: [
{
id: '1', name: 'FakeNameTask', description: null, category: null,
assignee: fakeUser,
processDefinitionKey: '1',
created: '2016-07-15T11:19:17.440+0000'
},
{
id: '2', name: 'FakeNameTask2', description: null, category: null,
assignee: fakeUser,
processDefinitionKey: '2',
created: '2016-07-15T11:19:17.440+0000'
}
]
};
export var secondFakeTaskList = {
size: 1, total: 1, start: 0,
data: [
{
id: '200', name: 'FakeNameTask', description: null, category: null,
assignee: fakeUser,
created: '2016-07-15T11:19:17.440+0000'
}
]
};
export var fakeErrorTaskList = {
error: 'wrong request'
};
export var fakeTaskDetails = { id: '999', name: 'fake-task-name', formKey: '99', assignee: fakeUser };
export var fakeTasksComment = {
size: 2, total: 2, start: 0,
data: [
{
id: 1, message: 'fake-message-1', created: '', createdBy: fakeUser
},
{
id: 2, message: 'fake-message-2', created: '', createdBy: fakeUser
}
]
};
export var fakeTasksChecklist = {
size: 1, total: 1, start: 0,
data: [
{
id: 1, name: 'FakeCheckTask1', description: null, category: null,
assignee: fakeUser,
created: '2016-07-15T11:19:17.440+0000'
},
{
id: 2, name: 'FakeCheckTask2', description: null, category: null,
assignee: fakeUser,
created: '2016-07-15T11:19:17.440+0000'
}
]
};
export var fakeRepresentationFilter1: FilterRepresentationModel = new FilterRepresentationModel({
appId: 1,
name: 'CONTAIN FILTER',
recent: true,
icon: 'glyphicon-align-left',
filter: {
processDefinitionId: null,
processDefinitionKey: null,
name: null,
state: 'open',
sort: 'created-desc',
assignment: 'involved',
dueAfter: null,
dueBefore: null
}
});
export var fakeRepresentationFilter2: FilterRepresentationModel = new FilterRepresentationModel({
appId: 2,
name: 'NO TASK FILTER',
recent: false,
icon: 'glyphicon-inbox',
filter: {
processDefinitionId: null,
processDefinitionKey: null,
name: null,
state: 'open',
sort: 'created-desc',
assignment: 'assignee',
dueAfter: null,
dueBefore: null
}
});
export var fakeAppPromise = new Promise(function (resolve, reject) {
resolve(fakeAppFilter);
});
export var fakeFormList = {
size: 2,
total: 2,
start: 0,
data: [{
id: 1,
name: 'form with all widgets',
description: '',
createdBy: 2,
createdByFullName: 'Admin Admin',
lastUpdatedBy: 2,
lastUpdatedByFullName: 'Admin Admin',
lastUpdated: 1491400951205,
latestVersion: true,
version: 4,
comment: null,
stencilSet: null,
referenceId: null,
modelType: 2,
favorite: null,
permission: 'write',
tenantId: null
}, {
id: 2,
name: 'uppy',
description: '',
createdBy: 2,
createdByFullName: 'Admin Admin',
lastUpdatedBy: 2,
lastUpdatedByFullName: 'Admin Admin',
lastUpdated: 1490951054477,
latestVersion: true,
version: 2,
comment: null,
stencilSet: null,
referenceId: null,
modelType: 2,
favorite: null,
permission: 'write',
tenantId: null
}]
};

View File

@ -22,3 +22,6 @@
</div>
</div>
</div>
<div class="menu-container" *ngIf="isEmpty()">
{{ 'APPS.NONE' | translate }}
</div>

View File

@ -59,7 +59,7 @@ describe('ActivitiApps', () => {
debugElement = fixture.debugElement;
service = fixture.debugElement.injector.get(ActivitiTaskListService);
getAppsSpy = spyOn(service, 'getDeployedApplications').and.returnValue(Observable.of());
getAppsSpy = spyOn(service, 'getDeployedApplications').and.returnValue(Observable.of(deployedApps));
componentHandler = jasmine.createSpyObj('componentHandler', [
'upgradeAllRegistered',
@ -79,6 +79,59 @@ describe('ActivitiApps', () => {
expect(getAppsSpy).toHaveBeenCalled();
});
it('should show the apps filterd by defaultAppId', () => {
component.filtersAppId = [{defaultAppId: 'fake-app-1'}];
fixture.detectChanges();
expect(component.isEmpty()).toBe(false);
expect(component.appList).toBeDefined();
expect(component.appList.length).toEqual(1);
});
it('should show the apps filterd by deploymentId', () => {
component.filtersAppId = [{deploymentId: '4'}];
fixture.detectChanges();
expect(component.isEmpty()).toBe(false);
expect(component.appList).toBeDefined();
expect(component.appList.length).toEqual(1);
expect(component.appList[0].deploymentId).toEqual('4');
});
it('should show the apps filterd by name', () => {
component.filtersAppId = [{name: 'App5'}];
fixture.detectChanges();
expect(component.isEmpty()).toBe(false);
expect(component.appList).toBeDefined();
expect(component.appList.length).toEqual(1);
expect(component.appList[0].name).toEqual('App5');
});
it('should show the apps filterd by id', () => {
component.filtersAppId = [{id: 6}];
fixture.detectChanges();
expect(component.isEmpty()).toBe(false);
expect(component.appList).toBeDefined();
expect(component.appList.length).toEqual(1);
expect(component.appList[0].id).toEqual(6);
});
it('should show the apps filterd by modelId', () => {
component.filtersAppId = [{modelId: 66}];
fixture.detectChanges();
expect(component.isEmpty()).toBe(false);
expect(component.appList).toBeDefined();
expect(component.appList.length).toEqual(2);
expect(component.appList[0].modelId).toEqual(66);
});
it('should show the apps filterd by tenandId', () => {
component.filtersAppId = [{tenantId: 9}];
fixture.detectChanges();
expect(component.isEmpty()).toBe(false);
expect(component.appList).toBeDefined();
expect(component.appList.length).toEqual(2);
expect(component.appList[0].tenantId).toEqual(9);
});
it('should emit an error when an error occurs loading apps', () => {
let emitSpy = spyOn(component.error, 'emit');
getAppsSpy.and.returnValue(Observable.throw({}));
@ -119,7 +172,7 @@ describe('ActivitiApps', () => {
it('should display all deployed apps', () => {
getAppsSpy.and.returnValue(Observable.of(deployedApps));
fixture.detectChanges();
expect(debugElement.queryAll(By.css('h1')).length).toBe(3);
expect(debugElement.queryAll(By.css('h1')).length).toBe(6);
});
it('should not display undeployed apps', () => {

View File

@ -45,6 +45,9 @@ export class ActivitiApps implements OnInit {
@Input()
layoutType: string = ActivitiApps.LAYOUT_GRID;
@Input()
filtersAppId: any[];
@Output()
appClick: EventEmitter<AppDefinitionRepresentationModel> = new EventEmitter<AppDefinitionRepresentationModel>();
@ -90,6 +93,7 @@ export class ActivitiApps implements OnInit {
private load() {
this.activitiTaskList.getDeployedApplications().subscribe(
(res) => {
res = this.filterApps(res);
res.forEach((app: AppDefinitionRepresentationModel) => {
if (app.defaultAppId === ActivitiApps.DEFAULT_TASKS_APP) {
app.name = ActivitiApps.DEFAULT_TASKS_APP_NAME;
@ -125,6 +129,27 @@ export class ActivitiApps implements OnInit {
return (this.currentApp !== undefined && appId === this.currentApp.id);
}
private filterApps(apps: AppDefinitionRepresentationModel []): AppDefinitionRepresentationModel[] {
let filteredApps = [];
if (this.filtersAppId) {
apps.filter((app: AppDefinitionRepresentationModel) => {
this.filtersAppId.forEach((filter) => {
if (app.defaultAppId === filter.defaultAppId ||
app.deploymentId === filter.deploymentId ||
app.name === filter.name ||
app.id === filter.id ||
app.modelId === filter.modelId ||
app.tenantId === filter.tenantId) {
filteredApps.push(app);
}
});
});
} else {
return apps;
}
return filteredApps;
}
/**
* Check if the value of the layoutType property is an allowed value
* @returns {boolean}

View File

@ -7,9 +7,11 @@
<div class="menu-container" *ngIf="checklist?.length > 0">
<ul class='mdl-list'>
<li class="mdl-list__item" *ngFor="let check of checklist">
<span class="mdl-list__item-primary-content" id="check-{{check.id}}">
<i class="material-icons mdl-list__item-icon">done</i>
{{check.name}}
<span class="mdl-chip mdl-chip--deletable" id="check-{{check.id}}">
<span class="mdl-chip__text">{{check.name}}</span>
<button type="button" class="mdl-chip__action" (click)="delete(check.id)">
<i id="remove-{{check.id}}" class="material-icons">cancel</i>
</button>
</span>
</li>
</ul>

View File

@ -140,6 +140,42 @@ describe('ActivitiChecklist', () => {
});
}));
it('should remove a checklist element', async(() => {
checklistComponent.taskId = 'new-fake-task-id';
checklistComponent.checklist.push(fakeTaskDetail);
fixture.detectChanges();
let checklistElementRemove = <HTMLElement> element.querySelector('#remove-fake-check-id');
expect(checklistElementRemove).toBeDefined();
expect(checklistElementRemove).not.toBeNull();
checklistElementRemove.click();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json'
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#fake-check-id')).toBeNull();
});
}));
it('should send an event when the checklist is deleted', (done) => {
checklistComponent.taskId = 'new-fake-task-id';
checklistComponent.checklist.push(fakeTaskDetail);
fixture.detectChanges();
let checklistElementRemove = <HTMLElement> element.querySelector('#remove-fake-check-id');
expect(checklistElementRemove).toBeDefined();
expect(checklistElementRemove).not.toBeNull();
checklistElementRemove.click();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json'
});
checklistComponent.checklistTaskDeleted.subscribe(() => {
expect(element.querySelector('#fake-check-id')).toBeNull();
done();
});
});
it('should show load task checklist on change', async(() => {
checklistComponent.taskId = 'new-fake-task-id';
checklistComponent.checklist.push(fakeTaskDetail);

View File

@ -44,6 +44,9 @@ export class ActivitiChecklist implements OnInit, OnChanges {
@Output()
checklistTaskCreated: EventEmitter<TaskDetailsModel> = new EventEmitter<TaskDetailsModel>();
@Output()
checklistTaskDeleted: EventEmitter<string> = new EventEmitter<string>();
@ViewChild('dialog')
dialog: any;
@ -129,6 +132,17 @@ export class ActivitiChecklist implements OnInit, OnChanges {
this.cancel();
}
public delete(taskId: string) {
this.activitiTaskList.deleteTask(taskId).subscribe(
() => {
this.checklist = this.checklist.filter(check => check.id !== taskId);
this.checklistTaskDeleted.emit(taskId);
},
(err) => {
this.logService.error(err);
});
}
public cancel() {
if (this.dialog) {
this.dialog.nativeElement.close();

View File

@ -14,6 +14,10 @@
color: rgb(68,138,255);
}
.activiti-filters__entry:hover {
opacity: 0.8;
}
.activiti-filters__entry.active .activiti-filters__entry-icon {
color: rgb(68,138,255);
}

View File

@ -3,7 +3,7 @@
<li class="mdl-list__item activiti-filters__entry" (click)="selectFilter(filter)" *ngFor="let filter of filters"
[class.active]="currentFilter === filter">
<span class="mdl-list__item-primary-content">
<i class="material-icons mdl-list__item-icon activiti-filters__entry-icon" [attr.data-automation-id]="filter.name + '_filter'" >assignment</i>
<i *ngIf="hasIcon" class="material-icons mdl-list__item-icon activiti-filters__entry-icon" [attr.data-automation-id]="filter.name + '_filter'" >assignment</i>
{{filter.name}}
</span>
</li>

View File

@ -47,6 +47,9 @@ export class ActivitiFilters implements OnInit, OnChanges {
@Input()
appName: string;
@Input()
hasIcon: boolean = true;
private filterObserver: Observer<FilterRepresentationModel>;
filter$: Observable<FilterRepresentationModel>;

View File

@ -38,13 +38,15 @@
[readOnly]="readOnlyForm"
[taskId]="taskDetails.id"
[assignee]="taskDetails?.assignee?.id"
(checklistTaskCreated)="onChecklistTaskCreated($event)">
(checklistTaskCreated)="onChecklistTaskCreated($event)"
(checklistTaskDeleted)="onChecklistTaskDeleted($event)">
</activiti-checklist>
</div>
</div>
</div>
<div *ngIf="isAssignedToMe()">
<activiti-form *ngIf="hasFormKey()" #activitiForm
[showDebugButton]="debugMode"
[taskId]="taskDetails.id"
[showTitle]="showFormTitle"
[showRefreshButton]="showFormRefreshButton"

View File

@ -42,6 +42,9 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
@ViewChild('errorDialog')
errorDialog: DebugElement;
@Input()
debugMode: boolean = false;
@Input()
taskId: string;
@ -96,6 +99,9 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
@Output()
taskCreated: EventEmitter<TaskDetailsModel> = new EventEmitter<TaskDetailsModel>();
@Output()
taskDeleted: EventEmitter<string> = new EventEmitter<string>();
@Output()
onError: EventEmitter<any> = new EventEmitter<any>();
@ -258,6 +264,10 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
this.taskCreated.emit(task);
}
onChecklistTaskDeleted(taskId: string) {
this.taskDeleted.emit(taskId);
}
onFormError(error: any) {
this.errorDialog.nativeElement.showModal();
this.onError.emit(error);

View File

@ -1,4 +1,7 @@
{
"APPS": {
"NONE": "No apps found."
},
"TASK_LIST": {
"MESSAGES": {
"NONE": "No tasks list found."

View File

@ -1,4 +1,7 @@
{
"APPS": {
"NONE": "Nessuna applicazione trovata."
},
"TASK_LIST": {
"MESSAGES": {
"NONE": "Nessuna lista tasks trovata."

View File

@ -54,7 +54,7 @@ export class AppDefinitionRepresentationModel {
* @returns {FilterRepresentationModel} .
*/
export class FilterRepresentationModel {
id: number;
id: string;
appId: string;
name: string;
recent: boolean;
@ -64,6 +64,7 @@ export class FilterRepresentationModel {
landingTaskId: string;
constructor(obj?: any) {
this.id = obj && obj.id || null;
this.appId = obj && obj.appId || null;
this.name = obj && obj.name || null;
this.recent = obj && obj.recent || false;

View File

@ -35,7 +35,7 @@ export class ActivitiTaskListService {
}
/**
* Retrive all the Deployed app
* Retrieve all the Deployed app
* @returns {Observable<any>}
*/
getDeployedApplications(name?: string): Observable<any> {
@ -50,7 +50,7 @@ export class ActivitiTaskListService {
}
/**
* Retrive all the Tasks filters
* Retrieve all the Tasks filters
* @returns {Observable<any>}
*/
getTaskListFilters(appId?: string): Observable<any> {
@ -65,6 +65,30 @@ export class ActivitiTaskListService {
}).catch(err => this.handleError(err));
}
/**
* Retrieve the Tasks filter by id
* @param taskId - string - The id of the filter
* @returns {Observable<FilterRepresentationModel>}
*/
getTaskFilterById(taskId: string, appId?: string): Observable<FilterRepresentationModel> {
return Observable.fromPromise(this.callApiTaskFilters(appId))
.map((response: any) => {
return response.data.find(filter => filter.id === taskId);
}).catch(err => this.handleError(err));
}
/**
* Retrieve the Tasks filter by name
* @param taskName - string - The name of the filter
* @returns {Observable<FilterRepresentationModel>}
*/
getTaskFilterByName(taskName: string, appId?: string): Observable<FilterRepresentationModel> {
return Observable.fromPromise(this.callApiTaskFilters(appId))
.map((response: any) => {
return response.data.find(filter => filter.name === taskName);
}).catch(err => this.handleError(err));
}
/**
* Return all the filters in the list where the task id belong
* @param taskId - string
@ -108,7 +132,7 @@ export class ActivitiTaskListService {
}
/**
* Retrive all the tasks filtered by filterModel
* Retrieve all the tasks filtered by filterModel
* @param filter - TaskFilterRepresentationModel
* @returns {any}
*/
@ -124,7 +148,7 @@ export class ActivitiTaskListService {
}
/**
* Retrive all the task details
* Retrieve all the task details
* @param id - taskId
* @returns {<TaskDetailsModel>}
*/
@ -137,7 +161,7 @@ export class ActivitiTaskListService {
}
/**
* Retrive all the task's comments
* Retrieve all the task's comments
* @param id - taskId
* @returns {<Comment[]>}
*/
@ -155,7 +179,7 @@ export class ActivitiTaskListService {
}
/**
* Retrive all the task's checklist
* Retrieve all the task's checklist
* @param id - taskId
* @returns {TaskDetailsModel}
*/
@ -172,7 +196,7 @@ export class ActivitiTaskListService {
}
/**
* Retrive all the form shared with this user
* Retrieve all the form shared with this user
* @returns {TaskDetailsModel}
*/
getFormList(): Observable<Form []> {
@ -256,6 +280,15 @@ export class ActivitiTaskListService {
}).catch(err => this.handleError(err));
}
/**
* Delete a task
* @param taskId - string
*/
deleteTask(taskId: string): Observable<TaskDetailsModel> {
return Observable.fromPromise(this.callApiDeleteTask(taskId))
.catch(err => this.handleError(err));
}
/**
* Add a filter
* @param filter - FilterRepresentationModel
@ -357,6 +390,10 @@ export class ActivitiTaskListService {
return this.apiService.getInstance().activiti.taskApi.addSubtask(task.parentTaskId, task);
}
private callApiDeleteTask(taskId: string) {
return this.apiService.getInstance().activiti.taskApi.deleteTask(taskId);
}
private callApiAddFilter(filter: FilterRepresentationModel) {
return this.apiService.getInstance().activiti.userFiltersApi.createUserTaskFilter(filter);
}

View File

@ -45,11 +45,13 @@ import { DataColumnComponent } from './src/components/data-column/data-column.co
import { DataColumnListComponent } from './src/components/data-column/data-column-list.component';
import { MATERIAL_DESIGN_DIRECTIVES } from './src/components/material/index';
import { CONTEXT_MENU_PROVIDERS, CONTEXT_MENU_DIRECTIVES } from './src/components/context-menu/index';
import { COLLAPSABLE_DIRECTIVES } from './src/components/collapsable/index';
export * from './src/services/index';
export * from './src/components/index';
export * from './src/components/data-column/data-column.component';
export * from './src/components/data-column/data-column-list.component';
export * from './src/components/collapsable/index';
export * from './src/directives/upload.directive';
export * from './src/utils/index';
export * from './src/events/base.event';
@ -94,6 +96,7 @@ export function createTranslateLoader(http: Http, logService: LogService) {
declarations: [
...MATERIAL_DESIGN_DIRECTIVES,
...CONTEXT_MENU_DIRECTIVES,
...COLLAPSABLE_DIRECTIVES,
UploadDirective,
DataColumnComponent,
DataColumnListComponent
@ -110,6 +113,7 @@ export function createTranslateLoader(http: Http, logService: LogService) {
TranslateModule,
...MATERIAL_DESIGN_DIRECTIVES,
...CONTEXT_MENU_DIRECTIVES,
...COLLAPSABLE_DIRECTIVES,
UploadDirective,
DataColumnComponent,
DataColumnListComponent

View File

@ -1,7 +1,7 @@
{
"name": "ng2-alfresco-core",
"description": "Alfresco Angular 2 Components core",
"version": "1.3.0",
"version": "1.4.0",
"author": "Alfresco Software, Ltd.",
"scripts": {
"clean": "npm install rimraf && npm run clean-build && rimraf dist node_modules typings",
@ -65,7 +65,7 @@
"@angular/platform-browser-dynamic": "2.2.2",
"@angular/router": "3.2.2",
"@angular/upgrade": "2.2.2",
"alfresco-js-api": "~1.3.0",
"alfresco-js-api": "~1.4.0",
"core-js": "2.4.1",
"dialog-polyfill": "0.4.7",
"element.scrollintoviewifneeded-polyfill": "1.0.1",

View File

@ -0,0 +1,35 @@
.adf-panel-heading {
float: left;
font-size: 14px;
font-weight: bold;
font-style: normal;
font-stretch: normal;
line-height: normal;
letter-spacing: normal;
text-align: left;
color: #000000;
width: 100%;
}
.adf-panel-heading-selected {
color: #448aff;
}
.adf-panel-heading-icon {
float: left;
}
.adf-panel-heading-text {
float: left;
padding-left: 20px;
padding-top: 4px;
}
.adf-panel-heading-toggle {
float: right;
cursor: pointer;
}
.adf-panel-heading-toggle:hover {
opacity: 0.4;
}

View File

@ -0,0 +1,16 @@
<div class="adf-panel adf-panel-default" [ngClass]="{'adf-panel-open': isOpen}">
<div class="adf-panel-heading" [ngClass]="{'adf-panel-heading-selected': isSelected}">
<div *ngIf="hasHeadingIcon()" class="adf-panel-heading-icon">
<i class="material-icons">{{headingIcon}}</i>
</div>
<div class="adf-panel-heading-text">{{heading}}</div>
<div id="accordion-button" class="adf-panel-heading-toggle" (click)="toggleOpen($event)">
<i class="material-icons">{{getAccordionIcon()}}</i>
</div>
</div>
<div class="adf-panel-collapse" [hidden]="!isOpen">
<div class="adf-panel-body">
<ng-content></ng-content>
</div>
</div>
</div>

View File

@ -0,0 +1,76 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { AccordionComponent } from './accordion.component';
import { AccordionGroupComponent } from './accordion-group.component';
describe('AccordionGroupComponent', () => {
let fixture: ComponentFixture<AccordionGroupComponent>;
let component: AccordionGroupComponent;
let element: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AccordionGroupComponent
],
providers: [AccordionComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AccordionGroupComponent);
element = fixture.nativeElement;
component = fixture.componentInstance;
});
it('should be closed by default', () => {
component.heading = 'Fake Header';
component.headingIcon = 'fake-icon';
fixture.whenStable().then(() => {
fixture.detectChanges();
let headerToggle = fixture.nativeElement.querySelector('.adf-panel-heading-toggle .material-icons');
expect(headerToggle.innerText).toEqual('expand_more');
let headerText = fixture.nativeElement.querySelector('.adf-panel-heading-text');
expect(headerText.innerText).toEqual('Fake Header');
let headerIcon = fixture.nativeElement.querySelector('.adf-panel-heading-icon .material-icons');
expect(headerIcon.innerText).toEqual('fake-icon');
});
});
it('should be open when click', () => {
component.isSelected = true;
component.heading = 'Fake Header';
component.headingIcon = 'fake-icon';
fixture.detectChanges();
element.querySelector('#accordion-button').click();
fixture.whenStable().then(() => {
fixture.detectChanges();
let headerText = fixture.nativeElement.querySelector('.adf-panel-heading-text');
expect(headerText.innerText).toEqual('Fake Header');
let headerIcon = fixture.nativeElement.querySelector('.adf-panel-heading-icon .material-icons');
expect(headerIcon.innerText).toEqual('fake-icon');
let headerToggle = fixture.nativeElement.querySelector('.adf-panel-heading-toggle .material-icons');
expect(headerToggle.innerText).toEqual('expand_less');
});
});
});

View File

@ -0,0 +1,79 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, OnDestroy } from '@angular/core';
import { AccordionComponent } from './accordion.component';
@Component({
selector: 'adf-accordion-group',
moduleId: module.id,
templateUrl: 'accordion-group.component.html',
styleUrls: ['./accordion-group.component.css']
})
export class AccordionGroupComponent implements OnDestroy {
private _isOpen: boolean = false;
private _isSelected: boolean = false;
@Input()
heading: string;
@Input()
headingIcon: string;
@Input()
set isOpen(value: boolean) {
this._isOpen = value;
if (value) {
this.accordion.closeOthers(this);
}
}
get isOpen() {
return this._isOpen;
}
@Input()
set isSelected(value: boolean) {
this._isSelected = value;
}
get isSelected() {
return this._isSelected;
}
constructor(private accordion: AccordionComponent) {
this.accordion.addGroup(this);
}
ngOnDestroy() {
this.accordion.removeGroup(this);
}
hasHeadingIcon() {
return this.headingIcon ? true : false;
}
toggleOpen(event: MouseEvent): void {
event.preventDefault();
this.isOpen = !this.isOpen;
}
getAccordionIcon(): string {
return this.isOpen ? 'expand_less' : 'expand_more';
}
}

View File

@ -0,0 +1,81 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { AccordionComponent } from './accordion.component';
import { AccordionGroupComponent } from './accordion-group.component';
describe('AccordionComponent', () => {
let fixture: ComponentFixture<AccordionComponent>;
let component: AccordionComponent;
let componentGroup1: AccordionGroupComponent;
let componentGroup2: AccordionGroupComponent;
let componentGroup3: AccordionGroupComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AccordionComponent
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AccordionComponent);
component = fixture.componentInstance;
});
afterEach(() => {
component.groups = [];
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should add the AccordionGroup', () => {
component.addGroup(componentGroup1);
expect(component.groups.length).toBe(1);
});
it('should close all the other group', () => {
componentGroup1 = new AccordionGroupComponent(component);
componentGroup2 = new AccordionGroupComponent(component);
componentGroup3 = new AccordionGroupComponent(component);
componentGroup1.isOpen = false;
componentGroup2.isOpen = true;
componentGroup3.isOpen = false;
expect(component.groups[0].isOpen).toBeFalsy();
expect(component.groups[1].isOpen).toBeTruthy();
expect(component.groups[2].isOpen).toBeFalsy();
componentGroup1.isOpen = true;
expect(component.groups[0].isOpen).toBeTruthy();
expect(component.groups[1].isOpen).toBeFalsy();
expect(component.groups[2].isOpen).toBeFalsy();
});
it('should remove the AccordionGroup', () => {
component.addGroup(componentGroup1);
component.removeGroup(componentGroup1);
expect(component.groups.length).toBe(0);
});
});

View File

@ -0,0 +1,51 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component } from '@angular/core';
import { AccordionGroupComponent } from './accordion-group.component';
@Component({
selector: 'adf-accordion',
template: `
<ng-content></ng-content>
`,
host: {
'class': 'panel-group'
}
})
export class AccordionComponent {
groups: Array<AccordionGroupComponent> = [];
addGroup(group: AccordionGroupComponent): void {
this.groups.push(group);
}
closeOthers(openGroup: AccordionGroupComponent): void {
this.groups.forEach((group: AccordionGroupComponent) => {
if (group !== openGroup) {
group.isOpen = false;
}
});
}
removeGroup(group: AccordionGroupComponent): void {
const index = this.groups.indexOf(group);
if (index !== -1) {
this.groups.splice(index, 1);
}
}
}

View File

@ -0,0 +1,24 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {AccordionComponent} from './accordion.component';
import {AccordionGroupComponent} from './accordion-group.component';
export const COLLAPSABLE_DIRECTIVES: [any] = [
AccordionComponent,
AccordionGroupComponent
];

View File

@ -17,3 +17,4 @@
export * from './context-menu/index';
export * from './material/index';
export * from './collapsable/index';

View File

@ -190,8 +190,20 @@ export class DataTableDemo {
this.data = new ObjectDataTableAdapter(
// data
[
{id: 1, name: 'Name 1', createdBy : { name: 'user'}, createdOn: 123, icon: 'http://example.com/img.png'},
{id: 2, name: 'Name 2', createdBy : { name: 'user 2'}, createdOn: 123, icon: 'http://example.com/img.png'}
{
id: 1,
name: 'Name 1',
createdBy : { name: 'user'},
createdOn: 123,
icon: 'http://example.com/img.png'
},
{
id: 2,
name: 'Name 2',
createdBy : { name: 'user 2'},
createdOn: 123,
icon: 'http://example.com/img.png'
}
]
);
}
@ -295,6 +307,52 @@ onRowClick(event) {
</alfresco-datatable>
```
#### Column Templates
It is possible assigning a custom column template like the following:
```html
<alfresco-datatable ...>
<data-columns>
<data-column title="Version" key="properties.cm:versionLabel">
<template let-value="value">
<span>V. {{value}}</span>
</template>
</data-column>
</data-columns>
</alfresco-datatable>
```
Example above shows access to the underlying cell value by binding `value` property to the underlying context `value`:
```html
<template let-value="value">
```
Alternatively you can get access to the entire data context using the following syntax:
```html
<template let-entry="$implicit">
```
That means you are going to create local variable `entry` that is bound to the data context via Angular's special `$implicit` keyword.
```html
<template let-entry="$implicit">
<span>V. {{entry.data.getValue(entry.row, entry.col)}}</span>
</template>
```
In the second case `entry` variable is holding a reference to the following data context:
```ts
{
data: DataTableAdapter,
row: DataRow,
col: DataColumn
}
```
#### rowClick event
_This event is emitted when user clicks the row._

View File

@ -47,9 +47,9 @@
"material-design-icons": "2.2.3",
"material-design-lite": "1.2.1",
"ng2-translate": "2.5.0",
"alfresco-js-api": "~1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-alfresco-datatable": "1.3.0"
"alfresco-js-api": "~1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-alfresco-datatable": "1.4.0"
},
"devDependencies": {
"@types/jasmine": "^2.2.33",

View File

@ -1,7 +1,7 @@
{
"name": "ng2-alfresco-datatable",
"description": "Alfresco Angular2 DataTable Component",
"version": "1.3.0",
"version": "1.4.0",
"author": "Alfresco Software, Ltd.",
"scripts": {
"clean": "npm install rimraf && npm run clean-build && rimraf dist node_modules typings",
@ -55,10 +55,10 @@
"@angular/platform-browser": "2.2.2",
"@angular/platform-browser-dynamic": "2.2.2",
"@angular/router": "3.2.2",
"alfresco-js-api": "~1.3.0",
"alfresco-js-api": "~1.4.0",
"core-js": "2.4.1",
"hammerjs": "2.0.8",
"ng2-alfresco-core": "1.3.0",
"ng2-alfresco-core": "1.4.0",
"ng2-translate": "2.5.0",
"reflect-metadata": "0.1.10",
"rxjs": "5.0.0-beta.12",

View File

@ -34,14 +34,14 @@
[class.alfresco-datatable__row--selected]="selectedRow === row"
[adf-upload]="allowDropFiles" [adf-upload-data]="row">
<!-- Actions (right) -->
<!-- Actions (left) -->
<td *ngIf="actions && actionsPosition === 'left'" class="alfresco-datatable__actions-cell">
<button [id]="'action_menu_' + idx" alfresco-mdl-button class="mdl-button--icon" [attr.data-automation-id]="actions_menu">
<i class="material-icons">more_vert</i>
</button>
<ul alfresco-mdl-menu class="mdl-menu--bottom-left"
[attr.for]="'action_menu_' + idx">
<li class="mdl-menu__item"
<li class="mdl-menu__item" [attr.disabled]="action.disabled"
[attr.data-automation-id]="action.title"
*ngFor="let action of getRowActions(row)"
(click)="onExecuteRowAction(row, action)">
@ -53,33 +53,38 @@
<td *ngIf="multiselect">
<md-checkbox [(ngModel)]="row.isSelected"></md-checkbox>
</td>
<td *ngFor="let col of data.getColumns()" [ngSwitch]="col.type"
<td *ngFor="let col of data.getColumns()"
class="mdl-data-table__cell--non-numeric non-selectable data-cell {{col.cssClass}}"
(click)="onRowClick(row, $event)"
(dblclick)="onRowDblClick(row, $event)"
[context-menu]="getContextMenuActions(row, col)"
[context-menu-enabled]="contextMenu">
<div *ngIf="!col.template" class="cell-container">
<div *ngSwitchCase="'image'" class="cell-value">
<i *ngIf="isIconValue(row, col)" class="material-icons icon-cell">{{asIconValue(row, col)}}</i>
<img *ngIf="!isIconValue(row, col)"
class="image-cell"
alt="{{iconAltTextKey(data.getValue(row, col))|translate}}"
src="{{data.getValue(row, col)}}"
(error)="onImageLoadingError($event)">
</div>
<div *ngSwitchCase="'date'" class="cell-value" [attr.data-automation-id]="'date_' + data.getValue(row, col)">
{{data.getValue(row, col)}}
</div>
<div *ngSwitchCase="'text'" class="cell-value" [attr.data-automation-id]="'text_' + data.getValue(row, col)">
{{data.getValue(row, col)}}
</div>
<span *ngSwitchDefault class="cell-value">
<!-- empty cell for unknown column type -->
</span>
<ng-container [ngSwitch]="col.type">
<div *ngSwitchCase="'image'" class="cell-value">
<i *ngIf="isIconValue(row, col)" class="material-icons icon-cell">{{asIconValue(row, col)}}</i>
<img *ngIf="!isIconValue(row, col)"
class="image-cell"
alt="{{iconAltTextKey(data.getValue(row, col))|translate}}"
src="{{data.getValue(row, col)}}"
(error)="onImageLoadingError($event)">
</div>
<div *ngSwitchCase="'date'" class="cell-value" [attr.data-automation-id]="'date_' + data.getValue(row, col)">
{{data.getValue(row, col)}}
</div>
<div *ngSwitchCase="'text'" class="cell-value" [attr.data-automation-id]="'text_' + data.getValue(row, col)">
{{data.getValue(row, col)}}
</div>
<span *ngSwitchDefault class="cell-value">
<!-- empty cell for unknown column type -->
</span>
</ng-container>
</div>
<div *ngIf="col.template" class="cell-container">
<template ngFor [ngForOf]="[{ data: data, row: row, col: col }]" [ngForTemplate]="col.template"></template>
<ng-container
[ngTemplateOutlet]="col.template"
[ngOutletContext]="{ $implicit: { data: data, row: row, col: col }, value: data.getValue(row, col) }">
</ng-container>
</div>
</td>
@ -90,7 +95,7 @@
</button>
<ul alfresco-mdl-menu class="mdl-menu--bottom-right"
[attr.for]="'action_menu_' + idx">
<li class="mdl-menu__item"
<li class="mdl-menu__item" [attr.disabled]="action.disabled"
[attr.data-automation-id]="action.title"
*ngFor="let action of getRowActions(row)"
(click)="onExecuteRowAction(row, action)">

View File

@ -414,4 +414,87 @@ describe('DataTable', () => {
dataTable.onImageLoadingError(event);
expect(event.target.src).toBe(originalSrc);
});
it('should disable the action if there is no permission and disableWithNoPermission true', () => {
dataTable.data = new ObjectDataTableAdapter(
[{id: 1, name: 'xyz', allowableOperations: ['create', 'update']}],
[]
);
let row = dataTable.data.getRows();
let actions = [
{
disableWithNoPermission: true,
permission: 'delete',
target: 'folder',
title: 'action2'
}
];
let updateActions = dataTable.checkPermissions(row[0], actions);
expect(updateActions[0].disabled).toBe(true);
});
it('should not disable the action if there is no permission and disableWithNoPermission false', () => {
dataTable.data = new ObjectDataTableAdapter(
[{id: 1, name: 'xyz', allowableOperations: ['create', 'update']}],
[]
);
let row = dataTable.data.getRows();
let actions = [
{
disableWithNoPermission: false,
permission: 'delete',
target: 'folder',
title: 'action2'
}
];
let updateActions = dataTable.checkPermissions(row[0], actions);
expect(updateActions[0].disabled).toBeUndefined();
});
it('should not disable the action if there is the right permission', () => {
dataTable.data = new ObjectDataTableAdapter(
[{ id: 1, name: 'xyz', allowableOperations: ['create', 'update', 'delete'] }],
[]
);
let row = dataTable.data.getRows();
let actions = [
{
permission: 'delete',
target: 'folder',
title: 'action2'
}
];
let updateActions = dataTable.checkPermissions(row[0], actions);
expect(updateActions[0].disabled).toBeUndefined();
});
it('should not disable the action if there are no permissions', () => {
dataTable.data = new ObjectDataTableAdapter(
[{id: 1, name: 'xyz', allowableOperations: null}],
[]
);
let row = dataTable.data.getRows();
let actions = [
{
permission: 'delete',
target: 'folder',
title: 'action2'
}
];
let updateActions = dataTable.checkPermissions(row[0], actions);
expect(updateActions[0].disabled).toBeUndefined();
});
});

View File

@ -236,10 +236,40 @@ export class DataTableComponent implements AfterContentInit, OnChanges {
getRowActions(row: DataRow, col: DataColumn): any[] {
let event = new DataCellEvent(row, col, []);
this.showRowActionsMenu.emit(event);
return event.value.actions;
return this.checkPermissions(row, event.value.actions);
}
checkPermissions(row: DataRow, actions: any[]) {
let actionsPermission = [];
actions.forEach((action) => {
actionsPermission.push(this.checkPermission(row, action));
});
return actionsPermission;
}
checkPermission(row: DataRow, action) {
if (action.permission) {
if (this.hasPermissions(row)) {
let permissions = row.getValue('allowableOperations');
let findPermission = permissions.find(permission => permission === action.permission);
if (!findPermission && action.disableWithNoPermission === true) {
action.disabled = true;
}
}
}
return action;
}
private hasPermissions(row: DataRow): boolean {
return row.getValue('allowableOperations') ? true : false;
}
onExecuteRowAction(row: DataRow, action: any) {
this.executeRowAction.emit(new DataRowActionEvent(row, action));
if (action.disabled) {
event.stopPropagation();
} else {
this.executeRowAction.emit(new DataRowActionEvent(row, action));
}
}
}

View File

@ -34,7 +34,6 @@
}
.mdl-paging__per-page-value {
position: absolute;
right: 36px;
top: 6px;
}

View File

@ -37,6 +37,10 @@
Before you start using this development framework, make sure you have installed all required software and done all the
necessary configuration [prerequisites](https://github.com/Alfresco/alfresco-ng2-components/blob/master/PREREQUISITES.md).
## See also
- [Walkthrough: adding indicators to clearly highlight information about a node](docs/metadata-indicators.md)
## Install
Follow the 3 steps below:
@ -612,6 +616,61 @@ The following action handlers are provided out-of-box:
All system handler names are case-insensitive, `handler="download"` and `handler="DOWNLOAD"`
will trigger the same `download` action.
##### Delete - Show notification message with no permission
You can show a notification error when the user don't have the right permission to perform the action.
The ContentActionComponent provides the event permissionEvent that is raised when the permission specified in the permission property is missing
You can subscribe to this event from your component and use the NotificationService to show a message.
```html
<alfresco-document-list ...>
<content-actions>
<content-action
target="document"
title="Delete"
permission="delete"
(permissionEvent)="onPermissionsFailed($event)"
handler="delete">
</content-action>
</content-actions>
</alfresco-document-list>
export class MyComponent {
onPermissionsFailed(event: any) {
this.notificationService.openSnackMessage(`you don't have the ${event.permission} permission to ${event.action} the ${event.type} `, 4000);
}
}
```
![Delete show notification message](docs/assets/content-action-notification-message.png)
##### Delete - Disable button checking the permission
You can easily disable a button when the user doesn't own the permission to perform the action related to the button.
The ContentActionComponent provides the property permission that must contain the permission to check and a property disableWithNoPermission that can be true if
you want see the button disabled.
```html
<alfresco-document-list ...>
<content-actions>
<content-action
target="document"
title="Delete"
permission="delete"
disableWithNoPermission="true"
handler="delete">
</content-action>
</content-actions>
</alfresco-document-list>
```
![Delete disable action button](docs/assets/content-action-disable-delete-button.png)
##### Download
Initiates download of the corresponding document file.
@ -632,7 +691,6 @@ Initiates download of the corresponding document file.
![Download document action](docs/assets/document-action-download.png)
#### Folder actions
Folder actions have the same declaration as document actions except ```taget="folder"``` attribute value.
@ -725,6 +783,7 @@ DocumentList emits the following events:
| `nodeDblClick` | emitted when user double-clicks list node |
| `folderChange` | emitted once current display folder has changed |
| `preview` | emitted when user acts upon files with either single or double click (depends on `navigation-mode`), recommended for Viewer components integration |
| `permissionError` | emitted when user is attempting to create a folder via action menu but it doesn't have the permission to do it |
## Advanced usage and customization

View File

@ -36,10 +36,10 @@
"systemjs": "0.19.27",
"zone.js": "0.6.26",
"ng2-translate": "2.5.0",
"alfresco-js-api": "~1.3.0",
"ng2-alfresco-core": "1.3.0",
"ng2-alfresco-datatable": "1.3.0",
"ng2-alfresco-documentlist": "1.3.0",
"alfresco-js-api": "~1.4.0",
"ng2-alfresco-core": "1.4.0",
"ng2-alfresco-datatable": "1.4.0",
"ng2-alfresco-documentlist": "1.4.0",
"material-design-icons": "2.2.3",
"material-design-lite": "1.2.1",
"intl": "^1.2.5"

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -0,0 +1,132 @@
# Walkthrough: adding indicators to clearly highlight information about a node
Every node object in the document list holds metadata information.
All metadata is stored inside `properties` property.
Here's an example of basic image-related metadata fetched from the server:
![](assets/metadata-01.png)
## Custom column template
```html
<alfresco-document-list ...>
<data-columns>
<data-column key="properties" [sortable]="false">
<template let-value="value">
<adf-metadata-icons [metadata]="value">
</adf-metadata-icons>
</template>
</data-column>
...
</data-columns>
</alfresco-document-list>
```
We are going to declare a column and bind its value to the entire `properties` object of the underlying node. The column will be using our custom `<adf-metadata-icons>` component to display icons based on metadata state.
## MetadataIconsComponent component
Let's create a simple `MetadataIconsComponent` component with a selector set to `adf-metadata-icons` as shown below:
```ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'adf-metadata-icons',
template: `
<div *ngIf="metadata">
<!-- render UI based on metadata -->
</div>
`
})
export class MetadataIconsComponent {
@Input()
metadata: any;
}
```
The component will expose a `metadata` property we can use from the outside and eventually bind data to similar to the following:
```html
<adf-metadata-icons [metadata]="nodeMetadata"></adf-metadata-icons>
```
As you have seen earlier the DataColumn binds to `properties` property of the node, and maps the runtime value as the `value` local variable within the template.
Next we propagate the `value` reference to the `<adf-metadata-icons>` component as `metadata` property.
```html
<data-column key="properties" [sortable]="false">
<template let-value="value">
<adf-metadata-icons [metadata]="value"></adf-metadata-icons>
</template>
</data-column>
```
So once rendered our component will automatically has access to entire set of node metadata. Let's build some visualization of the `cm:versionLabel` propery.
For demonstration purposes we are going to display several icons if underlying node has version `2.0`, and just a plain text version value for all other versions.
```html
<div *ngIf="metadata">
<ng-container *ngIf="metadata['cm:versionLabel'] === '2.0'">
<md-icon>portrait</md-icon>
<md-icon>photo_filter</md-icon>
<md-icon>rotate_90_degrees_ccw</md-icon>
</ng-container>
<div *ngIf="metadata['cm:versionLabel'] !== '2.0'">
{{metadata['cm:versionLabel']}}
</div>
</div>
```
Note: For a list of the icons that can be used with `<md-icon>` component please refer to this resource: [material.io/icons](https://material.io/icons/)
## Testing component
You will need to enable `versioning` feature for the Document List to be able uploading multiple versions of the file instead of renaming duplicates.
Drag and drop any image file to upload it and ensure it has `1.0` displayed in the column:
![](assets/metadata-02.png)
Now drop the same file again to upload a new version of the file.
You should now see icons instead of version label.
![](assets/metadata-03.png)
You can see on the screnshot above that only files with version `2.0` got extra icons.
## Conclusion
The full source code of the component can be found below:
```ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'adf-metadata-icons',
template: `
<div *ngIf="metadata">
<ng-container *ngIf="metadata['cm:versionLabel'] === '2.0'">
<md-icon>portrait</md-icon>
<md-icon>photo_filter</md-icon>
<md-icon>rotate_90_degrees_ccw</md-icon>
</ng-container>
<div *ngIf="metadata['cm:versionLabel'] !== '2.0'">
{{metadata['cm:versionLabel']}}
</div>
</div>
`
})
export class MetadataIconsComponent {
@Input()
metadata: any;
}
```
You can use this idea to build more complex indication experience based on the actual metdata state.

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