diff --git a/assets/element-templates.css b/assets/element-templates.css index c7392c53..80af07fd 100644 --- a/assets/element-templates.css +++ b/assets/element-templates.css @@ -9,6 +9,9 @@ --unknown-template-background-color: var(--color-red-360-100-45); --unknown-template-hover-background-color: var(--color-red-360-100-40); + --incompatible-template-background-color: rgb(255, 131, 43); + --incompatible-template-hover-background-color: hsl(25, 100%, 50%); + --select-template-information-text-color: var(--color-grey-225-10-55); --deprecated-template-hover-background-color: var(--color-grey-225-10-50); @@ -59,7 +62,8 @@ .bio-properties-panel-template-update-available:last-child, .bio-properties-panel-applied-template-button:last-child, -.bio-properties-panel-template-not-found:last-child { +.bio-properties-panel-template-not-found:last-child, +.bio-properties-panel-template-incompatible:last-child { margin-right: 32px; } @@ -77,7 +81,6 @@ background-color: var(--deprecated-template-hover-background-color); } - .bio-properties-panel-template-not-found .bio-properties-panel-group-header-button { background-color: var(--unknown-template-background-color); color: var(--select-template-label-color); @@ -88,13 +91,25 @@ background-color: var(--unknown-template-hover-background-color); } +.bio-properties-panel-template-incompatible .bio-properties-panel-group-header-button { + background-color: var(--incompatible-template-background-color); + color: var(--select-template-label-color); + fill: var(--select-template-fill-color); +} + +.bio-properties-panel-template-incompatible .bio-properties-panel-group-header-button:hover { + background-color: var(--incompatible-template-hover-background-color); +} + .bio-properties-panel-template-not-found-text, -.bio-properties-panel-template-update-available-text { +.bio-properties-panel-template-update-available-text, +.bio-properties-panel-template-incompatible-text { color: var(--select-template-information-text-color); } .bio-properties-panel-template-not-found-text, .bio-properties-panel-template-update-available-text, -.bio-properties-panel-deprecated-template-text { +.bio-properties-panel-deprecated-template-text, +.bio-properties-panel-template-incompatible-text { width: 216px; } diff --git a/karma.config.js b/karma.config.js index ead36993..9db925dc 100644 --- a/karma.config.js +++ b/karma.config.js @@ -1,6 +1,9 @@ /* eslint-env node */ const path = require('path'); + +const pkg = require('./package.json'); + const { DefinePlugin, NormalModuleReplacementPlugin @@ -93,6 +96,9 @@ module.exports = function(karma) { plugins: [ new DefinePlugin({ + // @nikku needs to be defined + 'process.env.PKG_VERSION': JSON.stringify(pkg.version), + // @barmac: process.env has to be defined to make @testing-library/preact work 'process.env': {} }), diff --git a/package-lock.json b/package-lock.json index da5dcb77..4fcf02c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.3.0", "license": "MIT", "dependencies": { - "@bpmn-io/element-templates-validator": "^2.1.0", + "@bpmn-io/element-templates-validator": "^2.2.0", "@bpmn-io/extract-process-variables": "^1.0.0", "bpmnlint": "^10.3.0", "classnames": "^2.3.1", @@ -17,6 +17,7 @@ "min-dash": "^4.0.0", "min-dom": "^4.0.3", "preact-markup": "^2.1.1", + "semver": "^7.6.3", "semver-compare": "^1.0.0", "uuid": "^11.0.0" }, @@ -33,6 +34,7 @@ "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-replace": "^6.0.1", "@testing-library/preact": "^2.0.1", "@testing-library/preact-hooks": "^1.1.0", "assert": "^2.1.0", @@ -64,6 +66,7 @@ "karma-webpack": "^5.0.1", "mocha": "^10.8.2", "mocha-test-container-support": "^0.2.0", + "modeler-moddle": "^0.2.0", "npm-run-all2": "^7.0.0", "puppeteer": "^23.7.0", "raw-loader": "^4.0.2", @@ -163,6 +166,15 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", @@ -209,6 +221,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", @@ -463,12 +484,13 @@ } }, "node_modules/@bpmn-io/element-templates-validator": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-2.1.0.tgz", - "integrity": "sha512-e8oYLUaZbL1ZuJjwXFyhhStbg0YgMNosIlzhKWdY7ysPhCFVMJlJ6yNYdaxyqfpPATTKb05uXMAsIgcqTQpoLg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-2.2.0.tgz", + "integrity": "sha512-YsgvHUSSf8oGpc6C5AchQwD7wWo41vmMw3Aa9dT+myI7vgNJlo32aRfdtNkBdRCaU1OZQMIZoF0bCEasRmKgPQ==", + "license": "MIT", "dependencies": { - "@camunda/element-templates-json-schema": "^0.18.0", - "@camunda/zeebe-element-templates-json-schema": "^0.20.0", + "@camunda/element-templates-json-schema": "^0.18.1", + "@camunda/zeebe-element-templates-json-schema": "^0.21.0", "json-source-map": "^0.6.1", "min-dash": "^4.1.1" } @@ -567,9 +589,10 @@ } }, "node_modules/@camunda/element-templates-json-schema": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@camunda/element-templates-json-schema/-/element-templates-json-schema-0.18.0.tgz", - "integrity": "sha512-k2k+1Z7UiW1TSA1oAvDQamgFZljH3hkFjU9VSpjVXnPgcjVxJMLX0mrHjLVtXhEx2tw576FzYGqlfudw6OOMKg==" + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@camunda/element-templates-json-schema/-/element-templates-json-schema-0.18.1.tgz", + "integrity": "sha512-gwQJHUYx1FrIJCgJISx2cpqTJYgnsqrJ6dpPX/R0p6ELyK6u4rHAi/m9QS1O4F6ua7dBlFFFOOtuIAbo5mAfAg==", + "license": "MIT" }, "node_modules/@camunda/linting": { "version": "3.23.0", @@ -623,9 +646,10 @@ } }, "node_modules/@camunda/zeebe-element-templates-json-schema": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.20.0.tgz", - "integrity": "sha512-7YRN32Nq73H8S1rCOy2/6cfx+fKiTnhveJYfP6aRaIi83ZSlhVomRJ5+pnPmlDJqdFeNcIx1qqQwVFAdgNPFhg==" + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.21.0.tgz", + "integrity": "sha512-etC2PXoHAQ+74xokXYczfb5HmLtisSYCeKSl1c5PCKD6nWWLpGaha86edkPvTKt6SbHCDLZ0dzWXY0iBpyWfow==", + "license": "MIT" }, "node_modules/@codemirror/autocomplete": { "version": "6.17.0", @@ -1189,19 +1213,6 @@ "node": ">=12" } }, - "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@puppeteer/browsers/node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -1347,6 +1358,28 @@ } } }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", + "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", @@ -2587,18 +2620,6 @@ "node": ">=10" } }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4333,6 +4354,15 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-mocha": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", @@ -4441,6 +4471,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-scope": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", @@ -6258,6 +6297,15 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", @@ -6980,6 +7028,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -7346,7 +7403,8 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/modeler-moddle/-/modeler-moddle-0.2.0.tgz", "integrity": "sha512-l8OUpvX94m3spe+RBwWFQ0bGvPBZ3FBCiSY3yNtDk52j0YRj+cnVOxTMQvVM+i6k1T326IfqYM3F9HJfPZtXRw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mri": { "version": "1.2.0", @@ -8840,12 +8898,14 @@ "dev": true }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-compare": { @@ -10327,6 +10387,14 @@ "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, "@babel/generator": { @@ -10362,6 +10430,14 @@ "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, "@babel/helper-module-imports": { @@ -10544,12 +10620,12 @@ } }, "@bpmn-io/element-templates-validator": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-2.1.0.tgz", - "integrity": "sha512-e8oYLUaZbL1ZuJjwXFyhhStbg0YgMNosIlzhKWdY7ysPhCFVMJlJ6yNYdaxyqfpPATTKb05uXMAsIgcqTQpoLg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-2.2.0.tgz", + "integrity": "sha512-YsgvHUSSf8oGpc6C5AchQwD7wWo41vmMw3Aa9dT+myI7vgNJlo32aRfdtNkBdRCaU1OZQMIZoF0bCEasRmKgPQ==", "requires": { - "@camunda/element-templates-json-schema": "^0.18.0", - "@camunda/zeebe-element-templates-json-schema": "^0.20.0", + "@camunda/element-templates-json-schema": "^0.18.1", + "@camunda/zeebe-element-templates-json-schema": "^0.21.0", "json-source-map": "^0.6.1", "min-dash": "^4.1.1" } @@ -10637,9 +10713,9 @@ } }, "@camunda/element-templates-json-schema": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@camunda/element-templates-json-schema/-/element-templates-json-schema-0.18.0.tgz", - "integrity": "sha512-k2k+1Z7UiW1TSA1oAvDQamgFZljH3hkFjU9VSpjVXnPgcjVxJMLX0mrHjLVtXhEx2tw576FzYGqlfudw6OOMKg==" + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@camunda/element-templates-json-schema/-/element-templates-json-schema-0.18.1.tgz", + "integrity": "sha512-gwQJHUYx1FrIJCgJISx2cpqTJYgnsqrJ6dpPX/R0p6ELyK6u4rHAi/m9QS1O4F6ua7dBlFFFOOtuIAbo5mAfAg==" }, "@camunda/linting": { "version": "3.23.0", @@ -10680,9 +10756,9 @@ } }, "@camunda/zeebe-element-templates-json-schema": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.20.0.tgz", - "integrity": "sha512-7YRN32Nq73H8S1rCOy2/6cfx+fKiTnhveJYfP6aRaIi83ZSlhVomRJ5+pnPmlDJqdFeNcIx1qqQwVFAdgNPFhg==" + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.21.0.tgz", + "integrity": "sha512-etC2PXoHAQ+74xokXYczfb5HmLtisSYCeKSl1c5PCKD6nWWLpGaha86edkPvTKt6SbHCDLZ0dzWXY0iBpyWfow==" }, "@codemirror/autocomplete": { "version": "6.17.0", @@ -11104,12 +11180,6 @@ "wrap-ansi": "^7.0.0" } }, - "semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true - }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -11187,6 +11257,16 @@ "resolve": "^1.22.1" } }, + "@rollup/plugin-replace": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", + "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + } + }, "@rollup/pluginutils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", @@ -12111,12 +12191,6 @@ "istanbul-lib-coverage": "^3.2.0", "semver": "^7.5.4" } - }, - "semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true } } }, @@ -13507,6 +13581,12 @@ "requires": { "esutils": "^2.0.2" } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, @@ -13577,6 +13657,12 @@ "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, @@ -14747,6 +14833,14 @@ "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, "istanbul-lib-report": { @@ -15322,6 +15416,14 @@ "dev": true, "requires": { "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, "media-typer": { @@ -16668,10 +16770,9 @@ } }, "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" }, "semver-compare": { "version": "1.0.0", diff --git a/package.json b/package.json index 3edce23e..01b329bf 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ ], "license": "MIT", "dependencies": { - "@bpmn-io/element-templates-validator": "^2.1.0", + "@bpmn-io/element-templates-validator": "^2.2.0", "@bpmn-io/extract-process-variables": "^1.0.0", "bpmnlint": "^10.3.0", "classnames": "^2.3.1", @@ -68,6 +68,7 @@ "min-dash": "^4.0.0", "min-dom": "^4.0.3", "preact-markup": "^2.1.1", + "semver": "^7.6.3", "semver-compare": "^1.0.0", "uuid": "^11.0.0" }, @@ -84,6 +85,7 @@ "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-replace": "^6.0.1", "@testing-library/preact": "^2.0.1", "@testing-library/preact-hooks": "^1.1.0", "assert": "^2.1.0", @@ -115,6 +117,7 @@ "karma-webpack": "^5.0.1", "mocha": "^10.8.2", "mocha-test-container-support": "^0.2.0", + "modeler-moddle": "^0.2.0", "npm-run-all2": "^7.0.0", "puppeteer": "^23.7.0", "raw-loader": "^4.0.2", diff --git a/rollup.config.mjs b/rollup.config.mjs index df420c17..74f091e7 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -4,6 +4,7 @@ import commonjs from '@rollup/plugin-commonjs'; import copy from 'rollup-plugin-copy'; import json from '@rollup/plugin-json'; import resolve from '@rollup/plugin-node-resolve'; +import replace from '@rollup/plugin-replace'; import { readFileSync @@ -81,6 +82,12 @@ function pgl(plugins = []) { function corePlugins(plugins = []) { return [ ...plugins, + replace({ + preventAssignment: true, + values: { + 'process.env.PKG_VERSION': JSON.stringify(pkg.version) + } + }), json(), resolve({ mainFields: [ diff --git a/src/cloud-element-templates/ElementTemplates.js b/src/cloud-element-templates/ElementTemplates.js index 08df40ea..2fecc163 100644 --- a/src/cloud-element-templates/ElementTemplates.js +++ b/src/cloud-element-templates/ElementTemplates.js @@ -9,8 +9,8 @@ import { default as DefaultElementTemplates } from '../element-templates/Element * Registry for element templates. */ export default class ElementTemplates extends DefaultElementTemplates { - constructor(templateElementFactory, commandStack, eventBus, modeling, injector) { - super(commandStack, eventBus, modeling, injector); + constructor(templateElementFactory, commandStack, eventBus, modeling, injector, config) { + super(commandStack, eventBus, modeling, injector, config); this._templateElementFactory = templateElementFactory; } @@ -75,6 +75,7 @@ export default class ElementTemplates extends DefaultElementTemplates { return context.element; } + } ElementTemplates.$inject = [ @@ -82,5 +83,6 @@ ElementTemplates.$inject = [ 'commandStack', 'eventBus', 'modeling', - 'injector' + 'injector', + 'config.elementTemplates', ]; diff --git a/src/cloud-element-templates/ElementTemplatesLoader.js b/src/cloud-element-templates/ElementTemplatesLoader.js index 2892b944..1f9df917 100644 --- a/src/cloud-element-templates/ElementTemplatesLoader.js +++ b/src/cloud-element-templates/ElementTemplatesLoader.js @@ -2,10 +2,16 @@ import { Validator } from './Validator'; import { default as TemplatesLoader } from '../element-templates/ElementTemplatesLoader'; +/** + * @param {Object|Array|Function} config + * @param {EventBus} eventBus + * @param {ElementTemplates} elementTemplates + * @param {Moddle} moddle + */ export default class ElementTemplatesLoader extends TemplatesLoader { - constructor(loadTemplates, eventBus, elementTemplates, moddle) { + constructor(config, eventBus, elementTemplates, moddle) { - super(loadTemplates, eventBus, elementTemplates, moddle); + super(config, eventBus, elementTemplates, moddle); this._elementTemplates = elementTemplates; } @@ -22,10 +28,8 @@ export default class ElementTemplatesLoader extends TemplatesLoader { elementTemplates.set(validTemplates); if (errors.length) { - this.templateErrors(errors); + this._templateErrors(errors); } - - this.templatesChanged(); } } diff --git a/src/cloud-element-templates/Validator.js b/src/cloud-element-templates/Validator.js index a2b95189..8b51d3ea 100644 --- a/src/cloud-element-templates/Validator.js +++ b/src/cloud-element-templates/Validator.js @@ -6,11 +6,16 @@ import { import semverCompare from 'semver-compare'; +import { + validRange as isSemverRangeValid +} from 'semver'; + import { validateZeebe as validateAgainstSchema, getZeebeSchemaPackage as getTemplateSchemaPackage, getZeebeSchemaVersion as getTemplateSchemaVersion } from '@bpmn-io/element-templates-validator'; +import { forEach } from 'min-dash'; const SUPPORTED_SCHEMA_VERSION = getTemplateSchemaVersion(); const SUPPORTED_SCHEMA_PACKAGE = getTemplateSchemaPackage(); @@ -31,8 +36,6 @@ export class Validator extends BaseValidator { * @return {Error} validation error, if any */ _validateTemplate(template) { - let err; - const id = template.id, version = template.version || '_', schema = template.$schema, @@ -78,25 +81,48 @@ export class Validator extends BaseValidator { } // (5) JSON schema compliance - const validationResult = validateAgainstSchema(template); + const schemaValidationResult = validateAgainstSchema(template); const { - errors, + errors: schemaErrors, valid - } = validationResult; + } = schemaValidationResult; if (!valid) { - err = new Error('invalid template'); - - filteredSchemaErrors(errors).forEach((error) => { + filteredSchemaErrors(schemaErrors).forEach((error) => { this._logError(error.message, template); }); + + return new Error('invalid template'); } - return err; + // (6) engines validation + const enginesError = this._validateEngines(template); + + if (enginesError) { + return enginesError; + } + + return null; } isSchemaValid(schema) { return schema && schema.includes(SUPPORTED_SCHEMA_PACKAGE); } + + _validateEngines(template) { + + let err; + + forEach(template.engines, (rangeStr, engine) => { + + if (!isSemverRangeValid(rangeStr)) { + err = this._logError(new Error( + `Engine <${engine}> specifies invalid semver range <${rangeStr}>` + ), template); + } + }); + + return err; + } } diff --git a/src/cloud-element-templates/linting/index.js b/src/cloud-element-templates/linting/index.js index 1853ba81..03555b76 100644 --- a/src/cloud-element-templates/linting/index.js +++ b/src/cloud-element-templates/linting/index.js @@ -1,4 +1,29 @@ -export { - elementTemplateLintRule, - ElementTemplateLinterPlugin -} from './LinterPlugin'; +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import StaticResolver from 'bpmnlint/lib/resolver/static-resolver'; + +import validate from './rules/element-templates-validate'; +import compatibility from './rules/element-templates-compatibility'; + +export const ElementTemplateLinterPlugin = function(templates) { + return { + config: { + rules: { + 'element-templates/validate': [ 'error', { templates } ], + 'element-templates/compatibility': [ 'warn', { templates } ] + } + }, + resolver: new StaticResolver({ + 'rule:bpmnlint-plugin-element-templates/validate': validate, + 'rule:bpmnlint-plugin-element-templates/compatibility': compatibility + }) + }; +}; diff --git a/src/cloud-element-templates/linting/rules/element-templates-compatibility.js b/src/cloud-element-templates/linting/rules/element-templates-compatibility.js new file mode 100644 index 00000000..9a10499f --- /dev/null +++ b/src/cloud-element-templates/linting/rules/element-templates-compatibility.js @@ -0,0 +1,106 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import ElementTemplates from '../../ElementTemplates'; +import EventBus from 'diagram-js/lib/core/EventBus'; + +import BpmnModdle from 'bpmn-moddle'; +import { is } from 'bpmn-js/lib/util/ModelUtil'; + +import zeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe'; + +import { Validator } from '../../Validator'; + +export default function({ templates = [] }) { + const moddle = new BpmnModdle({ zeebe: zeebeModdle }); + + const validator = new Validator(moddle).addAll(templates); + const validTemplates = validator.getValidTemplates(); + + // We use the ElementTemplates Module without the required bpmn-js modules + // As we only use it to facilitate template ID and version lookup, + // access to commandstack etc. is not required + const eventBus = new EventBus(); + const elementTemplates = new ElementTemplates(null, null, eventBus, null, null); + + elementTemplates.set(validTemplates); + + function isUpdateAvailable(template) { + + const latestTemplate = elementTemplates.getLatest(template.id, { deprecated: true })[0]; + + if (latestTemplate && latestTemplate !== template) { + return true; + } + + return false; + } + + function check(node, reporter) { + + if (is(node, 'bpmn:Definitions')) { + elementTemplates.setEngines(getEnginesConfig(node)); + } + + if (!is(node, 'bpmn:FlowElement')) { + return; + } + + let template = elementTemplates.get(node); + + if (!template) { + return; + } + + // Check compatibility + if (template.engines) { + const incomp = elementTemplates.getIncompatibleEngines(template); + Object.keys(incomp).forEach((engine) => { + reporter.report( + node.id, + getIncompatibilityText(engine, incomp[engine], isUpdateAvailable(template)), + { + name: node.name + } + ); + }); + } + } + + return { + check + }; + +}; + +// helpers ////////////////////// + +function getEnginesConfig(definitions) { + const engines = {}; + + const executionPlatform = definitions.get('modeler:executionPlatform'); + const executionPlatformVersion = definitions.get('modeler:executionPlatformVersion'); + + if (executionPlatform === 'Camunda Cloud' && executionPlatformVersion) { + engines.camunda = executionPlatformVersion; + } + + return engines; +} + + +function getIncompatibilityText(engine, { actual, required }, updateAvailable) { + const message = + `Element template incompatible with current <${engine}> environment. ` + + `Requires '${engine} ${required}'; found '${actual}'. ` + + `${updateAvailable ? 'Update available.' : ''}`; + + return message.trim(); +} diff --git a/src/cloud-element-templates/linting/LinterPlugin.js b/src/cloud-element-templates/linting/rules/element-templates-validate.js similarity index 76% rename from src/cloud-element-templates/linting/LinterPlugin.js rename to src/cloud-element-templates/linting/rules/element-templates-validate.js index 827eef08..50d51267 100644 --- a/src/cloud-element-templates/linting/LinterPlugin.js +++ b/src/cloud-element-templates/linting/rules/element-templates-validate.js @@ -8,20 +8,21 @@ * except in compliance with the MIT License. */ -import StaticResolver from 'bpmnlint/lib/resolver/static-resolver'; -import ElementTemplates from '../ElementTemplates'; -import { getPropertyValue, validateProperty } from '../util/propertyUtil'; +import ElementTemplates from '../../ElementTemplates'; +import EventBus from 'diagram-js/lib/core/EventBus'; -import { applyConditions } from '../Condition'; +import { getPropertyValue, validateProperty } from '../../util/propertyUtil'; + +import { applyConditions } from '../../Condition'; import BpmnModdle from 'bpmn-moddle'; import { is } from 'bpmn-js/lib/util/ModelUtil'; import zeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe'; -import { Validator } from '../Validator'; +import { Validator } from '../../Validator'; -export const elementTemplateLintRule = ({ templates = [] }) => { +export default function({ templates = [] }) { const moddle = new BpmnModdle({ zeebe: zeebeModdle }); const validator = new Validator(moddle).addAll(templates); @@ -30,7 +31,9 @@ export const elementTemplateLintRule = ({ templates = [] }) => { // We use the ElementTemplates Module without the required bpmn-js modules // As we only use it to facilitate template ID and version lookup, // access to commandstack etc. is not required - const elementTemplates = new ElementTemplates(); + const eventBus = new EventBus(); + const elementTemplates = new ElementTemplates(null, null, eventBus, null, null); + elementTemplates.set(validTemplates); function check(node, reporter) { @@ -89,21 +92,6 @@ export const elementTemplateLintRule = ({ templates = [] }) => { }; - -export const ElementTemplateLinterPlugin = function(templates) { - return { - config: { - rules: { - 'element-templates/validate': [ 'error', { templates } ] - } - }, - resolver: new StaticResolver({ - 'rule:bpmnlint-plugin-element-templates/validate': elementTemplateLintRule - }) - }; -}; - - // helpers ////////////////////// function getEntryId(property, template) { @@ -119,4 +107,4 @@ function getEntryId(property, template) { path.push(index); return path.join('-'); -} \ No newline at end of file +} diff --git a/src/components/ElementTemplatesGroup.js b/src/components/ElementTemplatesGroup.js index 121e3ddf..b9405c93 100644 --- a/src/components/ElementTemplatesGroup.js +++ b/src/components/ElementTemplatesGroup.js @@ -149,6 +149,8 @@ function TemplateGroupButtons({ element, getTemplateId }) { return ; } else if (templateState.type === 'DEPRECATED_TEMPLATE') { return ; + } else if (templateState.type === 'INCOMPATIBLE_TEMPLATE') { + return ; } else if (templateState.type === 'OUTDATED_TEMPLATE') { return ( }, + { entry: }, { separator: true }, { entry: translate('Update'), action: () => elementTemplates.applyTemplate(element, newerTemplate) }, { entry: translate('Unlink'), action: () => elementTemplates.unlinkTemplate(element) }, { entry: , action: () => elementTemplates.removeTemplate(element) } ]; + const cls = compatible + ? 'bio-properties-panel-template-update-available' + : 'bio-properties-panel-template-incompatible'; + + const text = compatible + ? translate('Update available') + : translate('Incompatible'); + return ( - + - { translate('Update available') } + { text } ); } -function UpdateAvailableText({ newerTemplate }) { +function UpdateAvailableText({ newerTemplate, compatible }) { const translate = useService('translate'); - const text = translate( - 'A new version of the template is available: {templateVersion}', - { templateVersion: getVersionOrDateFromTemplate(newerTemplate) } - ); + const text = compatible + ? translate( + 'A new version of the template is available: {templateVersion}', + { templateVersion: getVersionOrDateFromTemplate(newerTemplate) } + ) + : translate( + 'A version of this template is available that supports your environment: {templateVersion}', + { templateVersion: getVersionOrDateFromTemplate(newerTemplate) } + ); return
{text}
; } @@ -320,6 +335,39 @@ function DocumentationIcon() { ; } +function IncompatibleTemplate({ element }) { + const translate = useService('translate'), + elementTemplates = useService('elementTemplates'); + + const menuItems = [ + { entry: }, + { separator: true }, + { entry: translate('Unlink'), action: () => elementTemplates.unlinkTemplate(element) }, + { entry: , action: () => elementTemplates.removeTemplate(element) } + ]; + + return ( + + + { translate('Incompatible') } + + + + ); +} + +function IncompatibleText() { + const translate = useService('translate'); + + return ( +
+ { translate( + 'No compatible version of this template was found for your environment. Unlink to access the template’s data.' + ) } +
+ ); +} + // helper ////// @@ -347,10 +395,16 @@ function getTemplateState(elementTemplates, element, getTemplateId) { return { type: 'DEPRECATED_TEMPLATE', template }; } - const newerTemplate = elementTemplates.getLatest(templateId, { deprecated: true })[0]; + const compatible = elementTemplates.isCompatible(template); + + const latestTemplate = elementTemplates.getLatest(templateId, { deprecated: true })[0]; + + if (latestTemplate && latestTemplate !== template) { + return { type: 'OUTDATED_TEMPLATE', template, newerTemplate: latestTemplate, compatible }; + } - if (newerTemplate !== template) { - return { type: 'OUTDATED_TEMPLATE', template, newerTemplate }; + if (!compatible) { + return { type: 'INCOMPATIBLE_TEMPLATE', template }; } return { type: 'KNOWN_TEMPLATE', template }; diff --git a/src/element-templates/ElementTemplates.js b/src/element-templates/ElementTemplates.js index adc43213..8e92e474 100644 --- a/src/element-templates/ElementTemplates.js +++ b/src/element-templates/ElementTemplates.js @@ -2,10 +2,12 @@ import { filter, find, flatten, + has, isNil, isObject, isString, isUndefined, + reduce, values } from 'min-dash'; @@ -16,17 +18,36 @@ import { import { isAny } from 'bpmn-js/lib/util/ModelUtil'; +import { + valid as isSemverValid, + satisfies as isSemverCompatible, + coerce +} from 'semver'; + +// eslint-disable-next-line no-undef +const packageVersion = process.env.PKG_VERSION; + + /** * Registry for element templates. */ export default class ElementTemplates { - constructor(commandStack, eventBus, modeling, injector) { + constructor(commandStack, eventBus, modeling, injector, config) { this._commandStack = commandStack; this._eventBus = eventBus; this._injector = injector; this._modeling = modeling; - this._templates = {}; + this._templatesById = {}; + this._templates = []; + + config = config || {}; + + this._engines = this._coerceEngines(config.engines || {}); + + eventBus.on('elementTemplates.engines.changed', event => { + this.set(this._templates); + }); } /** @@ -38,7 +59,7 @@ export default class ElementTemplates { * @return {ElementTemplate} */ get(id, version) { - const templates = this._templates; + const templates = this._templatesById; let element; @@ -108,25 +129,115 @@ export default class ElementTemplates { * @param {Array} templates */ set(templates) { - this._templates = {}; + this._templatesById = {}; + this._templates = templates; templates.forEach((template) => { - const id = template.id, - version = isUndefined(template.version) ? '_' : template.version; + const id = template.id; + const version = isUndefined(template.version) ? '_' : template.version; - if (!this._templates[ id ]) { - this._templates[ id ] = { - latest: template - }; + if (!this._templatesById[ id ]) { + this._templatesById[ id ] = { }; } - this._templates[ id ][ version ] = template; + this._templatesById[ id ][ version ] = template; - const latestVerions = this._templates[ id ].latest.version; - if (isUndefined(latestVerions) || template.version > latestVerions) { - this._templates[ id ].latest = template; + const latest = this._templatesById[ id ].latest; + + if (this.isCompatible(template)) { + if (!latest || isUndefined(latest.version) || latest.version < version) { + this._templatesById[ id ].latest = template; + } } }); + + this._fire('changed'); + } + + getEngines() { + return this._engines; + } + + setEngines(engines) { + + this._engines = this._coerceEngines(engines); + + this._fire('engines.changed'); + } + + /** + * Ensures that only valid engines are kept around + * + * @param { Record } engines + * + * @return { Record } filtered, valid engines + */ + _coerceEngines(engines) { + + // we provide engine with the current + // package version; templates may use that engine to declare + // compatibility with this library + engines = { + elementTemplates: packageVersion, + ...engines + }; + + return reduce(engines, (validEngines, version, engine) => { + + const coercedVersion = coerce(version); + + if (!isSemverValid(coercedVersion)) { + console.error( + new Error(`Engine <${ engine }> specifies unparseable version <${version}>`) + ); + + return validEngines; + } + + return { + ...validEngines, + [ engine ]: coercedVersion.raw + }; + }, {}); + } + + /** + * Check if template is compatible with currently set engine version. + * + * @param {ElementTemplate} template + * + * @return {boolean} - true if compatible or no engine is set for elementTemplates or template. + */ + isCompatible(template) { + return !Object.keys(this.getIncompatibleEngines(template)).length; + } + + /** + * Get engines that are incompatible with the template. + * + * @param {any} template + * + * @return { Record { + + if (!has(localEngines, engine)) { + return result; + } + + if (!isSemverCompatible(localEngines[engine], templateEngines[engine])) { + result[engine] = { + actual: localEngines[engine], + required: templateEngines[engine] + }; + } + + return result; + }, {}); } /** @@ -138,20 +249,20 @@ export default class ElementTemplates { _getTemplateVerions(id, options = {}) { const { - latest: latestOnly, + latest: includeLatestOnly, deprecated: includeDeprecated } = options; - const templates = this._templates; + const templatesById = this._templatesById; const getVersions = (template) => { const { latest, ...versions } = template; - return latestOnly ? ( - !includeDeprecated && latest.deprecated ? [] : [ latest ] + return includeLatestOnly ? ( + !includeDeprecated && (latest && latest.deprecated) ? [] : (latest ? [ latest ] : []) ) : values(versions) ; }; if (isNil(id)) { - return flatten(values(templates).map(getVersions)); + return flatten(values(templatesById).map(getVersions)); } if (isObject(id)) { @@ -163,7 +274,7 @@ export default class ElementTemplates { } if (isString(id)) { - return templates[ id ] && getVersions(templates[ id ]); + return templatesById[ id ] && getVersions(templatesById[ id ]); } throw new Error('argument must be of type {string|djs.model.Base|undefined}'); @@ -208,11 +319,15 @@ export default class ElementTemplates { this._commandStack.execute('propertiesPanel.camunda.changeTemplate', context); - this._eventBus.fire(`elementTemplates.${action}`, payload); + this._fire(action, payload); return context.element; } + _fire(action, payload) { + return this._eventBus.fire(`elementTemplates.${action}`, payload); + } + /** * Remove template from a given element. * @@ -221,9 +336,7 @@ export default class ElementTemplates { * @return {djs.model.Base} the updated element */ removeTemplate(element) { - const eventBus = this._injector.get('eventBus'); - - eventBus.fire('elementTemplates.remove', { element }); + this._fire('remove', { element }); const context = { element @@ -252,7 +365,6 @@ ElementTemplates.$inject = [ 'commandStack', 'eventBus', 'modeling', - 'injector' -]; - - + 'injector', + 'config.elementTemplates' +]; \ No newline at end of file diff --git a/src/element-templates/ElementTemplatesLoader.js b/src/element-templates/ElementTemplatesLoader.js index f8c42ed1..f4c74c3d 100644 --- a/src/element-templates/ElementTemplatesLoader.js +++ b/src/element-templates/ElementTemplatesLoader.js @@ -1,6 +1,7 @@ import { isFunction, - isUndefined + isUndefined, + isArray, } from 'min-dash'; import { Validator } from './Validator'; @@ -14,18 +15,26 @@ import { Validator } from './Validator'; * descriptors or a node style callback to retrieve * the templates asynchronously. * - * @param {Array|Function} loadTemplates + * @param {Array|Function} config * @param {EventBus} eventBus * @param {ElementTemplates} elementTemplates * @param {Moddle} moddle */ export default class ElementTemplatesLoader { - constructor(loadTemplates, eventBus, elementTemplates, moddle) { - this._loadTemplates = loadTemplates; + constructor(config, eventBus, elementTemplates, moddle) { + this._loadTemplates; this._eventBus = eventBus; this._elementTemplates = elementTemplates; this._moddle = moddle; + if (isArray(config) || isFunction(config)) { + this._loadTemplates = config; + } + + if (config && config.loadTemplates) { + this._loadTemplates = config.loadTemplates; + } + eventBus.on('diagram.init', () => { this.reload(); }); @@ -45,7 +54,7 @@ export default class ElementTemplatesLoader { return loadTemplates((err, templates) => { if (err) { - return this.templateErrors([ err ]); + return this._templateErrors([ err ]); } this.setTemplates(templates); @@ -70,18 +79,12 @@ export default class ElementTemplatesLoader { elementTemplates.set(validTemplates); if (errors.length) { - this.templateErrors(errors); + this._templateErrors(errors); } - - this.templatesChanged(); - } - - templatesChanged() { - this._eventBus.fire('elementTemplates.changed'); } - templateErrors(errors) { - this._eventBus.fire('elementTemplates.errors', { + _templateErrors(errors) { + this._elementTemplates._fire('errors', { errors: errors }); } diff --git a/src/element-templates/Validator.js b/src/element-templates/Validator.js index 5f31e7b7..87db86f4 100644 --- a/src/element-templates/Validator.js +++ b/src/element-templates/Validator.js @@ -1,11 +1,16 @@ import { filter, + forEach, isArray, isString } from 'min-dash'; import semverCompare from 'semver-compare'; +import { + validRange as isSemverRangeValid +} from 'semver'; + import { validate as validateAgainstSchema, getSchemaVersion as getTemplateSchemaVersion @@ -14,6 +19,7 @@ import { const SUPPORTED_SCHEMA_VERSION = getTemplateSchemaVersion(); const MORPHABLE_TYPES = [ 'bpmn:Activity', 'bpmn:Event', 'bpmn:Gateway' ]; + /** * A element template validator. */ @@ -79,8 +85,6 @@ export class Validator { * @return {Error} validation error, if any */ _validateTemplate(template) { - let err; - const id = template.id, version = template.version || '_', schemaVersion = template.$schema && getSchemaVersion(template.$schema); @@ -110,21 +114,44 @@ export class Validator { } // (4) JSON schema compliance - const validationResult = validateAgainstSchema(template); + const schemaValidationResult = validateAgainstSchema(template); const { - errors, + errors: schemaErrors, valid - } = validationResult; + } = schemaValidationResult; if (!valid) { - err = new Error('invalid template'); - - filteredSchemaErrors(errors).forEach((error) => { + filteredSchemaErrors(schemaErrors).forEach((error) => { this._logError(error.message, template); }); + + return new Error('invalid template'); + } + + // (5) engines validation + const enginesError = this._validateEngines(template); + + if (enginesError) { + return enginesError; } + return null; + } + + _validateEngines(template) { + + let err; + + forEach(template.engines, (rangeStr, engine) => { + + if (!isSemverRangeValid(rangeStr)) { + err = this._logError(new Error( + `Engine <${engine}> specifies invalid semver range <${rangeStr}>` + ), template); + } + }); + return err; } diff --git a/test/TestHelper.js b/test/TestHelper.js index 1209f77b..584ceb90 100644 --- a/test/TestHelper.js +++ b/test/TestHelper.js @@ -20,9 +20,6 @@ import download from 'downloadjs'; import Modeler from 'bpmn-js/lib/Modeler'; -import BPMNModdle from 'bpmn-moddle'; -import zeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe'; - let PROPERTIES_PANEL_CONTAINER; global.chai.use(function(chai, utils) { @@ -231,56 +228,4 @@ document.addEventListener('keydown', function(event) { bpmnJS.saveXML({ format: true }).then(function(result) { download(result.xml, 'test.bpmn', 'application/xml'); }); -}); - -// Moddle helpers ////////////////////// -export async function createModdle(xml) { - const moddle = new BPMNModdle({ - zeebe: zeebeModdle - }); - - let root, warnings; - - try { - ({ - rootElement: root, - warnings = [] - } = await moddle.fromXML(xml, 'bpmn:Definitions', { lax: true })); - } catch (err) { - console.log(err); - } - - return { - root, - moddle, - context: { - warnings - }, - warnings - }; -} - -export function createDefinitions(xml = '') { - return ` - - ${ xml } - - `; -} - - -export function createProcess(bpmn = '', bpmndi = '') { - return createDefinitions(` - - ${ bpmn } - - ${ bpmndi } - `); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/test/spec/Example.spec.js b/test/spec/Example.spec.js index 736c7887..d1185a65 100644 --- a/test/spec/Example.spec.js +++ b/test/spec/Example.spec.js @@ -38,7 +38,7 @@ import CamundaBehaviorsModule from 'camunda-bpmn-js-behaviors/lib/camunda-platfo import ZeebeBehaviorsModule from 'camunda-bpmn-js-behaviors/lib/camunda-cloud'; import CamundaModdle from 'camunda-bpmn-moddle/resources/camunda'; - +import ModelerModdle from 'modeler-moddle/resources/modeler'; import ZeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe'; import CloudElementTemplatesPropertiesProviderModule from 'src/cloud-element-templates'; @@ -50,56 +50,105 @@ const singleStart = window.__env__ && window.__env__.SINGLE_START; insertCoreStyles(); insertBpmnStyles(); -insertCSS('bottom-panel.css', ` + +insertCSS('example.css', ` .test-container { display: flex; flex-direction: column; } + .test-container { + --example-properties-width: 20vw; + --example-bottom-height: 30vh; + } + + .bjs-container { + padding-right: var(--example-properties-width); + padding-bottom: var(--example-bottom-height); + } + .properties-panel-container { position: absolute; top: 0; right: 0; - width: 250px; + width: var(--example-properties-width); height: 100%; border-left: solid 1px #ccc; background-color: #f7f7f8; } - .panel { + .bottom-panel { position: absolute; bottom: 0; left: 0; - width: calc(100% - 250px - 1px); - height: 200px; + width: calc(100% - var(--example-properties-width) - 1px); + height: var(--example-bottom-height); display: flex; flex-direction: column; background-color: #f7f7f8; - padding: 5px; box-sizing: border-box; border-top: solid 1px #ccc; font-family: sans-serif; } - .panel .errors { + .bottom-panel .error-container { resize: none; flex-grow: 1; background-color: #f7f7f8; border: none; - margin-bottom: 5px; + padding: 5px; font-family: sans-serif; line-height: 1.5; outline: none; - overflow-y: scroll; + overflow: auto; } - .panel button, - .panel input { + .bottom-panel .error-item { + cursor: pointer; + } + + .bottom-panel .footer-container { + border-top: solid 1px #ccc; + padding: 5px; + } + + .bottom-panel button, + .bottom-panel input { width: 200px; } `); +const ChangeEnginesModule = { + + __init__: [ function(bpmnjs, eventBus, elementTemplates) { + + eventBus.on([ + 'import.done', + 'elements.changed' + ], function() { + const executionPlatformVersion = bpmnjs.getDefinitions().get('executionPlatformVersion'); + + if (elementTemplates.getEngines().camunda !== executionPlatformVersion) { + elementTemplates.setEngines({ + camunda: executionPlatformVersion + }); + } + }); + } ] +}; + +const LogTemplateErrorsModule = { + + __init__: [ function(eventBus) { + + eventBus.on('elementTemplates.errors', function({ errors }) { + console.error('element template parse errors', errors); + }); + } ] +}; + + describe('', function() { let modelerContainer; @@ -133,7 +182,8 @@ describe('', function() { LintingModule ], moddleExtensions = { - zeebe: ZeebeModdle + zeebe: ZeebeModdle, + modeler: ModelerModdle }, propertiesPanel = {}, description = {}, @@ -197,10 +247,13 @@ describe('', function() { ElementTemplatesIconsRenderer, CreateAppendAnythingModule, CreateAppendElementTemplatesModule, + ChangeEnginesModule, + LogTemplateErrorsModule, LintingModule ], moddleExtensions: { - zeebe: ZeebeModdle + zeebe: ZeebeModdle, + modeler: ModelerModdle }, propertiesPanel: { parent: null @@ -212,71 +265,11 @@ describe('', function() { } ); - const modeler = result.modeler; - - const linter = new Linter({ - plugins: [ - ElementTemplateLinterPlugin(elementTemplates) - ] - - }); - - const linting = modeler.get('linting'); - const bpmnjs = modeler.get('bpmnjs'); - const eventBus = modeler.get('eventBus'); - - const lint = () => { - const definitions = bpmnjs.getDefinitions(); - - linter.lint(definitions).then(reports => { - linting.setErrors(reports); - - const errorContainer = panel.querySelector('.errors'); - errorContainer.innerHTML = ''; - - reports.forEach((report) => { - let { id, message, node, data } = report; - node = node || (data && data.node); - const name = node && node.name; - - const errorMessage = `${ name || id }: ${ message }`; - const item = domify(`
${escapeHtml(errorMessage)}
`); - item.addEventListener('click', () => { - linting.showError(report); - }); - - errorContainer.appendChild(item); - }); - }); - }; - - lint(); - - eventBus.on('elements.changed', lint); - linting.activate(); - - const propertiesPanelParent = domify('
'); - - bpmnjs._container.appendChild(propertiesPanelParent); - - modeler.get('propertiesPanel').attachTo(propertiesPanelParent); - - const panel = domify(` -
-
-
- - - -
-
- `); - - bpmnjs._container.appendChild(panel); - - // then expect(result.error).not.to.exist; + + // and + createTestUI(result.modeler); }); @@ -301,28 +294,117 @@ describe('', function() { ElementTemplatesPropertiesProviderModule ], moddleExtensions: { - camunda: CamundaModdle + camunda: CamundaModdle, + modeler: ModelerModdle }, elementTemplates } ); - const modeler = result.modeler; - const bpmnjs = modeler.get('bpmnjs'); - const propertiesPanelParent = domify('
'); - - bpmnjs._container.appendChild(propertiesPanelParent); - - modeler.get('propertiesPanel').attachTo(propertiesPanelParent); - - // then expect(result.error).not.to.exist; + + // and then + createTestUI(result.modeler); }); }); -const escapeHtml = (unsafe) => { +function escapeHTML(unsafe) { return unsafe.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); -}; +} + +function createTestUI(modeler) { + + const linting = modeler.get('linting', false); + const elementTemplates = modeler.get('elementTemplates', false); + const propertiesPanel = modeler.get('propertiesPanel', false); + + const canvas = modeler.get('canvas'); + const modeling = modeler.get('modeling'); + const bpmnjs = modeler.get('bpmnjs'); + const eventBus = modeler.get('eventBus'); + + const container = bpmnjs._container; + + if (propertiesPanel) { + + const propertiesPanelParent = domify('
'); + + container.appendChild(propertiesPanelParent); + + propertiesPanel.attachTo(propertiesPanelParent); + } + + if (linting && elementTemplates) { + const linter = new Linter({ + plugins: [ + ElementTemplateLinterPlugin(elementTemplates.getAll()) + ] + }); + + const bottomPanel = domify(` +
+
+ +
+ `); + + container.appendChild(bottomPanel); + + bottomPanel.querySelector('input').value = bpmnjs.getDefinitions().get('executionPlatformVersion'); + + bottomPanel.querySelector('input').addEventListener('input', ({ target }) => { + modeling.updateModdleProperties( + canvas.getRootElement(), + bpmnjs.getDefinitions(), + { executionPlatformVersion: target.value } + ); + }); + + const lint = () => { + const definitions = bpmnjs.getDefinitions(); + + linter.lint(definitions).then(reports => { + linting.setErrors(reports); + + const errorContainer = bottomPanel.querySelector('.error-container'); + errorContainer.innerHTML = ''; + + reports.map((report) => { + const { id, message, category, rule, documentation } = report; + + if (category === 'rule-error') { + return domify(`
${ category } Rule <${ escapeHTML(rule) }> errored with the following message: ${ escapeHTML(message) }
`); + } + + const element = domify(`
${ category } ${ id }: ${escapeHTML(message) }
`); + + if (documentation.url) { + const documentationLink = domify(`ref`); + + documentationLink.addEventListener('click', e => e.stopPropagation()); + + element.appendChild(documentationLink); + } + + element.addEventListener('click', () => { + linting.showError(report); + }); + + return element; + }).forEach(item => errorContainer.appendChild(item)); + }); + }; + + linting.activate(); + + lint(); + + eventBus.on('elements.changed', lint); + } +} \ No newline at end of file diff --git a/test/spec/cloud-element-templates/ElementTemplates.engines-templates.json b/test/spec/cloud-element-templates/ElementTemplates.engines-templates.json new file mode 100644 index 00000000..da5ffbb9 --- /dev/null +++ b/test/spec/cloud-element-templates/ElementTemplates.engines-templates.json @@ -0,0 +1,82 @@ +[ + { + "id": "example.engines.test.multiple", + "name": " Test - Multiple", + "description": "does not match if { desktopModeler: >=1 } is provided", + "version": 2, + "engines": { + "camunda": "^8.6", + "webModeler": "^4.1", + "desktopModeler": "^0" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "id": "example.engines.test.multiple", + "name": " Test - Multiple", + "description": "matches if { camunda: ^8.6, webModeler: ^4.1 } engine is indicated, or properties are not provided", + "version": 1, + "engines": { + "camunda": "^8.6", + "webModeler": "^4.1" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + + { + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "matches if { camunda: ^8.6 }, or if no engine is provided", + "version": 3, + "engines": { + "camunda": "^8.6" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "matches if { camunda: ^8.5 }, or if no engine is provided", + "version": 2, + "engines": { + "camunda": "^8.5" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "always matches", + "version": 1, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + + { + "id": "example.engines.test.broken", + "name": " Test - broken Semver range", + "description": "specifies broken semver range", + "version": 1, + "engines": { + "camunda": "invalid-version" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + } +] \ No newline at end of file diff --git a/test/spec/cloud-element-templates/ElementTemplates.spec.js b/test/spec/cloud-element-templates/ElementTemplates.spec.js index b3fc70e4..69bcb0e6 100644 --- a/test/spec/cloud-element-templates/ElementTemplates.spec.js +++ b/test/spec/cloud-element-templates/ElementTemplates.spec.js @@ -24,14 +24,19 @@ import zeebeModdlePackage from 'zeebe-bpmn-moddle/resources/zeebe'; import diagramXML from './ElementTemplates.bpmn'; import integrationXML from './fixtures/integration.bpmn'; import messageTemplates from './ElementTemplates.message-templates.json'; +import enginesTemplates from './ElementTemplates.engines-templates.json'; import templates from './fixtures/simple'; +import falsyVersionTemplate from './fixtures/falsy-version'; import complexTemplates from './fixtures/complex'; import integrationTemplates from './fixtures/integration'; import { findExtensions, findExtension } from 'src/cloud-element-templates/Helper'; import { getLabel } from 'bpmn-js/lib/features/label-editing/LabelUtil'; import { findMessage } from 'src/cloud-element-templates/Helper'; +// eslint-disable-next-line no-undef +const packageVersion = process.env.PKG_VERSION; + describe('provider/cloud-element-templates - ElementTemplates', function() { @@ -53,13 +58,14 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { } })); - beforeEach(inject(function(elementTemplates) { - elementTemplates.set(templates); - })); - describe('get', function() { + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(templates); + })); + + it('should get template by ID', inject(function(elementTemplates) { // when @@ -147,6 +153,11 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { describe('getAll', function() { + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(templates); + })); + + it('should get all templates', inject(function(elementTemplates) { // when @@ -220,6 +231,11 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { describe('getLatest', function() { + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(templates); + })); + + it('should get all latest templates', inject(function(elementTemplates) { // when @@ -332,6 +348,206 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { })); + describe(' compatibility', function() { + + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(enginesTemplates); + })); + + + describe('should retrieve latest compatible', function() { + + it('single template', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.6.3' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.basic'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(3); + })); + + + it('all templates', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.6.3' + }); + + // when + const templates = elementTemplates.getLatest(); + + // then + // expect all compatible templates to be returned + // example.engines.test.multiple v2 + // example.engines.test.basic v2 + expect(templates).to.have.length(2); + })); + + }); + + + it('should retrieve older compatible', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.5' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.basic'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(2); + })); + + + it('should retrieve fallback (no meta-data)', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '4' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.basic'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(1); + })); + + + describe('should handle no context provided', function() { + + it('single template', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({}); + + // when + const templates = elementTemplates.getLatest('example.engines.test.basic'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(3); + })); + + + it('list templates', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({}); + + // when + const templates = elementTemplates.getLatest(); + + // then + // example.engines.test.multiple v2 + // example.engines.test.basic v3 + // example.engines.test.broken v1 + expect(templates).to.have.length(3); + })); + + }); + + + it('should support multiple engines', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.6', + webModeler: '4.3' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.multiple'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(2); + })); + + + it('should exclude engine', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.6', + webModeler: '4.3', + desktopModeler: '5.4' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.multiple'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(1); + })); + + + it('should ignore incompatible', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.4' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.multiple'); + + // then + expect(templates).to.be.empty; + })); + + + it('should handle broken provided at run-time', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: 'one-hundred' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.basic'); + + // then + // we ignore the context entry, assume it is not there + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(3); + })); + + + it('should handle broken provided by template', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.6' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.broken'); + + // then + expect(templates).to.be.empty; + + // and + // we still regard such template as a valid template + const template = elementTemplates.get('example.engines.test.broken', 1); + expect(template).to.exist; + })); + + }); + + it('should throw for invalid argument', inject(function(elementTemplates) { // then @@ -346,6 +562,11 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { describe('createElement', function() { + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(templates); + })); + + it('should create element', inject(function(elementTemplates) { // given @@ -526,8 +747,114 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { }); + describe('set', function() { + + it('should set templates', inject(function(elementTemplates) { + + // when + elementTemplates.set(templates.slice(0, 3)); + + // then + expect(elementTemplates.getAll()).to.have.length(3); + })); + + + it('should not ignore version set to 0', inject(function(elementTemplates) { + + // when + elementTemplates.set(falsyVersionTemplate); + + // then + expect(elementTemplates.get(falsyVersionTemplate[0].id, 0)).to.exist; + })); + + + it('should emit event', inject(function(elementTemplates, eventBus) { + + // given + const spy = sinon.spy(); + + eventBus.on('elementTemplates.changed', spy); + + // when + elementTemplates.set(templates); + + // then + expect(spy).to.have.been.calledOnce; + })); + + }); + + + describe('getEngines', function() { + + it('should provide default engine', inject(function(elementTemplates) { + + // then + expect( + elementTemplates.getEngines() + ).to.have.property('elementTemplates', packageVersion); + })); + + + it('should provide set engines', inject(function(elementTemplates) { + + // when + elementTemplates.setEngines({ + 'camunda': '8', + 'other': '100.5' + }); + + // then + expect(elementTemplates.getEngines()).to.eql({ + 'elementTemplates': packageVersion, + 'camunda': '8.0.0', + 'other': '100.5.0' + }); + + })); + + }); + + + describe('setEngines', function() { + + it('should emit event', inject(function(elementTemplates, eventBus) { + + // given + const spy = sinon.spy(); + + eventBus.on('elementTemplates.engines.changed', spy); + + // when + elementTemplates.setEngines({}); + + // then + expect(spy).to.have.been.calledOnce; + })); + + + it('should override engine', inject(function(elementTemplates) { + + // when + elementTemplates.setEngines({ + elementTemplates: '1.0.0' + }); + + // then + expect(elementTemplates.getEngines()).to.have.property('elementTemplates', '1.0.0'); + })); + + }); + + describe('applyTemplate', function() { + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(templates); + })); + + it('should set template on element', inject(function(elementRegistry, elementTemplates) { // given @@ -807,6 +1134,11 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { describe('unlinkTemplate', function() { + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(templates); + })); + + it('should unlink task template', inject(function(elementRegistry, elementTemplates) { // given @@ -840,6 +1172,7 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { expect(getBusinessObject(task).get('zeebe:modelerTemplateIcon')).to.not.exist; })); + it('should fire elementTemplates.unlink event', inject(function(elementRegistry, elementTemplates, eventBus) { // given @@ -862,6 +1195,11 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { describe('removeTemplate', function() { + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(templates); + })); + + it('should remove task template', inject(function(elementRegistry, elementTemplates) { // given @@ -1008,29 +1346,11 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { describe('updateTemplate', function() { - let container; - - beforeEach(function() { - container = TestContainer.get(this); - }); - - beforeEach(bootstrapModeler(diagramXML, { - container: container, - modules: [ - coreModule, - elementTemplatesCoreModule, - modelingModule, - { - propertiesPanel: [ 'value', { registerProvider() {} } ] - } - ], - moddleExtensions: { - zeebe: zeebeModdlePackage - }, - elementTemplates: [ + beforeEach(inject(function(elementTemplates) { + elementTemplates.set([ ...templates, ...messageTemplates - ] + ]); })); @@ -1120,11 +1440,205 @@ describe('provider/cloud-element-templates - ElementTemplates', function() { newTemplate }); })); + }); + + describe('isCompatible', function() { + + const compatibleTemplate = { + engines: { + camunda: '^8.5' + } + }; + + const incompatibleTemplate = { + engines: { + camunda: '^8.6' + } + }; + + + it('should accept compatible', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.5' + }); + + // then + expect(elementTemplates.isCompatible(compatibleTemplate)).to.be.true; + })); + + + it('should reject incompatible', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.5' + }); + + // then + expect(elementTemplates.isCompatible(incompatibleTemplate)).to.be.false; + })); + + + it('should accept non matching engine', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + nonMatchingEngine: '8.5' + }); + + // then + expect(elementTemplates.isCompatible(compatibleTemplate)).to.be.true; + expect(elementTemplates.isCompatible(incompatibleTemplate)).to.be.true; + })); + + }); + + + describe('error handling', function() { + + // given + const invalidEngines = { + camunda: '8.5', + invalid: 'not-a-semver' + }; + + it('should filter invalid on set', inject(function(elementTemplates) { + + // when + elementTemplates.setEngines(invalidEngines); + + // then + expect(elementTemplates.getEngines()).to.have.property('camunda'); + expect(elementTemplates.getEngines()).to.not.have.property('invalid'); + })); + }); + + + describe('getIncompatibleEngines', function() { + + it('should return incompatible engine', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.5' + }); + + const template = { + engines: { + camunda: '>= 8.6' + } + }; + + // then + const result = elementTemplates.getIncompatibleEngines(template); + + expect(result).to.eql({ + camunda: { + actual: '8.5.0', + required: '>= 8.6' + } + }); + })); + + + it('should return multiple', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.5', + desktopModeler: '5.30' + }); + + const template = { + engines: { + camunda: '>= 8.6', + desktopModeler: '^4.0' + } + }; + + // then + const result = elementTemplates.getIncompatibleEngines(template); + + expect(result).to.eql({ + camunda: { + actual: '8.5.0', + required: '>= 8.6' + }, + desktopModeler: { + actual: '5.30.0', + required: '^4.0' + } + }); + })); + + + it('should return empty object if compatible', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + nonMatchingEngine: '8.5' + }); + + const template = { + engines: { + camunda: '8.5', + } + }; + + // then + const result = elementTemplates.getIncompatibleEngines(template); + + expect(result).to.eql({}); + })); + + }); + +}); + + +describe('provider/cloud-element-templates - ElementTemplates - error handling on instantiation', function() { + + let container; + + beforeEach(function() { + container = TestContainer.get(this); + }); + + // given + const invalidEngines = { + camunda: '8.5', + invalid: 'not-a-semver' + }; + + beforeEach(bootstrapModeler(diagramXML, { + container: container, + modules: [ + coreModule, + modelingModule, + elementTemplatesCoreModule, + ], + elementTemplates: { + engines: invalidEngines + } + })); + + + it('should filter invalid on instantiation', inject(function(elementTemplates) { + + // then + expect(elementTemplates.getEngines()).to.have.property('camunda', '8.5.0'); + expect(elementTemplates.getEngines()).to.not.have.property('invalid'); + })); + }); + + describe('provider/cloud-element-templates - ElementTemplates - integration', function() { let container; diff --git a/test/spec/cloud-element-templates/Validator.spec.js b/test/spec/cloud-element-templates/Validator.spec.js index c63f4be6..1b2a8e8e 100644 --- a/test/spec/cloud-element-templates/Validator.spec.js +++ b/test/spec/cloud-element-templates/Validator.spec.js @@ -24,6 +24,7 @@ describe('provider/cloud-element-templates - Validator', function() { moddle = new BPMNModdle(); }); + describe('schema version', function() { it('should accept when template and library have the same version', function() { @@ -584,4 +585,42 @@ describe('provider/cloud-element-templates - Validator', function() { }); + + describe('engines validation', function() { + + it('should accept template with valid semver range', function() { + + // given + const templates = new Validator(moddle); + + const templateDescriptor = require('./fixtures/engines'); + + // when + templates.addAll(templateDescriptor); + + // then + expect(errors(templates)).to.be.empty; + + expect(valid(templates)).to.have.length(templateDescriptor.length); + }); + + + it('should reject template with invalid semver range', function() { + + // given + const templates = new Validator(moddle); + + const templateDescriptor = require('./fixtures/engines-invalid'); + + // when + templates.addAll(templateDescriptor); + + // then + expect(errors(templates)).to.contain('Engine specifies invalid semver range '); + + expect(valid(templates)).to.be.empty; + }); + + }); + }); diff --git a/test/spec/cloud-element-templates/fixtures/complex.bpmn b/test/spec/cloud-element-templates/fixtures/complex.bpmn index a755b46f..2cb154ba 100644 --- a/test/spec/cloud-element-templates/fixtures/complex.bpmn +++ b/test/spec/cloud-element-templates/fixtures/complex.bpmn @@ -1,5 +1,5 @@ - + Flow_0gw5hlt @@ -9,7 +9,7 @@ Flow_0636r17 - + @@ -94,7 +94,9 @@ - + + + @@ -139,6 +141,14 @@ + + + + + + + + diff --git a/test/spec/cloud-element-templates/fixtures/engines-invalid.json b/test/spec/cloud-element-templates/fixtures/engines-invalid.json new file mode 100644 index 00000000..540001e3 --- /dev/null +++ b/test/spec/cloud-element-templates/fixtures/engines-invalid.json @@ -0,0 +1,16 @@ +[ + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "example.engines.test.broken", + "name": " Test - broken Semver range", + "description": "specifies broken semver range", + "version": 1, + "engines": { + "camunda": "invalid-version" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + } +] \ No newline at end of file diff --git a/test/spec/cloud-element-templates/fixtures/engines.json b/test/spec/cloud-element-templates/fixtures/engines.json new file mode 100644 index 00000000..c525f144 --- /dev/null +++ b/test/spec/cloud-element-templates/fixtures/engines.json @@ -0,0 +1,97 @@ +[ + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "example.engines.test.multiple", + "name": " Test - Multiple", + "description": "does not match if { desktopModeler: >=1 } is provided", + "version": 2, + "engines": { + "camunda": "^8.6", + "webModeler": "^4.1", + "desktopModeler": "^0" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "example.engines.test.multiple", + "name": " Test - Multiple", + "description": "matches if { camunda: ^8.6, webModeler: ^4.1 } engine is indicated, or properties are not provided", + "version": 1, + "engines": { + "camunda": "^8.6", + "webModeler": "^4.1" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "matches if { camunda: ^8.6 }, or if no engine is provided", + "version": 3, + "engines": { + "camunda": "^8.6" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "matches if { camunda: ^8.5 }, or if no engine is provided", + "version": 2, + "engines": { + "camunda": "^8.5" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "always matches", + "version": 1, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "example.engines.test.incompatible", + "name": " Test - incompatible", + "description": "incompatible with all camunda versions", + "engines": { + "camunda": "0" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [ + { + "label": "Task Header 2", + "type": "String", + "value": "header-2-value", + "binding": { + "type": "zeebe:taskHeader", + "key": "header-2-key" + } + } + ] + } +] \ No newline at end of file diff --git a/test/spec/cloud-element-templates/fixtures/falsy-version.json b/test/spec/cloud-element-templates/fixtures/falsy-version.json new file mode 100644 index 00000000..86d6fd75 --- /dev/null +++ b/test/spec/cloud-element-templates/fixtures/falsy-version.json @@ -0,0 +1,10 @@ +[ + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "foo", + "name":"Foo 1", + "version": 0, + "appliesTo": [ "bpmn:Task" ], + "properties": [] + } +] \ No newline at end of file diff --git a/test/spec/cloud-element-templates/linting/LinterPlugin.json b/test/spec/cloud-element-templates/linting/LinterPlugin.json index 151a1136..a75cf405 100644 --- a/test/spec/cloud-element-templates/linting/LinterPlugin.json +++ b/test/spec/cloud-element-templates/linting/LinterPlugin.json @@ -216,5 +216,55 @@ "feel": "optional" } ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "compatible", + "id": "compatible", + "appliesTo": [ + "bpmn:Task" + ], + "engines": { + "camunda": ">1.0" + }, + "properties": [] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "incompatible", + "id": "incompatible", + "appliesTo": [ + "bpmn:Task" + ], + "engines": { + "camunda": "0" + }, + "properties": [] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "incompatible-updatable", + "id": "incompatible-updatable", + "version": 1, + "appliesTo": [ + "bpmn:Task" + ], + "engines": { + "camunda": "0" + }, + "properties": [] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "incompatible-updatable", + "id": "incompatible-updatable", + "version": 2, + "appliesTo": [ + "bpmn:Task" + ], + "engines": { + "camunda": "^8.5" + }, + "properties": [] } ] \ No newline at end of file diff --git a/test/spec/cloud-element-templates/linting/LinterPlugin.spec.js b/test/spec/cloud-element-templates/linting/LinterPlugin.spec.js index f56e0fbc..49e0f2d0 100644 --- a/test/spec/cloud-element-templates/linting/LinterPlugin.spec.js +++ b/test/spec/cloud-element-templates/linting/LinterPlugin.spec.js @@ -1,61 +1,59 @@ import RuleTester from 'bpmnlint/lib/testers/rule-tester'; -import { elementTemplateLintRule } from 'src/cloud-element-templates/linting'; - -import { - createDefinitions, - createModdle, - createProcess -} from '../../../TestHelper'; +import validateRule from 'src/cloud-element-templates/linting/rules/element-templates-validate'; +import compatibilityRule from 'src/cloud-element-templates/linting/rules/element-templates-compatibility'; import templates from './LinterPlugin.json'; +import BPMNModdle from 'bpmn-moddle'; +import zeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe'; + const valid = [ { name: 'Valid Template', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates } }, { name: 'Conditional Template - property hidden', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates } }, { name: 'No Template', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates } }, { name: 'All Messages', - moddleElement: createModdle(createDefinitions('')), + moddleElement: createDefinitions(''), config: { templates } }, { name: 'FEEL (Min Length)', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates } }, { name: 'FEEL (Max Length)', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates } }, { name: 'FEEL (Pattern)', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates } @@ -66,7 +64,7 @@ const valid = [ const invalid = [ { name: 'Template Not Found', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates }, @@ -77,7 +75,7 @@ const invalid = [ }, { name: 'Min Length', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates }, @@ -90,7 +88,7 @@ const invalid = [ }, { name: 'Max Length', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates }, @@ -103,7 +101,7 @@ const invalid = [ }, { name: 'Not Empty', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates }, @@ -115,7 +113,7 @@ const invalid = [ }, { name: 'Pattern', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates }, @@ -127,7 +125,7 @@ const invalid = [ }, { name: 'Pattern (custom message)', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates }, @@ -139,7 +137,7 @@ const invalid = [ }, { name: 'Conditional Template - property shown', - moddleElement: createModdle(createProcess('')), + moddleElement: createProcess(''), config: { templates }, @@ -152,8 +150,68 @@ const invalid = [ } ]; +const compatible = [ + { + name: 'Template compatible', + moddleElement: createProcess(''), + config: { + templates + } + }, + + { + name: 'Template compatible (no execution platform)', + moddleElement: createProcess( + '', + '', + { executionPlatform: '' } + ), + config: { + templates + } + }, + + { + name: 'Template compatible (no execution platform version)', + moddleElement: createProcess( + '', + '', + { executionPlatform: '8.5' }), + config: { + templates + } + }, +]; + +const incompatible = [ + { + name: 'Template incompatible', + moddleElement: createProcess(''), + config: { + templates + }, + report: { + id: 'Task_1', + message: 'Element template incompatible with current environment. Requires \'camunda 0\'; found \'8.5.0\'.' + } + }, + { + name: 'Template incompatible with update', + moddleElement: createProcess( + '' + ), + config: { + templates + }, + report: { + id: 'Task_1', + message: 'Element template incompatible with current environment. Requires \'camunda 0\'; found \'8.5.0\'. Update available.' + } + }, +]; + -describe('element-templates Linting', function() { +describe('cloud-element-templates/linting', function() { before(function() { @@ -163,9 +221,68 @@ describe('element-templates Linting', function() { } }); - RuleTester.verify('element-templates', elementTemplateLintRule, { + RuleTester.verify('element-templates/validate', validateRule, { valid, - invalid + invalid, }); -}); \ No newline at end of file + RuleTester.verify('element-templates/compatibility', compatibilityRule, { + valid: compatible, + invalid: incompatible + }); + +}); + + +// helpers /////////// + +async function createModdle(xml) { + const moddle = new BPMNModdle({ + zeebe: zeebeModdle + }); + + const { + rootElement: root, + warnings = [] + } = await moddle.fromXML(xml, 'bpmn:Definitions', { lax: true }); + + return { + root, + moddle, + context: { + warnings + }, + warnings + }; +} + +function createDefinitions(xml = '', { executionPlatform, executionPlatformVersion } = { + executionPlatform: 'Camunda Cloud', + executionPlatformVersion: '8.5' +}) { + + return createModdle(` + + ${ xml } + + `); +} + + +function createProcess(bpmn = '', bpmndi = '', options) { + return createDefinitions(` + + ${ bpmn } + + ${ bpmndi } + `, options); +} \ No newline at end of file diff --git a/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.bpmn b/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.bpmn index 315a3870..f02efd8e 100644 --- a/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.bpmn +++ b/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.bpmn @@ -4,6 +4,7 @@ + @@ -46,6 +47,9 @@ + + + diff --git a/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.json b/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.json index 02e613f3..2a65747d 100644 --- a/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.json +++ b/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.json @@ -94,5 +94,38 @@ } } ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "engines", + "name":"Engines 1", + "version": 1, + "appliesTo": [ "bpmn:Task" ], + "engines": { + "camunda": "^8.4" + }, + "properties": [] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "engines", + "name":"Engines 2", + "version": 2, + "appliesTo": [ "bpmn:Task" ], + "engines": { + "camunda": "^8.5" + }, + "properties": [] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "id": "engines", + "name":"Engines 3", + "version": 3, + "appliesTo": [ "bpmn:Task" ], + "engines": { + "camunda": "^8.6" + }, + "properties": [] } ] diff --git a/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.spec.js b/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.spec.js index 9195fa0c..edfbe1cb 100644 --- a/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.spec.js +++ b/test/spec/cloud-element-templates/properties-panel/ElementTemplatesPropertiesProvider.spec.js @@ -495,6 +495,75 @@ describe('provider/cloud-element-templates - ElementTemplatesPropertiesProvider' ); }); + describe('engines', function() { + + it('should display update button if latest is compatible', inject( + async function(elementRegistry, selection, elementTemplates) { + + // given + const element = elementRegistry.get('Task_4'); + + // when + elementTemplates.setEngines({ + camunda: '8.6' + }); + + await act(() => { + selection.select(element); + }); + + // then + const updateAvailable = domQuery('.bio-properties-panel-template-update-available', container); + expect(updateAvailable).to.exist; + }) + ); + + + it('should NOT display update button if latest is incompatible', inject( + async function(elementRegistry, selection, elementTemplates) { + + // given + const element = elementRegistry.get('Task_4'); + + // when + elementTemplates.setEngines({ + camunda: '8.5' + }); + + await act(() => { + selection.select(element); + }); + + // then + const updateAvailable = domQuery('.bio-properties-panel-template-update-available', container); + expect(updateAvailable).not.to.exist; + }) + ); + + + it('should display incompatible button when template is incompatible', inject( + async function(elementRegistry, selection, elementTemplates) { + + // given + const element = elementRegistry.get('Task_4'); + + // when + elementTemplates.setEngines({ + camunda: '8.0' + }); + + await act(() => { + selection.select(element); + }); + + // then + const incompatible = domQuery('.bio-properties-panel-template-incompatible', container); + expect(incompatible).to.exist; + }) + ); + + }); + describe('conditional entries', function() { diff --git a/test/spec/element-templates/ElementTemplates.engines-templates.json b/test/spec/element-templates/ElementTemplates.engines-templates.json new file mode 100644 index 00000000..1f4d7fef --- /dev/null +++ b/test/spec/element-templates/ElementTemplates.engines-templates.json @@ -0,0 +1,82 @@ +[ + { + "id": "example.engines.test.multiple", + "name": " Test - Multiple", + "description": "does not match if { desktopModeler: >=1 } is provided", + "version": 2, + "engines": { + "camunda": "^7.14", + "webModeler": "^4.1", + "desktopModeler": "^0" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "id": "example.engines.test.multiple", + "name": " Test - Multiple", + "description": "matches if { camunda: ^7.14, webModeler: ^4.1 } engine is indicated, or properties are not provided", + "version": 1, + "engines": { + "camunda": "^7.14", + "webModeler": "^4.1" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + + { + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "matches if { camunda: ^7.14 }, or if no engine is provided", + "version": 3, + "engines": { + "camunda": "^7.14" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "matches if { camunda: ^7.13 }, or if no engine is provided", + "version": 2, + "engines": { + "camunda": "^7.13" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "always matches", + "version": 1, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + + { + "id": "example.engines.test.broken", + "name": " Test - broken Semver range", + "description": "specifies broken semver range", + "version": 1, + "engines": { + "camunda": "invalid-version" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + } +] \ No newline at end of file diff --git a/test/spec/element-templates/ElementTemplates.spec.js b/test/spec/element-templates/ElementTemplates.spec.js index cd105620..b57ccc2d 100644 --- a/test/spec/element-templates/ElementTemplates.spec.js +++ b/test/spec/element-templates/ElementTemplates.spec.js @@ -20,6 +20,7 @@ import diagramXML from './ElementTemplates.bpmn'; import templates from './fixtures/simple'; import falsyVersionTemplate from './fixtures/falsy-version'; +import enginesTemplates from './ElementTemplates.engines-templates.json'; describe('provider/element-templates - ElementTemplates', function() { @@ -219,6 +220,206 @@ describe('provider/element-templates - ElementTemplates', function() { })); + describe(' compatibility', function() { + + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(enginesTemplates); + })); + + + describe('should retrieve latest compatible', function() { + + it('single template', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '7.14.3' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.basic'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(3); + })); + + + it('all templates', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '7.14.3' + }); + + // when + const templates = elementTemplates.getLatest(); + + // then + // expect all compatible templates to be returned + // example.engines.test.multiple v2 + // example.engines.test.basic v2 + expect(templates).to.have.length(2); + })); + + }); + + + it('should retrieve older compatible', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '7.13' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.basic'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(2); + })); + + + it('should retrieve fallback (no meta-data)', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '4' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.basic'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(1); + })); + + + describe('should handle no context provided', function() { + + it('single template', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({}); + + // when + const templates = elementTemplates.getLatest('example.engines.test.basic'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(3); + })); + + + it('list templates', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({}); + + // when + const templates = elementTemplates.getLatest(); + + // then + // example.engines.test.multiple v2 + // example.engines.test.basic v3 + // example.engines.test.broken v1 + expect(templates).to.have.length(3); + })); + + }); + + + it('should support multiple engines', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '7.14', + webModeler: '4.3' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.multiple'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(2); + })); + + + it('should exclude engine', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '7.14', + webModeler: '4.3', + desktopModeler: '5.4' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.multiple'); + + // then + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(1); + })); + + + it('should ignore incompatible', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '7.12' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.multiple'); + + // then + expect(templates).to.be.empty; + })); + + + it('should handle broken provided at run-time', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: 'one-hundred' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.basic'); + + // then + // we ignore the context entry, assume it is not there + expect(templates).to.have.length(1); + expect(templates[0].version).to.eql(3); + })); + + + it('should handle broken provided by template', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '7.14' + }); + + // when + const templates = elementTemplates.getLatest('example.engines.test.broken'); + + // then + expect(templates).to.be.empty; + + // and + // we still regard such template as a valid template + const template = elementTemplates.get('example.engines.test.broken', 1); + expect(template).to.exist; + })); + + }); + + it('should throw for invalid argument', inject(function(elementTemplates) { // then @@ -378,6 +579,40 @@ describe('provider/element-templates - ElementTemplates', function() { expect(elementTemplates.get(falsyVersionTemplate[0].id, 0)).to.exist; })); + + it('should emit event', inject(function(elementTemplates, eventBus) { + + // given + const spy = sinon.spy(); + + eventBus.on('elementTemplates.changed', spy); + + // when + elementTemplates.set(templates); + + // then + expect(spy).to.have.been.calledOnce; + })); + + }); + + + describe('setEngines', function() { + + it('should emit event', inject(function(elementTemplates, eventBus) { + + // given + const spy = sinon.spy(); + + eventBus.on('elementTemplates.engines.changed', spy); + + // when + elementTemplates.setEngines({}); + + // then + expect(spy).to.have.been.calledOnce; + })); + }); @@ -578,6 +813,80 @@ describe('provider/element-templates - ElementTemplates', function() { }); + + describe('isCompatible', function() { + + const compatibleTemplate = { + engines: { + camunda: '^8.5' + } + }; + + const incompatibleTemplate = { + engines: { + camunda: '^8.6' + } + }; + + + it('should accept compatible', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.5' + }); + + // then + expect(elementTemplates.isCompatible(compatibleTemplate)).to.be.true; + })); + + + it('should reject incompatible', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + camunda: '8.5' + }); + + // then + expect(elementTemplates.isCompatible(incompatibleTemplate)).to.be.false; + })); + + + it('should accept non matching engine', inject(function(elementTemplates) { + + // given + elementTemplates.setEngines({ + nonMatchingEngine: '8.5' + }); + + // then + expect(elementTemplates.isCompatible(compatibleTemplate)).to.be.true; + expect(elementTemplates.isCompatible(incompatibleTemplate)).to.be.true; + })); + + }); + + + describe('error handling', function() { + + // given + const invalidEngines = { + camunda: '7.12', + invalid: 'not-a-semver' + }; + + it('should filter invalid on set', inject(function(elementTemplates) { + + // when + elementTemplates.setEngines(invalidEngines); + + // then + expect(elementTemplates.getEngines()).to.have.property('camunda'); + expect(elementTemplates.getEngines()).to.not.have.property('invalid'); + })); + }); + }); diff --git a/test/spec/element-templates/ElementTemplatesLoader.spec.js b/test/spec/element-templates/ElementTemplatesLoader.spec.js index 366fc0b3..6c8558db 100644 --- a/test/spec/element-templates/ElementTemplatesLoader.spec.js +++ b/test/spec/element-templates/ElementTemplatesLoader.spec.js @@ -34,6 +34,58 @@ describe('provider/element-templates - ElementTemplatesLoader', function() { }); + describe('init with config={ loadTemplates } as Array', function() { + + beforeEach(bootstrapModeler(diagramXML, { + container: container, + modules, + moddleExtensions: { + camunda: camundaModdlePackage + }, + elementTemplates: { + loadTemplates: templateDescriptors + } + })); + + it('should configure elementTemplates service', inject(function(elementTemplates) { + + // then + expect(elementTemplates.getAll()).to.eql(templateDescriptors); + })); + + }); + + + describe('init with config={ loadTemplates } as function', function() { + + let provider = function(done) { + done(null, templateDescriptors); + }; + + const templateProviderFn = function(done) { + provider(done); + }; + + beforeEach(bootstrapModeler(diagramXML, { + container: container, + modules, + moddleExtensions: { + camunda: camundaModdlePackage + }, + elementTemplates: { + loadTemplates: templateProviderFn + } + })); + + it('should configure elementTemplates service', inject(function(elementTemplates) { + + // then + expect(elementTemplates.getAll()).to.eql(templateDescriptors); + })); + + }); + + describe('init with Array', function() { beforeEach(bootstrapModeler(diagramXML, { diff --git a/test/spec/element-templates/Validator.spec.js b/test/spec/element-templates/Validator.spec.js index a8a5726a..817f41fc 100644 --- a/test/spec/element-templates/Validator.spec.js +++ b/test/spec/element-templates/Validator.spec.js @@ -712,4 +712,42 @@ describe('provider/element-templates - Validator', function() { }); + + describe('engines validation', function() { + + it('should accept template with valid semver range', function() { + + // given + const templates = new Validator(moddle); + + const templateDescriptor = require('./fixtures/engines'); + + // when + templates.addAll(templateDescriptor); + + // then + expect(errors(templates)).to.be.empty; + + expect(valid(templates)).to.have.length(templateDescriptor.length); + }); + + + it('should reject template with invalid semver range', function() { + + // given + const templates = new Validator(moddle); + + const templateDescriptor = require('./fixtures/engines-invalid'); + + // when + templates.addAll(templateDescriptor); + + // then + expect(errors(templates)).to.contain('Engine specifies invalid semver range '); + + expect(valid(templates)).to.be.empty; + }); + + }); + }); diff --git a/test/spec/element-templates/fixtures/engines-invalid.json b/test/spec/element-templates/fixtures/engines-invalid.json new file mode 100644 index 00000000..a01cce2f --- /dev/null +++ b/test/spec/element-templates/fixtures/engines-invalid.json @@ -0,0 +1,15 @@ +[ + { + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "basic template with invalid engines", + "version": 3, + "engines": { + "camunda": "invalid-version" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + } +] \ No newline at end of file diff --git a/test/spec/element-templates/fixtures/engines.json b/test/spec/element-templates/fixtures/engines.json new file mode 100644 index 00000000..aec9dff8 --- /dev/null +++ b/test/spec/element-templates/fixtures/engines.json @@ -0,0 +1,91 @@ +[ + { + "id": "example.engines.test.multiple", + "name": " Test - Multiple", + "description": "does not match if { desktopModeler: >=1 } is provided", + "version": 2, + "engines": { + "camunda": "^7.13", + "webModeler": "^4.1", + "desktopModeler": "^0" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "id": "example.engines.test.multiple", + "name": " Test - Multiple", + "description": "matches if { camunda: ^7.13, webModeler: ^4.1 } engine is indicated, or properties are not provided", + "version": 1, + "engines": { + "camunda": "^7.13", + "webModeler": "^4.1" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + + { + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "matches if { camunda: ^7.13 }, or if no engine is provided", + "version": 3, + "engines": { + "camunda": "^7.13" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "matches if { camunda: <=7.12 }, or if no engine is provided", + "version": 2, + "engines": { + "camunda": "<=7.12" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + { + "id": "example.engines.test.basic", + "name": " Test - Basic", + "description": "always matches", + "version": 1, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [] + }, + + { + "id": "example.engines.test.incompatible", + "name": " Test - incompatible", + "description": "incompatible with all camunda versions", + "engines": { + "camunda": "0" + }, + "appliesTo": [ + "bpmn:Task" + ], + "properties": [ + { + "label": "Custom Property 2", + "type": "String", + "value": "property-2-value", + "binding": { + "type": "camunda:property", + "name": "property-2-key" + } + } + ] + } +] \ No newline at end of file