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.
@@ -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
+ }
@@ -6,7 +6,8 @@ exports.option = {
6
6
  name: 'choosenFollowUp',
7
7
  message: 'Select one?',
8
8
  choices: [
9
- require("./frontend.common").option
9
+ require("./frontend.common").option,
10
+ require("./frontend.formatList").option
10
11
  ]}
11
12
  ]
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
- var fs = require('fs');
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) , target, { clobber: true, filter: (src) => src.match(/node_modules/) == null }, (error) => {
14
- if (error) {
15
- // Error is an array of problems
16
- resolve(error.map(e => e.message).join("\n"));
17
- } else {
18
- resolve();
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 = '\n/* COB-CLI START ' + mergeFile + ' */ \n';
18
- let endStr = '\n/* COB-CLI END ' + mergeFile + ' */ \n';
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 = await execa('ssh', [cmdEnv.server, "[[ -f " + productDir + productScopeFilePath + " ]] && echo 'file' || echo '' " ])
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
- (isLocalFile || isRemoteFile)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cob-cli",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "A command line utility to help Cult of Bits partners develop with higher speed and reusing common code and best practices.",
5
5
  "preferGlobal": true,
6
6
  "repository": {
@@ -0,0 +1,11 @@
1
+ .currency_negative {
2
+ color:red;
3
+ }
4
+ .currency_positive {
5
+ color:green;
6
+ }
7
+
8
+ .currency_negative::after,
9
+ .currency_positive::after {
10
+ content: ' €';
11
+ }
@@ -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");