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-diagrams
- MODULE=ng2-activiti-analytics - MODULE=ng2-activiti-analytics
- MODULE=ng2-alfresco-userinfo - MODULE=ng2-alfresco-userinfo
- MODULE=ng2-alfresco-social
before_script: before_script:
- if ([ "$MODULE" != "ng2-alfresco-core" ]); then - 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-diagrams
- COMPONENT_NAME: ng2-activiti-analytics - COMPONENT_NAME: ng2-activiti-analytics
- COMPONENT_NAME: ng2-alfresco-userinfo - COMPONENT_NAME: ng2-alfresco-userinfo
- COMPONENT_NAME: ng2-alfresco-social
# Install scripts. (runs after repo cloning) # Install scripts. (runs after repo cloning)
install: 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="/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="/webscript" (click)="hideDrawer()">Webscript</a>
<a class="mdl-navigation__link" href="" routerLink="/tag" (click)="hideDrawer()">Tag</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="/about" (click)="hideDrawer()">About</a>
<a class="mdl-navigation__link" href="" routerLink="/settings" (click)="hideDrawer()">Settings</a> <a class="mdl-navigation__link" href="" routerLink="/settings" (click)="hideDrawer()">Settings</a>
</nav> </nav>

View File

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

View File

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

View File

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

View File

@ -19,20 +19,25 @@
<div class="page-content"> <div class="page-content">
<div class="mdl-grid"> <div class="mdl-grid">
<div class="mdl-cell mdl-cell--2-col task-column mdl-shadow--2dp"> <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"> <div class="list-buttons">
<activiti-start-task <activiti-start-task
[appId]="appId" [appId]="appId"
(onSuccess)="onStartTaskSuccess($event)"> (onSuccess)="onStartTaskSuccess($event)">
</activiti-start-task> </activiti-start-task>
</div> </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 <activiti-tasklist
[appId]="taskFilter?.appId" [appId]="taskFilter?.appId"
[processDefinitionKey]="taskFilter?.filter?.processDefinitionKey" [processDefinitionKey]="taskFilter?.filter?.processDefinitionKey"
@ -58,10 +63,12 @@
</div> </div>
<div class="mdl-cell mdl-cell--7-col task-column mdl-shadow--2dp"> <div class="mdl-cell mdl-cell--7-col task-column mdl-shadow--2dp">
<activiti-task-details #activitidetails <activiti-task-details #activitidetails
[debugMode]="true"
[taskId]="currentTaskId" [taskId]="currentTaskId"
(formCompleted)="onFormCompleted($event)" (formCompleted)="onFormCompleted($event)"
(formContentClicked)="onFormContentClick($event)" (formContentClicked)="onFormContentClick($event)"
(taskCreated)="onTaskCreated($event)"> (taskCreated)="onTaskCreated($event)"
(taskDeleted)="onTaskDeleted($event)">
</activiti-task-details> </activiti-task-details>
</div> </div>
</div> </div>
@ -76,13 +83,6 @@
<div class="page-content"> <div class="page-content">
<div class="mdl-grid"> <div class="mdl-grid">
<div class="mdl-cell mdl-cell--2-col task-column mdl-shadow--2dp"> <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"> <div class="list-buttons">
<button <button
md-raised-button md-raised-button
@ -92,6 +92,17 @@
<span>START PROCESS</span> <span>START PROCESS</span>
</button> </button>
</div> </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 <activiti-process-instance-list
*ngIf="processFilter?.hasFilter()" [appId]="processFilter.appId" *ngIf="processFilter?.hasFilter()" [appId]="processFilter.appId"
[processDefinitionKey]="processFilter.filter.processDefinitionKey" [processDefinitionKey]="processFilter.filter.processDefinitionKey"

View File

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

View File

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

View File

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

View File

@ -15,39 +15,63 @@
* limitations under the License. * 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 { 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 { DocumentActionsService, DocumentListComponent, ContentActionHandler, DocumentActionModel, FolderActionModel } from 'ng2-alfresco-documentlist';
import { FormService } from 'ng2-activiti-form'; import { FormService } from 'ng2-activiti-form';
import { UploadButtonComponent, UploadDragAreaComponent } from 'ng2-alfresco-upload';
@Component({ @Component({
selector: 'files-component', selector: 'files-component',
templateUrl: './files.component.html', templateUrl: './files.component.html',
styleUrls: ['./files.component.css'] 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- // The identifier of a node. You can also use one of these well-known aliases: -my- | -shared- | -root-
currentFolderId: string = '-my-'; currentFolderId: string = '-my-';
errorMessage: string = null; errorMessage: string = null;
fileNodeId: any; fileNodeId: any;
fileShowed: boolean = false; fileShowed: boolean = false;
@Input()
multipleFileUpload: boolean = false; multipleFileUpload: boolean = false;
@Input()
disableWithNoPermission: boolean = false;
@Input()
folderUpload: boolean = false; folderUpload: boolean = false;
@Input()
acceptedFilesTypeShow: boolean = false; acceptedFilesTypeShow: boolean = false;
@Input()
versioning: boolean = false; versioning: boolean = false;
@Input()
acceptedFilesType: string = '.jpg,.pdf,.js'; acceptedFilesType: string = '.jpg,.pdf,.js';
@Input()
enableUpload: boolean = true;
@ViewChild(DocumentListComponent) @ViewChild(DocumentListComponent)
documentList: DocumentListComponent; documentList: DocumentListComponent;
@ViewChild(UploadButtonComponent)
uploadButton: UploadButtonComponent;
@ViewChild(UploadDragAreaComponent)
uploadDragArea: UploadDragAreaComponent;
constructor(private documentActions: DocumentActionsService, constructor(private documentActions: DocumentActionsService,
private authService: AlfrescoAuthenticationService, private authService: AlfrescoAuthenticationService,
private formService: FormService, private formService: FormService,
private logService: LogService, private logService: LogService,
private changeDetector: ChangeDetectorRef, private changeDetector: ChangeDetectorRef,
private router: Router, private router: Router,
private notificationService: NotificationService,
@Optional() private route: ActivatedRoute) { @Optional() private route: ActivatedRoute) {
documentActions.setHandler('my-handler', this.myDocumentActionHandler.bind(this)); 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() { toggleFolder() {
this.multipleFileUpload = false; this.multipleFileUpload = false;
this.folderUpload = !this.folderUpload; this.folderUpload = !this.folderUpload;
return this.folderUpload; return this.folderUpload;
} }
toggleAcceptedFilesType() {
this.acceptedFilesTypeShow = !this.acceptedFilesTypeShow;
return this.acceptedFilesTypeShow;
}
toggleVersioning() {
this.versioning = !this.versioning;
return this.versioning;
}
ngOnInit() { ngOnInit() {
if (this.route) { if (this.route) {
this.route.params.forEach((params: Params) => { 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) { viewActivitiForm(event?: any) {
this.router.navigate(['/activiti/tasksnode', event.value.entry.id]); 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}`); window.alert(`Starting BPM process: ${processDefinition.id}`);
}.bind(this); }.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 { FormViewer } from './activiti/form-viewer.component';
export { WebscriptComponent } from './webscript/webscript.component'; export { WebscriptComponent } from './webscript/webscript.component';
export { TagComponent } from './tag/tag.component'; export { TagComponent } from './tag/tag.component';
export { SocialComponent } from './social/social.component';
export { AboutComponent } from './about/about.component'; export { AboutComponent } from './about/about.component';
export { FilesComponent } from './files/files.component'; export { FilesComponent } from './files/files.component';
export { FormNodeViewer } from './activiti/form-node-viewer.component'; export { FormNodeViewer } from './activiti/form-node-viewer.component';

View File

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

View File

@ -10,18 +10,31 @@
Content Services host URL configuration Content Services host URL configuration
</div> </div>
<nav class="mdl-navigation"> <nav class="mdl-navigation">
<div class="icon material-icons icon-margin">link</div> <i class="icon material-icons icon-margin">link</i>
<input type="text" class="mdl-textfield__input" id="ecmHost" data-automation-id="ecmHost" <div class="mdl-textfield mdl-js-textfield adf-setting-input-padding">
tabindex="1" (change)="onChangeECMHost($event)" value="{{ecmHost}}"/> <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> </nav>
<div class="mdl-card__supporting-text"> <div class="mdl-card__supporting-text">
Process Services host URL configuration Process Services host URL configuration
</div> </div>
<nav class="mdl-navigation"> <nav class="mdl-navigation">
<div class="icon material-icons icon-margin">link</div> <i class="icon material-icons icon-margin">link</i>
<input type="text" class="mdl-textfield__input" id="bpmHost" data-automation-id="bpmHost" <div class="mdl-textfield mdl-js-textfield adf-setting-input-padding">
tabindex="1" (change)="onChangeBPMHost($event)" value="{{bpmHost}}"/> <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> </nav>
</div> </div>
<div class="mdl-card__actions mdl-card--border"> <div class="mdl-card__actions mdl-card--border">

View File

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

View File

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

View File

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

View File

@ -428,6 +428,7 @@ describe('AnalyticsReportParametersComponent', () => {
}); });
describe('When the form is rendered correctly', () => { describe('When the form is rendered correctly', () => {
let validForm: boolean = true;
let values: any = { let values: any = {
dateRange: { dateRange: {
startDate: '2016-09-01', endDate: '2016-10-05' startDate: '2016-09-01', endDate: '2016-10-05'
@ -468,11 +469,17 @@ describe('AnalyticsReportParametersComponent', () => {
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
component.toggleParameters(); component.toggleParameters();
component.reportId = '1'; component.reportId = '1';
spyOn(component, 'isFormValid').and.returnValue(true); spyOn(component, 'isFormValid').and.callFake(() => {
return validForm;
});
fixture.detectChanges(); fixture.detectChanges();
}); });
})); }));
afterEach(() => {
validForm = true;
});
it('Should be able to change the report title', async(() => { it('Should be able to change the report title', async(() => {
let title: HTMLElement = element.querySelector('h4'); let title: HTMLElement = element.querySelector('h4');
title.click(); title.click();
@ -567,6 +574,52 @@ describe('AnalyticsReportParametersComponent', () => {
contentType: 'json' 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, SimpleChanges,
OnDestroy, OnDestroy,
AfterViewChecked, AfterViewChecked,
AfterContentChecked,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { FormGroup, FormBuilder, FormControl } from '@angular/forms'; import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms';
import * as moment from 'moment'; import * as moment from 'moment';
import { AlfrescoTranslationService, LogService, ContentService } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, LogService, ContentService } from 'ng2-alfresco-core';
import { AnalyticsService } from '../services/analytics.service'; import { AnalyticsService } from '../services/analytics.service';
@ -47,7 +48,7 @@ declare let dialogPolyfill: any;
templateUrl: './analytics-report-parameters.component.html', templateUrl: './analytics-report-parameters.component.html',
styleUrls: ['./analytics-report-parameters.component.css'] 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'; public static FORMAT_DATE_ACTIVITI: string = 'YYYY-MM-DD';
@ -102,6 +103,7 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
private reportParamQuery: ReportQuery; private reportParamQuery: ReportQuery;
private reportName: string; private reportName: string;
private hideParameters: boolean = true; private hideParameters: boolean = true;
private formValidState: boolean = false;
constructor(private translateService: AlfrescoTranslationService, constructor(private translateService: AlfrescoTranslationService,
private analyticsService: AnalyticsService, private analyticsService: AnalyticsService,
@ -131,6 +133,9 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
this.isEditable = false; this.isEditable = false;
if (this.reportForm) {
this.reportForm.reset();
}
let reportId = changes['reportId']; let reportId = changes['reportId'];
if (reportId && reportId.currentValue) { if (reportId && reportId.currentValue) {
this.getReportParams(reportId.currentValue); this.getReportParams(reportId.currentValue);
@ -147,42 +152,42 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
parameters.forEach((param: ReportParameterDetailsModel) => { parameters.forEach((param: ReportParameterDetailsModel) => {
switch (param.type) { switch (param.type) {
case 'dateRange' : case 'dateRange' :
formBuilderGroup.dateRange = new FormGroup({}); formBuilderGroup.dateRange = new FormGroup({}, Validators.required);
break; break;
case 'processDefinition': case 'processDefinition':
formBuilderGroup.processDefGroup = new FormGroup({ formBuilderGroup.processDefGroup = new FormGroup({
processDefinitionId: new FormControl() processDefinitionId: new FormControl(null, Validators.required, null)
}); }, Validators.required);
break; break;
case 'duration': case 'duration':
formBuilderGroup.durationGroup = new FormGroup({ formBuilderGroup.durationGroup = new FormGroup({
duration: new FormControl() duration: new FormControl(null, Validators.required, null)
}); }, Validators.required);
break; break;
case 'dateInterval': case 'dateInterval':
formBuilderGroup.dateIntervalGroup = new FormGroup({ formBuilderGroup.dateIntervalGroup = new FormGroup({
dateRangeInterval: new FormControl() dateRangeInterval: new FormControl(null, Validators.required, null)
}); }, Validators.required);
break; break;
case 'boolean': case 'boolean':
formBuilderGroup.typeFilteringGroup = new FormGroup({ formBuilderGroup.typeFilteringGroup = new FormGroup({
typeFiltering: new FormControl() typeFiltering: new FormControl(null, Validators.required, null)
}); }, Validators.required);
break; break;
case 'task': case 'task':
formBuilderGroup.taskGroup = new FormGroup({ formBuilderGroup.taskGroup = new FormGroup({
taskName: new FormControl() taskName: new FormControl(null, Validators.required, null)
}); }, Validators.required);
break; break;
case 'integer': case 'integer':
formBuilderGroup.processInstanceGroup = new FormGroup({ formBuilderGroup.processInstanceGroup = new FormGroup({
slowProcessInstanceInteger: new FormControl() slowProcessInstanceInteger: new FormControl(null, Validators.required, null)
}); }, Validators.required);
break; break;
case 'status': case 'status':
formBuilderGroup.statusGroup = new FormGroup({ formBuilderGroup.statusGroup = new FormGroup({
status: new FormControl() status: new FormControl(null, Validators.required, null)
}); }, Validators.required);
break; break;
default: default:
return; return;
@ -190,6 +195,7 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
}); });
this.reportForm = this.formBuilder.group(formBuilderGroup); this.reportForm = this.formBuilder.group(formBuilderGroup);
this.reportForm.valueChanges.subscribe(data => this.onValueChanged(data)); this.reportForm.valueChanges.subscribe(data => this.onValueChanged(data));
this.reportForm.statusChanges.subscribe(data => this.onStatusChanged(data));
} }
public getReportParams(reportId: string) { 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) { public convertMomentDate(date: string) {
return moment(date, AnalyticsReportParametersComponent.FORMAT_DATE_ACTIVITI, true) return moment(date, AnalyticsReportParametersComponent.FORMAT_DATE_ACTIVITI, true)
.format(AnalyticsReportParametersComponent.FORMAT_DATE_ACTIVITI) + 'T00:00:00.000Z'; .format(AnalyticsReportParametersComponent.FORMAT_DATE_ACTIVITI) + 'T00:00:00.000Z';
@ -346,14 +358,14 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
this.reportName = ''; this.reportName = '';
} }
isFormValid() {
return this.reportForm && this.reportForm.valid && this.reportForm.dirty;
}
isSaveAction() { isSaveAction() {
return this.action === 'Save'; return this.action === 'Save';
} }
isFormValid() {
return this.reportForm && this.reportForm.dirty && this.reportForm.valid;
}
doExport(paramQuery: ReportQuery) { doExport(paramQuery: ReportQuery) {
this.analyticsService.exportReportToCsv(this.reportId, paramQuery).subscribe( this.analyticsService.exportReportToCsv(this.reportId, paramQuery).subscribe(
(data: any) => { (data: any) => {
@ -375,12 +387,17 @@ export class AnalyticsReportParametersComponent implements OnInit, OnChanges, On
} }
ngAfterViewChecked() { ngAfterViewChecked() {
// workaround for MDL issues with dynamic components
if (componentHandler) { if (componentHandler) {
componentHandler.upgradeAllRegistered(); componentHandler.upgradeAllRegistered();
} }
} }
ngAfterContentChecked() {
if (this.reportForm && this.reportForm.valid) {
this.reportForm.markAsDirty();
}
}
toggleParameters() { toggleParameters() {
this.hideParameters = !this.hideParameters; this.hideParameters = !this.hideParameters;
} }

View File

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

View File

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

View File

@ -438,10 +438,33 @@ class MyComponent {
| getRestFieldValuesByProcessId | (processDefinitionId: string, field: string) | Observable\<any\> | | | getRestFieldValuesByProcessId | (processDefinitionId: string, field: string) | Observable\<any\> | |
| getRestFieldValuesColumnByProcessId | (processDefinitionId: string, field: string, column?: string) | Observable\<any\> | | | getRestFieldValuesColumnByProcessId | (processDefinitionId: string, field: string, column?: string) | Observable\<any\> | |
| getRestFieldValuesColumn | (taskId: string, field: string, column?: string) | Observable\<any\> | | | getRestFieldValuesColumn | (taskId: string, field: string, column?: string) | Observable\<any\> | |
| getWorkflowGroups\* | (filter: string, groupId?: string) | Observable\<GroupModel[]\> | | | getWorkflowGroups\ | (filter: string, groupId?: string) | Observable\<GroupModel[]\> | |
| getWorkflowUsers\* | (filter: string, groupId?: string) | Observable\<GroupUserModel[]\> | | | 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 ## See also

View File

@ -56,9 +56,9 @@
"moment": "2.15.1", "moment": "2.15.1",
"md-date-time-picker": "2.2.0", "md-date-time-picker": "2.2.0",
"ng2-translate": "2.5.0", "ng2-translate": "2.5.0",
"alfresco-js-api": "~1.3.0", "alfresco-js-api": "~1.4.0",
"ng2-alfresco-core": "1.3.0", "ng2-alfresco-core": "1.4.0",
"ng2-activiti-form": "1.3.0" "ng2-activiti-form": "1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/jasmine": "^2.2.33", "@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", "name": "ng2-activiti-form",
"description": "Alfresco Activiti Form Component for Angular 2", "description": "Alfresco Activiti Form Component for Angular 2",
"version": "1.3.0", "version": "1.4.0",
"author": "Alfresco Software, Ltd.", "author": "Alfresco Software, Ltd.",
"scripts": { "scripts": {
"clean": "npm install rimraf && npm run clean-build && rimraf dist node_modules typings", "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": "2.2.2",
"@angular/platform-browser-dynamic": "2.2.2", "@angular/platform-browser-dynamic": "2.2.2",
"@angular/router": "3.2.2", "@angular/router": "3.2.2",
"alfresco-js-api": "~1.3.0", "alfresco-js-api": "~1.4.0",
"core-js": "2.4.1", "core-js": "2.4.1",
"hammerjs": "2.0.8", "hammerjs": "2.0.8",
"md-date-time-picker": "2.2.0", "md-date-time-picker": "2.2.0",
"moment": "2.15.1", "moment": "2.15.1",
"ng2-alfresco-core": "1.3.0", "ng2-alfresco-core": "1.4.0",
"ng2-translate": "2.5.0", "ng2-translate": "2.5.0",
"reflect-metadata": "0.1.10", "reflect-metadata": "0.1.10",
"rxjs": "5.0.0-beta.12", "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'; import { WidgetVisibilityService } from './../services/widget-visibility.service';
declare let dialogPolyfill: any;
declare var componentHandler: any; declare var componentHandler: any;
/** /**
@ -118,7 +117,7 @@ export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges {
showSaveButton: boolean = true; showSaveButton: boolean = true;
@Input() @Input()
showDebugButton: boolean = true; showDebugButton: boolean = false;
@Input() @Input()
readOnly: boolean = false; readOnly: boolean = false;

View File

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

View File

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

View File

@ -42,30 +42,7 @@
</div> </div>
</div> </div>
<div *ngSwitchCase="'dynamic-table'"> <div *ngSwitchCase="'dynamic-table'">
<dynamic-table-widget [field]="field" [readOnly]="!tableEditable"></dynamic-table-widget>
<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>
</div> </div>
<div *ngSwitchCase="'upload'"> <div *ngSwitchCase="'upload'">
<div *ngIf="hasFile" class="mdl-grid"> <div *ngIf="hasFile" class="mdl-grid">

View File

@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { CoreModule, LogServiceMock } from 'ng2-alfresco-core'; import { CoreModule, LogServiceMock } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
@ -25,7 +26,6 @@ import { EcmModelService } from '../../../services/ecm-model.service';
import { FormFieldModel } from './../core/form-field.model'; import { FormFieldModel } from './../core/form-field.model';
import { FormFieldTypes } from '../core/form-field-types'; import { FormFieldTypes } from '../core/form-field-types';
import { FormModel } from '../core/form.model'; import { FormModel } from '../core/form.model';
import { DynamicTableColumn, DynamicTableRow } from './../dynamic-table/dynamic-table.widget.model';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service'; import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
describe('DisplayValueWidget', () => { describe('DisplayValueWidget', () => {
@ -441,6 +441,65 @@ describe('DisplayValueWidget', () => {
expect(widget.value).toBe('<invalid value>'); 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', () => { it('should not setup [DATE] field when missing value', () => {
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(null, {
type: FormFieldTypes.DISPLAY_VALUE, type: FormFieldTypes.DISPLAY_VALUE,
@ -549,135 +608,6 @@ describe('DisplayValueWidget', () => {
expect(widget.value).toBe(value); 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', () => { describe('UI check', () => {
let widgetUI: DisplayValueWidget; let widgetUI: DisplayValueWidget;
let fixture: ComponentFixture<DisplayValueWidget>; let fixture: ComponentFixture<DisplayValueWidget>;
@ -689,12 +619,16 @@ describe('DisplayValueWidget', () => {
window['componentHandler'] = componentHandler; window['componentHandler'] = componentHandler;
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CoreModule], imports: [CoreModule],
declarations: [DisplayValueWidget, ActivitiContent], declarations: [
DisplayValueWidget,
ActivitiContent
],
providers: [ providers: [
EcmModelService, EcmModelService,
FormService, FormService,
WidgetVisibilityService WidgetVisibilityService
] ],
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
}).compileComponents().then(() => { }).compileComponents().then(() => {
fixture = TestBed.createComponent(DisplayValueWidget); fixture = TestBed.createComponent(DisplayValueWidget);
widgetUI = fixture.componentInstance; widgetUI = fixture.componentInstance;

View File

@ -22,8 +22,8 @@ import { WidgetComponent } from './../widget.component';
import { FormFieldTypes } from '../core/form-field-types'; import { FormFieldTypes } from '../core/form-field-types';
import { FormService } from '../../../services/form.service'; import { FormService } from '../../../services/form.service';
import { FormFieldOption } from './../core/form-field-option'; 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 { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { NumberFieldValidator } from '../core/form-field-validator';
@Component({ @Component({
moduleId: module.id, moduleId: module.id,
@ -42,9 +42,7 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
linkText: string; linkText: string;
// dynamic table // dynamic table
rows: DynamicTableRow[] = []; tableEditable = false;
columns: DynamicTableColumn[] = [];
visibleColumns: DynamicTableColumn[] = [];
// upload/attach // upload/attach
hasFile: boolean = false; hasFile: boolean = false;
@ -64,6 +62,10 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
if (this.field.params['showDocumentContent'] !== undefined) { if (this.field.params['showDocumentContent'] !== undefined) {
this.showDocumentContent = !!this.field.params['showDocumentContent']; this.showDocumentContent = !!this.field.params['showDocumentContent'];
} }
if (this.field.params['tableEditable'] !== undefined) {
this.tableEditable = !!this.field.params['tableEditable'];
}
let originalField = this.field.params['field']; let originalField = this.field.params['field'];
if (originalField && originalField.type) { if (originalField && originalField.type) {
this.fieldType = originalField.type; this.fieldType = originalField.type;
@ -115,10 +117,15 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
break; break;
case FormFieldTypes.DATE: case FormFieldTypes.DATE:
if (this.value) { if (this.value) {
let d = moment(this.value.split('T')[0], 'YYYY-M-D'); let dateValue;
if (d.isValid()) { if (NumberFieldValidator.isNumber(this.value)) {
const displayFormat = originalField['dateDisplayFormat'] || this.field.defaultDateFormat; dateValue = moment(this.value);
this.value = d.format(displayFormat); } 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; break;
@ -132,16 +139,6 @@ export class DisplayValueWidget extends WidgetComponent implements OnInit {
this.linkUrl = this.getHyperlinkUrl(this.field); this.linkUrl = this.getHyperlinkUrl(this.field);
this.linkText = this.getHyperlinkText(this.field); this.linkText = this.getHyperlinkText(this.field);
break; 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: default:
this.value = this.field.value; this.value = this.field.value;
break; 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

@ -26,7 +26,7 @@
</table> </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" <button class="mdl-button mdl-js-button mdl-button--icon"
[disabled]="!hasSelection()" [disabled]="!hasSelection()"
(click)="moveSelectionUp()"> (click)="moveSelectionUp()">

View File

@ -54,9 +54,9 @@ export class DynamicTableModel extends FormWidgetModel {
this.field = field; this.field = field;
if (field.json) { if (field.json) {
const columns = this.getColumns(field);
if (field.json.columnDefinitions) { if (columns) {
this.columns = field.json.columnDefinitions.map(obj => <DynamicTableColumn> obj); this.columns = columns;
this.visibleColumns = this.columns.filter(col => col.visible); 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() { flushValue() {
if (this.field) { if (this.field) {
this.field.value = this.rows.map(r => r.value); this.field.value = this.rows.map(r => r.value);

View File

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

View File

@ -15,23 +15,46 @@
* limitations under the License. * limitations under the License.
*/ */
import { TestBed } from '@angular/core/testing'; import { ReflectiveInjector } from '@angular/core';
import {
AlfrescoAuthenticationService,
AlfrescoSettingsService,
AlfrescoApiService,
StorageService,
LogService
} from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx'; import { Observable } from 'rxjs/Rx';
import { CoreModule, AlfrescoApiService, LogService, LogServiceMock } from 'ng2-alfresco-core';
import { FormService } from './form.service'; import { FormService } from './form.service';
import { Response, ResponseOptions } from '@angular/http';
import { EcmModelService } from './ecm-model.service'; import { EcmModelService } from './ecm-model.service';
import { FormDefinitionModel } from '../models/form-definition.model'; import { FormDefinitionModel } from '../models/form-definition.model';
import { Response, ResponseOptions } from '@angular/http';
declare let jasmine: any; declare let jasmine: any;
describe('FormService', () => { let fakeGroupResponse = {
'size': 2,
'total': 2,
'start': 0,
'data': [{
'id': 2004,
'name': 'PEOPLE_GROUP',
'externalId': null,
'status': 'active',
'groups': null
}, { 'id': 2005, 'name': 'PEOPLE_GROUP_2', 'externalId': null, 'status': 'active', 'groups': null }]
};
let responseBody: any; let fakePeopleResponse = {
let service: FormService; 'size': 3,
let apiService: AlfrescoApiService; 'total': 3,
let logService: LogService; 'start': 0,
let bpmCli: any; 'data': [{ 'id': 2002, 'firstName': 'Peo', 'lastName': 'Ple', 'email': 'people' }, {
'id': 2003,
'firstName': 'Peo02',
'lastName': 'Ple02',
'email': 'people02'
}, { 'id': 2004, 'firstName': 'Peo03', 'lastName': 'Ple03', 'email': 'people03' }]
};
function createFakeBlob() { function createFakeBlob() {
let data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; let data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
@ -44,21 +67,26 @@ describe('FormService', () => {
return new Blob([bytes], { type: 'image/png' }); return new Blob([bytes], { type: 'image/png' });
} }
describe('Form service', () => {
let service, injector, apiService, logService;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ injector = ReflectiveInjector.resolveAndCreate([
imports: [ AlfrescoSettingsService,
CoreModule.forRoot() AlfrescoApiService,
], AlfrescoAuthenticationService,
providers: [
EcmModelService, EcmModelService,
StorageService,
FormService, FormService,
{ provide: LogService, useClass: LogServiceMock } LogService
] ]);
}); });
service = TestBed.get(FormService);
apiService = TestBed.get(AlfrescoApiService); beforeEach(() => {
bpmCli = apiService.getInstance().bpmAuth; service = injector.get(FormService);
logService = TestBed.get(LogService); apiService = injector.get(AlfrescoApiService);
logService = injector.get(LogService);
}); });
beforeEach(() => { beforeEach(() => {
@ -69,14 +97,51 @@ describe('FormService', () => {
jasmine.Ajax.uninstall(); jasmine.Ajax.uninstall();
}); });
it('should fetch and parse process definitions', (done) => { describe('Content tests', () => {
responseBody = {
let responseBody = {
data: [ data: [
{ id: '1' }, { id: '1' },
{ id: '2' } { id: '2' }
] ]
}; };
let values = {
field1: 'one',
field2: 'two'
};
let simpleResponseBody = { id: 1, modelType: 'test' };
let fileContentPdfResponseBody = {
id: 999,
name: 'fake-name.pdf',
created: '2017-01-23T12:12:53.219+0000',
createdBy: { id: 2, firstName: 'fake-admin', lastName: 'fake-last', 'email': 'fake-admin' },
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'application/pdf',
simpleType: 'pdf',
previewStatus: 'created',
thumbnailStatus: 'created'
};
let fileContentJpgResponseBody = {
id: 888,
name: 'fake-name.jpg',
created: '2017-01-23T12:12:53.219+0000',
createdBy: { id: 2, firstName: 'fake-admin', lastName: 'fake-last', 'email': 'fake-admin' },
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'image/jpeg',
simpleType: 'image',
previewStatus: 'unsupported',
thumbnailStatus: 'unsupported'
};
it('should fetch and parse process definitions', (done) => {
service.getProcessDefinitions().subscribe(result => { service.getProcessDefinitions().subscribe(result => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/process-definitions')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/process-definitions')).toBeTruthy();
expect(result).toEqual(JSON.parse(jasmine.Ajax.requests.mostRecent().response).data); expect(result).toEqual(JSON.parse(jasmine.Ajax.requests.mostRecent().response).data);
@ -91,13 +156,6 @@ describe('FormService', () => {
}); });
it('should fetch and parse tasks', (done) => { it('should fetch and parse tasks', (done) => {
responseBody = {
data: [
{id: '1'},
{id: '2'}
]
};
service.getTasks().subscribe(result => { service.getTasks().subscribe(result => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/tasks/query')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/tasks/query')).toBeTruthy();
expect(result).toEqual(JSON.parse(jasmine.Ajax.requests.mostRecent().response).data); expect(result).toEqual(JSON.parse(jasmine.Ajax.requests.mostRecent().response).data);
@ -112,29 +170,20 @@ describe('FormService', () => {
}); });
it('should fetch and parse the task by id', (done) => { it('should fetch and parse the task by id', (done) => {
responseBody = {
id: '1'
};
service.getTask('1').subscribe(result => { service.getTask('1').subscribe(result => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/tasks/1')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/tasks/1')).toBeTruthy();
expect(result.id).toEqual(responseBody.id); expect(result.id).toEqual('1');
done(); done();
}); });
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200, 'status': 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify(responseBody) responseText: JSON.stringify({ id: '1' })
}); });
}); });
it('should save task form', (done) => { it('should save task form', (done) => {
let values = {
field1: 'one',
field2: 'two'
};
service.saveTaskForm('1', values).subscribe(() => { service.saveTaskForm('1', values).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1/save-form')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1/save-form')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field1).toEqual(values.field1); expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field1).toEqual(values.field1);
@ -150,11 +199,6 @@ describe('FormService', () => {
}); });
it('should complete task form', (done) => { it('should complete task form', (done) => {
let values = {
field1: 'one',
field2: 'two'
};
service.completeTaskForm('1', values).subscribe(() => { service.completeTaskForm('1', values).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field1).toEqual(values.field1); expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field1).toEqual(values.field1);
@ -170,11 +214,6 @@ describe('FormService', () => {
}); });
it('should complete task form with a specific outcome', (done) => { it('should complete task form with a specific outcome', (done) => {
let values = {
field1: 'one',
field2: 'two'
};
service.completeTaskForm('1', values, 'custom').subscribe(() => { service.completeTaskForm('1', values, 'custom').subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field2).toEqual(values.field2); expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field2).toEqual(values.field2);
@ -191,41 +230,37 @@ describe('FormService', () => {
}); });
it('should get task form by id', (done) => { it('should get task form by id', (done) => {
responseBody = {id: 1};
service.getTaskForm('1').subscribe(result => { service.getTaskForm('1').subscribe(result => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1')).toBeTruthy();
expect(result.id).toEqual(responseBody.id); expect(result.id).toEqual(1);
done(); done();
}); });
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200, 'status': 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify(responseBody) responseText: JSON.stringify({ id: 1 })
}); });
}); });
it('should get form definition by id', (done) => { it('should get form definition by id', (done) => {
responseBody = {id: 1};
service.getFormDefinitionById('1').subscribe(result => { service.getFormDefinitionById('1').subscribe(result => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/form-models/1')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/form-models/1')).toBeTruthy();
expect(result.id).toEqual(responseBody.id); expect(result.id).toEqual(1);
done(); done();
}); });
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200, 'status': 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify(responseBody) responseText: JSON.stringify({ id: 1 })
}); });
}); });
it('should get form definition id by name', (done) => { it('should get form definition id by name', (done) => {
const formName = 'form1'; const formName = 'form1';
const formId = 1; const formId = 1;
responseBody = { let response = {
data: [ data: [
{ id: formId } { id: formId }
] ]
@ -240,7 +275,7 @@ describe('FormService', () => {
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200, 'status': 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify(responseBody) responseText: JSON.stringify(response)
}); });
}); });
@ -287,54 +322,56 @@ describe('FormService', () => {
}); });
it('should handle error with generic message', () => { it('should handle error with generic message', () => {
spyOn(logService, 'error').and.stub(); service.handleError(null).subscribe(() => {
}, (error) => {
service.handleError(null); expect(error).toBe(FormService.UNKNOWN_ERROR_MESSAGE);
expect(logService.error).toHaveBeenCalledWith(FormService.UNKNOWN_ERROR_MESSAGE); });
}); });
it('should handle error with error message', () => { it('should handle error with error message', () => {
spyOn(logService, 'error').and.stub();
const message = '<error>'; const message = '<error>';
service.handleError({message: message});
expect(logService.error).toHaveBeenCalledWith(message); service.handleError({ message: message }).subscribe(() => {
}, (error) => {
expect(error).toBe(message);
});
}); });
it('should handle error with detailed message', () => { it('should handle error with detailed message', () => {
spyOn(logService, 'error').and.stub();
service.handleError({ service.handleError({
status: '400', status: '400',
statusText: 'Bad request' statusText: 'Bad request'
}).subscribe(
() => {
},
(error) => {
expect(error).toBe('400 - Bad request');
}); });
expect(logService.error).toHaveBeenCalledWith('400 - Bad request');
}); });
it('should handle error with generic message', () => { it('should handle error with generic message', () => {
spyOn(logService, 'error').and.stub(); service.handleError({}).subscribe(() => {
service.handleError({}); }, (error) => {
expect(logService.error).toHaveBeenCalledWith(FormService.GENERIC_ERROR_MESSAGE); expect(error).toBe(FormService.GENERIC_ERROR_MESSAGE);
});
}); });
it('should get all the forms with modelType=2', (done) => { it('should get all the forms with modelType=2', (done) => {
responseBody = {};
service.getForms().subscribe(result => { service.getForms().subscribe(result => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('models?modelType=2')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('models?modelType=2')).toBeTruthy();
expect(result).toEqual(responseBody); expect(result).toEqual({});
done(); done();
}); });
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200, 'status': 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify(responseBody) responseText: JSON.stringify({})
}); });
}); });
it('should search for Form with modelType=2', (done) => { it('should search for Form with modelType=2', (done) => {
responseBody = {data: [{id: 1, name: 'findme'}, {id: 2, name: 'testform'}]}; let response = { data: [{ id: 1, name: 'findme' }, { id: 2, name: 'testform' }] };
service.searchFrom('findme').subscribe(result => { service.searchFrom('findme').subscribe(result => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('models?modelType=2')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('models?modelType=2')).toBeTruthy();
@ -346,13 +383,11 @@ describe('FormService', () => {
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200, 'status': 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify(responseBody) responseText: JSON.stringify(response)
}); });
}); });
it('should create a Form with modelType=2', (done) => { it('should create a Form with modelType=2', (done) => {
responseBody = {id: 1, modelType: 'test'};
service.createForm('testName').subscribe(result => { service.createForm('testName').subscribe(result => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/models')).toBeTruthy(); expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/models')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).modelType).toEqual(2); expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).modelType).toEqual(2);
@ -363,13 +398,11 @@ describe('FormService', () => {
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200, 'status': 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify(responseBody) responseText: JSON.stringify(simpleResponseBody)
}); });
}); });
it('should add form fields to a form', (done) => { it('should add form fields to a form', (done) => {
responseBody = {id: 1, modelType: 'test'};
let formId = '100'; let formId = '100';
let name = 'testName'; let name = 'testName';
let data = [{ name: 'name' }, { name: 'email' }]; let data = [{ name: 'name' }, { name: 'email' }];
@ -384,25 +417,12 @@ describe('FormService', () => {
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200, 'status': 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify(responseBody) responseText: JSON.stringify(simpleResponseBody)
}); });
}); });
it('should return the unsupported content when the file is an image', (done) => { it('should return the unsupported content when the file is an image', (done) => {
let contentId: number = 999; let contentId: number = 888;
responseBody = {
id: contentId,
name: 'fake-name.jpg',
created: '2017-01-23T12:12:53.219+0000',
createdBy: {id: 2, firstName: 'fake-admin', lastName: 'fake-last', 'email': 'fake-admin'},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'image/jpeg',
simpleType: 'image',
previewStatus: 'unsupported',
thumbnailStatus: 'unsupported'
};
service.getFileContent(contentId).subscribe(result => { service.getFileContent(contentId).subscribe(result => {
expect(result.id).toEqual(contentId); expect(result.id).toEqual(contentId);
@ -415,25 +435,12 @@ describe('FormService', () => {
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200, 'status': 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify(responseBody) responseText: JSON.stringify(fileContentJpgResponseBody)
}); });
}); });
it('should return the supported content when the file is a pdf', (done) => { it('should return the supported content when the file is a pdf', (done) => {
let contentId: number = 888; let contentId: number = 999;
responseBody = {
id: contentId,
name: 'fake-name.pdf',
created: '2017-01-23T12:12:53.219+0000',
createdBy: {id: 2, firstName: 'fake-admin', lastName: 'fake-last', 'email': 'fake-admin'},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'application/pdf',
simpleType: 'pdf',
previewStatus: 'created',
thumbnailStatus: 'created'
};
service.getFileContent(contentId).subscribe(result => { service.getFileContent(contentId).subscribe(result => {
expect(result.id).toEqual(contentId); expect(result.id).toEqual(contentId);
@ -446,22 +453,20 @@ describe('FormService', () => {
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200, 'status': 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify(responseBody) responseText: JSON.stringify(fileContentPdfResponseBody)
}); });
}); });
it('should return the raw content URL', () => { it('should return the raw content URL', () => {
let contentId: number = 999; let contentId: number = 999;
let contentRawUrl = service.getFileRawContentUrl(contentId); let contentUrl = service.getFileRawContentUrl(contentId);
expect(contentRawUrl).toEqual(`${bpmCli.basePath}/api/enterprise/content/${contentId}/raw`); expect(contentUrl).toContain(`/api/enterprise/content/${contentId}/raw`);
}); });
it('should return a Blob as thumbnail', (done) => { it('should return a Blob as thumbnail', (done) => {
let contentId: number = 999; let contentId: number = 999;
let blob = createFakeBlob(); let blob = createFakeBlob();
spyOn(service, 'getContentThumbnailUrl').and.returnValue(Observable.of(blob)); spyOn(service, 'getContentThumbnailUrl').and.returnValue(Observable.of(blob));
service.getContentThumbnailUrl(contentId).subscribe(result => { service.getContentThumbnailUrl(contentId).subscribe(result => {
expect(result).toEqual(jasmine.any(Blob)); expect(result).toEqual(jasmine.any(Blob));
expect(result.size).toEqual(48); expect(result.size).toEqual(48);
@ -470,12 +475,46 @@ describe('FormService', () => {
}); });
}); });
it('should return list of people', (done) => {
let fakeFilter: string = 'whatever';
service.getWorkflowUsers(fakeFilter).subscribe(result => {
expect(result).toBeDefined();
expect(result.length).toBe(3);
expect(result[0].id).toBe(2002);
expect(result[0].firstName).toBe('Peo');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakePeopleResponse)
});
});
it('should return list of groups', (done) => {
let fakeFilter: string = 'whatever';
service.getWorkflowGroups(fakeFilter).subscribe(result => {
expect(result).toBeDefined();
expect(result.length).toBe(2);
expect(result[0].id).toBe(2004);
expect(result[0].name).toBe('PEOPLE_GROUP');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGroupResponse)
});
});
it('should create a Form form a Node', (done) => { it('should create a Form form a Node', (done) => {
let nameForm = 'testNode'; let nameForm = 'testNode';
responseBody = {id: 1, modelType: 'test'};
let formId = 100; let formId = 100;
stubCreateForm(); stubCreateForm();
@ -532,4 +571,6 @@ describe('FormService', () => {
}); });
} }
}); });
});
}); });

View File

@ -294,67 +294,34 @@ export class FormService {
return Observable.fromPromise(alfrescoApi.activiti.taskApi.getRestFieldValuesColumn(taskId, field, column)); 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[]> { getWorkflowUsers(filter: string, groupId?: string): Observable<GroupUserModel[]> {
return Observable.create(observer => { let option: any = { filter: filter };
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: 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) { if (groupId) {
url += `&groupId=${groupId}`; option.groupId = groupId;
} }
xhr.open('GET', url, true); return Observable.fromPromise(this.getWorkflowUserApi(option))
xhr.setRequestHeader('Authorization', this.apiService.getInstance().getTicketBpm()); .map((response: any) => <GroupUserModel[]> response.data || [])
xhr.send(); .catch(err => this.handleError(err));
}); }
private getWorkflowUserApi(options: any) {
let alfrescoApi = this.apiService.getInstance();
return alfrescoApi.activiti.usersWorkflowApi.getUsers(options);
}
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));
}
private getWorkflowGroupsApi(options: any) {
let alfrescoApi = this.apiService.getInstance();
return alfrescoApi.activiti.groupsApi.getGroups(options);
} }
getFormId(res: any): string { 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 | | `onError` | Emitted when an error occurs |
| `ilterClick` | Emitted when the user selects a filter from the list | | `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 ### Start Process Button component
Displays a button which in turn displays a dialog when clicked, allowing the user Displays a button which in turn displays a dialog when clicked, allowing the user

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -70,10 +70,34 @@ export class ActivitiProcessService {
.catch(err => this.handleError(err)); .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 * Create and return the default filters
* @param appId * @param appId
* @returns {FilterRepresentationModel[]} * @returns {FilterProcessRepresentationModel[]}
*/ */
public createDefaultFilters(appId: number): Observable<FilterProcessRepresentationModel[]> { public createDefaultFilters(appId: number): Observable<FilterProcessRepresentationModel[]> {
let runnintFilter = this.getRunningFilterInstance(appId); let runnintFilter = this.getRunningFilterInstance(appId);
@ -294,6 +318,14 @@ export class ActivitiProcessService {
return this.apiService.getInstance().activiti.userFiltersApi.createUserProcessInstanceFilter(filter); 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) { private extractData(res: any) {
return res.data || {}; 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 ### Properties
| Name | Description | | Name | Description |
| --- | --- | | --- | --- | --- | --- |
|`appId`| { string } The id of the app. | |`appId`| { string } The id of the app. |
|`processDefinitionKey`| { string } The processDefinitionKey of the process. | |`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> | |`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 | |`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 | |`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 | |`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 | | `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 ```json
[ [
{type: 'text', key: 'id', title: 'Id'}, {"type": "text", "key": "id", "title": "Id"},
{type: 'text', key: 'name', title: 'Name', cssClass: 'full-width name-column', sortable: true}, {"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": "formKey", "title": "Form Key", "sortable": true},
{type: 'text', key: 'created', title: 'Created', 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 | | Name | Type | Required | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `layoutType` | {string} | required | Define the layout of the apps. There are two possible values: GRID or LIST. | | `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 ## Basic usage example Activiti Filter
@ -329,6 +355,27 @@ The component shows all the available filters.
No options 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 ## Basic usage example Activiti Checklist
The component shows the checklist task functionality. The component shows the checklist task functionality.

View File

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

View File

@ -17,22 +17,6 @@
import { AppDefinitionRepresentationModel } from '../models/filter.model'; 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({ export var nonDeployedApps = [new AppDefinitionRepresentationModel({
id: '1', id: '1',
name: '1', name: '1',
@ -46,6 +30,50 @@ export var nonDeployedApps = [new AppDefinitionRepresentationModel({
name: '3', name: '3',
icon: 'icon3' 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({ export var defaultApp = [new AppDefinitionRepresentationModel({
defaultAppId: 'tasks' 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>
</div> </div>
<div class="menu-container" *ngIf="isEmpty()">
{{ 'APPS.NONE' | translate }}
</div>

View File

@ -59,7 +59,7 @@ describe('ActivitiApps', () => {
debugElement = fixture.debugElement; debugElement = fixture.debugElement;
service = fixture.debugElement.injector.get(ActivitiTaskListService); 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', [ componentHandler = jasmine.createSpyObj('componentHandler', [
'upgradeAllRegistered', 'upgradeAllRegistered',
@ -79,6 +79,59 @@ describe('ActivitiApps', () => {
expect(getAppsSpy).toHaveBeenCalled(); 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', () => { it('should emit an error when an error occurs loading apps', () => {
let emitSpy = spyOn(component.error, 'emit'); let emitSpy = spyOn(component.error, 'emit');
getAppsSpy.and.returnValue(Observable.throw({})); getAppsSpy.and.returnValue(Observable.throw({}));
@ -119,7 +172,7 @@ describe('ActivitiApps', () => {
it('should display all deployed apps', () => { it('should display all deployed apps', () => {
getAppsSpy.and.returnValue(Observable.of(deployedApps)); getAppsSpy.and.returnValue(Observable.of(deployedApps));
fixture.detectChanges(); 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', () => { it('should not display undeployed apps', () => {

View File

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

View File

@ -7,9 +7,11 @@
<div class="menu-container" *ngIf="checklist?.length > 0"> <div class="menu-container" *ngIf="checklist?.length > 0">
<ul class='mdl-list'> <ul class='mdl-list'>
<li class="mdl-list__item" *ngFor="let check of checklist"> <li class="mdl-list__item" *ngFor="let check of checklist">
<span class="mdl-list__item-primary-content" id="check-{{check.id}}"> <span class="mdl-chip mdl-chip--deletable" id="check-{{check.id}}">
<i class="material-icons mdl-list__item-icon">done</i> <span class="mdl-chip__text">{{check.name}}</span>
{{check.name}} <button type="button" class="mdl-chip__action" (click)="delete(check.id)">
<i id="remove-{{check.id}}" class="material-icons">cancel</i>
</button>
</span> </span>
</li> </li>
</ul> </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(() => { it('should show load task checklist on change', async(() => {
checklistComponent.taskId = 'new-fake-task-id'; checklistComponent.taskId = 'new-fake-task-id';
checklistComponent.checklist.push(fakeTaskDetail); checklistComponent.checklist.push(fakeTaskDetail);

View File

@ -44,6 +44,9 @@ export class ActivitiChecklist implements OnInit, OnChanges {
@Output() @Output()
checklistTaskCreated: EventEmitter<TaskDetailsModel> = new EventEmitter<TaskDetailsModel>(); checklistTaskCreated: EventEmitter<TaskDetailsModel> = new EventEmitter<TaskDetailsModel>();
@Output()
checklistTaskDeleted: EventEmitter<string> = new EventEmitter<string>();
@ViewChild('dialog') @ViewChild('dialog')
dialog: any; dialog: any;
@ -129,6 +132,17 @@ export class ActivitiChecklist implements OnInit, OnChanges {
this.cancel(); 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() { public cancel() {
if (this.dialog) { if (this.dialog) {
this.dialog.nativeElement.close(); this.dialog.nativeElement.close();

View File

@ -14,6 +14,10 @@
color: rgb(68,138,255); color: rgb(68,138,255);
} }
.activiti-filters__entry:hover {
opacity: 0.8;
}
.activiti-filters__entry.active .activiti-filters__entry-icon { .activiti-filters__entry.active .activiti-filters__entry-icon {
color: rgb(68,138,255); 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" <li class="mdl-list__item activiti-filters__entry" (click)="selectFilter(filter)" *ngFor="let filter of filters"
[class.active]="currentFilter === filter"> [class.active]="currentFilter === filter">
<span class="mdl-list__item-primary-content"> <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}} {{filter.name}}
</span> </span>
</li> </li>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,173 +15,65 @@
* limitations under the License. * limitations under the License.
*/ */
import { TestBed, async } from '@angular/core/testing'; import { ReflectiveInjector } from '@angular/core';
import { CoreModule } from 'ng2-alfresco-core'; import { async } from '@angular/core/testing';
import {
AlfrescoAuthenticationService,
AlfrescoSettingsService,
AlfrescoApiService,
StorageService,
LogService
} from 'ng2-alfresco-core';
import { ActivitiTaskListService } from './activiti-tasklist.service'; import { ActivitiTaskListService } from './activiti-tasklist.service';
import { TaskDetailsModel } from '../models/task-details.model'; import { TaskDetailsModel } from '../models/task-details.model';
import { import {
FilterRepresentationModel, FilterRepresentationModel,
AppDefinitionRepresentationModel,
TaskQueryRequestRepresentationModel TaskQueryRequestRepresentationModel
} from '../models/filter.model'; } from '../models/filter.model';
import { Comment } from '../models/comment.model'; import { Comment } from '../models/comment.model';
import {
fakeFilters,
fakeAppPromise,
fakeAppFilter,
fakeFilter,
fakeTaskList,
fakeErrorTaskList,
fakeTasksComment,
fakeTasksChecklist,
fakeTaskDetails,
fakeUser,
fakeApps,
fakeRepresentationFilter1,
secondFakeTaskList,
fakeRepresentationFilter2,
fakeFormList,
fakeTaskListDifferentProcessDefinitionKey,
fakeFilterWithProcessDefinitionKey
} from '../assets/tasklist-service.mock';
declare let jasmine: any; declare let jasmine: any;
describe('Activiti TaskList Service', () => { describe('Activiti TaskList Service', () => {
let 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' }
}
]
};
let 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' }
}
]
};
let 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
}
]
};
let fakeFilter = {
sort: 'created-desc', text: '', state: 'open', assignment: 'fake-assignee'
};
let fakeUser = { id: 1, email: 'fake-email@dom.com', firstName: 'firstName', lastName: 'lastName' };
let 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'
}
]
};
let 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'
}
]
};
let fakeErrorTaskList = {
error: 'wrong request'
};
let fakeTaskDetails = { id: '999', name: 'fake-task-name', formKey: '99', assignee: fakeUser };
let 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
}
]
};
let 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'
}
]
};
let 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
}
});
let 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
}
});
let fakeAppPromise = new Promise(function (resolve, reject) {
resolve(fakeAppFilter);
});
let service: ActivitiTaskListService; let service: ActivitiTaskListService;
let injector;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ injector = ReflectiveInjector.resolveAndCreate([
imports: [ AlfrescoSettingsService,
CoreModule.forRoot() AlfrescoApiService,
], AlfrescoAuthenticationService,
providers: [ ActivitiTaskListService,
ActivitiTaskListService StorageService,
] LogService
]);
}); });
service = TestBed.get(ActivitiTaskListService);
beforeEach(() => {
service = injector.get(ActivitiTaskListService);
});
beforeEach(() => {
jasmine.Ajax.install(); jasmine.Ajax.install();
}); });
@ -189,6 +81,8 @@ describe('ActivitiTaskListService', () => {
jasmine.Ajax.uninstall(); jasmine.Ajax.uninstall();
}); });
describe('Content tests', () => {
it('should return the task list filters', (done) => { it('should return the task list filters', (done) => {
service.getTaskListFilters().subscribe( service.getTaskListFilters().subscribe(
(res) => { (res) => {
@ -207,6 +101,46 @@ describe('ActivitiTaskListService', () => {
}); });
}); });
it('should return the task filter by id', (done) => {
service.getTaskFilterById('2').subscribe(
(res: FilterRepresentationModel) => {
expect(res).toBeDefined();
expect(res.id).toEqual('2');
expect(res.name).toEqual('FakeMyTasks');
expect(res.filter.sort).toEqual('created-desc');
expect(res.filter.state).toEqual('open');
expect(res.filter.assignment).toEqual('fake-assignee');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeFilters)
});
});
it('should return the task filter by name', (done) => {
service.getTaskFilterByName('FakeMyTasks').subscribe(
(res: FilterRepresentationModel) => {
expect(res).toBeDefined();
expect(res.id).toEqual('2');
expect(res.name).toEqual('FakeMyTasks');
expect(res.filter.sort).toEqual('created-desc');
expect(res.filter.state).toEqual('open');
expect(res.filter.assignment).toEqual('fake-assignee');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeFilters)
});
});
it('should call the api withthe appId', (done) => { it('should call the api withthe appId', (done) => {
spyOn(service, 'callApiTaskFilters').and.returnValue((fakeAppPromise)); spyOn(service, 'callApiTaskFilters').and.returnValue((fakeAppPromise));
@ -257,6 +191,27 @@ describe('ActivitiTaskListService', () => {
}); });
}); });
it('should return the task list filtered by processDefinitionKey', (done) => {
service.getTasks(<TaskQueryRequestRepresentationModel>fakeFilterWithProcessDefinitionKey).subscribe(
res => {
expect(res).toBeDefined();
expect(res.length).toEqual(1);
expect(res[0].name).toEqual('FakeNameTask');
expect(res[0].assignee.email).toEqual('fake-email@dom.com');
expect(res[0].assignee.firstName).toEqual('firstName');
expect(res[0].assignee.lastName).toEqual('lastName');
expect(res[0].processDefinitionKey).toEqual('1');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeTaskListDifferentProcessDefinitionKey)
});
});
it('should throw an exception when the response is wrong', () => { it('should throw an exception when the response is wrong', () => {
service.getTasks(<TaskQueryRequestRepresentationModel>fakeFilter).subscribe( service.getTasks(<TaskQueryRequestRepresentationModel>fakeFilter).subscribe(
(res) => { (res) => {
@ -369,6 +324,19 @@ describe('ActivitiTaskListService', () => {
}); });
}); });
it('should remove a checklist task ', (done) => {
service.deleteTask('999').subscribe(
() => {
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json'
});
});
it('should add a comment task ', (done) => { it('should add a comment task ', (done) => {
service.addTaskComment('999', 'fake-comment-message').subscribe( service.addTaskComment('999', 'fake-comment-message').subscribe(
(res: Comment) => { (res: Comment) => {
@ -646,4 +614,25 @@ describe('ActivitiTaskListService', () => {
}); });
})); }));
it('should get possibile form list', (done) => {
service.getFormList().subscribe(
(res: any) => {
expect(res).toBeDefined();
expect(res.length).toBe(2);
expect(res[0].id).toBe(1);
expect(res[0].name).toBe('form with all widgets');
expect(res[1].id).toBe(2);
expect(res[1].name).toBe('uppy');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeFormList)
});
});
});
}); });

View File

@ -35,7 +35,7 @@ export class ActivitiTaskListService {
} }
/** /**
* Retrive all the Deployed app * Retrieve all the Deployed app
* @returns {Observable<any>} * @returns {Observable<any>}
*/ */
getDeployedApplications(name?: string): 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>} * @returns {Observable<any>}
*/ */
getTaskListFilters(appId?: string): Observable<any> { getTaskListFilters(appId?: string): Observable<any> {
@ -65,6 +65,30 @@ export class ActivitiTaskListService {
}).catch(err => this.handleError(err)); }).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 * Return all the filters in the list where the task id belong
* @param taskId - string * @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 * @param filter - TaskFilterRepresentationModel
* @returns {any} * @returns {any}
*/ */
@ -124,7 +148,7 @@ export class ActivitiTaskListService {
} }
/** /**
* Retrive all the task details * Retrieve all the task details
* @param id - taskId * @param id - taskId
* @returns {<TaskDetailsModel>} * @returns {<TaskDetailsModel>}
*/ */
@ -137,7 +161,7 @@ export class ActivitiTaskListService {
} }
/** /**
* Retrive all the task's comments * Retrieve all the task's comments
* @param id - taskId * @param id - taskId
* @returns {<Comment[]>} * @returns {<Comment[]>}
*/ */
@ -155,7 +179,7 @@ export class ActivitiTaskListService {
} }
/** /**
* Retrive all the task's checklist * Retrieve all the task's checklist
* @param id - taskId * @param id - taskId
* @returns {TaskDetailsModel} * @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} * @returns {TaskDetailsModel}
*/ */
getFormList(): Observable<Form []> { getFormList(): Observable<Form []> {
@ -256,6 +280,15 @@ export class ActivitiTaskListService {
}).catch(err => this.handleError(err)); }).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 * Add a filter
* @param filter - FilterRepresentationModel * @param filter - FilterRepresentationModel
@ -357,6 +390,10 @@ export class ActivitiTaskListService {
return this.apiService.getInstance().activiti.taskApi.addSubtask(task.parentTaskId, task); 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) { private callApiAddFilter(filter: FilterRepresentationModel) {
return this.apiService.getInstance().activiti.userFiltersApi.createUserTaskFilter(filter); 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 { DataColumnListComponent } from './src/components/data-column/data-column-list.component';
import { MATERIAL_DESIGN_DIRECTIVES } from './src/components/material/index'; import { MATERIAL_DESIGN_DIRECTIVES } from './src/components/material/index';
import { CONTEXT_MENU_PROVIDERS, CONTEXT_MENU_DIRECTIVES } from './src/components/context-menu/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/services/index';
export * from './src/components/index'; export * from './src/components/index';
export * from './src/components/data-column/data-column.component'; export * from './src/components/data-column/data-column.component';
export * from './src/components/data-column/data-column-list.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/directives/upload.directive';
export * from './src/utils/index'; export * from './src/utils/index';
export * from './src/events/base.event'; export * from './src/events/base.event';
@ -94,6 +96,7 @@ export function createTranslateLoader(http: Http, logService: LogService) {
declarations: [ declarations: [
...MATERIAL_DESIGN_DIRECTIVES, ...MATERIAL_DESIGN_DIRECTIVES,
...CONTEXT_MENU_DIRECTIVES, ...CONTEXT_MENU_DIRECTIVES,
...COLLAPSABLE_DIRECTIVES,
UploadDirective, UploadDirective,
DataColumnComponent, DataColumnComponent,
DataColumnListComponent DataColumnListComponent
@ -110,6 +113,7 @@ export function createTranslateLoader(http: Http, logService: LogService) {
TranslateModule, TranslateModule,
...MATERIAL_DESIGN_DIRECTIVES, ...MATERIAL_DESIGN_DIRECTIVES,
...CONTEXT_MENU_DIRECTIVES, ...CONTEXT_MENU_DIRECTIVES,
...COLLAPSABLE_DIRECTIVES,
UploadDirective, UploadDirective,
DataColumnComponent, DataColumnComponent,
DataColumnListComponent DataColumnListComponent

View File

@ -1,7 +1,7 @@
{ {
"name": "ng2-alfresco-core", "name": "ng2-alfresco-core",
"description": "Alfresco Angular 2 Components core", "description": "Alfresco Angular 2 Components core",
"version": "1.3.0", "version": "1.4.0",
"author": "Alfresco Software, Ltd.", "author": "Alfresco Software, Ltd.",
"scripts": { "scripts": {
"clean": "npm install rimraf && npm run clean-build && rimraf dist node_modules typings", "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/platform-browser-dynamic": "2.2.2",
"@angular/router": "3.2.2", "@angular/router": "3.2.2",
"@angular/upgrade": "2.2.2", "@angular/upgrade": "2.2.2",
"alfresco-js-api": "~1.3.0", "alfresco-js-api": "~1.4.0",
"core-js": "2.4.1", "core-js": "2.4.1",
"dialog-polyfill": "0.4.7", "dialog-polyfill": "0.4.7",
"element.scrollintoviewifneeded-polyfill": "1.0.1", "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 './context-menu/index';
export * from './material/index'; export * from './material/index';
export * from './collapsable/index';

View File

@ -190,8 +190,20 @@ export class DataTableDemo {
this.data = new ObjectDataTableAdapter( this.data = new ObjectDataTableAdapter(
// data // 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> </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 #### rowClick event
_This event is emitted when user clicks the row._ _This event is emitted when user clicks the row._

View File

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

View File

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

View File

@ -34,14 +34,14 @@
[class.alfresco-datatable__row--selected]="selectedRow === row" [class.alfresco-datatable__row--selected]="selectedRow === row"
[adf-upload]="allowDropFiles" [adf-upload-data]="row"> [adf-upload]="allowDropFiles" [adf-upload-data]="row">
<!-- Actions (right) --> <!-- Actions (left) -->
<td *ngIf="actions && actionsPosition === 'left'" class="alfresco-datatable__actions-cell"> <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"> <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> <i class="material-icons">more_vert</i>
</button> </button>
<ul alfresco-mdl-menu class="mdl-menu--bottom-left" <ul alfresco-mdl-menu class="mdl-menu--bottom-left"
[attr.for]="'action_menu_' + idx"> [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" [attr.data-automation-id]="action.title"
*ngFor="let action of getRowActions(row)" *ngFor="let action of getRowActions(row)"
(click)="onExecuteRowAction(row, action)"> (click)="onExecuteRowAction(row, action)">
@ -53,13 +53,14 @@
<td *ngIf="multiselect"> <td *ngIf="multiselect">
<md-checkbox [(ngModel)]="row.isSelected"></md-checkbox> <md-checkbox [(ngModel)]="row.isSelected"></md-checkbox>
</td> </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}}" class="mdl-data-table__cell--non-numeric non-selectable data-cell {{col.cssClass}}"
(click)="onRowClick(row, $event)" (click)="onRowClick(row, $event)"
(dblclick)="onRowDblClick(row, $event)" (dblclick)="onRowDblClick(row, $event)"
[context-menu]="getContextMenuActions(row, col)" [context-menu]="getContextMenuActions(row, col)"
[context-menu-enabled]="contextMenu"> [context-menu-enabled]="contextMenu">
<div *ngIf="!col.template" class="cell-container"> <div *ngIf="!col.template" class="cell-container">
<ng-container [ngSwitch]="col.type">
<div *ngSwitchCase="'image'" class="cell-value"> <div *ngSwitchCase="'image'" class="cell-value">
<i *ngIf="isIconValue(row, col)" class="material-icons icon-cell">{{asIconValue(row, col)}}</i> <i *ngIf="isIconValue(row, col)" class="material-icons icon-cell">{{asIconValue(row, col)}}</i>
<img *ngIf="!isIconValue(row, col)" <img *ngIf="!isIconValue(row, col)"
@ -77,9 +78,13 @@
<span *ngSwitchDefault class="cell-value"> <span *ngSwitchDefault class="cell-value">
<!-- empty cell for unknown column type --> <!-- empty cell for unknown column type -->
</span> </span>
</ng-container>
</div> </div>
<div *ngIf="col.template" class="cell-container"> <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> </div>
</td> </td>
@ -90,7 +95,7 @@
</button> </button>
<ul alfresco-mdl-menu class="mdl-menu--bottom-right" <ul alfresco-mdl-menu class="mdl-menu--bottom-right"
[attr.for]="'action_menu_' + idx"> [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" [attr.data-automation-id]="action.title"
*ngFor="let action of getRowActions(row)" *ngFor="let action of getRowActions(row)"
(click)="onExecuteRowAction(row, action)"> (click)="onExecuteRowAction(row, action)">

View File

@ -414,4 +414,87 @@ describe('DataTable', () => {
dataTable.onImageLoadingError(event); dataTable.onImageLoadingError(event);
expect(event.target.src).toBe(originalSrc); 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[] { getRowActions(row: DataRow, col: DataColumn): any[] {
let event = new DataCellEvent(row, col, []); let event = new DataCellEvent(row, col, []);
this.showRowActionsMenu.emit(event); 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) { onExecuteRowAction(row: DataRow, action: any) {
if (action.disabled) {
event.stopPropagation();
} else {
this.executeRowAction.emit(new DataRowActionEvent(row, action)); this.executeRowAction.emit(new DataRowActionEvent(row, action));
} }
} }
}

View File

@ -34,7 +34,6 @@
} }
.mdl-paging__per-page-value { .mdl-paging__per-page-value {
position: absolute;
right: 36px; right: 36px;
top: 6px; 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 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). 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 ## Install
Follow the 3 steps below: 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"` All system handler names are case-insensitive, `handler="download"` and `handler="DOWNLOAD"`
will trigger the same `download` action. 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 ##### Download
Initiates download of the corresponding document file. 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) ![Download document action](docs/assets/document-action-download.png)
#### Folder actions #### Folder actions
Folder actions have the same declaration as document actions except ```taget="folder"``` attribute value. 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 | | `nodeDblClick` | emitted when user double-clicks list node |
| `folderChange` | emitted once current display folder has changed | | `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 | | `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 ## Advanced usage and customization

View File

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