cob-cli 2.5.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/customizations/frontend.common.js +2 -2
- package/customizations/frontend.formatList.currency.js +17 -0
- package/customizations/frontend.formatList.js +13 -0
- package/customizations/frontend.js +2 -1
- package/customizations/keywords.audit.js +17 -0
- package/customizations/keywords.calc.js +17 -0
- package/customizations/keywords.js +13 -0
- package/lib/commands/customize.js +1 -0
- package/lib/task_lists/customize_copy.js +34 -11
- package/lib/task_lists/customize_mergeFiles.js +5 -4
- package/lib/task_lists/customize_questions.js +8 -1
- package/lib/task_lists/test_otherFilesContiousReload.js +3 -3
- package/package.json +1 -1
- package/templates/frontend/formatList/currency/css/_format_currency.css +11 -0
- package/templates/frontend/formatList/currency/css/customizations.__MERGE__.css +1 -0
- package/templates/frontend/formatList/currency/js/cob/_format_currency.__DEFINITION__.js +13 -0
- package/templates/frontend/formatList/currency/js/customizations2.__MERGE__.js +1 -0
- package/templates/keywords/audit/backend/scripts/_audit.groovy +82 -0
- package/templates/keywords/audit/frontend/js/cob/_audit.js +20 -0
- package/templates/keywords/audit/frontend/js/customizations2.__MERGE__.js +1 -0
- package/templates/keywords/calc/backend/scripts/_calc.groovy +160 -0
- package/templates/keywords/calc/frontend/js/cob/_calc.js +183 -0
- package/templates/keywords/calc/frontend/js/customizations2.__MERGE__.js +1 -0
|
@@ -10,6 +10,6 @@ exports.option = {
|
|
|
10
10
|
const { mergeFiles } = require("../lib/task_lists/customize_mergeFiles");
|
|
11
11
|
const target = "./recordm/customUI/"
|
|
12
12
|
await copy("../../templates/frontend/common/",target)
|
|
13
|
-
await mergeFiles()
|
|
13
|
+
await mergeFiles("Frontend.Common")
|
|
14
14
|
}
|
|
15
|
-
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
exports.option = {
|
|
2
|
+
name: 'FormatCurrency - Make $style[currency] available on definitions',
|
|
3
|
+
short: "Simple",
|
|
4
|
+
questions: [
|
|
5
|
+
require("../lib/task_lists/customize_questions").definitionNameQuestion
|
|
6
|
+
],
|
|
7
|
+
customization: async function (answers) {
|
|
8
|
+
console.log("\nApplying FormatCurrency frontend customizations ...")
|
|
9
|
+
const { copy } = require("../lib/task_lists/customize_copy");
|
|
10
|
+
const { mergeFiles } = require("../lib/task_lists/customize_mergeFiles");
|
|
11
|
+
|
|
12
|
+
const target = "./recordm/customUI/"
|
|
13
|
+
let substitutions = {"__DEFINITION__": answers.name}
|
|
14
|
+
await copy("../../templates/frontend/formatList/currency/",target, substitutions)
|
|
15
|
+
await mergeFiles("frontend.formatList.currency.js: " + answers.name )
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
exports.option = {
|
|
2
|
+
name: 'FormatList - Add classes to change the style of specified columns in search results',
|
|
3
|
+
short: "FormatList",
|
|
4
|
+
followUp: [ {
|
|
5
|
+
type: 'list',
|
|
6
|
+
name: 'choosenFollowUp',
|
|
7
|
+
message: 'Select one?',
|
|
8
|
+
choices: [
|
|
9
|
+
require("./frontend.formatList.currency").option//,
|
|
10
|
+
// require("./frontend.formatList.percentage").option
|
|
11
|
+
]}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
exports.option = {
|
|
2
|
+
name: 'Audit - Allows for $user in definitions (https://learning.cultofbits.com/docs/cob-platform/admins/managing-information/available-customizations/audit/)',
|
|
3
|
+
short: "Audit",
|
|
4
|
+
questions: [
|
|
5
|
+
],
|
|
6
|
+
customization: async function (answers) {
|
|
7
|
+
console.log("\nApplying Audit keyword customizations ...")
|
|
8
|
+
|
|
9
|
+
const { copy } = require("../lib/task_lists/customize_copy");
|
|
10
|
+
const { mergeFiles } = require("../lib/task_lists/customize_mergeFiles");
|
|
11
|
+
const fe_target = "./recordm/customUI/"
|
|
12
|
+
await copy("../../templates/keywords/audit/frontend",fe_target)
|
|
13
|
+
const be_target = "./integrationm/"
|
|
14
|
+
await copy("../../templates/keywords/audit/backend",be_target)
|
|
15
|
+
await mergeFiles("Keyword.Audit")
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
exports.option = {
|
|
2
|
+
name: 'Calc - Allows for $calc in definitions (https://learning.cultofbits.com)',
|
|
3
|
+
short: "Calc",
|
|
4
|
+
questions: [
|
|
5
|
+
],
|
|
6
|
+
customization: async function (answers) {
|
|
7
|
+
console.log("\nApplying Calc keyword customizations ...")
|
|
8
|
+
|
|
9
|
+
const { copy } = require("../lib/task_lists/customize_copy");
|
|
10
|
+
const { mergeFiles } = require("../lib/task_lists/customize_mergeFiles");
|
|
11
|
+
const fe_target = "./recordm/customUI/"
|
|
12
|
+
await copy("../../templates/keywords/calc/frontend",fe_target)
|
|
13
|
+
const be_target = "./integrationm/"
|
|
14
|
+
await copy("../../templates/keywords/calc/backend/",be_target)
|
|
15
|
+
await mergeFiles("Keyword.Calc")
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
exports.option = {
|
|
2
|
+
name: 'Keywords - Add a keyword for use in definitions (https://learning.cultofbits.com)',
|
|
3
|
+
short: "Keywords",
|
|
4
|
+
followUp: [ {
|
|
5
|
+
type: 'list',
|
|
6
|
+
name: 'choosenFollowUp',
|
|
7
|
+
message: 'Select one?',
|
|
8
|
+
choices: [
|
|
9
|
+
require("./keywords.calc").option,
|
|
10
|
+
require("./keywords.audit").option
|
|
11
|
+
]}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -19,6 +19,7 @@ async function customize(args) {
|
|
|
19
19
|
require("../../customizations/dashboard").option,
|
|
20
20
|
require("../../customizations/frontend").option,
|
|
21
21
|
require("../../customizations/backend").option,
|
|
22
|
+
require("../../customizations/keywords").option,
|
|
22
23
|
require("../../customizations/importer").option,
|
|
23
24
|
]
|
|
24
25
|
}]
|
|
@@ -1,23 +1,46 @@
|
|
|
1
1
|
const ncp = require('ncp');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
|
|
3
|
+
const {Transform} = require('stream')
|
|
4
4
|
|
|
5
5
|
// https://www.npmtrends.com/copyfiles-vs-cpx-vs-ncp-vs-npm-build-tools
|
|
6
6
|
// https://www.npmjs.com/package/ncp
|
|
7
7
|
// https://www.npmjs.com/package/copyfiles
|
|
8
8
|
|
|
9
|
-
function copy(source, target) {
|
|
10
|
-
console.log(" Copying template files...")
|
|
11
|
-
return new Promise(resolve => {
|
|
9
|
+
function copy(source, target, substitutions = {}) {
|
|
10
|
+
console.log(" Copying template files to '" + target + "'...")
|
|
11
|
+
return new Promise(async (resolve) => {
|
|
12
12
|
// Source is on cob-cli repo and Destination on the server repo
|
|
13
|
-
ncp(path.resolve(__dirname,source)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
await ncp(path.resolve(__dirname,source),
|
|
14
|
+
target,
|
|
15
|
+
{
|
|
16
|
+
clobber: true,
|
|
17
|
+
filter: (src) => src.match(/node_modules/) == null,
|
|
18
|
+
transform(read, write) {
|
|
19
|
+
const replaceVars = new Transform({
|
|
20
|
+
transform: (chunk, encoding, done) => done(null,chunk.toString().replace(/__.+__/g, m => substitutions[m]))
|
|
21
|
+
})
|
|
22
|
+
read.pipe(replaceVars).pipe(write)
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
(error,x) => {
|
|
26
|
+
if (error) {
|
|
27
|
+
// Error is an array of problems
|
|
28
|
+
resolve(error.map(e => e.message).join("\n"));
|
|
29
|
+
} else {
|
|
30
|
+
resolve();
|
|
31
|
+
}
|
|
19
32
|
}
|
|
20
|
-
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const { renameSync } = require('fs');
|
|
36
|
+
const fg = require('fast-glob');
|
|
37
|
+
|
|
38
|
+
const files = await fg(['**/*.__*__.*'], { onlyFiles: true, dot: true });
|
|
39
|
+
files
|
|
40
|
+
.forEach(file => {
|
|
41
|
+
if(file.match(/__MERGE__/)) return
|
|
42
|
+
renameSync(file, file.replace(/__.+__/g, m => substitutions[m]));
|
|
43
|
+
});
|
|
21
44
|
});
|
|
22
45
|
}
|
|
23
46
|
exports.copy = copy;
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
|
|
2
|
-
async function mergeFiles() {
|
|
2
|
+
async function mergeFiles(block) {
|
|
3
3
|
const fg = require('fast-glob');
|
|
4
4
|
const fs = require('fs-extra');
|
|
5
5
|
const mergeFiles = await fg(['**/*.__MERGE__.*'], { onlyFiles: false, dot: true });
|
|
6
6
|
for (let mergeFile of mergeFiles) {
|
|
7
7
|
let prodFile = mergeFile.replace(/\.__MERGE__/, "");
|
|
8
|
+
let blockMark = (block == undefined ? "" : block)
|
|
8
9
|
if (!fs.existsSync(prodFile)) {
|
|
9
10
|
// If prod file does not exist creates it
|
|
10
11
|
console.log(" Creating " + prodFile)
|
|
11
12
|
fs.closeSync(fs.openSync(prodFile, 'w'));
|
|
12
13
|
} else {
|
|
13
|
-
console.log(" Merging " + prodFile)
|
|
14
|
+
console.log(" Merging " + prodFile + " " + block)
|
|
14
15
|
}
|
|
15
16
|
let prodFileContent = fs.readFileSync(prodFile).toString();
|
|
16
17
|
let mergeFileContent = fs.readFileSync(mergeFile).toString();
|
|
17
|
-
let startStr = '
|
|
18
|
-
let endStr = '\n/* COB-CLI END ' +
|
|
18
|
+
let startStr = '/* COB-CLI START Customization ' + blockMark + ' */\n';
|
|
19
|
+
let endStr = '\n/* COB-CLI END Customization ' + blockMark + ' */\n';
|
|
19
20
|
|
|
20
21
|
if (prodFileContent.indexOf(startStr) < 0) {
|
|
21
22
|
// If previous customization does not exist
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
exports.definitionNameQuestion = {
|
|
2
|
+
type: 'input',
|
|
3
|
+
name: 'name',
|
|
4
|
+
message: 'What\'s the name of the definition?',
|
|
5
|
+
description: 'the name of the definition listing to customize'
|
|
6
|
+
};
|
|
7
|
+
|
|
1
8
|
exports.dashboardNameQuestion = {
|
|
2
9
|
type: 'input',
|
|
3
10
|
name: 'name',
|
|
@@ -30,4 +37,4 @@ exports.dashboardNameQuestion2 = {
|
|
|
30
37
|
|
|
31
38
|
return 'Only letters and numbers name, at least one';
|
|
32
39
|
}
|
|
33
|
-
};
|
|
40
|
+
};
|
|
@@ -64,7 +64,7 @@ async function otherFilesContiousReload(cmdEnv) {
|
|
|
64
64
|
let productDir = resolveCobPath(cmdEnv.server, "serverLive", product).split(":")[1];
|
|
65
65
|
let productScopeFilePath = changedFile.substring(changedFile.indexOf("/") + 1); // remove o directorio inicial do producto
|
|
66
66
|
|
|
67
|
-
let isRemoteFile =
|
|
67
|
+
let isRemoteFile = () => execa('ssh', [cmdEnv.server, "[[ -f " + productDir + productScopeFilePath + " ]] && echo 'file' || echo '' " ])
|
|
68
68
|
.then( r => {
|
|
69
69
|
return r.stdout == "file"
|
|
70
70
|
}).catch( () => {
|
|
@@ -77,8 +77,7 @@ async function otherFilesContiousReload(cmdEnv) {
|
|
|
77
77
|
} catch {}
|
|
78
78
|
|
|
79
79
|
if (
|
|
80
|
-
(
|
|
81
|
-
&& changedFile.indexOf("/customUI/") < 0
|
|
80
|
+
changedFile.indexOf("/customUI/") < 0
|
|
82
81
|
&& changedFile.indexOf("/node_modules/") < 0
|
|
83
82
|
&& !changedFile.startsWith(".git")
|
|
84
83
|
&& !changedFile.startsWith(".idea")
|
|
@@ -86,6 +85,7 @@ async function otherFilesContiousReload(cmdEnv) {
|
|
|
86
85
|
&& !changedFile.endsWith(".swp")
|
|
87
86
|
&& !changedFile.endsWith(".swap")
|
|
88
87
|
&& changedFile.indexOf(IN_PROGRESS_TEST_FILE) < 0
|
|
88
|
+
&& (isLocalFile || await isRemoteFile())
|
|
89
89
|
) {
|
|
90
90
|
try {
|
|
91
91
|
await addFileToCurrentTest(cmdEnv.server, changedFile, changedFiles, " Syncing ".bgRed.bold + " ", restoreChanges );
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "_format_currency.css";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const DEFINITION = "__DEFINITION__";
|
|
2
|
+
|
|
3
|
+
cob.custom.customize.push(function (core, utils, ui) {
|
|
4
|
+
core.customizeAllColumns(DEFINITION, (node, esDoc, colDef) => {
|
|
5
|
+
// Test $style[currency], by it self or with other styles
|
|
6
|
+
if(/\$style\[([^,]+,)*currency(,[^,]+)*\]/.exec(colDef.fieldDefDescription) != null) {
|
|
7
|
+
let value = esDoc[colDef.field] ? esDoc[colDef.field][0] : null
|
|
8
|
+
if(value) {
|
|
9
|
+
node.classList.add((value[0] === "-") ? "currency_negative" : "currency_positive")
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import("./cob/_format_currency.__DEFINITION__.js");
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import groovy.transform.Field
|
|
2
|
+
import org.codehaus.jettison.json.JSONObject
|
|
3
|
+
|
|
4
|
+
import com.google.common.cache.*
|
|
5
|
+
import java.util.concurrent.TimeUnit
|
|
6
|
+
|
|
7
|
+
// ========================================================================================================
|
|
8
|
+
@Field static cacheOfAuditFieldsForDefinition = CacheBuilder.newBuilder()
|
|
9
|
+
.expireAfterWrite(5, TimeUnit.MINUTES)
|
|
10
|
+
.build();
|
|
11
|
+
|
|
12
|
+
if (msg.product == "recordm-definition") cacheOfAuditFieldsForDefinition.invalidate(msg.type)
|
|
13
|
+
def auditFields = cacheOfAuditFieldsForDefinition.get(msg.type, { getAuditFields(msg.type) })
|
|
14
|
+
|
|
15
|
+
// ========================================================================================================
|
|
16
|
+
|
|
17
|
+
if (auditFields.size() > 0
|
|
18
|
+
&& msg.product == "recordm"
|
|
19
|
+
&& msg.user != "integrationm"
|
|
20
|
+
&& msg.action =~ "add|update" ) {
|
|
21
|
+
|
|
22
|
+
def updates = updateUser(auditFields,msg.instance.fields)
|
|
23
|
+
def result = actionPacks.recordm.update(msg.type, "recordmInstanceId:" + msg.instance.id, updates);
|
|
24
|
+
/**/log.info("[\$audit] ACTUALIZADA '${msg.type}' {{id:${msg.instance.id}, result:${result}, updates: ${updates}}}");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ========================================================================================================
|
|
28
|
+
def updateUser(auditFields,instanceFields) {
|
|
29
|
+
def userm = actionPacks.get("userm");
|
|
30
|
+
def updates = [:]
|
|
31
|
+
auditFields.each { auditField ->
|
|
32
|
+
if( auditField.op == "creator" && msg.action == "update" && msg.value(auditField.name) != null) return
|
|
33
|
+
if( auditField.args == "usermRef") {
|
|
34
|
+
updates << [(auditField.name) : userm.getUser(msg.user).data._links.self]
|
|
35
|
+
|
|
36
|
+
} else if( auditField.args == "username") {
|
|
37
|
+
updates << [(auditField.name) : msg.user]
|
|
38
|
+
|
|
39
|
+
} else if( auditField.args == "time") {
|
|
40
|
+
if(msg.action == 'add' && Math.abs(msg.value(auditField.name, Long.class)?:0 - msg._timestamp_) < 30000) return // Ignore changes less then 30s
|
|
41
|
+
if(msg.action == 'update' && !msg.diff) return // Only continues if there is at least one change
|
|
42
|
+
updates << [(auditField.name) : "" + msg._timestamp_]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return updates
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ========================================================================================================
|
|
49
|
+
def getAuditFields(definitionName) {
|
|
50
|
+
/**/log.info("[\$audit] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
|
|
51
|
+
/**/log.info("[\$audit] Update auditFields for $definitionName ... ");
|
|
52
|
+
|
|
53
|
+
// Obtém detalhes da definição
|
|
54
|
+
def definitionEncoded = URLEncoder.encode(definitionName, "utf-8").replace("+", "%20")
|
|
55
|
+
def resp = actionPacks.rmRest.get( "recordm/definitions/name/${definitionEncoded}".toString(), [:], "");
|
|
56
|
+
JSONObject definition = new JSONObject(resp);
|
|
57
|
+
|
|
58
|
+
def fieldsSize = definition.fieldDefinitions.length();
|
|
59
|
+
|
|
60
|
+
def fields = [:]
|
|
61
|
+
(0..fieldsSize-1).each { index ->
|
|
62
|
+
def fieldDefinition = definition.fieldDefinitions.getJSONObject(index)
|
|
63
|
+
def fieldDescription = fieldDefinition.get("description")
|
|
64
|
+
def fieldDefId = fieldDefinition.get("id")
|
|
65
|
+
def fieldName = fieldDefinition.get("name");
|
|
66
|
+
fields[fieldDefId] = [name:fieldName, description: fieldDescription]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Finalmente obtém a lista de campos que é necessário calcular
|
|
70
|
+
def auditFields = [];
|
|
71
|
+
fields.each { fieldId,field ->
|
|
72
|
+
def matcher = field.description =~ /[$]audit\.(creator|updater)\.(username|usermRef|time)/
|
|
73
|
+
if(matcher) {
|
|
74
|
+
def op = matcher[0][1]
|
|
75
|
+
def arg = matcher[0][2]
|
|
76
|
+
auditFields << [fieldId: fieldId, name:field.name, op : op, args: arg]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
log.info("[\$audit] fields for '$definitionName': $auditFields");
|
|
80
|
+
/**/log.info("[\$audit] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
|
|
81
|
+
return auditFields
|
|
82
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//----------------- $audit ------------------------
|
|
2
|
+
cob.custom.customize.push(function (core, utils, ui) {
|
|
3
|
+
core.customizeAllInstances((instance, presenter) =>
|
|
4
|
+
{
|
|
5
|
+
let userFPs = presenter.findFieldPs( fp => /[$]audit\.(creator|updater)\.(username|usermRef|time)/.exec(fp.field.fieldDefinition.description) )
|
|
6
|
+
userFPs.forEach( fp => {
|
|
7
|
+
fp.disable()
|
|
8
|
+
if(!instance.isNew() || presenter.isGroupEdit()) return //Only update if it's on create interface (updates will only be changed by the backend)
|
|
9
|
+
if(/[$]audit\.(creator|updater)\.username/.exec(fp.field.fieldDefinition.description)) {
|
|
10
|
+
fp.setValue(core.getCurrentLoggedInUser())
|
|
11
|
+
}
|
|
12
|
+
if(/[$]audit\.(creator|updater)\.usermRef/.exec(fp.field.fieldDefinition.description)) {
|
|
13
|
+
fp.setValue(core.getCurrentLoggedInUserUri())
|
|
14
|
+
}
|
|
15
|
+
if(/[$]audit\.(creator|updater)\.time/.exec(fp.field.fieldDefinition.description)) {
|
|
16
|
+
fp.setValue(Date.now())
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import("./cob/_audit.js");
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import groovy.transform.Field
|
|
2
|
+
import org.codehaus.jettison.json.JSONObject
|
|
3
|
+
|
|
4
|
+
import java.math.RoundingMode;
|
|
5
|
+
|
|
6
|
+
import com.google.common.cache.*
|
|
7
|
+
import java.util.concurrent.TimeUnit
|
|
8
|
+
|
|
9
|
+
// ========================================================================================================
|
|
10
|
+
@Field static cacheOfCalcFieldsForDefinition = CacheBuilder.newBuilder()
|
|
11
|
+
.expireAfterWrite(5, TimeUnit.MINUTES)
|
|
12
|
+
.build();
|
|
13
|
+
|
|
14
|
+
if (msg.product == "recordm-definition") cacheOfCalcFieldsForDefinition.invalidate(msg.type)
|
|
15
|
+
def calculationFields = cacheOfCalcFieldsForDefinition.get(msg.type, { getAllCalculationFields(msg.type) })
|
|
16
|
+
|
|
17
|
+
// ========================================================================================================
|
|
18
|
+
if (calculationFields.size() > 0
|
|
19
|
+
&& msg.product == "recordm"
|
|
20
|
+
&& msg.user != "integrationm"
|
|
21
|
+
&& msg.action =~ "add|update" ){
|
|
22
|
+
|
|
23
|
+
def updates = executeCalculations(calculationFields, msg.instance.fields)
|
|
24
|
+
def result = actionPacks.recordm.update(messageMap.type, "recordmInstanceId:" + messageMap.instance.id, updates);
|
|
25
|
+
log.info("[\$calc] ACTUALIZADA '${messageMap.type}' {{id:${messageMap.instance.id}, result:${result}, updates: ${updates}}}");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ==================================================
|
|
29
|
+
def getCalculationOperation(fieldDescription) {
|
|
30
|
+
def matcher = fieldDescription =~/.*[$]calc.([^(]+)/
|
|
31
|
+
def op = matcher[0][1]
|
|
32
|
+
return op
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ==================================================
|
|
36
|
+
def getCalculationArgNames(fieldDescription) {
|
|
37
|
+
def matcher = fieldDescription =~/.*[$]calc.[^(]+\(([^(]+)\)/
|
|
38
|
+
def argNamesArray = matcher[0][1].tokenize(",")
|
|
39
|
+
return argNamesArray;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ========================================================================================================
|
|
43
|
+
def executeCalculations(calculationFields,instanceFields) {
|
|
44
|
+
def updates = [:]
|
|
45
|
+
def atLeastOneChangeFlag = false;
|
|
46
|
+
def passCount = 0;
|
|
47
|
+
def temporaryResults = [:]
|
|
48
|
+
while(passCount++ == 0 || atLeastOneChangeFlag && passCount < 10) { //10 is just for security against loops
|
|
49
|
+
atLeastOneChangeFlag = false
|
|
50
|
+
calculationFields.each { calculation ->
|
|
51
|
+
def novoResultado = evaluateExpression(calculation,instanceFields,temporaryResults)
|
|
52
|
+
if(temporaryResults[calculation.fieldId] != novoResultado ) {
|
|
53
|
+
// log.info("[\$calc] {{passCount:${passCount}, field:${calculation.name} (${calculation.fieldId})" +
|
|
54
|
+
// ", calcType:${calculation.op}(${calculation.args})" +
|
|
55
|
+
// ", previousResult:${temporaryResults[calculation.fieldId]}" +
|
|
56
|
+
// ", calcValue:$novoResultado}}");
|
|
57
|
+
|
|
58
|
+
temporaryResults[calculation.fieldId] = novoResultado;
|
|
59
|
+
updates << [(calculation.name) : novoResultado]
|
|
60
|
+
atLeastOneChangeFlag = true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return updates
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ==================================================
|
|
68
|
+
def evaluateExpression(calculation,instanceFields,temporaryResults) {
|
|
69
|
+
// Realizar operação
|
|
70
|
+
def resultado = new BigDecimal(0)
|
|
71
|
+
def args = getCalculationArguments(calculation,instanceFields,temporaryResults)
|
|
72
|
+
|
|
73
|
+
if(calculation.op == "multiply" && args.size() > 0) {
|
|
74
|
+
resultado = 1
|
|
75
|
+
args.each { arg -> resultado = resultado.multiply(new BigDecimal(arg?.trim() ?: 0)) }
|
|
76
|
+
|
|
77
|
+
} else if (calculation.op == "divide" && args.size() == 2 && (args[1]?:0 != 0)) {
|
|
78
|
+
resultado = new BigDecimal(args[0]?.trim() ?:0);
|
|
79
|
+
resultado = resultado.divide(new BigDecimal(args[1]?.trim()), 8, RoundingMode.HALF_UP)
|
|
80
|
+
|
|
81
|
+
} else if(calculation.op == "sum") {
|
|
82
|
+
args.each { arg -> resultado = resultado + new BigDecimal(arg?.trim() ?: 0)}
|
|
83
|
+
|
|
84
|
+
} else if (calculation.op == "subtract" && args.size() == 2) {
|
|
85
|
+
resultado = new BigDecimal(args[0]?.trim() ?: 0);
|
|
86
|
+
resultado = resultado.subtract(new BigDecimal(args[1]?.trim() ?: 0))
|
|
87
|
+
}
|
|
88
|
+
return resultado.stripTrailingZeros().toPlainString()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ==================================================
|
|
92
|
+
def getCalculationArguments(calculation,instanceFields,temporaryResults) {
|
|
93
|
+
def values = calculation.args.collect { argName,argFieldIds ->
|
|
94
|
+
(""+argName).isNumber()
|
|
95
|
+
? argName * 1
|
|
96
|
+
: getAllAplicableValuesForVarName(calculation.fieldId,argName,argFieldIds,instanceFields,temporaryResults)
|
|
97
|
+
}
|
|
98
|
+
return values.flatten()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ==================================================
|
|
102
|
+
def getAllAplicableValuesForVarName(fieldId,varName,varFieldIds,instanceFields,temporaryResults) {
|
|
103
|
+
// log.info("[\$calc] find '$varName'($varFieldIds) in $instanceFields (temporaryResults=$temporaryResults) ");
|
|
104
|
+
def relevantFields = instanceFields.findAll{ instField -> varFieldIds.indexOf(instField.fieldDefinition.id) >= 0 }
|
|
105
|
+
|
|
106
|
+
def result = varFieldIds.collect { varFieldId ->
|
|
107
|
+
if(temporaryResults[varFieldId] != null) {
|
|
108
|
+
return temporaryResults[varFieldId]
|
|
109
|
+
} else {
|
|
110
|
+
return temporaryResults[varFieldId] = instanceFields.findAll{ instField -> varFieldId == instField.fieldDefinition.id }?.collect { it.value }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// log.info("[\$calc] values for '$varName'($varFieldIds) = $result (temporaryResults=$temporaryResults) " );
|
|
114
|
+
return result.flatten()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ========================================================================================================
|
|
118
|
+
def getAllCalculationFields(definitionName) {
|
|
119
|
+
log.info("[\$calc] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
|
|
120
|
+
log.info("[\$calc] update calculationFields for $definitionName... ");
|
|
121
|
+
|
|
122
|
+
// Obtém detalhes da definição
|
|
123
|
+
def definitionEncoded = URLEncoder.encode(definitionName, "utf-8").replace("+", "%20")
|
|
124
|
+
def resp = actionPacks.rmRest.get( "recordm/definitions/name/${definitionEncoded}".toString(), [:], "");
|
|
125
|
+
JSONObject definition = new JSONObject(resp);
|
|
126
|
+
|
|
127
|
+
def fieldsSize = definition.fieldDefinitions.length();
|
|
128
|
+
|
|
129
|
+
def fields = [:]
|
|
130
|
+
(0..fieldsSize-1).each { index ->
|
|
131
|
+
def fieldDefinition = definition.fieldDefinitions.getJSONObject(index)
|
|
132
|
+
def fieldDescription = fieldDefinition.get("description")
|
|
133
|
+
def fieldDefId = fieldDefinition.get("id")
|
|
134
|
+
def fieldName = fieldDefinition.get("name");
|
|
135
|
+
fields[fieldDefId] = [name:fieldName, description: fieldDescription]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Finalmente obtém a lista de campos que é necessário calcular
|
|
139
|
+
def calculationFields = [];
|
|
140
|
+
def previousId
|
|
141
|
+
fields.each { fieldId,field ->
|
|
142
|
+
if(field.description.toString() =~ /[$]calc\./) {
|
|
143
|
+
def op = getCalculationOperation(field.description)
|
|
144
|
+
def args = getCalculationArgNames(field.description)
|
|
145
|
+
argsFields = [:]
|
|
146
|
+
args.each { arg ->
|
|
147
|
+
if(arg == "previous") {
|
|
148
|
+
argsFields[arg] = [previousId]
|
|
149
|
+
} else {
|
|
150
|
+
argsFields[arg] = fields.findAll{fId,f -> f.description?.toString() =~ /.*[$]$arg.*/ }.collect { fId,f -> fId}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
calculationFields << [fieldId: fieldId, name:field.name, op : op, args : argsFields]
|
|
154
|
+
}
|
|
155
|
+
previousId = fieldId
|
|
156
|
+
}
|
|
157
|
+
log.info("[\$calc] fields for '$definitionName': $calculationFields");
|
|
158
|
+
log.info("[\$calc] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
|
|
159
|
+
return calculationFields
|
|
160
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//----------------- Enable do $calc ------------------------
|
|
2
|
+
cob.custom.customize.push(function (core, utils, ui) {
|
|
3
|
+
core.customizeAllInstances((instance, presenter) => calc_automation(instance, presenter))
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
function calc_automation(instance, presenter) {
|
|
7
|
+
registerAndExecuteCalculation()
|
|
8
|
+
|
|
9
|
+
//=========================================================
|
|
10
|
+
// Support functions
|
|
11
|
+
//=========================================================
|
|
12
|
+
function registerAndExecuteCalculation() {
|
|
13
|
+
registerExecuteCalculationsOnChanges()
|
|
14
|
+
executeCalculations()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//=========================================================
|
|
18
|
+
function getAllCalculationsFields() {
|
|
19
|
+
return presenter
|
|
20
|
+
.findFieldPs( fp => /[$]calc\.(.*)/.exec(fp.field.fieldDefinition.description) )
|
|
21
|
+
.map( calculationFp => {
|
|
22
|
+
calculationFp.disable()
|
|
23
|
+
return {
|
|
24
|
+
fp: calculationFp,
|
|
25
|
+
op: getCalculationOperation(calculationFp),
|
|
26
|
+
args: getCalculationArguments(calculationFp)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
//=========================================================
|
|
31
|
+
function getCalculationOperation(calculationFp) {
|
|
32
|
+
let fieldDescription = calculationFp.field.fieldDefinition.description
|
|
33
|
+
let expr = fieldDescription.substr(fieldDescription.indexOf("$calc.")+6)
|
|
34
|
+
let matcher = /([^(]+)/
|
|
35
|
+
let op = expr.match(matcher)[1]
|
|
36
|
+
return op
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//=========================================================
|
|
40
|
+
function getCalculationArguments(calculationFp) {
|
|
41
|
+
let argNames = getCalculationArgNames(calculationFp)
|
|
42
|
+
return argNames
|
|
43
|
+
.map( argName => isNaN(argName)
|
|
44
|
+
? getAllAplicableFpsForVarName(calculationFp, argName) // É uma variável, retorna todos os fp associados
|
|
45
|
+
: argName * 1 // é uma constante numérica
|
|
46
|
+
)
|
|
47
|
+
.flat()
|
|
48
|
+
|
|
49
|
+
//=========================================================
|
|
50
|
+
function getCalculationArgNames(calculationFp) {
|
|
51
|
+
let fieldDescription = calculationFp.field.fieldDefinition.description;
|
|
52
|
+
let expr = fieldDescription.substr( fieldDescription.indexOf("$calc.") + 6 );
|
|
53
|
+
let matcher = /[^(]+\(([^(]+)\)/;
|
|
54
|
+
let argNamesArray = expr.match(matcher)[1].split(",");
|
|
55
|
+
return argNamesArray;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//=========================================================
|
|
59
|
+
function getAllAplicableFpsForVarName(calculationFp, varName) {
|
|
60
|
+
let result
|
|
61
|
+
if(varName === "previous") {
|
|
62
|
+
let todosCampos = presenter.findFieldPs(() => true).map(fp => fp.field.id )
|
|
63
|
+
result = presenter.findFieldPs(fp => fp.field.id === todosCampos[todosCampos.indexOf(calculationFp.field.id)-1])
|
|
64
|
+
} else {
|
|
65
|
+
result = presenter.findFieldPs( fp => fp.field.fieldDefinition.description && fp.field.fieldDefinition.description.includes("$"+varName))
|
|
66
|
+
}
|
|
67
|
+
return result
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//=========================================================
|
|
73
|
+
function registerExecuteCalculationsOnChanges() {
|
|
74
|
+
let calculations = getAllCalculationsFields()
|
|
75
|
+
// eventos de field changes de qualquer das variáveis (ou seja, sempre que o argumento não for um número)
|
|
76
|
+
calculations.forEach(
|
|
77
|
+
calculation => calculation.args.forEach( arg => {
|
|
78
|
+
if(isNaN(arg)) {
|
|
79
|
+
if(arg.field.fieldDefinition.duplicable) {
|
|
80
|
+
//O caso de campos duplicáveis é diferente porque é necessário voltar a registar tudo e só depois calcular
|
|
81
|
+
presenter.onFieldChange(arg.field.fieldDefinition.name, () => registerAndExecuteCalculation() )
|
|
82
|
+
} else {
|
|
83
|
+
//No caso de campos normais só é necessário calcular tudo quando uma dependência muda
|
|
84
|
+
presenter.onFieldChange(arg, () => executeCalculations() )
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//=========================================================
|
|
92
|
+
function executeCalculations() {
|
|
93
|
+
console.group("[Calculations] eval all $calc");
|
|
94
|
+
let calculations = getAllCalculationsFields() // Get fresh values
|
|
95
|
+
let t0 = performance.now();
|
|
96
|
+
calculations.forEach( calculation => {
|
|
97
|
+
let t0parcial = performance.now();
|
|
98
|
+
let novoResultado = "" + evaluateExpression(calculation)
|
|
99
|
+
if(calculation.fp.getValue() != novoResultado ) {
|
|
100
|
+
calculation.fp.setValue(novoResultado)
|
|
101
|
+
console.groupCollapsed("[Calculations] updated field ", calculation.fp.field.fieldDefinition.id, " '", calculation.fp.field.fieldDefinition.name, "' with ",novoResultado)
|
|
102
|
+
console.debug("[Calculations]", calculation.op)
|
|
103
|
+
console.debug("[Calculations]", calculation.args.map(arg => isNaN(arg) ? arg.field.fieldDefinition : arg))
|
|
104
|
+
console.debug("[Calculations] subcalc took " + (performance.now() - t0parcial) + " milliseconds.");
|
|
105
|
+
console.groupEnd()
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
console.debug("[Calculations] total calc took " + (performance.now() - t0) + " milliseconds.");
|
|
110
|
+
console.groupEnd();
|
|
111
|
+
return ;
|
|
112
|
+
|
|
113
|
+
//=========================================================
|
|
114
|
+
function evaluateExpression(calculation) {
|
|
115
|
+
// Obter valores para variaveis
|
|
116
|
+
let values = calculation.args.map(arg =>
|
|
117
|
+
arg.getValue
|
|
118
|
+
? isNaN(arg.getValue() * 1)
|
|
119
|
+
? 0
|
|
120
|
+
: parseFloat(arg.getValue())
|
|
121
|
+
: arg
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Realizar operação
|
|
125
|
+
let resultado = new BigDecimal(0)
|
|
126
|
+
if (calculation.op === "multiply" && values.length > 0) {
|
|
127
|
+
resultado = new BigDecimal(1);
|
|
128
|
+
values.forEach(value => resultado = resultado.multiply(new BigDecimal(value)))
|
|
129
|
+
|
|
130
|
+
} else if (calculation.op === "divide" && values.length === 2 && values[1] !== 0 ) {
|
|
131
|
+
resultado = new BigDecimal(values[0]);
|
|
132
|
+
resultado = resultado.divide(new BigDecimal(values[1]))
|
|
133
|
+
|
|
134
|
+
} else if (calculation.op === "sum") {
|
|
135
|
+
values.forEach(value => resultado = resultado.add(new BigDecimal(value)))
|
|
136
|
+
|
|
137
|
+
} else if (calculation.op === "subtract" && values.length === 2) {
|
|
138
|
+
resultado = new BigDecimal(values[0]);
|
|
139
|
+
resultado = resultado.subtract(new BigDecimal(values[1]))
|
|
140
|
+
}
|
|
141
|
+
return resultado
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// From https://stackoverflow.com/questions/16742578/bigdecimal-in-javascript
|
|
147
|
+
class BigDecimal {
|
|
148
|
+
// Configuration: constants
|
|
149
|
+
static DECIMALS = 8; // number of decimals on all instances
|
|
150
|
+
static ROUNDED = true; // numbers are truncated (false) or rounded (true)
|
|
151
|
+
static SHIFT = BigInt("1" + "0".repeat(BigDecimal.DECIMALS)); // derived constant
|
|
152
|
+
constructor(value) {
|
|
153
|
+
if (value instanceof BigDecimal) return value;
|
|
154
|
+
let [ints, decis] = String(value).split(".").concat("");
|
|
155
|
+
this._n = BigInt(ints + decis.padEnd(BigDecimal.DECIMALS, "0")
|
|
156
|
+
.slice(0, BigDecimal.DECIMALS))
|
|
157
|
+
+ BigInt(BigDecimal.ROUNDED && decis[BigDecimal.DECIMALS] >= "5");
|
|
158
|
+
}
|
|
159
|
+
static fromBigInt(bigint) {
|
|
160
|
+
return Object.assign(Object.create(BigDecimal.prototype), { _n: bigint });
|
|
161
|
+
}
|
|
162
|
+
add(num) {
|
|
163
|
+
return BigDecimal.fromBigInt(this._n + new BigDecimal(num)._n);
|
|
164
|
+
}
|
|
165
|
+
subtract(num) {
|
|
166
|
+
return BigDecimal.fromBigInt(this._n - new BigDecimal(num)._n);
|
|
167
|
+
}
|
|
168
|
+
static _divRound(dividend, divisor) {
|
|
169
|
+
return BigDecimal.fromBigInt(dividend / divisor
|
|
170
|
+
+ (BigDecimal.ROUNDED ? dividend * 2n / divisor % 2n : 0n));
|
|
171
|
+
}
|
|
172
|
+
multiply(num) {
|
|
173
|
+
return BigDecimal._divRound(this._n * new BigDecimal(num)._n, BigDecimal.SHIFT);
|
|
174
|
+
}
|
|
175
|
+
divide(num) {
|
|
176
|
+
return BigDecimal._divRound(this._n * BigDecimal.SHIFT, new BigDecimal(num)._n);
|
|
177
|
+
}
|
|
178
|
+
toString() {
|
|
179
|
+
const s = this._n.toString().padStart(BigDecimal.DECIMALS+1, "0");
|
|
180
|
+
const decimals = s.slice(-BigDecimal.DECIMALS).replace(/\.?0+$/, "")
|
|
181
|
+
return s.slice(0, -BigDecimal.DECIMALS) + (decimals ? ".":"") + decimals;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import("./cob/_calc.js");
|