From 9b6268c6db3f8232a2366c76d9bb609f2a521269 Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Thu, 16 Apr 2026 08:25:45 +1200 Subject: [PATCH 1/7] New version. --- package.json | 2 +- src/renderer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d3460fc7..d75ea4fb 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "0.20260416.0", + "version": "0.20260416.1", "engines": { "bun": ">=1.2.0" }, diff --git a/src/renderer/package.json b/src/renderer/package.json index b7751be4..cca2b818 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -42,7 +42,7 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "0.20260416.0", + "version": "0.20260416.1", "scripts": { "build": "vite build && bun scripts/generate.version.js", "build:lib": "vite build --config vite.lib.config.ts && bunx --bun vue-tsc --project tsconfig.lib.types.json", From 95035a5ca299da6b9b367f973285d18ea74a3d48 Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Thu, 16 Apr 2026 08:26:15 +1200 Subject: [PATCH 2/7] Updated our dependencies. --- bun.lock | 10 +++++----- package.json | 2 +- src/renderer/bun.lock | 10 +++++----- src/renderer/package.json | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bun.lock b/bun.lock index 882c8e23..b2e4693b 100644 --- a/bun.lock +++ b/bun.lock @@ -34,7 +34,7 @@ "electron-vite": "^5.0.0", "esbuild": "^0.28.0", "node-addon-api": "^8.7.0", - "postcss": "^8.5.9", + "postcss": "^8.5.10", "rollup-plugin-visualizer": "^7.0.1", "stylelint": "^17.8.0", "stylelint-config-standard": "^40.0.0", @@ -256,7 +256,7 @@ "@napi-rs/keyring-win32-x64-msvc": ["@napi-rs/keyring-win32-x64-msvc@1.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xFlx/TsmqmCwNU9v+AVnEJgoEAlBYgzFF5Ihz1rMpPAt4qQWWkMd4sCyM1gMJ1A/GnRqRegDiQpwaxGUHFtFbA=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -616,7 +616,7 @@ "electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], - "electron-to-chromium": ["electron-to-chromium@1.5.336", "", {}, "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.337", "", {}, "sha512-15gKW9mRUNP9RdzhedJNypFUxtYWSXohFz2nTLzM272xbRXHws68kNDzyATG3qej+vUj/7Sn9hf5XTDh0XK6/w=="], "electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="], @@ -1014,7 +1014,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.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], + "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], "postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="], @@ -1310,7 +1310,7 @@ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], diff --git a/package.json b/package.json index d75ea4fb..265b431c 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "electron-vite": "^5.0.0", "esbuild": "^0.28.0", "node-addon-api": "^8.7.0", - "postcss": "^8.5.9", + "postcss": "^8.5.10", "rollup-plugin-visualizer": "^7.0.1", "stylelint": "^17.8.0", "stylelint-config-standard": "^40.0.0", diff --git a/src/renderer/bun.lock b/src/renderer/bun.lock index 90024bdf..8e92732f 100644 --- a/src/renderer/bun.lock +++ b/src/renderer/bun.lock @@ -24,7 +24,7 @@ "@vue/tsconfig": "^0.9.1", "autoprefixer": "^10.5.0", "esbuild": "^0.28.0", - "postcss": "^8.5.9", + "postcss": "^8.5.10", "rollup-plugin-visualizer": "^7.0.1", "stylelint": "^17.8.0", "stylelint-config-standard": "^40.0.0", @@ -187,7 +187,7 @@ "@napi-rs/keyring-win32-x64-msvc": ["@napi-rs/keyring-win32-x64-msvc@1.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xFlx/TsmqmCwNU9v+AVnEJgoEAlBYgzFF5Ihz1rMpPAt4qQWWkMd4sCyM1gMJ1A/GnRqRegDiQpwaxGUHFtFbA=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -383,7 +383,7 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "electron-to-chromium": ["electron-to-chromium@1.5.336", "", {}, "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.337", "", {}, "sha512-15gKW9mRUNP9RdzhedJNypFUxtYWSXohFz2nTLzM272xbRXHws68kNDzyATG3qej+vUj/7Sn9hf5XTDh0XK6/w=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -569,7 +569,7 @@ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], - "postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], + "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], "postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="], @@ -699,7 +699,7 @@ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], diff --git a/src/renderer/package.json b/src/renderer/package.json index cca2b818..5d4e04f6 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -84,7 +84,7 @@ "@vue/tsconfig": "^0.9.1", "autoprefixer": "^10.5.0", "esbuild": "^0.28.0", - "postcss": "^8.5.9", + "postcss": "^8.5.10", "rollup-plugin-visualizer": "^7.0.1", "stylelint": "^17.8.0", "stylelint-config-standard": "^40.0.0", From 56feb29a20beefa55c6fc47dcbd4a67bab664214 Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Thu, 16 Apr 2026 10:08:22 +1200 Subject: [PATCH 3/7] Added the 'type' property to our simulation data event. --- src/renderer/index.ts | 2 ++ src/renderer/src/components/ContentsComponent.vue | 3 +++ src/renderer/src/components/OpenCOR.vue | 1 + .../src/components/views/SimulationExperimentView.vue | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/src/renderer/index.ts b/src/renderer/index.ts index ff14b0ac..81ee1971 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -27,9 +27,11 @@ export interface IOpenCORExternalDataEvent { issues: string[]; } +export type OpenCORSimulationDataEventType = 'updated' | 'issue'; export type OpenCORSimulationData = Record; export interface IOpenCORSimulationDataEvent { + type: OpenCORSimulationDataEventType; simulationData: OpenCORSimulationData; issues: string[]; } diff --git a/src/renderer/src/components/ContentsComponent.vue b/src/renderer/src/components/ContentsComponent.vue index cd57e67e..eb826eab 100644 --- a/src/renderer/src/components/ContentsComponent.vue +++ b/src/renderer/src/components/ContentsComponent.vue @@ -249,6 +249,7 @@ const addExternalData = async ( const simulationData = (modelParameters: string[]): Promise => { if (!props.simulationOnly) { return Promise.resolve({ + type: 'issue', simulationData: common.emptySimulationData(modelParameters), issues: ['Simulation data can only be retrieved in simulation-only mode.'] }); @@ -258,6 +259,7 @@ const simulationData = (modelParameters: string[]): Promise { return { + type: 'issue', simulationData: common.emptySimulationData(modelParameters), issues: [common.formatError(error)] }; diff --git a/src/renderer/src/components/OpenCOR.vue b/src/renderer/src/components/OpenCOR.vue index 9096af4a..2003dcb3 100644 --- a/src/renderer/src/components/OpenCOR.vue +++ b/src/renderer/src/components/OpenCOR.vue @@ -233,6 +233,7 @@ const emitSimulationData = (): void => { if (!contents) { emit('simulationData', { + type: 'issue', simulationData: common.emptySimulationData(trackedSimulationData.value), issues: ['No contents available.'] }); diff --git a/src/renderer/src/components/views/SimulationExperimentView.vue b/src/renderer/src/components/views/SimulationExperimentView.vue index 49c78abd..82b6de6d 100644 --- a/src/renderer/src/components/views/SimulationExperimentView.vue +++ b/src/renderer/src/components/views/SimulationExperimentView.vue @@ -963,12 +963,14 @@ const simulationData = (modelParameters: string[]): Promise Date: Thu, 16 Apr 2026 09:53:02 +1200 Subject: [PATCH 4/7] Added the 'type' property to our external data event. --- src/renderer/index.ts | 3 +++ src/renderer/src/components/ContentsComponent.vue | 2 ++ src/renderer/src/components/OpenCOR.vue | 1 + .../src/components/views/SimulationExperimentView.vue | 8 ++++++++ 4 files changed, 14 insertions(+) diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 81ee1971..bd904421 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -22,7 +22,10 @@ export interface IOpenCORSimulationDataValue { unit: string; } +export type OpenCORExternalDataEventType = 'added' | 'issue'; + export interface IOpenCORExternalDataEvent { + type: OpenCORExternalDataEventType; csv: string; issues: string[]; } diff --git a/src/renderer/src/components/ContentsComponent.vue b/src/renderer/src/components/ContentsComponent.vue index eb826eab..71024eec 100644 --- a/src/renderer/src/components/ContentsComponent.vue +++ b/src/renderer/src/components/ContentsComponent.vue @@ -231,6 +231,7 @@ const addExternalData = async ( if (!simulationExperimentViews.length) { return Promise.resolve({ + type: 'issue', csv, issues: ['No simulation experiment view available.'] }); @@ -238,6 +239,7 @@ const addExternalData = async ( return simulationExperimentViews[0].addExternalData(csv, voiExpression, modelParameters).catch((error: unknown) => { return { + type: 'issue', csv, issues: [common.formatError(error)] }; diff --git a/src/renderer/src/components/OpenCOR.vue b/src/renderer/src/components/OpenCOR.vue index 2003dcb3..2b5eb144 100644 --- a/src/renderer/src/components/OpenCOR.vue +++ b/src/renderer/src/components/OpenCOR.vue @@ -161,6 +161,7 @@ const addExternalData = (csv: string, voiExpression: string | undefined, modelPa if (!contents) { emit('externalData', { + type: 'issue', csv, issues: ['No contents available.'] }); diff --git a/src/renderer/src/components/views/SimulationExperimentView.vue b/src/renderer/src/components/views/SimulationExperimentView.vue index 82b6de6d..1c4c571d 100644 --- a/src/renderer/src/components/views/SimulationExperimentView.vue +++ b/src/renderer/src/components/views/SimulationExperimentView.vue @@ -757,11 +757,13 @@ const addExternalData = async ( // Make sure that we are in simulation-only mode. const res: IOpenCORExternalDataEvent = { + type: 'added', csv, issues: [] }; if (!props.simulationOnly) { + res.type = 'issue'; res.issues = ['The exposed addExternalData() method is only available in simulation-only mode.']; return Promise.resolve(res); @@ -776,6 +778,7 @@ const addExternalData = async ( const response = await fetch(common.corsProxyUrl(csv)); if (!response.ok) { + res.type = 'issue'; res.issues = [`Could not retrieve the CSV file from ${csv} (status: ${response.status}).`]; return Promise.resolve(res); @@ -783,6 +786,7 @@ const addExternalData = async ( csvContents = await response.text(); } catch (error: unknown) { + res.type = 'issue'; res.issues = [ `Could not retrieve the CSV file from ${csv} (${common.formatMessage(common.formatError(error), false)}).` ]; @@ -803,6 +807,7 @@ const addExternalData = async ( const csvHash = common.xxh64(csvContents); if (addedExternalDataHashes.has(csvHash) || inFlightExternalDataHashes.has(csvHash)) { + res.type = 'issue'; res.issues = ['The external data has already been added.']; return Promise.resolve(res); @@ -818,6 +823,7 @@ const addExternalData = async ( try { parsedCsv = externalData.parseExternalCsvData(csvContents); } catch (error: unknown) { + res.type = 'issue'; res.issues.push(common.formatMessage(common.formatError(error))); return Promise.resolve(res); @@ -826,6 +832,7 @@ const addExternalData = async ( // Make sure that the number of model parameters matches the number of data columns in the CSV file. if (parsedCsv.headers.length - 1 !== modelParameters.length) { + res.type = 'issue'; res.issues = [ `The number of model parameters provided must match the number of data columns in the CSV file (i.e. ${parsedCsv.headers.length - 1}, not ${modelParameters.length}).` ]; @@ -848,6 +855,7 @@ const addExternalData = async ( } if (res.issues.length) { + res.type = 'issue'; return Promise.resolve(res); } From 66e75c65aec2c2a87032eae9cd502062078eeecf Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Thu, 16 Apr 2026 09:58:45 +1200 Subject: [PATCH 5/7] Added a file event to to let users know about a file having been opened, closed, etc. --- src/renderer/index.ts | 9 ++++ .../src/components/ContentsComponent.vue | 8 +++- src/renderer/src/components/OpenCOR.vue | 42 ++++++++++++------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/renderer/index.ts b/src/renderer/index.ts index bd904421..533897ad 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -14,6 +14,7 @@ export interface IOpenCORExpose { export interface IOpenCOREmits extends /* @vue-ignore */ Record { externalData: [IOpenCORExternalDataEvent]; + file: [IOpenCORFileEvent]; simulationData: [IOpenCORSimulationDataEvent]; } @@ -30,6 +31,14 @@ export interface IOpenCORExternalDataEvent { issues: string[]; } +export type OpenCORFileEventType = 'opened' | 'closed' | 'issue'; + +export interface IOpenCORFileEvent { + type: OpenCORFileEventType; + filePath: string; + issues: string[]; +} + export type OpenCORSimulationDataEventType = 'updated' | 'issue'; export type OpenCORSimulationData = Record; diff --git a/src/renderer/src/components/ContentsComponent.vue b/src/renderer/src/components/ContentsComponent.vue index 71024eec..825bbb8d 100644 --- a/src/renderer/src/components/ContentsComponent.vue +++ b/src/renderer/src/components/ContentsComponent.vue @@ -84,8 +84,10 @@ const props = defineProps<{ uiEnabled: boolean; }>(); -defineEmits<{ +const emit = defineEmits<{ (event: 'error', message: string): void; + (event: 'fileClosed', filePath: string): void; + (event: 'fileOpened', filePath: string): void; (event: 'simulationData'): void; }>(); @@ -179,6 +181,8 @@ const openFile = async (file: locApi.File, wait: boolean = false): Promise electronApi?.fileOpened(filePath); + emit('fileOpened', filePath); + if (wait) { await waitForTabsUpdate(); } @@ -200,6 +204,8 @@ const closeFile = async (filePath: string): Promise => { } electronApi?.fileClosed(filePath); + + emit('fileClosed', filePath); }; const closeCurrentFile = async (): Promise => { diff --git a/src/renderer/src/components/OpenCOR.vue b/src/renderer/src/components/OpenCOR.vue index 2b5eb144..e10edd08 100644 --- a/src/renderer/src/components/OpenCOR.vue +++ b/src/renderer/src/components/OpenCOR.vue @@ -62,6 +62,8 @@ :uiEnabled="compUiEnabled" :simulationOnly="!!omex" @error="onError" + @fileClosed="onFileClosed" + @fileOpened="onFileOpened" @simulationData="emitSimulationData" /> @@ -247,6 +249,16 @@ const emitSimulationData = (): void => { }); }; +// Methods to handle files. + +const onFileOpened = (filePath: string): void => { + emit('file', { type: 'opened', filePath, issues: [] }); +}; + +const onFileClosed = (filePath: string): void => { + emit('file', { type: 'closed', filePath, issues: [] }); +}; + // Exposed methods. defineExpose({ @@ -758,34 +770,32 @@ const processFile = async (fileFilePathOrFileContents: string | Uint8Array | Fil fileType === locApi.EFileType.SEDML_FILE || (props.omex && fileType !== locApi.EFileType.COMBINE_ARCHIVE) ) { + const issueMessage = + fileType === locApi.EFileType.IRRETRIEVABLE_FILE + ? 'The file could not be retrieved.' + : fileType === locApi.EFileType.SEDML_FILE && !props.omex + ? 'SED-ML files are not currently supported.' + : props.omex + ? 'Only COMBINE archives are supported.' + : 'Only CellML files and COMBINE archives are supported.'; + if (props.omex) { vue.nextTick(() => { - issues.value.push({ - type: locApi.EIssueType.ERROR, - description: - fileType === locApi.EFileType.IRRETRIEVABLE_FILE - ? 'The file could not be retrieved.' - : 'Only COMBINE archives are supported.' - }); + issues.value.push({ type: locApi.EIssueType.ERROR, description: issueMessage }); }); } else { addToast({ severity: 'error', summary: 'Opening a file', - detail: - filePath + - '\n\n' + - (fileType === locApi.EFileType.IRRETRIEVABLE_FILE - ? 'The file could not be retrieved.' - : fileType === locApi.EFileType.SEDML_FILE - ? 'SED-ML files are not currently supported.' - : 'Only CellML files and COMBINE archives are supported.'), + detail: `${filePath}\n\n${issueMessage}`, life: TOAST_LIFE }); } electronApi?.fileIssue(filePath); + emit('file', { type: 'issue', filePath, issues: [issueMessage] }); + return null; } @@ -813,6 +823,8 @@ const processFile = async (fileFilePathOrFileContents: string | Uint8Array | Fil electronApi?.fileIssue(filePath); + emit('file', { type: 'issue', filePath, issues: [common.formatMessage(common.formatError(error))] }); + return null; } finally { if (isRemoteFilePath) { From 343235314ba14d79fc4c9c7a5f8f6ca5c91535c5 Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Thu, 16 Apr 2026 10:32:30 +1200 Subject: [PATCH 6/7] Refactored event interfaces and unified event returns. --- src/renderer/index.ts | 49 +++++-- .../views/SimulationExperimentView.vue | 125 ++++++++++-------- 2 files changed, 107 insertions(+), 67 deletions(-) diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 533897ad..ae74f3da 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -23,29 +23,60 @@ export interface IOpenCORSimulationDataValue { unit: string; } -export type OpenCORExternalDataEventType = 'added' | 'issue'; +// External data events. -export interface IOpenCORExternalDataEvent { - type: OpenCORExternalDataEventType; +export interface IOpenCORExternalDataAddedEvent { + type: 'added'; csv: string; issues: string[]; } -export type OpenCORFileEventType = 'opened' | 'closed' | 'issue'; +export interface IOpenCORExternalDataIssueEvent { + type: 'issue'; + csv: string; + issues: string[]; +} + +export type IOpenCORExternalDataEvent = IOpenCORExternalDataAddedEvent | IOpenCORExternalDataIssueEvent; + +// File events. + +export interface IOpenCORFileOpenedEvent { + type: 'opened'; + filePath: string; + issues: string[]; +} + +export interface IOpenCORFileClosedEvent { + type: 'closed'; + filePath: string; + issues: string[]; +} -export interface IOpenCORFileEvent { - type: OpenCORFileEventType; +export interface IOpenCORFileIssueEvent { + type: 'issue'; filePath: string; issues: string[]; } -export type OpenCORSimulationDataEventType = 'updated' | 'issue'; +export type IOpenCORFileEvent = IOpenCORFileOpenedEvent | IOpenCORFileClosedEvent | IOpenCORFileIssueEvent; + +// Simulation data events. + export type OpenCORSimulationData = Record; -export interface IOpenCORSimulationDataEvent { - type: OpenCORSimulationDataEventType; +export interface IOpenCORSimulationDataUpdatedEvent { + type: 'updated'; simulationData: OpenCORSimulationData; issues: string[]; } +export interface IOpenCORSimulationDataIssueEvent { + type: 'issue'; + simulationData: OpenCORSimulationData; + issues: string[]; +} + +export type IOpenCORSimulationDataEvent = IOpenCORSimulationDataUpdatedEvent | IOpenCORSimulationDataIssueEvent; + export { default, default as OpenCOR } from './src/components/OpenCOR.vue'; diff --git a/src/renderer/src/components/views/SimulationExperimentView.vue b/src/renderer/src/components/views/SimulationExperimentView.vue index 1c4c571d..924fb527 100644 --- a/src/renderer/src/components/views/SimulationExperimentView.vue +++ b/src/renderer/src/components/views/SimulationExperimentView.vue @@ -754,19 +754,27 @@ const addExternalData = async ( voiExpression: string | undefined, modelParameters: string[] ): Promise => { - // Make sure that we are in simulation-only mode. - - const res: IOpenCORExternalDataEvent = { - type: 'added', - csv, - issues: [] + const addedEvent = (): IOpenCORExternalDataEvent => { + return { + type: 'added', + csv, + issues: [] + }; + }; + const issueEvent = (issueMessages: string[]): IOpenCORExternalDataEvent => { + return { + type: 'issue', + csv, + issues: issueMessages + }; }; - if (!props.simulationOnly) { - res.type = 'issue'; - res.issues = ['The exposed addExternalData() method is only available in simulation-only mode.']; + // Make sure that we are in simulation-only mode. - return Promise.resolve(res); + if (!props.simulationOnly) { + return Promise.resolve( + issueEvent(['The exposed addExternalData() method is only available in simulation-only mode.']) + ); } // Make sure that we can retrieve the CSV data. @@ -778,27 +786,25 @@ const addExternalData = async ( const response = await fetch(common.corsProxyUrl(csv)); if (!response.ok) { - res.type = 'issue'; - res.issues = [`Could not retrieve the CSV file from ${csv} (status: ${response.status}).`]; - - return Promise.resolve(res); + return Promise.resolve( + issueEvent([`Could not retrieve the CSV file from ${csv} (status: ${response.status}).`]) + ); } csvContents = await response.text(); } catch (error: unknown) { - res.type = 'issue'; - res.issues = [ - `Could not retrieve the CSV file from ${csv} (${common.formatMessage(common.formatError(error), false)}).` - ]; - - return Promise.resolve(res); + return Promise.resolve( + issueEvent([ + `Could not retrieve the CSV file from ${csv} (${common.formatMessage(common.formatError(error), false)}).` + ]) + ); } } else { csvContents = csv; } if (!csvContents) { - return Promise.resolve(res); + return Promise.resolve(addedEvent()); } // Make sure that we haven't already added this CSV data (based on a hash of the CSV contents) to avoid accidentally @@ -807,10 +813,7 @@ const addExternalData = async ( const csvHash = common.xxh64(csvContents); if (addedExternalDataHashes.has(csvHash) || inFlightExternalDataHashes.has(csvHash)) { - res.type = 'issue'; - res.issues = ['The external data has already been added.']; - - return Promise.resolve(res); + return Promise.resolve(issueEvent(['The external data has already been added.'])); } inFlightExternalDataHashes.add(csvHash); @@ -823,40 +826,36 @@ const addExternalData = async ( try { parsedCsv = externalData.parseExternalCsvData(csvContents); } catch (error: unknown) { - res.type = 'issue'; - res.issues.push(common.formatMessage(common.formatError(error))); - - return Promise.resolve(res); + return Promise.resolve(issueEvent([common.formatMessage(common.formatError(error))])); } // Make sure that the number of model parameters matches the number of data columns in the CSV file. if (parsedCsv.headers.length - 1 !== modelParameters.length) { - res.type = 'issue'; - res.issues = [ - `The number of model parameters provided must match the number of data columns in the CSV file (i.e. ${parsedCsv.headers.length - 1}, not ${modelParameters.length}).` - ]; - - return Promise.resolve(res); + return Promise.resolve( + issueEvent([ + `The number of model parameters provided must match the number of data columns in the CSV file (i.e. ${parsedCsv.headers.length - 1}, not ${modelParameters.length}).` + ]) + ); } // Make sure that the model parameters are valid. const trimmedModelParameters = modelParameters.map((modelParameter) => modelParameter?.trim() ?? ''); + const validationIssues: string[] = []; for (let i = 0; i < trimmedModelParameters.length; ++i) { const modelParameter = trimmedModelParameters[i]; if (!modelParameter) { - res.issues.push(`Model parameter #${i + 1} must be a non-empty string.`); + validationIssues.push(`Model parameter #${i + 1} must be a non-empty string.`); } else if (!/^\w+\/\w+$/.test(modelParameter)) { - res.issues.push(`Model parameter #${i + 1} must be of the form '/'.`); + validationIssues.push(`Model parameter #${i + 1} must be of the form '/'.`); } } - if (res.issues.length) { - res.type = 'issue'; - return Promise.resolve(res); + if (validationIssues.length) { + return Promise.resolve(issueEvent(validationIssues)); } // Determine the output IDs that are currently being used so that we can avoid conflicts when adding our external @@ -956,7 +955,7 @@ const addExternalData = async ( addedExternalDataHashes.add(csvHash); - return Promise.resolve(res); + return Promise.resolve(addedEvent()); } finally { inFlightExternalDataHashes.delete(csvHash); } @@ -965,26 +964,26 @@ const addExternalData = async ( // A helper function to retrieve simulation data for one or more model parameters. const simulationData = (modelParameters: string[]): Promise => { - const res: IOpenCORSimulationDataEvent = { - simulationData: common.emptySimulationData(modelParameters), - issues: [] - }; + const simulationDataResult = common.emptySimulationData(modelParameters); if (!props.simulationOnly) { - res.type = 'issue'; - res.issues = ['The exposed simulationData() method is only available in simulation-only mode.']; - - return Promise.resolve(res); + return Promise.resolve({ + type: 'issue', + simulationData: simulationDataResult, + issues: ['The exposed simulationData() method is only available in simulation-only mode.'] + }); } if (!interactiveInstanceTask) { - res.type = 'issue'; - res.issues = ['No SED-ML instance task available.']; - - return Promise.resolve(res); + return Promise.resolve({ + type: 'issue', + simulationData: simulationDataResult, + issues: ['No SED-ML instance task available.'] + }); } const instanceTask = interactiveInstanceTask as locSedApi.SedInstanceTask; + const issueMessages: string[] = []; for (const modelParameter of modelParameters) { const info = locCommon.simulationDataInfo( @@ -993,21 +992,31 @@ const simulationData = (modelParameters: string[]): Promise Date: Thu, 16 Apr 2026 10:36:18 +1200 Subject: [PATCH 7/7] Updated our test apps. ... to account for our new event data structure. --- src/renderer/src/AppWithExternalData.vue | 23 +++++++++++------- src/renderer/src/AppWithSimulationData.vue | 27 +++++++++++++--------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/renderer/src/AppWithExternalData.vue b/src/renderer/src/AppWithExternalData.vue index 8503d232..1fdc3671 100644 --- a/src/renderer/src/AppWithExternalData.vue +++ b/src/renderer/src/AppWithExternalData.vue @@ -113,14 +113,21 @@ const addDataFromFigshareUrl = async (): Promise => { const onExternalData = (event: IOpenCORExternalDataEvent) => { console.log('---[External data]---'); - if (event.issues.length > 0) { - console.log('Issues:'); - - event.issues.forEach((issue: string) => { - console.log(` - ${issue}`); - }); - } else { - console.log('No issues.'); + switch (event.type) { + case 'added': + console.log(`External data added for CSV: ${event.csv}`); + console.log('No issues.'); + + break; + case 'issue': + console.log(`External data issue for CSV: ${event.csv}`); + console.log('Issues:'); + + event.issues.forEach((issue: string) => { + console.log(` - ${issue}`); + }); + + break; } }; diff --git a/src/renderer/src/AppWithSimulationData.vue b/src/renderer/src/AppWithSimulationData.vue index 0013ee15..ab29633b 100644 --- a/src/renderer/src/AppWithSimulationData.vue +++ b/src/renderer/src/AppWithSimulationData.vue @@ -68,19 +68,24 @@ const untrackAllModelParameters = () => { const onSimulationData = (event: IOpenCORSimulationDataEvent) => { console.log('---[Simulation data]---'); - for (const modelParameter of Object.keys(event.simulationData)) { - console.log(`Simulation data for "${modelParameter}":`); - console.log(event.simulationData[modelParameter]); - } + switch (event.type) { + case 'updated': + for (const modelParameter of Object.keys(event.simulationData)) { + console.log(`Simulation data for "${modelParameter}":`); + console.log(event.simulationData[modelParameter]); + } - if (event.issues.length > 0) { - console.log('Issues:'); + console.log('No issues.'); - event.issues.forEach((issue: string) => { - console.log(` - ${issue}`); - }); - } else { - console.log('No issues.'); + break; + case 'issue': + console.log('Simulation data issue:'); + + event.issues.forEach((issue: string) => { + console.log(` - ${issue}`); + }); + + break; } };