为了简化流程引擎配置,支持用户自定义配置,针对Activit Explorer进行定制化
简化侧边节点,重分类,重命名
编辑stencilset.json,在stencils数组中,移除不需要的节点,比如
{
"type": "node",
"id": "StartErrorEvent",
"title": "异常事件",
"description": "A start event that catches a thrown BPMN error",
"view": "\n",
"icon": "startevent/error.png",
"groups": [
"启动事件"
],
"propertyPackages": [
"overrideidpackage",
"namepackage",
"documentationpackage",
"executionlistenerspackage",
"errorrefpackage"
],
"hiddenPropertyPackages": [],
"roles": [
"sequence_start",
"Startevents_all",
"StartEventsMorph",
"all"
]
},
同时,可以修改groups和title,进行自定义分组和命名
简化节点属性
编辑stencilset.json,找到对应的节点,比如"id": "UserTask",将不必要的properties从propertyPackages移动到hiddenPropertyPackages,例:
改动前
{
"type": "node",
"id": "UserTask",
"title": "用户活动",
"description": "分配给特定人的任务 ",
"view": "\n",
"icon": "activity/list/type.user.png",
"groups": [
"活动列表"
],
"propertyPackages": [
"tasklistenerspackage",
"overrideidpackage",
"namepackage",
"documentationpackage",
"asynchronousdefinitionpackage",
"exclusivedefinitionpackage",
"executionlistenerspackage",
"multiinstance_typepackage",
"multiinstance_cardinalitypackage",
"multiinstance_collectionpackage",
"multiinstance_variablepackage",
"multiinstance_conditionpackage",
"isforcompensationpackage",
"usertaskassignmentpackage",
"formkeydefinitionpackage",
"duedatedefinitionpackage",
"prioritydefinitionpackage",
"formpropertiespackage"
],
"hiddenPropertyPackages": [],
"roles": [
"Activity",
"sequence_start",
"sequence_end",
"ActivitiesMorph",
"all"
]
},
改动后
{
"type": "node",
"id": "UserTask",
"title": "人工任务",
"description": "分配给特定人的任务 ",
"view": "\n",
"icon": "activity/list/type.user.png",
"groups": [
"任务"
],
"propertyPackages": [
"namepackage",
"approvalwaypackage",
"approvalrulepackage",
"autocopypackage",
"isskipnobodypackage",
"isskipRepeatpackage",
"executionlistenerspackage",
"multiinstance_typepackage",
"isforcompensationpackage"
],
"hiddenPropertyPackages": [
"tasklistenerspackage",
"overrideidpackage",
"documentationpackage",
"asynchronousdefinitionpackage",
"exclusivedefinitionpackage",
"multiinstance_cardinalitypackage",
"multiinstance_collectionpackage",
"multiinstance_variablepackage",
"multiinstance_conditionpackage",
"usertaskassignmentpackage",
"formkeydefinitionpackage",
"duedatedefinitionpackage",
"prioritydefinitionpackage",
"formpropertiespackage"
],
"roles": [
"Activity",
"sequence_start",
"sequence_end",
"ActivitiesMorph",
"all"
]
},
增加自定义属性
上面的任务任务节点中,其中approvalwaypackage,approvalrulepackage, autocopypackage为自定义属性,在节点的propertyPackages进行引用外,还需要单独定义
{
"name": "approvalwaypackage",
"properties": [
{
"id": "approvalway",
"type": "kisbpm-multiinstance-approvalway",
"title": "审批方式",
"value": "",
"description": "一票通过或拒绝,全部通过",
"popular": true,
"refToView": "multiinstance"
}
]
},
{
"name": "approvalrulepackage",
"properties": [
{
"id": "approvalrule",
"type": "String",
"title": "审批规则",
"value": "",
"description": "指定岗位,指定人,或者部门负责人",
"popular": true
}
]
},
{
"name": "autocopypackage",
"properties": [
{
"id": "autocopy",
"type": "multiplecomplex",
"title": "自动抄送",
"value": "",
"description": "指定岗位,指定人,或者部门负责人",
"popular": true
}
]
},
其中
1、approvalrule指定"type": "String",最为简单,效果如下
2、approvalway指定"type": "kisbpm-multiinstance-approvalway","refToView": "multiinstance",表示为下拉列表,其中这里的kisbpm-multiinstance-approvalway需要额外在两处进行修改,效果如下
Activiti explorer前端使用的是AngularJS
2-1、properties.js 定义显示和写入模板
增加
"kisbpm-multiinstance-approvalway" : {
"readModeTemplateUrl": "editor-app/configuration/properties/multiinstance-property-display-approvalway.html",
"writeModeTemplateUrl": "editor-app/configuration/properties/multiinstance-property-write-approvalway.html"
},
2-2、增加定义的显示和写入模板, AngularJS语法
multiinstance-property-display-approvalway.html
PROPERTY.EMPTY
multiinstance-property-write-approvalway.html
3、autocopypackage指定"type": "multiplecomplex",需要额外增加读、写模板,并在properties.js中定义模板指向,效果如下
3-1、properties.js定义模板
"oryx-autocopy-multiplecomplex": {
"readModeTemplateUrl": "editor-app/configuration/properties/auto-copy-display-template.html",
"writeModeTemplateUrl": "editor-app/configuration/properties/auto-copy-write-template.html"
},
3-2、实现模板
auto-copy-display-template.html
{{'PROPERTY.AUTOCOPY.DISPLAY' | translate:property.value.autocopies}}
PROPERTY.AUTOCOPY.EMPTY
auto-copy-write-template.html
auto-copy-popup.html
实现properties-auto-copy-controller.js,并在modler.html中进行引用
/*
* Activiti Modeler component part of the Activiti project
* Copyright 2005-2014 Alfresco Software, Ltd. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
/*
* Auto copy
*/
var KisBpmAutoCopyCtrl = [ '$scope', '$modal', '$timeout', '$translate', function($scope, $modal, $timeout, $translate) {
// Config for the modal window
var opts = {
template: 'editor-app/configuration/properties/auto-copy-popup.html?version=' + Date.now(),
scope: $scope
};
// Open the dialog
$modal(opts);
}];
var KisBpmAutoCopyPopupCtrl = [ '$scope', '$q', '$translate', function($scope, $q, $translate) {
// Put json representing form properties on scope
if ($scope.property.value !== undefined && $scope.property.value !== null
&& $scope.property.value.autocopies !== undefined
&& $scope.property.value.autocopies !== null) {
if ($scope.property.value.autocopies.constructor == String)
{
$scope.autocopies = JSON.parse($scope.property.value.autocopies);
}
else
{
// Note that we clone the json object rather then setting it directly,
// this to cope with the fact that the user can click the cancel button and no changes should have happened
$scope.autocopies = angular.copy($scope.property.value.autocopies);
}
} else {
$scope.autocopies = [];
}
// Array to contain selected properties (yes - we only can select one, but ng-grid isn't smart enough)
$scope.selectedListeners = [];
$scope.selectedFields = [];
$scope.translationsRetrieved = false;
$scope.labels = {};
var typePromise = $translate('PROPERTY.AUTOCOPY.TYPE');
var assignerPromise = $translate('PROPERTY.AUTOCOPY.ASSIGNER');
var othersettingPromise = $translate('PROPERTY.AUTOCOPY.OTHERSETTING');
$q.all([typePromise, assignerPromise, othersettingPromise]).then(function(results) {
$scope.labels.typeLabel = results[0];
$scope.labels.assignerLabel = results[1];
$scope.labels.othersettingLabel = results[2];
$scope.translationsRetrieved = true;
// Config for grid
$scope.gridOptions = {
data: 'autocopies',
enableRowReordering: true,
headerRowHeight: 28,
multiSelect: false,
keepLastSelected : false,
selectedItems: $scope.selectedListeners,
afterSelectionChange: function (rowItem, event) {
$scope.selectedFields.length = 0;
if ($scope.selectedListeners.length > 0)
{
var fields = $scope.selectedListeners[0].fields;
}
},
columnDefs: [{ field: 'type', displayName: $scope.labels.typeLabel },
{ field: 'assigner', displayName: $scope.labels.assignerLabel},
{ field: 'othersetting', displayName: $scope.labels.othersettingLabel }]
};
this.typeItems = [
{ text: "指定岗位", value: 'station' },
{ text: "指定人", value: 'person' },
{ text: "部门负责人", value: 'deptHeader' },
];
// Config for field grid
$scope.gridFieldOptions = {
data: 'selectedListeners[0].fields',
enableRowReordering: true,
headerRowHeight: 28,
multiSelect: false,
keepLastSelected : false,
selectedItems: $scope.selectedFields,
columnDefs: [{ field: 'type', displayName: $scope.labels.typeLabel,
editorType: DropList,
editorSettings: {items: this.typeItems}},
{ field: 'assigner', displayName: $scope.labels.assignerLabel},
{ field: 'othersetting', displayName: $scope.labels.othersettingLabel }]
};
});
$scope.listenerDetailsChanged = function() {
};
// Click handler for add button
$scope.addNewListener = function() {
$scope.autocopies.push({ type : 'station',
assigner : '',
othersetting : ''});
};
// Click handler for remove button
$scope.removeListener = function() {
if ($scope.selectedListeners.length > 0) {
var index = $scope.autocopies.indexOf($scope.selectedListeners[0]);
$scope.gridOptions.selectItem(index, false);
$scope.autocopies.splice(index, 1);
$scope.selectedListeners.length = 0;
if (index < $scope.autocopies.length) {
$scope.gridOptions.selectItem(index + 1, true);
} else if ($scope.autocopies.length > 0) {
$scope.gridOptions.selectItem(index - 1, true);
}
}
};
// Click handler for up button
$scope.moveListenerUp = function() {
if ($scope.selectedListeners.length > 0) {
var index = $scope.autocopies.indexOf($scope.selectedListeners[0]);
if (index != 0) { // If it's the first, no moving up of course
// Reason for funny way of swapping, see https://github.com/angular-ui/ng-grid/issues/272
var temp = $scope.autocopies[index];
$scope.autocopies.splice(index, 1);
$timeout(function(){
$scope.autocopies.splice(index + -1, 0, temp);
}, 100);
}
}
};
// Click handler for down button
$scope.moveListenerDown = function() {
if ($scope.selectedListeners.length > 0) {
var index = $scope.autocopies.indexOf($scope.selectedListeners[0]);
if (index != $scope.autocopies.length - 1) { // If it's the last element, no moving down of course
// Reason for funny way of swapping, see https://github.com/angular-ui/ng-grid/issues/272
var temp = $scope.autocopies[index];
$scope.autocopies.splice(index, 1);
$timeout(function(){
$scope.autocopies.splice(index + 1, 0, temp);
}, 100);
}
}
};
$scope.fieldDetailsChanged = function() {
};
// Click handler for add button
$scope.addNewField = function() {
if ($scope.selectedListeners.length > 0)
{
if ($scope.selectedListeners[0].fields == undefined)
{
$scope.selectedListeners[0].fields = [];
}
$scope.selectedListeners[0].fields.push({
type : '',
assigner : '',
othersetting: ''});
}
};
// Click handler for remove button
$scope.removeField = function() {
if ($scope.selectedFields.length > 0) {
var index = $scope.selectedListeners[0].fields.indexOf($scope.selectedFields[0]);
$scope.gridFieldOptions.selectItem(index, false);
$scope.selectedListeners[0].fields.splice(index, 1);
$scope.selectedFields.length = 0;
if (index < $scope.selectedListeners[0].fields.length) {
$scope.gridFieldOptions.selectItem(index + 1, true);
} else if ($scope.selectedListeners[0].fields.length > 0) {
$scope.gridFieldOptions.selectItem(index - 1, true);
}
}
};
// Click handler for up button
$scope.moveFieldUp = function() {
if ($scope.selectedFields.length > 0) {
var index = $scope.selectedListeners[0].fields.indexOf($scope.selectedFields[0]);
if (index != 0) { // If it's the first, no moving up of course
// Reason for funny way of swapping, see https://github.com/angular-ui/ng-grid/issues/272
var temp = $scope.selectedListeners[0].fields[index];
$scope.selectedListeners[0].fields.splice(index, 1);
$timeout(function(){
$scope.selectedListeners[0].fields.splice(index + -1, 0, temp);
}, 100);
}
}
};
// Click handler for down button
$scope.moveFieldDown = function() {
if ($scope.selectedFields.length > 0) {
var index = $scope.selectedListeners[0].fields.indexOf($scope.selectedFields[0]);
if (index != $scope.selectedListeners[0].fields.length - 1) { // If it's the last element, no moving down of course
// Reason for funny way of swapping, see https://github.com/angular-ui/ng-grid/issues/272
var temp = $scope.selectedListeners[0].fields[index];
$scope.selectedListeners[0].fields.splice(index, 1);
$timeout(function(){
$scope.selectedListeners[0].fields.splice(index + 1, 0, temp);
}, 100);
}
}
};
// Click handler for save button
$scope.save = function() {
if ($scope.autocopies.length > 0) {
$scope.property.value = {};
$scope.property.value.autocopies = $scope.autocopies;
} else {
$scope.property.value = null;
}
$scope.updatePropertyInModel($scope.property);
$scope.close();
};
$scope.cancel = function() {
$scope.$hide();
$scope.property.mode = 'read';
};
// Close button handler
$scope.close = function() {
$scope.$hide();
$scope.property.mode = 'read';
};
}];
modeler.html
多语言定义在en.json中,这里直接修改了,也可以增加zh_cn.json,然后切换语言
"PROPERTY.AUTOCOPY.DISPLAY" : "{{length}}自动抄送",
"PROPERTY.AUTOCOPY.EMPTY" : "没有配置自动抄送",
"PROPERTY.AUTOCOPY.TYPE" : "审批规则",
"PROPERTY.AUTOCOPY.ASSIGNER" : "审批人",
"PROPERTY.AUTOCOPY.OTHERSETTING" : "其他设置",
"PROPERTY.AUTOCOPY.UNSELECTED" : "没有配置自动抄送",
"PROPERTY.AUTOCOPY.FIELDS.TYPE" : "审批规则",
"PROPERTY.AUTOCOPY.FIELDS.ASSIGNER" : "审批人",
"PROPERTY.AUTOCOPY.FIELDS.OTHERSETTING" : "其他设置",
"PROPERTY.AUTOCOPY.FIELDS.EMPTY" : "没有选择字段"
除了以上前端工作,自定义属性,还需要客制化流程图加载过程,以读取自定义属性
CustomUserTaskJsonConverter.java
public class CustomUserTaskJsonConverter extends UserTaskJsonConverter {
@Override
protected FlowElement convertJsonToElement(JsonNode elementNode, JsonNode modelNode, Map shapeMap) {
FlowElement flowElement = super.convertJsonToElement(elementNode, modelNode, shapeMap);
UserTask userTask = (UserTask) flowElement;
List customProperties = new ArrayList<>();
//添加扩展属性 审批方式
String approvalWay = this.getPropertyValueAsString(CommonConstants.APPROVAL_WAY, elementNode);
if (StringUtils.isNotEmpty(approvalWay)) {
CustomProperty customProperty = new CustomProperty();
customProperty.setName(CommonConstants.APPROVAL_WAY);
customProperty.setSimpleValue(approvalWay);
customProperties.add(customProperty);
}
//添加扩展属性 审批规则
String approvalRule = this.getPropertyValueAsString(CommonConstants.APPROVAL_RULE, elementNode);
if (StringUtils.isNotEmpty(approvalRule)) {
CustomProperty customProperty = new CustomProperty();
customProperty.setName(CommonConstants.APPROVAL_RULE);
customProperty.setSimpleValue(approvalRule);
customProperties.add(customProperty);
}
//添加扩展属性 自动抄送
JsonNode autoCopy = this.getProperty(CommonConstants.AUTO_COPY, elementNode);
if (ObjectUtil.isNotEmpty(autoCopy)) {
CustomProperty customProperty = new CustomProperty();
customProperty.setName(CommonConstants.AUTO_COPY);
customProperties.add(customProperty);
}
userTask.setCustomProperties(customProperties);
return userTask;
}
}
ModelServiceImpl.java
public class ModelServiceImpl implements ModelService {
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deploy(String modelId) {
try {
// 获取模型
Model model = repositoryService.getModel(modelId);
ObjectNode objectNode = (ObjectNode) new ObjectMapper()
.readTree(repositoryService.getModelEditorSource(model.getId()));
//从json 文件解析并添加自定义扩展属性到模型文件中
CustomBpmnJsonConverter.getConvertersToBpmnMap().put("UserTask", CustomUserTaskJsonConverter.class);
BpmnModel bpmnModel = new BpmnJsonConverter().convertToBpmnModel(objectNode);
}
...
}
待实现
- 指定岗位时,岗位下拉列表自动获取
- 指定人时,人员下拉列表自动获取
- 选择不同审批规则时,下拉获取不同的审批人列表
- 根据设置的审批方式实现任务监听,审批通过/拒绝时更新任务节点和流程实例状态
- 根据设置的审批规则,进入节点时,动态获取审批候选人
- 根据设置的自动抄送规则,节点审批通过/拒绝时,自动触发抄送