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 d3460fc7..265b431c 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" }, @@ -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/index.ts b/src/renderer/index.ts index ff14b0ac..ae74f3da 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]; } @@ -22,16 +23,60 @@ export interface IOpenCORSimulationDataValue { unit: string; } -export interface IOpenCORExternalDataEvent { +// External data events. + +export interface IOpenCORExternalDataAddedEvent { + type: 'added'; + csv: string; + issues: string[]; +} + +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 IOpenCORFileIssueEvent { + type: 'issue'; + filePath: string; + issues: string[]; +} + +export type IOpenCORFileEvent = IOpenCORFileOpenedEvent | IOpenCORFileClosedEvent | IOpenCORFileIssueEvent; + +// Simulation data events. + export type OpenCORSimulationData = Record; -export interface IOpenCORSimulationDataEvent { +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/package.json b/src/renderer/package.json index b7751be4..5d4e04f6 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", @@ -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", 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; } }; diff --git a/src/renderer/src/components/ContentsComponent.vue b/src/renderer/src/components/ContentsComponent.vue index cd57e67e..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 => { @@ -231,6 +237,7 @@ const addExternalData = async ( if (!simulationExperimentViews.length) { return Promise.resolve({ + type: 'issue', csv, issues: ['No simulation experiment view available.'] }); @@ -238,6 +245,7 @@ const addExternalData = async ( return simulationExperimentViews[0].addExternalData(csv, voiExpression, modelParameters).catch((error: unknown) => { return { + type: 'issue', csv, issues: [common.formatError(error)] }; @@ -249,6 +257,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 +267,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..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" /> @@ -161,6 +163,7 @@ const addExternalData = (csv: string, voiExpression: string | undefined, modelPa if (!contents) { emit('externalData', { + type: 'issue', csv, issues: ['No contents available.'] }); @@ -233,6 +236,7 @@ const emitSimulationData = (): void => { if (!contents) { emit('simulationData', { + type: 'issue', simulationData: common.emptySimulationData(trackedSimulationData.value), issues: ['No contents available.'] }); @@ -245,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({ @@ -756,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; } @@ -811,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) { diff --git a/src/renderer/src/components/views/SimulationExperimentView.vue b/src/renderer/src/components/views/SimulationExperimentView.vue index 49c78abd..924fb527 100644 --- a/src/renderer/src/components/views/SimulationExperimentView.vue +++ b/src/renderer/src/components/views/SimulationExperimentView.vue @@ -754,17 +754,27 @@ const addExternalData = async ( voiExpression: string | undefined, modelParameters: string[] ): Promise => { - // Make sure that we are in simulation-only mode. - - const res: IOpenCORExternalDataEvent = { - 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.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. @@ -776,25 +786,25 @@ const addExternalData = async ( const response = await fetch(common.corsProxyUrl(csv)); if (!response.ok) { - 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.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 @@ -803,9 +813,7 @@ const addExternalData = async ( const csvHash = common.xxh64(csvContents); if (addedExternalDataHashes.has(csvHash) || inFlightExternalDataHashes.has(csvHash)) { - 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); @@ -818,37 +826,36 @@ const addExternalData = async ( try { parsedCsv = externalData.parseExternalCsvData(csvContents); } catch (error: unknown) { - 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.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) { - 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 @@ -948,7 +955,7 @@ const addExternalData = async ( addedExternalDataHashes.add(csvHash); - return Promise.resolve(res); + return Promise.resolve(addedEvent()); } finally { inFlightExternalDataHashes.delete(csvHash); } @@ -957,24 +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.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.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( @@ -983,19 +992,31 @@ const simulationData = (modelParameters: string[]): Promise