diff --git a/bun.lock b/bun.lock index 18d7915c..b8cf2d05 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ "@vue/tsconfig": "^0.9.1", "@vueuse/core": "^15.0.0", "@wasm-fmt/clang-format": "^23.1.1", - "autoprefixer": "^10.6.1", + "autoprefixer": "^10.6.2", "cmake-js": "^8.0.0", "electron": "^45.0.0", "electron-builder": "^26.18.0", @@ -31,7 +31,7 @@ "electron-updater": "^6.8.11", "electron-vite": "^6.0.0", "node-addon-api": "^8.9.2", - "postcss": "^8.5.31", + "postcss": "^8.5.32", "primeicons": "7.0.0", "primevue": "4.5.5", "rollup-plugin-visualizer": "^7.1.1", @@ -433,7 +433,7 @@ "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], - "autoprefixer": ["autoprefixer@10.6.1", "", { "dependencies": { "browserslist": "^4.28.9", "caniuse-lite": "^1.0.30001810", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA=="], + "autoprefixer": ["autoprefixer@10.6.2", "", { "dependencies": { "browserslist": "^4.28.9", "caniuse-lite": "^1.0.30001810", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-VOw9/yGTGwPPgzFcODtcv6zhEPIeIWkRSe10L2ri2f5vZ7ID7lCt6jZzRF5/ubgOZ+szYydmEF5AYsA5jxX1tA=="], "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="], @@ -451,7 +451,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.29.4", "", { "dependencies": { "baseline-browser-mapping": "^2.11.29", "caniuse-lite": "^1.0.30001816", "electron-to-chromium": "^1.5.459", "node-releases": "^2.0.59", "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA=="], + "browserslist": ["browserslist@4.29.5", "", { "dependencies": { "baseline-browser-mapping": "^2.11.33", "caniuse-lite": "^1.0.30001819", "electron-to-chromium": "^1.5.464", "node-releases": "^2.0.60", "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], @@ -917,7 +917,7 @@ "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], - "postcss": ["postcss@8.5.31", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="], + "postcss": ["postcss@8.5.32", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g=="], "postcss-safe-parser": ["postcss-safe-parser@7.1.0", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-1WzZxRLaAFwEh6Do+zyGpjWV3nGJNxxhuh7Ubu/q1ICImMgZJnLwhoaEpRbG4pJppp2Y1ncL9ffA2f+LhrefQg=="], @@ -999,7 +999,7 @@ "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], @@ -1193,6 +1193,8 @@ "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], "builder-util/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -1221,6 +1223,8 @@ "electron-updater/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "electron-updater/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], diff --git a/package.json b/package.json index 06010543..8f5d396a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "1.20261025.2", + "version": "1.20261027.0", "engines": { "bun": ">=1.2.0" }, @@ -41,8 +41,8 @@ "lint": "bunx --bun biome lint --fix --error-on-warnings --max-diagnostics=none && bunx stylelint '**/*.css' --fix", "postinstall": "bun src/renderer/scripts/ensure.electron.ts", "prune:web": "bun src/renderer/scripts/prune.web.ts", - "release": "bun src/renderer/scripts/libopencor.ts && electron-builder", - "release:local": "bun src/renderer/scripts/libopencor.ts && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config.mac.notarize=false --publish=never", + "release": "electron-builder", + "release:local": "CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config.mac.identity=- --config.mac.hardenedRuntime=false --config.mac.notarize=false --publish=never", "start": "bun src/renderer/scripts/libopencor.ts && electron-vite preview", "start:web": "bun --cwd src/renderer start", "typecheck": "bun --bun src/renderer/scripts/type.check.ts --noEmit -p tsconfig.app.json", @@ -67,7 +67,7 @@ "@vue/tsconfig": "^0.9.1", "@vueuse/core": "^15.0.0", "@wasm-fmt/clang-format": "^23.1.1", - "autoprefixer": "^10.6.1", + "autoprefixer": "^10.6.2", "cmake-js": "^8.0.0", "electron": "^45.0.0", "electron-builder": "^26.18.0", @@ -75,7 +75,7 @@ "electron-updater": "^6.8.11", "electron-vite": "^6.0.0", "node-addon-api": "^8.9.2", - "postcss": "^8.5.31", + "postcss": "^8.5.32", "primeicons": "7.0.0", "primevue": "4.5.5", "rollup-plugin-visualizer": "^7.1.1", diff --git a/src/main/MainMenu.ts b/src/main/MainMenu.ts index e3c75703..e3bdd88b 100644 --- a/src/main/MainMenu.ts +++ b/src/main/MainMenu.ts @@ -249,6 +249,13 @@ export const enableDisableMainMenu = (enable: boolean): void => { } }); helpSubMenu.push({ type: 'separator' }); + helpSubMenu.push({ + label: 'Changelog', + click: () => { + mainWindow?.send('changelog'); + } + }); + helpSubMenu.push({ type: 'separator' }); helpSubMenu.push({ label: 'Report Issue', click: () => { diff --git a/src/preload/index.ts b/src/preload/index.ts index 2127288d..2900c536 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -118,6 +118,7 @@ electron.contextBridge.exposeInMainWorld('electronApi', { onAction: (callback: (action: string) => void) => onIpc('action', callback), onCanonicaliseRecentFilePaths: (callback: (filePaths: string[]) => void) => onIpc('canonicalise-recent-file-paths', callback), + onChangelog: (callback: () => void) => onIpc('changelog', callback), onCheckForUpdates: (callback: () => void) => onIpc('check-for-updates', callback), onEnableDisableUi: (callback: (enable: boolean) => void) => onIpc('enable-disable-ui', callback), onOpen: (callback: (filePath: string) => void) => onIpc('open', callback), diff --git a/src/renderer/README.md b/src/renderer/README.md index 21294ef4..44c1f2f8 100644 --- a/src/renderer/README.md +++ b/src/renderer/README.md @@ -154,7 +154,11 @@ import '@opencor/opencor/style.css'; - **Exposed methods:** - OpenCOR always exposes the following methods, which only have an effect when an `omex` prop is provided: + OpenCOR always exposes the following method: + + - `changelog()`: returns the changes for all the versions of OpenCOR up to the current one, as an array of `{ version: string; changes: { type: 'added' | 'removed' | 'improved' | 'fixed'; description: string }[] }` objects (see `IOpenCORChangelogEntry`), from the newest version to the oldest one. + + OpenCOR also always exposes the following methods, which only have an effect when an `omex` prop is provided: - `trackSimulationData(modelParameters: string[])`: adds the given parameter names to the list of tracked simulation data. - `untrackSimulationData(modelParameters: string[])`: removes the given parameter names from the list of tracked simulation data. diff --git a/src/renderer/bun.lock b/src/renderer/bun.lock index bd0c64e2..387e9299 100644 --- a/src/renderer/bun.lock +++ b/src/renderer/bun.lock @@ -16,8 +16,8 @@ "@vitejs/plugin-vue": "^6.0.10", "@vue/tsconfig": "^0.9.1", "@vueuse/core": "^15.0.0", - "autoprefixer": "^10.6.1", - "postcss": "^8.5.31", + "autoprefixer": "^10.6.2", + "postcss": "^8.5.32", "primeicons": "7.0.0", "primevue": "4.5.5", "rollup-plugin-visualizer": "^7.1.1", @@ -297,13 +297,13 @@ "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], - "autoprefixer": ["autoprefixer@10.6.1", "", { "dependencies": { "browserslist": "^4.28.9", "caniuse-lite": "^1.0.30001810", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA=="], + "autoprefixer": ["autoprefixer@10.6.2", "", { "dependencies": { "browserslist": "^4.28.9", "caniuse-lite": "^1.0.30001810", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-VOw9/yGTGwPPgzFcODtcv6zhEPIeIWkRSe10L2ri2f5vZ7ID7lCt6jZzRF5/ubgOZ+szYydmEF5AYsA5jxX1tA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-UvuxFU9P3hM29LrQdwu0cD6w6/+SFr+UlB9s/knI7+O2rK9xFxKUwSDWxGdbbqQ9/V7BJ5s0NI5nQAE4uJp3gQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.33", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-yiVKDX+qCMRKBE084lXvq9EcwFUhKZ2nU8TK1EQQuOgPNR/lmbGxXOeDdlGUqC2Zz11Cs9rk1d/8NfgBmlkt0w=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.29.4", "", { "dependencies": { "baseline-browser-mapping": "^2.11.29", "caniuse-lite": "^1.0.30001816", "electron-to-chromium": "^1.5.459", "node-releases": "^2.0.59", "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA=="], + "browserslist": ["browserslist@4.29.5", "", { "dependencies": { "baseline-browser-mapping": "^2.11.33", "caniuse-lite": "^1.0.30001819", "electron-to-chromium": "^1.5.464", "node-releases": "^2.0.60", "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ=="], "bun-types": ["bun-types@1.4.4", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-yjrJh5oGX7mMPKGd19YXyIjmEuj4G4zNC30bs7PM9Az7NQ3ndF3j6hbeRdvJTRQEhJ/FYQwW0a2moweEi2hMxQ=="], @@ -535,7 +535,7 @@ "pkg-types": ["pkg-types@2.4.0", "", { "dependencies": { "confbox": "^0.3.1", "exsolve": "^1.1.3", "pathe": "^2.0.3" } }, "sha512-07Fys7iVx0X6TcviMOgogM1Lk33ZyqOezMnU79S+kffWqwolG/5W9V8EgQSeYCIfFTYVgssRbnH630Oa2+soBA=="], - "postcss": ["postcss@8.5.31", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg=="], + "postcss": ["postcss@8.5.32", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g=="], "postcss-safe-parser": ["postcss-safe-parser@7.1.0", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-1WzZxRLaAFwEh6Do+zyGpjWV3nGJNxxhuh7Ubu/q1ICImMgZJnLwhoaEpRbG4pJppp2Y1ncL9ffA2f+LhrefQg=="], @@ -621,7 +621,7 @@ "unicorn-magic": ["unicorn-magic@0.4.1", "", {}, "sha512-lzlXPoVB0Uy/FvTaUgX39RbixYiSKoc3JSoSf01+0c2B6FR3qdYIPN7Qjo/AV7n6sqLCk0509cT3LRj1oSZ1+A=="], - "unplugin": ["unplugin@3.4.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.7", "webpack-virtual-modules": "^0.6.2" }, "peerDependencies": { "@farmfe/core": "*", "@rsbuild/core": "*", "@rspack/core": "*", "bun-types-no-globals": "*", "esbuild": "*", "rolldown": "*", "rollup": "*", "unloader": "*", "vite": "*", "webpack": "*" }, "optionalPeers": ["@farmfe/core", "@rsbuild/core", "@rspack/core", "bun-types-no-globals", "esbuild", "rolldown", "rollup", "unloader", "vite", "webpack"] }, "sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w=="], + "unplugin": ["unplugin@3.5.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.7", "webpack-virtual-modules": "^0.6.2" }, "peerDependencies": { "@farmfe/core": "*", "@rsbuild/core": "*", "@rspack/core": "*", "bun-types-no-globals": "*", "esbuild": "*", "rolldown": "*", "rollup": "*", "unloader": "*", "vite": "*", "webpack": "*" }, "optionalPeers": ["@farmfe/core", "@rsbuild/core", "@rspack/core", "bun-types-no-globals", "esbuild", "rolldown", "rollup", "unloader", "vite", "webpack"] }, "sha512-s9qc6Y8ChhQE9RZ9Se+znEtZA0oK31LmEBO9y97ixR3ErA8UwlzA4XNybg/41od8rQ8EKScVQM857NWnI+EWkQ=="], "unplugin-utils": ["unplugin-utils@0.3.2", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.4" } }, "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA=="], @@ -669,7 +669,7 @@ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" }, "bundled": true }, "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], diff --git a/src/renderer/changelog.json b/src/renderer/changelog.json new file mode 100644 index 00000000..1d49c5b8 --- /dev/null +++ b/src/renderer/changelog.json @@ -0,0 +1,13 @@ +[ + { + "version": "1.20261027.0", + "changes": [ + { "type": "added", "description": "A Changelog dialog, which lists the changes of all the versions of OpenCOR." }, + { "type": "added", "description": "A `changelog()` method to OpenCOR's component." }, + { + "type": "improved", + "description": "The update dialog now lists what has changed in the newer version of OpenCOR." + } + ] + } +] diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 09586385..0bfbec1c 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -5,7 +5,20 @@ export interface IOpenCORProps { theme?: OpenCORTheme; } +export type OpenCORChangelogChangeType = 'added' | 'removed' | 'improved' | 'fixed'; + +export interface IOpenCORChangelogChange { + type: OpenCORChangelogChangeType; + description: string; +} + +export interface IOpenCORChangelogEntry { + version: string; + changes: IOpenCORChangelogChange[]; +} + export interface IOpenCORExpose { + changelog: () => IOpenCORChangelogEntry[]; addExternalData: (csv: string, voiExpression: string | undefined, modelParameters: string[]) => void; trackSimulationData: (modelParameters: string[]) => void; untrackSimulationData: (modelParameters: string[]) => void; diff --git a/src/renderer/package.json b/src/renderer/package.json index cca992fb..77016345 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -42,7 +42,7 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "1.20261025.2", + "version": "1.20261027.0", "libopencorVersion": "1.20261020.0", "scripts": { "build": "vite build && bun scripts/generate.version.ts", @@ -73,8 +73,8 @@ "@vitejs/plugin-vue": "^6.0.10", "@vue/tsconfig": "^0.9.1", "@vueuse/core": "^15.0.0", - "autoprefixer": "^10.6.1", - "postcss": "^8.5.31", + "autoprefixer": "^10.6.2", + "postcss": "^8.5.32", "primeicons": "7.0.0", "primevue": "4.5.5", "rollup-plugin-visualizer": "^7.1.1", diff --git a/src/renderer/scripts/generate.version.ts b/src/renderer/scripts/generate.version.ts index 61b90183..10fd4d2f 100644 --- a/src/renderer/scripts/generate.version.ts +++ b/src/renderer/scripts/generate.version.ts @@ -25,6 +25,14 @@ if (!fs.existsSync(distAssetsPath)) { fs.writeFileSync(path.join(distAssetsPath, 'version.json'), JSON.stringify({ version }, null, 2)); +// Copy our changelog file, so that OpenCOR's Web app can tell the user what is new in a newer version (see +// src/common/changelog.ts). +// Note: like version.json, it has a fixed name, so it must never be cached for long (see public/.htaccess). It also +// gets uploaded before version.json when deploying our Web app (see .github/workflows/cd.yml and cddev.yml), so +// it is always available by the time a newer version gets reported. + +fs.copyFileSync(path.join(__dirname, '../changelog.json'), path.join(distAssetsPath, 'changelog.json')); + // Log the generated version. console.log(`Generated version.json with version ${version}.`); diff --git a/src/renderer/src/common/changelog.ts b/src/renderer/src/common/changelog.ts new file mode 100644 index 00000000..9b893d20 --- /dev/null +++ b/src/renderer/src/common/changelog.ts @@ -0,0 +1,157 @@ +import type { IOpenCORChangelogChange, IOpenCORChangelogEntry, OpenCORChangelogChangeType } from '../../index'; + +import changelogJson from '../../changelog.json'; + +import { formatError } from './common'; +import { electronApi } from './electronApi'; +import { currentVersion, isNewerVersion } from './version'; + +// The types of changes, in the order in which they are to be listed. + +const CHANGE_TYPES: OpenCORChangelogChangeType[] = ['added', 'removed', 'improved', 'fixed']; + +// Sanitise a changelog, i.e. keep only its valid entries (i.e. those with a valid version and at least one valid +// change) and their valid changes (i.e. those with a known type and a non-empty description), merge the entries that +// have the same version, sort the entries from the newest version to the oldest one, and sort the changes of each entry +// by type. +// Note: a changelog is maintained by hand, so we want to be robust against mistakes in it. In addition, OpenCOR fetches +// the changelog of a newer version (see fetchChangelog() below), so its format may have evolved in the meantime. + +const VERSION_REGEX = /^\d+\.\d+\.\d+$/; + +const isObject = (value: unknown): value is Record => { + return typeof value === 'object' && value !== null; +}; + +const isChangelogChange = (change: unknown): change is IOpenCORChangelogChange => { + return ( + isObject(change) && + CHANGE_TYPES.includes(change.type as OpenCORChangelogChangeType) && + typeof change.description === 'string' && + change.description.trim() !== '' + ); +}; + +const isChangelogEntry = (entry: unknown): entry is { version: string; changes: unknown[] } => { + return ( + isObject(entry) && + typeof entry.version === 'string' && + VERSION_REGEX.test(entry.version) && + Array.isArray(entry.changes) + ); +}; + +const sanitisedChangelog = (changelog: unknown): IOpenCORChangelogEntry[] => { + if (!Array.isArray(changelog)) { + return []; + } + + const entries = new Map(); + + for (const entry of changelog.filter(isChangelogEntry)) { + const changes = entry.changes.filter(isChangelogChange).map((change: IOpenCORChangelogChange) => { + return { type: change.type, description: change.description.trim() }; + }); + const existingEntry = entries.get(entry.version); + + if (existingEntry) { + existingEntry.changes.push(...changes); + } else { + entries.set(entry.version, { version: entry.version, changes }); + } + } + + return [...entries.values()] + .filter((entry: IOpenCORChangelogEntry) => { + return entry.changes.length > 0; + }) + .map((entry: IOpenCORChangelogEntry) => { + // Note: Array.prototype.sort() is stable, so changes of the same type remain in the order in which they are + // listed. + + entry.changes.sort((changeA: IOpenCORChangelogChange, changeB: IOpenCORChangelogChange) => { + return CHANGE_TYPES.indexOf(changeA.type) - CHANGE_TYPES.indexOf(changeB.type); + }); + + return entry; + }) + .sort((entryA: IOpenCORChangelogEntry, entryB: IOpenCORChangelogEntry) => { + if (isNewerVersion(entryA.version, entryB.version)) { + return -1; + } + + return isNewerVersion(entryB.version, entryA.version) ? 1 : 0; + }); +}; + +// Return the entries of a changelog for the versions after the given "from" version (excluded), if any, up to the given +// "to" version (included). + +const changelogEntries = ( + changelog: IOpenCORChangelogEntry[], + fromVersion: string | null, + toVersion: string +): IOpenCORChangelogEntry[] => { + return changelog.filter((entry: IOpenCORChangelogEntry) => { + return ( + (fromVersion === null || isNewerVersion(entry.version, fromVersion)) && !isNewerVersion(entry.version, toVersion) + ); + }); +}; + +// Our changelog, i.e. the changes for all the versions up to our current version. +// Note: we import our changelog.json file rather than inject it at build time (as we do with our current version, see +// version.ts), so that any change to it gets picked up straight away when running the development version of +// OpenCOR or of OpenCOR's Web app. + +const changelogEntriesUpToCurrentVersion = changelogEntries(sanitisedChangelog(changelogJson), null, currentVersion); + +const copiedChangelogEntry = (entry: IOpenCORChangelogEntry): IOpenCORChangelogEntry => { + return { + version: entry.version, + changes: entry.changes.map((change: IOpenCORChangelogChange) => { + return { type: change.type, description: change.description }; + }) + }; +}; + +const changelog = (): IOpenCORChangelogEntry[] => { + // Return a copy of our changelog, so that it cannot be modified by our caller. + + return changelogEntriesUpToCurrentVersion.map(copiedChangelogEntry); +}; + +// Fetch the changes for the versions after our current version (excluded) up to the given version (included). +// Note #1: OpenCOR fetches the changelog.json file of the given version from our GitHub repository (using the tag of +// that version) while OpenCOR's Web app fetches the changelog.json file that is deployed with it (see +// scripts/generate.version.ts), i.e. the changelog of the latest deployed version (which may be even newer +// than the given version) with cache busting (i.e. by adding a timestamp query parameter to the URL so that +// the browser doesn't serve a cached version). +// Note #2: we don't want to bother the user if we cannot fetch the changes, so we just return an empty list in that +// case. + +const fetchChangelog = async (toVersion: string): Promise => { + const url = electronApi + ? `https://raw.githubusercontent.com/opencor/webapp/v${toVersion}/src/renderer/changelog.json` + : `./assets/changelog.json?t=${Date.now()}`; + + try { + const response = await fetch(url); + + if (!response.ok) { + console.warn(`OpenCOR: failed to fetch the changelog (${response.status}: ${response.statusText}).`); + + return []; + } + + return changelogEntries(sanitisedChangelog(await response.json()), currentVersion, toVersion); + } catch (error: unknown) { + console.warn('OpenCOR: failed to fetch the changelog:', formatError(error)); + + return []; + } +}; + +// Export our changelog functions. + +export { changelog, fetchChangelog }; diff --git a/src/renderer/src/common/electronApi.ts b/src/renderer/src/common/electronApi.ts index 512ba37e..7fb87672 100644 --- a/src/renderer/src/common/electronApi.ts +++ b/src/renderer/src/common/electronApi.ts @@ -38,6 +38,7 @@ interface IElectronApi { onAbout: (callback: () => void) => () => void; onAction: (callback: (action: string) => void) => () => void; onCanonicaliseRecentFilePaths: (callback: (filePaths: string[]) => void) => () => void; + onChangelog: (callback: () => void) => () => void; onCheckForUpdates: (callback: () => void) => () => void; onEnableDisableUi: (callback: (enable: boolean) => void) => () => void; onOpen: (callback: (filePath: string) => void) => () => void; diff --git a/src/renderer/src/common/version.ts b/src/renderer/src/common/version.ts index f0940368..c48c8ae7 100644 --- a/src/renderer/src/common/version.ts +++ b/src/renderer/src/common/version.ts @@ -141,4 +141,4 @@ const forceReload = async (): Promise => { // Export the version checking functions and state. -export { currentVersion, forceReload, latestVersion, startCheck, updateAvailable }; +export { currentVersion, forceReload, isNewerVersion, latestVersion, startCheck, updateAvailable }; diff --git a/src/renderer/src/components/ChangelogComponent.vue b/src/renderer/src/components/ChangelogComponent.vue new file mode 100644 index 00000000..e8d53a4a --- /dev/null +++ b/src/renderer/src/components/ChangelogComponent.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/src/renderer/src/components/MainMenu.vue b/src/renderer/src/components/MainMenu.vue index 8642f997..ccd82326 100644 --- a/src/renderer/src/components/MainMenu.vue +++ b/src/renderer/src/components/MainMenu.vue @@ -52,6 +52,7 @@ const props = defineProps<{ const emit = defineEmits<{ about: []; + changelog: []; close: []; closeAll: []; open: []; @@ -154,6 +155,13 @@ const items = vue.computed(() => [ } }, { separator: true }, + { + label: 'Changelog', + command: () => { + emit('changelog'); + } + }, + { separator: true }, { label: 'Report Issue', command: () => { diff --git a/src/renderer/src/components/OpenCOR.vue b/src/renderer/src/components/OpenCOR.vue index 4cddf3f5..7296511a 100644 --- a/src/renderer/src/components/OpenCOR.vue +++ b/src/renderer/src/components/OpenCOR.vue @@ -33,6 +33,7 @@ - + - @@ -126,6 +126,7 @@ import type { IOpenCOREmits, IOpenCORExternalDataEvent, IOpenCORProps, IOpenCORS import '../assets/app.css'; import '../assets/primeicons-assets'; +import * as changelog from '../common/changelog'; import * as common from '../common/common'; import { FULL_URI_SCHEME, LONG_DELAY, SHORT_DELAY, TOAST_LIFE } from '../common/constants'; import { electronApi } from '../common/electronApi'; @@ -263,6 +264,10 @@ const onFileClosed = (filePath: string): void => { // Exposed methods. defineExpose({ + // General methods. + + changelog: changelog.changelog, + // Simulation-only methods. addExternalData, @@ -695,14 +700,20 @@ const onUpdateErrorDialogClose = (): void => { updateDownloadProgressVisible.value = false; }; -const desktopUpdateAvailableVisible = vue.ref(false); +// Note: our update available dialog is used by both OpenCOR (when our main process lets us know that an update is +// available) and OpenCOR's Web app (when the user clicks on our main menu's "New version available!" button, see +// onUpdateAvailable() below). We keep track of the version to update to, so that it doesn't change while our +// dialog is shown (e.g., if an even newer version of OpenCOR's Web app gets released in the meantime), which +// means that it always matches the changes that our dialog lists. + +const updateAvailableVisible = vue.ref(false); const updateDownloadProgressVisible = vue.ref(false); const updateVersion = vue.ref(''); const updateDownloadPercent = vue.ref(0); trackCleanup( electronApi?.onUpdateAvailable((version: string) => { - desktopUpdateAvailableVisible.value = true; + updateAvailableVisible.value = true; updateVersion.value = version; }) ); @@ -710,7 +721,7 @@ trackCleanup( const onDownloadAndInstall = (): void => { updateDownloadPercent.value = 0; // Just to be on the safe side. updateDownloadProgressVisible.value = true; - desktopUpdateAvailableVisible.value = false; + updateAvailableVisible.value = false; electronApi?.downloadAndInstallUpdate(); }; @@ -777,6 +788,24 @@ const onError = (message: string): void => { }); }; +// Changelog dialog. + +const changelogVisible = vue.ref(false); + +trackCleanup( + electronApi?.onChangelog(() => { + onChangelogMenu(); + }) +); + +const onChangelogMenu = (): void => { + if (props.omex) { + return; + } + + changelogVisible.value = true; +}; + // About dialog. const aboutVisible = vue.ref(false); @@ -813,12 +842,11 @@ const onSettingsMenu = (): void => { settingsVisible.value = true; }; -// Update available dialog. - -const webUpdateAvailableVisible = vue.ref(false); +// Update available dialog (for OpenCOR's Web app). const onUpdateAvailable = () => { - webUpdateAvailableVisible.value = true; + updateVersion.value = version.latestVersion.value; + updateAvailableVisible.value = true; }; // Open a file. diff --git a/src/renderer/src/components/dialogs/ChangelogDialog.vue b/src/renderer/src/components/dialogs/ChangelogDialog.vue new file mode 100644 index 00000000..34701c65 --- /dev/null +++ b/src/renderer/src/components/dialogs/ChangelogDialog.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/dialogs/UpdateAvailableDialog.vue b/src/renderer/src/components/dialogs/UpdateAvailableDialog.vue index 7fcd1107..963ff253 100644 --- a/src/renderer/src/components/dialogs/UpdateAvailableDialog.vue +++ b/src/renderer/src/components/dialogs/UpdateAvailableDialog.vue @@ -2,21 +2,35 @@
-
OpenCOR {{ latestVersion }} is available!
+
OpenCOR {{ version }} is available!
+
+
+
What's new:
+
-
+
+ Do you want to download it and install it? +
+
Feel free to reload OpenCOR whenever you are ready.
(Please note that any unsaved work will be lost.)
@@ -24,12 +38,44 @@