cob-cli 2.5.1 → 2.6.2

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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cob-cli",
3
- "version": "2.5.1",
3
+ "version": "2.6.2",
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,81 @@
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
+ if (msg.product != "recordm-definition" && msg.product != "recordm" ) return
9
+
10
+ @Field static cacheOfAuditFieldsForDefinition = CacheBuilder.newBuilder()
11
+ .expireAfterWrite(5, TimeUnit.MINUTES)
12
+ .build();
13
+
14
+ if (msg.product == "recordm-definition") cacheOfAuditFieldsForDefinition.invalidate(msg.type)
15
+
16
+ // ========================================================================================================
17
+ def auditFields = cacheOfAuditFieldsForDefinition.get(msg.type, { getAuditFields(msg.type) })
18
+ if (auditFields.size() > 0
19
+ && msg.user != "integrationm"
20
+ && msg.action =~ "add|update" ) {
21
+
22
+ def updates = getAuditFieldsUpdates(auditFields,msg.instance.fields)
23
+ def result = actionPacks.recordm.update(msg.type, "recordmInstanceId:" + msg.instance.id, updates);
24
+ if(updates) log.info("[\$audit] UPDATE '${msg.type}' id:${msg.instance.id}, updates: ${updates}, result:${result.getStatus()} | ${result.getStatusInfo()} ");
25
+ }
26
+
27
+ // ========================================================================================================
28
+ def getAuditFieldsUpdates(auditFields,instanceFields) {
29
+ def updates = [:]
30
+ auditFields.each { auditField ->
31
+ if( auditField.op == "creator" && msg.action == "update" && msg.value(auditField.name) != null) return // 'creator' fields are only changed in 'update' if the previous value was empty (meaning it was a field that was not visible)
32
+ if( msg.action == 'update' && !msg.diff) return // Only continues if there is at least one change
33
+ if( auditField.args == "usermURI") {
34
+ updates << [(auditField.name) : actionPacks.get("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
+ updates << [(auditField.name) : "" + msg._timestamp_]
42
+ }
43
+ }
44
+ return updates
45
+ }
46
+
47
+ // ========================================================================================================
48
+ def getAuditFields(definitionName) {
49
+ /**/log.info("[\$audit] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
50
+ /**/log.info("[\$audit] Update 'auditFields' for '$definitionName'... ");
51
+
52
+ // Obtém detalhes da definição
53
+ def definitionEncoded = URLEncoder.encode(definitionName, "utf-8").replace("+", "%20")
54
+ def resp = actionPacks.rmRest.get( "recordm/definitions/name/${definitionEncoded}".toString(), [:], "");
55
+ JSONObject definition = new JSONObject(resp);
56
+
57
+ def fieldsSize = definition.fieldDefinitions.length();
58
+
59
+ def fields = [:]
60
+ (0..fieldsSize-1).each { index ->
61
+ def fieldDefinition = definition.fieldDefinitions.getJSONObject(index)
62
+ def fieldDescription = fieldDefinition.get("description")
63
+ def fieldDefId = fieldDefinition.get("id")
64
+ def fieldName = fieldDefinition.get("name");
65
+ fields[fieldDefId] = [name:fieldName, description: fieldDescription]
66
+ }
67
+
68
+ // Finalmente obtém a lista de campos que é necessário calcular
69
+ def auditFields = [];
70
+ fields.each { fieldId,field ->
71
+ def matcher = field.description =~ /[$]audit\.(creator|updater)\.(username|usermURI|time)/
72
+ if(matcher) {
73
+ def op = matcher[0][1]
74
+ def arg = matcher[0][2]
75
+ auditFields << [fieldId: fieldId, name:field.name, op : op, args: arg]
76
+ }
77
+ }
78
+ log.info("[\$audit] fields for '$definitionName': $auditFields");
79
+ /**/log.info("[\$audit] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
80
+ return auditFields
81
+ }
@@ -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|usermURI|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)\.usermURI/.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,162 @@
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
+ if (msg.product != "recordm-definition" && msg.product != "recordm" ) return
11
+
12
+ @Field static cacheOfCalcFieldsForDefinition = CacheBuilder.newBuilder()
13
+ .expireAfterWrite(5, TimeUnit.MINUTES)
14
+ .build();
15
+
16
+ if (msg.product == "recordm-definition") cacheOfCalcFieldsForDefinition.invalidate(msg.type)
17
+
18
+ // ========================================================================================================
19
+ def calculationFields = cacheOfCalcFieldsForDefinition.get(msg.type, { getAllCalculationFields(msg.type) })
20
+ if (calculationFields.size() > 0
21
+ && msg.product == "recordm"
22
+ && msg.user != "integrationm"
23
+ && msg.action =~ "add|update" ){
24
+
25
+ def updates = executeCalculations(calculationFields, msg.instance.fields)
26
+ def result = actionPacks.recordm.update(messageMap.type, "recordmInstanceId:" + messageMap.instance.id, updates);
27
+ if(updates) log.info("[\$calc] UPDATE '${msg.type}' id:${msg.instance.id}, updates: ${updates}, result:${result.getStatus()} | ${result.getStatusInfo()} ");
28
+ }
29
+
30
+ // ==================================================
31
+ def getCalculationOperation(fieldDescription) {
32
+ def matcher = fieldDescription =~/.*[$]calc.([^(]+)/
33
+ def op = matcher[0][1]
34
+ return op
35
+ }
36
+
37
+ // ==================================================
38
+ def getCalculationArgNames(fieldDescription) {
39
+ def matcher = fieldDescription =~/.*[$]calc.[^(]+\(([^(]+)\)/
40
+ def argNamesArray = matcher[0][1].tokenize(",")
41
+ return argNamesArray;
42
+ }
43
+
44
+ // ========================================================================================================
45
+ def executeCalculations(calculationFields,instanceFields) {
46
+ def updates = [:]
47
+ def atLeastOneChangeFlag = false;
48
+ def passCount = 0;
49
+ def temporaryResults = [:]
50
+ while(passCount++ == 0 || atLeastOneChangeFlag && passCount < 10) { //10 is just for security against loops
51
+ atLeastOneChangeFlag = false
52
+ calculationFields.each { calculation ->
53
+ def novoResultado = evaluateExpression(calculation,instanceFields,temporaryResults)
54
+ if(temporaryResults[calculation.fieldId] != novoResultado ) {
55
+ // log.info("[\$calc] {{passCount:${passCount}, field:${calculation.name} (${calculation.fieldId})" +
56
+ // ", calcType:${calculation.op}(${calculation.args})" +
57
+ // ", previousResult:${temporaryResults[calculation.fieldId]}" +
58
+ // ", calcValue:$novoResultado}}");
59
+
60
+ temporaryResults[calculation.fieldId] = novoResultado;
61
+ updates << [(calculation.name) : novoResultado]
62
+ atLeastOneChangeFlag = true
63
+ }
64
+ }
65
+ }
66
+ return updates
67
+ }
68
+
69
+ // ==================================================
70
+ def evaluateExpression(calculation,instanceFields,temporaryResults) {
71
+ // Realizar operação
72
+ def resultado = new BigDecimal(0)
73
+ def args = getCalculationArguments(calculation,instanceFields,temporaryResults)
74
+
75
+ if(calculation.op == "multiply" && args.size() > 0) {
76
+ resultado = 1
77
+ args.each { arg -> resultado = resultado.multiply(new BigDecimal(arg?.trim() ?: 0)) }
78
+
79
+ } else if (calculation.op == "divide" && args.size() == 2 && (args[1]?:0 != 0)) {
80
+ resultado = new BigDecimal(args[0]?.trim() ?:0);
81
+ resultado = resultado.divide(new BigDecimal(args[1]?.trim()), 8, RoundingMode.HALF_UP)
82
+
83
+ } else if(calculation.op == "sum") {
84
+ args.each { arg -> resultado = resultado + new BigDecimal(arg?.trim() ?: 0)}
85
+
86
+ } else if (calculation.op == "subtract" && args.size() == 2) {
87
+ resultado = new BigDecimal(args[0]?.trim() ?: 0);
88
+ resultado = resultado.subtract(new BigDecimal(args[1]?.trim() ?: 0))
89
+ }
90
+ return resultado.stripTrailingZeros().toPlainString()
91
+ }
92
+
93
+ // ==================================================
94
+ def getCalculationArguments(calculation,instanceFields,temporaryResults) {
95
+ def values = calculation.args.collect { argName,argFieldIds ->
96
+ (""+argName).isNumber()
97
+ ? argName * 1
98
+ : getAllAplicableValuesForVarName(calculation.fieldId,argName,argFieldIds,instanceFields,temporaryResults)
99
+ }
100
+ return values.flatten()
101
+ }
102
+
103
+ // ==================================================
104
+ def getAllAplicableValuesForVarName(fieldId,varName,varFieldIds,instanceFields,temporaryResults) {
105
+ // log.info("[\$calc] find '$varName'($varFieldIds) in $instanceFields (temporaryResults=$temporaryResults) ");
106
+ def relevantFields = instanceFields.findAll{ instField -> varFieldIds.indexOf(instField.fieldDefinition.id) >= 0 }
107
+
108
+ def result = varFieldIds.collect { varFieldId ->
109
+ if(temporaryResults[varFieldId] != null) {
110
+ return temporaryResults[varFieldId]
111
+ } else {
112
+ return temporaryResults[varFieldId] = instanceFields.findAll{ instField -> varFieldId == instField.fieldDefinition.id }?.collect { it.value }
113
+ }
114
+ }
115
+ // log.info("[\$calc] values for '$varName'($varFieldIds) = $result (temporaryResults=$temporaryResults) " );
116
+ return result.flatten()
117
+ }
118
+
119
+ // ========================================================================================================
120
+ def getAllCalculationFields(definitionName) {
121
+ log.info("[\$calc] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
122
+ log.info("[\$calc] update 'calculationFields' for '$definitionName'... ");
123
+
124
+ // Obtém detalhes da definição
125
+ def definitionEncoded = URLEncoder.encode(definitionName, "utf-8").replace("+", "%20")
126
+ def resp = actionPacks.rmRest.get( "recordm/definitions/name/${definitionEncoded}".toString(), [:], "");
127
+ JSONObject definition = new JSONObject(resp);
128
+
129
+ def fieldsSize = definition.fieldDefinitions.length();
130
+
131
+ def fields = [:]
132
+ (0..fieldsSize-1).each { index ->
133
+ def fieldDefinition = definition.fieldDefinitions.getJSONObject(index)
134
+ def fieldDescription = fieldDefinition.get("description")
135
+ def fieldDefId = fieldDefinition.get("id")
136
+ def fieldName = fieldDefinition.get("name");
137
+ fields[fieldDefId] = [name:fieldName, description: fieldDescription]
138
+ }
139
+
140
+ // Finalmente obtém a lista de campos que é necessário calcular
141
+ def calculationFields = [];
142
+ def previousId
143
+ fields.each { fieldId,field ->
144
+ if(field.description.toString() =~ /[$]calc\./) {
145
+ def op = getCalculationOperation(field.description)
146
+ def args = getCalculationArgNames(field.description)
147
+ argsFields = [:]
148
+ args.each { arg ->
149
+ if(arg == "previous") {
150
+ argsFields[arg] = [previousId]
151
+ } else {
152
+ argsFields[arg] = fields.findAll{fId,f -> f.description?.toString() =~ /.*[$]$arg.*/ }.collect { fId,f -> fId}
153
+ }
154
+ }
155
+ calculationFields << [fieldId: fieldId, name:field.name, op : op, args : argsFields]
156
+ }
157
+ previousId = fieldId
158
+ }
159
+ log.info("[\$calc] fields for '$definitionName': $calculationFields");
160
+ log.info("[\$calc] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
161
+ return calculationFields
162
+ }
@@ -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");