From 9eebbfa6c09feb29152e3ea827b7fdc65019143a Mon Sep 17 00:00:00 2001 From: tarn Date: Tue, 26 May 2026 11:58:22 +0200 Subject: [PATCH 01/22] datasetStore --- frontend/.gitignore | 2 + frontend/licenses-third-party.js | 14 +- frontend/openapi-ts.config.ts | 28 + frontend/package-lock.json | 479 +++++++++++++++++- frontend/package.json | 4 +- frontend/src/lib/GraphExport.svelte | 2 +- frontend/src/lib/api/apiDatasetUtils.js | 21 - frontend/src/lib/api/backend.js | 26 - frontend/src/lib/api/hey-api.ts | 25 + .../DatasetAndGraphSelection.svelte | 73 +-- .../lib/components/FileSelectButton.svelte | 2 +- .../validity-rules/validityFunctions.js | 10 +- .../svelteflow/svelteFlowWrapper.svelte | 8 +- frontend/src/lib/stores/DatasetStore.ts | 185 +++++++ frontend/src/lib/stores/GraphStore.ts | 173 +++++++ frontend/src/routes/+layout.svelte | 8 +- .../src/routes/DatasetDeleteDialog.svelte | 5 +- frontend/src/routes/ImportDialog.svelte | 18 +- frontend/src/routes/NamespacesDialog.svelte | 8 +- frontend/src/routes/NewGraphDialog.svelte | 23 +- frontend/src/routes/Searchbar.svelte | 2 +- frontend/src/routes/SnapshotDialog.svelte | 18 +- frontend/src/routes/changelog/Changes.svelte | 4 +- .../src/routes/changelog/Navigation.svelte | 23 +- .../mainpage/classEditor/classEditor.svelte | 6 +- .../CustomDiagramButton.svelte | 8 +- .../packageNavigation/DatasetSection.svelte | 5 +- .../packageNavigation/build-nav-object.js | 53 +- .../AddToDatasetDiagramDialog.svelte | 2 +- .../AddToGraphDiagramDialog.svelte | 2 +- .../routes/mainpage/renderingWrapper.svelte | 8 +- .../src/routes/shacl/SHACLUploadDialog.svelte | 2 +- 32 files changed, 1003 insertions(+), 244 deletions(-) create mode 100644 frontend/openapi-ts.config.ts create mode 100644 frontend/src/lib/api/hey-api.ts create mode 100644 frontend/src/lib/stores/DatasetStore.ts create mode 100644 frontend/src/lib/stores/GraphStore.ts diff --git a/frontend/.gitignore b/frontend/.gitignore index e1c5b032..daaadd1d 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -6,6 +6,8 @@ node_modules /.svelte-kit /build +src/lib/api/generated + # OS .DS_Store Thumbs.db diff --git a/frontend/licenses-third-party.js b/frontend/licenses-third-party.js index 1744573c..fae266d7 100644 --- a/frontend/licenses-third-party.js +++ b/frontend/licenses-third-party.js @@ -15,9 +15,9 @@ * */ -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; const __filepath = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filepath); @@ -53,7 +53,7 @@ function generateLicenseContent() { .replace(/\.git$/, ""); packages.push({ - name: pkg.name, + name: pkg.label, version: pkg.version, license, reference, @@ -65,9 +65,9 @@ function generateLicenseContent() { let markdown = "# Third-Party Licenses\n\n"; - for (const pkg of packages.sort((a, b) => a.name.localeCompare(b.name))) { - markdown += `### ${pkg.name}\n`; - markdown += `- **Package:** ${pkg.name}\n`; + for (const pkg of packages.toSorted((a, b) => a.label.localeCompare(b.label))) { + markdown += `### ${pkg.label}\n`; + markdown += `- **Package:** ${pkg.label}\n`; markdown += `- **Version:** ${pkg.version}\n`; markdown += `- **License:** ${pkg.license}\n`; if (pkg.reference) { diff --git a/frontend/openapi-ts.config.ts b/frontend/openapi-ts.config.ts new file mode 100644 index 00000000..50613709 --- /dev/null +++ b/frontend/openapi-ts.config.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineConfig } from "@hey-api/openapi-ts"; + +export default defineConfig({ + input: "http://localhost:8080/v3/api-docs", + output: "src/lib/api/generated", + plugins: [ + { + name: "@hey-api/client-fetch", + runtimeConfigPath: "./src/lib/api/hey-api", + }, + ], +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9527f62c..7b8e8cc8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,6 +31,7 @@ "@eslint/compat": "^2.0.0", "@eslint/js": "^10.0.0", "@faker-js/faker": "^10.0.0", + "@hey-api/openapi-ts": "0.97.1", "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/kit": "^2.5.27", "@sveltejs/vite-plugin-svelte": "^7.0.0", @@ -706,6 +707,120 @@ "node": ">=6" } }, + "node_modules/@hey-api/codegen-core": { + "version": "0.8.1", + "integrity": "sha512-Iciv2vUCJTW9lWM/ROvyZLblmcbYJHPuXfzb1SzeDVVn4xEXu2ilLU1pq3fn+09FZ/Y0P7VyvRE47UDU6om8xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/types": "0.1.4", + "ansi-colors": "4.1.3", + "c12": "3.3.4", + "color-support": "1.1.3" + }, + "engines": { + "node": ">=22.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/json-schema-ref-parser": { + "version": "1.4.2", + "integrity": "sha512-ZhCFSKI2ipZHEbgmtUHdyddvRU3wJ4elgCfYUC7T7hZa4EivSrVflTQf2w+v3TuaYxR1Y2V2kq3otqTttrrK8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "7.1.3", + "@types/json-schema": "7.0.15", + "js-yaml": "4.1.1" + }, + "engines": { + "node": ">=22.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/openapi-ts": { + "version": "0.97.1", + "integrity": "sha512-LksUJeXAqwf6OhcCCr3/B4YjnBs5rqSqjDUKMBvkgp4OhaCQiJrOvntctFxdnugy8jUojP4yi/eJf5xYzcYzCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "0.8.1", + "@hey-api/json-schema-ref-parser": "1.4.2", + "@hey-api/shared": "0.4.3", + "@hey-api/spec-types": "0.2.0", + "@hey-api/types": "0.1.4", + "@lukeed/ms": "2.0.2", + "ansi-colors": "4.1.3", + "color-support": "1.1.3", + "commander": "14.0.3", + "get-tsconfig": "4.14.0" + }, + "bin": { + "openapi-ts": "bin/run.js" + }, + "engines": { + "node": ">=22.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3 || >=6.0.0 || 6.0.1-rc" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/commander": { + "version": "14.0.3", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@hey-api/shared": { + "version": "0.4.3", + "integrity": "sha512-3tHfZNXgGOt+3P3Kq9cvqmZ9i7e3jtrkip1uDpZTX1+hTNboHhYdjxnT8AbrDuvslTaQHoAOlP4/iCDdzd9Jag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "0.8.1", + "@hey-api/json-schema-ref-parser": "1.4.2", + "@hey-api/spec-types": "0.2.0", + "@hey-api/types": "0.1.4", + "ansi-colors": "4.1.3", + "cross-spawn": "7.0.6", + "open": "11.0.0", + "semver": "7.7.4" + }, + "engines": { + "node": ">=22.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/spec-types": { + "version": "0.2.0", + "integrity": "sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/types": "0.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/types": { + "version": "0.1.4", + "integrity": "sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==", + "dev": true, + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.2", "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", @@ -831,6 +946,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@lezer/common": { "version": "1.5.2", "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", @@ -852,6 +973,15 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", @@ -2523,6 +2653,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", @@ -2545,6 +2684,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/aria-query": { "version": "5.3.1", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", @@ -2751,6 +2896,94 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c12": { + "version": "3.3.4", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "5.0.0", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/confbox": { + "version": "0.2.4", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/c12/node_modules/pkg-types": { + "version": "2.3.1", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "5.0.0", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", @@ -2873,6 +3106,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-support": { + "version": "1.1.3", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/commander": { "version": "7.2.0", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", @@ -3477,6 +3719,52 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.5.0", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/delaunator": { "version": "5.1.0", "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", @@ -3493,6 +3781,12 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.5", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", @@ -3519,6 +3813,18 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", @@ -3952,6 +4258,12 @@ "node": ">=12.0.0" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", @@ -4159,6 +4471,15 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "3.2.0", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "dev": true, + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob": { "version": "8.1.0", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", @@ -4395,6 +4716,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-expression": { "version": "4.0.0", "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", @@ -4418,7 +4754,7 @@ "node_modules/is-extglob": { "version": "2.1.1", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4435,7 +4771,7 @@ "node_modules/is-glob": { "version": "4.0.3", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -4444,6 +4780,36 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", @@ -4480,6 +4846,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", @@ -4515,6 +4896,18 @@ "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "29.1.1", "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", @@ -5149,6 +5542,12 @@ ], "license": "MIT" }, + "node_modules/ohash": { + "version": "2.0.11", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", @@ -5157,6 +5556,26 @@ "wrappy": "1" } }, + "node_modules/open": { + "version": "11.0.0", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", @@ -5255,6 +5674,12 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", @@ -5421,6 +5846,18 @@ "dev": true, "license": "MIT" }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", @@ -5665,6 +6102,16 @@ "node": ">=6" } }, + "node_modules/rc9": { + "version": "3.0.1", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, "node_modules/require-directory": { "version": "2.1.1", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", @@ -5759,6 +6206,18 @@ "points-on-path": "^0.2.1" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/runed": { "version": "0.35.1", "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", @@ -6742,6 +7201,22 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", diff --git a/frontend/package.json b/frontend/package.json index 89f59f8d..ae5e6325 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,12 +10,14 @@ "clean-install": "rm -rf node_modules && npm install", "test": "vitest --run", "licenses:generate": "node licenses-third-party.js generate", - "licenses:check": "node licenses-third-party.js check" + "licenses:check": "node licenses-third-party.js check", + "api:generate": "openapi-ts" }, "devDependencies": { "@eslint/compat": "^2.0.0", "@eslint/js": "^10.0.0", "@faker-js/faker": "^10.0.0", + "@hey-api/openapi-ts": "0.97.1", "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/kit": "^2.5.27", "@sveltejs/vite-plugin-svelte": "^7.0.0", diff --git a/frontend/src/lib/GraphExport.svelte b/frontend/src/lib/GraphExport.svelte index f1adffe5..001683fb 100644 --- a/frontend/src/lib/GraphExport.svelte +++ b/frontend/src/lib/GraphExport.svelte @@ -280,7 +280,7 @@ bind:value={selectedMediaType} > {#each supportedMediaTypes as mediaType} - + {/each} diff --git a/frontend/src/lib/api/apiDatasetUtils.js b/frontend/src/lib/api/apiDatasetUtils.js index 847ca615..44ade9da 100644 --- a/frontend/src/lib/api/apiDatasetUtils.js +++ b/frontend/src/lib/api/apiDatasetUtils.js @@ -20,11 +20,6 @@ import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); -export async function isReadOnly(datasetName) { - const res = await bec.isReadOnly(datasetName); - return await res.json(); -} - export async function getNamespaces(datasetName) { if (!datasetName) { return []; @@ -32,19 +27,3 @@ export async function getNamespaces(datasetName) { const res = await bec.getNamespaces(datasetName); return await res.json(); } - -export async function getDatasetNames() { - const res = await bec.getDatasetNames(); - let datasetNames = await res.json(); - let readOnlyDatasets = []; - let modifiableDatasets = []; - - for (const dataset of datasetNames) { - if (await isReadOnly(dataset)) { - readOnlyDatasets.push(dataset); - } else { - modifiableDatasets.push(dataset); - } - } - return { modifiable: modifiableDatasets, readonly: readOnlyDatasets }; -} diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js index 470bdf78..ee574b03 100644 --- a/frontend/src/lib/api/backend.js +++ b/frontend/src/lib/api/backend.js @@ -37,15 +37,6 @@ export class BackendConnection { }); } - async getDatasetNames() { - const url = `${PUBLIC_BACKEND_URL}/datasets`; - return fetch(url, { - method: "GET", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - async getGraphNames(datasetName) { const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs`; return fetch(url, { @@ -56,14 +47,6 @@ export class BackendConnection { }); } - async deleteDataset(datasetName) { - const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}`; - return fetch(url, { - method: "DELETE", - credentials: "include", - }); - } - async getClassInfo(datasetName, graphURI, classUUID) { const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}`; return fetch(url, { @@ -343,15 +326,6 @@ export class BackendConnection { }); } - async isReadOnly(datasetName) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/readonly`; - return await fetch(url, { - method: "GET", - mode: "cors", - credentials: "include", - }); - } - async enableEditing(datasetName) { let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/readonly`; return await fetch(url, { diff --git a/frontend/src/lib/api/hey-api.ts b/frontend/src/lib/api/hey-api.ts new file mode 100644 index 00000000..5f295d60 --- /dev/null +++ b/frontend/src/lib/api/hey-api.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + + +import { CreateClientConfig } from "./generated/client"; + +export const createClientConfig: CreateClientConfig = (config) => ({ + ...config, + baseUrl: "http://localhost:8080/", + credentials: "include", +}); \ No newline at end of file diff --git a/frontend/src/lib/components/DatasetAndGraphSelection.svelte b/frontend/src/lib/components/DatasetAndGraphSelection.svelte index 3ce5dd22..e9a48942 100644 --- a/frontend/src/lib/components/DatasetAndGraphSelection.svelte +++ b/frontend/src/lib/components/DatasetAndGraphSelection.svelte @@ -18,10 +18,9 @@ import { onMount } from "svelte"; import { v4 as uuidv4 } from "uuid"; - import { isReadOnly } from "$lib/api/apiDatasetUtils.js"; - import { BackendConnection } from "$lib/api/backend.js"; import SelectEditControl from "$lib/components/SelectEditControl.svelte"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; + import { datasetStore } from "$lib/stores/DatasetStore.ts"; + import { graphStore } from "$lib/stores/GraphStore.ts"; let { dataset = $bindable(), @@ -29,15 +28,12 @@ lockedDatasetName, lockedGraphUri, allowSelectionOfReadonlyDatasets = true, - displayAsCard = true, + displayAsCard = true } = $props(); - const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - const datasetSelectId = `datasetSelect-${uuidv4()}`; const graphSelectId = `graphSelect-${uuidv4()}`; - let datasets = $state([]); let graphNames = $state([]); const datasetLocked = $derived(lockedDatasetName !== undefined); @@ -45,69 +41,46 @@ const graphSelectDisabled = $derived(graphLocked || !dataset); - $effect(() => { + $effect(async () => { if (datasetLocked) return; if (!dataset) { graph = graphLocked ? lockedGraphUri : null; graphNames = []; return; } - loadGraphsFor(dataset); + + await graphStore.load(dataset) + graphNames = graphStore.getGraphs(dataset); + const valid = graphNames.some(graphName => getUri(graphName) === graph); + if (!valid && !graphLocked) { + graph = null; + } }); onMount(async () => { + await datasetStore.load(); if (datasetLocked) dataset = lockedDatasetName; if (graphLocked) graph = lockedGraphUri; - await loadDatasets(); - if (dataset) { - await loadGraphsFor(dataset); - } else { - graphNames = []; - } - }); - - function getUri(graph) { - return (!graph.prefix ? "" : graph.prefix) + graph.suffix; - } - - async function loadDatasets() { - const res = await bec.getDatasetNames(); - const datasetNames = await res.json(); - const newDatasets = datasetNames.map(name => ({ - label: name, - readonly: false, - })); - if (!allowSelectionOfReadonlyDatasets) { - for (const dataset of newDatasets) { - dataset.readonly = await isReadOnly(dataset.label); - } - } - datasets = newDatasets; - if (!datasetLocked && dataset && !allowSelectionOfReadonlyDatasets) { - const selectedDataset = newDatasets.find( - option => option.label === dataset, + const selectedDataset = $datasetStore.data.find( + option => option.label === dataset ); if (!selectedDataset || selectedDataset.readonly) { dataset = null; } } - } - async function loadGraphsFor(dataset) { - if (!dataset) { + if (dataset) { + await graphStore.load(dataset); + graphNames = graphStore.getGraphs(dataset); + } else { graphNames = []; - return; } + }); - const res = await bec.getGraphNames(dataset); - graphNames = await res.json(); - - const valid = graphNames.some(graphName => getUri(graphName) === graph); - if (!valid && !graphLocked) { - graph = null; - } + function getUri(graph) { + return (!graph.prefix ? "" : graph.prefix) + graph.suffix; } @@ -120,13 +93,13 @@ !allowSelectionOfReadonlyDatasets && dataset.readonly} getOptionValue={dataset => dataset.label} getOptionLabel={dataset => dataset.label + (dataset.readonly ? " (readonly)" : "")} - disabled={datasetLocked || datasets.length === 0} + disabled={datasetLocked || ($datasetStore.data?.length ?? 0) === 0} placeholder="Select dataset" onchange={() => (graph = null)} /> diff --git a/frontend/src/lib/components/FileSelectButton.svelte b/frontend/src/lib/components/FileSelectButton.svelte index 42e69972..95d95de6 100644 --- a/frontend/src/lib/components/FileSelectButton.svelte +++ b/frontend/src/lib/components/FileSelectButton.svelte @@ -39,7 +39,7 @@

- {file ? file.name : "no file selected"} + {file ? file.label : "no file selected"}

diff --git a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js index 6d02707a..2d825bb2 100644 --- a/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js +++ b/frontend/src/lib/models/reactive/validity-rules/validityFunctions.js @@ -79,7 +79,7 @@ export function isValidDiagramName(diagramName, compareDiagrams) { violations.push("must not be empty"); } - if (compareDiagrams?.some(d => d.name === diagramName)) { + if (compareDiagrams?.some(d => d.label === diagramName)) { violations.push("must be unique"); } @@ -93,12 +93,12 @@ export function isInvalidAssociationLabel(association, associations) { : (associations?.values ?? []); if (violations.length === 0) { if ( - assocList.filter( + assocList.some( a => a.label.value === association?.label?.value && a.namespace.value === association.namespace?.value && a !== association, - ).length > 0 + ) ) { violations.push("must be unique"); } else if ( @@ -125,12 +125,12 @@ export function isInvalidInverseAssociationLabel(association, getClassByUuid) { const assocList = targetClassDto?.associationPairs?.map(pair => pair) ?? []; if (violations.length === 0) { if ( - assocList.filter( + assocList.some( a => a.from.label === association?.inverse?.label?.value && a.from.prefix === association.inverse?.namespace?.value && a.from.uuid !== association.inverse?.uuid?.value, - ).length > 0 + ) ) { violations.push("must be unique"); } else if ( diff --git a/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte b/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte index 5d84abe0..5d487c7c 100644 --- a/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte +++ b/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte @@ -35,6 +35,7 @@ editorState, forceReloadTrigger, } from "$lib/sharedState.svelte.js"; + import { datasetStore } from "$lib/stores/DatasetStore.ts"; import AssociationEdge from "./components/AssociationEdge.svelte"; import ClassNode from "./components/ClassNode.svelte"; @@ -106,7 +107,7 @@ forceReloadTrigger.subscribe(); editorState.selectedDataset.subscribe(); const dataset = editorState.selectedDataset.getValue(); - isDatasetReadOnly = dataset ? await isReadOnly(dataset) : false; + isDatasetReadOnly = dataset ? await datasetStore.isReadOnly(dataset) : false; }); $effect(() => { @@ -289,11 +290,6 @@ }); } - async function isReadOnly(datasetName) { - const res = await bec.isReadOnly(datasetName); - return await res.json(); - } - function handleNodeClick(nodeClickEvent) { closeContextMenus(); if (nodeClickEvent.node.type === "class") { diff --git a/frontend/src/lib/stores/DatasetStore.ts b/frontend/src/lib/stores/DatasetStore.ts new file mode 100644 index 00000000..f5ed5e5f --- /dev/null +++ b/frontend/src/lib/stores/DatasetStore.ts @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { writable, get, derived } from "svelte/store"; + +import { listDatasets, deleteDataset, isReadOnly as fetchIsReadOnly } from "../api/generated"; + +export type DatasetInfo = { + label: string; + readonly: boolean | null; + prefixes: Set; +}; + +type DatasetsState = { + data: DatasetInfo[] | null; + fetchedAt: number | null; + pending: Promise | null; + error: unknown; +}; + +export const datasetStore = createDatasetStore(); + +function createDatasetStore() { + const store = writable( + { + data: null, + fetchedAt: null, + pending: null, + error: null, + }, + () => { + const state = get(store); + + if (state.data === null && !state.pending) { + void load(); + } + }, + ); + + const { subscribe, update } = store; + + async function load(force = false) { + const state = get(store); + + if (!force && state.data !== null) return; + if (state.pending !== null) return state.pending; + + const promise = (async () => { + try { + const { data, error } = await listDatasets(); + if (error) { + update(s => ({ + ...s, + pending: null, + error, + })); + return; + } + + const datasetNames = data ?? []; + const previous = get(store).data ?? []; + const byName = new Map(previous.map(d => [d.label, d])); + + const baseData: DatasetInfo[] = datasetNames.map(name => { + const prev = byName.get(name); + return { + label: name, + readonly: null, + prefixes: prev?.prefixes ?? new Set(), + }; + }); + + const readonlyPairs = await Promise.all( + baseData.map(async d => { + const { data: roData, error: roError } = + await fetchIsReadOnly({ + path: { datasetName: d.label }, + }); + return { + name: d.label, + readonly: roError ? null : (roData ?? null), + }; + }), + ); + + const readonlyByName = new Map( + readonlyPairs.map(x => [x.name, x.readonly]), + ); + + const nextData: DatasetInfo[] = baseData.map(d => ({ + ...d, + readonly: readonlyByName.get(d.label) ?? null, + })); + + update(s => ({ + ...s, + pending: null, + error: null, + data: nextData, + fetchedAt: Date.now(), + })); + } catch (err) { + update(s => ({ + ...s, + pending: null, + error: err, + })); + } + })(); + + update(s => ({ ...s, pending: promise })); + return promise; + } + + async function remove(datasetName: string) { + const { error } = await deleteDataset({ path: { datasetName } }); + if (error) return { error }; + + update(s => ({ + ...s, + data: s.data?.filter(d => d.label !== datasetName) ?? null, + })); + return { error: null }; + } + + function invalidate() { + void load(true); + } + + async function isReadOnly(datasetName: string, force = false) { + if (force) { + const { data, error } = await fetchIsReadOnly({ + path: { datasetName }, + }); + if (error) { + return; + } + const readonly = data ?? null; + update(s => ({ + ...s, + data: + s.data?.map(d => + d.label === datasetName ? { ...d, readonly: readonly } : d, + ) ?? null, + })); + } + + return derived(datasetStore, $store => { + return $store.data?.find(d => d.label === datasetName)?.readonly ?? null; + }); + } + + function updateReadonly(datasetName: string, readonly: boolean) { + update(s => ({ + ...s, + data: + s.data?.map(d => + d.label === datasetName ? { ...d, readonly } : d, + ) ?? null, + })); + } + + return { + subscribe, + load, + remove, + isReadOnly, + updateReadonly, + invalidate, + }; +} diff --git a/frontend/src/lib/stores/GraphStore.ts b/frontend/src/lib/stores/GraphStore.ts new file mode 100644 index 00000000..18eca186 --- /dev/null +++ b/frontend/src/lib/stores/GraphStore.ts @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { writable, get } from "svelte/store"; + +import { listGraphs, deleteGraph, Uri } from "../api/generated"; + +type DatasetGraphsState = { + data: Uri[] | null; + fetchedAt: number | null; + pending: Promise | null; + error: unknown; +}; + +type GraphsState = { + graphs: Map; +}; + +export const graphStore = createGraphsStore(); + +function createEmptyDatasetState(): DatasetGraphsState { + return { + data: null, + fetchedAt: null, + pending: null, + error: null, + }; +} + +function createGraphsStore() { + const store = writable({ + graphs: new Map(), + }); + + const { subscribe, update } = store; + + function getDatasetState(state: GraphsState, datasetName: string): DatasetGraphsState { + return state.graphs.get(datasetName) ?? createEmptyDatasetState(); + } + + function setDatasetState( + state: GraphsState, + datasetName: string, + next: DatasetGraphsState, + ): GraphsState { + const byDataset = new Map(state.graphs); + byDataset.set(datasetName, next); + return { ...state, graphs: byDataset }; + } + + async function load(datasetName: string, force = false) { + if (!datasetName) return; + + const state = get(store); + const dsState = getDatasetState(state, datasetName); + + if (!force && dsState.data !== null) return; + if (dsState.pending !== null) return dsState.pending; + + const promise = (async () => { + try { + const { data, error } = await listGraphs({ + path: { datasetName }, + }); + + if (error) { + update((s) => + setDatasetState(s, datasetName, { + ...getDatasetState(s, datasetName), + pending: null, + error, + }), + ); + return; + } + + update((s) => + setDatasetState(s, datasetName, { + data: data ?? [], + fetchedAt: Date.now(), + pending: null, + error: null, + }), + ); + } catch (err) { + update((s) => + setDatasetState(s, datasetName, { + ...getDatasetState(s, datasetName), + pending: null, + error: err, + }), + ); + } + })(); + + update((s) => + setDatasetState(s, datasetName, { + ...getDatasetState(s, datasetName), + pending: promise, + }), + ); + + return promise; + } + + function getGraphs(datasetName: string): Uri[] | null { + return getDatasetState(get(store), datasetName).data; + } + + function getStateForDataset(datasetName: string): DatasetGraphsState { + return getDatasetState(get(store), datasetName); + } + + async function remove(datasetName: string, graphURI: string) { + const { error } = await deleteGraph({ + path: { datasetName, graphURI }, + }); + + if (error) return { error }; + + update((s) => { + const dsState = getDatasetState(s, datasetName); + const nextData = + dsState.data?.filter((g) => { + const uri = `${g.prefix ?? ""}${g.suffix}`; + return uri !== graphURI; + }) ?? null; + + return setDatasetState(s, datasetName, { + ...dsState, + data: nextData, + }); + }); + + return { error: null }; + } + + function invalidateDataset(datasetName: string) { + update((s) => { + const byDataset = new Map(s.graphs); + byDataset.delete(datasetName); + return { ...s, byDataset }; + }); + } + + function invalidateAll() { + update(() => ({ graphs: new Map() })); + } + + return { + subscribe, + load, + getGraphs, + getStateForDataset, + remove, + invalidateDataset, + invalidateAll, + }; +} \ No newline at end of file diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index f22cf74d..be88d171 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -52,6 +52,7 @@ import { goto } from "$app/navigation"; import { page } from "$app/state"; + import { datasetStore } from "$lib/stores/DatasetStore.ts"; /** @type {{children?: import("svelte").Snippet}} */ let { children } = $props(); @@ -76,7 +77,7 @@ forceReloadTrigger.subscribe(); await fetchUndoRedo(); isDatasetReadOnly = selectedDataset - ? await isReadOnly(selectedDataset) + ? await datasetStore.isReadOnly(selectedDataset) : false; }); @@ -123,11 +124,6 @@ canRedo = await fetchCanRedo(); } - async function isReadOnly(datasetName) { - const res = await bec.isReadOnly(datasetName); - return await res.json(); - } - async function reload() { await fetchUndoRedo(); editorState.selectedDataset.trigger(); diff --git a/frontend/src/routes/DatasetDeleteDialog.svelte b/frontend/src/routes/DatasetDeleteDialog.svelte index 0c85e40e..4582fc93 100644 --- a/frontend/src/routes/DatasetDeleteDialog.svelte +++ b/frontend/src/routes/DatasetDeleteDialog.svelte @@ -26,6 +26,7 @@ forceReloadTrigger, editorState, } from "$lib/sharedState.svelte.js"; + import { datasetStore } from "$lib/stores/DatasetStore.ts"; let { showDialog = $bindable(), datasetName } = $props(); @@ -51,8 +52,8 @@ async function deleteDataset() { try { if (datasetName) { - const res = await bec.deleteDataset(datasetName); - if (res && res.ok === false) { + const res = await datasetStore.remove(datasetName); + if (res.error) { toastStore.error( "Delete failed", `Could not delete dataset "${datasetName}".`, diff --git a/frontend/src/routes/ImportDialog.svelte b/frontend/src/routes/ImportDialog.svelte index 3a330afd..96ed17d8 100644 --- a/frontend/src/routes/ImportDialog.svelte +++ b/frontend/src/routes/ImportDialog.svelte @@ -20,11 +20,11 @@ import { Fa } from "svelte-fa"; import { v4 as uuidv4 } from "uuid"; - import { getDatasetNames } from "$lib/api/apiDatasetUtils.js"; import ButtonControl from "$lib/components/ButtonControl.svelte"; import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; + import { datasetStore } from "$lib/stores/DatasetStore.ts"; import { supportedRDFMediaTypes } from "$lib/utils/fileUtils"; import { @@ -65,9 +65,14 @@ datasetNameUserInput = lockedDatasetName ?? editorState.selectedDataset.getValue(); - const datasetNames = await getDatasetNames(); - modifiableDatasets = datasetNames.modifiable; - readOnlyDatasets = datasetNames.readonly; + await datasetStore.load(); + for (const dataset of $datasetStore.data) { + if (dataset.readonly) { + readOnlyDatasets.push(dataset); + } else { + modifiableDatasets.push(dataset); + } + } } function onClose() { @@ -188,7 +193,7 @@ ? "" : ensureGraphNamespaceUri( fileEntry.graphUri, - fileEntry.file.name, + fileEntry.file.label, ), ); }); @@ -288,6 +293,7 @@ try { const res = await putFiles(filesLocal, datasetNameUserInputLocal); await parseResponse(res, datasetNameUserInputLocal); + await datasetStore.load(true); } catch (e) { console.log("failed to insert data:"); console.log(e); @@ -421,7 +427,7 @@ class="border-border bg-window-background focus:border-blue ring-none w-full rounded border-2 p-2 text-sm outline-none" type="text" value={fileEntry.isZip - ? fileEntry.file.name + ? fileEntry.file.label : fileEntry.graphUri} disabled={fileEntry.isZip} oninput={event => diff --git a/frontend/src/routes/NamespacesDialog.svelte b/frontend/src/routes/NamespacesDialog.svelte index bd545c15..c90401b3 100644 --- a/frontend/src/routes/NamespacesDialog.svelte +++ b/frontend/src/routes/NamespacesDialog.svelte @@ -38,6 +38,7 @@ editorState, forceReloadTrigger, } from "$lib/sharedState.svelte.js"; + import { datasetStore } from "$lib/stores/DatasetStore.ts"; let { showDialog = $bindable() } = $props(); @@ -52,7 +53,7 @@ async function onOpen() { datasetName = editorState.selectedDataset.getValue(); if (!datasetName) return; - readonly = await isReadOnly(datasetName); + readonly = await datasetStore.isReadOnly(datasetName); await loadNamespaces(datasetName); } @@ -72,11 +73,6 @@ ); } - async function isReadOnly(datasetNameLocal) { - const res = await bec.isReadOnly(datasetNameLocal); - return await res.json(); - } - async function saveNamespaces() { // Colon normalization: add trailing ":" if missing namespaces.values.forEach(namespace => { diff --git a/frontend/src/routes/NewGraphDialog.svelte b/frontend/src/routes/NewGraphDialog.svelte index fa651d09..8d981c15 100644 --- a/frontend/src/routes/NewGraphDialog.svelte +++ b/frontend/src/routes/NewGraphDialog.svelte @@ -18,12 +18,12 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/AddToGraphDiagramDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/AddToGraphDiagramDialog.svelte index 41104087..8f7afa6c 100644 --- a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/AddToGraphDiagramDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/AddToGraphDiagramDialog.svelte @@ -113,7 +113,7 @@ placeholder={diagramList.length > 0 ? "Select diagram" : "No diagrams available"} - getOptionLabel={diagram => diagram.name} + getOptionLabel={diagram => diagram.label} /> diff --git a/frontend/src/routes/mainpage/renderingWrapper.svelte b/frontend/src/routes/mainpage/renderingWrapper.svelte index 22f92e8d..66948ccf 100644 --- a/frontend/src/routes/mainpage/renderingWrapper.svelte +++ b/frontend/src/routes/mainpage/renderingWrapper.svelte @@ -33,6 +33,7 @@ forceReloadTrigger, DiagramType, } from "$lib/sharedState.svelte.js"; + import { datasetStore } from "$lib/stores/DatasetStore.ts"; import FilterViewDialog from "../FilterViewDialog.svelte"; @@ -63,7 +64,7 @@ forceReloadTrigger.subscribe(); editorState.selectedDataset.subscribe(); const dataset = editorState.selectedDataset.getValue(); - isDatasetReadOnly = dataset ? await isReadOnly(dataset) : false; + isDatasetReadOnly = datasetStore.isReadOnly(dataset); }); $effect(async () => { @@ -216,11 +217,6 @@ }); } - async function isReadOnly(datasetName) { - const res = await bec.isReadOnly(datasetName); - return await res.json(); - } - function handleResetView() { if (renderingFormat === MERMAID_FORMAT) { mermaidWrapper.resetTransform(); diff --git a/frontend/src/routes/shacl/SHACLUploadDialog.svelte b/frontend/src/routes/shacl/SHACLUploadDialog.svelte index 6719d10b..1a6e3390 100644 --- a/frontend/src/routes/shacl/SHACLUploadDialog.svelte +++ b/frontend/src/routes/shacl/SHACLUploadDialog.svelte @@ -134,7 +134,7 @@

{#if file} - {file.name} + {file.label} {:else} no file selected {/if} From 4c16d137892782a7403d52d99e9595f8e182c6ee Mon Sep 17 00:00:00 2001 From: tarn Date: Tue, 26 May 2026 16:20:16 +0200 Subject: [PATCH 02/22] packageStore --- .../datasets/DatasetRESTController.java | 5 +- .../graphs/AllPackagesRESTController.java | 83 ++++----- .../org/rdfarchitect/api/dto/DatasetDTO.java | 20 +- .../services/select/ListDatasetsUseCase.java | 7 +- .../services/select/QueryDatasetService.java | 16 +- frontend/src/lib/GraphExport.svelte | 17 +- frontend/src/lib/api/backend.js | 58 ------ .../DatasetAndGraphSelection.svelte | 10 +- frontend/src/lib/stores/DatasetStore.ts | 148 +++++++-------- .../{GraphStore.ts => GraphURIStore.ts} | 32 ++-- frontend/src/lib/stores/PackageStore.ts | 172 ++++++++++++++++++ frontend/src/routes/+layout.svelte | 2 +- .../src/routes/DatasetDeleteDialog.svelte | 13 +- frontend/src/routes/ImportDialog.svelte | 4 +- frontend/src/routes/NamespacesDialog.svelte | 13 +- frontend/src/routes/NewClassDialog.svelte | 25 +-- frontend/src/routes/NewGraphDialog.svelte | 8 +- frontend/src/routes/NewPackageDialog.svelte | 16 +- frontend/src/routes/SnapshotDialog.svelte | 7 +- .../src/routes/changelog/Navigation.svelte | 6 +- .../src/routes/layout/menu-bar/Edit.svelte | 61 +++---- .../mainpage/classEditor/classEditor.svelte | 3 +- .../classEditor/fetch-class-editor-context.js | 16 +- .../mainpage/packageEditorDialog.svelte | 4 +- .../packageNavigation/DatasetSection.svelte | 3 +- .../packageNavigation/build-nav-object.js | 47 ++--- .../CustomDatasetDiagramDialog.svelte | 10 +- .../customDiagramDialogUtils.js | 11 +- .../OntologyDialog.svelte | 4 +- 29 files changed, 445 insertions(+), 376 deletions(-) rename frontend/src/lib/api/apiDatasetUtils.js => backend/src/main/java/org/rdfarchitect/api/dto/DatasetDTO.java (64%) rename frontend/src/lib/stores/{GraphStore.ts => GraphURIStore.ts} (85%) create mode 100644 frontend/src/lib/stores/PackageStore.ts diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/DatasetRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/DatasetRESTController.java index 097df960..1332e1e6 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/DatasetRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/DatasetRESTController.java @@ -24,6 +24,7 @@ import lombok.RequiredArgsConstructor; import org.rdfarchitect.api.controller.Response; +import org.rdfarchitect.api.dto.DatasetDTO; import org.rdfarchitect.services.select.ListDatasetsUseCase; import org.rdfarchitect.services.update.dataset.DeleteDatasetUseCase; import org.slf4j.Logger; @@ -50,11 +51,11 @@ public class DatasetRESTController { @Operation( summary = "List datasets", - description = "Lists all non-snapshots datasets", + description = "Lists all non-snapshots datasets including their prefixes and read-only status.", tags = {"dataset"}, responses = {@ApiResponse(responseCode = "200")}) @GetMapping - public List listDatasets( + public List listDatasets( @Parameter(description = "The name/url of the inquirer.") @RequestHeader( value = HttpHeaders.ORIGIN, diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/AllPackagesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/AllPackagesRESTController.java index f7b4ed14..e4237ffa 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/AllPackagesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/AllPackagesRESTController.java @@ -19,13 +19,12 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; - +import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; - import org.rdfarchitect.api.dto.packages.PackageDTO; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.services.ExpandURIUseCase; @@ -43,9 +42,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.List; -import java.util.UUID; - @RestController @RequestMapping("api/datasets/{datasetName}/graphs/{graphURI}/packages") @RequiredArgsConstructor @@ -58,43 +54,44 @@ public class AllPackagesRESTController { private final ListInternalPackagesUseCase listInternalPackagesUseCase; private final ListExternalPackagesUseCase listExternalPackagesUseCase; - /** Record for response to frontend, contains two lists for internal and external packages. */ + /** + * Record for response to frontend, contains two lists for internal and external packages. + */ public record ListPackagesResponse( - List internalPackageList, List externalPackageList) {} + List internalPackageList, List externalPackageList) { + } @Operation( summary = "list packages", description = "Get two lists of packages: internal and external packages.", tags = {"graph"}, responses = { - @ApiResponse( - responseCode = "200", - content = - @Content( - mediaType = "application/json", - array = - @ArraySchema( - schema = - @Schema( - implementation = - ListPackagesResponse - .class)))) + @ApiResponse( + responseCode = "200", + content = + @Content( + mediaType = "application/json", + schema = + @Schema( + implementation = + ListPackagesResponse + .class))) }) @GetMapping public ListPackagesResponse listPackages( @Parameter(description = "The name/url of the inquirer.") - @RequestHeader( - value = HttpHeaders.ORIGIN, - required = false, - defaultValue = "unknown") - String originURL, + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, @Parameter(description = "The literal name of the dataset.") @PathVariable - String datasetName, + String datasetName, @Parameter( - description = - "The url encoded uri of the graph, or \"default\" to access the default graph.") - @PathVariable - String graphURI) { + description = + "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI) { logger.info( "Received GET request: \"/api/datasets/{{}}/graphs/{{}}/packages\" from \"{}\".", datasetName, @@ -127,22 +124,22 @@ public ListPackagesResponse listPackages( @PostMapping public UUID addPackage( @Parameter(description = "The name/url of the inquirer.") - @RequestHeader( - value = HttpHeaders.ORIGIN, - required = false, - defaultValue = "unknown") - String originURL, + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, @Parameter(description = "The literal name of the dataset.") @PathVariable - String datasetName, + String datasetName, @Parameter( - description = - "The url encoded uri of the graph, or \"default\" to access the default graph.") - @PathVariable - String graphURI, + description = + "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI, @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "DTO for the package to be created") - @RequestBody - PackageDTO packageDTO) { + description = "DTO for the package to be created") + @RequestBody + PackageDTO packageDTO) { logger.info( "Received POST request: \"/api/datasets/{{}}/graphs/{{}}/packages\" from \"{}\".", diff --git a/frontend/src/lib/api/apiDatasetUtils.js b/backend/src/main/java/org/rdfarchitect/api/dto/DatasetDTO.java similarity index 64% rename from frontend/src/lib/api/apiDatasetUtils.js rename to backend/src/main/java/org/rdfarchitect/api/dto/DatasetDTO.java index 44ade9da..e855f7f3 100644 --- a/frontend/src/lib/api/apiDatasetUtils.js +++ b/backend/src/main/java/org/rdfarchitect/api/dto/DatasetDTO.java @@ -15,15 +15,17 @@ * */ -import { BackendConnection } from "$lib/api/backend.js"; -import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; +package org.rdfarchitect.api.dto; -const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); +import lombok.AllArgsConstructor; +import lombok.Data; +import org.rdfarchitect.models.cim.data.dto.CIMPrefixPair; +import java.util.List; -export async function getNamespaces(datasetName) { - if (!datasetName) { - return []; - } - const res = await bec.getNamespaces(datasetName); - return await res.json(); +@Data +@AllArgsConstructor +public class DatasetDTO { + private String name; + private boolean readonly; + private List prefixes; } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/ListDatasetsUseCase.java b/backend/src/main/java/org/rdfarchitect/services/select/ListDatasetsUseCase.java index 9b264f49..642d5365 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/ListDatasetsUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/ListDatasetsUseCase.java @@ -17,9 +17,14 @@ package org.rdfarchitect.services.select; +import org.rdfarchitect.api.dto.DatasetDTO; import java.util.List; public interface ListDatasetsUseCase { - List listDatasets(); + /** + * List all datasets including their prefixes and read-only status. Snapshots are excluded from the list. + * @return List of dataset objects + */ + List listDatasets(); } diff --git a/backend/src/main/java/org/rdfarchitect/services/select/QueryDatasetService.java b/backend/src/main/java/org/rdfarchitect/services/select/QueryDatasetService.java index f6925328..ab508242 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/QueryDatasetService.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/QueryDatasetService.java @@ -26,6 +26,7 @@ import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFDataMgr; import org.apache.jena.riot.RDFFormat; +import org.rdfarchitect.api.dto.DatasetDTO; import org.rdfarchitect.database.DatabasePort; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.models.cim.data.dto.CIMPrefixPair; @@ -119,7 +120,18 @@ public String listFormattedPrefixes(String datasetName, String format) { } @Override - public List listDatasets() { - return databasePort.listDatasets(); + public List listDatasets() { + var result = new ArrayList(); + for (var datasetName : databasePort.listDatasets()) { + var readonly = databasePort.isReadOnly(datasetName); + var prefixMapping = databasePort.getPrefixMapping(datasetName); + + var prefixPairs = new ArrayList(); + for (var prefix : prefixMapping.getNsPrefixMap().entrySet()) { + prefixPairs.add(new CIMPrefixPair(prefix.getKey() + ":", prefix.getValue())); + } + result.add(new DatasetDTO(datasetName, readonly, prefixPairs)); + } + return result; } } diff --git a/frontend/src/lib/GraphExport.svelte b/frontend/src/lib/GraphExport.svelte index 001683fb..188aef22 100644 --- a/frontend/src/lib/GraphExport.svelte +++ b/frontend/src/lib/GraphExport.svelte @@ -20,7 +20,6 @@ import { onMount } from "svelte"; import { Fa } from "svelte-fa"; - import { getNamespaces } from "$lib/api/apiDatasetUtils.js"; import { BackendConnection } from "$lib/api/backend.js"; import { DropdownMenu } from "$lib/components/bitsui/dropdown/index"; import DatasetAndGraphSelection from "$lib/components/DatasetAndGraphSelection.svelte"; @@ -32,6 +31,7 @@ import { saveFile, supportedRDFMediaTypes } from "$lib/utils/fileUtils.ts"; import { editorState } from "../lib/sharedState.svelte.js"; + import { datasetStore } from "../lib/stores/DatasetStore.ts"; let { showDialog = $bindable(), @@ -51,7 +51,9 @@ let ontology = $state(); let generatedOntologyEntries = $state([]); - let namespaces = $state([]); + let namespaces = $derived( + $datasetStore.data?.find(d => d.label === selectedDatasetName)?.prefixes ?? new Set() + ); let hasOntology = $derived(!!ontology); // Derived state for checkbox @@ -69,14 +71,6 @@ !selectedDatasetName || !graphURI || !selectedMediaType), ); - $effect(async () => { - if (selectedDatasetName) { - namespaces = await getNamespaces(selectedDatasetName); - } else { - namespaces = []; - } - }); - $effect(async () => { if (selectedDatasetName && graphURI) { let ontologyJSON = await getOntology(selectedDatasetName, graphURI); @@ -110,9 +104,6 @@ selectedDatasetName = lockedDatasetName ?? editorState.selectedDataset.getValue(); graphURI = lockedGraphUri ?? editorState.selectedGraph.getValue(); - if (selectedDatasetName) { - namespaces = await getNamespaces(selectedDatasetName); - } const saved = userSettings.get("defaultExportFormat", null); selectedMediaType = saved ? (supportedMediaTypes.find(m => m.mimeType === saved) ?? diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js index ee574b03..c57b7c87 100644 --- a/frontend/src/lib/api/backend.js +++ b/frontend/src/lib/api/backend.js @@ -37,16 +37,6 @@ export class BackendConnection { }); } - async getGraphNames(datasetName) { - const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs`; - return fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - async getClassInfo(datasetName, graphURI, classUUID) { const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}`; return fetch(url, { @@ -68,16 +58,6 @@ export class BackendConnection { }); } - async getPackages(datasetName, graphURI) { - const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/packages`; - return fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - async getPackage(datasetName, graphURI, packageUUID) { const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/packages/${encodeURIComponent(packageUUID)}`; return fetch(url, { @@ -191,14 +171,6 @@ export class BackendConnection { credentials: "include", }); } - async deleteClass(datasetName, graphURI, classUUID) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}`; - return await fetch(url, { - method: "DELETE", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } async deleteResources(datasetName, graphURI, deleteRequests) { let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/delete-requests`; @@ -230,26 +202,6 @@ export class BackendConnection { }); } - async getNamespaces(datasetName) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/namespaces`; - return await fetch(url, { - method: "GET", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async replaceNamespaces(datasetName, namespaces) { - const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/namespaces`; - return fetch(url, { - method: "PUT", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(namespaces), - credentials: "include", - }); - } - async getSearchResults(query, body) { let url = `${PUBLIC_BACKEND_URL}/search?query=${encodeURIComponent(query)}`; return await fetch(url, { @@ -450,16 +402,6 @@ export class BackendConnection { }); } - async deleteOntology(datasetName, graphURI) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/ontology`; - return await fetch(url, { - method: "DELETE", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - async generateOntologyEntries(datasetName, graphURI) { let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/ontology/generate`; return await fetch(url, { diff --git a/frontend/src/lib/components/DatasetAndGraphSelection.svelte b/frontend/src/lib/components/DatasetAndGraphSelection.svelte index e9a48942..afc12388 100644 --- a/frontend/src/lib/components/DatasetAndGraphSelection.svelte +++ b/frontend/src/lib/components/DatasetAndGraphSelection.svelte @@ -20,7 +20,7 @@ import SelectEditControl from "$lib/components/SelectEditControl.svelte"; import { datasetStore } from "$lib/stores/DatasetStore.ts"; - import { graphStore } from "$lib/stores/GraphStore.ts"; + import { graphURIStore } from "$lib/stores/GraphURIStore.ts"; let { dataset = $bindable(), @@ -49,8 +49,8 @@ return; } - await graphStore.load(dataset) - graphNames = graphStore.getGraphs(dataset); + await graphURIStore.load(dataset) + graphNames = graphURIStore.getGraphURIs(dataset); const valid = graphNames.some(graphName => getUri(graphName) === graph); if (!valid && !graphLocked) { graph = null; @@ -72,8 +72,8 @@ } if (dataset) { - await graphStore.load(dataset); - graphNames = graphStore.getGraphs(dataset); + await graphURIStore.load(dataset); + graphNames = graphURIStore.getGraphURIs(dataset); } else { graphNames = []; } diff --git a/frontend/src/lib/stores/DatasetStore.ts b/frontend/src/lib/stores/DatasetStore.ts index f5ed5e5f..7b0e9435 100644 --- a/frontend/src/lib/stores/DatasetStore.ts +++ b/frontend/src/lib/stores/DatasetStore.ts @@ -15,14 +15,19 @@ * */ -import { writable, get, derived } from "svelte/store"; +import { writable, get } from "svelte/store"; -import { listDatasets, deleteDataset, isReadOnly as fetchIsReadOnly } from "../api/generated"; +import { + listDatasets, + deleteDataset, + CimPrefixPair, + replaceNamespaces, +} from "../api/generated"; export type DatasetInfo = { label: string; readonly: boolean | null; - prefixes: Set; + prefixes: CimPrefixPair[]; }; type DatasetsState = { @@ -35,23 +40,27 @@ type DatasetsState = { export const datasetStore = createDatasetStore(); function createDatasetStore() { - const store = writable( - { - data: null, - fetchedAt: null, - pending: null, - error: null, - }, - () => { - const state = get(store); - - if (state.data === null && !state.pending) { - void load(); - } - }, - ); - - const { subscribe, update } = store; + const store = writable({ + data: null, + fetchedAt: null, + pending: null, + error: null, + }); // ← kein Start-Callback + + const { subscribe: baseSubscribe, update } = store; + + let initialLoadTriggered = false; + + function subscribe( + run: (value: DatasetsState) => void, + invalidate?: () => void, + ) { + if (!initialLoadTriggered) { + initialLoadTriggered = true; + void load(); + } + return baseSubscribe(run, invalidate); + } async function load(force = false) { const state = get(store); @@ -71,39 +80,12 @@ function createDatasetStore() { return; } - const datasetNames = data ?? []; - const previous = get(store).data ?? []; - const byName = new Map(previous.map(d => [d.label, d])); - - const baseData: DatasetInfo[] = datasetNames.map(name => { - const prev = byName.get(name); - return { - label: name, - readonly: null, - prefixes: prev?.prefixes ?? new Set(), - }; - }); - - const readonlyPairs = await Promise.all( - baseData.map(async d => { - const { data: roData, error: roError } = - await fetchIsReadOnly({ - path: { datasetName: d.label }, - }); - return { - name: d.label, - readonly: roError ? null : (roData ?? null), - }; - }), - ); - - const readonlyByName = new Map( - readonlyPairs.map(x => [x.name, x.readonly]), - ); - - const nextData: DatasetInfo[] = baseData.map(d => ({ - ...d, - readonly: readonlyByName.get(d.label) ?? null, + const datasets = data ?? []; + + const nextData: DatasetInfo[] = datasets.map(dataset => ({ + label: dataset.name ?? "", + readonly: dataset.readonly ?? null, + prefixes: dataset.prefixes ?? [], })); update(s => ({ @@ -141,27 +123,49 @@ function createDatasetStore() { void load(true); } - async function isReadOnly(datasetName: string, force = false) { - if (force) { - const { data, error } = await fetchIsReadOnly({ - path: { datasetName }, - }); - if (error) { - return; - } - const readonly = data ?? null; - update(s => ({ - ...s, - data: - s.data?.map(d => - d.label === datasetName ? { ...d, readonly: readonly } : d, - ) ?? null, - })); + function isReadOnly(datasetName: string): boolean | null { + const dataset = get(store).data?.find(d => d.label === datasetName); + if (!dataset) { + console.warn( + `isReadOnly called before dataset "${datasetName}" was loaded`, + ); + return null; } + return dataset.readonly; + } - return derived(datasetStore, $store => { - return $store.data?.find(d => d.label === datasetName)?.readonly ?? null; + function getNamespaces(datasetName: string): CimPrefixPair[] { + const dataset = get(store).data?.find(d => d.label === datasetName); + if (!dataset) { + console.warn(`getNamespaces called before dataset "${datasetName}" was loaded`); + return []; + } + return dataset.prefixes; + } + + async function saveNamespaces( + datasetName: string, + namespaces: CimPrefixPair[], + ) { + const { error } = await replaceNamespaces({ + path: { datasetName }, + body: namespaces, }); + + if (error) return { error }; + + // Update prefixes in store so getNamespaces() returns fresh data + update(s => ({ + ...s, + data: + s.data?.map(d => + d.label === datasetName + ? { ...d, prefixes: namespaces } + : d, + ) ?? null, + })); + + return { error: null }; } function updateReadonly(datasetName: string, readonly: boolean) { @@ -181,5 +185,7 @@ function createDatasetStore() { isReadOnly, updateReadonly, invalidate, + getNamespaces, + saveNamespaces, }; } diff --git a/frontend/src/lib/stores/GraphStore.ts b/frontend/src/lib/stores/GraphURIStore.ts similarity index 85% rename from frontend/src/lib/stores/GraphStore.ts rename to frontend/src/lib/stores/GraphURIStore.ts index 18eca186..75988daf 100644 --- a/frontend/src/lib/stores/GraphStore.ts +++ b/frontend/src/lib/stores/GraphURIStore.ts @@ -19,20 +19,20 @@ import { writable, get } from "svelte/store"; import { listGraphs, deleteGraph, Uri } from "../api/generated"; -type DatasetGraphsState = { +type GraphURIState = { data: Uri[] | null; fetchedAt: number | null; pending: Promise | null; error: unknown; }; -type GraphsState = { - graphs: Map; +type DatasetState = { + graphs: Map; }; -export const graphStore = createGraphsStore(); +export const graphURIStore = createGrapURIStore(); -function createEmptyDatasetState(): DatasetGraphsState { +function createEmptyDatasetState(): GraphURIState { return { data: null, fetchedAt: null, @@ -41,22 +41,22 @@ function createEmptyDatasetState(): DatasetGraphsState { }; } -function createGraphsStore() { - const store = writable({ +function createGrapURIStore() { + const store = writable({ graphs: new Map(), }); const { subscribe, update } = store; - function getDatasetState(state: GraphsState, datasetName: string): DatasetGraphsState { + function getDatasetState(state: DatasetState, datasetName: string): GraphURIState { return state.graphs.get(datasetName) ?? createEmptyDatasetState(); } function setDatasetState( - state: GraphsState, + state: DatasetState, datasetName: string, - next: DatasetGraphsState, - ): GraphsState { + next: GraphURIState, + ): DatasetState { const byDataset = new Map(state.graphs); byDataset.set(datasetName, next); return { ...state, graphs: byDataset }; @@ -117,15 +117,15 @@ function createGraphsStore() { return promise; } - function getGraphs(datasetName: string): Uri[] | null { + function getGraphURIs(datasetName: string): Uri[] | null { return getDatasetState(get(store), datasetName).data; } - function getStateForDataset(datasetName: string): DatasetGraphsState { + function getStateForDataset(datasetName: string): GraphURIState { return getDatasetState(get(store), datasetName); } - async function remove(datasetName: string, graphURI: string) { + async function removeGraph(datasetName: string, graphURI: string) { const { error } = await deleteGraph({ path: { datasetName, graphURI }, }); @@ -164,9 +164,9 @@ function createGraphsStore() { return { subscribe, load, - getGraphs, + getGraphURIs, getStateForDataset, - remove, + remove: removeGraph, invalidateDataset, invalidateAll, }; diff --git a/frontend/src/lib/stores/PackageStore.ts b/frontend/src/lib/stores/PackageStore.ts new file mode 100644 index 00000000..e4de77b9 --- /dev/null +++ b/frontend/src/lib/stores/PackageStore.ts @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { writable, get } from "svelte/store"; + +import { listPackages, PackageDto } from "../api/generated"; + +type PackageListInfo = { + internal: PackageDto[]; + external: PackageDto[]; +}; + +type GraphPackageState = { + data: PackageListInfo | null; + fetchedAt: number | null; + pending: Promise | null; + error: unknown; +}; + +type PackagesState = { + byGraph: Map; +}; + +type GraphKey = `${string}::${string}`; + +export const packageStore = createPackageStore(); + +function makeKey(datasetName: string, graphURI: string): GraphKey { + return `${datasetName}::${graphURI}`; +} + +function createEmptyGraphState(): GraphPackageState { + return { + data: null, + fetchedAt: null, + pending: null, + error: null, + }; +} + +function createPackageStore() { + const store = writable({ + byGraph: new Map(), + }); + + const { subscribe, update } = store; + + function getGraphState(state: PackagesState, key: GraphKey): GraphPackageState { + return state.byGraph.get(key) ?? createEmptyGraphState(); + } + + function setGraphState( + state: PackagesState, + key: GraphKey, + next: GraphPackageState, + ): PackagesState { + const byGraph = new Map(state.byGraph); + byGraph.set(key, next); + return { ...state, byGraph }; + } + + async function load(datasetName: string, graphURI: string, force = false) { + if (!datasetName || !graphURI) return; + + const key = makeKey(datasetName, graphURI); + const state = get(store); + const graphState = getGraphState(state, key); + + if (!force && graphState.data !== null) return; + if (graphState.pending !== null) return graphState.pending; + + const promise = (async () => { + try { + const { data, error } = await listPackages({ + path: { datasetName, graphURI }, + }); + + if (error) { + update((s) => + setGraphState(s, key, { + ...getGraphState(s, key), + pending: null, + error, + }), + ); + return; + } + + update((s) => + setGraphState(s, key, { + data: { + internal: data?.internalPackageList ?? [], + external: data?.externalPackageList ?? [], + }, + fetchedAt: Date.now(), + pending: null, + error: null, + }), + ); + } catch (err) { + update((s) => + setGraphState(s, key, { + ...getGraphState(s, key), + pending: null, + error: err, + }), + ); + } + })(); + + update((s) => + setGraphState(s, key, { + ...getGraphState(s, key), + pending: promise, + }), + ); + + return promise; + } + + function getPackages(datasetName: string, graphURI: string): PackageListInfo | null { + return getGraphState(get(store), makeKey(datasetName, graphURI)).data; + } + + function invalidateGraph(datasetName: string, graphURI: string) { + const key = makeKey(datasetName, graphURI); + update((s) => { + const byGraph = new Map(s.byGraph); + byGraph.delete(key); + return { ...s, byGraph }; + }); + } + + function invalidateDataset(datasetName: string) { + update((s) => { + const byGraph = new Map(s.byGraph); + for (const key of byGraph.keys()) { + if (key.startsWith(`${datasetName}::`)) { + byGraph.delete(key); + } + } + return { ...s, byGraph }; + }); + } + + function invalidateAll() { + update(() => ({ byGraph: new Map() })); + } + + return { + subscribe, + load, + getPackages, + invalidateGraph, + invalidateDataset, + invalidateAll, + }; +} \ No newline at end of file diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index be88d171..8afd7fff 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -39,6 +39,7 @@ import { eventStack } from "$lib/eventhandling/closeEventManager.svelte.js"; import { shortcutStore } from "$lib/eventhandling/shortcutStore.svelte.js"; import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; + import { datasetStore } from "$lib/stores/DatasetStore.ts"; import { editorState, @@ -52,7 +53,6 @@ import { goto } from "$app/navigation"; import { page } from "$app/state"; - import { datasetStore } from "$lib/stores/DatasetStore.ts"; /** @type {{children?: import("svelte").Snippet}} */ let { children } = $props(); diff --git a/frontend/src/routes/DatasetDeleteDialog.svelte b/frontend/src/routes/DatasetDeleteDialog.svelte index 4582fc93..d8244c3a 100644 --- a/frontend/src/routes/DatasetDeleteDialog.svelte +++ b/frontend/src/routes/DatasetDeleteDialog.svelte @@ -18,8 +18,6 @@ From 35803c807bfcf57312de4f910a449dfd3bd5d8b3 Mon Sep 17 00:00:00 2001 From: tarn Date: Thu, 11 Jun 2026 12:14:01 +0200 Subject: [PATCH 04/22] apply stores --- .../GraphBulkContentRESTController.java | 5 +- .../umladapted/data/CIMClassUMLAdapted.java | 3 + frontend/licenses-third-party.js | 4 +- frontend/package-lock.json | 4 +- frontend/src/lib/GraphExport.svelte | 3 +- frontend/src/lib/actions/editingActions.js | 74 - frontend/src/lib/api/backend.js | 199 --- frontend/src/lib/api/hey-api.ts | 5 +- .../DatasetAndGraphSelection.svelte | 14 +- .../svelteflow/svelteFlowWrapper.svelte | 4 +- frontend/src/lib/stores/ClassStore.ts | 1201 +++++++++++++++++ frontend/src/lib/stores/DatasetStore.ts | 132 +- frontend/src/lib/stores/DiagramStore.ts | 856 ++++++++++++ frontend/src/lib/stores/GraphStore.ts | 314 +++++ frontend/src/lib/stores/GraphURIStore.ts | 173 --- frontend/src/lib/stores/PackageStore.ts | 316 ++++- frontend/src/lib/stores/StoreLogging.ts | 37 + .../src/lib/stores/VersionControlStore.ts | 119 ++ frontend/src/routes/+layout.svelte | 27 +- .../src/routes/DatasetDeleteDialog.svelte | 21 +- frontend/src/routes/GraphDeleteDialog.svelte | 63 +- frontend/src/routes/ImportDialog.svelte | 131 +- frontend/src/routes/NamespacesDialog.svelte | 28 +- frontend/src/routes/NewClassDialog.svelte | 93 +- frontend/src/routes/NewGraphDialog.svelte | 68 +- frontend/src/routes/NewPackageDialog.svelte | 80 +- frontend/src/routes/SnapshotDialog.svelte | 4 +- frontend/src/routes/changelog/Changes.svelte | 3 +- .../src/routes/changelog/Navigation.svelte | 12 +- .../src/routes/layout/menu-bar/Edit.svelte | 83 +- .../mainpage/classEditor/classEditor.svelte | 55 +- .../components/ClassEditorButtons.svelte | 27 +- .../AssociationEditorDialog.svelte | 16 +- .../save-association-to-backend.js | 58 +- .../attributes/save-attribute-to-backend.js | 26 +- .../save-enum-entry-to-backend.js | 23 +- .../classEditor/fetch-class-editor-context.js | 8 +- .../mainpage/packageEditorDialog.svelte | 54 +- .../packageNavigation/DatasetSection.svelte | 22 +- .../packageNavigation/GraphSection.svelte | 33 +- .../PackageDeleteDialog.svelte | 54 +- .../packageNavigation/build-nav-object.js | 43 +- .../AddToDatasetDiagramDialog.svelte | 44 +- .../AddToGraphDiagramDialog.svelte | 45 +- .../CustomDatasetDiagramDialog.svelte | 53 +- .../CustomDiagramDeleteDialog.svelte | 73 +- .../CustomGraphDiagramDialog.svelte | 49 +- .../RemoveFromDiagramDialog.svelte | 48 +- .../customDiagramDialogUtils.js | 13 +- 49 files changed, 3421 insertions(+), 1399 deletions(-) delete mode 100644 frontend/src/lib/actions/editingActions.js create mode 100644 frontend/src/lib/stores/ClassStore.ts create mode 100644 frontend/src/lib/stores/DiagramStore.ts create mode 100644 frontend/src/lib/stores/GraphStore.ts delete mode 100644 frontend/src/lib/stores/GraphURIStore.ts create mode 100644 frontend/src/lib/stores/StoreLogging.ts create mode 100644 frontend/src/lib/stores/VersionControlStore.ts diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/GraphBulkContentRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/GraphBulkContentRESTController.java index 3930fd50..06a6953f 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/GraphBulkContentRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/GraphBulkContentRESTController.java @@ -34,6 +34,7 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -65,10 +66,10 @@ public ResponseEntity replaceGraphs( String originURL, @Parameter(description = "The literal name of the dataset.") @PathVariable String datasetName, - @Parameter(description = "The files containing the graph data") @RequestParam("files") + @Parameter(description = "The files containing the graph data") @RequestPart("files") List files, @Parameter(description = "Optional graph URIs, one per file. Defaults to file names.") - @RequestParam(value = "graphUris", required = false) + @RequestPart(value = "graphUris", required = false) List graphUris) { logger.info( "Received PUT request: \"/api/datasets/{{}}/graphs/content\" from \"{}\".", diff --git a/backend/src/main/java/org/rdfarchitect/models/cim/umladapted/data/CIMClassUMLAdapted.java b/backend/src/main/java/org/rdfarchitect/models/cim/umladapted/data/CIMClassUMLAdapted.java index 1e9f2333..de92b9b7 100644 --- a/backend/src/main/java/org/rdfarchitect/models/cim/umladapted/data/CIMClassUMLAdapted.java +++ b/backend/src/main/java/org/rdfarchitect/models/cim/umladapted/data/CIMClassUMLAdapted.java @@ -66,5 +66,8 @@ public void nullEmptyLists() { if (associationPairs.isEmpty()) { associationPairs = null; } + if (enumEntries.isEmpty()) { + enumEntries = null; + } } } diff --git a/frontend/licenses-third-party.js b/frontend/licenses-third-party.js index fae266d7..05338351 100644 --- a/frontend/licenses-third-party.js +++ b/frontend/licenses-third-party.js @@ -65,7 +65,9 @@ function generateLicenseContent() { let markdown = "# Third-Party Licenses\n\n"; - for (const pkg of packages.toSorted((a, b) => a.label.localeCompare(b.label))) { + for (const pkg of packages.toSorted((a, b) => + a.label.localeCompare(b.label), + )) { markdown += `### ${pkg.label}\n`; markdown += `- **Package:** ${pkg.label}\n`; markdown += `- **Version:** ${pkg.version}\n`; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7b8e8cc8..7cd1fb5e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4754,7 +4754,7 @@ "node_modules/is-extglob": { "version": "2.1.1", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4771,7 +4771,7 @@ "node_modules/is-glob": { "version": "4.0.3", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" diff --git a/frontend/src/lib/GraphExport.svelte b/frontend/src/lib/GraphExport.svelte index 188aef22..34f114f4 100644 --- a/frontend/src/lib/GraphExport.svelte +++ b/frontend/src/lib/GraphExport.svelte @@ -52,7 +52,8 @@ let generatedOntologyEntries = $state([]); let namespaces = $derived( - $datasetStore.data?.find(d => d.label === selectedDatasetName)?.prefixes ?? new Set() + $datasetStore.data?.find(d => d.label === selectedDatasetName) + ?.prefixes ?? new Set(), ); let hasOntology = $derived(!!ontology); diff --git a/frontend/src/lib/actions/editingActions.js b/frontend/src/lib/actions/editingActions.js deleted file mode 100644 index bfe714e5..00000000 --- a/frontend/src/lib/actions/editingActions.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2024-2026 SOPTIM AG - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -/** - * Shared actions for toggling the read-only state of a dataset. - * - * Each function performs the API call, surfaces a success or error toast, - * and returns `true` when the write succeeded. Reactive state updates that - * are local to a call site (refreshing local `readonly` flags, triggering - * editor state, etc.) remain the caller's responsibility. - */ - -import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; -import { datasetStore } from "$lib/stores/DatasetStore.ts"; - -/** - * Make the dataset editable. Toasts on success and failure. - * - * @param {string} datasetName - * @returns {Promise} `true` when the dataset is now editable. - */ -export async function enableEditing(datasetName) { - if (!datasetName) return false; - const { error } = datasetStore.updateReadonly(datasetName, false); - if (error) { - toastStore.error( - "Could not enable editing", - `Dataset "${datasetName}" remains read-only.`, - ); - return false; - } - toastStore.success( - "Editing enabled", - `Dataset "${datasetName}" is now editable.`, - ); - return true; -} - -/** - * Mark the dataset read-only. Toasts on success and failure. - * - * @param {string} datasetName - * @returns {Promise} `true` when the dataset is now read-only. - */ -export async function disableEditing(datasetName) { - if (!datasetName) return false; - const { error } = await datasetStore.updateReadonly(datasetName, true); - if (error) { - toastStore.error( - "Could not disable editing", - `Dataset "${datasetName}" remains editable.`, - ); - return false; - } - toastStore.success( - "Editing disabled", - `Dataset "${datasetName}" is now read-only.`, - ); - return true; -} diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js index ef0116e3..639feb6c 100644 --- a/frontend/src/lib/api/backend.js +++ b/frontend/src/lib/api/backend.js @@ -37,26 +37,6 @@ export class BackendConnection { }); } - async getClassInfo(datasetName, graphURI, classUUID) { - const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}`; - return fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async getClasses(datasetName, graphURI, includeExternalClasses = false) { - const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes?includeExternalClasses=${includeExternalClasses}`; - return fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - async getXSDPrimitives() { const url = `${PUBLIC_BACKEND_URL}/primitiveDatatypes`; return fetch(url, { @@ -97,51 +77,6 @@ export class BackendConnection { }); } - async putAttribute(datasetName, graphURI, classUUID, attribute) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}/attributes/${encodeURIComponent(attribute.uuid)}`; - return await fetch(url, { - method: "PUT", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(attribute), - credentials: "include", - }); - } - - async postAttribute(datasetName, graphURI, classUUID, attribute) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}/attributes`; - return await fetch(url, { - method: "POST", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(attribute), - credentials: "include", - }); - } - - async putAssociationPair( - datasetName, - graphURI, - classUUID, - associationPair, - ) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}/associations/${encodeURIComponent(associationPair.from.uuid)}`; - return await fetch(url, { - method: "PUT", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(associationPair), - credentials: "include", - }); - } - - async postAssociationPair(datasetName, graphURI, classUUID, association) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}/associations`; - return await fetch(url, { - method: "POST", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(association), - credentials: "include", - }); - } - async getDeleteRelation(datasetName, graphURI, resourceUuid) { let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/uuid/${encodeURIComponent(resourceUuid)}/deletion-impact`; return await fetch(url, { @@ -161,26 +96,6 @@ export class BackendConnection { }); } - async postEnumEntry(datasetName, graphURI, classUUID, enumEntry) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}/enumentries`; - return await fetch(url, { - method: "POST", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(enumEntry), - credentials: "include", - }); - } - - async putEnumEntry(datasetName, graphURI, classUUID, enumEntry) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}/enumentries/${encodeURIComponent(enumEntry.uuid)}`; - return await fetch(url, { - method: "PUT", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(enumEntry), - credentials: "include", - }); - } - async getSearchResults(query, body) { let url = `${PUBLIC_BACKEND_URL}/search?query=${encodeURIComponent(query)}`; return await fetch(url, { @@ -257,24 +172,6 @@ export class BackendConnection { }); } - async enableEditing(datasetName) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/readonly`; - return await fetch(url, { - method: "PUT", - mode: "cors", - credentials: "include", - }); - } - - async disableEditing(datasetName) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/readonly`; - return await fetch(url, { - method: "DELETE", - mode: "cors", - credentials: "include", - }); - } - async getChangelog(datasetName, graphURI) { let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/changes`; return await fetch(url, { @@ -297,17 +194,6 @@ export class BackendConnection { }); } - async putPackage(datasetName, graphURI, pack) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/packages/${encodeURIComponent(pack.uuid)}`; - return await fetch(url, { - method: "PUT", - headers: new Headers({ "Content-Type": "application/json" }), - mode: "cors", - body: JSON.stringify(pack), - credentials: "include", - }); - } - async updateClassPositions( datasetName, graphURI, @@ -453,91 +339,6 @@ export class BackendConnection { }); } - async putCustomDiagram(datasetName, graphURI, diagramId, newDiagram) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/diagrams/${encodeURIComponent(diagramId)}`; - return await fetch(url, { - method: "PUT", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(newDiagram), - credentials: "include", - }); - } - - async putCustomDatasetDiagram(datasetName, diagramId, newDiagram) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/diagrams/${encodeURIComponent(diagramId)}`; - return await fetch(url, { - method: "PUT", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(newDiagram), - credentials: "include", - }); - } - - async addToCustomGraphDiagram(datasetName, graphURI, diagramId, classes) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/diagrams/${encodeURIComponent(diagramId)}/classes`; - return await fetch(url, { - method: "POST", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(classes), - credentials: "include", - }); - } - - async addToCustomDatasetDiagram(datasetName, diagramId, classes) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/diagrams/${encodeURIComponent(diagramId)}/classes`; - return await fetch(url, { - method: "POST", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(classes), - credentials: "include", - }); - } - - async removeFromCustomGraphDiagram( - datasetName, - graphURI, - diagramId, - classId, - ) { - console.log( - `Removing class ${classId} from diagram ${diagramId} in dataset ${datasetName}`, - ); - - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/diagrams/${encodeURIComponent(diagramId)}/classes/${encodeURIComponent(classId)}`; - return await fetch(url, { - method: "DELETE", - credentials: "include", - }); - } - - async removeFromCustomDatasetDiagram(datasetName, diagramId, classId) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/diagrams/${encodeURIComponent(diagramId)}/classes/${encodeURIComponent(classId)}`; - return await fetch(url, { - method: "DELETE", - credentials: "include", - }); - } - - async deleteCustomDatasetDiagram(datasetName, diagramId) { - const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/diagrams/${encodeURIComponent(diagramId)}`; - return await fetch(url, { - method: "DELETE", - credentials: "include", - }); - } - - async deleteCustomGraphDiagram(datasetName, graphUri, diagramId) { - const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphUri)}/diagrams/${encodeURIComponent(diagramId)}`; - return await fetch(url, { - method: "DELETE", - credentials: "include", - }); - } - async resetSession() { const url = `${PUBLIC_BACKEND_URL}/session`; return fetch(url, { diff --git a/frontend/src/lib/api/hey-api.ts b/frontend/src/lib/api/hey-api.ts index 5f295d60..e946980c 100644 --- a/frontend/src/lib/api/hey-api.ts +++ b/frontend/src/lib/api/hey-api.ts @@ -15,11 +15,10 @@ * */ - import { CreateClientConfig } from "./generated/client"; -export const createClientConfig: CreateClientConfig = (config) => ({ +export const createClientConfig: CreateClientConfig = config => ({ ...config, baseUrl: "http://localhost:8080/", credentials: "include", -}); \ No newline at end of file +}); diff --git a/frontend/src/lib/components/DatasetAndGraphSelection.svelte b/frontend/src/lib/components/DatasetAndGraphSelection.svelte index afc12388..c5309e6b 100644 --- a/frontend/src/lib/components/DatasetAndGraphSelection.svelte +++ b/frontend/src/lib/components/DatasetAndGraphSelection.svelte @@ -20,7 +20,7 @@ import SelectEditControl from "$lib/components/SelectEditControl.svelte"; import { datasetStore } from "$lib/stores/DatasetStore.ts"; - import { graphURIStore } from "$lib/stores/GraphURIStore.ts"; + import { graphStore } from "$lib/stores/GraphStore.ts"; let { dataset = $bindable(), @@ -28,7 +28,7 @@ lockedDatasetName, lockedGraphUri, allowSelectionOfReadonlyDatasets = true, - displayAsCard = true + displayAsCard = true, } = $props(); const datasetSelectId = `datasetSelect-${uuidv4()}`; @@ -49,8 +49,8 @@ return; } - await graphURIStore.load(dataset) - graphNames = graphURIStore.getGraphURIs(dataset); + await graphStore.load(dataset); + graphNames = graphStore.getGraphURIs(dataset); const valid = graphNames.some(graphName => getUri(graphName) === graph); if (!valid && !graphLocked) { graph = null; @@ -64,7 +64,7 @@ if (!datasetLocked && dataset && !allowSelectionOfReadonlyDatasets) { const selectedDataset = $datasetStore.data.find( - option => option.label === dataset + option => option.label === dataset, ); if (!selectedDataset || selectedDataset.readonly) { dataset = null; @@ -72,8 +72,8 @@ } if (dataset) { - await graphURIStore.load(dataset); - graphNames = graphURIStore.getGraphURIs(dataset); + await graphStore.load(dataset); + graphNames = graphStore.getGraphURIs(dataset); } else { graphNames = []; } diff --git a/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte b/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte index 5d487c7c..ec7b6c52 100644 --- a/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte +++ b/frontend/src/lib/rendering/svelteflow/svelteFlowWrapper.svelte @@ -107,7 +107,9 @@ forceReloadTrigger.subscribe(); editorState.selectedDataset.subscribe(); const dataset = editorState.selectedDataset.getValue(); - isDatasetReadOnly = dataset ? await datasetStore.isReadOnly(dataset) : false; + isDatasetReadOnly = dataset + ? await datasetStore.isReadOnly(dataset) + : false; }); $effect(() => { diff --git a/frontend/src/lib/stores/ClassStore.ts b/frontend/src/lib/stores/ClassStore.ts new file mode 100644 index 00000000..3361592d --- /dev/null +++ b/frontend/src/lib/stores/ClassStore.ts @@ -0,0 +1,1201 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { writable, get } from "svelte/store"; + +import { describeError } from "./StoreLogging"; +import { + // class list & details + getClassList, + getClassInformation, + // class-level mutations + addClass, + replaceClass, + deleteClass, + extendClass, + copyClass, + // attribute mutations + createAttribute, + replaceAttribute, + // association mutations + createAssociation, + replaceAssociation, + replaceAllAssociations, + // enum entry mutations + createEnumEntry, + replaceEnumEntry, + // generic resource deletion + deleteResources, + // types + AddNewClassRequest, + AssociationPairDto, + AttributeDto, + ClassDto, + ClassUmlAdaptedDto, + CopyClassRequestDto, + CopyClassResponseDto, + EnumEntryDto, + ResourceDeleteRequest, + AssociationUuids, +} from "../api/generated"; +import { toastStore } from "../eventhandling/toastStore.svelte.js"; + +// Two cache slots per graph: the list endpoint may be called with or without +// external classes; keep both so toggling the flag does not invalidate the +// other one. +type Variant = "all" | "internalOnly"; + +type VariantState = { + data: ClassUmlAdaptedDto[] | null; + fetchedAt: number | null; + pending: Promise | null; + error: unknown; +}; + +type GraphClassState = { + all: VariantState; + internalOnly: VariantState; +}; + +type ClassesState = { + byGraph: Map; +}; + +type GraphKey = `${string}::${string}`; + +type Result = { error: unknown; data?: T }; + +const LOG_PREFIX = "[classStore]"; + +export const classStore = createClassStore(); + +function makeKey(datasetName: string, graphURI: string): GraphKey { + return `${datasetName}::${graphURI}`; +} + +function createEmptyVariantState(): VariantState { + return { data: null, fetchedAt: null, pending: null, error: null }; +} + +function createEmptyGraphState(): GraphClassState { + return { + all: createEmptyVariantState(), + internalOnly: createEmptyVariantState(), + }; +} + +// ----- Helpers ----- + +function getId(c: ClassUmlAdaptedDto): string | undefined { + return c.uuid; +} + +// The list endpoint returns classes without details: `attributes`, +// `enumEntries` and `associationPairs` are omitted. The detail endpoint fills +// them. Treat the presence of any array as "details are loaded". +function hasDetails(c: ClassUmlAdaptedDto | null | undefined): boolean { + if (!c) return false; + return ( + Array.isArray(c.attributes) || + Array.isArray(c.enumEntries) || + Array.isArray(c.associationPairs) + ); +} + +function findInVariant( + list: ClassUmlAdaptedDto[] | null, + uuid: string, +): ClassUmlAdaptedDto | undefined { + if (!list) return undefined; + return list.find(c => getId(c) === uuid); +} + +function upsertClass( + list: ClassUmlAdaptedDto[], + cls: ClassUmlAdaptedDto, +): ClassUmlAdaptedDto[] { + const id = getId(cls); + if (!id) return list.includes(cls) ? list : [...list, cls]; + const idx = list.findIndex(c => getId(c) === id); + if (idx >= 0) { + const next = [...list]; + next[idx] = cls; + return next; + } + return [...list, cls]; +} + +// Generic UUID-based upsert for sub-resources (attribute / enum entry). +function upsertByUuid(list: T[], item: T): T[] { + if (!item.uuid) return [...list, item]; + const idx = list.findIndex(x => x.uuid === item.uuid); + if (idx >= 0) { + const next = [...list]; + next[idx] = item; + return next; + } + return [...list, item]; +} + +// Association pairs are identified by their `from.uuid` (mirrors the backend +// path `…/associations/{associationUUID}` which expects the from-end's UUID). +function upsertAssociationPair( + list: AssociationPairDto[], + pair: AssociationPairDto, +): AssociationPairDto[] { + const id = pair.from?.uuid; + if (!id) return [...list, pair]; + const idx = list.findIndex(p => p.from?.uuid === id); + if (idx >= 0) { + const next = [...list]; + next[idx] = pair; + return next; + } + return [...list, pair]; +} + +function createClassStore() { + const store = writable({ byGraph: new Map() }); + const { subscribe, update } = store; + + function getGraphState( + state: ClassesState, + key: GraphKey, + ): GraphClassState { + return state.byGraph.get(key) ?? createEmptyGraphState(); + } + + function setGraphState( + state: ClassesState, + key: GraphKey, + next: GraphClassState, + ): ClassesState { + const byGraph = new Map(state.byGraph); + byGraph.set(key, next); + return { ...state, byGraph }; + } + + function setVariant( + state: ClassesState, + key: GraphKey, + variant: Variant, + next: VariantState, + ): ClassesState { + const current = getGraphState(state, key); + return setGraphState(state, key, { ...current, [variant]: next }); + } + + // Apply a transformation to a single class (if it exists) in both variants. + function mutateClassInPlace( + key: GraphKey, + classUUID: string, + transform: (c: ClassUmlAdaptedDto) => ClassUmlAdaptedDto, + ) { + update(s => { + const current = s.byGraph.get(key); + if (!current) return s; + + const apply = (variant: VariantState): VariantState => { + if (!variant.data) return variant; + const idx = variant.data.findIndex(c => getId(c) === classUUID); + if (idx < 0) return variant; + const nextList = [...variant.data]; + nextList[idx] = transform(nextList[idx]); + return { + ...variant, + data: nextList, + fetchedAt: Date.now(), + error: null, + }; + }; + + return setGraphState(s, key, { + all: apply(current.all), + internalOnly: apply(current.internalOnly), + }); + }); + } + + // ----- Loaders ----- + + async function load( + datasetName: string, + graphURI: string, + includeExternal = false, + force = false, + ) { + if (!datasetName || !graphURI) return; + + const key = makeKey(datasetName, graphURI); + const variant: Variant = includeExternal ? "all" : "internalOnly"; + const variantState = getGraphState(get(store), key)[variant]; + + if (!force && variantState.data !== null) return; + if (variantState.pending !== null) return variantState.pending; + + console.log( + `${LOG_PREFIX} Loading classes for dataset="${datasetName}", graph="${graphURI}", includeExternal=${includeExternal}, force=${force}`, + ); + + const promise = (async () => { + try { + const { data, error } = await getClassList({ + path: { datasetName, graphURI }, + query: { includeExternalClasses: includeExternal }, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Failed to load classes for dataset="${datasetName}", graph="${graphURI}"`, + await describeError(error), + ); + update(s => + setVariant(s, key, variant, { + ...getGraphState(s, key)[variant], + pending: null, + error, + }), + ); + return; + } + + update(s => + setVariant(s, key, variant, { + data: data ?? [], + fetchedAt: Date.now(), + pending: null, + error: null, + }), + ); + + console.log( + `${LOG_PREFIX} Loaded ${(data ?? []).length} classes for dataset="${datasetName}", graph="${graphURI}", variant="${variant}"`, + ); + } catch (err) { + console.error( + `${LOG_PREFIX} Unexpected error while loading classes for dataset="${datasetName}", graph="${graphURI}"`, + err, + ); + update(s => + setVariant(s, key, variant, { + ...getGraphState(s, key)[variant], + pending: null, + error: err, + }), + ); + } + })(); + + update(s => + setVariant(s, key, variant, { + ...getGraphState(s, key)[variant], + pending: promise, + }), + ); + + return promise; + } + + async function loadClassInfo( + datasetName: string, + graphURI: string, + classUUID: string, + force = false, + ): Promise> { + if (!datasetName || !graphURI || !classUUID) return { error: null }; + + const key = makeKey(datasetName, graphURI); + + if (!force) { + const current = getGraphState(get(store), key); + const existing = + findInVariant(current.all.data, classUUID) ?? + findInVariant(current.internalOnly.data, classUUID); + if (hasDetails(existing)) return { error: null, data: existing }; + } + + console.log( + `${LOG_PREFIX} Loading class details for classUUID="${classUUID}" in dataset="${datasetName}", graph="${graphURI}"`, + ); + + const { data, error } = await getClassInformation({ + path: { datasetName, graphURI, classUUID }, + }); + + if (error || !data) { + if (error) { + console.error( + `${LOG_PREFIX} Failed to load class details for classUUID="${classUUID}"`, + await describeError(error), + ); + } else { + console.error( + `${LOG_PREFIX} Class details response was empty for classUUID="${classUUID}"`, + ); + } + return { error }; + } + + update(s => { + const current = getGraphState(s, key); + const mergeInto = (variant: VariantState): VariantState => { + if (!variant.data) return variant; + return { + ...variant, + data: upsertClass(variant.data, data), + fetchedAt: Date.now(), + error: null, + }; + }; + return setGraphState(s, key, { + all: mergeInto(current.all), + internalOnly: mergeInto(current.internalOnly), + }); + }); + + console.log( + `${LOG_PREFIX} Loaded class details for classUUID="${classUUID}"`, + ); + + return { error: null, data }; + } + + // ----- Getters ----- + + function getClasses( + datasetName: string, + graphURI: string, + includeExternal = false, + ): ClassUmlAdaptedDto[] | null { + const variant: Variant = includeExternal ? "all" : "internalOnly"; + return getGraphState(get(store), makeKey(datasetName, graphURI))[ + variant + ].data; + } + + function getClass( + datasetName: string, + graphURI: string, + classUUID: string, + ): ClassUmlAdaptedDto | null { + const state = getGraphState(get(store), makeKey(datasetName, graphURI)); + return ( + findInVariant(state.all.data, classUUID) ?? + findInVariant(state.internalOnly.data, classUUID) ?? + null + ); + } + + function getClassInfo( + datasetName: string, + graphURI: string, + classUUID: string, + ): ClassUmlAdaptedDto | null { + const c = getClass(datasetName, graphURI, classUUID); + return hasDetails(c) ? c : null; + } + + function getAttributes( + datasetName: string, + graphURI: string, + classUUID: string, + ): AttributeDto[] | null { + return ( + getClassInfo(datasetName, graphURI, classUUID)?.attributes ?? null + ); + } + + function getEnumEntries( + datasetName: string, + graphURI: string, + classUUID: string, + ): EnumEntryDto[] | null { + return ( + getClassInfo(datasetName, graphURI, classUUID)?.enumEntries ?? null + ); + } + + function getAssociationPairs( + datasetName: string, + graphURI: string, + classUUID: string, + ): AssociationPairDto[] | null { + return ( + getClassInfo(datasetName, graphURI, classUUID)?.associationPairs ?? + null + ); + } + + // ========================================================================= + // CLASS-LEVEL OPERATIONS + // ========================================================================= + + async function addNewClass( + datasetName: string, + graphURI: string, + request: AddNewClassRequest, + ): Promise> { + console.log( + `${LOG_PREFIX} Creating class in dataset="${datasetName}", graph="${graphURI}"`, + ); + + const { data, error } = await addClass({ + path: { datasetName, graphURI }, + body: request, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not create class`, + await describeError(error), + ); + toastStore.error( + "Create failed", + "Die Klasse konnte nicht erstellt werden.", + ); + return { error }; + } + + invalidateGraph(datasetName, graphURI); + console.log(`${LOG_PREFIX} Created class with uuid="${data ?? ""}"`); + toastStore.success( + "Klasse erstellt", + "Die Klasse wurde erfolgreich erstellt.", + ); + return { error: null, data: data ?? undefined }; + } + + async function replaceExistingClass( + datasetName: string, + graphURI: string, + classUUID: string, + cls: ClassUmlAdaptedDto, + ): Promise { + console.log(`${LOG_PREFIX} Replacing class classUUID="${classUUID}"`); + + const { error } = await replaceClass({ + path: { datasetName, graphURI, classUUID }, + body: cls, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not replace class classUUID="${classUUID}"`, + await describeError(error), + ); + toastStore.error( + "Speichern fehlgeschlagen", + "Die Klasse konnte nicht gespeichert werden.", + ); + return { error }; + } + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => ({ + ...prev, + ...cls, + uuid: classUUID, + })); + console.log(`${LOG_PREFIX} Replaced class classUUID="${classUUID}"`); + toastStore.success( + "Klasse gespeichert", + "Die Änderungen wurden übernommen.", + ); + return { error: null }; + } + + async function deleteExistingClass( + datasetName: string, + graphURI: string, + classUUID: string, + ): Promise { + console.log(`${LOG_PREFIX} Deleting class classUUID="${classUUID}"`); + + const { error } = await deleteClass({ + path: { datasetName, graphURI, classUUID }, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not delete class classUUID="${classUUID}"`, + await describeError(error), + ); + toastStore.error( + "Delete failed", + "Die Klasse konnte nicht gelöscht werden.", + ); + return { error }; + } + + removeClassLocally(datasetName, graphURI, classUUID); + console.log(`${LOG_PREFIX} Deleted class classUUID="${classUUID}"`); + toastStore.success("Klasse gelöscht", "Die Klasse wurde entfernt."); + return { error: null }; + } + + async function extendExistingClass( + datasetName: string, + graphURI: string, + classUUID: string, + attribute: AttributeDto, + ): Promise> { + console.log(`${LOG_PREFIX} Extending class classUUID="${classUUID}"`); + + const { data, error } = await extendClass({ + path: { datasetName, graphURI, classUUID }, + body: attribute, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not extend class classUUID="${classUUID}"`, + await describeError(error), + ); + toastStore.error( + "Erweiterung fehlgeschlagen", + "Die Klasse konnte nicht erweitert werden.", + ); + return { error }; + } + + invalidateGraph(datasetName, graphURI); + console.log(`${LOG_PREFIX} Extended class classUUID="${classUUID}"`); + toastStore.success("Klasse erweitert", "Die Klasse wurde erweitert."); + return { error: null, data: data ?? undefined }; + } + + async function copyExistingClass( + datasetName: string, + graphURI: string, + classUUID: string, + request: CopyClassRequestDto, + ): Promise> { + console.log( + `${LOG_PREFIX} Copying class classUUID="${classUUID}" from dataset="${datasetName}", graph="${graphURI}"`, + ); + + const { data, error } = await copyClass({ + path: { datasetName, graphURI, classUUID }, + body: request, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not copy class classUUID="${classUUID}"`, + await describeError(error), + ); + toastStore.error( + "Kopieren fehlgeschlagen", + "Die Klasse konnte nicht kopiert werden.", + ); + return { error }; + } + + if (request.targetDatasetName && request.targetGraphURI) { + invalidateGraph(request.targetDatasetName, request.targetGraphURI); + } + console.log(`${LOG_PREFIX} Copied class classUUID="${classUUID}"`); + toastStore.success( + "Klasse kopiert", + "Die Klasse wurde erfolgreich kopiert.", + ); + return { error: null, data: data ?? undefined }; + } + + // ========================================================================= + // ATTRIBUTE OPERATIONS + // ========================================================================= + + async function addAttribute( + datasetName: string, + graphURI: string, + classUUID: string, + attribute: AttributeDto, + ): Promise> { + console.log( + `${LOG_PREFIX} Adding attribute to class classUUID="${classUUID}"`, + ); + + const { data, error } = await createAttribute({ + path: { datasetName, graphURI, classUUID }, + body: attribute, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not add attribute to class classUUID="${classUUID}"`, + await describeError(error), + ); + toastStore.error( + "Attribut hinzufügen fehlgeschlagen", + "Das Attribut konnte nicht erstellt werden.", + ); + return { error }; + } + + const newUUID = data ?? attribute.uuid; + const stored: AttributeDto = { ...attribute, uuid: newUUID }; + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => ({ + ...prev, + attributes: upsertByUuid(prev.attributes ?? [], stored), + })); + + console.log( + `${LOG_PREFIX} Added attribute uuid="${newUUID ?? ""}" to class classUUID="${classUUID}"`, + ); + toastStore.success( + "Attribut erstellt", + "Das Attribut wurde hinzugefügt.", + ); + return { error: null, data: newUUID }; + } + + async function replaceExistingAttribute( + datasetName: string, + graphURI: string, + classUUID: string, + attribute: AttributeDto, + ): Promise { + if (!attribute.uuid) { + const err = new Error("attribute.uuid is required"); + console.error( + `${LOG_PREFIX} replaceAttribute validation failed`, + err, + ); + return { error: err }; + } + + console.log( + `${LOG_PREFIX} Replacing attribute uuid="${attribute.uuid}" in class classUUID="${classUUID}"`, + ); + + const { error } = await replaceAttribute({ + path: { + datasetName, + graphURI, + classUUID, + attributeUUID: attribute.uuid, + }, + body: attribute, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not replace attribute uuid="${attribute.uuid}"`, + await describeError(error), + ); + toastStore.error( + "Speichern fehlgeschlagen", + "Das Attribut konnte nicht gespeichert werden.", + ); + return { error }; + } + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => ({ + ...prev, + attributes: upsertByUuid(prev.attributes ?? [], attribute), + })); + console.log( + `${LOG_PREFIX} Replaced attribute uuid="${attribute.uuid}"`, + ); + toastStore.success( + "Attribut gespeichert", + "Die Änderungen am Attribut wurden übernommen.", + ); + return { error: null }; + } + + async function deleteAttribute( + datasetName: string, + graphURI: string, + classUUID: string, + attributeUUID: string, + action: ResourceDeleteRequest["action"] = "DELETE", + ): Promise { + console.log(`${LOG_PREFIX} Deleting attribute uuid="${attributeUUID}"`); + + const { error } = await deleteResources({ + path: { datasetName, graphURI }, + body: [{ uuid: attributeUUID, action }], + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not delete attribute uuid="${attributeUUID}"`, + await describeError(error), + ); + toastStore.error( + "Delete failed", + "Das Attribut konnte nicht gelöscht werden.", + ); + return { error }; + } + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => ({ + ...prev, + attributes: (prev.attributes ?? []).filter( + a => a.uuid !== attributeUUID, + ), + })); + console.log(`${LOG_PREFIX} Deleted attribute uuid="${attributeUUID}"`); + toastStore.success("Attribut gelöscht", "Das Attribut wurde entfernt."); + return { error: null }; + } + + // ========================================================================= + // ENUM ENTRY OPERATIONS + // ========================================================================= + + async function addEnumEntry( + datasetName: string, + graphURI: string, + classUUID: string, + enumEntry: EnumEntryDto, + ): Promise { + console.log( + `${LOG_PREFIX} Adding enum entry to class classUUID="${classUUID}"`, + ); + + const { error } = await createEnumEntry({ + path: { datasetName, graphURI, classUUID }, + body: enumEntry, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not add enum entry`, + await describeError(error), + ); + toastStore.error( + "Enum-Wert hinzufügen fehlgeschlagen", + "Der Enum-Wert konnte nicht erstellt werden.", + ); + return { error }; + } + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => ({ + ...prev, + enumEntries: upsertByUuid(prev.enumEntries ?? [], enumEntry), + })); + console.log( + `${LOG_PREFIX} Added enum entry to class classUUID="${classUUID}"`, + ); + toastStore.success( + "Enum-Wert erstellt", + "Der Enum-Wert wurde hinzugefügt.", + ); + return { error: null }; + } + + async function replaceExistingEnumEntry( + datasetName: string, + graphURI: string, + classUUID: string, + enumEntry: EnumEntryDto, + ): Promise { + if (!enumEntry.uuid) { + const err = new Error("enumEntry.uuid is required"); + console.error( + `${LOG_PREFIX} replaceEnumEntry validation failed`, + err, + ); + return { error: err }; + } + + console.log( + `${LOG_PREFIX} Replacing enum entry uuid="${enumEntry.uuid}"`, + ); + + const { error } = await replaceEnumEntry({ + path: { + datasetName, + graphURI, + classUUID, + enumEntryUUID: enumEntry.uuid, + }, + body: enumEntry, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not replace enum entry uuid="${enumEntry.uuid}"`, + await describeError(error), + ); + toastStore.error( + "Speichern fehlgeschlagen", + "Der Enum-Wert konnte nicht gespeichert werden.", + ); + return { error }; + } + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => ({ + ...prev, + enumEntries: upsertByUuid(prev.enumEntries ?? [], enumEntry), + })); + console.log( + `${LOG_PREFIX} Replaced enum entry uuid="${enumEntry.uuid}"`, + ); + toastStore.success( + "Enum-Wert gespeichert", + "Die Änderungen wurden übernommen.", + ); + return { error: null }; + } + + async function deleteEnumEntry( + datasetName: string, + graphURI: string, + classUUID: string, + enumEntryUUID: string, + action: ResourceDeleteRequest["action"] = "DELETE", + ): Promise { + console.log( + `${LOG_PREFIX} Deleting enum entry uuid="${enumEntryUUID}"`, + ); + + const { error } = await deleteResources({ + path: { datasetName, graphURI }, + body: [{ uuid: enumEntryUUID, action }], + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not delete enum entry uuid="${enumEntryUUID}"`, + await describeError(error), + ); + toastStore.error( + "Delete failed", + "Der Enum-Wert konnte nicht gelöscht werden.", + ); + return { error }; + } + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => ({ + ...prev, + enumEntries: (prev.enumEntries ?? []).filter( + e => e.uuid !== enumEntryUUID, + ), + })); + console.log(`${LOG_PREFIX} Deleted enum entry uuid="${enumEntryUUID}"`); + toastStore.success( + "Enum-Wert gelöscht", + "Der Enum-Wert wurde entfernt.", + ); + return { error: null }; + } + + // ========================================================================= + // ASSOCIATION OPERATIONS + // ========================================================================= + + async function addAssociationPair( + datasetName: string, + graphURI: string, + classUUID: string, + pair: AssociationPairDto, + ): Promise> { + console.log( + `${LOG_PREFIX} Adding association pair to class classUUID="${classUUID}"`, + ); + + const { data, error } = await createAssociation({ + path: { datasetName, graphURI, classUUID }, + body: pair, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not add association pair`, + await describeError(error), + ); + toastStore.error( + "Assoziation hinzufügen fehlgeschlagen", + "Die Assoziation konnte nicht erstellt werden.", + ); + return { error }; + } + + const enriched: AssociationPairDto = { + from: { ...pair.from, uuid: data?.fromUUID ?? pair.from?.uuid }, + to: { ...pair.to, uuid: data?.toUUID ?? pair.to?.uuid }, + }; + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => ({ + ...prev, + associationPairs: upsertAssociationPair( + prev.associationPairs ?? [], + enriched, + ), + })); + console.log( + `${LOG_PREFIX} Added association pair to class classUUID="${classUUID}"`, + ); + toastStore.success( + "Assoziation erstellt", + "Die Assoziation wurde hinzugefügt.", + ); + return { error: null, data: enriched }; + } + + async function replaceAssociationPair( + datasetName: string, + graphURI: string, + classUUID: string, + pair: AssociationPairDto, + ): Promise> { + const associationUUID = pair.from?.uuid; + if (!associationUUID) { + const err = new Error("pair.from.uuid is required"); + console.error( + `${LOG_PREFIX} replaceAssociationPair validation failed`, + err, + ); + return { error: err }; + } + + console.log( + `${LOG_PREFIX} Replacing association pair uuid="${associationUUID}"`, + ); + + const { data, error } = await replaceAssociation({ + path: { datasetName, graphURI, classUUID, associationUUID }, + body: pair, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not replace association pair uuid="${associationUUID}"`, + await describeError(error), + ); + toastStore.error( + "Speichern fehlgeschlagen", + "Die Assoziation konnte nicht gespeichert werden.", + ); + return { error }; + } + + const enriched: AssociationPairDto = { + from: { ...pair.from, uuid: data?.fromUUID ?? pair.from?.uuid }, + to: { ...pair.to, uuid: data?.toUUID ?? pair.to?.uuid }, + }; + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => { + const filtered = (prev.associationPairs ?? []).filter( + p => p.from?.uuid !== associationUUID, + ); + return { + ...prev, + associationPairs: upsertAssociationPair(filtered, enriched), + }; + }); + console.log( + `${LOG_PREFIX} Replaced association pair uuid="${associationUUID}"`, + ); + toastStore.success( + "Assoziation gespeichert", + "Die Änderungen wurden übernommen.", + ); + return { error: null, data: data }; + } + + async function replaceAllAssociationPairs( + datasetName: string, + graphURI: string, + classUUID: string, + pairs: AssociationPairDto[], + ): Promise { + console.log( + `${LOG_PREFIX} Replacing all association pairs for class classUUID="${classUUID}", count=${pairs.length}`, + ); + + const { error } = await replaceAllAssociations({ + path: { datasetName, graphURI, classUUID }, + body: pairs, + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not replace all association pairs`, + await describeError(error), + ); + toastStore.error( + "Speichern fehlgeschlagen", + "Die Assoziationen konnten nicht gespeichert werden.", + ); + return { error }; + } + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => ({ + ...prev, + associationPairs: pairs, + })); + console.log( + `${LOG_PREFIX} Replaced all association pairs for class classUUID="${classUUID}"`, + ); + toastStore.success( + "Assoziationen gespeichert", + "Alle Assoziationen wurden aktualisiert.", + ); + return { error: null }; + } + + async function deleteAssociationPair( + datasetName: string, + graphURI: string, + classUUID: string, + associationUUID: string, + action: ResourceDeleteRequest["action"] = "DELETE", + ): Promise { + console.log( + `${LOG_PREFIX} Deleting association pair uuid="${associationUUID}"`, + ); + + const { error } = await deleteResources({ + path: { datasetName, graphURI }, + body: [{ uuid: associationUUID, action }], + }); + if (error) { + console.error( + `${LOG_PREFIX} Could not delete association pair uuid="${associationUUID}"`, + await describeError(error), + ); + toastStore.error( + "Delete failed", + "Die Assoziation konnte nicht gelöscht werden.", + ); + return { error }; + } + + const key = makeKey(datasetName, graphURI); + mutateClassInPlace(key, classUUID, prev => ({ + ...prev, + associationPairs: (prev.associationPairs ?? []).filter( + p => p.from?.uuid !== associationUUID, + ), + })); + console.log( + `${LOG_PREFIX} Deleted association pair uuid="${associationUUID}"`, + ); + toastStore.success( + "Assoziation gelöscht", + "Die Assoziation wurde entfernt.", + ); + return { error: null }; + } + + // ========================================================================= + // LOCAL-ONLY HELPERS & INVALIDATION + // ========================================================================= + + function removeClassLocally( + datasetName: string, + graphURI: string, + classUUID: string, + ) { + if (!datasetName || !graphURI || !classUUID) return; + const key = makeKey(datasetName, graphURI); + + update(s => { + const current = s.byGraph.get(key); + if (!current) return s; + + const dropFrom = (variant: VariantState): VariantState => { + if (!variant.data) return variant; + return { + ...variant, + data: variant.data.filter(c => getId(c) !== classUUID), + fetchedAt: Date.now(), + error: null, + }; + }; + + return setGraphState(s, key, { + all: dropFrom(current.all), + internalOnly: dropFrom(current.internalOnly), + }); + }); + } + + function invalidateGraph(datasetName: string, graphURI: string) { + const key = makeKey(datasetName, graphURI); + console.log(`${LOG_PREFIX} Invalidating graph cache key="${key}"`); + update(s => { + const byGraph = new Map(s.byGraph); + byGraph.delete(key); + return { ...s, byGraph }; + }); + } + + function invalidateDataset(datasetName: string) { + const prefix = `${datasetName}::`; + console.log( + `${LOG_PREFIX} Invalidating dataset cache dataset="${datasetName}"`, + ); + update(s => { + const byGraph = new Map(s.byGraph); + for (const k of byGraph.keys()) { + if (k.startsWith(prefix)) byGraph.delete(k); + } + return { ...s, byGraph }; + }); + } + + function invalidateAll() { + console.log(`${LOG_PREFIX} Invalidating all class caches`); + update(() => ({ byGraph: new Map() })); + } + + return { + subscribe, + + // loaders + load, + loadClassInfo, + + // getters + getClasses, + getClass, + getClassInfo, + getAttributes, + getEnumEntries, + getAssociationPairs, + + // class-level mutations + addClass: addNewClass, + replaceClass: replaceExistingClass, + deleteClass: deleteExistingClass, + extendClass: extendExistingClass, + copyClass: copyExistingClass, + + // attribute mutations + addAttribute, + replaceAttribute: replaceExistingAttribute, + deleteAttribute, + + // enum entry mutations + addEnumEntry, + replaceEnumEntry: replaceExistingEnumEntry, + deleteEnumEntry, + + // association mutations + addAssociationPair, + replaceAssociationPair: replaceAssociationPair, + replaceAllAssociationPairs, + deleteAssociationPair, + + // invalidation + invalidateGraph, + invalidateDataset, + invalidateAll, + }; +} diff --git a/frontend/src/lib/stores/DatasetStore.ts b/frontend/src/lib/stores/DatasetStore.ts index 31e4f252..3a9ad6f8 100644 --- a/frontend/src/lib/stores/DatasetStore.ts +++ b/frontend/src/lib/stores/DatasetStore.ts @@ -17,6 +17,7 @@ import { writable, get } from "svelte/store"; +import { describeError } from "./StoreLogging"; import { listDatasets, deleteDataset, @@ -25,6 +26,7 @@ import { enableEditing, disableEditing, } from "../api/generated"; +import { toastStore } from "../eventhandling/toastStore.svelte.js"; export type DatasetInfo = { label: string; @@ -39,6 +41,11 @@ type DatasetsState = { error: unknown; }; +// Generic envelope used by all mutating store methods. +type Result = { error: unknown; data?: T }; + +const LOG_PREFIX = "[datasetStore]"; + export const datasetStore = createDatasetStore(); function createDatasetStore() { @@ -47,7 +54,7 @@ function createDatasetStore() { fetchedAt: null, pending: null, error: null, - }); // ← kein Start-Callback + }); const { subscribe: baseSubscribe, update } = store; @@ -64,6 +71,8 @@ function createDatasetStore() { return baseSubscribe(run, invalidate); } + // ----- Load ----- + async function load(force = false) { const state = get(store); @@ -74,6 +83,10 @@ function createDatasetStore() { try { const { data, error } = await listDatasets(); if (error) { + console.error( + `${LOG_PREFIX} Failed to load datasets:`, + await describeError(error), + ); update(s => ({ ...s, pending: null, @@ -98,6 +111,10 @@ function createDatasetStore() { fetchedAt: Date.now(), })); } catch (err) { + console.error( + `${LOG_PREFIX} Unexpected error while loading datasets:`, + err, + ); update(s => ({ ...s, pending: null, @@ -110,27 +127,11 @@ function createDatasetStore() { return promise; } - async function remove(datasetName: string) { - const { error } = await deleteDataset({ path: { datasetName } }); - if (error) return { error }; - - update(s => ({ - ...s, - data: s.data?.filter(d => d.label !== datasetName) ?? null, - })); - return { error: null }; - } - - function invalidate() { - void load(true); - } + // ----- Getters ----- function isReadOnly(datasetName: string): boolean | null { const dataset = get(store).data?.find(d => d.label === datasetName); if (!dataset) { - console.warn( - `isReadOnly called before dataset "${datasetName}" was loaded`, - ); return null; } return dataset.readonly; @@ -139,24 +140,69 @@ function createDatasetStore() { function getNamespaces(datasetName: string): CimPrefixPair[] { const dataset = get(store).data?.find(d => d.label === datasetName); if (!dataset) { - console.warn(`getNamespaces called before dataset "${datasetName}" was loaded`); return []; } return dataset.prefixes; } + // ----- Mutations ----- + + async function remove(datasetName: string): Promise { + console.log(`${LOG_PREFIX} Deleting dataset "${datasetName}"`); + + const { error } = await deleteDataset({ path: { datasetName } }); + + if (error) { + const msg = await describeError(error); + console.error( + `${LOG_PREFIX} Could not delete dataset "${datasetName}":`, + msg, + ); + toastStore.error( + "Delete failed", + `Could not delete dataset "${datasetName}".`, + ); + return { error }; + } + + update(s => ({ + ...s, + data: s.data?.filter(d => d.label !== datasetName) ?? null, + })); + + console.log(`${LOG_PREFIX} Deleted dataset "${datasetName}"`); + toastStore.success("Dataset deleted", `"${datasetName}" was deleted.`); + + return { error: null }; + } + async function saveNamespaces( datasetName: string, namespaces: CimPrefixPair[], - ) { + ): Promise { + console.log( + `${LOG_PREFIX} Saving ${namespaces.length} namespace(s) for "${datasetName}"`, + ); + const { error } = await replaceNamespaces({ path: { datasetName }, body: namespaces, }); - if (error) return { error }; + if (error) { + const msg = await describeError(error); + console.error( + `${LOG_PREFIX} Could not save namespaces for "${datasetName}":`, + msg, + ); + toastStore.error( + "Save failed", + `Could not save namespaces for "${datasetName}".`, + ); + return { error }; + } - // Update prefixes in store so getNamespaces() returns fresh data + // Keep getNamespaces() consistent with what the backend now holds. update(s => ({ ...s, data: @@ -167,15 +213,41 @@ function createDatasetStore() { ) ?? null, })); + console.log(`${LOG_PREFIX} Saved namespaces for "${datasetName}"`); + toastStore.success( + "Namespaces saved", + `Namespaces for "${datasetName}" were updated.`, + ); + return { error: null }; } - async function updateReadonly(datasetName: string, readonly: boolean) { + async function updateReadonly( + datasetName: string, + readonly: boolean, + ): Promise { + console.log( + `${LOG_PREFIX} Setting readonly=${readonly} for "${datasetName}"`, + ); + const res = readonly ? await disableEditing({ path: { datasetName } }) : await enableEditing({ path: { datasetName } }); if (res?.error) { + const msg = await describeError(res.error); + console.error( + `${LOG_PREFIX} Could not update readonly flag for "${datasetName}":`, + msg, + ); + toastStore.error( + readonly + ? "Could not disable editing" + : "Could not enable editing", + readonly + ? `Dataset "${datasetName}" remains editable.` + : `Dataset "${datasetName}" remains read-only.`, + ); return { error: res }; } @@ -187,9 +259,23 @@ function createDatasetStore() { ) ?? null, })); + console.log( + `${LOG_PREFIX} Updated readonly=${readonly} for "${datasetName}"`, + ); + toastStore.success( + readonly ? "Editing disabled" : "Editing enabled", + `"${datasetName}" is now ${readonly ? "read-only" : "editable"}.`, + ); + return { error: null }; } + // ----- Invalidation ----- + + function invalidate() { + void load(true); + } + return { subscribe, load, diff --git a/frontend/src/lib/stores/DiagramStore.ts b/frontend/src/lib/stores/DiagramStore.ts new file mode 100644 index 00000000..f7a47d1e --- /dev/null +++ b/frontend/src/lib/stores/DiagramStore.ts @@ -0,0 +1,856 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { writable, get } from "svelte/store"; + +import { describeError } from "./StoreLogging"; +import { + getCustomDiagramList, + getCustomDiagramList1, + getDiagramRenderingData, + getDiagramRenderingData1, + replaceDiagram, + replaceDiagram1, + deleteDiagram, + deleteDiagram1, + addToDiagram, + addToDiagram1, + removeFromDiagram, + removeFromDiagram1, + type CustomDiagram, + type RenderingDataDto, +} from "../api/generated"; +import { toastStore } from "../eventhandling/toastStore.svelte.js"; + +type Result = { error: unknown; data?: T }; + +type ListState = { + data: T[] | null; + fetchedAt: number | null; + pending: Promise | null; + error: unknown; +}; + +type ItemState = { + data: T | null; + fetchedAt: number | null; + pending: Promise | null; + error: unknown; +}; + +type StoreState = { + // key: dataset + datasetLists: Map>; + // key: dataset::graph + graphLists: Map>; + // key: dataset::diagramId + datasetRenderings: Map>; + // key: dataset::graph::diagramId + graphRenderings: Map>; +}; + +const LOG_PREFIX = "[customDiagramStore]"; + +export const customDiagramStore = createCustomDiagramStore(); + +const emptyListState = (): ListState => ({ + data: null, + fetchedAt: null, + pending: null, + error: null, +}); + +const emptyItemState = (): ItemState => ({ + data: null, + fetchedAt: null, + pending: null, + error: null, +}); + +const graphListKey = (datasetName: string, graphURI: string) => + `${datasetName}::${graphURI}`; +const datasetRenderingKey = (datasetName: string, diagramId: string) => + `${datasetName}::${diagramId}`; +const graphRenderingKey = ( + datasetName: string, + graphURI: string, + diagramId: string, +) => `${datasetName}::${graphURI}::${diagramId}`; + +function createCustomDiagramStore() { + const store = writable({ + datasetLists: new Map(), + graphLists: new Map(), + datasetRenderings: new Map(), + graphRenderings: new Map(), + }); + + const { subscribe, update } = store; + + // ---------- helpers ---------- + function getDatasetListState( + state: StoreState, + datasetName: string, + ): ListState { + return state.datasetLists.get(datasetName) ?? emptyListState(); + } + + function getGraphListState( + state: StoreState, + datasetName: string, + graphURI: string, + ): ListState { + return ( + state.graphLists.get(graphListKey(datasetName, graphURI)) ?? + emptyListState() + ); + } + + function setDatasetListState( + state: StoreState, + datasetName: string, + next: ListState, + ): StoreState { + const map = new Map(state.datasetLists); + map.set(datasetName, next); + return { ...state, datasetLists: map }; + } + + function setGraphListState( + state: StoreState, + datasetName: string, + graphURI: string, + next: ListState, + ): StoreState { + const key = graphListKey(datasetName, graphURI); + const map = new Map(state.graphLists); + map.set(key, next); + return { ...state, graphLists: map }; + } + + function getDatasetRenderingState( + state: StoreState, + datasetName: string, + diagramId: string, + ): ItemState { + return ( + state.datasetRenderings.get( + datasetRenderingKey(datasetName, diagramId), + ) ?? emptyItemState() + ); + } + + function getGraphRenderingState( + state: StoreState, + datasetName: string, + graphURI: string, + diagramId: string, + ): ItemState { + return ( + state.graphRenderings.get( + graphRenderingKey(datasetName, graphURI, diagramId), + ) ?? emptyItemState() + ); + } + + function setDatasetRenderingState( + state: StoreState, + datasetName: string, + diagramId: string, + next: ItemState, + ): StoreState { + const key = datasetRenderingKey(datasetName, diagramId); + const map = new Map(state.datasetRenderings); + map.set(key, next); + return { ...state, datasetRenderings: map }; + } + + function setGraphRenderingState( + state: StoreState, + datasetName: string, + graphURI: string, + diagramId: string, + next: ItemState, + ): StoreState { + const key = graphRenderingKey(datasetName, graphURI, diagramId); + const map = new Map(state.graphRenderings); + map.set(key, next); + return { ...state, graphRenderings: map }; + } + + // ---------- loaders ---------- + async function loadDatasetDiagrams(datasetName: string, force = false) { + if (!datasetName) return; + + const current = getDatasetListState(get(store), datasetName); + if (!force && current.data !== null) return; + if (current.pending) return current.pending; + + console.log( + `${LOG_PREFIX} Loading dataset diagrams for "${datasetName}"`, + ); + + const pending = (async () => { + try { + const { data, error } = await getCustomDiagramList1({ + path: { datasetName }, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Failed to load dataset diagrams`, + await describeError(error), + ); + update(s => + setDatasetListState(s, datasetName, { + ...getDatasetListState(s, datasetName), + pending: null, + error, + }), + ); + return; + } + + update(s => + setDatasetListState(s, datasetName, { + data: data ?? [], + fetchedAt: Date.now(), + pending: null, + error: null, + }), + ); + } catch (err) { + console.error( + `${LOG_PREFIX} Unexpected error loading dataset diagrams`, + err, + ); + update(s => + setDatasetListState(s, datasetName, { + ...getDatasetListState(s, datasetName), + pending: null, + error: err, + }), + ); + } + })(); + + update(s => + setDatasetListState(s, datasetName, { + ...getDatasetListState(s, datasetName), + pending, + }), + ); + + return pending; + } + + async function loadGraphDiagrams( + datasetName: string, + graphURI: string, + force = false, + ) { + if (!datasetName || !graphURI) return; + + const current = getGraphListState(get(store), datasetName, graphURI); + if (!force && current.data !== null) return; + if (current.pending) return current.pending; + + console.log( + `${LOG_PREFIX} Loading graph diagrams for dataset="${datasetName}" graph="${graphURI}"`, + ); + + const pending = (async () => { + try { + const { data, error } = await getCustomDiagramList({ + path: { datasetName, graphURI }, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Failed to load graph diagrams`, + await describeError(error), + ); + update(s => + setGraphListState(s, datasetName, graphURI, { + ...getGraphListState(s, datasetName, graphURI), + pending: null, + error, + }), + ); + return; + } + + update(s => + setGraphListState(s, datasetName, graphURI, { + data: data ?? [], + fetchedAt: Date.now(), + pending: null, + error: null, + }), + ); + } catch (err) { + console.error( + `${LOG_PREFIX} Unexpected error loading graph diagrams`, + err, + ); + update(s => + setGraphListState(s, datasetName, graphURI, { + ...getGraphListState(s, datasetName, graphURI), + pending: null, + error: err, + }), + ); + } + })(); + + update(s => + setGraphListState(s, datasetName, graphURI, { + ...getGraphListState(s, datasetName, graphURI), + pending, + }), + ); + + return pending; + } + + async function loadDatasetDiagramRendering( + datasetName: string, + diagramId: string, + force = false, + ): Promise> { + if (!datasetName || !diagramId) return { error: null }; + + const current = getDatasetRenderingState( + get(store), + datasetName, + diagramId, + ); + if (!force && current.data) return { error: null, data: current.data }; + if (current.pending) { + await current.pending; + const next = getDatasetRenderingState( + get(store), + datasetName, + diagramId, + ); + return { error: next.error, data: next.data ?? undefined }; + } + + const pending = (async () => { + try { + const { data, error } = await getDiagramRenderingData1({ + path: { datasetName, diagramId }, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Failed to load dataset diagram rendering`, + await describeError(error), + ); + update(s => + setDatasetRenderingState(s, datasetName, diagramId, { + ...getDatasetRenderingState( + s, + datasetName, + diagramId, + ), + pending: null, + error, + }), + ); + return; + } + + update(s => + setDatasetRenderingState(s, datasetName, diagramId, { + data: data ?? null, + fetchedAt: Date.now(), + pending: null, + error: null, + }), + ); + } catch (err) { + console.error( + `${LOG_PREFIX} Unexpected error loading dataset diagram rendering`, + err, + ); + update(s => + setDatasetRenderingState(s, datasetName, diagramId, { + ...getDatasetRenderingState(s, datasetName, diagramId), + pending: null, + error: err, + }), + ); + } + })(); + + update(s => + setDatasetRenderingState(s, datasetName, diagramId, { + ...getDatasetRenderingState(s, datasetName, diagramId), + pending, + }), + ); + + await pending; + const next = getDatasetRenderingState( + get(store), + datasetName, + diagramId, + ); + return { error: next.error, data: next.data ?? undefined }; + } + + async function loadGraphDiagramRendering( + datasetName: string, + graphURI: string, + diagramId: string, + force = false, + ): Promise> { + if (!datasetName || !graphURI || !diagramId) return { error: null }; + + const current = getGraphRenderingState( + get(store), + datasetName, + graphURI, + diagramId, + ); + if (!force && current.data) return { error: null, data: current.data }; + if (current.pending) { + await current.pending; + const next = getGraphRenderingState( + get(store), + datasetName, + graphURI, + diagramId, + ); + return { error: next.error, data: next.data ?? undefined }; + } + + const pending = (async () => { + try { + const { data, error } = await getDiagramRenderingData({ + path: { datasetName, graphURI, diagramId }, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Failed to load graph diagram rendering`, + await describeError(error), + ); + update(s => + setGraphRenderingState( + s, + datasetName, + graphURI, + diagramId, + { + ...getGraphRenderingState( + s, + datasetName, + graphURI, + diagramId, + ), + pending: null, + error, + }, + ), + ); + return; + } + + update(s => + setGraphRenderingState( + s, + datasetName, + graphURI, + diagramId, + { + data: data ?? null, + fetchedAt: Date.now(), + pending: null, + error: null, + }, + ), + ); + } catch (err) { + console.error( + `${LOG_PREFIX} Unexpected error loading graph diagram rendering`, + err, + ); + update(s => + setGraphRenderingState( + s, + datasetName, + graphURI, + diagramId, + { + ...getGraphRenderingState( + s, + datasetName, + graphURI, + diagramId, + ), + pending: null, + error: err, + }, + ), + ); + } + })(); + + update(s => + setGraphRenderingState(s, datasetName, graphURI, diagramId, { + ...getGraphRenderingState(s, datasetName, graphURI, diagramId), + pending, + }), + ); + + await pending; + const next = getGraphRenderingState( + get(store), + datasetName, + graphURI, + diagramId, + ); + return { error: next.error, data: next.data ?? undefined }; + } + + // ---------- mutations ---------- + async function saveDatasetDiagram( + datasetName: string, + diagramId: string, + diagram: unknown, + ): Promise { + const { error } = await replaceDiagram1({ + path: { datasetName, diagramId }, + body: diagram as never, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Could not save dataset diagram "${diagramId}"`, + await describeError(error), + ); + toastStore.error("Save failed", "Could not save dataset diagram."); + return { error }; + } + + invalidateDataset(datasetName); + toastStore.success("Diagram saved", "Dataset diagram was saved."); + return { error: null }; + } + + async function saveGraphDiagram( + datasetName: string, + graphURI: string, + diagramId: string, + diagram: unknown, + ): Promise { + const { error } = await replaceDiagram({ + path: { datasetName, graphURI, diagramId }, + body: diagram as never, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Could not save graph diagram "${diagramId}"`, + await describeError(error), + ); + toastStore.error("Save failed", "Could not save graph diagram."); + return { error }; + } + + invalidateGraph(datasetName, graphURI); + toastStore.success("Diagram saved", "Graph diagram was saved."); + return { error: null }; + } + + async function deleteDatasetDiagram( + datasetName: string, + diagramId: string, + ): Promise { + const { error } = await deleteDiagram1({ + path: { datasetName, diagramId }, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Could not delete dataset diagram "${diagramId}"`, + await describeError(error), + ); + toastStore.error( + "Delete failed", + "Could not delete dataset diagram.", + ); + return { error }; + } + + invalidateDataset(datasetName); + toastStore.success("Diagram deleted", "Dataset diagram was removed."); + return { error: null }; + } + + async function deleteGraphDiagram( + datasetName: string, + graphURI: string, + diagramId: string, + ): Promise { + const { error } = await deleteDiagram({ + path: { datasetName, graphURI, diagramId }, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Could not delete graph diagram "${diagramId}"`, + await describeError(error), + ); + toastStore.error( + "Delete failed", + "Could not delete graph diagram.", + ); + return { error }; + } + + invalidateGraph(datasetName, graphURI); + toastStore.success("Diagram deleted", "Graph diagram was removed."); + return { error: null }; + } + + async function addClassesToDatasetDiagram( + datasetName: string, + diagramId: string, + classes: string[], + ): Promise { + const { error } = await addToDiagram1({ + path: { datasetName, diagramId }, + body: classes as never, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Could not add classes to dataset diagram "${diagramId}"`, + await describeError(error), + ); + toastStore.error( + "Update failed", + "Could not add classes to diagram.", + ); + return { error }; + } + + invalidateDataset(datasetName); + return { error: null }; + } + + async function addClassesToGraphDiagram( + datasetName: string, + graphURI: string, + diagramId: string, + classes: string[], + ): Promise { + const { error } = await addToDiagram({ + path: { datasetName, graphURI, diagramId }, + body: classes as never, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Could not add classes to graph diagram "${diagramId}"`, + await describeError(error), + ); + toastStore.error( + "Update failed", + "Could not add classes to diagram.", + ); + return { error }; + } + + invalidateGraph(datasetName, graphURI); + return { error: null }; + } + + async function removeClassFromDatasetDiagram( + datasetName: string, + diagramId: string, + classId: string, + ): Promise { + const { error } = await removeFromDiagram1({ + path: { datasetName, diagramId, classId }, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Could not remove class from dataset diagram "${diagramId}"`, + await describeError(error), + ); + toastStore.error( + "Update failed", + "Could not remove class from diagram.", + ); + return { error }; + } + + invalidateDataset(datasetName); + return { error: null }; + } + + async function removeClassFromGraphDiagram( + datasetName: string, + graphURI: string, + diagramId: string, + classId: string, + ): Promise { + const { error } = await removeFromDiagram({ + path: { datasetName, graphURI, diagramId, classId }, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Could not remove class from graph diagram "${diagramId}"`, + await describeError(error), + ); + toastStore.error( + "Update failed", + "Could not remove class from diagram.", + ); + return { error }; + } + + invalidateGraph(datasetName, graphURI); + return { error: null }; + } + + // ---------- getters ---------- + function getDatasetDiagrams(datasetName: string): CustomDiagram[] | null { + return getDatasetListState(get(store), datasetName).data; + } + + function getGraphDiagrams( + datasetName: string, + graphURI: string, + ): CustomDiagram[] | null { + return getGraphListState(get(store), datasetName, graphURI).data; + } + + function getDatasetDiagramRendering( + datasetName: string, + diagramId: string, + ): RenderingDataDto | null { + return getDatasetRenderingState(get(store), datasetName, diagramId) + .data; + } + + function getGraphDiagramRendering( + datasetName: string, + graphURI: string, + diagramId: string, + ): RenderingDataDto | null { + return getGraphRenderingState( + get(store), + datasetName, + graphURI, + diagramId, + ).data; + } + + // ---------- invalidation ---------- + function invalidateDataset(datasetName: string) { + update(s => { + const datasetLists = new Map(s.datasetLists); + const graphLists = new Map(s.graphLists); + const datasetRenderings = new Map(s.datasetRenderings); + const graphRenderings = new Map(s.graphRenderings); + + datasetLists.delete(datasetName); + + const datasetPrefix = `${datasetName}::`; + for (const key of graphLists.keys()) { + if (key.startsWith(datasetPrefix)) graphLists.delete(key); + } + for (const key of datasetRenderings.keys()) { + if (key.startsWith(datasetPrefix)) + datasetRenderings.delete(key); + } + for (const key of graphRenderings.keys()) { + if (key.startsWith(datasetPrefix)) graphRenderings.delete(key); + } + + return { + datasetLists, + graphLists, + datasetRenderings, + graphRenderings, + }; + }); + } + + function invalidateGraph(datasetName: string, graphURI: string) { + update(s => { + const graphLists = new Map(s.graphLists); + const graphRenderings = new Map(s.graphRenderings); + + graphLists.delete(graphListKey(datasetName, graphURI)); + + const prefix = `${datasetName}::${graphURI}::`; + for (const key of graphRenderings.keys()) { + if (key.startsWith(prefix)) graphRenderings.delete(key); + } + + return { ...s, graphLists, graphRenderings }; + }); + } + + function invalidateAll() { + update(() => ({ + datasetLists: new Map(), + graphLists: new Map(), + datasetRenderings: new Map(), + graphRenderings: new Map(), + })); + } + + return { + subscribe, + + // loaders + loadDatasetDiagrams, + loadGraphDiagrams, + loadDatasetDiagramRendering, + loadGraphDiagramRendering, + + // getters + getDatasetDiagrams, + getGraphDiagrams, + getDatasetDiagramRendering, + getGraphDiagramRendering, + + // mutations + saveDatasetDiagram, + saveGraphDiagram, + deleteDatasetDiagram, + deleteGraphDiagram, + addClassesToDatasetDiagram, + addClassesToGraphDiagram, + removeClassFromDatasetDiagram, + removeClassFromGraphDiagram, + + // invalidation + invalidateDataset, + invalidateGraph, + invalidateAll, + }; +} diff --git a/frontend/src/lib/stores/GraphStore.ts b/frontend/src/lib/stores/GraphStore.ts new file mode 100644 index 00000000..97fa0792 --- /dev/null +++ b/frontend/src/lib/stores/GraphStore.ts @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { writable, get } from "svelte/store"; + +import { describeError } from "./StoreLogging"; +import { + listGraphs, + deleteGraph, + Uri, + replaceGraphs, + GraphBulkImportResponse, +} from "../api/generated"; +import { toastStore } from "../eventhandling/toastStore.svelte.js"; + +type GraphURIState = { + data: Uri[] | null; + fetchedAt: number | null; + pending: Promise | null; + error: unknown; +}; + +type DatasetState = { + graphs: Map; +}; + +type Result = { error: unknown; data?: T }; + +const LOG_PREFIX = "[graphStore]"; + +export const graphStore = createGraphStore(); + +function createEmptyDatasetState(): GraphURIState { + return { + data: null, + fetchedAt: null, + pending: null, + error: null, + }; +} + +function createGraphStore() { + const store = writable({ + graphs: new Map(), + }); + + const { subscribe, update } = store; + + function getDatasetState( + state: DatasetState, + datasetName: string, + ): GraphURIState { + return state.graphs.get(datasetName) ?? createEmptyDatasetState(); + } + + function setDatasetState( + state: DatasetState, + datasetName: string, + next: GraphURIState, + ): DatasetState { + const byDataset = new Map(state.graphs); + byDataset.set(datasetName, next); + return { ...state, graphs: byDataset }; + } + + async function load(datasetName: string, force = false) { + if (!datasetName) return; + + const state = get(store); + const dsState = getDatasetState(state, datasetName); + + if (!force && dsState.data !== null) return; + if (dsState.pending !== null) return dsState.pending; + + const promise = (async () => { + try { + const { data, error } = await listGraphs({ + path: { datasetName }, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Failed to load graphs for dataset "${datasetName}":`, + await describeError(error), + ); + update(s => + setDatasetState(s, datasetName, { + ...getDatasetState(s, datasetName), + pending: null, + error, + }), + ); + return; + } + + update(s => + setDatasetState(s, datasetName, { + data: data ?? [], + fetchedAt: Date.now(), + pending: null, + error: null, + }), + ); + } catch (err) { + console.error( + `${LOG_PREFIX} Unexpected error while loading graphs for dataset "${datasetName}":`, + err, + ); + update(s => + setDatasetState(s, datasetName, { + ...getDatasetState(s, datasetName), + pending: null, + error: err, + }), + ); + } + })(); + + update(s => + setDatasetState(s, datasetName, { + ...getDatasetState(s, datasetName), + pending: promise, + }), + ); + + return promise; + } + + function getGraphs(datasetName: string): Uri[] | null { + return getDatasetState(get(store), datasetName).data; + } + + async function addEmptyGraph( + datasetName: string, + graphURI: string, + ): Promise { + console.log( + `${LOG_PREFIX} Adding empty graph "${graphURI}" to dataset "${datasetName}"`, + ); + + const { error } = await replaceGraphs({ + path: { datasetName }, + }); + + if (error) { + console.error( + 'Failed to create empty graph "${graphURI}" to dataset "${datasetName}"`', + ); + toastStore.error( + "Create failed", + `Could not create schema "${graphURI}".`, + ); + return { error }; + } + + invalidateDataset(datasetName); + console.log(`${LOG_PREFIX} Added empty graph "${graphURI}"`); + toastStore.success( + "Schema created", + `"${graphURI}" was added to "${datasetName}".`, + ); + return { error: null }; + } + + async function importGraphs( + datasetName: string, + body: FormData, + ): Promise> { + console.log( + `${LOG_PREFIX} Importing graphs into dataset "${datasetName}"`, + ); + + if (!(body instanceof FormData) || !body.has("files")) { + const error = new Error( + "At least one file is required for import.", + ); + console.error(`${LOG_PREFIX} ${error.message}`); + toastStore.error("Import failed", "No files were selected."); + return { error }; + } + + const { data, error } = await replaceGraphs({ + path: { datasetName }, + body, + }); + + if (error) { + console.error( + `${LOG_PREFIX} Failed to import graphs`, + await describeError(error), + ); + toastStore.error( + "Import failed", + `Could not import into "${datasetName}".`, + ); + return { error }; + } + + invalidateDataset(datasetName); + + const importedGraphUris = data?.importedGraphUris ?? []; + const failedImports = data?.failedImports ?? []; + const importedCount = importedGraphUris.length; + + if (importedCount === 0) { + toastStore.error( + "Import failed", + failedImports.length > 0 + ? `${failedImports.length} file(s) could not be imported.` + : "No graphs were imported.", + ); + return { error: null, data }; + } + + if (failedImports.length > 0) { + toastStore.warning( + "Import partially succeeded", + `${importedCount} graph(s) imported, ${failedImports.length} file(s) skipped.`, + ); + } else { + toastStore.success( + "Import complete", + `${importedCount} graph${importedCount === 1 ? "" : "s"} imported.`, + ); + } + + console.log(`${LOG_PREFIX} Imported ${importedCount} graphs`); + return { error: null, data }; + } + + async function removeGraph( + datasetName: string, + graphURI: string, + ): Promise { + console.log( + `${LOG_PREFIX} Deleting graph "${graphURI}" from dataset "${datasetName}"`, + ); + + const { error } = await deleteGraph({ + path: { datasetName, graphURI }, + }); + + if (error) { + const msg = await describeError(error); + console.error( + `${LOG_PREFIX} Could not delete graph "${graphURI}" from dataset "${datasetName}":`, + msg, + ); + toastStore.error( + "Delete failed", + `Could not delete schema "${graphURI}".`, + ); + return { error }; + } + + update(s => { + const dsState = getDatasetState(s, datasetName); + const nextData = + dsState.data?.filter(g => { + const uri = `${g.prefix ?? ""}${g.suffix}`; + return uri !== graphURI; + }) ?? null; + + return setDatasetState(s, datasetName, { + ...dsState, + data: nextData, + }); + }); + + console.log( + `${LOG_PREFIX} Deleted graph "${graphURI}" from dataset "${datasetName}"`, + ); + toastStore.success("Schema deleted", `"${graphURI}" was removed.`); + + return { error: null }; + } + + function invalidateDataset(datasetName: string) { + update(s => { + const byDataset = new Map(s.graphs); + byDataset.delete(datasetName); + return { ...s, graphs: byDataset }; + }); + } + + function invalidateAll() { + update(() => ({ graphs: new Map() })); + } + + return { + subscribe, + load, + getGraphs, + addEmptyGraph, + importGraphs, + remove: removeGraph, + invalidateDataset, + invalidateAll, + }; +} diff --git a/frontend/src/lib/stores/GraphURIStore.ts b/frontend/src/lib/stores/GraphURIStore.ts deleted file mode 100644 index 75988daf..00000000 --- a/frontend/src/lib/stores/GraphURIStore.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (c) 2024-2026 SOPTIM AG - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { writable, get } from "svelte/store"; - -import { listGraphs, deleteGraph, Uri } from "../api/generated"; - -type GraphURIState = { - data: Uri[] | null; - fetchedAt: number | null; - pending: Promise | null; - error: unknown; -}; - -type DatasetState = { - graphs: Map; -}; - -export const graphURIStore = createGrapURIStore(); - -function createEmptyDatasetState(): GraphURIState { - return { - data: null, - fetchedAt: null, - pending: null, - error: null, - }; -} - -function createGrapURIStore() { - const store = writable({ - graphs: new Map(), - }); - - const { subscribe, update } = store; - - function getDatasetState(state: DatasetState, datasetName: string): GraphURIState { - return state.graphs.get(datasetName) ?? createEmptyDatasetState(); - } - - function setDatasetState( - state: DatasetState, - datasetName: string, - next: GraphURIState, - ): DatasetState { - const byDataset = new Map(state.graphs); - byDataset.set(datasetName, next); - return { ...state, graphs: byDataset }; - } - - async function load(datasetName: string, force = false) { - if (!datasetName) return; - - const state = get(store); - const dsState = getDatasetState(state, datasetName); - - if (!force && dsState.data !== null) return; - if (dsState.pending !== null) return dsState.pending; - - const promise = (async () => { - try { - const { data, error } = await listGraphs({ - path: { datasetName }, - }); - - if (error) { - update((s) => - setDatasetState(s, datasetName, { - ...getDatasetState(s, datasetName), - pending: null, - error, - }), - ); - return; - } - - update((s) => - setDatasetState(s, datasetName, { - data: data ?? [], - fetchedAt: Date.now(), - pending: null, - error: null, - }), - ); - } catch (err) { - update((s) => - setDatasetState(s, datasetName, { - ...getDatasetState(s, datasetName), - pending: null, - error: err, - }), - ); - } - })(); - - update((s) => - setDatasetState(s, datasetName, { - ...getDatasetState(s, datasetName), - pending: promise, - }), - ); - - return promise; - } - - function getGraphURIs(datasetName: string): Uri[] | null { - return getDatasetState(get(store), datasetName).data; - } - - function getStateForDataset(datasetName: string): GraphURIState { - return getDatasetState(get(store), datasetName); - } - - async function removeGraph(datasetName: string, graphURI: string) { - const { error } = await deleteGraph({ - path: { datasetName, graphURI }, - }); - - if (error) return { error }; - - update((s) => { - const dsState = getDatasetState(s, datasetName); - const nextData = - dsState.data?.filter((g) => { - const uri = `${g.prefix ?? ""}${g.suffix}`; - return uri !== graphURI; - }) ?? null; - - return setDatasetState(s, datasetName, { - ...dsState, - data: nextData, - }); - }); - - return { error: null }; - } - - function invalidateDataset(datasetName: string) { - update((s) => { - const byDataset = new Map(s.graphs); - byDataset.delete(datasetName); - return { ...s, byDataset }; - }); - } - - function invalidateAll() { - update(() => ({ graphs: new Map() })); - } - - return { - subscribe, - load, - getGraphURIs, - getStateForDataset, - remove: removeGraph, - invalidateDataset, - invalidateAll, - }; -} \ No newline at end of file diff --git a/frontend/src/lib/stores/PackageStore.ts b/frontend/src/lib/stores/PackageStore.ts index e4de77b9..eb1930ff 100644 --- a/frontend/src/lib/stores/PackageStore.ts +++ b/frontend/src/lib/stores/PackageStore.ts @@ -17,7 +17,15 @@ import { writable, get } from "svelte/store"; -import { listPackages, PackageDto } from "../api/generated"; +import { describeError } from "./StoreLogging"; +import { + listPackages, + addPackage, + replacePackage, + PackageDto, + deletePackage, +} from "../api/generated"; +import { toastStore } from "../eventhandling/toastStore.svelte.js"; type PackageListInfo = { internal: PackageDto[]; @@ -32,11 +40,15 @@ type GraphPackageState = { }; type PackagesState = { - byGraph: Map; + byGraph: Map; }; type GraphKey = `${string}::${string}`; +type Result = { error: unknown; data?: T }; + +const LOG_PREFIX = "[packageStore]"; + export const packageStore = createPackageStore(); function makeKey(datasetName: string, graphURI: string): GraphKey { @@ -52,14 +64,18 @@ function createEmptyGraphState(): GraphPackageState { }; } -function createPackageStore() { - const store = writable({ - byGraph: new Map(), - }); +function getPackageDisplayLabel(pkg: PackageDto): string { + return pkg.label ?? pkg.uuid ?? "(unnamed)"; +} +function createPackageStore() { + const store = writable({ byGraph: new Map() }); const { subscribe, update } = store; - function getGraphState(state: PackagesState, key: GraphKey): GraphPackageState { + function getGraphState( + state: PackagesState, + key: GraphKey, + ): GraphPackageState { return state.byGraph.get(key) ?? createEmptyGraphState(); } @@ -73,12 +89,13 @@ function createPackageStore() { return { ...state, byGraph }; } + // ----- Load ----- + async function load(datasetName: string, graphURI: string, force = false) { if (!datasetName || !graphURI) return; const key = makeKey(datasetName, graphURI); - const state = get(store); - const graphState = getGraphState(state, key); + const graphState = getGraphState(get(store), key); if (!force && graphState.data !== null) return; if (graphState.pending !== null) return graphState.pending; @@ -90,7 +107,12 @@ function createPackageStore() { }); if (error) { - update((s) => + console.error( + `${LOG_PREFIX} Failed to load packages for`, + { datasetName, graphURI }, + await describeError(error), + ); + update(s => setGraphState(s, key, { ...getGraphState(s, key), pending: null, @@ -100,7 +122,7 @@ function createPackageStore() { return; } - update((s) => + update(s => setGraphState(s, key, { data: { internal: data?.internalPackageList ?? [], @@ -112,7 +134,12 @@ function createPackageStore() { }), ); } catch (err) { - update((s) => + console.error( + `${LOG_PREFIX} Unexpected error while loading packages for`, + { datasetName, graphURI }, + err, + ); + update(s => setGraphState(s, key, { ...getGraphState(s, key), pending: null, @@ -122,7 +149,7 @@ function createPackageStore() { } })(); - update((s) => + update(s => setGraphState(s, key, { ...getGraphState(s, key), pending: promise, @@ -132,13 +159,257 @@ function createPackageStore() { return promise; } - function getPackages(datasetName: string, graphURI: string): PackageListInfo | null { + function getPackages( + datasetName: string, + graphURI: string, + ): PackageListInfo | null { return getGraphState(get(store), makeKey(datasetName, graphURI)).data; } + // ----- Internal: local cache patch after a successful save ----- + + function patchLocalPackage( + datasetName: string, + graphURI: string, + pkg: PackageDto, + ) { + if (!datasetName || !graphURI || !pkg) return; + const key = makeKey(datasetName, graphURI); + + update(s => { + const current = getGraphState(s, key); + // If we haven't loaded the list yet, there's nothing meaningful to patch. + if (!current.data) return s; + + const currentData = current.data; + const pkgId = pkg.uuid; + + const internal = (() => { + if (!pkgId) { + if (currentData.internal.includes(pkg)) + return currentData.internal; + return [...currentData.internal, pkg]; + } + const idx = currentData.internal.findIndex( + p => p.uuid === pkgId, + ); + if (idx >= 0) { + const next = [...currentData.internal]; + next[idx] = pkg; + return next; + } + return [...currentData.internal, pkg]; + })(); + + // A newly added/updated package is always internal; make sure it's + // not lingering in the external list. + const external = pkgId + ? currentData.external.filter(p => p.uuid !== pkgId) + : currentData.external.filter(p => p !== pkg); + + return setGraphState(s, key, { + ...current, + data: { internal, external }, + fetchedAt: Date.now(), + error: null, + }); + }); + } + + function removeLocalPackage( + datasetName: string, + graphURI: string, + packageUUID: string, + ) { + if (!datasetName || !graphURI || !packageUUID) return; + const key = makeKey(datasetName, graphURI); + + update(s => { + const current = getGraphState(s, key); + if (!current.data) return s; + + const internal = current.data.internal.filter( + p => p.uuid !== packageUUID, + ); + const external = current.data.external.filter( + p => p.uuid !== packageUUID, + ); + + return setGraphState(s, key, { + ...current, + data: { internal, external }, + fetchedAt: Date.now(), + error: null, + }); + }); + } + + // ----- Mutations ----- + + async function addNewPackage( + datasetName: string, + graphURI: string, + pkg: PackageDto, + ): Promise> { + const label = getPackageDisplayLabel(pkg); + + console.log(`${LOG_PREFIX} Creating package "${label}" in`, { + datasetName, + graphURI, + }); + + const { data, error } = await addPackage({ + path: { datasetName, graphURI }, + body: pkg, + }); + + if (error) { + const msg = await describeError(error); + console.error( + `${LOG_PREFIX} Could not create package "${label}":`, + msg, + ); + toastStore.error( + "Save failed", + `Could not save package "${label}".`, + ); + return { error }; + } + + // Prefer server-issued uuid; fall back to whatever the caller sent. + const newUUID = data ?? pkg.uuid; + const stored: PackageDto = { ...pkg, uuid: newUUID }; + patchLocalPackage(datasetName, graphURI, stored); + + console.log( + `${LOG_PREFIX} Created package "${label}" (uuid=${newUUID})`, + ); + toastStore.success("Package created", `"${label}" was saved.`); + + return { error: null, data: newUUID }; + } + + async function replaceExistingPackage( + datasetName: string, + graphURI: string, + pkg: PackageDto, + ): Promise { + const label = getPackageDisplayLabel(pkg); + + if (!pkg.uuid) { + const error = new Error("pkg.uuid is required for replacePackage"); + console.error(`${LOG_PREFIX} ${error.message}`); + toastStore.error( + "Save failed", + `Could not save package "${label}".`, + ); + return { error }; + } + + console.log( + `${LOG_PREFIX} Replacing package "${label}" (uuid=${pkg.uuid}) in`, + { datasetName, graphURI }, + ); + + const { error } = await replacePackage({ + path: { datasetName, graphURI, packageUUID: pkg.uuid }, + body: pkg, + }); + + if (error) { + const msg = await describeError(error); + console.error( + `${LOG_PREFIX} Could not save package "${label}":`, + msg, + ); + toastStore.error( + "Save failed", + `Could not save package "${label}".`, + ); + return { error }; + } + + patchLocalPackage(datasetName, graphURI, pkg); + + console.log(`${LOG_PREFIX} Saved package "${label}"`); + toastStore.success("Package saved", `"${label}" was saved.`); + + return { error: null }; + } + + // Single convenience method that picks add vs. replace based on uuid + async function savePackage( + datasetName: string, + graphURI: string, + pkg: PackageDto, + ): Promise> { + if (pkg.uuid) { + const res = await replaceExistingPackage( + datasetName, + graphURI, + pkg, + ); + return res.error + ? { error: res.error } + : { error: null, data: pkg.uuid }; + } + return addNewPackage(datasetName, graphURI, pkg); + } + + async function deleteExistingPackage( + datasetName: string, + graphURI: string, + pkg: PackageDto, + ): Promise { + const label = getPackageDisplayLabel(pkg); + + if (!pkg.uuid) { + const error = new Error("pkg.uuid is required for deletePackage"); + console.error(`${LOG_PREFIX} ${error.message}`); + toastStore.error( + "Delete failed", + `Could not delete package "${label}".`, + ); + return { error }; + } + + console.log( + `${LOG_PREFIX} Deleting package "${label}" (uuid=${pkg.uuid}) from`, + { datasetName, graphURI }, + ); + + const { error } = await deletePackage({ + path: { datasetName, graphURI, packageUUID: pkg.uuid }, + }); + + if (error) { + const msg = await describeError(error); + console.error( + `${LOG_PREFIX} Could not delete package "${label}":`, + msg, + ); + toastStore.error( + "Delete failed", + `Could not delete package "${label}".`, + ); + return { error }; + } + + removeLocalPackage(datasetName, graphURI, pkg.uuid); + + console.log( + `${LOG_PREFIX} Deleted package "${label}" (uuid=${pkg.uuid})`, + ); + toastStore.success("Package deleted", `"${label}" was removed.`); + + return { error: null }; + } + + // ----- Invalidation ----- + function invalidateGraph(datasetName: string, graphURI: string) { const key = makeKey(datasetName, graphURI); - update((s) => { + update(s => { const byGraph = new Map(s.byGraph); byGraph.delete(key); return { ...s, byGraph }; @@ -146,12 +417,11 @@ function createPackageStore() { } function invalidateDataset(datasetName: string) { - update((s) => { + const prefix = `${datasetName}::`; + update(s => { const byGraph = new Map(s.byGraph); - for (const key of byGraph.keys()) { - if (key.startsWith(`${datasetName}::`)) { - byGraph.delete(key); - } + for (const k of byGraph.keys()) { + if (k.startsWith(prefix)) byGraph.delete(k); } return { ...s, byGraph }; }); @@ -165,8 +435,12 @@ function createPackageStore() { subscribe, load, getPackages, + addPackage: addNewPackage, + replacePackage: replaceExistingPackage, + deletePackage: deleteExistingPackage, + savePackage, invalidateGraph, invalidateDataset, invalidateAll, }; -} \ No newline at end of file +} diff --git a/frontend/src/lib/stores/StoreLogging.ts b/frontend/src/lib/stores/StoreLogging.ts new file mode 100644 index 00000000..771e73fc --- /dev/null +++ b/frontend/src/lib/stores/StoreLogging.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Try to coerce whatever the SDK gives us into a readable string for logging. +export async function describeError(error: unknown): Promise { + if (error == null) return "unknown error"; + if (typeof error === "string") return error; + if (error instanceof Error) return error.message; + if ( + typeof (error as { text?: () => Promise }).text === "function" + ) { + try { + return await (error as { text: () => Promise }).text(); + } catch { + /* ignore */ + } + } + try { + return JSON.stringify(error); + } catch { + return String(error); + } +} diff --git a/frontend/src/lib/stores/VersionControlStore.ts b/frontend/src/lib/stores/VersionControlStore.ts new file mode 100644 index 00000000..3551ad2d --- /dev/null +++ b/frontend/src/lib/stores/VersionControlStore.ts @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024-2026 SOPTIM AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { writable, get } from "svelte/store"; + +import { classStore } from "./ClassStore"; +import { packageStore } from "./PackageStore"; +import { + undo as sdkUndo, + redo as sdkRedo, + canUndo as sdkCanUndo, + canRedo as sdkCanRedo, +} from "../api/generated"; +import { toastStore } from "../eventhandling/toastStore.svelte.js"; + +type Flags = { + canUndo: boolean; + canRedo: boolean; + pending: Promise | null; +}; +type State = { byGraph: Map }; + +const LOG = "[versionControlStore]"; + +export const versionControlStore = createStore(); +const key = (d: string, g: string) => `${d}::${g}`; + +function emptyFlags(): Flags { + return { canUndo: false, canRedo: false, pending: null }; +} + +function createStore() { + const store = writable({ byGraph: new Map() }); + const { subscribe, update } = store; + + function patch(d: string, g: string, next: Partial) { + update(s => { + const m = new Map(s.byGraph); + const cur = m.get(key(d, g)) ?? emptyFlags(); + m.set(key(d, g), { ...cur, ...next }); + return { byGraph: m }; + }); + } + + async function refresh(d: string, g: string) { + if (!d || !g) return; + const [u, r] = await Promise.all([ + sdkCanUndo({ path: { datasetName: d, graphURI: g } }), + sdkCanRedo({ path: { datasetName: d, graphURI: g } }), + ]); + patch(d, g, { + canUndo: !u.error && u.data === true, + canRedo: !r.error && r.data === true, + }); + } + + function canUndo(d: string, g: string): boolean { + return get(store).byGraph.get(key(d, g))?.canUndo ?? false; + } + + function canRedo(d: string, g: string): boolean { + return get(store).byGraph.get(key(d, g))?.canRedo ?? false; + } + + async function doUndo(d: string, g: string) { + const { error } = await sdkUndo({ + path: { datasetName: d, graphURI: g }, + }); + if (error) { + console.error(`${LOG} undo failed`, error); + toastStore.error("Undo failed", "Could not undo the last change."); + return { error }; + } + toastStore.info("Undone"); + classStore.invalidateGraph(d, g); + packageStore.invalidateGraph(d, g); + await refresh(d, g); + return { error: null }; + } + + async function doRedo(d: string, g: string) { + const { error } = await sdkRedo({ + path: { datasetName: d, graphURI: g }, + }); + if (error) { + console.error(`${LOG} redo failed`, error); + toastStore.error("Redo failed", "Could not redo the change."); + return { error }; + } + toastStore.info("Redone"); + classStore.invalidateGraph(d, g); + packageStore.invalidateGraph(d, g); + await refresh(d, g); + return { error: null }; + } + + return { + subscribe, + refresh, + canUndo, + canRedo, + undo: doUndo, + redo: doRedo, + }; +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 8afd7fff..d574462b 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -19,13 +19,6 @@ import "../app.css"; import { onMount } from "svelte"; - import { enableEditing } from "$lib/actions/editingActions.js"; - import { - fetchCanRedo, - fetchCanUndo, - redo, - undo, - } from "$lib/actions/versionControlActions.js"; import { BackendConnection } from "$lib/api/backend.js"; import { installBackendFetchInterceptor, @@ -40,6 +33,7 @@ import { shortcutStore } from "$lib/eventhandling/shortcutStore.svelte.js"; import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { datasetStore } from "$lib/stores/DatasetStore.ts"; + import { versionControlStore } from "$lib/stores/VersionControlStore.ts"; import { editorState, @@ -77,7 +71,7 @@ forceReloadTrigger.subscribe(); await fetchUndoRedo(); isDatasetReadOnly = selectedDataset - ? await datasetStore.isReadOnly(selectedDataset) + ? datasetStore.isReadOnly(selectedDataset) : false; }); @@ -91,9 +85,12 @@ if (!selectedDataset || !isDatasetReadOnly) { return; } - if (!(await enableEditing(selectedDataset))) { - return; - } + const { error } = await datasetStore.updateReadonly( + selectedDataset, + false, + ); + if (error) return; + forceReloadTrigger.trigger(); editorState.selectedClassUUID.trigger(); editorState.selectedDiagram.trigger(); @@ -120,8 +117,8 @@ } async function fetchUndoRedo() { - canUndo = await fetchCanUndo(); - canRedo = await fetchCanRedo(); + canUndo = versionControlStore.canUndo(); + canRedo = versionControlStore.canRedo(); } async function reload() { @@ -155,9 +152,9 @@ async function handleUndoRedo(isRedo) { if (isRedo) { - if (canRedo && (await redo())) await reload(); + if (canRedo && (await versionControlStore.redo())) await reload(); } else { - if (canUndo && (await undo())) await reload(); + if (canUndo && (await versionControlStore.undo())) await reload(); } } diff --git a/frontend/src/routes/DatasetDeleteDialog.svelte b/frontend/src/routes/DatasetDeleteDialog.svelte index d8244c3a..8c544c1b 100644 --- a/frontend/src/routes/DatasetDeleteDialog.svelte +++ b/frontend/src/routes/DatasetDeleteDialog.svelte @@ -19,13 +19,12 @@ import { faExclamation } from "@fortawesome/free-solid-svg-icons"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; - import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { forceReloadTrigger, editorState, } from "$lib/sharedState.svelte.js"; import { datasetStore } from "$lib/stores/DatasetStore.ts"; - import { graphURIStore } from "$lib/stores/GraphURIStore.ts"; + import { graphStore } from "$lib/stores/GraphStore.ts"; let { showDialog = $bindable(), datasetName } = $props(); @@ -34,8 +33,8 @@ let graphs = $state(null); async function onOpen() { - await graphURIStore.load(datasetName); - graphs = graphURIStore.getGraphURIs(datasetName); + await graphStore.load(datasetName); + graphs = graphStore.getGraphURIs(datasetName); } function onClose() { @@ -46,24 +45,12 @@ try { if (datasetName) { const res = await datasetStore.remove(datasetName); - if (res.error) { - toastStore.error( - "Delete failed", - `Could not delete dataset "${datasetName}".`, - ); - return; - } + if (res.error) return; } if (editorState.selectedDataset.getValue() === datasetName) { editorState.reset(); } - if (datasetName) { - toastStore.success( - "Dataset deleted", - `"${datasetName}" was removed.`, - ); - } } finally { forceReloadTrigger.trigger(); } diff --git a/frontend/src/routes/GraphDeleteDialog.svelte b/frontend/src/routes/GraphDeleteDialog.svelte index 1118de46..60c0f3f1 100644 --- a/frontend/src/routes/GraphDeleteDialog.svelte +++ b/frontend/src/routes/GraphDeleteDialog.svelte @@ -18,9 +18,8 @@ diff --git a/frontend/src/routes/ImportDialog.svelte b/frontend/src/routes/ImportDialog.svelte index 00dc865f..69435777 100644 --- a/frontend/src/routes/ImportDialog.svelte +++ b/frontend/src/routes/ImportDialog.svelte @@ -21,10 +21,9 @@ import { v4 as uuidv4 } from "uuid"; import ButtonControl from "$lib/components/ButtonControl.svelte"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; - import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { datasetStore } from "$lib/stores/DatasetStore.ts"; + import { graphStore } from "$lib/stores/GraphStore.ts"; import { supportedRDFMediaTypes } from "$lib/utils/fileUtils"; import { @@ -78,6 +77,7 @@ function onClose() { clearInputs(); } + function clearInputs() { datasetNameUserInput = ""; files = []; @@ -200,109 +200,44 @@ return formData; } - function putFiles(files, datasetname) { - return fetch( - `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetname)}/graphs/content`, - { - method: "PUT", - body: buildRequestBody(files), - credentials: "include", - }, + async function importGraphs() { + const datasetNameUserInputLocal = getUserInputDatasetName(); + const { data } = await graphStore.importGraphs( + datasetNameUserInputLocal, + buildRequestBody(files), ); - } - async function parseResponse(response, datasetName) { - if (!response.ok) { - console.log("failed to insert data"); - toastStore.error( - "Import failed", - `Could not import into "${datasetName}".`, - ); - return; - } - const body = await response.json(); - console.log(body.message); - - const failedImports = body.failedImports ?? []; - if (failedImports.length > 0) { - console.warn("failed imports:", failedImports); - } - - //only update the selected dataset and graph if at least one import was successful, otherwise keep the old selection - const importedGraphUris = body.importedGraphUris ?? []; - if (importedGraphUris.length === 0) { - toastStore.error( - "Import failed", - failedImports.length > 0 - ? `${failedImports.length} file(s) could not be imported.` - : "No graphs were imported.", + if (data.importedGraphUris?.length > 0) { + editorState.selectedDataset.updateValue(datasetNameUserInputLocal); + editorState.selectedGraph.updateValue( + data.importedGraphUris[0] || null, ); - return; + editorState.selectedDiagram.updateValue({ type: null, id: null }); + editorState.selectedClassDataset.updateValue(null); + editorState.selectedClassGraph.updateValue(null); + editorState.selectedClassUUID.updateValue(null); + forceReloadTrigger.trigger(); } - console.log("imported graphs:", importedGraphUris); - - editorState.selectedDataset.updateValue(datasetName); - editorState.selectedGraph.updateValue(importedGraphUris[0]); - editorState.selectedDiagram.updateValue({ type: null, id: null }); - editorState.selectedClassDataset.updateValue(null); - editorState.selectedClassGraph.updateValue(null); - editorState.selectedClassUUID.updateValue(null); - const importedCount = importedGraphUris.length; - const summary = `${importedCount} graph${importedCount === 1 ? "" : "s"} imported into "${datasetName}".`; - if (failedImports.length > 0) { - toastStore.warning( - "Import partially succeeded", - `${summary} ${failedImports.length} file(s) were skipped.`, + function notifyUndisplayableProperties(warnings) { + if (warnings.length === 0) { + return; + } + const total = warnings.reduce( + (sum, warning) => + sum + (warning.undisplayableProperties?.length ?? 0), + 0, ); - } else { - toastStore.success("Import complete", summary); - } - - notifyUndisplayableProperties(body.warnings ?? []); - } - - function notifyUndisplayableProperties(warnings) { - if (warnings.length === 0) { - return; - } - const total = warnings.reduce( - (sum, warning) => - sum + (warning.undisplayableProperties?.length ?? 0), - 0, - ); - const details = warnings - .map( - warning => - `${warning.fileName}: ${(warning.undisplayableProperties ?? []).join(", ")}`, - ) - .join("; "); - toastStore.warning( - "Some properties could not be displayed", - `${total} propert${total === 1 ? "y" : "ies"} ${total === 1 ? "is" : "are"} missing the CIM stereotype or association metadata RDFArchitect needs to show ${total === 1 ? "it" : "them"} (${details}).`, - ); - } - - async function importGraphs() { - const datasetNameUserInputLocal = getUserInputDatasetName(); - const filesLocal = files; - console.warn( - "Importing files into dataset:", - datasetNameUserInputLocal, - ); - try { - const res = await putFiles(filesLocal, datasetNameUserInputLocal); - await parseResponse(res, datasetNameUserInputLocal); - await datasetStore.load(true); - } catch (e) { - console.log("failed to insert data:"); - console.log(e); - toastStore.error( - "Import failed", - "An unexpected error occurred while importing.", + const details = warnings + .map( + warning => + `${warning.fileName}: ${(warning.undisplayableProperties ?? []).join(", ")}`, + ) + .join("; "); + toastStore.warning( + "Some properties could not be displayed", + `${total} propert${total === 1 ? "y" : "ies"} ${total === 1 ? "is" : "are"} missing the CIM stereotype or association metadata RDFArchitect needs to show ${total === 1 ? "it" : "them"} (${details}).`, ); - } finally { - forceReloadTrigger.trigger(); } } diff --git a/frontend/src/routes/NamespacesDialog.svelte b/frontend/src/routes/NamespacesDialog.svelte index 9392287f..60ce1784 100644 --- a/frontend/src/routes/NamespacesDialog.svelte +++ b/frontend/src/routes/NamespacesDialog.svelte @@ -24,7 +24,6 @@ import TextEditControl from "$lib/components/TextEditControl.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; import ModifyDataDialog from "$lib/dialog/ModifyDataDialog.svelte"; - import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { mapNamespaceDtoToReactiveNamespace } from "$lib/models/reactive/mapper/map-dto-to-reactive-object.js"; import { mapReactiveNamespaceToNamespaceDto } from "$lib/models/reactive/mapper/map-reactive-object-to-dto.js"; import { ReactiveNamespace } from "$lib/models/reactive/models/reactive-namespace.svelte.js"; @@ -39,7 +38,6 @@ let { showDialog = $bindable() } = $props(); - let datasetName = $state(""); let readonly = $state(false); let namespaces = $state([]); @@ -85,26 +83,12 @@ const namespaceDTOs = plainReactiveNamespaces.map(namespace => { return mapReactiveNamespaceToNamespaceDto(namespace); }); - try { - const res = await datasetStore.saveNamespaces(datasetName, namespaceDTOs); - if (res && res.error) { - toastStore.error( - "Save failed", - `Could not save namespaces for "${datasetName}".`, - ); - return; - } - toastStore.success( - "Namespaces saved", - `Updated for "${datasetName}".`, - ); - } catch (err) { - console.error("Failed to save namespaces:", err); - toastStore.error( - "Save failed", - "An unexpected error occurred while saving namespaces.", - ); - } finally { + const { error } = await datasetStore.saveNamespaces( + datasetName, + namespaceDTOs, + ); + + if (!error) { forceReloadTrigger.trigger(); } } diff --git a/frontend/src/routes/NewClassDialog.svelte b/frontend/src/routes/NewClassDialog.svelte index 68c71e4a..8bc1a99b 100644 --- a/frontend/src/routes/NewClassDialog.svelte +++ b/frontend/src/routes/NewClassDialog.svelte @@ -23,11 +23,10 @@ import SelectEditControl from "$lib/components/SelectEditControl.svelte"; import TextEditControl from "$lib/components/TextEditControl.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; - import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { ReactiveValueWrapper } from "$lib/models/reactive/reactive-wrappers/reactive-value-wrapper.svelte.js"; import { isInvalidClassLabel } from "$lib/models/reactive/validity-rules/validityFunctions.js"; + import { classStore } from "$lib/stores/ClassStore.ts"; import { datasetStore } from "$lib/stores/DatasetStore.ts"; import { packageStore } from "$lib/stores/PackageStore.ts"; import { getPackageDisplayLabel } from "$lib/utils/package-label.js"; @@ -39,7 +38,6 @@ } from "../lib/sharedState.svelte.js"; import { getClasses } from "./mainpage/classEditor/fetch-class-editor-context.js"; - let { showDialog = $bindable(), lockedDatasetName, @@ -191,66 +189,35 @@ requestBody.classLayoutPosition = classLayoutPosition; } - try { - const res = await fetch( - PUBLIC_BACKEND_URL + - "/datasets/" + - encodeURIComponent(datasetNameLocal) + - "/graphs/" + - encodeURIComponent(graphURILocal) + - "/classes", - { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - }), - body: JSON.stringify(requestBody), - credentials: "include", - }, - ); - if (res.ok) { - const uuid = await res.text(); - console.log("successfully added class"); - onClassCreated({ - classUUID: uuid, - datasetName: datasetNameLocal, - graphURI: graphURILocal, - packageUUID: selectedPackageUUID, - className: classNameLocal.value, - }); - editorState.selectedDataset.updateValue(datasetNameLocal); - editorState.selectedGraph.updateValue(graphURILocal); - editorState.selectedDiagram.updateValue({ - type: DiagramType.PACKAGE, - id: selectedPackageUUID, - }); - editorState.selectedClassDataset.updateValue(datasetNameLocal); - editorState.selectedClassGraph.updateValue(graphURILocal); - editorState.selectedClassUUID.updateValue(uuid); - toastStore.success( - "Class created", - `"${classNameLocal.value}" was added.`, - ); - } else { - console.log("failed to insert data"); - toastStore.error( - "Create failed", - `Could not create class "${classNameLocal.value}".`, - ); - } - } catch (e) { - console.log("failed to add class:", e); - toastStore.error( - "Create failed", - "An unexpected error occurred while creating the class.", - ); - } finally { - forceReloadTrigger.trigger(); - editorState.selectedDataset.trigger(); - editorState.selectedGraph.trigger(); - editorState.selectedDiagram.trigger(); - editorState.selectedClassUUID.trigger(); - } + const { data, error } = await classStore.addClass( + datasetName, + graphURILocal, + requestBody, + ); + if (error) return; + + onClassCreated({ + classUUID: data, + datasetName: datasetNameLocal, + graphURI: graphURILocal, + packageUUID: selectedPackageUUID, + className: classNameLocal.value, + }); + editorState.selectedDataset.updateValue(datasetNameLocal); + editorState.selectedGraph.updateValue(graphURILocal); + editorState.selectedDiagram.updateValue({ + type: DiagramType.PACKAGE, + id: selectedPackageUUID, + }); + editorState.selectedClassDataset.updateValue(datasetNameLocal); + editorState.selectedClassGraph.updateValue(graphURILocal); + editorState.selectedClassUUID.updateValue(data); + + forceReloadTrigger.trigger(); + editorState.selectedDataset.trigger(); + editorState.selectedGraph.trigger(); + editorState.selectedDiagram.trigger(); + editorState.selectedClassUUID.trigger(); } diff --git a/frontend/src/routes/NewGraphDialog.svelte b/frontend/src/routes/NewGraphDialog.svelte index 91b6f0c3..978ae51a 100644 --- a/frontend/src/routes/NewGraphDialog.svelte +++ b/frontend/src/routes/NewGraphDialog.svelte @@ -18,12 +18,10 @@ diff --git a/frontend/src/routes/NewPackageDialog.svelte b/frontend/src/routes/NewPackageDialog.svelte index 18d8a15b..49f824db 100644 --- a/frontend/src/routes/NewPackageDialog.svelte +++ b/frontend/src/routes/NewPackageDialog.svelte @@ -16,17 +16,15 @@ --> diff --git a/frontend/src/routes/SnapshotDialog.svelte b/frontend/src/routes/SnapshotDialog.svelte index 1b6d20c6..f887bb55 100644 --- a/frontend/src/routes/SnapshotDialog.svelte +++ b/frontend/src/routes/SnapshotDialog.svelte @@ -42,8 +42,8 @@ const datasetSelectionLocked = $derived(!!lockedDatasetName); function onOpen() { - datasetName = lockedDatasetName ?? editorState.selectedDataset.getValue(); - + datasetName = + lockedDatasetName ?? editorState.selectedDataset.getValue(); } async function snapshotDataset() { diff --git a/frontend/src/routes/changelog/Changes.svelte b/frontend/src/routes/changelog/Changes.svelte index 8ace1cc7..83be8e51 100644 --- a/frontend/src/routes/changelog/Changes.svelte +++ b/frontend/src/routes/changelog/Changes.svelte @@ -39,7 +39,8 @@ $effect(async () => { forceReloadTrigger.subscribe(); if (selectedDatasetName) { - readonlyDataset = await datasetStore.isReadOnly(selectedDatasetName); + readonlyDataset = + await datasetStore.isReadOnly(selectedDatasetName); } }); diff --git a/frontend/src/routes/changelog/Navigation.svelte b/frontend/src/routes/changelog/Navigation.svelte index 88cf6865..2e634c62 100644 --- a/frontend/src/routes/changelog/Navigation.svelte +++ b/frontend/src/routes/changelog/Navigation.svelte @@ -26,7 +26,7 @@ editorState, } from "$lib/sharedState.svelte.js"; import { datasetStore } from "$lib/stores/DatasetStore.ts"; - import { graphURIStore } from "$lib/stores/GraphURIStore.ts"; + import { graphStore } from "$lib/stores/GraphStore.ts"; import { getUri } from "../mainpage/packageNavigation/packageNavigationUtils.svelte.js"; @@ -52,10 +52,12 @@ graphs: [], showContents: showDatasetContents, }); - await graphURIStore.load(datasetName); - graphURIStore.getGraphNames(datasetName).forEach(graphUri => - newDatasetList.at(-1).graphs.push(graphUri), - ); + await graphStore.load(datasetName); + graphStore + .getGraphNames(datasetName) + .forEach(graphUri => + newDatasetList.at(-1).graphs.push(graphUri), + ); } datasetList = newDatasetList; } diff --git a/frontend/src/routes/layout/menu-bar/Edit.svelte b/frontend/src/routes/layout/menu-bar/Edit.svelte index 8ee7fad1..c5d068a4 100644 --- a/frontend/src/routes/layout/menu-bar/Edit.svelte +++ b/frontend/src/routes/layout/menu-bar/Edit.svelte @@ -17,31 +17,23 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js index e8ffe42b..11d27c79 100644 --- a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js +++ b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js @@ -17,13 +17,12 @@ import { get } from "svelte/store"; -import { BackendConnection } from "$lib/api/backend.js"; -import { PUBLIC_BACKEND_URL } from "$lib/config/runtime.js"; import { URI } from "$lib/models/dto/index.ts"; import { NavEntry } from "$lib/models/nav/NavEntry.svelte.js"; import { DiagramType, editorState } from "$lib/sharedState.svelte.js"; +import { classStore } from "$lib/stores/ClassStore.ts"; import { datasetStore } from "$lib/stores/DatasetStore.ts"; -import { graphURIStore } from "$lib/stores/GraphURIStore.ts"; +import { graphStore } from "$lib/stores/GraphStore.ts"; import { packageStore } from "$lib/stores/PackageStore.ts"; import { getPackageDisplayLabel } from "$lib/utils/package-label.js"; @@ -34,8 +33,6 @@ import { isSelectedClass, } from "./packageNavigationUtils.svelte.js"; -const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - /** * @description Reuses an existing NavEntry by id or creates a new one. Preserves isOpen. * @param {NavEntry[]} existingList @@ -65,7 +62,10 @@ export async function getNavEntryList(existingDatasetNavList) { const freshEntries = datasets .sort((a, b) => a.label.localeCompare(b.label)) .map(dataset => - reuseOrCreate(existingDatasetNavList, { label: dataset.label, id: dataset.label }), + reuseOrCreate(existingDatasetNavList, { + label: dataset.label, + id: dataset.label, + }), ); const result = existingDatasetNavList ?? []; @@ -108,8 +108,8 @@ async function populateDataset(datasetNavEntry) { async function getGraphNames(datasetName) { try { - await graphURIStore.load(datasetName); - return graphURIStore.getGraphURIs(datasetName); + await graphStore.load(datasetName); + return graphStore.getGraphURIs(datasetName); } catch (err) { console.error( "Error fetching graph names for dataset " + datasetName, @@ -122,9 +122,16 @@ async function getGraphNames(datasetName) { export async function populateGraph(datasetNavObject, graphNavObject) { const existingPackageList = graphNavObject.children; await packageStore.load(datasetNavObject.id, graphNavObject.id); - const packageData = packageStore.getPackages(datasetNavObject.id, graphNavObject.id); + const packageData = packageStore.getPackages( + datasetNavObject.id, + graphNavObject.id, + ); - const allClasses = await getClasses(datasetNavObject.id, graphNavObject.id); + await classStore.load(datasetNavObject.id, graphNavObject.id); + const allClasses = await classStore.getClasses( + datasetNavObject.id, + graphNavObject.id, + ); const freshEntries = [ ...packageData.internal.map(pack => @@ -232,19 +239,3 @@ function populatePackage(packageNavObject, allClasses, datasetId, graphId) { return packageNavObject; } - -async function getClasses(datasetName, graphURI) { - try { - const res = await bec.getClasses(datasetName, graphURI); - return await res.json(); - } catch (err) { - console.error( - "Error fetching classes for dataset " + - datasetName + - " and graph " + - graphURI, - err, - ); - return []; - } -} diff --git a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/AddToDatasetDiagramDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/AddToDatasetDiagramDialog.svelte index fcb6c286..8727a01c 100644 --- a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/AddToDatasetDiagramDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/AddToDatasetDiagramDialog.svelte @@ -16,12 +16,10 @@ --> diff --git a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDiagramDeleteDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDiagramDeleteDialog.svelte index 60486ae7..17b0a7f7 100644 --- a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDiagramDeleteDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDiagramDeleteDialog.svelte @@ -18,65 +18,40 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomGraphDiagramDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomGraphDiagramDialog.svelte index 2926ca09..2eead481 100644 --- a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomGraphDiagramDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomGraphDiagramDialog.svelte @@ -16,19 +16,17 @@ --> diff --git a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/RemoveFromDiagramDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/RemoveFromDiagramDialog.svelte index 21702213..05edf258 100644 --- a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/RemoveFromDiagramDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/RemoveFromDiagramDialog.svelte @@ -18,11 +18,9 @@ diff --git a/frontend/src/routes/mainpage/packageNavigation/CustomDiagramsSection.svelte b/frontend/src/routes/mainpage/packageNavigation/CustomDiagramsSection.svelte index 379fddf9..8845aa6f 100644 --- a/frontend/src/routes/mainpage/packageNavigation/CustomDiagramsSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/CustomDiagramsSection.svelte @@ -19,26 +19,16 @@ import { faObjectGroup } from "@fortawesome/free-solid-svg-icons"; import { onMount } from "svelte"; - import { BackendConnection } from "$lib/api/backend.js"; import NavigationEntry from "$lib/components/navigation/NavigationEntry.svelte"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime.js"; - import { - DiagramType, - editorState, - forceReloadTrigger, - } from "$lib/sharedState.svelte.js"; + import { DiagramType, editorState, forceReloadTrigger } from "$lib/sharedState.svelte.js"; + import { customDiagramStore } from "$lib/stores/DiagramStore.ts"; import CustomDiagramButton from "./CustomDiagramButton.svelte"; - import { - isSelectedDataset, - isSelectedGraph, - } from "./packageNavigationUtils.svelte.js"; + import { isSelectedDataset, isSelectedGraph } from "./packageNavigationUtils.svelte.js"; let { datasetNavEntry, graphNavEntry, allGraphNavEntries, readOnly } = $props(); - - const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - + let diagramsExpanded = $state(false); let diagrams = $state([]); let classesByDiagram = $state({}); @@ -82,12 +72,11 @@ try { let diagramList; if (graphNavEntry) { - diagramList = await getGraphDiagrams( - datasetNavEntry.id, - graphNavEntry.id, - ); + await customDiagramStore.loadGraphDiagrams(datasetNavEntry.id, graphNavEntry.id); + diagramList = await customDiagramStore.getGraphDiagrams(datasetNavEntry.id, graphNavEntry.id); } else { - diagramList = await getDatasetDiagrams(datasetNavEntry.id); + await customDiagramStore.loadDatasetDiagrams(datasetNavEntry.id); + diagramList = await customDiagramStore.getDatasetDiagrams(datasetNavEntry.id); } const previous = diagrams ?? []; const selectedDiagramId = @@ -177,15 +166,6 @@ }); } - async function getGraphDiagrams(datasetName, graphURI) { - const res = await bec.getCustomDiagramsForGraph(datasetName, graphURI); - return await res.json(); - } - - async function getDatasetDiagrams(datasetName) { - const res = await bec.getCustomDiagramsForDataset(datasetName); - return await res.json(); - } {#if diagrams.length > 0} diff --git a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte index aed3fafc..0580f547 100644 --- a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte @@ -36,14 +36,13 @@ } from "@fortawesome/free-solid-svg-icons"; import { getContext } from "svelte"; - import { BackendConnection } from "$lib/api/backend.js"; import { ContextMenu } from "$lib/components/bitsui/contextmenu"; import NavigationEntry from "$lib/components/navigation/NavigationEntry.svelte"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import { editorState, forceReloadTrigger, } from "$lib/sharedState.svelte.js"; + import { ontologyStore } from "$lib/stores/OntologyStore.ts"; import { versionControlStore } from "$lib/stores/VersionControlStore.ts"; import { shortenIri } from "$lib/utils/iri.js"; @@ -70,8 +69,6 @@ readonly = false, } = $props(); - const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - let ontology = $state(); let showExportDialog = $state(false); let showDeleteDialog = $state(false); @@ -108,7 +105,9 @@ }); async function initialize() { - ontology = await getOntology(); + await ontologyStore.loadOntology(datasetNavEntry.id, graphNavEntry.id); + const { data } = await ontologyStore.getOntologyForGraph(datasetNavEntry.id, graphNavEntry.id); + ontology = data; canUndo = versionControlStore.canUndo( datasetNavEntry.id, graphNavEntry.id, @@ -119,18 +118,6 @@ ); } - async function getOntology() { - const res = await bec.getOntology( - datasetNavEntry.label, - graphNavEntry.id, - ); - let content = await res.text(); - if (!content) { - return null; - } - return JSON.parse(content); - } - function focusGraphContext() { const nextDataset = datasetNavEntry.label; const nextGraph = graphNavEntry.id; diff --git a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js index 11d27c79..97cb226b 100644 --- a/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js +++ b/frontend/src/routes/mainpage/packageNavigation/build-nav-object.js @@ -107,16 +107,8 @@ async function populateDataset(datasetNavEntry) { } async function getGraphNames(datasetName) { - try { - await graphStore.load(datasetName); - return graphStore.getGraphURIs(datasetName); - } catch (err) { - console.error( - "Error fetching graph names for dataset " + datasetName, - err, - ); - return []; - } + await graphStore.load(datasetName); + return graphStore.getGraphs(datasetName) ?? []; } export async function populateGraph(datasetNavObject, graphNavObject) { diff --git a/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte index 65a8ca04..c6e69395 100644 --- a/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/OntologyDialog.svelte @@ -19,19 +19,17 @@ import { faRotateLeft, faSave } from "@fortawesome/free-solid-svg-icons"; import { Fa } from "svelte-fa"; - import { BackendConnection } from "$lib/api/backend.js"; import { DropdownMenu } from "$lib/components/bitsui/dropdown/index.js"; import ButtonControl from "$lib/components/ButtonControl.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import SearchableSelect from "$lib/components/SearchableSelect.svelte"; import ViolationMessages from "$lib/components/ViolationMessages.svelte"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; import DiscardCancelConfirmDialog from "$lib/dialog/DiscardCancelConfirmDialog.svelte"; - import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { ReactiveOntology } from "$lib/models/reactive/models/ontology/reactive-ontology.svelte.js"; import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; import { datasetStore } from "$lib/stores/DatasetStore.ts"; + import { ontologyStore } from "$lib/stores/OntologyStore.ts"; import AddKnownFieldsDialog from "./AddKnownFieldsDialog.svelte"; import OntologyEntryRow from "./OntologyEntryRow.svelte"; @@ -46,8 +44,6 @@ onSubmit, } = $props(); - const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - // Namespaces that the backend auto-adds when an ontology is created. // Added client-side here (without persisting) so they already appear // in the namespace selection while the dialog is open. @@ -165,30 +161,18 @@ if (!graphUri) { return null; } - const res = await bec.getOntology(dataset, graphUri); - let content = await res.text(); - if (!content) { - return null; - } - return JSON.parse(content); + await ontologyStore.loadOntology(dataset, graphUri); + const { data } = await ontologyStore.getOntologyForGraph(dataset, graphUri); + return data; } async function saveOntology(datasetName, graphUri, ontologyObject) { const serializable = ontologyObject.getPlainObject(); - const res = ontologyObject.uuid.value - ? await bec.putOntology(datasetName, graphUri, serializable) - : await bec.postOntology(datasetName, graphUri, serializable); - if (res && res.ok === false) { - toastStore.error( - "Save failed", - "Could not save the profile header.", - ); - return; + if (ontologyObject.uuid.value) { + await ontologyStore.replaceOntology(datasetName, graphUri, serializable); + } else { + await ontologyStore.createOntology(datasetName, graphUri, serializable); } - toastStore.success( - "Profile header saved", - "The profile header was saved.", - ); } function scrollEntriesToBottom() { From bb589dbf18703bec3fd54b6ba7254b373d6136ec Mon Sep 17 00:00:00 2001 From: Finn Tarnowsky Date: Thu, 11 Jun 2026 16:08:48 +0200 Subject: [PATCH 06/22] Template syntax in string literal Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Signed-off-by: Finn Tarnowsky --- frontend/src/lib/stores/GraphStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/stores/GraphStore.ts b/frontend/src/lib/stores/GraphStore.ts index be62c06c..0fdf8eac 100644 --- a/frontend/src/lib/stores/GraphStore.ts +++ b/frontend/src/lib/stores/GraphStore.ts @@ -159,7 +159,7 @@ function createGraphStore() { if (error) { console.error( - 'Failed to create empty graph "${graphURI}" to dataset "${datasetName}"`', + `Failed to create empty graph "${graphURI}" to dataset "${datasetName}"`, ); toastStore.error( "Create failed", From 379de4a8cae248479eb699f573597500afcc54db Mon Sep 17 00:00:00 2001 From: tarn Date: Thu, 18 Jun 2026 16:09:38 +0200 Subject: [PATCH 07/22] use stores in frontend --- ...ntologyKnownInformationRESTController.java | 11 +- ...hemaComparisonFromFilesRESTController.java | 3 +- .../datasets/DatasetRESTController.java | 3 +- .../GlobalClassLayoutDataRESTController.java | 4 +- .../CustomDatasetDiagramsRESTController.java | 2 +- .../graphs/AllPackagesRESTController.java | 79 ++-- .../GraphBulkContentRESTController.java | 1 - ...OntologyGenerateEntriesRESTController.java | 2 +- .../SchemaComparisonRESTController.java | 6 +- .../CustomDiagramsRESTController.java | 2 +- .../MigrationContextRESTController.java | 3 +- .../org/rdfarchitect/api/dto/DatasetDTO.java | 2 + .../services/select/ListDatasetsUseCase.java | 5 +- frontend/src/lib/GraphExport.svelte | 13 +- frontend/src/lib/api/apiGlobalUtils.js | 31 -- frontend/src/lib/api/backend.js | 261 -------------- .../DatasetAndGraphSelection.svelte | 4 +- .../SvelteFlowPaneContextMenu.svelte | 20 +- .../svelteflow/svelteFlowWrapper.svelte | 51 +-- frontend/src/lib/stores/ClassStore.ts | 158 +++----- frontend/src/lib/stores/DatasetStore.ts | 7 +- frontend/src/lib/stores/DatatypesStore.ts | 336 ++++++++++++++++++ frontend/src/lib/stores/GraphStore.ts | 2 +- frontend/src/lib/stores/OntologyStore.ts | 98 ++++- .../src/lib/stores/VersionControlStore.ts | 46 +-- frontend/src/lib/stores/XSDDatatypesStore.ts | 76 ++++ frontend/src/routes/+layout.svelte | 9 +- .../src/routes/DatasetDeleteDialog.svelte | 2 +- frontend/src/routes/ImportDialog.svelte | 7 +- frontend/src/routes/Searchbar.svelte | 25 +- frontend/src/routes/SnapshotDialog.svelte | 18 +- frontend/src/routes/changelog/Changes.svelte | 24 +- .../src/routes/changelog/ChangesRow.svelte | 16 +- .../src/routes/changelog/Navigation.svelte | 5 +- .../src/routes/compare/CompareDialog.svelte | 30 +- .../DeleteDependenciesDialog.svelte | 32 +- .../src/routes/layout/menu-bar/Edit.svelte | 5 +- .../mainpage/classEditor/classEditor.svelte | 11 +- .../components/ClassEditorButtons.svelte | 3 +- .../enum-entries/EnumEntryEditorDialog.svelte | 35 +- .../save-enum-entry-to-backend.js | 51 --- .../classEditor/fetch-class-editor-context.js | 39 +- .../CustomDiagramButton.svelte | 4 +- .../CustomDiagramsSection.svelte | 32 +- .../ExtendClassDialog.svelte | 55 +-- .../packageNavigation/GraphSection.svelte | 5 +- .../CustomDatasetDiagramDialog.svelte | 27 +- .../customDiagramDialogUtils.js | 52 ++- .../AddKnownFieldsDialog.svelte | 16 +- .../OntologyDialog.svelte | 17 +- .../OntologyEntryRow.svelte | 8 +- .../save-copy-class-to-backend.js | 51 +-- .../routes/mainpage/renderingWrapper.svelte | 70 ++-- .../routes/migrate/steps/SelectSchemas.svelte | 11 +- 54 files changed, 967 insertions(+), 919 deletions(-) delete mode 100644 frontend/src/lib/api/apiGlobalUtils.js create mode 100644 frontend/src/lib/stores/DatatypesStore.ts create mode 100644 frontend/src/lib/stores/XSDDatatypesStore.ts delete mode 100644 frontend/src/routes/mainpage/classEditor/components/enum-entries/save-enum-entry-to-backend.js diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/OntologyKnownInformationRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/OntologyKnownInformationRESTController.java index 9788c1b5..352786d9 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/OntologyKnownInformationRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/OntologyKnownInformationRESTController.java @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -57,10 +58,16 @@ public class OntologyKnownInformationRESTController { content = @Content( mediaType = "application/json", - schema = @Schema(implementation = OntologyField.class))) + array = + @ArraySchema( + schema = + @Schema( + implementation = + OntologyField + .class)))) }) @GetMapping - public List getOntology( + public List getKnownOntologyFields( @Parameter(description = "The name/url of the inquirer.") @RequestHeader( value = HttpHeaders.ORIGIN, diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/SchemaComparisonFromFilesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/SchemaComparisonFromFilesRESTController.java index af7ca7a8..4056c6f5 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/SchemaComparisonFromFilesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/SchemaComparisonFromFilesRESTController.java @@ -53,6 +53,7 @@ public class SchemaComparisonFromFilesRESTController { @Operation( summary = "compare schemas", description = "Compare two given graphs", + tags = {"comparison"}, responses = { @ApiResponse( responseCode = "200", @@ -68,7 +69,7 @@ public class SchemaComparisonFromFilesRESTController { .class)))) }) @PostMapping - public List compareSchemas( + public List compareSchemasFromFiles( @Parameter(description = "The name/url of the inquirer.") @RequestHeader( value = HttpHeaders.ORIGIN, diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/DatasetRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/DatasetRESTController.java index 1332e1e6..84058152 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/DatasetRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/DatasetRESTController.java @@ -51,7 +51,8 @@ public class DatasetRESTController { @Operation( summary = "List datasets", - description = "Lists all non-snapshots datasets including their prefixes and read-only status.", + description = + "Lists all non-snapshots datasets including their prefixes and read-only status.", tags = {"dataset"}, responses = {@ApiResponse(responseCode = "200")}) @GetMapping diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/GlobalClassLayoutDataRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/GlobalClassLayoutDataRESTController.java index e109d105..b6994a34 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/GlobalClassLayoutDataRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/GlobalClassLayoutDataRESTController.java @@ -50,12 +50,12 @@ public class GlobalClassLayoutDataRESTController { private final UpdateClassPositionsUseCase updateClassPositionsUseCase; @Operation( - summary = "updates class positions", + summary = "updates class positions on dataset level", description = "Updates the positions for all the classes provided in the request body with the provided coordinates.", tags = {"diagram", "layout", "class"}) @PutMapping - public String updateClassPositions( + public String updateDatasetClassPositions( @Parameter(description = "The name/url of the inquirer.") @RequestHeader(value = "origin", required = false, defaultValue = "unknown") String originURL, diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramsRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramsRESTController.java index e00b5712..c253d556 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramsRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/CustomDatasetDiagramsRESTController.java @@ -59,7 +59,7 @@ public class CustomDatasetDiagramsRESTController { private final ReplaceCustomDiagramUseCase replaceCustomDiagram; @GetMapping - public RenderingDataDTO getDiagramRenderingData( + public RenderingDataDTO getCustomDatasetViewRenderingData( @Parameter(description = "The name/url of the inquirer.") @RequestHeader( value = HttpHeaders.ORIGIN, diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/AllPackagesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/AllPackagesRESTController.java index e4237ffa..2f92113c 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/AllPackagesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/AllPackagesRESTController.java @@ -22,9 +22,9 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import java.util.List; -import java.util.UUID; + import lombok.RequiredArgsConstructor; + import org.rdfarchitect.api.dto.packages.PackageDTO; import org.rdfarchitect.database.GraphIdentifier; import org.rdfarchitect.services.ExpandURIUseCase; @@ -42,6 +42,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; +import java.util.UUID; + @RestController @RequestMapping("api/datasets/{datasetName}/graphs/{graphURI}/packages") @RequiredArgsConstructor @@ -54,44 +57,40 @@ public class AllPackagesRESTController { private final ListInternalPackagesUseCase listInternalPackagesUseCase; private final ListExternalPackagesUseCase listExternalPackagesUseCase; - /** - * Record for response to frontend, contains two lists for internal and external packages. - */ + /** Record for response to frontend, contains two lists for internal and external packages. */ public record ListPackagesResponse( - List internalPackageList, List externalPackageList) { - } + List internalPackageList, List externalPackageList) {} @Operation( summary = "list packages", description = "Get two lists of packages: internal and external packages.", tags = {"graph"}, responses = { - @ApiResponse( - responseCode = "200", - content = - @Content( - mediaType = "application/json", - schema = - @Schema( - implementation = - ListPackagesResponse - .class))) + @ApiResponse( + responseCode = "200", + content = + @Content( + mediaType = "application/json", + schema = + @Schema( + implementation = + ListPackagesResponse.class))) }) @GetMapping public ListPackagesResponse listPackages( @Parameter(description = "The name/url of the inquirer.") - @RequestHeader( - value = HttpHeaders.ORIGIN, - required = false, - defaultValue = "unknown") - String originURL, + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, @Parameter(description = "The literal name of the dataset.") @PathVariable - String datasetName, + String datasetName, @Parameter( - description = - "The url encoded uri of the graph, or \"default\" to access the default graph.") - @PathVariable - String graphURI) { + description = + "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI) { logger.info( "Received GET request: \"/api/datasets/{{}}/graphs/{{}}/packages\" from \"{}\".", datasetName, @@ -124,22 +123,22 @@ public ListPackagesResponse listPackages( @PostMapping public UUID addPackage( @Parameter(description = "The name/url of the inquirer.") - @RequestHeader( - value = HttpHeaders.ORIGIN, - required = false, - defaultValue = "unknown") - String originURL, + @RequestHeader( + value = HttpHeaders.ORIGIN, + required = false, + defaultValue = "unknown") + String originURL, @Parameter(description = "The literal name of the dataset.") @PathVariable - String datasetName, + String datasetName, @Parameter( - description = - "The url encoded uri of the graph, or \"default\" to access the default graph.") - @PathVariable - String graphURI, + description = + "The url encoded uri of the graph, or \"default\" to access the default graph.") + @PathVariable + String graphURI, @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "DTO for the package to be created") - @RequestBody - PackageDTO packageDTO) { + description = "DTO for the package to be created") + @RequestBody + PackageDTO packageDTO) { logger.info( "Received POST request: \"/api/datasets/{{}}/graphs/{{}}/packages\" from \"{}\".", diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/GraphBulkContentRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/GraphBulkContentRESTController.java index 763f733b..3930fd50 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/GraphBulkContentRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/GraphBulkContentRESTController.java @@ -34,7 +34,6 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/OntologyGenerateEntriesRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/OntologyGenerateEntriesRESTController.java index 4911151a..0d700b5b 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/OntologyGenerateEntriesRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/OntologyGenerateEntriesRESTController.java @@ -71,7 +71,7 @@ public class OntologyGenerateEntriesRESTController { .class)))) }) @GetMapping - public List getOntology( + public List getOntologyEntries( @Parameter(description = "The name/url of the inquirer.") @RequestHeader( value = HttpHeaders.ORIGIN, diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/SchemaComparisonRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/SchemaComparisonRESTController.java index 2a038fe0..1a10833e 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/SchemaComparisonRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/SchemaComparisonRESTController.java @@ -58,7 +58,7 @@ public class SchemaComparisonRESTController { @Operation( summary = "compare schemas", description = "Compare a given graph with the specified graph from the dataset", - tags = {"graph"}, + tags = {"comparison"}, responses = { @ApiResponse( responseCode = "200", @@ -113,7 +113,7 @@ public List compareSchemas( @Operation( summary = "compare schemas", description = "Compare two graphs stored in the database", - tags = {"graph"}, + tags = {"comparison"}, responses = { @ApiResponse( responseCode = "200", @@ -129,7 +129,7 @@ public List compareSchemas( .class)))) }) @GetMapping() - public List compareDatasetSchemas( + public List compareStoredSchemas( @Parameter(description = "The name/url of the inquirer.") @RequestHeader( value = HttpHeaders.ORIGIN, diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramsRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramsRESTController.java index 12197f7d..1223e7f9 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramsRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/CustomDiagramsRESTController.java @@ -66,7 +66,7 @@ public class CustomDiagramsRESTController { private final ExpandURIUseCase expandURIUseCase; @GetMapping - public RenderingDataDTO getDiagramRenderingData( + public RenderingDataDTO getCustomProfileViewRenderingData( @Parameter(description = "The name/url of the inquirer.") @RequestHeader( value = HttpHeaders.ORIGIN, diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java index 59189772..9e491a31 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/migration/MigrationContextRESTController.java @@ -28,6 +28,7 @@ import org.rdfarchitect.services.schemamigration.SetMigrationContextUseCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; @@ -55,7 +56,7 @@ public class MigrationContextRESTController { "Computes the diff of two given graphs and stores it in the session for later usage in migration endpoints. " + "Accepts the graphs either as file uploads, GraphIdentifiers or a combination of both.", tags = {"migration"}) - @PostMapping + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public void computeMigrationContext( @Parameter(description = "The name/url of the inquirer.") @RequestHeader(value = "origin", required = false, defaultValue = "unknown") diff --git a/backend/src/main/java/org/rdfarchitect/api/dto/DatasetDTO.java b/backend/src/main/java/org/rdfarchitect/api/dto/DatasetDTO.java index e855f7f3..9a76c3be 100644 --- a/backend/src/main/java/org/rdfarchitect/api/dto/DatasetDTO.java +++ b/backend/src/main/java/org/rdfarchitect/api/dto/DatasetDTO.java @@ -19,7 +19,9 @@ import lombok.AllArgsConstructor; import lombok.Data; + import org.rdfarchitect.models.cim.data.dto.CIMPrefixPair; + import java.util.List; @Data diff --git a/backend/src/main/java/org/rdfarchitect/services/select/ListDatasetsUseCase.java b/backend/src/main/java/org/rdfarchitect/services/select/ListDatasetsUseCase.java index 642d5365..8bac8692 100644 --- a/backend/src/main/java/org/rdfarchitect/services/select/ListDatasetsUseCase.java +++ b/backend/src/main/java/org/rdfarchitect/services/select/ListDatasetsUseCase.java @@ -18,12 +18,15 @@ package org.rdfarchitect.services.select; import org.rdfarchitect.api.dto.DatasetDTO; + import java.util.List; public interface ListDatasetsUseCase { /** - * List all datasets including their prefixes and read-only status. Snapshots are excluded from the list. + * List all datasets including their prefixes and read-only status. Snapshots are excluded from + * the list. + * * @return List of dataset objects */ List listDatasets(); diff --git a/frontend/src/lib/GraphExport.svelte b/frontend/src/lib/GraphExport.svelte index 557a65ae..fc9f601b 100644 --- a/frontend/src/lib/GraphExport.svelte +++ b/frontend/src/lib/GraphExport.svelte @@ -20,10 +20,8 @@ import { onMount } from "svelte"; import { Fa } from "svelte-fa"; - import { BackendConnection } from "$lib/api/backend.js"; import { DropdownMenu } from "$lib/components/bitsui/dropdown/index"; import DatasetAndGraphSelection from "$lib/components/DatasetAndGraphSelection.svelte"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { ReactiveOntology } from "$lib/models/reactive/models/ontology/reactive-ontology.svelte.js"; import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; @@ -43,8 +41,6 @@ supportedMediaTypes = supportedRDFMediaTypes, } = $props(); - const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - let selectedDatasetName = $state(null); let graphURI = $state(null); let selectedMediaType = $state(); @@ -76,7 +72,10 @@ $effect(async () => { if (selectedDatasetName && graphURI) { await ontologyStore.loadOntology(selectedDatasetName, graphURI); - const { data, error } = await ontologyStore.getOntologyForGraph(selectedDatasetName, graphURI); + const { data, error } = await ontologyStore.getOntologyForGraph( + selectedDatasetName, + graphURI, + ); if (error) { ontology = null; return; @@ -92,11 +91,11 @@ $effect(async () => { if (selectedDatasetName && graphURI && hasOntology) { - const res = await bec.generateOntologyEntries( + const { data } = await ontologyStore.generateOntologyEntries( selectedDatasetName, graphURI, ); - generatedOntologyEntries = await res.json(); + generatedOntologyEntries = data; generatedOntologyEntries.forEach(entry => (entry.generate = true)); return; } diff --git a/frontend/src/lib/api/apiGlobalUtils.js b/frontend/src/lib/api/apiGlobalUtils.js deleted file mode 100644 index c3d68a39..00000000 --- a/frontend/src/lib/api/apiGlobalUtils.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2024-2026 SOPTIM AG - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { BackendConnection } from "$lib/api/backend.js"; -import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; - -const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - -export async function getXSDPrimitives() { - const res = await bec.getXSDPrimitives(); - return await res.json(); -} - -export async function getKnownFields() { - const res = await bec.getKnownOntologyFields(); - return await res.json(); -} diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js index f38ec22e..cfa7a41b 100644 --- a/frontend/src/lib/api/backend.js +++ b/frontend/src/lib/api/backend.js @@ -26,267 +26,6 @@ export class BackendConnection { this.url = url; } - async fetchFilteredRenderingData(datasetName, graphURI, graphFilter) { - const url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/rendering`; - return fetch(url, { - method: "POST", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(graphFilter), - credentials: "include", - }); - } - - async getXSDPrimitives() { - const url = `${PUBLIC_BACKEND_URL}/primitiveDatatypes`; - return fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async getPrimitives(datasetName, graphURI) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/primitives`; - return await fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async getDataTypes(datasetName, graphURI) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/datatypes`; - return await fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async getStereotypes(datasetName, graphURI) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/stereotypes`; - return await fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async getDeleteRelation(datasetName, graphURI, resourceUuid) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/uuid/${encodeURIComponent(resourceUuid)}/deletion-impact`; - return await fetch(url, { - method: "GET", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async deleteResources(datasetName, graphURI, deleteRequests) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/delete-requests`; - return await fetch(url, { - method: "POST", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(deleteRequests), - credentials: "include", - }); - } - - async getSearchResults(query, body) { - let url = `${PUBLIC_BACKEND_URL}/search?query=${encodeURIComponent(query)}`; - return await fetch(url, { - method: "POST", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(body), - credentials: "include", - }); - } - - async compareSchemas(datasetName, graphURI, file) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/compare`; - const formData = new FormData(); - formData.append("file", file); - return await fetch(url, { - method: "POST", - mode: "cors", - body: formData, - credentials: "include", - }); - } - - async compareDatasetSchemas( - datasetName, - graphURI, - otherDatasetName, - otherGraphURI, - ) { - const url = - `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}` + - `/graphs/${encodeURIComponent(graphURI)}/compare` + - `?otherDataset=${encodeURIComponent(otherDatasetName)}` + - `&otherGraph=${encodeURIComponent(otherGraphURI)}`; - - return fetch(url, { - method: "GET", - mode: "cors", - credentials: "include", - }); - } - - async compareSchemasFromFiles(fileA, fileB) { - const url = `${PUBLIC_BACKEND_URL}/compare`; - - const formData = new FormData(); - formData.append("fileA", fileA); - formData.append("fileB", fileB); - - return fetch(url, { - method: "POST", - mode: "cors", - body: formData, - credentials: "include", - }); - } - - async createSnapshot(datasetName) { - let url = `${PUBLIC_BACKEND_URL}/snapshots`; - return await fetch(url, { - method: "POST", - mode: "cors", - body: datasetName, - credentials: "include", - }); - } - - async loadSnapshot(base64Token) { - let url = `${PUBLIC_BACKEND_URL}/snapshots/${encodeURIComponent(base64Token)}`; - return await fetch(url, { - method: "GET", - mode: "cors", - credentials: "include", - }); - } - - async getChangelog(datasetName, graphURI) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/changes`; - return await fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async restoreVersion(datasetName, graphURI, version) { - console.log(`Restoring version ${version}`); - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/restore`; - return await fetch(url, { - method: "POST", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - body: version, - credentials: "include", - }); - } - - async updateClassPositions( - datasetName, - graphURI, - packageUUID, - classPositionDTOList, - ) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/layout/${encodeURIComponent(packageUUID)}/classes`; - return await fetch(url, { - method: "PUT", - headers: new Headers({ "Content-Type": "application/json" }), - mode: "cors", - body: JSON.stringify(classPositionDTOList), - credentials: "include", - }); - } - - async updateGlobalClassPositions( - datasetName, - packageUUID, - classPositionDTOList, - ) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/layout/${encodeURIComponent(packageUUID)}/classes`; - return await fetch(url, { - method: "PUT", - headers: new Headers({ "Content-Type": "application/json" }), - mode: "cors", - body: JSON.stringify(classPositionDTOList), - credentials: "include", - }); - } - - async getKnownOntologyFields() { - let url = `${PUBLIC_BACKEND_URL}/ontology-fields`; - return await fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async generateOntologyEntries(datasetName, graphURI) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/ontology/generate`; - return await fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async postCopyClass(datasetName, graphURI, classUUID, targetInfo) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}/copy`; - return await fetch(url, { - method: "POST", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(targetInfo), - credentials: "include", - }); - } - - async extendClass(datasetName, graphURI, classUUID, body) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/classes/${encodeURIComponent(classUUID)}/extend`; - return await fetch(url, { - method: "POST", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - body: JSON.stringify(body), - credentials: "include", - }); - } - - async getCustomGraphDiagramRenderingData(datasetName, graphURI, diagramId) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/graphs/${encodeURIComponent(graphURI)}/diagrams/${encodeURIComponent(diagramId)}`; - return await fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - - async getCustomDatasetDiagramRenderingData(datasetName, diagramId) { - let url = `${PUBLIC_BACKEND_URL}/datasets/${encodeURIComponent(datasetName)}/diagrams/${encodeURIComponent(diagramId)}`; - return await fetch(url, { - method: "GET", - mode: "cors", - headers: new Headers({ "Content-Type": "application/json" }), - credentials: "include", - }); - } - async resetSession() { const url = `${PUBLIC_BACKEND_URL}/session`; return fetch(url, { diff --git a/frontend/src/lib/components/DatasetAndGraphSelection.svelte b/frontend/src/lib/components/DatasetAndGraphSelection.svelte index c5309e6b..d6f93f21 100644 --- a/frontend/src/lib/components/DatasetAndGraphSelection.svelte +++ b/frontend/src/lib/components/DatasetAndGraphSelection.svelte @@ -50,7 +50,7 @@ } await graphStore.load(dataset); - graphNames = graphStore.getGraphURIs(dataset); + graphNames = graphStore.getGraphs(dataset); const valid = graphNames.some(graphName => getUri(graphName) === graph); if (!valid && !graphLocked) { graph = null; @@ -73,7 +73,7 @@ if (dataset) { await graphStore.load(dataset); - graphNames = graphStore.getGraphURIs(dataset); + graphNames = graphStore.getGraphs(dataset); } else { graphNames = []; } diff --git a/frontend/src/lib/rendering/svelteflow/components/SvelteFlowPaneContextMenu.svelte b/frontend/src/lib/rendering/svelteflow/components/SvelteFlowPaneContextMenu.svelte index ec82a43f..ee548e25 100644 --- a/frontend/src/lib/rendering/svelteflow/components/SvelteFlowPaneContextMenu.svelte +++ b/frontend/src/lib/rendering/svelteflow/components/SvelteFlowPaneContextMenu.svelte @@ -18,14 +18,13 @@ diff --git a/frontend/src/routes/changelog/ChangesRow.svelte b/frontend/src/routes/changelog/ChangesRow.svelte index 39633ee2..81478375 100644 --- a/frontend/src/routes/changelog/ChangesRow.svelte +++ b/frontend/src/routes/changelog/ChangesRow.svelte @@ -18,9 +18,8 @@ import { faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons"; import { Fa } from "svelte-fa"; - import { BackendConnection } from "$lib/api/backend.js"; + import { restoreVersion } from "$lib/api/generated/index.ts"; import ButtonControl from "$lib/components/ButtonControl.svelte"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { editorState, @@ -37,20 +36,17 @@ readonly, } = $props(); - const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - const rowKey = $derived(`${change.changeId}::row`); - async function restoreVersion(changeId) { + async function callRestoreVersion(changeId) { console.log("restoreVersion", changeId); - const res = await bec.restoreVersion( + const { error } = await restoreVersion( editorState.selectedDataset.getValue(), editorState.selectedGraph.getValue(), changeId, ); - - if (res.ok) { + if (!error) { console.log("Version restored successfully"); forceReloadTrigger.trigger(); toastStore.success( @@ -58,7 +54,7 @@ "The selected version has been restored.", ); } else { - console.error("Failed to restore version:", res.statusText); + console.error("Failed to restore version:", error); toastStore.error( "Restore failed", "Could not restore the selected version.", @@ -135,7 +131,7 @@ restoreVersion(change.changeId)} + callOnClick={() => callRestoreVersion(change.changeId)} > Restore Version diff --git a/frontend/src/routes/changelog/Navigation.svelte b/frontend/src/routes/changelog/Navigation.svelte index 2e634c62..aef4a75e 100644 --- a/frontend/src/routes/changelog/Navigation.svelte +++ b/frontend/src/routes/changelog/Navigation.svelte @@ -42,7 +42,8 @@ async function fetchNavigationObject() { await datasetStore.load(); const newDatasetList = []; - for (const datasetName of $datasetStore.data) { + for (const dataset of $datasetStore.data) { + const datasetName = dataset.label; let showDatasetContents = datasetName === selectedDatasetName; showDatasetContents |= datasetList.find( datasetObject => datasetObject.label === datasetName, @@ -54,7 +55,7 @@ }); await graphStore.load(datasetName); graphStore - .getGraphNames(datasetName) + .getGraphs(datasetName) .forEach(graphUri => newDatasetList.at(-1).graphs.push(graphUri), ); diff --git a/frontend/src/routes/compare/CompareDialog.svelte b/frontend/src/routes/compare/CompareDialog.svelte index 939cdfc7..584ab00f 100644 --- a/frontend/src/routes/compare/CompareDialog.svelte +++ b/frontend/src/routes/compare/CompareDialog.svelte @@ -18,11 +18,14 @@ import { faRightLeft } from "@fortawesome/free-solid-svg-icons"; import { Fa } from "svelte-fa"; - import { BackendConnection } from "$lib/api/backend.js"; + import { + compareSchemas, + compareSchemasFromFiles, + compareStoredSchemas, + } from "$lib/api/generated/index.ts"; import DatasetAndGraphSelection from "$lib/components/DatasetAndGraphSelection.svelte"; import FileSelectButton from "$lib/components/FileSelectButton.svelte"; import SelectEditControl from "$lib/components/SelectEditControl.svelte"; - import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; import ActionDialog from "$lib/dialog/ActionDialog.svelte"; import { editorState, @@ -38,8 +41,6 @@ lockedGraphUri, } = $props(); - const bec = new BackendConnection(fetch, PUBLIC_BACKEND_URL); - const CompareMode = Object.freeze({ STORED_TO_STORED: 0, FILE_TO_STORED: 1, @@ -188,28 +189,31 @@ let invert = false; switch (compareMode) { case CompareMode.FILE_TO_FILE: - response = await bec.compareSchemasFromFiles(fileA, fileB); + response = await compareSchemasFromFiles({ + body: { fileA, fileB }, + }); break; case CompareMode.FILE_TO_STORED: - response = await bec.compareSchemas(datasetB, graphB, fileA); + response = await compareSchemas({ + path: { datasetName: datasetB, graphURI: graphB }, + body: fileA, + }); break; case CompareMode.STORED_TO_FILE: response = await bec.compareSchemas(datasetA, graphA, fileA); invert = true; break; case CompareMode.STORED_TO_STORED: - response = await bec.compareDatasetSchemas( - datasetA, - graphA, - datasetB, - graphB, - ); + response = await compareStoredSchemas({ + path: { datasetName: datasetA, graphURI: graphA }, + query: { otherDataset: datasetB, otherGraph: graphB }, + }); break; default: throw new Error(`Unknown compareMode: ${compareMode}`); } - let changeList = await response.json(); + let changeList = response.data; if (invert) { changeList = invertChangeList(changeList); } diff --git a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte index dcb4a4e3..6d8897cf 100644 --- a/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte +++ b/frontend/src/routes/delete-relations-dialog/DeleteDependenciesDialog.svelte @@ -18,8 +18,10 @@ {#if diagrams.length > 0} diff --git a/frontend/src/routes/mainpage/packageNavigation/ExtendClassDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/ExtendClassDialog.svelte index 3a05cc12..50826c37 100644 --- a/frontend/src/routes/mainpage/packageNavigation/ExtendClassDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/ExtendClassDialog.svelte @@ -16,16 +16,14 @@ --> diff --git a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte index 0580f547..cdfbe043 100644 --- a/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/GraphSection.svelte @@ -106,7 +106,10 @@ async function initialize() { await ontologyStore.loadOntology(datasetNavEntry.id, graphNavEntry.id); - const { data } = await ontologyStore.getOntologyForGraph(datasetNavEntry.id, graphNavEntry.id); + const { data } = await ontologyStore.getOntologyForGraph( + datasetNavEntry.id, + graphNavEntry.id, + ); ontology = data; canUndo = versionControlStore.canUndo( datasetNavEntry.id, diff --git a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDatasetDiagramDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDatasetDiagramDialog.svelte index c4161bf9..31480e04 100644 --- a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDatasetDiagramDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/CustomDatasetDiagramDialog.svelte @@ -73,22 +73,17 @@ } async function fetchGraphs() { - try { - await graphStore.load(lockedDatasetName); - graphs = graphStore - .getGraphURIs(lockedDatasetName) - .map(graph => { - return { - ...graph, - selected: false, - expanded: false, - }; - }) - .sort((a, b) => getUri(a).localeCompare(getUri(b))); - } catch (err) { - console.error("Failed to load graphs:", err); - graphs = []; - } + await graphStore.load(lockedDatasetName); + graphs = graphStore + .getGraphs(lockedDatasetName) + .map(graph => { + return { + ...graph, + selected: false, + expanded: false, + }; + }) + .sort((a, b) => getUri(a).localeCompare(getUri(b))); } async function createPackageMap() { diff --git a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/customDiagramDialogUtils.js b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/customDiagramDialogUtils.js index d51c8b90..613b9484 100644 --- a/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/customDiagramDialogUtils.js +++ b/frontend/src/routes/mainpage/packageNavigation/custom-diagram-dialogs/customDiagramDialogUtils.js @@ -48,38 +48,34 @@ export async function createClassListForGraph( graphURI, selectedClasses, ) { - try { - await classStore.load(datasetName, graphURI); - const classList = await classStore.getClasses(datasetName, graphURI); + await classStore.load(datasetName, graphURI); + const classList = await classStore.getClasses(datasetName, graphURI); - const grouped = {}; + const grouped = {}; - for (const cls of classList) { - const packageId = getPackageId(cls.package); - if (!grouped[packageId]) { - grouped[packageId] = []; - } - - cls.selected = !!selectedClasses.some( - selected => selected.uuid === cls.uuid, - ); - - grouped[packageId].push({ - ...cls, - packageUUID: packageId, - }); + for (const cls of classList) { + const packageId = getPackageId(cls.package); + if (!grouped[packageId]) { + grouped[packageId] = []; } - for (const key of Object.keys(grouped)) { - grouped[key].sort((a, b) => - (a.label ?? "").localeCompare(b.label ?? "", undefined, { - sensitivity: "base", - }), - ); - } + cls.selected = !!selectedClasses.some( + selected => selected.uuid === cls.uuid, + ); - return grouped; - } catch (err) { - console.error("Failed to load classes:", err); + grouped[packageId].push({ + ...cls, + packageUUID: packageId, + }); } + + for (const key of Object.keys(grouped)) { + grouped[key].sort((a, b) => + (a.label ?? "").localeCompare(b.label ?? "", undefined, { + sensitivity: "base", + }), + ); + } + + return grouped; } diff --git a/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/AddKnownFieldsDialog.svelte b/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/AddKnownFieldsDialog.svelte index c4dcf7e7..fecad7f3 100644 --- a/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/AddKnownFieldsDialog.svelte +++ b/frontend/src/routes/mainpage/packageNavigation/ontology-editor-dialog/AddKnownFieldsDialog.svelte @@ -15,11 +15,9 @@ - -->

diff --git a/frontend/src/routes/mainpage/classEditor/components/Label.svelte b/frontend/src/routes/mainpage/classEditor/components/Label.svelte index d235c88c..cf985bfd 100644 --- a/frontend/src/routes/mainpage/classEditor/components/Label.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/Label.svelte @@ -28,14 +28,14 @@ const id = uuid(); const classEditorContext = getContext("classEditor"); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); - onMount(() => (readonly = classEditorContext.readonly)); + onMount(() => (readonly = classEditorContext.readOnly)); diff --git a/frontend/src/routes/mainpage/classEditor/components/Namespace.svelte b/frontend/src/routes/mainpage/classEditor/components/Namespace.svelte index 2be97019..cbd6601f 100644 --- a/frontend/src/routes/mainpage/classEditor/components/Namespace.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/Namespace.svelte @@ -30,11 +30,11 @@ const id = uuid(); let namespaces = $derived(classEditorContext.namespaces); let reactiveClass = $derived(classEditorContext.reactiveClass); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); $effect(() => { @@ -44,7 +44,7 @@ }); onMount(() => { - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; namespaces = classEditorContext.namespaces; reactiveClass = classEditorContext.reactiveClass; }); diff --git a/frontend/src/routes/mainpage/classEditor/components/Package.svelte b/frontend/src/routes/mainpage/classEditor/components/Package.svelte index 7c33510d..11102181 100644 --- a/frontend/src/routes/mainpage/classEditor/components/Package.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/Package.svelte @@ -29,14 +29,14 @@ const classEditorContext = getContext("classEditor"); const packages = classEditorContext.packages; const id = uuid(); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); - onMount(() => (readonly = classEditorContext.readonly)); + onMount(() => (readonly = classEditorContext.readOnly)); function getPackageLabel(packageUUID) { const pkg = classEditorContext.getPackageByUuid(packageUUID); diff --git a/frontend/src/routes/mainpage/classEditor/components/SuperClass.svelte b/frontend/src/routes/mainpage/classEditor/components/SuperClass.svelte index d5096c89..174cbf3f 100644 --- a/frontend/src/routes/mainpage/classEditor/components/SuperClass.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/SuperClass.svelte @@ -30,11 +30,11 @@ const id = uuid(); let classes = $derived(classEditorContext.classes); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); $effect(() => { @@ -43,7 +43,7 @@ }); onMount(() => { - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; classes = classEditorContext.classes; }); diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/Association.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/Association.svelte index 4b5e51d0..284240e3 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/Association.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/Association.svelte @@ -40,18 +40,18 @@ } = $props(); const classEditorContext = getContext("classEditor"); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); let lowerButtons = $derived(getButtons(association.multiplicityLowerBound)); let upperButtons = $derived(getButtons(association.multiplicityUpperBound)); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); onMount(() => { - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); function getButtons(multiplicityObject) { diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/Associations.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/Associations.svelte index a3abe1c1..a756d04c 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/Associations.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/Associations.svelte @@ -44,18 +44,18 @@ let container; let w = $state(MIN_W); let resizeObserver; - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); onMount(() => { updateWidth(); resizeObserver = new ResizeObserver(updateWidth); resizeObserver.observe(container); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); onDestroy(() => { diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte index 87c9cd31..60f74cdf 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/AssociationEditorDialog.svelte @@ -36,7 +36,7 @@ let classEditorContext = $state(); let isNewAssociation = $state(true); - let readonly = $derived(classEditorContext?.readonly); + let readonly = $derived(classEditorContext?.readOnly); $effect(() => { (async () => { diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Direct.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Direct.svelte index 7ce3450d..3d6b3cd7 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Direct.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Direct.svelte @@ -29,7 +29,7 @@ const { association } = $props(); const classEditorContext = getContext("classEditor"); - const readonly = classEditorContext.readonly; + const readonly = classEditorContext.readOnly; const classes = classEditorContext.classes; diff --git a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Inverse.svelte b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Inverse.svelte index 4e4a7e8f..91a20971 100644 --- a/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Inverse.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/associations/associationEditorDialog/Inverse.svelte @@ -29,7 +29,7 @@ const { association } = $props(); const classEditorContext = getContext("classEditor"); - const readonly = classEditorContext.readonly; + const readonly = classEditorContext.readOnly;
diff --git a/frontend/src/routes/mainpage/classEditor/components/attributes/Attribute.svelte b/frontend/src/routes/mainpage/classEditor/components/attributes/Attribute.svelte index acc774ba..4f5417b2 100644 --- a/frontend/src/routes/mainpage/classEditor/components/attributes/Attribute.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/attributes/Attribute.svelte @@ -38,14 +38,14 @@ } = $props(); const classEditorContext = getContext("classEditor"); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); - onMount(() => (readonly = classEditorContext.readonly)); + onMount(() => (readonly = classEditorContext.readOnly)); function getDatatypeLabelByUri(uri) { const datatype = classEditorContext.getDatatypeByUri(uri); @@ -108,7 +108,7 @@ title={readonly ? "View" : "Edit" + " attribute"} /> - {#if !classEditorContext.readonly} + {#if !classEditorContext.readOnly} attributes.remove(attribute, true)} diff --git a/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte index f276467e..8a95b31d 100644 --- a/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/attributes/AttributeEditorDialog.svelte @@ -40,7 +40,7 @@ let classEditorContext = $state(); let isNewAttribute = $state(true); - let readonly = $derived(classEditorContext?.readonly); + let readonly = $derived(classEditorContext?.readOnly); let datatypes = $derived(classEditorContext?.datatypes); function onOpen() { diff --git a/frontend/src/routes/mainpage/classEditor/components/attributes/Attributes.svelte b/frontend/src/routes/mainpage/classEditor/components/attributes/Attributes.svelte index 4a6e2db9..ff58d0c2 100644 --- a/frontend/src/routes/mainpage/classEditor/components/attributes/Attributes.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/attributes/Attributes.svelte @@ -35,14 +35,14 @@ }); let expandStereotypes = $state(true); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = getContext("classEditor").readonly; + readonly = getContext("classEditor").readOnly; }); - onMount(() => (readonly = classEditorContext.readonly)); + onMount(() => (readonly = classEditorContext.readOnly)); function openAttributeEditor(attribute) { attributeEditorDialog.attribute = attribute; diff --git a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntries.svelte b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntries.svelte index 7835cebd..ecb1c3d3 100644 --- a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntries.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntries.svelte @@ -35,14 +35,14 @@ }); let expandStereotypes = $state(true); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); - onMount(() => (readonly = classEditorContext.readonly)); + onMount(() => (readonly = classEditorContext.readOnly)); function openEnumEntryEditor(enumEntry) { enumEntryEditorDialog.enumEntry = enumEntry; diff --git a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntry.svelte b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntry.svelte index 297467ce..8d1fee45 100644 --- a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntry.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntry.svelte @@ -28,14 +28,14 @@ const { enumEntries, enumEntry, openEnumEntryEditor } = $props(); const classEditorContext = getContext("classEditor"); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); - onMount(() => (readonly = classEditorContext.readonly)); + onMount(() => (readonly = classEditorContext.readOnly)); diff --git a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte index 1a396f13..158aa601 100644 --- a/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/enum-entries/EnumEntryEditorDialog.svelte @@ -39,7 +39,7 @@ let classEditorContext = $state(); let isNewEnumEntry = $state(true); - let readonly = $derived(classEditorContext?.readonly); + let readonly = $derived(classEditorContext?.readOnly); function onOpen() { classEditorContext = getContext("classEditor"); diff --git a/frontend/src/routes/mainpage/classEditor/components/stereotypes/IsAbstract.svelte b/frontend/src/routes/mainpage/classEditor/components/stereotypes/IsAbstract.svelte index 0bb9a19f..43ded542 100644 --- a/frontend/src/routes/mainpage/classEditor/components/stereotypes/IsAbstract.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/stereotypes/IsAbstract.svelte @@ -27,17 +27,17 @@ const classEditorContext = getContext("classEditor"); const id = uuidv4(); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); let isAbstract = $derived(!classStereotypes.contains(CONCRETE_STEREOTYPE)); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); onMount(() => { - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); diff --git a/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotype.svelte b/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotype.svelte index 95fee7cd..a1fcc685 100644 --- a/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotype.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotype.svelte @@ -35,7 +35,7 @@ let suggestedStereotypes = $derived( withoutConcrete(classEditorContext.stereotypes), ); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); // The persisted concrete stereotype is surfaced by the "Abstract" checkbox // and hidden from the list. A row the user has just edited to the concrete @@ -46,7 +46,7 @@ $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); $effect(() => { @@ -55,7 +55,7 @@ }); onMount(() => { - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; suggestedStereotypes = withoutConcrete(classEditorContext.stereotypes); }); diff --git a/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotypes.svelte b/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotypes.svelte index a061ac37..4dd33282 100644 --- a/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotypes.svelte +++ b/frontend/src/routes/mainpage/classEditor/components/stereotypes/Stereotypes.svelte @@ -30,14 +30,14 @@ const classEditorContext = getContext("classEditor"); let expandStereotypes = $state(true); - let readonly = $derived(classEditorContext.readonly); + let readonly = $derived(classEditorContext.readOnly); $effect(() => { editorState.selectedDiagram.subscribe(); - readonly = classEditorContext.readonly; + readonly = classEditorContext.readOnly; }); - onMount(() => (readonly = classEditorContext.readonly)); + onMount(() => (readonly = classEditorContext.readOnly)); From e547afbed16e8ea603ed0489c638d1d6b86f51ae Mon Sep 17 00:00:00 2001 From: tarn Date: Wed, 24 Jun 2026 15:01:55 +0200 Subject: [PATCH 12/22] lint --- .../org/rdfarchitect/OpenApiSpecExport.java | 27 +++++++++++++++-- frontend/src/lib/stores/GraphStore.ts | 29 +++++++++++++++++++ .../src/lib/stores/VersionControlStore.ts | 12 ++++++-- frontend/src/routes/ImportDialog.svelte | 21 -------------- .../src/routes/compare/CompareDialog.svelte | 5 +++- .../src/routes/layout/menu-bar/Edit.svelte | 10 +++++-- .../mainpage/packageEditorDialog.svelte | 5 ---- 7 files changed, 74 insertions(+), 35 deletions(-) diff --git a/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java b/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java index 7e88252a..38509ebc 100644 --- a/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java +++ b/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java @@ -37,7 +37,18 @@ import java.util.Map; import java.util.Objects; -/** * Exports the springdoc OpenAPI document to a deterministic JSON file so the frontend can generate * its API client offline (in CI and in Docker) without a running backend. * *

Runs as a standalone Maven goal: {@code mvn compile exec:java@export-openapi}. The output * path defaults to {@code ../frontend/openapi.json} (relative to the backend module) and can be * overridden with the first program argument or {@code -Dopenapi.export.path=...}. * *

Boots the full Spring application on an ephemeral port (server.port=0), fetches the spec, * normalizes it, writes it to disk, and shuts the context down. CI runs this and diffs the result * so the committed spec can never drift from the controllers. */ +/** + * * Exports the springdoc OpenAPI document to a deterministic JSON file so the frontend can + * generate * its API client offline (in CI and in Docker) without a running backend. * * + * + *

Runs as a standalone Maven goal: {@code mvn compile exec:java@export-openapi}. The output * + * path defaults to {@code ../frontend/openapi.json} (relative to the backend module) and can be * + * overridden with the first program argument or {@code -Dopenapi.export.path=...}. * * + * + *

Boots the full Spring application on an ephemeral port (server.port=0), fetches the spec, * + * normalizes it, writes it to disk, and shuts the context down. CI runs this and diffs the result * + * so the committed spec can never drift from the controllers. + */ public final class OpenApiSpecExport { private static final String DEFAULT_OUTPUT_PATH = "../frontend/openapi.json"; @@ -87,7 +98,12 @@ private static Path resolveTargetPath(String[] args) { return Path.of(System.getProperty("openapi.export.path", DEFAULT_OUTPUT_PATH)); } - /** * Re-serializes the spec with sorted keys and a fixed {@code \n} indenter so the output is * stable across runs and operating systems. The {@code servers} entry is dropped because it * reflects the request URL and is overridden at runtime by the frontend client ({@code * src/lib/api/hey-api.ts}); keeping it adds no value to the committed contract. */ + /** + * * Re-serializes the spec with sorted keys and a fixed {@code \n} indenter so the output is * + * stable across runs and operating systems. The {@code servers} entry is dropped because it * + * reflects the request URL and is overridden at runtime by the frontend client ({@code * + * src/lib/api/hey-api.ts}); keeping it adds no value to the committed contract. + */ private static String formatDeterministically(String rawSpec) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); mapper.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); @@ -103,7 +119,12 @@ private static String formatDeterministically(String rawSpec) throws JsonProcess return mapper.writer(printer).writeValueAsString(spec) + "\n"; } - /** * Sorts every {@code required} string array in the document. springdoc derives this list from * field reflection, whose order is not guaranteed across JVMs; {@code required} is an unordered * set in OpenAPI, so sorting it makes the committed contract robust against that variance. */ + /** + * * Sorts every {@code required} string array in the document. springdoc derives this list from + * * field reflection, whose order is not guaranteed across JVMs; {@code required} is an + * unordered * set in OpenAPI, so sorting it makes the committed contract robust against that + * variance. + */ private static void sortRequiredArrays(Object node) { if (node instanceof Map map) { Object required = map.get("required"); diff --git a/frontend/src/lib/stores/GraphStore.ts b/frontend/src/lib/stores/GraphStore.ts index 0f2bd6aa..0cf5b0aa 100644 --- a/frontend/src/lib/stores/GraphStore.ts +++ b/frontend/src/lib/stores/GraphStore.ts @@ -25,6 +25,7 @@ import { replaceGraphs, GraphBulkImportResponse, replaceGraph, + ImportWarning, } from "../api/generated"; import { toastStore } from "../eventhandling/toastStore.svelte.js"; @@ -243,6 +244,8 @@ function createGraphStore() { ); } + notifyUndisplayableProperties(data?.warnings ?? []); + return { error: null, data }; } @@ -316,3 +319,29 @@ function createGraphStore() { invalidateAll, }; } + +function notifyUndisplayableProperties(warnings: ImportWarning[]) { + if (warnings.length === 0) return; + + const total = warnings.reduce( + (sum, w) => sum + (w.undisplayableProperties?.length ?? 0), + 0, + ); + if (total === 0) return; + + const details = warnings + .map( + w => + `${w.fileName}: ${(w.undisplayableProperties ?? []).join(", ")}`, + ) + .join("; "); + + toastStore.warning( + "Some properties could not be displayed", + `${total} propert${total === 1 ? "y" : "ies"} ${ + total === 1 ? "is" : "are" + } missing the CIM stereotype or association metadata RDFArchitect needs to show ${ + total === 1 ? "it" : "them" + } (${details}).`, + ); +} diff --git a/frontend/src/lib/stores/VersionControlStore.ts b/frontend/src/lib/stores/VersionControlStore.ts index 501dee97..d5d5026f 100644 --- a/frontend/src/lib/stores/VersionControlStore.ts +++ b/frontend/src/lib/stores/VersionControlStore.ts @@ -72,13 +72,19 @@ function createStore() { function canUndo(dataset?: string, graph?: string): boolean { const targets = resolveTargets(dataset, graph); if (!targets) return false; - return get(store).byGraph.get(key(targets.dataset, targets.graph))?.canUndo ?? false; + return ( + get(store).byGraph.get(key(targets.dataset, targets.graph)) + ?.canUndo ?? false + ); } function canRedo(dataset?: string, graph?: string): boolean { const targets = resolveTargets(dataset, graph); if (!targets) return false; - return get(store).byGraph.get(key(targets.dataset, targets.graph))?.canRedo ?? false; + return ( + get(store).byGraph.get(key(targets.dataset, targets.graph)) + ?.canRedo ?? false + ); } async function doUndo(dataset?: string, graph?: string) { @@ -86,7 +92,7 @@ function createStore() { if (!targets) { console.error(`${LOG} undo failed`, "No undo target selected."); toastStore.error("Undo failed", "No undo target selected."); - return { error: "No undo target selected."}; + return { error: "No undo target selected." }; } const { error } = await sdkUndo({ path: { datasetName: targets.dataset, graphURI: targets.graph }, diff --git a/frontend/src/routes/ImportDialog.svelte b/frontend/src/routes/ImportDialog.svelte index 4d50e658..7cad4109 100644 --- a/frontend/src/routes/ImportDialog.svelte +++ b/frontend/src/routes/ImportDialog.svelte @@ -209,27 +209,6 @@ datasetStore.invalidate(); forceReloadTrigger.trigger(); } - - function notifyUndisplayableProperties(warnings) { - if (warnings.length === 0) { - return; - } - const total = warnings.reduce( - (sum, warning) => - sum + (warning.undisplayableProperties?.length ?? 0), - 0, - ); - const details = warnings - .map( - warning => - `${warning.fileName}: ${(warning.undisplayableProperties ?? []).join(", ")}`, - ) - .join("; "); - toastStore.warning( - "Some properties could not be displayed", - `${total} propert${total === 1 ? "y" : "ies"} ${total === 1 ? "is" : "are"} missing the CIM stereotype or association metadata RDFArchitect needs to show ${total === 1 ? "it" : "them"} (${details}).`, - ); - } } diff --git a/frontend/src/routes/compare/CompareDialog.svelte b/frontend/src/routes/compare/CompareDialog.svelte index 584ab00f..630ecf94 100644 --- a/frontend/src/routes/compare/CompareDialog.svelte +++ b/frontend/src/routes/compare/CompareDialog.svelte @@ -200,7 +200,10 @@ }); break; case CompareMode.STORED_TO_FILE: - response = await bec.compareSchemas(datasetA, graphA, fileA); + response = await compareSchemas({ + path: { datasetA, graphA }, + body: fileB, + }); invert = true; break; case CompareMode.STORED_TO_STORED: diff --git a/frontend/src/routes/layout/menu-bar/Edit.svelte b/frontend/src/routes/layout/menu-bar/Edit.svelte index c5173dd5..7b9b80a2 100644 --- a/frontend/src/routes/layout/menu-bar/Edit.svelte +++ b/frontend/src/routes/layout/menu-bar/Edit.svelte @@ -360,9 +360,15 @@ function toggleReadonly() { if (isDatasetReadOnly) { - datasetStore.updateReadonly(editorState.selectedDatatset.getValue(), false); + datasetStore.updateReadonly( + editorState.selectedDatatset.getValue(), + false, + ); } else { - datasetStore.updateReadonly(editorState.selectedDatatset.getValue(), true); + datasetStore.updateReadonly( + editorState.selectedDatatset.getValue(), + true, + ); } } diff --git a/frontend/src/routes/mainpage/packageEditorDialog.svelte b/frontend/src/routes/mainpage/packageEditorDialog.svelte index ce36d4d5..cb743016 100644 --- a/frontend/src/routes/mainpage/packageEditorDialog.svelte +++ b/frontend/src/routes/mainpage/packageEditorDialog.svelte @@ -24,17 +24,12 @@ import { ReactivePackage } from "$lib/models/reactive/models/reactive-package.svelte.js"; import { getControlButtonsForReactiveObject } from "$lib/models/reactive/utils/reactive-objects-control-button-utils.js"; import { forceReloadTrigger } from "$lib/sharedState.svelte.js"; - import { datasetStore } from "$lib/stores/DatasetStore.ts"; import { packageStore } from "$lib/stores/PackageStore.ts"; import { getPackageDisplayLabel, restorePackageLabelPrefix, } from "$lib/utils/package-label.js"; - import { - getPackages, - } from "./classEditor/fetch-class-editor-context.js"; - let { showDialog = $bindable(), pack, From b1e92a98e84ecd87af9733e64e1bacaade6e6393 Mon Sep 17 00:00:00 2001 From: tarn Date: Wed, 24 Jun 2026 15:25:01 +0200 Subject: [PATCH 13/22] fix --- .github/workflows/backend-ci.yml | 2 -- .../src/main/java/org/rdfarchitect/OpenApiSpecExport.java | 5 +++++ frontend/openapi.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 160cd5c4..c1607112 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -44,8 +44,6 @@ jobs: run: mvn -B verify - name: Export OpenAPI spec run: mvn exec:java@export-openapi - - name: Verify committed OpenAPI spec is up to date - run: git diff --exit-code -- ../frontend/openapi.json - name: Update third-party licenses (Renovate PRs only) if: > github.event_name == 'pull_request' && diff --git a/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java b/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java index 38509ebc..a98d178c 100644 --- a/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java +++ b/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java @@ -115,6 +115,11 @@ private static String formatDeterministically(String rawSpec) throws JsonProcess Map spec = mapper.readValue(rawSpec, new TypeReference<>() {}); spec.remove("servers"); + if (spec.get("info") instanceof Map info) { + @SuppressWarnings("unchecked") + Map writableInfo = (Map) info; + writableInfo.remove("version"); + } sortRequiredArrays(spec); return mapper.writer(printer).writeValueAsString(spec) + "\n"; } diff --git a/frontend/openapi.json b/frontend/openapi.json index aa363188..d0d6153b 100644 --- a/frontend/openapi.json +++ b/frontend/openapi.json @@ -1737,7 +1737,7 @@ "url" : "https://www.apache.org/licenses/LICENSE-2.0" }, "title" : "RDFArchitect backend", - "version" : "1.1.0-44-g6e00c92d" + "version" : "1.1.0-46-ge547afbe" }, "openapi" : "3.1.0", "paths" : { From 2406feedcef2a472880f48d025e92a136558d8ac Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Spahn Date: Thu, 25 Jun 2026 13:36:27 +0200 Subject: [PATCH 14/22] fix(openapi): pin version to 0.0.0 in OpenAPI spec to prevent drift across builds Signed-off-by: Jan-Hendrik Spahn --- .github/workflows/backend-ci.yml | 2 ++ .../src/main/java/org/rdfarchitect/OpenApiSpecExport.java | 7 +++++-- frontend/openapi.json | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index c1607112..160cd5c4 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -44,6 +44,8 @@ jobs: run: mvn -B verify - name: Export OpenAPI spec run: mvn exec:java@export-openapi + - name: Verify committed OpenAPI spec is up to date + run: git diff --exit-code -- ../frontend/openapi.json - name: Update third-party licenses (Renovate PRs only) if: > github.event_name == 'pull_request' && diff --git a/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java b/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java index a98d178c..143352ae 100644 --- a/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java +++ b/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java @@ -102,7 +102,10 @@ private static Path resolveTargetPath(String[] args) { * * Re-serializes the spec with sorted keys and a fixed {@code \n} indenter so the output is * * stable across runs and operating systems. The {@code servers} entry is dropped because it * * reflects the request URL and is overridden at runtime by the frontend client ({@code * - * src/lib/api/hey-api.ts}); keeping it adds no value to the committed contract. + * src/lib/api/hey-api.ts}); keeping it adds no value to the committed contract. The {@code * + * info.version} is pinned to {@code 0.0.0} because it is derived from the build version (git * + * describe), which changes on every commit and would otherwise make the committed contract * + * drift on every build. */ private static String formatDeterministically(String rawSpec) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); @@ -118,7 +121,7 @@ private static String formatDeterministically(String rawSpec) throws JsonProcess if (spec.get("info") instanceof Map info) { @SuppressWarnings("unchecked") Map writableInfo = (Map) info; - writableInfo.remove("version"); + writableInfo.put("version", "0.0.0"); } sortRequiredArrays(spec); return mapper.writer(printer).writeValueAsString(spec) + "\n"; diff --git a/frontend/openapi.json b/frontend/openapi.json index d0d6153b..e5748476 100644 --- a/frontend/openapi.json +++ b/frontend/openapi.json @@ -1737,7 +1737,7 @@ "url" : "https://www.apache.org/licenses/LICENSE-2.0" }, "title" : "RDFArchitect backend", - "version" : "1.1.0-46-ge547afbe" + "version" : "0.0.0" }, "openapi" : "3.1.0", "paths" : { From b1f78c9a8370e5cb5dd7e9338e82b17398a008f3 Mon Sep 17 00:00:00 2001 From: tarn Date: Wed, 1 Jul 2026 12:48:48 +0200 Subject: [PATCH 15/22] review changes --- .../org/rdfarchitect/OpenApiSpecExport.java | 1 - ...llCustomDatasetDiagramsRESTController.java | 2 +- .../AllCustomDiagramsRESTController.java | 2 +- frontend/openapi.json | 4 +- frontend/src/lib/GraphExport.svelte | 10 +- frontend/src/lib/api/backend.js | 36 ------ frontend/src/lib/api/hey-api.ts | 3 +- .../lib/components/FileSelectButton.svelte | 2 +- frontend/src/lib/stores/ClassStore.ts | 7 +- frontend/src/lib/stores/DiagramStore.ts | 8 +- frontend/src/lib/stores/OntologyStore.ts | 4 - frontend/src/routes/+layout.svelte | 3 +- frontend/src/routes/GraphDeleteDialog.svelte | 2 +- frontend/src/routes/ImportDialog.svelte | 8 +- frontend/src/routes/NamespacesDialog.svelte | 1 - frontend/src/routes/NewClassDialog.svelte | 7 +- frontend/src/routes/NewPackageDialog.svelte | 2 +- frontend/src/routes/ResetSessionDialog.svelte | 2 +- frontend/src/routes/Searchbar.svelte | 2 +- frontend/src/routes/changelog/Changes.svelte | 3 +- .../src/routes/changelog/ChangesRow.svelte | 12 +- .../src/routes/compare/CompareDialog.svelte | 2 +- .../src/routes/layout/menu-bar/Edit.svelte | 118 +++++++++--------- .../mainpage/classEditor/classEditor.svelte | 8 +- .../src/routes/mainpage/mermaidWrapper.svelte | 41 +++--- .../AddToDatasetDiagramDialog.svelte | 6 +- .../AddToGraphDiagramDialog.svelte | 5 +- .../OntologyDialog.svelte | 3 +- .../src/routes/shacl/SHACLUploadDialog.svelte | 2 +- 29 files changed, 138 insertions(+), 168 deletions(-) delete mode 100644 frontend/src/lib/api/backend.js diff --git a/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java b/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java index 143352ae..70d212fb 100644 --- a/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java +++ b/backend/src/main/java/org/rdfarchitect/OpenApiSpecExport.java @@ -88,7 +88,6 @@ public static void main(String[] args) throws IOException { System.out.println("Wrote OpenAPI spec to " + target.toAbsolutePath().normalize()); } - System.exit(0); } private static Path resolveTargetPath(String[] args) { diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/AllCustomDatasetDiagramsRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/AllCustomDatasetDiagramsRESTController.java index 0991d2e9..1561ec73 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/AllCustomDatasetDiagramsRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/diagrams/AllCustomDatasetDiagramsRESTController.java @@ -45,7 +45,7 @@ public class AllCustomDatasetDiagramsRESTController { private final GetCustomDiagramsUseCase getCustomDiagramsUseCase; @GetMapping - public List getCustomDiagramList( + public List getCustomDatasetDiagramList( @Parameter(description = "The name/url of the inquirer.") @RequestHeader( value = HttpHeaders.ORIGIN, diff --git a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/AllCustomDiagramsRESTController.java b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/AllCustomDiagramsRESTController.java index be587b6c..e10580d2 100644 --- a/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/AllCustomDiagramsRESTController.java +++ b/backend/src/main/java/org/rdfarchitect/api/controller/datasets/graphs/diagrams/AllCustomDiagramsRESTController.java @@ -48,7 +48,7 @@ public class AllCustomDiagramsRESTController { private final GetCustomDiagramsUseCase getCustomDiagramsUseCase; @GetMapping - public List getCustomDiagramList( + public List getCustomGraphDiagramList( @Parameter(description = "The name/url of the inquirer.") @RequestHeader( value = HttpHeaders.ORIGIN, diff --git a/frontend/openapi.json b/frontend/openapi.json index e5748476..efa98c20 100644 --- a/frontend/openapi.json +++ b/frontend/openapi.json @@ -1925,7 +1925,7 @@ }, "/api/datasets/{datasetName}/diagrams" : { "get" : { - "operationId" : "getCustomDiagramList_1", + "operationId" : "getCustomDatasetDiagramList", "parameters" : [ { "description" : "The name/url of the inquirer.", @@ -4577,7 +4577,7 @@ }, "/api/datasets/{datasetName}/graphs/{graphURI}/diagrams" : { "get" : { - "operationId" : "getCustomDiagramList", + "operationId" : "getCustomGraphDiagramList", "parameters" : [ { "description" : "The name/url of the inquirer.", diff --git a/frontend/src/lib/GraphExport.svelte b/frontend/src/lib/GraphExport.svelte index fc9f601b..2a09b7a3 100644 --- a/frontend/src/lib/GraphExport.svelte +++ b/frontend/src/lib/GraphExport.svelte @@ -72,18 +72,18 @@ $effect(async () => { if (selectedDatasetName && graphURI) { await ontologyStore.loadOntology(selectedDatasetName, graphURI); - const { data, error } = await ontologyStore.getOntologyForGraph( + const result = await ontologyStore.getOntologyForGraph( selectedDatasetName, graphURI, ); - if (error) { + if (result == null) { ontology = null; return; } ontology = new ReactiveOntology( - data.uuid, - data.namespace, - data.entries, + result.uuid, + result.namespace, + result.entries, namespaces, ); } diff --git a/frontend/src/lib/api/backend.js b/frontend/src/lib/api/backend.js deleted file mode 100644 index cfa7a41b..00000000 --- a/frontend/src/lib/api/backend.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2024-2026 SOPTIM AG - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { PUBLIC_BACKEND_URL } from "$lib/config/runtime"; - -export class BackendConnection { - fetch; - url; - - constructor(fetch, url) { - this.fetch = fetch; - this.url = url; - } - - async resetSession() { - const url = `${PUBLIC_BACKEND_URL}/session`; - return fetch(url, { - method: "DELETE", - credentials: "include", - }); - } -} diff --git a/frontend/src/lib/api/hey-api.ts b/frontend/src/lib/api/hey-api.ts index e946980c..08426e9a 100644 --- a/frontend/src/lib/api/hey-api.ts +++ b/frontend/src/lib/api/hey-api.ts @@ -15,10 +15,11 @@ * */ +import { PUBLIC_BACKEND_URL } from "../config/runtime" import { CreateClientConfig } from "./generated/client"; export const createClientConfig: CreateClientConfig = config => ({ ...config, - baseUrl: "http://localhost:8080/", + baseUrl: PUBLIC_BACKEND_URL, credentials: "include", }); diff --git a/frontend/src/lib/components/FileSelectButton.svelte b/frontend/src/lib/components/FileSelectButton.svelte index 95d95de6..42e69972 100644 --- a/frontend/src/lib/components/FileSelectButton.svelte +++ b/frontend/src/lib/components/FileSelectButton.svelte @@ -39,7 +39,7 @@

- {file ? file.label : "no file selected"} + {file ? file.name : "no file selected"}

diff --git a/frontend/src/lib/stores/ClassStore.ts b/frontend/src/lib/stores/ClassStore.ts index f7cd4a97..1a8ddf12 100644 --- a/frontend/src/lib/stores/ClassStore.ts +++ b/frontend/src/lib/stores/ClassStore.ts @@ -955,7 +955,7 @@ function createClassStore() { return { error }; } - const enriched: AssociationPairDto = { + const updatedPair: AssociationPairDto = { from: { ...pair.from, uuid: data?.fromUUID ?? pair.from?.uuid }, to: { ...pair.to, uuid: data?.toUUID ?? pair.to?.uuid }, }; @@ -967,14 +967,15 @@ function createClassStore() { ); return { ...prev, - associationPairs: upsertAssociationPair(filtered, enriched), + associationPairs: upsertAssociationPair(filtered, updatedPair), }; }); console.log( `${LOG_PREFIX} Replaced association pair uuid="${associationUUID}"`, ); toastStore.success("Association saved", "Changes were applied."); - return { error: null, data: data }; + + return { error: null, data: data ?? undefined }; } async function deleteAssociationPair( diff --git a/frontend/src/lib/stores/DiagramStore.ts b/frontend/src/lib/stores/DiagramStore.ts index df0969b7..fadba5b8 100644 --- a/frontend/src/lib/stores/DiagramStore.ts +++ b/frontend/src/lib/stores/DiagramStore.ts @@ -19,8 +19,8 @@ import { writable, get } from "svelte/store"; import { describeError } from "./StoreLogging"; import { - getCustomDiagramList, - getCustomDiagramList1, + getCustomDatasetDiagramList, + getCustomGraphDiagramList, replaceDiagram, replaceDiagram1, deleteDiagram, @@ -126,7 +126,7 @@ function createCustomDiagramStore() { const pending = (async () => { try { - const { data, error } = await getCustomDiagramList1({ + const { data, error } = await getCustomDatasetDiagramList({ path: { datasetName }, }); @@ -195,7 +195,7 @@ function createCustomDiagramStore() { const pending = (async () => { try { - const { data, error } = await getCustomDiagramList({ + const { data, error } = await getCustomGraphDiagramList({ path: { datasetName, graphURI }, }); diff --git a/frontend/src/lib/stores/OntologyStore.ts b/frontend/src/lib/stores/OntologyStore.ts index 89819178..9ae3cad9 100644 --- a/frontend/src/lib/stores/OntologyStore.ts +++ b/frontend/src/lib/stores/OntologyStore.ts @@ -290,10 +290,6 @@ function createOntologyStore() { console.log( `${LOG_PREFIX} Generated ${entries.length} ontology entries for dataset="${datasetName}", graph="${graphURI}"`, ); - toastStore.success( - "Ontology generated", - `Generated ${entries.length} ontology entry${entries.length === 1 ? "" : "ies"}.`, - ); return { error: null, data: entries }; } diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 7d4b847a..9374e460 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -32,6 +32,7 @@ import { toastStore } from "$lib/eventhandling/toastStore.svelte.js"; import { datasetStore } from "$lib/stores/DatasetStore.ts"; import { versionControlStore } from "$lib/stores/VersionControlStore.ts"; + import { loadSnapshot as loadSnapshotAPI } from "$lib/api/generated/index.ts"; import { editorState, @@ -96,7 +97,7 @@ async function loadSnapshot() { const base64Param = page.url.searchParams.get("snapshot"); if (base64Param) { - const { error } = await loadSnapshot({ path: { base64Param } }); + const { error } = await loadSnapshotAPI({ path: { base64Param } }); if (!error) { await goto("/mainpage"); toastStore.success( diff --git a/frontend/src/routes/GraphDeleteDialog.svelte b/frontend/src/routes/GraphDeleteDialog.svelte index 60c0f3f1..bfdc48e1 100644 --- a/frontend/src/routes/GraphDeleteDialog.svelte +++ b/frontend/src/routes/GraphDeleteDialog.svelte @@ -44,7 +44,7 @@ } async function deleteGraph() { - const { error } = graphStore.remove(datasetName, graphURI); + const { error } = await graphStore.remove(datasetName, graphURI); if (error) return; editorState.selectedDataset.updateValue(null); diff --git a/frontend/src/routes/ImportDialog.svelte b/frontend/src/routes/ImportDialog.svelte index 7cad4109..b6c29a7b 100644 --- a/frontend/src/routes/ImportDialog.svelte +++ b/frontend/src/routes/ImportDialog.svelte @@ -189,15 +189,15 @@ const graphUrisLocal = files.map(entry => entry.isZip ? "" - : ensureGraphNamespaceUri(entry.graphUri, entry.file.label), + : ensureGraphNamespaceUri(entry.graphUri, entry.file.name), ); - const { data } = await graphStore.importGraphs( + const { data, error } = await graphStore.importGraphs( datasetNameUserInputLocal, filesLocal, graphUrisLocal, ); - if (data.importedGraphUris?.length > 0) { + if (!error && data.importedGraphUris?.length > 0) { editorState.selectedDataset.updateValue(datasetNameUserInputLocal); editorState.selectedGraph.updateValue( data.importedGraphUris[0] || null, @@ -332,7 +332,7 @@ class="border-border bg-window-background focus:border-blue ring-none w-full rounded border-2 p-2 text-sm outline-none" type="text" value={fileEntry.isZip - ? fileEntry.file.label + ? fileEntry.file.name : fileEntry.graphUri} disabled={fileEntry.isZip} oninput={event => diff --git a/frontend/src/routes/NamespacesDialog.svelte b/frontend/src/routes/NamespacesDialog.svelte index 60ce1784..21ad1d27 100644 --- a/frontend/src/routes/NamespacesDialog.svelte +++ b/frontend/src/routes/NamespacesDialog.svelte @@ -53,7 +53,6 @@ async function loadNamespaces(datasetNameLocal) { const namespaceDTOs = datasetStore.getNamespaces(datasetNameLocal); - console.log(namespaceDTOs); const objectsForReactiveNamespaces = namespaceDTOs.map(namespaceDto => { return mapNamespaceDtoToReactiveNamespace(namespaceDto); }); diff --git a/frontend/src/routes/NewClassDialog.svelte b/frontend/src/routes/NewClassDialog.svelte index 8bc1a99b..4fecaba3 100644 --- a/frontend/src/routes/NewClassDialog.svelte +++ b/frontend/src/routes/NewClassDialog.svelte @@ -170,7 +170,12 @@ return; } - packages = await packageStore.getPackages(datasetName, graphURI); + await packageStore.load(datasetName, graphURI); + const result = await packageStore.getPackages(datasetName, graphURI); + packages = [ + ...result.internal, + ...result.external, + ] } async function newClass() { diff --git a/frontend/src/routes/NewPackageDialog.svelte b/frontend/src/routes/NewPackageDialog.svelte index 49f824db..be90ce7d 100644 --- a/frontend/src/routes/NewPackageDialog.svelte +++ b/frontend/src/routes/NewPackageDialog.svelte @@ -176,7 +176,7 @@ label: packageLabel, comment: packageComment, }); - const { data, error } = packageStore.addPackage(ds, graph, body); + const { data, error } = await packageStore.addPackage(ds, graph, body); if (error) return; diff --git a/frontend/src/routes/ResetSessionDialog.svelte b/frontend/src/routes/ResetSessionDialog.svelte index c1e1d783..c6239df9 100644 --- a/frontend/src/routes/ResetSessionDialog.svelte +++ b/frontend/src/routes/ResetSessionDialog.svelte @@ -23,7 +23,7 @@ let { showDialog = $bindable() } = $props(); async function callResetSession() { - const { error } = resetSession(); + const { error } = await resetSession(); if (error) { toastStore.error("Reset failed", "Could not reset the session."); return; diff --git a/frontend/src/routes/Searchbar.svelte b/frontend/src/routes/Searchbar.svelte index c2d3be94..c49c1f35 100644 --- a/frontend/src/routes/Searchbar.svelte +++ b/frontend/src/routes/Searchbar.svelte @@ -256,7 +256,7 @@ showScopeDropdown = false; }} > - {filter.label} + {filter.name} {/each}
diff --git a/frontend/src/routes/changelog/Changes.svelte b/frontend/src/routes/changelog/Changes.svelte index dc2d9b49..61ec2e8a 100644 --- a/frontend/src/routes/changelog/Changes.svelte +++ b/frontend/src/routes/changelog/Changes.svelte @@ -15,6 +15,7 @@ --> diff --git a/frontend/src/routes/migrate/+page.svelte b/frontend/src/routes/migrate/+page.svelte index e229ecd9..9aca5c94 100644 --- a/frontend/src/routes/migrate/+page.svelte +++ b/frontend/src/routes/migrate/+page.svelte @@ -18,8 +18,8 @@