diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 818f6e47b..cd58a02f8 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +## [9.1.2-alpha.1](https://github.com/parse-community/parse-dashboard/compare/9.1.1...9.1.2-alpha.1) (2026-05-18) + + +### Bug Fixes + +* Chart not displayed when formula references an undefined field ([#3360](https://github.com/parse-community/parse-dashboard/issues/3360)) ([9666935](https://github.com/parse-community/parse-dashboard/commit/9666935659a28761c9e69242fe5aef730dffdfbc)) + ## [9.1.1-alpha.1](https://github.com/parse-community/parse-dashboard/compare/9.1.0...9.1.1-alpha.1) (2026-04-07) diff --git a/package-lock.json b/package-lock.json index b5a3b2606..22f98ea90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-dashboard", - "version": "9.1.1", + "version": "9.1.2-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "parse-dashboard", - "version": "9.1.1", + "version": "9.1.2-alpha.1", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@babel/runtime": "7.29.2", @@ -17,9 +17,9 @@ "commander": "13.1.0", "connect-flash": "0.1.1", "copy-to-clipboard": "3.3.3", - "core-js": "3.48.0", + "core-js": "3.49.0", "csrf-sync": "4.2.1", - "diff": "8.0.3", + "diff": "8.0.4", "expr-eval-fork": "3.0.3", "express": "5.2.1", "express-session": "1.19.0", @@ -88,12 +88,12 @@ "jest-environment-jsdom": "30.0.5", "madge": "8.0.0", "marked": "17.0.5", - "mongodb-runner": "^6.6.0", + "mongodb-runner": "6.7.3", "parse-server": "9.7.0", "prettier": "3.8.1", "puppeteer": "24.37.2", "react-test-renderer": "16.13.1", - "sass": "1.98.0", + "sass": "1.99.0", "sass-loader": "16.0.7", "semantic-release": "25.0.3", "semver": "7.7.4", @@ -101,7 +101,7 @@ "typescript": "6.0.2", "webpack": "5.105.1", "webpack-cli": "7.0.2", - "ws": "8.19.0", + "ws": "8.20.0", "yaml": "2.8.3" }, "engines": { @@ -4870,9 +4870,9 @@ } }, "node_modules/@mongodb-js/oidc-mock-provider": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-mock-provider/-/oidc-mock-provider-0.13.6.tgz", - "integrity": "sha512-2K7KfCwWquonVKvi/MEPWG+Q8vnTGLMcm1I5En0zf9Jqk6CFuoEnojVK7IJtVHdXYp9oR6fnNCMzir5xs965mQ==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-mock-provider/-/oidc-mock-provider-0.13.7.tgz", + "integrity": "sha512-OARcW2flFuMtA0TxHsE5oItrpr06w/w0Ihi1Rnqbakyc7/y/yARemBCP4iJX1qLKPLJYwGlEmWrAA4ocs5wrKg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5981,6 +5981,28 @@ "node": ">=20.19.0 <21 || >=22.12.0 <23 || >=24.1.0 <25" } }, + "node_modules/@parse/push-adapter/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@peculiar/asn1-cms": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", @@ -13565,9 +13587,9 @@ } }, "node_modules/core-js": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", - "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -14528,9 +14550,9 @@ "license": "BSD-3-Clause" }, "node_modules/diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -21740,14 +21762,14 @@ } }, "node_modules/mongodb-runner": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-6.7.1.tgz", - "integrity": "sha512-bYCk1EC0wp4boNyZYJ0Prk6Ye5Mq4ibANzL2RrI2vglSCunjaMfaMIFVJl7isyABwAqJah2RT8va/q8QKa26qQ==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-6.7.3.tgz", + "integrity": "sha512-wNGwVAuMh9+LF3ajfGTiGL2YV/dssrlf5eUuDuTwNC0xL/gSAZGYUJRGVeOQYy4ISMd8G+xTSPltwcoxy+F0UQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@mongodb-js/mongodb-downloader": "^1.1.7", - "@mongodb-js/oidc-mock-provider": "^0.13.6", + "@mongodb-js/mongodb-downloader": "^1.1.9", + "@mongodb-js/oidc-mock-provider": "^0.13.7", "@mongodb-js/saslprep": "^1.4.6", "@peculiar/x509": "^1.14.2", "debug": "^4.4.0", @@ -25219,28 +25241,6 @@ "node": ">=10" } }, - "node_modules/parse-server/node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/parse/node_modules/@babel/runtime": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", @@ -25262,6 +25262,27 @@ "node": ">=6.9.0" } }, + "node_modules/parse/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -26380,6 +26401,28 @@ "node": ">=18" } }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/puppeteer/node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -27672,9 +27715,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", - "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -31593,9 +31636,10 @@ } }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 2529f4af0..ddb8d73de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-dashboard", - "version": "9.1.1", + "version": "9.1.2-alpha.1", "repository": { "type": "git", "url": "https://github.com/parse-community/parse-dashboard" @@ -45,9 +45,9 @@ "commander": "13.1.0", "connect-flash": "0.1.1", "copy-to-clipboard": "3.3.3", - "core-js": "3.48.0", + "core-js": "3.49.0", "csrf-sync": "4.2.1", - "diff": "8.0.3", + "diff": "8.0.4", "expr-eval-fork": "3.0.3", "express": "5.2.1", "express-session": "1.19.0", @@ -118,12 +118,12 @@ "jest-environment-jsdom": "30.0.5", "madge": "8.0.0", "marked": "17.0.5", - "mongodb-runner": "^6.6.0", + "mongodb-runner": "6.7.3", "parse-server": "9.7.0", "prettier": "3.8.1", "puppeteer": "24.37.2", "react-test-renderer": "16.13.1", - "sass": "1.98.0", + "sass": "1.99.0", "sass-loader": "16.0.7", "semantic-release": "25.0.3", "semver": "7.7.4", @@ -131,7 +131,7 @@ "typescript": "6.0.2", "webpack": "5.105.1", "webpack-cli": "7.0.2", - "ws": "8.19.0", + "ws": "8.20.0", "yaml": "2.8.3" }, "scripts": { diff --git a/src/lib/FormulaEvaluator.js b/src/lib/FormulaEvaluator.js index f2a59faf3..3eb950901 100644 --- a/src/lib/FormulaEvaluator.js +++ b/src/lib/FormulaEvaluator.js @@ -140,7 +140,19 @@ export function evaluateFormula(formula, variables) { try { const processedFormula = preprocessFormula(formula); const expr = parser.parse(processedFormula); - let result = expr.evaluate(variables); + + // Default any variables referenced by the formula but missing from the + // provided variables to 0. This keeps charts rendering when a referenced + // field has no value on a row (consistent with the "sum" operator, which + // already treats missing fields as 0). + const safeVariables = { ...(variables || {}) }; + for (const varName of expr.variables()) { + if (!(varName in safeVariables)) { + safeVariables[varName] = 0; + } + } + + let result = expr.evaluate(safeVariables); // Convert boolean results to numbers (for comparison operators) if (typeof result === 'boolean') { diff --git a/src/lib/tests/FormulaEvaluator.test.js b/src/lib/tests/FormulaEvaluator.test.js index e607c4aa8..05d1a0b9f 100644 --- a/src/lib/tests/FormulaEvaluator.test.js +++ b/src/lib/tests/FormulaEvaluator.test.js @@ -169,8 +169,15 @@ describe('FormulaEvaluator', () => { expect(evaluateFormula('invalid syntax !!!', { x: 10 })).toBe(null); }); - it('should return null for undefined variables', () => { - expect(evaluateFormula('unknownVar * 2', {})).toBe(null); + it('should treat undefined variables as 0', () => { + // Undefined variables should default to 0 so charts still render + // when a referenced field has no value on a row (consistent with the + // behavior of the "sum" operator, which already treats missing fields + // as 0). + expect(evaluateFormula('unknownVar * 2', {})).toBe(0); + expect(evaluateFormula('x + unknownVar', { x: 5 })).toBe(5); + expect(evaluateFormula('x * y', { x: 10 })).toBe(0); + expect(evaluateFormula('a + b + c', { b: 7 })).toBe(7); }); it('should return null for NaN results', () => { diff --git a/src/lib/tests/GraphDataUtils.test.js b/src/lib/tests/GraphDataUtils.test.js index 5bae460dc..3de87a962 100644 --- a/src/lib/tests/GraphDataUtils.test.js +++ b/src/lib/tests/GraphDataUtils.test.js @@ -332,6 +332,55 @@ describe('GraphDataUtils', () => { // Should not throw, chart should render with regular values expect(result).toHaveProperty('datasets'); }); + + it('should render chart when a referenced field is undefined on some rows', () => { + // Reproduces the bug where a formula referencing a field that is + // undefined on a row caused the entire chart to be empty. Undefined + // fields should be treated as 0 instead. + const dataWithMissingField = [ + { attributes: { month: 'Jan', price: 10 } }, // quantity undefined + { attributes: { month: 'Feb', price: 20, quantity: 3 } }, + { attributes: { month: 'Mar', price: 15 } }, // quantity undefined + ]; + + const calculatedValues = [{ + name: 'Total', + operator: 'formula', + formula: 'price * quantity', + }]; + + const result = processBarLineData(dataWithMissingField, 'month', [], null, calculatedValues); + + expect(result).toHaveProperty('datasets'); + expect(result.datasets.length).toBe(1); + expect(result.datasets[0].label).toBe('Total'); + // Jan: 10 * 0 = 0, Feb: 20 * 3 = 60, Mar: 15 * 0 = 0 + expect(result.datasets[0].data).toContain(60); + }); + + it('should render chart when the referenced field is undefined on every row', () => { + // Even more extreme case: the field exists in the schema but no row + // has a value for it. The chart should still render (using 0 for the + // missing field) rather than disappearing entirely. + const dataWithAllMissing = [ + { attributes: { month: 'Jan', price: 10 } }, + { attributes: { month: 'Feb', price: 20 } }, + ]; + + const calculatedValues = [{ + name: 'Total', + operator: 'formula', + formula: 'price + quantity', + }]; + + const result = processBarLineData(dataWithAllMissing, 'month', [], null, calculatedValues); + + expect(result).toHaveProperty('datasets'); + expect(result.datasets.length).toBe(1); + // Jan: 10 + 0 = 10, Feb: 20 + 0 = 20 + expect(result.datasets[0].data).toContain(10); + expect(result.datasets[0].data).toContain(20); + }); }); describe('processPieData with formula', () => {