/* Copyright 2005-2015 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. */ 'use strict'; var extScope; angular.module('flowableModeler') .controller('DecisionTableEditorController', ['$rootScope', '$scope', '$q', '$translate', '$http', '$timeout', '$location', '$modal', '$route', '$routeParams', 'DecisionTableService', 'UtilityService', 'uiGridConstants', 'appResourceRoot', 'hotRegisterer', function ($rootScope, $scope, $q, $translate, $http, $timeout, $location, $modal, $route, $routeParams, DecisionTableService, UtilityService, uiGridConstants, appResourceRoot, hotRegisterer) { extScope = $scope; $rootScope.decisionTableChanges = false; var hotDecisionTableEditorInstance; var hitPolicies = ['FIRST', 'ANY', 'UNIQUE', 'PRIORITY', 'RULE ORDER', 'OUTPUT ORDER', 'COLLECT']; var stringOperators = ['==', '!=', 'IS IN', 'IS NOT IN']; var numberOperators = ['==', '!=', '<', '>', '>=', '<=', 'IS IN', 'IS NOT IN']; var booleanOperators = ['==', '!=']; var dateOperators = ['==', '!=', '<', '>', '>=', '<=', 'IS IN', 'IS NOT IN']; var collectionOperators = ['ANY OF', 'NONE OF', 'ALL OF', 'NOT ALL OF', '==', '!=']; var allOperators = ['==', '!=', '<', '>', '>=', '<=', 'ANY OF', 'NONE OF', 'ALL OF', 'NOT ALL OF', 'IS IN', 'IS NOT IN']; var collectOperators = { 'SUM': '+', 'MIN': '<', 'MAX': '>', 'COUNT': '#' }; var columnIdCounter = 0; var hitPolicyHeaderElement; var dateFormat = 'YYYY-MM-DD'; // Export name to grid's scope $scope.appResourceRoot = appResourceRoot; // Model init $scope.status = {loading: true}; $scope.model = { rulesData: [], columnDefs: [], columnVariableIdMap: {}, startOutputExpression: 0, selectedRow: undefined, availableInputVariableTypes: ['string', 'number', 'boolean', 'date', 'collection'], availableOutputVariableTypes: ['string', 'number', 'boolean', 'date'] }; // Hot Model init $scope.model.hotSettings = { contextMenu: { items: { "insert_row_above": { name: 'Insert rule above', callback: function (key, options) { if (hotDecisionTableEditorInstance.getSelected()) { $scope.addRule(hotDecisionTableEditorInstance.getSelected()[0]); } } }, "insert_row_below": { name: 'Add rule below', callback: function (key, options) { if (hotDecisionTableEditorInstance.getSelected()) { $scope.addRule(hotDecisionTableEditorInstance.getSelected()[0] + 1); } } }, "clear_row": { name: 'Clear rule', callback: function (key, options) { if (hotDecisionTableEditorInstance.getSelected()) { $scope.clearRule(hotDecisionTableEditorInstance.getSelected()[0]); } } }, "remove_row": { name: 'Remove this rule', disabled: function () { // if only 1 rule disable if (hotDecisionTableEditorInstance.getSelected()) { return $scope.model.rulesData.length < 2; } else { return false; } } }, "hsep1": "---------", "move_rule_up": { name: 'Move rule up', disabled: function () { if (hotDecisionTableEditorInstance.getSelected()) { return hotDecisionTableEditorInstance.getSelected()[0] === 0; } }, callback: function (key, options) { $scope.moveRuleUpwards(); } }, "move_rule_down": { name: 'Move rule down', disabled: function () { if (hotDecisionTableEditorInstance.getSelected()) { return hotDecisionTableEditorInstance.getSelected()[0] + 1 === $scope.model.rulesData.length; } }, callback: function (key, options) { $scope.moveRuleDownwards(); } }, "hsep2": "---------", "add_input": { name: 'Add input', callback: function (key, options) { if (hotDecisionTableEditorInstance.getSelected()) { $scope.openInputExpressionEditor(hotDecisionTableEditorInstance.getSelected()[1], true); } } }, "add_output": { name: 'Add output', callback: function (key, options) { if (hotDecisionTableEditorInstance.getSelected()) { if (hotDecisionTableEditorInstance.getSelected()[1] < $scope.model.startOutputExpression) { $scope.openOutputExpressionEditor($scope.currentDecisionTable.outputExpressions.length, true); } else { $scope.openOutputExpressionEditor((hotDecisionTableEditorInstance.getSelected()[1] - $scope.model.startOutputExpression), true); } } } }, "hsep3": "---------", "undo": { name: 'Undo' }, "redo": { name: 'Redo' } } }, manualColumnResize: false, stretchH: 'all', outsideClickDeselects: false, viewportColumnRenderingOffset: $scope.model.columnDefs.length + 10 }; $scope.hitPolicies = []; hitPolicies.forEach(function (id) { $scope.hitPolicies.push({ id: id, label: 'DECISION-TABLE.HIT-POLICIES.' + id }); }); $scope.collectOperators = []; Object.keys(collectOperators).forEach(function (key) { $scope.collectOperators.push({ id: key, label: 'DECISION-TABLE.COLLECT-OPERATORS.' + key }); }); $scope.addRule = function (rowNumber) { if (rowNumber !== undefined) { $scope.model.rulesData.splice(rowNumber, 0, createDefaultRow()); } else { $scope.model.rulesData.push(createDefaultRow()); } if (hotDecisionTableEditorInstance) { hotDecisionTableEditorInstance.render(); } }; $scope.removeRule = function () { $scope.model.rulesData.splice($scope.model.selectedRow, 1); }; $scope.clearRule = function (rowNumber) { if (rowNumber === undefined) { return; } $scope.model.rulesData[rowNumber] = createDefaultRow(); if (hotDecisionTableEditorInstance) { hotDecisionTableEditorInstance.render(); } }; $scope.moveRuleUpwards = function () { $scope.model.rulesData.splice($scope.model.selectedRow - 1, 0, $scope.model.rulesData.splice($scope.model.selectedRow, 1)[0]); if (hotDecisionTableEditorInstance) { hotDecisionTableEditorInstance.render(); } }; $scope.moveRuleDownwards = function () { $scope.model.rulesData.splice($scope.model.selectedRow + 1, 0, $scope.model.rulesData.splice($scope.model.selectedRow, 1)[0]); if (hotDecisionTableEditorInstance) { hotDecisionTableEditorInstance.render(); } }; $scope.doAfterGetColHeader = function (col, TH) { if ($scope.model.columnDefs[col] && $scope.model.columnDefs[col].expressionType === 'input-operator') { TH.className += "input-operator-header"; } else if ($scope.model.columnDefs[col] && $scope.model.columnDefs[col].expressionType === 'input-expression') { TH.className += "input-expression-header"; if ($scope.model.startOutputExpression - 1 === col) { TH.className += " last"; } } else if ($scope.model.columnDefs[col] && $scope.model.columnDefs[col].expressionType === 'output') { TH.className += "output-header"; if ($scope.model.startOutputExpression === col) { TH.className += " first"; } } }; $scope.doAfterModifyColWidth = function (width, col) { if ($scope.model.columnDefs[col] && $scope.model.columnDefs[col].width) { var settingsWidth = $scope.model.columnDefs[col].width; if (settingsWidth > width) { return settingsWidth; } } return width; }; $scope.doAfterOnCellMouseDown = function (event, coords, TD) { // clicked hit policy indicator if (coords.row === 0 && coords.col === 0 && TD.className === '') { $scope.openHitPolicyEditor(); } else { if (coords && coords.row !== undefined) { $timeout(function () { $scope.model.selectedRow = coords.row; }); } } }; $scope.doAfterScroll = function () { if (hotDecisionTableEditorInstance) { hotDecisionTableEditorInstance.render(); } }; $scope.doAfterRender = function (isForced) { if (hitPolicyHeaderElement) { var element = document.querySelector("thead > tr > th:first-of-type"); if (element) { var firstChild = element.firstChild; element.className = 'hit-policy-container'; element.replaceChild(hitPolicyHeaderElement[0], firstChild); } } if (hotDecisionTableEditorInstance === undefined) { $timeout(function () { hotDecisionTableEditorInstance = hotRegisterer.getInstance('decision-table-editor'); hotDecisionTableEditorInstance.validateCells(); }); } else { hotDecisionTableEditorInstance.validateCells(); } }; $scope.dumpData = function () { console.log($scope.currentDecisionTable); console.log($scope.model.rulesData); console.log($scope.model.columnDefs); }; $scope.doAfterValidate = function (isValid, value, row, prop, source) { if (isCorrespondingCollectionOperator(row, prop)) { return true; } else if (isCustomExpression(value) || isDashValue(value)) { disableCorrespondingOperatorCell(row, prop); return true; } else { enableCorrespondingOperatorCell(row, prop); } }; // dummy validator for text fields in order to trigger the post validation hook var textValidator = function(value, callback) { callback(true); }; var isCustomExpression = function (val) { return !!(val != null && (String(val).startsWith('${') || String(val).startsWith('#{'))); }; var isDashValue = function (val) { return !!(val != null && "-" === val); }; var isCorrespondingCollectionOperator = function (row, prop) { var operatorCol = getCorrespondingOperatorCell(row, prop); var operatorCellMeta = hotDecisionTableEditorInstance.getCellMeta(row, operatorCol); var isCollectionOperator = false; if (isOperatorCell(operatorCellMeta)) { var operatorValue = hotDecisionTableEditorInstance.getDataAtCell(row, operatorCol); if (operatorValue === "IN" || operatorValue === "NOT IN" || operatorValue === "ANY" || operatorValue === "NOT ANY") { isCollectionOperator = true; } } return isCollectionOperator; }; var disableCorrespondingOperatorCell = function (row, prop) { var operatorCol = getCorrespondingOperatorCell(row, prop); var operatorCellMeta = hotDecisionTableEditorInstance.getCellMeta(row, operatorCol); if (!isOperatorCell(operatorCellMeta)) { return; } if (operatorCellMeta.className != null && operatorCellMeta.className.indexOf('custom-expression-operator') !== -1) { return; } var currentEditor = hotDecisionTableEditorInstance.getCellEditor(row, operatorCol); hotDecisionTableEditorInstance.setCellMeta(row, operatorCol, 'className', operatorCellMeta.className + ' custom-expression-operator'); hotDecisionTableEditorInstance.setCellMeta(row, operatorCol, 'originalEditor', currentEditor); hotDecisionTableEditorInstance.setCellMeta(row, operatorCol, 'editor', false); hotDecisionTableEditorInstance.setDataAtCell(row, operatorCol, null); }; var enableCorrespondingOperatorCell = function (row, prop) { var operatorCol = getCorrespondingOperatorCell(row, prop); var operatorCellMeta = hotDecisionTableEditorInstance.getCellMeta(row, operatorCol); if (!isOperatorCell(operatorCellMeta)) { return; } if (operatorCellMeta == null || operatorCellMeta.className == null || operatorCellMeta.className.indexOf('custom-expression-operator') == -1) { return; } operatorCellMeta.className = operatorCellMeta.className.replace('custom-expression-operator', ''); hotDecisionTableEditorInstance.setCellMeta(row, operatorCol, 'className', operatorCellMeta.className); hotDecisionTableEditorInstance.setCellMeta(row, operatorCol, 'editor', operatorCellMeta.originalEditor); hotDecisionTableEditorInstance.setDataAtCell(row, operatorCol, '=='); }; var getCorrespondingOperatorCell = function (row, prop) { var currentCol = hotDecisionTableEditorInstance.propToCol(prop); if (currentCol < 1) { return; } var operatorCol = currentCol - 1; return operatorCol; }; var isOperatorCell = function (cellMeta) { return !(cellMeta == null || cellMeta.prop == null || typeof cellMeta.prop !== 'string'|| cellMeta.prop.endsWith("_operator") === false); }; var createNewInputExpression = function (inputExpression) { var newInputExpression; if (inputExpression) { newInputExpression = { id: _generateColumnId(), label: inputExpression.label, variableId: inputExpression.variableId, type: inputExpression.type, newVariable: inputExpression.newVariable, entries: inputExpression.entries }; } else { newInputExpression = { id: _generateColumnId(), label: null, variableId: null, type: null, newVariable: null, entries: null }; } return newInputExpression; }; var createNewOutputExpression = function (outputExpression) { var newOutputExpression; if (outputExpression) { newOutputExpression = { id: _generateColumnId(), label: outputExpression.label, variableId: outputExpression.variableId, type: outputExpression.type, newVariable: outputExpression.newVariable, entries: outputExpression.entries }; } else { newOutputExpression = { id: _generateColumnId(), label: null, variableId: null, type: null, newVariable: null, entries: null }; } return newOutputExpression; }; $scope.addNewInputExpression = function (inputExpression, insertPos) { var newInputExpression = createNewInputExpression(inputExpression); // insert expression at position or just add if (insertPos !== undefined && insertPos !== -1) { $scope.currentDecisionTable.inputExpressions.splice(insertPos, 0, newInputExpression); } else { $scope.currentDecisionTable.inputExpressions.push(newInputExpression); } // update data model with initial values $scope.model.rulesData.forEach(function (rowData) { rowData[newInputExpression.id + '_expression'] = '-'; }); // update column definitions off the source model $scope.evaluateDecisionHeaders($scope.currentDecisionTable); }; $scope.addNewOutputExpression = function (outputExpression, insertPos) { var newOutputExpression = createNewOutputExpression(outputExpression); // insert expression at position or just add if (insertPos !== undefined && insertPos !== -1) { $scope.currentDecisionTable.outputExpressions.splice(insertPos, 0, newOutputExpression); } else { $scope.currentDecisionTable.outputExpressions.push(newOutputExpression); } // update column definitions off the source model $scope.evaluateDecisionHeaders($scope.currentDecisionTable); }; $scope.removeInputExpression = function (expressionPos) { var removedElements = $scope.currentDecisionTable.inputExpressions.splice(expressionPos, 1); removePropertyFromGrid(removedElements[0].id, 'input'); $scope.evaluateDecisionHeaders($scope.currentDecisionTable); }; var removePropertyFromGrid = function (key, type) { if ($scope.model.rulesData) { $scope.model.rulesData.forEach(function (rowData) { if (type === 'input') { delete rowData[key + '_operator']; delete rowData[key + '_expression']; } else if (type === 'output') { delete rowData[key]; } }); } }; $scope.removeOutputExpression = function (expressionPos) { var removedElements = $scope.currentDecisionTable.outputExpressions.splice(expressionPos, 1); removePropertyFromGrid(removedElements[0].id, 'output'); $scope.evaluateDecisionHeaders($scope.currentDecisionTable); }; $scope.openInputExpressionEditor = function (expressionPos, newExpression) { var editTemplate = 'views/popup/decision-table-edit-input-expression.html'; $scope.model.newExpression = !!newExpression; if (!$scope.model.newExpression) { $scope.model.selectedExpression = $scope.currentDecisionTable.inputExpressions[expressionPos]; } else { if (expressionPos >= $scope.model.startOutputExpression) { $scope.model.selectedColumn = $scope.model.startOutputExpression - 1; } else { $scope.model.selectedColumn = Math.floor(expressionPos / 2); } } _internalCreateModal({ template: editTemplate, scope: $scope }, $modal, $scope); }; $scope.openOutputExpressionEditor = function (expressionPos, newExpression) { var editTemplate = 'views/popup/decision-table-edit-output-expression.html'; $scope.model.newExpression = !!newExpression; $scope.model.hitPolicy = $scope.currentDecisionTable.hitIndicator; $scope.model.selectedColumn = expressionPos; if (!$scope.model.newExpression) { $scope.model.selectedExpression = $scope.currentDecisionTable.outputExpressions[expressionPos]; } _internalCreateModal({ template: editTemplate, scope: $scope }, $modal, $scope); }; $scope.openHitPolicyEditor = function () { var editTemplate = 'views/popup/decision-table-edit-hit-policy.html'; $scope.model.hitPolicy = $scope.currentDecisionTable.hitIndicator; $scope.model.collectOperator = $scope.currentDecisionTable.collectOperator; _internalCreateModal({ template: editTemplate, scope: $scope }, $modal, $scope); }; var _loadDecisionTableDefinition = function (modelId) { DecisionTableService.fetchDecisionTableDetails(modelId).then(function (decisionTable) { $rootScope.currentDecisionTable = decisionTable.decisionTableDefinition; $rootScope.currentDecisionTable.id = decisionTable.id; $rootScope.currentDecisionTable.key = decisionTable.decisionTableDefinition.key; $rootScope.currentDecisionTable.name = decisionTable.name; $rootScope.currentDecisionTable.description = decisionTable.description; $scope.model.lastUpdatedBy = decisionTable.lastUpdatedBy; $scope.model.createdBy = decisionTable.createdBy; // decision table model to used in save dialog $rootScope.currentDecisionTableModel = { id: decisionTable.id, name: decisionTable.name, key: decisionTable.key, description: decisionTable.description }; if (!$rootScope.currentDecisionTable.hitIndicator) { $rootScope.currentDecisionTable.hitIndicator = hitPolicies[0]; } var hitPolicyHeaderString = '
'; hitPolicyHeaderElement = angular.element(hitPolicyHeaderString); evaluateDecisionTableGrid($rootScope.currentDecisionTable); $timeout(function () { // Flip switch in timeout to start watching all decision-related models // after next digest cycle, to prevent first false-positive $scope.status.loading = false; $rootScope.decisionTableChanges = false; }); }); }; var composeInputOperatorColumnDefinition = function (inputExpression) { var expressionPosition = $scope.currentDecisionTable.inputExpressions.indexOf(inputExpression); var columnDefinition = { data: inputExpression.id + '_operator', expressionType: 'input-operator', expression: inputExpression, width: '70', className: 'input-operator-cell', type: 'dropdown', source: getOperatorsForColumnType(inputExpression.type) }; if ($scope.currentDecisionTable.inputExpressions.length !== 1) { columnDefinition.title = ''; } return columnDefinition; }; var getOperatorsForColumnType = function (type) { switch (type) { case 'number': return numberOperators; case 'date': return dateOperators; case 'boolean': return booleanOperators; case 'string': return stringOperators; case 'collection': return collectionOperators; default: return allOperators; } }; var composeInputExpressionColumnDefinition = function (inputExpression) { var expressionPosition = $scope.currentDecisionTable.inputExpressions.indexOf(inputExpression); var type; switch (inputExpression.type) { case 'date': type = 'date'; break; case 'number': type = 'numeric'; break; case 'boolean': type = 'dropdown'; break; default: type = 'text'; } var columnDefinition = { data: inputExpression.id + '_expression', type: type, title: '