From 4122567fe8a2d9b5fc1b05e7dfbb4d315c34e8fd Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 9 Oct 2025 15:26:04 +0200 Subject: [PATCH 01/41] base setup --- frontend/webEditor/.gitignore | 2 +- frontend/webEditor/.husky/.gitignore | 1 - frontend/webEditor/.husky/pre-commit | 9 - frontend/webEditor/.vscode/launch.json | 34 - frontend/webEditor/.vscode/settings.json | 5 - frontend/webEditor/package copy.json | 43 + frontend/webEditor/package-lock.json | 3285 ++++------------- frontend/webEditor/package.json | 4 +- .../webEditor/src/common/commandPalette.ts | 0 .../webEditor/src/common/commonStyling.css | 136 - .../src/common/customCommandStack.ts | 35 - .../webEditor/src/common/deleteKeyListener.ts | 67 - frontend/webEditor/src/common/di.config.ts | 58 - .../src/common/fitToScreenKeyListener.ts | 25 - frontend/webEditor/src/common/helpUi.css | 16 - frontend/webEditor/src/common/helpUi.ts | 58 - .../webEditor/src/common/labelEditNoScroll.ts | 45 - .../webEditor/src/common/loadingIndicator.css | 29 - .../webEditor/src/common/loadingIndicator.ts | 46 - frontend/webEditor/src/favicon.ico | Bin 165662 -> 0 bytes .../src/features/autoLayout/command.ts | 103 - .../src/features/autoLayout/di.config.ts | 18 - .../src/features/autoLayout/keyListener.ts | 21 - .../src/features/autoLayout/layouter.ts | 420 --- .../commandPalette/commandPalette.css | 58 - .../features/commandPalette/commandPalette.ts | 209 -- .../commandPalette/commandPaletteProvider.ts | 71 - .../src/features/commandPalette/di.config.ts | 12 - .../features/constraintMenu/AutoCompletion.ts | 300 -- .../features/constraintMenu/ConstraintMenu.ts | 339 -- .../features/constraintMenu/DslLanguage.ts | 409 -- .../src/features/constraintMenu/actions.ts | 14 - .../src/features/constraintMenu/commands.ts | 73 - .../constraintMenu/constraintMenu.css | 145 - .../constraintMenu/constraintRegistry.ts | 102 - .../src/features/constraintMenu/di.config.ts | 23 - .../src/features/copyPaste/di.config.ts | 16 - .../src/features/copyPaste/keyListener.ts | 63 - .../src/features/copyPaste/pasteCommand.ts | 218 -- .../dfdElements/AssignmentLanguage.ts | 477 --- .../dfdElements/behaviorRefactorer.ts | 244 -- .../src/features/dfdElements/di.config.ts | 71 - .../features/dfdElements/dynamicChildren.ts | 88 - .../src/features/dfdElements/edges.tsx | 166 - .../dfdElements/editLabelValidator.css | 26 - .../dfdElements/editLabelValidator.ts | 102 - .../features/dfdElements/elementStyles.css | 117 - .../src/features/dfdElements/labels.tsx | 62 - .../features/dfdElements/nodeAnnotationUi.css | 14 - .../features/dfdElements/nodeAnnotationUi.ts | 218 -- .../src/features/dfdElements/nodes.tsx | 315 -- .../features/dfdElements/outputPortEditUi.css | 22 - .../features/dfdElements/outputPortEditUi.ts | 483 --- .../src/features/dfdElements/portSnapper.ts | 192 - .../src/features/dfdElements/ports.tsx | 178 - .../src/features/editorMode/command.ts | 87 - .../src/features/editorMode/di.config.ts | 23 - .../editorMode/editorModeController.ts | 40 - .../src/features/editorMode/modeSwitchUi.css | 11 - .../src/features/editorMode/modeSwitchUi.ts | 57 - .../src/features/editorMode/sprottyHooks.ts | 58 - .../webEditor/src/features/labels/commands.ts | 355 -- .../src/features/labels/di.config.ts | 35 - .../src/features/labels/dropListener.ts | 60 - .../src/features/labels/elementFeature.ts | 29 - .../src/features/labels/labelRenderer.tsx | 125 - .../src/features/labels/labelTypeEditor.css | 69 - .../src/features/labels/labelTypeEditor.ts | 383 -- .../src/features/labels/labelTypeRegistry.ts | 59 - .../src/features/serialize/analyze.ts | 81 - .../features/serialize/defaultDiagram.json | 623 ---- .../src/features/serialize/di.config.ts | 27 - .../src/features/serialize/dropListener.ts | 30 - .../webEditor/src/features/serialize/image.ts | 107 - .../src/features/serialize/keyListener.ts | 31 - .../webEditor/src/features/serialize/load.ts | 367 -- .../src/features/serialize/loadDFDandDD.ts | 128 - .../features/serialize/loadDefaultDiagram.ts | 138 - .../src/features/serialize/loadPalladio.ts | 143 - .../webEditor/src/features/serialize/save.ts | 102 - .../src/features/serialize/saveDFDandDD.ts | 122 - .../features/serialize/webSocketHandler.ts | 73 - .../src/features/settingsMenu/LayoutMethod.ts | 5 - .../features/settingsMenu/SettingsManager.ts | 94 - .../src/features/settingsMenu/actions.ts | 67 - .../settingsMenu/annotationManager.ts | 24 - .../src/features/settingsMenu/commands.ts | 272 -- .../src/features/settingsMenu/di.config.ts | 30 - .../features/settingsMenu/settingsMenu.css | 109 - .../src/features/settingsMenu/settingsMenu.ts | 123 - .../src/features/settingsMenu/themeManager.ts | 71 - .../src/features/toolPalette/creationTool.ts | 298 -- .../src/features/toolPalette/di.config.ts | 45 - .../features/toolPalette/edgeCreationTool.ts | 120 - .../features/toolPalette/nodeCreationTool.ts | 24 - .../features/toolPalette/portCreationTool.ts | 67 - .../src/features/toolPalette/toolPalette.css | 63 - .../src/features/toolPalette/toolPalette.tsx | 275 -- frontend/webEditor/src/index.ts | 164 +- frontend/webEditor/src/page.css | 28 - frontend/webEditor/src/theme.css | 28 - frontend/webEditor/src/utils.ts | 86 - 102 files changed, 704 insertions(+), 13939 deletions(-) delete mode 100644 frontend/webEditor/.husky/.gitignore delete mode 100755 frontend/webEditor/.husky/pre-commit delete mode 100644 frontend/webEditor/.vscode/launch.json delete mode 100644 frontend/webEditor/.vscode/settings.json create mode 100644 frontend/webEditor/package copy.json delete mode 100644 frontend/webEditor/src/common/commandPalette.ts delete mode 100644 frontend/webEditor/src/common/commonStyling.css delete mode 100644 frontend/webEditor/src/common/customCommandStack.ts delete mode 100644 frontend/webEditor/src/common/deleteKeyListener.ts delete mode 100644 frontend/webEditor/src/common/di.config.ts delete mode 100644 frontend/webEditor/src/common/fitToScreenKeyListener.ts delete mode 100644 frontend/webEditor/src/common/helpUi.css delete mode 100644 frontend/webEditor/src/common/helpUi.ts delete mode 100644 frontend/webEditor/src/common/labelEditNoScroll.ts delete mode 100644 frontend/webEditor/src/common/loadingIndicator.css delete mode 100644 frontend/webEditor/src/common/loadingIndicator.ts delete mode 100644 frontend/webEditor/src/favicon.ico delete mode 100644 frontend/webEditor/src/features/autoLayout/command.ts delete mode 100644 frontend/webEditor/src/features/autoLayout/di.config.ts delete mode 100644 frontend/webEditor/src/features/autoLayout/keyListener.ts delete mode 100644 frontend/webEditor/src/features/autoLayout/layouter.ts delete mode 100644 frontend/webEditor/src/features/commandPalette/commandPalette.css delete mode 100644 frontend/webEditor/src/features/commandPalette/commandPalette.ts delete mode 100644 frontend/webEditor/src/features/commandPalette/commandPaletteProvider.ts delete mode 100644 frontend/webEditor/src/features/commandPalette/di.config.ts delete mode 100644 frontend/webEditor/src/features/constraintMenu/AutoCompletion.ts delete mode 100644 frontend/webEditor/src/features/constraintMenu/ConstraintMenu.ts delete mode 100644 frontend/webEditor/src/features/constraintMenu/DslLanguage.ts delete mode 100644 frontend/webEditor/src/features/constraintMenu/actions.ts delete mode 100644 frontend/webEditor/src/features/constraintMenu/commands.ts delete mode 100644 frontend/webEditor/src/features/constraintMenu/constraintMenu.css delete mode 100644 frontend/webEditor/src/features/constraintMenu/constraintRegistry.ts delete mode 100644 frontend/webEditor/src/features/constraintMenu/di.config.ts delete mode 100644 frontend/webEditor/src/features/copyPaste/di.config.ts delete mode 100644 frontend/webEditor/src/features/copyPaste/keyListener.ts delete mode 100644 frontend/webEditor/src/features/copyPaste/pasteCommand.ts delete mode 100644 frontend/webEditor/src/features/dfdElements/AssignmentLanguage.ts delete mode 100644 frontend/webEditor/src/features/dfdElements/behaviorRefactorer.ts delete mode 100644 frontend/webEditor/src/features/dfdElements/di.config.ts delete mode 100644 frontend/webEditor/src/features/dfdElements/dynamicChildren.ts delete mode 100644 frontend/webEditor/src/features/dfdElements/edges.tsx delete mode 100644 frontend/webEditor/src/features/dfdElements/editLabelValidator.css delete mode 100644 frontend/webEditor/src/features/dfdElements/editLabelValidator.ts delete mode 100644 frontend/webEditor/src/features/dfdElements/elementStyles.css delete mode 100644 frontend/webEditor/src/features/dfdElements/labels.tsx delete mode 100644 frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.css delete mode 100644 frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.ts delete mode 100644 frontend/webEditor/src/features/dfdElements/nodes.tsx delete mode 100644 frontend/webEditor/src/features/dfdElements/outputPortEditUi.css delete mode 100644 frontend/webEditor/src/features/dfdElements/outputPortEditUi.ts delete mode 100644 frontend/webEditor/src/features/dfdElements/portSnapper.ts delete mode 100644 frontend/webEditor/src/features/dfdElements/ports.tsx delete mode 100644 frontend/webEditor/src/features/editorMode/command.ts delete mode 100644 frontend/webEditor/src/features/editorMode/di.config.ts delete mode 100644 frontend/webEditor/src/features/editorMode/editorModeController.ts delete mode 100644 frontend/webEditor/src/features/editorMode/modeSwitchUi.css delete mode 100644 frontend/webEditor/src/features/editorMode/modeSwitchUi.ts delete mode 100644 frontend/webEditor/src/features/editorMode/sprottyHooks.ts delete mode 100644 frontend/webEditor/src/features/labels/commands.ts delete mode 100644 frontend/webEditor/src/features/labels/di.config.ts delete mode 100644 frontend/webEditor/src/features/labels/dropListener.ts delete mode 100644 frontend/webEditor/src/features/labels/elementFeature.ts delete mode 100644 frontend/webEditor/src/features/labels/labelRenderer.tsx delete mode 100644 frontend/webEditor/src/features/labels/labelTypeEditor.css delete mode 100644 frontend/webEditor/src/features/labels/labelTypeEditor.ts delete mode 100644 frontend/webEditor/src/features/labels/labelTypeRegistry.ts delete mode 100644 frontend/webEditor/src/features/serialize/analyze.ts delete mode 100644 frontend/webEditor/src/features/serialize/defaultDiagram.json delete mode 100644 frontend/webEditor/src/features/serialize/di.config.ts delete mode 100644 frontend/webEditor/src/features/serialize/dropListener.ts delete mode 100644 frontend/webEditor/src/features/serialize/image.ts delete mode 100644 frontend/webEditor/src/features/serialize/keyListener.ts delete mode 100644 frontend/webEditor/src/features/serialize/load.ts delete mode 100644 frontend/webEditor/src/features/serialize/loadDFDandDD.ts delete mode 100644 frontend/webEditor/src/features/serialize/loadDefaultDiagram.ts delete mode 100644 frontend/webEditor/src/features/serialize/loadPalladio.ts delete mode 100644 frontend/webEditor/src/features/serialize/save.ts delete mode 100644 frontend/webEditor/src/features/serialize/saveDFDandDD.ts delete mode 100644 frontend/webEditor/src/features/serialize/webSocketHandler.ts delete mode 100644 frontend/webEditor/src/features/settingsMenu/LayoutMethod.ts delete mode 100644 frontend/webEditor/src/features/settingsMenu/SettingsManager.ts delete mode 100644 frontend/webEditor/src/features/settingsMenu/actions.ts delete mode 100644 frontend/webEditor/src/features/settingsMenu/annotationManager.ts delete mode 100644 frontend/webEditor/src/features/settingsMenu/commands.ts delete mode 100644 frontend/webEditor/src/features/settingsMenu/di.config.ts delete mode 100644 frontend/webEditor/src/features/settingsMenu/settingsMenu.css delete mode 100644 frontend/webEditor/src/features/settingsMenu/settingsMenu.ts delete mode 100644 frontend/webEditor/src/features/settingsMenu/themeManager.ts delete mode 100644 frontend/webEditor/src/features/toolPalette/creationTool.ts delete mode 100644 frontend/webEditor/src/features/toolPalette/di.config.ts delete mode 100644 frontend/webEditor/src/features/toolPalette/edgeCreationTool.ts delete mode 100644 frontend/webEditor/src/features/toolPalette/nodeCreationTool.ts delete mode 100644 frontend/webEditor/src/features/toolPalette/portCreationTool.ts delete mode 100644 frontend/webEditor/src/features/toolPalette/toolPalette.css delete mode 100644 frontend/webEditor/src/features/toolPalette/toolPalette.tsx delete mode 100644 frontend/webEditor/src/page.css delete mode 100644 frontend/webEditor/src/theme.css delete mode 100644 frontend/webEditor/src/utils.ts diff --git a/frontend/webEditor/.gitignore b/frontend/webEditor/.gitignore index 76add878..f06235c4 100644 --- a/frontend/webEditor/.gitignore +++ b/frontend/webEditor/.gitignore @@ -1,2 +1,2 @@ node_modules -dist \ No newline at end of file +dist diff --git a/frontend/webEditor/.husky/.gitignore b/frontend/webEditor/.husky/.gitignore deleted file mode 100644 index 31354ec1..00000000 --- a/frontend/webEditor/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/frontend/webEditor/.husky/pre-commit b/frontend/webEditor/.husky/pre-commit deleted file mode 100755 index 0ad91c2c..00000000 --- a/frontend/webEditor/.husky/pre-commit +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env sh -set -e - -# Only run when WebEditor files are staged (optional guard) -if git diff --cached --name-only --diff-filter=ACMRT | grep -q '^Frontend/WebEditor/'; then - REPO_ROOT="$(git rev-parse --show-toplevel)" - cd "$REPO_ROOT/Frontend/WebEditor" - npx lint-staged -fi diff --git a/frontend/webEditor/.vscode/launch.json b/frontend/webEditor/.vscode/launch.json deleted file mode 100644 index d4733d5d..00000000 --- a/frontend/webEditor/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch vite and debug in MS Edge", - "request": "launch", - "type": "node", - "cwd": "${workspaceFolder}", - "runtimeExecutable": "npm", - "runtimeArgs": ["run-script", "dev"], - "serverReadyAction": { - "action": "debugWithEdge", - "pattern": "Local:\\s+http://localhost:([0-9]+)", - "uriFormat": "http://localhost:%s" - } - }, - { - "name": "Launch vite and debug in Chrome", - "request": "launch", - "type": "node", - "cwd": "${workspaceFolder}", - "runtimeExecutable": "npm", - "runtimeArgs": ["run-script", "dev"], - "serverReadyAction": { - "action": "debugWithChrome", - "pattern": "Local:\\s+http://localhost:([0-9]+)", - "uriFormat": "http://localhost:%s" - } - } - ] -} diff --git a/frontend/webEditor/.vscode/settings.json b/frontend/webEditor/.vscode/settings.json deleted file mode 100644 index ed1ac12d..00000000 --- a/frontend/webEditor/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - // It's annoying to get spelling infos about wWords for components that are used in this project - // So these are added here to the dictionary. - "cSpell.words": ["sprotty", "inversify", "codicon", "zorder"] -} diff --git a/frontend/webEditor/package copy.json b/frontend/webEditor/package copy.json new file mode 100644 index 00000000..5f09ab6c --- /dev/null +++ b/frontend/webEditor/package copy.json @@ -0,0 +1,43 @@ +{ + "name": "data-flow-analysis-web-editor", + "version": "0.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/DataFlowAnalysis/OnlineEditor.git" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.35.0", + "@fortawesome/fontawesome-free": "^7.0.0", + "@vscode/codicons": "^0.0.39", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", + "husky": "^9.1.7", + "inversify": "^6.2.2", + "lint-staged": "^16.1.6", + "monaco-editor": "^0.52.2", + "prettier": "^3.6.2", + "reflect-metadata": "^0.2.2", + "sprotty": "^1.4.0", + "sprotty-elk": "^1.4.0", + "sprotty-protocol": "^1.4.0", + "typescript": "^5.8.3", + "typescript-eslint": "^8.44.0", + "vite": "^7.1.7" + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "format": "prettier --write \"./**/*.{html,css,ts,tsx,json}\"", + "lint": "eslint --max-warnings 0 --no-warn-ignored", + "prepare": "husky" + }, + "lint-staged": { + "*.{html,css,ts,tsx,json}": [ + "npm run lint", + "npm run format" + ] + } +} diff --git a/frontend/webEditor/package-lock.json b/frontend/webEditor/package-lock.json index 164edf26..b1bd0730 100644 --- a/frontend/webEditor/package-lock.json +++ b/frontend/webEditor/package-lock.json @@ -1,12 +1,12 @@ { "name": "data-flow-analysis-web-editor", - "version": "0.1.0", - "lockfileVersion": 2, + "version": "0.0.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "data-flow-analysis-web-editor", - "version": "0.1.0", + "version": "0.0.0", "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.35.0", @@ -29,9 +29,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", "cpu": [ "ppc64" ], @@ -46,9 +46,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", "cpu": [ "arm" ], @@ -63,9 +63,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", "cpu": [ "arm64" ], @@ -80,9 +80,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", "cpu": [ "x64" ], @@ -97,9 +97,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", "cpu": [ "x64" ], @@ -131,9 +131,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", "cpu": [ "arm64" ], @@ -148,9 +148,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", "cpu": [ "x64" ], @@ -165,9 +165,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", "cpu": [ "arm64" ], @@ -199,9 +199,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", "cpu": [ "ia32" ], @@ -216,9 +216,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", "cpu": [ "loong64" ], @@ -233,9 +233,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", "cpu": [ "mips64el" ], @@ -250,9 +250,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", "cpu": [ "ppc64" ], @@ -267,9 +267,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", "cpu": [ "riscv64" ], @@ -284,9 +284,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", "cpu": [ "s390x" ], @@ -301,9 +301,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", "cpu": [ "x64" ], @@ -318,9 +318,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", "cpu": [ "arm64" ], @@ -335,9 +335,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", "cpu": [ "x64" ], @@ -352,9 +352,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", "cpu": [ "arm64" ], @@ -369,9 +369,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", "cpu": [ "x64" ], @@ -385,10 +385,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", "cpu": [ "x64" ], @@ -403,9 +420,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", "cpu": [ "arm64" ], @@ -420,9 +437,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", "cpu": [ "ia32" ], @@ -437,9 +454,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", "cpu": [ "x64" ], @@ -454,9 +471,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -477,6 +494,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -489,6 +507,7 @@ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -498,6 +517,7 @@ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -508,19 +528,24 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -553,10 +578,11 @@ } }, "node_modules/@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -569,17 +595,19 @@ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -587,10 +615,11 @@ } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz", - "integrity": "sha512-X48nISrSOa89zu2VMljC4XaRf8NmgTwQBVHfS2Nu5G00ZwM31oOVrAtGxZF3b6wDYf9lJsf/Eq4cCSFKIkOWPQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz", + "integrity": "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==", "dev": true, + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" } @@ -600,41 +629,31 @@ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -644,10 +663,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -660,13 +680,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.4.0.tgz", "integrity": "sha512-qfRJ/3iOlCL/VfJq8+4o5X4oA14cZSBbpAmHsYj8EsIit1xDndoOl0xKOyglKtQD4u4gdNVxMHx4RWARk/I4QA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@inversifyjs/core": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-1.3.5.tgz", "integrity": "sha512-B4MFXabhNTAmrfgB+yeD6wd/GIvmvWC6IQ8Rh/j2C3Ix69kmqwz9pr8Jt3E+Nho9aEHOQCZaGmrALgtqRd+oEQ==", "dev": true, + "license": "MIT", "dependencies": { "@inversifyjs/common": "1.4.0", "@inversifyjs/reflect-metadata-utils": "0.2.4" @@ -677,85 +699,17 @@ "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-0.2.4.tgz", "integrity": "sha512-u95rV3lKfG+NT2Uy/5vNzoDujos8vN8O18SSA5UyhxsGYd4GLQn/eUsGXfOsfa7m34eKrDelTKRUX1m/BcNX5w==", "dev": true, + "license": "MIT", "peerDependencies": { "reflect-metadata": "0.2.2" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -769,6 +723,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -778,6 +733,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -787,260 +743,308 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1050,7 +1054,8 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -1060,16 +1065,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", - "integrity": "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", + "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/type-utils": "8.44.0", - "@typescript-eslint/utils": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/type-utils": "8.46.0", + "@typescript-eslint/utils": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1083,7 +1089,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.44.0", + "@typescript-eslint/parser": "^8.46.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1093,20 +1099,22 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz", - "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz", + "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4" }, "engines": { @@ -1122,13 +1130,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.0.tgz", - "integrity": "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz", + "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.44.0", - "@typescript-eslint/types": "^8.44.0", + "@typescript-eslint/tsconfig-utils": "^8.46.0", + "@typescript-eslint/types": "^8.46.0", "debug": "^4.3.4" }, "engines": { @@ -1143,13 +1152,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.0.tgz", - "integrity": "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz", + "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0" + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1160,10 +1170,11 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.0.tgz", - "integrity": "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz", + "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1176,14 +1187,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.0.tgz", - "integrity": "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz", + "integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/utils": "8.44.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/utils": "8.46.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1200,10 +1212,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz", - "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", + "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1213,15 +1226,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.0.tgz", - "integrity": "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz", + "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.44.0", - "@typescript-eslint/tsconfig-utils": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", + "@typescript-eslint/project-service": "8.46.0", + "@typescript-eslint/tsconfig-utils": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1245,6 +1259,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1254,6 +1269,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1265,15 +1281,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.0.tgz", - "integrity": "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz", + "integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0" + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1288,12 +1305,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.0.tgz", - "integrity": "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz", + "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/types": "8.46.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1308,7 +1326,8 @@ "version": "0.0.39", "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.39.tgz", "integrity": "sha512-gO09UZrOBONyzI8LWPRsCahnmUR16hkRQCJOSJzX8L4dC5aa6YGP4nS+gh5oSekMlM8LFJXMAgqBMGGiktdRJw==", - "dev": true + "dev": true, + "license": "CC-BY-4.0" }, "node_modules/acorn": { "version": "8.15.0", @@ -1328,6 +1347,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1337,6 +1357,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1349,10 +1370,11 @@ } }, "node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", "dev": true, + "license": "MIT", "dependencies": { "environment": "^1.0.0" }, @@ -1364,10 +1386,11 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1376,12 +1399,16 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -1391,25 +1418,29 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/autocompleter": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/autocompleter/-/autocompleter-9.3.2.tgz", "integrity": "sha512-rLbf2TLGOD7y+gOS36ksrZdIsvoHa2KXc2A7503w+NBRPrcF73zzFeYBxEcV/iMPjaBH3jFhNIYObZ7zt1fkCQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1428,30 +1459,28 @@ "node": ">=8" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/chalk": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", - "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -1462,6 +1491,7 @@ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" }, @@ -1473,16 +1503,17 @@ } }, "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.0.tgz", + "integrity": "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==", "dev": true, + "license": "MIT", "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1493,6 +1524,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1504,27 +1536,32 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", "dev": true, - "optional": true, - "peer": true + "license": "MIT", + "engines": { + "node": ">=20" + } }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -1542,9 +1579,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1563,25 +1600,29 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/elkjs": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", - "dev": true + "dev": true, + "license": "EPL-2.0" }, "node_modules/emoji-regex": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -1590,9 +1631,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1603,31 +1644,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" } }, "node_modules/escape-string-regexp": { @@ -1635,6 +1677,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1643,19 +1686,20 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -1707,6 +1751,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -1747,49 +1792,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -1813,6 +1815,7 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -1838,6 +1841,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1847,6 +1851,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -1855,19 +1860,22 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1884,6 +1892,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1895,19 +1904,22 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -1917,6 +1929,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -1928,7 +1941,8 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fill-range": { "version": "7.1.1", @@ -1948,6 +1962,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -1964,6 +1979,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -1976,7 +1992,8 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -1984,6 +2001,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1993,10 +2011,11 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", - "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2009,6 +2028,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -2021,6 +2041,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2032,13 +2053,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2064,6 +2087,7 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -2073,6 +2097,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2089,6 +2114,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -2112,17 +2138,22 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2133,6 +2164,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2162,6 +2194,7 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2173,25 +2206,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -2201,6 +2238,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -2209,32 +2247,17 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lint-staged": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.6.tgz", - "integrity": "sha512-U4kuulU3CKIytlkLlaHcGgKscNfJPNTiDF2avIUGFCv7K95/DCYQ7Ra62ydeRWmgQGg9zJYw2dzdbztwJlqrow==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.3.tgz", + "integrity": "sha512-1OnJEESB9zZqsp61XHH2fvpS1es3hRCxMplF/AJUDa8Ho8VrscYDIuxGrj3m8KPXbcWZ8fT9XTMUhEQmOVKpKw==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^5.6.0", - "commander": "^14.0.0", - "debug": "^4.4.1", - "lilconfig": "^3.1.3", - "listr2": "^9.0.3", + "commander": "^14.0.1", + "listr2": "^9.0.4", "micromatch": "^4.0.8", - "nano-spawn": "^1.0.2", + "nano-spawn": "^1.0.3", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" @@ -2249,23 +2272,14 @@ "url": "https://opencollective.com/lint-staged" } }, - "node_modules/lint-staged/node_modules/commander": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", - "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/listr2": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.3.tgz", - "integrity": "sha512-0aeh5HHHgmq1KRdMMDHfhMWQmIT/m7nRDTlxlFqni2Sp0had9baqsjJRvDGdlvgd6NmPE0nPloOipiQJGFtTHQ==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.4.tgz", + "integrity": "sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ==", "dev": true, + "license": "MIT", "dependencies": { - "cli-truncate": "^4.0.0", + "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", @@ -2281,6 +2295,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -2295,13 +2310,15 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, + "license": "MIT", "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", @@ -2316,42 +2333,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -2375,6 +2362,7 @@ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2387,6 +2375,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2409,9 +2398,9 @@ "license": "MIT" }, "node_modules/nano-spawn": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", - "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.3.tgz", + "integrity": "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==", "dev": true, "license": "MIT", "engines": { @@ -2444,13 +2433,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" }, @@ -2466,6 +2457,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -2483,6 +2475,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -2498,6 +2491,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -2513,6 +2507,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -2525,6 +2520,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2564,6 +2560,7 @@ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true, + "license": "MIT", "bin": { "pidtree": "bin/pidtree.js" }, @@ -2605,6 +2602,7 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -2614,6 +2612,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -2629,6 +2628,7 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2651,7 +2651,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/reflect-metadata": { "version": "0.2.2", @@ -2665,6 +2666,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -2674,6 +2676,7 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" @@ -2690,6 +2693,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -2699,13 +2703,15 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.8" }, @@ -2717,26 +2723,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" } }, @@ -2759,6 +2767,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -2768,6 +2777,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2803,6 +2813,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -2811,41 +2822,45 @@ } }, "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/snabbdom": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz", "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.3.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2856,23 +2871,12 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/sprotty": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/sprotty/-/sprotty-1.4.0.tgz", "integrity": "sha512-QGZZQAM2pOa1QxJUG05Ox76RJOKuvKloT1nCkvs6SD5w/HfkcL0mjq1Om1+fb5NAalDzurrJL6agKUReST3TFw==", "dev": true, + "license": "(EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)", "dependencies": { "autocompleter": "^9.1.2", "file-saver": "^2.0.5", @@ -2887,6 +2891,7 @@ "resolved": "https://registry.npmjs.org/sprotty-elk/-/sprotty-elk-1.4.0.tgz", "integrity": "sha512-ewSoKgqmgR3lw0EQpjYOrlzpPofCB7UyXr5k9vfpF2ho5HEswGcpoPzLtqojB5UM0TlBYnm1S59ekN+RMhY4ng==", "dev": true, + "license": "(EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0)", "dependencies": { "elkjs": "^0.8.2", "sprotty-protocol": "^1.4.0" @@ -2907,32 +2912,34 @@ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.19" } }, "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", + "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -2948,6 +2955,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -2960,6 +2968,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2967,31 +2976,12 @@ "node": ">=8" } }, - "node_modules/terser": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", - "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, + "license": "MIT", "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" @@ -3008,6 +2998,7 @@ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.0.0" }, @@ -3025,6 +3016,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3036,7 +3028,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -3056,6 +3049,7 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.12" }, @@ -3068,6 +3062,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -3076,9 +3071,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3090,15 +3085,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.44.0.tgz", - "integrity": "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz", + "integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.44.0", - "@typescript-eslint/parser": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/utils": "8.44.0" + "@typescript-eslint/eslint-plugin": "8.46.0", + "@typescript-eslint/parser": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/utils": "8.46.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3117,15 +3113,17 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/vite": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", - "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", + "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -3200,6 +3198,7 @@ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.0.0" }, @@ -3217,6 +3216,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3245,15 +3245,17 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -3266,11 +3268,43 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yaml": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, @@ -3283,6 +3317,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3290,2013 +3325,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "dev": true, - "optional": true - }, - "@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.4.3" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - } - } - }, - "@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true - }, - "@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "requires": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - } - }, - "@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", - "dev": true - }, - "@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.15" - } - }, - "@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", - "dev": true - }, - "@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true - }, - "@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", - "dev": true, - "requires": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" - } - }, - "@fortawesome/fontawesome-free": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz", - "integrity": "sha512-X48nISrSOa89zu2VMljC4XaRf8NmgTwQBVHfS2Nu5G00ZwM31oOVrAtGxZF3b6wDYf9lJsf/Eq4cCSFKIkOWPQ==", - "dev": true - }, - "@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true - }, - "@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "requires": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "dependencies": { - "@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true - } - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true - }, - "@inversifyjs/common": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.4.0.tgz", - "integrity": "sha512-qfRJ/3iOlCL/VfJq8+4o5X4oA14cZSBbpAmHsYj8EsIit1xDndoOl0xKOyglKtQD4u4gdNVxMHx4RWARk/I4QA==", - "dev": true - }, - "@inversifyjs/core": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-1.3.5.tgz", - "integrity": "sha512-B4MFXabhNTAmrfgB+yeD6wd/GIvmvWC6IQ8Rh/j2C3Ix69kmqwz9pr8Jt3E+Nho9aEHOQCZaGmrALgtqRd+oEQ==", - "dev": true, - "requires": { - "@inversifyjs/common": "1.4.0", - "@inversifyjs/reflect-metadata-utils": "0.2.4" - } - }, - "@inversifyjs/reflect-metadata-utils": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-0.2.4.tgz", - "integrity": "sha512-u95rV3lKfG+NT2Uy/5vNzoDujos8vN8O18SSA5UyhxsGYd4GLQn/eUsGXfOsfa7m34eKrDelTKRUX1m/BcNX5w==", - "dev": true, - "requires": {} - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "optional": true, - "peer": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "optional": true, - "peer": true - }, - "@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true, - "optional": true, - "peer": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", - "dev": true, - "optional": true - }, - "@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", - "integrity": "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/type-utils": "8.44.0", - "@typescript-eslint/utils": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "dependencies": { - "ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true - } - } - }, - "@typescript-eslint/parser": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz", - "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/project-service": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.0.tgz", - "integrity": "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==", - "dev": true, - "requires": { - "@typescript-eslint/tsconfig-utils": "^8.44.0", - "@typescript-eslint/types": "^8.44.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.0.tgz", - "integrity": "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0" - } - }, - "@typescript-eslint/tsconfig-utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.0.tgz", - "integrity": "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==", - "dev": true, - "requires": {} - }, - "@typescript-eslint/type-utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.0.tgz", - "integrity": "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/utils": "8.44.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - } - }, - "@typescript-eslint/types": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz", - "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.0.tgz", - "integrity": "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==", - "dev": true, - "requires": { - "@typescript-eslint/project-service": "8.44.0", - "@typescript-eslint/tsconfig-utils": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/visitor-keys": "8.44.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "@typescript-eslint/utils": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.0.tgz", - "integrity": "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.44.0", - "@typescript-eslint/types": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.0.tgz", - "integrity": "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "8.44.0", - "eslint-visitor-keys": "^4.2.1" - } - }, - "@vscode/codicons": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.39.tgz", - "integrity": "sha512-gO09UZrOBONyzI8LWPRsCahnmUR16hkRQCJOSJzX8L4dC5aa6YGP4nS+gh5oSekMlM8LFJXMAgqBMGGiktdRJw==", - "dev": true - }, - "acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "dev": true, - "requires": { - "environment": "^1.0.0" - } - }, - "ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", - "dev": true - }, - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "autocompleter": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/autocompleter/-/autocompleter-9.3.2.tgz", - "integrity": "sha512-rLbf2TLGOD7y+gOS36ksrZdIsvoHa2KXc2A7503w+NBRPrcF73zzFeYBxEcV/iMPjaBH3jFhNIYObZ7zt1fkCQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "requires": { - "fill-range": "^7.1.1" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", - "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", - "dev": true - }, - "cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "requires": { - "restore-cursor": "^5.0.0" - } - }, - "cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "requires": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "requires": { - "ms": "^2.1.3" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "elkjs": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", - "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", - "dev": true - }, - "emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "dev": true - }, - "environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true - }, - "esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "dependencies": { - "@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - } - } - }, - "eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "requires": {} - }, - "eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true - }, - "espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "requires": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - } - }, - "esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "requires": { - "flat-cache": "^4.0.0" - } - }, - "file-saver": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", - "dev": true - }, - "fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "requires": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - } - }, - "flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "optional": true - }, - "get-east-asian-width": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", - "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", - "dev": true - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true - }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true - }, - "ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true - }, - "import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "inversify": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.2.2.tgz", - "integrity": "sha512-KB836KHbZ9WrUnB8ax5MtadOwnqQYa+ZJO3KWbPFgcr4RIEnHM621VaqFZzOZd9+U7ln6upt9n0wJei7x2BNqw==", - "dev": true, - "requires": { - "@inversifyjs/common": "1.4.0", - "@inversifyjs/core": "1.3.5" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "requires": { - "json-buffer": "3.0.1" - } - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true - }, - "lint-staged": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.6.tgz", - "integrity": "sha512-U4kuulU3CKIytlkLlaHcGgKscNfJPNTiDF2avIUGFCv7K95/DCYQ7Ra62ydeRWmgQGg9zJYw2dzdbztwJlqrow==", - "dev": true, - "requires": { - "chalk": "^5.6.0", - "commander": "^14.0.0", - "debug": "^4.4.1", - "lilconfig": "^3.1.3", - "listr2": "^9.0.3", - "micromatch": "^4.0.8", - "nano-spawn": "^1.0.2", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.8.1" - }, - "dependencies": { - "commander": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", - "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", - "dev": true - } - } - }, - "listr2": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.3.tgz", - "integrity": "sha512-0aeh5HHHgmq1KRdMMDHfhMWQmIT/m7nRDTlxlFqni2Sp0had9baqsjJRvDGdlvgd6NmPE0nPloOipiQJGFtTHQ==", - "dev": true, - "requires": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "requires": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "requires": { - "get-east-asian-width": "^1.3.1" - } - }, - "slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", - "dev": true, - "requires": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - } - } - } - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "requires": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - } - }, - "mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "monaco-editor": { - "version": "0.52.2", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", - "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", - "dev": true - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "nano-spawn": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", - "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", - "dev": true - }, - "nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "requires": { - "mimic-function": "^5.0.0" - } - }, - "optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true - }, - "postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "requires": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true - }, - "punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "requires": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - } - }, - "reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true - }, - "rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true - }, - "rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", - "@types/estree": "1.0.8", - "fsevents": "~2.3.2" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true - }, - "slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - } - }, - "snabbdom": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz", - "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "peer": true - }, - "source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "sprotty": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sprotty/-/sprotty-1.4.0.tgz", - "integrity": "sha512-QGZZQAM2pOa1QxJUG05Ox76RJOKuvKloT1nCkvs6SD5w/HfkcL0mjq1Om1+fb5NAalDzurrJL6agKUReST3TFw==", - "dev": true, - "requires": { - "autocompleter": "^9.1.2", - "file-saver": "^2.0.5", - "inversify": "^6.1.3", - "snabbdom": "~3.5.1", - "sprotty-protocol": "^1.4.0", - "tinyqueue": "^2.0.3" - } - }, - "sprotty-elk": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sprotty-elk/-/sprotty-elk-1.4.0.tgz", - "integrity": "sha512-ewSoKgqmgR3lw0EQpjYOrlzpPofCB7UyXr5k9vfpF2ho5HEswGcpoPzLtqojB5UM0TlBYnm1S59ekN+RMhY4ng==", - "dev": true, - "requires": { - "elkjs": "^0.8.2", - "inversify": "^6.1.3", - "sprotty-protocol": "^1.4.0" - } - }, - "sprotty-protocol": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sprotty-protocol/-/sprotty-protocol-1.4.0.tgz", - "integrity": "sha512-+AAskW3Mzcq5UhMnummp4wwJ1dYdgT7/utmWoHtjfrK7JTJq9G/VWWlHnTnQGzHHyma03Loy2AozToXoArQuAQ==", - "dev": true - }, - "string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true - }, - "string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "requires": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - } - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "terser": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", - "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - } - }, - "tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "requires": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "dependencies": { - "fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "requires": {} - }, - "picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true - } - } - }, - "tinyqueue": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", - "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "requires": {} - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true - }, - "typescript-eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.44.0.tgz", - "integrity": "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==", - "dev": true, - "requires": { - "@typescript-eslint/eslint-plugin": "8.44.0", - "@typescript-eslint/parser": "8.44.0", - "@typescript-eslint/typescript-estree": "8.44.0", - "@typescript-eslint/utils": "8.44.0" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "vite": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", - "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", - "dev": true, - "requires": { - "esbuild": "^0.25.0", - "fdir": "^6.5.0", - "fsevents": "~2.3.3", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "dependencies": { - "fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "requires": {} - }, - "picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true - } - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true - }, - "wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", - "dev": true, - "requires": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - } - }, - "yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } } } diff --git a/frontend/webEditor/package.json b/frontend/webEditor/package.json index 8f3beb39..5f09ab6c 100644 --- a/frontend/webEditor/package.json +++ b/frontend/webEditor/package.json @@ -1,10 +1,10 @@ { "name": "data-flow-analysis-web-editor", - "version": "0.1.0", + "version": "0.0.0", "private": true, "repository": { "type": "git", - "url": "https://github.com/DataFlowAnalysis/WebEditor.git" + "url": "https://github.com/DataFlowAnalysis/OnlineEditor.git" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", diff --git a/frontend/webEditor/src/common/commandPalette.ts b/frontend/webEditor/src/common/commandPalette.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/webEditor/src/common/commonStyling.css b/frontend/webEditor/src/common/commonStyling.css deleted file mode 100644 index b121db8b..00000000 --- a/frontend/webEditor/src/common/commonStyling.css +++ /dev/null @@ -1,136 +0,0 @@ -.ui-float { - position: absolute; - border-radius: 10px; - background-color: var(--color-primary); -} - -/* Styling for keyboard symbols. - Copied from the example at https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd - with adapted colors */ -kbd { - background-color: var(--color-primary); - color: var(--color-foreground); - - border-radius: 3px; - border: 1px solid var(--color-foreground); - box-shadow: - 0 1px 1px var(--color-foreground), - 0 2px 0 0 var(--color-background) inset; - display: inline-block; - font-size: 0.85em; - font-weight: 700; - line-height: 1; - padding: 2px 4px; - white-space: nowrap; -} - -/* accordion */ -.accordion-content { - display: grid; - /* This transition is used when closing the accordion. Here the x direction should start slow and then end fast, thus ease-out */ - transition: - grid-template-rows 300ms ease, - /* ease-in animation: https://cubic-bezier.com/#.7,0,1,.6 */ grid-template-columns 300ms - cubic-bezier(0.7, 0, 1, 0.6), - padding-top 300ms ease; - - grid-template-rows: 0fr; - grid-template-columns: 0fr; - padding-top: 0; -} - -.accordion-state:checked ~ .accordion-content { - grid-template-rows: 1fr; - grid-template-columns: 1fr; - - /* This transition is used when opening the accordion. Here the x direction should start fast and then end slow, thus ease-in */ - transition: - grid-template-rows 300ms ease, - /* ease-out animation: https://cubic-bezier.com/#0,.7,.4,1 */ /* mirrored version of the curve above */ - grid-template-columns 300ms cubic-bezier(0, 0.7, 0.4, 1), - padding-top 300ms ease; - - /* space between accordion button and the content, otherwise they would be directly next to each other without any spacing */ - padding-top: 8px; -} - -/* needed to hide the content when the accordion is closed */ -.accordion-content * { - overflow: hidden; - white-space: nowrap; - text-overflow: clip; -} - -/* drop-down icon */ -.accordion-button { - /* Make the text unselectable. When rapidly clicking the accordion button, - the text would be selected otherwise due to a double click. */ - -webkit-user-select: none; - user-select: none; - - /* Default orientation of the arrow: pointing down */ - --arrow-scale: 1; -} - -.accordion-button.flip-arrow { - /* Default orientation of the arrow: pointing up */ - --arrow-scale: -1; -} - -.accordion-button.cevron-right { - /* space for the icon */ - padding-right: 2em; -} - -.accordion-button.cevron-left { - /* space for the icon */ - padding-left: 2em; -} - -.accordion-button.cevron-right::after { - content: ""; - background-image: url("@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg"); - right: 1em; - position: absolute; - display: inline-block; - - /* only filter=invert(1) if dark mode is enabled aka --dark-mode is set to 1 */ - filter: invert(var(--dark-mode)); - - width: 16px; - height: 16px; - background-size: 16px 16px; - - vertical-align: text-top; - transition: transform 500ms ease; - transform: scaleY(var(--arrow-scale)); -} - -.accordion-button.cevron-left::before { - content: ""; - background-image: url("@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg"); - left: 1em; - position: absolute; - display: inline-block; - - /* only filter=invert(1) if dark mode is enabled aka --dark-mode is set to 1 */ - filter: invert(var(--dark-mode)); - - width: 16px; - height: 16px; - background-size: 16px 16px; - - vertical-align: text-top; - transition: transform 500ms ease; - transform: scaleY(var(--arrow-scale)); -} - -.accordion-state:checked ~ label .accordion-button::after { - /* flip arrow in y direction */ - transform: scaleY(calc(var(--arrow-scale) * -1)); -} - -.accordion-state:checked ~ label .accordion-button::before { - /* flip arrow in y direction */ - transform: scaleY(calc(var(--arrow-scale) * -1)); -} diff --git a/frontend/webEditor/src/common/customCommandStack.ts b/frontend/webEditor/src/common/customCommandStack.ts deleted file mode 100644 index fd5de380..00000000 --- a/frontend/webEditor/src/common/customCommandStack.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - BringToFrontCommand, - CenterCommand, - CommandStack, - FitToScreenCommand, - HiddenCommand, - ICommand, - SelectCommand, - SetViewportCommand, -} from "sprotty"; - -/** - * Custom command stack implementations that only pushes - * commands that modify the diagram to the undo stack. - * Commands like selections, viewport moves etc. are filtered out - * and not pushed to the undo stack. Because of this they will not - * be undone when the user presses Ctrl+Z. - * - * This is done because the commands like selections clutter up - * the stack and the user has to undo many commands without - * really knowing what they are undoing when the selections/viewport moves - * are small. - */ -export class DiagramModificationCommandStack extends CommandStack { - protected override isPushToUndoStack(command: ICommand): boolean { - return !( - command instanceof HiddenCommand || - command instanceof SelectCommand || - command instanceof SetViewportCommand || - command instanceof BringToFrontCommand || - command instanceof FitToScreenCommand || - command instanceof CenterCommand - ); - } -} diff --git a/frontend/webEditor/src/common/deleteKeyListener.ts b/frontend/webEditor/src/common/deleteKeyListener.ts deleted file mode 100644 index 015fe387..00000000 --- a/frontend/webEditor/src/common/deleteKeyListener.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - CommitModelAction, - KeyListener, - SModelElementImpl, - isDeletable, - isSelectable, - SConnectableElementImpl, - SChildElementImpl, -} from "sprotty"; -import { Action, DeleteElementAction } from "sprotty-protocol"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; - -/** - * Custom sprotty key listener that deletes all selected elements when the user presses the delete key. - */ -export class DeleteKeyListener extends KeyListener { - override keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] { - if (matchesKeystroke(event, "Delete")) { - return this.deleteSelectedElements(element); - } - return []; - } - - private deleteSelectedElements(element: SModelElementImpl): Action[] { - const index = element.root.index; - const selectedElements = Array.from( - index - .all() - .filter((e) => isDeletable(e) && isSelectable(e) && e.selected) - .filter((e) => e.id !== e.root.id), // Deleting the model root would be a bad idea - ); - - const deleteElementIds = selectedElements.flatMap((e) => { - const ids = [e.id]; - - if (e instanceof SConnectableElementImpl) { - // This element can be connected to other elements, so we need to delete all edges connected to it as well. - // Otherwise the edges would be left dangling in the graph. - ids.push(...this.getEdgeIdsOfElement(e)); - } - if (e instanceof SChildElementImpl) { - // Add all children and their edges to the list of elements to delete - // This is needed when the edges are not connected to the element itself but to a port of the element. - e.children.forEach((child) => { - ids.push(child.id); - if (child instanceof SConnectableElementImpl) { - ids.push(...this.getEdgeIdsOfElement(child)); - } - }); - } - - return ids; - }); - - if (deleteElementIds.length > 0) { - const uniqueIds = [...new Set(deleteElementIds)]; - - return [DeleteElementAction.create(uniqueIds), CommitModelAction.create()]; - } else { - return []; - } - } - - private getEdgeIdsOfElement(element: SConnectableElementImpl): string[] { - return [...element.incomingEdges.map((e) => e.id), ...element.outgoingEdges.map((e) => e.id)]; - } -} diff --git a/frontend/webEditor/src/common/di.config.ts b/frontend/webEditor/src/common/di.config.ts deleted file mode 100644 index 58706e39..00000000 --- a/frontend/webEditor/src/common/di.config.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ContainerModule } from "inversify"; -import { - CenterGridSnapper, - CenterKeyboardListener, - ConsoleLogger, - CreateElementCommand, - LocalModelSource, - LogLevel, - TYPES, - configureCommand, - configureViewerOptions, -} from "sprotty"; -import { HelpUI } from "./helpUi"; -import { DeleteKeyListener } from "./deleteKeyListener"; -import { EDITOR_TYPES } from "../utils"; -import { DynamicChildrenProcessor } from "../features/dfdElements/dynamicChildren"; -import { FitToScreenKeyListener as CenterDiagramKeyListener } from "./fitToScreenKeyListener"; -import { DiagramModificationCommandStack } from "./customCommandStack"; - -import "./commonStyling.css"; -import { LoadingIndicator } from "./loadingIndicator"; - -export const commonModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(DeleteKeyListener).toSelf().inSingletonScope(); - bind(TYPES.KeyListener).toService(DeleteKeyListener); - bind(CenterDiagramKeyListener).toSelf().inSingletonScope(); - rebind(CenterKeyboardListener).toService(CenterDiagramKeyListener); - - bind(HelpUI).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(HelpUI); - bind(EDITOR_TYPES.DefaultUIElement).toService(HelpUI); - - bind(LoadingIndicator).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(LoadingIndicator); - bind(EDITOR_TYPES.DefaultUIElement).toService(LoadingIndicator); - - bind(DynamicChildrenProcessor).toSelf().inSingletonScope(); - - unbind(TYPES.ICommandStack); - bind(TYPES.ICommandStack).to(DiagramModificationCommandStack).inSingletonScope(); - - // Sprotty configuration - bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope(); - rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); - rebind(TYPES.LogLevel).toConstantValue(LogLevel.log); - bind(TYPES.ISnapper).to(CenterGridSnapper); - - // For some reason the CreateElementAction and Command exist but in no sprotty module is the command registered, so we need to do this here. - const context = { bind, unbind, isBound, rebind }; - configureCommand(context, CreateElementCommand); - - // Configure zoom limits - // Without these you could zoom in/out to infinity by accident resulting in your diagram being "gone". - // You can still get back to the diagram using the fit to screen action but these zoom limits prevents this from happening in the most cases. - configureViewerOptions(context, { - zoomLimits: { min: 0.05, max: 20 }, - }); -}); diff --git a/frontend/webEditor/src/common/fitToScreenKeyListener.ts b/frontend/webEditor/src/common/fitToScreenKeyListener.ts deleted file mode 100644 index ce46cae7..00000000 --- a/frontend/webEditor/src/common/fitToScreenKeyListener.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { KeyListener, SModelElementImpl } from "sprotty"; -import { Action, CenterAction } from "sprotty-protocol"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import { createDefaultFitToScreenAction } from "../utils"; - -/** - * Key listener that fits the diagram to the screen when pressing Ctrl+Shift+F - * and centers the diagram when pressing Ctrl+Shift+C. - * - * Custom version of the CenterKeyboardListener from sprotty because that one - * does not allow setting a padding. - */ -export class FitToScreenKeyListener extends KeyListener { - override keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] { - if (matchesKeystroke(event, "KeyC", "ctrlCmd", "shift")) { - return [CenterAction.create([])]; - } - - if (matchesKeystroke(event, "KeyF", "ctrlCmd", "shift")) { - return [createDefaultFitToScreenAction(element.root)]; - } - - return []; - } -} diff --git a/frontend/webEditor/src/common/helpUi.css b/frontend/webEditor/src/common/helpUi.css deleted file mode 100644 index 65782c88..00000000 --- a/frontend/webEditor/src/common/helpUi.css +++ /dev/null @@ -1,16 +0,0 @@ -div.help-ui { - left: 20px; - bottom: 20px; - padding: 10px 10px; -} - -#help-ui-accordion-label .accordion-button::before { - content: ""; - background-image: url("@fortawesome/fontawesome-free/svgs/regular/circle-question.svg"); - display: inline-block; - filter: invert(var(--dark-mode)); - height: 16px; - width: 16px; - background-size: 16px 16px; - vertical-align: text-top; -} diff --git a/frontend/webEditor/src/common/helpUi.ts b/frontend/webEditor/src/common/helpUi.ts deleted file mode 100644 index 839b0125..00000000 --- a/frontend/webEditor/src/common/helpUi.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { AbstractUIExtension } from "sprotty"; -import { injectable } from "inversify"; - -import "./helpUi.css"; - -@injectable() -export class HelpUI extends AbstractUIExtension { - static readonly ID = "help-ui"; - - id(): string { - return HelpUI.ID; - } - - containerClass(): string { - return HelpUI.ID; - } - - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.innerHTML = ` - - -
-
-

CTRL+Space: Command Palette

-

CTRL+Z: Undo

-

CTRL+Shift+Z: Redo

-

Del: Delete selected elements

-

T: Toggle Label Type Edit UI

-

CTRL+O: Load diagram from json

-

CTRL+Shift+O: Open default diagram

-

CTRL+S: Save diagram to json

-

CTRL+L: Automatically layout diagram

-

CTRL+Shift+F: Fit diagram to screen

-

CTRL+C: Copy selected elements

-

CTRL+V: Paste previously copied elements

-

Esc: Disable current creation tool

-

Toggle Creation Tool: Refer to key in the tool palette

-
-
- `; - - // Set `help-enabled` class on body element when keyboard shortcut overview is open. - const checkbox = containerElement.querySelector("#accordion-state-help") as HTMLInputElement; - const bodyElement = document.querySelector("body") as HTMLBodyElement; - checkbox.addEventListener("change", () => { - if (checkbox.checked) { - bodyElement.classList.add("help-enabled"); - } else { - bodyElement.classList.remove("help-enabled"); - } - }); - } -} diff --git a/frontend/webEditor/src/common/labelEditNoScroll.ts b/frontend/webEditor/src/common/labelEditNoScroll.ts deleted file mode 100644 index 89ad85d1..00000000 --- a/frontend/webEditor/src/common/labelEditNoScroll.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ContainerModule } from "inversify"; -import { - EditLabelAction, - EditLabelActionHandler, - EditLabelUI, - SModelRootImpl, - TYPES, - configureActionHandler, -} from "sprotty"; - -// For our use-case the sprotty container is at (0, 0) and fills the whole screen. -// Scrolling is disabled using CSS which disallows scrolling from the user. -// However the page might still be scrolled due to focus events. -// This is the case for the default sprotty EditLabelUI. -// When editing a label at a position where the edit control -// of the UI would be outside the viewport (at the right or bottom) -// the page would scroll to the right/bottom due to the focus event. -// To circumvent this we inherit from the default EditLabelUI and change it to -// scroll the page back to the page origin at (0, 0) if it has been moved due to the -// focus event. - -class NoScrollEditLabelUI extends EditLabelUI { - protected override onBeforeShow( - containerElement: HTMLElement, - root: Readonly, - ...contextElementIds: string[] - ): void { - super.onBeforeShow(containerElement, root, ...contextElementIds); - - // Scroll page to 0,0 if not already there - if (window.scrollX !== 0 || window.scrollY !== 0) { - window.scrollTo(0, 0); - } - } -} - -export const noScrollLabelEditUiModule = new ContainerModule((bind, _unbind, isBound) => { - // Provide the same stuff as the labelEditUiModule from sprotty but use our own EditLabelUI - // instead of the default one. - // When using this module the original sprotty labelEditUiModule must not be loaded aswell. - const context = { bind, isBound }; - configureActionHandler(context, EditLabelAction.KIND, EditLabelActionHandler); - bind(NoScrollEditLabelUI).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(NoScrollEditLabelUI); -}); diff --git a/frontend/webEditor/src/common/loadingIndicator.css b/frontend/webEditor/src/common/loadingIndicator.css deleted file mode 100644 index 068ef24c..00000000 --- a/frontend/webEditor/src/common/loadingIndicator.css +++ /dev/null @@ -1,29 +0,0 @@ -#loading-indicator-wrapper { - position: fixed; - top: 0; - left: 0; - bottom: 0; - right: 0; - z-index: 9999; - width: 100vw; - height: 100vh; - - flex-direction: column; - justify-content: center; - align-items: center; - gap: 20px; - font-size: xx-large; - font-weight: bold; - color: white; - - background-color: rgba(0, 0, 0, 0.8); -} - -#turning-circle { - border: 20px solid white; - border-top: 20px solid #3498db; - border-radius: 9999px; - width: 100px; - height: 100px; - animation: spin 2s linear infinite; -} diff --git a/frontend/webEditor/src/common/loadingIndicator.ts b/frontend/webEditor/src/common/loadingIndicator.ts deleted file mode 100644 index 60d8be44..00000000 --- a/frontend/webEditor/src/common/loadingIndicator.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { AbstractUIExtension } from "sprotty"; -import "./loadingIndicator.css"; - -export class LoadingIndicator extends AbstractUIExtension { - static readonly ID = "loading-indicator"; - private loadingIndicatorWrapper: HTMLElement | undefined; - private loadingIndicatorText: HTMLElement | undefined; - - id(): string { - return LoadingIndicator.ID; - } - containerClass(): string { - return LoadingIndicator.ID; - } - protected initializeContents(containerElement: HTMLElement): void { - this.loadingIndicatorWrapper = document.createElement("div"); - this.loadingIndicatorWrapper.id = "loading-indicator-wrapper"; - this.loadingIndicatorWrapper.style.display = "none"; - - const loadingIndicator = document.createElement("div"); - loadingIndicator.id = "turning-circle"; - this.loadingIndicatorWrapper.appendChild(loadingIndicator); - - this.loadingIndicatorText = document.createElement("div"); - this.loadingIndicatorText.id = "loading-indicator-text"; - this.loadingIndicatorWrapper.appendChild(this.loadingIndicatorText); - - containerElement.appendChild(this.loadingIndicatorWrapper); - } - - public showIndicator(text?: string) { - if (this.loadingIndicatorWrapper) { - this.loadingIndicatorWrapper.style.display = "flex"; - if (this.loadingIndicatorText) { - this.loadingIndicatorText.innerText = text || "Loading..."; - } - this.loadingIndicatorWrapper.focus(); - } - } - - public hideIndicator() { - if (this.loadingIndicatorWrapper) { - this.loadingIndicatorWrapper.style.display = "none"; - } - } -} diff --git a/frontend/webEditor/src/favicon.ico b/frontend/webEditor/src/favicon.ico deleted file mode 100644 index 168d55edfffe68a150f70828021ad83be04ca940..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165662 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 { - this.loadingIndicator?.showIndicator("Layouting..."); - this.oldModelSchema = context.modelFactory.createSchema(context.root); - - if (!this.layoutEngine) throw new Error("Missing injects"); - - this.usedMethod = this.settingsManager?.layoutMethod ?? LayoutMethod.LINES; - if ( - this.settingsManager && - (this.usedMethod === LayoutMethod.WRAPPING || this.usedMethod === LayoutMethod.CIRCLES) - ) { - this.oldHideLabels = this.settingsManager.hideEdgeLabels; - this.settingsManager.hideEdgeLabels = true; - } - - // Layouting is normally done on the graph schema. - // This is not viable for us because the dfd nodes have a dynamically computed size. - // This is only available on loaded classes of the elements, not the json schema. - // Thankfully the node implementation classes have all needed properties as well. - // So we can just force cast the graph from the loaded version into the "json graph schema". - // Using of the "bounds" property that the implementation classes have is done using DfdElkLayoutEngine. - const newModel = await this.layoutEngine.layout(context.root as unknown as SGraph); - // Here we need to cast back. - this.newModel = newModel as unknown as SModelRootImpl; - this.loadingIndicator?.hideIndicator(); - return this.newModel; - } - - undo(context: CommandExecutionContext): SModelRootImpl { - if (!this.oldModelSchema) { - // No old schema saved because the layout was not executed due to read-only mode. - return context.root; - } - if (this.settingsManager && this.oldHideLabels) { - this.settingsManager.hideEdgeLabels = this.oldHideLabels; - } - this.loadingIndicator?.showIndicator("Undoing layouting..."); - - LoadDiagramCommand.preprocessModelSchema(this.oldModelSchema); - - this.loadingIndicator?.hideIndicator(); - - return context.modelFactory.createRoot(this.oldModelSchema); - } - - redo(context: CommandExecutionContext): SModelRootImpl { - if (!this.newModel) { - // No new model saved because the layout was not executed due to read-only mode. - return context.root; - } - if ( - this.settingsManager && - (this.usedMethod === LayoutMethod.WRAPPING || this.usedMethod === LayoutMethod.CIRCLES) - ) { - this.settingsManager.hideEdgeLabels = true; - } - - return this.newModel; - } -} diff --git a/frontend/webEditor/src/features/autoLayout/di.config.ts b/frontend/webEditor/src/features/autoLayout/di.config.ts deleted file mode 100644 index 23414a2d..00000000 --- a/frontend/webEditor/src/features/autoLayout/di.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ContainerModule } from "inversify"; -import { TYPES, configureCommand } from "sprotty"; -import { ElkFactory, ILayoutConfigurator, ILayoutPostprocessor } from "sprotty-elk"; -import { LayoutModelCommand } from "./command"; -import { CircleLayoutPostProcessor, DfdElkLayoutEngine, DfdLayoutConfigurator, elkFactory } from "./layouter"; -import { AutoLayoutKeyListener } from "./keyListener"; - -export const autoLayoutModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(DfdElkLayoutEngine).toSelf().inSingletonScope(); - bind(TYPES.IModelLayoutEngine).toService(DfdElkLayoutEngine); - rebind(ILayoutConfigurator).to(DfdLayoutConfigurator); - bind(ILayoutPostprocessor).to(CircleLayoutPostProcessor).inSingletonScope(); - bind(ElkFactory).toConstantValue(elkFactory); - bind(TYPES.KeyListener).to(AutoLayoutKeyListener).inSingletonScope(); - - const context = { bind, unbind, isBound, rebind }; - configureCommand(context, LayoutModelCommand); -}); diff --git a/frontend/webEditor/src/features/autoLayout/keyListener.ts b/frontend/webEditor/src/features/autoLayout/keyListener.ts deleted file mode 100644 index b6eee7c6..00000000 --- a/frontend/webEditor/src/features/autoLayout/keyListener.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CommitModelAction, KeyListener, SModelElementImpl } from "sprotty"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import { Action } from "sprotty-protocol"; -import { LayoutModelAction } from "./command"; -import { createDefaultFitToScreenAction } from "../../utils"; - -export class AutoLayoutKeyListener extends KeyListener { - keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] { - if (matchesKeystroke(event, "KeyL", "ctrlCmd")) { - event.preventDefault(); - - return [ - LayoutModelAction.create(), - CommitModelAction.create(), - createDefaultFitToScreenAction(element.root), - ]; - } - - return []; - } -} diff --git a/frontend/webEditor/src/features/autoLayout/layouter.ts b/frontend/webEditor/src/features/autoLayout/layouter.ts deleted file mode 100644 index 5c08b4bb..00000000 --- a/frontend/webEditor/src/features/autoLayout/layouter.ts +++ /dev/null @@ -1,420 +0,0 @@ -import ElkConstructor, { ElkExtendedEdge, ElkLabel, ElkNode } from "elkjs/lib/elk.bundled"; -import { injectable, inject } from "inversify"; -import { - DefaultLayoutConfigurator, - ElkFactory, - ElkLayoutEngine, - IElementFilter, - ILayoutConfigurator, - ILayoutPostprocessor, -} from "sprotty-elk"; -import { SChildElementImpl, SShapeElementImpl, isBoundsAware } from "sprotty"; -import { SShapeElement, SModelIndex, SEdge, SLabel } from "sprotty-protocol"; -import { ElkShape, LayoutOptions } from "elkjs"; -import { SettingsManager } from "../settingsMenu/SettingsManager"; -import { LayoutMethod } from "../settingsMenu/LayoutMethod"; -import { calculateTextSize } from "../../utils"; - -export class DfdLayoutConfigurator extends DefaultLayoutConfigurator { - constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) { - super(); - } - - protected override graphOptions(): LayoutOptions { - // Elk settings. See https://eclipse.dev/elk/reference.html for available options. - return { - [LayoutMethod.LINES]: { - "org.eclipse.elk.algorithm": "org.eclipse.elk.layered", - "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "30.0", - "org.eclipse.elk.layered.spacing.edgeNodeBetweenLayers": "20.0", - "org.eclipse.elk.port.borderOffset": "14.0", - // Do not do micro layout for nodes, which includes the node dimensions etc. - // These are all automatically determined by our dfd node views - "org.eclipse.elk.omitNodeMicroLayout": "true", - // Balanced graph > straight edges - "org.eclipse.elk.layered.nodePlacement.favorStraightEdges": "false", - }, - [LayoutMethod.WRAPPING]: { - "org.eclipse.elk.algorithm": "org.eclipse.elk.layered", - "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "10.0", //Save more space between layers (long names might break this!) - "org.eclipse.elk.layered.spacing.edgeNodeBetweenLayers": "5.0", //Save more space between layers (long names might break this!) - "org.eclipse.elk.edgeRouting": "ORTHOGONAL", //Edges should be routed orthogonal to each another - "org.eclipse.elk.layered.layering.strategy": "COFFMAN_GRAHAM", - "org.eclipse.elk.layered.compaction.postCompaction.strategy": "LEFT_RIGHT_CONSTRAINT_LOCKING", //Compact the resulting graph horizontally - "org.eclipse.elk.layered.wrapping.strategy": "MULTI_EDGE", //Allow wrapping of multiple edges - "org.eclipse.elk.layered.wrapping.correctionFactor": "2.0", //Allow the wrapping to occur earlier - // Do not do micro layout for nodes, which includes the node dimensions etc. - // These are all automatically determined by our dfd node views - "org.eclipse.elk.omitNodeMicroLayout": "true", - "org.eclipse.elk.port.borderOffset": "14.0", - }, - [LayoutMethod.CIRCLES]: { - "org.eclipse.elk.algorithm": "org.eclipse.elk.stress", - "org.eclipse.elk.force.repulsion": "5.0", - "org.eclipse.elk.force.iterations": "100", //Reduce iterations for faster formatting, did not notice differences with more iterations - "org.eclipse.elk.force.repulsivePower": "1", //Edges should repel vertices as well - // Do not do micro layout for nodes, which includes the node dimensions etc. - // These are all automatically determined by our dfd node views - "org.eclipse.elk.omitNodeMicroLayout": "true", - }, - }[this.settings.layoutMethod]; - } -} - -export const elkFactory = () => - new ElkConstructor({ - algorithms: ["layered", "stress"], - }); - -/** - * Layout engine for the DFD editor. - * This class inherits the default ElkLayoutEngine but overrides the transformShape method. - * This is necessary because the default ElkLayoutEngine uses the size property of the shapes to determine their sizes. - * However with dynamically sized shapes, the size property is set to -1, which is undesired. - * Instead in this case the size should be determined by the bounds property which is dynamically computed. - * - * Additionally it centers ports on the node edge instead of putting them right next to the node at the edge. - */ -@injectable() -export class DfdElkLayoutEngine extends ElkLayoutEngine { - constructor( - @inject(ElkFactory) elkFactory: ElkFactory, - @inject(IElementFilter) elementFilter: IElementFilter, - @inject(ILayoutConfigurator) configurator: ILayoutConfigurator, - @inject(SettingsManager) protected readonly settings: SettingsManager, - @inject(ILayoutPostprocessor) protected readonly postprocessor: ILayoutPostprocessor, - ) { - super(elkFactory, elementFilter, configurator, undefined, postprocessor); - } - - protected override transformShape(elkShape: ElkShape, sshape: SShapeElementImpl | SShapeElement): void { - if (sshape.position) { - elkShape.x = sshape.position.x; - elkShape.y = sshape.position.y; - } - if ("bounds" in sshape) { - elkShape.width = sshape.bounds.width ?? sshape.size.width; - elkShape.height = sshape.bounds.height ?? sshape.size.height; - } - } - - protected override transformEdge(sedge: SEdge, index: SModelIndex): ElkExtendedEdge { - // remove all middle points of edge and only keep source and target - const elkEdge = super.transformEdge(sedge, index); - elkEdge.sections = []; - return elkEdge; - } - - protected override transformLabel(slabel: SLabel, index: SModelIndex): ElkLabel { - const e = super.transformLabel(slabel, index); - if (this.settings.layoutMethod === LayoutMethod.WRAPPING) { - return e; - } - const size = calculateTextSize(slabel.text ?? ""); - e.height = size.height; - e.width = size.width; - return e; - } - - protected override applyShape(sshape: SShapeElement, elkShape: ElkShape, index: SModelIndex): void { - // Check if this is a port, if yes we want to center it on the node edge instead of putting it right next to the node at the edge - if (this.getBasicType(sshape) === "port") { - // Because we use actually pass SShapeElementImpl instead of SShapeElement to this method - // we can access the parent property and the bounds of the parent which is the node of this port. - if (sshape instanceof SChildElementImpl && isBoundsAware(sshape.parent)) { - const parent = sshape.parent; - if ( - elkShape.x !== undefined && - elkShape.width !== undefined && - elkShape.y !== undefined && - elkShape.height !== undefined - ) { - // Note that the port x and y coordinates are relative to the parent node. - - // Move inwards from being adjacent to the node edge by half of the port width/height - // depending on which edge the port is on. - - // depending on the mode the ports may be placed differently - if (this.settings.layoutMethod === LayoutMethod.CIRCLES) { - if (elkShape.x <= 0) - // Left edge - elkShape.x -= elkShape.width / 2; - if (elkShape.y <= 0) - // Top edge - elkShape.y -= elkShape.height / 2; - if (elkShape.x >= parent.bounds.width) - // Right edge - elkShape.x -= elkShape.width / 2; - if (elkShape.y >= parent.bounds.height) - // Bottom edge - elkShape.y -= elkShape.height / 2; - } else { - if (elkShape.x <= 0) - // Left edge - elkShape.x += elkShape.width / 2; - if (elkShape.y <= 0) - // Top edge - elkShape.y += elkShape.height / 2; - if (elkShape.x >= parent.bounds.width) - // Right edge - elkShape.x -= elkShape.width / 2; - if (elkShape.y >= parent.bounds.height) - // Bottom edge - elkShape.y -= elkShape.height / 2; - } - } - } - } - - super.applyShape(sshape, elkShape, index); - - const parent = index.getParent(sshape.id); - const parentType = parent ? this.getBasicType(parent) : "unknown"; - if (this.getBasicType(sshape) === "label" && parentType == "edge") { - sshape.size = { - width: -1, - height: -1, - }; - } - } - - protected applyEdge(sedge: SEdge, elkEdge: ElkExtendedEdge, index: SModelIndex): void { - if (this.settings.layoutMethod === LayoutMethod.CIRCLES) { - // In the circles layout method, we want to make sure that the edge is not straight - // This is because the circles layout method does not support straight edges - elkEdge.sections = []; - } - super.applyEdge(sedge, elkEdge, index); - } -} - -@injectable() -export class CircleLayoutPostProcessor implements ILayoutPostprocessor { - private portToNodes: Map = new Map(); - private connectedPorts: Map = new Map(); - private nodeSquares: Map = new Map(); - - constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) {} - - postprocess(elkGraph: ElkNode): void { - if (this.settings.layoutMethod !== LayoutMethod.CIRCLES) { - return; - } - this.connectedPorts = new Map(); - if (!elkGraph.edges || !elkGraph.children) { - return; - } - for (const edge of elkGraph.edges) { - for (const source of edge.sources) { - if (!this.connectedPorts.has(source)) { - this.connectedPorts.set(source, []); - } - for (const target of edge.targets) { - if (!this.connectedPorts.has(target)) { - this.connectedPorts.set(target, []); - } - this.connectedPorts.get(source)?.push(target); - this.connectedPorts.get(target)?.push(source); - } - } - } - - this.portToNodes = new Map(); - this.nodeSquares = new Map(); - for (const node of elkGraph.children) { - if (node.ports) { - for (const port of node.ports) { - this.portToNodes.set(port.id, node.id); - } - this.nodeSquares.set(node.id, this.getNodeSquare(node)); - } - } - - for (const [port, connected] of this.connectedPorts) { - if (connected.length === 0) { - continue; - } - const intersections = connected.map((connection) => { - const line = this.getLine(port, connection); - const node = this.portToNodes.get(port); - if (!node) { - return { x: 0, y: 0 }; - } - const square = this.nodeSquares.get(node); - if (!square) { - return { x: 0, y: 0 }; - } - const intersection = this.getIntersection(square, line); - return intersection; - }); - const average = { - x: intersections.reduce((sum, intersection) => sum + intersection.x, 0) / intersections.length, - y: intersections.reduce((sum, intersection) => sum + intersection.y, 0) / intersections.length, - }; - - const node = this.portToNodes.get(port); - if (!node) { - continue; - } - const square = this.nodeSquares.get(node); - if (!square) { - continue; - } - const closestPointOnEdge = { - x: average.x, - y: average.y, - }; - - const topEdge = { x1: square.x, y1: square.y, x2: square.x + square.width, y2: square.y }; - const bottomEdge = { - x1: square.x, - y1: square.y + square.height, - x2: square.x + square.width, - y2: square.y + square.height, - }; - const leftEdge = { x1: square.x, y1: square.y, x2: square.x, y2: square.y + square.height }; - const rightEdge = { - x1: square.x + square.width, - y1: square.y, - x2: square.x + square.width, - y2: square.y + square.height, - }; - const distances = [ - { distance: Math.abs(average.y - square.y), dimension: "y", edge: topEdge }, - { distance: Math.abs(average.y - (square.y + square.height)), dimension: "y", edge: bottomEdge }, - { distance: Math.abs(average.x - square.x), dimension: "x", edge: leftEdge }, - { distance: Math.abs(average.x - (square.x + square.width)), dimension: "x", edge: rightEdge }, - ]; - distances.sort((a, b) => a.distance - b.distance); - const closestEdge = distances[0].edge; - if (distances[0].dimension === "y") { - closestPointOnEdge.x = clamp(average.x, closestEdge.x1, closestEdge.x2); - closestPointOnEdge.y = closestEdge.y1; - } else { - closestPointOnEdge.x = closestEdge.x1; - closestPointOnEdge.y = clamp(average.y, closestEdge.y1, closestEdge.y2); - } - - const nodeElk = elkGraph.children.find((child) => child.id === node); - if (!nodeElk) { - continue; - } - const portElk = nodeElk.ports?.find((p) => p.id === port); - if (!portElk) { - continue; - } - portElk.x = closestPointOnEdge.x - (nodeElk.x ?? 0); - portElk.y = closestPointOnEdge.y - (nodeElk.y ?? 0); - } - } - - getNodeSquare(node: ElkNode): Square { - return { - x: node.x ?? 0, - y: node.y ?? 0, - width: node.width ?? 0, - height: node.height ?? 0, - }; - } - - getCenter(square: Square): { x: number; y: number } { - return { - x: square.x + square.width / 2, - y: square.y + square.height / 2, - }; - } - - getLine(port1: string, port2: string): Line { - const node1 = this.portToNodes.get(port1); - const node2 = this.portToNodes.get(port2); - if (!node1 || !node2) { - return { - x1: 0, - y1: 0, - x2: 0, - y2: 0, - }; - } - const square1 = this.nodeSquares.get(node1)!; - const square2 = this.nodeSquares.get(node2)!; - const center1 = this.getCenter(square1); - const center2 = this.getCenter(square2); - - return { - x1: center1.x, - y1: center1.y, - x2: center2.x, - y2: center2.y, - }; - } - - getIntersection(square: Square, line: Line): { x: number; y: number } { - const topLeft = { x: square.x, y: square.y }; - const topRight = { x: square.x + square.width, y: square.y }; - const bottomLeft = { x: square.x, y: square.y + square.height }; - const bottomRight = { x: square.x + square.width, y: square.y + square.height }; - - const intersections = [ - this.getLineIntersection(line, { x1: topLeft.x, y1: topLeft.y, x2: topRight.x, y2: topRight.y }), - this.getLineIntersection(line, { x1: topRight.x, y1: topRight.y, x2: bottomRight.x, y2: bottomRight.y }), - this.getLineIntersection(line, { - x1: bottomRight.x, - y1: bottomRight.y, - x2: bottomLeft.x, - y2: bottomLeft.y, - }), - this.getLineIntersection(line, { x1: bottomLeft.x, y1: bottomLeft.y, x2: topLeft.x, y2: topLeft.y }), - ]; - - const inLineBounds = intersections.filter((intersection) => { - return ( - intersection.x >= Math.min(line.x1, line.x2) && - intersection.x <= Math.max(line.x1, line.x2) && - intersection.y >= Math.min(line.y1, line.y2) && - intersection.y <= Math.max(line.y1, line.y2) - ); - }); - return inLineBounds[0] ?? { x: 0, y: 0 }; - } - - private getLineIntersection(line1: Line, line2: Line): { x: number; y: number } { - const x1 = line1.x1; - const y1 = line1.y1; - const x2 = line1.x2; - const y2 = line1.y2; - const x3 = line2.x1; - const y3 = line2.y1; - const x4 = line2.x2; - const y4 = line2.y2; - - const denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); - if (denominator === 0) { - return { x: 0, y: 0 }; - } - - const x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denominator; - const y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denominator; - - return { x, y }; - } -} - -interface Square { - x: number; - y: number; - width: number; - height: number; -} - -interface Line { - x1: number; - y1: number; - x2: number; - y2: number; -} - -function clamp(value: number, l1: number, l2: number): number { - const min = Math.min(l1, l2); - const max = Math.max(l1, l2); - return Math.max(min, Math.min(max, value)); -} diff --git a/frontend/webEditor/src/features/commandPalette/commandPalette.css b/frontend/webEditor/src/features/commandPalette/commandPalette.css deleted file mode 100644 index 6d77c390..00000000 --- a/frontend/webEditor/src/features/commandPalette/commandPalette.css +++ /dev/null @@ -1,58 +0,0 @@ -/* Overrides for sprotty command palette css (should be imported in commandPalette.ts before this .css file */ - -.command-palette { - transition: opacity 0.2s ease-in-out; - display: flex; - flex-direction: column; - row-gap: 4px; - width: 350px; -} - -.command-palette input { - color: var(--color-foreground); - background: var(--color-primary); -} - -.command-palette-suggestions-holder { - width: 100%; -} - -.command-palette-suggestion { - display: grid; - grid-template-columns: 24px 1fr 24px 0px; - background: var(--color-primary); - overflow: visible; - height: 20px; - min-width: 100%; - white-space: nowrap; - width: 100%; - cursor: pointer; -} - -.command-palette-suggestion:hover, -.command-palette-suggestion.selected { - background: var(--color-background); -} - -.command-palette-suggestion-children { - position: relative; - top: 0px; - right: 0px; - display: none; - background: var(--color-primary); - width: fit-content; - height: fit-content; - border-left: 4px solid var(--color-spacer); - box-shadow: - 0 4px 8px 0 rgba(0, 0, 0, 0.2), - 0 6px 20px 0 rgba(0, 0, 0, 0.19); -} - -.command-palette-suggestion:hover > .command-palette-suggestion-children, -.command-palette-suggestion.expanded > .command-palette-suggestion-children { - display: block; -} - -.command-palette .fa-solid { - text-align: center; -} diff --git a/frontend/webEditor/src/features/commandPalette/commandPalette.ts b/frontend/webEditor/src/features/commandPalette/commandPalette.ts deleted file mode 100644 index cee2490a..00000000 --- a/frontend/webEditor/src/features/commandPalette/commandPalette.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { injectable } from "inversify"; -import { CommandPalette, LabeledAction, SModelRootImpl } from "sprotty"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import { FolderAction } from "./commandPaletteProvider"; -import "./commandPalette.css"; - -@injectable() -export class CustomCommandPalette extends CommandPalette { - static readonly ID = "command-palette"; - - protected suggestionElement?: HTMLElement; - protected index = -1; - protected childIndex = -1; - protected insideChild = false; - protected actions: (LabeledAction | FolderAction)[] = []; - protected filteredActions: (LabeledAction | FolderAction)[] = []; - - protected initializeContents(containerElement: HTMLElement) { - containerElement.style.position = "absolute"; - containerElement.style.top = "100px"; - containerElement.style.left = "100px"; - this.inputElement = document.createElement("input"); - this.inputElement.style.width = "100%"; - this.inputElement.addEventListener("keydown", (event) => this.processKeyStrokeInInput(event)); - this.inputElement.addEventListener("input", () => this.updateSuggestions()); - this.inputElement.onblur = () => window.setTimeout(() => this.hide(), 200); - this.suggestionElement = document.createElement("div"); - this.suggestionElement.className = "command-palette-suggestions-holder"; - containerElement.appendChild(this.inputElement); - containerElement.appendChild(this.suggestionElement); - } - - override show(root: Readonly, ...contextElementIds: string[]) { - super.show(root, ...contextElementIds); - this.autoCompleteResult.destroy(); - this.index = -1; - this.childIndex = -1; - this.insideChild = false; - this.filteredActions = []; - this.actions = []; - this.suggestionElement!.innerHTML = ""; - this.inputElement!.value = ""; - this.inputElement!.focus(); - this.actionProviderRegistry - .getActions(root, "", this.mousePositionTracker.lastPositionOnDiagram) - .then((actions) => (this.actions = actions)) - .then(() => this.updateSuggestions()); - } - - protected updateSuggestions() { - if (!this.suggestionElement) { - return; - } - this.suggestionElement!.innerHTML = ""; - const searchText = this.inputElement!.value.toLowerCase(); - this.filteredActions = []; - for (const action of this.actions) { - if (this.matchFilter(action, searchText)) { - this.filteredActions.push(action); - continue; - } - if (action instanceof FolderAction) { - const filteredChildren = action.children.filter((child) => this.matchFilter(child, searchText)); - if (filteredChildren.length > 0) { - this.filteredActions.push(new FolderAction(action.label, filteredChildren, action.icon)); - continue; - } - } - } - if (this.index >= this.filteredActions.length) { - this.index = -1; - } - for (const [idx, action] of this.filteredActions.entries()) { - const suggestion = this.renderSuggestion(action); - if (idx === this.index) { - suggestion.classList.add("expanded"); - if (!this.insideChild) { - suggestion.classList.add("selected"); - } - } - this.suggestionElement!.appendChild(suggestion); - } - } - - private renderSuggestion(action: LabeledAction | FolderAction) { - const suggestion = document.createElement("div"); - suggestion.className = "command-palette-suggestion"; - const icon = document.createElement("span"); - icon.className = this.getIconClasses(action.icon); - suggestion.appendChild(icon); - const label = document.createElement("span"); - label.className = "command-palette-suggestion-label"; - label.innerText = action.label; - suggestion.appendChild(label); - const arrow = document.createElement("span"); - suggestion.appendChild(arrow); - if (action instanceof FolderAction) { - arrow.className = "codicon codicon-chevron-right"; - suggestion.appendChild(arrow); - const childHolder = document.createElement("div"); - childHolder.className = "command-palette-suggestion-children"; - for (const [idx, childAction] of action.children.entries()) { - const childSuggestion = this.renderSuggestion(childAction); - if (this.insideChild && this.childIndex === idx) { - childSuggestion.classList.add("selected"); - } - childHolder.appendChild(childSuggestion); - } - suggestion.appendChild(childHolder); - } - suggestion.addEventListener("click", () => { - if (!(action instanceof FolderAction)) { - this.executeAction(action); - } - }); - return suggestion; - } - - private getIconClasses(icon?: string) { - if (!icon) { - return "codicon codicon-gear"; - } - if (icon.startsWith("fa-")) { - return "fa-solid " + icon; - } - if (icon.startsWith("codicon-")) { - return "codicon " + icon; - } - return "codicon codicon-" + icon; - } - - private matchFilter(action: LabeledAction, searchText: string): boolean { - return action.label.toLowerCase().includes(searchText); - } - - id(): string { - return CustomCommandPalette.ID; - } - containerClass(): string { - return CustomCommandPalette.ID; - } - - protected processKeyStrokeInInput(event: KeyboardEvent) { - if (matchesKeystroke(event, "Escape")) { - this.hide(); - } - - if (matchesKeystroke(event, "ArrowDown")) { - if (this.insideChild) { - this.childIndex = - (this.childIndex + 1) % (this.filteredActions[this.index] as FolderAction).children.length; - } else { - if (this.index === -1) { - this.index = 0; - } else { - this.index = (this.index + 1) % this.suggestionElement!.children.length; - } - } - } - if (matchesKeystroke(event, "ArrowUp")) { - if (this.insideChild) { - this.childIndex = - (this.childIndex - 1 + (this.filteredActions[this.index] as FolderAction).children.length) % - (this.filteredActions[this.index] as FolderAction).children.length; - } else { - if (this.index === -1) { - this.index = this.suggestionElement!.children.length - 1; - } else { - this.index = - (this.index - 1 + this.suggestionElement!.children.length) % - this.suggestionElement!.children.length; - } - } - } - if (matchesKeystroke(event, "ArrowRight")) { - if (!this.insideChild && this.filteredActions[this.index] instanceof FolderAction) { - event.preventDefault(); - this.insideChild = true; - this.childIndex = 0; - } - } - if (matchesKeystroke(event, "ArrowLeft")) { - if (this.insideChild) { - event.preventDefault(); - this.insideChild = false; - this.childIndex = -1; - } - } - if (matchesKeystroke(event, "Enter")) { - if (this.insideChild) { - this.executeAction((this.filteredActions[this.index] as FolderAction).children[this.childIndex]); - } else { - if (this.index !== -1) { - this.executeAction(this.filteredActions[this.index]); - } - } - this.hide(); - } - this.updateSuggestions(); - } - - protected executeAction(input: LabeledAction) { - this.actionDispatcherProvider() - .then((actionDispatcher) => actionDispatcher.dispatchAll(input.actions)) - .catch((reason) => - this.logger.error(this, "No action dispatcher available to execute command palette action", reason), - ); - } -} diff --git a/frontend/webEditor/src/features/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/features/commandPalette/commandPaletteProvider.ts deleted file mode 100644 index b09c669d..00000000 --- a/frontend/webEditor/src/features/commandPalette/commandPaletteProvider.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { inject, injectable } from "inversify"; -import { ICommandPaletteActionProvider, LabeledAction, SModelRootImpl, CommitModelAction } from "sprotty"; -import { LoadDiagramAction } from "../serialize/load"; -import { createDefaultFitToScreenAction } from "../../utils"; -import { SaveDiagramAction } from "../serialize/save"; -import { LoadDefaultDiagramAction } from "../serialize/loadDefaultDiagram"; -import { LayoutModelAction } from "../autoLayout/command"; - -import "@vscode/codicons/dist/codicon.css"; -import "sprotty/css/command-palette.css"; -import { SaveDFDandDDAction } from "../serialize/saveDFDandDD"; -import { LoadDFDandDDAction } from "../serialize/loadDFDandDD"; -import { LoadPalladioAction } from "../serialize/loadPalladio"; -import { SaveImageAction } from "../serialize/image"; -import { SettingsManager } from "../settingsMenu/SettingsManager"; - -/** - * Provides possible actions for the command palette. - */ -@injectable() -export class ServerCommandPaletteActionProvider implements ICommandPaletteActionProvider { - constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) {} - - async getActions(root: Readonly): Promise<(LabeledAction | FolderAction)[]> { - const fitToScreenAction = createDefaultFitToScreenAction(root); - const commitAction = CommitModelAction.create(); - - return [ - new FolderAction( - "Load", - [ - new LabeledAction("Load diagram from JSON", [LoadDiagramAction.create(), commitAction], "json"), - new LabeledAction("Load DFD and DD", [LoadDFDandDDAction.create(), commitAction], "coffee"), - new LabeledAction("Load Palladio", [LoadPalladioAction.create(), commitAction], "fa-puzzle-piece"), - ], - "go-to-file", - ), - new FolderAction( - "Save", - [ - new LabeledAction("Save diagram as JSON", [SaveDiagramAction.create()], "json"), - new LabeledAction( - "Save diagram as DFD and DD", - [SaveDFDandDDAction.create(), commitAction], - "coffee", - ), - new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"), - ], - "save", - ), - - new LabeledAction("Load default diagram", [LoadDefaultDiagramAction.create(), commitAction], "clear-all"), - new LabeledAction("Fit to Screen", [fitToScreenAction], "screen-normal"), - new LabeledAction( - "Layout diagram (Method: " + this.settings.layoutMethod + ")", - [LayoutModelAction.create(), commitAction, fitToScreenAction], - "layout", - ), - ]; - } -} - -export class FolderAction extends LabeledAction { - constructor( - label: string, - readonly children: LabeledAction[], - icon?: string, - ) { - super(label, [], icon); - } -} diff --git a/frontend/webEditor/src/features/commandPalette/di.config.ts b/frontend/webEditor/src/features/commandPalette/di.config.ts deleted file mode 100644 index 93aaddce..00000000 --- a/frontend/webEditor/src/features/commandPalette/di.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ContainerModule } from "inversify"; -import { CommandPalette, TYPES } from "sprotty"; -import { ServerCommandPaletteActionProvider } from "./commandPaletteProvider"; -import { CustomCommandPalette } from "./commandPalette"; -import "./commandPalette.css"; - -export const commandPaletteModule = new ContainerModule((bind, _, __, rebind) => { - rebind(CommandPalette).to(CustomCommandPalette).inSingletonScope(); - - bind(ServerCommandPaletteActionProvider).toSelf().inSingletonScope(); - bind(TYPES.ICommandPaletteActionProvider).toService(ServerCommandPaletteActionProvider); -}); diff --git a/frontend/webEditor/src/features/constraintMenu/AutoCompletion.ts b/frontend/webEditor/src/features/constraintMenu/AutoCompletion.ts deleted file mode 100644 index 6877bac0..00000000 --- a/frontend/webEditor/src/features/constraintMenu/AutoCompletion.ts +++ /dev/null @@ -1,300 +0,0 @@ -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; - -export interface RequiredCompletionParts { - kind: monaco.languages.CompletionItemKind; - insertText: string; - startOffset?: number; -} - -export interface ValidationError { - message: string; - line: number; - startColumn: number; - endColumn: number; -} - -export interface Token { - text: string; - line: number; - column: number; - whiteSpaceAfter?: string; -} - -export type WordCompletion = RequiredCompletionParts & Partial; - -export interface AbstractWord { - /** - * Calculates the completion options for the given word - * @param word Can be taken into account for returning completion options - * @returns Array of completion options. Can contain all options from @link{monaco.languages.CompletionItem} - */ - completionOptions(word: string): WordCompletion[]; - - /** - * Verifies if the given word is valid - * An empty array means that the word is valid - * The strings in the array are error messages - * @param word The word to verify - * @returns Array of all error messages - */ - verifyWord(word: string): string[]; -} - -export class ConstantWord implements AbstractWord { - constructor(protected word: string) {} - - verifyWord(word: string): string[] { - if (word == this.word) { - return []; - } else { - return [`Expected keyword "${this.word}"`]; - } - } - - completionOptions(): WordCompletion[] { - return [ - { - insertText: this.word, - kind: monaco.languages.CompletionItemKind.Keyword, - }, - ]; - } -} - -export class AnyWord implements AbstractWord { - completionOptions(): WordCompletion[] { - return []; - } - verifyWord(word: string): string[] { - if (word.length > 0) { - return []; - } else { - return ["Expected a word"]; - } - } -} - -export class NegatableWord implements AbstractWord { - constructor(protected word: AbstractWord) {} - - verifyWord(word: string): string[] { - if (word.startsWith("!")) { - return this.word.verifyWord(word.substring(1)); - } - return this.word.verifyWord(word); - } - - completionOptions(part: string): WordCompletion[] { - if (part.startsWith("!")) { - const options = this.word.completionOptions(part.substring(1)); - return options.map((o) => ({ - ...o, - startOffset: (o.startOffset ?? 0) + 1, - })); - } - return this.word.completionOptions(part); - } -} - -export class AutoCompleteTree { - constructor(protected roots: AutoCompleteNode[]) {} - - protected tokenize(text: string[]): Token[] { - if (!text || text.length == 0) { - return []; - } - - const tokens: Token[] = []; - for (const [lineNumber, line] of text.entries()) { - const lineTokens = line.split(/(\s+)/); - let column = 0; - for (let i = 0; i < lineTokens.length; i += 2) { - const token = lineTokens[i]; - if (token.length > 0) { - tokens.push({ - text: token, - line: lineNumber + 1, - column: column + 1, - whiteSpaceAfter: lineTokens[i + 1], - }); - } - column += token.length; - column += lineTokens[i + 1] ? lineTokens[i + 1].length : 0; // Add whitespace length - } - } - - return tokens; - } - - /** - * Checks the set content for errors - * @returns An array of errors. An empty array means that the content is valid - */ - public verify(lines: string[]): ValidationError[] { - const tokens = this.tokenize(lines); - return this.verifyNode(this.roots, tokens, 0, false, true); - } - - private verifyNode( - nodes: AutoCompleteNode[], - tokens: Token[], - index: number, - comesFromFinal: boolean, - skipStartCheck = false, - ): ValidationError[] { - if (index >= tokens.length) { - if (nodes.length == 0 || comesFromFinal) { - return []; - } else { - return [ - { - message: "Unexpected end of line", - line: tokens[index - 1].line, - startColumn: tokens[index - 1].column + tokens[index - 1].text.length - 1, - endColumn: tokens[index - 1].column + tokens[index - 1].text.length, - }, - ]; - } - } - if (!skipStartCheck && tokens[index].column == 1) { - const matchesAnyRoot = this.roots.some((r) => r.word.verifyWord(tokens[index].text).length === 0); - if (matchesAnyRoot) { - return this.verifyNode(this.roots, tokens, index, false, true); - } - } - - const foundErrors: ValidationError[] = []; - let childErrors: ValidationError[] = []; - for (const n of nodes) { - const v = n.word.verifyWord(tokens[index].text); - if (v.length > 0) { - foundErrors.push({ - message: v[0], - startColumn: tokens[index].column, - endColumn: tokens[index].column + tokens[index].text.length, - line: tokens[index].line, - }); - continue; - } - - const childResult = this.verifyNode(n.children, tokens, index + 1, n.canBeFinal || false); - if (childResult.length == 0) { - return []; - } else { - childErrors = childErrors.concat(childResult); - } - } - if (childErrors.length > 0) { - return deduplicateErrors(childErrors); - } - return deduplicateErrors(foundErrors); - } - - /** - * Calculates the completion options for the current content - */ - public getCompletion(lines: string[]): monaco.languages.CompletionItem[] { - const tokens = this.tokenize(lines); - const endsWithWhitespace = - (lines.length > 0 && lines[lines.length - 1].charAt(lines[lines.length - 1].length - 1).match(/\s/)) || - lines[lines.length - 1].length == 0; - if (endsWithWhitespace) { - tokens.push({ - text: "", - line: lines.length, - column: lines[lines.length - 1].length + 1, - }); - } - - let result: WordCompletion[] = []; - if (tokens.length == 0) { - for (const r of this.roots) { - result = result.concat(r.word.completionOptions("")); - } - } else { - result = this.completeNode(this.roots, tokens, 0); - } - return this.transformResults(result, tokens); - } - - private completeNode( - nodes: AutoCompleteNode[], - tokens: Token[], - index: number, - cameFromFinal = false, - skipStartCheck = false, - ): WordCompletion[] { - // check for new start - if (!skipStartCheck && tokens[index].column == 1) { - const matchesAnyRoot = this.roots.some((n) => n.word.verifyWord(tokens[index].text).length === 0); - if (matchesAnyRoot) { - return this.completeNode(this.roots, tokens, index, cameFromFinal, true); - } else if (cameFromFinal || nodes.length == 0) { - return this.completeNode([...this.roots, ...nodes], tokens, index, cameFromFinal, true); - } - } - - let result: WordCompletion[] = []; - if (index == tokens.length - 1) { - for (const node of nodes) { - result = result.concat(node.word.completionOptions(tokens[index].text)); - } - return result; - } - for (const n of nodes) { - if (n.word.verifyWord(tokens[index].text).length > 0) { - continue; - } - result = result.concat(this.completeNode(n.children, tokens, index + 1, n.canBeFinal || false)); - } - return result; - } - - private transformResults(comp: WordCompletion[], tokens: Token[]): monaco.languages.CompletionItem[] { - const result: monaco.languages.CompletionItem[] = []; - const filtered = comp.filter( - (c, idx) => comp.findIndex((c2) => c2.insertText === c.insertText && c2.kind === c.kind) === idx, - ); - for (const c of filtered) { - const r = this.transformResult(c, tokens); - result.push(r); - } - return result; - } - - private transformResult(comp: WordCompletion, tokens: Token[]): monaco.languages.CompletionItem { - const wordStart = tokens.length == 0 ? 1 : tokens[tokens.length - 1].column; - const lineNumber = tokens.length == 0 ? 1 : tokens[tokens.length - 1].line; - return { - insertText: comp.insertText, - kind: comp.kind, - label: comp.label ?? comp.insertText, - insertTextRules: comp.insertTextRules, - range: new monaco.Range( - lineNumber, - wordStart + (comp.startOffset ?? 0), - lineNumber, - wordStart + (comp.startOffset ?? 0) + comp.insertText.length, - ), - }; - } -} - -function deduplicateErrors(errors: ValidationError[]): ValidationError[] { - const seen = new Set(); - return errors.filter((error) => { - const key = `${error.line}-${error.startColumn}-${error.endColumn}-${error.message}`; - if (seen.has(key)) { - return false; - } - seen.add(key); - return true; - }); -} - -export interface AutoCompleteNode { - word: W; - children: AutoCompleteNode[]; - canBeFinal?: boolean; - viewAsLeaf?: boolean; -} diff --git a/frontend/webEditor/src/features/constraintMenu/ConstraintMenu.ts b/frontend/webEditor/src/features/constraintMenu/ConstraintMenu.ts deleted file mode 100644 index 6e35e682..00000000 --- a/frontend/webEditor/src/features/constraintMenu/ConstraintMenu.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import "./constraintMenu.css"; -import { AbstractUIExtension, IActionDispatcher, LocalModelSource, TYPES } from "sprotty"; -import { ConstraintRegistry } from "./constraintRegistry"; - -// Enable hover feature that is used to show validation errors. -// Inline completions are enabled to allow autocompletion of keywords and inputs/label types/label values. -import "monaco-editor/esm/vs/editor/contrib/hover/browser/hoverContribution"; -import "monaco-editor/esm/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.js"; -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -import { - constraintDslLanguageMonarchDefinition, - DSL_LANGUAGE_ID, - MonacoEditorConstraintDslCompletionProvider, -} from "./DslLanguage"; -import { AutoCompleteTree } from "./AutoCompletion"; -import { TreeBuilder } from "./DslLanguage"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { EditorModeController } from "../editorMode/editorModeController"; -import { Switchable, ThemeManager } from "../settingsMenu/themeManager"; -import { AnalyzeDiagramAction } from "../serialize/analyze"; -import { ChooseConstraintAction } from "./actions"; - -@injectable() -export class ConstraintMenu extends AbstractUIExtension implements Switchable { - static readonly ID = "constraint-menu"; - private editorContainer: HTMLDivElement = document.createElement("div") as HTMLDivElement; - private validationLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; - private editor?: monaco.editor.IStandaloneCodeEditor; - private tree: AutoCompleteTree; - private forceReadOnly: boolean; - private optionsMenu?: HTMLDivElement; - private ignoreCheckboxChange = false; - - constructor( - @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, - @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, - @inject(TYPES.ModelSource) modelSource: LocalModelSource, - @inject(TYPES.IActionDispatcher) private readonly dispatcher: IActionDispatcher, - @inject(EditorModeController) - @optional() - editorModeController?: EditorModeController, - ) { - super(); - this.constraintRegistry = constraintRegistry; - this.tree = new AutoCompleteTree(TreeBuilder.buildTree(modelSource, labelTypeRegistry)); - this.forceReadOnly = editorModeController?.getCurrentMode() !== "edit"; - editorModeController?.onModeChange(() => { - this.forceReadOnly = editorModeController!.isReadOnly(); - }); - constraintRegistry.onUpdate(() => { - if (this.editor) { - const editorText = this.editor.getValue(); - // Only update the editor if the constraints have changed - if (editorText !== this.constraintRegistry.getConstraintsAsText()) { - this.editor.setValue(this.constraintRegistry.getConstraintsAsText() || ""); - } - } - }); - } - - id(): string { - return ConstraintMenu.ID; - } - containerClass(): string { - return ConstraintMenu.ID; - } - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.innerHTML = ` - - - `; - - const title = containerElement.querySelector("#constraint-menu-expand-title") as HTMLElement; - title.appendChild(this.buildOptionsButton()); - - const accordionContent = document.createElement("div"); - accordionContent.classList.add("accordion-content"); - const contentDiv = document.createElement("div"); - contentDiv.id = "constraint-menu-content"; - accordionContent.appendChild(contentDiv); - contentDiv.appendChild(this.buildConstraintInputWrapper()); - containerElement.appendChild(this.buildRunButton()); - containerElement.appendChild(accordionContent); - } - - private buildConstraintInputWrapper(): HTMLElement { - const wrapper = document.createElement("div"); - wrapper.id = "constraint-menu-input"; - wrapper.appendChild(this.editorContainer); - this.validationLabel.id = "validation-label"; - this.validationLabel.classList.add("valid"); - this.validationLabel.innerText = "Valid constraints"; - wrapper.appendChild(this.validationLabel); - const keyboardShortcutLabel = document.createElement("div"); - keyboardShortcutLabel.innerHTML = "Press CTRL+Space for autocompletion"; - wrapper.appendChild(keyboardShortcutLabel); - - monaco.languages.register({ id: DSL_LANGUAGE_ID }); - monaco.languages.setMonarchTokensProvider(DSL_LANGUAGE_ID, constraintDslLanguageMonarchDefinition); - monaco.languages.registerCompletionItemProvider( - DSL_LANGUAGE_ID, - new MonacoEditorConstraintDslCompletionProvider(this.tree), - ); - - const monacoTheme = ThemeManager.useDarkMode ? "vs-dark" : "vs"; - this.editor = monaco.editor.create(this.editorContainer, { - minimap: { - // takes too much space, not useful for our use case - enabled: false, - }, - folding: false, // Not supported by our language definition - wordBasedSuggestions: "off", // Does not really work for our use case - scrollBeyondLastLine: false, // Not needed - theme: monacoTheme, - wordWrap: "on", - language: DSL_LANGUAGE_ID, - scrollBeyondLastColumn: 0, - scrollbar: { - horizontal: "hidden", - vertical: "auto", - // avoid can not scroll page when hover monaco - alwaysConsumeMouseWheel: false, - }, - lineNumbers: "on", - readOnly: this.forceReadOnly, - }); - - this.editor?.setValue(this.constraintRegistry.getConstraintsAsText() || ""); - - this.editor?.onDidChangeModelContent(() => { - if (!this.editor) { - return; - } - - const model = this.editor?.getModel(); - if (!model) { - return; - } - - this.constraintRegistry.setConstraints(model.getLinesContent()); - - const content = model.getLinesContent(); - const marker: monaco.editor.IMarkerData[] = []; - const emptyContent = content.length == 0 || (content.length == 1 && content[0] === ""); - // empty content gets accepted as valid as it represents no constraints - if (!emptyContent) { - const errors = this.tree.verify(content); - marker.push( - ...errors.map((e) => ({ - severity: monaco.MarkerSeverity.Error, - startLineNumber: e.line, - startColumn: e.startColumn, - endLineNumber: e.line, - endColumn: e.endColumn, - message: e.message, - })), - ); - } - - this.validationLabel.innerText = - marker.length == 0 ? "Valid constraints" : `Invalid constraints: ${marker.length} errors`; - this.validationLabel.classList.toggle("valid", marker.length == 0); - - monaco.editor.setModelMarkers(model, "constraint", marker); - }); - - return wrapper; - } - - private buildRunButton(): HTMLElement { - const wrapper = document.createElement("div"); - wrapper.id = "run-button-container"; - - const button = document.createElement("button"); - button.id = "run-button"; - button.innerHTML = "Run"; - button.onclick = () => { - this.dispatcher.dispatch(AnalyzeDiagramAction.create()); - }; - - wrapper.appendChild(button); - return wrapper; - } - - protected onBeforeShow(): void { - this.resizeEditor(); - } - - private resizeEditor(): void { - // Resize editor to fit content. - // Has ranges for height and width to prevent the editor from getting too small or too large. - const e = this.editor; - if (!e) { - return; - } - - // For the height we can use the content height from the editor. - const height = e.getContentHeight(); - - // For the width we cannot really do this. - // Monaco needs about 500ms to figure out the correct width when initially showing the editor. - // In the mean time the width will be too small and after the update - // the window size will jump visibly. - // So for the width we use this calculation to approximate the width. - const maxLineLength = e - .getValue() - .split("\n") - .reduce((max, line) => Math.max(max, line.length), 0); - const width = 100 + maxLineLength * 8; - - const clamp = (value: number, range: readonly [number, number]) => - Math.min(range[1], Math.max(range[0], value)); - - const heightRange = [200, 200] as const; - const widthRange = [500, 750] as const; - - const cHeight = clamp(height, heightRange); - const cWidth = clamp(width, widthRange); - - e.layout({ height: cHeight, width: cWidth }); - } - - switchTheme(useDark: boolean): void { - this.editor?.updateOptions({ theme: useDark ? "vs-dark" : "vs" }); - } - - private buildOptionsButton(): HTMLElement { - const btn = document.createElement("button"); - btn.id = "constraint-options-button"; - btn.title = "Filter…"; - btn.innerHTML = ''; - btn.onclick = () => this.toggleOptionsMenu(); - return btn; - } - - /** show or hide the menu, generate checkboxes on the fly */ - private toggleOptionsMenu(): void { - if (this.optionsMenu) { - this.optionsMenu.remove(); - this.optionsMenu = undefined; - return; - } - - // 1) create container - this.optionsMenu = document.createElement("div"); - this.optionsMenu.id = "constraint-options-menu"; - - // 2) add the “All constraints” checkbox at the top - const allConstraints = document.createElement("label"); - allConstraints.classList.add("options-item"); - - const allCb = document.createElement("input"); - allCb.type = "checkbox"; - allCb.value = "ALL"; - allCb.checked = this.constraintRegistry - .getConstraintList() - .map((c) => c.name) - .every((c) => this.constraintRegistry.getSelectedConstraints().includes(c)); - - allCb.onchange = () => { - if (!this.optionsMenu) return; - - this.ignoreCheckboxChange = true; - try { - if (allCb.checked) { - this.optionsMenu.querySelectorAll("input[type=checkbox]").forEach((cb) => { - if (cb !== allCb) cb.checked = true; - }); - this.dispatcher.dispatch( - ChooseConstraintAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), - ); - } else { - this.optionsMenu.querySelectorAll("input[type=checkbox]").forEach((cb) => { - if (cb !== allCb) cb.checked = false; - }); - this.dispatcher.dispatch(ChooseConstraintAction.create([])); - } - } finally { - this.ignoreCheckboxChange = false; - } - }; - - allConstraints.appendChild(allCb); - allConstraints.appendChild(document.createTextNode("All constraints")); - this.optionsMenu.appendChild(allConstraints); - - // 2) pull your dynamic items - const items = this.constraintRegistry.getConstraintList(); - - // 3) for each item build a checkbox - items.forEach((item) => { - const label = document.createElement("label"); - label.classList.add("options-item"); - - const cb = document.createElement("input"); - cb.type = "checkbox"; - cb.value = item.name; - cb.checked = this.constraintRegistry.getSelectedConstraints().includes(cb.value); - - cb.onchange = () => { - if (this.ignoreCheckboxChange) return; - - const checkboxes = this.optionsMenu!.querySelectorAll("input[type=checkbox]"); - const individualCheckboxes = Array.from(checkboxes).filter((cb) => cb !== allCb); - const selected = individualCheckboxes.filter((cb) => cb.checked).map((cb) => cb.value); - - allCb.checked = individualCheckboxes.every((cb) => cb.checked); - - this.dispatcher.dispatch(ChooseConstraintAction.create(selected)); - }; - - label.appendChild(cb); - label.appendChild(document.createTextNode(item.name)); - this.optionsMenu!.appendChild(label); - }); - - this.editorContainer.appendChild(this.optionsMenu); - - // optional: click-outside handler - const onClickOutside = (e: MouseEvent) => { - const target = e.target as Node; - if (!this.optionsMenu || this.optionsMenu.contains(target)) return; - - const button = document.getElementById("constraint-options-button"); - if (button && button.contains(target)) return; - - this.optionsMenu.remove(); - this.optionsMenu = undefined; - document.removeEventListener("click", onClickOutside); - }; - document.addEventListener("click", onClickOutside); - } -} diff --git a/frontend/webEditor/src/features/constraintMenu/DslLanguage.ts b/frontend/webEditor/src/features/constraintMenu/DslLanguage.ts deleted file mode 100644 index 73d15d60..00000000 --- a/frontend/webEditor/src/features/constraintMenu/DslLanguage.ts +++ /dev/null @@ -1,409 +0,0 @@ -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -import { - AbstractWord, - AnyWord, - AutoCompleteNode, - AutoCompleteTree, - ConstantWord, - NegatableWord, - WordCompletion, -} from "./AutoCompletion"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { SModelRoot } from "sprotty-protocol"; -import { ArrowEdge } from "../dfdElements/edges"; -import { LocalModelSource } from "sprotty"; - -export const DSL_LANGUAGE_ID = "constraint-dsl"; - -export class MonacoEditorConstraintDslCompletionProvider implements monaco.languages.CompletionItemProvider { - constructor(private tree: AutoCompleteTree) {} - - triggerCharacters = [".", "(", " ", ","]; - - provideCompletionItems( - model: monaco.editor.ITextModel, - position: monaco.Position, - ): monaco.languages.ProviderResult { - const allLines = model.getLinesContent(); - const includedLines: string[] = []; - for (let i = 0; i < position.lineNumber - 1; i++) { - includedLines.push(allLines[i]); - } - const currentLine = allLines[position.lineNumber - 1].substring(0, position.column - 1); - includedLines.push(currentLine); - - const r = this.tree.getCompletion(includedLines); - return { - suggestions: r, - }; - } -} - -export const constraintDslLanguageMonarchDefinition: monaco.languages.IMonarchLanguage = { - keywords: ["data", "vertex", "neverFlows", "to", "where", "named", "present", "empty", "type"], - - symbols: /[=> { - getLeaves(destinationSelector).forEach((n) => { - n.canBeFinal = true; - n.children.push(conditionalSelector); - }); - }); - const nodeDestinationSelector: AutoCompleteNode = { - word: new ConstantWord("vertex"), - children: destinationSelectors, - }; - - const neverFlows: AutoCompleteNode = { - word: new ConstantWord("neverFlows"), - children: [nodeDestinationSelector, conditionalSelector], - canBeFinal: true, - }; - - const dataSourceSelector: AutoCompleteNode = { - word: new ConstantWord("data"), - children: [], - }; - - const nodeSelectors = getAbstractSelectors(modelSource, labelTypeRegistry); - nodeSelectors.forEach((nodeSelector) => { - getLeaves(nodeSelector).forEach((n) => { - n.children.push(dataSourceSelector); - n.children.push(neverFlows); - }); - }); - const nodeSourceSelector: AutoCompleteNode = { - word: new ConstantWord("vertex"), - children: nodeSelectors, - }; - - const dataSelectors = getAbstractSelectors(modelSource, labelTypeRegistry); - dataSelectors.forEach((dataSelector) => { - getLeaves(dataSelector).forEach((n) => { - n.children.push(nodeSourceSelector); - n.children.push(neverFlows); - }); - }); - dataSourceSelector.children = dataSelectors; - - const nameNode: AutoCompleteNode = { - word: new NameWord(), - children: [nodeSourceSelector, dataSourceSelector], - }; - - const startNode: AutoCompleteNode = { - word: new ConstantWord("-"), - children: [nameNode], - }; - - return [startNode]; - } - - function getLeaves(node: AutoCompleteNode): AutoCompleteNode[] { - if (node.children.length == 0) { - return [node]; - } - let result: AutoCompleteNode[] = []; - for (const n of node.children) { - result = result.concat(getLeaves(n)); - } - return result; - } - - function getAbstractSelectors( - modelSource: LocalModelSource, - labelTypeRegistry: LabelTypeRegistry, - ): AutoCompleteNode[] { - const vertexTypeSelector: AutoCompleteNode = { - word: new ConstantWord("type"), - children: [ - new NegatableWord(new ConstantWord("EXTERNAL")), - new NegatableWord(new ConstantWord("PROCESS")), - new NegatableWord(new ConstantWord("STORE")), - ].map((w) => ({ word: w, children: [] })), - }; - const characteristicsSelector = { - word: new NegatableWord(new CharacteristicSelectorData(labelTypeRegistry)), - children: [], - }; - const dataCharacteristicListSelector = { - word: new NegatableWord(new CharacteristicSelectorDataList(labelTypeRegistry)), - children: [], - }; - const variableNameSelector = { - word: new ConstantWord("named"), - children: [ - { - word: new VariableName(modelSource), - children: [], - }, - ], - }; - return [vertexTypeSelector, characteristicsSelector, dataCharacteristicListSelector, variableNameSelector]; - } - - function getConditionalSelectors(): AutoCompleteNode[] { - const variableConditionalSelector: AutoCompleteNode = { - word: new ConstantWord("present"), - children: [ - { - word: new NegatableWord(new ConstraintVariableReference()), - children: [], - }, - ], - }; - - const emptySetOperationSelector: AutoCompleteNode = { - word: new ConstantWord("empty"), - children: [ - { - word: new IntersectionWord(), - children: [], - }, - ], - }; - - return [variableConditionalSelector, emptySetOperationSelector]; - } - - class IntersectionWord implements AbstractWord { - private constraintVariableReference: ConstraintVariableReference; - - constructor() { - this.constraintVariableReference = new ConstraintVariableReference(); - } - - completionOptions(word: string): WordCompletion[] { - if (!word.startsWith("intersection(")) { - if (!"intersection(".includes(word)) { - return []; - } - return [ - { - label: "intersection()", - insertText: "intersection($0)", - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - kind: monaco.languages.CompletionItemKind.Function, - }, - ]; - } - const attributes = word.substring("intersection(".length, word.length - 1).split(","); - if (attributes.length > 2) { - return []; - } - return this.constraintVariableReference.completionOptions(); - } - verifyWord(word: string): string[] { - if (!word.startsWith("intersection(")) { - return ['Expected keyword "intersection"']; - } - const attributes = word.substring("intersection(".length, word.length - 1).split(","); - if (attributes.length > 2) { - return ['Expected at most 2 attributes in "intersection"']; - } - return attributes.flatMap((a) => this.constraintVariableReference.verifyWord(a)); - } - } - - class ConstraintVariableReference extends AnyWord {} - - class CharacteristicSelectorData implements AbstractWord { - constructor(private readonly labelTypeRegistry: LabelTypeRegistry) {} - - completionOptions(word: string): WordCompletion[] { - const parts = word.split("."); - - if (parts.length == 1) { - return this.labelTypeRegistry.getLabelTypes().map((l) => ({ - insertText: l.name, - kind: monaco.languages.CompletionItemKind.Class, - })); - } else if (parts.length == 2) { - const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); - if (!type) { - return []; - } - - const possibleValues: WordCompletion[] = type.values.map((l) => ({ - insertText: l.text, - kind: monaco.languages.CompletionItemKind.Enum, - startOffset: parts[0].length + 1, - })); - possibleValues.push({ - insertText: "$" + type.name, - kind: monaco.languages.CompletionItemKind.Variable, - startOffset: parts[0].length + 1, - }); - return possibleValues; - } - - return []; - } - - verifyWord(word: string): string[] { - const parts = word.split("."); - - if (parts.length > 2) { - return ["Expected at most 2 parts in characteristic selector"]; - } - - const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); - if (!type) { - return ['Unknown label type "' + parts[0] + '"']; - } - - if (parts.length < 2) { - return ["Expected characteristic to have value"]; - } - - if (parts[1].startsWith("$") && parts[1].length >= 2) { - return []; - } - - const label = type.values.find((l) => l.text === parts[1]); - if (!label) { - return ['Unknown label value "' + parts[1] + '" for type "' + parts[0] + '"']; - } - - return []; - } - } - - class NameWord implements AbstractWord { - completionOptions(word: string): WordCompletion[] { - if (word.length === 0) { - return []; - } - return [ - { - insertText: ":", - kind: monaco.languages.CompletionItemKind.Keyword, - }, - ]; - } - - verifyWord(word: string): string[] { - const name = word.split(":")[0]; - if (name.length === 0) { - return ["Expected a name"]; - } - if (!word.endsWith(":")) { - return ['Expected ":" at the end of name']; - } - return []; - } - } - - class VariableName implements AbstractWord { - constructor(private readonly modelSource: LocalModelSource) {} - - completionOptions(): WordCompletion[] { - return this.getAllPortNames().map((n) => ({ - insertText: n, - kind: monaco.languages.CompletionItemKind.Variable, - })); - } - verifyWord(word: string): string[] { - if (this.getAllPortNames().includes(word)) { - return []; - } - return ['Unknown variable name "' + word + '"']; - } - - private getAllPortNames(): string[] { - const portEdgeNameMap: Map = new Map(); - const graph = this.modelSource.model as SModelRoot; - if (graph.children === undefined) { - return []; - } - for (const element of graph.children) { - const edge = element as ArrowEdge; - if (edge.text !== undefined && edge.targetId !== undefined) { - const edgeName = edge.text!; - const target = edge.targetId; - if (portEdgeNameMap.has(target)) { - portEdgeNameMap.get(target)?.push(edgeName); - } else { - portEdgeNameMap.set(target, [edgeName]); - } - } - } - - return Array.from(portEdgeNameMap.keys()).map((key) => portEdgeNameMap.get(key)!.sort().join("|")); - } - } - - class CharacteristicSelectorDataList implements AbstractWord { - private characteristicSelectorData: CharacteristicSelectorData; - - constructor(labelTypeRegistry: LabelTypeRegistry) { - this.characteristicSelectorData = new CharacteristicSelectorData(labelTypeRegistry); - } - - completionOptions(word: string): WordCompletion[] { - const parts = word.split(","); - const last = parts[parts.length - 1]; - - return this.characteristicSelectorData.completionOptions(last); - } - verifyWord(word: string): string[] { - const parts = word.split(","); - for (let i = 0; i < parts.length; i++) { - const r = this.characteristicSelectorData.verifyWord(parts[i]); - if (r.length > 0) { - return r; - } - } - - return []; - } - } -} diff --git a/frontend/webEditor/src/features/constraintMenu/actions.ts b/frontend/webEditor/src/features/constraintMenu/actions.ts deleted file mode 100644 index f69f1104..00000000 --- a/frontend/webEditor/src/features/constraintMenu/actions.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Action } from "sprotty-protocol"; - -export interface ChooseConstraintAction extends Action { - kind: typeof ChooseConstraintAction.KIND; - names: string[]; -} - -export namespace ChooseConstraintAction { - export const KIND = "choose-constraint"; - - export function create(names: string[]): ChooseConstraintAction { - return { kind: KIND, names }; - } -} diff --git a/frontend/webEditor/src/features/constraintMenu/commands.ts b/frontend/webEditor/src/features/constraintMenu/commands.ts deleted file mode 100644 index d131ca6d..00000000 --- a/frontend/webEditor/src/features/constraintMenu/commands.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { inject, injectable } from "inversify"; -import { Command, CommandExecutionContext, CommandReturn, TYPES } from "sprotty"; -import { DfdNodeImpl } from "../dfdElements/nodes"; -import { ChooseConstraintAction } from "./actions"; -import { getBasicType } from "sprotty-protocol"; -import { AnnnotationsManager } from "../settingsMenu/annotationManager"; -import { ConstraintRegistry } from "./constraintRegistry"; - -@injectable() -export class ChooseConstraintCommand extends Command { - static readonly KIND = ChooseConstraintAction.KIND; - - constructor( - @inject(TYPES.Action) private action: ChooseConstraintAction, - @inject(AnnnotationsManager) private annnotationsManager: AnnnotationsManager, - @inject(ConstraintRegistry) private constraintRegistry: ConstraintRegistry, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - this.annnotationsManager.clearTfgs(); - const names = this.action.names; - this.constraintRegistry.setSelectedConstraints(names); - - const nodes = context.root.children.filter((node) => getBasicType(node) === "node") as DfdNodeImpl[]; - if (names.length === 0) { - nodes.forEach((node) => { - node.setColor("var(--color-primary)"); - }); - return context.root; - } - - nodes.forEach((node) => { - const annotations = node.annotations!; - let wasAdjusted = false; - if (this.constraintRegistry.selectedContainsAllConstraints()) { - annotations.forEach((annotation) => { - if (annotation.message.startsWith("Constraint")) { - wasAdjusted = true; - node.setColor(annotation.color!); - } - }); - } - names.forEach((name) => { - annotations.forEach((annotation) => { - if (annotation.message.startsWith("Constraint ") && annotation.message.split(" ")[1] === name) { - node.setColor(annotation.color!); - wasAdjusted = true; - this.annnotationsManager.addTfg(annotation.tfg!); - } - }); - }); - if (!wasAdjusted) node.setColor("var(--color-primary)"); - }); - - nodes.forEach((node) => { - const inTFG = node.annotations!.filter((annotation) => - this.annnotationsManager.getSelectedTfgs().has(annotation.tfg!), - ); - if (inTFG.length > 0) node.setColor("var(--color-highlighted)", false); - }); - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - redo(context: CommandExecutionContext): CommandReturn { - return context.root; - } -} diff --git a/frontend/webEditor/src/features/constraintMenu/constraintMenu.css b/frontend/webEditor/src/features/constraintMenu/constraintMenu.css deleted file mode 100644 index 5801bc35..00000000 --- a/frontend/webEditor/src/features/constraintMenu/constraintMenu.css +++ /dev/null @@ -1,145 +0,0 @@ -div.constraint-menu { - right: 20px; - bottom: 20px; - padding: 10px 10px; -} - -.accordion-content:has(.monaco-editor.focused) * { - overflow: visible; -} - -#constraint-menu-expand-title { - padding-right: 85px; -} - -#run-button-container { - position: absolute; - right: 6px; - bottom: 6px; - width: 80px; - z-index: 50; -} - -#run-button { - background-color: green; - color: white; - border: none; - border-radius: 8px; - padding: 5px 10px; - text-align: center; - text-decoration: none; - display: inline-block; - width: 100%; - cursor: pointer; -} - -#run-button::before { - content: ""; - background-image: url("@fortawesome/fontawesome-free/svgs/solid/play.svg"); - display: inline-block; - filter: invert(1); - height: 16px; - width: 16px; - background-size: 16px 16px; - vertical-align: text-top; -} - -#constraint-menu-input { - min-width: 300px; -} - -#constraint-menu-input * { - overflow: visible; -} - -#constraint-menu-input .overflow-guard { - overflow: hidden; -} - -#validation-label { - height: 1rem; - color: var(--color-error); -} - -#validation-label.valid { - color: var(--color-valid); -} - -#constraint-menu-list { - grid-row-start: 1; - grid-row-end: 2; - grid-column-start: 2; - overflow: scroll; - max-height: 210px; -} - -#constraint-menu-list * { - color: var(--color-foreground); -} - -.constrain-label input { - background-color: var(--color-background); - text-align: center; - border: 1px solid var(--color-foreground); - border-radius: 15px; - padding: 3px; - margin: 4px; -} - -.constrain-label.selected input { - border: 2px solid var(--color-foreground); -} - -.constrain-label button { - background-color: transparent; - border: none; - cursor: pointer; - padding: 0; -} - -.constraint-add { - padding: 0; - border: none; - background-color: transparent; - cursor: pointer; - display: flex; - align-items: center; - gap: 5px; -} - -#constraint-options-button { - position: absolute; - top: 6px; - right: 6px; - background: transparent; - border: none; - font-size: 1.2em; - cursor: pointer; - color: var(--color-foreground); - padding: 2px; -} - -#constraint-options-menu { - position: absolute; - top: 30px; /* just under the header */ - right: 6px; - background: var(--color-background); - border: 1px solid var(--color-foreground); - border-radius: 4px; - padding: 8px; - z-index: 100; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); -} - -#constraint-options-menu .options-item { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 4px; - font-size: 0.9em; - color: var(--color-foreground); -} - -#constraint-options-menu .options-item:last-child { - margin-bottom: 0; -} diff --git a/frontend/webEditor/src/features/constraintMenu/constraintRegistry.ts b/frontend/webEditor/src/features/constraintMenu/constraintRegistry.ts deleted file mode 100644 index 3b773635..00000000 --- a/frontend/webEditor/src/features/constraintMenu/constraintRegistry.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { injectable } from "inversify"; - -export interface Constraint { - name: string; - constraint: string; -} - -@injectable() -export class ConstraintRegistry { - private constraints: Constraint[] = []; - private updateCallbacks: (() => void)[] = []; - private selectedConstraints: string[] = this.constraints.map((c) => c.name); - - public setConstraints(constraints: string[]): void { - this.constraints = this.splitIntoConstraintTexts(constraints).map((c) => this.mapToConstraint(c)); - } - - public setConstraintsFromArray(constraints: Constraint[]): void { - this.constraints = constraints.map((c) => ({ - name: c.name, - constraint: c.constraint, - })); - this.constraintListChanged(); - } - - public setSelectedConstraints(constraints: string[]): void { - this.selectedConstraints = constraints; - } - - public getSelectedConstraints(): string[] { - return this.selectedConstraints; - } - - public clearConstraints(): void { - this.constraints = []; - this.constraintListChanged(); - } - - public constraintListChanged(): void { - this.updateCallbacks.forEach((cb) => cb()); - } - - public onUpdate(callback: () => void): void { - this.updateCallbacks.push(callback); - } - - public getConstraintsAsText(): string { - return this.constraints.map((c) => `- ${c.name}: ${c.constraint}`).join("\n"); - } - - public getConstraintList(): Constraint[] { - return this.constraints; - } - - public selectedContainsAllConstraints(): boolean { - return this.getConstraintList() - .map((c) => c.name) - .every((c) => this.getSelectedConstraints().includes(c)); - } - - public setAllConstraintsAsSelected(): void { - this.selectedConstraints = this.constraints.map((c) => c.name); - } - - private splitIntoConstraintTexts(text: string[]): string[] { - const constraints: string[] = []; - let currentConstraint = ""; - for (const line of text) { - if (line.startsWith("- ")) { - if (currentConstraint !== "") { - constraints.push(currentConstraint); - } - currentConstraint = line; - } else { - currentConstraint += `\n${line}`; - } - } - if (currentConstraint !== "") { - constraints.push(currentConstraint); - } - return constraints; - } - - private mapToConstraint(constraint: string): Constraint { - // the brackets ensure its a capturing split - const parts = constraint.split(/(\s+)/); - // if less than 3 parts are present no name or constraint can be extracted (e.g. "- " -> ["-", " "]) - if (parts.length < 3) { - return { name: "", constraint: "" }; - } - let name = parts[2]; - if (name.endsWith(":")) { - name = name.slice(0, -1); - } - let constraintText = ""; - // the first 4 parts are "- ", whitespace, `${name}:`, whitespace --> Thus the constraint starts at index 4 - for (let i = 4; i < parts.length; i++) { - constraintText += parts[i]; - } - return { name, constraint: constraintText }; - } -} diff --git a/frontend/webEditor/src/features/constraintMenu/di.config.ts b/frontend/webEditor/src/features/constraintMenu/di.config.ts deleted file mode 100644 index 9a35f761..00000000 --- a/frontend/webEditor/src/features/constraintMenu/di.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ContainerModule } from "inversify"; -import { EDITOR_TYPES } from "../../utils"; -import { ConstraintMenu } from "./ConstraintMenu"; -import { configureCommand, TYPES } from "sprotty"; -import { ConstraintRegistry } from "./constraintRegistry"; -import { SWITCHABLE } from "../settingsMenu/themeManager"; -import { ChooseConstraintCommand } from "./commands"; - -// This module contains an UI extension that adds a tool palette to the editor. -// This tool palette allows the user to create new nodes and edges. -// Additionally it contains the tools that are used to create the nodes and edges. - -export const constraintMenuModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(ConstraintRegistry).toSelf().inSingletonScope(); - - bind(ConstraintMenu).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(ConstraintMenu); - bind(EDITOR_TYPES.DefaultUIElement).toService(ConstraintMenu); - bind(SWITCHABLE).toService(ConstraintMenu); - - const context = { bind, unbind, isBound, rebind }; - configureCommand(context, ChooseConstraintCommand); -}); diff --git a/frontend/webEditor/src/features/copyPaste/di.config.ts b/frontend/webEditor/src/features/copyPaste/di.config.ts deleted file mode 100644 index 21fca863..00000000 --- a/frontend/webEditor/src/features/copyPaste/di.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ContainerModule } from "inversify"; -import { TYPES, configureCommand } from "sprotty"; -import { CopyPasteKeyListener } from "./keyListener"; -import { PasteElementsCommand } from "./pasteCommand"; - -/** - * This feature allows the user to copy and paste elements. - * When ctrl+c is pressed, all selected elements are copied into an internal array. - * When ctrl+v is pressed, all elements in the internal array are pasted with an fixed offset. - * Nodes are copied with their ports and edges are copied if source and target were copied as well. - */ -export const copyPasteModule = new ContainerModule((bind, unbind, isBound, rebind) => { - const context = { bind, unbind, isBound, rebind }; - bind(TYPES.KeyListener).to(CopyPasteKeyListener).inSingletonScope(); - configureCommand(context, PasteElementsCommand); -}); diff --git a/frontend/webEditor/src/features/copyPaste/keyListener.ts b/frontend/webEditor/src/features/copyPaste/keyListener.ts deleted file mode 100644 index 1d70f51d..00000000 --- a/frontend/webEditor/src/features/copyPaste/keyListener.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { inject, injectable } from "inversify"; -import { PasteElementsAction } from "./pasteCommand"; -import { - CommitModelAction, - KeyListener, - MousePositionTracker, - SModelElementImpl, - SModelRootImpl, - isSelected, -} from "sprotty"; -import { Action } from "sprotty-protocol"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; - -/** - * This class is responsible for listening to ctrl+c and ctrl+v events. - * On copy the selected elements are copied into an internal array. - * On paste the {@link PasteElementsAction} is executed to paste the elements. - * This is done inside a command, so that it can be undone/redone. - */ -@injectable() -export class CopyPasteKeyListener implements KeyListener { - private copyElements: SModelElementImpl[] = []; - - constructor(@inject(MousePositionTracker) private readonly mousePositionTracker: MousePositionTracker) {} - - keyUp(): Action[] { - return []; - } - - keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] { - if (matchesKeystroke(event, "KeyC", "ctrl")) { - return this.copy(element.root); - } else if (matchesKeystroke(event, "KeyV", "ctrl")) { - return this.paste(); - } - - return []; - } - - /** - * Copy all selected elements into the "clipboard" (the internal element array) - */ - private copy(root: SModelRootImpl): Action[] { - this.copyElements = []; // Clear the clipboard - - // Find selected elements - root.index - .all() - .filter((element) => isSelected(element)) - .forEach((e) => this.copyElements.push(e)); - - return []; - } - - /** - * Pastes elements by creating new elements and copying the properties of the copied elements. - * This is done inside a command, so that it can be undone/redone. - */ - private paste(): Action[] { - const targetPosition = this.mousePositionTracker.lastPositionOnDiagram ?? { x: 0, y: 0 }; - return [PasteElementsAction.create(this.copyElements, targetPosition), CommitModelAction.create()]; - } -} diff --git a/frontend/webEditor/src/features/copyPaste/pasteCommand.ts b/frontend/webEditor/src/features/copyPaste/pasteCommand.ts deleted file mode 100644 index 846af490..00000000 --- a/frontend/webEditor/src/features/copyPaste/pasteCommand.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { - Command, - CommandExecutionContext, - CommandReturn, - SChildElementImpl, - SEdgeImpl, - SModelElementImpl, - SNodeImpl, - TYPES, - isSelectable, -} from "sprotty"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { generateRandomSprottyId } from "../../utils"; -import { DfdNode, DfdNodeImpl } from "../dfdElements/nodes"; -import { Action, Point, SEdge, SModelElement } from "sprotty-protocol"; -import { LoadDiagramCommand } from "../serialize/load"; -import { EditorModeController } from "../editorMode/editorModeController"; - -export interface PasteElementsAction extends Action { - kind: typeof PasteElementsAction.KIND; - copyElements: SModelElementImpl[]; - targetPosition: Point; -} -export namespace PasteElementsAction { - export const KIND = "paste-clipboard-elements"; - export function create(copyElements: SModelElementImpl[], targetPosition: Point): PasteElementsAction { - return { - kind: KIND, - copyElements, - targetPosition, - }; - } -} - -/** - * This command is used to paste elements that were copied by the CopyPasteFeature. - * It creates new elements and copies the properties of the copied elements. - * This is done inside a command, so that it can be undone/redone. - */ -@injectable() -export class PasteElementsCommand extends Command { - public static readonly KIND = PasteElementsAction.KIND; - - @inject(DynamicChildrenProcessor) - private dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - - private newElements: SChildElementImpl[] = []; - // This maps the element id of the copy source element to the - // id that the newly created copy target element has. - private copyElementIdMapping: Record = {}; - - constructor(@inject(TYPES.Action) private readonly action: PasteElementsAction) { - super(); - } - - /** - * Selects the newly created copy and deselects the copy source. - */ - private setSelection(context: CommandExecutionContext, selection: "old" | "new"): void { - Object.entries(this.copyElementIdMapping).forEach(([oldId, newId]) => { - const oldElement = context.root.index.getById(oldId); - const newElement = context.root.index.getById(newId); - - if (oldElement && isSelectable(oldElement)) { - oldElement.selected = selection === "old"; - } - if (newElement && isSelectable(newElement)) { - newElement.selected = selection === "new"; - } - }); - } - - /** - * Calculates the offset between the copy source elements and the set paste target position. - * Does this by finding the top left position of the copy source elements and subtracting it from the target position. - * - * @returns The offset between the top left position of the copy source elements and the target position. - */ - private computeElementOffset(): Point { - const sourcePosition = { x: Infinity, y: Infinity }; - - this.action.copyElements.forEach((element) => { - if (!(element instanceof SNodeImpl)) { - return; - } - - if (element.position.x < sourcePosition.x) { - sourcePosition.x = element.position.x; - } - if (element.position.y < sourcePosition.y) { - sourcePosition.y = element.position.y; - } - }); - - if (sourcePosition.x === Infinity || sourcePosition.y === Infinity) { - return { x: 0, y: 0 }; - } - - // Compute delta between top left position of copy source elements and the target position - return Point.subtract(this.action.targetPosition, sourcePosition); - } - - execute(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - // Step 1: copy nodes and their ports - const positionOffset = this.computeElementOffset(); - this.action.copyElements.forEach((element) => { - if (!(element instanceof SNodeImpl)) { - return; - } - - // createSchema only does a shallow copy, so we need to do an additional deep copy here because - // we want to support copying elements with objects and arrays in them. - const schema = JSON.parse(JSON.stringify(context.modelFactory.createSchema(element))) as SModelElement; - // Remove json artifacts - LoadDiagramCommand.preprocessModelSchema(schema); - - schema.id = generateRandomSprottyId(); - this.copyElementIdMapping[element.id] = schema.id; - if ("position" in schema) { - schema.position = Point.add(element.position, positionOffset); - } - - // Regenerate dynamic sub elements - this.dynamicChildrenProcessor.processGraphChildren(schema, "remove"); - - if (element instanceof DfdNodeImpl) { - // Special case for DfdNodes: copy ports and give the nodes new ids. - (schema as DfdNode).ports.forEach((port) => { - const oldPortId = port.id; - port.id = generateRandomSprottyId(); - this.copyElementIdMapping[oldPortId] = port.id; - }); - } - - this.dynamicChildrenProcessor.processGraphChildren(schema, "set"); - - const newElement = context.modelFactory.createElement(schema); - this.newElements.push(newElement); - }); - - // Step 2: copy edges - // If the source and target element of an edge are copied, the edge can be copied as well. - // If only one of them is copied, the edge is not copied. - this.action.copyElements.forEach((element) => { - if (!(element instanceof SEdgeImpl)) { - return; - } - - const newSourceId = this.copyElementIdMapping[element.sourceId]; - const newTargetId = this.copyElementIdMapping[element.targetId]; - - if (!newSourceId || !newTargetId) { - // Not both source and target are copied, ignore this edge - return; - } - - const schema = JSON.parse(JSON.stringify(context.modelFactory.createSchema(element))) as SEdge; - LoadDiagramCommand.preprocessModelSchema(schema); - - schema.id = generateRandomSprottyId(); - this.copyElementIdMapping[element.id] = schema.id; - - schema.sourceId = newSourceId; - schema.targetId = newTargetId; - - // Regenerate dynamic sub elements (the edge label) - this.dynamicChildrenProcessor.processGraphChildren(schema, "remove"); - this.dynamicChildrenProcessor.processGraphChildren(schema, "set"); - - const newElement = context.modelFactory.createElement(schema); - this.newElements.push(newElement); - }); - - // Step 3: add new elements to the model and select them - this.newElements.forEach((element) => { - context.root.add(element); - }); - this.setSelection(context, "new"); - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - // Remove elements from the model - this.newElements.forEach((element) => { - context.root.remove(element); - }); - // Select the old elements - this.setSelection(context, "old"); - - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - this.newElements.forEach((element) => { - context.root.add(element); - }); - this.setSelection(context, "new"); - - return context.root; - } -} diff --git a/frontend/webEditor/src/features/dfdElements/AssignmentLanguage.ts b/frontend/webEditor/src/features/dfdElements/AssignmentLanguage.ts deleted file mode 100644 index 6751570d..00000000 --- a/frontend/webEditor/src/features/dfdElements/AssignmentLanguage.ts +++ /dev/null @@ -1,477 +0,0 @@ -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -import { - AbstractWord, - AutoCompleteNode, - AutoCompleteTree, - ConstantWord, - Token, - WordCompletion, -} from "../constraintMenu/AutoCompletion"; -import { SModelElementImpl, SParentElementImpl, SPortImpl } from "sprotty"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { DfdNodeImpl } from "./nodes"; - -export class MonacoEditorAssignmentLanguageCompletionProvider implements monaco.languages.CompletionItemProvider { - constructor(private tree: AutoCompleteTree) {} - - triggerCharacters = [".", ";", " ", ",", "("]; - - provideCompletionItems( - model: monaco.editor.ITextModel, - position: monaco.Position, - ): monaco.languages.ProviderResult { - const allLines = model.getLinesContent(); - const includedLines: string[] = []; - for (let i = 0; i < position.lineNumber - 1; i++) { - includedLines.push(allLines[i]); - } - const currentLine = allLines[position.lineNumber - 1].substring(0, position.column - 1); - includedLines.push(currentLine); - - const r = this.tree.getCompletion(includedLines); - return { - suggestions: r, - }; - } -} - -const startOfLineKeywords = ["forward", "assign", "set", "unset"]; -const statementKeywords = [...startOfLineKeywords, "if", "from"]; -const constantsKeywords = ["TRUE", "FALSE"]; -export const assignemntLanguageMonarchDefinition: monaco.languages.IMonarchLanguage = { - keywords: [...statementKeywords, ...constantsKeywords], - - operators: ["=", "||", "&&", "!"], - - symbols: /[=>[]) { - super(roots); - } - - public replace(lines: string[], replacement: ReplacementData): string[] { - const tokens = this.tokenize(lines); - const replaced = this.replaceToken(this.roots, tokens, 0, replacement); - const newLines: string[] = []; - let currentLine = ""; - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - const newText = replaced[i]; - currentLine += newText; - currentLine += token.whiteSpaceAfter || ""; - if (i == tokens.length - 1 || tokens[i + 1].line !== token.line) { - newLines.push(currentLine); - currentLine = ""; - } - } - return newLines; - } - - private replaceToken( - nodes: AutoCompleteNode[], - tokens: Token[], - index: number, - replacement: ReplacementData, - skipStartCheck = false, - ): string[] { - if (index >= tokens.length) { - return []; - } - // check for new start - if (!skipStartCheck && tokens[index].column == 1) { - const matchesAnyRoot = this.roots.some((n) => n.word.verifyWord(tokens[index].text).length === 0); - if (matchesAnyRoot) { - return this.replaceToken(this.roots, tokens, index, replacement, true); - } - } - let text = tokens[index].text; - for (const n of nodes) { - if ((n.word as ReplaceableAbstractWord).replaceWord) { - text = (n.word as ReplaceableAbstractWord).replaceWord(text, replacement); - } - } - return [ - text, - ...this.replaceToken( - nodes.flatMap((n) => n.children), - tokens, - index + 1, - replacement, - ), - ]; - } -} - -export namespace TreeBuilder { - export function buildTree( - labelTypeRegistry: LabelTypeRegistry, - port?: SPortImpl, - ): AutoCompleteNode[] { - return [ - buildSetOrUnsetStatement(labelTypeRegistry, "set"), - buildSetOrUnsetStatement(labelTypeRegistry, "unset"), - buildForwardStatement(port), - buildAssignStatement(labelTypeRegistry, port), - ]; - } - - function buildSetOrUnsetStatement( - labelTypeRegistry: LabelTypeRegistry, - keyword: string, - ): AutoCompleteNode { - const labelNode: AutoCompleteNode = { - word: new LabelListWord(labelTypeRegistry), - children: [], - }; - return { - word: new ConstantWord(keyword), - children: [labelNode], - }; - } - - function buildForwardStatement(port?: SPortImpl) { - const inputNode: AutoCompleteNode = { - word: new InputListWord(port), - children: [], - }; - return { - word: new ConstantWord("forward"), - children: [inputNode], - }; - } - - function buildAssignStatement( - labelTypeRegistry: LabelTypeRegistry, - port?: SPortImpl, - ): AutoCompleteNode { - const fromNode: AutoCompleteNode = { - word: new ConstantWord("from"), - children: [ - { - word: new InputListWord(port), - children: [], - }, - ], - }; - const ifNode: AutoCompleteNode = { - word: new ConstantWord("if"), - children: buildCondition(labelTypeRegistry, fromNode, port), - }; - return { - word: new ConstantWord("assign"), - children: [ - { - word: new LabelWord(labelTypeRegistry), - children: [ifNode], - }, - ], - }; - } - - function buildCondition(labelTypeRegistry: LabelTypeRegistry, nextNode: AutoCompleteNode, port?: SPortImpl) { - const connectors: AutoCompleteNode[] = ["&&", "||"].map((o) => ({ - word: new ConstantWord(o), - children: [], - })); - - const expressors: AutoCompleteNode[] = [ - new ConstantWord("TRUE"), - new ConstantWord("FALSE"), - new InputLabelWord(labelTypeRegistry, port), - ].map((e) => ({ - word: e, - children: [...connectors, nextNode], - canBeFinal: true, - })); - - connectors.forEach((c) => { - c.children = expressors; - }); - return expressors; - } -} - -abstract class InputAwareWord { - constructor(private port?: SPortImpl) {} - - protected getAvailableInputs(): string[] { - const parent = this.port?.parent; - if (parent && parent instanceof DfdNodeImpl) { - return parent.getAvailableInputs().filter((input) => input !== undefined) as string[]; - } - return []; - } - - private getSelectedPorts(node: SModelElementImpl): SPortImpl[] { - if (node instanceof SPortImpl && node.selected) { - return [node]; - } - if (node instanceof SParentElementImpl) { - return node.children.flatMap((child) => this.getSelectedPorts(child)); - } - return []; - } -} - -class LabelWord implements ReplaceableAbstractWord { - constructor(private readonly labelTypeRegistry: LabelTypeRegistry) {} - - completionOptions(word: string): WordCompletion[] { - const parts = word.split("."); - - if (parts.length == 1) { - return this.labelTypeRegistry.getLabelTypes().map((l) => ({ - insertText: l.name, - kind: monaco.languages.CompletionItemKind.Class, - })); - } else if (parts.length == 2) { - const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); - if (!type) { - return []; - } - - return type.values.map((l) => ({ - insertText: l.text, - kind: monaco.languages.CompletionItemKind.Enum, - startOffset: parts[0].length + 1, - })); - } - - return []; - } - - verifyWord(word: string): string[] { - const parts = word.split("."); - - if (parts.length > 2) { - return ["Expected at most 2 parts in characteristic selector"]; - } - - const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); - if (!type) { - return ['Unknown label type "' + parts[0] + '"']; - } - - if (parts.length < 2) { - return ["Expected characteristic to have value"]; - } - - if (parts[1].startsWith("$") && parts[1].length >= 2) { - return []; - } - - const label = type.values.find((l) => l.text === parts[1]); - if (!label) { - return ['Unknown label value "' + parts[1] + '" for type "' + parts[0] + '"']; - } - - return []; - } - - replaceWord(text: string, replacement: ReplacementData) { - if (replacement.type == "Label" && text == replacement.old) { - return replacement.replacement; - } - return text; - } -} - -class LabelListWord implements ReplaceableAbstractWord { - labelWord: LabelWord; - - constructor(labelTypeRegistry: LabelTypeRegistry) { - this.labelWord = new LabelWord(labelTypeRegistry); - } - - completionOptions(word: string): WordCompletion[] { - const parts = word.split(","); - const lastPart = parts[parts.length - 1]; - const prefixLength = parts.slice(0, -1).reduce((acc, part) => acc + part.length + 1, 0); // +1 for the commas - return this.labelWord.completionOptions(lastPart).map((c) => ({ - ...c, - startOffset: prefixLength + (c.startOffset ?? 0), - })); - } - - verifyWord(word: string): string[] { - const parts = word.split(","); - const errors: string[] = []; - for (const part of parts) { - errors.push(...this.labelWord.verifyWord(part)); - } - return errors; - } - - replaceWord(text: string, replacement: ReplacementData) { - const parts = text.split(","); - const newParts = parts.map((part) => this.labelWord.replaceWord(part, replacement)); - return newParts.join(","); - } -} - -class InputWord extends InputAwareWord implements ReplaceableAbstractWord { - completionOptions(): WordCompletion[] { - const inputs = this.getAvailableInputs(); - return inputs.map((input) => ({ - insertText: input, - kind: monaco.languages.CompletionItemKind.Variable, - })); - } - - verifyWord(word: string): string[] { - const availableInputs = this.getAvailableInputs(); - if (availableInputs.includes(word)) { - return []; - } - return [`Unknown input "${word}"`]; - } - - replaceWord(text: string, replacement: ReplacementData) { - if (replacement.type == "Input" && text == replacement.old) { - return replacement.replacement; - } - return text; - } -} - -class InputListWord implements ReplaceableAbstractWord { - private inputWord: InputWord; - constructor(port?: SPortImpl) { - this.inputWord = new InputWord(port); - } - - completionOptions(word: string): WordCompletion[] { - const parts = word.split(","); - // remove last one as we are completing that one - if (parts.length > 1) { - parts.pop(); - } - const startOffset = parts.reduce((acc, part) => acc + part.length + 1, 0); // +1 for the commas - return this.inputWord - .completionOptions() - .filter((c) => !parts.includes(c.insertText)) - .map((c) => ({ - ...c, - startOffset: startOffset + (c.startOffset ?? 0), - })); - } - - verifyWord(word: string): string[] { - const parts = word.split(","); - const errors: string[] = []; - for (const part of parts) { - errors.push(...this.inputWord.verifyWord(part)); - } - return errors; - } - - replaceWord(text: string, replacement: ReplacementData) { - const parts = text.split(","); - const newParts = parts.map((part) => this.inputWord.replaceWord(part, replacement)); - return newParts.join(","); - } -} - -class InputLabelWord implements ReplaceableAbstractWord { - private inputWord: InputWord; - private labelWord: LabelWord; - - constructor(labelTypeRegistry: LabelTypeRegistry, port?: SPortImpl) { - this.inputWord = new InputWord(port); - this.labelWord = new LabelWord(labelTypeRegistry); - } - - completionOptions(word: string): WordCompletion[] { - const parts = this.getParts(word); - if (parts[1] === undefined) { - return this.inputWord.completionOptions().map((c) => ({ - ...c, - insertText: c.insertText, - })); - } else if (parts.length >= 2) { - return this.labelWord.completionOptions(parts[1]).map((c) => ({ - ...c, - insertText: c.insertText, - startOffset: (c.startOffset ?? 0) + parts[0].length + 1, // +1 for the dot - })); - } - return []; - } - - verifyWord(word: string): string[] { - const parts = this.getParts(word); - const inputErrors = this.inputWord.verifyWord(parts[0]); - if (inputErrors.length > 0) { - return inputErrors; - } - if (parts[1] === undefined) { - return ["Expected input and label separated by a dot"]; - } - const labelErrors = this.labelWord.verifyWord(parts[1]); - return [...inputErrors, ...labelErrors]; - } - - replaceWord(text: string, replacement: ReplacementData) { - const [input, label] = this.getParts(text); - if (replacement.type == "Input" && input === replacement.old) { - return replacement.replacement + (label ? "." + label : ""); - } else if (replacement.type == "Label" && label === replacement.old) { - return input + "." + replacement.replacement; - } - return text; - } - - private getParts(text: string): [string, string] | [string, undefined] { - if (text.includes(".")) { - const index = text.indexOf("."); - const input = text.substring(0, index); - const label = text.substring(index + 1); - return [input, label]; - } - return [text, undefined]; - } -} diff --git a/frontend/webEditor/src/features/dfdElements/behaviorRefactorer.ts b/frontend/webEditor/src/features/dfdElements/behaviorRefactorer.ts deleted file mode 100644 index 049e3719..00000000 --- a/frontend/webEditor/src/features/dfdElements/behaviorRefactorer.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { inject, injectable } from "inversify"; -import { LabelType, LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { - Command, - CommandExecutionContext, - CommandReturn, - ICommandStack, - ILogger, - ModelSource, - SEdgeImpl, - SLabelImpl, - SModelElementImpl, - SParentElementImpl, - TYPES, -} from "sprotty"; -import { DfdInputPortImpl, DfdOutputPortImpl } from "./ports"; -import { ApplyLabelEditAction } from "sprotty-protocol"; -import { DfdNodeImpl } from "./nodes"; -import { ReplaceAutoCompleteTree, TreeBuilder } from "./AssignmentLanguage"; - -interface LabelChange { - oldLabel: string; - newLabel: string; -} - -/** - * This class listens to changes in the label type registry and updates the behavior of the DFD elements accordingly. - * When a label type/value is renamed, the behavior of the DFD elements is updated to reflect the new name. - * Also provides a method to refactor the behavior of a DFD element when the name of an input is changed. - */ -@injectable() -export class DFDBehaviorRefactorer { - private previousLabelTypes: LabelType[] = []; - - constructor( - @inject(LabelTypeRegistry) private readonly registry: LabelTypeRegistry, - @inject(TYPES.ILogger) private readonly logger: ILogger, - @inject(TYPES.ICommandStack) private readonly commandStack: ICommandStack, - ) { - if (this.registry) { - this.previousLabelTypes = structuredClone(this.registry.getLabelTypes()); - this.registry?.onUpdate(() => { - this.handleLabelUpdate().catch((error) => - this.logger.error(this, "Error while processing label type registry update", error), - ); - }); - } - } - - private async handleLabelUpdate(): Promise { - this.logger.log(this, "Handling label type registry update"); - const currentLabelTypes = this.registry.getLabelTypes() ?? []; - - const changedLabels: LabelChange[] = []; - for (const newLabel of currentLabelTypes) { - const oldLabel = this.previousLabelTypes.find((label) => label.id === newLabel.id); - if (!oldLabel) { - continue; - } - if (oldLabel.name !== newLabel.name) { - for (const newValue of newLabel.values) { - const oldValue = oldLabel.values.find((value) => value.id === newValue.id); - if (!oldValue) { - continue; - } - changedLabels.push({ - oldLabel: `${oldLabel.name}.${oldValue.text}`, - newLabel: `${newLabel.name}.${newValue.text}`, - }); - } - } - for (const newValue of newLabel.values) { - const oldValue = oldLabel.values.find((value) => value.id === newValue.id); - if (!oldValue) { - continue; - } - if (oldValue.text !== newValue.text) { - changedLabels.push({ - oldLabel: `${newLabel.name}.${oldValue.text}`, - newLabel: `${newLabel.name}.${newValue.text}`, - }); - } - } - } - - this.logger.log(this, "Changed labels", changedLabels); - - const model = await this.commandStack.executeAll([]); - this.traverseDfdOutputPorts(model, (port) => { - const tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(this.registry, port)); - this.renameLabelsForPort(port, changedLabels, tree); - }); - - this.previousLabelTypes = structuredClone(currentLabelTypes); - } - - private renameLabelsForPort(port: DfdOutputPortImpl, labelChanges: LabelChange[], tree: ReplaceAutoCompleteTree) { - let lines = port.getBehavior().split(/\n/); - for (const change of labelChanges) { - lines = tree.replace(lines, { old: change.oldLabel, replacement: change.newLabel, type: "Label" }); - } - port.setBehavior(lines.join("\n")); - } - - private traverseDfdOutputPorts(element: SModelElementImpl, cb: (port: DfdOutputPortImpl) => void) { - if (element instanceof DfdOutputPortImpl) { - cb(element); - } - - if (element instanceof SParentElementImpl) { - element.children.forEach((child) => this.traverseDfdOutputPorts(child, cb)); - } - } - - processInputLabelRename( - label: SLabelImpl, - port: DfdInputPortImpl, - oldLabelText: string, - newLabelText: string, - ): Map { - label.text = oldLabelText; - const oldInputName = port.getName(); - label.text = newLabelText; - const newInputName = port.getName(); - - const behaviorChanges: Map = new Map(); - const node = port.parent; - if (!(node instanceof DfdNodeImpl) || !oldInputName || !newInputName) { - return behaviorChanges; - } - - const tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(this.registry, port)); - - node.children.forEach((child) => { - if (!(child instanceof DfdOutputPortImpl)) { - return; - } - - behaviorChanges.set(child.id, this.processInputRenameForPort(child, oldInputName, newInputName, tree)); - }); - - return behaviorChanges; - } - - private processInputRenameForPort( - port: DfdOutputPortImpl, - oldInputName: string, - newInputName: string, - tree: ReplaceAutoCompleteTree, - ): string { - const lines = port.getBehavior().split("\n"); - const newLines = tree.replace(lines, { old: oldInputName, replacement: newInputName, type: "Input" }); - return newLines.join("\n"); - } -} - -/** - * A command that refactors the behavior of DFD output ports when the name of an input is changed. - * Designed to be added as a command handler for the ApplyLabelEditAction to automatically - * detect all edit of labels on a edge element. - * When a label is changed, the old and new input name of the dfd input port that the edge - * is pointing to is used to update the behavior of all dfd output ports that are connected to the same node. - */ -export class RefactorInputNameInDFDBehaviorCommand extends Command { - static readonly KIND = ApplyLabelEditAction.KIND; - - constructor( - @inject(TYPES.Action) protected readonly action: ApplyLabelEditAction, - @inject(TYPES.ModelSource) protected readonly modelSource: ModelSource, - @inject(DFDBehaviorRefactorer) protected readonly refactorer: DFDBehaviorRefactorer, - ) { - super(); - } - - private oldBehaviors: Map = new Map(); - private newBehaviors: Map = new Map(); - - execute(context: CommandExecutionContext): CommandReturn { - // This command will be executed after the ApplyLabelEditCommand. - // Therefore the label will already be changed in the model. - // To get the old value we get the label from the model source, - // which still has the old value because the model commit will be done after this command. - const modelBeforeChange = context.modelFactory.createRoot(this.modelSource.model); - const labelBeforeChange = modelBeforeChange.index.getById(this.action.labelId); - if (!(labelBeforeChange instanceof SLabelImpl)) { - // should not happen - return context.root; - } - - const oldInputName = labelBeforeChange.text; - const newInputName = this.action.text; - const edge = labelBeforeChange.parent; - if (!(edge instanceof SEdgeImpl)) { - // should not happen - return context.root; - } - - const port = edge.target; - if (!(port instanceof DfdInputPortImpl)) { - // Edge does not point to a dfd port, but maybe some node directly. - // Cannot be used in behaviors in this case so we don't need to refactor anything. - return context.root; - } - - const behaviorChanges: Map = this.refactorer.processInputLabelRename( - labelBeforeChange, - port, - oldInputName, - newInputName, - ); - behaviorChanges.forEach((updatedBehavior, id) => { - const port = context.root.index.getById(id); - if (port instanceof DfdOutputPortImpl) { - this.oldBehaviors.set(id, port.getBehavior()); - this.newBehaviors.set(id, updatedBehavior); - port.setBehavior(updatedBehavior); - } - }); - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - this.oldBehaviors.forEach((oldBehavior, id) => { - const port = context.root.index.getById(id); - if (port instanceof DfdOutputPortImpl) { - port.setBehavior(oldBehavior); - } - }); - - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - this.newBehaviors.forEach((newBehavior, id) => { - const port = context.root.index.getById(id); - if (port instanceof DfdOutputPortImpl) { - port.setBehavior(newBehavior); - } - }); - - return context.root; - } -} diff --git a/frontend/webEditor/src/features/dfdElements/di.config.ts b/frontend/webEditor/src/features/dfdElements/di.config.ts deleted file mode 100644 index 86ef126a..00000000 --- a/frontend/webEditor/src/features/dfdElements/di.config.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ContainerModule } from "inversify"; -import { - SGraphImpl, - SGraphView, - SLabelImpl, - configureModelElement, - editLabelFeature, - withEditLabelFeature, - SLabelView, - SRoutingHandleImpl, - TYPES, - configureCommand, -} from "sprotty"; -import { FunctionNodeImpl, FunctionNodeView, IONodeImpl, IONodeView, StorageNodeImpl, StorageNodeView } from "./nodes"; -import { ArrowEdgeImpl, ArrowEdgeView, CustomRoutingHandleView } from "./edges"; -import { DfdInputPortImpl, DfdInputPortView, DfdOutputPortImpl, DfdOutputPortView } from "./ports"; -import { FilledBackgroundLabelView, DfdPositionalLabelView } from "./labels"; -import { AlwaysSnapPortsMoveMouseListener, ReSnapPortsAfterLabelChangeCommand, PortAwareSnapper } from "./portSnapper"; -import { OutputPortEditUIMouseListener, OutputPortEditUI, SetDfdOutputPortBehaviorCommand } from "./outputPortEditUi"; -import { DfdEditLabelValidator, DfdEditLabelValidatorDecorator } from "./editLabelValidator"; -import { DfdNodeAnnotationUI, DfdNodeAnnotationUIMouseListener } from "./nodeAnnotationUi"; -import { DFDBehaviorRefactorer, RefactorInputNameInDFDBehaviorCommand } from "./behaviorRefactorer"; - -import "./elementStyles.css"; -import { SWITCHABLE } from "../settingsMenu/themeManager"; - -export const dfdElementsModule = new ContainerModule((bind, unbind, isBound, rebind) => { - const context = { bind, unbind, isBound, rebind }; - - rebind(TYPES.ISnapper).to(PortAwareSnapper).inSingletonScope(); - bind(TYPES.MouseListener).to(AlwaysSnapPortsMoveMouseListener).inSingletonScope(); - configureCommand(context, ReSnapPortsAfterLabelChangeCommand); - - bind(OutputPortEditUI).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(OutputPortEditUI); - bind(SWITCHABLE).toService(OutputPortEditUI); - - bind(TYPES.MouseListener).to(OutputPortEditUIMouseListener).inSingletonScope(); - configureCommand(context, SetDfdOutputPortBehaviorCommand); - - bind(TYPES.IEditLabelValidator).to(DfdEditLabelValidator).inSingletonScope(); - bind(TYPES.IEditLabelValidationDecorator).to(DfdEditLabelValidatorDecorator).inSingletonScope(); - - bind(DfdNodeAnnotationUIMouseListener).toSelf().inSingletonScope(); - bind(TYPES.MouseListener).toService(DfdNodeAnnotationUIMouseListener); - bind(TYPES.IUIExtension).to(DfdNodeAnnotationUI).inSingletonScope(); - - bind(DFDBehaviorRefactorer).toSelf().inSingletonScope(); - configureCommand(context, RefactorInputNameInDFDBehaviorCommand); - - configureModelElement(context, "graph", SGraphImpl, SGraphView); - configureModelElement(context, "node:storage", StorageNodeImpl, StorageNodeView); - configureModelElement(context, "node:function", FunctionNodeImpl, FunctionNodeView); - configureModelElement(context, "node:input-output", IONodeImpl, IONodeView); - configureModelElement(context, "edge:arrow", ArrowEdgeImpl, ArrowEdgeView, { - enable: [withEditLabelFeature], - }); - configureModelElement(context, "label", SLabelImpl, SLabelView, { - enable: [editLabelFeature], - }); - configureModelElement(context, "label:filled-background", SLabelImpl, FilledBackgroundLabelView, { - enable: [editLabelFeature], - }); - configureModelElement(context, "label:positional", SLabelImpl, DfdPositionalLabelView, { - enable: [editLabelFeature], - }); - configureModelElement(context, "port:dfd-input", DfdInputPortImpl, DfdInputPortView); - configureModelElement(context, "port:dfd-output", DfdOutputPortImpl, DfdOutputPortView); - configureModelElement(context, "routing-point", SRoutingHandleImpl, CustomRoutingHandleView); - configureModelElement(context, "volatile-routing-point", SRoutingHandleImpl, CustomRoutingHandleView); -}); diff --git a/frontend/webEditor/src/features/dfdElements/dynamicChildren.ts b/frontend/webEditor/src/features/dfdElements/dynamicChildren.ts deleted file mode 100644 index 0752b8a3..00000000 --- a/frontend/webEditor/src/features/dfdElements/dynamicChildren.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { injectable, multiInject } from "inversify"; -import { SModelElementRegistration, SNodeImpl, SEdgeImpl, TYPES } from "sprotty"; -import { SModelElement, SEdge, SNode } from "sprotty-protocol"; - -// This file contains helpers to dynamically specify the children of a sprotty element. -// Element children are generally used for e.g. labels in sprotty or other sub elements. -// You could embed everything into one element but it is often easier to use children. -// E.g. for editable labels you would need to implement a custom label edit ui which is pretty complicated. - -// Normally, the children of a sprotty element are specified in the model. -// However this means that the children are saved together with the model. -// Imagine you want to change the children of a node at some point, e.g to add another text label, -// move it slightly or align the text differently. -// When you save the children you would need to migrate the previously saved models -// to the new children. - -// This is undesirable as the display of a node should not be hardcoded in the serialized model. -// To circumvent this, these helper classes were developed. -// The model is saved without children and the children are added dynamically by the runtime -// by each model element using the setChildren method. -// This sets the `children` array of the element and also loads data from the parent element into the children -// (e.g. texts for labels). -// When the model is saved, the removeChildren method is called to remove the children again. -// This method also needs to save the data of the children in the parent element, so it is properly saved. -// This ensures that the display of a node is a implementation detail and not encoded in the saved models. - -// Abstract classes that define the both abstract methods setChildren and removeChildren - -export abstract class DynamicChildrenNode extends SNodeImpl { - abstract setChildren(schema: SNode): void; - abstract removeChildren(schema: SNode): void; -} - -export abstract class DynamicChildrenEdge extends SEdgeImpl { - abstract setChildren(schema: SEdge): void; - abstract removeChildren(schema: SEdge): void; -} - -@injectable() -export class DynamicChildrenProcessor { - @multiInject(TYPES.SModelElementRegistration) - private readonly elementRegistrations: SModelElementRegistration[] = []; - - /** - * Recursively either adds or removes the children of a model graph. - * Recursively traverses the graph, gets the registration of the corresponding element type, - * checks whether it extends a DynamicChildren* abstract class and then calls the corresponding method. - */ - public processGraphChildren(graphElement: SModelElement | SEdge, action: "set" | "remove"): void { - // When removing children we need to remove them from children to parents to do it correctly. - // When setting children we need to do it the other way around to set the children - // of the elements that have been set by the parent first. - if (action === "remove") { - graphElement.children?.forEach((child) => this.processGraphChildren(child, action)); - } - - const registration = this.elementRegistrations.find((r) => r.type === graphElement.type); - if (registration) { - // If registration is undefined some element hasn't been registered but used, so this shouldn't happen - // if the model is valid. - - // Create a instance of the element. - // Ideally the *Children methods should be static, but static methods can't be abstract. - // So we need to create a instance we can then call the method on. - const impl = new registration.constr(); - if (impl instanceof DynamicChildrenNode) { - if (action === "set") { - impl.setChildren(graphElement); - } else { - impl.removeChildren(graphElement); - } - } - - // sourceId is only present in edges and ensures that the graphElement is an edge (to calm the type system) - if (impl instanceof DynamicChildrenEdge && "sourceId" in graphElement) { - if (action === "set") { - impl.setChildren(graphElement); - } else { - impl.removeChildren(graphElement); - } - } - } - - if (action === "set") { - graphElement.children?.forEach((child) => this.processGraphChildren(child, action)); - } - } -} diff --git a/frontend/webEditor/src/features/dfdElements/edges.tsx b/frontend/webEditor/src/features/dfdElements/edges.tsx deleted file mode 100644 index 5e363601..00000000 --- a/frontend/webEditor/src/features/dfdElements/edges.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/** @jsx svg */ -import { inject, injectable } from "inversify"; -import { - PolylineEdgeViewWithGapsOnIntersections, - SEdgeImpl, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - svg, - RenderingContext, - IViewArgs, - WithEditableLabel, - isEditableLabel, - SRoutingHandleView, -} from "sprotty"; -import { VNode } from "snabbdom"; -import { Point, angleOfPoint, toDegrees, SEdge, SLabel } from "sprotty-protocol"; -import { DynamicChildrenEdge } from "./dynamicChildren"; -import { SettingsManager } from "../settingsMenu/SettingsManager"; - -export interface ArrowEdge extends SEdge { - text?: string; -} - -export class ArrowEdgeImpl extends DynamicChildrenEdge implements WithEditableLabel { - text?: string; - - setChildren(schema: ArrowEdge): void { - schema.children = [ - { - type: "label:filled-background", - text: schema.text ?? "", - id: schema.id + "-label", - edgePlacement: { - position: 0.5, - side: "on", - rotate: false, - }, - } as SLabel, - ]; - } - - removeChildren(schema: ArrowEdge): void { - const label = schema.children?.find((element) => element.type.startsWith("label")) as SLabel | undefined; - schema.text = label?.text ?? ""; - schema.children = []; - } - - get editableLabel() { - const label = this.children.find((element) => element.type.startsWith("label")); - if (label && isEditableLabel(label)) { - return label; - } - - return undefined; - } -} - -@injectable() -export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { - constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) { - super(); - } - - override render(edge: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { - // In the default implementation children of the edge are always rendered, because they - // may be visible when the rest of the edge is not. - // We only have the edge label as an children which only must be rendered when the rest of the edge is visible. - // So as an optimization for big diagrams we don't render the label when the rest of the edge is not visible either. - // Otherwise all these labels would be added to the DOM, making it slow.. - const route = this.edgeRouterRegistry.route(edge, args); - if (!this.isVisible(edge, route, context)) { - return undefined; - } - - return this.superRender(edge, context, args); - } - - superRender(edge: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { - const route = this.edgeRouterRegistry.route(edge, args); - if (route.length === 0) { - return this.renderDanglingEdge("Cannot compute route", edge, context); - } - if (!this.isVisible(edge, route, context)) { - if (edge.children.length === 0) { - return undefined; - } - // The children of an edge are not necessarily inside the bounding box of the route, - // so we need to render a group to ensure the children have a chance to be rendered. - return {this.settings.hideEdgeLabels ? [] : context.renderChildren(edge, { route })}; - } - - return ( - - {this.renderLine(edge, route)} - {this.renderAdditionals(edge, route, context)} - {this.renderJunctionPoints(edge, route, context, args)} - {this.settings.hideEdgeLabels ? [] : context.renderChildren(edge, { route })} - - ); - } - - /** - * Renders an arrow at the end of the edge. - */ - protected override renderAdditionals(edge: SEdgeImpl, segments: Point[], context: RenderingContext): VNode[] { - const additionals = super.renderAdditionals(edge, segments, context); - const p1 = segments[segments.length - 2]; - const p2 = segments[segments.length - 1]; - const arrow = ( - - ); - additionals.push(arrow); - return additionals; - } - - /** - * Renders the edge line. - * In contrast to the default implementation that we override here, - * this implementation makes the edge line 10px shorter at the end to make space for the arrow without any overlap. - */ - protected renderLine(edge: SEdgeImpl, segments: Point[]): VNode { - const firstPoint = segments[0]; - let path = `M ${firstPoint.x},${firstPoint.y}`; - for (let i = 1; i < segments.length; i++) { - const p = segments[i]; - if (i === segments.length - 1) { - // Make edge line 9.5px shorter to make space for the arrow - // The arrow is 10px long, but we only shorten by 9.5 px to have overlap at the edge between line and arrow. - // Otherwise edges would be exactly next to each other which would result in a small gap and flickering if you zoom in enough. - const prevP = segments[i - 1]; - const dx = p.x - prevP.x; - const dy = p.y - prevP.y; - const length = Math.sqrt(dx * dx + dy * dy); - const ratio = (length - 9.5) / length; - path += ` L ${prevP.x + dx * ratio},${prevP.y + dy * ratio}`; - } else { - // Lines between points in between are not shortened - path += ` L ${p.x},${p.y}`; - } - } - return ( - - {/* This is the actual path being rendered */} - - {/* This is a transparent path that is rendered on top of the actual path to make it easier to select the edge */} - - - ); - } -} - -/** - * Smaller version of the default edge routing handle. - */ -@injectable() -export class CustomRoutingHandleView extends SRoutingHandleView { - getRadius(): number { - return 5; - } -} diff --git a/frontend/webEditor/src/features/dfdElements/editLabelValidator.css b/frontend/webEditor/src/features/dfdElements/editLabelValidator.css deleted file mode 100644 index 63d28b91..00000000 --- a/frontend/webEditor/src/features/dfdElements/editLabelValidator.css +++ /dev/null @@ -1,26 +0,0 @@ -/* Label edit UI validation results styling */ - -.label-edit .label-validation-results { - position: absolute; - /* position top is set dynamically to be under the input field */ - - background-color: var(--color-primary); - padding: 8px; - border-radius: 5px; -} - -.label-edit .label-validation-results::before { - width: 16px; - height: 16px; - background-size: 16px 16px; - margin-right: 4px; /* space between the icon and the text */ - - content: ""; - display: inline-block; - vertical-align: middle; - - /* Uses the font awesome exclamation circle as a shape/mask and fills it with the error color */ - background-color: var(--color-error); - -webkit-mask: url("@fortawesome/fontawesome-free/svgs/solid/circle-exclamation.svg"); - mask: url("@fortawesome/fontawesome-free/svgs/solid/circle-exclamation.svg"); -} diff --git a/frontend/webEditor/src/features/dfdElements/editLabelValidator.ts b/frontend/webEditor/src/features/dfdElements/editLabelValidator.ts deleted file mode 100644 index 2ef9dece..00000000 --- a/frontend/webEditor/src/features/dfdElements/editLabelValidator.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { injectable } from "inversify"; -import { - EditLabelValidationResult, - EditableLabel, - IEditLabelValidationDecorator, - IEditLabelValidator, - SChildElementImpl, - SEdgeImpl, - SModelElementImpl, -} from "sprotty"; -import { DfdInputPortImpl } from "./ports"; -import { DfdNodeImpl } from "./nodes"; - -import "./editLabelValidator.css"; - -/** - * Validator for the label of an dfd edge. - * Ensures that the label of an dfd edge is unique within the node that the edge is connected to. - * Does not do any validation if the label is not a child of an dfd edge. - */ -@injectable() -export class DfdEditLabelValidator implements IEditLabelValidator { - async validate(value: string, label: EditableLabel & SModelElementImpl): Promise { - // Check whether we have an dfd edge label and a non-empty label value - if (!(label instanceof SChildElementImpl)) { - return { severity: "ok" }; - } - - const labelParent = label.parent; - if (!(labelParent instanceof SEdgeImpl)) { - return { severity: "ok" }; - } - - // Labels on edges are not allowed to have spaces in them - if (value.includes(" ")) { - return { severity: "error", message: "Input name cannot contain spaces" }; - } - - // Labels on edges are not allowed to commas in them - if (value.includes(",")) { - return { severity: "error", message: "Input name cannot contain commas" }; - } - - // Labels on edges are not allowed to be empty - if (value.length == 0) { - return { severity: "error", message: "Input name cannot be empty" }; - } - - // Get node and edge names that are in use - const edge = labelParent; - const edgeTarget = edge.target; - if (!(edgeTarget instanceof DfdInputPortImpl)) { - return { severity: "ok" }; - } - - const inputPort = edgeTarget; - const node = inputPort.parent as DfdNodeImpl; - const usedEdgeNames = node.getEdgeTexts((e) => e.id !== edge.id); // filter out the edge we are currently editing - - // Check whether the label value is already used (case insensitive) - if (usedEdgeNames.find((name) => name.toLowerCase() === value.toLowerCase())) { - return { severity: "error", message: "Input name already used" }; - } - - return { severity: "ok" }; - } -} - -/** - * Renders the validation result of an dfd edge label to the label edit ui. - */ -@injectable() -export class DfdEditLabelValidatorDecorator implements IEditLabelValidationDecorator { - private readonly cssClass = "label-validation-results"; - - decorate(input: HTMLInputElement | HTMLTextAreaElement, validationResult: EditLabelValidationResult): void { - const containerElement = input.parentElement; - if (!containerElement) { - return; - } - - // Only display something when there is a validation error or warning - if (validationResult.severity !== "ok") { - const span = document.createElement("span"); - span.innerText = validationResult.message ?? validationResult.severity; - span.classList.add(this.cssClass); - - // Place validation notice right under the input field - span.style.top = `${input.clientHeight}px`; - // Rest is styled in the corresponding css file, as it is not dynamic - - containerElement.appendChild(span); - } - } - - dispose(input: HTMLInputElement | HTMLTextAreaElement): void { - const containerElement = input.parentElement; - if (containerElement) { - containerElement.querySelector(`span.${this.cssClass}`)?.remove(); - } - } -} diff --git a/frontend/webEditor/src/features/dfdElements/elementStyles.css b/frontend/webEditor/src/features/dfdElements/elementStyles.css deleted file mode 100644 index 91c2d742..00000000 --- a/frontend/webEditor/src/features/dfdElements/elementStyles.css +++ /dev/null @@ -1,117 +0,0 @@ -/* This file contains styling for the node views defined in nodes.tsx, edge.tsx and ports.tsx */ - -/* sprotty-* classes are automatically added by sprotty and the other ones - are added in the definition inside nodes.tsx, edge.tsx and ports.tsx */ - -/* Nodes */ - -.sprotty-node rect, -.sprotty-node line, -.sprotty-node circle { - /* stroke color defaults to be the foreground color of the theme. - Alternatively it can be overwritten by setting the --color variable - As a inline style attribute for the specific node. - Used as a highlighter to mark nodes with errors. - This is essentially a "optional parameter" to this css rule. - See https://stackoverflow.com/questions/17893823/how-to-pass-parameters-to-css-classes */ - stroke: var(--color-foreground); - stroke-width: 1; - /* Background fill of the node. - When --color is unset this is just --color-primary. - If this node is annotated and --color is set, it will be included in the color mix. */ - fill: color-mix(in srgb, var(--color-primary), var(--color, transparent) 40%); -} - -.sprotty-node .node-label text { - font-size: 5pt; -} - -.sprotty-node .node-label rect, -.sprotty-node .node-label .label-delete circle { - fill: var(--color-primary); - stroke: var(--color-foreground); - stroke-width: 0.5; -} - -.sprotty-node .node-label .label-delete text { - fill: var(--color-foreground); - font-size: 5px; -} - -/* Edges */ - -.sprotty-edge { - stroke: var(--color-foreground); - fill: none; - stroke-width: 1; -} - -/* On top of the actual edge path we draw a transparent path with a larger stroke width. - This makes it easier to select the edge with the mouse. */ -.sprotty-edge path.select-path { - stroke: transparent; - /* make the "invisible hitbox" 8 pixels wide. This is the same width as the arrow head */ - stroke-width: 8; -} - -.sprotty-edge .arrow { - fill: var(--color-foreground); - stroke: none; -} - -.sprotty-edge > .sprotty-routing-handle { - fill: var(--color-foreground); - stroke: none; -} - -.sprotty-edge .label-background rect { - fill: var(--color-background); - stroke-width: 0; -} - -/* Ports */ - -.sprotty-port rect { - stroke: var(--port-border, var(--color-foreground)); - fill: color-mix(in srgb, var(--port-color, var(--color-primary)), var(--color-background) 25%); - stroke-width: 0.5; -} - -.sprotty-port .port-text { - font-size: 4pt; -} - -/* All nodes/misc */ - -.sprotty-node.selected circle, -.sprotty-node.selected rect, -.sprotty-node.selected line, -.sprotty-edge.selected { - stroke-width: 2; -} - -.sprotty-port.selected rect { - stroke-width: 1; -} - -text { - stroke-width: 0; - fill: var(--color-foreground); - font-family: "Arial", sans-serif; - font-size: 11pt; - text-anchor: middle; - dominant-baseline: central; - - -webkit-user-select: none; - user-select: none; -} - -/* elements with the sprotty-missing class use a node type that has not been registered. - Because of this sprotty does not know what to do with them and renders their content and specifies them as missing. - To make these errors very visible we make them red here. - Ideally a user should never see this. */ -.sprotty-missing { - stroke-width: 1; - stroke: var(--color-error); - fill: var(--color-error); -} diff --git a/frontend/webEditor/src/features/dfdElements/labels.tsx b/frontend/webEditor/src/features/dfdElements/labels.tsx deleted file mode 100644 index f7dec3ca..00000000 --- a/frontend/webEditor/src/features/dfdElements/labels.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/** @jsx svg */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { IViewArgs, SLabelImpl, SNodeImpl, ShapeView, RenderingContext, svg } from "sprotty"; -import { VNode } from "snabbdom"; -import { injectable } from "inversify"; -import { Point } from "sprotty-protocol"; -import { calculateTextSize } from "../../utils"; - -export interface DfdPositionalLabelArgs extends IViewArgs { - xPosition: number; - yPosition: number; -} - -@injectable() -export class DfdPositionalLabelView extends ShapeView { - private getPosition(label: Readonly, args?: DfdPositionalLabelArgs | IViewArgs): Point { - if (args && "xPosition" in args && "yPosition" in args) { - return { x: args.xPosition, y: args.yPosition }; - } else { - const parentSize = (label.parent as SNodeImpl | undefined)?.bounds; - const width = parentSize?.width ?? 0; - const height = parentSize?.height ?? 0; - return { x: width / 2, y: height / 2 }; - } - } - - render(label: Readonly, _context: RenderingContext, args?: DfdPositionalLabelArgs): VNode | undefined { - const position = this.getPosition(label, args); - - return ( - - {label.text} - - ); - } -} - -/** - * A sprotty label view that renders the label text with a filled background behind it. - * This is used to make the element behind the label invisible. - */ -@injectable() -export class FilledBackgroundLabelView extends ShapeView { - static readonly PADDING = 5; - - render(label: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(label, context)) { - return undefined; - } - - const size = calculateTextSize(label.text); - const width = size.width + FilledBackgroundLabelView.PADDING; - const height = size.height + FilledBackgroundLabelView.PADDING; - - return ( - - {label.text ? : undefined} - {label.text} - - ); - } -} diff --git a/frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.css b/frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.css deleted file mode 100644 index 393960a7..00000000 --- a/frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.css +++ /dev/null @@ -1,14 +0,0 @@ -.dfd-node-annotation-ui { - /* don't break lines into multiple when they don't fit on screen. - Just let the popup clip outside the screen when there is not enough space and let the - user move the popup at a position where the text is fully visible */ - white-space: nowrap; -} - -.dfd-node-annotation-ui p { - margin: 12px; -} - -.dfd-node-annotation-ui i.fa { - margin-right: 5px; -} diff --git a/frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.ts b/frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.ts deleted file mode 100644 index b8bc0976..00000000 --- a/frontend/webEditor/src/features/dfdElements/nodeAnnotationUi.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { inject, injectable } from "inversify"; -import { - AbstractUIExtension, - IActionDispatcher, - MouseListener, - SChildElementImpl, - SModelElementImpl, - SModelRootImpl, - SetUIExtensionVisibilityAction, - TYPES, -} from "sprotty"; -import { Action } from "sprotty-protocol"; -import { DfdNodeImpl } from "./nodes"; - -import "@fortawesome/fontawesome-free/css/all.min.css"; -import "./nodeAnnotationUi.css"; -import { SettingsManager } from "../settingsMenu/SettingsManager"; -import { Mode } from "../settingsMenu/annotationManager"; - -export class DfdNodeAnnotationUIMouseListener extends MouseListener { - private stillTimeout: number | undefined; - private lastPosition = { x: 0, y: 0 }; - - constructor(@inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher) { - super(); - } - - mouseMove(target: SModelElementImpl, event: MouseEvent): Action[] { - const dfdNode = this.findDfdNode(target); - if (!dfdNode) { - if (this.stillTimeout) { - clearTimeout(this.stillTimeout); - this.stillTimeout = undefined; - } - return []; - } - - if (this.lastPosition.x !== event.clientX || this.lastPosition.y !== event.clientY) { - this.lastPosition = { x: event.clientX, y: event.clientY }; - // Mouse has moved, so we reset the timeout - if (this.stillTimeout) { - clearTimeout(this.stillTimeout); - } - this.stillTimeout = setTimeout(() => { - // When the mouse has not moved for 500ms, we show the popup - this.stillTimeout = undefined; - - if (dfdNode.opacity !== 1) { - // Only show when opacity is 1. - // The opacity is not 1 when the node is currently being created but has not been - // placed yet. - // In this case we don't want to show the popup - // and interfere with the creation process. - return; - } - - this.showPopup(dfdNode); - }, 500); - } - - return []; - } - - private findDfdNode(currentNode: SModelElementImpl): DfdNodeImpl | undefined { - if (currentNode instanceof DfdNodeImpl) { - return currentNode; - } else if (currentNode instanceof SChildElementImpl && currentNode.parent) { - return this.findDfdNode(currentNode.parent); - } else { - return undefined; - } - } - - private showPopup(target: DfdNodeImpl): void { - if (!target.annotations) { - // no annotation. No need to show the popup. - return; - } - - this.actionDispatcher.dispatch( - SetUIExtensionVisibilityAction.create({ - extensionId: DfdNodeAnnotationUI.ID, - visible: true, - contextElementsId: [target.id], - }), - ); - } - - public getMousePosition(): { x: number; y: number } { - return this.lastPosition; - } -} - -@injectable() -export class DfdNodeAnnotationUI extends AbstractUIExtension { - static readonly ID = "dfd-node-annotation-ui"; - - private readonly annotationParagraph = document.createElement("p") as HTMLParagraphElement; - - constructor( - @inject(DfdNodeAnnotationUIMouseListener) - private readonly mouseListener: DfdNodeAnnotationUIMouseListener, - @inject(SettingsManager) private settings: SettingsManager, - ) { - super(); - } - - id(): string { - return DfdNodeAnnotationUI.ID; - } - - containerClass(): string { - return this.id(); - } - - protected override initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.appendChild(this.annotationParagraph); - - document.addEventListener("mousemove", (event) => { - if (containerElement.style.visibility === "hidden") { - // Not visible anyway, no need to do the check - return; - } - - // If mouse not in popup => hide - const rect = containerElement.getBoundingClientRect(); - if ( - event.clientX < rect.left || - event.clientX > rect.right || - event.clientY < rect.top || - event.clientY > rect.bottom - ) { - this.hide(); - } - }); - } - - protected override onBeforeShow( - containerElement: HTMLElement, - root: Readonly, - ...contextElementIds: string[] - ): void { - if (contextElementIds.length !== 1) { - this.annotationParagraph.innerText = - "UI Error: Expected exactly one context element id, but got " + contextElementIds.length; - return; - } - - const node = root.index.getById(contextElementIds[0]); - if (!(node instanceof DfdNodeImpl)) { - this.annotationParagraph.innerText = - "UI Error: Expected context element to be a DfdNodeImpl, but got " + node; - return; - } - - // Clear previous content - this.annotationParagraph.innerText = ""; - - // Set position - // 2 offset to ensure the mouse is inside the popup when showing it. - // Otherwise it would be on the node instead of the popup because of the rounded corners. - // When moving the cursor from the node to the popup, the popup would move a bit - // because the cursor is going a bit over the model and then the popup would re-show - // with the new position after the timeout. - const mousePosition = this.mouseListener.getMousePosition(); - const annotationPosition = { - x: mousePosition.x - 2, - y: mousePosition.y - 2, - }; - containerElement.style.left = `${annotationPosition.x}px`; - containerElement.style.top = `${annotationPosition.y}px`; - - // Set tooltip size and scroll to prevent them from growing out of the screen - containerElement.style.overflowY = "auto"; - this.annotationParagraph.style.whiteSpace = "normal"; - this.annotationParagraph.style.wordBreak = "break-word"; - const screenWidth = window.innerWidth; - const screenHeight = window.innerHeight; - containerElement.style.maxWidth = `${Math.max(screenWidth - annotationPosition.x - 50, 100)}px`; - containerElement.style.maxHeight = `${Math.max(screenHeight - annotationPosition.y - 50, 50)}px`; - - // Set content - if (!node.annotations || node.annotations.length == 0) { - this.annotationParagraph.innerText = "No errors"; - return; - } - - this.annotationParagraph.innerHTML = ""; - - const mode = this.settings.getCurrentLabelMode(); - - node.annotations.forEach((a) => { - if ( - ((mode === Mode.INCOMING || mode === Mode.ALL) && a.message.trim().startsWith("Incoming")) || - ((mode === Mode.OUTGOING || mode === Mode.ALL) && a.message.trim().startsWith("Propagated")) || - a.message.startsWith("Constraint") - ) { - const line = document.createElement("div"); - line.style.display = "flex"; - line.style.alignItems = "center"; - line.style.gap = "6px"; // some spacing between icon and text - - if (a.icon) { - const iconI = document.createElement("i"); - iconI.classList.add("fa", `fa-${a.icon}`); - line.appendChild(iconI); - } - - const textSpan = document.createElement("span"); - textSpan.innerText = a.message; - line.appendChild(textSpan); - - this.annotationParagraph.appendChild(line); - } - }); - } -} diff --git a/frontend/webEditor/src/features/dfdElements/nodes.tsx b/frontend/webEditor/src/features/dfdElements/nodes.tsx deleted file mode 100644 index 8bae434e..00000000 --- a/frontend/webEditor/src/features/dfdElements/nodes.tsx +++ /dev/null @@ -1,315 +0,0 @@ -/** @jsx svg */ -import { - SNodeImpl, - WithEditableLabel, - isEditableLabel, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - svg, - withEditLabelFeature, - RenderingContext, - ShapeView, -} from "sprotty"; -import { SNode, SLabel, Bounds, SModelElement, SPort } from "sprotty-protocol"; -import { inject, injectable, optional } from "inversify"; -import { VNode, VNodeStyle } from "snabbdom"; -import { LabelAssignment } from "../labels/labelTypeRegistry"; -import { DynamicChildrenNode } from "./dynamicChildren"; -import { containsDfdLabelFeature } from "../labels/elementFeature"; -import { calculateTextSize } from "../../utils"; -import { DfdNodeLabelRenderer } from "../labels/labelRenderer"; -import { DfdPositionalLabelArgs } from "./labels"; -import { DfdInputPortImpl } from "./ports"; -import { ArrowEdgeImpl } from "./edges"; - -export interface DfdNode extends SNode { - text: string; - labels: LabelAssignment[]; - ports: SPort[]; - annotations?: DfdNodeAnnotation[]; -} - -export interface DfdNodeAnnotation { - message: string; - color?: string; - icon?: string; - tfg?: number; -} - -export abstract class DfdNodeImpl extends DynamicChildrenNode implements WithEditableLabel { - static readonly DEFAULT_FEATURES = [...SNodeImpl.DEFAULT_FEATURES, withEditLabelFeature, containsDfdLabelFeature]; - static readonly DEFAULT_WIDTH = 50; - static readonly WIDTH_PADDING = 12; - static readonly NODE_COLOR = "var(--color-primary)"; - static readonly HIGHLIGHTED_COLOR = "var(--color-highlighted)"; - - text: string = ""; - color?: string; - labels: LabelAssignment[] = []; - ports: SPort[] = []; - hideLabels: boolean = false; - minimumWidth: number = DfdNodeImpl.DEFAULT_WIDTH; - annotations: DfdNodeAnnotation[] = []; - - override setChildren(schema: DfdNode): void { - const children: SModelElement[] = [ - { - type: "label:positional", - text: schema.text ?? "", - id: schema.id + "-label", - } as SLabel, - ]; - - schema.ports?.forEach((port) => { - // Remove wrongly serialized features Set. - // Refer to preprocessModelSchema in the load action for more information. - if ("features" in port) { - delete port.features; - } - - children.push(port); - }); - schema.children = children; - } - - override removeChildren(schema: DfdNode): void { - const label = schema.children?.find((element) => element.type === "label:positional") as SLabel | undefined; - const ports = schema.children?.filter((element) => element.type.startsWith("port")) ?? []; - - schema.text = label?.text ?? ""; - schema.ports = ports as SPort[]; - schema.children = []; - } - - get editableLabel() { - const label = this.children.find((element) => element.type === "label:positional"); - if (label && isEditableLabel(label)) { - return label; - } - - return undefined; - } - - protected calculateWidth(): number { - if (this.hideLabels) { - return this.minimumWidth + DfdNodeImpl.WIDTH_PADDING; - } - const textWidth = calculateTextSize(this.text).width; - const labelWidths = this.labels.map( - (labelAssignment) => DfdNodeLabelRenderer.computeLabelContent(labelAssignment)[1], - ); - - const neededWidth = Math.max(...labelWidths, textWidth, DfdNodeImpl.DEFAULT_WIDTH); - return neededWidth + DfdNodeImpl.WIDTH_PADDING; - } - - protected abstract calculateHeight(): number; - - override get bounds(): Bounds { - return { - x: this.position.x, - y: this.position.y, - width: this.calculateWidth(), - height: this.calculateHeight(), - }; - } - - /** - * Gets the names of all available input ports. - * @returns a list of the names of all available input ports. - * Can include undefined if a port has no named edges connected to it. - */ - getAvailableInputs(): (string | undefined)[] { - return this.children - .filter((child) => child instanceof DfdInputPortImpl) - .map((child) => child as DfdInputPortImpl) - .map((child) => child.getName()); - } - - /** - * Gets the text of all dfd edges that are connected to the input ports of this node. - * Applies the passed filter to the edges. - * If a edge has no label, the empty string is returned. - */ - getEdgeTexts(edgePredicate: (e: ArrowEdgeImpl) => boolean): string[] { - const inputPorts = this.children - .filter((child) => child instanceof DfdInputPortImpl) - .map((child) => child as DfdInputPortImpl); - - return inputPorts - .flatMap((port) => port.incomingEdges) - .filter((edge) => edge instanceof ArrowEdgeImpl) - .map((edge) => edge as ArrowEdgeImpl) - .filter(edgePredicate) - .map((edge) => edge.editableLabel?.text ?? ""); - } - - /** - * Generates the per-node inline style object for the view. - * Contains the opacity and the color of the node that may be set by the annotation (if any). - */ - geViewStyleObject(): VNodeStyle { - const style: VNodeStyle = { - opacity: this.opacity.toString(), - }; - - style["--border"] = "#FFFFFF"; - - if (this.color) style["--color"] = this.color; - - return style; - } - - public setColor(color: string, override: boolean = true) { - if (override || this.color === DfdNodeImpl.NODE_COLOR) this.color = color; - } -} - -@injectable() -export class StorageNodeImpl extends DfdNodeImpl { - protected override calculateHeight(): number { - const hasLabels = this.labels.length > 0; - if (hasLabels && !this.hideLabels) { - return ( - StorageNodeImpl.LABEL_START_HEIGHT + - this.labels.length * DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT + - DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN - ); - } else { - return StorageNodeImpl.TEXT_HEIGHT; - } - } - - protected override calculateWidth(): number { - return super.calculateWidth() + StorageNodeImpl.LEFT_PADDING; - } - - static readonly TEXT_HEIGHT = 32; - static readonly LABEL_START_HEIGHT = 28; - static readonly LEFT_PADDING = 10; -} - -@injectable() -export class StorageNodeView extends ShapeView { - constructor(@inject(DfdNodeLabelRenderer) @optional() private readonly labelRenderer?: DfdNodeLabelRenderer) { - super(); - } - - render(node: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - - const { width, height } = node.bounds; - const leftPadding = StorageNodeImpl.LEFT_PADDING / 2; - - return ( - - - - {context.renderChildren(node, { - xPosition: width / 2 + leftPadding, - yPosition: StorageNodeImpl.TEXT_HEIGHT / 2, - } as DfdPositionalLabelArgs)} - {this.labelRenderer?.renderNodeLabels(node, StorageNodeImpl.LABEL_START_HEIGHT, leftPadding)} - - ); - } -} - -export class FunctionNodeImpl extends DfdNodeImpl { - protected override calculateHeight(): number { - const hasLabels = this.labels.length > 0; - if (hasLabels && !this.hideLabels) { - return ( - // height for text - FunctionNodeImpl.LABEL_START_HEIGHT + - // height for the labels - this.labels.length * DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT + - // Spacing between last label and the under edge of the node rectangle - DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN - ); - } else { - return FunctionNodeImpl.LABEL_START_HEIGHT + FunctionNodeImpl.SEPARATOR_NO_LABEL_PADDING; - } - } - - static readonly TEXT_HEIGHT = 28; - static readonly SEPARATOR_NO_LABEL_PADDING = 4; - static readonly SEPARATOR_LABEL_PADDING = 4; - static readonly LABEL_START_HEIGHT = this.TEXT_HEIGHT + this.SEPARATOR_LABEL_PADDING; - static readonly BORDER_RADIUS = 5; -} - -@injectable() -export class FunctionNodeView extends ShapeView { - constructor(@inject(DfdNodeLabelRenderer) @optional() private readonly labelRenderer?: DfdNodeLabelRenderer) { - super(); - } - - render(node: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - - const { width, height } = node.bounds; - const r = FunctionNodeImpl.BORDER_RADIUS; - - return ( - - - - {context.renderChildren(node, { - xPosition: width / 2, - yPosition: FunctionNodeImpl.TEXT_HEIGHT / 2, - } as DfdPositionalLabelArgs)} - {this.labelRenderer?.renderNodeLabels(node, FunctionNodeImpl.LABEL_START_HEIGHT)} - - ); - } -} - -@injectable() -export class IONodeImpl extends DfdNodeImpl { - protected override calculateHeight(): number { - const hasLabels = this.labels.length > 0; - if (hasLabels && !this.hideLabels) { - return ( - IONodeImpl.LABEL_START_HEIGHT + - this.labels.length * DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT + - DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN - ); - } else { - return IONodeImpl.TEXT_HEIGHT; - } - } - - static readonly TEXT_HEIGHT = 32; - static readonly LABEL_START_HEIGHT = 28; -} - -@injectable() -export class IONodeView extends ShapeView { - constructor(@inject(DfdNodeLabelRenderer) @optional() private readonly labelRenderer?: DfdNodeLabelRenderer) { - super(); - } - - render(node: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - - const { width, height } = node.bounds; - - return ( - - - - {context.renderChildren(node, { - xPosition: width / 2, - yPosition: IONodeImpl.TEXT_HEIGHT / 2, - } as DfdPositionalLabelArgs)} - {this.labelRenderer?.renderNodeLabels(node, IONodeImpl.LABEL_START_HEIGHT)} - - ); - } -} diff --git a/frontend/webEditor/src/features/dfdElements/outputPortEditUi.css b/frontend/webEditor/src/features/dfdElements/outputPortEditUi.css deleted file mode 100644 index 90e50abf..00000000 --- a/frontend/webEditor/src/features/dfdElements/outputPortEditUi.css +++ /dev/null @@ -1,22 +0,0 @@ -.output-port-edit-ui { - position: absolute; - padding: 10px; - - -webkit-user-select: none; - user-select: none; - - background: var(--color-primary); -} - -.output-port-edit-ui div.unavailable-inputs { - /* spacing between editor and this text */ - padding-bottom: 5px; -} - -.output-port-edit-ui div.validation-label.validation-error { - color: var(--color-error); -} - -.output-port-edit-ui div.validation-label.validation-success { - color: var(--color-valid); -} diff --git a/frontend/webEditor/src/features/dfdElements/outputPortEditUi.ts b/frontend/webEditor/src/features/dfdElements/outputPortEditUi.ts deleted file mode 100644 index 59f8c359..00000000 --- a/frontend/webEditor/src/features/dfdElements/outputPortEditUi.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { - AbstractUIExtension, - ActionDispatcher, - Command, - CommandExecutionContext, - CommandReturn, - CommitModelAction, - MouseListener, - MouseTool, - SModelElementImpl, - SModelRootImpl, - SetUIExtensionVisibilityAction, - TYPES, - ViewerOptions, - getAbsoluteClientBounds, -} from "sprotty"; -import { Action } from "sprotty-protocol"; -import { DOMHelper } from "sprotty/lib/base/views/dom-helper"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -import { DfdOutputPortImpl } from "./ports"; -import { DfdNodeImpl } from "./nodes"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { EditorModeController } from "../editorMode/editorModeController"; -import { DFDBehaviorRefactorer } from "./behaviorRefactorer"; - -// Enable hover feature that is used to show validation errors. -// Inline completions are enabled to allow autocompletion of keywords and inputs/label types/label values. -import "monaco-editor/esm/vs/editor/contrib/hover/browser/hoverContribution"; -import "monaco-editor/esm/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.js"; - -import "./outputPortEditUi.css"; -import { ThemeManager, Switchable } from "../settingsMenu/themeManager"; -import { - assignemntLanguageMonarchDefinition, - MonacoEditorAssignmentLanguageCompletionProvider, - ReplaceAutoCompleteTree, - TreeBuilder, -} from "./AssignmentLanguage"; - -/** - * Detects when a dfd output port is double clicked and shows the OutputPortEditUI - * with the clicked port as context element. - */ -@injectable() -export class OutputPortEditUIMouseListener extends MouseListener { - private editUIVisible = false; - - mouseDown(target: SModelElementImpl): (Action | Promise)[] { - if (this.editUIVisible) { - // The user has clicked somewhere on the sprotty diagram (not the port edit UI) - // while the UI was open. In this case we hide the UI. - // This may not be exactly accurate because the UI can close itself when - // the change was saved but in those cases editUIVisible is still true. - // However hiding it one more time here for those cases is not a problem. - // Because it is already hidden, nothing will happen and after one click - // editUIVisible will be false again. - this.editUIVisible = false; - return [ - SetUIExtensionVisibilityAction.create({ - extensionId: OutputPortEditUI.ID, - visible: false, - contextElementsId: [target.id], - }), - ]; - } - - return []; - } - - doubleClick(target: SModelElementImpl): (Action | Promise)[] { - if (target instanceof DfdOutputPortImpl) { - // The user has double clicked on a dfd output port - // => show the OutputPortEditUI for this port. - this.editUIVisible = true; - return [ - SetUIExtensionVisibilityAction.create({ - extensionId: OutputPortEditUI.ID, - visible: true, - contextElementsId: [target.id], - }), - ]; - } - - return []; - } -} - -/** - * UI that allows editing the behavior text of a dfd output port (DfdOutputPortImpl). - */ -@injectable() -export class OutputPortEditUI extends AbstractUIExtension implements Switchable { - static readonly ID = "output-port-edit-ui"; - - private unavailableInputsLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; - private editorContainer: HTMLDivElement = document.createElement("div") as HTMLDivElement; - private validationLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; - - private port: DfdOutputPortImpl | undefined; - private editor?: monaco.editor.IStandaloneCodeEditor; - private tree?: ReplaceAutoCompleteTree; - private completionProvider?: monaco.IDisposable; - - private static readonly DFD_LANGUAGE_NAME = "dfd-behavior"; - - constructor( - @inject(TYPES.IActionDispatcher) private actionDispatcher: ActionDispatcher, - @inject(TYPES.ViewerOptions) private viewerOptions: ViewerOptions, - @inject(TYPES.DOMHelper) private domHelper: DOMHelper, - @inject(MouseTool) private mouseTool: MouseTool, - // Load label type registry watcher that handles changes to the behavior of - // output ports when label types are changed. - // It has to be loaded somewhere for inversify to create it and start watching. - // Since this is thematically related to the output port edit UI, it is loaded here. - // @ts-expect-error TS6133: 'labelTypeRegistry' is declared but its value is never read. - @inject(DFDBehaviorRefactorer) private readonly _labelTypeChangeWatcher: DFDBehaviorRefactorer, - - @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController, - ) { - super(); - } - - id(): string { - return OutputPortEditUI.ID; - } - - containerClass(): string { - // The container element gets this class name by the sprotty base class. - return this.id(); - } - - protected initializeContents(containerElement: HTMLElement): void { - containerElement.appendChild(this.unavailableInputsLabel); - containerElement.appendChild(this.editorContainer); - containerElement.appendChild(this.validationLabel); - const keyboardShortcutLabel = document.createElement("div"); - keyboardShortcutLabel.innerHTML = "Press CTRL+Space for autocompletion"; - containerElement.appendChild(keyboardShortcutLabel); - - containerElement.classList.add("ui-float"); - this.unavailableInputsLabel.classList.add("unavailable-inputs"); - this.editorContainer.classList.add("monaco-container"); - this.validationLabel.classList.add("validation-label"); - - // Initialize the monaco editor and setup the language for highlighting and autocomplete. - - monaco.languages.register({ id: OutputPortEditUI.DFD_LANGUAGE_NAME }); - monaco.languages.setMonarchTokensProvider( - OutputPortEditUI.DFD_LANGUAGE_NAME, - assignemntLanguageMonarchDefinition, - ); - this.registerCompletionProvider(); - - const monacoTheme = (ThemeManager?.useDarkMode ?? true) ? "vs-dark" : "vs"; - this.editor = monaco.editor.create(this.editorContainer, { - minimap: { - // takes too much space, not useful for our use case - enabled: false, - }, - lineNumbersMinChars: 3, // default is 5, which we'll never need. Save a bit of space. - folding: false, // Not supported by our language definition - wordBasedSuggestions: "off", // Does not really work for our use case - scrollBeyondLastLine: false, // Not needed - theme: monacoTheme, - language: OutputPortEditUI.DFD_LANGUAGE_NAME, - }); - - this.configureHandlers(containerElement); - } - - private resizeEditor(): void { - // Resize editor to fit content. - // Has ranges for height and width to prevent the editor from getting too small or too large. - const e = this.editor; - if (!e) { - return; - } - - // For the height we can use the content height from the editor. - const height = e.getContentHeight(); - - // For the width we cannot really do this. - // Monaco needs about 500ms to figure out the correct width when initially showing the editor. - // In the mean time the width will be too small and after the update - // the window size will jump visibly. - // So for the width we use this calculation to approximate the width. - const maxLineLength = e - .getValue() - .split("\n") - .reduce((max, line) => Math.max(max, line.length), 0); - const width = 100 + maxLineLength * 8; - - const clamp = (value: number, range: readonly [number, number]) => - Math.min(range[1], Math.max(range[0], value)); - - const heightRange = [100, 350] as const; - const widthRange = [275, 650] as const; - - const cHeight = clamp(height, heightRange); - const cWidth = clamp(width, widthRange); - - e.layout({ height: cHeight, width: cWidth }); - } - - private configureHandlers(containerElement: HTMLElement): void { - // If the user unfocuses the editor, save the changes. - this.editor?.onDidBlurEditorText(() => { - this.save(); - }); - - // Run behavior validation when the behavior text changes. - this.editor?.onDidChangeModelContent(() => { - this.validateBehavior(); - }); - - // When the content size of the editor changes, resize the editor accordingly. - this.editor?.onDidContentSizeChange(() => { - this.resizeEditor(); - }); - - // Hide/"close this window" when pressing escape. - containerElement.addEventListener("keydown", (event) => { - if (matchesKeystroke(event, "Escape")) { - this.hide(); - } - }); - - containerElement.addEventListener("mouseleave", () => { - // User might refactor some label type/value. - // Doing so will change the behavior text of all ports referencing the label type/value. - // Save the value so the user doesn't lose their work. - // After the change of the behavior text, it will be reloaded into here with the refactoring done. - this.save(); - }); - this.labelTypeRegistry?.onUpdate(() => { - // The update handler for the refactoring might be after our handler. - // Delay update to the next event loop tick to ensure the refactoring is done. - setTimeout(() => { - if (this.editor && this.port) { - this.editor?.setValue(this.port?.getBehavior()); - } - }, 0); - }); - - // Configure editor readonly depending on editor mode. - // Is set after opening the editor each time but the - // editor mode may change while the editor is open, making this handler necessary. - this.editorModeController?.onModeChange(() => { - this.editor?.updateOptions({ - readOnly: this.editorModeController?.isReadOnly() ?? false, - }); - }); - - // we allow aliasing here so it is available in the inner class, as this would refer to the inner class - // eslint-disable-next-line @typescript-eslint/no-this-alias - const portEditUi = this; - class ZoomMouseListener extends MouseListener { - wheel(): (Action | Promise)[] { - // Re-set position of the UI after next event loop tick. - // In the current event loop tick the scoll is still processed and the - // position of the port may change after the scroll processing, so we need to wait for that. - setTimeout(() => { - portEditUi.setPosition(containerElement); - }); - return []; - } - } - this.mouseTool.register(new ZoomMouseListener()); - } - - protected onBeforeShow( - containerElement: HTMLElement, - root: Readonly, - ...contextElementIds: string[] - ): void { - // Loads data for the port that shall be edited, which is defined by the context element id. - if (contextElementIds.length !== 1) { - throw new Error( - "Expected exactly one context element id which should be the port that shall be shown in the UI.", - ); - } - this.port = root.index.getById(contextElementIds[0]) as DfdOutputPortImpl; - this.setPosition(containerElement); - - const parent = this.port.parent; - if (!(parent instanceof DfdNodeImpl)) { - throw new Error("Expected parent to be a DfdNodeImpl."); - } - - const availableInputNames = parent.getAvailableInputs(); - const countUnavailableDueToMissingName = availableInputNames.filter((name) => name === undefined).length; - - if (countUnavailableDueToMissingName > 0) { - const unavailableInputsText = - countUnavailableDueToMissingName > 1 - ? `There are ${countUnavailableDueToMissingName} inputs that don't have a named edge and cannot be used` - : `There is ${countUnavailableDueToMissingName} input that doesn't have a named edge and cannot be used`; - - this.unavailableInputsLabel.innerText = unavailableInputsText; - this.unavailableInputsLabel.style.display = "block"; - } else { - this.unavailableInputsLabel.innerText = ""; - this.unavailableInputsLabel.style.display = "none"; - } - - // Load the current behavior text of the port into the text editor. - this.editor?.setValue(this.port.getBehavior()); - this.editor?.getModel()?.setEOL(monaco.editor.EndOfLineSequence.LF); - this.resizeEditor(); - - // Configure editor readonly depending on editor mode - this.editor?.updateOptions({ - readOnly: this.editorModeController?.isReadOnly() ?? false, - }); - - this.tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(this.labelTypeRegistry, this.port)); - - // Validation of loaded behavior text. - this.validateBehavior(); - - this.registerCompletionProvider(); - - // Wait for the next event loop tick to focus the port edit UI. - // The user may have clicked more times before the show click was processed - // (showing the UI takes some time due to finding the element in the graph, etc.). - // There might still be some clicks in the event loop queue queue which would de-focus the port edit UI. - // Instead process them (fast as no UI is shown or similar slow tasks are done) and then focus the UI. - setTimeout(() => { - this.editor?.focus(); - }, 0); // 0ms => next event loop tick - } - - private registerCompletionProvider() { - if (!this.tree) { - return; - } - this.completionProvider?.dispose(); - this.completionProvider = monaco.languages.registerCompletionItemProvider( - OutputPortEditUI.DFD_LANGUAGE_NAME, - new MonacoEditorAssignmentLanguageCompletionProvider(this.tree), - ); - } - - /** - * Sets the position of the UI to the position of the port that is currently edited. - */ - private setPosition(containerElement: HTMLElement) { - if (!this.port) { - return; - } - - const bounds = getAbsoluteClientBounds(this.port, this.domHelper, this.viewerOptions); - containerElement.style.left = `${bounds.x}px`; - containerElement.style.top = `${bounds.y}px`; - } - - private validateBehavior(): void { - if (!this.port) { - return; - } - - if (!this.editor) { - return; - } - if (!this.tree) { - return; - } - - const model = this.editor?.getModel(); - if (!model) { - return; - } - - const content = model.getLinesContent(); - const marker: monaco.editor.IMarkerData[] = []; - const emptyContent = content.length == 0 || (content.length == 1 && content[0] === ""); - // empty content gets accepted as valid as it represents no constraints - if (!emptyContent) { - const errors = this.tree.verify(content); - marker.push( - ...errors.map((e) => ({ - severity: monaco.MarkerSeverity.Error, - startLineNumber: e.line, - startColumn: e.startColumn, - endLineNumber: e.line, - endColumn: e.endColumn, - message: e.message, - })), - ); - } - - if (marker.length == 0) { - this.validationLabel.innerText = "Assignments are valid"; - this.validationLabel.classList.remove("validation-error"); - this.validationLabel.classList.add("validation-success"); - } else { - this.validationLabel.innerText = `Assignments are invalid: ${marker.length} error${ - marker.length === 1 ? "" : "s" - }.`; - this.validationLabel.classList.remove("validation-success"); - this.validationLabel.classList.add("validation-error"); - } - - monaco.editor.setModelMarkers(model, "assignment", marker); - } - - /** - * Saves the current behavior text inside the editor to the port. - */ - private save(): void { - if (!this.port) { - throw new Error("Cannot save without set port."); - } - - const behaviorText = this.editor?.getValue() ?? ""; - this.actionDispatcher.dispatch(SetDfdOutputPortBehaviorAction.create(this.port.id, behaviorText)); - this.actionDispatcher.dispatch(CommitModelAction.create()); - } - - public getCurrentEditingPort(): DfdOutputPortImpl | undefined { - return this.port; - } - - switchTheme(useDark: boolean): void { - this.editor?.updateOptions({ theme: useDark ? "vs-dark" : "vs" }); - } -} - -/** - * Sets the behavior property of a dfd output port (DfdOutputPortImpl). - * This is used by the OutputPortEditUI but implemented as an action for undo/redo support. - */ -export interface SetDfdOutputPortBehaviorAction extends Action { - kind: typeof SetDfdOutputPortBehaviorAction.KIND; - portId: string; - behavior: string; -} -export namespace SetDfdOutputPortBehaviorAction { - export const KIND = "setDfdOutputPortBehavior"; - export function create(portId: string, behavior: string): SetDfdOutputPortBehaviorAction { - return { - kind: KIND, - portId, - behavior, - }; - } -} - -@injectable() -export class SetDfdOutputPortBehaviorCommand extends Command { - static readonly KIND = SetDfdOutputPortBehaviorAction.KIND; - - constructor(@inject(TYPES.Action) private action: SetDfdOutputPortBehaviorAction) { - super(); - } - - private oldBehavior: string | undefined; - - execute(context: CommandExecutionContext): CommandReturn { - const port = context.root.index.getById(this.action.portId) as DfdOutputPortImpl; - this.oldBehavior = port.getBehavior(); - port.setBehavior(this.action.behavior); - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - const port = context.root.index.getById(this.action.portId) as DfdOutputPortImpl; - if (this.oldBehavior) { - port.setBehavior(this.oldBehavior); - } - - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} diff --git a/frontend/webEditor/src/features/dfdElements/portSnapper.ts b/frontend/webEditor/src/features/dfdElements/portSnapper.ts deleted file mode 100644 index eb8e0fc1..00000000 --- a/frontend/webEditor/src/features/dfdElements/portSnapper.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { inject, injectable } from "inversify"; -import { - CenterGridSnapper, - Command, - CommandExecutionContext, - CommandReturn, - ISnapper, - MoveMouseListener, - SChildElementImpl, - SModelElementImpl, - SNodeImpl, - SPortImpl, - TYPES, - isBoundsAware, -} from "sprotty"; -import { ApplyLabelEditAction, Point } from "sprotty-protocol"; - -/** - * A grid snapper that snaps to the nearest grid point. - * Same as CenterGridSnapper but allows to specify the grid size at construction time. - */ -class ConfigurableGridSnapper extends CenterGridSnapper { - constructor(private readonly gridSize: number) { - super(); - } - - override get gridX() { - return this.gridSize; - } - - override get gridY() { - return this.gridSize; - } -} - -/** - * A snapper that snaps ports to be on top of the nearest edge of the node. - * For nodes this snapper uses a grid with a grid size of 5 while for ports it uses a grid size of 2 - * to allow for more precise positioning of ports. - */ -@injectable() -export class PortAwareSnapper implements ISnapper { - private readonly nodeSnapper = new ConfigurableGridSnapper(5); - // The port grid size is a multiple of the node grid size to ensure - // that the ports of two nodes neighboring each other can be aligned. - // If the grid size would be different, it may occur that the ports - // of two nodes that start on different heights may not be aligned, - // so make sure that the node grid size is a multiple of the port grid size. - private readonly portSnapper = new ConfigurableGridSnapper(2.5); - - private snapPort(position: Point, element: SPortImpl): Point { - const parentElement = element.parent; - - if (parentElement instanceof SPortImpl) { - // Parent is not a node, so we cannot snap the port to the node edges - return position; - } - - if (!isBoundsAware(parentElement)) { - // Cannot get the parent size, just return the original position and don't snap - return position; - } - - const parentBounds = parentElement.bounds; - - // Clamp the position to be inside the parent bounds - const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); - - position = this.portSnapper.snap(position, element); - const clampX = clamp(position.x, 0, parentBounds.width); - const clampY = clamp(position.y, 0, parentBounds.height); - - // Determine the closest edge - const distances = [ - { x: clampX, y: 0 }, // Top edge - { x: 0, y: clampY }, // Left edge - { x: parentBounds.width, y: clampY }, // Right edge - { x: clampX, y: parentBounds.height }, // Bottom edge - ]; - - const closestEdge = distances.reduce((prev, curr) => - Math.hypot(curr.x - position.x, curr.y - position.y) < Math.hypot(prev.x - position.x, prev.y - position.y) - ? curr - : prev, - ); - - // The position currently points exactly on the edge. - // This position is used as the top left point when the port is drawn. - // However we want the port to be centered on the node edge instead of the top left being on top of the edge. - // So we move the port by half of the width/height to the left/top to center it on the node edge. - const snappedX = closestEdge.x - element.bounds.width / 2; - const snappedY = closestEdge.y - element.bounds.height / 2; - - return { x: snappedX, y: snappedY }; - } - - snap(position: Point, element: SModelElementImpl): Point { - if (element instanceof SPortImpl) { - return this.snapPort(position, element); - } else { - return this.nodeSnapper.snap(position, element); - } - } -} - -/** - * Custom MoveMouseListener that only allows to disable snapping for nodes. - * For use with PortAwareSnapper which snaps the ports to the node edges. - * Snapping can normally be temporarily disabled by holding down the Shift key. - * This would allow you to move a port to any position in the diagram and not just on the node edges. - * This is not wanted why we disallow fine moving without snapping for ports. - */ -export class AlwaysSnapPortsMoveMouseListener extends MoveMouseListener { - protected snap(position: Point, element: SModelElementImpl, isSnap: boolean): Point { - // Snap if it is active or always for ports - if (this.snapper && (isSnap || element instanceof SPortImpl)) { - return this.snapper.snap(position, element); - } else { - return position; - } - } -} - -/** - * Command that snaps all ports of the node to the grid after a label was added/removed. - * Runs after {@link ApplyLabelEditAction} to ensure the ports are snapped to the grid after the label was moved. - * - * This is done by implementing another command for {@link ApplyLabelEditAction} - * and registering it as well. That way this command will be executed after the {@link ApplyLabelEditCommand} - */ -@injectable() -export class ReSnapPortsAfterLabelChangeCommand extends Command { - static readonly KIND = ApplyLabelEditAction.KIND; - - @inject(TYPES.ISnapper) - private snapper?: ISnapper; - - constructor(@inject(TYPES.Action) private readonly action: ApplyLabelEditAction) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - const label = context.root.index.getById(this.action.labelId); - if (!(label instanceof SChildElementImpl) || !this.snapper) { - return context.root; - } - - const node = label.parent; - if (!(node instanceof SNodeImpl)) { - return context.root; - } - - snapPortsOfNode(node, this.snapper); - return context.root; - } - - // undo/redo: resnap aswell. Same as execute - - undo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} - -/** - * Snaps all ports of the given node to the grid using the given snapper. - * Useful to ensure all ports are on are snapped onto an node edge using - * {@link PortAwareSnapper} after resizing the node. - */ -export function snapPortsOfNode(node: SNodeImpl, snapper: ISnapper): void { - if (!(node instanceof SChildElementImpl)) { - // Element has no children which could be ports - return; - } - - node.children.forEach((child) => { - if (child instanceof SPortImpl) { - // PortAwareSnapper expects the center of the port as input. - // However the stored position points to the top left of the port, - // so we need to adjust the position by half of the width/height. - const pos = { ...child.position }; - const { width, height } = child.bounds; - pos.x += width / 2; - pos.y += height / 2; - - child.position = snapper.snap(pos, child); - } - }); -} diff --git a/frontend/webEditor/src/features/dfdElements/ports.tsx b/frontend/webEditor/src/features/dfdElements/ports.tsx deleted file mode 100644 index c666c44d..00000000 --- a/frontend/webEditor/src/features/dfdElements/ports.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/** @jsx svg */ -import { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - svg, - ShapeView, - SPortImpl, - RenderingContext, - moveFeature, - deletableFeature, - withEditLabelFeature, - isEditableLabel, - SRoutableElementImpl, -} from "sprotty"; -import { Bounds, SPort } from "sprotty-protocol"; -import { injectable } from "inversify"; -import { VNode, VNodeStyle } from "snabbdom"; -import { ArrowEdgeImpl } from "./edges"; -import { AutoCompleteTree } from "../constraintMenu/AutoCompletion"; -import { TreeBuilder } from "./AssignmentLanguage"; -import { labelTypeRegistry } from "../.."; - -const defaultPortFeatures = [...SPortImpl.DEFAULT_FEATURES, moveFeature, deletableFeature]; -const portSize = 7; - -export type DfdInputPort = SPort; - -@injectable() -export class DfdInputPortImpl extends SPortImpl { - static readonly DEFAULT_FEATURES = defaultPortFeatures; - - override get bounds(): Bounds { - return { - x: this.position.x, - y: this.position.y, - width: portSize, - height: portSize, - }; - } - - /** - * Builds the name of the input port from the names of the incoming dfd edges. - * @returns either the concatenated names of the incoming edges or undefined if there are no named incoming edges. - */ - getName(): string | undefined { - const edgeNames: string[] = []; - - this.incomingEdges.forEach((edge) => { - if (edge instanceof ArrowEdgeImpl) { - const name = edge.editableLabel?.text; - if (name) { - edgeNames.push(name); - } - } else { - return undefined; - } - }); - - if (edgeNames.length === 0) { - return undefined; - } else { - return edgeNames.sort().join("|"); - } - } - - canConnect(_routable: SRoutableElementImpl, role: "source" | "target"): boolean { - // Only allow edges into this port - return role === "target"; - } -} - -export class DfdInputPortView extends ShapeView { - render(node: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - - const { width, height } = node.bounds; - - return ( - - - - I - - {context.renderChildren(node)} - - ); - } -} - -export interface DfdOutputPort extends SPort { - behavior: string; -} - -@injectable() -export class DfdOutputPortImpl extends SPortImpl { - static readonly DEFAULT_FEATURES = [...defaultPortFeatures, withEditLabelFeature]; - - private behavior: string = ""; - private validBehavior: boolean = true; - - override get bounds(): Bounds { - return { - x: this.position.x, - y: this.position.y, - width: portSize, - height: portSize, - }; - } - - get editableLabel() { - const label = this.children.find((element) => element.type === "label:invisible"); - if (label && isEditableLabel(label)) { - return label; - } - - return undefined; - } - - canConnect(_routable: SRoutableElementImpl, role: "source" | "target"): boolean { - // Only allow edges from this port outwards - return role === "source"; - } - - /** - * Generates the per-node inline style object for the view. - */ - geViewStyleObject(): VNodeStyle { - const style: VNodeStyle = { - opacity: this.opacity.toString(), - }; - if (!labelTypeRegistry) return style; - - if (!this.validBehavior) { - style["--port-border"] = "#ff0000"; - style["--port-color"] = "#ff6961"; - } - - return style; - } - - public setBehavior(value: string) { - this.behavior = value; - if (value === "") { - this.validBehavior = true; - return; - } - const errors = new AutoCompleteTree(TreeBuilder.buildTree(labelTypeRegistry, this)).verify( - this.behavior.split("\n"), - ); - this.validBehavior = errors.length === 0; - } - - public getBehavior() { - return this.behavior; - } -} - -@injectable() -export class DfdOutputPortView extends ShapeView { - render(node: Readonly, context: RenderingContext): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - - const { width, height } = node.bounds; - - return ( - - - - O - - {context.renderChildren(node)} - - ); - } -} diff --git a/frontend/webEditor/src/features/editorMode/command.ts b/frontend/webEditor/src/features/editorMode/command.ts deleted file mode 100644 index 4ea79a2b..00000000 --- a/frontend/webEditor/src/features/editorMode/command.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { inject } from "inversify"; -import { Command, TYPES, CommandExecutionContext, CommandReturn } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { DfdNodeAnnotation, DfdNodeImpl } from "../dfdElements/nodes"; -import { EditorMode, EditorModeController } from "./editorModeController"; - -export interface ChangeEditorModeAction extends Action { - kind: typeof ChangeEditorModeAction.KIND; - newMode: EditorMode; -} -export namespace ChangeEditorModeAction { - export const KIND = "changeEditorMode"; - - export function create(newMode: EditorMode): ChangeEditorModeAction { - return { - kind: KIND, - newMode, - }; - } -} - -export class ChangeEditorModeCommand extends Command { - static readonly KIND = ChangeEditorModeAction.KIND; - - private oldMode?: EditorMode; - private oldNodeAnnotations: Map = new Map(); - - @inject(EditorModeController) - private readonly controller?: EditorModeController; - - constructor(@inject(TYPES.Action) private action: ChangeEditorModeAction) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - if (!this.controller) throw new Error("Missing injects"); - - this.oldMode = this.controller.getCurrentMode(); - this.controller.setMode(this.action.newMode); - this.postModeSwitch(context); - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - if (!this.controller) throw new Error("Missing injects"); - - if (!this.oldMode) { - // This should never happen because execute() is called before undo() is called. - throw new Error("No old mode to restore"); - } - this.controller.setMode(this.oldMode); - this.undoPostModeSwitch(context); - - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } - - private postModeSwitch(context: CommandExecutionContext): void { - if (this.oldMode === "view" && this.action.newMode === "edit") { - // Remove annotations when enabling editing - - this.oldNodeAnnotations.clear(); - context.root.index.all().forEach((element) => { - if (element instanceof DfdNodeImpl && element.annotations) { - this.oldNodeAnnotations.set(element.id, element.annotations); - element.annotations = []; - } - }); - } - } - - private undoPostModeSwitch(context: CommandExecutionContext): void { - if (this.oldMode === "view" && this.action.newMode === "edit") { - // Restore annotations when disabling editing - this.oldNodeAnnotations.forEach((annotation, id) => { - const element = context.root.index.getById(id); - if (element instanceof DfdNodeImpl) { - element.annotations = annotation; - } - }); - } - } -} diff --git a/frontend/webEditor/src/features/editorMode/di.config.ts b/frontend/webEditor/src/features/editorMode/di.config.ts deleted file mode 100644 index 4920a487..00000000 --- a/frontend/webEditor/src/features/editorMode/di.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ContainerModule } from "inversify"; -import { DeleteElementCommand, EditLabelMouseListener, TYPES, configureCommand } from "sprotty"; -import { EditorModeController } from "./editorModeController"; -import { EditorModeSwitchUi } from "./modeSwitchUi"; -import { EDITOR_TYPES } from "../../utils"; -import { EditorModeAwareDeleteElementCommand, EditorModeAwareEditLabelMouseListener } from "./sprottyHooks"; -import { ChangeEditorModeCommand } from "./command"; - -export const editorModeModule = new ContainerModule((bind, unbind, isBound, rebind) => { - const context = { bind, unbind, isBound, rebind }; - - bind(EditorModeController).toSelf().inSingletonScope(); - bind(EditorModeSwitchUi).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(EditorModeSwitchUi); - bind(EDITOR_TYPES.DefaultUIElement).toService(EditorModeSwitchUi); - - configureCommand(context, ChangeEditorModeCommand); - - // Sprotty hooks that hook into the edit label, move and edit module - // to intercept model modifications to prevent them when the editor is in a read-only mode. - rebind(EditLabelMouseListener).to(EditorModeAwareEditLabelMouseListener); - rebind(DeleteElementCommand).to(EditorModeAwareDeleteElementCommand); -}); diff --git a/frontend/webEditor/src/features/editorMode/editorModeController.ts b/frontend/webEditor/src/features/editorMode/editorModeController.ts deleted file mode 100644 index db4ac77a..00000000 --- a/frontend/webEditor/src/features/editorMode/editorModeController.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { injectable } from "inversify"; - -export type EditorMode = "edit" | "view"; - -/** - * Holds the current editor mode in a central place. - * Used to get the current mode in places where it is used. - * - * Changes to the mode should be done using the ChangeEditorModeCommand - * and not directly on this class when done interactively - * for undo/redo support and actions that are done to the model - * when the mode changes. - */ -@injectable() -export class EditorModeController { - private mode: EditorMode = "edit"; - private modeChangeCallbacks: ((mode: EditorMode) => void)[] = []; - - getCurrentMode(): EditorMode { - return this.mode; - } - - setMode(mode: EditorMode) { - this.mode = mode; - - this.modeChangeCallbacks.forEach((callback) => callback(mode)); - } - - setDefaultMode() { - this.mode = "edit"; - } - - onModeChange(callback: (mode: EditorMode) => void) { - this.modeChangeCallbacks.push(callback); - } - - isReadOnly(): boolean { - return this.mode !== "edit"; - } -} diff --git a/frontend/webEditor/src/features/editorMode/modeSwitchUi.css b/frontend/webEditor/src/features/editorMode/modeSwitchUi.css deleted file mode 100644 index c451b909..00000000 --- a/frontend/webEditor/src/features/editorMode/modeSwitchUi.css +++ /dev/null @@ -1,11 +0,0 @@ -.editor-mode-switcher { - /* Position the switcher in the top left corner */ - top: 40px; - padding: 8px; - left: 40px; - line-height: 1.5; - - /* Make text non-selectable */ - -webkit-user-select: none; /* Safari only supports user select using the -webkit prefix */ - user-select: none; -} diff --git a/frontend/webEditor/src/features/editorMode/modeSwitchUi.ts b/frontend/webEditor/src/features/editorMode/modeSwitchUi.ts deleted file mode 100644 index 26c976c4..00000000 --- a/frontend/webEditor/src/features/editorMode/modeSwitchUi.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { AbstractUIExtension } from "sprotty"; -import { EditorMode, EditorModeController } from "./editorModeController"; -import { inject, injectable } from "inversify"; - -import "./modeSwitchUi.css"; - -/** - * UI that shows the current editor mode (unless it is edit mode) - * with details about the mode. - */ -@injectable() -export class EditorModeSwitchUi extends AbstractUIExtension { - static readonly ID = "editor-mode-switcher"; - - constructor( - @inject(EditorModeController) - private readonly editorModeController: EditorModeController, - ) { - super(); - } - - id(): string { - return EditorModeSwitchUi.ID; - } - containerClass(): string { - return this.id(); - } - - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.style.visibility = "hidden"; - this.editorModeController.onModeChange((mode) => this.reRender(mode)); - this.reRender(this.editorModeController.getCurrentMode()); - } - - private reRender(mode: EditorMode): void { - this.containerElement.innerHTML = ""; - switch (mode) { - case "edit": - this.containerElement.style.visibility = "hidden"; - break; - case "view": - this.containerElement.style.visibility = "visible"; - this.renderViewMode(); - break; - default: - throw new Error(`Unknown editor mode: ${mode}`); - } - } - - private renderViewMode(): void { - this.containerElement.innerHTML = ` - Currently viewing model in read only mode.
- Enabling editing will remove the annotations.
- `; - } -} diff --git a/frontend/webEditor/src/features/editorMode/sprottyHooks.ts b/frontend/webEditor/src/features/editorMode/sprottyHooks.ts deleted file mode 100644 index 59b070e4..00000000 --- a/frontend/webEditor/src/features/editorMode/sprottyHooks.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { inject, injectable } from "inversify"; -import { - CommandExecutionContext, - CommandReturn, - DeleteElementCommand, - EditLabelMouseListener, - SModelElementImpl, -} from "sprotty"; -import { EditorModeController } from "./editorModeController"; -import { Action } from "sprotty-protocol"; - -@injectable() -export class EditorModeAwareEditLabelMouseListener extends EditLabelMouseListener { - constructor( - @inject(EditorModeController) - private readonly editorModeController: EditorModeController, - ) { - super(); - } - - doubleClick(target: SModelElementImpl, event: MouseEvent): (Action | Promise)[] { - if (this.editorModeController.isReadOnly()) { - return []; - } - - return super.doubleClick(target, event); - } -} - -@injectable() -export class EditorModeAwareDeleteElementCommand extends DeleteElementCommand { - @inject(EditorModeController) - private readonly editorModeController?: EditorModeController; - - execute(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - return super.execute(context); - } - - undo(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - return super.undo(context); - } - - redo(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - return super.redo(context); - } -} diff --git a/frontend/webEditor/src/features/labels/commands.ts b/frontend/webEditor/src/features/labels/commands.ts deleted file mode 100644 index 1ea864af..00000000 --- a/frontend/webEditor/src/features/labels/commands.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { Action } from "sprotty-protocol"; -import { - Command, - CommandExecutionContext, - CommandReturn, - ISnapper, - isSelected, - SChildElementImpl, - SModelElementImpl, - SNodeImpl, - SParentElementImpl, - TYPES, -} from "sprotty"; -import { injectable, inject, optional } from "inversify"; -import { ContainsDfdLabels, containsDfdLabels } from "./elementFeature"; -import { LabelAssignment, LabelTypeRegistry } from "./labelTypeRegistry"; -import { snapPortsOfNode } from "../dfdElements/portSnapper"; -import { EditorModeController } from "../editorMode/editorModeController"; - -interface LabelAction extends Action { - element?: ContainsDfdLabels & SNodeImpl; - labelAssignment: LabelAssignment; -} -abstract class LabelCommand extends Command { - @inject(EditorModeController) - @optional() - protected readonly editorModeController?: EditorModeController; - - protected elements?: SModelElementImpl[]; - - constructor( - @inject(TYPES.Action) protected action: LabelAction, - @inject(TYPES.ISnapper) protected snapper: ISnapper, - ) { - super(); - } - - protected fetchElements(context: CommandExecutionContext): SModelElementImpl[] { - if (this.editorModeController?.isReadOnly()) { - return []; - } - - const allElements = getAllElements(context.root.children); - const selectedElements = allElements.filter((element) => isSelected(element)); - - const selectionHasElement = - selectedElements.find((element) => element.id === this.action.element?.id) !== undefined; - if (selectionHasElement) { - return selectedElements; - } - return this.action.element ? [this.action.element] : selectedElements; - } - - protected addLabel(context: CommandExecutionContext) { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - if (this.elements === undefined) { - this.elements = this.fetchElements(context); - } - - this.elements.forEach((element) => { - if (containsDfdLabels(element)) { - const hasBeenAdded = - element.labels.find((as) => { - return ( - as.labelTypeId === this.action.labelAssignment.labelTypeId && - as.labelTypeValueId === this.action.labelAssignment.labelTypeValueId - ); - }) !== undefined; - if (!hasBeenAdded) { - element.labels.push(this.action.labelAssignment); - if (element instanceof SNodeImpl) { - snapPortsOfNode(element, this.snapper); - } - } - } - }); - - return context.root; - } - - protected removeLabel(context: CommandExecutionContext) { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - if (this.elements === undefined) { - this.elements = this.fetchElements(context); - } - - this.elements.forEach((element) => { - if (containsDfdLabels(element)) { - const labels = element.labels; - const idx = labels.findIndex( - (l) => - l.labelTypeId == this.action.labelAssignment.labelTypeId && - l.labelTypeValueId == this.action.labelAssignment.labelTypeValueId, - ); - if (idx >= 0) { - labels.splice(idx, 1); - if (element instanceof SNodeImpl) { - snapPortsOfNode(element, this.snapper); - } - } - } - }); - - return context.root; - } -} - -export interface AddLabelAssignmentAction extends LabelAction { - kind: typeof AddLabelAssignmentAction.TYPE; -} -export namespace AddLabelAssignmentAction { - export const TYPE = "add-label-assignment"; - export function create( - labelAssignment: LabelAssignment, - element?: ContainsDfdLabels & SNodeImpl, - ): AddLabelAssignmentAction { - return { - kind: TYPE, - element, - labelAssignment, - }; - } -} - -@injectable() -export class AddLabelAssignmentCommand extends LabelCommand { - public static readonly KIND = AddLabelAssignmentAction.TYPE; - - constructor(@inject(TYPES.Action) action: AddLabelAssignmentAction, @inject(TYPES.ISnapper) snapper: ISnapper) { - super(action, snapper); - } - - execute(context: CommandExecutionContext): CommandReturn { - return this.addLabel(context); - } - - undo(context: CommandExecutionContext): CommandReturn { - return this.removeLabel(context); - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} - -export interface DeleteLabelAssignmentAction extends LabelAction { - kind: typeof DeleteLabelAssignmentAction.TYPE; -} -export namespace DeleteLabelAssignmentAction { - export const TYPE = "delete-label-assignment"; - export function create( - labelAssignment: LabelAssignment, - element?: ContainsDfdLabels & SNodeImpl, - ): DeleteLabelAssignmentAction { - return { - kind: TYPE, - element, - labelAssignment, - }; - } -} - -@injectable() -export class DeleteLabelAssignmentCommand extends LabelCommand { - public static readonly KIND = DeleteLabelAssignmentAction.TYPE; - - constructor(@inject(TYPES.Action) action: DeleteLabelAssignmentAction, @inject(TYPES.ISnapper) snapper: ISnapper) { - super(action, snapper); - } - - execute(context: CommandExecutionContext): CommandReturn { - return this.removeLabel(context); - } - - undo(context: CommandExecutionContext): CommandReturn { - return this.addLabel(context); - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} - -/** - * Recursively traverses the sprotty diagram graph and removes all labels that match the given predicate. - * @param predicate a function deciding whether the label assignment should be kept - */ -function removeLabelsFromGraph( - element: SModelElementImpl | SParentElementImpl, - snapper: ISnapper, - predicate: (type: LabelAssignment) => boolean, -): void { - if (containsDfdLabels(element)) { - const filteredLabels = element.labels.filter(predicate); - if (filteredLabels.length !== element.labels.length) { - element.labels = filteredLabels; - if (containsDfdLabels(element) && element instanceof SNodeImpl) { - snapPortsOfNode(element, snapper); - } - } - } - - if ("children" in element) { - element.children.forEach((child) => removeLabelsFromGraph(child, snapper, predicate)); - } -} - -export interface DeleteLabelTypeValueAction extends Action { - kind: typeof DeleteLabelTypeValueAction.TYPE; - registry: LabelTypeRegistry; - labelTypeId: string; - labelTypeValueId: string; -} -export namespace DeleteLabelTypeValueAction { - export const TYPE = "delete-label-type-value"; - export function create( - registry: LabelTypeRegistry, - labelTypeId: string, - labelTypeValueId: string, - ): DeleteLabelTypeValueAction { - return { - kind: TYPE, - registry, - labelTypeId, - labelTypeValueId, - }; - } -} - -@injectable() -export class DeleteLabelTypeValueCommand extends Command { - public static readonly KIND = DeleteLabelTypeValueAction.TYPE; - - @inject(EditorModeController) - @optional() - private readonly editorModeController?: EditorModeController; - - constructor( - @inject(TYPES.Action) private action: DeleteLabelTypeValueAction, - @inject(TYPES.ISnapper) private snapper: ISnapper, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - const labelType = this.action.registry.getLabelType(this.action.labelTypeId); - if (!labelType) { - return context.root; - } - - const labelTypeValue = labelType.values.find((value) => value.id === this.action.labelTypeValueId); - if (!labelTypeValue) { - return context.root; - } - - removeLabelsFromGraph(context.root, this.snapper, (label) => { - return ( - label.labelTypeId !== this.action.labelTypeId || label.labelTypeValueId !== this.action.labelTypeValueId - ); - }); - - const index = labelType.values.indexOf(labelTypeValue); - if (index > -1) { - labelType.values.splice(index, 1); - this.action.registry.labelTypeChanged(); - } - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} - -export interface DeleteLabelTypeAction extends Action { - kind: typeof DeleteLabelTypeAction.TYPE; - registry: LabelTypeRegistry; - labelTypeId: string; -} -export namespace DeleteLabelTypeAction { - export const TYPE = "delete-label-type"; - export function create(registry: LabelTypeRegistry, labelTypeId: string): DeleteLabelTypeAction { - return { - kind: TYPE, - registry, - labelTypeId, - }; - } -} - -@injectable() -export class DeleteLabelTypeCommand extends Command { - public static readonly KIND = DeleteLabelTypeAction.TYPE; - - @inject(EditorModeController) - @optional() - private readonly editorModeController?: EditorModeController; - - constructor( - @inject(TYPES.Action) private action: DeleteLabelTypeAction, - @inject(TYPES.ISnapper) private snapper: ISnapper, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - if (this.editorModeController?.isReadOnly()) { - return context.root; - } - - const labelType = this.action.registry.getLabelType(this.action.labelTypeId); - if (!labelType) { - return context.root; - } - - removeLabelsFromGraph(context.root, this.snapper, (label) => label.labelTypeId !== this.action.labelTypeId); - this.action.registry.unregisterLabelType(labelType); - - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} - -function getAllElements(elements: readonly SChildElementImpl[]): SModelElementImpl[] { - const elementsList: SModelElementImpl[] = []; - for (const element of elements) { - elementsList.push(element); - if ("children" in element) { - elementsList.push(...getAllElements(element.children)); - } - } - return elementsList; -} diff --git a/frontend/webEditor/src/features/labels/di.config.ts b/frontend/webEditor/src/features/labels/di.config.ts deleted file mode 100644 index a0f085e3..00000000 --- a/frontend/webEditor/src/features/labels/di.config.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ContainerModule } from "inversify"; -import { LabelTypeRegistry, globalLabelTypeRegistry } from "./labelTypeRegistry"; -import { DfdNodeLabelRenderer } from "./labelRenderer"; -import { EDITOR_TYPES } from "../../utils"; -import { DfdLabelMouseDropListener } from "./dropListener"; -import { LabelTypeEditorUI } from "./labelTypeEditor"; -import { TYPES, configureCommand } from "sprotty"; -import { - AddLabelAssignmentCommand, - DeleteLabelAssignmentCommand, - DeleteLabelTypeCommand, - DeleteLabelTypeValueCommand, -} from "./commands"; - -// This module contains the components required for the dfd node labels. -// This includes a registry for the label types, a UI to manage them, -// a renderer to display them inside nodes and commands to add/delete them to nodes. - -export const dfdLabelModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(LabelTypeRegistry).toConstantValue(globalLabelTypeRegistry); - bind(DfdNodeLabelRenderer).toSelf().inSingletonScope(); - bind(DfdLabelMouseDropListener).toSelf().inSingletonScope(); - bind(TYPES.MouseListener).toService(DfdLabelMouseDropListener); - - bind(LabelTypeEditorUI).toSelf().inSingletonScope(); - bind(TYPES.KeyListener).toService(LabelTypeEditorUI); - bind(TYPES.IUIExtension).toService(LabelTypeEditorUI); - bind(EDITOR_TYPES.DefaultUIElement).to(LabelTypeEditorUI); - - const context = { bind, unbind, isBound, rebind }; - configureCommand(context, AddLabelAssignmentCommand); - configureCommand(context, DeleteLabelAssignmentCommand); - configureCommand(context, DeleteLabelTypeValueCommand); - configureCommand(context, DeleteLabelTypeCommand); -}); diff --git a/frontend/webEditor/src/features/labels/dropListener.ts b/frontend/webEditor/src/features/labels/dropListener.ts deleted file mode 100644 index f05bbff8..00000000 --- a/frontend/webEditor/src/features/labels/dropListener.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { injectable, inject } from "inversify"; -import { LabelAssignment } from "./labelTypeRegistry"; -import { Action } from "sprotty-protocol"; -import { - SModelElementImpl, - SChildElementImpl, - MouseListener, - CommitModelAction, - ILogger, - TYPES, - SNodeImpl, -} from "sprotty"; -import { AddLabelAssignmentAction } from "./commands"; -import { getParentWithDfdLabels } from "./elementFeature"; - -export const LABEL_ASSIGNMENT_MIME_TYPE = "application/x-label-assignment"; - -/** - * Mouse Listener that handles the drop of label assignments. - * These can be started by dragging a label type value from the label type editor UI. - * Adds the label to the element that the label value was dropped on. - */ -@injectable() -export class DfdLabelMouseDropListener extends MouseListener { - constructor(@inject(TYPES.ILogger) private logger: ILogger) { - super(); - } - - override dragOver(_target: SModelElementImpl, event: MouseEvent): Action[] { - // Prevent the dragover prevent to indicated that the drop is possible - // Check https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dragover_event for more details - event.preventDefault(); - return []; - } - - override drop(target: SChildElementImpl, event: DragEvent): Action[] { - const labelAssignmentJson = event.dataTransfer?.getData(LABEL_ASSIGNMENT_MIME_TYPE); - if (!labelAssignmentJson) { - return []; - } - - const dfdLabelElement = getParentWithDfdLabels(target); - if (!dfdLabelElement) { - this.logger.info( - this, - "Aborted drop of label assignment because the target element nor the parent elements have the dfd label feature", - ); - return []; - } - - if (!(dfdLabelElement instanceof SNodeImpl)) { - this.logger.info(this, "Aborted drop of label assignment because the target element is not a node"); - return []; - } - - const labelAssignment = JSON.parse(labelAssignmentJson) as LabelAssignment; - this.logger.info(this, "Adding label assignment to element", dfdLabelElement, labelAssignment); - return [AddLabelAssignmentAction.create(labelAssignment, dfdLabelElement), CommitModelAction.create()]; - } -} diff --git a/frontend/webEditor/src/features/labels/elementFeature.ts b/frontend/webEditor/src/features/labels/elementFeature.ts deleted file mode 100644 index df18c4f0..00000000 --- a/frontend/webEditor/src/features/labels/elementFeature.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { SChildElementImpl, SModelElementImpl, SParentElementImpl, SShapeElementImpl } from "sprotty"; -import { LabelAssignment } from "./labelTypeRegistry"; - -export const containsDfdLabelFeature = Symbol("dfd-label-feature"); - -export interface ContainsDfdLabels extends SModelElementImpl { - labels: LabelAssignment[]; -} - -export function containsDfdLabels(element: T): element is T & ContainsDfdLabels { - return element.features?.has(containsDfdLabelFeature) ?? false; -} - -// Traverses the graph upwards to find any element having the dfd label feature. -// This is needed because you may select/drop onto a child element of the node implementing and displaying dfd labels. -// If the element itself and no parent has the feature undefined is returned. -export function getParentWithDfdLabels( - element: SChildElementImpl | SParentElementImpl | SShapeElementImpl, -): (SModelElementImpl & ContainsDfdLabels) | undefined { - if (containsDfdLabels(element)) { - return element; - } - - if ("parent" in element) { - return getParentWithDfdLabels(element.parent); - } - - return undefined; -} diff --git a/frontend/webEditor/src/features/labels/labelRenderer.tsx b/frontend/webEditor/src/features/labels/labelRenderer.tsx deleted file mode 100644 index aa0281bd..00000000 --- a/frontend/webEditor/src/features/labels/labelRenderer.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/** @jsx svg */ -import { injectable, inject, optional } from "inversify"; -import { VNode } from "snabbdom"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { IActionDispatcher, SNodeImpl, TYPES, svg } from "sprotty"; -import { calculateTextSize } from "../../utils"; -import { LabelAssignment, LabelTypeRegistry, globalLabelTypeRegistry } from "./labelTypeRegistry"; -import { DeleteLabelAssignmentAction } from "./commands"; -import { ContainsDfdLabels } from "./elementFeature"; -import { SettingsManager } from "../settingsMenu/SettingsManager"; - -@injectable() -export class DfdNodeLabelRenderer { - static readonly LABEL_HEIGHT = 10; - static readonly LABEL_SPACE_BETWEEN = 2; - static readonly LABEL_SPACING_HEIGHT = DfdNodeLabelRenderer.LABEL_HEIGHT + DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN; - static readonly LABEL_TEXT_PADDING = 8; - - constructor( - @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, - @inject(SettingsManager) private readonly settingsManager: SettingsManager, - @inject(LabelTypeRegistry) @optional() private readonly labelTypeRegistry?: LabelTypeRegistry, - ) {} - - /** - * Gets the label type of the assignment and builds the text to display. - * From this text the width of the label is calculated using the corresponding font size and padding. - * @returns a tuple containing the text and the width of the label in pixel - */ - static computeLabelContent(label: LabelAssignment): [string, number] { - const labelType = globalLabelTypeRegistry.getLabelType(label.labelTypeId); - const labelTypeValue = labelType?.values.find((value) => value.id === label.labelTypeValueId); - if (!labelType || !labelTypeValue) { - return ["", 0]; - } - - const text = `${labelType.name}: ${labelTypeValue.text}`; - const width = calculateTextSize(text, "5pt sans-serif").width + DfdNodeLabelRenderer.LABEL_TEXT_PADDING; - - return [text, width]; - } - - renderSingleNodeLabel(node: ContainsDfdLabels & SNodeImpl, label: LabelAssignment, x: number, y: number): VNode { - const [text, width] = DfdNodeLabelRenderer.computeLabelContent(label); - const xLeft = x - width / 2; - const xRight = x + width / 2; - const height = DfdNodeLabelRenderer.LABEL_HEIGHT; - const radius = height / 2; - - const deleteLabelHandler = () => { - const action = DeleteLabelAssignmentAction.create(label, node); - this.actionDispatcher.dispatch(action); - }; - - return ( - - - - {text} - - { - // Put a x button to delete the element on the right upper edge - node.hoverFeedback ? ( - - - - X - - - ) : undefined - } - - ); - } - - /** - * Sorts the labels alphabetically by label type name (primary) and label type value text (secondary). - * - * @param labels the labels to sort. The operation is performed in-place. - */ - private sortLabels(labels: LabelAssignment[]): void { - labels.sort((a, b) => { - const labelTypeA = this.labelTypeRegistry?.getLabelType(a.labelTypeId); - const labelTypeB = this.labelTypeRegistry?.getLabelType(b.labelTypeId); - if (!labelTypeA || !labelTypeB) { - return 0; - } - - if (labelTypeA.name < labelTypeB.name) { - return -1; - } else if (labelTypeA.name > labelTypeB.name) { - return 1; - } else { - const labelTypeValueA = labelTypeA.values.find((value) => value.id === a.labelTypeValueId); - const labelTypeValueB = labelTypeB.values.find((value) => value.id === b.labelTypeValueId); - if (!labelTypeValueA || !labelTypeValueB) { - return 0; - } - - return labelTypeValueA.text.localeCompare(labelTypeValueB.text); - } - }); - } - - renderNodeLabels( - node: ContainsDfdLabels & SNodeImpl, - baseY: number, - xOffset = 0, - labelSpacing = DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT, - ): VNode | undefined { - if (this.settingsManager.simplifyNodeNames) { - return undefined; - } - this.sortLabels(node.labels); - return ( - - {node.labels.map((label, i) => { - const x = node.bounds.width / 2; - const y = baseY + i * labelSpacing; - return this.renderSingleNodeLabel(node, label, x + xOffset, y); - })} - - ); - } -} diff --git a/frontend/webEditor/src/features/labels/labelTypeEditor.css b/frontend/webEditor/src/features/labels/labelTypeEditor.css deleted file mode 100644 index 19839b30..00000000 --- a/frontend/webEditor/src/features/labels/labelTypeEditor.css +++ /dev/null @@ -1,69 +0,0 @@ -/* General UI */ - -div.label-type-editor-ui { - padding: 10px; - top: 150px; - right: 40px; - /* limit height so that max width has still 40px to bottom edge of the parent element - 100% is the full sprotty viewer height, 150px the space above the element, - 40px is the space that should be left under the editor and 2*10px is the padding */ - max-height: calc(100vh - 150px - 40px - 2 * 10px); - overflow: auto; /* Show a scroll bar if content in the editor does not fully fit */ -} - -.label-type-editor-ui * { - color: var(--color-foreground); -} - -.label-type-editor-ui .codicon { - vertical-align: middle; -} - -.label-type-editor-ui hr { - height: 1px; - border: 0; - background-color: var(--color-foreground); -} - -.label-type-editor-ui button { - background-color: transparent; - border: none; - cursor: pointer; - padding: 0; -} - -/* Label Types */ - -.label-type { - padding-bottom: 5px; -} - -.label-type-value, -.label-type-value-add { - margin-left: 10px; -} - -.label-type-name { - font-size: 12pt; -} - -/* This is the input field for the label type name */ -.label-type-editor-ui input { - background-color: transparent; - outline: none; - border: none; -} - -/* Label Type value */ -.label-type-value input { - background-color: var(--color-background); - text-align: center; - border: 1px solid var(--color-foreground); - border-radius: 15px; - padding: 3px; - margin: 4px; -} - -#accordion-state-label-title { - padding-right: 5px; -} diff --git a/frontend/webEditor/src/features/labels/labelTypeEditor.ts b/frontend/webEditor/src/features/labels/labelTypeEditor.ts deleted file mode 100644 index b5232e2f..00000000 --- a/frontend/webEditor/src/features/labels/labelTypeEditor.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { injectable, inject, optional } from "inversify"; -import { calculateTextSize, generateRandomSprottyId } from "../../utils"; -import { - AbstractUIExtension, - CommandStack, - CommitModelAction, - IActionDispatcher, - ISnapper, - KeyListener, - SModelElementImpl, - TYPES, -} from "sprotty"; -import { LabelAssignment, LabelType, LabelTypeRegistry, LabelTypeValue } from "./labelTypeRegistry"; -import { AddLabelAssignmentAction, DeleteLabelTypeAction, DeleteLabelTypeValueAction } from "./commands"; -import { LABEL_ASSIGNMENT_MIME_TYPE } from "./dropListener"; -import { Action } from "sprotty-protocol"; -import { snapPortsOfNode } from "../dfdElements/portSnapper"; -import { DfdNodeImpl } from "../dfdElements/nodes"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import { EditorModeController } from "../editorMode/editorModeController"; - -import "../../common/commonStyling.css"; -import "./labelTypeEditor.css"; - -@injectable() -export class LabelTypeEditorUI extends AbstractUIExtension implements KeyListener { - private accordionStateElement: HTMLInputElement = document.createElement("input"); - - constructor( - @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, - @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, - @inject(TYPES.ICommandStack) private readonly commandStack: CommandStack, - @inject(TYPES.ISnapper) private readonly snapper: ISnapper, - @inject(EditorModeController) - @optional() - private readonly editorModeController: EditorModeController, - ) { - super(); - labelTypeRegistry.onUpdate(() => this.reRender()); - - this.accordionStateElement.type = "checkbox"; - this.accordionStateElement.id = "accordion-state-label-types"; - this.accordionStateElement.classList.add("accordion-state"); - this.accordionStateElement.hidden = true; - } - - static readonly ID = "label-type-editor-ui"; - - id(): string { - return LabelTypeEditorUI.ID; - } - - containerClass(): string { - return LabelTypeEditorUI.ID; - } - - private reRender(): void { - if (!this.containerElement) { - // The ui extension has not been initialized yet. - return; - } - - // Remove all children - this.containerElement.innerHTML = ""; - // Re-render - this.initializeContents(this.containerElement); - - // Re-render sprotty model viewport by dispatching some command. - // sprotty automatically triggers a re-render after any command is executed as it may change the model. - - // CommitModelAction is a great idea because that way we don't have to call it - // each time we do some operation on the model inside the UI, like when removing a label type, - // we also need to commit the removal from the model. - // We can just do it here and not worry about it in the buttons/change handlers inside the ui. - // All changes are propagated through the label type registry. - this.actionDispatcher.dispatch(CommitModelAction.create()); - } - - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.innerHTML = ` - -
-
-
- `; - // Add input used by the label and the accordion-content div - // This element is not re-created on new renders and reused to save the expansion state of the accordion - // This is important because the ui is re-rendered on every change to the label type registry - containerElement.prepend(this.accordionStateElement); - - const innerContainerElement = containerElement.querySelector(".label-type-edit-ui-inner"); - if (!innerContainerElement) { - throw new Error("Could not find inner container element"); - } - - this.labelTypeRegistry.getLabelTypes().forEach((labelType, idx) => { - innerContainerElement.appendChild(this.renderLabelType(labelType)); - - if (idx < this.labelTypeRegistry.getLabelTypes().length - 1) { - // Add a horizontal line between label types - const horizontalLine = document.createElement("hr"); - innerContainerElement.appendChild(horizontalLine); - } - }); - - // Render add button for whole label type - const addButton = document.createElement("button"); - addButton.innerHTML = ' Label Type'; - addButton.onclick = () => { - if (this.editorModeController?.isReadOnly()) { - return; - } - - const labelType: LabelType = { - id: generateRandomSprottyId(), - name: "", - values: [ - { - id: generateRandomSprottyId(), - text: "Value", - }, - ], - }; - this.labelTypeRegistry.registerLabelType(labelType); - - // Select the text input element of the new label type to allow entering the name - const inputElement: HTMLElement | null = innerContainerElement.querySelector( - `.label-type-${labelType.id} input`, - ); - inputElement?.focus(); - }; - innerContainerElement.appendChild(addButton); - } - - private renderLabelType(labelType: LabelType): HTMLElement { - const labelTypeElement = document.createElement("div"); - labelTypeElement.classList.add("label-type"); - labelTypeElement.classList.add(`label-type-${labelType.id}`); - - const labelTypeNameInput = document.createElement("input"); - labelTypeNameInput.value = labelType.name; - labelTypeNameInput.placeholder = "Label Type Name"; - labelTypeNameInput.classList.add("label-type-name"); - - this.dynamicallySetInputSize(labelTypeNameInput); - - // Disallow spaces in label type names and changes when readonly - labelTypeNameInput.onbeforeinput = (event) => { - if (event.data?.includes(" ")) { - event.preventDefault(); - } - - if (this.editorModeController && this.editorModeController.isReadOnly()) { - event.preventDefault(); - } - }; - - labelTypeNameInput.onchange = () => { - const newLabelTypeName = labelTypeNameInput.value; - // Check for duplicate and don't change the name if it is a duplicate - if (this.labelTypeRegistry.getLabelTypes().some((type) => type.name === newLabelTypeName)) { - // Undo change in UI - labelTypeNameInput.value = labelType.name; - return; - } - - labelType.name = labelTypeNameInput.value; - this.labelTypeRegistry.labelTypeChanged(); - this.reSnapPorts(labelType.id); - }; - // Only allow alphanumerical characters - labelTypeNameInput.oninput = this.onInputOnlyAlphanumeric; - - labelTypeElement.appendChild(labelTypeNameInput); - - const deleteButton = document.createElement("button"); - deleteButton.innerHTML = ''; - deleteButton.onclick = () => { - this.actionDispatcher.dispatch(DeleteLabelTypeAction.create(this.labelTypeRegistry, labelType.id)); - }; - labelTypeElement.appendChild(deleteButton); - - labelType.values.forEach((possibleValue) => { - labelTypeElement.appendChild(this.renderLabelTypeValue(labelType, possibleValue)); - }); - - // Add + button - const addButton = document.createElement("button"); - addButton.classList.add("label-type-value-add"); - addButton.innerHTML = ' Value'; - addButton.onclick = () => { - if (this.editorModeController?.isReadOnly()) { - return; - } - - const labelValue: LabelTypeValue = { - id: generateRandomSprottyId(), - text: "", - }; - labelType.values.push(labelValue); - - // Insert label type last but before the button - const newValueElement = this.renderLabelTypeValue(labelType, labelValue); - labelTypeElement.insertBefore(newValueElement, labelTypeElement.lastChild); - - // Select the text input element of the new value to allow entering the value - newValueElement.querySelector("input")?.focus(); - }; - labelTypeElement.appendChild(addButton); - - return labelTypeElement; - } - - private renderLabelTypeValue(labelType: LabelType, labelTypeValue: LabelTypeValue): HTMLElement { - const valueElement = document.createElement("div"); - valueElement.classList.add("label-type-value"); - - const valueInput = document.createElement("input"); - valueInput.value = labelTypeValue.text; - valueInput.placeholder = "Value"; - this.dynamicallySetInputSize(valueInput); - - // Disallow spaces in label type values and changes when readonly - valueInput.onbeforeinput = (event) => { - if (event.data?.includes(" ")) { - event.preventDefault(); - } - - if (this.editorModeController && this.editorModeController.isReadOnly()) { - event.preventDefault(); - } - }; - - valueInput.onchange = () => { - const newValue = valueInput.value; - // Check for duplicate and don't change the value if it is a duplicate - if (labelType.values.some((value) => value.text === newValue)) { - // Undo change in UI - valueInput.value = labelTypeValue.text; - return; - } - - labelTypeValue.text = valueInput.value; - this.labelTypeRegistry.labelTypeChanged(); - this.reSnapPorts(labelType.id); - }; - // Only allow alphanumerical characters - valueInput.oninput = this.onInputOnlyAlphanumeric; - - // Allow dragging to create a label assignment - valueInput.draggable = true; - valueInput.ondragstart = (event) => { - const assignment: LabelAssignment = { - labelTypeId: labelType.id, - labelTypeValueId: labelTypeValue.id, - }; - const assignmentJson = JSON.stringify(assignment); - event.dataTransfer?.setData(LABEL_ASSIGNMENT_MIME_TYPE, assignmentJson); - }; - - valueInput.onclick = () => { - if (valueInput.getAttribute("clicked") === "true") { - return; - } - - valueInput.setAttribute("clicked", "true"); - setTimeout(() => { - if (valueInput.getAttribute("clicked") === "true") { - this.actionDispatcher.dispatch( - AddLabelAssignmentAction.create({ - labelTypeId: labelType.id, - labelTypeValueId: labelTypeValue.id, - }), - ); - valueInput.removeAttribute("clicked"); - } - }, 500); - }; - valueInput.ondblclick = () => { - valueInput.removeAttribute("clicked"); - valueInput.focus(); - }; - valueInput.onfocus = (event) => { - // we check for the single click here, since this gets triggered before the ondblclick event - if (valueInput.getAttribute("clicked") !== "true") { - event.preventDefault(); - // the blur needs to occur with a delay, as otherwise chromium browsers prevent the drag - setTimeout(() => { - valueInput.blur(); - }, 0); - } - }; - - valueElement.appendChild(valueInput); - - const deleteButton = document.createElement("button"); - deleteButton.innerHTML = ''; - deleteButton.onclick = () => { - this.actionDispatcher.dispatch( - DeleteLabelTypeValueAction.create(this.labelTypeRegistry, labelType.id, labelTypeValue.id), - ); - }; - valueElement.appendChild(deleteButton); - return valueElement; - } - - /** - * Sets and dynamically updates the size property of the passed input element. - * When the text is zero the width is set to the placeholder length to make place for it. - * When the text is changed the size gets updated with the keyup event. - * @param inputElement the html dom input element to set the size property for - */ - private dynamicallySetInputSize(inputElement: HTMLInputElement): void { - const handleResize = () => { - const displayText = inputElement.value || inputElement.placeholder; - const { width } = calculateTextSize(displayText, window.getComputedStyle(inputElement).font); - - // Values have higher padding for the rounded border - const widthPadding = inputElement.classList.contains("label-type-name") ? 2 : 8; - const finalWidth = width + widthPadding; - - inputElement.style.width = finalWidth + "px"; - }; - - inputElement.onkeyup = handleResize; - - // The inputElement is not added to the DOM yet, so we cannot set the size now. - // Wait for next JS tick, after which the element has been added to the DOM and we can set the initial size - setTimeout(handleResize, 0); - } - - keyDown(_element: SModelElementImpl, event: KeyboardEvent): Action[] { - // Toggle the accordion on press of T - if (matchesKeystroke(event, "KeyT")) { - this.accordionStateElement.checked = !this.accordionStateElement.checked; - } - - return []; - } - - keyUp(): Action[] { - return []; - } - - /** - * Re-snaps the ports of all nodes that have a label of the changed label type. - * When a label type or label type value is changed the size of nodes that - * use that label type could be changed to fit the longer/shorter label text - * more appropriately. Because of the node size change the ports may be out of - * place and need to be re-snapped. - * - * This is called only for changes made by the UI because label type deletions - * handle the resnapping in the corresponding commands and for addition - * it is not necessary because the label type or value cannot be used already. - */ - private async reSnapPorts(changedLabelTypeId: string): Promise { - const root = await this.commandStack.executeAll([]); - root.children.forEach((node) => { - if (!(node instanceof DfdNodeImpl)) { - return; - } - - // Only do the snapping if the node contains a label of the changed label type - if (node.labels.some((labelAs) => labelAs.labelTypeId === changedLabelTypeId)) { - snapPortsOfNode(node, this.snapper); - } - }); - - // Commit the model to trigger a re-render and save the changes into the model source - this.actionDispatcher.dispatch(CommitModelAction.create()); - } - - /** - * onInput handler for inputs that restricts the input to only alphanumeric characters and underscores - */ - private onInputOnlyAlphanumeric(event: Event): void { - const input = event.target as HTMLInputElement; - input.value = input.value.replace(/[^a-zA-Z0-9_]/g, ""); - } -} diff --git a/frontend/webEditor/src/features/labels/labelTypeRegistry.ts b/frontend/webEditor/src/features/labels/labelTypeRegistry.ts deleted file mode 100644 index 7f71d3f9..00000000 --- a/frontend/webEditor/src/features/labels/labelTypeRegistry.ts +++ /dev/null @@ -1,59 +0,0 @@ -export interface LabelType { - id: string; - name: string; - values: LabelTypeValue[]; -} - -export interface LabelTypeValue { - id: string; - text: string; -} - -export interface LabelAssignment { - labelTypeId: string; - labelTypeValueId: string; -} - -export class LabelTypeRegistry { - private labelTypes: LabelType[] = []; - private updateCallbacks: (() => void)[] = []; - - public registerLabelType(labelType: LabelType): void { - this.labelTypes.push(labelType); - this.updateCallbacks.forEach((cb) => cb()); - } - - public unregisterLabelType(labelType: LabelType): void { - this.labelTypes = this.labelTypes.filter((type) => type.id !== labelType.id); - this.updateCallbacks.forEach((cb) => cb()); - } - - public clearLabelTypes(): void { - this.labelTypes = []; - this.updateCallbacks.forEach((cb) => cb()); - } - - public labelTypeChanged(): void { - this.updateCallbacks.forEach((cb) => cb()); - } - - public onUpdate(callback: () => void): void { - this.updateCallbacks.push(callback); - } - - public getLabelTypes(): LabelType[] { - return this.labelTypes; - } - - public getLabelType(id: string): LabelType | undefined { - return this.labelTypes.find((type) => type.id === id); - } -} - -// Usually we would add the registry to a inversify container module and inject it using dependency injection where needed. -// Sadly some places where the registry is used are not inside a inversify container. -// An example for this are the node implementation classes that need the registry to compute the bounds of the node. -// These classes are not managed by inversify and therefore we cannot inject the registry there. -// To solve this we export this registry instance as a global variable for these situations. -// For all other situations where inversify can be used this exact same instance is available to be injected as well. -export const globalLabelTypeRegistry = new LabelTypeRegistry(); diff --git a/frontend/webEditor/src/features/serialize/analyze.ts b/frontend/webEditor/src/features/serialize/analyze.ts deleted file mode 100644 index 30c562cc..00000000 --- a/frontend/webEditor/src/features/serialize/analyze.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { Command, CommandExecutionContext, LocalModelSource, SModelRootImpl, TYPES } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { EditorModeController } from "../editorMode/editorModeController"; -import { sendMessage } from "./webSocketHandler"; -import { ConstraintRegistry } from "../constraintMenu/constraintRegistry"; -import { CURRENT_VERSION, SavedDiagram } from "./save"; -import { LoadingIndicator } from "../../common/loadingIndicator"; - -export interface AnalyzeDiagramAction extends Action { - kind: typeof AnalyzeDiagramAction.KIND; - suggestedFileName: string; -} -export namespace AnalyzeDiagramAction { - export const KIND = "analyze-diagram"; - - export function create(suggestedFileName?: string): AnalyzeDiagramAction { - return { - kind: KIND, - suggestedFileName: suggestedFileName ?? "diagram.json", - }; - } -} - -@injectable() -export class AnalyzeDiagramCommand extends Command { - static readonly KIND = AnalyzeDiagramAction.KIND; - @inject(TYPES.ModelSource) - private modelSource: LocalModelSource = new LocalModelSource(); - @inject(DynamicChildrenProcessor) - private dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(LabelTypeRegistry) - @optional() - private labelTypeRegistry?: LabelTypeRegistry; - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - @inject(ConstraintRegistry) - @optional() - private readonly constraintRegistry?: ConstraintRegistry; - @inject(LoadingIndicator) - @optional() - private loadingIndicator?: LoadingIndicator; - - constructor() { - super(); - } - - execute(context: CommandExecutionContext): SModelRootImpl { - this.loadingIndicator?.showIndicator("Analyzing diagram..."); - // Convert the model to JSON - // Do a copy because we're going to modify it - const modelCopy = JSON.parse(JSON.stringify(this.modelSource.model)); - // Remove element children that are implementation detail - this.dynamicChildrenProcessor.processGraphChildren(modelCopy, "remove"); - - // Export the diagram as a JSON data URL. - const diagram: SavedDiagram = { - model: modelCopy, - labelTypes: this.labelTypeRegistry?.getLabelTypes(), - constraints: this.constraintRegistry?.getConstraintList(), - mode: this.editorModeController?.getCurrentMode(), - version: CURRENT_VERSION, - }; - const diagramJson = JSON.stringify(diagram, undefined, 4); - sendMessage("Json:" + diagramJson); - return context.root; - } - - // Saving cannot be meaningfully undone/redone - - undo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - redo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } -} diff --git a/frontend/webEditor/src/features/serialize/defaultDiagram.json b/frontend/webEditor/src/features/serialize/defaultDiagram.json deleted file mode 100644 index cf59979a..00000000 --- a/frontend/webEditor/src/features/serialize/defaultDiagram.json +++ /dev/null @@ -1,623 +0,0 @@ -{ - "model": { - "canvasBounds": { - "x": 0, - "y": 0, - "width": 1278, - "height": 1324 - }, - "scroll": { - "x": 181.68489464915504, - "y": -12.838536201820945 - }, - "zoom": 6.057478948161569, - "position": { - "x": 0, - "y": 0 - }, - "size": { - "width": -1, - "height": -1 - }, - "features": {}, - "type": "graph", - "id": "root", - "children": [ - { - "position": { - "x": 84, - "y": 54 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "text": "User", - "labels": [ - { - "labelTypeId": "gvia09", - "labelTypeValueId": "g10hr" - } - ], - "ports": [ - { - "position": { - "x": 58.5, - "y": 7 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "nhcrad", - "type": "port:dfd-input", - "children": [] - }, - { - "position": { - "x": 31, - "y": 38.5 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "behavior": "set Sensitivity.Personal", - "features": {}, - "id": "4wbyft", - "type": "port:dfd-output", - "children": [] - }, - { - "position": { - "x": 58.5, - "y": 25.5 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "behavior": "set Sensitivity.Public", - "features": {}, - "id": "wksxi8", - "type": "port:dfd-output", - "children": [] - } - ], - "features": {}, - "id": "7oii5l", - "type": "node:input-output", - "children": [] - }, - { - "position": { - "x": 249, - "y": 67 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "text": "view", - "labels": [], - "ports": [ - { - "position": { - "x": -3.5, - "y": 13 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "ti4ri7", - "type": "port:dfd-input", - "children": [] - }, - { - "position": { - "x": 58.5, - "y": 13 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "behavior": "forward request", - "features": {}, - "id": "bsqjm", - "type": "port:dfd-output", - "children": [] - } - ], - "features": {}, - "id": "0bh7yh", - "type": "node:function", - "children": [] - }, - { - "position": { - "x": 249, - "y": 22 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "text": "display", - "labels": [], - "ports": [ - { - "position": { - "x": 58.5, - "y": 15 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "0hfzu", - "type": "port:dfd-input", - "children": [] - }, - { - "position": { - "x": -3.5, - "y": 9 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "behavior": "forward items", - "features": {}, - "id": "y1p7qq", - "type": "port:dfd-output", - "children": [] - } - ], - "features": {}, - "id": "4myuyr", - "type": "node:function", - "children": [] - }, - { - "position": { - "x": 364, - "y": 152 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "text": "encrypt", - "labels": [], - "ports": [ - { - "position": { - "x": -3.5, - "y": 15.5 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "kqjy4g", - "type": "port:dfd-input", - "children": [] - }, - { - "position": { - "x": 29, - "y": -3.5 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "behavior": "forward data\nset Encryption.Encrypted", - "features": {}, - "id": "3wntc", - "type": "port:dfd-output", - "children": [] - } - ], - "features": {}, - "id": "3n988k", - "type": "node:function", - "children": [] - }, - { - "position": { - "x": 104, - "y": 157 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "text": "buy", - "labels": [], - "ports": [ - { - "position": { - "x": 19, - "y": -3.5 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "2331e8", - "type": "port:dfd-input", - "children": [] - }, - { - "position": { - "x": 58.5, - "y": 10.5 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "behavior": "forward data", - "features": {}, - "id": "vnkg73", - "type": "port:dfd-output", - "children": [] - } - ], - "features": {}, - "id": "z9v1jp", - "type": "node:function", - "children": [] - }, - { - "position": { - "x": 233.5, - "y": 157 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "text": "process", - "labels": [], - "ports": [ - { - "position": { - "x": -3.5, - "y": 10.5 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "xyepdb", - "type": "port:dfd-input", - "children": [] - }, - { - "position": { - "x": 59.5, - "y": 10.5 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "behavior": "forward data", - "features": {}, - "id": "eedb56", - "type": "port:dfd-output", - "children": [] - } - ], - "features": {}, - "id": "js61f", - "type": "node:function", - "children": [] - }, - { - "position": { - "x": 422.5, - "y": 59 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "text": "Database", - "labels": [ - { - "labelTypeId": "gvia09", - "labelTypeValueId": "5hnugm" - } - ], - "ports": [ - { - "position": { - "x": -3.5, - "y": 23 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "scljwi", - "type": "port:dfd-input", - "children": [] - }, - { - "position": { - "x": -3.5, - "y": 0.5 - }, - "size": { - "width": -1, - "height": -1 - }, - "strokeWidth": 0, - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "behavior": "set Sensitivity.Public", - "features": {}, - "id": "1j7bn5", - "type": "port:dfd-output", - "children": [] - } - ], - "features": {}, - "id": "8j2r1g", - "type": "node:storage", - "children": [] - }, - { - "routingPoints": [], - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "vq8g3l", - "type": "edge:arrow", - "sourceId": "4wbyft", - "targetId": "2331e8", - "text": "data", - "children": [] - }, - { - "routingPoints": [], - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "xrzc19", - "type": "edge:arrow", - "sourceId": "vnkg73", - "targetId": "xyepdb", - "text": "data", - "children": [] - }, - { - "routingPoints": [], - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "ufflto", - "type": "edge:arrow", - "sourceId": "eedb56", - "targetId": "kqjy4g", - "text": "data", - "children": [] - }, - { - "routingPoints": [], - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "ojjvtp", - "type": "edge:arrow", - "sourceId": "3wntc", - "targetId": "scljwi", - "text": "data", - "children": [] - }, - { - "routingPoints": [], - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "c9n88l", - "type": "edge:arrow", - "sourceId": "bsqjm", - "targetId": "scljwi", - "text": "request", - "children": [] - }, - { - "routingPoints": [], - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "uflsc", - "type": "edge:arrow", - "sourceId": "wksxi8", - "targetId": "ti4ri7", - "text": "request", - "routerKind": "polyline", - "children": [] - }, - { - "routingPoints": [], - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "n81f3b", - "type": "edge:arrow", - "sourceId": "1j7bn5", - "targetId": "0hfzu", - "text": "items", - "children": [] - }, - { - "routingPoints": [], - "selected": false, - "hoverFeedback": false, - "opacity": 1, - "features": {}, - "id": "hi397b", - "type": "edge:arrow", - "sourceId": "y1p7qq", - "targetId": "nhcrad", - "text": "items", - "children": [] - } - ] - }, - "labelTypes": [ - { - "id": "4h3wzk", - "name": "Sensitivity", - "values": [ - { - "id": "zzvphn", - "text": "Personal" - }, - { - "id": "veaan9", - "text": "Public" - } - ] - }, - { - "id": "gvia09", - "name": "Location", - "values": [ - { - "id": "g10hr", - "text": "EU" - }, - { - "id": "5hnugm", - "text": "nonEU" - } - ] - }, - { - "id": "84rllz", - "name": "Encryption", - "values": [ - { - "id": "2r6xe6", - "text": "Encrypted" - } - ] - } - ], - "constraints": [ - { - "name": "Test", - "constraint": "data Sensitivity.Personal neverFlows vertex Location.nonEU" - } - ], - "mode": "edit", - "version": 1 -} diff --git a/frontend/webEditor/src/features/serialize/di.config.ts b/frontend/webEditor/src/features/serialize/di.config.ts deleted file mode 100644 index 9ac28f36..00000000 --- a/frontend/webEditor/src/features/serialize/di.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { TYPES, configureCommand } from "sprotty"; -import { LoadDiagramCommand } from "./load"; -import { SaveDiagramCommand } from "./save"; -import { LoadDefaultDiagramCommand } from "./loadDefaultDiagram"; -import { ContainerModule } from "inversify"; -import { SerializeKeyListener } from "./keyListener"; -import { SerializeDropHandler } from "./dropListener"; -import { AnalyzeDiagramCommand } from "./analyze"; -import { LoadDFDandDDCommand } from "./loadDFDandDD"; -import { SaveDFDandDDCommand } from "./saveDFDandDD"; -import { LoadPalladioCommand } from "./loadPalladio"; -import { SaveImageCommand } from "./image"; - -export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { - const context = { bind, unbind, isBound, rebind }; - configureCommand(context, LoadDiagramCommand); - configureCommand(context, LoadDefaultDiagramCommand); - configureCommand(context, SaveDiagramCommand); - configureCommand(context, AnalyzeDiagramCommand); - configureCommand(context, LoadDFDandDDCommand); - configureCommand(context, SaveDFDandDDCommand); - configureCommand(context, LoadPalladioCommand); - configureCommand(context, SaveImageCommand); - - bind(TYPES.KeyListener).to(SerializeKeyListener).inSingletonScope(); - bind(TYPES.MouseListener).to(SerializeDropHandler).inSingletonScope(); -}); diff --git a/frontend/webEditor/src/features/serialize/dropListener.ts b/frontend/webEditor/src/features/serialize/dropListener.ts deleted file mode 100644 index 8db156e3..00000000 --- a/frontend/webEditor/src/features/serialize/dropListener.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { inject, injectable } from "inversify"; -import { ILogger, MouseListener, SModelElementImpl, TYPES } from "sprotty"; -import { LoadDiagramAction } from "./load"; -import { Action } from "sprotty-protocol"; - -@injectable() -export class SerializeDropHandler extends MouseListener { - constructor(@inject(TYPES.ILogger) private readonly logger: ILogger) { - super(); - } - - drop(_target: SModelElementImpl, ev: DragEvent): (Action | Promise)[] { - this.logger.log(this, "Drop event detected", ev); - - // Prevent default behavior which would open the file in the browser - ev.preventDefault(); - - const file = ev.dataTransfer?.files[0]; - if (!file) { - return []; - } - - if (file.type !== "application/json") { - alert("Diagram file must be in JSON format"); - return []; - } - - return [LoadDiagramAction.create(file)]; - } -} diff --git a/frontend/webEditor/src/features/serialize/image.ts b/frontend/webEditor/src/features/serialize/image.ts deleted file mode 100644 index 2f6a9256..00000000 --- a/frontend/webEditor/src/features/serialize/image.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Command, CommandExecutionContext, CommandReturn } from "sprotty"; -// typescript does not recognize css files as modules -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-expect-error -import themeCss from "../../theme.css?raw"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-expect-error -import elementCss from "../dfdElements/elementStyles.css?raw"; -import { Action } from "sprotty-protocol"; -import { getModelFileName } from "../.."; - -export interface SaveImageAction extends Action { - kind: typeof SaveImageAction.KIND; -} -export namespace SaveImageAction { - export const KIND = "save-image"; - - export function create(): SaveImageAction { - return { - kind: KIND, - }; - } -} - -export class SaveImageCommand extends Command { - static readonly KIND = SaveImageAction.KIND; - execute(context: CommandExecutionContext): CommandReturn { - const root = document.getElementById("sprotty_root"); - if (!root) return context.root; - const firstChild = root.children[0]; - if (!firstChild) return context.root; - const innerSvg = firstChild.innerHTML; - /* The result svg will render (0,0) as the top left corner of the svg. - * We calculate the minimum translation of all children. - * We then offset the whole svg by this opposite of this amount. - */ - const minTranslate = { x: Infinity, y: Infinity }; - for (const child of firstChild.children) { - const childTranslate = this.getMinTranslate(child as HTMLElement); - minTranslate.x = Math.min(minTranslate.x, childTranslate.x); - minTranslate.y = Math.min(minTranslate.y, childTranslate.y); - } - const svg = `${innerSvg}`; - - const blob = new Blob([svg], { type: "image/svg+xml" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = getModelFileName() + ".svg"; - link.click(); - - return context.root; - } - undo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - redo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - - /** - * Gets the minimum translation of an element relative to the svg. - * This is done by recursively getting the translation of all child elements - * @param e the element to get the translation from - * @param parentOffset Offset of the containing element - * @returns Minimum absolute offset of any child element relative to the svg - */ - private getMinTranslate( - e: HTMLElement, - parentOffset: { x: number; y: number } = { x: 0, y: 0 }, - ): { x: number; y: number } { - const myTranslate = this.getTranslate(e, parentOffset); - const minTranslate = myTranslate; - - const children = e.children; - for (const child of children) { - const childTranslate = this.getMinTranslate(child as HTMLElement, myTranslate); - minTranslate.x = Math.min(minTranslate.x, childTranslate.x); - minTranslate.y = Math.min(minTranslate.y, childTranslate.y); - } - return minTranslate; - } - - /** - * Calculates the absolute translation of an element relative to the svg. - * If the element has no translation, the offset of the parent is returned. - * @param e the element to get the translation from - * @param parentOffset Offset of the containing element - * @returns Offset of the child relative to the svg - */ - private getTranslate( - e: HTMLElement, - parentOffset: { x: number; y: number } = { x: 0, y: 0 }, - ): { x: number; y: number } { - const transform = e.getAttribute("transform"); - if (!transform) return parentOffset; - const translateMatch = transform.match(/translate\(([^)]+)\)/); - if (!translateMatch) return parentOffset; - const translate = translateMatch[1].match(/(-?[0-9.]+)(?:, | |,)(-?[0-9.]+)/); - if (!translate) return parentOffset; - const x = parseFloat(translate[1]); - const y = parseFloat(translate[2]); - const newX = x + parentOffset.x; - const newY = y + parentOffset.y; - return { x: newX, y: newY }; - } -} diff --git a/frontend/webEditor/src/features/serialize/keyListener.ts b/frontend/webEditor/src/features/serialize/keyListener.ts deleted file mode 100644 index f4e40a6f..00000000 --- a/frontend/webEditor/src/features/serialize/keyListener.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { injectable } from "inversify"; -import { CommitModelAction, KeyListener, SModelElementImpl } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import { LoadDefaultDiagramAction } from "./loadDefaultDiagram"; -import { LoadDiagramAction } from "./load"; -import { SaveDiagramAction } from "./save"; -import { AnalyzeDiagramAction } from "./analyze"; - -@injectable() -export class SerializeKeyListener extends KeyListener { - keyDown(_element: SModelElementImpl, event: KeyboardEvent): Action[] { - if (matchesKeystroke(event, "KeyO", "ctrlCmd")) { - // Prevent the browser file open dialog from opening - event.preventDefault(); - - return [LoadDiagramAction.create(), CommitModelAction.create()]; - } else if (matchesKeystroke(event, "KeyO", "ctrlCmd", "shift")) { - event.preventDefault(); - return [LoadDefaultDiagramAction.create(), CommitModelAction.create()]; - } else if (matchesKeystroke(event, "KeyS", "ctrlCmd")) { - event.preventDefault(); - return [SaveDiagramAction.create()]; - } else if (matchesKeystroke(event, "KeyA", "ctrlCmd", "shift")) { - event.preventDefault(); - return [AnalyzeDiagramAction.create()]; - } - - return []; - } -} diff --git a/frontend/webEditor/src/features/serialize/load.ts b/frontend/webEditor/src/features/serialize/load.ts deleted file mode 100644 index 5ce725b1..00000000 --- a/frontend/webEditor/src/features/serialize/load.ts +++ /dev/null @@ -1,367 +0,0 @@ -import { - ActionDispatcher, - Command, - CommandExecutionContext, - EMPTY_ROOT, - ILogger, - NullLogger, - SModelRootImpl, - SNodeImpl, - TYPES, - isLocateable, -} from "sprotty"; -import { Action, SModelElement, SModelRoot } from "sprotty-protocol"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { inject, optional } from "inversify"; -import { createDefaultFitToScreenAction } from "../../utils"; -import { SavedDiagram } from "./save"; -import { LabelType, LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { LayoutModelAction } from "../autoLayout/command"; -import { EditorMode, EditorModeController } from "../editorMode/editorModeController"; -import { Constraint, ConstraintRegistry } from "../constraintMenu/constraintRegistry"; -import { LoadingIndicator } from "../../common/loadingIndicator"; -import { ChooseConstraintAction } from "../constraintMenu/actions"; - -export interface LoadDiagramAction extends Action { - kind: typeof LoadDiagramAction.KIND; - file: File | undefined; -} -export namespace LoadDiagramAction { - export const KIND = "load-diagram"; - - export function create(file?: File): LoadDiagramAction { - return { - kind: KIND, - file, - }; - } -} - -export class LoadDiagramCommand extends Command { - static readonly KIND = LoadDiagramAction.KIND; - - constructor(@inject(TYPES.Action) private readonly action: LoadDiagramAction) { - super(); - } - - // After loading a diagram, this command dispatches other actions like fit to screen - // and optional auto layouting. However when returning a new model in the execute method, - // the diagram is not directly updated. We need to wait for the - // InitializeCanvasBoundsCommand to be fired and finish before we can do things like fit to screen. - // Because of that we block the execution newly dispatched actions including - // the actions we dispatched after loading the diagram until - // the InitializeCanvasBoundsCommand has been processed. - // This works because the canvasBounds property is always removed before loading a diagram, - // requiring the InitializeCanvasBoundsCommand to be fired. - readonly blockUntil = LoadDiagramCommand.loadBlockUntilFn; - static readonly loadBlockUntilFn = (action: Action) => { - return action.kind === "initializeCanvasBounds"; - }; - - @inject(TYPES.ILogger) - private readonly logger: ILogger = new NullLogger(); - @inject(DynamicChildrenProcessor) - private readonly dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(TYPES.IActionDispatcher) - private readonly actionDispatcher: ActionDispatcher = new ActionDispatcher(); - @inject(LabelTypeRegistry) - @optional() - private readonly labelTypeRegistry?: LabelTypeRegistry; - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - @inject(ConstraintRegistry) - @optional() - private readonly constraintRegistry?: ConstraintRegistry; - @inject(LoadingIndicator) - @optional() - private readonly loadingIndicator?: LoadingIndicator; - - private oldRoot: SModelRootImpl | undefined; - private newRoot: SModelRootImpl | undefined; - private oldLabelTypes: LabelType[] | undefined; - private newLabelTypes: LabelType[] | undefined; - private oldEditorMode: EditorMode | undefined; - private newEditorMode: EditorMode | undefined; - private oldFileName: string | undefined; - private newFileName: string | undefined; - private oldConstrains: Constraint[] | undefined; - private newConstrains: Constraint[] | undefined; - - /** - * Gets the model file from the action or opens a file picker dialog if no file is provided. - * @returns A promise that resolves to the model file. - */ - private getModelFile(): Promise { - if (this.action.file) { - return Promise.resolve(this.action.file); - } - - // Open a file picker dialog if no file is provided in the action. - // The cleaner way to do this would be showOpenFilePicker(), - // but safari and firefox don't support it at the time of writing this code: - // https://developer.mozilla.org/en-US/docs/web/api/window/showOpenFilePicker#browser_compatibility - const input = document.createElement("input"); - input.type = "file"; - input.accept = "application/json"; - const fileLoadPromise = new Promise((resolve, reject) => { - // This event is fired when the user successfully submits the file picker dialog. - input.onchange = () => { - if (input.files && input.files.length > 0) { - const file = input.files[0]; - if (file.type !== "application/json") { - reject("Diagram file must be in JSON format"); - return; - } - - resolve(file); - } else { - reject("No file selected"); - } - }; - // The focus event is fired when the file picker dialog is closed. - // This includes cases where a file was selected and when the dialog was canceled and no file was selected. - // If a file was selected the change event above is fired after the focus event. - // So if a file was selected the focus event should be ignored and the promise is resolved in the onchange handler. - // If the file dialog was canceled undefined should be resolved by the focus handler. - // Because we don't know whether the change event will follow the focus event, - // we have a 300ms timeout before resolving the promise. - // If the promise was already resolved by the onchange handler, this won't do anything. - window.addEventListener( - "focus", - () => { - setTimeout(() => { - resolve(undefined); - }, 300); - }, - { once: true }, - ); - }); - input.click(); - - return fileLoadPromise; - } - - async execute(context: CommandExecutionContext): Promise { - this.loadingIndicator?.showIndicator("Loading model..."); - this.oldRoot = context.root; - try { - const file = await this.getModelFile(); - if (!file) { - // No file was selected, skip - this.loadingIndicator?.hideIndicator(); - return context.root; - } - - const newDiagram = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - const json = reader.result as string; - try { - const model = JSON.parse(json); - resolve(model); - } catch (error) { - reject(error); - } - }; - reader.onerror = () => { - reject(reader.error); - }; - reader.readAsText(file); - }); - - const newSchema = newDiagram?.model; - if (!newSchema) { - this.logger.info(this, "Model loading aborted"); - this.newRoot = this.oldRoot; - this.loadingIndicator?.hideIndicator(); - return this.oldRoot; - } - - // Load sprotty model - LoadDiagramCommand.preprocessModelSchema(newSchema); - this.dynamicChildrenProcessor.processGraphChildren(newSchema, "set"); - this.newRoot = context.modelFactory.createRoot(newSchema); - - this.logger.info(this, "Model loaded successfully"); - - if (this.labelTypeRegistry) { - // Load label types - this.oldLabelTypes = this.labelTypeRegistry.getLabelTypes(); - this.newLabelTypes = newDiagram?.labelTypes; - this.labelTypeRegistry.clearLabelTypes(); - if (newDiagram?.labelTypes) { - newDiagram.labelTypes.forEach((labelType) => { - this.labelTypeRegistry?.registerLabelType(labelType); - }); - - this.logger.info(this, "Label types loaded successfully"); - } - } - - if (this.editorModeController) { - // Load editor mode - this.oldEditorMode = this.editorModeController.getCurrentMode(); - this.newEditorMode = newDiagram?.mode; - if (newDiagram?.mode) { - this.editorModeController.setMode(newDiagram.mode); - } else { - this.editorModeController.setDefaultMode(); - } - - this.logger.info(this, "Editor mode loaded successfully"); - } - - if (this.constraintRegistry) { - // Load label types - this.oldConstrains = this.constraintRegistry.getConstraintList(); - this.newConstrains = newDiagram?.constraints; - this.constraintRegistry.clearConstraints(); - if (newDiagram?.constraints) { - this.constraintRegistry.setConstraintsFromArray(newDiagram.constraints); - - this.logger.info(this, "Constraints loaded successfully"); - } - } - - postLoadActions(this.newRoot, this.actionDispatcher); - - if (this.constraintRegistry) { - this.constraintRegistry.setAllConstraintsAsSelected(); - this.actionDispatcher.dispatch( - ChooseConstraintAction.create(this.constraintRegistry!.getConstraintList().map((c) => c.name)), - ); - } - - this.oldFileName = currentFileName; - this.newFileName = file.name; - setFileNameInPageTitle(file.name); - - this.loadingIndicator?.hideIndicator(); - return this.newRoot; - } catch (error) { - this.logger.error(this, "Error loading model", error); - alert("Error loading model: " + error); - this.newRoot = this.oldRoot; - - this.loadingIndicator?.hideIndicator(); - return this.oldRoot; - } - } - - /** - * Before a saved model schema can be loaded, it needs to be preprocessed. - * Currently this means that the features property is removed from all model elements recursively. - * Additionally the canvasBounds property is removed from the root element, because it may change - * depending on browser window. - * In the future this method may be extended to preprocess other properties. - * - * The feature property at runtime is a js Set with the relevant features. - * E.g. for the top graph this is the viewportFeature among others. - * When converting js Sets objects into json, the result is an empty js object. - * When loading the object is converted into an empty js Set and the features are lost. - * Because of this the editor won't work properly after loading a model. - * To prevent this, the features property is removed before loading the model. - * When the features property is missing it gets rebuilt on loading with the currently used features. - * - * @param modelSchema The model schema to preprocess - */ - public static preprocessModelSchema(modelSchema: SModelRoot): void { - // These properties are all not included in the root typing and if present are not loaded and handled correctly. So they are removed. - if ("features" in modelSchema) { - delete modelSchema["features"]; - } - if ("canvasBounds" in modelSchema) { - delete modelSchema["canvasBounds"]; - } - - if (modelSchema.children) { - modelSchema.children.forEach((child: SModelElement) => this.preprocessModelSchema(child)); - } - } - - undo(context: CommandExecutionContext): SModelRootImpl { - this.loadingIndicator?.showIndicator("Undoing load..."); - this.labelTypeRegistry?.clearLabelTypes(); - this.oldLabelTypes?.forEach((labelType) => this.labelTypeRegistry?.registerLabelType(labelType)); - if (this.oldEditorMode) { - this.editorModeController?.setMode(this.oldEditorMode); - } - this.constraintRegistry?.clearConstraints(); - if (this.oldConstrains) { - this.constraintRegistry?.setConstraintsFromArray(this.oldConstrains); - } - setFileNameInPageTitle(this.oldFileName); - - this.loadingIndicator?.hideIndicator(); - return this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); - } - - redo(context: CommandExecutionContext): SModelRootImpl { - this.loadingIndicator?.showIndicator("Redoing load..."); - this.labelTypeRegistry?.clearLabelTypes(); - this.newLabelTypes?.forEach((labelType) => this.labelTypeRegistry?.registerLabelType(labelType)); - if (this.editorModeController) { - if (this.newEditorMode) { - this.editorModeController.setMode(this.newEditorMode); - } else { - this.editorModeController.setDefaultMode(); - } - } - this.constraintRegistry?.clearConstraints(); - if (this.newConstrains) { - this.constraintRegistry?.setConstraintsFromArray(this.newConstrains); - } - setFileNameInPageTitle(this.newFileName); - - this.loadingIndicator?.hideIndicator(); - return this.newRoot ?? this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); - } -} - -/** - * Utility function to fit the diagram to the screen after loading a model inside a command. - * Captures all element ids and dispatches a FitToScreenAction. - * Also performs auto layouting if there are unpositioned nodes. - */ -export async function postLoadActions( - newRoot: SModelRootImpl | undefined, - actionDispatcher: ActionDispatcher, -): Promise { - if (!newRoot) { - return; - } - - // Layouting: - const containsUnPositionedNodes = newRoot.children - .filter((child) => child instanceof SNodeImpl) - .some((child) => isLocateable(child) && child.position.x === 0 && child.position.y === 0); - if (containsUnPositionedNodes) { - await actionDispatcher.dispatch(LayoutModelAction.create()); - } - - // fit to screen is done after auto layouting because that may change the bounds of the diagram - // requiring another fit to screen. - await actionDispatcher.dispatch(createDefaultFitToScreenAction(newRoot, false)); -} - -let initialPageTitle: string | undefined; -export let currentFileName: string | undefined; - -/** - * Sets the file name in the page title. - * If the given file name is undefined, no file name is displayed in the page title. - * The current file name is stored in the exported currentFileName variable. - */ -export function setFileNameInPageTitle(filename: string | undefined) { - if (!initialPageTitle) { - initialPageTitle = document.title; - } - - currentFileName = filename; - if (filename) { - document.title = `${filename} - ${initialPageTitle}`; - } else { - document.title = initialPageTitle; - } -} diff --git a/frontend/webEditor/src/features/serialize/loadDFDandDD.ts b/frontend/webEditor/src/features/serialize/loadDFDandDD.ts deleted file mode 100644 index b0a646d0..00000000 --- a/frontend/webEditor/src/features/serialize/loadDFDandDD.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Command, CommandExecutionContext, ILogger, NullLogger, SModelRootImpl, TYPES } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { sendMessage } from "./webSocketHandler"; -import { setModelFileName } from "../../index"; -import { setFileNameInPageTitle } from "./load"; -import { inject, optional } from "inversify"; -import { LoadingIndicator } from "../../common/loadingIndicator"; - -export interface LoadDFDandDDAction extends Action { - kind: typeof LoadDFDandDDAction.KIND; - file: File | undefined; -} -export namespace LoadDFDandDDAction { - export const KIND = "load-dfd"; - - export function create(file?: File): LoadDFDandDDAction { - return { - kind: KIND, - file, - }; - } -} - -export class LoadDFDandDDCommand extends Command { - static readonly KIND = LoadDFDandDDAction.KIND; - - @inject(TYPES.ILogger) - private readonly logger: ILogger = new NullLogger(); - @inject(LoadingIndicator) - @optional() - protected loadingIndicator?: LoadingIndicator; - - constructor() { - super(); - } - - /** - * Gets the model file from the action or opens a file picker dialog if no file is provided. - * @returns A promise that resolves to the model file. - */ - private getModelFiles(): Promise { - // Open a file picker dialog if no file is provided in the action. - // The cleaner way to do this would be showOpenFilePicker(), - // but safari and firefox don't support it at the time of writing this code: - // https://developer.mozilla.org/en-US/docs/web/api/window/showOpenFilePicker#browser_compatibility - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".dataflowdiagram, .datadictionary"; - input.multiple = true; - - const fileLoadPromise = new Promise((resolve, reject) => { - // This event is fired when the user successfully submits the file picker dialog. - input.onchange = () => { - if (input.files && input.files.length === 2) { - const files = Array.from(input.files); - const dataflowFile = files.find((file) => file.name.endsWith(".dataflowdiagram")); - const dictionaryFile = files.find((file) => file.name.endsWith(".datadictionary")); - - if (dataflowFile && dictionaryFile) { - resolve([dataflowFile, dictionaryFile]); - } else { - reject("Please select one .dataflowdiagram file and one .datadictionary file."); - } - } else { - reject("You must select exactly two files: one .dataflowdiagram and one .datadictionary."); - } - }; - }); - input.click(); - - return fileLoadPromise; - } - - async execute(context: CommandExecutionContext): Promise { - this.loadingIndicator?.showIndicator("Loading DFD and DD files..."); - try { - const [dataflowFile, dictionaryFile] = (await this.getModelFiles()) ?? []; - - // Read the content of both files - const dataflowFileContent = await this.readFileContent(dataflowFile); - const dictionaryFileContent = await this.readFileContent(dictionaryFile); - - // Send each file's content in separate WebSocket messages - sendMessage( - "DFD:" + - this.getFileNameWithoutExtension(dataflowFile) + - ":" + - dataflowFileContent + - "\n:DD:\n" + - dictionaryFileContent, - ); - setModelFileName(dataflowFile.name.substring(0, dataflowFile.name.lastIndexOf("."))); - setFileNameInPageTitle(dataflowFile.name); - return context.root; - } catch (error) { - this.logger.error(this, (error as Error).message); - this.loadingIndicator?.hideIndicator(); - return context.root; - } - } - - undo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - redo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - /** - * Utility function to read the content of a file as a string. - * @param file The file to read. - * @returns A promise that resolves to the file content as a string. - */ - private readFileContent(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.onerror = () => reject(reader.error); - reader.readAsText(file); - }); - } - - getFileNameWithoutExtension(file: File): string { - const fileName = file.name; - return fileName.substring(0, fileName.lastIndexOf(".")) || fileName; - } -} diff --git a/frontend/webEditor/src/features/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/features/serialize/loadDefaultDiagram.ts deleted file mode 100644 index 88cf830a..00000000 --- a/frontend/webEditor/src/features/serialize/loadDefaultDiagram.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { - ActionDispatcher, - Command, - CommandExecutionContext, - CommandReturn, - EMPTY_ROOT, - ILogger, - NullLogger, - SModelRootImpl, - TYPES, -} from "sprotty"; -import { Action } from "sprotty-protocol"; -import { LabelType, LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { LoadDiagramCommand, currentFileName, postLoadActions, setFileNameInPageTitle } from "./load"; -import { SavedDiagram } from "./save"; -import { ConstraintRegistry } from "../constraintMenu/constraintRegistry"; -import { EditorMode, EditorModeController } from "../editorMode/editorModeController"; - -import defaultDiagramData from "./defaultDiagram.json"; -const defaultDiagram = defaultDiagramData as SavedDiagram; - -export interface LoadDefaultDiagramAction extends Action { - readonly kind: typeof LoadDefaultDiagramAction.KIND; -} -export namespace LoadDefaultDiagramAction { - export const KIND = "loadDefaultDiagram"; - - export function create(): LoadDefaultDiagramAction { - return { - kind: KIND, - }; - } -} - -@injectable() -export class LoadDefaultDiagramCommand extends Command { - readonly blockUntil = LoadDiagramCommand.loadBlockUntilFn; - - static readonly KIND = LoadDefaultDiagramAction.KIND; - @inject(TYPES.ILogger) - private readonly logger: ILogger = new NullLogger(); - @inject(DynamicChildrenProcessor) - private readonly dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(TYPES.IActionDispatcher) - private readonly actionDispatcher: ActionDispatcher = new ActionDispatcher(); - @inject(LabelTypeRegistry) - @optional() - private readonly labelTypeRegistry?: LabelTypeRegistry; - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - @inject(ConstraintRegistry) - @optional() - private readonly constraintRegistry?: ConstraintRegistry; - - private oldRoot: SModelRootImpl | undefined; - private newRoot: SModelRootImpl | undefined; - private oldLabelTypes: LabelType[] | undefined; - private oldEditorMode: EditorMode | undefined; - private oldFileName: string | undefined; - - execute(context: CommandExecutionContext): CommandReturn { - this.oldRoot = context.root; - - const graphCopy = JSON.parse(JSON.stringify(defaultDiagram.model)); - LoadDiagramCommand.preprocessModelSchema(graphCopy); - this.dynamicChildrenProcessor.processGraphChildren(graphCopy, "set"); - this.newRoot = context.modelFactory.createRoot(graphCopy); - - this.logger.info(this, "Default Model loaded successfully"); - - if (this.labelTypeRegistry) { - this.oldLabelTypes = this.labelTypeRegistry.getLabelTypes(); - this.labelTypeRegistry.clearLabelTypes(); - defaultDiagram.labelTypes?.forEach((labelType) => { - this.labelTypeRegistry?.registerLabelType(labelType); - }); - this.logger.info(this, "Default Label Types loaded successfully"); - } - - if (this.editorModeController) { - this.oldEditorMode = this.editorModeController.getCurrentMode(); - if (defaultDiagram.mode) { - this.editorModeController.setMode(defaultDiagram.mode); - } else { - this.editorModeController.setDefaultMode(); - } - - this.logger.info(this, "Default Editor Mode loaded successfully"); - } - - if (this.constraintRegistry) { - // Load label types - this.constraintRegistry.clearConstraints(); - if (defaultDiagram?.constraints) { - this.constraintRegistry.setConstraintsFromArray(defaultDiagram.constraints); - this.logger.info(this, "Constraints loaded successfully"); - } - } - - postLoadActions(this.newRoot, this.actionDispatcher); - - this.oldFileName = currentFileName; - setFileNameInPageTitle(undefined); - - return this.newRoot; - } - - undo(context: CommandExecutionContext): SModelRootImpl { - this.labelTypeRegistry?.clearLabelTypes(); - this.oldLabelTypes?.forEach((labelType) => this.labelTypeRegistry?.registerLabelType(labelType)); - if (this.oldEditorMode) { - this.editorModeController?.setMode(this.oldEditorMode); - } - setFileNameInPageTitle(this.oldFileName); - - return this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); - } - - redo(context: CommandExecutionContext): SModelRootImpl { - this.labelTypeRegistry?.clearLabelTypes(); - defaultDiagram.labelTypes?.forEach((labelType) => { - this.labelTypeRegistry?.registerLabelType(labelType); - }); - if (this.editorModeController) { - if (defaultDiagram.mode) { - this.editorModeController.setMode(defaultDiagram.mode); - } else { - this.editorModeController.setDefaultMode(); - } - } - setFileNameInPageTitle(undefined); - - return this.newRoot ?? this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); - } -} diff --git a/frontend/webEditor/src/features/serialize/loadPalladio.ts b/frontend/webEditor/src/features/serialize/loadPalladio.ts deleted file mode 100644 index 357af949..00000000 --- a/frontend/webEditor/src/features/serialize/loadPalladio.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Command, CommandExecutionContext, ILogger, NullLogger, SModelRootImpl, TYPES } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { setModelFileName } from "../../index"; -import { sendMessage } from "./webSocketHandler"; -import { inject, optional } from "inversify"; -import { LoadingIndicator } from "../../common/loadingIndicator"; - -export interface LoadPalladioAction extends Action { - kind: typeof LoadPalladioAction.KIND; - file: File | undefined; -} -export namespace LoadPalladioAction { - export const KIND = "load-pcm"; - - export function create(file?: File): LoadPalladioAction { - return { - kind: KIND, - file, - }; - } -} - -export class LoadPalladioCommand extends Command { - static readonly KIND = LoadPalladioAction.KIND; - - @inject(TYPES.ILogger) - private readonly logger: ILogger = new NullLogger(); - @inject(LoadingIndicator) - @optional() - protected loadingIndicator?: LoadingIndicator; - - constructor() { - super(); - } - - /** - * Gets the model file from the action or opens a file picker dialog if no file is provided. - * @returns A promise that resolves to the model file. - */ - private getModelFiles(): Promise { - // Open a file picker dialog if no file is provided in the action. - // The cleaner way to do this would be showOpenFilePicker(), - // but safari and firefox don't support it at the time of writing this code: - // https://developer.mozilla.org/en-US/docs/web/api/window/showOpenFilePicker#browser_compatibility - const input = document.createElement("input"); - input.type = "file"; - input.accept = - ".pddc, .allocation, .nodecharacteristics, .repository, .resourceenvironment, .system, .usagemodel"; - input.multiple = true; - - const fileLoadPromise = new Promise((resolve, reject) => { - // This event is fired when the user successfully submits the file picker dialog. - input.onchange = () => { - if (input.files && input.files.length === 7) { - const files = Array.from(input.files); - const requiredFiles = { - pddc: files.find((file) => file.name.endsWith(".pddc")), - allocation: files.find((file) => file.name.endsWith(".allocation")), - nodecharacteristics: files.find((file) => file.name.endsWith(".nodecharacteristics")), - repository: files.find((file) => file.name.endsWith(".repository")), - resourceenvironment: files.find((file) => file.name.endsWith(".resourceenvironment")), - system: files.find((file) => file.name.endsWith(".system")), - usagemodel: files.find((file) => file.name.endsWith(".usagemodel")), - }; - - // Check if each required file type is present - const allFilesPresent = Object.values(requiredFiles).every((file) => file !== undefined); - - if (allFilesPresent) { - resolve(Object.values(requiredFiles) as File[]); - } else { - reject( - "Please select one file of each required type: .pddc, .allocation, .nodecharacteristics, .repository, .resourceenvironment, .system, .usagemodel", - ); - } - } else { - reject("You must select exactly 7 files"); - } - }; - }); - input.click(); - - return fileLoadPromise; - } - - async execute(context: CommandExecutionContext): Promise { - this.loadingIndicator?.showIndicator("Loading model files..."); - try { - // Fetch all required files - const files = (await this.getModelFiles()) ?? []; // Ensure getModelFiles() returns exactly seven files - - // Read the content of each file and structure them - const fileContents = await Promise.all( - files.map(async (file) => ({ - name: file.name, // Full filename with extension - content: await this.readFileContent(file), - })), - ); - - // Construct the message format for WebSocket - const message = [...fileContents.map(({ name, content }) => `${name}:${content}`)].join("---FILE---"); - - // Send the structured message over WebSocket - sendMessage(message); - - // Set the model file name and page title based on one of the files (e.g., the first file) - setModelFileName(files[0].name.substring(0, files[0].name.lastIndexOf("."))); - - return context.root; - } catch (error) { - this.logger.error(this, (error as Error).message); - this.loadingIndicator?.hideIndicator(); - return context.root; - } - } - - undo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - redo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - /** - * Utility function to read the content of a file as a string. - * @param file The file to read. - * @returns A promise that resolves to the file content as a string. - */ - private readFileContent(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.onerror = () => reject(reader.error); - reader.readAsText(file); - }); - } - - getFileNameWithoutExtension(file: File): string { - const fileName = file.name; - return fileName.substring(0, fileName.lastIndexOf(".")) || fileName; - } -} diff --git a/frontend/webEditor/src/features/serialize/save.ts b/frontend/webEditor/src/features/serialize/save.ts deleted file mode 100644 index e5523ee1..00000000 --- a/frontend/webEditor/src/features/serialize/save.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { Command, CommandExecutionContext, LocalModelSource, SModelRootImpl, TYPES } from "sprotty"; -import { Action, SModelRoot } from "sprotty-protocol"; -import { LabelType, LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { EditorMode, EditorModeController } from "../editorMode/editorModeController"; -import { Constraint, ConstraintRegistry } from "../constraintMenu/constraintRegistry"; - -/** - * Type that contains all data related to a diagram. - * This contains the sprotty diagram model and other data related to it. - */ -export interface SavedDiagram { - model: SModelRoot; - labelTypes?: LabelType[]; - constraints?: Constraint[]; - mode?: EditorMode; - version: number; -} -export const CURRENT_VERSION = 1; - -export interface SaveDiagramAction extends Action { - kind: typeof SaveDiagramAction.KIND; - suggestedFileName: string; -} -export namespace SaveDiagramAction { - export const KIND = "save-diagram"; - - export function create(suggestedFileName?: string): SaveDiagramAction { - return { - kind: KIND, - suggestedFileName: suggestedFileName ?? "diagram.json", - }; - } -} - -@injectable() -export class SaveDiagramCommand extends Command { - static readonly KIND = SaveDiagramAction.KIND; - @inject(TYPES.ModelSource) - private modelSource: LocalModelSource = new LocalModelSource(); - @inject(DynamicChildrenProcessor) - private dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(LabelTypeRegistry) - @optional() - private labelTypeRegistry?: LabelTypeRegistry; - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - @inject(ConstraintRegistry) - @optional() - private constraintRegistry?: ConstraintRegistry; - - constructor(@inject(TYPES.Action) private action: SaveDiagramAction) { - super(); - } - - execute(context: CommandExecutionContext): SModelRootImpl { - // Convert the model to JSON - // Do a copy because we're going to modify it - const modelCopy = JSON.parse(JSON.stringify(this.modelSource.model)); - // Remove element children that are implementation detail - this.dynamicChildrenProcessor.processGraphChildren(modelCopy, "remove"); - - // Export the diagram as a JSON data URL. - const diagram: SavedDiagram = { - model: modelCopy, - labelTypes: this.labelTypeRegistry?.getLabelTypes(), - constraints: this.constraintRegistry?.getConstraintList(), - mode: this.editorModeController?.getCurrentMode(), - version: CURRENT_VERSION, - }; - const diagramJson = JSON.stringify(diagram, undefined, 4); - const jsonBlob = new Blob([diagramJson], { type: "application/json" }); - const jsonUrl = URL.createObjectURL(jsonBlob); - - // Download the JSON file using a temporary anchor element. - // The cleaner way to do this would be showSaveFilePicker(), - // but safari and firefox don't support it at the time of writing this code: - // https://developer.mozilla.org/en-US/docs/web/api/window/showsavefilepicker#browser_compatibility - const tempLink = document.createElement("a"); - tempLink.href = jsonUrl; - tempLink.setAttribute("download", this.action.suggestedFileName); - tempLink.click(); - - // Free the url data - URL.revokeObjectURL(jsonUrl); - tempLink.remove(); - - return context.root; - } - - // Saving cannot be meaningfully undone/redone - - undo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - redo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } -} diff --git a/frontend/webEditor/src/features/serialize/saveDFDandDD.ts b/frontend/webEditor/src/features/serialize/saveDFDandDD.ts deleted file mode 100644 index 201aef25..00000000 --- a/frontend/webEditor/src/features/serialize/saveDFDandDD.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { inject, injectable, optional } from "inversify"; -import { Command, CommandExecutionContext, LocalModelSource, SModelRootImpl, TYPES } from "sprotty"; -import { Action } from "sprotty-protocol"; -import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { EditorModeController } from "../editorMode/editorModeController"; -import { sendMessage } from "./webSocketHandler"; -import { CURRENT_VERSION, SavedDiagram } from "./save"; -import { ConstraintRegistry } from "../constraintMenu/constraintRegistry"; -import { getModelFileName } from "../../index"; - -export interface SaveDFDandDDAction extends Action { - kind: typeof SaveDFDandDDAction.KIND; - file: File | undefined; -} -export namespace SaveDFDandDDAction { - export const KIND = "save-dfd"; - - export function create(file?: File): SaveDFDandDDAction { - return { - kind: KIND, - file, - }; - } -} - -@injectable() -export class SaveDFDandDDCommand extends Command { - static readonly KIND = SaveDFDandDDAction.KIND; - @inject(TYPES.ModelSource) - private modelSource: LocalModelSource = new LocalModelSource(); - @inject(DynamicChildrenProcessor) - private dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor(); - @inject(LabelTypeRegistry) - @optional() - private labelTypeRegistry?: LabelTypeRegistry; - @inject(EditorModeController) - @optional() - private editorModeController?: EditorModeController; - @inject(ConstraintRegistry) - @optional() - private readonly constraintRegistry?: ConstraintRegistry; - - constructor() { - super(); - } - - execute(context: CommandExecutionContext): SModelRootImpl { - // Convert the model to JSON - // Do a copy because we're going to modify it - const modelCopy = JSON.parse(JSON.stringify(this.modelSource.model)); - // Remove element children that are implementation detail - this.dynamicChildrenProcessor.processGraphChildren(modelCopy, "remove"); - - // Export the diagram as a JSON data URL. - const diagram: SavedDiagram = { - model: modelCopy, - labelTypes: this.labelTypeRegistry?.getLabelTypes(), - constraints: this.constraintRegistry?.getConstraintList(), - mode: this.editorModeController?.getCurrentMode(), - version: CURRENT_VERSION, - }; - const diagramJson = JSON.stringify(diagram, undefined, 4); - sendMessage("Json2DFD:" + getModelFileName() + ":" + diagramJson); - return context.root; - } - - // Saving cannot be meaningfully undone/redone - - undo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } - - redo(context: CommandExecutionContext): SModelRootImpl { - return context.root; - } -} - -export class SaveDFDandDD { - private dfdString: string = ""; - private ddString: string = ""; - - /** - * Constructor to initialize the XML strings and filenames. - * @param xmlString1 - The first XML string to save. - * @param xmlString2 - The second XML string to save. - * @param filename - The name for the first XML file (default: "example"). - */ - constructor(message: string) { - // Define the closing tag - const closingTag = ""; - const endIndex = message.indexOf(closingTag); - - if (endIndex !== -1) { - // Extract everything up to and including the closing tag - this.dfdString = message.slice(0, endIndex + closingTag.length).trim(); - - // Extract everything after the closing tag - this.ddString = message.slice(endIndex + closingTag.length).trim(); - } - } - - /** - * Method to save both XML files by creating Blob objects and triggering downloads. - */ - public saveDiagramAsDFD(): void { - this.saveFile(this.dfdString, ".dataflowdiagram"); - this.saveFile(this.ddString, ".datadictionary"); - } - - private saveFile(file: string, ending: string) { - const blob = new Blob([file], { type: "application/xml" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.setAttribute("download", getModelFileName() + ending); - document.body.appendChild(link); // Append link to the body - link.click(); // Programmatically click to trigger download - URL.revokeObjectURL(url); // Revoke the URL after download - link.remove(); // Remove the link from the DOM - } -} diff --git a/frontend/webEditor/src/features/serialize/webSocketHandler.ts b/frontend/webEditor/src/features/serialize/webSocketHandler.ts deleted file mode 100644 index 51fa9213..00000000 --- a/frontend/webEditor/src/features/serialize/webSocketHandler.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { getModelFileName, logger, setModelSource, loadingIndicator } from "../../index"; -import { SaveDFDandDD } from "./saveDFDandDD"; - -const webSocketAdress = `wss://websocket.dataflowanalysis.org/events/`; - -let ws: WebSocket; -let wsId = 0; - -/** - * Initializes the WebSocket and sets up all event handlers. - */ -function initWebSocket() { - ws = new WebSocket(webSocketAdress); - - ws.onopen = () => { - logger.log(ws, "WebSocket connection established."); - }; - - ws.onclose = () => { - logger.log(ws, "WebSocket connection closed. Reconnecting..."); - loadingIndicator.hideIndicator(); - initWebSocket(); - }; - - ws.onerror = () => { - logger.log(ws, "WebSocket encountered an error. Reconnecting..."); - loadingIndicator.hideIndicator(); - initWebSocket(); - }; - - ws.onmessage = (event) => { - logger.log(ws, "WebSocketID:", wsId); - logger.log(ws, event.data); - - // Example of specific handling for certain messages: - if (event.data.startsWith("Error:")) { - alert(event.data); - loadingIndicator.hideIndicator(); - return; - } - if (event.data.startsWith("ID assigned:")) { - wsId = parseInt(event.data.split(":")[1].trim(), 10); - loadingIndicator.hideIndicator(); - return; - } - - let message = event.data; - const name = message.split(":")[0]; - message = message.replace(name + ":", ""); - - if (event.data.trim().endsWith("")) { - const saveDFDandDD = new SaveDFDandDD(message); - saveDFDandDD.saveDiagramAsDFD(); - loadingIndicator.hideIndicator(); - return; - } - - // Otherwise, treat incoming data as JSON for model source: - setModelSource( - new File([new Blob([message], { type: "application/json" })], name + ".json", { - type: "application/json", - }), - ); - loadingIndicator.hideIndicator(); - }; -} - -export function sendMessage(message: string) { - ws.send(wsId + ":" + getModelFileName() + ":" + message); -} - -// Initialize immediately upon module load -initWebSocket(); diff --git a/frontend/webEditor/src/features/settingsMenu/LayoutMethod.ts b/frontend/webEditor/src/features/settingsMenu/LayoutMethod.ts deleted file mode 100644 index c2890bef..00000000 --- a/frontend/webEditor/src/features/settingsMenu/LayoutMethod.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum LayoutMethod { - LINES = "Lines", - WRAPPING = "Wrapping Lines", - CIRCLES = "Circles", -} diff --git a/frontend/webEditor/src/features/settingsMenu/SettingsManager.ts b/frontend/webEditor/src/features/settingsMenu/SettingsManager.ts deleted file mode 100644 index 3916d736..00000000 --- a/frontend/webEditor/src/features/settingsMenu/SettingsManager.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { inject, injectable } from "inversify"; -import { ActionDispatcher, TYPES } from "sprotty"; -import { ChangeEdgeLabelVisibilityAction, CompleteLayoutProcessAction, SimplifyNodeNamesAction } from "./actions"; -import { LayoutMethod } from "./LayoutMethod"; -import { Mode } from "./annotationManager"; - -@injectable() -export class SettingsManager { - private _layoutMethod: LayoutMethod = LayoutMethod.LINES; - private _layoutMethodSelect?: HTMLSelectElement; - private _hideEdgeLabels = false; - private _hideEdgeLabelsCheckbox?: HTMLInputElement; - private _simplifyNodeNames = false; - private _simplifyNodeNamesCheckbox?: HTMLInputElement; - private _labelModeSelector?: HTMLSelectElement; - private static readonly layoutMethodLocalStorageKey = "dfdwebeditor:settings"; - - constructor(@inject(TYPES.IActionDispatcher) protected readonly dispatcher: ActionDispatcher) { - this.layoutMethod = (localStorage.getItem(SettingsManager.layoutMethodLocalStorageKey) ?? - LayoutMethod.LINES) as LayoutMethod; - } - - public get layoutMethod(): LayoutMethod { - return this._layoutMethod; - } - - public set layoutMethod(layoutMethod: LayoutMethod) { - this._layoutMethod = layoutMethod; - localStorage.setItem(SettingsManager.layoutMethodLocalStorageKey, layoutMethod); - if (this._layoutMethodSelect) { - this._layoutMethodSelect.value = layoutMethod; - } - } - - public bindLayoutMethodSelect(select: HTMLSelectElement) { - this._layoutMethodSelect = select; - this._layoutMethodSelect.value = this._layoutMethod; - this._layoutMethodSelect.value = this._layoutMethod; - this._layoutMethodSelect.addEventListener("change", () => { - this.dispatcher.dispatch( - CompleteLayoutProcessAction.create(this._layoutMethodSelect!.value as LayoutMethod), - ); - }); - } - - public get hideEdgeLabels(): boolean { - return this._hideEdgeLabels; - } - - public set hideEdgeLabels(hideEdgeLabels: boolean) { - this._hideEdgeLabels = hideEdgeLabels; - if (this._hideEdgeLabelsCheckbox) { - this._hideEdgeLabelsCheckbox.checked = hideEdgeLabels; - } - } - - public bindHideEdgeLabelsCheckbox(checkbox: HTMLInputElement) { - this._hideEdgeLabelsCheckbox = checkbox; - this._hideEdgeLabelsCheckbox.checked = this._hideEdgeLabels; - this._hideEdgeLabelsCheckbox.addEventListener("change", () => { - this.dispatcher.dispatch(ChangeEdgeLabelVisibilityAction.create(this._hideEdgeLabelsCheckbox!.checked)); - }); - } - - public get simplifyNodeNames(): boolean { - return this._simplifyNodeNames; - } - - public set simplifyNodeNames(simplifyNodeNames: boolean) { - this._simplifyNodeNames = simplifyNodeNames; - if (this._simplifyNodeNamesCheckbox) { - this._simplifyNodeNamesCheckbox.checked = simplifyNodeNames; - } - } - - public bindSimplifyNodeNamesCheckbox(checkbox: HTMLInputElement) { - this._simplifyNodeNamesCheckbox = checkbox; - this._simplifyNodeNamesCheckbox.checked = this._simplifyNodeNames; - this._simplifyNodeNamesCheckbox.addEventListener("change", () => { - this.dispatcher.dispatch( - SimplifyNodeNamesAction.create(this._simplifyNodeNamesCheckbox!.checked ? "hide" : "show"), - ); - }); - } - - public bindLabelModeSelector(labelModeSelector: HTMLSelectElement) { - this._labelModeSelector = labelModeSelector; - labelModeSelector.value = Mode.INCOMING; - } - - public getCurrentLabelMode(): Mode { - return this._labelModeSelector!.value as Mode; - } -} diff --git a/frontend/webEditor/src/features/settingsMenu/actions.ts b/frontend/webEditor/src/features/settingsMenu/actions.ts deleted file mode 100644 index 5a2c50da..00000000 --- a/frontend/webEditor/src/features/settingsMenu/actions.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Action } from "sprotty-protocol"; -import { LayoutMethod } from "./LayoutMethod"; -import { Theme } from "./themeManager"; - -export interface SimplifyNodeNamesAction extends Action { - kind: typeof SimplifyNodeNamesAction.KIND; - mode: SimplifyNodeNamesAction.Mode; -} -export namespace SimplifyNodeNamesAction { - export const KIND = "simplify-node-names"; - export type Mode = "hide" | "show"; - - export function create(mode?: SimplifyNodeNamesAction.Mode): SimplifyNodeNamesAction { - return { - kind: KIND, - mode: mode ?? "hide", - }; - } -} - -export interface ChangeEdgeLabelVisibilityAction extends Action { - kind: typeof ChangeEdgeLabelVisibilityAction.KIND; - hide: boolean; -} -export namespace ChangeEdgeLabelVisibilityAction { - export const KIND = "hide-edge-labels"; - - export function create(hide: boolean = true): ChangeEdgeLabelVisibilityAction { - return { kind: KIND, hide }; - } -} - -export interface CompleteLayoutProcessAction extends Action { - kind: typeof CompleteLayoutProcessAction.KIND; - method: LayoutMethod; -} -export namespace CompleteLayoutProcessAction { - export const KIND = "complete-layout-process"; - - export function create(method: LayoutMethod): CompleteLayoutProcessAction { - return { kind: KIND, method }; - } -} - -export interface ChangeThemeAction extends Action { - kind: typeof ChangeThemeAction.KIND; - theme: Theme; -} -export namespace ChangeThemeAction { - export const KIND = "change-theme"; - - export function create(theme: Theme = Theme.SYSTEM_DEFAULT): ChangeThemeAction { - return { kind: KIND, theme }; - } -} - -export interface ReSnapPortsAfterChangeAction extends Action { - kind: typeof ReSnapPortsAfterChangeAction.KIND; -} - -export namespace ReSnapPortsAfterChangeAction { - export const KIND = "resnap-ports-after-change"; - - export function create(): Action { - return { kind: KIND }; - } -} diff --git a/frontend/webEditor/src/features/settingsMenu/annotationManager.ts b/frontend/webEditor/src/features/settingsMenu/annotationManager.ts deleted file mode 100644 index 064304b9..00000000 --- a/frontend/webEditor/src/features/settingsMenu/annotationManager.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { injectable } from "inversify"; - -export enum Mode { - INCOMING = "Incoming Labels", - OUTGOING = "Outgoing Labels", - ALL = "All Labels", -} - -@injectable() -export class AnnnotationsManager { - private selectedTfgs = new Set(); - - public getSelectedTfgs(): Set { - return this.selectedTfgs; - } - public clearTfgs() { - this.selectedTfgs = new Set(); - } - public addTfg(hash: number) { - this.selectedTfgs.add(hash); - } - - constructor() {} -} diff --git a/frontend/webEditor/src/features/settingsMenu/commands.ts b/frontend/webEditor/src/features/settingsMenu/commands.ts deleted file mode 100644 index 5f7ea414..00000000 --- a/frontend/webEditor/src/features/settingsMenu/commands.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { inject, injectable } from "inversify"; -import { - ActionDispatcher, - Command, - CommandExecutionContext, - CommandReturn, - CommitModelAction, - ILogger, - ISnapper, - NullLogger, - SLabelImpl, - SModelRootImpl, - SPortImpl, - TYPES, -} from "sprotty"; -import { getBasicType, RedoAction, UndoAction } from "sprotty-protocol"; -import { DfdNodeImpl } from "../dfdElements/nodes"; -import { SettingsManager } from "./SettingsManager"; -import { - ChangeEdgeLabelVisibilityAction, - ChangeThemeAction, - CompleteLayoutProcessAction, - ReSnapPortsAfterChangeAction, - SimplifyNodeNamesAction, -} from "./actions"; -import { ArrowEdgeImpl } from "../dfdElements/edges"; -import { createDefaultFitToScreenAction } from "../../utils"; -import { LayoutMethod } from "./LayoutMethod"; -import { Theme, ThemeManager } from "./themeManager"; -import { LayoutModelAction } from "../autoLayout/command"; -import { snapPortsOfNode } from "../dfdElements/portSnapper"; -import { EditorModeController } from "../editorMode/editorModeController"; - -@injectable() -export class NodeNameReplacementRegistry { - private registry: Map = new Map(); - private nextNumber = 1; - - public get(id: string) { - const v = this.registry.get(id); - if (v !== undefined) { - return v; - } - const newName = this.nextNumber.toString(); - this.nextNumber++; - this.registry.set(id, newName); - return newName; - } -} - -@injectable() -export class SimplifyNodeNamesCommand extends Command { - static readonly KIND = SimplifyNodeNamesAction.KIND; - private readonly portMove: ReSnapPortsAfterChangeCommand; - - constructor( - @inject(TYPES.Action) private action: SimplifyNodeNamesAction, - @inject(SettingsManager) private settings: SettingsManager, - @inject(NodeNameReplacementRegistry) private registry: NodeNameReplacementRegistry, - @inject(TYPES.ISnapper) snapper: ISnapper, - @inject(EditorModeController) private editorModeController: EditorModeController, - ) { - super(); - this.portMove = new ReSnapPortsAfterChangeCommand(snapper); - } - - execute(context: CommandExecutionContext): CommandReturn { - this.perform(context, this.action.mode); - return this.portMove.execute(context); - } - undo(context: CommandExecutionContext): CommandReturn { - this.perform(context, this.action.mode === "hide" ? "show" : "hide"); - return this.portMove.undo(context); - } - redo(context: CommandExecutionContext): CommandReturn { - this.perform(context, this.action.mode); - return this.portMove.redo(context); - } - - private perform(context: CommandExecutionContext, mode: SimplifyNodeNamesAction.Mode): CommandReturn { - this.settings.simplifyNodeNames = mode === "hide"; - const nodes = context.root.children.filter((node) => getBasicType(node) === "node") as DfdNodeImpl[]; - nodes.forEach((node) => { - const label = node.children.find((element) => element.type === "label:positional") as - | SLabelImpl - | undefined; - if (!label) { - return; - } - label.text = mode === "hide" ? this.registry.get(node.id) : (node.text ?? ""); - node.hideLabels = mode === "hide"; - node.minimumWidth = mode === "hide" ? DfdNodeImpl.DEFAULT_WIDTH / 2 : DfdNodeImpl.DEFAULT_WIDTH; - }); - if (mode === "hide") { - this.editorModeController.setMode("view"); - } - - return context.root; - } -} - -@injectable() -export class ChangeEdgeLabelVisibilityCommand extends Command { - static readonly KIND = ChangeEdgeLabelVisibilityAction.KIND; - - constructor( - @inject(TYPES.Action) private action: ChangeEdgeLabelVisibilityAction, - @inject(SettingsManager) private settings: SettingsManager, - @inject(EditorModeController) private editorModeController: EditorModeController, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - return this.perform(context, this.action.hide); - } - undo(context: CommandExecutionContext): CommandReturn { - return this.perform(context, !this.action.hide); - } - redo(context: CommandExecutionContext): CommandReturn { - return this.perform(context, this.action.hide); - } - - private perform(context: CommandExecutionContext, hide: boolean): SModelRootImpl { - this.settings.hideEdgeLabels = hide; - const edges = context.root.children.filter((node) => getBasicType(node) === "edge") as ArrowEdgeImpl[]; - edges.forEach((edge) => { - const label = edge.children.find((element) => element.type === "label:filled-background") as - | SLabelImpl - | undefined; - if (!label) { - return; - } - label.text = hide ? "" : (edge.text ?? ""); - }); - if (hide) { - this.editorModeController.setMode("view"); - } - - return context.root; - } -} - -@injectable() -export class CompleteLayoutProcessCommand extends Command { - static readonly KIND = CompleteLayoutProcessAction.KIND; - private previousMethod?: LayoutMethod; - - @inject(TYPES.ILogger) - private readonly logger: ILogger = new NullLogger(); - - constructor( - @inject(TYPES.Action) private action: CompleteLayoutProcessAction, - @inject(TYPES.IActionDispatcher) private actionDispatcher: ActionDispatcher, - @inject(SettingsManager) private settings: SettingsManager, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - this.logger.info(this, "CompleteLayoutProcessCommand", this.action.method); - this.previousMethod = this.settings.layoutMethod; - this.settings.layoutMethod = this.action.method; - this.actionDispatcher.dispatchAll([ - LayoutModelAction.create(), - CommitModelAction.create(), - createDefaultFitToScreenAction(context.root), - ]); - return context.root; - } - undo(context: CommandExecutionContext): CommandReturn { - this.settings.layoutMethod = this.previousMethod ?? LayoutMethod.LINES; - this.actionDispatcher.dispatch(UndoAction.create()); - return context.root; - } - redo(context: CommandExecutionContext): CommandReturn { - this.previousMethod = this.settings.layoutMethod; - this.settings.layoutMethod = this.action.method; - this.actionDispatcher.dispatch(RedoAction.create()); - return context.root; - } -} - -@injectable() -export class ChangeThemeCommand extends Command { - static readonly KIND = ChangeThemeAction.KIND; - private previousTheme?: Theme; - - constructor( - @inject(TYPES.Action) private action: ChangeThemeAction, - @inject(ThemeManager) private themeManager: ThemeManager, - ) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - this.previousTheme = this.themeManager.theme; - this.themeManager.theme = this.action.theme; - return context.root; - } - undo(context: CommandExecutionContext): CommandReturn { - this.themeManager.theme = this.previousTheme ?? Theme.SYSTEM_DEFAULT; - return context.root; - } - redo(context: CommandExecutionContext): CommandReturn { - this.previousTheme = this.themeManager.theme; - this.themeManager.theme = this.action.theme; - return context.root; - } -} - -@injectable() -export class ReSnapPortsAfterChangeCommand extends Command { - static readonly KIND = ReSnapPortsAfterChangeAction.KIND; - private previousPositions: Map = new Map(); - - constructor(@inject(TYPES.ISnapper) private readonly snapper: ISnapper) { - super(); - } - - execute(context: CommandExecutionContext): CommandReturn { - const model = context.root; - - model.children.forEach((node) => { - if (node instanceof DfdNodeImpl) { - this.savePortPositions(node); - } - }); - - model.children.forEach((node) => { - if (node instanceof DfdNodeImpl) { - snapPortsOfNode(node, this.snapper); - } - }); - return model; - } - undo(context: CommandExecutionContext): CommandReturn { - const model = context.root; - model.children.forEach((node) => { - if (node instanceof DfdNodeImpl) { - node.children.forEach((child) => { - if (child instanceof SPortImpl) { - const pos = this.previousPositions.get(child.id); - if (pos) { - child.position = pos; - } - } - }); - } - }); - return model; - } - redo(context: CommandExecutionContext): CommandReturn { - const model = context.root; - model.children.forEach((node) => { - if (node instanceof DfdNodeImpl) { - snapPortsOfNode(node, this.snapper); - } - }); - return model; - } - - private savePortPositions(element: DfdNodeImpl) { - element.children.forEach((child) => { - if (child instanceof SPortImpl) { - this.previousPositions.set(child.id, { x: child.position.x, y: child.position.y }); - } else if (child instanceof DfdNodeImpl) { - this.savePortPositions(child); - } - }); - } -} diff --git a/frontend/webEditor/src/features/settingsMenu/di.config.ts b/frontend/webEditor/src/features/settingsMenu/di.config.ts deleted file mode 100644 index 0cffe855..00000000 --- a/frontend/webEditor/src/features/settingsMenu/di.config.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ContainerModule } from "inversify"; -import { SettingsUI } from "./settingsMenu"; -import { ThemeManager } from "./themeManager"; -import { EDITOR_TYPES } from "../../utils"; -import { configureCommand, TYPES } from "sprotty"; -import { - ChangeEdgeLabelVisibilityCommand, - ChangeThemeCommand, - CompleteLayoutProcessCommand, - NodeNameReplacementRegistry, - SimplifyNodeNamesCommand, -} from "./commands"; -import { SettingsManager } from "./SettingsManager"; -import { AnnnotationsManager } from "./annotationManager"; - -export const settingsModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(SettingsManager).toSelf().inSingletonScope(); - bind(NodeNameReplacementRegistry).toSelf().inSingletonScope(); - bind(ThemeManager).toSelf().inSingletonScope(); - bind(SettingsUI).toSelf().inSingletonScope(); - bind(AnnnotationsManager).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(SettingsUI); - bind(EDITOR_TYPES.DefaultUIElement).toService(SettingsUI); - const context = { bind, unbind, isBound, rebind }; - - configureCommand(context, SimplifyNodeNamesCommand); - configureCommand(context, ChangeEdgeLabelVisibilityCommand); - configureCommand(context, CompleteLayoutProcessCommand); - configureCommand(context, ChangeThemeCommand); -}); diff --git a/frontend/webEditor/src/features/settingsMenu/settingsMenu.css b/frontend/webEditor/src/features/settingsMenu/settingsMenu.css deleted file mode 100644 index cc839597..00000000 --- a/frontend/webEditor/src/features/settingsMenu/settingsMenu.css +++ /dev/null @@ -1,109 +0,0 @@ -div.settings-ui { - left: 20px; - bottom: 70px; - padding: 10px 10px; -} - -#settings-ui-accordion-label .accordion-button::before { - content: ""; - background-image: url("@fortawesome/fontawesome-free/svgs/solid/gear.svg"); - display: inline-block; - filter: invert(var(--dark-mode)); - height: 16px; - width: 16px; - background-size: 16px 16px; - vertical-align: text-top; -} - -#settings-content { - display: grid; - gap: 8px 6px; - - align-items: center; -} - -#settings-content > label { - grid-column-start: 1; -} - -#settings-content > input, -#settings-content > select, -#settings-content > label.switch { - grid-column-start: 2; -} - -#settings-content select { - background-color: var(--color-background); - color: var(--color-foreground); - border: 1px solid var(--color-foreground); - border-radius: 6px; -} - -.switch input:disabled + .slider { - background-color: color-mix(in srgb, var(--color-primary) 50%, #555 50%); -} - -.switch input:disabled + .slider:before { - background-color: color-mix(in srgb, var(--color-background) 50%, #555 50%); -} - -/* https://www.w3schools.com/HOWTO/howto_css_switch.asp */ -/* The switch - the box around the slider */ -.switch { - position: relative; - display: inline-block; - width: 30px; - height: 17px; -} - -/* Hide default HTML checkbox */ -.switch input { - opacity: 0; - width: 0; - height: 0; -} - -/* The slider */ -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--color-background); - -webkit-transition: 0.4s; - transition: 0.4s; -} - -.slider:before { - position: absolute; - content: ""; - height: 13px; - width: 13px; - left: 2px; - bottom: 2px; - background-color: var(--color-primary); - -webkit-transition: 0.3s; - transition: 0.3s; -} - -input:checked + .slider { - background-color: var(--color-background); -} - -input:checked + .slider:before { - -webkit-transform: translateX(13px); - -ms-transform: translateX(13px); - transform: translateX(13px); - background-color: var(--color-foreground); -} - -/* Rounded sliders */ -.slider.round { - border-radius: 17px; -} - -.slider.round:before { - border-radius: 50%; -} diff --git a/frontend/webEditor/src/features/settingsMenu/settingsMenu.ts b/frontend/webEditor/src/features/settingsMenu/settingsMenu.ts deleted file mode 100644 index b7adb98e..00000000 --- a/frontend/webEditor/src/features/settingsMenu/settingsMenu.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { AbstractUIExtension, ActionDispatcher, TYPES } from "sprotty"; -import { inject, injectable } from "inversify"; - -import "./settingsMenu.css"; -import { Theme, ThemeManager } from "./themeManager"; -import { SettingsManager } from "./SettingsManager"; -import { LayoutMethod } from "./LayoutMethod"; -import { EditorModeController } from "../editorMode/editorModeController"; -import { ChangeEditorModeAction } from "../editorMode/command"; -import { Mode } from "./annotationManager"; - -@injectable() -export class SettingsUI extends AbstractUIExtension { - static readonly ID = "settings-ui"; - - constructor( - @inject(SettingsManager) protected readonly settings: SettingsManager, - @inject(ThemeManager) protected readonly themeManager: ThemeManager, - @inject(EditorModeController) private editorModeController: EditorModeController, - @inject(TYPES.IActionDispatcher) protected readonly dispatcher: ActionDispatcher, - ) { - super(); - } - - id(): string { - return SettingsUI.ID; - } - - containerClass(): string { - return SettingsUI.ID; - } - - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - containerElement.innerHTML = ` - - -
-
- - - - - - - - - - - - - - - -
-
- `; - - // Set `settings-enabled` class on body element when keyboard shortcut overview is open. - const checkbox = containerElement.querySelector("#accordion-state-settings") as HTMLInputElement; - const bodyElement = document.querySelector("body") as HTMLBodyElement; - checkbox.addEventListener("change", () => { - if (checkbox.checked) { - bodyElement.classList.add("settings-enabled"); - } else { - bodyElement.classList.remove("settings-enabled"); - } - }); - - const layoutOptionSelect = containerElement.querySelector("#setting-layout-option") as HTMLSelectElement; - this.settings.bindLayoutMethodSelect(layoutOptionSelect); - - const themeOptionSelect = containerElement.querySelector("#setting-theme") as HTMLSelectElement; - this.themeManager.bindThemeSelect(themeOptionSelect); - - const hideEdgeLabelsCheckbox = containerElement.querySelector("#setting-hide-edge-labels") as HTMLInputElement; - this.settings.bindHideEdgeLabelsCheckbox(hideEdgeLabelsCheckbox); - - const simplifyNodeNamesCheckbox = containerElement.querySelector( - "#setting-simplify-node-names", - ) as HTMLInputElement; - this.settings.bindSimplifyNodeNamesCheckbox(simplifyNodeNamesCheckbox); - - const readOnlyCheckbox = containerElement.querySelector("#setting-read-only") as HTMLInputElement; - this.editorModeController.onModeChange((mode) => { - readOnlyCheckbox.checked = mode !== "edit"; - }); - if (this.editorModeController.isReadOnly()) { - readOnlyCheckbox.checked = true; - } - readOnlyCheckbox.addEventListener("change", () => { - this.dispatcher.dispatch(ChangeEditorModeAction.create(readOnlyCheckbox.checked ? "view" : "edit")); - }); - - const labelModeSelector = containerElement.querySelector("#setting-mode-option") as HTMLSelectElement; - this.settings.bindLabelModeSelector(labelModeSelector); - } -} diff --git a/frontend/webEditor/src/features/settingsMenu/themeManager.ts b/frontend/webEditor/src/features/settingsMenu/themeManager.ts deleted file mode 100644 index 068b05d7..00000000 --- a/frontend/webEditor/src/features/settingsMenu/themeManager.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { inject, injectable, multiInject } from "inversify"; -import { ActionDispatcher, TYPES } from "sprotty"; -import { ChangeThemeAction } from "./actions"; - -export enum Theme { - LIGHT = "Light", - DARK = "Dark", - SYSTEM_DEFAULT = "System Default", -} - -export const SWITCHABLE = Symbol("Switchable"); - -export interface Switchable { - switchTheme(useDark: boolean): void; -} - -@injectable() -export class ThemeManager { - private static _theme: Theme = Theme.SYSTEM_DEFAULT; - private static SYSTEM_DEFAULT = - window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? Theme.DARK : Theme.LIGHT; - private themeSelect?: HTMLSelectElement; - private static readonly localStorageKey = "dfdwebeditor:theme"; - - constructor( - @multiInject(SWITCHABLE) protected switchables: Switchable[], - @inject(TYPES.IActionDispatcher) protected readonly dispatcher: ActionDispatcher, - ) { - this.theme = (localStorage.getItem(ThemeManager.localStorageKey) ?? ThemeManager.SYSTEM_DEFAULT) as Theme; - } - - get useDarkMode(): boolean { - return ThemeManager.useDarkMode; - } - - static get useDarkMode(): boolean { - if (ThemeManager._theme == Theme.SYSTEM_DEFAULT) { - return ThemeManager.SYSTEM_DEFAULT == Theme.DARK; - } - return ThemeManager._theme == Theme.DARK; - } - - get theme(): Theme { - return ThemeManager._theme; - } - - set theme(theme: Theme) { - ThemeManager._theme = theme; - if (this.themeSelect) { - this.themeSelect.value = theme; - } - - const rootElement = document.querySelector(":root") as HTMLElement; - const sprottyElement = document.querySelector("#sprotty") as HTMLElement; - - const value = this.useDarkMode ? "dark" : "light"; - rootElement.setAttribute("data-theme", value); - sprottyElement.setAttribute("data-theme", value); - localStorage.setItem(ThemeManager.localStorageKey, theme); - - this.switchables.forEach((s) => s.switchTheme(this.useDarkMode)); - } - - bindThemeSelect(themeSelect: HTMLSelectElement) { - this.themeSelect = themeSelect; - this.themeSelect.value = this.theme; - this.themeSelect.addEventListener("change", () => { - this.dispatcher.dispatch(ChangeThemeAction.create(themeSelect.value as Theme)); - }); - } -} diff --git a/frontend/webEditor/src/features/toolPalette/creationTool.ts b/frontend/webEditor/src/features/toolPalette/creationTool.ts deleted file mode 100644 index 4068113d..00000000 --- a/frontend/webEditor/src/features/toolPalette/creationTool.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { - ActionDispatcher, - CommandExecutionContext, - CommandReturn, - CommandStack, - CommitModelAction, - ICommand, - ILogger, - IModelFactory, - ISnapper, - KeyListener, - MouseListener, - MousePositionTracker, - MouseTool, - SChildElementImpl, - SEdgeImpl, - SGraphImpl, - SModelElementImpl, - SNodeImpl, - SParentElementImpl, - SPortImpl, - TYPES, -} from "sprotty"; -import { inject, injectable, multiInject } from "inversify"; -import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren"; -import { Action, Point, SEdge, SNode, SPort } from "sprotty-protocol"; -import { EDITOR_TYPES } from "../../utils"; - -type Positionable = { position?: Point }; -type Schema = (SNode | SEdge | SPort) & Positionable; -type Impl = SNodeImpl | SEdgeImpl | SPortImpl; -export type AnyCreationTool = CreationTool; - -/** - * Common interface between all tools used by the tool palette to create new elements. - * These tools are meant to be enabled, allow the user to perform some action like creating a new node or edge, - * and then they should disable themselves when the action is done. - * Alternatively they can be disabled from the UI or other code to cancel the tool usage. - */ -@injectable() -export abstract class CreationTool extends MouseListener { - protected element?: I; - protected readonly previewOpacity = 0.5; - protected insertIntoGraphRootAfterCreation = true; - protected elementType = ""; - - constructor( - @inject(MouseTool) protected mouseTool: MouseTool, - @inject(MousePositionTracker) protected mousePositionTracker: MousePositionTracker, - @inject(DynamicChildrenProcessor) protected dynamicChildrenProcessor: DynamicChildrenProcessor, - @inject(TYPES.IModelFactory) protected modelFactory: IModelFactory, - @inject(TYPES.IActionDispatcher) protected actionDispatcher: ActionDispatcher, - @inject(TYPES.ICommandStack) protected commandStack: CommandStack, - @inject(TYPES.ISnapper) protected snapper: ISnapper, - @inject(TYPES.ILogger) protected logger: ILogger, - ) { - super(); - } - - abstract createElementSchema(): S; - - protected async createElement(): Promise { - const schema = this.createElementSchema(); - - // Create the element with the preview opacity to indicated it is not placed yet - // Only set opacity if it is not already set in the schema - schema.opacity ??= this.previewOpacity; - - // Add any dynamically declared children to the node schema. - this.dynamicChildrenProcessor.processGraphChildren(schema, "set"); - - const element = this.modelFactory.createElement(schema) as I; - if (this.insertIntoGraphRootAfterCreation) { - const root = await this.commandStack.executeAll([]); - root.add(element); - } - - return element; - } - - enable(elementType: string): void { - this.elementType = elementType; - this.mouseTool.register(this); - this.createElement() - .then((element) => { - this.element = element; - this.logger.log(this, "Created element", element); - - // Show element at current mouse position - if (this.mousePositionTracker.lastPositionOnDiagram) { - this.updateElementPosition(this.mousePositionTracker.lastPositionOnDiagram); - } - }) - .catch((error) => { - this.logger.error(this, "Failed to create element", error); - }); - } - - disable(): void { - this.mouseTool.deregister(this); - - if (this.element) { - // Element is not placed yet but we're disabling the tool. - // This means the creation was cancelled and the element should be deleted. - - // Get root before removing the element, needed for re-render - let root: SGraphImpl | undefined; - try { - root = this.element.root as SGraphImpl; - } catch (error) { - // element has no assigned root - void error; - } - - // Remove element from graph - this.element.parent?.remove(this.element); - this.element = undefined; - - // Re-render the graph to remove the element from the preview. - // Root may be unavailable e.g. when the element hasn't been inserted into - // the diagram yet. Skipping the render in those cases is fine as the element - // wasn't rendered in such case anyway. - if (root) { - this.commandStack.update(root); - } - - this.logger.info(this, "Cancelled element creation"); - } - } - - protected finishPlacingElement(): void { - if (this.element) { - const elementParent = this.element.parent; - // Remove the element as it was only added as a temporary preview element - elementParent.remove(this.element); - - // Make node fully visible - this.element.opacity = 1; - - // Set via a command for redo/undo support. - // This inserts the created element properly into the model in contrast to the - // temporary add done previously. - this.actionDispatcher.dispatch(AddElementToGraphAction.create(this.element, elementParent)); - - this.logger.log(this, "Finalized element creation of element", this.element); - this.element = undefined; // Unset to prevent further actions - } - this.disable(); - } - - private updateElementPosition(mousePosition: Point): void { - if (!this.element) { - return; - } - - const newPosition = { ...mousePosition }; - - if (this.element instanceof SEdgeImpl) { - // Snap the edge target to the mouse position, if there is a target element. - if (this.element.targetId && this.element.target) { - if (!Point.equals(this.element.target.position, newPosition)) { - this.element.target.position = newPosition; - // Trigger re-rendering of the edge - this.commandStack.update(this.element.root); - } - } - } else { - const previousPosition = this.element.position; - - // Adapt the mouse position depending on element type - if (this.element instanceof SNodeImpl) { - // The node should be created to have its center at the mouse position. - // Because of this, we need to adjust the position by half the size of the element. - const { width, height } = this.element.bounds; - newPosition.x -= width / 2; - newPosition.y -= height / 2; - } else if (this.element instanceof SPortImpl) { - // Port positions must be relative to the target node. - // So we need to convert the absolute graph position of the mouse - // to a position relative to the target node. - const parent = this.element.parent; - if (parent instanceof SNodeImpl) { - newPosition.x -= parent.position.x; - newPosition.y -= parent.position.y; - } - } - - // Snap the element to the corresponding grid - const newPositionSnapped = this.snapper.snap(newPosition, this.element); - - // Only update if the position after snapping has changed (aka the effective position). - if (!Point.equals(previousPosition, newPositionSnapped)) { - this.element.position = newPositionSnapped; - // Trigger re-rendering of the node/port - this.commandStack.update(this.element.root); - } - } - } - - mouseMove(target: SModelElementImpl, event: MouseEvent): (Action | Promise)[] { - const mousePosition = this.calculateMousePosition(target, event); - this.updateElementPosition(mousePosition); - return []; - } - - mouseDown(_target: SModelElementImpl, event: MouseEvent): Action[] { - event.preventDefault(); // prevents additional click onto the newly created element - - this.finishPlacingElement(); - - return [ - CommitModelAction.create(), // Save to element ModelSource - ]; - } - - /** - * Calculates the mouse position in graph coordinates. - */ - protected calculateMousePosition(target: SModelElementImpl, event: MouseEvent): Point { - const root = target.root as SGraphImpl; - - const calcPos = (axis: "x" | "y") => { - // Position of the top left viewport corner in the whole graph - const rootPosition = root.scroll[axis]; - // Offset of the mouse position from the top left viewport corner in screen pixels - const screenOffset = axis === "x" ? event.offsetX : event.offsetY; - // Offset of the mouse position from the top left viewport corner in graph coordinates - const screenOffsetNormalized = screenOffset / root.zoom; - - // Add position - return rootPosition + screenOffsetNormalized; - }; - return { - x: calcPos("x"), - y: calcPos("y"), - }; - } -} - -/** - * Adds the given element to the graph at the root level. - */ -export interface AddElementToGraphAction extends Action { - kind: typeof AddElementToGraphAction.TYPE; - element: SChildElementImpl; - parent: SParentElementImpl; -} -export namespace AddElementToGraphAction { - export const TYPE = "addElementToGraph"; - export function create(element: SChildElementImpl, parent: SParentElementImpl): AddElementToGraphAction { - return { - kind: TYPE, - element, - parent, - }; - } -} - -@injectable() -export class AddElementToGraphCommand implements ICommand { - public static readonly KIND = AddElementToGraphAction.TYPE; - - constructor(@inject(TYPES.Action) private action: AddElementToGraphAction) {} - - execute(context: CommandExecutionContext): CommandReturn { - this.action.parent.add(this.action.element); - return context.root; - } - - undo(context: CommandExecutionContext): CommandReturn { - this.action.element.parent.remove(this.action.element); - return context.root; - } - - redo(context: CommandExecutionContext): CommandReturn { - return this.execute(context); - } -} - -/** - * Util key listener that disables all registered creation tools when the escape key is pressed. - */ -@injectable() -export class CreationToolDisableKeyListener extends KeyListener { - @multiInject(EDITOR_TYPES.CreationTool) protected tools: AnyCreationTool[] = []; - - override keyDown(_element: SModelElementImpl, event: KeyboardEvent): Action[] { - if (event.key === "Escape") { - this.disableAllTools(); - } - - return []; - } - - private disableAllTools(): void { - this.tools.forEach((tool) => tool.disable()); - } -} diff --git a/frontend/webEditor/src/features/toolPalette/di.config.ts b/frontend/webEditor/src/features/toolPalette/di.config.ts deleted file mode 100644 index 2caa5a82..00000000 --- a/frontend/webEditor/src/features/toolPalette/di.config.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ContainerModule } from "inversify"; -import { EDITOR_TYPES } from "../../utils"; -import { AddElementToGraphCommand, CreationToolDisableKeyListener } from "./creationTool"; -import { EdgeCreationTool } from "./edgeCreationTool"; -import { NodeCreationTool } from "./nodeCreationTool"; -import { PortCreationTool } from "./portCreationTool"; -import { ToolPaletteUI } from "./toolPalette"; -import { - CommitModelAction, - EmptyView, - SNodeImpl, - TYPES, - configureActionHandler, - configureCommand, - configureModelElement, -} from "sprotty"; - -// This module contains an UI extension that adds a tool palette to the editor. -// This tool palette allows the user to create new nodes and edges. -// Additionally it contains the tools that are used to create the nodes and edges. - -export const toolPaletteModule = new ContainerModule((bind, unbind, isBound, rebind) => { - const context = { bind, unbind, isBound, rebind }; - - bind(CreationToolDisableKeyListener).toSelf().inSingletonScope(); - bind(TYPES.KeyListener).toService(CreationToolDisableKeyListener); - - configureModelElement(context, "empty-node", SNodeImpl, EmptyView); - configureCommand(context, AddElementToGraphCommand); - - bind(NodeCreationTool).toSelf().inSingletonScope(); - bind(EDITOR_TYPES.CreationTool).toService(NodeCreationTool); - - bind(EdgeCreationTool).toSelf().inSingletonScope(); - bind(EDITOR_TYPES.CreationTool).toService(EdgeCreationTool); - - bind(PortCreationTool).toSelf().inSingletonScope(); - bind(EDITOR_TYPES.CreationTool).toService(PortCreationTool); - - bind(ToolPaletteUI).toSelf().inSingletonScope(); - configureActionHandler(context, CommitModelAction.KIND, ToolPaletteUI); - bind(TYPES.IUIExtension).toService(ToolPaletteUI); - bind(TYPES.KeyListener).toService(ToolPaletteUI); - bind(EDITOR_TYPES.DefaultUIElement).toService(ToolPaletteUI); -}); diff --git a/frontend/webEditor/src/features/toolPalette/edgeCreationTool.ts b/frontend/webEditor/src/features/toolPalette/edgeCreationTool.ts deleted file mode 100644 index ead081e7..00000000 --- a/frontend/webEditor/src/features/toolPalette/edgeCreationTool.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { injectable } from "inversify"; -import { - Connectable, - isConnectable, - SChildElementImpl, - SEdgeImpl, - SModelElementImpl, - SParentElementImpl, -} from "sprotty"; -import { Action, SEdge, SNode } from "sprotty-protocol"; -import { generateRandomSprottyId } from "../../utils"; -import { CreationTool } from "./creationTool"; - -@injectable() -export class EdgeCreationTool extends CreationTool { - // Pseudo element that is used as a target for the edge while it is being created - private edgeTargetElement?: SChildElementImpl; - - // We will insert the edge ourselves once we determined the source element. - // Then we can also insert the edge target element at the mouse position - // and have the source and target element inserted. - // Otherwise sprotty would not be able to compute the path of the edge - // and show dangling elements. - protected override insertIntoGraphRootAfterCreation = false; - - createElementSchema(): SEdge { - return { - id: generateRandomSprottyId(), - type: this.elementType, - sourceId: "", - targetId: "", - }; - } - - disable(): void { - if (this.edgeTargetElement) { - // Pseudo edge target element must always be removed - // regardless of whether the edge creation was successful or cancelled - this.edgeTargetElement.parent?.remove(this.edgeTargetElement); - this.edgeTargetElement = undefined; - } - - super.disable(); - } - - mouseDown(target: SModelElementImpl, event: MouseEvent): Action[] { - if (!this.element) { - // This shouldn't happen - return []; - } - - const clickedElement = this.findConnectable(target); - if (!clickedElement) { - // Nothing can be connected to this element or its parents, invalid choice - return []; - } - - if (this.element.sourceId) { - // Source already set, so we're setting the target now - - if (clickedElement.canConnect(this.element, "target")) { - this.element.targetId = clickedElement.id; - - // Finalize creation and disable the tool - const actions = super.mouseDown(clickedElement, event); - // mouseDown() calls the parent class disable implementation, but we overwrite it here, so we need to call it manually - this.disable(); - return actions; - } - } else { - // Source not set yet, so we're setting the source now - if (clickedElement.canConnect(this.element, "source")) { - this.element.sourceId = clickedElement.id; - - // Insert the edge to make it visible. - const root = target.root; - root.add(this.element); - - // Create a new target element - // For previewing the edge it must be able to be rendered - // which means source and target *must* be set even though - // we don't know the target yet. - // To work around this we create a dummy target element - // that is snapped to the current mouse position. - // It is a SPort because a normal node - this.edgeTargetElement = this.modelFactory.createElement({ - id: generateRandomSprottyId(), - type: "empty-node", - position: this.calculateMousePosition(target, event), - } as SNode); - // Add empty node to the graph and as a edge target - root.add(this.edgeTargetElement); - this.element.targetId = this.edgeTargetElement.id; - } - } - return []; - } - - /** - * Recursively searches through the element's parents until a connectable element is found. - * This is required because the user may click on elements inside a node, which are not connectable. - * E.g. a the user clicks on a label inside the node but in this case the edge should be connected to the node itself. - * - * @param element Element to start searching from - * @returns The first connectable element found or undefined if none was found - */ - private findConnectable( - element: SChildElementImpl | SParentElementImpl | SModelElementImpl, - ): (Connectable & SModelElementImpl) | undefined { - if (isConnectable(element)) { - return element; - } - - if ("parent" in element && element.parent) { - return this.findConnectable(element.parent); - } else { - return undefined; - } - } -} diff --git a/frontend/webEditor/src/features/toolPalette/nodeCreationTool.ts b/frontend/webEditor/src/features/toolPalette/nodeCreationTool.ts deleted file mode 100644 index 8b1afe0f..00000000 --- a/frontend/webEditor/src/features/toolPalette/nodeCreationTool.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { injectable } from "inversify"; -import { generateRandomSprottyId } from "../../utils"; -import { SNodeImpl } from "sprotty"; -import { SNode } from "sprotty-protocol"; -import { CreationTool } from "./creationTool"; - -/** - * Creates a node when the user clicks somewhere on the root graph. - * The type of the node can be set using the parameter in the enable function. - * Automatically disables itself after creating a node. - */ -@injectable() -export class NodeCreationTool extends CreationTool { - createElementSchema(): SNode { - const defaultText = this.elementType.replace("node:", ""); - const defaultTextCapitalized = defaultText.charAt(0).toUpperCase() + defaultText.slice(1); - - return { - id: generateRandomSprottyId(), - type: this.elementType, - text: defaultTextCapitalized, - } as SNode; - } -} diff --git a/frontend/webEditor/src/features/toolPalette/portCreationTool.ts b/frontend/webEditor/src/features/toolPalette/portCreationTool.ts deleted file mode 100644 index 7dab8549..00000000 --- a/frontend/webEditor/src/features/toolPalette/portCreationTool.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { injectable } from "inversify"; -import { CommitModelAction, SChildElementImpl, SModelElementImpl, SPortImpl, SShapeElementImpl } from "sprotty"; -import { Action, SPort } from "sprotty-protocol"; -import { generateRandomSprottyId } from "../../utils"; -import { CreationTool } from "./creationTool"; - -@injectable() -export class PortCreationTool extends CreationTool { - createElementSchema(): SPort { - return { - id: generateRandomSprottyId(), - type: this.elementType, - opacity: 0, - }; - } - - mouseMove(target: SModelElementImpl, event: MouseEvent): (Action | Promise)[] { - if (!this.element) { - return []; - } - - const currentParent = this.element.parent; - const targetNode = this.findNodeElement(target); - - if (targetNode) { - // We're hovering over a node, add the port to the node (if not already) - if (currentParent !== targetNode && target instanceof SChildElementImpl) { - this.element.opacity = this.previewOpacity; - currentParent.remove(this.element); - target.add(this.element); - this.commandStack.update(this.element.root); - } - } else { - // We're not hovering over a node. - // Add the port to the root graph (if not already) and hide it. - if (currentParent !== target.root) { - this.element.opacity = 0; - currentParent.remove(this.element); - target.root.add(this.element); - this.commandStack.update(this.element.root); - } - } - - return super.mouseMove(target, event); - } - - mouseDown(target: SModelElementImpl, event: MouseEvent): Action[] { - if (this.element?.parent === target.root) { - this.disable(); - // Run some action to re-render the tool palette ui - // showing that the tool is disabled - return [CommitModelAction.create()]; - } - - return super.mouseDown(target, event); - } - - private findNodeElement(target: SModelElementImpl): SModelElementImpl | undefined { - if (target.type.startsWith("node")) { - return target; - } - if (target instanceof SChildElementImpl && target.parent instanceof SShapeElementImpl) { - return this.findNodeElement(target.parent); - } - return undefined; - } -} diff --git a/frontend/webEditor/src/features/toolPalette/toolPalette.css b/frontend/webEditor/src/features/toolPalette/toolPalette.css deleted file mode 100644 index dd458abb..00000000 --- a/frontend/webEditor/src/features/toolPalette/toolPalette.css +++ /dev/null @@ -1,63 +0,0 @@ -.tool-palette { - top: 40px; - padding: 3px; - right: 40px; - - /* Make text of the elements non-selectable */ - -webkit-user-select: none; /* Safari only supports user select using the -webkit prefix */ - user-select: none; - - /* grid layout (two tools per row) */ - display: grid; - grid-template-columns: 1fr 1fr 1fr; -} - -.tool-palette .tool { - width: 32px; - height: 32px; - border-radius: 5px; - padding: 2px; - margin: 2px; -} - -.tool-palette .tool svg line, -.tool-palette .tool svg path, -.tool-palette .tool svg rect, -.tool-palette .tool svg circle { - stroke: var(--color-foreground); - fill: transparent; -} -.tool-palette .tool svg .fill { - fill: var(--color-foreground); -} - -.tool-palette .tool svg text { - fill: var(--color-foreground); - font-size: 10px; - font-family: sans-serif; - text-anchor: middle; - dominant-baseline: central; -} - -.tool-palette .tool:hover { - cursor: pointer; - background-color: var(--color-tool-palette-hover); -} - -.tool-palette .tool.active { - background-color: var(--color-tool-palette-selected); -} - -/* Show keyboard shortcuts for each tool when help is opened */ -.tool-palette .tool .shortcut { - position: relative; - bottom: 16px; - left: -4px; - font-size: 0.75em; - - transition: opacity 300ms ease-in-out; - opacity: 0; -} -body.help-enabled .tool-palette .tool .shortcut { - opacity: 1; -} diff --git a/frontend/webEditor/src/features/toolPalette/toolPalette.tsx b/frontend/webEditor/src/features/toolPalette/toolPalette.tsx deleted file mode 100644 index 78becb03..00000000 --- a/frontend/webEditor/src/features/toolPalette/toolPalette.tsx +++ /dev/null @@ -1,275 +0,0 @@ -/** @jsx svg */ -import { injectable, inject, multiInject, optional } from "inversify"; -import { VNode } from "snabbdom"; -import { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - svg, - AbstractUIExtension, - IActionDispatcher, - IActionHandler, - ICommand, - TYPES, - PatcherProvider, - CommitModelAction, - SModelElementImpl, - KeyListener, -} from "sprotty"; -import { KeyCode, matchesKeystroke } from "sprotty/lib/utils/keyboard"; -import { Action } from "sprotty-protocol"; -import { NodeCreationTool } from "./nodeCreationTool"; -import { EdgeCreationTool } from "./edgeCreationTool"; -import { PortCreationTool } from "./portCreationTool"; -import { AnyCreationTool } from "./creationTool"; -import { EDITOR_TYPES } from "../../utils"; -import { EditorModeController } from "../editorMode/editorModeController"; - -import "../../common/commonStyling.css"; -import "./toolPalette.css"; - -/** - * UI extension that adds a tool palette to the diagram in the upper right. - * Currently this only allows activating the CreateEdgeTool. - */ -@injectable() -export class ToolPaletteUI extends AbstractUIExtension implements IActionHandler, KeyListener { - static readonly ID = "tool-palette"; - private readonly keyboardShortcuts: Map void> = new Map(); - - constructor( - @inject(TYPES.IActionDispatcher) protected readonly actionDispatcher: IActionDispatcher, - @inject(TYPES.PatcherProvider) protected readonly patcherProvider: PatcherProvider, - @inject(NodeCreationTool) protected readonly nodeCreationTool: NodeCreationTool, - @inject(EdgeCreationTool) protected readonly edgeCreationTool: EdgeCreationTool, - @inject(PortCreationTool) protected readonly portCreationTool: PortCreationTool, - @multiInject(EDITOR_TYPES.CreationTool) protected readonly allTools: AnyCreationTool[], - @inject(EditorModeController) - @optional() - protected readonly editorModeController: EditorModeController, - ) { - super(); - } - - id(): string { - return ToolPaletteUI.ID; - } - - containerClass(): string { - // The container element gets this class name by the sprotty base class. - return "tool-palette"; - } - - /** - * This method creates the sub elements of the tool palette. - * This is called by the sprotty base class after creating the container element. - */ - protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add("ui-float"); - document.addEventListener("keydown", (event) => { - if (matchesKeystroke(event, "Escape")) { - this.disableTools(); - } - }); - - this.addTool( - containerElement, - this.nodeCreationTool, - "Storage node", - (tool) => tool.enable("node:storage"), - - - - - Sto - - , - "Digit1", - ); - - this.addTool( - containerElement, - this.nodeCreationTool, - "Input/Output node", - (tool) => tool.enable("node:input-output"), - - - - IO - - , - "Digit2", - ); - - this.addTool( - containerElement, - this.nodeCreationTool, - "Function node", - (tool) => tool.enable("node:function"), - - - - - Fun - - , - "Digit3", - ); - - this.addTool( - containerElement, - this.edgeCreationTool, - "Edge", - (tool) => tool.enable("edge:arrow"), - - - - , - "Digit4", - ); - - this.addTool( - containerElement, - this.portCreationTool, - "Input port", - (tool) => tool.enable("port:dfd-input"), - - - - I - - , - "Digit5", - ); - - this.addTool( - containerElement, - this.portCreationTool, - "Output port", - (tool) => tool.enable("port:dfd-output"), - - - - O - - , - "Digit6", - ); - - containerElement.classList.add("tool-palette"); - } - - /** - * Utility function that adds a tool to the tool palette. - * - * @param container the base container html element of the tool palette - * @param toolId the id of the sprotty tool that should be activated when the tool is clicked - * @param name the name of the tool that is displayed as a alt text/tooltip - * @param clicked callback that is called when the tool is clicked. Can be used to configure the calling tool - * @param svgCode vnode for the svg logo of the tool. Will be placed in a 32x32 svg element - * @param enableKey optional key for a keyboard shortcut to activate the tool - */ - private addTool( - container: HTMLElement, - tool: T, - name: string, - enable: (tool: T) => void, - svgCode: VNode, - enableKey?: KeyCode, - ): void { - const toolElement = document.createElement("div"); - toolElement.classList.add("tool"); - - toolElement.addEventListener("click", () => { - if (toolElement.classList.contains("active") || this.editorModeController?.isReadOnly()) { - tool.disable(); - toolElement.classList.remove("active"); - } else { - // Disable all other tools - this.disableTools(); - - // Enable the selected tool - enable(tool); - - // Mark the tool as active - toolElement.classList.add("active"); - } - }); - - container.appendChild(toolElement); - - // When patching the snabbdom vnode into a DOM element, the element is replaced. - // So we create a dummy sub element inside the tool element and patch the svg node into that. - // This results in the toolElement holding the content. When patching directly onto the toolElement, - // it would be replaced by the svg node and the tool class would be removed with it, which we don't want. - const subElement = document.createElement("div"); - toolElement.appendChild(subElement); - const svgNode = ( - - {name} - {svgCode} - - ); - this.patcherProvider.patcher(subElement, svgNode); - - const shortcutElement = document.createElement("kbd"); - shortcutElement.classList.add("shortcut"); - shortcutElement.textContent = enableKey?.replace("Key", "").replace("Digit", "") ?? ""; - toolElement.appendChild(shortcutElement); - - if (enableKey) { - this.keyboardShortcuts.set(enableKey, () => { - toolElement.click(); - }); - - // Also add the shortcut for the corresponding numpad key - if (enableKey.startsWith("Digit")) { - this.keyboardShortcuts.set(enableKey.replace("Digit", "Numpad") as KeyCode, () => { - toolElement.click(); - }); - } - } - } - - private disableTools(): void { - this.allTools.forEach((tool) => tool.disable()); - this.markAllToolsInactive(); - } - - private markAllToolsInactive(): void { - if (!this.containerElement) return; - - // Remove active class from all tools, resulting in none of the tools being shown as active - this.containerElement.childNodes.forEach((node) => { - if (node instanceof HTMLElement) { - node.classList.remove("active"); - } - }); - } - - handle(action: Action): void | Action | ICommand { - // Some change has been made to the model. - // This may indicate the end of a tool action, so we show all tools to be inactive. - if (action.kind === CommitModelAction.KIND) { - this.markAllToolsInactive(); - } - } - - keyDown(_element: SModelElementImpl, event: KeyboardEvent): Action[] { - this.keyboardShortcuts.forEach((callback, key) => { - if (matchesKeystroke(event, key)) { - callback(); - } - }); - - return []; - } - - keyUp(): Action[] { - // ignored - return []; - } -} diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 0b285e73..120bfd1d 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -1,40 +1,6 @@ -import "reflect-metadata"; - import { Container } from "inversify"; -import { - AbstractUIExtension, - ActionDispatcher, - CommitModelAction, - ILogger, - LocalModelSource, - SetUIExtensionVisibilityAction, - TYPES, - labelEditUiModule, - loadDefaultModules, +import { loadDefaultModules, labelEditUiModule } from "sprotty"; -import { elkLayoutModule } from "sprotty-elk"; -import { autoLayoutModule } from "./features/autoLayout/di.config"; -import { commonModule } from "./common/di.config"; -import { noScrollLabelEditUiModule } from "./common/labelEditNoScroll"; -import { dfdLabelModule } from "./features/labels/di.config"; -import { toolPaletteModule } from "./features/toolPalette/di.config"; -import { serializeModule } from "./features/serialize/di.config"; -import { LoadDefaultDiagramAction } from "./features/serialize/loadDefaultDiagram"; -import { dfdElementsModule } from "./features/dfdElements/di.config"; -import { copyPasteModule } from "./features/copyPaste/di.config"; -import { EDITOR_TYPES } from "./utils"; -import { editorModeModule } from "./features/editorMode/di.config"; -import { constraintMenuModule } from "./features/constraintMenu/di.config"; - -import "sprotty/css/sprotty.css"; -import "sprotty/css/edit-label.css"; -import "./theme.css"; -import "./page.css"; -import { settingsModule } from "./features/settingsMenu/di.config"; -import { LoadDiagramAction } from "./features/serialize/load"; -import { commandPaletteModule } from "./features/commandPalette/di.config"; -import { LoadingIndicator } from "./common/loadingIndicator"; -import { LabelTypeRegistry } from "./features/labels/labelTypeRegistry"; const container = new Container(); @@ -43,130 +9,4 @@ loadDefaultModules(container, { exclude: [ labelEditUiModule, // We provide our own label edit ui inheriting from the default one (noScrollLabelEditUiModule) ], -}); - -// sprotty-elk layouting extension -container.load(elkLayoutModule); - -// Custom modules that we provide ourselves -container.load( - commonModule, - settingsModule, - noScrollLabelEditUiModule, - autoLayoutModule, - dfdElementsModule, - serializeModule, - dfdLabelModule, - editorModeModule, - toolPaletteModule, - copyPasteModule, - constraintMenuModule, - commandPaletteModule, -); - -const dispatcher = container.get(TYPES.IActionDispatcher); -const defaultUIElements = container.getAll(EDITOR_TYPES.DefaultUIElement); -const modelSource = container.get(TYPES.ModelSource); -export const logger = container.get(TYPES.ILogger); - -let modelFileName = "diagram"; - -export function setModelFileName(name: string): void { - modelFileName = name; -} - -export function getModelFileName(): string { - return modelFileName; -} -export const labelTypeRegistry = container.get(LabelTypeRegistry); - -export function setModelSource(file: File): void { - modelSource - .setModel({ - type: "graph", - id: "root", - children: [], - }) - .then(() => - dispatcher.dispatchAll([ - // Show the default uis after startup - ...defaultUIElements.map((uiElement) => { - return SetUIExtensionVisibilityAction.create({ - extensionId: uiElement.id(), - visible: true, - }); - }), - // Then load the default diagram and commit the temporary model to the model source - LoadDiagramAction.create(file), - CommitModelAction.create(), - ]), - ) - .then(() => { - // Focus the sprotty svg container to enable keyboard shortcuts - // because those only work if the svg container is focused. - // Allows to e.g. use the file open shortcut without having to click - // on the sprotty svg container first. - const sprottySvgContainer = document.getElementById("sprotty_root"); - sprottySvgContainer?.focus(); - }) - .catch((error) => { - logger.error(null, "Failed to show default UIs and load default diagram", error); - }); -} - -function getQueryFileName(): string | null { - const urlParams = new URLSearchParams(window.location.search); - return urlParams.get("file"); -} - -// Set empty model as starting point. -// In contrast to the default diagram later this is not undoable which would bring the editor -// into an invalid state where no root element is present. -modelSource - .setModel({ - type: "graph", - id: "root", - children: [], - }) - .then(async () => { - const queryFileName = getQueryFileName(); - let queryFile: File | null = null; - if (queryFileName) { - try { - const response = await fetch(queryFileName); - if (!response.ok) { - throw new Error(`Failed to fetch file: ${response.statusText}`); - } - const blob = await response.blob(); - queryFile = new File([blob], queryFileName, { type: blob.type }); - } catch (error) { - logger.error(null, `Failed to load file from query parameter: ${queryFileName}`, error); - } - } - - dispatcher.dispatchAll([ - // Show the default uis after startup - ...defaultUIElements.map((uiElement) => { - return SetUIExtensionVisibilityAction.create({ - extensionId: uiElement.id(), - visible: true, - }); - }), - // Then load the default diagram or query diagram and commit the temporary model to the model source - queryFile ? LoadDiagramAction.create(queryFile) : LoadDefaultDiagramAction.create(), - CommitModelAction.create(), - ]); - }) - .then(() => { - // Focus the sprotty svg container to enable keyboard shortcuts - // because those only work if the svg container is focused. - // Allows to e.g. use the file open shortcut without having to click - // on the sprotty svg container first. - const sprottySvgContainer = document.getElementById("sprotty_root"); - sprottySvgContainer?.focus(); - }) - .catch((error) => { - logger.error(null, "Failed to show default UIs and load default diagram", error); - }); - -export const loadingIndicator = container.get(LoadingIndicator); +}); \ No newline at end of file diff --git a/frontend/webEditor/src/page.css b/frontend/webEditor/src/page.css deleted file mode 100644 index fd00b8a3..00000000 --- a/frontend/webEditor/src/page.css +++ /dev/null @@ -1,28 +0,0 @@ -/* Styling of the page around the sprotty container */ - -body { - background-color: var(--color-background); - color: var(--color-foreground); - padding: 0; - margin: 0; - /* The sprotty container is slight larger than the browser - viewport, so we need this to avoid a horizontal scrollbar. */ - overflow: hidden; -} - -/* Make sprotty relative to be able to position ui elements - inside the sprotty viewport using position absolute - and absolute top/left/bottom/right values. */ -#sprotty { - position: relative; -} - -svg.sprotty-graph { - width: 100%; - height: 100vh; - outline: none; -} - -.sprotty-hidden { - display: none; -} diff --git a/frontend/webEditor/src/theme.css b/frontend/webEditor/src/theme.css deleted file mode 100644 index d88c16a3..00000000 --- a/frontend/webEditor/src/theme.css +++ /dev/null @@ -1,28 +0,0 @@ -:root { - --color-foreground: #000; - --color-background: #fff; - --color-primary: #dfdfdf; - --color-spacer: #e5e5e5; - --color-error: #f00; - --color-valid: #00b600; - --color-highlighted: #77777a; - --color-tool-palette-hover: #ccc; - --color-tool-palette-selected: #bbb; - --dark-mode: 0; -} - -:root[data-theme="dark"] { - --color-foreground: #fff; - --color-background: #1d1c22; - --color-spacer: var(--color-background); - --color-primary: #302e38; - --color-valid: #0f0; - --color-tool-palette-hover: #f00; - --color-tool-palette-selected: #f00; - --dark-mode: 1; -} - -#sprotty[data-theme="dark"] div { - /* Use default dark theme browser styles for scrollbars etc. inside all UI extensions */ - color-scheme: dark; -} diff --git a/frontend/webEditor/src/utils.ts b/frontend/webEditor/src/utils.ts deleted file mode 100644 index 5f64cfa5..00000000 --- a/frontend/webEditor/src/utils.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { SModelRootImpl } from "sprotty"; -import { FitToScreenAction, getBasicType, SModelRoot } from "sprotty-protocol"; - -/** - * Type identifiers for use with inversify. - */ -export const EDITOR_TYPES = { - // Enableable and disableable tools that can be used to create new elements. - CreationTool: Symbol("CreationTool"), - // All IUIExtension instances that are bound to this symbol will - // be loaded and enabled at editor startup. - DefaultUIElement: Symbol("DefaultUIElement"), -}; - -export const FIT_TO_SCREEN_PADDING = 75; - -/** - * Generates a fit to screen action that fits all nodes on the screen - * with the default padding. - */ -export function createDefaultFitToScreenAction(root: SModelRootImpl | SModelRoot, animate = true): FitToScreenAction { - const elementIds = root.children?.filter((child) => getBasicType(child) === "node").map((child) => child.id) ?? []; - - return FitToScreenAction.create(elementIds, { - padding: FIT_TO_SCREEN_PADDING, - animate, - }); -} - -export function generateRandomSprottyId(): string { - return Math.random().toString(36).substring(7); -} - -interface TextSize { - width: number; - height: number; -} -const contextMap = new Map }>(); - -/** - * Calculates the width and height of the given text in the given font using a 2d canvas. - * Because this operation requires interacting with the browser including styling etc. - * this is rather expensive. Therefore the result is cached with the font and text as a cache key - * The default width for empty text is 20px. - * Big diagrams with hundreds of text elements (edges, nodes, labels) would not be possible without caching this operation. - * - * @param text the text you need the size for - * @param font the font to use, defaults to "11pt sans-serif" - * @returns the width and height of the text in pixels. This does not include any padding or margin - */ -export function calculateTextSize(text: string | undefined, font: string = "11pt sans-serif"): TextSize { - if (!text || text.length === 0) { - return { width: 20, height: 20 }; - } - - // Get context for the given font or create a new one if it does not exist yet - let contextObj = contextMap.get(font); - if (!contextObj) { - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - if (!context) { - throw new Error("Could not create canvas context used to measure text width"); - } - - context.font = font; // This is slow. Thats why we have one instance per font to avoid redoing this - contextObj = { context, cache: new Map() }; - contextMap.set(font, contextObj); - } - - const { context, cache } = contextObj; - - // Get text width from cache or compute it - const cachedMetrics = cache.get(text); - if (cachedMetrics) { - return cachedMetrics; - } else { - const metrics = context.measureText(text); - const textSize: TextSize = { - width: Math.round(metrics.width), - height: Math.round(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent), - }; - - cache.set(text, textSize); - return textSize; - } -} From 82b329ba46711f88e71d3b87c446f5d424ede85c Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Sat, 11 Oct 2025 19:17:04 +0200 Subject: [PATCH 02/41] help ui and startup --- frontend/webEditor/dependencyGraph.md | 12 ++ .../src/accordionUiExtension/accordion.css | 110 ++++++++++++++++++ .../src/accordionUiExtension/index.ts | 61 ++++++++++ .../webEditor/src/assets/commonStyling.css | 26 +++++ frontend/webEditor/src/assets/page.css | 31 +++++ frontend/webEditor/src/assets/theme.css | 28 +++++ frontend/webEditor/src/commonModule.ts | 13 +++ frontend/webEditor/src/diagram/di.config.ts | 8 ++ frontend/webEditor/src/editorTypes.ts | 10 ++ frontend/webEditor/src/helpUi/di.config.ts | 10 ++ frontend/webEditor/src/helpUi/helpUi.css | 16 +++ frontend/webEditor/src/helpUi/helpUi.ts | 43 +++++++ frontend/webEditor/src/index.ts | 23 +++- .../startUpAgent/LoadDefaultUiExtensions.ts | 19 +++ .../webEditor/src/startUpAgent/SprottyInit.ts | 15 +++ .../src/startUpAgent/StartUpAgent.ts | 5 + .../webEditor/src/startUpAgent/di.config.ts | 8 ++ frontend/webEditor/src/vite-env.d.ts | 3 + 18 files changed, 438 insertions(+), 3 deletions(-) create mode 100644 frontend/webEditor/dependencyGraph.md create mode 100644 frontend/webEditor/src/accordionUiExtension/accordion.css create mode 100644 frontend/webEditor/src/accordionUiExtension/index.ts create mode 100644 frontend/webEditor/src/assets/commonStyling.css create mode 100644 frontend/webEditor/src/assets/page.css create mode 100644 frontend/webEditor/src/assets/theme.css create mode 100644 frontend/webEditor/src/commonModule.ts create mode 100644 frontend/webEditor/src/diagram/di.config.ts create mode 100644 frontend/webEditor/src/editorTypes.ts create mode 100644 frontend/webEditor/src/helpUi/di.config.ts create mode 100644 frontend/webEditor/src/helpUi/helpUi.css create mode 100644 frontend/webEditor/src/helpUi/helpUi.ts create mode 100644 frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts create mode 100644 frontend/webEditor/src/startUpAgent/SprottyInit.ts create mode 100644 frontend/webEditor/src/startUpAgent/StartUpAgent.ts create mode 100644 frontend/webEditor/src/startUpAgent/di.config.ts create mode 100644 frontend/webEditor/src/vite-env.d.ts diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md new file mode 100644 index 00000000..2688eac9 --- /dev/null +++ b/frontend/webEditor/dependencyGraph.md @@ -0,0 +1,12 @@ +```mermaid +stateDiagram-v2 + [*] --> helpUi + [*] --> startUpAgent + helpUi --> accordionUiExtension + helpUi --> editorTypes + startUpAgent --> editorTypes + [*] --> commonModule + + classDef diLess font-style:italic,stroke:#0ff + class accordionUiExtension,editorTypes diLess +``` \ No newline at end of file diff --git a/frontend/webEditor/src/accordionUiExtension/accordion.css b/frontend/webEditor/src/accordionUiExtension/accordion.css new file mode 100644 index 00000000..8bd64783 --- /dev/null +++ b/frontend/webEditor/src/accordionUiExtension/accordion.css @@ -0,0 +1,110 @@ +/* accordion */ +.accordion-content { + display: grid; + /* This transition is used when closing the accordion. Here the x direction should start slow and then end fast, thus ease-out */ + transition: + grid-template-rows 300ms ease, + /* ease-in animation: https://cubic-bezier.com/#.7,0,1,.6 */ grid-template-columns 300ms + cubic-bezier(0.7, 0, 1, 0.6), + padding-top 300ms ease; + + grid-template-rows: 0fr; + grid-template-columns: 0fr; + padding-top: 0; +} + +.accordion-state:checked ~ .accordion-content { + grid-template-rows: 1fr; + grid-template-columns: 1fr; + + /* This transition is used when opening the accordion. Here the x direction should start fast and then end slow, thus ease-in */ + transition: + grid-template-rows 300ms ease, + /* ease-out animation: https://cubic-bezier.com/#0,.7,.4,1 */ /* mirrored version of the curve above */ + grid-template-columns 300ms cubic-bezier(0, 0.7, 0.4, 1), + padding-top 300ms ease; + + /* space between accordion button and the content, otherwise they would be directly next to each other without any spacing */ + padding-top: 8px; +} + +/* needed to hide the content when the accordion is closed */ +.accordion-content * { + overflow: hidden; + white-space: nowrap; + text-overflow: clip; +} + +/* drop-down icon */ +.accordion-button { + /* Make the text unselectable. When rapidly clicking the accordion button, + the text would be selected otherwise due to a double click. */ + -webkit-user-select: none; + user-select: none; + + /* Default orientation of the chevron: pointing down */ + --chevron-scale: 1; +} + +.accordion-button.flip-chevron { + /* Default orientation of the chevron: pointing up */ + --chevron-scale: -1; +} + +.accordion-button.chevron-right { + /* space for the icon */ + padding-right: 2em; +} + +.accordion-button.chevron-left { + /* space for the icon */ + padding-left: 2em; +} + +.accordion-button.chevron-right::after { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg"); + right: 1em; + position: absolute; + display: inline-block; + + /* only filter=invert(1) if dark mode is enabled aka --dark-mode is set to 1 */ + filter: invert(var(--dark-mode)); + + width: 16px; + height: 16px; + background-size: 16px 16px; + + vertical-align: text-top; + transition: transform 500ms ease; + transform: scaleY(var(--chevron-scale)); +} + +.accordion-button.chevron-left::before { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg"); + left: 1em; + position: absolute; + display: inline-block; + + /* only filter=invert(1) if dark mode is enabled aka --dark-mode is set to 1 */ + filter: invert(var(--dark-mode)); + + width: 16px; + height: 16px; + background-size: 16px 16px; + + vertical-align: text-top; + transition: transform 500ms ease; + transform: scaleY(var(--chevron-scale)); +} + +.accordion-state:checked ~ label .accordion-button::after { + /* flip chevron in y direction */ + transform: scaleY(calc(var(--chevron-scale) * -1)); +} + +.accordion-state:checked ~ label .accordion-button::before { + /* flip chevron in y direction */ + transform: scaleY(calc(var(--chevron-scale) * -1)); +} diff --git a/frontend/webEditor/src/accordionUiExtension/index.ts b/frontend/webEditor/src/accordionUiExtension/index.ts new file mode 100644 index 00000000..5ae7b9fd --- /dev/null +++ b/frontend/webEditor/src/accordionUiExtension/index.ts @@ -0,0 +1,61 @@ +import { AbstractUIExtension } from "sprotty"; +import { injectable } from "inversify"; +import "./accordion.css" + +/** + * Base class for an expandable accordion floating element + */ +@injectable() +export abstract class AccordionUiExtension extends AbstractUIExtension { + + constructor(private chevronPosition: 'left'|'right', private chevronOrientation: 'up'|'down') { + super(); + } + + protected initializeContents(containerElement: HTMLElement): void { + containerElement.classList.add('ui-float'); + + // create hidden checkbox used for toggling + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + const checkboxId = this.id() + '-checkbox'; + checkbox.id = checkboxId; + checkbox.classList.add('accordion-state'); + checkbox.hidden = true + + // create clickable label for the checkbox + const label = document.createElement('label') + label.htmlFor = checkboxId + // create header inside label + const header = document.createElement('div') + header.classList.add(`chevron-${this.chevronPosition}`, 'accordion-button') + if (this.chevronOrientation === 'up') { + header.classList.add('flip-chevron') + } + this.initializeHeaderContent(header); + label.appendChild(header) + + // create content holder and initialize it + const accordionContent = document.createElement('div') + accordionContent.classList.add('accordion-content') + const contentHolder = document.createElement('div') + this.initializeHidableContent(contentHolder) + accordionContent.appendChild(contentHolder); + + containerElement.appendChild(checkbox) + containerElement.appendChild(label) + containerElement.appendChild(accordionContent) + } + + /** + * Initializes the hidable content of the accordion element + * @param contentElement The containing element of the content + */ + protected abstract initializeHidableContent(contentElement: HTMLElement): void; + + /** + * Initializes the header of the accordion element + * @param contentElement The containing element of the header + */ + protected abstract initializeHeaderContent(headerElement: HTMLElement): void; +} \ No newline at end of file diff --git a/frontend/webEditor/src/assets/commonStyling.css b/frontend/webEditor/src/assets/commonStyling.css new file mode 100644 index 00000000..5ca9c56c --- /dev/null +++ b/frontend/webEditor/src/assets/commonStyling.css @@ -0,0 +1,26 @@ +.ui-float { + position: absolute; + border-radius: 10px; + background-color: var(--color-primary); +} + +/* Styling for keyboard symbols. + Copied from the example at https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd + with adapted colors */ +kbd { + background-color: var(--color-primary); + color: var(--color-foreground); + + border-radius: 3px; + border: 1px solid var(--color-foreground); + box-shadow: + 0 1px 1px var(--color-foreground), + 0 2px 0 0 var(--color-background) inset; + display: inline-block; + font-size: 0.85em; + font-weight: 700; + line-height: 1; + padding: 2px 4px; + white-space: nowrap; +} + diff --git a/frontend/webEditor/src/assets/page.css b/frontend/webEditor/src/assets/page.css new file mode 100644 index 00000000..93ff3dd5 --- /dev/null +++ b/frontend/webEditor/src/assets/page.css @@ -0,0 +1,31 @@ +/* Styling of the page around the sprotty container */ + +body { + background-color: var(--color-background); + color: var(--color-foreground); + padding: 0; + margin: 0; + /* The sprotty container is slight larger than the browser + viewport, so we need this to avoid a horizontal scrollbar. */ + overflow: hidden; +} + +/* Make sprotty relative to be able to position ui elements + inside the sprotty viewport using position absolute + and absolute top/left/bottom/right values. */ +#sprotty { + position: relative; + height: 100vh; + width: 100vw; + /*temporary*/ font-family: Helvetica Neue,Helvetica,Arial,sans-serif; padding:0; +} + +svg.sprotty-graph { + width: 100%; + height: 100%; + outline: none; +} + +.sprotty-hidden { + display: none; +} diff --git a/frontend/webEditor/src/assets/theme.css b/frontend/webEditor/src/assets/theme.css new file mode 100644 index 00000000..d88c16a3 --- /dev/null +++ b/frontend/webEditor/src/assets/theme.css @@ -0,0 +1,28 @@ +:root { + --color-foreground: #000; + --color-background: #fff; + --color-primary: #dfdfdf; + --color-spacer: #e5e5e5; + --color-error: #f00; + --color-valid: #00b600; + --color-highlighted: #77777a; + --color-tool-palette-hover: #ccc; + --color-tool-palette-selected: #bbb; + --dark-mode: 0; +} + +:root[data-theme="dark"] { + --color-foreground: #fff; + --color-background: #1d1c22; + --color-spacer: var(--color-background); + --color-primary: #302e38; + --color-valid: #0f0; + --color-tool-palette-hover: #f00; + --color-tool-palette-selected: #f00; + --dark-mode: 1; +} + +#sprotty[data-theme="dark"] div { + /* Use default dark theme browser styles for scrollbars etc. inside all UI extensions */ + color-scheme: dark; +} diff --git a/frontend/webEditor/src/commonModule.ts b/frontend/webEditor/src/commonModule.ts new file mode 100644 index 00000000..cef2a327 --- /dev/null +++ b/frontend/webEditor/src/commonModule.ts @@ -0,0 +1,13 @@ +import { ContainerModule } from "inversify"; +import { TYPES, LocalModelSource, ConsoleLogger, LogLevel, configureViewerOptions } from "sprotty"; + +export const commonModule = new ContainerModule((bind, unbind, isBound, rebind) => { + + bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope(); + rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); + rebind(TYPES.LogLevel).toConstantValue(LogLevel.warn); // TODO: set to log again + const context = { bind, unbind, isBound, rebind }; + configureViewerOptions(context, { + zoomLimits: { min: 0.05, max: 20 }, + }); +}); \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/di.config.ts b/frontend/webEditor/src/diagram/di.config.ts new file mode 100644 index 00000000..49337334 --- /dev/null +++ b/frontend/webEditor/src/diagram/di.config.ts @@ -0,0 +1,8 @@ +import { ContainerModule } from "inversify"; +import { configureModelElement, SGraphImpl, SGraphView } from "sprotty"; + +export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + + configureModelElement(context, "graph", SGraphImpl, SGraphView); +}); \ No newline at end of file diff --git a/frontend/webEditor/src/editorTypes.ts b/frontend/webEditor/src/editorTypes.ts new file mode 100644 index 00000000..9a302780 --- /dev/null +++ b/frontend/webEditor/src/editorTypes.ts @@ -0,0 +1,10 @@ +/** + * Type identifiers for use with inversify. + */ +export const EDITOR_TYPES = { + // Enableable and disableable tools that can be used to create new elements. + CreationTool: Symbol("CreationTool"), + // All IUIExtension instances that are bound to this symbol will + // be loaded and enabled at editor startup. + DefaultUIElement: Symbol("DefaultUIElement"), +}; \ No newline at end of file diff --git a/frontend/webEditor/src/helpUi/di.config.ts b/frontend/webEditor/src/helpUi/di.config.ts new file mode 100644 index 00000000..7270805b --- /dev/null +++ b/frontend/webEditor/src/helpUi/di.config.ts @@ -0,0 +1,10 @@ +import { ContainerModule } from "inversify"; +import { TYPES } from "sprotty"; +import { HelpUI } from "./helpUi"; +import { EDITOR_TYPES } from "../editorTypes"; + +export const helpUiModule = new ContainerModule((bind) => { + bind(HelpUI).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(HelpUI); + bind(EDITOR_TYPES.DefaultUIElement).toService(HelpUI); +}); \ No newline at end of file diff --git a/frontend/webEditor/src/helpUi/helpUi.css b/frontend/webEditor/src/helpUi/helpUi.css new file mode 100644 index 00000000..d06b5e9c --- /dev/null +++ b/frontend/webEditor/src/helpUi/helpUi.css @@ -0,0 +1,16 @@ +div.help-ui { + left: 20px; + bottom: 20px; + padding: 10px 10px; +} + +.help-accordion-icon::before { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/regular/circle-question.svg"); + display: inline-block; + filter: invert(var(--dark-mode)); + height: 16px; + width: 16px; + background-size: 16px 16px; + vertical-align: text-top; +} diff --git a/frontend/webEditor/src/helpUi/helpUi.ts b/frontend/webEditor/src/helpUi/helpUi.ts new file mode 100644 index 00000000..65cb9f8e --- /dev/null +++ b/frontend/webEditor/src/helpUi/helpUi.ts @@ -0,0 +1,43 @@ +import { injectable } from "inversify"; +import "./helpUi.css"; +import { AccordionUiExtension } from "../accordionUiExtension"; + +@injectable() +export class HelpUI extends AccordionUiExtension { + static readonly ID = "help-ui"; + + constructor() { + super('right', 'up') + } + + id() { + return HelpUI.ID; + } + + containerClass() { + return HelpUI.ID; + } + + protected initializeHidableContent(contentElement: HTMLElement) { + contentElement.innerHTML = ` +

CTRL+Space: Command Palette

+

CTRL+Z: Undo

+

CTRL+Shift+Z: Redo

+

Del: Delete selected elements

+

T: Toggle Label Type Edit UI

+

CTRL+O: Load diagram from json

+

CTRL+Shift+O: Open default diagram

+

CTRL+S: Save diagram to json

+

CTRL+L: Automatically layout diagram

+

CTRL+Shift+F: Fit diagram to screen

+

CTRL+C: Copy selected elements

+

CTRL+V: Paste previously copied elements

+

Esc: Disable current creation tool

+

Toggle Creation Tool: Refer to key in the tool palette

+ `; + } + protected initializeHeaderContent(headerElement: HTMLElement) { + headerElement.classList.add('help-accordion-icon'); + headerElement.innerText = 'Keyboard Shortcuts | Help' + } +} diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 120bfd1d..d896c8a9 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -1,6 +1,12 @@ import { Container } from "inversify"; -import { loadDefaultModules, labelEditUiModule -} from "sprotty"; +import { loadDefaultModules, labelEditUiModule } from "sprotty"; +import "./assets/commonStyling.css" +import "./assets/page.css" +import "./assets/theme.css" +import { helpUiModule } from "./helpUi/di.config"; +import { IStartUpAgent, StartUpAgent } from "./startUpAgent/StartUpAgent"; +import { startUpAgentModule } from "./startUpAgent/di.config"; +import { commonModule } from "./commonModule"; const container = new Container(); @@ -9,4 +15,15 @@ loadDefaultModules(container, { exclude: [ labelEditUiModule, // We provide our own label edit ui inheriting from the default one (noScrollLabelEditUiModule) ], -}); \ No newline at end of file +}); + +container.load( + helpUiModule, + commonModule, + startUpAgentModule +) + +const startUpAgents = container.getAll(StartUpAgent) +for (const startUpAgent of startUpAgents) { + startUpAgent.run() +} diff --git a/frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts b/frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts new file mode 100644 index 00000000..54d59a32 --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts @@ -0,0 +1,19 @@ +import { inject, injectable, multiInject } from "inversify"; +import { EDITOR_TYPES } from "../editorTypes"; +import { AbstractUIExtension, ActionDispatcher, SetUIExtensionVisibilityAction, TYPES } from "sprotty"; +import { IStartUpAgent } from "./StartUpAgent"; + +@injectable() +export class LoadDefaultUiExtensionsStartUpAgent implements IStartUpAgent { + constructor( + @inject(TYPES.IActionDispatcher) private actionDispatcher: ActionDispatcher, + @multiInject(EDITOR_TYPES.DefaultUIElement) private defaultUiElements: AbstractUIExtension[], + ) {} + + public run() { + const uiVisibilityActions = this.defaultUiElements.map((e) => + SetUIExtensionVisibilityAction.create({ extensionId: e.id(), visible: true }), + ); + this.actionDispatcher.dispatchAll(uiVisibilityActions); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/startUpAgent/SprottyInit.ts b/frontend/webEditor/src/startUpAgent/SprottyInit.ts new file mode 100644 index 00000000..fc264fb5 --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/SprottyInit.ts @@ -0,0 +1,15 @@ +import { inject } from "inversify"; +import { IStartUpAgent } from "./StartUpAgent"; +import { LocalModelSource, TYPES } from "sprotty"; + +export class SprottyInitializerStartUpAgents implements IStartUpAgent { + constructor(@inject(TYPES.ModelSource) private modelSource: LocalModelSource) {} + + run() { + this.modelSource.setModel({ + type: "graph", + id: "root", + children: [], + }); + } +} diff --git a/frontend/webEditor/src/startUpAgent/StartUpAgent.ts b/frontend/webEditor/src/startUpAgent/StartUpAgent.ts new file mode 100644 index 00000000..5127a482 --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/StartUpAgent.ts @@ -0,0 +1,5 @@ +export interface IStartUpAgent { + run(): void; +} + +export const StartUpAgent = Symbol('StartUpAgent') \ No newline at end of file diff --git a/frontend/webEditor/src/startUpAgent/di.config.ts b/frontend/webEditor/src/startUpAgent/di.config.ts new file mode 100644 index 00000000..bb158091 --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/di.config.ts @@ -0,0 +1,8 @@ +import { ContainerModule } from "inversify"; +import { StartUpAgent } from "./StartUpAgent"; +import { LoadDefaultUiExtensionsStartUpAgent } from "./LoadDefaultUiExtensions"; + +export const startUpAgentModule = new ContainerModule((bind) => { + bind(StartUpAgent).to(LoadDefaultUiExtensionsStartUpAgent) + +}) \ No newline at end of file diff --git a/frontend/webEditor/src/vite-env.d.ts b/frontend/webEditor/src/vite-env.d.ts new file mode 100644 index 00000000..357d6d1d --- /dev/null +++ b/frontend/webEditor/src/vite-env.d.ts @@ -0,0 +1,3 @@ +/// + +declare module '*.css'; \ No newline at end of file From a35367a924983003277c4e52536176bb038f9647 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 31 Oct 2025 09:47:57 +0100 Subject: [PATCH 03/41] labels --- frontend/webEditor/dependencyGraph.md | 8 +- frontend/webEditor/src/helpUi/helpUi.css | 24 ++-- frontend/webEditor/src/index.ts | 7 +- frontend/webEditor/src/labels/LabelType.ts | 15 ++ .../webEditor/src/labels/LabelTypeEditorUi.ts | 131 ++++++++++++++++++ .../webEditor/src/labels/LabelTypeRegistry.ts | 90 ++++++++++++ frontend/webEditor/src/labels/di.config.ts | 13 ++ .../src/labels/labelTypeEditorUi.css | 47 +++++++ frontend/webEditor/src/utils/TextSize.ts | 54 ++++++++ .../webEditor/src/utils/UiElementFactory.ts | 28 ++++ .../webEditor/src/utils/baseUiElements.css | 6 + frontend/webEditor/src/utils/idGenerator.ts | 3 + 12 files changed, 412 insertions(+), 14 deletions(-) create mode 100644 frontend/webEditor/src/labels/LabelType.ts create mode 100644 frontend/webEditor/src/labels/LabelTypeEditorUi.ts create mode 100644 frontend/webEditor/src/labels/LabelTypeRegistry.ts create mode 100644 frontend/webEditor/src/labels/di.config.ts create mode 100644 frontend/webEditor/src/labels/labelTypeEditorUi.css create mode 100644 frontend/webEditor/src/utils/TextSize.ts create mode 100644 frontend/webEditor/src/utils/UiElementFactory.ts create mode 100644 frontend/webEditor/src/utils/baseUiElements.css create mode 100644 frontend/webEditor/src/utils/idGenerator.ts diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md index 2688eac9..eadb6dd1 100644 --- a/frontend/webEditor/dependencyGraph.md +++ b/frontend/webEditor/dependencyGraph.md @@ -6,7 +6,11 @@ stateDiagram-v2 helpUi --> editorTypes startUpAgent --> editorTypes [*] --> commonModule + [*] --> labels + labels --> utils + labels --> editorTypes + labels --> accordionUiExtension - classDef diLess font-style:italic,stroke:#0ff - class accordionUiExtension,editorTypes diLess + classDef diLess font-style:italic,stroke:#0fa + class accordionUiExtension,editorTypes,utils diLess ``` \ No newline at end of file diff --git a/frontend/webEditor/src/helpUi/helpUi.css b/frontend/webEditor/src/helpUi/helpUi.css index d06b5e9c..b0351b89 100644 --- a/frontend/webEditor/src/helpUi/helpUi.css +++ b/frontend/webEditor/src/helpUi/helpUi.css @@ -1,16 +1,18 @@ -div.help-ui { +.help-ui { left: 20px; bottom: 20px; padding: 10px 10px; -} -.help-accordion-icon::before { - content: ""; - background-image: url("@fortawesome/fontawesome-free/svgs/regular/circle-question.svg"); - display: inline-block; - filter: invert(var(--dark-mode)); - height: 16px; - width: 16px; - background-size: 16px 16px; - vertical-align: text-top; + .help-accordion-icon::before { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/regular/circle-question.svg"); + display: inline-block; + filter: invert(var(--dark-mode)); + height: 16px; + width: 16px; + background-size: 16px 16px; + vertical-align: text-top; + } } + + diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index d896c8a9..9f071944 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -1,12 +1,16 @@ +import "reflect-metadata"; import { Container } from "inversify"; import { loadDefaultModules, labelEditUiModule } from "sprotty"; +import "sprotty/css/sprotty.css"; import "./assets/commonStyling.css" import "./assets/page.css" import "./assets/theme.css" +import "@vscode/codicons/dist/codicon.css"; import { helpUiModule } from "./helpUi/di.config"; import { IStartUpAgent, StartUpAgent } from "./startUpAgent/StartUpAgent"; import { startUpAgentModule } from "./startUpAgent/di.config"; import { commonModule } from "./commonModule"; +import { labelModule } from "./labels/di.config"; const container = new Container(); @@ -20,7 +24,8 @@ loadDefaultModules(container, { container.load( helpUiModule, commonModule, - startUpAgentModule + startUpAgentModule, + labelModule ) const startUpAgents = container.getAll(StartUpAgent) diff --git a/frontend/webEditor/src/labels/LabelType.ts b/frontend/webEditor/src/labels/LabelType.ts new file mode 100644 index 00000000..2fc2b067 --- /dev/null +++ b/frontend/webEditor/src/labels/LabelType.ts @@ -0,0 +1,15 @@ +export interface LabelType { + id: string; + name: string; + values: LabelTypeValue[]; +} + +export interface LabelTypeValue { + id: string; + text: string; +} + +export interface LabelAssignment { + labelTypeId: string; + labelTypeValueId: string; +} \ No newline at end of file diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts new file mode 100644 index 00000000..3a8be915 --- /dev/null +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -0,0 +1,131 @@ +import { AccordionUiExtension } from "../accordionUiExtension"; +import { UiElementFactory } from "../utils/UiElementFactory"; +import { LabelType } from "./LabelType"; +import { inject } from "inversify"; +import { LabelTypeRegistry } from "./LabelTypeRegistry"; + +import './labelTypeEditorUi.css' +import { dynamicallySetInputSize } from "../utils/TextSize"; + +export class LabelTypeEditorUi extends AccordionUiExtension { + static readonly ID = "label-type-editor-ui"; + private labelSectionContainer?: HTMLElement + + constructor(@inject(LabelTypeRegistry) private labelTypeRegistry: LabelTypeRegistry) { + super('left', 'down') + labelTypeRegistry.onUpdate(() => this.renderLabelTypes()) + } + + id(): string { + return LabelTypeEditorUi.ID + } + containerClass(): string { + return LabelTypeEditorUi.ID + } + + protected initializeHidableContent(contentElement: HTMLElement) { + const addButton = UiElementFactory.buildAddButton('Label Type') + + addButton.onclick = () => { + this.labelTypeRegistry.registerLabelType('') + } + + this.labelSectionContainer = document.createElement('div') + this.renderLabelTypes() + + contentElement.appendChild(this.labelSectionContainer) + contentElement.appendChild(addButton) + } + protected initializeHeaderContent(headerElement: HTMLElement) { + headerElement.innerText = 'Label Types' + } + + private renderLabelTypes(): void { + if (!this.labelSectionContainer) { + return + } + this.labelSectionContainer.innerHTML = ''; + const labelTypes = this.labelTypeRegistry.getLabelTypes() + for (let i = 0; i < labelTypes.length; i++) { + this.labelSectionContainer.appendChild(this.buildLabelTypeSection(labelTypes[i])) + if (i < labelTypes.length - 1) { + this.labelSectionContainer.appendChild(document.createElement('hr')) + } + } + } + + private buildLabelTypeSection(labelType: LabelType): HTMLElement { + const section = document.createElement('div') + section.classList.add('label-section') + + const nameInput = document.createElement('input') + nameInput.classList.add('label-type-name') + const deleteButton = UiElementFactory.buildDeleteButton() + const labelTypeValueHolder = document.createElement('div') + labelTypeValueHolder.classList.add('label-type-values') + const addButton = UiElementFactory.buildAddButton('Value') + addButton.classList.add('label-type-value-add') + + nameInput.value = labelType.name + nameInput.placeholder = 'Label Type Name' + nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput) + setTimeout(() => dynamicallySetInputSize(nameInput), 0) + nameInput.onchange = () => { + this.labelTypeRegistry.updateLabelTypeName(labelType.id, nameInput.value) + } + + for (let i = 0; i < labelType.values.length; i++) { + labelTypeValueHolder.appendChild(this.buildLabelTypeValue(labelType, i)) + } + + addButton.onclick = () => { + this.labelTypeRegistry.registerLabelTypeValue(labelType.id, '') + } + + deleteButton.onclick = () => { + this.labelTypeRegistry.unregisterLabelType(labelType.id) + } + + section.appendChild(nameInput) + section.appendChild(deleteButton) + section.appendChild(labelTypeValueHolder) + section.appendChild(addButton) + + return section + } + + private buildLabelTypeValue(labelType: LabelType, valueIndex: number) { + const holder = document.createElement('div'); + holder.classList.add('label-type-value') + const nameInput = document.createElement('input'); + nameInput.classList.add('label-type-value-name') + const deleteButton = UiElementFactory.buildDeleteButton() + + const value = labelType.values[valueIndex] + + + nameInput.value = value.text + nameInput.placeholder = 'Value' + nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput) + setTimeout(() => dynamicallySetInputSize(nameInput), 0) + + nameInput.onchange = () => { + this.labelTypeRegistry.updateLabelTypeValueText(labelType.id, value.id, nameInput.value) + } + + deleteButton.onclick = () => { + this.labelTypeRegistry.unregisterLabelTypeValue(labelType.id, value.id) + } + + holder.appendChild(nameInput) + holder.appendChild(deleteButton) + return holder + } + + private onInputHandler(event: InputEvent, input: HTMLInputElement) { + if (!event.data?.match(/^[a-zA-Z0-9]*$/)) { + event.preventDefault() + } + dynamicallySetInputSize(input) + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/labels/LabelTypeRegistry.ts b/frontend/webEditor/src/labels/LabelTypeRegistry.ts new file mode 100644 index 00000000..66e95eed --- /dev/null +++ b/frontend/webEditor/src/labels/LabelTypeRegistry.ts @@ -0,0 +1,90 @@ +import { generateRandomSprottyId } from "../utils/idGenerator"; +import { LabelType, LabelTypeValue } from "./LabelType"; + +export class LabelTypeRegistry { + private labelTypes: LabelType[] = []; + private updateCallbacks: (() => void)[] = []; + + public registerLabelType(name: string): LabelType { + const labelType: LabelType = { + id: generateRandomSprottyId(), + name, + values: [] + } + this.labelTypes.push(labelType); + this.registerLabelTypeValue(labelType.id, 'Value') + this.labelTypeChanged() + return labelType + } + + public unregisterLabelType(id: string): void { + this.labelTypes = this.labelTypes.filter((type) => type.id !== id); + this.labelTypeChanged() + } + + public updateLabelTypeName(id: string, name: string): void { + const labelType = this.labelTypes.find(l => l.id === id) + if (!labelType) { + throw `No Label Type with id ${id} found` + } + labelType.name = name + this.labelTypeChanged() + } + + public registerLabelTypeValue(labelTypeId: string, text: string): LabelTypeValue { + const labelTypeValue: LabelTypeValue = { + id: generateRandomSprottyId(), + text + } + const labelType = this.labelTypes.find((type) => type.id === labelTypeId) + if (!labelType) { + throw `No Label Type with id ${labelTypeId} found` + } + labelType.values.push(labelTypeValue) + this.labelTypeChanged() + return labelTypeValue + } + + public unregisterLabelTypeValue(labelTypeId: string, labelTypeValueId: string): void { + const labelType = this.labelTypes.find((type) => type.id === labelTypeId) + if (!labelType) { + throw `No Label Type with id ${labelTypeId} found` + } + labelType.values = labelType.values.filter((value) => value.id !== labelTypeValueId) + this.labelTypeChanged() + } + + public updateLabelTypeValueText(labelTypeId: string, labelTypeValueId: string, text: string) { + const labelType = this.labelTypes.find((type) => type.id === labelTypeId) + if (!labelType) { + throw `No Label Type with id ${labelTypeId} found` + } + const value = labelType.values.find(l => l.id === labelTypeValueId) + if (!value) { + throw `Label Type ${labelType.name} has no value with id ${labelTypeValueId}` + } + value.text = text + this.labelTypeChanged() + } + + public clearLabelTypes(): void { + this.labelTypes = []; + this.updateCallbacks.forEach((cb) => cb()); + } + + public labelTypeChanged(): void { + this.updateCallbacks.forEach((cb) => cb()); + } + + public onUpdate(callback: () => void): void { + this.updateCallbacks.push(callback); + } + + public getLabelTypes(): LabelType[] { + return this.labelTypes; + } + + public getLabelType(id: string): LabelType | undefined { + return this.labelTypes.find((type) => type.id === id); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/labels/di.config.ts b/frontend/webEditor/src/labels/di.config.ts new file mode 100644 index 00000000..023375fc --- /dev/null +++ b/frontend/webEditor/src/labels/di.config.ts @@ -0,0 +1,13 @@ +import { ContainerModule } from "inversify"; +import { LabelTypeRegistry } from "./LabelTypeRegistry"; +import { LabelTypeEditorUi } from "./LabelTypeEditorUi"; +import { TYPES } from "sprotty"; +import { EDITOR_TYPES } from "../editorTypes"; + +export const labelModule = new ContainerModule((bind) => { + bind(LabelTypeRegistry).toSelf().inSingletonScope() + + bind(LabelTypeEditorUi).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(LabelTypeEditorUi); + bind(EDITOR_TYPES.DefaultUIElement).to(LabelTypeEditorUi); +}) \ No newline at end of file diff --git a/frontend/webEditor/src/labels/labelTypeEditorUi.css b/frontend/webEditor/src/labels/labelTypeEditorUi.css new file mode 100644 index 00000000..754617f8 --- /dev/null +++ b/frontend/webEditor/src/labels/labelTypeEditorUi.css @@ -0,0 +1,47 @@ +.label-type-editor-ui { + padding: 10px; + top: 150px; + right: 40px; + /* limit height so that max width has still 40px to bottom edge of the parent element + 100% is the full sprotty viewer height, 150px the space above the element, + 40px is the space that should be left under the editor and 2*10px is the padding */ + max-height: calc(100vh - 150px - 40px - 2 * 10px); + overflow: auto; + + * { + color: var(--color-foreground); + } + .codicon { + vertical-align: middle; + } + + hr { + height: 1px; + border: 0; + background-color: var(--color-foreground); + } + + input { + background-color: transparent; + outline: none; + border: none; + } + + .label-type-name { + font-size: 12pt; + } + + .label-type-values, .label-type-value-add { + margin-left: 10px; + } +} + +/* Label Type value */ +.label-type-value input { + background-color: var(--color-background); + text-align: center; + border: 1px solid var(--color-foreground); + border-radius: 15px; + padding: 3px; + margin: 4px; +} diff --git a/frontend/webEditor/src/utils/TextSize.ts b/frontend/webEditor/src/utils/TextSize.ts new file mode 100644 index 00000000..f12c0566 --- /dev/null +++ b/frontend/webEditor/src/utils/TextSize.ts @@ -0,0 +1,54 @@ +export function dynamicallySetInputSize(inputElement: HTMLInputElement): void { + const displayText = inputElement.value || inputElement.placeholder; + const { width } = calculateTextSize(displayText, window.getComputedStyle(inputElement).font); + + // Values have higher padding for the rounded border + const widthPadding = inputElement.classList.contains("label-type-name") ? 2 : 8; + const finalWidth = width + widthPadding; + + inputElement.style.width = finalWidth + "px"; +} + +interface TextSize { + width: number; + height: number; +} + +const contextMap = new Map }>(); + +export function calculateTextSize(text: string | undefined, font: string = "11pt sans-serif"): TextSize { + if (!text || text.length === 0) { + return { width: 20, height: 20 }; + } + + // Get context for the given font or create a new one if it does not exist yet + let contextObj = contextMap.get(font); + if (!contextObj) { + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + if (!context) { + throw new Error("Could not create canvas context used to measure text width"); + } + + context.font = font; // This is slow. Thats why we have one instance per font to avoid redoing this + contextObj = { context, cache: new Map() }; + contextMap.set(font, contextObj); + } + + const { context, cache } = contextObj; + + // Get text width from cache or compute it + const cachedMetrics = cache.get(text); + if (cachedMetrics) { + return cachedMetrics; + } else { + const metrics = context.measureText(text); + const textSize: TextSize = { + width: Math.ceil(metrics.width), + height: Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent), + }; + + cache.set(text, textSize); + return textSize; + } +} diff --git a/frontend/webEditor/src/utils/UiElementFactory.ts b/frontend/webEditor/src/utils/UiElementFactory.ts new file mode 100644 index 00000000..3e4741b8 --- /dev/null +++ b/frontend/webEditor/src/utils/UiElementFactory.ts @@ -0,0 +1,28 @@ +import './baseUiElements.css' + +export class UiElementFactory { + + private constructor() {} + + public static buildDeleteButton() { + const button = document.createElement('button'); + button.classList.add('delete-button') + const symbol = document.createElement('span'); + symbol.classList.add('codicon', 'codicon-trash') + button.appendChild(symbol) + return button + } + + public static buildAddButton(text: string) { + const button = document.createElement('button'); + button.classList.add('add-button') + const symbol = document.createElement('span'); + symbol.classList.add('codicon', 'codicon-add') + button.appendChild(symbol) + const textHolder = document.createElement('span'); + textHolder.innerText = text + button.appendChild(textHolder) + return button + } +} + diff --git a/frontend/webEditor/src/utils/baseUiElements.css b/frontend/webEditor/src/utils/baseUiElements.css new file mode 100644 index 00000000..8d1c7e73 --- /dev/null +++ b/frontend/webEditor/src/utils/baseUiElements.css @@ -0,0 +1,6 @@ +button.delete-button, button.add-button { + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; +} \ No newline at end of file diff --git a/frontend/webEditor/src/utils/idGenerator.ts b/frontend/webEditor/src/utils/idGenerator.ts new file mode 100644 index 00000000..4c89dedd --- /dev/null +++ b/frontend/webEditor/src/utils/idGenerator.ts @@ -0,0 +1,3 @@ +export function generateRandomSprottyId(): string { + return Math.random().toString(36).substring(7); +} \ No newline at end of file From b717154d5626989d58e61f33abede7b3044edae1 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 31 Oct 2025 11:24:15 +0100 Subject: [PATCH 04/41] add some basic types --- frontend/webEditor/dependencyGraph.md | 8 +++- .../webEditor/src/constraint/Constraint.ts | 4 ++ .../webEditor/src/editorMode/EditorMode.ts | 1 + .../src/editorMode/EditorModeController.ts | 39 +++++++++++++++++++ .../webEditor/src/serialize/SavedDiagram.ts | 13 +++++++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 frontend/webEditor/src/constraint/Constraint.ts create mode 100644 frontend/webEditor/src/editorMode/EditorMode.ts create mode 100644 frontend/webEditor/src/editorMode/EditorModeController.ts create mode 100644 frontend/webEditor/src/serialize/SavedDiagram.ts diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md index eadb6dd1..34b6a1f8 100644 --- a/frontend/webEditor/dependencyGraph.md +++ b/frontend/webEditor/dependencyGraph.md @@ -11,6 +11,12 @@ stateDiagram-v2 labels --> editorTypes labels --> accordionUiExtension + serialize --> labels + serialize --> constraint + serialize --> editorMode + classDef diLess font-style:italic,stroke:#0fa class accordionUiExtension,editorTypes,utils diLess -``` \ No newline at end of file +``` + +green packages do not export a module \ No newline at end of file diff --git a/frontend/webEditor/src/constraint/Constraint.ts b/frontend/webEditor/src/constraint/Constraint.ts new file mode 100644 index 00000000..4b1e27e5 --- /dev/null +++ b/frontend/webEditor/src/constraint/Constraint.ts @@ -0,0 +1,4 @@ +export interface Constraint { + name: string; + constraint: string; +} \ No newline at end of file diff --git a/frontend/webEditor/src/editorMode/EditorMode.ts b/frontend/webEditor/src/editorMode/EditorMode.ts new file mode 100644 index 00000000..73c47884 --- /dev/null +++ b/frontend/webEditor/src/editorMode/EditorMode.ts @@ -0,0 +1 @@ +export type EditorMode = "edit" | "view"; \ No newline at end of file diff --git a/frontend/webEditor/src/editorMode/EditorModeController.ts b/frontend/webEditor/src/editorMode/EditorModeController.ts new file mode 100644 index 00000000..b25b8879 --- /dev/null +++ b/frontend/webEditor/src/editorMode/EditorModeController.ts @@ -0,0 +1,39 @@ +import { injectable } from "inversify"; +import { EditorMode } from "./EditorMode"; + +/** + * Holds the current editor mode in a central place. + * Used to get the current mode in places where it is used. + * + * Changes to the mode should be done using the ChangeEditorModeCommand + * and not directly on this class when done interactively + * for undo/redo support and actions that are done to the model + * when the mode changes. + */ +@injectable() +export class EditorModeController { + private mode: EditorMode = "edit"; + private modeChangeCallbacks: ((mode: EditorMode) => void)[] = []; + + getCurrentMode(): EditorMode { + return this.mode; + } + + setMode(mode: EditorMode) { + this.mode = mode; + + this.modeChangeCallbacks.forEach((callback) => callback(mode)); + } + + setDefaultMode() { + this.mode = "edit"; + } + + onModeChange(callback: (mode: EditorMode) => void) { + this.modeChangeCallbacks.push(callback); + } + + isReadOnly(): boolean { + return this.mode !== "edit"; + } +} diff --git a/frontend/webEditor/src/serialize/SavedDiagram.ts b/frontend/webEditor/src/serialize/SavedDiagram.ts new file mode 100644 index 00000000..2257b805 --- /dev/null +++ b/frontend/webEditor/src/serialize/SavedDiagram.ts @@ -0,0 +1,13 @@ +import { SModelRoot } from "sprotty-protocol"; +import { Constraint } from "../constraint/Constraint"; +import { EditorMode } from "../editorMode/EditorMode"; +import { LabelType } from "../labels/LabelType"; + +export interface SavedDiagram { + model: SModelRoot; + labelTypes?: LabelType[]; + constraints?: Constraint[]; + mode?: EditorMode; + version: number; +} +export const CURRENT_VERSION = 1; \ No newline at end of file From 6b28c7d35a481a78cdbccb1305c9b4993172f7cb Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 31 Oct 2025 13:40:09 +0100 Subject: [PATCH 05/41] Diagram loading from json --- frontend/webEditor/dependencyGraph.md | 3 + frontend/webEditor/src/index.ts | 4 +- .../webEditor/src/labels/LabelTypeRegistry.ts | 5 + .../src/serialize/defaultDiagram.json | 623 ++++++++++++++++++ frontend/webEditor/src/serialize/di.config.ts | 8 + .../src/serialize/loadDefaultDiagram.ts | 9 + frontend/webEditor/src/serialize/loadJson.ts | 180 +++++ .../src/startUpAgent/LoadDefaultDiagram.ts | 13 + .../webEditor/src/startUpAgent/SprottyInit.ts | 2 +- .../webEditor/src/startUpAgent/di.config.ts | 5 +- 10 files changed, 849 insertions(+), 3 deletions(-) create mode 100644 frontend/webEditor/src/serialize/defaultDiagram.json create mode 100644 frontend/webEditor/src/serialize/di.config.ts create mode 100644 frontend/webEditor/src/serialize/loadDefaultDiagram.ts create mode 100644 frontend/webEditor/src/serialize/loadJson.ts create mode 100644 frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md index 34b6a1f8..8364955f 100644 --- a/frontend/webEditor/dependencyGraph.md +++ b/frontend/webEditor/dependencyGraph.md @@ -14,6 +14,9 @@ stateDiagram-v2 serialize --> labels serialize --> constraint serialize --> editorMode + [*] --> serialize + + serialize --> commonModule: logger classDef diLess font-style:italic,stroke:#0fa class accordionUiExtension,editorTypes,utils diLess diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 9f071944..51839031 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -11,6 +11,7 @@ import { IStartUpAgent, StartUpAgent } from "./startUpAgent/StartUpAgent"; import { startUpAgentModule } from "./startUpAgent/di.config"; import { commonModule } from "./commonModule"; import { labelModule } from "./labels/di.config"; +import { serializeModule } from "./serialize/di.config"; const container = new Container(); @@ -25,7 +26,8 @@ container.load( helpUiModule, commonModule, startUpAgentModule, - labelModule + labelModule, + serializeModule ) const startUpAgents = container.getAll(StartUpAgent) diff --git a/frontend/webEditor/src/labels/LabelTypeRegistry.ts b/frontend/webEditor/src/labels/LabelTypeRegistry.ts index 66e95eed..ddd3ea4d 100644 --- a/frontend/webEditor/src/labels/LabelTypeRegistry.ts +++ b/frontend/webEditor/src/labels/LabelTypeRegistry.ts @@ -31,6 +31,11 @@ export class LabelTypeRegistry { this.labelTypeChanged() } + public setLabelTypes(labelTypes: LabelType[]) { + this.labelTypes = labelTypes; + this.labelTypeChanged() + } + public registerLabelTypeValue(labelTypeId: string, text: string): LabelTypeValue { const labelTypeValue: LabelTypeValue = { id: generateRandomSprottyId(), diff --git a/frontend/webEditor/src/serialize/defaultDiagram.json b/frontend/webEditor/src/serialize/defaultDiagram.json new file mode 100644 index 00000000..cf59979a --- /dev/null +++ b/frontend/webEditor/src/serialize/defaultDiagram.json @@ -0,0 +1,623 @@ +{ + "model": { + "canvasBounds": { + "x": 0, + "y": 0, + "width": 1278, + "height": 1324 + }, + "scroll": { + "x": 181.68489464915504, + "y": -12.838536201820945 + }, + "zoom": 6.057478948161569, + "position": { + "x": 0, + "y": 0 + }, + "size": { + "width": -1, + "height": -1 + }, + "features": {}, + "type": "graph", + "id": "root", + "children": [ + { + "position": { + "x": 84, + "y": 54 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "text": "User", + "labels": [ + { + "labelTypeId": "gvia09", + "labelTypeValueId": "g10hr" + } + ], + "ports": [ + { + "position": { + "x": 58.5, + "y": 7 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "nhcrad", + "type": "port:dfd-input", + "children": [] + }, + { + "position": { + "x": 31, + "y": 38.5 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "behavior": "set Sensitivity.Personal", + "features": {}, + "id": "4wbyft", + "type": "port:dfd-output", + "children": [] + }, + { + "position": { + "x": 58.5, + "y": 25.5 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "behavior": "set Sensitivity.Public", + "features": {}, + "id": "wksxi8", + "type": "port:dfd-output", + "children": [] + } + ], + "features": {}, + "id": "7oii5l", + "type": "node:input-output", + "children": [] + }, + { + "position": { + "x": 249, + "y": 67 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "text": "view", + "labels": [], + "ports": [ + { + "position": { + "x": -3.5, + "y": 13 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "ti4ri7", + "type": "port:dfd-input", + "children": [] + }, + { + "position": { + "x": 58.5, + "y": 13 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "behavior": "forward request", + "features": {}, + "id": "bsqjm", + "type": "port:dfd-output", + "children": [] + } + ], + "features": {}, + "id": "0bh7yh", + "type": "node:function", + "children": [] + }, + { + "position": { + "x": 249, + "y": 22 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "text": "display", + "labels": [], + "ports": [ + { + "position": { + "x": 58.5, + "y": 15 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "0hfzu", + "type": "port:dfd-input", + "children": [] + }, + { + "position": { + "x": -3.5, + "y": 9 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "behavior": "forward items", + "features": {}, + "id": "y1p7qq", + "type": "port:dfd-output", + "children": [] + } + ], + "features": {}, + "id": "4myuyr", + "type": "node:function", + "children": [] + }, + { + "position": { + "x": 364, + "y": 152 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "text": "encrypt", + "labels": [], + "ports": [ + { + "position": { + "x": -3.5, + "y": 15.5 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "kqjy4g", + "type": "port:dfd-input", + "children": [] + }, + { + "position": { + "x": 29, + "y": -3.5 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "behavior": "forward data\nset Encryption.Encrypted", + "features": {}, + "id": "3wntc", + "type": "port:dfd-output", + "children": [] + } + ], + "features": {}, + "id": "3n988k", + "type": "node:function", + "children": [] + }, + { + "position": { + "x": 104, + "y": 157 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "text": "buy", + "labels": [], + "ports": [ + { + "position": { + "x": 19, + "y": -3.5 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "2331e8", + "type": "port:dfd-input", + "children": [] + }, + { + "position": { + "x": 58.5, + "y": 10.5 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "behavior": "forward data", + "features": {}, + "id": "vnkg73", + "type": "port:dfd-output", + "children": [] + } + ], + "features": {}, + "id": "z9v1jp", + "type": "node:function", + "children": [] + }, + { + "position": { + "x": 233.5, + "y": 157 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "text": "process", + "labels": [], + "ports": [ + { + "position": { + "x": -3.5, + "y": 10.5 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "xyepdb", + "type": "port:dfd-input", + "children": [] + }, + { + "position": { + "x": 59.5, + "y": 10.5 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "behavior": "forward data", + "features": {}, + "id": "eedb56", + "type": "port:dfd-output", + "children": [] + } + ], + "features": {}, + "id": "js61f", + "type": "node:function", + "children": [] + }, + { + "position": { + "x": 422.5, + "y": 59 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "text": "Database", + "labels": [ + { + "labelTypeId": "gvia09", + "labelTypeValueId": "5hnugm" + } + ], + "ports": [ + { + "position": { + "x": -3.5, + "y": 23 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "scljwi", + "type": "port:dfd-input", + "children": [] + }, + { + "position": { + "x": -3.5, + "y": 0.5 + }, + "size": { + "width": -1, + "height": -1 + }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "behavior": "set Sensitivity.Public", + "features": {}, + "id": "1j7bn5", + "type": "port:dfd-output", + "children": [] + } + ], + "features": {}, + "id": "8j2r1g", + "type": "node:storage", + "children": [] + }, + { + "routingPoints": [], + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "vq8g3l", + "type": "edge:arrow", + "sourceId": "4wbyft", + "targetId": "2331e8", + "text": "data", + "children": [] + }, + { + "routingPoints": [], + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "xrzc19", + "type": "edge:arrow", + "sourceId": "vnkg73", + "targetId": "xyepdb", + "text": "data", + "children": [] + }, + { + "routingPoints": [], + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "ufflto", + "type": "edge:arrow", + "sourceId": "eedb56", + "targetId": "kqjy4g", + "text": "data", + "children": [] + }, + { + "routingPoints": [], + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "ojjvtp", + "type": "edge:arrow", + "sourceId": "3wntc", + "targetId": "scljwi", + "text": "data", + "children": [] + }, + { + "routingPoints": [], + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "c9n88l", + "type": "edge:arrow", + "sourceId": "bsqjm", + "targetId": "scljwi", + "text": "request", + "children": [] + }, + { + "routingPoints": [], + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "uflsc", + "type": "edge:arrow", + "sourceId": "wksxi8", + "targetId": "ti4ri7", + "text": "request", + "routerKind": "polyline", + "children": [] + }, + { + "routingPoints": [], + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "n81f3b", + "type": "edge:arrow", + "sourceId": "1j7bn5", + "targetId": "0hfzu", + "text": "items", + "children": [] + }, + { + "routingPoints": [], + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "features": {}, + "id": "hi397b", + "type": "edge:arrow", + "sourceId": "y1p7qq", + "targetId": "nhcrad", + "text": "items", + "children": [] + } + ] + }, + "labelTypes": [ + { + "id": "4h3wzk", + "name": "Sensitivity", + "values": [ + { + "id": "zzvphn", + "text": "Personal" + }, + { + "id": "veaan9", + "text": "Public" + } + ] + }, + { + "id": "gvia09", + "name": "Location", + "values": [ + { + "id": "g10hr", + "text": "EU" + }, + { + "id": "5hnugm", + "text": "nonEU" + } + ] + }, + { + "id": "84rllz", + "name": "Encryption", + "values": [ + { + "id": "2r6xe6", + "text": "Encrypted" + } + ] + } + ], + "constraints": [ + { + "name": "Test", + "constraint": "data Sensitivity.Personal neverFlows vertex Location.nonEU" + } + ], + "mode": "edit", + "version": 1 +} diff --git a/frontend/webEditor/src/serialize/di.config.ts b/frontend/webEditor/src/serialize/di.config.ts new file mode 100644 index 00000000..36fdeef6 --- /dev/null +++ b/frontend/webEditor/src/serialize/di.config.ts @@ -0,0 +1,8 @@ +import { ContainerModule } from "inversify"; +import { configureCommand } from "sprotty"; +import { LoadJsonCommand } from "./loadJson"; + +export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + configureCommand(context, LoadJsonCommand); +}) \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts new file mode 100644 index 00000000..922c1e20 --- /dev/null +++ b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts @@ -0,0 +1,9 @@ +import { LoadJsonAction } from "./loadJson"; +import defaultDiagram from './defaultDiagram.json' +import { SavedDiagram } from "./SavedDiagram"; + +export namespace LoadDefaultDiagramAction { + export function create() { + return LoadJsonAction.create(defaultDiagram as SavedDiagram) + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/loadJson.ts b/frontend/webEditor/src/serialize/loadJson.ts new file mode 100644 index 00000000..e61ff1d5 --- /dev/null +++ b/frontend/webEditor/src/serialize/loadJson.ts @@ -0,0 +1,180 @@ +import { Command, CommandExecutionContext, CommandReturn, EMPTY_ROOT, ILogger, SModelRootImpl, TYPES } from "sprotty"; +import { SavedDiagram } from "./SavedDiagram"; +import { Action, SModelElement, SModelRoot } from "sprotty-protocol"; +import { inject } from "inversify"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { EditorModeController } from "../editorMode/EditorModeController"; +import { Constraint } from "../constraint/Constraint"; +import { EditorMode } from "../editorMode/EditorMode"; +import { LabelType } from "../labels/LabelType"; + +interface LoadJsonAction extends Action { + kind: typeof LoadJsonAction.KIND; + json: SavedDiagram; + name: string; +} + +export namespace LoadJsonAction { + export const KIND = "load-json"; + + export function create(json: SavedDiagram, name?: string): LoadJsonAction { + return { + kind: KIND, + json, + name: name ?? "diagram.json", + }; + } +} + +export class LoadJsonCommand extends Command { + static readonly KIND = LoadJsonAction.KIND; + + /* After loading a diagram, this command dispatches other actions like fit to screen and optional auto layouting. However when returning a new model in the execute method, the diagram is not directly updated. We need to wait for the InitializeCanvasBoundsCommand to be fired and finish before we can do things like fit to screen. + Because of that we block the execution newly dispatched actions including the actions we dispatched after loading the diagram until the InitializeCanvasBoundsCommand has been processed. + This works because the canvasBounds property is always removed loading a diagram, requiring the InitializeCanvasBoundsCommand to be fired. */ + readonly blockUntil = LoadJsonCommand.loadBlockUntilFn; + static readonly loadBlockUntilFn = (action: Action) => { + return action.kind === "initializeCanvasBounds"; + }; + + private oldRoot: SModelRootImpl | undefined; + private newRoot: SModelRootImpl | undefined; + private oldLabelTypes: LabelType[] | undefined; + private oldEditorMode: EditorMode | undefined; + private oldFileName: string | undefined; + private oldConstrains: Constraint[] | undefined; + + constructor( + @inject(TYPES.Action) private readonly action: LoadJsonAction, + @inject(TYPES.ILogger) private readonly logger: ILogger, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + @inject(EditorModeController) private editorModeController: EditorModeController, + ) { + super(); + } + + execute(context: CommandExecutionContext): CommandReturn { + this.oldRoot = context.root; + + try { + const newSchema = LoadJsonCommand.preprocessModelSchema(this.action.json.model); + this.newRoot = context.modelFactory.createRoot(newSchema); + + this.logger.info(this, "Model loaded successfully"); + + this.oldLabelTypes = this.labelTypeRegistry.getLabelTypes(); + const newLabelTypes = this.action.json.labelTypes; + this.labelTypeRegistry.clearLabelTypes(); + if (newLabelTypes) { + this.labelTypeRegistry.setLabelTypes(newLabelTypes); + this.logger.info(this, "Label types loaded successfully"); + } else { + this.labelTypeRegistry.clearLabelTypes(); + } + + this.oldEditorMode = this.editorModeController.getCurrentMode(); + const newEditorMode = this.action.json.mode; + if (newEditorMode) { + this.editorModeController.setMode(newEditorMode); + } else { + this.editorModeController.setDefaultMode(); + } + this.logger.info(this, "Editor mode loaded successfully"); + + // TODO: load constraints + + // TODO: post load actions like layout + + // TODO: load file name + //this.oldFileName = currentFileName; + + return this.newRoot; + } catch (error) { + this.logger.error(this, "Error loading model", error); + this.newRoot = this.oldRoot; + return this.oldRoot; + } + } + + undo(context: CommandExecutionContext): CommandReturn { + if (this.oldLabelTypes) { + this.labelTypeRegistry.setLabelTypes(this.oldLabelTypes); + } else { + this.labelTypeRegistry.clearLabelTypes(); + } + + if (this.oldEditorMode) { + this.editorModeController.setMode(this.oldEditorMode); + } else { + this.editorModeController.setDefaultMode(); + } + + if (this.oldEditorMode) { + this.editorModeController?.setMode(this.oldEditorMode); + } + + // TODO: load constraints + + // TODO: load file name + + return this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); + } + + redo(context: CommandExecutionContext): CommandReturn { + const newLabelTypes = this.action.json.labelTypes; + this.labelTypeRegistry.clearLabelTypes(); + if (newLabelTypes) { + this.labelTypeRegistry.setLabelTypes(newLabelTypes); + this.logger.info(this, "Label types loaded successfully"); + } else { + this.labelTypeRegistry.clearLabelTypes(); + } + + const newEditorMode = this.action.json.mode; + if (newEditorMode) { + this.editorModeController.setMode(newEditorMode); + } else { + this.editorModeController.setDefaultMode(); + } + this.logger.info(this, "Editor mode loaded successfully"); + + // TODO: load constraints + + // TODO: load file name + //this.oldFileName = currentFileName; + + return this.newRoot ?? this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); + } + + /** + * Before a saved model schema can be loaded, it needs to be preprocessed. + * Currently this means that the features property is removed from all model elements recursively. + * Additionally the canvasBounds property is removed from the root element, because it may change + * depending on browser window. + * In the future this method may be extended to preprocess other properties. + * + * The feature property at runtime is a js Set with the relevant features. + * E.g. for the top graph this is the viewportFeature among others. + * When converting js Sets objects into json, the result is an empty js object. + * When loading the object is converted into an empty js Set and the features are lost. + * Because of this the editor won't work properly after loading a model. + * To prevent this, the features property is removed before loading the model. + * When the features property is missing it gets rebuilt on loading with the currently used features. + * + * @param modelSchema The model schema to preprocess + */ + private static preprocessModelSchema(modelSchema: SModelRoot): SModelRoot { + // These properties are all not included in the root typing and if present are not loaded and handled correctly. So they are removed. + if ("features" in modelSchema) { + delete modelSchema["features"]; + } + if ("canvasBounds" in modelSchema) { + delete modelSchema["canvasBounds"]; + } + + if (modelSchema.children) { + modelSchema.children.forEach((child: SModelElement) => this.preprocessModelSchema(child)); + } + return modelSchema; + } +} diff --git a/frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts b/frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts new file mode 100644 index 00000000..a226cdd0 --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts @@ -0,0 +1,13 @@ +import { ActionDispatcher, TYPES } from "sprotty"; +import { IStartUpAgent } from "./StartUpAgent"; +import { LoadDefaultDiagramAction } from "../serialize/loadDefaultDiagram"; +import { inject } from "inversify"; + +export class LoadDefaultDiagramStartUpAgent implements IStartUpAgent { + constructor(@inject(TYPES.IActionDispatcher) private actionDispatcher: ActionDispatcher) {} + + run(): void { + this.actionDispatcher.dispatch(LoadDefaultDiagramAction.create()) + } + +} \ No newline at end of file diff --git a/frontend/webEditor/src/startUpAgent/SprottyInit.ts b/frontend/webEditor/src/startUpAgent/SprottyInit.ts index fc264fb5..4d426c31 100644 --- a/frontend/webEditor/src/startUpAgent/SprottyInit.ts +++ b/frontend/webEditor/src/startUpAgent/SprottyInit.ts @@ -2,7 +2,7 @@ import { inject } from "inversify"; import { IStartUpAgent } from "./StartUpAgent"; import { LocalModelSource, TYPES } from "sprotty"; -export class SprottyInitializerStartUpAgents implements IStartUpAgent { +export class SprottyInitializerStartUpAgent implements IStartUpAgent { constructor(@inject(TYPES.ModelSource) private modelSource: LocalModelSource) {} run() { diff --git a/frontend/webEditor/src/startUpAgent/di.config.ts b/frontend/webEditor/src/startUpAgent/di.config.ts index bb158091..8f400039 100644 --- a/frontend/webEditor/src/startUpAgent/di.config.ts +++ b/frontend/webEditor/src/startUpAgent/di.config.ts @@ -1,8 +1,11 @@ import { ContainerModule } from "inversify"; import { StartUpAgent } from "./StartUpAgent"; import { LoadDefaultUiExtensionsStartUpAgent } from "./LoadDefaultUiExtensions"; +import { LoadDefaultDiagramStartUpAgent } from "./LoadDefaultDiagram"; +import { SprottyInitializerStartUpAgent } from "./SprottyInit"; export const startUpAgentModule = new ContainerModule((bind) => { bind(StartUpAgent).to(LoadDefaultUiExtensionsStartUpAgent) - + bind(StartUpAgent).to(SprottyInitializerStartUpAgent) + bind(StartUpAgent).to(LoadDefaultDiagramStartUpAgent) }) \ No newline at end of file From cd6c0068b4d6334a79cc078e6609d04b0b9a9b08 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 31 Oct 2025 15:06:44 +0100 Subject: [PATCH 06/41] add editor mode module --- frontend/webEditor/src/editorMode/di.config.ts | 6 ++++++ frontend/webEditor/src/index.ts | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 frontend/webEditor/src/editorMode/di.config.ts diff --git a/frontend/webEditor/src/editorMode/di.config.ts b/frontend/webEditor/src/editorMode/di.config.ts new file mode 100644 index 00000000..8e8c067a --- /dev/null +++ b/frontend/webEditor/src/editorMode/di.config.ts @@ -0,0 +1,6 @@ +import { ContainerModule } from "inversify"; +import { EditorModeController } from "./EditorModeController"; + +export const editorModeModule = new ContainerModule((bind) => { + bind(EditorModeController).toSelf().inSingletonScope(); +}) \ No newline at end of file diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 51839031..dbb4d534 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -12,6 +12,8 @@ import { startUpAgentModule } from "./startUpAgent/di.config"; import { commonModule } from "./commonModule"; import { labelModule } from "./labels/di.config"; import { serializeModule } from "./serialize/di.config"; +import { editorModeModule } from "./editorMode/di.config"; +import { diagramModule } from "./diagram/di.config"; const container = new Container(); @@ -27,6 +29,8 @@ container.load( commonModule, startUpAgentModule, labelModule, + editorModeModule, + diagramModule, serializeModule ) From 7c39483fdbc0421db36f82030d77199c68cad6e8 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Sat, 1 Nov 2025 16:53:21 +0100 Subject: [PATCH 07/41] rendering --- frontend/webEditor/dependencyGraph.md | 6 +- .../src/annotation/DFDNodeAnnotation.ts | 6 + .../webEditor/src/diagram/ModelFactory.ts | 31 ++++ frontend/webEditor/src/diagram/di.config.ts | 35 ++++- .../webEditor/src/diagram/edges/ArrowEdge.tsx | 117 +++++++++++++++ .../src/diagram/labels/DfdPositionalLabel.tsx | 35 +++++ .../src/diagram/nodes/DfdFunctionNode.tsx | 53 +++++++ .../webEditor/src/diagram/nodes/DfdIONode.tsx | 48 +++++++ .../src/diagram/nodes/DfdNodeLabels.tsx | 135 ++++++++++++++++++ .../src/diagram/nodes/DfdStorageNode.tsx | 54 +++++++ .../webEditor/src/diagram/nodes/common.ts | 134 +++++++++++++++++ .../src/diagram/ports/DfdInputPort.tsx | 63 ++++++++ .../src/diagram/ports/DfdOutputPort.tsx | 88 ++++++++++++ .../webEditor/src/diagram/ports/common.ts | 18 +++ frontend/webEditor/src/diagram/style.css | 115 +++++++++++++++ .../webEditor/src/startUpAgent/di.config.ts | 2 +- 16 files changed, 937 insertions(+), 3 deletions(-) create mode 100644 frontend/webEditor/src/annotation/DFDNodeAnnotation.ts create mode 100644 frontend/webEditor/src/diagram/ModelFactory.ts create mode 100644 frontend/webEditor/src/diagram/edges/ArrowEdge.tsx create mode 100644 frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx create mode 100644 frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx create mode 100644 frontend/webEditor/src/diagram/nodes/DfdIONode.tsx create mode 100644 frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx create mode 100644 frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx create mode 100644 frontend/webEditor/src/diagram/nodes/common.ts create mode 100644 frontend/webEditor/src/diagram/ports/DfdInputPort.tsx create mode 100644 frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx create mode 100644 frontend/webEditor/src/diagram/ports/common.ts create mode 100644 frontend/webEditor/src/diagram/style.css diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md index 8364955f..3e13d6df 100644 --- a/frontend/webEditor/dependencyGraph.md +++ b/frontend/webEditor/dependencyGraph.md @@ -15,9 +15,13 @@ stateDiagram-v2 serialize --> constraint serialize --> editorMode [*] --> serialize - serialize --> commonModule: logger + [*] --> editorMode + serialize --> editorMode + + [*] --> diagram + classDef diLess font-style:italic,stroke:#0fa class accordionUiExtension,editorTypes,utils diLess ``` diff --git a/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts b/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts new file mode 100644 index 00000000..eda7cb48 --- /dev/null +++ b/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts @@ -0,0 +1,6 @@ +export interface DfdNodeAnnotation { + message: string; + color?: string; + icon?: string; + tfg?: number; +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/ModelFactory.ts b/frontend/webEditor/src/diagram/ModelFactory.ts new file mode 100644 index 00000000..a36c8fd4 --- /dev/null +++ b/frontend/webEditor/src/diagram/ModelFactory.ts @@ -0,0 +1,31 @@ +import { injectable } from "inversify"; +import { SChildElementImpl, SModelElementImpl, SModelFactory, SParentElementImpl } from "sprotty"; +import { DfdNode } from "./nodes/common"; +import { SLabel, SModelElement } from "sprotty-protocol"; + +@injectable() +export class CustomModelFactory extends SModelFactory { + override createElement(schema: SModelElement | SModelElementImpl, parent?: SParentElementImpl): SChildElementImpl { + if ( + (schema.type === "node:storage" || + schema.type === "node:function" || + schema.type === "node:input-output") && + !(schema instanceof SModelElementImpl) + ) { + const dfdSchema = schema as DfdNode; + schema.children = schema.children ?? []; + for (const port of dfdSchema.ports) { + if ("features" in port) { + delete port.features + } + } + schema.children.push(...dfdSchema.ports, { + type: "label:positional", + text: dfdSchema.text ?? "", + id: schema.id + "-label", + } as SLabel); + } + + return super.createElement(schema, parent); + } +} diff --git a/frontend/webEditor/src/diagram/di.config.ts b/frontend/webEditor/src/diagram/di.config.ts index 49337334..636929af 100644 --- a/frontend/webEditor/src/diagram/di.config.ts +++ b/frontend/webEditor/src/diagram/di.config.ts @@ -1,8 +1,41 @@ import { ContainerModule } from "inversify"; -import { configureModelElement, SGraphImpl, SGraphView } from "sprotty"; +import { configureModelElement, editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SRoutingHandleImpl, TYPES, withEditLabelFeature } from "sprotty"; +import { ArrowEdgeImpl, ArrowEdgeView, CustomRoutingHandleView } from "./edges/ArrowEdge"; +import { DfdInputPortImpl, DfdInputPortView } from "./ports/DfdInputPort"; +import { DfdOutputPortImpl, DfdOutputPortView } from "./ports/DfdOutputPort"; +import { StorageNodeImpl, StorageNodeView } from "./nodes/DfdStorageNode"; +import { FunctionNodeImpl, FunctionNodeView } from "./nodes/DfdFunctionNode"; +import { IONodeImpl, IONodeView } from "./nodes/DfdIONode"; +import './style.css' +import { DfdPositionalLabelView } from "./labels/DfdPositionalLabel"; +import { CustomModelFactory } from "./ModelFactory"; +import { DfdNodeLabelRenderer } from "./nodes/DfdNodeLabels"; export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; configureModelElement(context, "graph", SGraphImpl, SGraphView); + + configureModelElement(context, "node:storage", StorageNodeImpl, StorageNodeView); + configureModelElement(context, "node:function", FunctionNodeImpl, FunctionNodeView); + configureModelElement(context, "node:input-output", IONodeImpl, IONodeView); + + configureModelElement(context, "edge:arrow", ArrowEdgeImpl, ArrowEdgeView, { + enable: [withEditLabelFeature], + }); + configureModelElement(context, "routing-point", SRoutingHandleImpl, CustomRoutingHandleView); + configureModelElement(context, "volatile-routing-point", SRoutingHandleImpl, CustomRoutingHandleView); + + configureModelElement(context, "port:dfd-input", DfdInputPortImpl, DfdInputPortView); + configureModelElement(context, "port:dfd-output", DfdOutputPortImpl, DfdOutputPortView); + + configureModelElement(context, "label", SLabelImpl, SLabelView, { + enable: [editLabelFeature], + }); + configureModelElement(context, "label:positional", SLabelImpl, DfdPositionalLabelView, { + enable: [editLabelFeature], + }); + + rebind(TYPES.IModelFactory).to(CustomModelFactory); + bind(DfdNodeLabelRenderer).toSelf().inSingletonScope() }); \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx b/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx new file mode 100644 index 00000000..5e0aea1e --- /dev/null +++ b/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx @@ -0,0 +1,117 @@ +/** @jsx svg */ +import { injectable } from "inversify"; +import { + PolylineEdgeViewWithGapsOnIntersections, + SEdgeImpl, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + svg, + RenderingContext, + IViewArgs, + WithEditableLabel, + isEditableLabel, + SRoutingHandleView, +} from "sprotty"; +import { VNode } from "snabbdom"; +import { Point, angleOfPoint, toDegrees, SEdge } from "sprotty-protocol"; + +export interface ArrowEdge extends SEdge { + text?: string; +} + +export class ArrowEdgeImpl extends SEdgeImpl implements WithEditableLabel { + text?: string; + + get editableLabel() { + const label = this.children.find((element) => element.type.startsWith("label")); + if (label && isEditableLabel(label)) { + return label; + } + + return undefined; + } +} + +@injectable() +export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { + + override render(edge: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { + // In the default implementation children of the edge are always rendered, because they + // may be visible when the rest of the edge is not. + // We only have the edge label as an children which only must be rendered when the rest of the edge is visible. + // So as an optimization for big diagrams we don't render the label when the rest of the edge is not visible either. + // Otherwise all these labels would be added to the DOM, making it slow.. + const route = this.edgeRouterRegistry.route(edge, args); + if (!this.isVisible(edge, route, context)) { + return undefined; + } + + return super.render(edge, context, args); + } + + + /** + * Renders an arrow at the end of the edge. + */ + protected override renderAdditionals(edge: SEdgeImpl, segments: Point[], context: RenderingContext): VNode[] { + const additionals = super.renderAdditionals(edge, segments, context); + const p1 = segments[segments.length - 2]; + const p2 = segments[segments.length - 1]; + const arrow = ( + + ); + additionals.push(arrow); + return additionals; + } + + /** + * Renders the edge line. + * In contrast to the default implementation that we override here, + * this implementation makes the edge line 10px shorter at the end to make space for the arrow without any overlap. + */ + protected renderLine(edge: SEdgeImpl, segments: Point[]): VNode { + const firstPoint = segments[0]; + let path = `M ${firstPoint.x},${firstPoint.y}`; + for (let i = 1; i < segments.length; i++) { + const p = segments[i]; + if (i === segments.length - 1) { + // Make edge line 9.5px shorter to make space for the arrow + // The arrow is 10px long, but we only shorten by 9.5 px to have overlap at the edge between line and arrow. + // Otherwise edges would be exactly next to each other which would result in a small gap and flickering if you zoom in enough. + const prevP = segments[i - 1]; + const dx = p.x - prevP.x; + const dy = p.y - prevP.y; + const length = Math.sqrt(dx * dx + dy * dy); + const ratio = (length - 9.5) / length; + path += ` L ${prevP.x + dx * ratio},${prevP.y + dy * ratio}`; + } else { + // Lines between points in between are not shortened + path += ` L ${p.x},${p.y}`; + } + } + return ( + + {/* This is the actual path being rendered */} + + {/* This is a transparent path that is rendered on top of the actual path to make it easier to select the edge */} + + + ); + } +} + +/** + * Smaller version of the default edge routing handle. + */ +@injectable() +export class CustomRoutingHandleView extends SRoutingHandleView { + getRadius(): number { + return 5; + } +} diff --git a/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx b/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx new file mode 100644 index 00000000..7bb32c65 --- /dev/null +++ b/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx @@ -0,0 +1,35 @@ +/** @jsx svg */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { IViewArgs, SLabelImpl, SNodeImpl, ShapeView, RenderingContext, svg } from "sprotty"; +import { VNode } from "snabbdom"; +import { injectable } from "inversify"; +import { Point } from "sprotty-protocol"; + +export interface DfdPositionalLabelArgs extends IViewArgs { + xPosition: number; + yPosition: number; +} + +@injectable() +export class DfdPositionalLabelView extends ShapeView { + private getPosition(label: Readonly, args?: DfdPositionalLabelArgs | IViewArgs): Point { + if (args && "xPosition" in args && "yPosition" in args) { + return { x: args.xPosition, y: args.yPosition }; + } else { + const parentSize = (label.parent as SNodeImpl | undefined)?.bounds; + const width = parentSize?.width ?? 0; + const height = parentSize?.height ?? 0; + return { x: width / 2, y: height / 2 }; + } + } + + render(label: Readonly, _context: RenderingContext, args?: DfdPositionalLabelArgs): VNode | undefined { + const position = this.getPosition(label, args); + + return ( + + {label.text} + + ); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx b/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx new file mode 100644 index 00000000..7dd229a5 --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx @@ -0,0 +1,53 @@ +/** @jsx svg */ +import { inject, injectable } from "inversify"; +import { DfdNodeImpl } from "./common"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { ShapeView, RenderingContext, svg } from "sprotty"; +import { VNode } from "snabbdom"; +import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +export class FunctionNodeImpl extends DfdNodeImpl { + static readonly TEXT_HEIGHT = 28; + static readonly SEPARATOR_NO_LABEL_PADDING = 4; + static readonly SEPARATOR_LABEL_PADDING = 4; + static readonly LABEL_START_HEIGHT = this.TEXT_HEIGHT + this.SEPARATOR_LABEL_PADDING; + static readonly BORDER_RADIUS = 5; + + protected noLabelHeight(): number { + return FunctionNodeImpl.LABEL_START_HEIGHT + FunctionNodeImpl.SEPARATOR_NO_LABEL_PADDING; + } + protected labelStartHeight(): number { + return FunctionNodeImpl.LABEL_START_HEIGHT + } + + +} + +@injectable() +export class FunctionNodeView extends ShapeView { + constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { + super(); + } + + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + const r = FunctionNodeImpl.BORDER_RADIUS; + + return ( + + + + {context.renderChildren(node, { + xPosition: width / 2, + yPosition: FunctionNodeImpl.TEXT_HEIGHT / 2, + } as DfdPositionalLabelArgs)} + {this.labelRenderer.renderNodeLabels(node, FunctionNodeImpl.LABEL_START_HEIGHT)} + + ); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx b/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx new file mode 100644 index 00000000..fd3d6a4d --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx @@ -0,0 +1,48 @@ +/** @jsx svg */ +import { inject, injectable } from "inversify"; +import { DfdNodeImpl } from "./common"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { ShapeView, svg, RenderingContext } from "sprotty"; +import { VNode } from "snabbdom"; +import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +@injectable() +export class IONodeImpl extends DfdNodeImpl { + static readonly TEXT_HEIGHT = 32; + static readonly LABEL_START_HEIGHT = 28; + + protected noLabelHeight(): number { + return IONodeImpl.TEXT_HEIGHT; + } + protected labelStartHeight(): number { + return IONodeImpl.LABEL_START_HEIGHT + } +} + +@injectable() +export class IONodeView extends ShapeView { + + constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { + super(); + } + + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + + return ( + + + {context.renderChildren(node, { + xPosition: width / 2, + yPosition: IONodeImpl.TEXT_HEIGHT / 2, + } as DfdPositionalLabelArgs)} + {this.labelRenderer.renderNodeLabels(node, IONodeImpl.LABEL_START_HEIGHT)} + + ); + } +} diff --git a/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx new file mode 100644 index 00000000..34913173 --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx @@ -0,0 +1,135 @@ +/** @jsx svg */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { IActionDispatcher, SModelElementImpl, SNodeImpl, svg, TYPES } from "sprotty"; +import { LabelAssignment, LabelType, LabelTypeValue } from "../../labels/LabelType"; +import { inject, injectable } from "inversify"; +import { LabelTypeRegistry } from "../../labels/LabelTypeRegistry"; +import { calculateTextSize } from "../../utils/TextSize"; +import { VNode } from "snabbdom"; + +@injectable() +export class DfdNodeLabelRenderer { + static readonly LABEL_HEIGHT = 10; + static readonly LABEL_SPACE_BETWEEN = 2; + static readonly LABEL_SPACING_HEIGHT = DfdNodeLabelRenderer.LABEL_HEIGHT + DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN; + static readonly LABEL_TEXT_PADDING = 8; + + constructor( + @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + ) {} + + private getLabel(label: LabelAssignment): {type: LabelType, value: LabelTypeValue} | undefined { + const labelType = this.labelTypeRegistry.getLabelType(label.labelTypeId); + const labelTypeValue = labelType?.values.find((value) => value.id === label.labelTypeValueId); + if (!labelType || ! labelTypeValue) { + return undefined + } + return { + type: labelType, + value: labelTypeValue + } + } + + /** + * Gets the label type of the assignment and builds the text to display. + * From this text the width of the label is calculated using the corresponding font size and padding. + * @returns a tuple containing the text and the width of the label in pixel + */ + computeLabelContent(labelAssignment: LabelAssignment): [string, number] { + const label = this.getLabel(labelAssignment) + if (!label) { + return ["", 0]; + } + + const text = `${label.type.name}: ${label.value.text}`; + const width = calculateTextSize(text, "5pt sans-serif").width + DfdNodeLabelRenderer.LABEL_TEXT_PADDING; + + return [text, width]; + } + + renderSingleNodeLabel(node: ContainsDfdLabels & SNodeImpl, label: LabelAssignment, x: number, y: number): VNode { + const [text, width] = this.computeLabelContent(label); + const xLeft = x - width / 2; + const xRight = x + width / 2; + const height = DfdNodeLabelRenderer.LABEL_HEIGHT; + const radius = height / 2; + + const deleteLabelHandler = () => { + // TODO + /* const action = DeleteLabelAssignmentAction.create(label, node); + this.actionDispatcher.dispatch(action);*/ + }; + + return ( + + + + {text} + + { + // Put a x button to delete the element on the right upper edge + node.hoverFeedback ? ( + + + + X + + + ) : undefined + } + + ); + } + + /** + * Sorts the labels alphabetically by label type name (primary) and label type value text (secondary). + * + * @param labels the labels to sort. The operation is performed in-place. + */ + private sortLabels(labels: LabelAssignment[]): void { + labels.sort((a, b) => { + const labelTypeA = this.getLabel(a) + const labelTypeB = this.getLabel(b) + if (!labelTypeA || !labelTypeB) { + return 0; + } + + if (labelTypeA.type.name < labelTypeB.type.name) { + return -1; + } else if (labelTypeA.type.name > labelTypeB.type.name) { + return 1; + } else { + return labelTypeA.value.text.localeCompare(labelTypeB.value.text); + } + }); + } + + renderNodeLabels( + node: ContainsDfdLabels & SNodeImpl, + baseY: number, + xOffset = 0, + labelSpacing = DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT, + ): VNode | undefined { + this.sortLabels(node.labels); + return ( + + {node.labels.map((label, i) => { + const x = node.bounds.width / 2; + const y = baseY + i * labelSpacing; + return this.renderSingleNodeLabel(node, label, x + xOffset, y); + })} + + ); + } +} + +export const containsDfdLabelFeature = Symbol("dfd-label-feature"); + +export interface ContainsDfdLabels extends SModelElementImpl { + labels: LabelAssignment[]; +} + +export function containsDfdLabels(element: T): element is T & ContainsDfdLabels { + return element.features?.has(containsDfdLabelFeature) ?? false; +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx b/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx new file mode 100644 index 00000000..083f859d --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx @@ -0,0 +1,54 @@ +/** @jsx svg */ +import { injectable, inject } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, RenderingContext, ShapeView } from "sprotty"; +import { DfdNodeImpl } from "./common"; +import { VNode } from "snabbdom"; +import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +@injectable() +export class StorageNodeImpl extends DfdNodeImpl { + static readonly TEXT_HEIGHT = 32; + static readonly LABEL_START_HEIGHT = 28; + static readonly LEFT_PADDING = 10; + + protected noLabelHeight(): number { + return StorageNodeImpl.TEXT_HEIGHT; + } + protected labelStartHeight(): number { + return StorageNodeImpl.LABEL_START_HEIGHT; + } + + protected override calculateWidth(): number { + return super.calculateWidth() + StorageNodeImpl.LEFT_PADDING; + } +} + +@injectable() +export class StorageNodeView extends ShapeView { + constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { + super(); + } + + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + const leftPadding = StorageNodeImpl.LEFT_PADDING / 2; + + return ( + + + + {context.renderChildren(node, { + xPosition: width / 2 + leftPadding, + yPosition: StorageNodeImpl.TEXT_HEIGHT / 2, + } as DfdPositionalLabelArgs)} + {this.labelRenderer.renderNodeLabels(node, StorageNodeImpl.LABEL_START_HEIGHT, leftPadding)} + + ); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/nodes/common.ts b/frontend/webEditor/src/diagram/nodes/common.ts new file mode 100644 index 00000000..5c4edd9e --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/common.ts @@ -0,0 +1,134 @@ +import { Bounds, SNode, SPort } from "sprotty-protocol"; +import { DfdNodeAnnotation } from "../../annotation/DFDNodeAnnotation"; +import { LabelAssignment } from "../../labels/LabelType"; +import { isEditableLabel, SNodeImpl, WithEditableLabel, withEditLabelFeature } from "sprotty"; +import { calculateTextSize } from "../../utils/TextSize"; +import { ArrowEdgeImpl } from "../edges/ArrowEdge"; +import { VNodeStyle } from "snabbdom"; +import { DfdInputPortImpl } from "../ports/DfdInputPort"; +import { inject } from "inversify"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +export interface DfdNode extends SNode { + text: string; + labels: LabelAssignment[]; + ports: SPort[]; + annotations?: DfdNodeAnnotation[]; +} + +export abstract class DfdNodeImpl extends SNodeImpl implements WithEditableLabel { + static readonly DEFAULT_FEATURES = [...SNodeImpl.DEFAULT_FEATURES, withEditLabelFeature/*, containsDfdLabelFeature*/]; + static readonly DEFAULT_WIDTH = 50; + static readonly WIDTH_PADDING = 12; + static readonly NODE_COLOR = "var(--color-primary)"; + static readonly HIGHLIGHTED_COLOR = "var(--color-highlighted)"; +@inject(DfdNodeLabelRenderer) private readonly dfdNodeLabelRenderer?: DfdNodeLabelRenderer + text: string = ""; + color?: string; + labels: LabelAssignment[] = []; + ports: SPort[] = []; + hideLabels: boolean = false; + minimumWidth: number = DfdNodeImpl.DEFAULT_WIDTH; + annotations: DfdNodeAnnotation[] = []; + + constructor() { + super() + } + + get editableLabel() { + const label = this.children.find((element) => element.type === "label:positional"); + if (label && isEditableLabel(label)) { + return label; + } + + return undefined; + } + + protected calculateWidth(): number { + if (this.hideLabels) { + return this.minimumWidth + DfdNodeImpl.WIDTH_PADDING; + } + const textWidth = calculateTextSize(this.text).width; + const labelWidths = this.labels.map( + (labelAssignment) => this.dfdNodeLabelRenderer?.computeLabelContent(labelAssignment)[1] ?? 0 + ); + + const neededWidth = Math.max(...labelWidths, textWidth, DfdNodeImpl.DEFAULT_WIDTH); + return neededWidth + DfdNodeImpl.WIDTH_PADDING; + } + + protected calculateHeight(): number { + const hasLabels = this.labels.length > 0; + if (hasLabels && !this.hideLabels) { + return ( + this.labelStartHeight() + + this.labels.length * DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT + + DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN + ); + } else { + return this.noLabelHeight(); + } + } + + protected abstract noLabelHeight(): number + protected abstract labelStartHeight(): number + + override get bounds(): Bounds { + return { + x: this.position.x, + y: this.position.y, + width: this.calculateWidth(), + height: this.calculateHeight(), + }; + } + + /** + * Gets the names of all available input ports. + * @returns a list of the names of all available input ports. + * Can include undefined if a port has no named edges connected to it. + */ + getAvailableInputs(): (string | undefined)[] { + return this.children + .filter((child) => child instanceof DfdInputPortImpl) + .map((child) => child as DfdInputPortImpl) + .map((child) => child.getName()); + } + + /** + * Gets the text of all dfd edges that are connected to the input ports of this node. + * Applies the passed filter to the edges. + * If a edge has no label, the empty string is returned. + */ + getEdgeTexts(edgePredicate: (e: ArrowEdgeImpl) => boolean): string[] { + const inputPorts = this.children + .filter((child) => child instanceof DfdInputPortImpl) + .map((child) => child as DfdInputPortImpl); + + return inputPorts + .flatMap((port) => port.incomingEdges) + .filter((edge) => edge instanceof ArrowEdgeImpl) + .map((edge) => edge as ArrowEdgeImpl) + .filter(edgePredicate) + .map((edge) => edge.editableLabel?.text ?? ""); + } + + /** + * Generates the per-node inline style object for the view. + * Contains the opacity and the color of the node that may be set by the annotation (if any). + */ + geViewStyleObject(): VNodeStyle { + const style: VNodeStyle = { + opacity: this.opacity.toString(), + }; + + style["--border"] = "#FFFFFF"; + + if (this.color) style["--color"] = this.color; + + return style; + } + + public setColor(color: string, override: boolean = true) { + if (override || this.color === DfdNodeImpl.NODE_COLOR) this.color = color; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/ports/DfdInputPort.tsx b/frontend/webEditor/src/diagram/ports/DfdInputPort.tsx new file mode 100644 index 00000000..e342ecd9 --- /dev/null +++ b/frontend/webEditor/src/diagram/ports/DfdInputPort.tsx @@ -0,0 +1,63 @@ +/** @jsx svg */ +import { injectable } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, SRoutableElementImpl, ShapeView, SPortImpl, RenderingContext } from "sprotty"; +import { SPort } from "sprotty-protocol"; +import { ArrowEdgeImpl } from "../edges/ArrowEdge"; +import { DfdPortImpl } from "./common"; +import { VNode } from "snabbdom"; + +export type DfdInputPort = SPort; + +@injectable() +export class DfdInputPortImpl extends DfdPortImpl { + /** + * Builds the name of the input port from the names of the incoming dfd edges. + * @returns either the concatenated names of the incoming edges or undefined if there are no named incoming edges. + */ + getName(): string | undefined { + const edgeNames: string[] = []; + + this.incomingEdges.forEach((edge) => { + if (edge instanceof ArrowEdgeImpl) { + const name = edge.editableLabel?.text; + if (name) { + edgeNames.push(name); + } + } else { + return undefined; + } + }); + + if (edgeNames.length === 0) { + return undefined; + } else { + return edgeNames.sort().join("|"); + } + } + + canConnect(_routable: SRoutableElementImpl, role: "source" | "target"): boolean { + // Only allow edges into this port + return role === "target"; + } +} + +export class DfdInputPortView extends ShapeView { + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + + return ( + + + + I + + {context.renderChildren(node)} + + ); + } +} diff --git a/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx b/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx new file mode 100644 index 00000000..f3173abf --- /dev/null +++ b/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx @@ -0,0 +1,88 @@ +/** @jsx svg */ +import { injectable } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, isEditableLabel, SRoutableElementImpl, ShapeView, RenderingContext } from "sprotty"; +import { SPort } from "sprotty-protocol"; +import { DfdPortImpl } from "./common"; +import { VNode, VNodeStyle } from "snabbdom"; + +export interface DfdOutputPort extends SPort { + behavior: string; +} + +@injectable() +export class DfdOutputPortImpl extends DfdPortImpl { + private behavior: string = ""; + private validBehavior: boolean = true; + + + get editableLabel() { + const label = this.children.find((element) => element.type === "label:invisible"); + if (label && isEditableLabel(label)) { + return label; + } + + return undefined; + } + + canConnect(_routable: SRoutableElementImpl, role: "source" | "target"): boolean { + // Only allow edges from this port outwards + return role === "source"; + } + + /** + * Generates the per-node inline style object for the view. + */ + geViewStyleObject(): VNodeStyle { + const style: VNodeStyle = { + opacity: this.opacity.toString(), + }; + // TODO + // if (!labelTypeRegistry) return style; + + if (!this.validBehavior) { + style["--port-border"] = "#ff0000"; + style["--port-color"] = "#ff6961"; + } + + return style; + } + + public setBehavior(value: string) { + this.behavior = value; + if (value === "") { + this.validBehavior = true; + return; + } + // TODO + const errors = []/*new AutoCompleteTree(TreeBuilder.buildTree(labelTypeRegistry, this)).verify( + this.behavior.split("\n"), + );*/ + this.validBehavior = errors.length === 0; + } + + public getBehavior() { + return this.behavior; + } +} + +@injectable() +export class DfdOutputPortView extends ShapeView { + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + + return ( + + + + O + + {context.renderChildren(node)} + + ); + } +} diff --git a/frontend/webEditor/src/diagram/ports/common.ts b/frontend/webEditor/src/diagram/ports/common.ts new file mode 100644 index 00000000..11116192 --- /dev/null +++ b/frontend/webEditor/src/diagram/ports/common.ts @@ -0,0 +1,18 @@ +import { deletableFeature, moveFeature, SPortImpl } from "sprotty"; +import { Bounds } from "sprotty-protocol"; + +export const defaultPortFeatures = [...SPortImpl.DEFAULT_FEATURES, moveFeature, deletableFeature]; +const portSize = 7; + +export abstract class DfdPortImpl extends SPortImpl { + static readonly DEFAULT_FEATURES = defaultPortFeatures; + + override get bounds(): Bounds { + return { + x: this.position.x, + y: this.position.y, + width: portSize, + height: portSize, + }; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/style.css b/frontend/webEditor/src/diagram/style.css new file mode 100644 index 00000000..ca699f5c --- /dev/null +++ b/frontend/webEditor/src/diagram/style.css @@ -0,0 +1,115 @@ +/* sprotty-* classes are automatically added by sprotty and the other ones + are added in the definition inside nodes.tsx, edge.tsx and ports.tsx */ + +/* Nodes */ + +.sprotty-node { + rect, + line, + circle { + /* stroke color defaults to be the foreground color of the theme. + Alternatively it can be overwritten by setting the --color variable + As a inline style attribute for the specific node. + Used as a highlighter to mark nodes with errors. + This is essentially a "optional parameter" to this css rule. + See https://stackoverflow.com/questions/17893823/how-to-pass-parameters-to-css-classes */ + stroke: var(--color-foreground); + stroke-width: 1; + /* Background fill of the node. + When --color is unset this is just --color-primary. + If this node is annotated and --color is set, it will be included in the color mix. */ + fill: color-mix(in srgb, var(--color-primary), var(--color, transparent) 40%); + } + + .node-label text { + font-size: 5pt; + } + .node-label rect, + .node-label .label-delete circle { + fill: var(--color-primary); + stroke: var(--color-foreground); + stroke-width: 0.5; + } + + .node-label .label-delete text { + fill: var(--color-foreground); + font-size: 5px; + } +} +/* Edges */ + +.sprotty-edge { + stroke: var(--color-foreground); + fill: none; + stroke-width: 1; + + /* On top of the actual edge path we draw a transparent path with a larger stroke width. + This makes it easier to select the edge with the mouse. */ + .sprotty-edge path.select-path { + stroke: transparent; + /* make the "invisible hitbox" 8 pixels wide. This is the same width as the arrow head */ + stroke-width: 8; + } + + .arrow { + fill: var(--color-foreground); + stroke: none; + } + + .label-background rect { + fill: var(--color-background); + stroke-width: 0; + } +} + +.sprotty-edge > .sprotty-routing-handle { + fill: var(--color-foreground); + stroke: none; +} + +/* Ports */ +.sprotty-port { + rect { + stroke: var(--port-border, var(--color-foreground)); + fill: color-mix(in srgb, var(--port-color, var(--color-primary)), var(--color-background) 25%); + stroke-width: 0.5; + } + .port-text { + font-size: 4pt; + } +} + +/* All nodes/misc */ + +.sprotty-node.selected circle, +.sprotty-node.selected rect, +.sprotty-node.selected line, +.sprotty-edge.selected { + stroke-width: 2; +} + +.sprotty-port.selected rect { + stroke-width: 1; +} + +text { + stroke-width: 0; + fill: var(--color-foreground); + font-family: "Arial", sans-serif; + font-size: 11pt; + text-anchor: middle; + dominant-baseline: central; + + -webkit-user-select: none; + user-select: none; +} + +/* elements with the sprotty-missing class use a node type that has not been registered. + Because of this sprotty does not know what to do with them and renders their content and specifies them as missing. + To make these errors very visible we make them red here. + Ideally a user should never see this. */ +.sprotty-missing { + stroke-width: 1; + stroke: var(--color-error); + fill: var(--color-error); +} diff --git a/frontend/webEditor/src/startUpAgent/di.config.ts b/frontend/webEditor/src/startUpAgent/di.config.ts index 8f400039..53f3e36a 100644 --- a/frontend/webEditor/src/startUpAgent/di.config.ts +++ b/frontend/webEditor/src/startUpAgent/di.config.ts @@ -6,6 +6,6 @@ import { SprottyInitializerStartUpAgent } from "./SprottyInit"; export const startUpAgentModule = new ContainerModule((bind) => { bind(StartUpAgent).to(LoadDefaultUiExtensionsStartUpAgent) - bind(StartUpAgent).to(SprottyInitializerStartUpAgent) + //bind(StartUpAgent).to(SprottyInitializerStartUpAgent) bind(StartUpAgent).to(LoadDefaultDiagramStartUpAgent) }) \ No newline at end of file From 4e7a607b71319d25f5ee21fb7b85e9345ce0a51f Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Sat, 1 Nov 2025 16:53:21 +0100 Subject: [PATCH 08/41] rendering --- frontend/webEditor/dependencyGraph.md | 7 +- .../src/annotation/DFDNodeAnnotation.ts | 6 + .../webEditor/src/diagram/ModelFactory.ts | 31 ++++ frontend/webEditor/src/diagram/di.config.ts | 35 ++++- .../webEditor/src/diagram/edges/ArrowEdge.tsx | 117 +++++++++++++++ .../src/diagram/labels/DfdPositionalLabel.tsx | 35 +++++ .../src/diagram/nodes/DfdFunctionNode.tsx | 53 +++++++ .../webEditor/src/diagram/nodes/DfdIONode.tsx | 48 +++++++ .../src/diagram/nodes/DfdNodeLabels.tsx | 135 ++++++++++++++++++ .../src/diagram/nodes/DfdStorageNode.tsx | 54 +++++++ .../webEditor/src/diagram/nodes/common.ts | 134 +++++++++++++++++ .../src/diagram/ports/DfdInputPort.tsx | 63 ++++++++ .../src/diagram/ports/DfdOutputPort.tsx | 88 ++++++++++++ .../webEditor/src/diagram/ports/common.ts | 18 +++ frontend/webEditor/src/diagram/style.css | 115 +++++++++++++++ .../webEditor/src/startUpAgent/di.config.ts | 2 +- 16 files changed, 938 insertions(+), 3 deletions(-) create mode 100644 frontend/webEditor/src/annotation/DFDNodeAnnotation.ts create mode 100644 frontend/webEditor/src/diagram/ModelFactory.ts create mode 100644 frontend/webEditor/src/diagram/edges/ArrowEdge.tsx create mode 100644 frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx create mode 100644 frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx create mode 100644 frontend/webEditor/src/diagram/nodes/DfdIONode.tsx create mode 100644 frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx create mode 100644 frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx create mode 100644 frontend/webEditor/src/diagram/nodes/common.ts create mode 100644 frontend/webEditor/src/diagram/ports/DfdInputPort.tsx create mode 100644 frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx create mode 100644 frontend/webEditor/src/diagram/ports/common.ts create mode 100644 frontend/webEditor/src/diagram/style.css diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md index 8364955f..3f3ec150 100644 --- a/frontend/webEditor/dependencyGraph.md +++ b/frontend/webEditor/dependencyGraph.md @@ -15,9 +15,14 @@ stateDiagram-v2 serialize --> constraint serialize --> editorMode [*] --> serialize - serialize --> commonModule: logger + [*] --> editorMode + serialize --> editorMode + + [*] --> diagram + diagram --> labels + classDef diLess font-style:italic,stroke:#0fa class accordionUiExtension,editorTypes,utils diLess ``` diff --git a/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts b/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts new file mode 100644 index 00000000..eda7cb48 --- /dev/null +++ b/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts @@ -0,0 +1,6 @@ +export interface DfdNodeAnnotation { + message: string; + color?: string; + icon?: string; + tfg?: number; +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/ModelFactory.ts b/frontend/webEditor/src/diagram/ModelFactory.ts new file mode 100644 index 00000000..a36c8fd4 --- /dev/null +++ b/frontend/webEditor/src/diagram/ModelFactory.ts @@ -0,0 +1,31 @@ +import { injectable } from "inversify"; +import { SChildElementImpl, SModelElementImpl, SModelFactory, SParentElementImpl } from "sprotty"; +import { DfdNode } from "./nodes/common"; +import { SLabel, SModelElement } from "sprotty-protocol"; + +@injectable() +export class CustomModelFactory extends SModelFactory { + override createElement(schema: SModelElement | SModelElementImpl, parent?: SParentElementImpl): SChildElementImpl { + if ( + (schema.type === "node:storage" || + schema.type === "node:function" || + schema.type === "node:input-output") && + !(schema instanceof SModelElementImpl) + ) { + const dfdSchema = schema as DfdNode; + schema.children = schema.children ?? []; + for (const port of dfdSchema.ports) { + if ("features" in port) { + delete port.features + } + } + schema.children.push(...dfdSchema.ports, { + type: "label:positional", + text: dfdSchema.text ?? "", + id: schema.id + "-label", + } as SLabel); + } + + return super.createElement(schema, parent); + } +} diff --git a/frontend/webEditor/src/diagram/di.config.ts b/frontend/webEditor/src/diagram/di.config.ts index 49337334..636929af 100644 --- a/frontend/webEditor/src/diagram/di.config.ts +++ b/frontend/webEditor/src/diagram/di.config.ts @@ -1,8 +1,41 @@ import { ContainerModule } from "inversify"; -import { configureModelElement, SGraphImpl, SGraphView } from "sprotty"; +import { configureModelElement, editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SRoutingHandleImpl, TYPES, withEditLabelFeature } from "sprotty"; +import { ArrowEdgeImpl, ArrowEdgeView, CustomRoutingHandleView } from "./edges/ArrowEdge"; +import { DfdInputPortImpl, DfdInputPortView } from "./ports/DfdInputPort"; +import { DfdOutputPortImpl, DfdOutputPortView } from "./ports/DfdOutputPort"; +import { StorageNodeImpl, StorageNodeView } from "./nodes/DfdStorageNode"; +import { FunctionNodeImpl, FunctionNodeView } from "./nodes/DfdFunctionNode"; +import { IONodeImpl, IONodeView } from "./nodes/DfdIONode"; +import './style.css' +import { DfdPositionalLabelView } from "./labels/DfdPositionalLabel"; +import { CustomModelFactory } from "./ModelFactory"; +import { DfdNodeLabelRenderer } from "./nodes/DfdNodeLabels"; export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; configureModelElement(context, "graph", SGraphImpl, SGraphView); + + configureModelElement(context, "node:storage", StorageNodeImpl, StorageNodeView); + configureModelElement(context, "node:function", FunctionNodeImpl, FunctionNodeView); + configureModelElement(context, "node:input-output", IONodeImpl, IONodeView); + + configureModelElement(context, "edge:arrow", ArrowEdgeImpl, ArrowEdgeView, { + enable: [withEditLabelFeature], + }); + configureModelElement(context, "routing-point", SRoutingHandleImpl, CustomRoutingHandleView); + configureModelElement(context, "volatile-routing-point", SRoutingHandleImpl, CustomRoutingHandleView); + + configureModelElement(context, "port:dfd-input", DfdInputPortImpl, DfdInputPortView); + configureModelElement(context, "port:dfd-output", DfdOutputPortImpl, DfdOutputPortView); + + configureModelElement(context, "label", SLabelImpl, SLabelView, { + enable: [editLabelFeature], + }); + configureModelElement(context, "label:positional", SLabelImpl, DfdPositionalLabelView, { + enable: [editLabelFeature], + }); + + rebind(TYPES.IModelFactory).to(CustomModelFactory); + bind(DfdNodeLabelRenderer).toSelf().inSingletonScope() }); \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx b/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx new file mode 100644 index 00000000..5e0aea1e --- /dev/null +++ b/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx @@ -0,0 +1,117 @@ +/** @jsx svg */ +import { injectable } from "inversify"; +import { + PolylineEdgeViewWithGapsOnIntersections, + SEdgeImpl, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + svg, + RenderingContext, + IViewArgs, + WithEditableLabel, + isEditableLabel, + SRoutingHandleView, +} from "sprotty"; +import { VNode } from "snabbdom"; +import { Point, angleOfPoint, toDegrees, SEdge } from "sprotty-protocol"; + +export interface ArrowEdge extends SEdge { + text?: string; +} + +export class ArrowEdgeImpl extends SEdgeImpl implements WithEditableLabel { + text?: string; + + get editableLabel() { + const label = this.children.find((element) => element.type.startsWith("label")); + if (label && isEditableLabel(label)) { + return label; + } + + return undefined; + } +} + +@injectable() +export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { + + override render(edge: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { + // In the default implementation children of the edge are always rendered, because they + // may be visible when the rest of the edge is not. + // We only have the edge label as an children which only must be rendered when the rest of the edge is visible. + // So as an optimization for big diagrams we don't render the label when the rest of the edge is not visible either. + // Otherwise all these labels would be added to the DOM, making it slow.. + const route = this.edgeRouterRegistry.route(edge, args); + if (!this.isVisible(edge, route, context)) { + return undefined; + } + + return super.render(edge, context, args); + } + + + /** + * Renders an arrow at the end of the edge. + */ + protected override renderAdditionals(edge: SEdgeImpl, segments: Point[], context: RenderingContext): VNode[] { + const additionals = super.renderAdditionals(edge, segments, context); + const p1 = segments[segments.length - 2]; + const p2 = segments[segments.length - 1]; + const arrow = ( + + ); + additionals.push(arrow); + return additionals; + } + + /** + * Renders the edge line. + * In contrast to the default implementation that we override here, + * this implementation makes the edge line 10px shorter at the end to make space for the arrow without any overlap. + */ + protected renderLine(edge: SEdgeImpl, segments: Point[]): VNode { + const firstPoint = segments[0]; + let path = `M ${firstPoint.x},${firstPoint.y}`; + for (let i = 1; i < segments.length; i++) { + const p = segments[i]; + if (i === segments.length - 1) { + // Make edge line 9.5px shorter to make space for the arrow + // The arrow is 10px long, but we only shorten by 9.5 px to have overlap at the edge between line and arrow. + // Otherwise edges would be exactly next to each other which would result in a small gap and flickering if you zoom in enough. + const prevP = segments[i - 1]; + const dx = p.x - prevP.x; + const dy = p.y - prevP.y; + const length = Math.sqrt(dx * dx + dy * dy); + const ratio = (length - 9.5) / length; + path += ` L ${prevP.x + dx * ratio},${prevP.y + dy * ratio}`; + } else { + // Lines between points in between are not shortened + path += ` L ${p.x},${p.y}`; + } + } + return ( + + {/* This is the actual path being rendered */} + + {/* This is a transparent path that is rendered on top of the actual path to make it easier to select the edge */} + + + ); + } +} + +/** + * Smaller version of the default edge routing handle. + */ +@injectable() +export class CustomRoutingHandleView extends SRoutingHandleView { + getRadius(): number { + return 5; + } +} diff --git a/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx b/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx new file mode 100644 index 00000000..7bb32c65 --- /dev/null +++ b/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx @@ -0,0 +1,35 @@ +/** @jsx svg */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { IViewArgs, SLabelImpl, SNodeImpl, ShapeView, RenderingContext, svg } from "sprotty"; +import { VNode } from "snabbdom"; +import { injectable } from "inversify"; +import { Point } from "sprotty-protocol"; + +export interface DfdPositionalLabelArgs extends IViewArgs { + xPosition: number; + yPosition: number; +} + +@injectable() +export class DfdPositionalLabelView extends ShapeView { + private getPosition(label: Readonly, args?: DfdPositionalLabelArgs | IViewArgs): Point { + if (args && "xPosition" in args && "yPosition" in args) { + return { x: args.xPosition, y: args.yPosition }; + } else { + const parentSize = (label.parent as SNodeImpl | undefined)?.bounds; + const width = parentSize?.width ?? 0; + const height = parentSize?.height ?? 0; + return { x: width / 2, y: height / 2 }; + } + } + + render(label: Readonly, _context: RenderingContext, args?: DfdPositionalLabelArgs): VNode | undefined { + const position = this.getPosition(label, args); + + return ( + + {label.text} + + ); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx b/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx new file mode 100644 index 00000000..7dd229a5 --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx @@ -0,0 +1,53 @@ +/** @jsx svg */ +import { inject, injectable } from "inversify"; +import { DfdNodeImpl } from "./common"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { ShapeView, RenderingContext, svg } from "sprotty"; +import { VNode } from "snabbdom"; +import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +export class FunctionNodeImpl extends DfdNodeImpl { + static readonly TEXT_HEIGHT = 28; + static readonly SEPARATOR_NO_LABEL_PADDING = 4; + static readonly SEPARATOR_LABEL_PADDING = 4; + static readonly LABEL_START_HEIGHT = this.TEXT_HEIGHT + this.SEPARATOR_LABEL_PADDING; + static readonly BORDER_RADIUS = 5; + + protected noLabelHeight(): number { + return FunctionNodeImpl.LABEL_START_HEIGHT + FunctionNodeImpl.SEPARATOR_NO_LABEL_PADDING; + } + protected labelStartHeight(): number { + return FunctionNodeImpl.LABEL_START_HEIGHT + } + + +} + +@injectable() +export class FunctionNodeView extends ShapeView { + constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { + super(); + } + + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + const r = FunctionNodeImpl.BORDER_RADIUS; + + return ( + + + + {context.renderChildren(node, { + xPosition: width / 2, + yPosition: FunctionNodeImpl.TEXT_HEIGHT / 2, + } as DfdPositionalLabelArgs)} + {this.labelRenderer.renderNodeLabels(node, FunctionNodeImpl.LABEL_START_HEIGHT)} + + ); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx b/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx new file mode 100644 index 00000000..fd3d6a4d --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx @@ -0,0 +1,48 @@ +/** @jsx svg */ +import { inject, injectable } from "inversify"; +import { DfdNodeImpl } from "./common"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { ShapeView, svg, RenderingContext } from "sprotty"; +import { VNode } from "snabbdom"; +import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +@injectable() +export class IONodeImpl extends DfdNodeImpl { + static readonly TEXT_HEIGHT = 32; + static readonly LABEL_START_HEIGHT = 28; + + protected noLabelHeight(): number { + return IONodeImpl.TEXT_HEIGHT; + } + protected labelStartHeight(): number { + return IONodeImpl.LABEL_START_HEIGHT + } +} + +@injectable() +export class IONodeView extends ShapeView { + + constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { + super(); + } + + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + + return ( + + + {context.renderChildren(node, { + xPosition: width / 2, + yPosition: IONodeImpl.TEXT_HEIGHT / 2, + } as DfdPositionalLabelArgs)} + {this.labelRenderer.renderNodeLabels(node, IONodeImpl.LABEL_START_HEIGHT)} + + ); + } +} diff --git a/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx new file mode 100644 index 00000000..34913173 --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx @@ -0,0 +1,135 @@ +/** @jsx svg */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { IActionDispatcher, SModelElementImpl, SNodeImpl, svg, TYPES } from "sprotty"; +import { LabelAssignment, LabelType, LabelTypeValue } from "../../labels/LabelType"; +import { inject, injectable } from "inversify"; +import { LabelTypeRegistry } from "../../labels/LabelTypeRegistry"; +import { calculateTextSize } from "../../utils/TextSize"; +import { VNode } from "snabbdom"; + +@injectable() +export class DfdNodeLabelRenderer { + static readonly LABEL_HEIGHT = 10; + static readonly LABEL_SPACE_BETWEEN = 2; + static readonly LABEL_SPACING_HEIGHT = DfdNodeLabelRenderer.LABEL_HEIGHT + DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN; + static readonly LABEL_TEXT_PADDING = 8; + + constructor( + @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + ) {} + + private getLabel(label: LabelAssignment): {type: LabelType, value: LabelTypeValue} | undefined { + const labelType = this.labelTypeRegistry.getLabelType(label.labelTypeId); + const labelTypeValue = labelType?.values.find((value) => value.id === label.labelTypeValueId); + if (!labelType || ! labelTypeValue) { + return undefined + } + return { + type: labelType, + value: labelTypeValue + } + } + + /** + * Gets the label type of the assignment and builds the text to display. + * From this text the width of the label is calculated using the corresponding font size and padding. + * @returns a tuple containing the text and the width of the label in pixel + */ + computeLabelContent(labelAssignment: LabelAssignment): [string, number] { + const label = this.getLabel(labelAssignment) + if (!label) { + return ["", 0]; + } + + const text = `${label.type.name}: ${label.value.text}`; + const width = calculateTextSize(text, "5pt sans-serif").width + DfdNodeLabelRenderer.LABEL_TEXT_PADDING; + + return [text, width]; + } + + renderSingleNodeLabel(node: ContainsDfdLabels & SNodeImpl, label: LabelAssignment, x: number, y: number): VNode { + const [text, width] = this.computeLabelContent(label); + const xLeft = x - width / 2; + const xRight = x + width / 2; + const height = DfdNodeLabelRenderer.LABEL_HEIGHT; + const radius = height / 2; + + const deleteLabelHandler = () => { + // TODO + /* const action = DeleteLabelAssignmentAction.create(label, node); + this.actionDispatcher.dispatch(action);*/ + }; + + return ( + + + + {text} + + { + // Put a x button to delete the element on the right upper edge + node.hoverFeedback ? ( + + + + X + + + ) : undefined + } + + ); + } + + /** + * Sorts the labels alphabetically by label type name (primary) and label type value text (secondary). + * + * @param labels the labels to sort. The operation is performed in-place. + */ + private sortLabels(labels: LabelAssignment[]): void { + labels.sort((a, b) => { + const labelTypeA = this.getLabel(a) + const labelTypeB = this.getLabel(b) + if (!labelTypeA || !labelTypeB) { + return 0; + } + + if (labelTypeA.type.name < labelTypeB.type.name) { + return -1; + } else if (labelTypeA.type.name > labelTypeB.type.name) { + return 1; + } else { + return labelTypeA.value.text.localeCompare(labelTypeB.value.text); + } + }); + } + + renderNodeLabels( + node: ContainsDfdLabels & SNodeImpl, + baseY: number, + xOffset = 0, + labelSpacing = DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT, + ): VNode | undefined { + this.sortLabels(node.labels); + return ( + + {node.labels.map((label, i) => { + const x = node.bounds.width / 2; + const y = baseY + i * labelSpacing; + return this.renderSingleNodeLabel(node, label, x + xOffset, y); + })} + + ); + } +} + +export const containsDfdLabelFeature = Symbol("dfd-label-feature"); + +export interface ContainsDfdLabels extends SModelElementImpl { + labels: LabelAssignment[]; +} + +export function containsDfdLabels(element: T): element is T & ContainsDfdLabels { + return element.features?.has(containsDfdLabelFeature) ?? false; +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx b/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx new file mode 100644 index 00000000..083f859d --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx @@ -0,0 +1,54 @@ +/** @jsx svg */ +import { injectable, inject } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, RenderingContext, ShapeView } from "sprotty"; +import { DfdNodeImpl } from "./common"; +import { VNode } from "snabbdom"; +import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +@injectable() +export class StorageNodeImpl extends DfdNodeImpl { + static readonly TEXT_HEIGHT = 32; + static readonly LABEL_START_HEIGHT = 28; + static readonly LEFT_PADDING = 10; + + protected noLabelHeight(): number { + return StorageNodeImpl.TEXT_HEIGHT; + } + protected labelStartHeight(): number { + return StorageNodeImpl.LABEL_START_HEIGHT; + } + + protected override calculateWidth(): number { + return super.calculateWidth() + StorageNodeImpl.LEFT_PADDING; + } +} + +@injectable() +export class StorageNodeView extends ShapeView { + constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { + super(); + } + + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + const leftPadding = StorageNodeImpl.LEFT_PADDING / 2; + + return ( + + + + {context.renderChildren(node, { + xPosition: width / 2 + leftPadding, + yPosition: StorageNodeImpl.TEXT_HEIGHT / 2, + } as DfdPositionalLabelArgs)} + {this.labelRenderer.renderNodeLabels(node, StorageNodeImpl.LABEL_START_HEIGHT, leftPadding)} + + ); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/nodes/common.ts b/frontend/webEditor/src/diagram/nodes/common.ts new file mode 100644 index 00000000..5c4edd9e --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/common.ts @@ -0,0 +1,134 @@ +import { Bounds, SNode, SPort } from "sprotty-protocol"; +import { DfdNodeAnnotation } from "../../annotation/DFDNodeAnnotation"; +import { LabelAssignment } from "../../labels/LabelType"; +import { isEditableLabel, SNodeImpl, WithEditableLabel, withEditLabelFeature } from "sprotty"; +import { calculateTextSize } from "../../utils/TextSize"; +import { ArrowEdgeImpl } from "../edges/ArrowEdge"; +import { VNodeStyle } from "snabbdom"; +import { DfdInputPortImpl } from "../ports/DfdInputPort"; +import { inject } from "inversify"; +import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; + +export interface DfdNode extends SNode { + text: string; + labels: LabelAssignment[]; + ports: SPort[]; + annotations?: DfdNodeAnnotation[]; +} + +export abstract class DfdNodeImpl extends SNodeImpl implements WithEditableLabel { + static readonly DEFAULT_FEATURES = [...SNodeImpl.DEFAULT_FEATURES, withEditLabelFeature/*, containsDfdLabelFeature*/]; + static readonly DEFAULT_WIDTH = 50; + static readonly WIDTH_PADDING = 12; + static readonly NODE_COLOR = "var(--color-primary)"; + static readonly HIGHLIGHTED_COLOR = "var(--color-highlighted)"; +@inject(DfdNodeLabelRenderer) private readonly dfdNodeLabelRenderer?: DfdNodeLabelRenderer + text: string = ""; + color?: string; + labels: LabelAssignment[] = []; + ports: SPort[] = []; + hideLabels: boolean = false; + minimumWidth: number = DfdNodeImpl.DEFAULT_WIDTH; + annotations: DfdNodeAnnotation[] = []; + + constructor() { + super() + } + + get editableLabel() { + const label = this.children.find((element) => element.type === "label:positional"); + if (label && isEditableLabel(label)) { + return label; + } + + return undefined; + } + + protected calculateWidth(): number { + if (this.hideLabels) { + return this.minimumWidth + DfdNodeImpl.WIDTH_PADDING; + } + const textWidth = calculateTextSize(this.text).width; + const labelWidths = this.labels.map( + (labelAssignment) => this.dfdNodeLabelRenderer?.computeLabelContent(labelAssignment)[1] ?? 0 + ); + + const neededWidth = Math.max(...labelWidths, textWidth, DfdNodeImpl.DEFAULT_WIDTH); + return neededWidth + DfdNodeImpl.WIDTH_PADDING; + } + + protected calculateHeight(): number { + const hasLabels = this.labels.length > 0; + if (hasLabels && !this.hideLabels) { + return ( + this.labelStartHeight() + + this.labels.length * DfdNodeLabelRenderer.LABEL_SPACING_HEIGHT + + DfdNodeLabelRenderer.LABEL_SPACE_BETWEEN + ); + } else { + return this.noLabelHeight(); + } + } + + protected abstract noLabelHeight(): number + protected abstract labelStartHeight(): number + + override get bounds(): Bounds { + return { + x: this.position.x, + y: this.position.y, + width: this.calculateWidth(), + height: this.calculateHeight(), + }; + } + + /** + * Gets the names of all available input ports. + * @returns a list of the names of all available input ports. + * Can include undefined if a port has no named edges connected to it. + */ + getAvailableInputs(): (string | undefined)[] { + return this.children + .filter((child) => child instanceof DfdInputPortImpl) + .map((child) => child as DfdInputPortImpl) + .map((child) => child.getName()); + } + + /** + * Gets the text of all dfd edges that are connected to the input ports of this node. + * Applies the passed filter to the edges. + * If a edge has no label, the empty string is returned. + */ + getEdgeTexts(edgePredicate: (e: ArrowEdgeImpl) => boolean): string[] { + const inputPorts = this.children + .filter((child) => child instanceof DfdInputPortImpl) + .map((child) => child as DfdInputPortImpl); + + return inputPorts + .flatMap((port) => port.incomingEdges) + .filter((edge) => edge instanceof ArrowEdgeImpl) + .map((edge) => edge as ArrowEdgeImpl) + .filter(edgePredicate) + .map((edge) => edge.editableLabel?.text ?? ""); + } + + /** + * Generates the per-node inline style object for the view. + * Contains the opacity and the color of the node that may be set by the annotation (if any). + */ + geViewStyleObject(): VNodeStyle { + const style: VNodeStyle = { + opacity: this.opacity.toString(), + }; + + style["--border"] = "#FFFFFF"; + + if (this.color) style["--color"] = this.color; + + return style; + } + + public setColor(color: string, override: boolean = true) { + if (override || this.color === DfdNodeImpl.NODE_COLOR) this.color = color; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/ports/DfdInputPort.tsx b/frontend/webEditor/src/diagram/ports/DfdInputPort.tsx new file mode 100644 index 00000000..e342ecd9 --- /dev/null +++ b/frontend/webEditor/src/diagram/ports/DfdInputPort.tsx @@ -0,0 +1,63 @@ +/** @jsx svg */ +import { injectable } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, SRoutableElementImpl, ShapeView, SPortImpl, RenderingContext } from "sprotty"; +import { SPort } from "sprotty-protocol"; +import { ArrowEdgeImpl } from "../edges/ArrowEdge"; +import { DfdPortImpl } from "./common"; +import { VNode } from "snabbdom"; + +export type DfdInputPort = SPort; + +@injectable() +export class DfdInputPortImpl extends DfdPortImpl { + /** + * Builds the name of the input port from the names of the incoming dfd edges. + * @returns either the concatenated names of the incoming edges or undefined if there are no named incoming edges. + */ + getName(): string | undefined { + const edgeNames: string[] = []; + + this.incomingEdges.forEach((edge) => { + if (edge instanceof ArrowEdgeImpl) { + const name = edge.editableLabel?.text; + if (name) { + edgeNames.push(name); + } + } else { + return undefined; + } + }); + + if (edgeNames.length === 0) { + return undefined; + } else { + return edgeNames.sort().join("|"); + } + } + + canConnect(_routable: SRoutableElementImpl, role: "source" | "target"): boolean { + // Only allow edges into this port + return role === "target"; + } +} + +export class DfdInputPortView extends ShapeView { + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + + return ( + + + + I + + {context.renderChildren(node)} + + ); + } +} diff --git a/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx b/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx new file mode 100644 index 00000000..f3173abf --- /dev/null +++ b/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx @@ -0,0 +1,88 @@ +/** @jsx svg */ +import { injectable } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, isEditableLabel, SRoutableElementImpl, ShapeView, RenderingContext } from "sprotty"; +import { SPort } from "sprotty-protocol"; +import { DfdPortImpl } from "./common"; +import { VNode, VNodeStyle } from "snabbdom"; + +export interface DfdOutputPort extends SPort { + behavior: string; +} + +@injectable() +export class DfdOutputPortImpl extends DfdPortImpl { + private behavior: string = ""; + private validBehavior: boolean = true; + + + get editableLabel() { + const label = this.children.find((element) => element.type === "label:invisible"); + if (label && isEditableLabel(label)) { + return label; + } + + return undefined; + } + + canConnect(_routable: SRoutableElementImpl, role: "source" | "target"): boolean { + // Only allow edges from this port outwards + return role === "source"; + } + + /** + * Generates the per-node inline style object for the view. + */ + geViewStyleObject(): VNodeStyle { + const style: VNodeStyle = { + opacity: this.opacity.toString(), + }; + // TODO + // if (!labelTypeRegistry) return style; + + if (!this.validBehavior) { + style["--port-border"] = "#ff0000"; + style["--port-color"] = "#ff6961"; + } + + return style; + } + + public setBehavior(value: string) { + this.behavior = value; + if (value === "") { + this.validBehavior = true; + return; + } + // TODO + const errors = []/*new AutoCompleteTree(TreeBuilder.buildTree(labelTypeRegistry, this)).verify( + this.behavior.split("\n"), + );*/ + this.validBehavior = errors.length === 0; + } + + public getBehavior() { + return this.behavior; + } +} + +@injectable() +export class DfdOutputPortView extends ShapeView { + render(node: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(node, context)) { + return undefined; + } + + const { width, height } = node.bounds; + + return ( + + + + O + + {context.renderChildren(node)} + + ); + } +} diff --git a/frontend/webEditor/src/diagram/ports/common.ts b/frontend/webEditor/src/diagram/ports/common.ts new file mode 100644 index 00000000..11116192 --- /dev/null +++ b/frontend/webEditor/src/diagram/ports/common.ts @@ -0,0 +1,18 @@ +import { deletableFeature, moveFeature, SPortImpl } from "sprotty"; +import { Bounds } from "sprotty-protocol"; + +export const defaultPortFeatures = [...SPortImpl.DEFAULT_FEATURES, moveFeature, deletableFeature]; +const portSize = 7; + +export abstract class DfdPortImpl extends SPortImpl { + static readonly DEFAULT_FEATURES = defaultPortFeatures; + + override get bounds(): Bounds { + return { + x: this.position.x, + y: this.position.y, + width: portSize, + height: portSize, + }; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/style.css b/frontend/webEditor/src/diagram/style.css new file mode 100644 index 00000000..ca699f5c --- /dev/null +++ b/frontend/webEditor/src/diagram/style.css @@ -0,0 +1,115 @@ +/* sprotty-* classes are automatically added by sprotty and the other ones + are added in the definition inside nodes.tsx, edge.tsx and ports.tsx */ + +/* Nodes */ + +.sprotty-node { + rect, + line, + circle { + /* stroke color defaults to be the foreground color of the theme. + Alternatively it can be overwritten by setting the --color variable + As a inline style attribute for the specific node. + Used as a highlighter to mark nodes with errors. + This is essentially a "optional parameter" to this css rule. + See https://stackoverflow.com/questions/17893823/how-to-pass-parameters-to-css-classes */ + stroke: var(--color-foreground); + stroke-width: 1; + /* Background fill of the node. + When --color is unset this is just --color-primary. + If this node is annotated and --color is set, it will be included in the color mix. */ + fill: color-mix(in srgb, var(--color-primary), var(--color, transparent) 40%); + } + + .node-label text { + font-size: 5pt; + } + .node-label rect, + .node-label .label-delete circle { + fill: var(--color-primary); + stroke: var(--color-foreground); + stroke-width: 0.5; + } + + .node-label .label-delete text { + fill: var(--color-foreground); + font-size: 5px; + } +} +/* Edges */ + +.sprotty-edge { + stroke: var(--color-foreground); + fill: none; + stroke-width: 1; + + /* On top of the actual edge path we draw a transparent path with a larger stroke width. + This makes it easier to select the edge with the mouse. */ + .sprotty-edge path.select-path { + stroke: transparent; + /* make the "invisible hitbox" 8 pixels wide. This is the same width as the arrow head */ + stroke-width: 8; + } + + .arrow { + fill: var(--color-foreground); + stroke: none; + } + + .label-background rect { + fill: var(--color-background); + stroke-width: 0; + } +} + +.sprotty-edge > .sprotty-routing-handle { + fill: var(--color-foreground); + stroke: none; +} + +/* Ports */ +.sprotty-port { + rect { + stroke: var(--port-border, var(--color-foreground)); + fill: color-mix(in srgb, var(--port-color, var(--color-primary)), var(--color-background) 25%); + stroke-width: 0.5; + } + .port-text { + font-size: 4pt; + } +} + +/* All nodes/misc */ + +.sprotty-node.selected circle, +.sprotty-node.selected rect, +.sprotty-node.selected line, +.sprotty-edge.selected { + stroke-width: 2; +} + +.sprotty-port.selected rect { + stroke-width: 1; +} + +text { + stroke-width: 0; + fill: var(--color-foreground); + font-family: "Arial", sans-serif; + font-size: 11pt; + text-anchor: middle; + dominant-baseline: central; + + -webkit-user-select: none; + user-select: none; +} + +/* elements with the sprotty-missing class use a node type that has not been registered. + Because of this sprotty does not know what to do with them and renders their content and specifies them as missing. + To make these errors very visible we make them red here. + Ideally a user should never see this. */ +.sprotty-missing { + stroke-width: 1; + stroke: var(--color-error); + fill: var(--color-error); +} diff --git a/frontend/webEditor/src/startUpAgent/di.config.ts b/frontend/webEditor/src/startUpAgent/di.config.ts index 8f400039..53f3e36a 100644 --- a/frontend/webEditor/src/startUpAgent/di.config.ts +++ b/frontend/webEditor/src/startUpAgent/di.config.ts @@ -6,6 +6,6 @@ import { SprottyInitializerStartUpAgent } from "./SprottyInit"; export const startUpAgentModule = new ContainerModule((bind) => { bind(StartUpAgent).to(LoadDefaultUiExtensionsStartUpAgent) - bind(StartUpAgent).to(SprottyInitializerStartUpAgent) + //bind(StartUpAgent).to(SprottyInitializerStartUpAgent) bind(StartUpAgent).to(LoadDefaultDiagramStartUpAgent) }) \ No newline at end of file From c03f775ac6017ba1c36875fefea9c9986b46e212 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Tue, 4 Nov 2025 10:26:06 +0100 Subject: [PATCH 09/41] websocket handler --- frontend/webEditor/dependencyGraph.md | 22 +++-- frontend/webEditor/src/index.ts | 4 +- frontend/webEditor/src/webSocket/di.config.ts | 7 ++ frontend/webEditor/src/webSocket/webSocket.ts | 83 +++++++++++++++++++ 4 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 frontend/webEditor/src/webSocket/di.config.ts create mode 100644 frontend/webEditor/src/webSocket/webSocket.ts diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md index 3f3ec150..380702eb 100644 --- a/frontend/webEditor/dependencyGraph.md +++ b/frontend/webEditor/dependencyGraph.md @@ -1,12 +1,10 @@ ```mermaid stateDiagram-v2 - [*] --> helpUi - [*] --> startUpAgent + + helpUi --> accordionUiExtension helpUi --> editorTypes startUpAgent --> editorTypes - [*] --> commonModule - [*] --> labels labels --> utils labels --> editorTypes labels --> accordionUiExtension @@ -14,15 +12,23 @@ stateDiagram-v2 serialize --> labels serialize --> constraint serialize --> editorMode - [*] --> serialize serialize --> commonModule: logger - [*] --> editorMode serialize --> editorMode - - [*] --> diagram + diagram --> labels + webSocket --> commonModule: logger + +%% [*] --> commonModule +%% [*] --> labels +%% [*] --> serialize +%% [*] --> editorMode +%% [*] --> diagram +%% [*] --> webSocket +%% [*] --> helpUi +%% [*] --> startUpAgent + classDef diLess font-style:italic,stroke:#0fa class accordionUiExtension,editorTypes,utils diLess ``` diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index dbb4d534..6d63b4a0 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -14,6 +14,7 @@ import { labelModule } from "./labels/di.config"; import { serializeModule } from "./serialize/di.config"; import { editorModeModule } from "./editorMode/di.config"; import { diagramModule } from "./diagram/di.config"; +import { webSocketModule } from "./webSocket/di.config"; const container = new Container(); @@ -31,7 +32,8 @@ container.load( labelModule, editorModeModule, diagramModule, - serializeModule + serializeModule, + webSocketModule ) const startUpAgents = container.getAll(StartUpAgent) diff --git a/frontend/webEditor/src/webSocket/di.config.ts b/frontend/webEditor/src/webSocket/di.config.ts new file mode 100644 index 00000000..82c9bd07 --- /dev/null +++ b/frontend/webEditor/src/webSocket/di.config.ts @@ -0,0 +1,7 @@ +import { ContainerModule } from "inversify"; +import { DfdWebSocket } from "./webSocket"; + +export const webSocketModule = new ContainerModule((bind) => { + bind(DfdWebSocket).toSelf().inSingletonScope(); + +}); \ No newline at end of file diff --git a/frontend/webEditor/src/webSocket/webSocket.ts b/frontend/webEditor/src/webSocket/webSocket.ts new file mode 100644 index 00000000..274b1e24 --- /dev/null +++ b/frontend/webEditor/src/webSocket/webSocket.ts @@ -0,0 +1,83 @@ +import { inject, injectable } from "inversify"; +import { ILogger, TYPES } from "sprotty"; + +@injectable() +export class DfdWebSocket { + + private webSocket?: WebSocket + private webSocketId = -1 + private lastRequest: { + resolve?: (v: string) => void + reject?: (e: Error) => void + } = {} + private static readonly WS_URL = "wss://websocket.dataflowanalysis.org/events/" + + constructor(@inject(TYPES.ILogger) private readonly logger: ILogger) { + this.init() + } + + private init() { + this.webSocket = new WebSocket(DfdWebSocket.WS_URL) + + this.webSocket.onopen = () => { + this.logger.log(this, "WebSocket connection established.") + } + + this.webSocket.onclose = () => { + this.logger.log(this, "WebSocket connection closed. Reconnecting...") + this.reject(new Error("WebSocket connection closed")) + this.init() + } + this.webSocket.onerror = () => { + this.logger.log(this, "WebSocket error occurred.") + this.reject(new Error("WebSocket error occurred")) + this.init() + } + + this.webSocket.onmessage = (event) => { + const message = event.data as string + this.logger.log(this, "WebSocket message received: " + message) + + if (message.startsWith("Error:")) { + this.reject(new Error(message)) + } + + if (message.startsWith("ID assigned:")) { + const parts = message.split(":") + this.webSocketId = parseInt(parts[2].trim()) + this.logger.log(this, "WebSocket ID assigned: " + this.webSocketId) + return + } + + if (this.lastRequest.resolve) { + this.lastRequest.resolve(message) + this.lastRequest.resolve = undefined + this.lastRequest.reject = undefined + } else { + this.logger.log(this, "No pending request to resolve.") + } + } + } + + private reject(error: Error) { + if (this.lastRequest.reject) { + this.lastRequest.reject(error) + this.lastRequest.resolve = undefined + this.lastRequest.reject = undefined + } + } + + public sendMessage(message: string): Promise { + const result = new Promise((resolve, reject) => { + this.lastRequest.resolve = resolve + this.lastRequest.reject = reject + }) + if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) { + this.reject(new Error("WebSocket is not connected")) + return result + } + this.webSocket.send(this.webSocketId + ":" + "TODO: MODEL NAME" + ":" + message) + return result + } + +} \ No newline at end of file From 1cb2a28bfb2573de24e6da5633d7815f0d2afd6d Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Tue, 4 Nov 2025 10:46:23 +0100 Subject: [PATCH 10/41] copy old command palette --- frontend/webEditor/dependencyGraph.md | 4 +- .../src/commandPalette/commandPalette.css | 58 +++++ .../src/commandPalette/commandPalette.ts | 210 ++++++++++++++++++ .../commandPalette/commandPaletteProvider.ts | 89 ++++++++ .../webEditor/src/commandPalette/di.config.ts | 11 + frontend/webEditor/src/index.ts | 4 +- 6 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 frontend/webEditor/src/commandPalette/commandPalette.css create mode 100644 frontend/webEditor/src/commandPalette/commandPalette.ts create mode 100644 frontend/webEditor/src/commandPalette/commandPaletteProvider.ts create mode 100644 frontend/webEditor/src/commandPalette/di.config.ts diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md index 380702eb..967fdd58 100644 --- a/frontend/webEditor/dependencyGraph.md +++ b/frontend/webEditor/dependencyGraph.md @@ -1,6 +1,5 @@ ```mermaid stateDiagram-v2 - helpUi --> accordionUiExtension helpUi --> editorTypes @@ -20,6 +19,8 @@ stateDiagram-v2 webSocket --> commonModule: logger + commandPalette --> serialize + %% [*] --> commonModule %% [*] --> labels %% [*] --> serialize @@ -28,6 +29,7 @@ stateDiagram-v2 %% [*] --> webSocket %% [*] --> helpUi %% [*] --> startUpAgent +%% [*] --> commandPalette classDef diLess font-style:italic,stroke:#0fa class accordionUiExtension,editorTypes,utils diLess diff --git a/frontend/webEditor/src/commandPalette/commandPalette.css b/frontend/webEditor/src/commandPalette/commandPalette.css new file mode 100644 index 00000000..3d88b0ab --- /dev/null +++ b/frontend/webEditor/src/commandPalette/commandPalette.css @@ -0,0 +1,58 @@ +/* Overrides for sprotty command palette css (should be imported in commandPalette.ts before this .css file */ + +.command-palette { + transition: opacity 0.2s ease-in-out; + display: flex; + flex-direction: column; + row-gap: 4px; + width: 350px; +} + +.command-palette input { + color: var(--color-foreground); + background: var(--color-primary); +} + +.command-palette-suggestions-holder { + width: 100%; +} + +.command-palette-suggestion { + display: grid; + grid-template-columns: 24px 1fr 24px 0px; + background: var(--color-primary); + overflow: visible; + height: 20px; + min-width: 100%; + white-space: nowrap; + width: 100%; + cursor: pointer; +} + +.command-palette-suggestion:hover, +.command-palette-suggestion.selected { + background: var(--color-background); +} + +.command-palette-suggestion-children { + position: relative; + top: 0px; + right: 0px; + display: none; + background: var(--color-primary); + width: fit-content; + height: fit-content; + border-left: 4px solid var(--color-spacer); + box-shadow: + 0 4px 8px 0 rgba(0, 0, 0, 0.2), + 0 6px 20px 0 rgba(0, 0, 0, 0.19); +} + +.command-palette-suggestion:hover > .command-palette-suggestion-children, +.command-palette-suggestion.expanded > .command-palette-suggestion-children { + display: block; +} + +.command-palette .fa-solid { + text-align: center; +} \ No newline at end of file diff --git a/frontend/webEditor/src/commandPalette/commandPalette.ts b/frontend/webEditor/src/commandPalette/commandPalette.ts new file mode 100644 index 00000000..a5131658 --- /dev/null +++ b/frontend/webEditor/src/commandPalette/commandPalette.ts @@ -0,0 +1,210 @@ +import { injectable } from "inversify"; +import { CommandPalette, LabeledAction, SModelRootImpl } from "sprotty"; +import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; +import "./commandPalette.css"; +import "sprotty/css/command-palette.css"; +import { FolderAction } from "./CommandPaletteProvider"; + +@injectable() +export class WebEditorCommandPalette extends CommandPalette { + static readonly ID = "command-palette"; + + protected suggestionElement?: HTMLElement; + protected index = -1; + protected childIndex = -1; + protected insideChild = false; + protected actions: (LabeledAction | FolderAction)[] = []; + protected filteredActions: (LabeledAction | FolderAction)[] = []; + + protected initializeContents(containerElement: HTMLElement) { + containerElement.style.position = "absolute"; + containerElement.style.top = "100px"; + containerElement.style.left = "100px"; + this.inputElement = document.createElement("input"); + this.inputElement.style.width = "100%"; + this.inputElement.addEventListener("keydown", (event) => this.processKeyStrokeInInput(event)); + this.inputElement.addEventListener("input", () => this.updateSuggestions()); + this.inputElement.onblur = () => window.setTimeout(() => this.hide(), 200); + this.suggestionElement = document.createElement("div"); + this.suggestionElement.className = "command-palette-suggestions-holder"; + containerElement.appendChild(this.inputElement); + containerElement.appendChild(this.suggestionElement); + } + + override show(root: Readonly, ...contextElementIds: string[]) { + super.show(root, ...contextElementIds); + this.autoCompleteResult.destroy(); + this.index = -1; + this.childIndex = -1; + this.insideChild = false; + this.filteredActions = []; + this.actions = []; + this.suggestionElement!.innerHTML = ""; + this.inputElement!.value = ""; + this.inputElement!.focus(); + this.actionProviderRegistry + .getActions(root, "", this.mousePositionTracker.lastPositionOnDiagram) + .then((actions) => (this.actions = actions)) + .then(() => this.updateSuggestions()); + } + + protected updateSuggestions() { + if (!this.suggestionElement) { + return; + } + this.suggestionElement!.innerHTML = ""; + const searchText = this.inputElement!.value.toLowerCase(); + this.filteredActions = []; + for (const action of this.actions) { + if (this.matchFilter(action, searchText)) { + this.filteredActions.push(action); + continue; + } + if (action instanceof FolderAction) { + const filteredChildren = action.children.filter((child) => this.matchFilter(child, searchText)); + if (filteredChildren.length > 0) { + this.filteredActions.push(new FolderAction(action.label, filteredChildren, action.icon)); + continue; + } + } + } + if (this.index >= this.filteredActions.length) { + this.index = -1; + } + for (const [idx, action] of this.filteredActions.entries()) { + const suggestion = this.renderSuggestion(action); + if (idx === this.index) { + suggestion.classList.add("expanded"); + if (!this.insideChild) { + suggestion.classList.add("selected"); + } + } + this.suggestionElement!.appendChild(suggestion); + } + } + + private renderSuggestion(action: LabeledAction | FolderAction) { + const suggestion = document.createElement("div"); + suggestion.className = "command-palette-suggestion"; + const icon = document.createElement("span"); + icon.className = this.getIconClasses(action.icon); + suggestion.appendChild(icon); + const label = document.createElement("span"); + label.className = "command-palette-suggestion-label"; + label.innerText = action.label; + suggestion.appendChild(label); + const arrow = document.createElement("span"); + suggestion.appendChild(arrow); + if (action instanceof FolderAction) { + arrow.className = "codicon codicon-chevron-right"; + suggestion.appendChild(arrow); + const childHolder = document.createElement("div"); + childHolder.className = "command-palette-suggestion-children"; + for (const [idx, childAction] of action.children.entries()) { + const childSuggestion = this.renderSuggestion(childAction); + if (this.insideChild && this.childIndex === idx) { + childSuggestion.classList.add("selected"); + } + childHolder.appendChild(childSuggestion); + } + suggestion.appendChild(childHolder); + } + suggestion.addEventListener("click", () => { + if (!(action instanceof FolderAction)) { + this.executeAction(action); + } + }); + return suggestion; + } + + private getIconClasses(icon?: string) { + if (!icon) { + return "codicon codicon-gear"; + } + if (icon.startsWith("fa-")) { + return "fa-solid " + icon; + } + if (icon.startsWith("codicon-")) { + return "codicon " + icon; + } + return "codicon codicon-" + icon; + } + + private matchFilter(action: LabeledAction, searchText: string): boolean { + return action.label.toLowerCase().includes(searchText); + } + + id(): string { + return WebEditorCommandPalette.ID; + } + containerClass(): string { + return WebEditorCommandPalette.ID; + } + + protected processKeyStrokeInInput(event: KeyboardEvent) { + if (matchesKeystroke(event, "Escape")) { + this.hide(); + } + + if (matchesKeystroke(event, "ArrowDown")) { + if (this.insideChild) { + this.childIndex = + (this.childIndex + 1) % (this.filteredActions[this.index] as FolderAction).children.length; + } else { + if (this.index === -1) { + this.index = 0; + } else { + this.index = (this.index + 1) % this.suggestionElement!.children.length; + } + } + } + if (matchesKeystroke(event, "ArrowUp")) { + if (this.insideChild) { + this.childIndex = + (this.childIndex - 1 + (this.filteredActions[this.index] as FolderAction).children.length) % + (this.filteredActions[this.index] as FolderAction).children.length; + } else { + if (this.index === -1) { + this.index = this.suggestionElement!.children.length - 1; + } else { + this.index = + (this.index - 1 + this.suggestionElement!.children.length) % + this.suggestionElement!.children.length; + } + } + } + if (matchesKeystroke(event, "ArrowRight")) { + if (!this.insideChild && this.filteredActions[this.index] instanceof FolderAction) { + event.preventDefault(); + this.insideChild = true; + this.childIndex = 0; + } + } + if (matchesKeystroke(event, "ArrowLeft")) { + if (this.insideChild) { + event.preventDefault(); + this.insideChild = false; + this.childIndex = -1; + } + } + if (matchesKeystroke(event, "Enter")) { + if (this.insideChild) { + this.executeAction((this.filteredActions[this.index] as FolderAction).children[this.childIndex]); + } else { + if (this.index !== -1) { + this.executeAction(this.filteredActions[this.index]); + } + } + this.hide(); + } + this.updateSuggestions(); + } + + protected executeAction(input: LabeledAction) { + this.actionDispatcherProvider() + .then((actionDispatcher) => actionDispatcher.dispatchAll(input.actions)) + .catch((reason) => + this.logger.error(this, "No action dispatcher available to execute command palette action", reason), + ); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts new file mode 100644 index 00000000..cbe27730 --- /dev/null +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -0,0 +1,89 @@ +import { inject, injectable } from "inversify"; +import { ICommandPaletteActionProvider, LabeledAction, SModelRootImpl, CommitModelAction } from "sprotty"; +import { LoadDiagramAction } from "../serialize/load"; +import { createDefaultFitToScreenAction } from "../../utils"; +import { SaveDiagramAction } from "../serialize/save"; +import { LoadDefaultDiagramAction } from "../serialize/loadDefaultDiagram"; +import { LayoutModelAction } from "../autoLayout/command"; + +import { SaveDFDandDDAction } from "../serialize/saveDFDandDD"; +import { LoadDFDandDDAction } from "../serialize/loadDFDandDD"; +import { LoadPalladioAction } from "../serialize/loadPalladio"; +import { SaveImageAction } from "../serialize/image"; +import { SettingsManager } from "../settingsMenu/SettingsManager"; +import { Action } from "sprotty-protocol"; +import { LayoutMethod } from "../settingsMenu/LayoutMethod"; + +/** + * Provides possible actions for the command palette. + */ +@injectable() +export class WebEditorCommandPaletteActionProvider implements ICommandPaletteActionProvider { + constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) {} + + async getActions(root: Readonly): Promise<(LabeledAction | FolderAction)[]> { + const fitToScreenAction = createDefaultFitToScreenAction(root); + const commitAction = CommitModelAction.create(); + + return [ + new FolderAction( + "Load", + [ + new LabeledAction("Load diagram from JSON", [LoadDiagramAction.create(), commitAction], "json"), + new LabeledAction("Load DFD and DD", [LoadDFDandDDAction.create(), commitAction], "coffee"), + new LabeledAction("Load Palladio", [LoadPalladioAction.create(), commitAction], "fa-puzzle-piece"), + ], + "go-to-file", + ), + new FolderAction( + "Save", + [ + new LabeledAction("Save diagram as JSON", [SaveDiagramAction.create()], "json"), + new LabeledAction( + "Save diagram as DFD and DD", + [SaveDFDandDDAction.create(), commitAction], + "coffee", + ), + new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"), + ], + "save", + ), + + new LabeledAction("Load default diagram", [LoadDefaultDiagramAction.create(), commitAction], "clear-all"), + new LabeledAction("Fit to Screen", [fitToScreenAction], "screen-normal"), + new FolderAction( + "Layout diagram (Method: Lines)", + [ + new LabeledAction( + "Layout: Lines", + [LayoutModelAction.create(LayoutMethod.LINES), commitAction, fitToScreenAction], + "grabber", + ), + new LabeledAction( + "Layout: Wrapping Lines", + [LayoutModelAction.create(LayoutMethod.WRAPPING), commitAction, fitToScreenAction], + "word-wrap", + ), + new LabeledAction( + "Layout: Circles", + [LayoutModelAction.create(LayoutMethod.CIRCLES), commitAction, fitToScreenAction], + "circle-large", + ), + ], + "layout", + [LayoutModelAction.create(LayoutMethod.LINES), commitAction, fitToScreenAction], + ), + ]; + } +} + +export class FolderAction extends LabeledAction { + constructor( + label: string, + readonly children: LabeledAction[], + icon?: string, + actions: Action[] = [], + ) { + super(label, actions, icon); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/commandPalette/di.config.ts b/frontend/webEditor/src/commandPalette/di.config.ts new file mode 100644 index 00000000..b8b22f48 --- /dev/null +++ b/frontend/webEditor/src/commandPalette/di.config.ts @@ -0,0 +1,11 @@ +import { ContainerModule } from "inversify"; +import { CommandPalette, TYPES } from "sprotty"; +import { WebEditorCommandPalette } from "./CommandPalette"; +import { WebEditorCommandPaletteActionProvider } from "./CommandPaletteProvider"; + +export const commandPaletteModule = new ContainerModule((bind, _, __, rebind) => { + rebind(CommandPalette).to(WebEditorCommandPalette).inSingletonScope(); + + bind(WebEditorCommandPaletteActionProvider).toSelf().inSingletonScope(); + bind(TYPES.ICommandPaletteActionProvider).toService(WebEditorCommandPaletteActionProvider); +}); \ No newline at end of file diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 6d63b4a0..bd495f28 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -15,6 +15,7 @@ import { serializeModule } from "./serialize/di.config"; import { editorModeModule } from "./editorMode/di.config"; import { diagramModule } from "./diagram/di.config"; import { webSocketModule } from "./webSocket/di.config"; +import { commandPaletteModule } from "./commandPalette/di.config"; const container = new Container(); @@ -33,7 +34,8 @@ container.load( editorModeModule, diagramModule, serializeModule, - webSocketModule + webSocketModule, + commandPaletteModule ) const startUpAgents = container.getAll(StartUpAgent) From f8139a39d7ec648e9b0528ff5c3079eeaee975bf Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Tue, 4 Nov 2025 20:41:43 +0100 Subject: [PATCH 11/41] serialization base --- .../commandPalette/commandPaletteProvider.ts | 35 +++++------- frontend/webEditor/src/serialize/di.config.ts | 10 +++- .../webEditor/src/serialize/fileChooser.ts | 43 +++++++++++++++ .../src/serialize/loadDefaultDiagram.ts | 33 ++++++++++-- .../src/serialize/loadDfdAndDdFile.ts | 41 ++++++++++++++ frontend/webEditor/src/serialize/loadJson.ts | 53 ++++++++----------- .../webEditor/src/serialize/loadJsonFile.ts | 42 +++++++++++++++ .../src/serialize/loadPalladioFile.ts | 46 ++++++++++++++++ frontend/webEditor/src/webSocket/webSocket.ts | 15 +++++- 9 files changed, 259 insertions(+), 59 deletions(-) create mode 100644 frontend/webEditor/src/serialize/fileChooser.ts create mode 100644 frontend/webEditor/src/serialize/loadDfdAndDdFile.ts create mode 100644 frontend/webEditor/src/serialize/loadJsonFile.ts create mode 100644 frontend/webEditor/src/serialize/loadPalladioFile.ts diff --git a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts index cbe27730..a345316d 100644 --- a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -1,57 +1,50 @@ -import { inject, injectable } from "inversify"; +import { injectable } from "inversify"; import { ICommandPaletteActionProvider, LabeledAction, SModelRootImpl, CommitModelAction } from "sprotty"; -import { LoadDiagramAction } from "../serialize/load"; -import { createDefaultFitToScreenAction } from "../../utils"; -import { SaveDiagramAction } from "../serialize/save"; + import { LoadDefaultDiagramAction } from "../serialize/loadDefaultDiagram"; -import { LayoutModelAction } from "../autoLayout/command"; -import { SaveDFDandDDAction } from "../serialize/saveDFDandDD"; -import { LoadDFDandDDAction } from "../serialize/loadDFDandDD"; -import { LoadPalladioAction } from "../serialize/loadPalladio"; -import { SaveImageAction } from "../serialize/image"; -import { SettingsManager } from "../settingsMenu/SettingsManager"; +import { LoadJsonFileAction } from "../serialize/loadJsonFile"; import { Action } from "sprotty-protocol"; -import { LayoutMethod } from "../settingsMenu/LayoutMethod"; +import { LoadDfdAndDdFileAction } from "../serialize/loadDfdAndDdFile"; +import { LoadPalladioFileAction } from "../serialize/loadPalladioFile"; /** * Provides possible actions for the command palette. */ @injectable() export class WebEditorCommandPaletteActionProvider implements ICommandPaletteActionProvider { - constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) {} async getActions(root: Readonly): Promise<(LabeledAction | FolderAction)[]> { - const fitToScreenAction = createDefaultFitToScreenAction(root); + //const fitToScreenAction = createDefaultFitToScreenAction(root); const commitAction = CommitModelAction.create(); return [ new FolderAction( "Load", [ - new LabeledAction("Load diagram from JSON", [LoadDiagramAction.create(), commitAction], "json"), - new LabeledAction("Load DFD and DD", [LoadDFDandDDAction.create(), commitAction], "coffee"), - new LabeledAction("Load Palladio", [LoadPalladioAction.create(), commitAction], "fa-puzzle-piece"), + new LabeledAction("Load diagram from JSON", [LoadJsonFileAction.create(), commitAction], "json"), + new LabeledAction("Load DFD and DD", [LoadDfdAndDdFileAction.create(), commitAction], "coffee"), + new LabeledAction("Load Palladio", [LoadPalladioFileAction.create(), commitAction], "fa-puzzle-piece"), ], "go-to-file", ), new FolderAction( "Save", [ - new LabeledAction("Save diagram as JSON", [SaveDiagramAction.create()], "json"), + /*new LabeledAction("Save diagram as JSON", [SaveDiagramAction.create()], "json"), new LabeledAction( "Save diagram as DFD and DD", [SaveDFDandDDAction.create(), commitAction], "coffee", ), - new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"), + new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"),*/ ], "save", ), new LabeledAction("Load default diagram", [LoadDefaultDiagramAction.create(), commitAction], "clear-all"), - new LabeledAction("Fit to Screen", [fitToScreenAction], "screen-normal"), - new FolderAction( + //new LabeledAction("Fit to Screen", [fitToScreenAction], "screen-normal"), + /*new FolderAction( "Layout diagram (Method: Lines)", [ new LabeledAction( @@ -72,7 +65,7 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct ], "layout", [LayoutModelAction.create(LayoutMethod.LINES), commitAction, fitToScreenAction], - ), + ),*/ ]; } } diff --git a/frontend/webEditor/src/serialize/di.config.ts b/frontend/webEditor/src/serialize/di.config.ts index 36fdeef6..e72f92fe 100644 --- a/frontend/webEditor/src/serialize/di.config.ts +++ b/frontend/webEditor/src/serialize/di.config.ts @@ -1,8 +1,14 @@ import { ContainerModule } from "inversify"; import { configureCommand } from "sprotty"; -import { LoadJsonCommand } from "./loadJson"; +import { LoadDefaultDiagramCommand } from "./loadDefaultDiagram"; +import { LoadDfdAndDdFileCommand } from "./loadDfdAndDdFile"; +import { LoadJsonFileCommand } from "./loadJsonFile"; +import { LoadPalladioFileCommand } from "./loadPalladioFile"; export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; - configureCommand(context, LoadJsonCommand); + configureCommand(context, LoadDefaultDiagramCommand); + configureCommand(context, LoadJsonFileCommand); + configureCommand(context, LoadDfdAndDdFileCommand); + configureCommand(context, LoadPalladioFileCommand); }) \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/fileChooser.ts b/frontend/webEditor/src/serialize/fileChooser.ts new file mode 100644 index 00000000..d6a820ae --- /dev/null +++ b/frontend/webEditor/src/serialize/fileChooser.ts @@ -0,0 +1,43 @@ +import { FileData } from "./loadJson"; + +function getFiles(acceptedTypes: string[], amount: number): Promise { + const input = document.createElement("input"); + input.type = "file"; + input.accept = acceptedTypes.join(","); + input.multiple = amount > 1; + const fileLoadPromise = new Promise((resolve, reject) => { + input.onchange = () => { + if (!input.files || input.files.length !== amount) { + reject("No file selected"); + return; + } + resolve(Array.from(input.files)); + }; + }); + + input.click(); + + return fileLoadPromise; +} + +function readFile(file: File): Promise> { + return new Promise>((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => + resolve({ + fileName: file.name, + content: reader.result as string, // since we use readAsText reader.result is always a string + }); + reader.onerror = () => reject(reader.error); + reader.readAsText(file); + }); +} + +export async function chooseFiles(acceptedTypes: string[], amount: number): Promise[]> { + const files = await getFiles(acceptedTypes, amount); + return Promise.all(files.map(readFile)); +} + +export function chooseFile(acceptedTypes: string[]): Promise | undefined> { + return chooseFiles(acceptedTypes, 1).then((files) => (files ? files[0] : undefined)); +} diff --git a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts index 922c1e20..cd1be5cc 100644 --- a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts +++ b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts @@ -1,9 +1,36 @@ -import { LoadJsonAction } from "./loadJson"; +import { FileData, LoadJsonCommand } from "./loadJson"; import defaultDiagram from './defaultDiagram.json' import { SavedDiagram } from "./SavedDiagram"; +import { Action } from "sprotty-protocol"; +import { inject } from "inversify"; +import { TYPES, ILogger } from "sprotty"; +import { EditorModeController } from "../editorMode/EditorModeController"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; export namespace LoadDefaultDiagramAction { - export function create() { - return LoadJsonAction.create(defaultDiagram as SavedDiagram) + export const KIND = "loadDefaultDiagram"; + + export function create(): Action { + return { kind: KIND }; + } +} + +export class LoadDefaultDiagramCommand extends LoadJsonCommand { + static readonly KIND = LoadDefaultDiagramAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(EditorModeController) editorModeController: EditorModeController, + ) { + super(logger, labelTypeRegistry, editorModeController); + } + + protected async getFile(): Promise | undefined> { + return { + fileName: "diagram.json", + content: defaultDiagram as SavedDiagram + } } } \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts new file mode 100644 index 00000000..29cfe09e --- /dev/null +++ b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts @@ -0,0 +1,41 @@ +import { Action } from "sprotty-protocol"; +import { FileData, LoadJsonCommand } from "./loadJson"; +import { chooseFiles } from "./fileChooser"; +import { inject } from "inversify"; +import { DfdWebSocket } from "../webSocket/webSocket"; +import { TYPES, ILogger } from "sprotty"; +import { EditorModeController } from "../editorMode/EditorModeController"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { SavedDiagram } from "./SavedDiagram"; + +export namespace LoadDfdAndDdFileAction { + export const KIND = "loadDfdAndDdFile"; + + export function create(): Action { + return { kind: KIND }; + } +} + +export class LoadDfdAndDdFileCommand extends LoadJsonCommand { + static readonly KIND = LoadDfdAndDdFileAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(EditorModeController) editorModeController: EditorModeController, + @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, + ) { + super(logger, labelTypeRegistry, editorModeController); + } + + protected async getFile(): Promise | undefined> { + const files = await chooseFiles([".dataflowdiagram", ".datadictionary"], 2); + const dataflowFileContent = files.find((file) => file.fileName.endsWith(".dataflowdiagram"))?.content; + const dictionaryFileContent = files.find((file) => file.fileName.endsWith(".datadictionary"))?.content; + if (!dataflowFileContent || !dictionaryFileContent) { + return undefined; + } + return this.dfdWebSocket.requestDiagram("DFD:" + dataflowFileContent + "\n:DD:\n" + dictionaryFileContent); + } +} diff --git a/frontend/webEditor/src/serialize/loadJson.ts b/frontend/webEditor/src/serialize/loadJson.ts index e61ff1d5..c10bd56c 100644 --- a/frontend/webEditor/src/serialize/loadJson.ts +++ b/frontend/webEditor/src/serialize/loadJson.ts @@ -1,34 +1,18 @@ -import { Command, CommandExecutionContext, CommandReturn, EMPTY_ROOT, ILogger, SModelRootImpl, TYPES } from "sprotty"; +import { Command, CommandExecutionContext, CommandReturn, EMPTY_ROOT, ILogger, SModelRootImpl } from "sprotty"; import { SavedDiagram } from "./SavedDiagram"; import { Action, SModelElement, SModelRoot } from "sprotty-protocol"; -import { inject } from "inversify"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { EditorModeController } from "../editorMode/EditorModeController"; import { Constraint } from "../constraint/Constraint"; import { EditorMode } from "../editorMode/EditorMode"; import { LabelType } from "../labels/LabelType"; -interface LoadJsonAction extends Action { - kind: typeof LoadJsonAction.KIND; - json: SavedDiagram; - name: string; +export interface FileData { + fileName: string; + content: T; } -export namespace LoadJsonAction { - export const KIND = "load-json"; - - export function create(json: SavedDiagram, name?: string): LoadJsonAction { - return { - kind: KIND, - json, - name: name ?? "diagram.json", - }; - } -} - -export class LoadJsonCommand extends Command { - static readonly KIND = LoadJsonAction.KIND; - +export abstract class LoadJsonCommand extends Command { /* After loading a diagram, this command dispatches other actions like fit to screen and optional auto layouting. However when returning a new model in the execute method, the diagram is not directly updated. We need to wait for the InitializeCanvasBoundsCommand to be fired and finish before we can do things like fit to screen. Because of that we block the execution newly dispatched actions including the actions we dispatched after loading the diagram until the InitializeCanvasBoundsCommand has been processed. This works because the canvasBounds property is always removed loading a diagram, requiring the InitializeCanvasBoundsCommand to be fired. */ @@ -43,27 +27,34 @@ export class LoadJsonCommand extends Command { private oldEditorMode: EditorMode | undefined; private oldFileName: string | undefined; private oldConstrains: Constraint[] | undefined; + private file: FileData | undefined; constructor( - @inject(TYPES.Action) private readonly action: LoadJsonAction, - @inject(TYPES.ILogger) private readonly logger: ILogger, - @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, - @inject(EditorModeController) private editorModeController: EditorModeController, + private readonly logger: ILogger, + private readonly labelTypeRegistry: LabelTypeRegistry, + private editorModeController: EditorModeController, ) { super(); } - execute(context: CommandExecutionContext): CommandReturn { + protected abstract getFile(): Promise | undefined>; + + async execute(context: CommandExecutionContext): Promise { this.oldRoot = context.root; + this.file = await this.getFile().catch(() => undefined); + if (!this.file) { + return context.root; + } + try { - const newSchema = LoadJsonCommand.preprocessModelSchema(this.action.json.model); + const newSchema = LoadJsonCommand.preprocessModelSchema(this.file.content.model); this.newRoot = context.modelFactory.createRoot(newSchema); this.logger.info(this, "Model loaded successfully"); this.oldLabelTypes = this.labelTypeRegistry.getLabelTypes(); - const newLabelTypes = this.action.json.labelTypes; + const newLabelTypes = this.file.content.labelTypes; this.labelTypeRegistry.clearLabelTypes(); if (newLabelTypes) { this.labelTypeRegistry.setLabelTypes(newLabelTypes); @@ -73,7 +64,7 @@ export class LoadJsonCommand extends Command { } this.oldEditorMode = this.editorModeController.getCurrentMode(); - const newEditorMode = this.action.json.mode; + const newEditorMode = this.file.content.mode; if (newEditorMode) { this.editorModeController.setMode(newEditorMode); } else { @@ -121,7 +112,7 @@ export class LoadJsonCommand extends Command { } redo(context: CommandExecutionContext): CommandReturn { - const newLabelTypes = this.action.json.labelTypes; + const newLabelTypes = this.file?.content.labelTypes; this.labelTypeRegistry.clearLabelTypes(); if (newLabelTypes) { this.labelTypeRegistry.setLabelTypes(newLabelTypes); @@ -130,7 +121,7 @@ export class LoadJsonCommand extends Command { this.labelTypeRegistry.clearLabelTypes(); } - const newEditorMode = this.action.json.mode; + const newEditorMode = this.file?.content.mode; if (newEditorMode) { this.editorModeController.setMode(newEditorMode); } else { diff --git a/frontend/webEditor/src/serialize/loadJsonFile.ts b/frontend/webEditor/src/serialize/loadJsonFile.ts new file mode 100644 index 00000000..74985381 --- /dev/null +++ b/frontend/webEditor/src/serialize/loadJsonFile.ts @@ -0,0 +1,42 @@ +import { Action } from "sprotty-protocol"; +import { FileData, LoadJsonCommand } from "./loadJson"; +import { chooseFile } from "./fileChooser"; +import { inject } from "inversify"; +import { TYPES, ILogger } from "sprotty"; +import { EditorModeController } from "../editorMode/EditorModeController"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { SavedDiagram } from "./SavedDiagram"; + +export namespace LoadJsonFileAction { + export const KIND = "loadJsonFile"; + + export function create(): Action { + return { kind: KIND }; + } +} + + +export class LoadJsonFileCommand extends LoadJsonCommand { + static readonly KIND = LoadJsonFileAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(EditorModeController) editorModeController: EditorModeController, +) { + super(logger, labelTypeRegistry, editorModeController); +} + + protected async getFile(): Promise | undefined> { + const file = await chooseFile(["application/json"]) + if (!file) { + return undefined + } + return { + fileName: file.fileName, + content: JSON.parse(file.content) as SavedDiagram + } + } + +} \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/loadPalladioFile.ts b/frontend/webEditor/src/serialize/loadPalladioFile.ts new file mode 100644 index 00000000..2419ed55 --- /dev/null +++ b/frontend/webEditor/src/serialize/loadPalladioFile.ts @@ -0,0 +1,46 @@ +import { Action } from "sprotty-protocol"; +import { FileData, LoadJsonCommand } from "./loadJson"; +import { chooseFiles } from "./fileChooser"; +import { inject } from "inversify"; +import { DfdWebSocket } from "../webSocket/webSocket"; +import { TYPES, ILogger } from "sprotty"; +import { EditorModeController } from "../editorMode/EditorModeController"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { SavedDiagram } from "./SavedDiagram"; + +export namespace LoadPalladioFileAction { + export const KIND = "loadPcmFile"; + + export function create(): Action { + return { kind: KIND }; + } +} + +export class LoadPalladioFileCommand extends LoadJsonCommand { + static readonly KIND = LoadPalladioFileAction.KIND; + private static readonly FILE_ENDINGS = [".pddc", ".allocation", ".nodecharacteristics", ".repository", ".resourceenvironment", ".system", ".usagemodel"]; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(EditorModeController) editorModeController: EditorModeController, + @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, + ) { + super(logger, labelTypeRegistry, editorModeController); + } + + protected async getFile(): Promise | undefined> { + const files = await chooseFiles(LoadPalladioFileCommand.FILE_ENDINGS, LoadPalladioFileCommand.FILE_ENDINGS.length); + + if ( + LoadPalladioFileCommand.FILE_ENDINGS.some(ending => + !files.find(file => file.fileName.endsWith(ending)) + ) + ) { + throw new Error("Please select one file of each required type: .pddc, .allocation, .nodecharacteristics, .repository, .resourceenvironment, .system, .usagemodel"); + } + + return this.dfdWebSocket.requestDiagram(files.map((f) => `${f.fileName}:${f.content}`).join("---FILE---")); + } +} diff --git a/frontend/webEditor/src/webSocket/webSocket.ts b/frontend/webEditor/src/webSocket/webSocket.ts index 274b1e24..27c2a60c 100644 --- a/frontend/webEditor/src/webSocket/webSocket.ts +++ b/frontend/webEditor/src/webSocket/webSocket.ts @@ -44,7 +44,7 @@ export class DfdWebSocket { if (message.startsWith("ID assigned:")) { const parts = message.split(":") - this.webSocketId = parseInt(parts[2].trim()) + this.webSocketId = parseInt(parts[1].trim()) this.logger.log(this, "WebSocket ID assigned: " + this.webSocketId) return } @@ -67,6 +67,16 @@ export class DfdWebSocket { } } + public async requestDiagram(message: string) { + const result = await this.sendMessage(message) + const name = result.split(":")[0] + const diagramMessage = result.replace(name + ":", "") + return { + fileName: name, + content: JSON.parse(diagramMessage) + } + } + public sendMessage(message: string): Promise { const result = new Promise((resolve, reject) => { this.lastRequest.resolve = resolve @@ -76,7 +86,8 @@ export class DfdWebSocket { this.reject(new Error("WebSocket is not connected")) return result } - this.webSocket.send(this.webSocketId + ":" + "TODO: MODEL NAME" + ":" + message) + + this.webSocket.send(this.webSocketId + ":" + "TODO: DIAGRAM NAME" + ":" + message) return result } From 25b0c7e2a2613d97ea0a0f240a1beb711e9c09af Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Tue, 4 Nov 2025 21:06:02 +0100 Subject: [PATCH 12/41] add fit to screen action --- frontend/webEditor/dependencyGraph.md | 7 ++++++- .../src/commandPalette/commandPalette.ts | 2 +- .../src/commandPalette/commandPaletteProvider.ts | 5 +++-- frontend/webEditor/src/fitToScreen/action.ts | 16 ++++++++++++++++ .../src/serialize/loadDefaultDiagram.ts | 5 +++-- .../webEditor/src/serialize/loadDfdAndDdFile.ts | 5 +++-- frontend/webEditor/src/serialize/loadJson.ts | 5 ++++- frontend/webEditor/src/serialize/loadJsonFile.ts | 5 +++-- .../webEditor/src/serialize/loadPalladioFile.ts | 5 +++-- 9 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 frontend/webEditor/src/fitToScreen/action.ts diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md index 967fdd58..31fba06f 100644 --- a/frontend/webEditor/dependencyGraph.md +++ b/frontend/webEditor/dependencyGraph.md @@ -21,6 +21,11 @@ stateDiagram-v2 commandPalette --> serialize + serialize --> webSocket + + commandPalette --> fitToScreen + serialize --> fitToScreen + %% [*] --> commonModule %% [*] --> labels %% [*] --> serialize @@ -32,7 +37,7 @@ stateDiagram-v2 %% [*] --> commandPalette classDef diLess font-style:italic,stroke:#0fa - class accordionUiExtension,editorTypes,utils diLess + class accordionUiExtension,editorTypes,utils,fitToScreen diLess ``` green packages do not export a module \ No newline at end of file diff --git a/frontend/webEditor/src/commandPalette/commandPalette.ts b/frontend/webEditor/src/commandPalette/commandPalette.ts index a5131658..3d05063f 100644 --- a/frontend/webEditor/src/commandPalette/commandPalette.ts +++ b/frontend/webEditor/src/commandPalette/commandPalette.ts @@ -3,7 +3,7 @@ import { CommandPalette, LabeledAction, SModelRootImpl } from "sprotty"; import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; import "./commandPalette.css"; import "sprotty/css/command-palette.css"; -import { FolderAction } from "./CommandPaletteProvider"; +import { FolderAction } from "./commandPaletteProvider"; @injectable() export class WebEditorCommandPalette extends CommandPalette { diff --git a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts index a345316d..1d66b770 100644 --- a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -7,6 +7,7 @@ import { LoadJsonFileAction } from "../serialize/loadJsonFile"; import { Action } from "sprotty-protocol"; import { LoadDfdAndDdFileAction } from "../serialize/loadDfdAndDdFile"; import { LoadPalladioFileAction } from "../serialize/loadPalladioFile"; +import { DefaultFitToScreenAction } from "../fitToScreen/action"; /** * Provides possible actions for the command palette. @@ -15,7 +16,7 @@ import { LoadPalladioFileAction } from "../serialize/loadPalladioFile"; export class WebEditorCommandPaletteActionProvider implements ICommandPaletteActionProvider { async getActions(root: Readonly): Promise<(LabeledAction | FolderAction)[]> { - //const fitToScreenAction = createDefaultFitToScreenAction(root); + const fitToScreenAction = DefaultFitToScreenAction.create(root); const commitAction = CommitModelAction.create(); return [ @@ -43,7 +44,7 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct ), new LabeledAction("Load default diagram", [LoadDefaultDiagramAction.create(), commitAction], "clear-all"), - //new LabeledAction("Fit to Screen", [fitToScreenAction], "screen-normal"), + new LabeledAction("Fit to Screen", [fitToScreenAction], "screen-normal"), /*new FolderAction( "Layout diagram (Method: Lines)", [ diff --git a/frontend/webEditor/src/fitToScreen/action.ts b/frontend/webEditor/src/fitToScreen/action.ts new file mode 100644 index 00000000..6d2b1009 --- /dev/null +++ b/frontend/webEditor/src/fitToScreen/action.ts @@ -0,0 +1,16 @@ +import { SModelRootImpl } from "sprotty"; +import { SModelRoot, FitToScreenAction, getBasicType } from "sprotty-protocol"; + +export const FIT_TO_SCREEN_PADDING = 75; + +export namespace DefaultFitToScreenAction { + export function create(root: SModelRootImpl | SModelRoot, animate = true): FitToScreenAction { + const elementIds = + root.children?.filter((child) => getBasicType(child) === "node").map((child) => child.id) ?? []; + + return FitToScreenAction.create(elementIds, { + padding: FIT_TO_SCREEN_PADDING, + animate, + }); + } +} diff --git a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts index cd1be5cc..47719803 100644 --- a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts +++ b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts @@ -3,7 +3,7 @@ import defaultDiagram from './defaultDiagram.json' import { SavedDiagram } from "./SavedDiagram"; import { Action } from "sprotty-protocol"; import { inject } from "inversify"; -import { TYPES, ILogger } from "sprotty"; +import { TYPES, ILogger, ActionDispatcher } from "sprotty"; import { EditorModeController } from "../editorMode/EditorModeController"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; @@ -23,8 +23,9 @@ export class LoadDefaultDiagramCommand extends LoadJsonCommand { @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, @inject(EditorModeController) editorModeController: EditorModeController, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, ) { - super(logger, labelTypeRegistry, editorModeController); + super(logger, labelTypeRegistry, editorModeController, actionDispatcher); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts index 29cfe09e..66cbbf36 100644 --- a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts @@ -3,7 +3,7 @@ import { FileData, LoadJsonCommand } from "./loadJson"; import { chooseFiles } from "./fileChooser"; import { inject } from "inversify"; import { DfdWebSocket } from "../webSocket/webSocket"; -import { TYPES, ILogger } from "sprotty"; +import { TYPES, ILogger, ActionDispatcher } from "sprotty"; import { EditorModeController } from "../editorMode/EditorModeController"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; @@ -25,8 +25,9 @@ export class LoadDfdAndDdFileCommand extends LoadJsonCommand { @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, @inject(EditorModeController) editorModeController: EditorModeController, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, ) { - super(logger, labelTypeRegistry, editorModeController); + super(logger, labelTypeRegistry, editorModeController, actionDispatcher); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadJson.ts b/frontend/webEditor/src/serialize/loadJson.ts index c10bd56c..9dcde0a2 100644 --- a/frontend/webEditor/src/serialize/loadJson.ts +++ b/frontend/webEditor/src/serialize/loadJson.ts @@ -1,4 +1,4 @@ -import { Command, CommandExecutionContext, CommandReturn, EMPTY_ROOT, ILogger, SModelRootImpl } from "sprotty"; +import { ActionDispatcher, Command, CommandExecutionContext, CommandReturn, EMPTY_ROOT, ILogger, SModelRootImpl } from "sprotty"; import { SavedDiagram } from "./SavedDiagram"; import { Action, SModelElement, SModelRoot } from "sprotty-protocol"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; @@ -6,6 +6,7 @@ import { EditorModeController } from "../editorMode/EditorModeController"; import { Constraint } from "../constraint/Constraint"; import { EditorMode } from "../editorMode/EditorMode"; import { LabelType } from "../labels/LabelType"; +import { DefaultFitToScreenAction } from "../fitToScreen/action"; export interface FileData { fileName: string; @@ -33,6 +34,7 @@ export abstract class LoadJsonCommand extends Command { private readonly logger: ILogger, private readonly labelTypeRegistry: LabelTypeRegistry, private editorModeController: EditorModeController, + private actionDispatcher: ActionDispatcher ) { super(); } @@ -75,6 +77,7 @@ export abstract class LoadJsonCommand extends Command { // TODO: load constraints // TODO: post load actions like layout + this.actionDispatcher.dispatch(DefaultFitToScreenAction.create(this.newRoot)) // TODO: load file name //this.oldFileName = currentFileName; diff --git a/frontend/webEditor/src/serialize/loadJsonFile.ts b/frontend/webEditor/src/serialize/loadJsonFile.ts index 74985381..a595e354 100644 --- a/frontend/webEditor/src/serialize/loadJsonFile.ts +++ b/frontend/webEditor/src/serialize/loadJsonFile.ts @@ -2,7 +2,7 @@ import { Action } from "sprotty-protocol"; import { FileData, LoadJsonCommand } from "./loadJson"; import { chooseFile } from "./fileChooser"; import { inject } from "inversify"; -import { TYPES, ILogger } from "sprotty"; +import { TYPES, ILogger, ActionDispatcher } from "sprotty"; import { EditorModeController } from "../editorMode/EditorModeController"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; @@ -24,8 +24,9 @@ export class LoadJsonFileCommand extends LoadJsonCommand { @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, @inject(EditorModeController) editorModeController: EditorModeController, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, ) { - super(logger, labelTypeRegistry, editorModeController); + super(logger, labelTypeRegistry, editorModeController, actionDispatcher); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadPalladioFile.ts b/frontend/webEditor/src/serialize/loadPalladioFile.ts index 2419ed55..18dd68c3 100644 --- a/frontend/webEditor/src/serialize/loadPalladioFile.ts +++ b/frontend/webEditor/src/serialize/loadPalladioFile.ts @@ -3,7 +3,7 @@ import { FileData, LoadJsonCommand } from "./loadJson"; import { chooseFiles } from "./fileChooser"; import { inject } from "inversify"; import { DfdWebSocket } from "../webSocket/webSocket"; -import { TYPES, ILogger } from "sprotty"; +import { TYPES, ILogger, ActionDispatcher } from "sprotty"; import { EditorModeController } from "../editorMode/EditorModeController"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; @@ -25,9 +25,10 @@ export class LoadPalladioFileCommand extends LoadJsonCommand { @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, @inject(EditorModeController) editorModeController: EditorModeController, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, ) { - super(logger, labelTypeRegistry, editorModeController); + super(logger, labelTypeRegistry, editorModeController, actionDispatcher); } protected async getFile(): Promise | undefined> { From c8fd9c34d9a3e972589c355dda3a1664390ac35a Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Wed, 5 Nov 2025 11:18:28 +0100 Subject: [PATCH 13/41] copy layouting and slightly adjust it --- frontend/webEditor/dependencyGraph.md | 4 + .../commandPalette/commandPaletteProvider.ts | 6 +- frontend/webEditor/src/index.ts | 6 +- frontend/webEditor/src/layout/command.ts | 62 +++ frontend/webEditor/src/layout/di.config.ts | 19 + frontend/webEditor/src/layout/keyListener.ts | 22 + frontend/webEditor/src/layout/layoutMethod.ts | 5 + frontend/webEditor/src/layout/layouter.ts | 423 ++++++++++++++++++ 8 files changed, 544 insertions(+), 3 deletions(-) create mode 100644 frontend/webEditor/src/layout/command.ts create mode 100644 frontend/webEditor/src/layout/di.config.ts create mode 100644 frontend/webEditor/src/layout/keyListener.ts create mode 100644 frontend/webEditor/src/layout/layoutMethod.ts create mode 100644 frontend/webEditor/src/layout/layouter.ts diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md index 31fba06f..21c0b420 100644 --- a/frontend/webEditor/dependencyGraph.md +++ b/frontend/webEditor/dependencyGraph.md @@ -26,6 +26,10 @@ stateDiagram-v2 commandPalette --> fitToScreen serialize --> fitToScreen + layout --> fitToScreen + commandPalette --> layout + +%% [*] --> layout %% [*] --> commonModule %% [*] --> labels %% [*] --> serialize diff --git a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts index 1d66b770..ad2701cf 100644 --- a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -8,6 +8,8 @@ import { Action } from "sprotty-protocol"; import { LoadDfdAndDdFileAction } from "../serialize/loadDfdAndDdFile"; import { LoadPalladioFileAction } from "../serialize/loadPalladioFile"; import { DefaultFitToScreenAction } from "../fitToScreen/action"; +import { LayoutMethod } from "../layout/layoutMethod"; +import { LayoutModelAction } from "../layout/command"; /** * Provides possible actions for the command palette. @@ -45,7 +47,7 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct new LabeledAction("Load default diagram", [LoadDefaultDiagramAction.create(), commitAction], "clear-all"), new LabeledAction("Fit to Screen", [fitToScreenAction], "screen-normal"), - /*new FolderAction( + new FolderAction( "Layout diagram (Method: Lines)", [ new LabeledAction( @@ -66,7 +68,7 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct ], "layout", [LayoutModelAction.create(LayoutMethod.LINES), commitAction, fitToScreenAction], - ),*/ + ), ]; } } diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index bd495f28..b0d259c8 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -16,6 +16,8 @@ import { editorModeModule } from "./editorMode/di.config"; import { diagramModule } from "./diagram/di.config"; import { webSocketModule } from "./webSocket/di.config"; import { commandPaletteModule } from "./commandPalette/di.config"; +import { layoutModule } from "./layout/di.config"; +import { elkLayoutModule } from "sprotty-elk"; const container = new Container(); @@ -35,7 +37,9 @@ container.load( diagramModule, serializeModule, webSocketModule, - commandPaletteModule + commandPaletteModule, + elkLayoutModule, + layoutModule ) const startUpAgents = container.getAll(StartUpAgent) diff --git a/frontend/webEditor/src/layout/command.ts b/frontend/webEditor/src/layout/command.ts new file mode 100644 index 00000000..28a3908b --- /dev/null +++ b/frontend/webEditor/src/layout/command.ts @@ -0,0 +1,62 @@ +import { inject } from "inversify"; +import { Command, CommandExecutionContext, SModelRootImpl, TYPES } from "sprotty"; +import { Action, IModelLayoutEngine, SGraph } from "sprotty-protocol"; +import { DfdLayoutConfigurator } from "./layouter"; +import { LayoutMethod } from "./layoutMethod"; + +export interface LayoutModelAction extends Action { + kind: typeof LayoutModelAction.KIND; + layoutMethod: LayoutMethod; +} +export namespace LayoutModelAction { + export const KIND = "layoutModel"; + + export function create(method: LayoutMethod): LayoutModelAction { + return { + kind: KIND, + layoutMethod: method, + }; + } +} + +export class LayoutModelCommand extends Command { + static readonly KIND = LayoutModelAction.KIND; + + private oldRoot?: SModelRootImpl; + private newModel?: SModelRootImpl; + + constructor( + @inject(TYPES.Action) private readonly action: LayoutModelAction, + @inject(TYPES.IModelLayoutEngine) private readonly layoutEngine: IModelLayoutEngine, + @inject(DfdLayoutConfigurator) private readonly configurator: DfdLayoutConfigurator + ) { + super(); + } + + async execute(context: CommandExecutionContext): Promise { + //this.loadingIndicator?.showIndicator("Layouting..."); + this.oldRoot = context.root + + this.configurator.method = this.action.layoutMethod + // Layouting is normally done on the graph schema. + // This is not viable for us because the dfd nodes have a dynamically computed size. + // This is only available on loaded classes of the elements, not the json schema. + // Thankfully the node implementation classes have all needed properties as well. + // So we can just force cast the graph from the loaded version into the "json graph schema". + // Using of the "bounds" property that the implementation classes have is done using DfdElkLayoutEngine. + const newModel = await this.layoutEngine.layout(context.root as unknown as SGraph); + + // Here we need to cast back. + this.newModel = newModel as unknown as SModelRootImpl; + //this.loadingIndicator?.hideIndicator(); + return this.newModel; + } + + undo(context: CommandExecutionContext): SModelRootImpl { + return this.oldRoot ?? context.root + } + + redo(context: CommandExecutionContext): SModelRootImpl { + return this.newModel ?? context.root + } +} diff --git a/frontend/webEditor/src/layout/di.config.ts b/frontend/webEditor/src/layout/di.config.ts new file mode 100644 index 00000000..9d00c07d --- /dev/null +++ b/frontend/webEditor/src/layout/di.config.ts @@ -0,0 +1,19 @@ +import { ContainerModule } from "inversify"; +import { TYPES, configureCommand } from "sprotty"; +import { ElkFactory, ILayoutConfigurator, ILayoutPostprocessor } from "sprotty-elk"; +import { LayoutModelCommand } from "./command"; +import { CircleLayoutPostProcessor, DfdElkLayoutEngine, DfdLayoutConfigurator, elkFactory } from "./layouter"; +import { AutoLayoutKeyListener } from "./keyListener"; + +export const layoutModule = new ContainerModule((bind, unbind, isBound, rebind) => { + bind(DfdElkLayoutEngine).toSelf().inSingletonScope(); + bind(TYPES.IModelLayoutEngine).toService(DfdElkLayoutEngine); + bind(DfdLayoutConfigurator).to(DfdLayoutConfigurator); + rebind(ILayoutConfigurator).to(DfdLayoutConfigurator); + bind(ILayoutPostprocessor).to(CircleLayoutPostProcessor).inSingletonScope(); + bind(ElkFactory).toConstantValue(elkFactory); + bind(TYPES.KeyListener).to(AutoLayoutKeyListener).inSingletonScope(); + + const context = { bind, unbind, isBound, rebind }; + configureCommand(context, LayoutModelCommand); +}); diff --git a/frontend/webEditor/src/layout/keyListener.ts b/frontend/webEditor/src/layout/keyListener.ts new file mode 100644 index 00000000..61eac364 --- /dev/null +++ b/frontend/webEditor/src/layout/keyListener.ts @@ -0,0 +1,22 @@ +import { CommitModelAction, KeyListener, SModelElementImpl } from "sprotty"; +import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; +import { Action } from "sprotty-protocol"; +import { LayoutModelAction } from "./command"; +import { DefaultFitToScreenAction } from "../fitToScreen/action"; +import { LayoutMethod } from "./layoutMethod"; + +export class AutoLayoutKeyListener extends KeyListener { + keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] { + if (matchesKeystroke(event, "KeyL", "ctrlCmd")) { + event.preventDefault(); + + return [ + LayoutModelAction.create(LayoutMethod.LINES), + CommitModelAction.create(), + DefaultFitToScreenAction.create(element.root), + ]; + } + + return []; + } +} diff --git a/frontend/webEditor/src/layout/layoutMethod.ts b/frontend/webEditor/src/layout/layoutMethod.ts new file mode 100644 index 00000000..c2890bef --- /dev/null +++ b/frontend/webEditor/src/layout/layoutMethod.ts @@ -0,0 +1,5 @@ +export enum LayoutMethod { + LINES = "Lines", + WRAPPING = "Wrapping Lines", + CIRCLES = "Circles", +} diff --git a/frontend/webEditor/src/layout/layouter.ts b/frontend/webEditor/src/layout/layouter.ts new file mode 100644 index 00000000..08e31228 --- /dev/null +++ b/frontend/webEditor/src/layout/layouter.ts @@ -0,0 +1,423 @@ +import ElkConstructor, { ElkExtendedEdge, ElkLabel, ElkNode } from "elkjs/lib/elk.bundled"; +import { injectable, inject } from "inversify"; +import { + DefaultLayoutConfigurator, + ElkFactory, + ElkLayoutEngine, + IElementFilter, + ILayoutPostprocessor, +} from "sprotty-elk"; +import { SChildElementImpl, SShapeElementImpl, isBoundsAware } from "sprotty"; +import { SShapeElement, SModelIndex, SEdge, SLabel } from "sprotty-protocol"; +import { ElkShape, LayoutOptions } from "elkjs"; +import { LayoutMethod } from "./layoutMethod"; +import { calculateTextSize } from "../utils/TextSize"; + +export class DfdLayoutConfigurator extends DefaultLayoutConfigurator { + + private static _method: LayoutMethod = LayoutMethod.LINES + + set method(method: LayoutMethod) { + DfdLayoutConfigurator._method = method + } + get method() { + return DfdLayoutConfigurator._method + } + + protected override graphOptions(): LayoutOptions { + // Elk settings. See https://eclipse.dev/elk/reference.html for available options. + return { + [LayoutMethod.LINES]: { + "org.eclipse.elk.algorithm": "org.eclipse.elk.layered", + "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "30.0", + "org.eclipse.elk.layered.spacing.edgeNodeBetweenLayers": "20.0", + "org.eclipse.elk.port.borderOffset": "14.0", + // Do not do micro layout for nodes, which includes the node dimensions etc. + // These are all automatically determined by our dfd node views + "org.eclipse.elk.omitNodeMicroLayout": "true", + // Balanced graph > straight edges + "org.eclipse.elk.layered.nodePlacement.favorStraightEdges": "false", + }, + [LayoutMethod.WRAPPING]: { + "org.eclipse.elk.algorithm": "org.eclipse.elk.layered", + "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "10.0", //Save more space between layers (long names might break this!) + "org.eclipse.elk.layered.spacing.edgeNodeBetweenLayers": "5.0", //Save more space between layers (long names might break this!) + "org.eclipse.elk.edgeRouting": "ORTHOGONAL", //Edges should be routed orthogonal to each another + "org.eclipse.elk.layered.layering.strategy": "COFFMAN_GRAHAM", + "org.eclipse.elk.layered.compaction.postCompaction.strategy": "LEFT_RIGHT_CONSTRAINT_LOCKING", //Compact the resulting graph horizontally + "org.eclipse.elk.layered.wrapping.strategy": "MULTI_EDGE", //Allow wrapping of multiple edges + "org.eclipse.elk.layered.wrapping.correctionFactor": "2.0", //Allow the wrapping to occur earlier + // Do not do micro layout for nodes, which includes the node dimensions etc. + // These are all automatically determined by our dfd node views + "org.eclipse.elk.omitNodeMicroLayout": "true", + "org.eclipse.elk.port.borderOffset": "14.0", + }, + [LayoutMethod.CIRCLES]: { + "org.eclipse.elk.algorithm": "org.eclipse.elk.stress", + "org.eclipse.elk.force.repulsion": "5.0", + "org.eclipse.elk.force.iterations": "100", //Reduce iterations for faster formatting, did not notice differences with more iterations + "org.eclipse.elk.force.repulsivePower": "1", //Edges should repel vertices as well + // Do not do micro layout for nodes, which includes the node dimensions etc. + // These are all automatically determined by our dfd node views + "org.eclipse.elk.omitNodeMicroLayout": "true", + }, + }[this.method]; + } +} + +export const elkFactory = () => + new ElkConstructor({ + algorithms: ["layered", "stress"], + }); + +/** + * Layout engine for the DFD editor. + * This class inherits the default ElkLayoutEngine but overrides the transformShape method. + * This is necessary because the default ElkLayoutEngine uses the size property of the shapes to determine their sizes. + * However with dynamically sized shapes, the size property is set to -1, which is undesired. + * Instead in this case the size should be determined by the bounds property which is dynamically computed. + * + * Additionally it centers ports on the node edge instead of putting them right next to the node at the edge. + */ +@injectable() +export class DfdElkLayoutEngine extends ElkLayoutEngine { + constructor( + @inject(ElkFactory) elkFactory: ElkFactory, + @inject(IElementFilter) elementFilter: IElementFilter, + @inject(DfdLayoutConfigurator) protected readonly configurator: DfdLayoutConfigurator, + @inject(ILayoutPostprocessor) protected readonly postprocessor: ILayoutPostprocessor, + ) { + super(elkFactory, elementFilter, configurator, undefined, postprocessor); + } + + protected override transformShape(elkShape: ElkShape, sshape: SShapeElementImpl | SShapeElement): void { + if (sshape.position) { + elkShape.x = sshape.position.x; + elkShape.y = sshape.position.y; + } + if ("bounds" in sshape) { + elkShape.width = sshape.bounds.width ?? sshape.size.width; + elkShape.height = sshape.bounds.height ?? sshape.size.height; + } + } + + protected override transformEdge(sedge: SEdge, index: SModelIndex): ElkExtendedEdge { + // remove all middle points of edge and only keep source and target + const elkEdge = super.transformEdge(sedge, index); + elkEdge.sections = []; + return elkEdge; + } + + protected override transformLabel(slabel: SLabel, index: SModelIndex): ElkLabel { + const e = super.transformLabel(slabel, index); + if (this.configurator.method === LayoutMethod.WRAPPING) { + return e; + } + const size = calculateTextSize(slabel.text ?? ""); + e.height = size.height; + e.width = size.width; + return e; + } + + protected override applyShape(sshape: SShapeElement, elkShape: ElkShape, index: SModelIndex): void { + // Check if this is a port, if yes we want to center it on the node edge instead of putting it right next to the node at the edge + if (this.getBasicType(sshape) === "port") { + // Because we use actually pass SShapeElementImpl instead of SShapeElement to this method + // we can access the parent property and the bounds of the parent which is the node of this port. + if (sshape instanceof SChildElementImpl && isBoundsAware(sshape.parent)) { + const parent = sshape.parent; + if ( + elkShape.x !== undefined && + elkShape.width !== undefined && + elkShape.y !== undefined && + elkShape.height !== undefined + ) { + // Note that the port x and y coordinates are relative to the parent node. + + // Move inwards from being adjacent to the node edge by half of the port width/height + // depending on which edge the port is on. + + // depending on the mode the ports may be placed differently + if (this.configurator.method === LayoutMethod.CIRCLES) { + if (elkShape.x <= 0) + // Left edge + elkShape.x -= elkShape.width / 2; + if (elkShape.y <= 0) + // Top edge + elkShape.y -= elkShape.height / 2; + if (elkShape.x >= parent.bounds.width) + // Right edge + elkShape.x -= elkShape.width / 2; + if (elkShape.y >= parent.bounds.height) + // Bottom edge + elkShape.y -= elkShape.height / 2; + } else { + if (elkShape.x <= 0) + // Left edge + elkShape.x += elkShape.width / 2; + if (elkShape.y <= 0) + // Top edge + elkShape.y += elkShape.height / 2; + if (elkShape.x >= parent.bounds.width) + // Right edge + elkShape.x -= elkShape.width / 2; + if (elkShape.y >= parent.bounds.height) + // Bottom edge + elkShape.y -= elkShape.height / 2; + } + } + } + } + + super.applyShape(sshape, elkShape, index); + + const parent = index.getParent(sshape.id); + const parentType = parent ? this.getBasicType(parent) : "unknown"; + if (this.getBasicType(sshape) === "label" && parentType == "edge") { + sshape.size = { + width: -1, + height: -1, + }; + } + } + + protected applyEdge(sedge: SEdge, elkEdge: ElkExtendedEdge, index: SModelIndex): void { + if (this.configurator.method === LayoutMethod.CIRCLES) { + // In the circles layout method, we want to make sure that the edge is not straight + // This is because the circles layout method does not support straight edges + elkEdge.sections = []; + } + super.applyEdge(sedge, elkEdge, index); + } +} + +@injectable() +export class CircleLayoutPostProcessor implements ILayoutPostprocessor { + private portToNodes: Map = new Map(); + private connectedPorts: Map = new Map(); + private nodeSquares: Map = new Map(); + + constructor(@inject(DfdLayoutConfigurator) private readonly configurator: DfdLayoutConfigurator) {} + + postprocess(elkGraph: ElkNode): void { + if (this.configurator.method !== LayoutMethod.CIRCLES) { + return; + } + this.connectedPorts = new Map(); + if (!elkGraph.edges || !elkGraph.children) { + return; + } + for (const edge of elkGraph.edges) { + for (const source of edge.sources) { + if (!this.connectedPorts.has(source)) { + this.connectedPorts.set(source, []); + } + for (const target of edge.targets) { + if (!this.connectedPorts.has(target)) { + this.connectedPorts.set(target, []); + } + this.connectedPorts.get(source)?.push(target); + this.connectedPorts.get(target)?.push(source); + } + } + } + + this.portToNodes = new Map(); + this.nodeSquares = new Map(); + for (const node of elkGraph.children) { + if (node.ports) { + for (const port of node.ports) { + this.portToNodes.set(port.id, node.id); + } + this.nodeSquares.set(node.id, this.getNodeSquare(node)); + } + } + + for (const [port, connected] of this.connectedPorts) { + if (connected.length === 0) { + continue; + } + const intersections = connected.map((connection) => { + const line = this.getLine(port, connection); + const node = this.portToNodes.get(port); + if (!node) { + return { x: 0, y: 0 }; + } + const square = this.nodeSquares.get(node); + if (!square) { + return { x: 0, y: 0 }; + } + const intersection = this.getIntersection(square, line); + return intersection; + }); + const average = { + x: intersections.reduce((sum, intersection) => sum + intersection.x, 0) / intersections.length, + y: intersections.reduce((sum, intersection) => sum + intersection.y, 0) / intersections.length, + }; + + const node = this.portToNodes.get(port); + if (!node) { + continue; + } + const square = this.nodeSquares.get(node); + if (!square) { + continue; + } + const closestPointOnEdge = { + x: average.x, + y: average.y, + }; + + const topEdge = { x1: square.x, y1: square.y, x2: square.x + square.width, y2: square.y }; + const bottomEdge = { + x1: square.x, + y1: square.y + square.height, + x2: square.x + square.width, + y2: square.y + square.height, + }; + const leftEdge = { x1: square.x, y1: square.y, x2: square.x, y2: square.y + square.height }; + const rightEdge = { + x1: square.x + square.width, + y1: square.y, + x2: square.x + square.width, + y2: square.y + square.height, + }; + const distances = [ + { distance: Math.abs(average.y - square.y), dimension: "y", edge: topEdge }, + { distance: Math.abs(average.y - (square.y + square.height)), dimension: "y", edge: bottomEdge }, + { distance: Math.abs(average.x - square.x), dimension: "x", edge: leftEdge }, + { distance: Math.abs(average.x - (square.x + square.width)), dimension: "x", edge: rightEdge }, + ]; + distances.sort((a, b) => a.distance - b.distance); + const closestEdge = distances[0].edge; + if (distances[0].dimension === "y") { + closestPointOnEdge.x = clamp(average.x, closestEdge.x1, closestEdge.x2); + closestPointOnEdge.y = closestEdge.y1; + } else { + closestPointOnEdge.x = closestEdge.x1; + closestPointOnEdge.y = clamp(average.y, closestEdge.y1, closestEdge.y2); + } + + const nodeElk = elkGraph.children.find((child) => child.id === node); + if (!nodeElk) { + continue; + } + const portElk = nodeElk.ports?.find((p) => p.id === port); + if (!portElk) { + continue; + } + portElk.x = closestPointOnEdge.x - (nodeElk.x ?? 0); + portElk.y = closestPointOnEdge.y - (nodeElk.y ?? 0); + } + } + + getNodeSquare(node: ElkNode): Square { + return { + x: node.x ?? 0, + y: node.y ?? 0, + width: node.width ?? 0, + height: node.height ?? 0, + }; + } + + getCenter(square: Square): { x: number; y: number } { + return { + x: square.x + square.width / 2, + y: square.y + square.height / 2, + }; + } + + getLine(port1: string, port2: string): Line { + const node1 = this.portToNodes.get(port1); + const node2 = this.portToNodes.get(port2); + if (!node1 || !node2) { + return { + x1: 0, + y1: 0, + x2: 0, + y2: 0, + }; + } + const square1 = this.nodeSquares.get(node1)!; + const square2 = this.nodeSquares.get(node2)!; + const center1 = this.getCenter(square1); + const center2 = this.getCenter(square2); + + return { + x1: center1.x, + y1: center1.y, + x2: center2.x, + y2: center2.y, + }; + } + + getIntersection(square: Square, line: Line): { x: number; y: number } { + const topLeft = { x: square.x, y: square.y }; + const topRight = { x: square.x + square.width, y: square.y }; + const bottomLeft = { x: square.x, y: square.y + square.height }; + const bottomRight = { x: square.x + square.width, y: square.y + square.height }; + + const intersections = [ + this.getLineIntersection(line, { x1: topLeft.x, y1: topLeft.y, x2: topRight.x, y2: topRight.y }), + this.getLineIntersection(line, { x1: topRight.x, y1: topRight.y, x2: bottomRight.x, y2: bottomRight.y }), + this.getLineIntersection(line, { + x1: bottomRight.x, + y1: bottomRight.y, + x2: bottomLeft.x, + y2: bottomLeft.y, + }), + this.getLineIntersection(line, { x1: bottomLeft.x, y1: bottomLeft.y, x2: topLeft.x, y2: topLeft.y }), + ]; + + const inLineBounds = intersections.filter((intersection) => { + return ( + intersection.x >= Math.min(line.x1, line.x2) && + intersection.x <= Math.max(line.x1, line.x2) && + intersection.y >= Math.min(line.y1, line.y2) && + intersection.y <= Math.max(line.y1, line.y2) + ); + }); + return inLineBounds[0] ?? { x: 0, y: 0 }; + } + + private getLineIntersection(line1: Line, line2: Line): { x: number; y: number } { + const x1 = line1.x1; + const y1 = line1.y1; + const x2 = line1.x2; + const y2 = line1.y2; + const x3 = line2.x1; + const y3 = line2.y1; + const x4 = line2.x2; + const y4 = line2.y2; + + const denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + if (denominator === 0) { + return { x: 0, y: 0 }; + } + + const x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denominator; + const y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denominator; + + return { x, y }; + } +} + +interface Square { + x: number; + y: number; + width: number; + height: number; +} + +interface Line { + x1: number; + y1: number; + x2: number; + y2: number; +} + +function clamp(value: number, l1: number, l2: number): number { + const min = Math.min(l1, l2); + const max = Math.max(l1, l2); + return Math.max(min, Math.min(max, value)); +} From 60feec209bbe7d433c975703c69eabc233b095a0 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Wed, 5 Nov 2025 14:37:51 +0100 Subject: [PATCH 14/41] saving basis --- frontend/webEditor/dependencyGraph.md | 2 + .../commandPalette/commandPaletteProvider.ts | 10 +-- frontend/webEditor/src/commonModule.ts | 2 +- .../webEditor/src/diagram/ModelFactory.ts | 31 ---------- frontend/webEditor/src/diagram/di.config.ts | 4 +- .../webEditor/src/serialize/ModelFactory.ts | 62 +++++++++++++++++++ frontend/webEditor/src/serialize/di.config.ts | 9 ++- .../src/serialize/saveDfdAndDdFile.ts | 49 +++++++++++++++ frontend/webEditor/src/serialize/saveFile.ts | 33 ++++++++++ .../webEditor/src/serialize/saveJsonFile.ts | 35 +++++++++++ .../src/serialize/savedDiagramCreator.ts | 28 +++++++++ .../webEditor/src/startUpAgent/SprottyInit.ts | 15 ----- .../webEditor/src/startUpAgent/di.config.ts | 4 +- .../src/startUpAgent/webSocketConnect.ts | 12 ++++ 14 files changed, 238 insertions(+), 58 deletions(-) delete mode 100644 frontend/webEditor/src/diagram/ModelFactory.ts create mode 100644 frontend/webEditor/src/serialize/ModelFactory.ts create mode 100644 frontend/webEditor/src/serialize/saveDfdAndDdFile.ts create mode 100644 frontend/webEditor/src/serialize/saveFile.ts create mode 100644 frontend/webEditor/src/serialize/saveJsonFile.ts create mode 100644 frontend/webEditor/src/serialize/savedDiagramCreator.ts delete mode 100644 frontend/webEditor/src/startUpAgent/SprottyInit.ts create mode 100644 frontend/webEditor/src/startUpAgent/webSocketConnect.ts diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md index 21c0b420..7783b09d 100644 --- a/frontend/webEditor/dependencyGraph.md +++ b/frontend/webEditor/dependencyGraph.md @@ -29,6 +29,8 @@ stateDiagram-v2 layout --> fitToScreen commandPalette --> layout + startUpAgent --> webSocket + %% [*] --> layout %% [*] --> commonModule %% [*] --> labels diff --git a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts index ad2701cf..93133e98 100644 --- a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -1,8 +1,6 @@ import { injectable } from "inversify"; import { ICommandPaletteActionProvider, LabeledAction, SModelRootImpl, CommitModelAction } from "sprotty"; - import { LoadDefaultDiagramAction } from "../serialize/loadDefaultDiagram"; - import { LoadJsonFileAction } from "../serialize/loadJsonFile"; import { Action } from "sprotty-protocol"; import { LoadDfdAndDdFileAction } from "../serialize/loadDfdAndDdFile"; @@ -10,6 +8,8 @@ import { LoadPalladioFileAction } from "../serialize/loadPalladioFile"; import { DefaultFitToScreenAction } from "../fitToScreen/action"; import { LayoutMethod } from "../layout/layoutMethod"; import { LayoutModelAction } from "../layout/command"; +import { SaveJsonFileAction } from "../serialize/saveJsonFile"; +import { SaveDfdAndDdFileAction } from "../serialize/saveDfdAndDdFile"; /** * Provides possible actions for the command palette. @@ -34,13 +34,13 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct new FolderAction( "Save", [ - /*new LabeledAction("Save diagram as JSON", [SaveDiagramAction.create()], "json"), + new LabeledAction("Save diagram as JSON", [SaveJsonFileAction.create()], "json"), new LabeledAction( "Save diagram as DFD and DD", - [SaveDFDandDDAction.create(), commitAction], + [SaveDfdAndDdFileAction.create()], "coffee", ), - new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"),*/ + //new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"), ], "save", ), diff --git a/frontend/webEditor/src/commonModule.ts b/frontend/webEditor/src/commonModule.ts index cef2a327..7119ee2b 100644 --- a/frontend/webEditor/src/commonModule.ts +++ b/frontend/webEditor/src/commonModule.ts @@ -5,7 +5,7 @@ export const commonModule = new ContainerModule((bind, unbind, isBound, rebind) bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope(); rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); - rebind(TYPES.LogLevel).toConstantValue(LogLevel.warn); // TODO: set to log again + rebind(TYPES.LogLevel).toConstantValue(LogLevel.log); // TODO: set to log again const context = { bind, unbind, isBound, rebind }; configureViewerOptions(context, { zoomLimits: { min: 0.05, max: 20 }, diff --git a/frontend/webEditor/src/diagram/ModelFactory.ts b/frontend/webEditor/src/diagram/ModelFactory.ts deleted file mode 100644 index a36c8fd4..00000000 --- a/frontend/webEditor/src/diagram/ModelFactory.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { injectable } from "inversify"; -import { SChildElementImpl, SModelElementImpl, SModelFactory, SParentElementImpl } from "sprotty"; -import { DfdNode } from "./nodes/common"; -import { SLabel, SModelElement } from "sprotty-protocol"; - -@injectable() -export class CustomModelFactory extends SModelFactory { - override createElement(schema: SModelElement | SModelElementImpl, parent?: SParentElementImpl): SChildElementImpl { - if ( - (schema.type === "node:storage" || - schema.type === "node:function" || - schema.type === "node:input-output") && - !(schema instanceof SModelElementImpl) - ) { - const dfdSchema = schema as DfdNode; - schema.children = schema.children ?? []; - for (const port of dfdSchema.ports) { - if ("features" in port) { - delete port.features - } - } - schema.children.push(...dfdSchema.ports, { - type: "label:positional", - text: dfdSchema.text ?? "", - id: schema.id + "-label", - } as SLabel); - } - - return super.createElement(schema, parent); - } -} diff --git a/frontend/webEditor/src/diagram/di.config.ts b/frontend/webEditor/src/diagram/di.config.ts index 636929af..9a310287 100644 --- a/frontend/webEditor/src/diagram/di.config.ts +++ b/frontend/webEditor/src/diagram/di.config.ts @@ -1,5 +1,5 @@ import { ContainerModule } from "inversify"; -import { configureModelElement, editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SRoutingHandleImpl, TYPES, withEditLabelFeature } from "sprotty"; +import { configureModelElement, editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SRoutingHandleImpl, withEditLabelFeature } from "sprotty"; import { ArrowEdgeImpl, ArrowEdgeView, CustomRoutingHandleView } from "./edges/ArrowEdge"; import { DfdInputPortImpl, DfdInputPortView } from "./ports/DfdInputPort"; import { DfdOutputPortImpl, DfdOutputPortView } from "./ports/DfdOutputPort"; @@ -8,7 +8,6 @@ import { FunctionNodeImpl, FunctionNodeView } from "./nodes/DfdFunctionNode"; import { IONodeImpl, IONodeView } from "./nodes/DfdIONode"; import './style.css' import { DfdPositionalLabelView } from "./labels/DfdPositionalLabel"; -import { CustomModelFactory } from "./ModelFactory"; import { DfdNodeLabelRenderer } from "./nodes/DfdNodeLabels"; export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { @@ -36,6 +35,5 @@ export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) enable: [editLabelFeature], }); - rebind(TYPES.IModelFactory).to(CustomModelFactory); bind(DfdNodeLabelRenderer).toSelf().inSingletonScope() }); \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/ModelFactory.ts b/frontend/webEditor/src/serialize/ModelFactory.ts new file mode 100644 index 00000000..0f7f759c --- /dev/null +++ b/frontend/webEditor/src/serialize/ModelFactory.ts @@ -0,0 +1,62 @@ +import { injectable } from "inversify"; +import { SChildElementImpl, SModelElementImpl, SModelFactory, SParentElementImpl } from "sprotty"; +import { DfdNode } from "../diagram/nodes/common"; +import { getBasicType, SLabel, SModelElement } from "sprotty-protocol"; + +@injectable() +export class DfdModelFactory extends SModelFactory { + override createElement(schema: SModelElement | SModelElementImpl, parent?: SParentElementImpl): SChildElementImpl { + if ( + (schema.type === "node:storage" || + schema.type === "node:function" || + schema.type === "node:input-output") && + !(schema instanceof SModelElementImpl) + ) { + const dfdSchema = schema as DfdNode; + schema.children = schema.children ?? []; + for (const port of dfdSchema.ports) { + if ("features" in port) { + delete port.features + } + } + schema.children.push(...dfdSchema.ports, { + type: "label:positional", + text: dfdSchema.text ?? "", + id: schema.id + "-label", + } as SLabel); + } + + return super.createElement(schema, parent); + } + + override createSchema(element: SModelElementImpl): SModelElement { + const schema = super.createSchema(element); + + if ( + (schema.type === "node:storage" || + schema.type === "node:function" || + schema.type === "node:input-output") && + (element instanceof SModelElementImpl) + ) { + const dfdSchema = schema as DfdNode; + const ports = dfdSchema.children?.filter( + (child) => + getBasicType(child) === 'port' + ) ?? []; + dfdSchema.ports = ports + + const labelValue = schema.children?.find( + (child) => child.type === "label:positional" + ) as SLabel | undefined; + + if (labelValue) { + dfdSchema.text = labelValue.text; + } + + dfdSchema.children = [] + return dfdSchema + } + + return schema; + } +} diff --git a/frontend/webEditor/src/serialize/di.config.ts b/frontend/webEditor/src/serialize/di.config.ts index e72f92fe..cef99f48 100644 --- a/frontend/webEditor/src/serialize/di.config.ts +++ b/frontend/webEditor/src/serialize/di.config.ts @@ -1,9 +1,12 @@ import { ContainerModule } from "inversify"; -import { configureCommand } from "sprotty"; +import { configureCommand, TYPES } from "sprotty"; import { LoadDefaultDiagramCommand } from "./loadDefaultDiagram"; import { LoadDfdAndDdFileCommand } from "./loadDfdAndDdFile"; import { LoadJsonFileCommand } from "./loadJsonFile"; import { LoadPalladioFileCommand } from "./loadPalladioFile"; +import { DfdModelFactory } from "./ModelFactory"; +import { SaveJsonFileCommand } from "./saveJsonFile"; +import { SaveDfdAndDdFileCommand } from "./saveDfdAndDdFile"; export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; @@ -11,4 +14,8 @@ export const serializeModule = new ContainerModule((bind, unbind, isBound, rebin configureCommand(context, LoadJsonFileCommand); configureCommand(context, LoadDfdAndDdFileCommand); configureCommand(context, LoadPalladioFileCommand); + configureCommand(context, SaveJsonFileCommand) + configureCommand(context, SaveDfdAndDdFileCommand) + + rebind(TYPES.IModelFactory).to(DfdModelFactory); }) \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts new file mode 100644 index 00000000..72e61045 --- /dev/null +++ b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts @@ -0,0 +1,49 @@ +import { CommandExecutionContext, TYPES } from "sprotty"; +import { FileData } from "./loadJson"; +import { SaveFileCommand } from "./SaveFile"; +import { inject } from "inversify"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { EditorModeController } from "../editorMode/EditorModeController"; +import { DfdWebSocket } from "../webSocket/webSocket"; +import { Action } from "sprotty-protocol"; + +export namespace SaveDfdAndDdFileAction { + export const KIND = 'saveDfdAndDdFile' + export function create(): Action { + return { kind: KIND } + } +} + +export class SaveDfdAndDdFileCommand extends SaveFileCommand { + + static readonly KIND = SaveDfdAndDdFileAction.KIND; + private static readonly CLOSING_TAG = ""; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, + @inject(EditorModeController) editorModeController: EditorModeController, + @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket + ) { + super(LabelTypeRegistry, editorModeController); + } + + async getFiles(context: CommandExecutionContext): Promise[]> { + const savedDiagram = this.createSavedDiagram(context); + + const response = await this.dfdWebSocket.sendMessage("Json2DFD:" + JSON.stringify(savedDiagram)); + const endIndex = response.indexOf(SaveDfdAndDdFileCommand.CLOSING_TAG) + SaveDfdAndDdFileCommand.CLOSING_TAG.length; + const dfdContent = response.substring(0, endIndex).trim(); + const ddContent = response.substring(endIndex).trim(); + + const fileName = 'TODO' + return Promise.resolve([{ + fileName: fileName + ".dataflowdiagram", + content: dfdContent + }, { + fileName: fileName + ".datadictionary", + content: ddContent + }]); + } + +} \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/saveFile.ts b/frontend/webEditor/src/serialize/saveFile.ts new file mode 100644 index 00000000..39e0b18c --- /dev/null +++ b/frontend/webEditor/src/serialize/saveFile.ts @@ -0,0 +1,33 @@ +import { CommandExecutionContext, CommandReturn, SModelRootImpl } from "sprotty"; +import { FileData } from "./loadJson"; +import { SavedDiagramCreatorCommand } from "./savedDiagramCreator"; + +export abstract class SaveFileCommand extends SavedDiagramCreatorCommand { + + abstract getFiles(context: CommandExecutionContext): Promise[]>; + + async execute(context: CommandExecutionContext): Promise { + const files = await this.getFiles(context) + for (const file of files) { + this.downloadFile(file); + } + + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + + private downloadFile(file: FileData) { + const element = document.createElement('a'); + const fileBlob = new Blob([file.content], { type: 'application/json' }); + element.href = URL.createObjectURL(fileBlob); + element.download = file.fileName; + element.click(); + URL.revokeObjectURL(element.href); + element.remove() + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/saveJsonFile.ts b/frontend/webEditor/src/serialize/saveJsonFile.ts new file mode 100644 index 00000000..1ad63495 --- /dev/null +++ b/frontend/webEditor/src/serialize/saveJsonFile.ts @@ -0,0 +1,35 @@ +import { CommandExecutionContext, TYPES } from "sprotty"; +import { FileData } from "./loadJson"; +import { SaveFileCommand } from "./SaveFile"; +import { EditorModeController } from "../editorMode/EditorModeController"; +import { inject } from "inversify"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { Action } from "sprotty-protocol"; + +export namespace SaveJsonFileAction { + export const KIND = 'saveJsonFile' + export function create(): Action { + return { kind: KIND } + } +} + +export class SaveJsonFileCommand extends SaveFileCommand { + static readonly KIND = SaveJsonFileAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, + @inject(EditorModeController) editorModeController: EditorModeController + ) { + super(LabelTypeRegistry, editorModeController); + } + + getFiles(context: CommandExecutionContext): Promise[]> { + const fileData: FileData = { + fileName: "TODO.json", + content: JSON.stringify(this.createSavedDiagram(context)) + }; + return Promise.resolve([fileData]); + } + +} \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/savedDiagramCreator.ts b/frontend/webEditor/src/serialize/savedDiagramCreator.ts new file mode 100644 index 00000000..a91a45a2 --- /dev/null +++ b/frontend/webEditor/src/serialize/savedDiagramCreator.ts @@ -0,0 +1,28 @@ +import { Command, CommandExecutionContext } from "sprotty"; +import { CURRENT_VERSION, SavedDiagram } from "./SavedDiagram"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { EditorModeController } from "../editorMode/EditorModeController"; + +export abstract class SavedDiagramCreatorCommand extends Command { + + constructor( + private readonly labelTypeRegistry: LabelTypeRegistry, + private readonly editorModeController: EditorModeController + ) { + super() + } + + protected createSavedDiagram(context: CommandExecutionContext): SavedDiagram { + const schema = context.modelFactory.createSchema(context.root); + + return { + model: schema, + labelTypes: this.labelTypeRegistry.getLabelTypes(), + // TODO + constraints: [], + mode: this.editorModeController.getCurrentMode(), + version: CURRENT_VERSION + } + } + +} \ No newline at end of file diff --git a/frontend/webEditor/src/startUpAgent/SprottyInit.ts b/frontend/webEditor/src/startUpAgent/SprottyInit.ts deleted file mode 100644 index 4d426c31..00000000 --- a/frontend/webEditor/src/startUpAgent/SprottyInit.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { inject } from "inversify"; -import { IStartUpAgent } from "./StartUpAgent"; -import { LocalModelSource, TYPES } from "sprotty"; - -export class SprottyInitializerStartUpAgent implements IStartUpAgent { - constructor(@inject(TYPES.ModelSource) private modelSource: LocalModelSource) {} - - run() { - this.modelSource.setModel({ - type: "graph", - id: "root", - children: [], - }); - } -} diff --git a/frontend/webEditor/src/startUpAgent/di.config.ts b/frontend/webEditor/src/startUpAgent/di.config.ts index 53f3e36a..09473bd1 100644 --- a/frontend/webEditor/src/startUpAgent/di.config.ts +++ b/frontend/webEditor/src/startUpAgent/di.config.ts @@ -2,10 +2,10 @@ import { ContainerModule } from "inversify"; import { StartUpAgent } from "./StartUpAgent"; import { LoadDefaultUiExtensionsStartUpAgent } from "./LoadDefaultUiExtensions"; import { LoadDefaultDiagramStartUpAgent } from "./LoadDefaultDiagram"; -import { SprottyInitializerStartUpAgent } from "./SprottyInit"; +import { WebSocketConnectStartUpAgent } from "./webSocketConnect"; export const startUpAgentModule = new ContainerModule((bind) => { bind(StartUpAgent).to(LoadDefaultUiExtensionsStartUpAgent) - //bind(StartUpAgent).to(SprottyInitializerStartUpAgent) bind(StartUpAgent).to(LoadDefaultDiagramStartUpAgent) + bind(StartUpAgent).to(WebSocketConnectStartUpAgent) }) \ No newline at end of file diff --git a/frontend/webEditor/src/startUpAgent/webSocketConnect.ts b/frontend/webEditor/src/startUpAgent/webSocketConnect.ts new file mode 100644 index 00000000..e25b9e0a --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/webSocketConnect.ts @@ -0,0 +1,12 @@ +import { IStartUpAgent } from "./StartUpAgent"; +import { inject } from "inversify"; +import { DfdWebSocket } from "../webSocket/webSocket"; + +export class WebSocketConnectStartUpAgent implements IStartUpAgent { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(@inject(DfdWebSocket) _: DfdWebSocket) {} + + run(): void { + } + +} \ No newline at end of file From 10954a8d76fa9856b00fc587208691720d0ca805 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Fri, 7 Nov 2025 15:32:28 +0100 Subject: [PATCH 15/41] add file name --- frontend/webEditor/src/fileName/di.config.ts | 6 ++++++ frontend/webEditor/src/fileName/fileName.ts | 14 ++++++++++++++ frontend/webEditor/src/index.ts | 4 +++- .../webEditor/src/serialize/loadDefaultDiagram.ts | 4 +++- .../webEditor/src/serialize/loadDfdAndDdFile.ts | 13 +++++++++++-- frontend/webEditor/src/serialize/loadJson.ts | 13 +++++++------ frontend/webEditor/src/serialize/loadJsonFile.ts | 7 ++++++- .../webEditor/src/serialize/loadPalladioFile.ts | 11 +++++++++-- .../webEditor/src/serialize/saveDfdAndDdFile.ts | 6 ++++-- frontend/webEditor/src/serialize/saveJsonFile.ts | 8 +++++--- frontend/webEditor/src/webSocket/webSocket.ts | 5 +++-- 11 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 frontend/webEditor/src/fileName/di.config.ts create mode 100644 frontend/webEditor/src/fileName/fileName.ts diff --git a/frontend/webEditor/src/fileName/di.config.ts b/frontend/webEditor/src/fileName/di.config.ts new file mode 100644 index 00000000..e09aecea --- /dev/null +++ b/frontend/webEditor/src/fileName/di.config.ts @@ -0,0 +1,6 @@ +import { ContainerModule } from "inversify"; +import { FileName } from "./fileName"; + +export const fileNameModule = new ContainerModule((bind) => { + bind(FileName).toSelf().inSingletonScope(); +}) \ No newline at end of file diff --git a/frontend/webEditor/src/fileName/fileName.ts b/frontend/webEditor/src/fileName/fileName.ts new file mode 100644 index 00000000..f4fc0c46 --- /dev/null +++ b/frontend/webEditor/src/fileName/fileName.ts @@ -0,0 +1,14 @@ +export class FileName { + private name: string = 'diagram'; + + getName(): string { + return this.name; + } + + setName(newName: string): void { + const lastIndex = newName.lastIndexOf('.'); + this.name = lastIndex === -1 ? newName : newName.substring(0, lastIndex); + + document.title = this.name + '.json - DFD WebEditor'; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index b0d259c8..ff6aa15e 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -18,6 +18,7 @@ import { webSocketModule } from "./webSocket/di.config"; import { commandPaletteModule } from "./commandPalette/di.config"; import { layoutModule } from "./layout/di.config"; import { elkLayoutModule } from "sprotty-elk"; +import { fileNameModule } from "./fileName/di.config"; const container = new Container(); @@ -39,7 +40,8 @@ container.load( webSocketModule, commandPaletteModule, elkLayoutModule, - layoutModule + layoutModule, + fileNameModule ) const startUpAgents = container.getAll(StartUpAgent) diff --git a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts index 47719803..4ff59de9 100644 --- a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts +++ b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts @@ -6,6 +6,7 @@ import { inject } from "inversify"; import { TYPES, ILogger, ActionDispatcher } from "sprotty"; import { EditorModeController } from "../editorMode/EditorModeController"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { FileName } from "../fileName/fileName"; export namespace LoadDefaultDiagramAction { export const KIND = "loadDefaultDiagram"; @@ -24,8 +25,9 @@ export class LoadDefaultDiagramCommand extends LoadJsonCommand { @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, @inject(EditorModeController) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(FileName) fileName: FileName ) { - super(logger, labelTypeRegistry, editorModeController, actionDispatcher); + super(logger, labelTypeRegistry, editorModeController, actionDispatcher, fileName); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts index 66cbbf36..f856dd04 100644 --- a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts @@ -7,6 +7,7 @@ import { TYPES, ILogger, ActionDispatcher } from "sprotty"; import { EditorModeController } from "../editorMode/EditorModeController"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; +import { FileName } from "../fileName/fileName"; export namespace LoadDfdAndDdFileAction { export const KIND = "loadDfdAndDdFile"; @@ -24,10 +25,11 @@ export class LoadDfdAndDdFileCommand extends LoadJsonCommand { @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, @inject(EditorModeController) editorModeController: EditorModeController, + @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, ) { - super(logger, labelTypeRegistry, editorModeController, actionDispatcher); + super(logger, labelTypeRegistry, editorModeController, actionDispatcher, fileName); } protected async getFile(): Promise | undefined> { @@ -37,6 +39,13 @@ export class LoadDfdAndDdFileCommand extends LoadJsonCommand { if (!dataflowFileContent || !dictionaryFileContent) { return undefined; } - return this.dfdWebSocket.requestDiagram("DFD:" + dataflowFileContent + "\n:DD:\n" + dictionaryFileContent); + + const oldFileName = this.fileName.getName(); + this.fileName.setName(files[0].fileName); + + return this.dfdWebSocket.requestDiagram("DFD:" + dataflowFileContent + "\n:DD:\n" + dictionaryFileContent).catch((e) => { + this.fileName.setName(oldFileName); + throw e; + }) } } diff --git a/frontend/webEditor/src/serialize/loadJson.ts b/frontend/webEditor/src/serialize/loadJson.ts index 9dcde0a2..b3ba254d 100644 --- a/frontend/webEditor/src/serialize/loadJson.ts +++ b/frontend/webEditor/src/serialize/loadJson.ts @@ -7,6 +7,7 @@ import { Constraint } from "../constraint/Constraint"; import { EditorMode } from "../editorMode/EditorMode"; import { LabelType } from "../labels/LabelType"; import { DefaultFitToScreenAction } from "../fitToScreen/action"; +import { FileName } from "../fileName/fileName"; export interface FileData { fileName: string; @@ -34,7 +35,8 @@ export abstract class LoadJsonCommand extends Command { private readonly logger: ILogger, private readonly labelTypeRegistry: LabelTypeRegistry, private editorModeController: EditorModeController, - private actionDispatcher: ActionDispatcher + private actionDispatcher: ActionDispatcher, + protected fileName: FileName ) { super(); } @@ -79,8 +81,8 @@ export abstract class LoadJsonCommand extends Command { // TODO: post load actions like layout this.actionDispatcher.dispatch(DefaultFitToScreenAction.create(this.newRoot)) - // TODO: load file name - //this.oldFileName = currentFileName; + this.oldFileName = this.fileName.getName(); + this.fileName.setName(this.file.fileName); return this.newRoot; } catch (error) { @@ -109,7 +111,7 @@ export abstract class LoadJsonCommand extends Command { // TODO: load constraints - // TODO: load file name + this.fileName.setName(this.oldFileName ?? 'diagram'); return this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); } @@ -134,8 +136,7 @@ export abstract class LoadJsonCommand extends Command { // TODO: load constraints - // TODO: load file name - //this.oldFileName = currentFileName; + this.fileName.setName(this.file?.fileName ?? 'diagram'); return this.newRoot ?? this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); } diff --git a/frontend/webEditor/src/serialize/loadJsonFile.ts b/frontend/webEditor/src/serialize/loadJsonFile.ts index a595e354..0b43dc27 100644 --- a/frontend/webEditor/src/serialize/loadJsonFile.ts +++ b/frontend/webEditor/src/serialize/loadJsonFile.ts @@ -6,6 +6,7 @@ import { TYPES, ILogger, ActionDispatcher } from "sprotty"; import { EditorModeController } from "../editorMode/EditorModeController"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; +import { FileName } from "../fileName/fileName"; export namespace LoadJsonFileAction { export const KIND = "loadJsonFile"; @@ -25,8 +26,9 @@ export class LoadJsonFileCommand extends LoadJsonCommand { @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, @inject(EditorModeController) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(FileName) fileName: FileName, ) { - super(logger, labelTypeRegistry, editorModeController, actionDispatcher); + super(logger, labelTypeRegistry, editorModeController, actionDispatcher, fileName); } protected async getFile(): Promise | undefined> { @@ -34,6 +36,9 @@ export class LoadJsonFileCommand extends LoadJsonCommand { if (!file) { return undefined } + + this.fileName.setName(file.fileName) + return { fileName: file.fileName, content: JSON.parse(file.content) as SavedDiagram diff --git a/frontend/webEditor/src/serialize/loadPalladioFile.ts b/frontend/webEditor/src/serialize/loadPalladioFile.ts index 18dd68c3..e9270fb1 100644 --- a/frontend/webEditor/src/serialize/loadPalladioFile.ts +++ b/frontend/webEditor/src/serialize/loadPalladioFile.ts @@ -7,6 +7,7 @@ import { TYPES, ILogger, ActionDispatcher } from "sprotty"; import { EditorModeController } from "../editorMode/EditorModeController"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; +import { FileName } from "../fileName/fileName"; export namespace LoadPalladioFileAction { export const KIND = "loadPcmFile"; @@ -26,9 +27,10 @@ export class LoadPalladioFileCommand extends LoadJsonCommand { @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, @inject(EditorModeController) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, ) { - super(logger, labelTypeRegistry, editorModeController, actionDispatcher); + super(logger, labelTypeRegistry, editorModeController, actionDispatcher, fileName); } protected async getFile(): Promise | undefined> { @@ -41,7 +43,12 @@ export class LoadPalladioFileCommand extends LoadJsonCommand { ) { throw new Error("Please select one file of each required type: .pddc, .allocation, .nodecharacteristics, .repository, .resourceenvironment, .system, .usagemodel"); } + const oldFileName = this.fileName.getName(); + this.fileName.setName(files[0].fileName) - return this.dfdWebSocket.requestDiagram(files.map((f) => `${f.fileName}:${f.content}`).join("---FILE---")); + return this.dfdWebSocket.requestDiagram(files.map((f) => `${f.fileName}:${f.content}`).join("---FILE---")).catch((e) => { + this.fileName.setName(oldFileName); + throw e; + }); } } diff --git a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts index 72e61045..91284703 100644 --- a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts @@ -6,6 +6,7 @@ import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { EditorModeController } from "../editorMode/EditorModeController"; import { DfdWebSocket } from "../webSocket/webSocket"; import { Action } from "sprotty-protocol"; +import { FileName } from "../fileName/fileName"; export namespace SaveDfdAndDdFileAction { export const KIND = 'saveDfdAndDdFile' @@ -23,7 +24,8 @@ export class SaveDfdAndDdFileCommand extends SaveFileCommand { @inject(TYPES.Action) _: Action, @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, @inject(EditorModeController) editorModeController: EditorModeController, - @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket + @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, + @inject(FileName) private readonly fileName: FileName ) { super(LabelTypeRegistry, editorModeController); } @@ -36,7 +38,7 @@ export class SaveDfdAndDdFileCommand extends SaveFileCommand { const dfdContent = response.substring(0, endIndex).trim(); const ddContent = response.substring(endIndex).trim(); - const fileName = 'TODO' + const fileName = this.fileName.getName(); return Promise.resolve([{ fileName: fileName + ".dataflowdiagram", content: dfdContent diff --git a/frontend/webEditor/src/serialize/saveJsonFile.ts b/frontend/webEditor/src/serialize/saveJsonFile.ts index 1ad63495..16a2b510 100644 --- a/frontend/webEditor/src/serialize/saveJsonFile.ts +++ b/frontend/webEditor/src/serialize/saveJsonFile.ts @@ -1,10 +1,11 @@ import { CommandExecutionContext, TYPES } from "sprotty"; import { FileData } from "./loadJson"; -import { SaveFileCommand } from "./SaveFile"; +import { SaveFileCommand } from "./saveFile"; import { EditorModeController } from "../editorMode/EditorModeController"; import { inject } from "inversify"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { Action } from "sprotty-protocol"; +import { FileName } from "../fileName/fileName"; export namespace SaveJsonFileAction { export const KIND = 'saveJsonFile' @@ -19,14 +20,15 @@ export class SaveJsonFileCommand extends SaveFileCommand { constructor( @inject(TYPES.Action) _: Action, @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, - @inject(EditorModeController) editorModeController: EditorModeController + @inject(EditorModeController) editorModeController: EditorModeController, + @inject(FileName) private readonly fileName: FileName ) { super(LabelTypeRegistry, editorModeController); } getFiles(context: CommandExecutionContext): Promise[]> { const fileData: FileData = { - fileName: "TODO.json", + fileName: this.fileName.getName() + ".json", content: JSON.stringify(this.createSavedDiagram(context)) }; return Promise.resolve([fileData]); diff --git a/frontend/webEditor/src/webSocket/webSocket.ts b/frontend/webEditor/src/webSocket/webSocket.ts index 27c2a60c..49f14bc2 100644 --- a/frontend/webEditor/src/webSocket/webSocket.ts +++ b/frontend/webEditor/src/webSocket/webSocket.ts @@ -1,5 +1,6 @@ import { inject, injectable } from "inversify"; import { ILogger, TYPES } from "sprotty"; +import { FileName } from "../fileName/fileName"; @injectable() export class DfdWebSocket { @@ -12,7 +13,7 @@ export class DfdWebSocket { } = {} private static readonly WS_URL = "wss://websocket.dataflowanalysis.org/events/" - constructor(@inject(TYPES.ILogger) private readonly logger: ILogger) { + constructor(@inject(TYPES.ILogger) private readonly logger: ILogger, @inject(FileName) private readonly fileName: FileName) { this.init() } @@ -87,7 +88,7 @@ export class DfdWebSocket { return result } - this.webSocket.send(this.webSocketId + ":" + "TODO: DIAGRAM NAME" + ":" + message) + this.webSocket.send(this.webSocketId + ":" + this.fileName.getName() + ":" + message) return result } From 492da26873a40bddc04356d0c68a2f60ed010197 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Fri, 14 Nov 2025 12:48:54 +0100 Subject: [PATCH 16/41] simple settings implementation --- .../webEditor/src/editorMode/EditorMode.ts | 1 - .../src/editorMode/EditorModeController.ts | 39 -------- .../webEditor/src/editorMode/di.config.ts | 6 -- frontend/webEditor/src/index.ts | 6 +- .../src/serialize/loadDefaultDiagram.ts | 5 +- .../src/serialize/loadDfdAndDdFile.ts | 5 +- frontend/webEditor/src/serialize/loadJson.ts | 19 ++-- .../webEditor/src/serialize/loadJsonFile.ts | 5 +- .../src/serialize/loadPalladioFile.ts | 5 +- .../src/serialize/saveDfdAndDdFile.ts | 7 +- .../webEditor/src/serialize/saveJsonFile.ts | 5 +- .../src/serialize/savedDiagramCreator.ts | 4 +- frontend/webEditor/src/settings/Settings.ts | 12 +++ frontend/webEditor/src/settings/SettingsUi.ts | 74 ++++++++++++++ .../webEditor/src/settings/SettingsValue.ts | 28 ++++++ frontend/webEditor/src/settings/Theme.ts | 6 ++ frontend/webEditor/src/settings/di.config.ts | 17 ++++ frontend/webEditor/src/settings/editorMode.ts | 19 ++++ frontend/webEditor/src/settings/initialize.ts | 26 +++++ .../webEditor/src/settings/settingsUi.css | 98 +++++++++++++++++++ .../webEditor/src/startUpAgent/di.config.ts | 2 + .../src/startUpAgent/settingsInit.ts | 16 +++ 22 files changed, 331 insertions(+), 74 deletions(-) delete mode 100644 frontend/webEditor/src/editorMode/EditorMode.ts delete mode 100644 frontend/webEditor/src/editorMode/EditorModeController.ts delete mode 100644 frontend/webEditor/src/editorMode/di.config.ts create mode 100644 frontend/webEditor/src/settings/Settings.ts create mode 100644 frontend/webEditor/src/settings/SettingsUi.ts create mode 100644 frontend/webEditor/src/settings/SettingsValue.ts create mode 100644 frontend/webEditor/src/settings/Theme.ts create mode 100644 frontend/webEditor/src/settings/di.config.ts create mode 100644 frontend/webEditor/src/settings/editorMode.ts create mode 100644 frontend/webEditor/src/settings/initialize.ts create mode 100644 frontend/webEditor/src/settings/settingsUi.css create mode 100644 frontend/webEditor/src/startUpAgent/settingsInit.ts diff --git a/frontend/webEditor/src/editorMode/EditorMode.ts b/frontend/webEditor/src/editorMode/EditorMode.ts deleted file mode 100644 index 73c47884..00000000 --- a/frontend/webEditor/src/editorMode/EditorMode.ts +++ /dev/null @@ -1 +0,0 @@ -export type EditorMode = "edit" | "view"; \ No newline at end of file diff --git a/frontend/webEditor/src/editorMode/EditorModeController.ts b/frontend/webEditor/src/editorMode/EditorModeController.ts deleted file mode 100644 index b25b8879..00000000 --- a/frontend/webEditor/src/editorMode/EditorModeController.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { injectable } from "inversify"; -import { EditorMode } from "./EditorMode"; - -/** - * Holds the current editor mode in a central place. - * Used to get the current mode in places where it is used. - * - * Changes to the mode should be done using the ChangeEditorModeCommand - * and not directly on this class when done interactively - * for undo/redo support and actions that are done to the model - * when the mode changes. - */ -@injectable() -export class EditorModeController { - private mode: EditorMode = "edit"; - private modeChangeCallbacks: ((mode: EditorMode) => void)[] = []; - - getCurrentMode(): EditorMode { - return this.mode; - } - - setMode(mode: EditorMode) { - this.mode = mode; - - this.modeChangeCallbacks.forEach((callback) => callback(mode)); - } - - setDefaultMode() { - this.mode = "edit"; - } - - onModeChange(callback: (mode: EditorMode) => void) { - this.modeChangeCallbacks.push(callback); - } - - isReadOnly(): boolean { - return this.mode !== "edit"; - } -} diff --git a/frontend/webEditor/src/editorMode/di.config.ts b/frontend/webEditor/src/editorMode/di.config.ts deleted file mode 100644 index 8e8c067a..00000000 --- a/frontend/webEditor/src/editorMode/di.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ContainerModule } from "inversify"; -import { EditorModeController } from "./EditorModeController"; - -export const editorModeModule = new ContainerModule((bind) => { - bind(EditorModeController).toSelf().inSingletonScope(); -}) \ No newline at end of file diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index ff6aa15e..22c240e9 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -12,13 +12,13 @@ import { startUpAgentModule } from "./startUpAgent/di.config"; import { commonModule } from "./commonModule"; import { labelModule } from "./labels/di.config"; import { serializeModule } from "./serialize/di.config"; -import { editorModeModule } from "./editorMode/di.config"; import { diagramModule } from "./diagram/di.config"; import { webSocketModule } from "./webSocket/di.config"; import { commandPaletteModule } from "./commandPalette/di.config"; import { layoutModule } from "./layout/di.config"; import { elkLayoutModule } from "sprotty-elk"; import { fileNameModule } from "./fileName/di.config"; +import { settingsModule } from "./settings/di.config"; const container = new Container(); @@ -34,14 +34,14 @@ container.load( commonModule, startUpAgentModule, labelModule, - editorModeModule, diagramModule, serializeModule, webSocketModule, commandPaletteModule, elkLayoutModule, layoutModule, - fileNameModule + fileNameModule, + settingsModule ) const startUpAgents = container.getAll(StartUpAgent) diff --git a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts index 4ff59de9..3e532fb2 100644 --- a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts +++ b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts @@ -4,9 +4,10 @@ import { SavedDiagram } from "./SavedDiagram"; import { Action } from "sprotty-protocol"; import { inject } from "inversify"; import { TYPES, ILogger, ActionDispatcher } from "sprotty"; -import { EditorModeController } from "../editorMode/EditorModeController"; +import { EditorModeController } from "../settings/editorMode"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; export namespace LoadDefaultDiagramAction { export const KIND = "loadDefaultDiagram"; @@ -23,7 +24,7 @@ export class LoadDefaultDiagramCommand extends LoadJsonCommand { @inject(TYPES.Action) _: Action, @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, - @inject(EditorModeController) editorModeController: EditorModeController, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(FileName) fileName: FileName ) { diff --git a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts index f856dd04..7bc2bab8 100644 --- a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts @@ -4,10 +4,11 @@ import { chooseFiles } from "./fileChooser"; import { inject } from "inversify"; import { DfdWebSocket } from "../webSocket/webSocket"; import { TYPES, ILogger, ActionDispatcher } from "sprotty"; -import { EditorModeController } from "../editorMode/EditorModeController"; +import { EditorModeController } from "../settings/editorMode"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; export namespace LoadDfdAndDdFileAction { export const KIND = "loadDfdAndDdFile"; @@ -24,7 +25,7 @@ export class LoadDfdAndDdFileCommand extends LoadJsonCommand { @inject(TYPES.Action) _: Action, @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, - @inject(EditorModeController) editorModeController: EditorModeController, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, diff --git a/frontend/webEditor/src/serialize/loadJson.ts b/frontend/webEditor/src/serialize/loadJson.ts index b3ba254d..68e88b53 100644 --- a/frontend/webEditor/src/serialize/loadJson.ts +++ b/frontend/webEditor/src/serialize/loadJson.ts @@ -2,9 +2,8 @@ import { ActionDispatcher, Command, CommandExecutionContext, CommandReturn, EMPT import { SavedDiagram } from "./SavedDiagram"; import { Action, SModelElement, SModelRoot } from "sprotty-protocol"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; -import { EditorModeController } from "../editorMode/EditorModeController"; +import { EditorModeController, EditorMode } from "../settings/editorMode"; import { Constraint } from "../constraint/Constraint"; -import { EditorMode } from "../editorMode/EditorMode"; import { LabelType } from "../labels/LabelType"; import { DefaultFitToScreenAction } from "../fitToScreen/action"; import { FileName } from "../fileName/fileName"; @@ -67,12 +66,12 @@ export abstract class LoadJsonCommand extends Command { this.labelTypeRegistry.clearLabelTypes(); } - this.oldEditorMode = this.editorModeController.getCurrentMode(); + this.oldEditorMode = this.editorModeController.get(); const newEditorMode = this.file.content.mode; if (newEditorMode) { - this.editorModeController.setMode(newEditorMode); + this.editorModeController.set(newEditorMode); } else { - this.editorModeController.setDefaultMode(); + this.editorModeController.setDefault(); } this.logger.info(this, "Editor mode loaded successfully"); @@ -100,13 +99,13 @@ export abstract class LoadJsonCommand extends Command { } if (this.oldEditorMode) { - this.editorModeController.setMode(this.oldEditorMode); + this.editorModeController.set(this.oldEditorMode); } else { - this.editorModeController.setDefaultMode(); + this.editorModeController.setDefault(); } if (this.oldEditorMode) { - this.editorModeController?.setMode(this.oldEditorMode); + this.editorModeController.set(this.oldEditorMode); } // TODO: load constraints @@ -128,9 +127,9 @@ export abstract class LoadJsonCommand extends Command { const newEditorMode = this.file?.content.mode; if (newEditorMode) { - this.editorModeController.setMode(newEditorMode); + this.editorModeController.set(newEditorMode); } else { - this.editorModeController.setDefaultMode(); + this.editorModeController.setDefault(); } this.logger.info(this, "Editor mode loaded successfully"); diff --git a/frontend/webEditor/src/serialize/loadJsonFile.ts b/frontend/webEditor/src/serialize/loadJsonFile.ts index 0b43dc27..0bb7127d 100644 --- a/frontend/webEditor/src/serialize/loadJsonFile.ts +++ b/frontend/webEditor/src/serialize/loadJsonFile.ts @@ -3,10 +3,11 @@ import { FileData, LoadJsonCommand } from "./loadJson"; import { chooseFile } from "./fileChooser"; import { inject } from "inversify"; import { TYPES, ILogger, ActionDispatcher } from "sprotty"; -import { EditorModeController } from "../editorMode/EditorModeController"; +import { EditorModeController } from "../settings/editorMode"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; export namespace LoadJsonFileAction { export const KIND = "loadJsonFile"; @@ -24,7 +25,7 @@ export class LoadJsonFileCommand extends LoadJsonCommand { @inject(TYPES.Action) _: Action, @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, - @inject(EditorModeController) editorModeController: EditorModeController, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(FileName) fileName: FileName, ) { diff --git a/frontend/webEditor/src/serialize/loadPalladioFile.ts b/frontend/webEditor/src/serialize/loadPalladioFile.ts index e9270fb1..01445637 100644 --- a/frontend/webEditor/src/serialize/loadPalladioFile.ts +++ b/frontend/webEditor/src/serialize/loadPalladioFile.ts @@ -4,10 +4,11 @@ import { chooseFiles } from "./fileChooser"; import { inject } from "inversify"; import { DfdWebSocket } from "../webSocket/webSocket"; import { TYPES, ILogger, ActionDispatcher } from "sprotty"; -import { EditorModeController } from "../editorMode/EditorModeController"; +import { EditorModeController } from "../settings/editorMode"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; export namespace LoadPalladioFileAction { export const KIND = "loadPcmFile"; @@ -25,7 +26,7 @@ export class LoadPalladioFileCommand extends LoadJsonCommand { @inject(TYPES.Action) _: Action, @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, - @inject(EditorModeController) editorModeController: EditorModeController, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, diff --git a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts index 91284703..757e523c 100644 --- a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts @@ -1,12 +1,13 @@ import { CommandExecutionContext, TYPES } from "sprotty"; import { FileData } from "./loadJson"; -import { SaveFileCommand } from "./SaveFile"; +import { SaveFileCommand } from "./saveFile"; import { inject } from "inversify"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; -import { EditorModeController } from "../editorMode/EditorModeController"; +import { EditorModeController } from "../settings/editorMode"; import { DfdWebSocket } from "../webSocket/webSocket"; import { Action } from "sprotty-protocol"; import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; export namespace SaveDfdAndDdFileAction { export const KIND = 'saveDfdAndDdFile' @@ -23,7 +24,7 @@ export class SaveDfdAndDdFileCommand extends SaveFileCommand { constructor( @inject(TYPES.Action) _: Action, @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, - @inject(EditorModeController) editorModeController: EditorModeController, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, @inject(FileName) private readonly fileName: FileName ) { diff --git a/frontend/webEditor/src/serialize/saveJsonFile.ts b/frontend/webEditor/src/serialize/saveJsonFile.ts index 16a2b510..6a474b43 100644 --- a/frontend/webEditor/src/serialize/saveJsonFile.ts +++ b/frontend/webEditor/src/serialize/saveJsonFile.ts @@ -1,11 +1,12 @@ import { CommandExecutionContext, TYPES } from "sprotty"; import { FileData } from "./loadJson"; import { SaveFileCommand } from "./saveFile"; -import { EditorModeController } from "../editorMode/EditorModeController"; +import { EditorModeController } from "../settings/editorMode"; import { inject } from "inversify"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { Action } from "sprotty-protocol"; import { FileName } from "../fileName/fileName"; +import { SETTINGS } from "../settings/Settings"; export namespace SaveJsonFileAction { export const KIND = 'saveJsonFile' @@ -20,7 +21,7 @@ export class SaveJsonFileCommand extends SaveFileCommand { constructor( @inject(TYPES.Action) _: Action, @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, - @inject(EditorModeController) editorModeController: EditorModeController, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(FileName) private readonly fileName: FileName ) { super(LabelTypeRegistry, editorModeController); diff --git a/frontend/webEditor/src/serialize/savedDiagramCreator.ts b/frontend/webEditor/src/serialize/savedDiagramCreator.ts index a91a45a2..860d6fb0 100644 --- a/frontend/webEditor/src/serialize/savedDiagramCreator.ts +++ b/frontend/webEditor/src/serialize/savedDiagramCreator.ts @@ -1,7 +1,7 @@ import { Command, CommandExecutionContext } from "sprotty"; import { CURRENT_VERSION, SavedDiagram } from "./SavedDiagram"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; -import { EditorModeController } from "../editorMode/EditorModeController"; +import { EditorModeController } from "../settings/editorMode"; export abstract class SavedDiagramCreatorCommand extends Command { @@ -20,7 +20,7 @@ export abstract class SavedDiagramCreatorCommand extends Command { labelTypes: this.labelTypeRegistry.getLabelTypes(), // TODO constraints: [], - mode: this.editorModeController.getCurrentMode(), + mode: this.editorModeController.get(), version: CURRENT_VERSION } } diff --git a/frontend/webEditor/src/settings/Settings.ts b/frontend/webEditor/src/settings/Settings.ts new file mode 100644 index 00000000..1decf1c3 --- /dev/null +++ b/frontend/webEditor/src/settings/Settings.ts @@ -0,0 +1,12 @@ +import { BoolSettingsValue } from "./SettingsValue" + +export const SETTINGS = { + Theme: Symbol("Theme"), + Mode: Symbol("EditorMode"), + HideEdgeNames: Symbol("HideEdgeNames"), + SimplifyNodeNames: Symbol("SimplifyNodeNames"), + ShownLabels: Symbol("ShownLabels"), +} + +export type SimplifyNodeNames = BoolSettingsValue +export type HideEdgeNames = BoolSettingsValue \ No newline at end of file diff --git a/frontend/webEditor/src/settings/SettingsUi.ts b/frontend/webEditor/src/settings/SettingsUi.ts new file mode 100644 index 00000000..6e27aad1 --- /dev/null +++ b/frontend/webEditor/src/settings/SettingsUi.ts @@ -0,0 +1,74 @@ +import { inject, injectable } from "inversify"; +import "./settingsUi.css"; +import { SettingsValue } from "./SettingsValue"; +import { AccordionUiExtension } from "../accordionUiExtension"; +import { HideEdgeNames, SETTINGS, SimplifyNodeNames } from "./Settings"; +import { EditorModeController } from "./editorMode"; + +@injectable() +export class SettingsUI extends AccordionUiExtension { + static readonly ID = "settings-ui"; + + constructor(@inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, + @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, +@inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController) { + super('right', 'up') + } + + id(): string { + return SettingsUI.ID; + } + + containerClass(): string { + return SettingsUI.ID; + } + + protected initializeHidableContent(contentElement: HTMLElement): void { + const grid = document.createElement('div'); + grid.id = 'settings-content' + contentElement.appendChild(grid); + this.addBooleanSwitch(grid, "Hide Edge Names", this.hideEdgeNames); + this.addBooleanSwitch(grid, "Simplify Node Names", this.simplifyNodeNames); + this.addSwitch(grid, "Read Only", this.editorModeController, {true: "view", false: "edit"}); + } + + protected initializeHeaderContent(headerElement: HTMLElement): void { + headerElement.classList.add('settings-accordion-icon'); + headerElement.innerText = 'Settings' + } + + private addBooleanSwitch(container: HTMLElement, title: string, value: SettingsValue): void { + this.addSwitch(container, title, value, {true: true, false: false}); + } + + private addSwitch(container: HTMLElement, title: string, value: SettingsValue, map: {'true':T, 'false': T}): void { + const inversedMap = { + [map.true.toString()]: true, + [map.false.toString()]: false + }; + const textLabel = document.createElement("label"); + textLabel.textContent = title; + textLabel.htmlFor = `setting-${title.toLowerCase().replace(/\s+/g, '-')}`; + + const switchLabel = document.createElement("label"); + switchLabel.classList.add("switch"); + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.id = `setting-${title.toLowerCase().replace(/\s+/g, '-')}`; + checkbox.checked = inversedMap[value.get().toString()]; + switchLabel.appendChild(checkbox); + const sliderSpan = document.createElement("span"); + sliderSpan.classList.add("slider", "round"); + switchLabel.appendChild(sliderSpan); + + container.appendChild(textLabel); + container.appendChild(switchLabel); + + switchLabel.addEventListener("change", () => { + value.set(map[checkbox.checked ? 'true' : 'false']); + }); + value.registerListener((newValue) => { + checkbox.checked = inversedMap[newValue.toString()]; + }); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/settings/SettingsValue.ts b/frontend/webEditor/src/settings/SettingsValue.ts new file mode 100644 index 00000000..e8b02605 --- /dev/null +++ b/frontend/webEditor/src/settings/SettingsValue.ts @@ -0,0 +1,28 @@ +export class SettingsValue { + private value: T; + private listeners: Array<(newValue: T) => void> = []; + + constructor(initialValue: T) { + this.value = initialValue; + } + + get(): T { + return this.value; + } + + set(newValue: T): void { + this.value = newValue; + this.listeners.forEach(listener => listener(newValue)); + } + + registerListener(listener: (newValue: T) => void): void { + this.listeners.push(listener); + } + +} + +export class BoolSettingsValue extends SettingsValue { + constructor(initialValue: boolean = false) { + super(initialValue); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/settings/Theme.ts b/frontend/webEditor/src/settings/Theme.ts new file mode 100644 index 00000000..87c9333c --- /dev/null +++ b/frontend/webEditor/src/settings/Theme.ts @@ -0,0 +1,6 @@ + +export enum Theme { + LIGHT = "Light", + DARK = "Dark", + SYSTEM_DEFAULT = "System Default", +} \ No newline at end of file diff --git a/frontend/webEditor/src/settings/di.config.ts b/frontend/webEditor/src/settings/di.config.ts new file mode 100644 index 00000000..b821b411 --- /dev/null +++ b/frontend/webEditor/src/settings/di.config.ts @@ -0,0 +1,17 @@ +import { ContainerModule } from "inversify"; +import { SettingsUI } from "./SettingsUi"; +import { EDITOR_TYPES } from "../editorTypes"; +import { SETTINGS } from "./Settings"; +import { BoolSettingsValue } from "./SettingsValue"; +import { TYPES } from "sprotty"; +import { EditorModeController } from "./editorMode"; + +export const settingsModule = new ContainerModule((bind) => { + bind(SettingsUI).toSelf().inSingletonScope(); + bind(EDITOR_TYPES.DefaultUIElement).toService(SettingsUI) + bind(TYPES.IUIExtension).toService(SettingsUI); + + bind(SETTINGS.HideEdgeNames).to(BoolSettingsValue).inSingletonScope(); + bind(SETTINGS.SimplifyNodeNames).to(BoolSettingsValue).inSingletonScope(); + bind(SETTINGS.Mode).to(EditorModeController).inSingletonScope(); +}) \ No newline at end of file diff --git a/frontend/webEditor/src/settings/editorMode.ts b/frontend/webEditor/src/settings/editorMode.ts new file mode 100644 index 00000000..10532758 --- /dev/null +++ b/frontend/webEditor/src/settings/editorMode.ts @@ -0,0 +1,19 @@ +import { SettingsValue } from "./SettingsValue"; + +export type EditorMode = "edit" | "view"; + + +export class EditorModeController extends SettingsValue { + + constructor() { + super("edit"); + } + + setDefault(): void { + this.set("edit"); + } + + isReadOnly(): boolean { + return this.get() !== "edit"; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/settings/initialize.ts b/frontend/webEditor/src/settings/initialize.ts new file mode 100644 index 00000000..66c7215e --- /dev/null +++ b/frontend/webEditor/src/settings/initialize.ts @@ -0,0 +1,26 @@ +import { EditorModeController } from "./editorMode"; +import { HideEdgeNames, SimplifyNodeNames } from "./Settings"; + +export function linkReadOnly( + editorModeController: EditorModeController, + simplifyNodeNames: SimplifyNodeNames, + hideEdgeNames: HideEdgeNames +): void { + editorModeController.registerListener(() => { + if(!editorModeController.isReadOnly()) { + simplifyNodeNames.set(false); + hideEdgeNames.set(false); + } + }); + + simplifyNodeNames.registerListener((newValue) => { + if(newValue) { + editorModeController.set("view"); + } + }); + hideEdgeNames.registerListener((newValue) => { + if(newValue) { + editorModeController.set("view"); + } + }); +} \ No newline at end of file diff --git a/frontend/webEditor/src/settings/settingsUi.css b/frontend/webEditor/src/settings/settingsUi.css new file mode 100644 index 00000000..6a8a810f --- /dev/null +++ b/frontend/webEditor/src/settings/settingsUi.css @@ -0,0 +1,98 @@ +div.settings-ui { + left: 20px; + bottom: 70px; + padding: 10px 10px; +} + +#settings-content { + display: grid; + gap: 8px 6px; + + align-items: center; +} + +#settings-content > label { + grid-column-start: 1; +} + +#settings-content > input, +#settings-content > select, +#settings-content > label.switch { + grid-column-start: 2; +} + +#settings-content select { + background-color: var(--color-background); + color: var(--color-foreground); + border: 1px solid var(--color-foreground); + border-radius: 6px; +} + +.switch input:disabled + .slider { + background-color: color-mix(in srgb, var(--color-primary) 50%, #555 50%); +} + +.switch input:disabled + .slider:before { + background-color: color-mix(in srgb, var(--color-background) 50%, #555 50%); +} + +/* https://www.w3schools.com/HOWTO/howto_css_switch.asp */ +/* The switch - the box around the slider */ +.switch { + position: relative; + display: inline-block; + width: 30px; + height: 17px; +} + +/* Hide default HTML checkbox */ +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +/* The slider */ +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--color-background); + -webkit-transition: 0.4s; + transition: 0.4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 13px; + width: 13px; + left: 2px; + bottom: 2px; + background-color: var(--color-primary); + -webkit-transition: 0.3s; + transition: 0.3s; +} + +input:checked + .slider { + background-color: var(--color-background); +} + +input:checked + .slider:before { + -webkit-transform: translateX(13px); + -ms-transform: translateX(13px); + transform: translateX(13px); + background-color: var(--color-foreground); +} + +/* Rounded sliders */ +.slider.round { + border-radius: 17px; +} + +.slider.round:before { + border-radius: 50%; +} \ No newline at end of file diff --git a/frontend/webEditor/src/startUpAgent/di.config.ts b/frontend/webEditor/src/startUpAgent/di.config.ts index 09473bd1..4b1f6acd 100644 --- a/frontend/webEditor/src/startUpAgent/di.config.ts +++ b/frontend/webEditor/src/startUpAgent/di.config.ts @@ -3,9 +3,11 @@ import { StartUpAgent } from "./StartUpAgent"; import { LoadDefaultUiExtensionsStartUpAgent } from "./LoadDefaultUiExtensions"; import { LoadDefaultDiagramStartUpAgent } from "./LoadDefaultDiagram"; import { WebSocketConnectStartUpAgent } from "./webSocketConnect"; +import { SettingsInitStartUpAgent } from "./settingsInit"; export const startUpAgentModule = new ContainerModule((bind) => { bind(StartUpAgent).to(LoadDefaultUiExtensionsStartUpAgent) bind(StartUpAgent).to(LoadDefaultDiagramStartUpAgent) bind(StartUpAgent).to(WebSocketConnectStartUpAgent) + bind(StartUpAgent).to(SettingsInitStartUpAgent) }) \ No newline at end of file diff --git a/frontend/webEditor/src/startUpAgent/settingsInit.ts b/frontend/webEditor/src/startUpAgent/settingsInit.ts new file mode 100644 index 00000000..11368577 --- /dev/null +++ b/frontend/webEditor/src/startUpAgent/settingsInit.ts @@ -0,0 +1,16 @@ +import { IStartUpAgent } from "./StartUpAgent"; +import { inject } from "inversify"; +import { linkReadOnly } from "../settings/initialize"; +import { EditorModeController } from "../settings/editorMode"; +import { SETTINGS, HideEdgeNames, SimplifyNodeNames } from "../settings/Settings"; + +export class SettingsInitStartUpAgent implements IStartUpAgent { + constructor(@inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, + @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController) {} + + run(): void { + linkReadOnly(this.editorModeController, this.simplifyNodeNames, this.hideEdgeNames); + } + +} \ No newline at end of file From d67b302458dcae632fd48fff3e7a751890837d3d Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 14 Nov 2025 13:11:49 +0100 Subject: [PATCH 17/41] add theme switch --- frontend/webEditor/src/settings/SettingsUi.ts | 37 ++++++++++++++++++- frontend/webEditor/src/settings/Theme.ts | 33 +++++++++++++++++ frontend/webEditor/src/settings/di.config.ts | 2 + .../src/startUpAgent/settingsInit.ts | 4 +- 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/frontend/webEditor/src/settings/SettingsUi.ts b/frontend/webEditor/src/settings/SettingsUi.ts index 6e27aad1..c9e6c621 100644 --- a/frontend/webEditor/src/settings/SettingsUi.ts +++ b/frontend/webEditor/src/settings/SettingsUi.ts @@ -4,12 +4,15 @@ import { SettingsValue } from "./SettingsValue"; import { AccordionUiExtension } from "../accordionUiExtension"; import { HideEdgeNames, SETTINGS, SimplifyNodeNames } from "./Settings"; import { EditorModeController } from "./editorMode"; +import { Theme, ThemeManager } from "./Theme"; @injectable() export class SettingsUI extends AccordionUiExtension { static readonly ID = "settings-ui"; - constructor(@inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, + constructor( + @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, + @inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController) { super('right', 'up') @@ -27,6 +30,7 @@ export class SettingsUI extends AccordionUiExtension { const grid = document.createElement('div'); grid.id = 'settings-content' contentElement.appendChild(grid); + this.addDropDown(grid, "Theme", this.themeManager, [Theme.SYSTEM_DEFAULT, Theme.LIGHT, Theme.DARK]) this.addBooleanSwitch(grid, "Hide Edge Names", this.hideEdgeNames); this.addBooleanSwitch(grid, "Simplify Node Names", this.simplifyNodeNames); this.addSwitch(grid, "Read Only", this.editorModeController, {true: "view", false: "edit"}); @@ -41,7 +45,7 @@ export class SettingsUI extends AccordionUiExtension { this.addSwitch(container, title, value, {true: true, false: false}); } - private addSwitch(container: HTMLElement, title: string, value: SettingsValue, map: {'true':T, 'false': T}): void { + private addSwitch(container: HTMLElement, title: string, value: SettingsValue, map: {'true':T, 'false': T}): void { const inversedMap = { [map.true.toString()]: true, [map.false.toString()]: false @@ -71,4 +75,33 @@ export class SettingsUI extends AccordionUiExtension { checkbox.checked = inversedMap[newValue.toString()]; }); } + + private addDropDown(container: HTMLElement, title: string, value: SettingsValue, values: T[]) { + const textLabel = document.createElement("label"); + textLabel.textContent = title; + textLabel.htmlFor = `setting-${title.toLowerCase().replace(/\s+/g, '-')}`; + + const dropDown = document.createElement('select') + for (const v of values) { + const option = document.createElement('option') + option.value = v.toString() + option.innerText = v.toString() + dropDown.appendChild(option) + } + dropDown.value = value.get().toString() + + dropDown.onchange = () => { + const newValue = values.find(v => v.toString() === dropDown.value) + if (newValue) { + value.set(newValue) + } + } + + container.appendChild(textLabel) + container.appendChild(dropDown) + } +} + +interface ToString { + toString: () => string } \ No newline at end of file diff --git a/frontend/webEditor/src/settings/Theme.ts b/frontend/webEditor/src/settings/Theme.ts index 87c9333c..98c92f1a 100644 --- a/frontend/webEditor/src/settings/Theme.ts +++ b/frontend/webEditor/src/settings/Theme.ts @@ -1,6 +1,39 @@ +import { SettingsValue } from "./SettingsValue"; export enum Theme { LIGHT = "Light", DARK = "Dark", SYSTEM_DEFAULT = "System Default", +} + +type ApplyableTheme = Theme.LIGHT | Theme.DARK + +export class ThemeManager extends SettingsValue { + private static SYSTEM_DEFAULT: ApplyableTheme = + window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? Theme.DARK : Theme.LIGHT; + public static readonly LOCAL_STORAGE_KEY = "dfdwebeditor:theme"; + + constructor() { + super((localStorage.getItem(ThemeManager.LOCAL_STORAGE_KEY) ?? ThemeManager.SYSTEM_DEFAULT) as Theme) + } + + getTheme(): ApplyableTheme { + const value = this.get() + if (value === Theme.SYSTEM_DEFAULT) { + return ThemeManager.SYSTEM_DEFAULT + } + return value + } +} + +export function registerThemeSwitch(themeManager: ThemeManager) { + themeManager.registerListener(() => { + const rootElement = document.querySelector(":root") as HTMLElement; + const sprottyElement = document.querySelector("#sprotty") as HTMLElement; + + const value = themeManager.getTheme() === Theme.DARK ? "dark" : "light"; + rootElement.setAttribute("data-theme", value); + sprottyElement.setAttribute("data-theme", value); + localStorage.setItem(ThemeManager.LOCAL_STORAGE_KEY, themeManager.get()) + }) } \ No newline at end of file diff --git a/frontend/webEditor/src/settings/di.config.ts b/frontend/webEditor/src/settings/di.config.ts index b821b411..b9538afb 100644 --- a/frontend/webEditor/src/settings/di.config.ts +++ b/frontend/webEditor/src/settings/di.config.ts @@ -5,12 +5,14 @@ import { SETTINGS } from "./Settings"; import { BoolSettingsValue } from "./SettingsValue"; import { TYPES } from "sprotty"; import { EditorModeController } from "./editorMode"; +import { ThemeManager } from "./Theme"; export const settingsModule = new ContainerModule((bind) => { bind(SettingsUI).toSelf().inSingletonScope(); bind(EDITOR_TYPES.DefaultUIElement).toService(SettingsUI) bind(TYPES.IUIExtension).toService(SettingsUI); + bind(SETTINGS.Theme).to(ThemeManager).inSingletonScope() bind(SETTINGS.HideEdgeNames).to(BoolSettingsValue).inSingletonScope(); bind(SETTINGS.SimplifyNodeNames).to(BoolSettingsValue).inSingletonScope(); bind(SETTINGS.Mode).to(EditorModeController).inSingletonScope(); diff --git a/frontend/webEditor/src/startUpAgent/settingsInit.ts b/frontend/webEditor/src/startUpAgent/settingsInit.ts index 11368577..24e4f6bd 100644 --- a/frontend/webEditor/src/startUpAgent/settingsInit.ts +++ b/frontend/webEditor/src/startUpAgent/settingsInit.ts @@ -3,14 +3,16 @@ import { inject } from "inversify"; import { linkReadOnly } from "../settings/initialize"; import { EditorModeController } from "../settings/editorMode"; import { SETTINGS, HideEdgeNames, SimplifyNodeNames } from "../settings/Settings"; +import { registerThemeSwitch, ThemeManager } from "../settings/Theme"; export class SettingsInitStartUpAgent implements IStartUpAgent { - constructor(@inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, + constructor(@inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, @inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController) {} run(): void { linkReadOnly(this.editorModeController, this.simplifyNodeNames, this.hideEdgeNames); + registerThemeSwitch(this.themeManager) } } \ No newline at end of file From 19e6aa385fc4535c710557d6d78536640c72ee11 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 14 Nov 2025 13:16:13 +0100 Subject: [PATCH 18/41] add settings icon --- .../webEditor/src/settings/settingsUi.css | 105 ++++++++++-------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/frontend/webEditor/src/settings/settingsUi.css b/frontend/webEditor/src/settings/settingsUi.css index 6a8a810f..e1edcd9f 100644 --- a/frontend/webEditor/src/settings/settingsUi.css +++ b/frontend/webEditor/src/settings/settingsUi.css @@ -1,98 +1,109 @@ div.settings-ui { - left: 20px; - bottom: 70px; - padding: 10px 10px; + left: 20px; + bottom: 70px; + padding: 10px 10px; +} + +.settings-accordion-icon::before { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/solid/gear.svg"); + display: inline-block; + filter: invert(var(--dark-mode)); + height: 16px; + width: 16px; + background-size: 16px 16px; + vertical-align: text-top; } #settings-content { - display: grid; - gap: 8px 6px; + display: grid; + gap: 8px 6px; - align-items: center; + align-items: center; } #settings-content > label { - grid-column-start: 1; + grid-column-start: 1; } #settings-content > input, #settings-content > select, #settings-content > label.switch { - grid-column-start: 2; + grid-column-start: 2; } #settings-content select { - background-color: var(--color-background); - color: var(--color-foreground); - border: 1px solid var(--color-foreground); - border-radius: 6px; + background-color: var(--color-background); + color: var(--color-foreground); + border: 1px solid var(--color-foreground); + border-radius: 6px; } .switch input:disabled + .slider { - background-color: color-mix(in srgb, var(--color-primary) 50%, #555 50%); + background-color: color-mix(in srgb, var(--color-primary) 50%, #555 50%); } .switch input:disabled + .slider:before { - background-color: color-mix(in srgb, var(--color-background) 50%, #555 50%); + background-color: color-mix(in srgb, var(--color-background) 50%, #555 50%); } /* https://www.w3schools.com/HOWTO/howto_css_switch.asp */ /* The switch - the box around the slider */ .switch { - position: relative; - display: inline-block; - width: 30px; - height: 17px; + position: relative; + display: inline-block; + width: 30px; + height: 17px; } /* Hide default HTML checkbox */ .switch input { - opacity: 0; - width: 0; - height: 0; + opacity: 0; + width: 0; + height: 0; } /* The slider */ .slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--color-background); - -webkit-transition: 0.4s; - transition: 0.4s; + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--color-background); + -webkit-transition: 0.4s; + transition: 0.4s; } .slider:before { - position: absolute; - content: ""; - height: 13px; - width: 13px; - left: 2px; - bottom: 2px; - background-color: var(--color-primary); - -webkit-transition: 0.3s; - transition: 0.3s; + position: absolute; + content: ""; + height: 13px; + width: 13px; + left: 2px; + bottom: 2px; + background-color: var(--color-primary); + -webkit-transition: 0.3s; + transition: 0.3s; } input:checked + .slider { - background-color: var(--color-background); + background-color: var(--color-background); } input:checked + .slider:before { - -webkit-transform: translateX(13px); - -ms-transform: translateX(13px); - transform: translateX(13px); - background-color: var(--color-foreground); + -webkit-transform: translateX(13px); + -ms-transform: translateX(13px); + transform: translateX(13px); + background-color: var(--color-foreground); } /* Rounded sliders */ .slider.round { - border-radius: 17px; + border-radius: 17px; } .slider.round:before { - border-radius: 50%; -} \ No newline at end of file + border-radius: 50%; +} From d38f225371ed793c71ff60645fbcf9cce129f67b Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Mon, 17 Nov 2025 13:34:18 +0100 Subject: [PATCH 19/41] editable labels --- frontend/webEditor/src/diagram/di.config.ts | 16 +++++- .../src/diagram/labels/EditLabelDecorator.ts | 38 +++++++++++++ .../src/diagram/labels/EditLabelValidator.ts | 57 +++++++++++++++++++ .../diagram/labels/FilledBackgroundLabel.tsx | 32 +++++++++++ .../src/diagram/labels/NoScrollEditLabelUI.ts | 30 ++++++++++ .../src/diagram/labels/editLabelDecorator.css | 26 +++++++++ .../webEditor/src/serialize/ModelFactory.ts | 45 +++++++++++++-- 7 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts create mode 100644 frontend/webEditor/src/diagram/labels/EditLabelValidator.ts create mode 100644 frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx create mode 100644 frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts create mode 100644 frontend/webEditor/src/diagram/labels/editLabelDecorator.css diff --git a/frontend/webEditor/src/diagram/di.config.ts b/frontend/webEditor/src/diagram/di.config.ts index 9a310287..69c139b3 100644 --- a/frontend/webEditor/src/diagram/di.config.ts +++ b/frontend/webEditor/src/diagram/di.config.ts @@ -1,5 +1,5 @@ import { ContainerModule } from "inversify"; -import { configureModelElement, editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SRoutingHandleImpl, withEditLabelFeature } from "sprotty"; +import { configureActionHandler, configureModelElement, EditLabelAction, EditLabelActionHandler, editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SRoutingHandleImpl, TYPES, withEditLabelFeature } from "sprotty"; import { ArrowEdgeImpl, ArrowEdgeView, CustomRoutingHandleView } from "./edges/ArrowEdge"; import { DfdInputPortImpl, DfdInputPortView } from "./ports/DfdInputPort"; import { DfdOutputPortImpl, DfdOutputPortView } from "./ports/DfdOutputPort"; @@ -9,9 +9,19 @@ import { IONodeImpl, IONodeView } from "./nodes/DfdIONode"; import './style.css' import { DfdPositionalLabelView } from "./labels/DfdPositionalLabel"; import { DfdNodeLabelRenderer } from "./nodes/DfdNodeLabels"; +import { FilledBackgroundLabelView } from "./labels/FilledBackgroundLabel"; +import { DfdEditLabelValidatorDecorator } from "./labels/EditLabelDecorator"; +import { DfdEditLabelValidator } from "./labels/EditLabelValidator"; +import { NoScrollEditLabelUI } from "./labels/NoScrollEditLabelUI"; export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; + + bind(TYPES.IEditLabelValidator).to(DfdEditLabelValidator).inSingletonScope(); + bind(TYPES.IEditLabelValidationDecorator).to(DfdEditLabelValidatorDecorator).inSingletonScope(); + configureActionHandler(context, EditLabelAction.KIND, EditLabelActionHandler); + bind(NoScrollEditLabelUI).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(NoScrollEditLabelUI); configureModelElement(context, "graph", SGraphImpl, SGraphView); @@ -34,6 +44,10 @@ export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) configureModelElement(context, "label:positional", SLabelImpl, DfdPositionalLabelView, { enable: [editLabelFeature], }); + configureModelElement(context, "label:filled-background", SLabelImpl, FilledBackgroundLabelView, { + enable: [editLabelFeature], + }); bind(DfdNodeLabelRenderer).toSelf().inSingletonScope() + }); \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts b/frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts new file mode 100644 index 00000000..99a1bbe6 --- /dev/null +++ b/frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts @@ -0,0 +1,38 @@ +import { injectable } from "inversify"; +import { IEditLabelValidationDecorator, EditLabelValidationResult } from "sprotty"; +import "./editLabelDecorator.css" + +/** + * Renders the validation result of an dfd edge label to the label edit ui. + */ +@injectable() +export class DfdEditLabelValidatorDecorator implements IEditLabelValidationDecorator { + private readonly cssClass = "label-validation-results"; + + decorate(input: HTMLInputElement | HTMLTextAreaElement, validationResult: EditLabelValidationResult): void { + const containerElement = input.parentElement; + if (!containerElement) { + return; + } + + // Only display something when there is a validation error or warning + if (validationResult.severity !== "ok") { + const span = document.createElement("span"); + span.innerText = validationResult.message ?? validationResult.severity; + span.classList.add(this.cssClass); + + // Place validation notice right under the input field + span.style.top = `${input.clientHeight}px`; + // Rest is styled in the corresponding css file, as it is not dynamic + + containerElement.appendChild(span); + } + } + + dispose(input: HTMLInputElement | HTMLTextAreaElement): void { + const containerElement = input.parentElement; + if (containerElement) { + containerElement.querySelector(`span.${this.cssClass}`)?.remove(); + } + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/labels/EditLabelValidator.ts b/frontend/webEditor/src/diagram/labels/EditLabelValidator.ts new file mode 100644 index 00000000..c0c47ccb --- /dev/null +++ b/frontend/webEditor/src/diagram/labels/EditLabelValidator.ts @@ -0,0 +1,57 @@ +import { injectable } from "inversify"; +import { IEditLabelValidator, EditableLabel, SModelElementImpl, EditLabelValidationResult, SChildElementImpl, SEdgeImpl } from "sprotty"; +import { DfdNodeImpl } from "../nodes/common"; +import { DfdInputPortImpl } from "../ports/DfdInputPort"; + +/** + * Validator for the label of an dfd edge. + * Ensures that the label of an dfd edge is unique within the node that the edge is connected to. + * Does not do any validation if the label is not a child of an dfd edge. + */ +@injectable() +export class DfdEditLabelValidator implements IEditLabelValidator { + async validate(value: string, label: EditableLabel & SModelElementImpl): Promise { + // Check whether we have an dfd edge label and a non-empty label value + if (!(label instanceof SChildElementImpl)) { + return { severity: "ok" }; + } + + const labelParent = label.parent; + if (!(labelParent instanceof SEdgeImpl)) { + return { severity: "ok" }; + } + + // Labels on edges are not allowed to have spaces in them + if (value.includes(" ")) { + return { severity: "error", message: "Input name cannot contain spaces" }; + } + + // Labels on edges are not allowed to commas in them + if (value.includes(",")) { + return { severity: "error", message: "Input name cannot contain commas" }; + } + + // Labels on edges are not allowed to be empty + if (value.length == 0) { + return { severity: "error", message: "Input name cannot be empty" }; + } + + // Get node and edge names that are in use + const edge = labelParent; + const edgeTarget = edge.target; + if (!(edgeTarget instanceof DfdInputPortImpl)) { + return { severity: "ok" }; + } + + const inputPort = edgeTarget; + const node = inputPort.parent as DfdNodeImpl; + const usedEdgeNames = node.getEdgeTexts((e) => e.id !== edge.id); // filter out the edge we are currently editing + + // Check whether the label value is already used (case insensitive) + if (usedEdgeNames.find((name) => name.toLowerCase() === value.toLowerCase())) { + return { severity: "error", message: "Input name already used" }; + } + + return { severity: "ok" }; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx b/frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx new file mode 100644 index 00000000..55c254e9 --- /dev/null +++ b/frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx @@ -0,0 +1,32 @@ +/** @jsx svg */ +import { injectable } from "inversify"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { svg, ShapeView, SLabelImpl, RenderingContext } from "sprotty"; +import { calculateTextSize } from "../../utils/TextSize"; +import { VNode } from "snabbdom"; + +/** + * A sprotty label view that renders the label text with a filled background behind it. + * This is used to make the element behind the label invisible. + */ +@injectable() +export class FilledBackgroundLabelView extends ShapeView { + static readonly PADDING = 5; + + render(label: Readonly, context: RenderingContext): VNode | undefined { + if (!this.isVisible(label, context)) { + return undefined; + } + + const size = calculateTextSize(label.text); + const width = size.width + FilledBackgroundLabelView.PADDING; + const height = size.height + FilledBackgroundLabelView.PADDING; + + return ( + + {label.text ? : undefined} + {label.text} + + ); + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts b/frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts new file mode 100644 index 00000000..727eb513 --- /dev/null +++ b/frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts @@ -0,0 +1,30 @@ +import { + EditLabelUI, + SModelRootImpl, +} from "sprotty"; + +// For our use-case the sprotty container is at (0, 0) and fills the whole screen. +// Scrolling is disabled using CSS which disallows scrolling from the user. +// However the page might still be scrolled due to focus events. +// This is the case for the default sprotty EditLabelUI. +// When editing a label at a position where the edit control +// of the UI would be outside the viewport (at the right or bottom) +// the page would scroll to the right/bottom due to the focus event. +// To circumvent this we inherit from the default EditLabelUI and change it to +// scroll the page back to the page origin at (0, 0) if it has been moved due to the +// focus event. + +export class NoScrollEditLabelUI extends EditLabelUI { + protected override onBeforeShow( + containerElement: HTMLElement, + root: Readonly, + ...contextElementIds: string[] + ): void { + super.onBeforeShow(containerElement, root, ...contextElementIds); + + // Scroll page to 0,0 if not already there + if (window.scrollX !== 0 || window.scrollY !== 0) { + window.scrollTo(0, 0); + } + } +} diff --git a/frontend/webEditor/src/diagram/labels/editLabelDecorator.css b/frontend/webEditor/src/diagram/labels/editLabelDecorator.css new file mode 100644 index 00000000..63d28b91 --- /dev/null +++ b/frontend/webEditor/src/diagram/labels/editLabelDecorator.css @@ -0,0 +1,26 @@ +/* Label edit UI validation results styling */ + +.label-edit .label-validation-results { + position: absolute; + /* position top is set dynamically to be under the input field */ + + background-color: var(--color-primary); + padding: 8px; + border-radius: 5px; +} + +.label-edit .label-validation-results::before { + width: 16px; + height: 16px; + background-size: 16px 16px; + margin-right: 4px; /* space between the icon and the text */ + + content: ""; + display: inline-block; + vertical-align: middle; + + /* Uses the font awesome exclamation circle as a shape/mask and fills it with the error color */ + background-color: var(--color-error); + -webkit-mask: url("@fortawesome/fontawesome-free/svgs/solid/circle-exclamation.svg"); + mask: url("@fortawesome/fontawesome-free/svgs/solid/circle-exclamation.svg"); +} diff --git a/frontend/webEditor/src/serialize/ModelFactory.ts b/frontend/webEditor/src/serialize/ModelFactory.ts index 0f7f759c..06d9ad05 100644 --- a/frontend/webEditor/src/serialize/ModelFactory.ts +++ b/frontend/webEditor/src/serialize/ModelFactory.ts @@ -2,15 +2,18 @@ import { injectable } from "inversify"; import { SChildElementImpl, SModelElementImpl, SModelFactory, SParentElementImpl } from "sprotty"; import { DfdNode } from "../diagram/nodes/common"; import { getBasicType, SLabel, SModelElement } from "sprotty-protocol"; +import { ArrowEdge } from "../diagram/edges/ArrowEdge"; @injectable() export class DfdModelFactory extends SModelFactory { override createElement(schema: SModelElement | SModelElementImpl, parent?: SParentElementImpl): SChildElementImpl { + if (schema instanceof SModelElementImpl) { + return super.createElement(schema, parent); + } if ( - (schema.type === "node:storage" || + schema.type === "node:storage" || schema.type === "node:function" || - schema.type === "node:input-output") && - !(schema instanceof SModelElementImpl) + schema.type === "node:input-output" ) { const dfdSchema = schema as DfdNode; schema.children = schema.children ?? []; @@ -26,17 +29,32 @@ export class DfdModelFactory extends SModelFactory { } as SLabel); } + if (schema.type === "edge:arrow") { + const dfdSchema = schema as ArrowEdge + schema.children = schema.children ?? [] + schema.children.push({ + type: "label:filled-background", + text: dfdSchema.text ?? "", + id: schema.id + "-label", + edgePlacement: { + position: 0.5, + side: "on", + rotate: false, + } + } as SLabel) + } + return super.createElement(schema, parent); + } override createSchema(element: SModelElementImpl): SModelElement { const schema = super.createSchema(element); if ( - (schema.type === "node:storage" || + schema.type === "node:storage" || schema.type === "node:function" || - schema.type === "node:input-output") && - (element instanceof SModelElementImpl) + schema.type === "node:input-output" ) { const dfdSchema = schema as DfdNode; const ports = dfdSchema.children?.filter( @@ -57,6 +75,21 @@ export class DfdModelFactory extends SModelFactory { return dfdSchema } + if (schema.type === "edge:arrow") { + const dfdSchema = schema as ArrowEdge + + const labelValue = schema.children?.find( + (child) => child.type === "label:filled-background" + ) as SLabel | undefined; + + if (labelValue) { + dfdSchema.text = labelValue.text; + } + + dfdSchema.children = [] + return dfdSchema + } + return schema; } } From f67a2a859d9989f44f7dc122bf7b8334349c18c4 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Mon, 17 Nov 2025 14:54:14 +0100 Subject: [PATCH 20/41] add tool palette --- frontend/webEditor/src/diagram/di.config.ts | 11 +- .../src/diagram/ports/portSnapper.ts | 192 ++++++++++++ frontend/webEditor/src/index.ts | 4 +- .../webEditor/src/toolPalette/creationTool.ts | 293 ++++++++++++++++++ .../webEditor/src/toolPalette/di.config.ts | 45 +++ .../src/toolPalette/edgeCreationTool.ts | 120 +++++++ .../src/toolPalette/nodeCreationTool.ts | 24 ++ .../src/toolPalette/portCreationTool.ts | 67 ++++ .../webEditor/src/toolPalette/toolPalette.css | 63 ++++ .../webEditor/src/toolPalette/toolPalette.tsx | 275 ++++++++++++++++ 10 files changed, 1090 insertions(+), 4 deletions(-) create mode 100644 frontend/webEditor/src/diagram/ports/portSnapper.ts create mode 100644 frontend/webEditor/src/toolPalette/creationTool.ts create mode 100644 frontend/webEditor/src/toolPalette/di.config.ts create mode 100644 frontend/webEditor/src/toolPalette/edgeCreationTool.ts create mode 100644 frontend/webEditor/src/toolPalette/nodeCreationTool.ts create mode 100644 frontend/webEditor/src/toolPalette/portCreationTool.ts create mode 100644 frontend/webEditor/src/toolPalette/toolPalette.css create mode 100644 frontend/webEditor/src/toolPalette/toolPalette.tsx diff --git a/frontend/webEditor/src/diagram/di.config.ts b/frontend/webEditor/src/diagram/di.config.ts index 69c139b3..62a50bb7 100644 --- a/frontend/webEditor/src/diagram/di.config.ts +++ b/frontend/webEditor/src/diagram/di.config.ts @@ -1,5 +1,5 @@ import { ContainerModule } from "inversify"; -import { configureActionHandler, configureModelElement, EditLabelAction, EditLabelActionHandler, editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SRoutingHandleImpl, TYPES, withEditLabelFeature } from "sprotty"; +import { configureActionHandler, configureCommand, configureModelElement, EditLabelAction, EditLabelActionHandler, editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SRoutingHandleImpl, TYPES, withEditLabelFeature } from "sprotty"; import { ArrowEdgeImpl, ArrowEdgeView, CustomRoutingHandleView } from "./edges/ArrowEdge"; import { DfdInputPortImpl, DfdInputPortView } from "./ports/DfdInputPort"; import { DfdOutputPortImpl, DfdOutputPortView } from "./ports/DfdOutputPort"; @@ -13,6 +13,7 @@ import { FilledBackgroundLabelView } from "./labels/FilledBackgroundLabel"; import { DfdEditLabelValidatorDecorator } from "./labels/EditLabelDecorator"; import { DfdEditLabelValidator } from "./labels/EditLabelValidator"; import { NoScrollEditLabelUI } from "./labels/NoScrollEditLabelUI"; +import { PortAwareSnapper, AlwaysSnapPortsMoveMouseListener, ReSnapPortsAfterLabelChangeCommand } from "./ports/portSnapper"; export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; @@ -20,8 +21,12 @@ export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) bind(TYPES.IEditLabelValidator).to(DfdEditLabelValidator).inSingletonScope(); bind(TYPES.IEditLabelValidationDecorator).to(DfdEditLabelValidatorDecorator).inSingletonScope(); configureActionHandler(context, EditLabelAction.KIND, EditLabelActionHandler); - bind(NoScrollEditLabelUI).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(NoScrollEditLabelUI); + bind(NoScrollEditLabelUI).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(NoScrollEditLabelUI); + + bind(TYPES.ISnapper).to(PortAwareSnapper).inSingletonScope(); + bind(TYPES.MouseListener).to(AlwaysSnapPortsMoveMouseListener).inSingletonScope(); + configureCommand(context, ReSnapPortsAfterLabelChangeCommand); configureModelElement(context, "graph", SGraphImpl, SGraphView); diff --git a/frontend/webEditor/src/diagram/ports/portSnapper.ts b/frontend/webEditor/src/diagram/ports/portSnapper.ts new file mode 100644 index 00000000..eb8e0fc1 --- /dev/null +++ b/frontend/webEditor/src/diagram/ports/portSnapper.ts @@ -0,0 +1,192 @@ +import { inject, injectable } from "inversify"; +import { + CenterGridSnapper, + Command, + CommandExecutionContext, + CommandReturn, + ISnapper, + MoveMouseListener, + SChildElementImpl, + SModelElementImpl, + SNodeImpl, + SPortImpl, + TYPES, + isBoundsAware, +} from "sprotty"; +import { ApplyLabelEditAction, Point } from "sprotty-protocol"; + +/** + * A grid snapper that snaps to the nearest grid point. + * Same as CenterGridSnapper but allows to specify the grid size at construction time. + */ +class ConfigurableGridSnapper extends CenterGridSnapper { + constructor(private readonly gridSize: number) { + super(); + } + + override get gridX() { + return this.gridSize; + } + + override get gridY() { + return this.gridSize; + } +} + +/** + * A snapper that snaps ports to be on top of the nearest edge of the node. + * For nodes this snapper uses a grid with a grid size of 5 while for ports it uses a grid size of 2 + * to allow for more precise positioning of ports. + */ +@injectable() +export class PortAwareSnapper implements ISnapper { + private readonly nodeSnapper = new ConfigurableGridSnapper(5); + // The port grid size is a multiple of the node grid size to ensure + // that the ports of two nodes neighboring each other can be aligned. + // If the grid size would be different, it may occur that the ports + // of two nodes that start on different heights may not be aligned, + // so make sure that the node grid size is a multiple of the port grid size. + private readonly portSnapper = new ConfigurableGridSnapper(2.5); + + private snapPort(position: Point, element: SPortImpl): Point { + const parentElement = element.parent; + + if (parentElement instanceof SPortImpl) { + // Parent is not a node, so we cannot snap the port to the node edges + return position; + } + + if (!isBoundsAware(parentElement)) { + // Cannot get the parent size, just return the original position and don't snap + return position; + } + + const parentBounds = parentElement.bounds; + + // Clamp the position to be inside the parent bounds + const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); + + position = this.portSnapper.snap(position, element); + const clampX = clamp(position.x, 0, parentBounds.width); + const clampY = clamp(position.y, 0, parentBounds.height); + + // Determine the closest edge + const distances = [ + { x: clampX, y: 0 }, // Top edge + { x: 0, y: clampY }, // Left edge + { x: parentBounds.width, y: clampY }, // Right edge + { x: clampX, y: parentBounds.height }, // Bottom edge + ]; + + const closestEdge = distances.reduce((prev, curr) => + Math.hypot(curr.x - position.x, curr.y - position.y) < Math.hypot(prev.x - position.x, prev.y - position.y) + ? curr + : prev, + ); + + // The position currently points exactly on the edge. + // This position is used as the top left point when the port is drawn. + // However we want the port to be centered on the node edge instead of the top left being on top of the edge. + // So we move the port by half of the width/height to the left/top to center it on the node edge. + const snappedX = closestEdge.x - element.bounds.width / 2; + const snappedY = closestEdge.y - element.bounds.height / 2; + + return { x: snappedX, y: snappedY }; + } + + snap(position: Point, element: SModelElementImpl): Point { + if (element instanceof SPortImpl) { + return this.snapPort(position, element); + } else { + return this.nodeSnapper.snap(position, element); + } + } +} + +/** + * Custom MoveMouseListener that only allows to disable snapping for nodes. + * For use with PortAwareSnapper which snaps the ports to the node edges. + * Snapping can normally be temporarily disabled by holding down the Shift key. + * This would allow you to move a port to any position in the diagram and not just on the node edges. + * This is not wanted why we disallow fine moving without snapping for ports. + */ +export class AlwaysSnapPortsMoveMouseListener extends MoveMouseListener { + protected snap(position: Point, element: SModelElementImpl, isSnap: boolean): Point { + // Snap if it is active or always for ports + if (this.snapper && (isSnap || element instanceof SPortImpl)) { + return this.snapper.snap(position, element); + } else { + return position; + } + } +} + +/** + * Command that snaps all ports of the node to the grid after a label was added/removed. + * Runs after {@link ApplyLabelEditAction} to ensure the ports are snapped to the grid after the label was moved. + * + * This is done by implementing another command for {@link ApplyLabelEditAction} + * and registering it as well. That way this command will be executed after the {@link ApplyLabelEditCommand} + */ +@injectable() +export class ReSnapPortsAfterLabelChangeCommand extends Command { + static readonly KIND = ApplyLabelEditAction.KIND; + + @inject(TYPES.ISnapper) + private snapper?: ISnapper; + + constructor(@inject(TYPES.Action) private readonly action: ApplyLabelEditAction) { + super(); + } + + execute(context: CommandExecutionContext): CommandReturn { + const label = context.root.index.getById(this.action.labelId); + if (!(label instanceof SChildElementImpl) || !this.snapper) { + return context.root; + } + + const node = label.parent; + if (!(node instanceof SNodeImpl)) { + return context.root; + } + + snapPortsOfNode(node, this.snapper); + return context.root; + } + + // undo/redo: resnap aswell. Same as execute + + undo(context: CommandExecutionContext): CommandReturn { + return this.execute(context); + } + + redo(context: CommandExecutionContext): CommandReturn { + return this.execute(context); + } +} + +/** + * Snaps all ports of the given node to the grid using the given snapper. + * Useful to ensure all ports are on are snapped onto an node edge using + * {@link PortAwareSnapper} after resizing the node. + */ +export function snapPortsOfNode(node: SNodeImpl, snapper: ISnapper): void { + if (!(node instanceof SChildElementImpl)) { + // Element has no children which could be ports + return; + } + + node.children.forEach((child) => { + if (child instanceof SPortImpl) { + // PortAwareSnapper expects the center of the port as input. + // However the stored position points to the top left of the port, + // so we need to adjust the position by half of the width/height. + const pos = { ...child.position }; + const { width, height } = child.bounds; + pos.x += width / 2; + pos.y += height / 2; + + child.position = snapper.snap(pos, child); + } + }); +} diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 22c240e9..ee68f31a 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -19,6 +19,7 @@ import { layoutModule } from "./layout/di.config"; import { elkLayoutModule } from "sprotty-elk"; import { fileNameModule } from "./fileName/di.config"; import { settingsModule } from "./settings/di.config"; +import { toolPaletteModule } from "./toolPalette/di.config"; const container = new Container(); @@ -41,7 +42,8 @@ container.load( elkLayoutModule, layoutModule, fileNameModule, - settingsModule + settingsModule, + toolPaletteModule ) const startUpAgents = container.getAll(StartUpAgent) diff --git a/frontend/webEditor/src/toolPalette/creationTool.ts b/frontend/webEditor/src/toolPalette/creationTool.ts new file mode 100644 index 00000000..08d4ec61 --- /dev/null +++ b/frontend/webEditor/src/toolPalette/creationTool.ts @@ -0,0 +1,293 @@ +import { + ActionDispatcher, + CommandExecutionContext, + CommandReturn, + CommandStack, + CommitModelAction, + ICommand, + ILogger, + IModelFactory, + ISnapper, + KeyListener, + MouseListener, + MousePositionTracker, + MouseTool, + SChildElementImpl, + SEdgeImpl, + SGraphImpl, + SModelElementImpl, + SNodeImpl, + SParentElementImpl, + SPortImpl, + TYPES, +} from "sprotty"; +import { inject, injectable, multiInject } from "inversify"; +import { Action, Point, SEdge, SNode, SPort } from "sprotty-protocol"; +import { EDITOR_TYPES } from "../editorTypes"; + +type Positionable = { position?: Point }; +type Schema = (SNode | SEdge | SPort) & Positionable; +type Impl = SNodeImpl | SEdgeImpl | SPortImpl; +export type AnyCreationTool = CreationTool; + +/** + * Common interface between all tools used by the tool palette to create new elements. + * These tools are meant to be enabled, allow the user to perform some action like creating a new node or edge, + * and then they should disable themselves when the action is done. + * Alternatively they can be disabled from the UI or other code to cancel the tool usage. + */ +@injectable() +export abstract class CreationTool extends MouseListener { + protected element?: I; + protected readonly previewOpacity = 0.5; + protected insertIntoGraphRootAfterCreation = true; + protected elementType = ""; + + constructor( + @inject(MouseTool) protected mouseTool: MouseTool, + @inject(MousePositionTracker) protected mousePositionTracker: MousePositionTracker, + @inject(TYPES.IModelFactory) protected modelFactory: IModelFactory, + @inject(TYPES.IActionDispatcher) protected actionDispatcher: ActionDispatcher, + @inject(TYPES.ICommandStack) protected commandStack: CommandStack, + @inject(TYPES.ISnapper) protected snapper: ISnapper, + @inject(TYPES.ILogger) protected logger: ILogger, + ) { + super(); + } + + abstract createElementSchema(): S; + + protected async createElement(): Promise { + const schema = this.createElementSchema(); + + // Create the element with the preview opacity to indicated it is not placed yet + // Only set opacity if it is not already set in the schema + schema.opacity ??= this.previewOpacity; + + const element = this.modelFactory.createElement(schema) as I; + if (this.insertIntoGraphRootAfterCreation) { + const root = await this.commandStack.executeAll([]); + root.add(element); + } + + return element; + } + + enable(elementType: string): void { + this.elementType = elementType; + this.mouseTool.register(this); + this.createElement() + .then((element) => { + this.element = element; + this.logger.log(this, "Created element", element); + + // Show element at current mouse position + if (this.mousePositionTracker.lastPositionOnDiagram) { + this.updateElementPosition(this.mousePositionTracker.lastPositionOnDiagram); + } + }) + .catch((error) => { + this.logger.error(this, "Failed to create element", error); + }); + } + + disable(): void { + this.mouseTool.deregister(this); + + if (this.element) { + // Element is not placed yet but we're disabling the tool. + // This means the creation was cancelled and the element should be deleted. + + // Get root before removing the element, needed for re-render + let root: SGraphImpl | undefined; + try { + root = this.element.root as SGraphImpl; + } catch (error) { + // element has no assigned root + void error; + } + + // Remove element from graph + this.element.parent?.remove(this.element); + this.element = undefined; + + // Re-render the graph to remove the element from the preview. + // Root may be unavailable e.g. when the element hasn't been inserted into + // the diagram yet. Skipping the render in those cases is fine as the element + // wasn't rendered in such case anyway. + if (root) { + this.commandStack.update(root); + } + + this.logger.info(this, "Cancelled element creation"); + } + } + + protected finishPlacingElement(): void { + if (this.element) { + const elementParent = this.element.parent; + // Remove the element as it was only added as a temporary preview element + elementParent.remove(this.element); + + // Make node fully visible + this.element.opacity = 1; + + // Set via a command for redo/undo support. + // This inserts the created element properly into the model in contrast to the + // temporary add done previously. + this.actionDispatcher.dispatch(AddElementToGraphAction.create(this.element, elementParent)); + + this.logger.log(this, "Finalized element creation of element", this.element); + this.element = undefined; // Unset to prevent further actions + } + this.disable(); + } + + private updateElementPosition(mousePosition: Point): void { + if (!this.element) { + return; + } + + const newPosition = { ...mousePosition }; + + if (this.element instanceof SEdgeImpl) { + // Snap the edge target to the mouse position, if there is a target element. + if (this.element.targetId && this.element.target) { + if (!Point.equals(this.element.target.position, newPosition)) { + this.element.target.position = newPosition; + // Trigger re-rendering of the edge + this.commandStack.update(this.element.root); + } + } + } else { + const previousPosition = this.element.position; + + // Adapt the mouse position depending on element type + if (this.element instanceof SNodeImpl) { + // The node should be created to have its center at the mouse position. + // Because of this, we need to adjust the position by half the size of the element. + const { width, height } = this.element.bounds; + newPosition.x -= width / 2; + newPosition.y -= height / 2; + } else if (this.element instanceof SPortImpl) { + // Port positions must be relative to the target node. + // So we need to convert the absolute graph position of the mouse + // to a position relative to the target node. + const parent = this.element.parent; + if (parent instanceof SNodeImpl) { + newPosition.x -= parent.position.x; + newPosition.y -= parent.position.y; + } + } + + // Snap the element to the corresponding grid + const newPositionSnapped = this.snapper.snap(newPosition, this.element); + + // Only update if the position after snapping has changed (aka the effective position). + if (!Point.equals(previousPosition, newPositionSnapped)) { + this.element.position = newPositionSnapped; + // Trigger re-rendering of the node/port + this.commandStack.update(this.element.root); + } + } + } + + mouseMove(target: SModelElementImpl, event: MouseEvent): (Action | Promise)[] { + const mousePosition = this.calculateMousePosition(target, event); + this.updateElementPosition(mousePosition); + return []; + } + + mouseDown(_target: SModelElementImpl, event: MouseEvent): Action[] { + event.preventDefault(); // prevents additional click onto the newly created element + + this.finishPlacingElement(); + + return [ + CommitModelAction.create(), // Save to element ModelSource + ]; + } + + /** + * Calculates the mouse position in graph coordinates. + */ + protected calculateMousePosition(target: SModelElementImpl, event: MouseEvent): Point { + const root = target.root as SGraphImpl; + + const calcPos = (axis: "x" | "y") => { + // Position of the top left viewport corner in the whole graph + const rootPosition = root.scroll[axis]; + // Offset of the mouse position from the top left viewport corner in screen pixels + const screenOffset = axis === "x" ? event.offsetX : event.offsetY; + // Offset of the mouse position from the top left viewport corner in graph coordinates + const screenOffsetNormalized = screenOffset / root.zoom; + + // Add position + return rootPosition + screenOffsetNormalized; + }; + return { + x: calcPos("x"), + y: calcPos("y"), + }; + } +} + +/** + * Adds the given element to the graph at the root level. + */ +export interface AddElementToGraphAction extends Action { + kind: typeof AddElementToGraphAction.TYPE; + element: SChildElementImpl; + parent: SParentElementImpl; +} +export namespace AddElementToGraphAction { + export const TYPE = "addElementToGraph"; + export function create(element: SChildElementImpl, parent: SParentElementImpl): AddElementToGraphAction { + return { + kind: TYPE, + element, + parent, + }; + } +} + +@injectable() +export class AddElementToGraphCommand implements ICommand { + public static readonly KIND = AddElementToGraphAction.TYPE; + + constructor(@inject(TYPES.Action) private action: AddElementToGraphAction) {} + + execute(context: CommandExecutionContext): CommandReturn { + this.action.parent.add(this.action.element); + return context.root; + } + + undo(context: CommandExecutionContext): CommandReturn { + this.action.element.parent.remove(this.action.element); + return context.root; + } + + redo(context: CommandExecutionContext): CommandReturn { + return this.execute(context); + } +} + +/** + * Util key listener that disables all registered creation tools when the escape key is pressed. + */ +@injectable() +export class CreationToolDisableKeyListener extends KeyListener { + @multiInject(EDITOR_TYPES.CreationTool) protected tools: AnyCreationTool[] = []; + + override keyDown(_element: SModelElementImpl, event: KeyboardEvent): Action[] { + if (event.key === "Escape") { + this.disableAllTools(); + } + + return []; + } + + private disableAllTools(): void { + this.tools.forEach((tool) => tool.disable()); + } +} diff --git a/frontend/webEditor/src/toolPalette/di.config.ts b/frontend/webEditor/src/toolPalette/di.config.ts new file mode 100644 index 00000000..38e36297 --- /dev/null +++ b/frontend/webEditor/src/toolPalette/di.config.ts @@ -0,0 +1,45 @@ +import { ContainerModule } from "inversify"; +import { AddElementToGraphCommand, CreationToolDisableKeyListener } from "./creationTool"; +import { EdgeCreationTool } from "./edgeCreationTool"; +import { NodeCreationTool } from "./nodeCreationTool"; +import { PortCreationTool } from "./portCreationTool"; +import { ToolPaletteUI } from "./toolPalette"; +import { + CommitModelAction, + EmptyView, + SNodeImpl, + TYPES, + configureActionHandler, + configureCommand, + configureModelElement, +} from "sprotty"; +import { EDITOR_TYPES } from "../editorTypes"; + +// This module contains an UI extension that adds a tool palette to the editor. +// This tool palette allows the user to create new nodes and edges. +// Additionally it contains the tools that are used to create the nodes and edges. + +export const toolPaletteModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + + bind(CreationToolDisableKeyListener).toSelf().inSingletonScope(); + bind(TYPES.KeyListener).toService(CreationToolDisableKeyListener); + + configureModelElement(context, "empty-node", SNodeImpl, EmptyView); + configureCommand(context, AddElementToGraphCommand); + + bind(NodeCreationTool).toSelf().inSingletonScope(); + bind(EDITOR_TYPES.CreationTool).toService(NodeCreationTool); + + bind(EdgeCreationTool).toSelf().inSingletonScope(); + bind(EDITOR_TYPES.CreationTool).toService(EdgeCreationTool); + + bind(PortCreationTool).toSelf().inSingletonScope(); + bind(EDITOR_TYPES.CreationTool).toService(PortCreationTool); + + bind(ToolPaletteUI).toSelf().inSingletonScope(); + configureActionHandler(context, CommitModelAction.KIND, ToolPaletteUI); + bind(TYPES.IUIExtension).toService(ToolPaletteUI); + bind(TYPES.KeyListener).toService(ToolPaletteUI); + bind(EDITOR_TYPES.DefaultUIElement).toService(ToolPaletteUI); +}); diff --git a/frontend/webEditor/src/toolPalette/edgeCreationTool.ts b/frontend/webEditor/src/toolPalette/edgeCreationTool.ts new file mode 100644 index 00000000..d05b4383 --- /dev/null +++ b/frontend/webEditor/src/toolPalette/edgeCreationTool.ts @@ -0,0 +1,120 @@ +import { injectable } from "inversify"; +import { + Connectable, + isConnectable, + SChildElementImpl, + SEdgeImpl, + SModelElementImpl, + SParentElementImpl, +} from "sprotty"; +import { Action, SEdge, SNode } from "sprotty-protocol"; +import { CreationTool } from "./creationTool"; +import { generateRandomSprottyId } from "../utils/idGenerator"; + +@injectable() +export class EdgeCreationTool extends CreationTool { + // Pseudo element that is used as a target for the edge while it is being created + private edgeTargetElement?: SChildElementImpl; + + // We will insert the edge ourselves once we determined the source element. + // Then we can also insert the edge target element at the mouse position + // and have the source and target element inserted. + // Otherwise sprotty would not be able to compute the path of the edge + // and show dangling elements. + protected override insertIntoGraphRootAfterCreation = false; + + createElementSchema(): SEdge { + return { + id: generateRandomSprottyId(), + type: this.elementType, + sourceId: "", + targetId: "", + }; + } + + disable(): void { + if (this.edgeTargetElement) { + // Pseudo edge target element must always be removed + // regardless of whether the edge creation was successful or cancelled + this.edgeTargetElement.parent?.remove(this.edgeTargetElement); + this.edgeTargetElement = undefined; + } + + super.disable(); + } + + mouseDown(target: SModelElementImpl, event: MouseEvent): Action[] { + if (!this.element) { + // This shouldn't happen + return []; + } + + const clickedElement = this.findConnectable(target); + if (!clickedElement) { + // Nothing can be connected to this element or its parents, invalid choice + return []; + } + + if (this.element.sourceId) { + // Source already set, so we're setting the target now + + if (clickedElement.canConnect(this.element, "target")) { + this.element.targetId = clickedElement.id; + + // Finalize creation and disable the tool + const actions = super.mouseDown(clickedElement, event); + // mouseDown() calls the parent class disable implementation, but we overwrite it here, so we need to call it manually + this.disable(); + return actions; + } + } else { + // Source not set yet, so we're setting the source now + if (clickedElement.canConnect(this.element, "source")) { + this.element.sourceId = clickedElement.id; + + // Insert the edge to make it visible. + const root = target.root; + root.add(this.element); + + // Create a new target element + // For previewing the edge it must be able to be rendered + // which means source and target *must* be set even though + // we don't know the target yet. + // To work around this we create a dummy target element + // that is snapped to the current mouse position. + // It is a SPort because a normal node + this.edgeTargetElement = this.modelFactory.createElement({ + id: generateRandomSprottyId(), + type: "empty-node", + position: this.calculateMousePosition(target, event), + } as SNode); + // Add empty node to the graph and as a edge target + root.add(this.edgeTargetElement); + this.element.targetId = this.edgeTargetElement.id; + } + } + return []; + } + + /** + * Recursively searches through the element's parents until a connectable element is found. + * This is required because the user may click on elements inside a node, which are not connectable. + * E.g. a the user clicks on a label inside the node but in this case the edge should be connected to the node itself. + * + * @param element Element to start searching from + * @returns The first connectable element found or undefined if none was found + */ + private findConnectable( + element: SChildElementImpl | SParentElementImpl | SModelElementImpl, + ): (Connectable & SModelElementImpl) | undefined { + if (isConnectable(element)) { + return element; + } + + if ("parent" in element && element.parent) { + return this.findConnectable(element.parent); + } else { + return undefined; + } + } +} diff --git a/frontend/webEditor/src/toolPalette/nodeCreationTool.ts b/frontend/webEditor/src/toolPalette/nodeCreationTool.ts new file mode 100644 index 00000000..4fc4dafe --- /dev/null +++ b/frontend/webEditor/src/toolPalette/nodeCreationTool.ts @@ -0,0 +1,24 @@ +import { injectable } from "inversify"; +import { SNodeImpl } from "sprotty"; +import { SNode } from "sprotty-protocol"; +import { CreationTool } from "./creationTool"; +import { generateRandomSprottyId } from "../utils/idGenerator"; + +/** + * Creates a node when the user clicks somewhere on the root graph. + * The type of the node can be set using the parameter in the enable function. + * Automatically disables itself after creating a node. + */ +@injectable() +export class NodeCreationTool extends CreationTool { + createElementSchema(): SNode { + const defaultText = this.elementType.replace("node:", ""); + const defaultTextCapitalized = defaultText.charAt(0).toUpperCase() + defaultText.slice(1); + + return { + id: generateRandomSprottyId(), + type: this.elementType, + text: defaultTextCapitalized, + } as SNode; + } +} diff --git a/frontend/webEditor/src/toolPalette/portCreationTool.ts b/frontend/webEditor/src/toolPalette/portCreationTool.ts new file mode 100644 index 00000000..88b36e1c --- /dev/null +++ b/frontend/webEditor/src/toolPalette/portCreationTool.ts @@ -0,0 +1,67 @@ +import { injectable } from "inversify"; +import { CommitModelAction, SChildElementImpl, SModelElementImpl, SPortImpl, SShapeElementImpl } from "sprotty"; +import { Action, SPort } from "sprotty-protocol"; +import { CreationTool } from "./creationTool"; +import { generateRandomSprottyId } from "../utils/idGenerator"; + +@injectable() +export class PortCreationTool extends CreationTool { + createElementSchema(): SPort { + return { + id: generateRandomSprottyId(), + type: this.elementType, + opacity: 0, + }; + } + + mouseMove(target: SModelElementImpl, event: MouseEvent): (Action | Promise)[] { + if (!this.element) { + return []; + } + + const currentParent = this.element.parent; + const targetNode = this.findNodeElement(target); + + if (targetNode) { + // We're hovering over a node, add the port to the node (if not already) + if (currentParent !== targetNode && target instanceof SChildElementImpl) { + this.element.opacity = this.previewOpacity; + currentParent.remove(this.element); + target.add(this.element); + this.commandStack.update(this.element.root); + } + } else { + // We're not hovering over a node. + // Add the port to the root graph (if not already) and hide it. + if (currentParent !== target.root) { + this.element.opacity = 0; + currentParent.remove(this.element); + target.root.add(this.element); + this.commandStack.update(this.element.root); + } + } + + return super.mouseMove(target, event); + } + + mouseDown(target: SModelElementImpl, event: MouseEvent): Action[] { + if (this.element?.parent === target.root) { + this.disable(); + // Run some action to re-render the tool palette ui + // showing that the tool is disabled + return [CommitModelAction.create()]; + } + + return super.mouseDown(target, event); + } + + private findNodeElement(target: SModelElementImpl): SModelElementImpl | undefined { + if (target.type.startsWith("node")) { + return target; + } + if (target instanceof SChildElementImpl && target.parent instanceof SShapeElementImpl) { + return this.findNodeElement(target.parent); + } + return undefined; + } +} diff --git a/frontend/webEditor/src/toolPalette/toolPalette.css b/frontend/webEditor/src/toolPalette/toolPalette.css new file mode 100644 index 00000000..dd458abb --- /dev/null +++ b/frontend/webEditor/src/toolPalette/toolPalette.css @@ -0,0 +1,63 @@ +.tool-palette { + top: 40px; + padding: 3px; + right: 40px; + + /* Make text of the elements non-selectable */ + -webkit-user-select: none; /* Safari only supports user select using the -webkit prefix */ + user-select: none; + + /* grid layout (two tools per row) */ + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} + +.tool-palette .tool { + width: 32px; + height: 32px; + border-radius: 5px; + padding: 2px; + margin: 2px; +} + +.tool-palette .tool svg line, +.tool-palette .tool svg path, +.tool-palette .tool svg rect, +.tool-palette .tool svg circle { + stroke: var(--color-foreground); + fill: transparent; +} +.tool-palette .tool svg .fill { + fill: var(--color-foreground); +} + +.tool-palette .tool svg text { + fill: var(--color-foreground); + font-size: 10px; + font-family: sans-serif; + text-anchor: middle; + dominant-baseline: central; +} + +.tool-palette .tool:hover { + cursor: pointer; + background-color: var(--color-tool-palette-hover); +} + +.tool-palette .tool.active { + background-color: var(--color-tool-palette-selected); +} + +/* Show keyboard shortcuts for each tool when help is opened */ +.tool-palette .tool .shortcut { + position: relative; + bottom: 16px; + left: -4px; + font-size: 0.75em; + + transition: opacity 300ms ease-in-out; + opacity: 0; +} +body.help-enabled .tool-palette .tool .shortcut { + opacity: 1; +} diff --git a/frontend/webEditor/src/toolPalette/toolPalette.tsx b/frontend/webEditor/src/toolPalette/toolPalette.tsx new file mode 100644 index 00000000..d2daa8fd --- /dev/null +++ b/frontend/webEditor/src/toolPalette/toolPalette.tsx @@ -0,0 +1,275 @@ +/** @jsx svg */ +import { injectable, inject, multiInject, optional } from "inversify"; +import { VNode } from "snabbdom"; +import { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + svg, + AbstractUIExtension, + IActionDispatcher, + IActionHandler, + ICommand, + TYPES, + PatcherProvider, + CommitModelAction, + SModelElementImpl, + KeyListener, +} from "sprotty"; +import { KeyCode, matchesKeystroke } from "sprotty/lib/utils/keyboard"; +import { Action } from "sprotty-protocol"; +import { NodeCreationTool } from "./nodeCreationTool"; +import { EdgeCreationTool } from "./edgeCreationTool"; +import { PortCreationTool } from "./portCreationTool"; +import { AnyCreationTool } from "./creationTool"; + +import "./toolPalette.css"; +import { EDITOR_TYPES } from "../editorTypes"; +import { SETTINGS } from "../settings/Settings"; +import { EditorModeController } from "../settings/editorMode"; + +/** + * UI extension that adds a tool palette to the diagram in the upper right. + * Currently this only allows activating the CreateEdgeTool. + */ +@injectable() +export class ToolPaletteUI extends AbstractUIExtension implements IActionHandler, KeyListener { + static readonly ID = "tool-palette"; + private readonly keyboardShortcuts: Map void> = new Map(); + + constructor( + @inject(TYPES.IActionDispatcher) protected readonly actionDispatcher: IActionDispatcher, + @inject(TYPES.PatcherProvider) protected readonly patcherProvider: PatcherProvider, + @inject(NodeCreationTool) protected readonly nodeCreationTool: NodeCreationTool, + @inject(EdgeCreationTool) protected readonly edgeCreationTool: EdgeCreationTool, + @inject(PortCreationTool) protected readonly portCreationTool: PortCreationTool, + @multiInject(EDITOR_TYPES.CreationTool) protected readonly allTools: AnyCreationTool[], + @inject(SETTINGS.Mode) + @optional() + protected readonly editorModeController: EditorModeController, + ) { + super(); + } + + id(): string { + return ToolPaletteUI.ID; + } + + containerClass(): string { + // The container element gets this class name by the sprotty base class. + return "tool-palette"; + } + + /** + * This method creates the sub elements of the tool palette. + * This is called by the sprotty base class after creating the container element. + */ + protected initializeContents(containerElement: HTMLElement): void { + containerElement.classList.add("ui-float"); + document.addEventListener("keydown", (event) => { + if (matchesKeystroke(event, "Escape")) { + this.disableTools(); + } + }); + + this.addTool( + containerElement, + this.nodeCreationTool, + "Storage node", + (tool) => tool.enable("node:storage"), + + + + + Sto + + , + "Digit1", + ); + + this.addTool( + containerElement, + this.nodeCreationTool, + "Input/Output node", + (tool) => tool.enable("node:input-output"), + + + + IO + + , + "Digit2", + ); + + this.addTool( + containerElement, + this.nodeCreationTool, + "Function node", + (tool) => tool.enable("node:function"), + + + + + Fun + + , + "Digit3", + ); + + this.addTool( + containerElement, + this.edgeCreationTool, + "Edge", + (tool) => tool.enable("edge:arrow"), + + + + , + "Digit4", + ); + + this.addTool( + containerElement, + this.portCreationTool, + "Input port", + (tool) => tool.enable("port:dfd-input"), + + + + I + + , + "Digit5", + ); + + this.addTool( + containerElement, + this.portCreationTool, + "Output port", + (tool) => tool.enable("port:dfd-output"), + + + + O + + , + "Digit6", + ); + + containerElement.classList.add("tool-palette"); + } + + /** + * Utility function that adds a tool to the tool palette. + * + * @param container the base container html element of the tool palette + * @param toolId the id of the sprotty tool that should be activated when the tool is clicked + * @param name the name of the tool that is displayed as a alt text/tooltip + * @param clicked callback that is called when the tool is clicked. Can be used to configure the calling tool + * @param svgCode vnode for the svg logo of the tool. Will be placed in a 32x32 svg element + * @param enableKey optional key for a keyboard shortcut to activate the tool + */ + private addTool( + container: HTMLElement, + tool: T, + name: string, + enable: (tool: T) => void, + svgCode: VNode, + enableKey?: KeyCode, + ): void { + const toolElement = document.createElement("div"); + toolElement.classList.add("tool"); + + toolElement.addEventListener("click", () => { + if (toolElement.classList.contains("active") || this.editorModeController?.isReadOnly()) { + tool.disable(); + toolElement.classList.remove("active"); + } else { + // Disable all other tools + this.disableTools(); + + // Enable the selected tool + enable(tool); + + // Mark the tool as active + toolElement.classList.add("active"); + } + }); + + container.appendChild(toolElement); + + // When patching the snabbdom vnode into a DOM element, the element is replaced. + // So we create a dummy sub element inside the tool element and patch the svg node into that. + // This results in the toolElement holding the content. When patching directly onto the toolElement, + // it would be replaced by the svg node and the tool class would be removed with it, which we don't want. + const subElement = document.createElement("div"); + toolElement.appendChild(subElement); + const svgNode = ( + + {name} + {svgCode} + + ); + this.patcherProvider.patcher(subElement, svgNode); + + const shortcutElement = document.createElement("kbd"); + shortcutElement.classList.add("shortcut"); + shortcutElement.textContent = enableKey?.replace("Key", "").replace("Digit", "") ?? ""; + toolElement.appendChild(shortcutElement); + + if (enableKey) { + this.keyboardShortcuts.set(enableKey, () => { + toolElement.click(); + }); + + // Also add the shortcut for the corresponding numpad key + if (enableKey.startsWith("Digit")) { + this.keyboardShortcuts.set(enableKey.replace("Digit", "Numpad") as KeyCode, () => { + toolElement.click(); + }); + } + } + } + + private disableTools(): void { + this.allTools.forEach((tool) => tool.disable()); + this.markAllToolsInactive(); + } + + private markAllToolsInactive(): void { + if (!this.containerElement) return; + + // Remove active class from all tools, resulting in none of the tools being shown as active + this.containerElement.childNodes.forEach((node) => { + if (node instanceof HTMLElement) { + node.classList.remove("active"); + } + }); + } + + handle(action: Action): void | Action | ICommand { + // Some change has been made to the model. + // This may indicate the end of a tool action, so we show all tools to be inactive. + if (action.kind === CommitModelAction.KIND) { + this.markAllToolsInactive(); + } + } + + keyDown(_element: SModelElementImpl, event: KeyboardEvent): Action[] { + this.keyboardShortcuts.forEach((callback, key) => { + if (matchesKeystroke(event, key)) { + callback(); + } + }); + + return []; + } + + keyUp(): Action[] { + // ignored + return []; + } +} From 4101e430025b90c2bb8e328f8508ff8d1d18ab24 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Mon, 17 Nov 2025 18:31:56 +0100 Subject: [PATCH 21/41] add simple constraint menu --- .../src/constraint/ConstraintMenu.ts | 317 ++++++++++++++++++ .../src/constraint/constraintMenu.css | 145 ++++++++ .../src/constraint/constraintRegistry.ts | 102 ++++++ .../webEditor/src/constraint/di.config.ts | 13 + frontend/webEditor/src/index.ts | 4 +- 5 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 frontend/webEditor/src/constraint/ConstraintMenu.ts create mode 100644 frontend/webEditor/src/constraint/constraintMenu.css create mode 100644 frontend/webEditor/src/constraint/constraintRegistry.ts create mode 100644 frontend/webEditor/src/constraint/di.config.ts diff --git a/frontend/webEditor/src/constraint/ConstraintMenu.ts b/frontend/webEditor/src/constraint/ConstraintMenu.ts new file mode 100644 index 00000000..aa317127 --- /dev/null +++ b/frontend/webEditor/src/constraint/ConstraintMenu.ts @@ -0,0 +1,317 @@ +import { inject, injectable } from "inversify"; +import "./constraintMenu.css"; +import { IActionDispatcher, LocalModelSource, TYPES } from "sprotty"; +import { ConstraintRegistry } from "./constraintRegistry"; + +// Enable hover feature that is used to show validation errors. +// Inline completions are enabled to allow autocompletion of keywords and inputs/label types/label values. +import "monaco-editor/esm/vs/editor/contrib/hover/browser/hoverContribution"; +import "monaco-editor/esm/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.js"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { SETTINGS } from "../settings/Settings"; +import { EditorModeController } from "../settings/editorMode"; +import { AccordionUiExtension } from "../accordionUiExtension"; + +@injectable() +export class ConstraintMenu extends AccordionUiExtension { + static readonly ID = "constraint-menu"; + private editorContainer: HTMLDivElement = document.createElement("div") as HTMLDivElement; + private validationLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; + private editor?: monaco.editor.IStandaloneCodeEditor; + private forceReadOnly: boolean; + private optionsMenu?: HTMLDivElement; + private ignoreCheckboxChange = false; + + constructor( + @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(TYPES.ModelSource) modelSource: LocalModelSource, + @inject(TYPES.IActionDispatcher) private readonly dispatcher: IActionDispatcher, + @inject(SETTINGS.Mode) + editorModeController: EditorModeController, + ) { + super("left", "up"); + this.constraintRegistry = constraintRegistry; + this.forceReadOnly = editorModeController.get() !== "edit"; + editorModeController.registerListener(() => { + this.forceReadOnly = editorModeController.isReadOnly(); + }); + constraintRegistry.onUpdate(() => { + if (this.editor) { + const editorText = this.editor.getValue(); + // Only update the editor if the constraints have changed + if (editorText !== this.constraintRegistry.getConstraintsAsText()) { + this.editor.setValue(this.constraintRegistry.getConstraintsAsText() || ""); + } + } + }); + } + + id(): string { + return ConstraintMenu.ID; + } + containerClass(): string { + return ConstraintMenu.ID; + } + + + protected initializeHeaderContent(headerElement: HTMLElement): void { + headerElement.id = 'constraint-menu-expand-title' + headerElement.innerText = 'Constraints' + headerElement.appendChild(this.buildOptionsButton()); + } + + protected initializeHidableContent(contentElement: HTMLElement): void { + const contentDiv = document.createElement("div"); + contentDiv.id = "constraint-menu-content"; + contentDiv.appendChild(this.buildConstraintInputWrapper()); + contentElement.appendChild(contentDiv) + } + + protected initializeContents(containerElement: HTMLElement): void { + super.initializeContents(containerElement); + containerElement.appendChild(this.buildRunButton()); + } + + private buildConstraintInputWrapper(): HTMLElement { + const wrapper = document.createElement("div"); + wrapper.id = "constraint-menu-input"; + wrapper.appendChild(this.editorContainer); + this.validationLabel.id = "validation-label"; + this.validationLabel.classList.add("valid"); + this.validationLabel.innerText = "Valid constraints"; + wrapper.appendChild(this.validationLabel); + const keyboardShortcutLabel = document.createElement("div"); + keyboardShortcutLabel.innerHTML = "Press CTRL+Space for autocompletion"; + wrapper.appendChild(keyboardShortcutLabel); + + const monacoTheme = /*ThemeManager.useDarkMode ?*/ "vs-dark" //: "vs"; + this.editor = monaco.editor.create(this.editorContainer, { + minimap: { + // takes too much space, not useful for our use case + enabled: false, + }, + folding: false, // Not supported by our language definition + wordBasedSuggestions: "off", // Does not really work for our use case + scrollBeyondLastLine: false, // Not needed + theme: monacoTheme, + wordWrap: "on", + //language: DSL_LANGUAGE_ID, + scrollBeyondLastColumn: 0, + scrollbar: { + horizontal: "hidden", + vertical: "auto", + // avoid can not scroll page when hover monaco + alwaysConsumeMouseWheel: false, + }, + lineNumbers: "on", + readOnly: this.forceReadOnly, + }); + + this.editor.setValue(this.constraintRegistry.getConstraintsAsText() || ""); + + this.editor.onDidChangeModelContent(() => { + if (!this.editor) { + return; + } + + const model = this.editor?.getModel(); + if (!model) { + return; + } + + this.constraintRegistry.setConstraints(model.getLinesContent()); + + const content = model.getLinesContent(); + const marker: monaco.editor.IMarkerData[] = []; + const emptyContent = content.length == 0 || (content.length == 1 && content[0] === ""); + // empty content gets accepted as valid as it represents no constraints + if (!emptyContent) { + /*const errors = this.tree.verify(content); + marker.push( + ...errors.map((e) => ({ + severity: monaco.MarkerSeverity.Error, + startLineNumber: e.line, + startColumn: e.startColumn, + endLineNumber: e.line, + endColumn: e.endColumn, + message: e.message, + })), + );*/ + } + + this.validationLabel.innerText = + marker.length == 0 ? "Valid constraints" : `Invalid constraints: ${marker.length} errors`; + this.validationLabel.classList.toggle("valid", marker.length == 0); + + monaco.editor.setModelMarkers(model, "constraint", marker); + }); + + return wrapper; + } + + private buildRunButton(): HTMLElement { + const wrapper = document.createElement("div"); + wrapper.id = "run-button-container"; + + const button = document.createElement("button"); + button.id = "run-button"; + button.innerHTML = "Run"; + button.onclick = () => { + //this.dispatcher.dispatch(AnalyzeDiagramAction.create()); + }; + + wrapper.appendChild(button); + return wrapper; + } + + protected onBeforeShow(): void { + this.resizeEditor(); + } + + private resizeEditor(): void { + // Resize editor to fit content. + // Has ranges for height and width to prevent the editor from getting too small or too large. + const e = this.editor; + if (!e) { + return; + } + + // For the height we can use the content height from the editor. + const height = e.getContentHeight(); + + // For the width we cannot really do this. + // Monaco needs about 500ms to figure out the correct width when initially showing the editor. + // In the mean time the width will be too small and after the update + // the window size will jump visibly. + // So for the width we use this calculation to approximate the width. + const maxLineLength = e + .getValue() + .split("\n") + .reduce((max, line) => Math.max(max, line.length), 0); + const width = 100 + maxLineLength * 8; + + const clamp = (value: number, range: readonly [number, number]) => + Math.min(range[1], Math.max(range[0], value)); + + const heightRange = [200, 200] as const; + const widthRange = [500, 750] as const; + + const cHeight = clamp(height, heightRange); + const cWidth = clamp(width, widthRange); + + e.layout({ height: cHeight, width: cWidth }); + } + + switchTheme(useDark: boolean): void { + this.editor?.updateOptions({ theme: useDark ? "vs-dark" : "vs" }); + } + + private buildOptionsButton(): HTMLElement { + const btn = document.createElement("button"); + btn.id = "constraint-options-button"; + btn.title = "Filter…"; + btn.innerHTML = ''; + btn.onclick = () => this.toggleOptionsMenu(); + return btn; + } + + /** show or hide the menu, generate checkboxes on the fly */ + private toggleOptionsMenu(): void { + if (this.optionsMenu !== undefined) { + this.optionsMenu.remove(); + this.optionsMenu = undefined; + return; + } + + // 1) create container + this.optionsMenu = document.createElement("div"); + this.optionsMenu.id = "constraint-options-menu"; + + // 2) add the “All constraints” checkbox at the top + const allConstraints = document.createElement("label"); + allConstraints.classList.add("options-item"); + + const allCb = document.createElement("input"); + allCb.type = "checkbox"; + allCb.value = "ALL"; + allCb.checked = this.constraintRegistry + .getConstraintList() + .map((c) => c.name) + .every((c) => this.constraintRegistry.getSelectedConstraints().includes(c)); + + allCb.onchange = () => { + if (!this.optionsMenu) return; + + this.ignoreCheckboxChange = true; + try { + if (allCb.checked) { + this.optionsMenu.querySelectorAll("input[type=checkbox]").forEach((cb) => { + if (cb !== allCb) cb.checked = true; + }); + /*this.dispatcher.dispatch( + ChooseConstraintAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), + );*/ + } else { + this.optionsMenu.querySelectorAll("input[type=checkbox]").forEach((cb) => { + if (cb !== allCb) cb.checked = false; + }); + //this.dispatcher.dispatch(ChooseConstraintAction.create([])); + } + } finally { + this.ignoreCheckboxChange = false; + } + }; + + allConstraints.appendChild(allCb); + allConstraints.appendChild(document.createTextNode("All constraints")); + this.optionsMenu.appendChild(allConstraints); + + // 2) pull your dynamic items + const items = this.constraintRegistry.getConstraintList(); + + // 3) for each item build a checkbox + items.forEach((item) => { + const label = document.createElement("label"); + label.classList.add("options-item"); + + const cb = document.createElement("input"); + cb.type = "checkbox"; + cb.value = item.name; + cb.checked = this.constraintRegistry.getSelectedConstraints().includes(cb.value); + + cb.onchange = () => { + if (this.ignoreCheckboxChange) return; + + const checkboxes = this.optionsMenu!.querySelectorAll("input[type=checkbox]"); + const individualCheckboxes = Array.from(checkboxes).filter((cb) => cb !== allCb); + const selected = individualCheckboxes.filter((cb) => cb.checked).map((cb) => cb.value); + + allCb.checked = individualCheckboxes.every((cb) => cb.checked); + + //this.dispatcher.dispatch(ChooseConstraintAction.create(selected)); + }; + + label.appendChild(cb); + label.appendChild(document.createTextNode(item.name)); + this.optionsMenu!.appendChild(label); + }); + + this.editorContainer.appendChild(this.optionsMenu); + + // optional: click-outside handler + const onClickOutside = (e: MouseEvent) => { + const target = e.target as Node; + if (!this.optionsMenu || this.optionsMenu.contains(target)) return; + + const button = document.getElementById("constraint-options-button"); + if (button && button.contains(target)) return; + + this.optionsMenu.remove(); + this.optionsMenu = undefined; + document.removeEventListener("click", onClickOutside); + }; + document.addEventListener("click", onClickOutside); + } +} diff --git a/frontend/webEditor/src/constraint/constraintMenu.css b/frontend/webEditor/src/constraint/constraintMenu.css new file mode 100644 index 00000000..5801bc35 --- /dev/null +++ b/frontend/webEditor/src/constraint/constraintMenu.css @@ -0,0 +1,145 @@ +div.constraint-menu { + right: 20px; + bottom: 20px; + padding: 10px 10px; +} + +.accordion-content:has(.monaco-editor.focused) * { + overflow: visible; +} + +#constraint-menu-expand-title { + padding-right: 85px; +} + +#run-button-container { + position: absolute; + right: 6px; + bottom: 6px; + width: 80px; + z-index: 50; +} + +#run-button { + background-color: green; + color: white; + border: none; + border-radius: 8px; + padding: 5px 10px; + text-align: center; + text-decoration: none; + display: inline-block; + width: 100%; + cursor: pointer; +} + +#run-button::before { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/solid/play.svg"); + display: inline-block; + filter: invert(1); + height: 16px; + width: 16px; + background-size: 16px 16px; + vertical-align: text-top; +} + +#constraint-menu-input { + min-width: 300px; +} + +#constraint-menu-input * { + overflow: visible; +} + +#constraint-menu-input .overflow-guard { + overflow: hidden; +} + +#validation-label { + height: 1rem; + color: var(--color-error); +} + +#validation-label.valid { + color: var(--color-valid); +} + +#constraint-menu-list { + grid-row-start: 1; + grid-row-end: 2; + grid-column-start: 2; + overflow: scroll; + max-height: 210px; +} + +#constraint-menu-list * { + color: var(--color-foreground); +} + +.constrain-label input { + background-color: var(--color-background); + text-align: center; + border: 1px solid var(--color-foreground); + border-radius: 15px; + padding: 3px; + margin: 4px; +} + +.constrain-label.selected input { + border: 2px solid var(--color-foreground); +} + +.constrain-label button { + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; +} + +.constraint-add { + padding: 0; + border: none; + background-color: transparent; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; +} + +#constraint-options-button { + position: absolute; + top: 6px; + right: 6px; + background: transparent; + border: none; + font-size: 1.2em; + cursor: pointer; + color: var(--color-foreground); + padding: 2px; +} + +#constraint-options-menu { + position: absolute; + top: 30px; /* just under the header */ + right: 6px; + background: var(--color-background); + border: 1px solid var(--color-foreground); + border-radius: 4px; + padding: 8px; + z-index: 100; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); +} + +#constraint-options-menu .options-item { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; + font-size: 0.9em; + color: var(--color-foreground); +} + +#constraint-options-menu .options-item:last-child { + margin-bottom: 0; +} diff --git a/frontend/webEditor/src/constraint/constraintRegistry.ts b/frontend/webEditor/src/constraint/constraintRegistry.ts new file mode 100644 index 00000000..3b773635 --- /dev/null +++ b/frontend/webEditor/src/constraint/constraintRegistry.ts @@ -0,0 +1,102 @@ +import { injectable } from "inversify"; + +export interface Constraint { + name: string; + constraint: string; +} + +@injectable() +export class ConstraintRegistry { + private constraints: Constraint[] = []; + private updateCallbacks: (() => void)[] = []; + private selectedConstraints: string[] = this.constraints.map((c) => c.name); + + public setConstraints(constraints: string[]): void { + this.constraints = this.splitIntoConstraintTexts(constraints).map((c) => this.mapToConstraint(c)); + } + + public setConstraintsFromArray(constraints: Constraint[]): void { + this.constraints = constraints.map((c) => ({ + name: c.name, + constraint: c.constraint, + })); + this.constraintListChanged(); + } + + public setSelectedConstraints(constraints: string[]): void { + this.selectedConstraints = constraints; + } + + public getSelectedConstraints(): string[] { + return this.selectedConstraints; + } + + public clearConstraints(): void { + this.constraints = []; + this.constraintListChanged(); + } + + public constraintListChanged(): void { + this.updateCallbacks.forEach((cb) => cb()); + } + + public onUpdate(callback: () => void): void { + this.updateCallbacks.push(callback); + } + + public getConstraintsAsText(): string { + return this.constraints.map((c) => `- ${c.name}: ${c.constraint}`).join("\n"); + } + + public getConstraintList(): Constraint[] { + return this.constraints; + } + + public selectedContainsAllConstraints(): boolean { + return this.getConstraintList() + .map((c) => c.name) + .every((c) => this.getSelectedConstraints().includes(c)); + } + + public setAllConstraintsAsSelected(): void { + this.selectedConstraints = this.constraints.map((c) => c.name); + } + + private splitIntoConstraintTexts(text: string[]): string[] { + const constraints: string[] = []; + let currentConstraint = ""; + for (const line of text) { + if (line.startsWith("- ")) { + if (currentConstraint !== "") { + constraints.push(currentConstraint); + } + currentConstraint = line; + } else { + currentConstraint += `\n${line}`; + } + } + if (currentConstraint !== "") { + constraints.push(currentConstraint); + } + return constraints; + } + + private mapToConstraint(constraint: string): Constraint { + // the brackets ensure its a capturing split + const parts = constraint.split(/(\s+)/); + // if less than 3 parts are present no name or constraint can be extracted (e.g. "- " -> ["-", " "]) + if (parts.length < 3) { + return { name: "", constraint: "" }; + } + let name = parts[2]; + if (name.endsWith(":")) { + name = name.slice(0, -1); + } + let constraintText = ""; + // the first 4 parts are "- ", whitespace, `${name}:`, whitespace --> Thus the constraint starts at index 4 + for (let i = 4; i < parts.length; i++) { + constraintText += parts[i]; + } + return { name, constraint: constraintText }; + } +} diff --git a/frontend/webEditor/src/constraint/di.config.ts b/frontend/webEditor/src/constraint/di.config.ts new file mode 100644 index 00000000..fb9d63f5 --- /dev/null +++ b/frontend/webEditor/src/constraint/di.config.ts @@ -0,0 +1,13 @@ +import { ContainerModule } from "inversify"; +import { TYPES } from "sprotty"; +import { EDITOR_TYPES } from "../editorTypes"; +import { ConstraintMenu } from "./ConstraintMenu"; +import { ConstraintRegistry } from "./constraintRegistry"; + +export const constraintModule = new ContainerModule((bind) => { + bind(ConstraintRegistry).toSelf().inSingletonScope(); + + bind(ConstraintMenu).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(ConstraintMenu); + bind(EDITOR_TYPES.DefaultUIElement).toService(ConstraintMenu); +}) \ No newline at end of file diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index ee68f31a..4c16ee9a 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -20,6 +20,7 @@ import { elkLayoutModule } from "sprotty-elk"; import { fileNameModule } from "./fileName/di.config"; import { settingsModule } from "./settings/di.config"; import { toolPaletteModule } from "./toolPalette/di.config"; +import { constraintModule } from "./constraint/di.config"; const container = new Container(); @@ -43,7 +44,8 @@ container.load( layoutModule, fileNameModule, settingsModule, - toolPaletteModule + toolPaletteModule, + constraintModule ) const startUpAgents = container.getAll(StartUpAgent) From eb8851d8c19fe6d57f0125d3bef06598edc00c48 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 21 Nov 2025 16:26:10 +0100 Subject: [PATCH 22/41] simple dsl language implementation --- .../src/constraint/ConstraintMenu.ts | 21 +- frontend/webEditor/src/constraint/language.ts | 379 ++++++++++++++++++ .../webEditor/src/languages/autocomplete.ts | 108 +++++ frontend/webEditor/src/languages/tokenize.ts | 48 +++ frontend/webEditor/src/languages/verify.ts | 85 ++++ frontend/webEditor/src/languages/words.ts | 81 ++++ 6 files changed, 719 insertions(+), 3 deletions(-) create mode 100644 frontend/webEditor/src/constraint/language.ts create mode 100644 frontend/webEditor/src/languages/autocomplete.ts create mode 100644 frontend/webEditor/src/languages/tokenize.ts create mode 100644 frontend/webEditor/src/languages/verify.ts create mode 100644 frontend/webEditor/src/languages/words.ts diff --git a/frontend/webEditor/src/constraint/ConstraintMenu.ts b/frontend/webEditor/src/constraint/ConstraintMenu.ts index aa317127..ab0233d6 100644 --- a/frontend/webEditor/src/constraint/ConstraintMenu.ts +++ b/frontend/webEditor/src/constraint/ConstraintMenu.ts @@ -12,6 +12,11 @@ import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SETTINGS } from "../settings/Settings"; import { EditorModeController } from "../settings/editorMode"; import { AccordionUiExtension } from "../accordionUiExtension"; +import { LanguageTreeNode, tokenize } from "../languages/tokenize"; +import { Word } from "../languages/words"; +import { constraintDslLanguageMonarchDefinition, ConstraintDslTreeBuilder, DSL_LANGUAGE_ID } from "./language"; +import { verify } from "../languages/verify"; +import { DfdCompletionItemProvider } from "../languages/autocomplete"; @injectable() export class ConstraintMenu extends AccordionUiExtension { @@ -22,6 +27,7 @@ export class ConstraintMenu extends AccordionUiExtension { private forceReadOnly: boolean; private optionsMenu?: HTMLDivElement; private ignoreCheckboxChange = false; + private readonly tree: LanguageTreeNode[] constructor( @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, @@ -46,6 +52,8 @@ export class ConstraintMenu extends AccordionUiExtension { } } }); + + this.tree = ConstraintDslTreeBuilder.buildTree(modelSource, labelTypeRegistry) } id(): string { @@ -86,6 +94,13 @@ export class ConstraintMenu extends AccordionUiExtension { keyboardShortcutLabel.innerHTML = "Press CTRL+Space for autocompletion"; wrapper.appendChild(keyboardShortcutLabel); + monaco.languages.register({ id: DSL_LANGUAGE_ID }); + monaco.languages.setMonarchTokensProvider(DSL_LANGUAGE_ID, constraintDslLanguageMonarchDefinition); + monaco.languages.registerCompletionItemProvider( + DSL_LANGUAGE_ID, + new DfdCompletionItemProvider(this.tree), + ); + const monacoTheme = /*ThemeManager.useDarkMode ?*/ "vs-dark" //: "vs"; this.editor = monaco.editor.create(this.editorContainer, { minimap: { @@ -97,7 +112,7 @@ export class ConstraintMenu extends AccordionUiExtension { scrollBeyondLastLine: false, // Not needed theme: monacoTheme, wordWrap: "on", - //language: DSL_LANGUAGE_ID, + language: DSL_LANGUAGE_ID, scrollBeyondLastColumn: 0, scrollbar: { horizontal: "hidden", @@ -128,7 +143,7 @@ export class ConstraintMenu extends AccordionUiExtension { const emptyContent = content.length == 0 || (content.length == 1 && content[0] === ""); // empty content gets accepted as valid as it represents no constraints if (!emptyContent) { - /*const errors = this.tree.verify(content); + const errors = verify(tokenize(content), this.tree) marker.push( ...errors.map((e) => ({ severity: monaco.MarkerSeverity.Error, @@ -138,7 +153,7 @@ export class ConstraintMenu extends AccordionUiExtension { endColumn: e.endColumn, message: e.message, })), - );*/ + ); } this.validationLabel.innerText = diff --git a/frontend/webEditor/src/constraint/language.ts b/frontend/webEditor/src/constraint/language.ts new file mode 100644 index 00000000..b3e1abd6 --- /dev/null +++ b/frontend/webEditor/src/constraint/language.ts @@ -0,0 +1,379 @@ +import { LocalModelSource } from "sprotty"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { AnyWord, ConstantWord, NegatableWord, Word } from "../languages/words"; +import { LanguageTreeNode } from "../languages/tokenize"; +import { SModelRoot } from "sprotty-protocol"; +import { ArrowEdge } from "../diagram/edges/ArrowEdge"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { WordCompletion } from "../languages/autocomplete"; + +export const DSL_LANGUAGE_ID = "dfd-constraint" + +export const constraintDslLanguageMonarchDefinition: monaco.languages.IMonarchLanguage = { + keywords: ["data", "vertex", "neverFlows", "to", "where", "named", "present", "empty", "type"], + + symbols: /[=>[] { + const conditions = getConditionalSelectors(); + const conditionalSelector: LanguageTreeNode = { + word: new ConstantWord("where"), + children: conditions, + }; + + const destinationSelectors = getAbstractSelectors(modelSource, labelTypeRegistry); + destinationSelectors.forEach((destinationSelector) => { + getLeaves(destinationSelector).forEach((n) => { + n.canBeFinal = true; + n.children.push(conditionalSelector); + }); + }); + const nodeDestinationSelector: LanguageTreeNode = { + word: new ConstantWord("vertex"), + children: destinationSelectors, + }; + + const neverFlows: LanguageTreeNode = { + word: new ConstantWord("neverFlows"), + children: [nodeDestinationSelector, conditionalSelector], + canBeFinal: true, + }; + + const dataSourceSelector: LanguageTreeNode = { + word: new ConstantWord("data"), + children: [], + }; + + const nodeSelectors = getAbstractSelectors(modelSource, labelTypeRegistry); + nodeSelectors.forEach((nodeSelector) => { + getLeaves(nodeSelector).forEach((n) => { + n.children.push(dataSourceSelector); + n.children.push(neverFlows); + }); + }); + const nodeSourceSelector: LanguageTreeNode = { + word: new ConstantWord("vertex"), + children: nodeSelectors, + }; + + const dataSelectors = getAbstractSelectors(modelSource, labelTypeRegistry); + dataSelectors.forEach((dataSelector) => { + getLeaves(dataSelector).forEach((n) => { + n.children.push(nodeSourceSelector); + n.children.push(neverFlows); + }); + }); + dataSourceSelector.children = dataSelectors; + + const nameNode: LanguageTreeNode = { + word: new NameWord(), + children: [nodeSourceSelector, dataSourceSelector], + }; + + const startNode: LanguageTreeNode = { + word: new ConstantWord("-"), + children: [nameNode], + }; + + return [startNode]; + } + + function getLeaves(node: LanguageTreeNode): LanguageTreeNode[] { + if (node.children.length == 0) { + return [node]; + } + let result: LanguageTreeNode[] = []; + for (const n of node.children) { + result = result.concat(getLeaves(n)); + } + return result; + } + + function getAbstractSelectors( + modelSource: LocalModelSource, + labelTypeRegistry: LabelTypeRegistry, + ): LanguageTreeNode[] { + const vertexTypeSelector: LanguageTreeNode = { + word: new ConstantWord("type"), + children: [ + new NegatableWord(new ConstantWord("EXTERNAL")), + new NegatableWord(new ConstantWord("PROCESS")), + new NegatableWord(new ConstantWord("STORE")), + ].map((w) => ({ word: w, children: [] })), + }; + const characteristicsSelector = { + word: new NegatableWord(new CharacteristicSelectorData(labelTypeRegistry)), + children: [], + }; + const dataCharacteristicListSelector = { + word: new NegatableWord(new CharacteristicSelectorDataList(labelTypeRegistry)), + children: [], + }; + const variableNameSelector = { + word: new ConstantWord("named"), + children: [ + { + word: new VariableName(modelSource), + children: [], + }, + ], + }; + return [vertexTypeSelector, characteristicsSelector, dataCharacteristicListSelector, variableNameSelector]; + } + + function getConditionalSelectors(): LanguageTreeNode[] { + const variableConditionalSelector: LanguageTreeNode = { + word: new ConstantWord("present"), + children: [ + { + word: new NegatableWord(new ConstraintVariableReference()), + children: [], + }, + ], + }; + + const emptySetOperationSelector: LanguageTreeNode = { + word: new ConstantWord("empty"), + children: [ + { + word: new IntersectionWord(), + children: [], + }, + ], + }; + + return [variableConditionalSelector, emptySetOperationSelector]; + } + + class IntersectionWord implements Word { + private constraintVariableReference: ConstraintVariableReference; + + constructor() { + this.constraintVariableReference = new ConstraintVariableReference(); + } + + completionOptions(word: string): WordCompletion[] { + if (!word.startsWith("intersection(")) { + if (!"intersection(".includes(word)) { + return []; + } + return [ + { + label: "intersection()", + insertText: "intersection($0)", + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: monaco.languages.CompletionItemKind.Function, + }, + ]; + } + const attributes = word.substring("intersection(".length, word.length - 1).split(","); + if (attributes.length > 2) { + return []; + } + return this.constraintVariableReference.completionOptions(); + } + verify(word: string): string[] { + if (!word.startsWith("intersection(")) { + return ['Expected keyword "intersection"']; + } + const attributes = word.substring("intersection(".length, word.length - 1).split(","); + if (attributes.length > 2) { + return ['Expected at most 2 attributes in "intersection"']; + } + return attributes.flatMap((a) => this.constraintVariableReference.verify(a)); + } + } + + class ConstraintVariableReference extends AnyWord {} + + class CharacteristicSelectorData implements Word { + constructor(private readonly labelTypeRegistry: LabelTypeRegistry) {} + + completionOptions(word: string): WordCompletion[] { + const parts = word.split("."); + + if (parts.length == 1) { + return this.labelTypeRegistry.getLabelTypes().map((l) => ({ + insertText: l.name, + kind: monaco.languages.CompletionItemKind.Class, + })); + } else if (parts.length == 2) { + const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); + if (!type) { + return []; + } + + const possibleValues: WordCompletion[] = type.values.map((l) => ({ + insertText: l.text, + kind: monaco.languages.CompletionItemKind.Enum, + startOffset: parts[0].length + 1, + })); + possibleValues.push({ + insertText: "$" + type.name, + kind: monaco.languages.CompletionItemKind.Variable, + startOffset: parts[0].length + 1, + }); + return possibleValues; + } + + return []; + } + + verify(word: string): string[] { + const parts = word.split("."); + + if (parts.length > 2) { + return ["Expected at most 2 parts in characteristic selector"]; + } + + const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); + if (!type) { + return ['Unknown label type "' + parts[0] + '"']; + } + + if (parts.length < 2) { + return ["Expected characteristic to have value"]; + } + + if (parts[1].startsWith("$") && parts[1].length >= 2) { + return []; + } + + const label = type.values.find((l) => l.text === parts[1]); + if (!label) { + return ['Unknown label value "' + parts[1] + '" for type "' + parts[0] + '"']; + } + + return []; + } + } + + class NameWord implements Word { + completionOptions(word: string): WordCompletion[] { + if (word.length === 0) { + return []; + } + return [ + { + insertText: ":", + kind: monaco.languages.CompletionItemKind.Keyword, + }, + ]; + } + + verify(word: string): string[] { + const name = word.split(":")[0]; + if (name.length === 0) { + return ["Expected a name"]; + } + if (!word.endsWith(":")) { + return ['Expected ":" at the end of name']; + } + return []; + } + } + + class VariableName implements Word { + constructor(private readonly modelSource: LocalModelSource) {} + + completionOptions(): WordCompletion[] { + return this.getAllPortNames().map((n) => ({ + insertText: n, + kind: monaco.languages.CompletionItemKind.Variable, + })); + } + verify(word: string): string[] { + if (this.getAllPortNames().includes(word)) { + return []; + } + return ['Unknown variable name "' + word + '"']; + } + + private getAllPortNames(): string[] { + const portEdgeNameMap: Map = new Map(); + const graph = this.modelSource.model as SModelRoot; + if (graph.children === undefined) { + return []; + } + for (const element of graph.children) { + const edge = element as ArrowEdge; + if (edge.text !== undefined && edge.targetId !== undefined) { + const edgeName = edge.text!; + const target = edge.targetId; + if (portEdgeNameMap.has(target)) { + portEdgeNameMap.get(target)?.push(edgeName); + } else { + portEdgeNameMap.set(target, [edgeName]); + } + } + } + + return Array.from(portEdgeNameMap.keys()).map((key) => portEdgeNameMap.get(key)!.sort().join("|")); + } + } + + class CharacteristicSelectorDataList implements Word { + private characteristicSelectorData: CharacteristicSelectorData; + + constructor(labelTypeRegistry: LabelTypeRegistry) { + this.characteristicSelectorData = new CharacteristicSelectorData(labelTypeRegistry); + } + + completionOptions(word: string): WordCompletion[] { + const parts = word.split(","); + const last = parts[parts.length - 1]; + + return this.characteristicSelectorData.completionOptions(last); + } + verify(word: string): string[] { + const parts = word.split(","); + for (let i = 0; i < parts.length; i++) { + const r = this.characteristicSelectorData.verify(parts[i]); + if (r.length > 0) { + return r; + } + } + + return []; + } + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/languages/autocomplete.ts b/frontend/webEditor/src/languages/autocomplete.ts new file mode 100644 index 00000000..856c77fb --- /dev/null +++ b/frontend/webEditor/src/languages/autocomplete.ts @@ -0,0 +1,108 @@ +import { LanguageTreeNode, Token, tokenize } from "./tokenize"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { VerifyWord } from "./verify"; + +interface RequiredCompletionParts { + kind: monaco.languages.CompletionItemKind; + insertText: string; + startOffset?: number; +} + +export type WordCompletion = RequiredCompletionParts & Partial; + +export interface CompletionWord extends VerifyWord { + completionOptions(currentWord: string): WordCompletion[]; +} +type CompletionLanguageTreeNode = LanguageTreeNode; + +export function complete(tokens: Token[], tree: CompletionLanguageTreeNode[]): monaco.languages.CompletionItem[] { + return transformResults(completeNode(tokens, tree, 0, tree), tokens); +} + +function completeNode( + tokens: Token[], + nodes: CompletionLanguageTreeNode[], + index: number, + roots: CompletionLanguageTreeNode[], + cameFromFinal = false, + skipStartCheck = false, +): WordCompletion[] { + // check for new start + if (!skipStartCheck && tokens[index].column == 1) { + const matchesAnyRoot = roots.some((n) => n.word.verify(tokens[index].text).length === 0); + if (matchesAnyRoot) { + return completeNode(tokens, roots, index, roots, cameFromFinal, true); + } else if (cameFromFinal || nodes.length == 0) { + return completeNode(tokens, [...roots, ...nodes], index, roots, cameFromFinal, true); + } + } + + let result: WordCompletion[] = []; + if (index == tokens.length - 1) { + for (const node of nodes) { + result = result.concat(node.word.completionOptions(tokens[index].text)); + } + return result; + } + for (const n of nodes) { + if (n.word.verify(tokens[index].text).length > 0) { + continue; + } + result = result.concat(completeNode(tokens, n.children, index + 1, roots, n.canBeFinal || false)); + } + return result; +} + +function transformResults(comp: WordCompletion[], tokens: Token[]): monaco.languages.CompletionItem[] { + const result: monaco.languages.CompletionItem[] = []; + const filtered = comp.filter( + (c, idx) => comp.findIndex((c2) => c2.insertText === c.insertText && c2.kind === c.kind) === idx, + ); + for (const c of filtered) { + const r = transformResult(c, tokens); + result.push(r); + } + return result; +} + +function transformResult(comp: WordCompletion, tokens: Token[]): monaco.languages.CompletionItem { + const wordStart = tokens.length == 0 ? 1 : tokens[tokens.length - 1].column; + const lineNumber = tokens.length == 0 ? 1 : tokens[tokens.length - 1].line; + return { + insertText: comp.insertText, + kind: comp.kind, + label: comp.label ?? comp.insertText, + insertTextRules: comp.insertTextRules, + range: new monaco.Range( + lineNumber, + wordStart + (comp.startOffset ?? 0), + lineNumber, + wordStart + (comp.startOffset ?? 0) + comp.insertText.length, + ), + }; +} + +export class DfdCompletionItemProvider implements monaco.languages.CompletionItemProvider { + constructor(private tree: CompletionLanguageTreeNode[]) {} + + triggerCharacters = [".", "(", " ", ","]; + + provideCompletionItems( + model: monaco.editor.ITextModel, + position: monaco.Position, + ): monaco.languages.ProviderResult { + const allLines = model.getLinesContent(); + const includedLines: string[] = []; + for (let i = 0; i < position.lineNumber - 1; i++) { + includedLines.push(allLines[i]); + } + const currentLine = allLines[position.lineNumber - 1].substring(0, position.column - 1); + includedLines.push(currentLine); + + const tokens = tokenize(includedLines); + const r = complete(tokens, this.tree); + return { + suggestions: r, + }; + } +} diff --git a/frontend/webEditor/src/languages/tokenize.ts b/frontend/webEditor/src/languages/tokenize.ts new file mode 100644 index 00000000..6420568e --- /dev/null +++ b/frontend/webEditor/src/languages/tokenize.ts @@ -0,0 +1,48 @@ +export interface Token { + text: string; + line: number; + column: number; + whiteSpaceAfter?: string; +} + +export interface LanguageTreeNode { + word: W; + children: LanguageTreeNode[]; + canBeFinal?: boolean; + viewAsLeaf?: boolean; +} + +export function tokenize(text: string[]): Token[] { + if (!text || text.length == 0) { + return []; + } + + const tokens: Token[] = []; + for (const [lineNumber, line] of text.entries()) { + const lineTokens = line.split(/(\s+)/); + let column = 0; + for (let i = 0; i < lineTokens.length; i += 2) { + const token = lineTokens[i]; + if (token.length > 0) { + tokens.push({ + text: token, + line: lineNumber + 1, + column: column + 1, + whiteSpaceAfter: lineTokens[i + 1], + }); + } + column += token.length; + column += lineTokens[i + 1] ? lineTokens[i + 1].length : 0; // Add whitespace length + } + if (lineTokens.length % 2 == 1) { + tokens.push({ + text: "", + line: lineNumber + 1, + column + }) + } + } + + + return tokens; +} diff --git a/frontend/webEditor/src/languages/verify.ts b/frontend/webEditor/src/languages/verify.ts new file mode 100644 index 00000000..6dfb8f52 --- /dev/null +++ b/frontend/webEditor/src/languages/verify.ts @@ -0,0 +1,85 @@ +import { LanguageTreeNode, Token } from "./tokenize"; + +export interface ValidationError { + message: string; + line: number; + startColumn: number; + endColumn: number; +} + +export interface VerifyWord { + verify: (word: string) => string[]; +} +type VerifyLanguageTreeNode = LanguageTreeNode; + +export function verify(tokens: Token[], tree: VerifyLanguageTreeNode[]): ValidationError[] { + return verifyNode(tokens, tree, 0, false, tree, true); +} + +function verifyNode( + tokens: Token[], + nodes: VerifyLanguageTreeNode[], + index: number, + comesFromFinal: boolean, + roots: VerifyLanguageTreeNode[], + skipStartCheck = false, +): ValidationError[] { + if (index >= tokens.length) { + if (nodes.length == 0 || comesFromFinal) { + return []; + } else { + return [ + { + message: "Unexpected end of line", + line: tokens[index - 1].line, + startColumn: tokens[index - 1].column + tokens[index - 1].text.length - 1, + endColumn: tokens[index - 1].column + tokens[index - 1].text.length, + }, + ]; + } + } + if (!skipStartCheck && tokens[index].column == 1) { + const matchesAnyRoot = roots.some((r) => r.word.verify(tokens[index].text).length === 0); + if (matchesAnyRoot) { + return verifyNode(tokens, roots, index, false, roots, true); + } + } + + const foundErrors: ValidationError[] = []; + let childErrors: ValidationError[] = []; + for (const n of nodes) { + const v = n.word.verify(tokens[index].text); + if (v.length > 0) { + foundErrors.push({ + message: v[0], + startColumn: tokens[index].column, + endColumn: tokens[index].column + tokens[index].text.length, + line: tokens[index].line, + }); + continue; + } + + const childResult = verifyNode(tokens, n.children, index + 1, n.canBeFinal || false, roots); + if (childResult.length == 0) { + return []; + } else { + childErrors = childErrors.concat(childResult); + } + } + if (childErrors.length > 0) { + return deduplicateErrors(childErrors); + } + return deduplicateErrors(foundErrors); +} + +function deduplicateErrors(errors: ValidationError[]): ValidationError[] { + const seen = new Set(); + return errors.filter((error) => { + const key = `${error.line}-${error.startColumn}-${error.endColumn}-${error.message}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); +} diff --git a/frontend/webEditor/src/languages/words.ts b/frontend/webEditor/src/languages/words.ts new file mode 100644 index 00000000..2de55da3 --- /dev/null +++ b/frontend/webEditor/src/languages/words.ts @@ -0,0 +1,81 @@ +import { CompletionWord, WordCompletion } from "./autocomplete"; +import { VerifyWord } from "./verify"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; + +export type Word = VerifyWord & CompletionWord; + +export class ConstantWord implements Word { + constructor(private readonly word: string) {} + + verify(word: string) { + if (word === this.word) { + return []; + } else { + return [`Expected keyword "${this.word}"`]; + } + } + completionOptions(): WordCompletion[] { + return [ + { + insertText: this.word, + kind: monaco.languages.CompletionItemKind.Keyword, + }, + ]; + } +} + +export class AnyWord implements Word { + verify(word: string): string[] { + if (word.length > 0) { + return []; + } else { + return ["Expected a symbol"]; + } + } + completionOptions(): WordCompletion[] { + return []; + } +} + +export class NegatableWord implements Word { + constructor(protected word: Word) {} + + verify(word: string): string[] { + if (word.startsWith("!")) { + return this.word.verify(word.substring(1)); + } + return this.word.verify(word); + } + + completionOptions(part: string): WordCompletion[] { + if (part.startsWith("!")) { + const options = this.word.completionOptions(part.substring(1)); + return options.map((o) => ({ + ...o, + startOffset: (o.startOffset ?? 0) + 1, + })); + } + return this.word.completionOptions(part); + } +} + +export class ListWord implements Word { + constructor(protected word: Word) {} + + verify(word: string): string[] { + const parts = word.split(","); + for (const part of parts) { + const verify = this.word.verify(part); + if (verify.length > 0) { + return verify; + } + } + return []; + } + completionOptions(word: string): WordCompletion[] { + const parts = word.split(","); + const last = parts[parts.length - 1]; + + return this.word.completionOptions(last); + } +} From cceb1c5cd85722429826b784b58a6d0e9d444ae4 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Wed, 26 Nov 2025 13:23:00 +0100 Subject: [PATCH 23/41] update tokenization --- frontend/webEditor/src/languages/tokenize.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/webEditor/src/languages/tokenize.ts b/frontend/webEditor/src/languages/tokenize.ts index 6420568e..f8fc9f9e 100644 --- a/frontend/webEditor/src/languages/tokenize.ts +++ b/frontend/webEditor/src/languages/tokenize.ts @@ -21,24 +21,22 @@ export function tokenize(text: string[]): Token[] { for (const [lineNumber, line] of text.entries()) { const lineTokens = line.split(/(\s+)/); let column = 0; - for (let i = 0; i < lineTokens.length; i += 2) { + for (let i = 0; i < lineTokens.length; i ++) { const token = lineTokens[i]; - if (token.length > 0) { + if (!token.match(/\s+/) && token.length > 0) { tokens.push({ text: token, line: lineNumber + 1, column: column + 1, - whiteSpaceAfter: lineTokens[i + 1], }); } column += token.length; - column += lineTokens[i + 1] ? lineTokens[i + 1].length : 0; // Add whitespace length } - if (lineTokens.length % 2 == 1) { + if (line.match(/\s$/) || line.length == 0) { tokens.push({ text: "", line: lineNumber + 1, - column + column: column + 1 }) } } From 084c6f3104e25d435551c68aac3e4fd1feca7af7 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 27 Nov 2025 12:07:06 +0100 Subject: [PATCH 24/41] analyze command --- .../src/constraint/ConstraintMenu.ts | 4 +- frontend/webEditor/src/serialize/analyze.ts | 47 +++++++++++++++++++ frontend/webEditor/src/serialize/di.config.ts | 2 + frontend/webEditor/src/serialize/loadJson.ts | 8 ++-- 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 frontend/webEditor/src/serialize/analyze.ts diff --git a/frontend/webEditor/src/constraint/ConstraintMenu.ts b/frontend/webEditor/src/constraint/ConstraintMenu.ts index ab0233d6..48fba732 100644 --- a/frontend/webEditor/src/constraint/ConstraintMenu.ts +++ b/frontend/webEditor/src/constraint/ConstraintMenu.ts @@ -17,6 +17,7 @@ import { Word } from "../languages/words"; import { constraintDslLanguageMonarchDefinition, ConstraintDslTreeBuilder, DSL_LANGUAGE_ID } from "./language"; import { verify } from "../languages/verify"; import { DfdCompletionItemProvider } from "../languages/autocomplete"; +import { AnalyzeAction } from "../serialize/analyze"; @injectable() export class ConstraintMenu extends AccordionUiExtension { @@ -174,7 +175,7 @@ export class ConstraintMenu extends AccordionUiExtension { button.id = "run-button"; button.innerHTML = "Run"; button.onclick = () => { - //this.dispatcher.dispatch(AnalyzeDiagramAction.create()); + this.dispatcher.dispatch(AnalyzeAction.create()); }; wrapper.appendChild(button); @@ -265,6 +266,7 @@ export class ConstraintMenu extends AccordionUiExtension { this.optionsMenu.querySelectorAll("input[type=checkbox]").forEach((cb) => { if (cb !== allCb) cb.checked = true; }); + // TODO /*this.dispatcher.dispatch( ChooseConstraintAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), );*/ diff --git a/frontend/webEditor/src/serialize/analyze.ts b/frontend/webEditor/src/serialize/analyze.ts new file mode 100644 index 00000000..17522371 --- /dev/null +++ b/frontend/webEditor/src/serialize/analyze.ts @@ -0,0 +1,47 @@ +import { ActionDispatcher, CommandExecutionContext, ILogger, TYPES } from "sprotty"; +import { FileData, LoadJsonCommand } from "./loadJson"; +import { CURRENT_VERSION, SavedDiagram } from "./SavedDiagram"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { SETTINGS } from "../settings/Settings"; +import { FileName } from "../fileName/fileName"; +import { DfdWebSocket } from "../webSocket/webSocket"; +import { inject } from "inversify"; +import { EditorModeController } from "../settings/editorMode"; +import { Action } from "sprotty-protocol"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; + +export namespace AnalyzeAction { + export const KIND = "analyze"; + + export function create(): Action { + return { kind: KIND }; + } +} +export class AnalyzeCommand extends LoadJsonCommand { + static readonly KIND = AnalyzeAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(FileName) fileName: FileName, + @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + ) { + super(logger, labelTypeRegistry, editorModeController, actionDispatcher, fileName); + } + + protected async getFile(context: CommandExecutionContext): Promise | undefined> { + const savedDiagram = { + model: context.modelFactory.createSchema(context.root), + labelTypes: this.labelTypeRegistry.getLabelTypes(), + constraints: this.constraintRegistry.getConstraintList(), + mode: this.editorModeController.get(), + version: CURRENT_VERSION + } + return await this.dfdWebSocket.requestDiagram("Json:" + JSON.stringify(savedDiagram)) + } + +} \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/di.config.ts b/frontend/webEditor/src/serialize/di.config.ts index cef99f48..b9361e4d 100644 --- a/frontend/webEditor/src/serialize/di.config.ts +++ b/frontend/webEditor/src/serialize/di.config.ts @@ -7,6 +7,7 @@ import { LoadPalladioFileCommand } from "./loadPalladioFile"; import { DfdModelFactory } from "./ModelFactory"; import { SaveJsonFileCommand } from "./saveJsonFile"; import { SaveDfdAndDdFileCommand } from "./saveDfdAndDdFile"; +import { AnalyzeCommand } from "./analyze"; export const serializeModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; @@ -16,6 +17,7 @@ export const serializeModule = new ContainerModule((bind, unbind, isBound, rebin configureCommand(context, LoadPalladioFileCommand); configureCommand(context, SaveJsonFileCommand) configureCommand(context, SaveDfdAndDdFileCommand) + configureCommand(context, AnalyzeCommand) rebind(TYPES.IModelFactory).to(DfdModelFactory); }) \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/loadJson.ts b/frontend/webEditor/src/serialize/loadJson.ts index 68e88b53..ff479d99 100644 --- a/frontend/webEditor/src/serialize/loadJson.ts +++ b/frontend/webEditor/src/serialize/loadJson.ts @@ -32,20 +32,20 @@ export abstract class LoadJsonCommand extends Command { constructor( private readonly logger: ILogger, - private readonly labelTypeRegistry: LabelTypeRegistry, - private editorModeController: EditorModeController, + protected readonly labelTypeRegistry: LabelTypeRegistry, + protected editorModeController: EditorModeController, private actionDispatcher: ActionDispatcher, protected fileName: FileName ) { super(); } - protected abstract getFile(): Promise | undefined>; + protected abstract getFile(context: CommandExecutionContext): Promise | undefined>; async execute(context: CommandExecutionContext): Promise { this.oldRoot = context.root; - this.file = await this.getFile().catch(() => undefined); + this.file = await this.getFile(context).catch(() => undefined); if (!this.file) { return context.root; } From 9a636ae39c8714cd02347f8498f4df38be8ae12f Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 27 Nov 2025 14:19:35 +0100 Subject: [PATCH 25/41] add constraint registry to save load --- frontend/webEditor/src/commonModule.ts | 2 +- .../webEditor/src/serialize/SavedDiagram.ts | 2 +- frontend/webEditor/src/serialize/analyze.ts | 4 ++-- .../src/serialize/loadDefaultDiagram.ts | 4 +++- .../src/serialize/loadDfdAndDdFile.ts | 4 +++- frontend/webEditor/src/serialize/loadJson.ts | 21 ++++++++++++++++--- .../webEditor/src/serialize/loadJsonFile.ts | 4 +++- .../src/serialize/loadPalladioFile.ts | 4 +++- .../src/serialize/saveDfdAndDdFile.ts | 8 ++++--- .../webEditor/src/serialize/saveJsonFile.ts | 4 +++- .../src/serialize/savedDiagramCreator.ts | 5 +++-- 11 files changed, 45 insertions(+), 17 deletions(-) diff --git a/frontend/webEditor/src/commonModule.ts b/frontend/webEditor/src/commonModule.ts index 7119ee2b..365e77d7 100644 --- a/frontend/webEditor/src/commonModule.ts +++ b/frontend/webEditor/src/commonModule.ts @@ -5,7 +5,7 @@ export const commonModule = new ContainerModule((bind, unbind, isBound, rebind) bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope(); rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); - rebind(TYPES.LogLevel).toConstantValue(LogLevel.log); // TODO: set to log again + rebind(TYPES.LogLevel).toConstantValue(LogLevel.log); const context = { bind, unbind, isBound, rebind }; configureViewerOptions(context, { zoomLimits: { min: 0.05, max: 20 }, diff --git a/frontend/webEditor/src/serialize/SavedDiagram.ts b/frontend/webEditor/src/serialize/SavedDiagram.ts index 2257b805..48ddb801 100644 --- a/frontend/webEditor/src/serialize/SavedDiagram.ts +++ b/frontend/webEditor/src/serialize/SavedDiagram.ts @@ -1,7 +1,7 @@ import { SModelRoot } from "sprotty-protocol"; import { Constraint } from "../constraint/Constraint"; -import { EditorMode } from "../editorMode/EditorMode"; import { LabelType } from "../labels/LabelType"; +import { EditorMode } from "../settings/editorMode"; export interface SavedDiagram { model: SModelRoot; diff --git a/frontend/webEditor/src/serialize/analyze.ts b/frontend/webEditor/src/serialize/analyze.ts index 17522371..5b0b3650 100644 --- a/frontend/webEditor/src/serialize/analyze.ts +++ b/frontend/webEditor/src/serialize/analyze.ts @@ -24,13 +24,13 @@ export class AnalyzeCommand extends LoadJsonCommand { @inject(TYPES.Action) _: Action, @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, - @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, ) { - super(logger, labelTypeRegistry, editorModeController, actionDispatcher, fileName); + super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName); } protected async getFile(context: CommandExecutionContext): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts index 3e532fb2..f51ef861 100644 --- a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts +++ b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts @@ -8,6 +8,7 @@ import { EditorModeController } from "../settings/editorMode"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; export namespace LoadDefaultDiagramAction { export const KIND = "loadDefaultDiagram"; @@ -24,11 +25,12 @@ export class LoadDefaultDiagramCommand extends LoadJsonCommand { @inject(TYPES.Action) _: Action, @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(FileName) fileName: FileName ) { - super(logger, labelTypeRegistry, editorModeController, actionDispatcher, fileName); + super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts index 7bc2bab8..f034b17a 100644 --- a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts @@ -9,6 +9,7 @@ import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; export namespace LoadDfdAndDdFileAction { export const KIND = "loadDfdAndDdFile"; @@ -25,12 +26,13 @@ export class LoadDfdAndDdFileCommand extends LoadJsonCommand { @inject(TYPES.Action) _: Action, @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, ) { - super(logger, labelTypeRegistry, editorModeController, actionDispatcher, fileName); + super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadJson.ts b/frontend/webEditor/src/serialize/loadJson.ts index ff479d99..af54ac02 100644 --- a/frontend/webEditor/src/serialize/loadJson.ts +++ b/frontend/webEditor/src/serialize/loadJson.ts @@ -7,6 +7,7 @@ import { Constraint } from "../constraint/Constraint"; import { LabelType } from "../labels/LabelType"; import { DefaultFitToScreenAction } from "../fitToScreen/action"; import { FileName } from "../fileName/fileName"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; export interface FileData { fileName: string; @@ -33,6 +34,7 @@ export abstract class LoadJsonCommand extends Command { constructor( private readonly logger: ILogger, protected readonly labelTypeRegistry: LabelTypeRegistry, + protected constraintRegistry: ConstraintRegistry, protected editorModeController: EditorModeController, private actionDispatcher: ActionDispatcher, protected fileName: FileName @@ -75,7 +77,13 @@ export abstract class LoadJsonCommand extends Command { } this.logger.info(this, "Editor mode loaded successfully"); - // TODO: load constraints + this.oldConstrains = this.constraintRegistry.getConstraintList() + const newConstraints = this.file.content.constraints + if (newConstraints) { + this.constraintRegistry.setConstraintsFromArray(newConstraints) + } else { + this.constraintRegistry.clearConstraints() + } // TODO: post load actions like layout this.actionDispatcher.dispatch(DefaultFitToScreenAction.create(this.newRoot)) @@ -108,7 +116,9 @@ export abstract class LoadJsonCommand extends Command { this.editorModeController.set(this.oldEditorMode); } - // TODO: load constraints + if (this.oldConstrains) { + this.constraintRegistry.setConstraintsFromArray(this.oldConstrains) + } this.fileName.setName(this.oldFileName ?? 'diagram'); @@ -133,7 +143,12 @@ export abstract class LoadJsonCommand extends Command { } this.logger.info(this, "Editor mode loaded successfully"); - // TODO: load constraints + const newConstraints = this.file?.content.constraints + if (newConstraints) { + this.constraintRegistry.setConstraintsFromArray(newConstraints) + } else { + this.constraintRegistry.clearConstraints() + } this.fileName.setName(this.file?.fileName ?? 'diagram'); diff --git a/frontend/webEditor/src/serialize/loadJsonFile.ts b/frontend/webEditor/src/serialize/loadJsonFile.ts index 0bb7127d..aa097976 100644 --- a/frontend/webEditor/src/serialize/loadJsonFile.ts +++ b/frontend/webEditor/src/serialize/loadJsonFile.ts @@ -8,6 +8,7 @@ import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; export namespace LoadJsonFileAction { export const KIND = "loadJsonFile"; @@ -25,11 +26,12 @@ export class LoadJsonFileCommand extends LoadJsonCommand { @inject(TYPES.Action) _: Action, @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(FileName) fileName: FileName, ) { - super(logger, labelTypeRegistry, editorModeController, actionDispatcher, fileName); + super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadPalladioFile.ts b/frontend/webEditor/src/serialize/loadPalladioFile.ts index 01445637..80734fa5 100644 --- a/frontend/webEditor/src/serialize/loadPalladioFile.ts +++ b/frontend/webEditor/src/serialize/loadPalladioFile.ts @@ -9,6 +9,7 @@ import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { SavedDiagram } from "./SavedDiagram"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; export namespace LoadPalladioFileAction { export const KIND = "loadPcmFile"; @@ -26,12 +27,13 @@ export class LoadPalladioFileCommand extends LoadJsonCommand { @inject(TYPES.Action) _: Action, @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, ) { - super(logger, labelTypeRegistry, editorModeController, actionDispatcher, fileName); + super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts index 757e523c..0e2717f2 100644 --- a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts @@ -8,6 +8,7 @@ import { DfdWebSocket } from "../webSocket/webSocket"; import { Action } from "sprotty-protocol"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; export namespace SaveDfdAndDdFileAction { export const KIND = 'saveDfdAndDdFile' @@ -22,13 +23,14 @@ export class SaveDfdAndDdFileCommand extends SaveFileCommand { private static readonly CLOSING_TAG = ""; constructor( - @inject(TYPES.Action) _: Action, - @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, + @inject(TYPES.Action) _: Action, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, @inject(FileName) private readonly fileName: FileName ) { - super(LabelTypeRegistry, editorModeController); + super(labelTypeRegistry, constraintRegistry, editorModeController); } async getFiles(context: CommandExecutionContext): Promise[]> { diff --git a/frontend/webEditor/src/serialize/saveJsonFile.ts b/frontend/webEditor/src/serialize/saveJsonFile.ts index 6a474b43..4ba55851 100644 --- a/frontend/webEditor/src/serialize/saveJsonFile.ts +++ b/frontend/webEditor/src/serialize/saveJsonFile.ts @@ -7,6 +7,7 @@ import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { Action } from "sprotty-protocol"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; export namespace SaveJsonFileAction { export const KIND = 'saveJsonFile' @@ -21,10 +22,11 @@ export class SaveJsonFileCommand extends SaveFileCommand { constructor( @inject(TYPES.Action) _: Action, @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(FileName) private readonly fileName: FileName ) { - super(LabelTypeRegistry, editorModeController); + super(LabelTypeRegistry, constraintRegistry, editorModeController); } getFiles(context: CommandExecutionContext): Promise[]> { diff --git a/frontend/webEditor/src/serialize/savedDiagramCreator.ts b/frontend/webEditor/src/serialize/savedDiagramCreator.ts index 860d6fb0..713a4002 100644 --- a/frontend/webEditor/src/serialize/savedDiagramCreator.ts +++ b/frontend/webEditor/src/serialize/savedDiagramCreator.ts @@ -2,11 +2,13 @@ import { Command, CommandExecutionContext } from "sprotty"; import { CURRENT_VERSION, SavedDiagram } from "./SavedDiagram"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { EditorModeController } from "../settings/editorMode"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; export abstract class SavedDiagramCreatorCommand extends Command { constructor( private readonly labelTypeRegistry: LabelTypeRegistry, + private readonly constraintRegistry: ConstraintRegistry, private readonly editorModeController: EditorModeController ) { super() @@ -18,8 +20,7 @@ export abstract class SavedDiagramCreatorCommand extends Command { return { model: schema, labelTypes: this.labelTypeRegistry.getLabelTypes(), - // TODO - constraints: [], + constraints: this.constraintRegistry.getConstraintList(), mode: this.editorModeController.get(), version: CURRENT_VERSION } From 984f06d03c836959033c8babf6e54c93008f183a Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 28 Nov 2025 13:53:10 +0100 Subject: [PATCH 26/41] add and remove labels --- .../src/diagram/nodes/DfdNodeLabels.tsx | 17 +- .../webEditor/src/diagram/nodes/common.ts | 3 +- .../webEditor/src/labels/LabelTypeEditorUi.ts | 187 +++++++++++------- frontend/webEditor/src/labels/command.ts | 153 ++++++++++++++ frontend/webEditor/src/labels/di.config.ts | 10 +- frontend/webEditor/src/labels/dragAndDrop.ts | 74 +++++++ frontend/webEditor/src/labels/feature.ts | 12 ++ 7 files changed, 370 insertions(+), 86 deletions(-) create mode 100644 frontend/webEditor/src/labels/command.ts create mode 100644 frontend/webEditor/src/labels/dragAndDrop.ts create mode 100644 frontend/webEditor/src/labels/feature.ts diff --git a/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx index 34913173..2c2c13a1 100644 --- a/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx +++ b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx @@ -1,11 +1,13 @@ /** @jsx svg */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { IActionDispatcher, SModelElementImpl, SNodeImpl, svg, TYPES } from "sprotty"; +import { IActionDispatcher, SNodeImpl, svg, TYPES } from "sprotty"; import { LabelAssignment, LabelType, LabelTypeValue } from "../../labels/LabelType"; import { inject, injectable } from "inversify"; import { LabelTypeRegistry } from "../../labels/LabelTypeRegistry"; import { calculateTextSize } from "../../utils/TextSize"; import { VNode } from "snabbdom"; +import { ContainsDfdLabels } from "../../labels/feature"; +import { RemoveLabelAssignmentAction } from "../../labels/command"; @injectable() export class DfdNodeLabelRenderer { @@ -56,9 +58,7 @@ export class DfdNodeLabelRenderer { const radius = height / 2; const deleteLabelHandler = () => { - // TODO - /* const action = DeleteLabelAssignmentAction.create(label, node); - this.actionDispatcher.dispatch(action);*/ + this.actionDispatcher.dispatch(RemoveLabelAssignmentAction.create(label, node)); }; return ( @@ -124,12 +124,3 @@ export class DfdNodeLabelRenderer { } } -export const containsDfdLabelFeature = Symbol("dfd-label-feature"); - -export interface ContainsDfdLabels extends SModelElementImpl { - labels: LabelAssignment[]; -} - -export function containsDfdLabels(element: T): element is T & ContainsDfdLabels { - return element.features?.has(containsDfdLabelFeature) ?? false; -} \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/nodes/common.ts b/frontend/webEditor/src/diagram/nodes/common.ts index 5c4edd9e..902431cf 100644 --- a/frontend/webEditor/src/diagram/nodes/common.ts +++ b/frontend/webEditor/src/diagram/nodes/common.ts @@ -8,6 +8,7 @@ import { VNodeStyle } from "snabbdom"; import { DfdInputPortImpl } from "../ports/DfdInputPort"; import { inject } from "inversify"; import { DfdNodeLabelRenderer } from "./DfdNodeLabels"; +import { containsDfdLabelFeature } from "../../labels/feature"; export interface DfdNode extends SNode { text: string; @@ -17,7 +18,7 @@ export interface DfdNode extends SNode { } export abstract class DfdNodeImpl extends SNodeImpl implements WithEditableLabel { - static readonly DEFAULT_FEATURES = [...SNodeImpl.DEFAULT_FEATURES, withEditLabelFeature/*, containsDfdLabelFeature*/]; + static readonly DEFAULT_FEATURES = [...SNodeImpl.DEFAULT_FEATURES, withEditLabelFeature, containsDfdLabelFeature]; static readonly DEFAULT_WIDTH = 50; static readonly WIDTH_PADDING = 12; static readonly NODE_COLOR = "var(--color-primary)"; diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts index 3a8be915..6bc27d2e 100644 --- a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -1,131 +1,178 @@ import { AccordionUiExtension } from "../accordionUiExtension"; import { UiElementFactory } from "../utils/UiElementFactory"; -import { LabelType } from "./LabelType"; +import { LabelAssignment, LabelType } from "./LabelType"; import { inject } from "inversify"; import { LabelTypeRegistry } from "./LabelTypeRegistry"; -import './labelTypeEditorUi.css' +import "./labelTypeEditorUi.css"; import { dynamicallySetInputSize } from "../utils/TextSize"; +import { LABEL_ASSIGNMENT_MIME_TYPE } from "./dragAndDrop"; +import { AddLabelAssignmentAction } from "./command"; +import { IActionDispatcher, TYPES } from "sprotty"; export class LabelTypeEditorUi extends AccordionUiExtension { static readonly ID = "label-type-editor-ui"; - private labelSectionContainer?: HTMLElement + private labelSectionContainer?: HTMLElement; - constructor(@inject(LabelTypeRegistry) private labelTypeRegistry: LabelTypeRegistry) { - super('left', 'down') - labelTypeRegistry.onUpdate(() => this.renderLabelTypes()) + constructor(@inject(LabelTypeRegistry) private labelTypeRegistry: LabelTypeRegistry, @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher) { + super("left", "down"); + labelTypeRegistry.onUpdate(() => this.renderLabelTypes()); } - + id(): string { - return LabelTypeEditorUi.ID + return LabelTypeEditorUi.ID; } containerClass(): string { - return LabelTypeEditorUi.ID + return LabelTypeEditorUi.ID; } protected initializeHidableContent(contentElement: HTMLElement) { - const addButton = UiElementFactory.buildAddButton('Label Type') + const addButton = UiElementFactory.buildAddButton("Label Type"); addButton.onclick = () => { - this.labelTypeRegistry.registerLabelType('') - } + this.labelTypeRegistry.registerLabelType(""); + }; - this.labelSectionContainer = document.createElement('div') - this.renderLabelTypes() + this.labelSectionContainer = document.createElement("div"); + this.renderLabelTypes(); - contentElement.appendChild(this.labelSectionContainer) - contentElement.appendChild(addButton) + contentElement.appendChild(this.labelSectionContainer); + contentElement.appendChild(addButton); } protected initializeHeaderContent(headerElement: HTMLElement) { - headerElement.innerText = 'Label Types' + headerElement.innerText = "Label Types"; } private renderLabelTypes(): void { if (!this.labelSectionContainer) { - return + return; } - this.labelSectionContainer.innerHTML = ''; - const labelTypes = this.labelTypeRegistry.getLabelTypes() + this.labelSectionContainer.innerHTML = ""; + const labelTypes = this.labelTypeRegistry.getLabelTypes(); for (let i = 0; i < labelTypes.length; i++) { - this.labelSectionContainer.appendChild(this.buildLabelTypeSection(labelTypes[i])) + this.labelSectionContainer.appendChild(this.buildLabelTypeSection(labelTypes[i])); if (i < labelTypes.length - 1) { - this.labelSectionContainer.appendChild(document.createElement('hr')) + this.labelSectionContainer.appendChild(document.createElement("hr")); } } } private buildLabelTypeSection(labelType: LabelType): HTMLElement { - const section = document.createElement('div') - section.classList.add('label-section') - - const nameInput = document.createElement('input') - nameInput.classList.add('label-type-name') - const deleteButton = UiElementFactory.buildDeleteButton() - const labelTypeValueHolder = document.createElement('div') - labelTypeValueHolder.classList.add('label-type-values') - const addButton = UiElementFactory.buildAddButton('Value') - addButton.classList.add('label-type-value-add') - - nameInput.value = labelType.name - nameInput.placeholder = 'Label Type Name' - nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput) - setTimeout(() => dynamicallySetInputSize(nameInput), 0) + const section = document.createElement("div"); + section.classList.add("label-section"); + + const nameInput = document.createElement("input"); + nameInput.classList.add("label-type-name"); + const deleteButton = UiElementFactory.buildDeleteButton(); + const labelTypeValueHolder = document.createElement("div"); + labelTypeValueHolder.classList.add("label-type-values"); + const addButton = UiElementFactory.buildAddButton("Value"); + addButton.classList.add("label-type-value-add"); + + nameInput.value = labelType.name; + nameInput.placeholder = "Label Type Name"; + nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput); + setTimeout(() => dynamicallySetInputSize(nameInput), 0); nameInput.onchange = () => { - this.labelTypeRegistry.updateLabelTypeName(labelType.id, nameInput.value) - } + this.labelTypeRegistry.updateLabelTypeName(labelType.id, nameInput.value); + }; for (let i = 0; i < labelType.values.length; i++) { - labelTypeValueHolder.appendChild(this.buildLabelTypeValue(labelType, i)) + labelTypeValueHolder.appendChild(this.buildLabelTypeValue(labelType, i)); } addButton.onclick = () => { - this.labelTypeRegistry.registerLabelTypeValue(labelType.id, '') - } + this.labelTypeRegistry.registerLabelTypeValue(labelType.id, ""); + }; deleteButton.onclick = () => { - this.labelTypeRegistry.unregisterLabelType(labelType.id) - } + this.labelTypeRegistry.unregisterLabelType(labelType.id); + }; - section.appendChild(nameInput) - section.appendChild(deleteButton) - section.appendChild(labelTypeValueHolder) - section.appendChild(addButton) + section.appendChild(nameInput); + section.appendChild(deleteButton); + section.appendChild(labelTypeValueHolder); + section.appendChild(addButton); - return section + return section; } private buildLabelTypeValue(labelType: LabelType, valueIndex: number) { - const holder = document.createElement('div'); - holder.classList.add('label-type-value') - const nameInput = document.createElement('input'); - nameInput.classList.add('label-type-value-name') - const deleteButton = UiElementFactory.buildDeleteButton() + const holder = document.createElement("div"); + holder.classList.add("label-type-value"); + const nameInput = document.createElement("input"); + nameInput.classList.add("label-type-value-name"); + const deleteButton = UiElementFactory.buildDeleteButton(); - const value = labelType.values[valueIndex] + const value = labelType.values[valueIndex]; - - nameInput.value = value.text - nameInput.placeholder = 'Value' - nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput) - setTimeout(() => dynamicallySetInputSize(nameInput), 0) + nameInput.value = value.text; + nameInput.placeholder = "Value"; + nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput); + setTimeout(() => dynamicallySetInputSize(nameInput), 0); nameInput.onchange = () => { - this.labelTypeRegistry.updateLabelTypeValueText(labelType.id, value.id, nameInput.value) - } + this.labelTypeRegistry.updateLabelTypeValueText(labelType.id, value.id, nameInput.value); + }; deleteButton.onclick = () => { - this.labelTypeRegistry.unregisterLabelTypeValue(labelType.id, value.id) - } + this.labelTypeRegistry.unregisterLabelTypeValue(labelType.id, value.id); + }; + + // Allow dragging to create a label assignment + nameInput.draggable = true; + nameInput.ondragstart = (event) => { + const assignment: LabelAssignment = { + labelTypeId: labelType.id, + labelTypeValueId: value.id, + }; + const assignmentJson = JSON.stringify(assignment); + event.dataTransfer?.setData(LABEL_ASSIGNMENT_MIME_TYPE, assignmentJson); + }; + + // Only edit on double click + nameInput.onclick = () => { + if (nameInput.getAttribute("clicked") === "true") { + return; + } + + nameInput.setAttribute("clicked", "true"); + setTimeout(() => { + if (nameInput.getAttribute("clicked") === "true") { + this.actionDispatcher.dispatch( + AddLabelAssignmentAction.create({ + labelTypeId: labelType.id, + labelTypeValueId: value.id, + }), + ); + nameInput.removeAttribute("clicked"); + } + }, 500); + }; + nameInput.ondblclick = () => { + nameInput.removeAttribute("clicked"); + nameInput.focus(); + }; + nameInput.onfocus = (event) => { + // we check for the single click here, since this gets triggered before the ondblclick event + if (nameInput.getAttribute("clicked") !== "true") { + event.preventDefault(); + // the blur needs to occur with a delay, as otherwise chromium browsers prevent the drag + setTimeout(() => { + nameInput.blur(); + }, 0); + } + }; - holder.appendChild(nameInput) - holder.appendChild(deleteButton) - return holder + holder.appendChild(nameInput); + holder.appendChild(deleteButton); + return holder; } private onInputHandler(event: InputEvent, input: HTMLInputElement) { if (!event.data?.match(/^[a-zA-Z0-9]*$/)) { - event.preventDefault() + event.preventDefault(); } - dynamicallySetInputSize(input) + dynamicallySetInputSize(input); } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/labels/command.ts b/frontend/webEditor/src/labels/command.ts new file mode 100644 index 00000000..b24d285d --- /dev/null +++ b/frontend/webEditor/src/labels/command.ts @@ -0,0 +1,153 @@ +import { + Command, + CommandExecutionContext, + CommandReturn, + ISnapper, + isSelected, + SChildElementImpl, + SModelElementImpl, + SNodeImpl, + TYPES, +} from "sprotty"; +import { LabelAssignment } from "./LabelType"; +import { Action } from "sprotty-protocol"; +import { snapPortsOfNode } from "../diagram/ports/portSnapper"; +import { EditorModeController } from "../settings/editorMode"; +import { inject, injectable } from "inversify"; +import { SETTINGS } from "../settings/Settings"; +import { ContainsDfdLabels, containsDfdLabels } from "./feature"; + +interface LabelAction extends Action { + action: "add" | "remove"; + element?: ContainsDfdLabels & SNodeImpl; + labelAssignment: LabelAssignment; +} + +export namespace AddLabelAssignmentAction { + export function create(labelAssignment: LabelAssignment, element?: ContainsDfdLabels & SNodeImpl): LabelAction { + return { + kind: LabelCommand.KIND, + action: 'add', + labelAssignment, + element + } + } +} + +export namespace RemoveLabelAssignmentAction { + export function create(labelAssignment: LabelAssignment, element?: ContainsDfdLabels & SNodeImpl): LabelAction { + return { + kind: LabelCommand.KIND, + action: 'remove', + labelAssignment, + element + } + } +} + +@injectable() +export class LabelCommand implements Command { + public static readonly KIND = 'labelAction'; + + private elements?: ContainsDfdLabels[]; + + constructor( + @inject(TYPES.Action) private readonly action: LabelAction, + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, + @inject(TYPES.ISnapper) private readonly snapper: ISnapper, + ) {} + + execute(context: CommandExecutionContext): CommandReturn { + if (this.editorModeController.isReadOnly()) { + return context.root; + } + if (this.action.element) { + this.elements = [this.action.element]; + } else { + const allElements = getAllElements(context.root.children); + this.elements = allElements.filter((element) => isSelected(element) && containsDfdLabels(element)); + } + + if (this.action.action == "add") { + this.addLabel(); + } else { + this.removeLabel(); + } + + return context.root; + } + + undo(context: CommandExecutionContext): CommandReturn { + if (this.editorModeController.isReadOnly()) { + return context.root; + } + + if (this.action.action == "add") { + this.removeLabel(); + } else { + this.addLabel(); + } + + return context.root; + } + + redo(context: CommandExecutionContext): CommandReturn { + if (this.editorModeController.isReadOnly()) { + return context.root; + } + + if (this.action.action == "add") { + this.addLabel(); + } else { + this.removeLabel(); + } + + return context.root; + } + + private addLabel() { + this.elements?.forEach((element) => { + const hasBeenAdded = + element.labels.find((as) => { + return ( + as.labelTypeId === this.action.labelAssignment.labelTypeId && + as.labelTypeValueId === this.action.labelAssignment.labelTypeValueId + ); + }) !== undefined; + if (!hasBeenAdded) { + element.labels.push(this.action.labelAssignment); + if (element instanceof SNodeImpl) { + snapPortsOfNode(element, this.snapper); + } + } + }); + } + + private removeLabel() { + this.elements?.forEach((element) => { + const labels = element.labels; + const idx = labels.findIndex( + (l) => + l.labelTypeId == this.action.labelAssignment.labelTypeId && + l.labelTypeValueId == this.action.labelAssignment.labelTypeValueId, + ); + if (idx >= 0) { + labels.splice(idx, 1); + if (element instanceof SNodeImpl) { + snapPortsOfNode(element, this.snapper); + } + } + }); + } +} + +function getAllElements(elements: readonly SChildElementImpl[]): SModelElementImpl[] { + const elementsList: SModelElementImpl[] = []; + for (const element of elements) { + elementsList.push(element); + if ("children" in element) { + elementsList.push(...getAllElements(element.children)); + } + } + return elementsList; +} diff --git a/frontend/webEditor/src/labels/di.config.ts b/frontend/webEditor/src/labels/di.config.ts index 023375fc..e0ea48b5 100644 --- a/frontend/webEditor/src/labels/di.config.ts +++ b/frontend/webEditor/src/labels/di.config.ts @@ -1,13 +1,19 @@ import { ContainerModule } from "inversify"; import { LabelTypeRegistry } from "./LabelTypeRegistry"; import { LabelTypeEditorUi } from "./LabelTypeEditorUi"; -import { TYPES } from "sprotty"; +import { configureCommand, TYPES } from "sprotty"; import { EDITOR_TYPES } from "../editorTypes"; +import { LabelCommand } from "./command"; +import { DfdLabelMouseDropListener } from "./dragAndDrop"; -export const labelModule = new ContainerModule((bind) => { +export const labelModule = new ContainerModule((bind, _, isBound) => { bind(LabelTypeRegistry).toSelf().inSingletonScope() bind(LabelTypeEditorUi).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(LabelTypeEditorUi); bind(EDITOR_TYPES.DefaultUIElement).to(LabelTypeEditorUi); + + + configureCommand({ bind, isBound }, LabelCommand); + bind(TYPES.MouseListener).to(DfdLabelMouseDropListener); }) \ No newline at end of file diff --git a/frontend/webEditor/src/labels/dragAndDrop.ts b/frontend/webEditor/src/labels/dragAndDrop.ts new file mode 100644 index 00000000..e1503bc9 --- /dev/null +++ b/frontend/webEditor/src/labels/dragAndDrop.ts @@ -0,0 +1,74 @@ +import { injectable, inject } from "inversify"; +import { Action } from "sprotty-protocol"; +import { + SModelElementImpl, + SChildElementImpl, + MouseListener, + CommitModelAction, + ILogger, + TYPES, + SNodeImpl +} from "sprotty"; +import { LabelAssignment } from "./LabelType"; +import { AddLabelAssignmentAction } from "./command"; +import { containsDfdLabels, ContainsDfdLabels } from "./feature"; + +export const LABEL_ASSIGNMENT_MIME_TYPE = "application/x-label-assignment"; + +/** + * Mouse Listener that handles the drop of label assignments. + * These can be started by dragging a label type value from the label type editor UI. + * Adds the label to the element that the label value was dropped on. + */ +@injectable() +export class DfdLabelMouseDropListener extends MouseListener { + constructor(@inject(TYPES.ILogger) private logger: ILogger) { + super(); + } + + override dragOver(_target: SModelElementImpl, event: MouseEvent): Action[] { + // Prevent the dragover prevent to indicated that the drop is possible + // Check https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dragover_event for more details + event.preventDefault(); + return []; + } + + override drop(target: SChildElementImpl, event: DragEvent): Action[] { + const labelAssignmentJson = event.dataTransfer?.getData(LABEL_ASSIGNMENT_MIME_TYPE); + if (!labelAssignmentJson) { + return []; + } + + const dfdLabelElement = getParentWithDfdLabels(target); + if (!dfdLabelElement) { + this.logger.info( + this, + "Aborted drop of label assignment because the target element nor the parent elements have the dfd label feature", + ); + return []; + } + + if (!(dfdLabelElement instanceof SNodeImpl)) { + this.logger.info(this, "Aborted drop of label assignment because the target element is not a node"); + return []; + } + + const labelAssignment = JSON.parse(labelAssignmentJson) as LabelAssignment; + this.logger.info(this, "Adding label assignment to element", dfdLabelElement, labelAssignment); + return [AddLabelAssignmentAction.create(labelAssignment, dfdLabelElement), CommitModelAction.create()]; + } +} + +function getParentWithDfdLabels( + element: SChildElementImpl | SModelElementImpl, +): (SModelElementImpl & ContainsDfdLabels) | undefined { + if (containsDfdLabels(element)) { + return element; + } + + if ("parent" in element) { + return getParentWithDfdLabels(element.parent); + } + + return undefined; +} \ No newline at end of file diff --git a/frontend/webEditor/src/labels/feature.ts b/frontend/webEditor/src/labels/feature.ts new file mode 100644 index 00000000..deb6b57a --- /dev/null +++ b/frontend/webEditor/src/labels/feature.ts @@ -0,0 +1,12 @@ +import { SModelElementImpl } from "sprotty"; +import { LabelAssignment } from "./LabelType"; + +export const containsDfdLabelFeature = Symbol("dfd-label-feature"); + +export interface ContainsDfdLabels extends SModelElementImpl { + labels: LabelAssignment[]; +} + +export function containsDfdLabels(element: T): element is T & ContainsDfdLabels { + return element.features?.has(containsDfdLabelFeature) ?? false; +} \ No newline at end of file From 1a6232672af120ce722b782e446fcf13b1f16845 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Mon, 1 Dec 2025 07:33:57 +0100 Subject: [PATCH 27/41] better label type edtor refresh --- .../webEditor/src/labels/LabelTypeEditorUi.ts | 17 ++++++++++++++--- .../webEditor/src/labels/LabelTypeRegistry.ts | 10 ++++++++-- frontend/webEditor/src/utils/TextSize.ts | 3 +++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts index 3a8be915..0d6c4d39 100644 --- a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -44,14 +44,23 @@ export class LabelTypeEditorUi extends AccordionUiExtension { if (!this.labelSectionContainer) { return } - this.labelSectionContainer.innerHTML = ''; + const width = this.labelSectionContainer.scrollWidth + const height = this.labelSectionContainer.scrollHeight + this.labelSectionContainer.style.width = `${width}px` + this.labelSectionContainer.style.height = `${height}px` + const fragment = document.createDocumentFragment() const labelTypes = this.labelTypeRegistry.getLabelTypes() for (let i = 0; i < labelTypes.length; i++) { - this.labelSectionContainer.appendChild(this.buildLabelTypeSection(labelTypes[i])) + fragment.appendChild(this.buildLabelTypeSection(labelTypes[i])) if (i < labelTypes.length - 1) { - this.labelSectionContainer.appendChild(document.createElement('hr')) + fragment.appendChild(document.createElement('hr')) } } + this.labelSectionContainer!.replaceChildren(fragment) + this.labelSectionContainer!.style.width = '' + this.labelSectionContainer!.style.height = '' + + } private buildLabelTypeSection(labelType: LabelType): HTMLElement { @@ -69,6 +78,7 @@ export class LabelTypeEditorUi extends AccordionUiExtension { nameInput.value = labelType.name nameInput.placeholder = 'Label Type Name' nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput) + dynamicallySetInputSize(nameInput) setTimeout(() => dynamicallySetInputSize(nameInput), 0) nameInput.onchange = () => { this.labelTypeRegistry.updateLabelTypeName(labelType.id, nameInput.value) @@ -107,6 +117,7 @@ export class LabelTypeEditorUi extends AccordionUiExtension { nameInput.value = value.text nameInput.placeholder = 'Value' nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput) + nameInput.style.width = '0px' setTimeout(() => dynamicallySetInputSize(nameInput), 0) nameInput.onchange = () => { diff --git a/frontend/webEditor/src/labels/LabelTypeRegistry.ts b/frontend/webEditor/src/labels/LabelTypeRegistry.ts index ddd3ea4d..e43a499d 100644 --- a/frontend/webEditor/src/labels/LabelTypeRegistry.ts +++ b/frontend/webEditor/src/labels/LabelTypeRegistry.ts @@ -12,7 +12,7 @@ export class LabelTypeRegistry { values: [] } this.labelTypes.push(labelType); - this.registerLabelTypeValue(labelType.id, 'Value') + this._registerLabelTypeValue(labelType.id, 'Value', true) this.labelTypeChanged() return labelType } @@ -37,6 +37,10 @@ export class LabelTypeRegistry { } public registerLabelTypeValue(labelTypeId: string, text: string): LabelTypeValue { + return this._registerLabelTypeValue(labelTypeId, text) + } + + private _registerLabelTypeValue(labelTypeId: string, text: string, surpressUpdate=false): LabelTypeValue { const labelTypeValue: LabelTypeValue = { id: generateRandomSprottyId(), text @@ -46,7 +50,9 @@ export class LabelTypeRegistry { throw `No Label Type with id ${labelTypeId} found` } labelType.values.push(labelTypeValue) - this.labelTypeChanged() + if (!surpressUpdate) { + this.labelTypeChanged() + } return labelTypeValue } diff --git a/frontend/webEditor/src/utils/TextSize.ts b/frontend/webEditor/src/utils/TextSize.ts index f12c0566..5d67856f 100644 --- a/frontend/webEditor/src/utils/TextSize.ts +++ b/frontend/webEditor/src/utils/TextSize.ts @@ -20,6 +20,9 @@ export function calculateTextSize(text: string | undefined, font: string = "11pt if (!text || text.length === 0) { return { width: 20, height: 20 }; } + if (font == "") { + font = "11pt sans-serif"; + } // Get context for the given font or create a new one if it does not exist yet let contextObj = contextMap.get(font); From 0ff8c594efe98f7e2174c6c436cd600ef41a1f76 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Tue, 2 Dec 2025 08:00:24 +0100 Subject: [PATCH 28/41] assignment menu --- .../src/assignment/AssignmentEditUi.ts | 254 +++++++++++++++ .../src/assignment/assignmentEditUi.css | 22 ++ .../webEditor/src/assignment/clickListener.ts | 54 ++++ .../webEditor/src/assignment/di.config.ts | 11 + frontend/webEditor/src/assignment/language.ts | 296 ++++++++++++++++++ .../src/constraint/ConstraintMenu.ts | 12 +- .../webEditor/src/constraint/di.config.ts | 2 + .../src/diagram/ports/DfdOutputPort.tsx | 25 +- frontend/webEditor/src/helpUi/helpUi.css | 1 + frontend/webEditor/src/index.ts | 4 +- frontend/webEditor/src/settings/Theme.ts | 21 +- .../webEditor/src/settings/settingsUi.css | 1 + .../src/startUpAgent/settingsInit.ts | 8 +- 13 files changed, 690 insertions(+), 21 deletions(-) create mode 100644 frontend/webEditor/src/assignment/AssignmentEditUi.ts create mode 100644 frontend/webEditor/src/assignment/assignmentEditUi.css create mode 100644 frontend/webEditor/src/assignment/clickListener.ts create mode 100644 frontend/webEditor/src/assignment/di.config.ts create mode 100644 frontend/webEditor/src/assignment/language.ts diff --git a/frontend/webEditor/src/assignment/AssignmentEditUi.ts b/frontend/webEditor/src/assignment/AssignmentEditUi.ts new file mode 100644 index 00000000..b64b5ba3 --- /dev/null +++ b/frontend/webEditor/src/assignment/AssignmentEditUi.ts @@ -0,0 +1,254 @@ +import { inject, injectable } from "inversify"; +import { AbstractUIExtension, getAbsoluteClientBounds, SModelRootImpl, TYPES, ViewerOptions } from "sprotty"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort"; +import { DOMHelper } from "sprotty/lib/base/views/dom-helper"; +import { DfdNodeImpl } from "../diagram/nodes/common"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { ASSIGNMENT_LANGUAGE_ID, assignmentLanguageMonarchDefinition, AssignmentLanguageTreeBuilder } from "./language"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { DfdCompletionItemProvider } from "../languages/autocomplete"; +import { LanguageTreeNode, tokenize } from "../languages/tokenize"; +import { Word } from "../languages/words"; +import { Theme, ThemeManager } from "../settings/Theme"; +import { SETTINGS } from "../settings/Settings"; +import { verify } from "../languages/verify"; +import "./assignmentEditUi.css"; +import { EditorModeController } from "../settings/editorMode"; +import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; + +@injectable() +export class AssignmentEditUi extends AbstractUIExtension { + public static readonly ID = "assignment-edit-ui"; + + private port?: DfdOutputPortImpl; + private tree?: LanguageTreeNode[]; + private editorContainer: HTMLDivElement = document.createElement("div") as HTMLDivElement; + private validationLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; + private unavailableInputsLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; + private editor?: monaco.editor.IStandaloneCodeEditor; + + constructor( + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, + @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, + @inject(TYPES.ViewerOptions) private viewerOptions: ViewerOptions, + @inject(TYPES.DOMHelper) private domHelper: DOMHelper, + ) { + super(); + + editorModeController.registerListener(() => { + this.editor?.updateOptions({ + readOnly: this.editorModeController.isReadOnly(), + }); + }); + } + + id(): string { + return AssignmentEditUi.ID; + } + + containerClass(): string { + return AssignmentEditUi.ID; + } + + protected initializeContents(containerElement: HTMLElement): void { + containerElement.classList.add("ui-float"); + + containerElement.appendChild(this.unavailableInputsLabel); + this.unavailableInputsLabel.classList.add("unavailable-inputs"); + containerElement.appendChild(this.editorContainer); + this.editorContainer.classList.add("monaco-container"); + containerElement.appendChild(this.validationLabel); + this.validationLabel.classList.add("validation-label"); + + const keyboardShortcutLabel = document.createElement("div"); + keyboardShortcutLabel.innerHTML = "Press CTRL+Space for autocompletion"; + containerElement.appendChild(keyboardShortcutLabel); + + monaco.languages.register({ id: ASSIGNMENT_LANGUAGE_ID }); + monaco.languages.setMonarchTokensProvider(ASSIGNMENT_LANGUAGE_ID, assignmentLanguageMonarchDefinition); + + const monacoTheme = this.themeManager.getTheme() === Theme.DARK ? "vs-dark" : "vs"; + this.editor = monaco.editor.create(this.editorContainer, { + minimap: { + // takes too much space, not useful for our use case + enabled: false, + }, + lineNumbersMinChars: 3, // default is 5, which we'll never need. Save a bit of space. + folding: false, // Not supported by our language definition + wordBasedSuggestions: "off", // Does not really work for our use case + scrollBeyondLastLine: false, // Not needed + theme: monacoTheme, + language: ASSIGNMENT_LANGUAGE_ID, + readOnly: this.editorModeController.isReadOnly() + }); + + this.editor.onDidChangeModelContent(() => { + this.validate(); + }); + + this.editor.onDidContentSizeChange(() => { + this.resizeEditor(); + }); + + this.labelTypeRegistry?.onUpdate(() => { + // The update handler for the refactoring might be after our handler. + // Delay update to the next event loop tick to ensure the refactoring is done. + setTimeout(() => { + if (this.editor && this.port) { + this.editor?.setValue(this.port?.getBehavior()); + } + }, 0); + }); + + // Hide/"close this window" when pressing escape. + containerElement.addEventListener("keydown", (event) => { + if (matchesKeystroke(event, "Escape")) { + this.hide(); + } + }); + } + + protected onBeforeShow( + containerElement: HTMLElement, + root: Readonly, + ...contextElementIds: string[] + ): void { + // Loads data for the port that shall be edited, which is defined by the context element id. + if (contextElementIds.length !== 1) { + throw new Error( + "Expected exactly one context element id which should be the port that shall be shown in the UI.", + ); + } + this.setPort(root.index.getById(contextElementIds[0]) as DfdOutputPortImpl, containerElement); + + this.checkForUnavailableInputs(); + + this.resizeEditor(); + + this.editor?.focus() + } + + private setPort(port: DfdOutputPortImpl, containerElement: HTMLElement) { + this.port = port; + + const bounds = getAbsoluteClientBounds(this.port, this.domHelper, this.viewerOptions); + containerElement.style.left = `${bounds.x}px`; + containerElement.style.top = `${bounds.y}px`; + + this.tree = AssignmentLanguageTreeBuilder.buildTree(port, this.labelTypeRegistry); + monaco.languages.registerCompletionItemProvider( + ASSIGNMENT_LANGUAGE_ID, + new DfdCompletionItemProvider(this.tree), + ); + if (!this.editor) { + throw new Error("Expected editor to be initialized"); + } + + this.editor.setValue(port.getBehavior()); + } + + private checkForUnavailableInputs() { + if (!this.port) { + throw new Error("Expected Assignment Edit Ui to be assigned to a port"); + } + + const parent = this.port.parent; + if (!(parent instanceof DfdNodeImpl)) { + throw new Error("Expected parent to be a DfdNodeImpl."); + } + + const availableInputNames = parent.getAvailableInputs(); + const countUnavailableDueToMissingName = availableInputNames.filter((name) => name === undefined).length; + + if (countUnavailableDueToMissingName > 0) { + const unavailableInputsText = + countUnavailableDueToMissingName > 1 + ? `There are ${countUnavailableDueToMissingName} inputs that don't have a named edge and cannot be used` + : `There is ${countUnavailableDueToMissingName} input that doesn't have a named edge and cannot be used`; + + this.unavailableInputsLabel.innerText = unavailableInputsText; + this.unavailableInputsLabel.style.display = "block"; + } else { + this.unavailableInputsLabel.innerText = ""; + this.unavailableInputsLabel.style.display = "none"; + } + } + + private resizeEditor(): void { + // Resize editor to fit content. + // Has ranges for height and width to prevent the editor from getting too small or too large. + if (!this.editor) { + return; + } + + // For the height we can use the content height from the editor. + const height = this.editor.getContentHeight(); + + // For the width we cannot really do this. + // Monaco needs about 500ms to figure out the correct width when initially showing the editor. + // In the mean time the width will be too small and after the update + // the window size will jump visibly. + // So for the width we use this calculation to approximate the width. + const maxLineLength = this.editor + .getValue() + .split("\n") + .reduce((max, line) => Math.max(max, line.length), 0); + const width = 100 + maxLineLength * 8; + + const clamp = (value: number, range: readonly [number, number]) => + Math.min(range[1], Math.max(range[0], value)); + + const heightRange = [100, 350] as const; + const widthRange = [275, 650] as const; + + const cHeight = clamp(height, heightRange); + const cWidth = clamp(width, widthRange); + + this.editor.layout({ height: cHeight, width: cWidth }); + } + + private validate() { + if (!this.editor || !this.tree) { + return; + } + + const model = this.editor?.getModel(); + if (!model) { + return; + } + + const content = model.getLinesContent(); + this.port?.setBehavior(content.join("\n")); + const marker: monaco.editor.IMarkerData[] = []; + const emptyContent = content.length == 0 || (content.length == 1 && content[0] === ""); + // empty content gets accepted as valid as it represents no constraints + if (!emptyContent) { + const errors = verify(tokenize(content), this.tree); + marker.push( + ...errors.map((e) => ({ + severity: monaco.MarkerSeverity.Error, + startLineNumber: e.line, + startColumn: e.startColumn, + endLineNumber: e.line, + endColumn: e.endColumn, + message: e.message, + })), + ); + } + + if (marker.length == 0) { + this.validationLabel.innerText = "Assignments are valid"; + this.validationLabel.classList.remove("validation-error"); + this.validationLabel.classList.add("validation-success"); + } else { + this.validationLabel.innerText = `Assignments are invalid: ${marker.length} error${ + marker.length === 1 ? "" : "s" + }.`; + this.validationLabel.classList.remove("validation-success"); + this.validationLabel.classList.add("validation-error"); + } + + monaco.editor.setModelMarkers(model, "constraint", marker); + } +} diff --git a/frontend/webEditor/src/assignment/assignmentEditUi.css b/frontend/webEditor/src/assignment/assignmentEditUi.css new file mode 100644 index 00000000..b7c6c81e --- /dev/null +++ b/frontend/webEditor/src/assignment/assignmentEditUi.css @@ -0,0 +1,22 @@ +.assignment-edit-ui { + position: absolute; + padding: 10px; + + -webkit-user-select: none; + user-select: none; + + background: var(--color-primary); + + div.unavailable-inputs { + /* spacing between editor and this text */ + padding-bottom: 5px; + } + + div.validation-label.validation-error { + color: var(--color-error); + } + + div.validation-label.validation-success { + color: var(--color-valid); + } +} diff --git a/frontend/webEditor/src/assignment/clickListener.ts b/frontend/webEditor/src/assignment/clickListener.ts new file mode 100644 index 00000000..424a09b4 --- /dev/null +++ b/frontend/webEditor/src/assignment/clickListener.ts @@ -0,0 +1,54 @@ +import { injectable } from "inversify"; +import { MouseListener, SModelElementImpl, SetUIExtensionVisibilityAction } from "sprotty"; +import { Action } from "sprotty-protocol"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort"; +import { AssignmentEditUi } from "./AssignmentEditUi"; + +/** + * Detects when a dfd output port is double clicked and shows the OutputPortEditUI + * with the clicked port as context element. + */ +@injectable() +export class OutputPortEditUIMouseListener extends MouseListener { + private editUIVisible = false; + + mouseDown(target: SModelElementImpl): (Action | Promise)[] { + if (this.editUIVisible) { + // The user has clicked somewhere on the sprotty diagram (not the port edit UI) + // while the UI was open. In this case we hide the UI. + // This may not be exactly accurate because the UI can close itself when + // the change was saved but in those cases editUIVisible is still true. + // However hiding it one more time here for those cases is not a problem. + // Because it is already hidden, nothing will happen and after one click + // editUIVisible will be false again. + this.editUIVisible = false; + return [ + SetUIExtensionVisibilityAction.create({ + extensionId: AssignmentEditUi.ID, + visible: false, + contextElementsId: [target.id], + }), + ]; + } + + return []; + } + + doubleClick(target: SModelElementImpl): (Action | Promise)[] { + console.debug(target.type) + if (target instanceof DfdOutputPortImpl) { + // The user has double clicked on a dfd output port + // => show the OutputPortEditUI for this port. + this.editUIVisible = true; + return [ + SetUIExtensionVisibilityAction.create({ + extensionId: AssignmentEditUi.ID, + visible: true, + contextElementsId: [target.id], + }), + ]; + } + + return []; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/assignment/di.config.ts b/frontend/webEditor/src/assignment/di.config.ts new file mode 100644 index 00000000..caf4a173 --- /dev/null +++ b/frontend/webEditor/src/assignment/di.config.ts @@ -0,0 +1,11 @@ +import { ContainerModule } from "inversify"; +import { AssignmentEditUi } from "./AssignmentEditUi"; +import { TYPES } from "sprotty"; +import { OutputPortEditUIMouseListener } from "./clickListener"; + +export const assignmentModule = new ContainerModule((bind) => { + bind(AssignmentEditUi).toSelf().inSingletonScope() + bind(TYPES.IUIExtension).toService(AssignmentEditUi); + + bind(TYPES.MouseListener).to(OutputPortEditUIMouseListener).inSingletonScope(); +}) \ No newline at end of file diff --git a/frontend/webEditor/src/assignment/language.ts b/frontend/webEditor/src/assignment/language.ts new file mode 100644 index 00000000..032e6e9c --- /dev/null +++ b/frontend/webEditor/src/assignment/language.ts @@ -0,0 +1,296 @@ +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { LanguageTreeNode } from "../languages/tokenize"; +import { ConstantWord, ListWord, Word } from "../languages/words"; +import { DfdNodeImpl } from "../diagram/nodes/common"; +import { WordCompletion } from "../languages/autocomplete"; + +export const ASSIGNMENT_LANGUAGE_ID = 'dfd-assignment-language' + +const startOfLineKeywords = ["forward", "assign", "set", "unset"]; +const statementKeywords = [...startOfLineKeywords, "if", "from"]; +const constantsKeywords = ["TRUE", "FALSE"]; +export const assignmentLanguageMonarchDefinition: monaco.languages.IMonarchLanguage = { + keywords: [...statementKeywords, ...constantsKeywords], + + operators: ["=", "||", "&&", "!"], + + symbols: /[=>[] { + return [ + buildSetOrUnsetStatement(labelTypeRegistry, "set"), + buildSetOrUnsetStatement(labelTypeRegistry, "unset"), + buildForwardStatement(port), + buildAssignStatement(labelTypeRegistry, port), + ]; + } + + function buildSetOrUnsetStatement( + labelTypeRegistry: LabelTypeRegistry, + keyword: string, + ): LanguageTreeNode { + const labelNode: LanguageTreeNode = { + word: new ListWord(new LabelWord(labelTypeRegistry)), + children: [], + }; + return { + word: new ConstantWord(keyword), + children: [labelNode], + }; + } + + function buildForwardStatement(port: DfdOutputPortImpl): LanguageTreeNode { + const inputNode: LanguageTreeNode = { + word: new ListWord(new InputWord(port)), + children: [], + }; + return { + word: new ConstantWord("forward"), + children: [inputNode], + }; + } + + function buildAssignStatement( + labelTypeRegistry: LabelTypeRegistry, + port: DfdOutputPortImpl, + ): LanguageTreeNode { + const fromNode: LanguageTreeNode = { + word: new ConstantWord("from"), + children: [ + { + word: new ListWord(new InputWord(port)), + children: [], + }, + ], + }; + const ifNode: LanguageTreeNode = { + word: new ConstantWord("if"), + children: buildCondition(labelTypeRegistry, fromNode, port), + }; + return { + word: new ConstantWord("assign"), + children: [ + { + word: new LabelWord(labelTypeRegistry), + children: [ifNode], + }, + ], + }; + } + + function buildCondition(labelTypeRegistry: LabelTypeRegistry, nextNode: LanguageTreeNode, port: DfdOutputPortImpl) { + const connectors: LanguageTreeNode[] = ["&&", "||"].map((o) => ({ + word: new ConstantWord(o), + children: [], + })); + + const expressors: LanguageTreeNode[] = [ + new ConstantWord("TRUE"), + new ConstantWord("FALSE"), + new InputLabelWord(labelTypeRegistry, port), + ].map((e) => ({ + word: e, + children: [...connectors, nextNode], + canBeFinal: true, + })); + + connectors.forEach((c) => { + c.children = expressors; + }); + return expressors; + } +} + +abstract class InputAwareWord { + constructor(private port: DfdOutputPortImpl) {} + + protected getAvailableInputs(): string[] { + const parent = this.port.parent; + if (parent instanceof DfdNodeImpl) { + return parent.getAvailableInputs().filter((input) => input !== undefined) as string[]; + } + return []; + } +} + +class LabelWord implements Word { + constructor(private readonly labelTypeRegistry: LabelTypeRegistry) {} + + completionOptions(word: string): WordCompletion[] { + const parts = word.split("."); + + if (parts.length == 1) { + return this.labelTypeRegistry.getLabelTypes().map((l) => ({ + insertText: l.name, + kind: monaco.languages.CompletionItemKind.Class, + })); + } else if (parts.length == 2) { + const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); + if (!type) { + return []; + } + + return type.values.map((l) => ({ + insertText: l.text, + kind: monaco.languages.CompletionItemKind.Enum, + startOffset: parts[0].length + 1, + })); + } + + return []; + } + + verify(word: string): string[] { + const parts = word.split("."); + + if (parts.length > 2) { + return ["Expected at most 2 parts in characteristic selector"]; + } + + const type = this.labelTypeRegistry.getLabelTypes().find((l) => l.name === parts[0]); + if (!type) { + return ['Unknown label type "' + parts[0] + '"']; + } + + if (parts.length < 2) { + return ["Expected characteristic to have value"]; + } + + if (parts[1].startsWith("$") && parts[1].length >= 2) { + return []; + } + + const label = type.values.find((l) => l.text === parts[1]); + if (!label) { + return ['Unknown label value "' + parts[1] + '" for type "' + parts[0] + '"']; + } + + return []; + } +/* + replaceWord(text: string, replacement: ReplacementData) { + if (replacement.type == "Label" && text == replacement.old) { + return replacement.replacement; + } + return text; + }*/ +} + +class InputWord extends InputAwareWord implements Word { + completionOptions(): WordCompletion[] { + const inputs = this.getAvailableInputs(); + return inputs.map((input) => ({ + insertText: input, + kind: monaco.languages.CompletionItemKind.Variable, + })); + } + + verify(word: string): string[] { + const availableInputs = this.getAvailableInputs(); + if (availableInputs.includes(word)) { + return []; + } + return [`Unknown input "${word}"`]; + } + + /*replaceWord(text: string, replacement: ReplacementData) { + if (replacement.type == "Input" && text == replacement.old) { + return replacement.replacement; + } + return text; + }*/ +} + +class InputLabelWord implements Word { + private inputWord: InputWord; + private labelWord: LabelWord; + + constructor(labelTypeRegistry: LabelTypeRegistry, port: DfdOutputPortImpl) { + this.inputWord = new InputWord(port); + this.labelWord = new LabelWord(labelTypeRegistry); + } + + completionOptions(word: string): WordCompletion[] { + const parts = this.getParts(word); + if (parts[1] === undefined) { + return this.inputWord.completionOptions().map((c) => ({ + ...c, + insertText: c.insertText, + })); + } else if (parts.length >= 2) { + return this.labelWord.completionOptions(parts[1]).map((c) => ({ + ...c, + insertText: c.insertText, + startOffset: (c.startOffset ?? 0) + parts[0].length + 1, // +1 for the dot + })); + } + return []; + } + + verify(word: string): string[] { + const parts = this.getParts(word); + const inputErrors = this.inputWord.verify(parts[0]); + if (inputErrors.length > 0) { + return inputErrors; + } + if (parts[1] === undefined) { + return ["Expected input and label separated by a dot"]; + } + const labelErrors = this.labelWord.verify(parts[1]); + return [...inputErrors, ...labelErrors]; + } + + /*replaceWord(text: string, replacement: ReplacementData) { + const [input, label] = this.getParts(text); + if (replacement.type == "Input" && input === replacement.old) { + return replacement.replacement + (label ? "." + label : ""); + } else if (replacement.type == "Label" && label === replacement.old) { + return input + "." + replacement.replacement; + } + return text; + }*/ + + private getParts(text: string): [string, string] | [string, undefined] { + if (text.includes(".")) { + const index = text.indexOf("."); + const input = text.substring(0, index); + const label = text.substring(index + 1); + return [input, label]; + } + return [text, undefined]; + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/constraint/ConstraintMenu.ts b/frontend/webEditor/src/constraint/ConstraintMenu.ts index 48fba732..99ab56a8 100644 --- a/frontend/webEditor/src/constraint/ConstraintMenu.ts +++ b/frontend/webEditor/src/constraint/ConstraintMenu.ts @@ -18,10 +18,11 @@ import { constraintDslLanguageMonarchDefinition, ConstraintDslTreeBuilder, DSL_L import { verify } from "../languages/verify"; import { DfdCompletionItemProvider } from "../languages/autocomplete"; import { AnalyzeAction } from "../serialize/analyze"; +import { ApplyableTheme, Theme, ThemeManager, ThemeSwitchable } from "../settings/Theme"; @injectable() -export class ConstraintMenu extends AccordionUiExtension { - static readonly ID = "constraint-menu"; +export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitchable { + static readonly ID = "constraint-menu"; private editorContainer: HTMLDivElement = document.createElement("div") as HTMLDivElement; private validationLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; private editor?: monaco.editor.IStandaloneCodeEditor; @@ -37,6 +38,7 @@ export class ConstraintMenu extends AccordionUiExtension { @inject(TYPES.IActionDispatcher) private readonly dispatcher: IActionDispatcher, @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager ) { super("left", "up"); this.constraintRegistry = constraintRegistry; @@ -102,7 +104,7 @@ export class ConstraintMenu extends AccordionUiExtension { new DfdCompletionItemProvider(this.tree), ); - const monacoTheme = /*ThemeManager.useDarkMode ?*/ "vs-dark" //: "vs"; + const monacoTheme = this.themeManager.getTheme() === Theme.DARK ? "vs-dark" : "vs"; this.editor = monaco.editor.create(this.editorContainer, { minimap: { // takes too much space, not useful for our use case @@ -220,8 +222,8 @@ export class ConstraintMenu extends AccordionUiExtension { e.layout({ height: cHeight, width: cWidth }); } - switchTheme(useDark: boolean): void { - this.editor?.updateOptions({ theme: useDark ? "vs-dark" : "vs" }); + switchTheme(theme: ApplyableTheme): void { + this.editor?.updateOptions({ theme: theme == Theme.DARK ? "vs-dark" : "vs" }); } private buildOptionsButton(): HTMLElement { diff --git a/frontend/webEditor/src/constraint/di.config.ts b/frontend/webEditor/src/constraint/di.config.ts index fb9d63f5..61884b78 100644 --- a/frontend/webEditor/src/constraint/di.config.ts +++ b/frontend/webEditor/src/constraint/di.config.ts @@ -3,6 +3,7 @@ import { TYPES } from "sprotty"; import { EDITOR_TYPES } from "../editorTypes"; import { ConstraintMenu } from "./ConstraintMenu"; import { ConstraintRegistry } from "./constraintRegistry"; +import { ThemeSwitchable } from "../settings/Theme"; export const constraintModule = new ContainerModule((bind) => { bind(ConstraintRegistry).toSelf().inSingletonScope(); @@ -10,4 +11,5 @@ export const constraintModule = new ContainerModule((bind) => { bind(ConstraintMenu).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(ConstraintMenu); bind(EDITOR_TYPES.DefaultUIElement).toService(ConstraintMenu); + bind(ThemeSwitchable).toService(ConstraintMenu) }) \ No newline at end of file diff --git a/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx b/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx index f3173abf..32c24814 100644 --- a/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx +++ b/frontend/webEditor/src/diagram/ports/DfdOutputPort.tsx @@ -1,10 +1,14 @@ /** @jsx svg */ -import { injectable } from "inversify"; +import { inject, injectable } from "inversify"; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { svg, isEditableLabel, SRoutableElementImpl, ShapeView, RenderingContext } from "sprotty"; import { SPort } from "sprotty-protocol"; import { DfdPortImpl } from "./common"; import { VNode, VNodeStyle } from "snabbdom"; +import { LabelTypeRegistry } from "../../labels/LabelTypeRegistry"; +import { LanguageTreeNode, tokenize } from "../../languages/tokenize"; +import { verify, VerifyWord } from "../../languages/verify"; +import { AssignmentLanguageTreeBuilder } from "../../assignment/language"; export interface DfdOutputPort extends SPort { behavior: string; @@ -14,7 +18,12 @@ export interface DfdOutputPort extends SPort { export class DfdOutputPortImpl extends DfdPortImpl { private behavior: string = ""; private validBehavior: boolean = true; + private tree?: LanguageTreeNode[]; + @inject(LabelTypeRegistry) private labelTypeRegistry?: LabelTypeRegistry; + constructor() { + super(); + } get editableLabel() { const label = this.children.find((element) => element.type === "label:invisible"); @@ -37,8 +46,6 @@ export class DfdOutputPortImpl extends DfdPortImpl { const style: VNodeStyle = { opacity: this.opacity.toString(), }; - // TODO - // if (!labelTypeRegistry) return style; if (!this.validBehavior) { style["--port-border"] = "#ff0000"; @@ -54,10 +61,14 @@ export class DfdOutputPortImpl extends DfdPortImpl { this.validBehavior = true; return; } - // TODO - const errors = []/*new AutoCompleteTree(TreeBuilder.buildTree(labelTypeRegistry, this)).verify( - this.behavior.split("\n"), - );*/ + + if (!this.tree) { + if (!this.labelTypeRegistry) { + return; + } + this.tree = AssignmentLanguageTreeBuilder.buildTree(this, this.labelTypeRegistry); + } + const errors = verify(tokenize(this.behavior.split("\n")), this.tree); this.validBehavior = errors.length === 0; } diff --git a/frontend/webEditor/src/helpUi/helpUi.css b/frontend/webEditor/src/helpUi/helpUi.css index b0351b89..650ceb33 100644 --- a/frontend/webEditor/src/helpUi/helpUi.css +++ b/frontend/webEditor/src/helpUi/helpUi.css @@ -12,6 +12,7 @@ width: 16px; background-size: 16px 16px; vertical-align: text-top; + margin-right: 4px; } } diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 4c16ee9a..47f1c5b8 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -21,6 +21,7 @@ import { fileNameModule } from "./fileName/di.config"; import { settingsModule } from "./settings/di.config"; import { toolPaletteModule } from "./toolPalette/di.config"; import { constraintModule } from "./constraint/di.config"; +import { assignmentModule } from "./assignment/di.config"; const container = new Container(); @@ -45,7 +46,8 @@ container.load( fileNameModule, settingsModule, toolPaletteModule, - constraintModule + constraintModule, + assignmentModule ) const startUpAgents = container.getAll(StartUpAgent) diff --git a/frontend/webEditor/src/settings/Theme.ts b/frontend/webEditor/src/settings/Theme.ts index 98c92f1a..4ef7dfde 100644 --- a/frontend/webEditor/src/settings/Theme.ts +++ b/frontend/webEditor/src/settings/Theme.ts @@ -6,7 +6,7 @@ export enum Theme { SYSTEM_DEFAULT = "System Default", } -type ApplyableTheme = Theme.LIGHT | Theme.DARK +export type ApplyableTheme = Theme.LIGHT | Theme.DARK export class ThemeManager extends SettingsValue { private static SYSTEM_DEFAULT: ApplyableTheme = @@ -26,14 +26,27 @@ export class ThemeManager extends SettingsValue { } } -export function registerThemeSwitch(themeManager: ThemeManager) { +export const ThemeSwitchable = Symbol('ThemeSwitchable') + +export interface ThemeSwitchable { + switchTheme: (newTheme: ApplyableTheme) => void +} + +export function registerThemeSwitch(themeManager: ThemeManager, switchables: ThemeSwitchable[]) { themeManager.registerListener(() => { - const rootElement = document.querySelector(":root") as HTMLElement; + setTheme(themeManager, switchables) + }) + setTheme(themeManager, switchables) +} + +function setTheme(themeManager: ThemeManager, switchables: ThemeSwitchable[]) { + const rootElement = document.querySelector(":root") as HTMLElement; const sprottyElement = document.querySelector("#sprotty") as HTMLElement; const value = themeManager.getTheme() === Theme.DARK ? "dark" : "light"; rootElement.setAttribute("data-theme", value); sprottyElement.setAttribute("data-theme", value); localStorage.setItem(ThemeManager.LOCAL_STORAGE_KEY, themeManager.get()) - }) + + switchables.forEach(s => s.switchTheme(themeManager.getTheme())) } \ No newline at end of file diff --git a/frontend/webEditor/src/settings/settingsUi.css b/frontend/webEditor/src/settings/settingsUi.css index e1edcd9f..90517902 100644 --- a/frontend/webEditor/src/settings/settingsUi.css +++ b/frontend/webEditor/src/settings/settingsUi.css @@ -13,6 +13,7 @@ div.settings-ui { width: 16px; background-size: 16px 16px; vertical-align: text-top; + margin-right: 4px; } #settings-content { diff --git a/frontend/webEditor/src/startUpAgent/settingsInit.ts b/frontend/webEditor/src/startUpAgent/settingsInit.ts index 24e4f6bd..81ac68b0 100644 --- a/frontend/webEditor/src/startUpAgent/settingsInit.ts +++ b/frontend/webEditor/src/startUpAgent/settingsInit.ts @@ -1,18 +1,18 @@ import { IStartUpAgent } from "./StartUpAgent"; -import { inject } from "inversify"; +import { inject, multiInject } from "inversify"; import { linkReadOnly } from "../settings/initialize"; import { EditorModeController } from "../settings/editorMode"; import { SETTINGS, HideEdgeNames, SimplifyNodeNames } from "../settings/Settings"; -import { registerThemeSwitch, ThemeManager } from "../settings/Theme"; +import { registerThemeSwitch, ThemeManager, ThemeSwitchable } from "../settings/Theme"; export class SettingsInitStartUpAgent implements IStartUpAgent { constructor(@inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, @inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, - @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController) {} + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, @multiInject(ThemeSwitchable) private readonly switchables: ThemeSwitchable[]) {} run(): void { linkReadOnly(this.editorModeController, this.simplifyNodeNames, this.hideEdgeNames); - registerThemeSwitch(this.themeManager) + registerThemeSwitch(this.themeManager, this.switchables) } } \ No newline at end of file From 77ecf6217d165c94ddaf0ae215bb61d179e2d977 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Tue, 2 Dec 2025 08:08:43 +0100 Subject: [PATCH 29/41] clarify command name --- .../src/diagram/nodes/DfdNodeLabels.tsx | 2 +- .../webEditor/src/labels/LabelTypeEditorUi.ts | 2 +- .../{command.ts => assignmentCommand.ts} | 14 +-- frontend/webEditor/src/labels/di.config.ts | 4 +- frontend/webEditor/src/labels/dragAndDrop.ts | 2 +- .../webEditor/src/labels/registryCommands.ts | 107 ++++++++++++++++++ 6 files changed, 119 insertions(+), 12 deletions(-) rename frontend/webEditor/src/labels/{command.ts => assignmentCommand.ts} (91%) create mode 100644 frontend/webEditor/src/labels/registryCommands.ts diff --git a/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx index 2c2c13a1..c391edb5 100644 --- a/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx +++ b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx @@ -7,7 +7,7 @@ import { LabelTypeRegistry } from "../../labels/LabelTypeRegistry"; import { calculateTextSize } from "../../utils/TextSize"; import { VNode } from "snabbdom"; import { ContainsDfdLabels } from "../../labels/feature"; -import { RemoveLabelAssignmentAction } from "../../labels/command"; +import { RemoveLabelAssignmentAction } from "../../labels/assignmentCommand"; @injectable() export class DfdNodeLabelRenderer { diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts index 715f9493..7b4cd090 100644 --- a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -7,7 +7,7 @@ import { LabelTypeRegistry } from "./LabelTypeRegistry"; import "./labelTypeEditorUi.css"; import { dynamicallySetInputSize } from "../utils/TextSize"; import { LABEL_ASSIGNMENT_MIME_TYPE } from "./dragAndDrop"; -import { AddLabelAssignmentAction } from "./command"; +import { AddLabelAssignmentAction } from "./assignmentCommand"; import { IActionDispatcher, TYPES } from "sprotty"; export class LabelTypeEditorUi extends AccordionUiExtension { diff --git a/frontend/webEditor/src/labels/command.ts b/frontend/webEditor/src/labels/assignmentCommand.ts similarity index 91% rename from frontend/webEditor/src/labels/command.ts rename to frontend/webEditor/src/labels/assignmentCommand.ts index b24d285d..80f3d45d 100644 --- a/frontend/webEditor/src/labels/command.ts +++ b/frontend/webEditor/src/labels/assignmentCommand.ts @@ -17,16 +17,16 @@ import { inject, injectable } from "inversify"; import { SETTINGS } from "../settings/Settings"; import { ContainsDfdLabels, containsDfdLabels } from "./feature"; -interface LabelAction extends Action { +interface LabelAssignmentAction extends Action { action: "add" | "remove"; element?: ContainsDfdLabels & SNodeImpl; labelAssignment: LabelAssignment; } export namespace AddLabelAssignmentAction { - export function create(labelAssignment: LabelAssignment, element?: ContainsDfdLabels & SNodeImpl): LabelAction { + export function create(labelAssignment: LabelAssignment, element?: ContainsDfdLabels & SNodeImpl): LabelAssignmentAction { return { - kind: LabelCommand.KIND, + kind: LabelAssignmentCommand.KIND, action: 'add', labelAssignment, element @@ -35,9 +35,9 @@ export namespace AddLabelAssignmentAction { } export namespace RemoveLabelAssignmentAction { - export function create(labelAssignment: LabelAssignment, element?: ContainsDfdLabels & SNodeImpl): LabelAction { + export function create(labelAssignment: LabelAssignment, element?: ContainsDfdLabels & SNodeImpl): LabelAssignmentAction { return { - kind: LabelCommand.KIND, + kind: LabelAssignmentCommand.KIND, action: 'remove', labelAssignment, element @@ -46,13 +46,13 @@ export namespace RemoveLabelAssignmentAction { } @injectable() -export class LabelCommand implements Command { +export class LabelAssignmentCommand implements Command { public static readonly KIND = 'labelAction'; private elements?: ContainsDfdLabels[]; constructor( - @inject(TYPES.Action) private readonly action: LabelAction, + @inject(TYPES.Action) private readonly action: LabelAssignmentAction, @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, @inject(TYPES.ISnapper) private readonly snapper: ISnapper, ) {} diff --git a/frontend/webEditor/src/labels/di.config.ts b/frontend/webEditor/src/labels/di.config.ts index e0ea48b5..1b86b4b3 100644 --- a/frontend/webEditor/src/labels/di.config.ts +++ b/frontend/webEditor/src/labels/di.config.ts @@ -3,7 +3,7 @@ import { LabelTypeRegistry } from "./LabelTypeRegistry"; import { LabelTypeEditorUi } from "./LabelTypeEditorUi"; import { configureCommand, TYPES } from "sprotty"; import { EDITOR_TYPES } from "../editorTypes"; -import { LabelCommand } from "./command"; +import { LabelAssignmentCommand } from "./assignmentCommand"; import { DfdLabelMouseDropListener } from "./dragAndDrop"; export const labelModule = new ContainerModule((bind, _, isBound) => { @@ -14,6 +14,6 @@ export const labelModule = new ContainerModule((bind, _, isBound) => { bind(EDITOR_TYPES.DefaultUIElement).to(LabelTypeEditorUi); - configureCommand({ bind, isBound }, LabelCommand); + configureCommand({ bind, isBound }, LabelAssignmentCommand); bind(TYPES.MouseListener).to(DfdLabelMouseDropListener); }) \ No newline at end of file diff --git a/frontend/webEditor/src/labels/dragAndDrop.ts b/frontend/webEditor/src/labels/dragAndDrop.ts index e1503bc9..96a8f209 100644 --- a/frontend/webEditor/src/labels/dragAndDrop.ts +++ b/frontend/webEditor/src/labels/dragAndDrop.ts @@ -10,7 +10,7 @@ import { SNodeImpl } from "sprotty"; import { LabelAssignment } from "./LabelType"; -import { AddLabelAssignmentAction } from "./command"; +import { AddLabelAssignmentAction } from "./assignmentCommand"; import { containsDfdLabels, ContainsDfdLabels } from "./feature"; export const LABEL_ASSIGNMENT_MIME_TYPE = "application/x-label-assignment"; diff --git a/frontend/webEditor/src/labels/registryCommands.ts b/frontend/webEditor/src/labels/registryCommands.ts new file mode 100644 index 00000000..27ea6cd3 --- /dev/null +++ b/frontend/webEditor/src/labels/registryCommands.ts @@ -0,0 +1,107 @@ +import { Action } from "sprotty-protocol"; +import { LabelTypeRegistry } from "./LabelTypeRegistry"; +import { Command, CommandExecutionContext, CommandReturn, SParentElementImpl, TYPES } from "sprotty"; +import { DfdNodeImpl } from "../diagram/nodes/common"; +import { LabelAssignment } from "./LabelType"; +import { inject } from "inversify"; + +// TODO: readd +abstract class LabelCommand implements Command { + + constructor(protected labelTypeRegistry: LabelTypeRegistry) {} + abstract execute(context: CommandExecutionContext): CommandReturn + abstract undo(context: CommandExecutionContext): CommandReturn + abstract redo(context: CommandExecutionContext): CommandReturn + + addLabelType() { + return this.labelTypeRegistry.registerLabelType('').id + } + + deleteLabelType(id: string, root: SParentElementImpl) { + this.labelTypeRegistry.unregisterLabelType(id) + this.removeLabelAssignments(root, (a) => a.labelTypeId === id) + } + + addLabelTypeValue(typeId: string) { + return this.labelTypeRegistry.registerLabelTypeValue(typeId, "").id + } + + deleteLabelTypeValue(typeId: string, valueId: string, root: SParentElementImpl) { + this.labelTypeRegistry.unregisterLabelTypeValue(typeId, valueId) + this.removeLabelAssignments(root, (a) => a.labelTypeId === typeId && a.labelTypeValueId === valueId) + } + + removeLabelAssignments(node: SParentElementImpl, filter: (s: LabelAssignment) => boolean) { + if (node instanceof DfdNodeImpl) { + node.labels = node.labels.filter(filter) + } + for (const child of node.children) { + this.removeLabelAssignments(child, filter) + } + } +} + +export namespace AddLabelTypeAction { + export const KIND = 'add-label-type' + export function create(): Action { + return { kind: KIND} + } +} + +export class AddLabelTypeCommand extends LabelCommand { + static readonly KIND = AddLabelTypeAction.KIND + private addedId?: string + + constructor(@inject(TYPES.Action) _: Action, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry) { + super(labelTypeRegistry) + } + + execute(context: CommandExecutionContext): CommandReturn { + this.addedId = this.addLabelType() + return context.root + } + undo(context: CommandExecutionContext): CommandReturn { + this.deleteLabelType(this.addedId!, context.root) + return context.root + } + redo(context: CommandExecutionContext): CommandReturn { + this.addedId = this.addLabelType() + return context.root + } +} + +interface AddLabelTypeValueAction extends Action { + typeId: string +} + +namespace AddLabelTypeValueAction { + export const KIND = 'add-label-type-value' + export function create(typeId: string): AddLabelTypeValueAction { + return { + kind: KIND, + typeId + } + } +} + +export class AddLabelTypeValueCommand extends LabelCommand { + static readonly KIND = AddLabelTypeValueAction.KIND + private addedId?: string + + constructor(@inject(TYPES.Action) private action: AddLabelTypeValueAction, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry) { + super(labelTypeRegistry) + } + + execute(context: CommandExecutionContext): CommandReturn { + this.addedId = this.addLabelTypeValue(this.action.typeId) + return context.root + } + undo(context: CommandExecutionContext): CommandReturn { + this.deleteLabelTypeValue(this.action.typeId, this.addedId!, context.root) + return context.root + } + redo(context: CommandExecutionContext): CommandReturn { + this.addedId = this.addLabelTypeValue(this.action.typeId) + return context.root + } +} \ No newline at end of file From 948b7892c9964b5f07e9ad2eebe4dd7d11b72f0b Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Tue, 2 Dec 2025 09:09:18 +0100 Subject: [PATCH 30/41] node annotations --- frontend/webEditor/src/diagram/di.config.ts | 6 + .../webEditor/src/diagram/nodes/annotation.ts | 196 ++++++++++++++++++ .../src/diagram/nodes/nodeAnnotationUi.css | 14 ++ frontend/webEditor/src/index.ts | 1 + frontend/webEditor/src/settings/SettingsUi.ts | 7 +- .../webEditor/src/settings/ShownLabels.ts | 13 ++ frontend/webEditor/src/settings/di.config.ts | 2 + 7 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 frontend/webEditor/src/diagram/nodes/annotation.ts create mode 100644 frontend/webEditor/src/diagram/nodes/nodeAnnotationUi.css create mode 100644 frontend/webEditor/src/settings/ShownLabels.ts diff --git a/frontend/webEditor/src/diagram/di.config.ts b/frontend/webEditor/src/diagram/di.config.ts index 62a50bb7..a10f9344 100644 --- a/frontend/webEditor/src/diagram/di.config.ts +++ b/frontend/webEditor/src/diagram/di.config.ts @@ -14,6 +14,7 @@ import { DfdEditLabelValidatorDecorator } from "./labels/EditLabelDecorator"; import { DfdEditLabelValidator } from "./labels/EditLabelValidator"; import { NoScrollEditLabelUI } from "./labels/NoScrollEditLabelUI"; import { PortAwareSnapper, AlwaysSnapPortsMoveMouseListener, ReSnapPortsAfterLabelChangeCommand } from "./ports/portSnapper"; +import { DfdNodeAnnotationUI, DfdNodeAnnotationUIMouseListener } from "./nodes/annotation"; export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; @@ -28,6 +29,11 @@ export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) bind(TYPES.MouseListener).to(AlwaysSnapPortsMoveMouseListener).inSingletonScope(); configureCommand(context, ReSnapPortsAfterLabelChangeCommand); + bind(DfdNodeAnnotationUI).toSelf().inSingletonScope() + bind(TYPES.IUIExtension).toService(DfdNodeAnnotationUI) + bind(DfdNodeAnnotationUIMouseListener).toSelf().inSingletonScope() + bind(TYPES.MouseListener).toService(DfdNodeAnnotationUIMouseListener) + configureModelElement(context, "graph", SGraphImpl, SGraphView); configureModelElement(context, "node:storage", StorageNodeImpl, StorageNodeView); diff --git a/frontend/webEditor/src/diagram/nodes/annotation.ts b/frontend/webEditor/src/diagram/nodes/annotation.ts new file mode 100644 index 00000000..c8b2fe95 --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/annotation.ts @@ -0,0 +1,196 @@ +import { inject, injectable } from "inversify"; +import { MouseListener, TYPES, IActionDispatcher, SModelElementImpl, SChildElementImpl, SetUIExtensionVisibilityAction, AbstractUIExtension, SModelRootImpl } from "sprotty"; +import { Action } from "sprotty-protocol"; +import { DfdNodeImpl } from "./common"; +import "./nodeAnnotationUi.css" +import { ShownLabels, ShownLabelsValue } from "../../settings/ShownLabels"; +import { SETTINGS } from "../../settings/Settings"; + +export class DfdNodeAnnotationUIMouseListener extends MouseListener { + private stillTimeout: number | undefined; + private lastTarget?: DfdNodeImpl + private lastPosition = { x: 0, y: 0 }; + + constructor(@inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher) { + super(); + } + + mouseMove(target: SModelElementImpl, event: MouseEvent): Action[] { + const dfdNode = this.findDfdNode(target); + if (!dfdNode) { + if (this.stillTimeout) { + clearTimeout(this.stillTimeout); + this.stillTimeout = undefined; + } + return this.hidePopup(); + } + this.lastPosition = { x: event.clientX, y: event.clientY }; + + if (dfdNode === this.lastTarget) { + return [] + } + + this.stillTimeout = setTimeout(() => { + // When the mouse has not moved for 500ms, we show the popup + this.stillTimeout = undefined; + + if (dfdNode.opacity !== 1) { + // Only show when opacity is 1. + // The opacity is not 1 when the node is currently being created but has not been + // placed yet. + // In this case we don't want to show the popup + // and interfere with the creation process. + return; + } + + this.showPopup(dfdNode); + }, 500); + + if (this.lastTarget !== dfdNode) { + this.lastTarget = dfdNode + return this.hidePopup() + } + return [] + } + + private findDfdNode(currentNode: SModelElementImpl): DfdNodeImpl | undefined { + if (currentNode instanceof DfdNodeImpl) { + return currentNode; + } else if (currentNode instanceof SChildElementImpl && currentNode.parent) { + return this.findDfdNode(currentNode.parent); + } else { + return undefined; + } + } + + private showPopup(target: DfdNodeImpl): void { + if (!target.annotations) { + // no annotation. No need to show the popup. + return; + } + + this.actionDispatcher.dispatch( + SetUIExtensionVisibilityAction.create({ + extensionId: DfdNodeAnnotationUI.ID, + visible: true, + contextElementsId: [target.id], + }), + ); + } + + private hidePopup() { + return [SetUIExtensionVisibilityAction.create({extensionId: DfdNodeAnnotationUI.ID, visible: false})] + } + + public getMousePosition(): { x: number; y: number } { + return this.lastPosition; + } +} + +@injectable() +export class DfdNodeAnnotationUI extends AbstractUIExtension { + static readonly ID = "dfd-node-annotation-ui"; + + private readonly annotationParagraph = document.createElement("p") as HTMLParagraphElement; + + constructor( + @inject(DfdNodeAnnotationUIMouseListener) + private readonly mouseListener: DfdNodeAnnotationUIMouseListener, + @inject(SETTINGS.ShownLabels) private shownLabels: ShownLabelsValue, + ) { + super(); + } + + id(): string { + return DfdNodeAnnotationUI.ID; + } + + containerClass(): string { + return this.id(); + } + + protected override initializeContents(containerElement: HTMLElement): void { + containerElement.classList.add("ui-float"); + containerElement.appendChild(this.annotationParagraph); + } + + protected override onBeforeShow( + containerElement: HTMLElement, + root: Readonly, + ...contextElementIds: string[] + ): void { + if (contextElementIds.length !== 1) { + this.annotationParagraph.innerText = + "UI Error: Expected exactly one context element id, but got " + contextElementIds.length; + return; + } + + const node = root.index.getById(contextElementIds[0]); + if (!(node instanceof DfdNodeImpl)) { + this.annotationParagraph.innerText = + "UI Error: Expected context element to be a DfdNodeImpl, but got " + node; + return; + } + + // Clear previous content + this.annotationParagraph.innerText = ""; + + // Set position + // 2 offset to ensure the mouse is inside the popup when showing it. + // Otherwise it would be on the node instead of the popup because of the rounded corners. + // When moving the cursor from the node to the popup, the popup would move a bit + // because the cursor is going a bit over the model and then the popup would re-show + // with the new position after the timeout. + const mousePosition = this.mouseListener.getMousePosition(); + const annotationPosition = { + x: mousePosition.x - 2, + y: mousePosition.y - 2, + }; + containerElement.style.left = `${annotationPosition.x}px`; + containerElement.style.top = `${annotationPosition.y}px`; + + // Set tooltip size and scroll to prevent them from growing out of the screen + containerElement.style.overflowY = "auto"; + this.annotationParagraph.style.whiteSpace = "normal"; + this.annotationParagraph.style.wordBreak = "break-word"; + const screenWidth = window.innerWidth; + const screenHeight = window.innerHeight; + containerElement.style.maxWidth = `${Math.max(screenWidth - annotationPosition.x - 50, 100)}px`; + containerElement.style.maxHeight = `${Math.max(screenHeight - annotationPosition.y - 50, 50)}px`; + + // Set content + if (!node.annotations || node.annotations.length == 0) { + this.annotationParagraph.innerText = "No errors"; + return; + } + + this.annotationParagraph.innerHTML = ""; + + const mode = this.shownLabels.get(); + + node.annotations.forEach((a) => { + if ( + ((mode === ShownLabels.INCOMING || mode === ShownLabels.ALL) && a.message.trim().startsWith("Incoming")) || + ((mode === ShownLabels.OUTGOING || mode === ShownLabels.ALL) && a.message.trim().startsWith("Propagated")) || + a.message.startsWith("Constraint") + ) { + const line = document.createElement("div"); + line.style.display = "flex"; + line.style.alignItems = "center"; + line.style.gap = "6px"; // some spacing between icon and text + + if (a.icon) { + const iconI = document.createElement("i"); + iconI.classList.add("fa", `fa-${a.icon}`); + line.appendChild(iconI); + } + + const textSpan = document.createElement("span"); + textSpan.innerText = a.message; + line.appendChild(textSpan); + + this.annotationParagraph.appendChild(line); + } + }); + } +} diff --git a/frontend/webEditor/src/diagram/nodes/nodeAnnotationUi.css b/frontend/webEditor/src/diagram/nodes/nodeAnnotationUi.css new file mode 100644 index 00000000..393960a7 --- /dev/null +++ b/frontend/webEditor/src/diagram/nodes/nodeAnnotationUi.css @@ -0,0 +1,14 @@ +.dfd-node-annotation-ui { + /* don't break lines into multiple when they don't fit on screen. + Just let the popup clip outside the screen when there is not enough space and let the + user move the popup at a position where the text is fully visible */ + white-space: nowrap; +} + +.dfd-node-annotation-ui p { + margin: 12px; +} + +.dfd-node-annotation-ui i.fa { + margin-right: 5px; +} diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 47f1c5b8..03ea742f 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -6,6 +6,7 @@ import "./assets/commonStyling.css" import "./assets/page.css" import "./assets/theme.css" import "@vscode/codicons/dist/codicon.css"; +import "@fortawesome/fontawesome-free/css/all.min.css"; import { helpUiModule } from "./helpUi/di.config"; import { IStartUpAgent, StartUpAgent } from "./startUpAgent/StartUpAgent"; import { startUpAgentModule } from "./startUpAgent/di.config"; diff --git a/frontend/webEditor/src/settings/SettingsUi.ts b/frontend/webEditor/src/settings/SettingsUi.ts index c9e6c621..02f0e6e9 100644 --- a/frontend/webEditor/src/settings/SettingsUi.ts +++ b/frontend/webEditor/src/settings/SettingsUi.ts @@ -5,6 +5,7 @@ import { AccordionUiExtension } from "../accordionUiExtension"; import { HideEdgeNames, SETTINGS, SimplifyNodeNames } from "./Settings"; import { EditorModeController } from "./editorMode"; import { Theme, ThemeManager } from "./Theme"; +import { ShownLabels, ShownLabelsValue } from "./ShownLabels"; @injectable() export class SettingsUI extends AccordionUiExtension { @@ -12,9 +13,10 @@ export class SettingsUI extends AccordionUiExtension { constructor( @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, + @inject(SETTINGS.ShownLabels) private readonly shownLabels: ShownLabelsValue, @inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, - @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, -@inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController) { + @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController) { super('right', 'up') } @@ -31,6 +33,7 @@ export class SettingsUI extends AccordionUiExtension { grid.id = 'settings-content' contentElement.appendChild(grid); this.addDropDown(grid, "Theme", this.themeManager, [Theme.SYSTEM_DEFAULT, Theme.LIGHT, Theme.DARK]) + this.addDropDown(grid, "Shown Labels", this.shownLabels, [ShownLabels.INCOMING, ShownLabels.OUTGOING, ShownLabels.ALL]) this.addBooleanSwitch(grid, "Hide Edge Names", this.hideEdgeNames); this.addBooleanSwitch(grid, "Simplify Node Names", this.simplifyNodeNames); this.addSwitch(grid, "Read Only", this.editorModeController, {true: "view", false: "edit"}); diff --git a/frontend/webEditor/src/settings/ShownLabels.ts b/frontend/webEditor/src/settings/ShownLabels.ts new file mode 100644 index 00000000..57235398 --- /dev/null +++ b/frontend/webEditor/src/settings/ShownLabels.ts @@ -0,0 +1,13 @@ +import { SettingsValue } from "./SettingsValue"; + +export enum ShownLabels { + INCOMING = "Incoming", + OUTGOING = "Outgoing", + ALL = "All" +} + +export class ShownLabelsValue extends SettingsValue { + constructor() { + super(ShownLabels.ALL) + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/settings/di.config.ts b/frontend/webEditor/src/settings/di.config.ts index b9538afb..50de944d 100644 --- a/frontend/webEditor/src/settings/di.config.ts +++ b/frontend/webEditor/src/settings/di.config.ts @@ -6,6 +6,7 @@ import { BoolSettingsValue } from "./SettingsValue"; import { TYPES } from "sprotty"; import { EditorModeController } from "./editorMode"; import { ThemeManager } from "./Theme"; +import { ShownLabelsValue } from "./ShownLabels"; export const settingsModule = new ContainerModule((bind) => { bind(SettingsUI).toSelf().inSingletonScope(); @@ -16,4 +17,5 @@ export const settingsModule = new ContainerModule((bind) => { bind(SETTINGS.HideEdgeNames).to(BoolSettingsValue).inSingletonScope(); bind(SETTINGS.SimplifyNodeNames).to(BoolSettingsValue).inSingletonScope(); bind(SETTINGS.Mode).to(EditorModeController).inSingletonScope(); + bind(SETTINGS.ShownLabels).to(ShownLabelsValue).inSingletonScope() }) \ No newline at end of file From 1f9eaa7206ffba8e376457acd1c20fb049ad6b7d Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Tue, 2 Dec 2025 10:00:46 +0100 Subject: [PATCH 31/41] constraint selection --- .../src/constraint/ConstraintMenu.ts | 14 +-- .../webEditor/src/constraint/di.config.ts | 9 +- .../webEditor/src/constraint/selection.ts | 86 +++++++++++++++++++ .../webEditor/src/constraint/tfgManager.ts | 18 ++++ 4 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 frontend/webEditor/src/constraint/selection.ts create mode 100644 frontend/webEditor/src/constraint/tfgManager.ts diff --git a/frontend/webEditor/src/constraint/ConstraintMenu.ts b/frontend/webEditor/src/constraint/ConstraintMenu.ts index 99ab56a8..71481edb 100644 --- a/frontend/webEditor/src/constraint/ConstraintMenu.ts +++ b/frontend/webEditor/src/constraint/ConstraintMenu.ts @@ -19,6 +19,7 @@ import { verify } from "../languages/verify"; import { DfdCompletionItemProvider } from "../languages/autocomplete"; import { AnalyzeAction } from "../serialize/analyze"; import { ApplyableTheme, Theme, ThemeManager, ThemeSwitchable } from "../settings/Theme"; +import { SelectConstraintsAction } from "./selection"; @injectable() export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitchable { @@ -177,7 +178,7 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha button.id = "run-button"; button.innerHTML = "Run"; button.onclick = () => { - this.dispatcher.dispatch(AnalyzeAction.create()); + this.dispatcher.dispatchAll([AnalyzeAction.create(), SelectConstraintsAction.create(this.constraintRegistry.getConstraintList().map(c => c.name))]); }; wrapper.appendChild(button); @@ -268,15 +269,14 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha this.optionsMenu.querySelectorAll("input[type=checkbox]").forEach((cb) => { if (cb !== allCb) cb.checked = true; }); - // TODO - /*this.dispatcher.dispatch( - ChooseConstraintAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), - );*/ + this.dispatcher.dispatch( + SelectConstraintsAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), + ); } else { this.optionsMenu.querySelectorAll("input[type=checkbox]").forEach((cb) => { if (cb !== allCb) cb.checked = false; }); - //this.dispatcher.dispatch(ChooseConstraintAction.create([])); + this.dispatcher.dispatch(SelectConstraintsAction.create([])); } } finally { this.ignoreCheckboxChange = false; @@ -309,7 +309,7 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha allCb.checked = individualCheckboxes.every((cb) => cb.checked); - //this.dispatcher.dispatch(ChooseConstraintAction.create(selected)); + this.dispatcher.dispatch(SelectConstraintsAction.create(selected)); }; label.appendChild(cb); diff --git a/frontend/webEditor/src/constraint/di.config.ts b/frontend/webEditor/src/constraint/di.config.ts index 61884b78..e9b1f101 100644 --- a/frontend/webEditor/src/constraint/di.config.ts +++ b/frontend/webEditor/src/constraint/di.config.ts @@ -1,15 +1,20 @@ import { ContainerModule } from "inversify"; -import { TYPES } from "sprotty"; +import { configureCommand, TYPES } from "sprotty"; import { EDITOR_TYPES } from "../editorTypes"; import { ConstraintMenu } from "./ConstraintMenu"; import { ConstraintRegistry } from "./constraintRegistry"; import { ThemeSwitchable } from "../settings/Theme"; +import { TFGManager } from "./tfgManager"; +import { SelectConstraintsCommand } from "./selection"; -export const constraintModule = new ContainerModule((bind) => { +export const constraintModule = new ContainerModule((bind, unbind, isBound) => { bind(ConstraintRegistry).toSelf().inSingletonScope(); bind(ConstraintMenu).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(ConstraintMenu); bind(EDITOR_TYPES.DefaultUIElement).toService(ConstraintMenu); bind(ThemeSwitchable).toService(ConstraintMenu) + + bind(TFGManager).toSelf().inSingletonScope() + configureCommand({bind, isBound}, SelectConstraintsCommand) }) \ No newline at end of file diff --git a/frontend/webEditor/src/constraint/selection.ts b/frontend/webEditor/src/constraint/selection.ts new file mode 100644 index 00000000..9047e1f5 --- /dev/null +++ b/frontend/webEditor/src/constraint/selection.ts @@ -0,0 +1,86 @@ +import { Command, CommandExecutionContext, CommandReturn, SModelRootImpl, TYPES } from "sprotty"; +import { TFGManager } from "./tfgManager"; +import { ConstraintRegistry } from "./constraintRegistry"; +import { Action, getBasicType } from "sprotty-protocol"; +import { DfdNodeImpl } from "../diagram/nodes/common"; +import { inject } from "inversify"; + +function selectConstraints(selectedConstraintNames: string[], root: SModelRootImpl, constraintRegistry: ConstraintRegistry, tfgManager: TFGManager) { + tfgManager.clearTfgs(); + constraintRegistry.setSelectedConstraints(selectedConstraintNames); + + const nodes = root.children.filter((node) => getBasicType(node) === "node") as DfdNodeImpl[]; + if (selectedConstraintNames.length === 0) { + nodes.forEach((node) => { + node.setColor("var(--color-primary)"); + }); + return root; + } + + nodes.forEach((node) => { + const annotations = node.annotations!; + let wasAdjusted = false; + if (constraintRegistry.selectedContainsAllConstraints()) { + annotations.forEach((annotation) => { + if (annotation.message.startsWith("Constraint")) { + wasAdjusted = true; + node.setColor(annotation.color!); + } + }); + } + selectedConstraintNames.forEach((name) => { + annotations.forEach((annotation) => { + if (annotation.message.startsWith("Constraint ") && annotation.message.split(" ")[1] === name) { + node.setColor(annotation.color!); + wasAdjusted = true; + tfgManager.addTfg(annotation.tfg!); + } + }); + }); + if (!wasAdjusted) node.setColor("var(--color-primary)"); + }); + + nodes.forEach((node) => { + const inTFG = node.annotations!.filter((annotation) => + tfgManager.getSelectedTfgs().has(annotation.tfg!), + ); + if (inTFG.length > 0) node.setColor("var(--color-highlighted)", false); + }); + + return root +} + +interface SelectConstraintsAction extends Action { + selectedConstraintNames: string[] +} + +export namespace SelectConstraintsAction { + export const KIND = 'select-constraints' + export function create(selectedConstraintNames: string[]): SelectConstraintsAction { + return { + kind: KIND, + selectedConstraintNames + } + } +} + +export class SelectConstraintsCommand extends Command { + static readonly KIND = SelectConstraintsAction.KIND + private oldConstraintSelection?: string[] + + constructor(@inject(TYPES.Action) private readonly action: SelectConstraintsAction, @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, @inject(TFGManager) private readonly tfgManager: TFGManager) { + super() + } + + execute(context: CommandExecutionContext): CommandReturn { + this.oldConstraintSelection = this.constraintRegistry.getSelectedConstraints() + return selectConstraints(this.action.selectedConstraintNames, context.root, this.constraintRegistry, this.tfgManager) + } + undo(context: CommandExecutionContext): CommandReturn { + return selectConstraints(this.oldConstraintSelection ?? [], context.root, this.constraintRegistry, this.tfgManager) + } + redo(context: CommandExecutionContext): CommandReturn { + return selectConstraints(this.action.selectedConstraintNames, context.root, this.constraintRegistry, this.tfgManager) + } + +} \ No newline at end of file diff --git a/frontend/webEditor/src/constraint/tfgManager.ts b/frontend/webEditor/src/constraint/tfgManager.ts new file mode 100644 index 00000000..46bcd1e9 --- /dev/null +++ b/frontend/webEditor/src/constraint/tfgManager.ts @@ -0,0 +1,18 @@ +import { injectable } from "inversify"; + +@injectable() +export class TFGManager { + private selectedTfgs = new Set(); + + public getSelectedTfgs(): Set { + return this.selectedTfgs; + } + public clearTfgs() { + this.selectedTfgs = new Set(); + } + public addTfg(hash: number) { + this.selectedTfgs.add(hash); + } + + constructor() {} +} From 35b08f24edb979ee36e31471bad71437a738129c Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Tue, 2 Dec 2025 10:12:18 +0100 Subject: [PATCH 32/41] fix node creation --- frontend/webEditor/src/assignment/clickListener.ts | 1 - frontend/webEditor/src/toolPalette/nodeCreationTool.ts | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/webEditor/src/assignment/clickListener.ts b/frontend/webEditor/src/assignment/clickListener.ts index 424a09b4..699ab17b 100644 --- a/frontend/webEditor/src/assignment/clickListener.ts +++ b/frontend/webEditor/src/assignment/clickListener.ts @@ -35,7 +35,6 @@ export class OutputPortEditUIMouseListener extends MouseListener { } doubleClick(target: SModelElementImpl): (Action | Promise)[] { - console.debug(target.type) if (target instanceof DfdOutputPortImpl) { // The user has double clicked on a dfd output port // => show the OutputPortEditUI for this port. diff --git a/frontend/webEditor/src/toolPalette/nodeCreationTool.ts b/frontend/webEditor/src/toolPalette/nodeCreationTool.ts index 4fc4dafe..f99e429b 100644 --- a/frontend/webEditor/src/toolPalette/nodeCreationTool.ts +++ b/frontend/webEditor/src/toolPalette/nodeCreationTool.ts @@ -19,6 +19,8 @@ export class NodeCreationTool extends CreationTool { id: generateRandomSprottyId(), type: this.elementType, text: defaultTextCapitalized, + ports: [], + labels: [] } as SNode; } } From 6a4cf26401d273c72fef5ca631d157c8b7216427 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Tue, 2 Dec 2025 11:29:13 +0100 Subject: [PATCH 33/41] add read only mode --- .../src/constraint/ConstraintMenu.ts | 10 ++-- .../src/editModeOverwrites/di.config.ts | 8 +++ .../src/editModeOverwrites/overwrites.ts | 59 +++++++++++++++++++ frontend/webEditor/src/index.ts | 4 +- .../webEditor/src/labels/LabelTypeEditorUi.ts | 37 +++++++++++- 5 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 frontend/webEditor/src/editModeOverwrites/di.config.ts create mode 100644 frontend/webEditor/src/editModeOverwrites/overwrites.ts diff --git a/frontend/webEditor/src/constraint/ConstraintMenu.ts b/frontend/webEditor/src/constraint/ConstraintMenu.ts index 71481edb..b587aecf 100644 --- a/frontend/webEditor/src/constraint/ConstraintMenu.ts +++ b/frontend/webEditor/src/constraint/ConstraintMenu.ts @@ -27,7 +27,6 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha private editorContainer: HTMLDivElement = document.createElement("div") as HTMLDivElement; private validationLabel: HTMLDivElement = document.createElement("div") as HTMLDivElement; private editor?: monaco.editor.IStandaloneCodeEditor; - private forceReadOnly: boolean; private optionsMenu?: HTMLDivElement; private ignoreCheckboxChange = false; private readonly tree: LanguageTreeNode[] @@ -38,14 +37,15 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha @inject(TYPES.ModelSource) modelSource: LocalModelSource, @inject(TYPES.IActionDispatcher) private readonly dispatcher: IActionDispatcher, @inject(SETTINGS.Mode) - editorModeController: EditorModeController, + private readonly editorModeController: EditorModeController, @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager ) { super("left", "up"); this.constraintRegistry = constraintRegistry; - this.forceReadOnly = editorModeController.get() !== "edit"; editorModeController.registerListener(() => { - this.forceReadOnly = editorModeController.isReadOnly(); + this.editor?.updateOptions({ + readOnly: editorModeController.isReadOnly() + }) }); constraintRegistry.onUpdate(() => { if (this.editor) { @@ -125,7 +125,7 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha alwaysConsumeMouseWheel: false, }, lineNumbers: "on", - readOnly: this.forceReadOnly, + readOnly: this.editorModeController.isReadOnly(), }); this.editor.setValue(this.constraintRegistry.getConstraintsAsText() || ""); diff --git a/frontend/webEditor/src/editModeOverwrites/di.config.ts b/frontend/webEditor/src/editModeOverwrites/di.config.ts new file mode 100644 index 00000000..d0e882c0 --- /dev/null +++ b/frontend/webEditor/src/editModeOverwrites/di.config.ts @@ -0,0 +1,8 @@ +import { ContainerModule } from "inversify"; +import { DeleteElementCommand, EditLabelMouseListener } from "sprotty"; +import { EditorModeAwareDeleteElementCommand, EditorModeAwareEditLabelMouseListener } from "./overwrites"; + +export const editorModeOverwritesModule = new ContainerModule((_, __, ___, rebind) => { + rebind(EditLabelMouseListener).to(EditorModeAwareEditLabelMouseListener); + rebind(DeleteElementCommand).to(EditorModeAwareDeleteElementCommand); +}); diff --git a/frontend/webEditor/src/editModeOverwrites/overwrites.ts b/frontend/webEditor/src/editModeOverwrites/overwrites.ts new file mode 100644 index 00000000..8e01069f --- /dev/null +++ b/frontend/webEditor/src/editModeOverwrites/overwrites.ts @@ -0,0 +1,59 @@ +import { inject, injectable } from "inversify"; +import { + CommandExecutionContext, + CommandReturn, + DeleteElementCommand, + EditLabelMouseListener, + SModelElementImpl, +} from "sprotty"; +import { Action } from "sprotty-protocol"; +import { SETTINGS } from "../settings/Settings"; +import { EditorModeController } from "../settings/editorMode"; + +@injectable() +export class EditorModeAwareEditLabelMouseListener extends EditLabelMouseListener { + constructor( + @inject(SETTINGS.Mode) + private readonly editorModeController: EditorModeController, + ) { + super(); + } + + doubleClick(target: SModelElementImpl, event: MouseEvent): (Action | Promise)[] { + if (this.editorModeController.isReadOnly()) { + return []; + } + + return super.doubleClick(target, event); + } +} + +@injectable() +export class EditorModeAwareDeleteElementCommand extends DeleteElementCommand { + @inject(SETTINGS.Mode) + private readonly editorModeController?: EditorModeController; + + execute(context: CommandExecutionContext): CommandReturn { + if (this.editorModeController?.isReadOnly()) { + return context.root; + } + + return super.execute(context); + } + + undo(context: CommandExecutionContext): CommandReturn { + if (this.editorModeController?.isReadOnly()) { + return context.root; + } + + return super.undo(context); + } + + redo(context: CommandExecutionContext): CommandReturn { + if (this.editorModeController?.isReadOnly()) { + return context.root; + } + + return super.redo(context); + } +} diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 03ea742f..e7372977 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -23,6 +23,7 @@ import { settingsModule } from "./settings/di.config"; import { toolPaletteModule } from "./toolPalette/di.config"; import { constraintModule } from "./constraint/di.config"; import { assignmentModule } from "./assignment/di.config"; +import { editorModeOverwritesModule } from "./editModeOverwrites/di.config"; const container = new Container(); @@ -48,7 +49,8 @@ container.load( settingsModule, toolPaletteModule, constraintModule, - assignmentModule + assignmentModule, + editorModeOverwritesModule ) const startUpAgents = container.getAll(StartUpAgent) diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts index 7b4cd090..3c685b34 100644 --- a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -9,12 +9,14 @@ import { dynamicallySetInputSize } from "../utils/TextSize"; import { LABEL_ASSIGNMENT_MIME_TYPE } from "./dragAndDrop"; import { AddLabelAssignmentAction } from "./assignmentCommand"; import { IActionDispatcher, TYPES } from "sprotty"; +import { SETTINGS } from "../settings/Settings"; +import { EditorModeController } from "../settings/editorMode"; export class LabelTypeEditorUi extends AccordionUiExtension { static readonly ID = "label-type-editor-ui"; private labelSectionContainer?: HTMLElement; - constructor(@inject(LabelTypeRegistry) private labelTypeRegistry: LabelTypeRegistry, @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher) { + constructor(@inject(LabelTypeRegistry) private labelTypeRegistry: LabelTypeRegistry, @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController) { super("left", "down"); labelTypeRegistry.onUpdate(() => this.renderLabelTypes()); } @@ -30,6 +32,9 @@ export class LabelTypeEditorUi extends AccordionUiExtension { const addButton = UiElementFactory.buildAddButton("Label Type"); addButton.onclick = () => { + if (this.editorModeController.isReadOnly()) { + return + } this.labelTypeRegistry.registerLabelType(""); }; @@ -86,16 +91,27 @@ export class LabelTypeEditorUi extends AccordionUiExtension { nameInput.onchange = () => { this.labelTypeRegistry.updateLabelTypeName(labelType.id, nameInput.value); }; + nameInput.onfocus = () => { + if (this.editorModeController.isReadOnly()) { + nameInput.blur() + } + } for (let i = 0; i < labelType.values.length; i++) { labelTypeValueHolder.appendChild(this.buildLabelTypeValue(labelType, i)); } addButton.onclick = () => { + if (this.editorModeController.isReadOnly()) { + return + } this.labelTypeRegistry.registerLabelTypeValue(labelType.id, ""); }; deleteButton.onclick = () => { + if (this.editorModeController.isReadOnly()) { + return + } this.labelTypeRegistry.unregisterLabelType(labelType.id); }; @@ -124,16 +140,25 @@ export class LabelTypeEditorUi extends AccordionUiExtension { setTimeout(() => dynamicallySetInputSize(nameInput), 0) nameInput.onchange = () => { + if (this.editorModeController.isReadOnly()) { + return + } this.labelTypeRegistry.updateLabelTypeValueText(labelType.id, value.id, nameInput.value); }; deleteButton.onclick = () => { + if (this.editorModeController.isReadOnly()) { + return + } this.labelTypeRegistry.unregisterLabelTypeValue(labelType.id, value.id); }; // Allow dragging to create a label assignment nameInput.draggable = true; nameInput.ondragstart = (event) => { + if (this.editorModeController.isReadOnly()) { + return + } const assignment: LabelAssignment = { labelTypeId: labelType.id, labelTypeValueId: value.id, @@ -144,6 +169,9 @@ export class LabelTypeEditorUi extends AccordionUiExtension { // Only edit on double click nameInput.onclick = () => { + if (this.editorModeController.isReadOnly()) { + return + } if (nameInput.getAttribute("clicked") === "true") { return; } @@ -162,10 +190,17 @@ export class LabelTypeEditorUi extends AccordionUiExtension { }, 500); }; nameInput.ondblclick = () => { + if (this.editorModeController.isReadOnly()) { + return + } nameInput.removeAttribute("clicked"); nameInput.focus(); }; nameInput.onfocus = (event) => { + if (this.editorModeController.isReadOnly()) { + nameInput.blur() + return + } // we check for the single click here, since this gets triggered before the ondblclick event if (nameInput.getAttribute("clicked") !== "true") { event.preventDefault(); From 8c72ba3f891fb6fb91fa638548d761ee0d4a6b33 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Tue, 2 Dec 2025 11:49:42 +0100 Subject: [PATCH 34/41] hide edge names --- .../webEditor/src/diagram/edges/ArrowEdge.tsx | 42 ++++++++++++------- .../webEditor/src/settings/hideEdgeNames.ts | 0 2 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 frontend/webEditor/src/settings/hideEdgeNames.ts diff --git a/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx b/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx index 5e0aea1e..fd2c9068 100644 --- a/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx +++ b/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx @@ -1,5 +1,5 @@ /** @jsx svg */ -import { injectable } from "inversify"; +import { inject, injectable } from "inversify"; import { PolylineEdgeViewWithGapsOnIntersections, SEdgeImpl, @@ -13,6 +13,7 @@ import { } from "sprotty"; import { VNode } from "snabbdom"; import { Point, angleOfPoint, toDegrees, SEdge } from "sprotty-protocol"; +import { HideEdgeNames, SETTINGS } from "../../settings/Settings"; export interface ArrowEdge extends SEdge { text?: string; @@ -34,18 +35,8 @@ export class ArrowEdgeImpl extends SEdgeImpl implements WithEditableLabel { @injectable() export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { - override render(edge: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { - // In the default implementation children of the edge are always rendered, because they - // may be visible when the rest of the edge is not. - // We only have the edge label as an children which only must be rendered when the rest of the edge is visible. - // So as an optimization for big diagrams we don't render the label when the rest of the edge is not visible either. - // Otherwise all these labels would be added to the DOM, making it slow.. - const route = this.edgeRouterRegistry.route(edge, args); - if (!this.isVisible(edge, route, context)) { - return undefined; - } - - return super.render(edge, context, args); + constructor(@inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames) { + super() } @@ -75,7 +66,7 @@ export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { * In contrast to the default implementation that we override here, * this implementation makes the edge line 10px shorter at the end to make space for the arrow without any overlap. */ - protected renderLine(edge: SEdgeImpl, segments: Point[]): VNode { + override renderLine(edge: SEdgeImpl, segments: Point[]): VNode { const firstPoint = segments[0]; let path = `M ${firstPoint.x},${firstPoint.y}`; for (let i = 1; i < segments.length; i++) { @@ -104,6 +95,29 @@ export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { ); } + + override render(edge: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { + // In the default implementation children of the edge are always rendered, because they + // may be visible when the rest of the edge is not. + // We only have the edge label as an children which only must be rendered when the rest of the edge is visible. + // So as an optimization for big diagrams we don't render the label when the rest of the edge is not visible either. + // Otherwise all these labels would be added to the DOM, making it slow.. + const route = this.edgeRouterRegistry.route(edge, args); + if (!this.isVisible(edge, route, context)) { + return undefined; + } + if (route.length === 0) { + return this.renderDanglingEdge("Cannot compute route", edge, context); + } + + + return + {this.renderLine(edge, route)} + {this.renderAdditionals(edge, route, context)} + {this.renderJunctionPoints(edge, route, context, args)} + { this.hideEdgeNames.get() ? undefined : context.renderChildren(edge, { route }) } + ; + } } /** diff --git a/frontend/webEditor/src/settings/hideEdgeNames.ts b/frontend/webEditor/src/settings/hideEdgeNames.ts new file mode 100644 index 00000000..e69de29b From e3357ab09fc1c59708554d5f41bb41f042cf14d4 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Tue, 2 Dec 2025 13:30:20 +0100 Subject: [PATCH 35/41] readonly features --- .../webEditor/src/settings/SettingsValue.ts | 5 +- frontend/webEditor/src/settings/di.config.ts | 11 ++- .../webEditor/src/settings/hideEdgeNames.ts | 25 ++++++ frontend/webEditor/src/settings/initialize.ts | 12 +++ .../src/settings/simplifyNodeNames.ts | 76 +++++++++++++++++++ .../src/startUpAgent/settingsInit.ts | 6 +- 6 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 frontend/webEditor/src/settings/simplifyNodeNames.ts diff --git a/frontend/webEditor/src/settings/SettingsValue.ts b/frontend/webEditor/src/settings/SettingsValue.ts index e8b02605..c3f961c3 100644 --- a/frontend/webEditor/src/settings/SettingsValue.ts +++ b/frontend/webEditor/src/settings/SettingsValue.ts @@ -11,8 +11,11 @@ export class SettingsValue { } set(newValue: T): void { + const oldValue = this.value; this.value = newValue; - this.listeners.forEach(listener => listener(newValue)); + if (oldValue !== newValue) { + this.listeners.forEach(listener => listener(newValue)); + } } registerListener(listener: (newValue: T) => void): void { diff --git a/frontend/webEditor/src/settings/di.config.ts b/frontend/webEditor/src/settings/di.config.ts index 50de944d..15d2a94b 100644 --- a/frontend/webEditor/src/settings/di.config.ts +++ b/frontend/webEditor/src/settings/di.config.ts @@ -3,12 +3,14 @@ import { SettingsUI } from "./SettingsUi"; import { EDITOR_TYPES } from "../editorTypes"; import { SETTINGS } from "./Settings"; import { BoolSettingsValue } from "./SettingsValue"; -import { TYPES } from "sprotty"; +import { configureCommand, TYPES } from "sprotty"; import { EditorModeController } from "./editorMode"; import { ThemeManager } from "./Theme"; import { ShownLabelsValue } from "./ShownLabels"; +import { HideEdgeNamesCommand } from "./hideEdgeNames"; +import { NodeNameRegistry, SimplifyNodeNamesCommand } from "./simplifyNodeNames"; -export const settingsModule = new ContainerModule((bind) => { +export const settingsModule = new ContainerModule((bind, _, isBound) => { bind(SettingsUI).toSelf().inSingletonScope(); bind(EDITOR_TYPES.DefaultUIElement).toService(SettingsUI) bind(TYPES.IUIExtension).toService(SettingsUI); @@ -18,4 +20,9 @@ export const settingsModule = new ContainerModule((bind) => { bind(SETTINGS.SimplifyNodeNames).to(BoolSettingsValue).inSingletonScope(); bind(SETTINGS.Mode).to(EditorModeController).inSingletonScope(); bind(SETTINGS.ShownLabels).to(ShownLabelsValue).inSingletonScope() + + const context = {bind, isBound} + configureCommand(context, HideEdgeNamesCommand) + configureCommand(context, SimplifyNodeNamesCommand) + bind(NodeNameRegistry).toSelf().inSingletonScope(); }) \ No newline at end of file diff --git a/frontend/webEditor/src/settings/hideEdgeNames.ts b/frontend/webEditor/src/settings/hideEdgeNames.ts index e69de29b..6e7eff55 100644 --- a/frontend/webEditor/src/settings/hideEdgeNames.ts +++ b/frontend/webEditor/src/settings/hideEdgeNames.ts @@ -0,0 +1,25 @@ +import { Command, CommandExecutionContext, CommandReturn } from "sprotty"; +import { Action } from "sprotty-protocol"; + +export namespace HideEdgeNamesAction { + export const KIND = 'hide-edge-names' + export function create(): Action { + return { + kind: KIND + } + } +} + +export class HideEdgeNamesCommand extends Command { + static readonly KIND = HideEdgeNamesAction.KIND + + execute(context: CommandExecutionContext): CommandReturn { + return context.root + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/settings/initialize.ts b/frontend/webEditor/src/settings/initialize.ts index 66c7215e..8b7e1f0b 100644 --- a/frontend/webEditor/src/settings/initialize.ts +++ b/frontend/webEditor/src/settings/initialize.ts @@ -1,5 +1,8 @@ +import { IActionDispatcher } from "sprotty"; import { EditorModeController } from "./editorMode"; import { HideEdgeNames, SimplifyNodeNames } from "./Settings"; +import { HideEdgeNamesAction } from "./hideEdgeNames"; +import { SimplifyNodeNamesAction } from "./simplifyNodeNames"; export function linkReadOnly( editorModeController: EditorModeController, @@ -23,4 +26,13 @@ export function linkReadOnly( editorModeController.set("view"); } }); +} + +export function addCommands( + actionDispatcher: IActionDispatcher, + simplifyNodeNames: SimplifyNodeNames, + hideEdgeNames: HideEdgeNames +) { + hideEdgeNames.registerListener(() => actionDispatcher.dispatch(HideEdgeNamesAction.create())) + simplifyNodeNames.registerListener(() => actionDispatcher.dispatch(SimplifyNodeNamesAction.create())) } \ No newline at end of file diff --git a/frontend/webEditor/src/settings/simplifyNodeNames.ts b/frontend/webEditor/src/settings/simplifyNodeNames.ts new file mode 100644 index 00000000..9ed763a2 --- /dev/null +++ b/frontend/webEditor/src/settings/simplifyNodeNames.ts @@ -0,0 +1,76 @@ +import { inject, injectable } from "inversify"; +import { Command, CommandExecutionContext, CommandReturn, SParentElementImpl, TYPES } from "sprotty"; +import { DfdNodeImpl } from "../diagram/nodes/common"; +import { Action } from "sprotty-protocol"; +import { SETTINGS, SimplifyNodeNames } from "./Settings"; + +@injectable() +export class NodeNameRegistry { + private plainNames: Map + private anonymousNames: Map + private nextNummber = 1 + + constructor() { + this.plainNames = new Map() + this.anonymousNames = new Map() + } + + + public setPlainName(node: DfdNodeImpl) { + if (node.editableLabel && this.plainNames.has(node.id)) { + node.editableLabel.text = this.plainNames.get(node.id)! + } + } + + public setAnonymousName(node: DfdNodeImpl) { + if (node instanceof DfdNodeImpl && node.editableLabel) { + this.plainNames.set(node.id, node.editableLabel.text) + } + if (!this.anonymousNames.has(node.id)) { + this.anonymousNames.set(node.id, this.nextNummber) + this.nextNummber++ + } + if (node.editableLabel) { + node.editableLabel.text = this.anonymousNames.get(node.id)!.toString() + } + } +} + + +export namespace SimplifyNodeNamesAction { + export const KIND = 'simplify-node-names' + export function create(): Action { + return { + kind: KIND + } + } +} + +export class SimplifyNodeNamesCommand extends Command { + static readonly KIND = SimplifyNodeNamesAction.KIND + + constructor(@inject(TYPES.Action) _: Action, @inject(NodeNameRegistry) private readonly nodeNameRegistry: NodeNameRegistry, @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames) { + super() + } + + execute(context: CommandExecutionContext): CommandReturn { + this.iterate(context.root, (n) => this.simplifyNodeNames.get() ? this.nodeNameRegistry.setAnonymousName(n) : this.nodeNameRegistry.setPlainName(n)) + return context.root + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root + } + + iterate(node: SParentElementImpl, f: (n: DfdNodeImpl) => void) { + if (node instanceof DfdNodeImpl) { + f(node) + } + + for (const child of node.children) { + this.iterate(child, f) + } + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/startUpAgent/settingsInit.ts b/frontend/webEditor/src/startUpAgent/settingsInit.ts index 81ac68b0..ef70edc8 100644 --- a/frontend/webEditor/src/startUpAgent/settingsInit.ts +++ b/frontend/webEditor/src/startUpAgent/settingsInit.ts @@ -1,18 +1,20 @@ import { IStartUpAgent } from "./StartUpAgent"; import { inject, multiInject } from "inversify"; -import { linkReadOnly } from "../settings/initialize"; +import { addCommands, linkReadOnly } from "../settings/initialize"; import { EditorModeController } from "../settings/editorMode"; import { SETTINGS, HideEdgeNames, SimplifyNodeNames } from "../settings/Settings"; import { registerThemeSwitch, ThemeManager, ThemeSwitchable } from "../settings/Theme"; +import { ActionDispatcher, TYPES } from "sprotty"; export class SettingsInitStartUpAgent implements IStartUpAgent { constructor(@inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, @inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, - @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, @multiInject(ThemeSwitchable) private readonly switchables: ThemeSwitchable[]) {} + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, @multiInject(ThemeSwitchable) private readonly switchables: ThemeSwitchable[], @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: ActionDispatcher) {} run(): void { linkReadOnly(this.editorModeController, this.simplifyNodeNames, this.hideEdgeNames); registerThemeSwitch(this.themeManager, this.switchables) + addCommands(this.actionDispatcher, this.simplifyNodeNames, this.hideEdgeNames) } } \ No newline at end of file From 5add22ef4f7570d2d8beabe1e2e51074270403c2 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Tue, 2 Dec 2025 13:52:36 +0100 Subject: [PATCH 36/41] loading indicator --- frontend/webEditor/src/index.ts | 4 +- frontend/webEditor/src/layout/command.ts | 8 ++- .../src/loadingIndicator/di.config.ts | 10 +++ .../src/loadingIndicator/loadingIndicator.css | 29 +++++++++ .../src/loadingIndicator/loadingIndicator.ts | 58 +++++++++++++++++ frontend/webEditor/src/serialize/analyze.ts | 4 +- .../src/serialize/loadDefaultDiagram.ts | 6 +- .../src/serialize/loadDfdAndDdFile.ts | 4 +- frontend/webEditor/src/serialize/loadJson.ts | 12 +++- .../webEditor/src/serialize/loadJsonFile.ts | 4 +- .../src/serialize/loadPalladioFile.ts | 4 +- .../src/serialize/saveDfdAndDdFile.ts | 6 +- frontend/webEditor/src/serialize/saveFile.ts | 62 ++++++++++++------- .../webEditor/src/serialize/saveJsonFile.ts | 6 +- 14 files changed, 178 insertions(+), 39 deletions(-) create mode 100644 frontend/webEditor/src/loadingIndicator/di.config.ts create mode 100644 frontend/webEditor/src/loadingIndicator/loadingIndicator.css create mode 100644 frontend/webEditor/src/loadingIndicator/loadingIndicator.ts diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index e7372977..73ff15a8 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -24,6 +24,7 @@ import { toolPaletteModule } from "./toolPalette/di.config"; import { constraintModule } from "./constraint/di.config"; import { assignmentModule } from "./assignment/di.config"; import { editorModeOverwritesModule } from "./editModeOverwrites/di.config"; +import { loadingIndicatorModule } from "./loadingIndicator/di.config"; const container = new Container(); @@ -50,7 +51,8 @@ container.load( toolPaletteModule, constraintModule, assignmentModule, - editorModeOverwritesModule + editorModeOverwritesModule, + loadingIndicatorModule ) const startUpAgents = container.getAll(StartUpAgent) diff --git a/frontend/webEditor/src/layout/command.ts b/frontend/webEditor/src/layout/command.ts index 28a3908b..d97c1905 100644 --- a/frontend/webEditor/src/layout/command.ts +++ b/frontend/webEditor/src/layout/command.ts @@ -3,6 +3,7 @@ import { Command, CommandExecutionContext, SModelRootImpl, TYPES } from "sprotty import { Action, IModelLayoutEngine, SGraph } from "sprotty-protocol"; import { DfdLayoutConfigurator } from "./layouter"; import { LayoutMethod } from "./layoutMethod"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export interface LayoutModelAction extends Action { kind: typeof LayoutModelAction.KIND; @@ -28,13 +29,14 @@ export class LayoutModelCommand extends Command { constructor( @inject(TYPES.Action) private readonly action: LayoutModelAction, @inject(TYPES.IModelLayoutEngine) private readonly layoutEngine: IModelLayoutEngine, - @inject(DfdLayoutConfigurator) private readonly configurator: DfdLayoutConfigurator + @inject(DfdLayoutConfigurator) private readonly configurator: DfdLayoutConfigurator , + @inject(LoadingIndicator) private readonly loadingIndicator: LoadingIndicator ) { super(); } async execute(context: CommandExecutionContext): Promise { - //this.loadingIndicator?.showIndicator("Layouting..."); + this.loadingIndicator.showIndicator("Layouting..."); this.oldRoot = context.root this.configurator.method = this.action.layoutMethod @@ -48,7 +50,7 @@ export class LayoutModelCommand extends Command { // Here we need to cast back. this.newModel = newModel as unknown as SModelRootImpl; - //this.loadingIndicator?.hideIndicator(); + this.loadingIndicator.hideIndicator(); return this.newModel; } diff --git a/frontend/webEditor/src/loadingIndicator/di.config.ts b/frontend/webEditor/src/loadingIndicator/di.config.ts new file mode 100644 index 00000000..ac47fd7f --- /dev/null +++ b/frontend/webEditor/src/loadingIndicator/di.config.ts @@ -0,0 +1,10 @@ +import { ContainerModule } from "inversify"; +import { LoadingIndicator } from "./loadingIndicator"; +import { TYPES } from "sprotty"; +import { EDITOR_TYPES } from "../editorTypes"; + +export const loadingIndicatorModule = new ContainerModule((bind) => { + bind(LoadingIndicator).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(LoadingIndicator); + bind(EDITOR_TYPES.DefaultUIElement).toService(LoadingIndicator); +}) \ No newline at end of file diff --git a/frontend/webEditor/src/loadingIndicator/loadingIndicator.css b/frontend/webEditor/src/loadingIndicator/loadingIndicator.css new file mode 100644 index 00000000..73da5e65 --- /dev/null +++ b/frontend/webEditor/src/loadingIndicator/loadingIndicator.css @@ -0,0 +1,29 @@ +#loading-indicator-wrapper { + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 9999; + width: 100vw; + height: 100vh; + + flex-direction: column; + justify-content: center; + align-items: center; + gap: 20px; + font-size: xx-large; + font-weight: bold; + color: white; + + background-color: rgba(0, 0, 0, 0.8); +} + +#turning-circle { + border: 20px solid white; + border-top: 20px solid #3498db; + border-radius: 9999px; + width: 100px; + height: 100px; + animation: spin 2s linear infinite; +} \ No newline at end of file diff --git a/frontend/webEditor/src/loadingIndicator/loadingIndicator.ts b/frontend/webEditor/src/loadingIndicator/loadingIndicator.ts new file mode 100644 index 00000000..c75c74a1 --- /dev/null +++ b/frontend/webEditor/src/loadingIndicator/loadingIndicator.ts @@ -0,0 +1,58 @@ +import { AbstractUIExtension } from "sprotty"; +import "./loadingIndicator.css"; + +export class LoadingIndicator extends AbstractUIExtension { + static readonly ID = "loading-indicator"; + private loadingIndicatorWrapper: HTMLElement | undefined; + private loadingIndicatorText: HTMLElement | undefined; + private waitTimeout?: number; + + id(): string { + return LoadingIndicator.ID; + } + containerClass(): string { + return LoadingIndicator.ID; + } + protected initializeContents(containerElement: HTMLElement): void { + this.loadingIndicatorWrapper = document.createElement("div"); + this.loadingIndicatorWrapper.id = "loading-indicator-wrapper"; + this.loadingIndicatorWrapper.style.display = "none"; + + const loadingIndicator = document.createElement("div"); + loadingIndicator.id = "turning-circle"; + this.loadingIndicatorWrapper.appendChild(loadingIndicator); + + this.loadingIndicatorText = document.createElement("div"); + this.loadingIndicatorText.id = "loading-indicator-text"; + this.loadingIndicatorWrapper.appendChild(this.loadingIndicatorText); + + containerElement.appendChild(this.loadingIndicatorWrapper); + } + + public showIndicator(text?: string) { + this.waitTimeout = setTimeout(() => { + if (!this.waitTimeout) { + return + } + if (this.loadingIndicatorWrapper) { + this.loadingIndicatorWrapper.style.display = "flex"; + if (this.loadingIndicatorText) { + this.loadingIndicatorText.innerText = text || "Loading..."; + } + this.loadingIndicatorWrapper.focus(); + this.waitTimeout = undefined; + } + }, 200) + + } + + public hideIndicator() { + if (this.waitTimeout) { + clearTimeout(this.waitTimeout); + this.waitTimeout = undefined; + } + if (this.loadingIndicatorWrapper) { + this.loadingIndicatorWrapper.style.display = "none"; + } + } +} \ No newline at end of file diff --git a/frontend/webEditor/src/serialize/analyze.ts b/frontend/webEditor/src/serialize/analyze.ts index 5b0b3650..0deb0c2b 100644 --- a/frontend/webEditor/src/serialize/analyze.ts +++ b/frontend/webEditor/src/serialize/analyze.ts @@ -9,6 +9,7 @@ import { inject } from "inversify"; import { EditorModeController } from "../settings/editorMode"; import { Action } from "sprotty-protocol"; import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export namespace AnalyzeAction { export const KIND = "analyze"; @@ -29,8 +30,9 @@ export class AnalyzeCommand extends LoadJsonCommand { @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator ) { - super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName); + super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName, loadingIndicator); } protected async getFile(context: CommandExecutionContext): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts index f51ef861..ce4bbc0e 100644 --- a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts +++ b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts @@ -9,6 +9,7 @@ import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export namespace LoadDefaultDiagramAction { export const KIND = "loadDefaultDiagram"; @@ -28,9 +29,10 @@ export class LoadDefaultDiagramCommand extends LoadJsonCommand { @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, - @inject(FileName) fileName: FileName + @inject(FileName) fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator ) { - super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName); + super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName, loadingIndicator); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts index f034b17a..23a3233e 100644 --- a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts @@ -10,6 +10,7 @@ import { SavedDiagram } from "./SavedDiagram"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export namespace LoadDfdAndDdFileAction { export const KIND = "loadDfdAndDdFile"; @@ -31,8 +32,9 @@ export class LoadDfdAndDdFileCommand extends LoadJsonCommand { @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator ) { - super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName); + super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName, loadingIndicator); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadJson.ts b/frontend/webEditor/src/serialize/loadJson.ts index af54ac02..e5a0860f 100644 --- a/frontend/webEditor/src/serialize/loadJson.ts +++ b/frontend/webEditor/src/serialize/loadJson.ts @@ -8,6 +8,7 @@ import { LabelType } from "../labels/LabelType"; import { DefaultFitToScreenAction } from "../fitToScreen/action"; import { FileName } from "../fileName/fileName"; import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export interface FileData { fileName: string; @@ -37,7 +38,8 @@ export abstract class LoadJsonCommand extends Command { protected constraintRegistry: ConstraintRegistry, protected editorModeController: EditorModeController, private actionDispatcher: ActionDispatcher, - protected fileName: FileName + protected fileName: FileName, + private loadingIndicator: LoadingIndicator ) { super(); } @@ -45,10 +47,12 @@ export abstract class LoadJsonCommand extends Command { protected abstract getFile(context: CommandExecutionContext): Promise | undefined>; async execute(context: CommandExecutionContext): Promise { + this.loadingIndicator.showIndicator("Loading model..."); this.oldRoot = context.root; this.file = await this.getFile(context).catch(() => undefined); if (!this.file) { + this.loadingIndicator.hide() return context.root; } @@ -91,15 +95,18 @@ export abstract class LoadJsonCommand extends Command { this.oldFileName = this.fileName.getName(); this.fileName.setName(this.file.fileName); + this.loadingIndicator.hide() return this.newRoot; } catch (error) { this.logger.error(this, "Error loading model", error); this.newRoot = this.oldRoot; + this.loadingIndicator.hide() return this.oldRoot; } } undo(context: CommandExecutionContext): CommandReturn { + this.loadingIndicator.showIndicator("Reverting model load..."); if (this.oldLabelTypes) { this.labelTypeRegistry.setLabelTypes(this.oldLabelTypes); } else { @@ -122,10 +129,12 @@ export abstract class LoadJsonCommand extends Command { this.fileName.setName(this.oldFileName ?? 'diagram'); + this.loadingIndicator.hide() return this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); } redo(context: CommandExecutionContext): CommandReturn { + this.loadingIndicator.showIndicator("Re-applying model load..."); const newLabelTypes = this.file?.content.labelTypes; this.labelTypeRegistry.clearLabelTypes(); if (newLabelTypes) { @@ -152,6 +161,7 @@ export abstract class LoadJsonCommand extends Command { this.fileName.setName(this.file?.fileName ?? 'diagram'); + this.loadingIndicator.hide() return this.newRoot ?? this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); } diff --git a/frontend/webEditor/src/serialize/loadJsonFile.ts b/frontend/webEditor/src/serialize/loadJsonFile.ts index aa097976..ea1ad532 100644 --- a/frontend/webEditor/src/serialize/loadJsonFile.ts +++ b/frontend/webEditor/src/serialize/loadJsonFile.ts @@ -9,6 +9,7 @@ import { SavedDiagram } from "./SavedDiagram"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export namespace LoadJsonFileAction { export const KIND = "loadJsonFile"; @@ -30,8 +31,9 @@ export class LoadJsonFileCommand extends LoadJsonCommand { @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(FileName) fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator ) { - super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName); + super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName, loadingIndicator); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/loadPalladioFile.ts b/frontend/webEditor/src/serialize/loadPalladioFile.ts index 80734fa5..4acca8c3 100644 --- a/frontend/webEditor/src/serialize/loadPalladioFile.ts +++ b/frontend/webEditor/src/serialize/loadPalladioFile.ts @@ -10,6 +10,7 @@ import { SavedDiagram } from "./SavedDiagram"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export namespace LoadPalladioFileAction { export const KIND = "loadPcmFile"; @@ -32,8 +33,9 @@ export class LoadPalladioFileCommand extends LoadJsonCommand { @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator ) { - super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName); + super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName, loadingIndicator); } protected async getFile(): Promise | undefined> { diff --git a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts index 0e2717f2..3b1bd5bc 100644 --- a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts @@ -9,6 +9,7 @@ import { Action } from "sprotty-protocol"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export namespace SaveDfdAndDdFileAction { export const KIND = 'saveDfdAndDdFile' @@ -28,9 +29,10 @@ export class SaveDfdAndDdFileCommand extends SaveFileCommand { @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, - @inject(FileName) private readonly fileName: FileName + @inject(FileName) private readonly fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator ) { - super(labelTypeRegistry, constraintRegistry, editorModeController); + super(labelTypeRegistry, constraintRegistry, editorModeController, loadingIndicator); } async getFiles(context: CommandExecutionContext): Promise[]> { diff --git a/frontend/webEditor/src/serialize/saveFile.ts b/frontend/webEditor/src/serialize/saveFile.ts index 39e0b18c..17a414f4 100644 --- a/frontend/webEditor/src/serialize/saveFile.ts +++ b/frontend/webEditor/src/serialize/saveFile.ts @@ -1,33 +1,47 @@ import { CommandExecutionContext, CommandReturn, SModelRootImpl } from "sprotty"; import { FileData } from "./loadJson"; import { SavedDiagramCreatorCommand } from "./savedDiagramCreator"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; +import { EditorModeController } from "../settings/editorMode"; export abstract class SaveFileCommand extends SavedDiagramCreatorCommand { + constructor( + labelTypeRegistry: LabelTypeRegistry, + constraintRegistry: ConstraintRegistry, + editorModeController: EditorModeController, + private readonly loadingIndicator: LoadingIndicator, + ) { + super(labelTypeRegistry, constraintRegistry, editorModeController); + } - abstract getFiles(context: CommandExecutionContext): Promise[]>; + abstract getFiles(context: CommandExecutionContext): Promise[]>; - async execute(context: CommandExecutionContext): Promise { - const files = await this.getFiles(context) - for (const file of files) { - this.downloadFile(file); - } + async execute(context: CommandExecutionContext): Promise { + this.loadingIndicator.showIndicator("Saving diagram..."); + const files = await this.getFiles(context); + for (const file of files) { + this.downloadFile(file); + } - return context.root; - } - undo(context: CommandExecutionContext): CommandReturn { - return context.root; - } - redo(context: CommandExecutionContext): CommandReturn { - return context.root; - } + this.loadingIndicator.hide(); + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } - private downloadFile(file: FileData) { - const element = document.createElement('a'); - const fileBlob = new Blob([file.content], { type: 'application/json' }); - element.href = URL.createObjectURL(fileBlob); - element.download = file.fileName; - element.click(); - URL.revokeObjectURL(element.href); - element.remove() - } -} \ No newline at end of file + private downloadFile(file: FileData) { + const element = document.createElement("a"); + const fileBlob = new Blob([file.content], { type: "application/json" }); + element.href = URL.createObjectURL(fileBlob); + element.download = file.fileName; + element.click(); + URL.revokeObjectURL(element.href); + element.remove(); + } +} diff --git a/frontend/webEditor/src/serialize/saveJsonFile.ts b/frontend/webEditor/src/serialize/saveJsonFile.ts index 4ba55851..4cba3db7 100644 --- a/frontend/webEditor/src/serialize/saveJsonFile.ts +++ b/frontend/webEditor/src/serialize/saveJsonFile.ts @@ -8,6 +8,7 @@ import { Action } from "sprotty-protocol"; import { FileName } from "../fileName/fileName"; import { SETTINGS } from "../settings/Settings"; import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export namespace SaveJsonFileAction { export const KIND = 'saveJsonFile' @@ -24,9 +25,10 @@ export class SaveJsonFileCommand extends SaveFileCommand { @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, - @inject(FileName) private readonly fileName: FileName + @inject(FileName) private readonly fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator ) { - super(LabelTypeRegistry, constraintRegistry, editorModeController); + super(LabelTypeRegistry, constraintRegistry, editorModeController, loadingIndicator); } getFiles(context: CommandExecutionContext): Promise[]> { From bff02d1654ef4f4a7681949ff2c18dda620131df Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Tue, 2 Dec 2025 13:54:38 +0100 Subject: [PATCH 37/41] fix format --- frontend/webEditor/.husky/pre-commit | 9 ++ frontend/webEditor/package copy.json | 43 ----- frontend/webEditor/package.json | 82 +++++----- .../src/accordionUiExtension/index.ts | 50 +++--- .../src/annotation/DFDNodeAnnotation.ts | 2 +- .../webEditor/src/assets/commonStyling.css | 1 - frontend/webEditor/src/assets/page.css | 8 +- .../src/assignment/AssignmentEditUi.ts | 4 +- .../src/assignment/assignmentEditUi.css | 2 +- .../webEditor/src/assignment/clickListener.ts | 2 +- .../webEditor/src/assignment/di.config.ts | 4 +- frontend/webEditor/src/assignment/language.ts | 17 +- .../src/commandPalette/commandPalette.css | 64 ++++---- .../src/commandPalette/commandPalette.ts | 2 +- .../commandPalette/commandPaletteProvider.ts | 15 +- .../webEditor/src/commandPalette/di.config.ts | 8 +- frontend/webEditor/src/commonModule.ts | 3 +- .../webEditor/src/constraint/Constraint.ts | 2 +- .../src/constraint/ConstraintMenu.ts | 31 ++-- .../webEditor/src/constraint/di.config.ts | 8 +- frontend/webEditor/src/constraint/language.ts | 9 +- .../webEditor/src/constraint/selection.ts | 59 ++++--- frontend/webEditor/src/diagram/di.config.ts | 39 +++-- .../webEditor/src/diagram/edges/ArrowEdge.tsx | 19 ++- .../src/diagram/labels/DfdPositionalLabel.tsx | 2 +- .../src/diagram/labels/EditLabelDecorator.ts | 4 +- .../src/diagram/labels/EditLabelValidator.ts | 11 +- .../diagram/labels/FilledBackgroundLabel.tsx | 2 +- .../src/diagram/labels/NoScrollEditLabelUI.ts | 5 +- .../src/diagram/nodes/DfdFunctionNode.tsx | 6 +- .../webEditor/src/diagram/nodes/DfdIONode.tsx | 3 +- .../src/diagram/nodes/DfdNodeLabels.tsx | 17 +- .../src/diagram/nodes/DfdStorageNode.tsx | 6 +- .../webEditor/src/diagram/nodes/annotation.ts | 55 ++++--- .../webEditor/src/diagram/nodes/common.ts | 16 +- .../webEditor/src/diagram/ports/common.ts | 2 +- frontend/webEditor/src/editorTypes.ts | 2 +- frontend/webEditor/src/fileName/di.config.ts | 2 +- frontend/webEditor/src/fileName/fileName.ts | 8 +- frontend/webEditor/src/helpUi/di.config.ts | 4 +- frontend/webEditor/src/helpUi/helpUi.css | 2 - frontend/webEditor/src/helpUi/helpUi.ts | 6 +- frontend/webEditor/src/index.ts | 14 +- frontend/webEditor/src/labels/LabelType.ts | 2 +- .../webEditor/src/labels/LabelTypeEditorUi.ts | 77 ++++----- .../webEditor/src/labels/LabelTypeRegistry.ts | 64 ++++---- .../webEditor/src/labels/assignmentCommand.ts | 24 +-- frontend/webEditor/src/labels/di.config.ts | 5 +- frontend/webEditor/src/labels/dragAndDrop.ts | 4 +- frontend/webEditor/src/labels/feature.ts | 2 +- .../src/labels/labelTypeEditorUi.css | 3 +- .../webEditor/src/labels/registryCommands.ts | 80 +++++----- frontend/webEditor/src/languages/tokenize.ts | 7 +- frontend/webEditor/src/layout/command.ts | 12 +- frontend/webEditor/src/layout/layouter.ts | 7 +- .../src/loadingIndicator/di.config.ts | 8 +- .../src/loadingIndicator/loadingIndicator.css | 46 +++--- .../src/loadingIndicator/loadingIndicator.ts | 7 +- .../webEditor/src/serialize/ModelFactory.ts | 56 +++---- .../webEditor/src/serialize/SavedDiagram.ts | 2 +- frontend/webEditor/src/serialize/analyze.ts | 29 ++-- frontend/webEditor/src/serialize/di.config.ts | 10 +- .../src/serialize/loadDefaultDiagram.ts | 20 ++- .../src/serialize/loadDfdAndDdFile.ts | 24 ++- frontend/webEditor/src/serialize/loadJson.ts | 44 +++--- .../webEditor/src/serialize/loadJsonFile.ts | 60 +++---- .../src/serialize/loadPalladioFile.ts | 49 ++++-- .../src/serialize/saveDfdAndDdFile.ts | 80 +++++----- .../webEditor/src/serialize/saveJsonFile.ts | 47 +++--- .../src/serialize/savedDiagramCreator.ts | 36 ++--- frontend/webEditor/src/settings/Settings.ts | 18 +-- frontend/webEditor/src/settings/SettingsUi.ts | 75 +++++---- .../webEditor/src/settings/SettingsValue.ts | 45 +++--- .../webEditor/src/settings/ShownLabels.ts | 6 +- frontend/webEditor/src/settings/Theme.ts | 38 ++--- frontend/webEditor/src/settings/di.config.ts | 26 +-- frontend/webEditor/src/settings/editorMode.ts | 22 ++- .../webEditor/src/settings/hideEdgeNames.ts | 34 ++-- frontend/webEditor/src/settings/initialize.ts | 50 +++--- .../src/settings/simplifyNodeNames.ts | 108 +++++++------ .../src/startUpAgent/LoadDefaultDiagram.ts | 5 +- .../startUpAgent/LoadDefaultUiExtensions.ts | 2 +- .../src/startUpAgent/StartUpAgent.ts | 2 +- .../webEditor/src/startUpAgent/di.config.ts | 10 +- .../src/startUpAgent/settingsInit.ts | 18 ++- .../src/startUpAgent/webSocketConnect.ts | 6 +- .../src/toolPalette/nodeCreationTool.ts | 2 +- .../webEditor/src/utils/UiElementFactory.ts | 34 ++-- .../webEditor/src/utils/baseUiElements.css | 5 +- frontend/webEditor/src/utils/idGenerator.ts | 2 +- frontend/webEditor/src/vite-env.d.ts | 2 +- frontend/webEditor/src/webSocket/di.config.ts | 5 +- frontend/webEditor/src/webSocket/webSocket.ts | 149 +++++++++--------- 93 files changed, 1105 insertions(+), 1014 deletions(-) create mode 100644 frontend/webEditor/.husky/pre-commit delete mode 100644 frontend/webEditor/package copy.json diff --git a/frontend/webEditor/.husky/pre-commit b/frontend/webEditor/.husky/pre-commit new file mode 100644 index 00000000..55cbc310 --- /dev/null +++ b/frontend/webEditor/.husky/pre-commit @@ -0,0 +1,9 @@ +#!/usr/bin/env sh +set -e + +# Only run when WebEditor files are staged (optional guard) +if git diff --cached --name-only --diff-filter=ACMRT | grep -q '^Frontend/WebEditor/'; then + REPO_ROOT="$(git rev-parse --show-toplevel)" + cd "$REPO_ROOT/Frontend/WebEditor" + npx lint-staged +fi \ No newline at end of file diff --git a/frontend/webEditor/package copy.json b/frontend/webEditor/package copy.json deleted file mode 100644 index 5f09ab6c..00000000 --- a/frontend/webEditor/package copy.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "data-flow-analysis-web-editor", - "version": "0.0.0", - "private": true, - "repository": { - "type": "git", - "url": "https://github.com/DataFlowAnalysis/OnlineEditor.git" - }, - "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.35.0", - "@fortawesome/fontawesome-free": "^7.0.0", - "@vscode/codicons": "^0.0.39", - "eslint": "^9.32.0", - "eslint-config-prettier": "^10.1.8", - "husky": "^9.1.7", - "inversify": "^6.2.2", - "lint-staged": "^16.1.6", - "monaco-editor": "^0.52.2", - "prettier": "^3.6.2", - "reflect-metadata": "^0.2.2", - "sprotty": "^1.4.0", - "sprotty-elk": "^1.4.0", - "sprotty-protocol": "^1.4.0", - "typescript": "^5.8.3", - "typescript-eslint": "^8.44.0", - "vite": "^7.1.7" - }, - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "format": "prettier --write \"./**/*.{html,css,ts,tsx,json}\"", - "lint": "eslint --max-warnings 0 --no-warn-ignored", - "prepare": "husky" - }, - "lint-staged": { - "*.{html,css,ts,tsx,json}": [ - "npm run lint", - "npm run format" - ] - } -} diff --git a/frontend/webEditor/package.json b/frontend/webEditor/package.json index 5f09ab6c..75129e2f 100644 --- a/frontend/webEditor/package.json +++ b/frontend/webEditor/package.json @@ -1,43 +1,43 @@ { - "name": "data-flow-analysis-web-editor", - "version": "0.0.0", - "private": true, - "repository": { - "type": "git", - "url": "https://github.com/DataFlowAnalysis/OnlineEditor.git" - }, - "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.35.0", - "@fortawesome/fontawesome-free": "^7.0.0", - "@vscode/codicons": "^0.0.39", - "eslint": "^9.32.0", - "eslint-config-prettier": "^10.1.8", - "husky": "^9.1.7", - "inversify": "^6.2.2", - "lint-staged": "^16.1.6", - "monaco-editor": "^0.52.2", - "prettier": "^3.6.2", - "reflect-metadata": "^0.2.2", - "sprotty": "^1.4.0", - "sprotty-elk": "^1.4.0", - "sprotty-protocol": "^1.4.0", - "typescript": "^5.8.3", - "typescript-eslint": "^8.44.0", - "vite": "^7.1.7" - }, - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "format": "prettier --write \"./**/*.{html,css,ts,tsx,json}\"", - "lint": "eslint --max-warnings 0 --no-warn-ignored", - "prepare": "husky" - }, - "lint-staged": { - "*.{html,css,ts,tsx,json}": [ - "npm run lint", - "npm run format" - ] - } + "name": "data-flow-analysis-web-editor", + "version": "0.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/DataFlowAnalysis/OnlineEditor.git" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.35.0", + "@fortawesome/fontawesome-free": "^7.0.0", + "@vscode/codicons": "^0.0.39", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", + "husky": "^9.1.7", + "inversify": "^6.2.2", + "lint-staged": "^16.1.6", + "monaco-editor": "^0.52.2", + "prettier": "^3.6.2", + "reflect-metadata": "^0.2.2", + "sprotty": "^1.4.0", + "sprotty-elk": "^1.4.0", + "sprotty-protocol": "^1.4.0", + "typescript": "^5.8.3", + "typescript-eslint": "^8.44.0", + "vite": "^7.1.7" + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "format": "prettier --write \"./**/*.{html,css,ts,tsx,json}\"", + "lint": "eslint --max-warnings 0 --no-warn-ignored", + "prepare": "husky" + }, + "lint-staged": { + "*.{html,css,ts,tsx,json}": [ + "npm run lint", + "npm run format" + ] + } } diff --git a/frontend/webEditor/src/accordionUiExtension/index.ts b/frontend/webEditor/src/accordionUiExtension/index.ts index 5ae7b9fd..8319974c 100644 --- a/frontend/webEditor/src/accordionUiExtension/index.ts +++ b/frontend/webEditor/src/accordionUiExtension/index.ts @@ -1,50 +1,52 @@ import { AbstractUIExtension } from "sprotty"; import { injectable } from "inversify"; -import "./accordion.css" +import "./accordion.css"; /** * Base class for an expandable accordion floating element */ @injectable() export abstract class AccordionUiExtension extends AbstractUIExtension { - - constructor(private chevronPosition: 'left'|'right', private chevronOrientation: 'up'|'down') { + constructor( + private chevronPosition: "left" | "right", + private chevronOrientation: "up" | "down", + ) { super(); } protected initializeContents(containerElement: HTMLElement): void { - containerElement.classList.add('ui-float'); + containerElement.classList.add("ui-float"); // create hidden checkbox used for toggling - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - const checkboxId = this.id() + '-checkbox'; + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + const checkboxId = this.id() + "-checkbox"; checkbox.id = checkboxId; - checkbox.classList.add('accordion-state'); - checkbox.hidden = true + checkbox.classList.add("accordion-state"); + checkbox.hidden = true; // create clickable label for the checkbox - const label = document.createElement('label') - label.htmlFor = checkboxId + const label = document.createElement("label"); + label.htmlFor = checkboxId; // create header inside label - const header = document.createElement('div') - header.classList.add(`chevron-${this.chevronPosition}`, 'accordion-button') - if (this.chevronOrientation === 'up') { - header.classList.add('flip-chevron') + const header = document.createElement("div"); + header.classList.add(`chevron-${this.chevronPosition}`, "accordion-button"); + if (this.chevronOrientation === "up") { + header.classList.add("flip-chevron"); } this.initializeHeaderContent(header); - label.appendChild(header) + label.appendChild(header); // create content holder and initialize it - const accordionContent = document.createElement('div') - accordionContent.classList.add('accordion-content') - const contentHolder = document.createElement('div') - this.initializeHidableContent(contentHolder) + const accordionContent = document.createElement("div"); + accordionContent.classList.add("accordion-content"); + const contentHolder = document.createElement("div"); + this.initializeHidableContent(contentHolder); accordionContent.appendChild(contentHolder); - containerElement.appendChild(checkbox) - containerElement.appendChild(label) - containerElement.appendChild(accordionContent) + containerElement.appendChild(checkbox); + containerElement.appendChild(label); + containerElement.appendChild(accordionContent); } /** @@ -58,4 +60,4 @@ export abstract class AccordionUiExtension extends AbstractUIExtension { * @param contentElement The containing element of the header */ protected abstract initializeHeaderContent(headerElement: HTMLElement): void; -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts b/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts index eda7cb48..dc8f7f9c 100644 --- a/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts +++ b/frontend/webEditor/src/annotation/DFDNodeAnnotation.ts @@ -3,4 +3,4 @@ export interface DfdNodeAnnotation { color?: string; icon?: string; tfg?: number; -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/assets/commonStyling.css b/frontend/webEditor/src/assets/commonStyling.css index 5ca9c56c..48d7cb3c 100644 --- a/frontend/webEditor/src/assets/commonStyling.css +++ b/frontend/webEditor/src/assets/commonStyling.css @@ -23,4 +23,3 @@ kbd { padding: 2px 4px; white-space: nowrap; } - diff --git a/frontend/webEditor/src/assets/page.css b/frontend/webEditor/src/assets/page.css index 93ff3dd5..6ac44b3d 100644 --- a/frontend/webEditor/src/assets/page.css +++ b/frontend/webEditor/src/assets/page.css @@ -17,7 +17,13 @@ body { position: relative; height: 100vh; width: 100vw; - /*temporary*/ font-family: Helvetica Neue,Helvetica,Arial,sans-serif; padding:0; + /*temporary*/ + font-family: + Helvetica Neue, + Helvetica, + Arial, + sans-serif; + padding: 0; } svg.sprotty-graph { diff --git a/frontend/webEditor/src/assignment/AssignmentEditUi.ts b/frontend/webEditor/src/assignment/AssignmentEditUi.ts index b64b5ba3..a0c5570c 100644 --- a/frontend/webEditor/src/assignment/AssignmentEditUi.ts +++ b/frontend/webEditor/src/assignment/AssignmentEditUi.ts @@ -80,7 +80,7 @@ export class AssignmentEditUi extends AbstractUIExtension { scrollBeyondLastLine: false, // Not needed theme: monacoTheme, language: ASSIGNMENT_LANGUAGE_ID, - readOnly: this.editorModeController.isReadOnly() + readOnly: this.editorModeController.isReadOnly(), }); this.editor.onDidChangeModelContent(() => { @@ -126,7 +126,7 @@ export class AssignmentEditUi extends AbstractUIExtension { this.resizeEditor(); - this.editor?.focus() + this.editor?.focus(); } private setPort(port: DfdOutputPortImpl, containerElement: HTMLElement) { diff --git a/frontend/webEditor/src/assignment/assignmentEditUi.css b/frontend/webEditor/src/assignment/assignmentEditUi.css index b7c6c81e..52466f57 100644 --- a/frontend/webEditor/src/assignment/assignmentEditUi.css +++ b/frontend/webEditor/src/assignment/assignmentEditUi.css @@ -8,7 +8,7 @@ background: var(--color-primary); div.unavailable-inputs { - /* spacing between editor and this text */ + /* spacing between editor and this text */ padding-bottom: 5px; } diff --git a/frontend/webEditor/src/assignment/clickListener.ts b/frontend/webEditor/src/assignment/clickListener.ts index 699ab17b..2d021048 100644 --- a/frontend/webEditor/src/assignment/clickListener.ts +++ b/frontend/webEditor/src/assignment/clickListener.ts @@ -50,4 +50,4 @@ export class OutputPortEditUIMouseListener extends MouseListener { return []; } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/assignment/di.config.ts b/frontend/webEditor/src/assignment/di.config.ts index caf4a173..854923bd 100644 --- a/frontend/webEditor/src/assignment/di.config.ts +++ b/frontend/webEditor/src/assignment/di.config.ts @@ -4,8 +4,8 @@ import { TYPES } from "sprotty"; import { OutputPortEditUIMouseListener } from "./clickListener"; export const assignmentModule = new ContainerModule((bind) => { - bind(AssignmentEditUi).toSelf().inSingletonScope() + bind(AssignmentEditUi).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(AssignmentEditUi); bind(TYPES.MouseListener).to(OutputPortEditUIMouseListener).inSingletonScope(); -}) \ No newline at end of file +}); diff --git a/frontend/webEditor/src/assignment/language.ts b/frontend/webEditor/src/assignment/language.ts index 032e6e9c..0d89f92a 100644 --- a/frontend/webEditor/src/assignment/language.ts +++ b/frontend/webEditor/src/assignment/language.ts @@ -6,7 +6,7 @@ import { ConstantWord, ListWord, Word } from "../languages/words"; import { DfdNodeImpl } from "../diagram/nodes/common"; import { WordCompletion } from "../languages/autocomplete"; -export const ASSIGNMENT_LANGUAGE_ID = 'dfd-assignment-language' +export const ASSIGNMENT_LANGUAGE_ID = "dfd-assignment-language"; const startOfLineKeywords = ["forward", "assign", "set", "unset"]; const statementKeywords = [...startOfLineKeywords, "if", "from"]; @@ -59,10 +59,7 @@ export namespace AssignmentLanguageTreeBuilder { ]; } - function buildSetOrUnsetStatement( - labelTypeRegistry: LabelTypeRegistry, - keyword: string, - ): LanguageTreeNode { + function buildSetOrUnsetStatement(labelTypeRegistry: LabelTypeRegistry, keyword: string): LanguageTreeNode { const labelNode: LanguageTreeNode = { word: new ListWord(new LabelWord(labelTypeRegistry)), children: [], @@ -112,7 +109,11 @@ export namespace AssignmentLanguageTreeBuilder { }; } - function buildCondition(labelTypeRegistry: LabelTypeRegistry, nextNode: LanguageTreeNode, port: DfdOutputPortImpl) { + function buildCondition( + labelTypeRegistry: LabelTypeRegistry, + nextNode: LanguageTreeNode, + port: DfdOutputPortImpl, + ) { const connectors: LanguageTreeNode[] = ["&&", "||"].map((o) => ({ word: new ConstantWord(o), children: [], @@ -201,7 +202,7 @@ class LabelWord implements Word { return []; } -/* + /* replaceWord(text: string, replacement: ReplacementData) { if (replacement.type == "Label" && text == replacement.old) { return replacement.replacement; @@ -293,4 +294,4 @@ class InputLabelWord implements Word { } return [text, undefined]; } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/commandPalette/commandPalette.css b/frontend/webEditor/src/commandPalette/commandPalette.css index 3d88b0ab..6d77c390 100644 --- a/frontend/webEditor/src/commandPalette/commandPalette.css +++ b/frontend/webEditor/src/commandPalette/commandPalette.css @@ -1,58 +1,58 @@ /* Overrides for sprotty command palette css (should be imported in commandPalette.ts before this .css file */ .command-palette { - transition: opacity 0.2s ease-in-out; - display: flex; - flex-direction: column; - row-gap: 4px; - width: 350px; + transition: opacity 0.2s ease-in-out; + display: flex; + flex-direction: column; + row-gap: 4px; + width: 350px; } .command-palette input { - color: var(--color-foreground); - background: var(--color-primary); + color: var(--color-foreground); + background: var(--color-primary); } .command-palette-suggestions-holder { - width: 100%; + width: 100%; } .command-palette-suggestion { - display: grid; - grid-template-columns: 24px 1fr 24px 0px; - background: var(--color-primary); - overflow: visible; - height: 20px; - min-width: 100%; - white-space: nowrap; - width: 100%; - cursor: pointer; + display: grid; + grid-template-columns: 24px 1fr 24px 0px; + background: var(--color-primary); + overflow: visible; + height: 20px; + min-width: 100%; + white-space: nowrap; + width: 100%; + cursor: pointer; } .command-palette-suggestion:hover, .command-palette-suggestion.selected { - background: var(--color-background); + background: var(--color-background); } .command-palette-suggestion-children { - position: relative; - top: 0px; - right: 0px; - display: none; - background: var(--color-primary); - width: fit-content; - height: fit-content; - border-left: 4px solid var(--color-spacer); - box-shadow: - 0 4px 8px 0 rgba(0, 0, 0, 0.2), - 0 6px 20px 0 rgba(0, 0, 0, 0.19); + position: relative; + top: 0px; + right: 0px; + display: none; + background: var(--color-primary); + width: fit-content; + height: fit-content; + border-left: 4px solid var(--color-spacer); + box-shadow: + 0 4px 8px 0 rgba(0, 0, 0, 0.2), + 0 6px 20px 0 rgba(0, 0, 0, 0.19); } .command-palette-suggestion:hover > .command-palette-suggestion-children, .command-palette-suggestion.expanded > .command-palette-suggestion-children { - display: block; + display: block; } .command-palette .fa-solid { - text-align: center; -} \ No newline at end of file + text-align: center; +} diff --git a/frontend/webEditor/src/commandPalette/commandPalette.ts b/frontend/webEditor/src/commandPalette/commandPalette.ts index 3d05063f..eb2dc822 100644 --- a/frontend/webEditor/src/commandPalette/commandPalette.ts +++ b/frontend/webEditor/src/commandPalette/commandPalette.ts @@ -207,4 +207,4 @@ export class WebEditorCommandPalette extends CommandPalette { this.logger.error(this, "No action dispatcher available to execute command palette action", reason), ); } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts index 93133e98..ad0e53e2 100644 --- a/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts +++ b/frontend/webEditor/src/commandPalette/commandPaletteProvider.ts @@ -16,7 +16,6 @@ import { SaveDfdAndDdFileAction } from "../serialize/saveDfdAndDdFile"; */ @injectable() export class WebEditorCommandPaletteActionProvider implements ICommandPaletteActionProvider { - async getActions(root: Readonly): Promise<(LabeledAction | FolderAction)[]> { const fitToScreenAction = DefaultFitToScreenAction.create(root); const commitAction = CommitModelAction.create(); @@ -27,7 +26,11 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct [ new LabeledAction("Load diagram from JSON", [LoadJsonFileAction.create(), commitAction], "json"), new LabeledAction("Load DFD and DD", [LoadDfdAndDdFileAction.create(), commitAction], "coffee"), - new LabeledAction("Load Palladio", [LoadPalladioFileAction.create(), commitAction], "fa-puzzle-piece"), + new LabeledAction( + "Load Palladio", + [LoadPalladioFileAction.create(), commitAction], + "fa-puzzle-piece", + ), ], "go-to-file", ), @@ -35,11 +38,7 @@ export class WebEditorCommandPaletteActionProvider implements ICommandPaletteAct "Save", [ new LabeledAction("Save diagram as JSON", [SaveJsonFileAction.create()], "json"), - new LabeledAction( - "Save diagram as DFD and DD", - [SaveDfdAndDdFileAction.create()], - "coffee", - ), + new LabeledAction("Save diagram as DFD and DD", [SaveDfdAndDdFileAction.create()], "coffee"), //new LabeledAction("Save viewport as image", [SaveImageAction.create()], "device-camera"), ], "save", @@ -82,4 +81,4 @@ export class FolderAction extends LabeledAction { ) { super(label, actions, icon); } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/commandPalette/di.config.ts b/frontend/webEditor/src/commandPalette/di.config.ts index b8b22f48..ba48c8e5 100644 --- a/frontend/webEditor/src/commandPalette/di.config.ts +++ b/frontend/webEditor/src/commandPalette/di.config.ts @@ -4,8 +4,8 @@ import { WebEditorCommandPalette } from "./CommandPalette"; import { WebEditorCommandPaletteActionProvider } from "./CommandPaletteProvider"; export const commandPaletteModule = new ContainerModule((bind, _, __, rebind) => { - rebind(CommandPalette).to(WebEditorCommandPalette).inSingletonScope(); + rebind(CommandPalette).to(WebEditorCommandPalette).inSingletonScope(); - bind(WebEditorCommandPaletteActionProvider).toSelf().inSingletonScope(); - bind(TYPES.ICommandPaletteActionProvider).toService(WebEditorCommandPaletteActionProvider); -}); \ No newline at end of file + bind(WebEditorCommandPaletteActionProvider).toSelf().inSingletonScope(); + bind(TYPES.ICommandPaletteActionProvider).toService(WebEditorCommandPaletteActionProvider); +}); diff --git a/frontend/webEditor/src/commonModule.ts b/frontend/webEditor/src/commonModule.ts index 365e77d7..d0dde0ad 100644 --- a/frontend/webEditor/src/commonModule.ts +++ b/frontend/webEditor/src/commonModule.ts @@ -2,7 +2,6 @@ import { ContainerModule } from "inversify"; import { TYPES, LocalModelSource, ConsoleLogger, LogLevel, configureViewerOptions } from "sprotty"; export const commonModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(TYPES.ModelSource).to(LocalModelSource).inSingletonScope(); rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); rebind(TYPES.LogLevel).toConstantValue(LogLevel.log); @@ -10,4 +9,4 @@ export const commonModule = new ContainerModule((bind, unbind, isBound, rebind) configureViewerOptions(context, { zoomLimits: { min: 0.05, max: 20 }, }); -}); \ No newline at end of file +}); diff --git a/frontend/webEditor/src/constraint/Constraint.ts b/frontend/webEditor/src/constraint/Constraint.ts index 4b1e27e5..64deeb2f 100644 --- a/frontend/webEditor/src/constraint/Constraint.ts +++ b/frontend/webEditor/src/constraint/Constraint.ts @@ -1,4 +1,4 @@ export interface Constraint { name: string; constraint: string; -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/constraint/ConstraintMenu.ts b/frontend/webEditor/src/constraint/ConstraintMenu.ts index b587aecf..bed9d06f 100644 --- a/frontend/webEditor/src/constraint/ConstraintMenu.ts +++ b/frontend/webEditor/src/constraint/ConstraintMenu.ts @@ -1,6 +1,6 @@ import { inject, injectable } from "inversify"; import "./constraintMenu.css"; -import { IActionDispatcher, LocalModelSource, TYPES } from "sprotty"; +import { IActionDispatcher, LocalModelSource, TYPES } from "sprotty"; import { ConstraintRegistry } from "./constraintRegistry"; // Enable hover feature that is used to show validation errors. @@ -29,7 +29,7 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha private editor?: monaco.editor.IStandaloneCodeEditor; private optionsMenu?: HTMLDivElement; private ignoreCheckboxChange = false; - private readonly tree: LanguageTreeNode[] + private readonly tree: LanguageTreeNode[]; constructor( @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, @@ -38,14 +38,14 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha @inject(TYPES.IActionDispatcher) private readonly dispatcher: IActionDispatcher, @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, - @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager + @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, ) { super("left", "up"); this.constraintRegistry = constraintRegistry; editorModeController.registerListener(() => { this.editor?.updateOptions({ - readOnly: editorModeController.isReadOnly() - }) + readOnly: editorModeController.isReadOnly(), + }); }); constraintRegistry.onUpdate(() => { if (this.editor) { @@ -57,7 +57,7 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha } }); - this.tree = ConstraintDslTreeBuilder.buildTree(modelSource, labelTypeRegistry) + this.tree = ConstraintDslTreeBuilder.buildTree(modelSource, labelTypeRegistry); } id(): string { @@ -67,10 +67,9 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha return ConstraintMenu.ID; } - protected initializeHeaderContent(headerElement: HTMLElement): void { - headerElement.id = 'constraint-menu-expand-title' - headerElement.innerText = 'Constraints' + headerElement.id = "constraint-menu-expand-title"; + headerElement.innerText = "Constraints"; headerElement.appendChild(this.buildOptionsButton()); } @@ -78,7 +77,7 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha const contentDiv = document.createElement("div"); contentDiv.id = "constraint-menu-content"; contentDiv.appendChild(this.buildConstraintInputWrapper()); - contentElement.appendChild(contentDiv) + contentElement.appendChild(contentDiv); } protected initializeContents(containerElement: HTMLElement): void { @@ -100,10 +99,7 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha monaco.languages.register({ id: DSL_LANGUAGE_ID }); monaco.languages.setMonarchTokensProvider(DSL_LANGUAGE_ID, constraintDslLanguageMonarchDefinition); - monaco.languages.registerCompletionItemProvider( - DSL_LANGUAGE_ID, - new DfdCompletionItemProvider(this.tree), - ); + monaco.languages.registerCompletionItemProvider(DSL_LANGUAGE_ID, new DfdCompletionItemProvider(this.tree)); const monacoTheme = this.themeManager.getTheme() === Theme.DARK ? "vs-dark" : "vs"; this.editor = monaco.editor.create(this.editorContainer, { @@ -147,7 +143,7 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha const emptyContent = content.length == 0 || (content.length == 1 && content[0] === ""); // empty content gets accepted as valid as it represents no constraints if (!emptyContent) { - const errors = verify(tokenize(content), this.tree) + const errors = verify(tokenize(content), this.tree); marker.push( ...errors.map((e) => ({ severity: monaco.MarkerSeverity.Error, @@ -178,7 +174,10 @@ export class ConstraintMenu extends AccordionUiExtension implements ThemeSwitcha button.id = "run-button"; button.innerHTML = "Run"; button.onclick = () => { - this.dispatcher.dispatchAll([AnalyzeAction.create(), SelectConstraintsAction.create(this.constraintRegistry.getConstraintList().map(c => c.name))]); + this.dispatcher.dispatchAll([ + AnalyzeAction.create(), + SelectConstraintsAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)), + ]); }; wrapper.appendChild(button); diff --git a/frontend/webEditor/src/constraint/di.config.ts b/frontend/webEditor/src/constraint/di.config.ts index e9b1f101..a79e4235 100644 --- a/frontend/webEditor/src/constraint/di.config.ts +++ b/frontend/webEditor/src/constraint/di.config.ts @@ -13,8 +13,8 @@ export const constraintModule = new ContainerModule((bind, unbind, isBound) => { bind(ConstraintMenu).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(ConstraintMenu); bind(EDITOR_TYPES.DefaultUIElement).toService(ConstraintMenu); - bind(ThemeSwitchable).toService(ConstraintMenu) + bind(ThemeSwitchable).toService(ConstraintMenu); - bind(TFGManager).toSelf().inSingletonScope() - configureCommand({bind, isBound}, SelectConstraintsCommand) -}) \ No newline at end of file + bind(TFGManager).toSelf().inSingletonScope(); + configureCommand({ bind, isBound }, SelectConstraintsCommand); +}); diff --git a/frontend/webEditor/src/constraint/language.ts b/frontend/webEditor/src/constraint/language.ts index b3e1abd6..8746b318 100644 --- a/frontend/webEditor/src/constraint/language.ts +++ b/frontend/webEditor/src/constraint/language.ts @@ -7,7 +7,7 @@ import { ArrowEdge } from "../diagram/edges/ArrowEdge"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; import { WordCompletion } from "../languages/autocomplete"; -export const DSL_LANGUAGE_ID = "dfd-constraint" +export const DSL_LANGUAGE_ID = "dfd-constraint"; export const constraintDslLanguageMonarchDefinition: monaco.languages.IMonarchLanguage = { keywords: ["data", "vertex", "neverFlows", "to", "where", "named", "present", "empty", "type"], @@ -53,7 +53,10 @@ export const constraintDslLanguageMonarchDefinition: monaco.languages.IMonarchLa }; export namespace ConstraintDslTreeBuilder { - export function buildTree(modelSource: LocalModelSource, labelTypeRegistry: LabelTypeRegistry): LanguageTreeNode[] { + export function buildTree( + modelSource: LocalModelSource, + labelTypeRegistry: LabelTypeRegistry, + ): LanguageTreeNode[] { const conditions = getConditionalSelectors(); const conditionalSelector: LanguageTreeNode = { word: new ConstantWord("where"), @@ -376,4 +379,4 @@ export namespace ConstraintDslTreeBuilder { return []; } } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/constraint/selection.ts b/frontend/webEditor/src/constraint/selection.ts index 9047e1f5..03718085 100644 --- a/frontend/webEditor/src/constraint/selection.ts +++ b/frontend/webEditor/src/constraint/selection.ts @@ -5,7 +5,12 @@ import { Action, getBasicType } from "sprotty-protocol"; import { DfdNodeImpl } from "../diagram/nodes/common"; import { inject } from "inversify"; -function selectConstraints(selectedConstraintNames: string[], root: SModelRootImpl, constraintRegistry: ConstraintRegistry, tfgManager: TFGManager) { +function selectConstraints( + selectedConstraintNames: string[], + root: SModelRootImpl, + constraintRegistry: ConstraintRegistry, + tfgManager: TFGManager, +) { tfgManager.clearTfgs(); constraintRegistry.setSelectedConstraints(selectedConstraintNames); @@ -41,46 +46,62 @@ function selectConstraints(selectedConstraintNames: string[], root: SModelRootIm }); nodes.forEach((node) => { - const inTFG = node.annotations!.filter((annotation) => - tfgManager.getSelectedTfgs().has(annotation.tfg!), - ); + const inTFG = node.annotations!.filter((annotation) => tfgManager.getSelectedTfgs().has(annotation.tfg!)); if (inTFG.length > 0) node.setColor("var(--color-highlighted)", false); }); - return root + return root; } interface SelectConstraintsAction extends Action { - selectedConstraintNames: string[] + selectedConstraintNames: string[]; } export namespace SelectConstraintsAction { - export const KIND = 'select-constraints' + export const KIND = "select-constraints"; export function create(selectedConstraintNames: string[]): SelectConstraintsAction { return { kind: KIND, - selectedConstraintNames - } + selectedConstraintNames, + }; } } export class SelectConstraintsCommand extends Command { - static readonly KIND = SelectConstraintsAction.KIND - private oldConstraintSelection?: string[] + static readonly KIND = SelectConstraintsAction.KIND; + private oldConstraintSelection?: string[]; - constructor(@inject(TYPES.Action) private readonly action: SelectConstraintsAction, @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, @inject(TFGManager) private readonly tfgManager: TFGManager) { - super() + constructor( + @inject(TYPES.Action) private readonly action: SelectConstraintsAction, + @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, + @inject(TFGManager) private readonly tfgManager: TFGManager, + ) { + super(); } execute(context: CommandExecutionContext): CommandReturn { - this.oldConstraintSelection = this.constraintRegistry.getSelectedConstraints() - return selectConstraints(this.action.selectedConstraintNames, context.root, this.constraintRegistry, this.tfgManager) + this.oldConstraintSelection = this.constraintRegistry.getSelectedConstraints(); + return selectConstraints( + this.action.selectedConstraintNames, + context.root, + this.constraintRegistry, + this.tfgManager, + ); } undo(context: CommandExecutionContext): CommandReturn { - return selectConstraints(this.oldConstraintSelection ?? [], context.root, this.constraintRegistry, this.tfgManager) + return selectConstraints( + this.oldConstraintSelection ?? [], + context.root, + this.constraintRegistry, + this.tfgManager, + ); } redo(context: CommandExecutionContext): CommandReturn { - return selectConstraints(this.action.selectedConstraintNames, context.root, this.constraintRegistry, this.tfgManager) + return selectConstraints( + this.action.selectedConstraintNames, + context.root, + this.constraintRegistry, + this.tfgManager, + ); } - -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/diagram/di.config.ts b/frontend/webEditor/src/diagram/di.config.ts index a10f9344..358fc9fa 100644 --- a/frontend/webEditor/src/diagram/di.config.ts +++ b/frontend/webEditor/src/diagram/di.config.ts @@ -1,24 +1,42 @@ import { ContainerModule } from "inversify"; -import { configureActionHandler, configureCommand, configureModelElement, EditLabelAction, EditLabelActionHandler, editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SRoutingHandleImpl, TYPES, withEditLabelFeature } from "sprotty"; +import { + configureActionHandler, + configureCommand, + configureModelElement, + EditLabelAction, + EditLabelActionHandler, + editLabelFeature, + SGraphImpl, + SGraphView, + SLabelImpl, + SLabelView, + SRoutingHandleImpl, + TYPES, + withEditLabelFeature, +} from "sprotty"; import { ArrowEdgeImpl, ArrowEdgeView, CustomRoutingHandleView } from "./edges/ArrowEdge"; import { DfdInputPortImpl, DfdInputPortView } from "./ports/DfdInputPort"; import { DfdOutputPortImpl, DfdOutputPortView } from "./ports/DfdOutputPort"; import { StorageNodeImpl, StorageNodeView } from "./nodes/DfdStorageNode"; import { FunctionNodeImpl, FunctionNodeView } from "./nodes/DfdFunctionNode"; import { IONodeImpl, IONodeView } from "./nodes/DfdIONode"; -import './style.css' +import "./style.css"; import { DfdPositionalLabelView } from "./labels/DfdPositionalLabel"; import { DfdNodeLabelRenderer } from "./nodes/DfdNodeLabels"; import { FilledBackgroundLabelView } from "./labels/FilledBackgroundLabel"; import { DfdEditLabelValidatorDecorator } from "./labels/EditLabelDecorator"; import { DfdEditLabelValidator } from "./labels/EditLabelValidator"; import { NoScrollEditLabelUI } from "./labels/NoScrollEditLabelUI"; -import { PortAwareSnapper, AlwaysSnapPortsMoveMouseListener, ReSnapPortsAfterLabelChangeCommand } from "./ports/portSnapper"; +import { + PortAwareSnapper, + AlwaysSnapPortsMoveMouseListener, + ReSnapPortsAfterLabelChangeCommand, +} from "./ports/portSnapper"; import { DfdNodeAnnotationUI, DfdNodeAnnotationUIMouseListener } from "./nodes/annotation"; export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; - + bind(TYPES.IEditLabelValidator).to(DfdEditLabelValidator).inSingletonScope(); bind(TYPES.IEditLabelValidationDecorator).to(DfdEditLabelValidatorDecorator).inSingletonScope(); configureActionHandler(context, EditLabelAction.KIND, EditLabelActionHandler); @@ -29,10 +47,10 @@ export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) bind(TYPES.MouseListener).to(AlwaysSnapPortsMoveMouseListener).inSingletonScope(); configureCommand(context, ReSnapPortsAfterLabelChangeCommand); - bind(DfdNodeAnnotationUI).toSelf().inSingletonScope() - bind(TYPES.IUIExtension).toService(DfdNodeAnnotationUI) - bind(DfdNodeAnnotationUIMouseListener).toSelf().inSingletonScope() - bind(TYPES.MouseListener).toService(DfdNodeAnnotationUIMouseListener) + bind(DfdNodeAnnotationUI).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(DfdNodeAnnotationUI); + bind(DfdNodeAnnotationUIMouseListener).toSelf().inSingletonScope(); + bind(TYPES.MouseListener).toService(DfdNodeAnnotationUIMouseListener); configureModelElement(context, "graph", SGraphImpl, SGraphView); @@ -59,6 +77,5 @@ export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) enable: [editLabelFeature], }); - bind(DfdNodeLabelRenderer).toSelf().inSingletonScope() - -}); \ No newline at end of file + bind(DfdNodeLabelRenderer).toSelf().inSingletonScope(); +}); diff --git a/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx b/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx index fd2c9068..ef2c23d5 100644 --- a/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx +++ b/frontend/webEditor/src/diagram/edges/ArrowEdge.tsx @@ -34,12 +34,10 @@ export class ArrowEdgeImpl extends SEdgeImpl implements WithEditableLabel { @injectable() export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { - constructor(@inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames) { - super() + super(); } - /** * Renders an arrow at the end of the edge. */ @@ -110,13 +108,14 @@ export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections { return this.renderDanglingEdge("Cannot compute route", edge, context); } - - return - {this.renderLine(edge, route)} - {this.renderAdditionals(edge, route, context)} - {this.renderJunctionPoints(edge, route, context, args)} - { this.hideEdgeNames.get() ? undefined : context.renderChildren(edge, { route }) } - ; + return ( + + {this.renderLine(edge, route)} + {this.renderAdditionals(edge, route, context)} + {this.renderJunctionPoints(edge, route, context, args)} + {this.hideEdgeNames.get() ? undefined : context.renderChildren(edge, { route })} + + ); } } diff --git a/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx b/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx index 7bb32c65..1c4ebe92 100644 --- a/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx +++ b/frontend/webEditor/src/diagram/labels/DfdPositionalLabel.tsx @@ -32,4 +32,4 @@ export class DfdPositionalLabelView extends ShapeView { ); } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts b/frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts index 99a1bbe6..ab8aaddc 100644 --- a/frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts +++ b/frontend/webEditor/src/diagram/labels/EditLabelDecorator.ts @@ -1,6 +1,6 @@ import { injectable } from "inversify"; import { IEditLabelValidationDecorator, EditLabelValidationResult } from "sprotty"; -import "./editLabelDecorator.css" +import "./editLabelDecorator.css"; /** * Renders the validation result of an dfd edge label to the label edit ui. @@ -35,4 +35,4 @@ export class DfdEditLabelValidatorDecorator implements IEditLabelValidationDecor containerElement.querySelector(`span.${this.cssClass}`)?.remove(); } } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/diagram/labels/EditLabelValidator.ts b/frontend/webEditor/src/diagram/labels/EditLabelValidator.ts index c0c47ccb..7751f118 100644 --- a/frontend/webEditor/src/diagram/labels/EditLabelValidator.ts +++ b/frontend/webEditor/src/diagram/labels/EditLabelValidator.ts @@ -1,5 +1,12 @@ import { injectable } from "inversify"; -import { IEditLabelValidator, EditableLabel, SModelElementImpl, EditLabelValidationResult, SChildElementImpl, SEdgeImpl } from "sprotty"; +import { + IEditLabelValidator, + EditableLabel, + SModelElementImpl, + EditLabelValidationResult, + SChildElementImpl, + SEdgeImpl, +} from "sprotty"; import { DfdNodeImpl } from "../nodes/common"; import { DfdInputPortImpl } from "../ports/DfdInputPort"; @@ -54,4 +61,4 @@ export class DfdEditLabelValidator implements IEditLabelValidator { return { severity: "ok" }; } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx b/frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx index 55c254e9..e8cc209f 100644 --- a/frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx +++ b/frontend/webEditor/src/diagram/labels/FilledBackgroundLabel.tsx @@ -29,4 +29,4 @@ export class FilledBackgroundLabelView extends ShapeView { ); } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts b/frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts index 727eb513..444e4065 100644 --- a/frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts +++ b/frontend/webEditor/src/diagram/labels/NoScrollEditLabelUI.ts @@ -1,7 +1,4 @@ -import { - EditLabelUI, - SModelRootImpl, -} from "sprotty"; +import { EditLabelUI, SModelRootImpl } from "sprotty"; // For our use-case the sprotty container is at (0, 0) and fills the whole screen. // Scrolling is disabled using CSS which disallows scrolling from the user. diff --git a/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx b/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx index 7dd229a5..bcca50ed 100644 --- a/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx +++ b/frontend/webEditor/src/diagram/nodes/DfdFunctionNode.tsx @@ -18,10 +18,8 @@ export class FunctionNodeImpl extends DfdNodeImpl { return FunctionNodeImpl.LABEL_START_HEIGHT + FunctionNodeImpl.SEPARATOR_NO_LABEL_PADDING; } protected labelStartHeight(): number { - return FunctionNodeImpl.LABEL_START_HEIGHT + return FunctionNodeImpl.LABEL_START_HEIGHT; } - - } @injectable() @@ -50,4 +48,4 @@ export class FunctionNodeView extends ShapeView { ); } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx b/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx index fd3d6a4d..65bb9894 100644 --- a/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx +++ b/frontend/webEditor/src/diagram/nodes/DfdIONode.tsx @@ -16,13 +16,12 @@ export class IONodeImpl extends DfdNodeImpl { return IONodeImpl.TEXT_HEIGHT; } protected labelStartHeight(): number { - return IONodeImpl.LABEL_START_HEIGHT + return IONodeImpl.LABEL_START_HEIGHT; } } @injectable() export class IONodeView extends ShapeView { - constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { super(); } diff --git a/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx index c391edb5..fef034de 100644 --- a/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx +++ b/frontend/webEditor/src/diagram/nodes/DfdNodeLabels.tsx @@ -21,16 +21,16 @@ export class DfdNodeLabelRenderer { @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, ) {} - private getLabel(label: LabelAssignment): {type: LabelType, value: LabelTypeValue} | undefined { + private getLabel(label: LabelAssignment): { type: LabelType; value: LabelTypeValue } | undefined { const labelType = this.labelTypeRegistry.getLabelType(label.labelTypeId); const labelTypeValue = labelType?.values.find((value) => value.id === label.labelTypeValueId); - if (!labelType || ! labelTypeValue) { - return undefined + if (!labelType || !labelTypeValue) { + return undefined; } return { type: labelType, - value: labelTypeValue - } + value: labelTypeValue, + }; } /** @@ -39,7 +39,7 @@ export class DfdNodeLabelRenderer { * @returns a tuple containing the text and the width of the label in pixel */ computeLabelContent(labelAssignment: LabelAssignment): [string, number] { - const label = this.getLabel(labelAssignment) + const label = this.getLabel(labelAssignment); if (!label) { return ["", 0]; } @@ -89,8 +89,8 @@ export class DfdNodeLabelRenderer { */ private sortLabels(labels: LabelAssignment[]): void { labels.sort((a, b) => { - const labelTypeA = this.getLabel(a) - const labelTypeB = this.getLabel(b) + const labelTypeA = this.getLabel(a); + const labelTypeB = this.getLabel(b); if (!labelTypeA || !labelTypeB) { return 0; } @@ -123,4 +123,3 @@ export class DfdNodeLabelRenderer { ); } } - diff --git a/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx b/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx index 083f859d..cbd895d5 100644 --- a/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx +++ b/frontend/webEditor/src/diagram/nodes/DfdStorageNode.tsx @@ -28,8 +28,8 @@ export class StorageNodeImpl extends DfdNodeImpl { @injectable() export class StorageNodeView extends ShapeView { constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) { - super(); - } + super(); + } render(node: Readonly, context: RenderingContext): VNode | undefined { if (!this.isVisible(node, context)) { @@ -51,4 +51,4 @@ export class StorageNodeView extends ShapeView { ); } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/diagram/nodes/annotation.ts b/frontend/webEditor/src/diagram/nodes/annotation.ts index c8b2fe95..a0d5e002 100644 --- a/frontend/webEditor/src/diagram/nodes/annotation.ts +++ b/frontend/webEditor/src/diagram/nodes/annotation.ts @@ -1,14 +1,23 @@ import { inject, injectable } from "inversify"; -import { MouseListener, TYPES, IActionDispatcher, SModelElementImpl, SChildElementImpl, SetUIExtensionVisibilityAction, AbstractUIExtension, SModelRootImpl } from "sprotty"; +import { + MouseListener, + TYPES, + IActionDispatcher, + SModelElementImpl, + SChildElementImpl, + SetUIExtensionVisibilityAction, + AbstractUIExtension, + SModelRootImpl, +} from "sprotty"; import { Action } from "sprotty-protocol"; import { DfdNodeImpl } from "./common"; -import "./nodeAnnotationUi.css" +import "./nodeAnnotationUi.css"; import { ShownLabels, ShownLabelsValue } from "../../settings/ShownLabels"; import { SETTINGS } from "../../settings/Settings"; export class DfdNodeAnnotationUIMouseListener extends MouseListener { private stillTimeout: number | undefined; - private lastTarget?: DfdNodeImpl + private lastTarget?: DfdNodeImpl; private lastPosition = { x: 0, y: 0 }; constructor(@inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher) { @@ -27,30 +36,30 @@ export class DfdNodeAnnotationUIMouseListener extends MouseListener { this.lastPosition = { x: event.clientX, y: event.clientY }; if (dfdNode === this.lastTarget) { - return [] + return []; } this.stillTimeout = setTimeout(() => { - // When the mouse has not moved for 500ms, we show the popup - this.stillTimeout = undefined; - - if (dfdNode.opacity !== 1) { - // Only show when opacity is 1. - // The opacity is not 1 when the node is currently being created but has not been - // placed yet. - // In this case we don't want to show the popup - // and interfere with the creation process. - return; - } + // When the mouse has not moved for 500ms, we show the popup + this.stillTimeout = undefined; + + if (dfdNode.opacity !== 1) { + // Only show when opacity is 1. + // The opacity is not 1 when the node is currently being created but has not been + // placed yet. + // In this case we don't want to show the popup + // and interfere with the creation process. + return; + } - this.showPopup(dfdNode); + this.showPopup(dfdNode); }, 500); if (this.lastTarget !== dfdNode) { - this.lastTarget = dfdNode - return this.hidePopup() + this.lastTarget = dfdNode; + return this.hidePopup(); } - return [] + return []; } private findDfdNode(currentNode: SModelElementImpl): DfdNodeImpl | undefined { @@ -79,7 +88,7 @@ export class DfdNodeAnnotationUIMouseListener extends MouseListener { } private hidePopup() { - return [SetUIExtensionVisibilityAction.create({extensionId: DfdNodeAnnotationUI.ID, visible: false})] + return [SetUIExtensionVisibilityAction.create({ extensionId: DfdNodeAnnotationUI.ID, visible: false })]; } public getMousePosition(): { x: number; y: number } { @@ -170,8 +179,10 @@ export class DfdNodeAnnotationUI extends AbstractUIExtension { node.annotations.forEach((a) => { if ( - ((mode === ShownLabels.INCOMING || mode === ShownLabels.ALL) && a.message.trim().startsWith("Incoming")) || - ((mode === ShownLabels.OUTGOING || mode === ShownLabels.ALL) && a.message.trim().startsWith("Propagated")) || + ((mode === ShownLabels.INCOMING || mode === ShownLabels.ALL) && + a.message.trim().startsWith("Incoming")) || + ((mode === ShownLabels.OUTGOING || mode === ShownLabels.ALL) && + a.message.trim().startsWith("Propagated")) || a.message.startsWith("Constraint") ) { const line = document.createElement("div"); diff --git a/frontend/webEditor/src/diagram/nodes/common.ts b/frontend/webEditor/src/diagram/nodes/common.ts index 902431cf..b49ba37c 100644 --- a/frontend/webEditor/src/diagram/nodes/common.ts +++ b/frontend/webEditor/src/diagram/nodes/common.ts @@ -23,17 +23,17 @@ export abstract class DfdNodeImpl extends SNodeImpl implements WithEditableLabel static readonly WIDTH_PADDING = 12; static readonly NODE_COLOR = "var(--color-primary)"; static readonly HIGHLIGHTED_COLOR = "var(--color-highlighted)"; -@inject(DfdNodeLabelRenderer) private readonly dfdNodeLabelRenderer?: DfdNodeLabelRenderer + @inject(DfdNodeLabelRenderer) private readonly dfdNodeLabelRenderer?: DfdNodeLabelRenderer; text: string = ""; color?: string; labels: LabelAssignment[] = []; ports: SPort[] = []; hideLabels: boolean = false; minimumWidth: number = DfdNodeImpl.DEFAULT_WIDTH; - annotations: DfdNodeAnnotation[] = []; + annotations: DfdNodeAnnotation[] = []; constructor() { - super() + super(); } get editableLabel() { @@ -51,14 +51,14 @@ export abstract class DfdNodeImpl extends SNodeImpl implements WithEditableLabel } const textWidth = calculateTextSize(this.text).width; const labelWidths = this.labels.map( - (labelAssignment) => this.dfdNodeLabelRenderer?.computeLabelContent(labelAssignment)[1] ?? 0 + (labelAssignment) => this.dfdNodeLabelRenderer?.computeLabelContent(labelAssignment)[1] ?? 0, ); const neededWidth = Math.max(...labelWidths, textWidth, DfdNodeImpl.DEFAULT_WIDTH); return neededWidth + DfdNodeImpl.WIDTH_PADDING; } - protected calculateHeight(): number { + protected calculateHeight(): number { const hasLabels = this.labels.length > 0; if (hasLabels && !this.hideLabels) { return ( @@ -71,8 +71,8 @@ export abstract class DfdNodeImpl extends SNodeImpl implements WithEditableLabel } } - protected abstract noLabelHeight(): number - protected abstract labelStartHeight(): number + protected abstract noLabelHeight(): number; + protected abstract labelStartHeight(): number; override get bounds(): Bounds { return { @@ -132,4 +132,4 @@ export abstract class DfdNodeImpl extends SNodeImpl implements WithEditableLabel public setColor(color: string, override: boolean = true) { if (override || this.color === DfdNodeImpl.NODE_COLOR) this.color = color; } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/diagram/ports/common.ts b/frontend/webEditor/src/diagram/ports/common.ts index 11116192..69c9db23 100644 --- a/frontend/webEditor/src/diagram/ports/common.ts +++ b/frontend/webEditor/src/diagram/ports/common.ts @@ -15,4 +15,4 @@ export abstract class DfdPortImpl extends SPortImpl { height: portSize, }; } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/editorTypes.ts b/frontend/webEditor/src/editorTypes.ts index 9a302780..45eac078 100644 --- a/frontend/webEditor/src/editorTypes.ts +++ b/frontend/webEditor/src/editorTypes.ts @@ -7,4 +7,4 @@ export const EDITOR_TYPES = { // All IUIExtension instances that are bound to this symbol will // be loaded and enabled at editor startup. DefaultUIElement: Symbol("DefaultUIElement"), -}; \ No newline at end of file +}; diff --git a/frontend/webEditor/src/fileName/di.config.ts b/frontend/webEditor/src/fileName/di.config.ts index e09aecea..8ba255a5 100644 --- a/frontend/webEditor/src/fileName/di.config.ts +++ b/frontend/webEditor/src/fileName/di.config.ts @@ -3,4 +3,4 @@ import { FileName } from "./fileName"; export const fileNameModule = new ContainerModule((bind) => { bind(FileName).toSelf().inSingletonScope(); -}) \ No newline at end of file +}); diff --git a/frontend/webEditor/src/fileName/fileName.ts b/frontend/webEditor/src/fileName/fileName.ts index f4fc0c46..30156179 100644 --- a/frontend/webEditor/src/fileName/fileName.ts +++ b/frontend/webEditor/src/fileName/fileName.ts @@ -1,14 +1,14 @@ export class FileName { - private name: string = 'diagram'; + private name: string = "diagram"; getName(): string { return this.name; } setName(newName: string): void { - const lastIndex = newName.lastIndexOf('.'); + const lastIndex = newName.lastIndexOf("."); this.name = lastIndex === -1 ? newName : newName.substring(0, lastIndex); - document.title = this.name + '.json - DFD WebEditor'; + document.title = this.name + ".json - DFD WebEditor"; } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/helpUi/di.config.ts b/frontend/webEditor/src/helpUi/di.config.ts index 7270805b..8fd5320c 100644 --- a/frontend/webEditor/src/helpUi/di.config.ts +++ b/frontend/webEditor/src/helpUi/di.config.ts @@ -3,8 +3,8 @@ import { TYPES } from "sprotty"; import { HelpUI } from "./helpUi"; import { EDITOR_TYPES } from "../editorTypes"; -export const helpUiModule = new ContainerModule((bind) => { +export const helpUiModule = new ContainerModule((bind) => { bind(HelpUI).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(HelpUI); bind(EDITOR_TYPES.DefaultUIElement).toService(HelpUI); -}); \ No newline at end of file +}); diff --git a/frontend/webEditor/src/helpUi/helpUi.css b/frontend/webEditor/src/helpUi/helpUi.css index 650ceb33..a5e496ec 100644 --- a/frontend/webEditor/src/helpUi/helpUi.css +++ b/frontend/webEditor/src/helpUi/helpUi.css @@ -15,5 +15,3 @@ margin-right: 4px; } } - - diff --git a/frontend/webEditor/src/helpUi/helpUi.ts b/frontend/webEditor/src/helpUi/helpUi.ts index 65cb9f8e..76f94676 100644 --- a/frontend/webEditor/src/helpUi/helpUi.ts +++ b/frontend/webEditor/src/helpUi/helpUi.ts @@ -7,7 +7,7 @@ export class HelpUI extends AccordionUiExtension { static readonly ID = "help-ui"; constructor() { - super('right', 'up') + super("right", "up"); } id() { @@ -37,7 +37,7 @@ export class HelpUI extends AccordionUiExtension { `; } protected initializeHeaderContent(headerElement: HTMLElement) { - headerElement.classList.add('help-accordion-icon'); - headerElement.innerText = 'Keyboard Shortcuts | Help' + headerElement.classList.add("help-accordion-icon"); + headerElement.innerText = "Keyboard Shortcuts | Help"; } } diff --git a/frontend/webEditor/src/index.ts b/frontend/webEditor/src/index.ts index 73ff15a8..ce0f74b7 100644 --- a/frontend/webEditor/src/index.ts +++ b/frontend/webEditor/src/index.ts @@ -2,9 +2,9 @@ import "reflect-metadata"; import { Container } from "inversify"; import { loadDefaultModules, labelEditUiModule } from "sprotty"; import "sprotty/css/sprotty.css"; -import "./assets/commonStyling.css" -import "./assets/page.css" -import "./assets/theme.css" +import "./assets/commonStyling.css"; +import "./assets/page.css"; +import "./assets/theme.css"; import "@vscode/codicons/dist/codicon.css"; import "@fortawesome/fontawesome-free/css/all.min.css"; import { helpUiModule } from "./helpUi/di.config"; @@ -52,10 +52,10 @@ container.load( constraintModule, assignmentModule, editorModeOverwritesModule, - loadingIndicatorModule -) + loadingIndicatorModule, +); -const startUpAgents = container.getAll(StartUpAgent) +const startUpAgents = container.getAll(StartUpAgent); for (const startUpAgent of startUpAgents) { - startUpAgent.run() + startUpAgent.run(); } diff --git a/frontend/webEditor/src/labels/LabelType.ts b/frontend/webEditor/src/labels/LabelType.ts index 2fc2b067..1e7b78e1 100644 --- a/frontend/webEditor/src/labels/LabelType.ts +++ b/frontend/webEditor/src/labels/LabelType.ts @@ -12,4 +12,4 @@ export interface LabelTypeValue { export interface LabelAssignment { labelTypeId: string; labelTypeValueId: string; -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts index 3c685b34..afe51f2a 100644 --- a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -16,7 +16,11 @@ export class LabelTypeEditorUi extends AccordionUiExtension { static readonly ID = "label-type-editor-ui"; private labelSectionContainer?: HTMLElement; - constructor(@inject(LabelTypeRegistry) private labelTypeRegistry: LabelTypeRegistry, @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController) { + constructor( + @inject(LabelTypeRegistry) private labelTypeRegistry: LabelTypeRegistry, + @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: IActionDispatcher, + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, + ) { super("left", "down"); labelTypeRegistry.onUpdate(() => this.renderLabelTypes()); } @@ -33,7 +37,7 @@ export class LabelTypeEditorUi extends AccordionUiExtension { addButton.onclick = () => { if (this.editorModeController.isReadOnly()) { - return + return; } this.labelTypeRegistry.registerLabelType(""); }; @@ -52,23 +56,21 @@ export class LabelTypeEditorUi extends AccordionUiExtension { if (!this.labelSectionContainer) { return; } - const width = this.labelSectionContainer.scrollWidth - const height = this.labelSectionContainer.scrollHeight - this.labelSectionContainer.style.width = `${width}px` - this.labelSectionContainer.style.height = `${height}px` - const fragment = document.createDocumentFragment() - const labelTypes = this.labelTypeRegistry.getLabelTypes() + const width = this.labelSectionContainer.scrollWidth; + const height = this.labelSectionContainer.scrollHeight; + this.labelSectionContainer.style.width = `${width}px`; + this.labelSectionContainer.style.height = `${height}px`; + const fragment = document.createDocumentFragment(); + const labelTypes = this.labelTypeRegistry.getLabelTypes(); for (let i = 0; i < labelTypes.length; i++) { - fragment.appendChild(this.buildLabelTypeSection(labelTypes[i])) + fragment.appendChild(this.buildLabelTypeSection(labelTypes[i])); if (i < labelTypes.length - 1) { - fragment.appendChild(document.createElement('hr')) + fragment.appendChild(document.createElement("hr")); } } - this.labelSectionContainer!.replaceChildren(fragment) - this.labelSectionContainer!.style.width = '' - this.labelSectionContainer!.style.height = '' - - + this.labelSectionContainer!.replaceChildren(fragment); + this.labelSectionContainer!.style.width = ""; + this.labelSectionContainer!.style.height = ""; } private buildLabelTypeSection(labelType: LabelType): HTMLElement { @@ -83,19 +85,19 @@ export class LabelTypeEditorUi extends AccordionUiExtension { const addButton = UiElementFactory.buildAddButton("Value"); addButton.classList.add("label-type-value-add"); - nameInput.value = labelType.name - nameInput.placeholder = 'Label Type Name' - nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput) - dynamicallySetInputSize(nameInput) - setTimeout(() => dynamicallySetInputSize(nameInput), 0) + nameInput.value = labelType.name; + nameInput.placeholder = "Label Type Name"; + nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput); + dynamicallySetInputSize(nameInput); + setTimeout(() => dynamicallySetInputSize(nameInput), 0); nameInput.onchange = () => { this.labelTypeRegistry.updateLabelTypeName(labelType.id, nameInput.value); }; nameInput.onfocus = () => { if (this.editorModeController.isReadOnly()) { - nameInput.blur() + nameInput.blur(); } - } + }; for (let i = 0; i < labelType.values.length; i++) { labelTypeValueHolder.appendChild(this.buildLabelTypeValue(labelType, i)); @@ -103,14 +105,14 @@ export class LabelTypeEditorUi extends AccordionUiExtension { addButton.onclick = () => { if (this.editorModeController.isReadOnly()) { - return + return; } this.labelTypeRegistry.registerLabelTypeValue(labelType.id, ""); }; deleteButton.onclick = () => { if (this.editorModeController.isReadOnly()) { - return + return; } this.labelTypeRegistry.unregisterLabelType(labelType.id); }; @@ -130,25 +132,24 @@ export class LabelTypeEditorUi extends AccordionUiExtension { nameInput.classList.add("label-type-value-name"); const deleteButton = UiElementFactory.buildDeleteButton(); - const value = labelType.values[valueIndex] + const value = labelType.values[valueIndex]; - - nameInput.value = value.text - nameInput.placeholder = 'Value' - nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput) - nameInput.style.width = '0px' - setTimeout(() => dynamicallySetInputSize(nameInput), 0) + nameInput.value = value.text; + nameInput.placeholder = "Value"; + nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput); + nameInput.style.width = "0px"; + setTimeout(() => dynamicallySetInputSize(nameInput), 0); nameInput.onchange = () => { if (this.editorModeController.isReadOnly()) { - return + return; } this.labelTypeRegistry.updateLabelTypeValueText(labelType.id, value.id, nameInput.value); }; deleteButton.onclick = () => { if (this.editorModeController.isReadOnly()) { - return + return; } this.labelTypeRegistry.unregisterLabelTypeValue(labelType.id, value.id); }; @@ -157,7 +158,7 @@ export class LabelTypeEditorUi extends AccordionUiExtension { nameInput.draggable = true; nameInput.ondragstart = (event) => { if (this.editorModeController.isReadOnly()) { - return + return; } const assignment: LabelAssignment = { labelTypeId: labelType.id, @@ -170,7 +171,7 @@ export class LabelTypeEditorUi extends AccordionUiExtension { // Only edit on double click nameInput.onclick = () => { if (this.editorModeController.isReadOnly()) { - return + return; } if (nameInput.getAttribute("clicked") === "true") { return; @@ -191,15 +192,15 @@ export class LabelTypeEditorUi extends AccordionUiExtension { }; nameInput.ondblclick = () => { if (this.editorModeController.isReadOnly()) { - return + return; } nameInput.removeAttribute("clicked"); nameInput.focus(); }; nameInput.onfocus = (event) => { if (this.editorModeController.isReadOnly()) { - nameInput.blur() - return + nameInput.blur(); + return; } // we check for the single click here, since this gets triggered before the ondblclick event if (nameInput.getAttribute("clicked") !== "true") { diff --git a/frontend/webEditor/src/labels/LabelTypeRegistry.ts b/frontend/webEditor/src/labels/LabelTypeRegistry.ts index e43a499d..35ee3295 100644 --- a/frontend/webEditor/src/labels/LabelTypeRegistry.ts +++ b/frontend/webEditor/src/labels/LabelTypeRegistry.ts @@ -9,73 +9,73 @@ export class LabelTypeRegistry { const labelType: LabelType = { id: generateRandomSprottyId(), name, - values: [] - } + values: [], + }; this.labelTypes.push(labelType); - this._registerLabelTypeValue(labelType.id, 'Value', true) - this.labelTypeChanged() - return labelType + this._registerLabelTypeValue(labelType.id, "Value", true); + this.labelTypeChanged(); + return labelType; } public unregisterLabelType(id: string): void { this.labelTypes = this.labelTypes.filter((type) => type.id !== id); - this.labelTypeChanged() + this.labelTypeChanged(); } public updateLabelTypeName(id: string, name: string): void { - const labelType = this.labelTypes.find(l => l.id === id) - if (!labelType) { - throw `No Label Type with id ${id} found` + const labelType = this.labelTypes.find((l) => l.id === id); + if (!labelType) { + throw `No Label Type with id ${id} found`; } - labelType.name = name - this.labelTypeChanged() + labelType.name = name; + this.labelTypeChanged(); } public setLabelTypes(labelTypes: LabelType[]) { this.labelTypes = labelTypes; - this.labelTypeChanged() + this.labelTypeChanged(); } public registerLabelTypeValue(labelTypeId: string, text: string): LabelTypeValue { - return this._registerLabelTypeValue(labelTypeId, text) + return this._registerLabelTypeValue(labelTypeId, text); } - private _registerLabelTypeValue(labelTypeId: string, text: string, surpressUpdate=false): LabelTypeValue { + private _registerLabelTypeValue(labelTypeId: string, text: string, surpressUpdate = false): LabelTypeValue { const labelTypeValue: LabelTypeValue = { id: generateRandomSprottyId(), - text - } - const labelType = this.labelTypes.find((type) => type.id === labelTypeId) + text, + }; + const labelType = this.labelTypes.find((type) => type.id === labelTypeId); if (!labelType) { - throw `No Label Type with id ${labelTypeId} found` + throw `No Label Type with id ${labelTypeId} found`; } - labelType.values.push(labelTypeValue) + labelType.values.push(labelTypeValue); if (!surpressUpdate) { - this.labelTypeChanged() + this.labelTypeChanged(); } - return labelTypeValue + return labelTypeValue; } public unregisterLabelTypeValue(labelTypeId: string, labelTypeValueId: string): void { - const labelType = this.labelTypes.find((type) => type.id === labelTypeId) + const labelType = this.labelTypes.find((type) => type.id === labelTypeId); if (!labelType) { - throw `No Label Type with id ${labelTypeId} found` + throw `No Label Type with id ${labelTypeId} found`; } - labelType.values = labelType.values.filter((value) => value.id !== labelTypeValueId) - this.labelTypeChanged() + labelType.values = labelType.values.filter((value) => value.id !== labelTypeValueId); + this.labelTypeChanged(); } public updateLabelTypeValueText(labelTypeId: string, labelTypeValueId: string, text: string) { - const labelType = this.labelTypes.find((type) => type.id === labelTypeId) + const labelType = this.labelTypes.find((type) => type.id === labelTypeId); if (!labelType) { - throw `No Label Type with id ${labelTypeId} found` + throw `No Label Type with id ${labelTypeId} found`; } - const value = labelType.values.find(l => l.id === labelTypeValueId) + const value = labelType.values.find((l) => l.id === labelTypeValueId); if (!value) { - throw `Label Type ${labelType.name} has no value with id ${labelTypeValueId}` + throw `Label Type ${labelType.name} has no value with id ${labelTypeValueId}`; } - value.text = text - this.labelTypeChanged() + value.text = text; + this.labelTypeChanged(); } public clearLabelTypes(): void { @@ -98,4 +98,4 @@ export class LabelTypeRegistry { public getLabelType(id: string): LabelType | undefined { return this.labelTypes.find((type) => type.id === id); } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/labels/assignmentCommand.ts b/frontend/webEditor/src/labels/assignmentCommand.ts index 80f3d45d..4a63efe5 100644 --- a/frontend/webEditor/src/labels/assignmentCommand.ts +++ b/frontend/webEditor/src/labels/assignmentCommand.ts @@ -24,30 +24,36 @@ interface LabelAssignmentAction extends Action { } export namespace AddLabelAssignmentAction { - export function create(labelAssignment: LabelAssignment, element?: ContainsDfdLabels & SNodeImpl): LabelAssignmentAction { + export function create( + labelAssignment: LabelAssignment, + element?: ContainsDfdLabels & SNodeImpl, + ): LabelAssignmentAction { return { kind: LabelAssignmentCommand.KIND, - action: 'add', + action: "add", labelAssignment, - element - } + element, + }; } } export namespace RemoveLabelAssignmentAction { - export function create(labelAssignment: LabelAssignment, element?: ContainsDfdLabels & SNodeImpl): LabelAssignmentAction { + export function create( + labelAssignment: LabelAssignment, + element?: ContainsDfdLabels & SNodeImpl, + ): LabelAssignmentAction { return { kind: LabelAssignmentCommand.KIND, - action: 'remove', + action: "remove", labelAssignment, - element - } + element, + }; } } @injectable() export class LabelAssignmentCommand implements Command { - public static readonly KIND = 'labelAction'; + public static readonly KIND = "labelAction"; private elements?: ContainsDfdLabels[]; diff --git a/frontend/webEditor/src/labels/di.config.ts b/frontend/webEditor/src/labels/di.config.ts index 1b86b4b3..066837e7 100644 --- a/frontend/webEditor/src/labels/di.config.ts +++ b/frontend/webEditor/src/labels/di.config.ts @@ -7,13 +7,12 @@ import { LabelAssignmentCommand } from "./assignmentCommand"; import { DfdLabelMouseDropListener } from "./dragAndDrop"; export const labelModule = new ContainerModule((bind, _, isBound) => { - bind(LabelTypeRegistry).toSelf().inSingletonScope() + bind(LabelTypeRegistry).toSelf().inSingletonScope(); bind(LabelTypeEditorUi).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(LabelTypeEditorUi); bind(EDITOR_TYPES.DefaultUIElement).to(LabelTypeEditorUi); - configureCommand({ bind, isBound }, LabelAssignmentCommand); bind(TYPES.MouseListener).to(DfdLabelMouseDropListener); -}) \ No newline at end of file +}); diff --git a/frontend/webEditor/src/labels/dragAndDrop.ts b/frontend/webEditor/src/labels/dragAndDrop.ts index 96a8f209..2b0fef80 100644 --- a/frontend/webEditor/src/labels/dragAndDrop.ts +++ b/frontend/webEditor/src/labels/dragAndDrop.ts @@ -7,7 +7,7 @@ import { CommitModelAction, ILogger, TYPES, - SNodeImpl + SNodeImpl, } from "sprotty"; import { LabelAssignment } from "./LabelType"; import { AddLabelAssignmentAction } from "./assignmentCommand"; @@ -71,4 +71,4 @@ function getParentWithDfdLabels( } return undefined; -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/labels/feature.ts b/frontend/webEditor/src/labels/feature.ts index deb6b57a..2cf25d0f 100644 --- a/frontend/webEditor/src/labels/feature.ts +++ b/frontend/webEditor/src/labels/feature.ts @@ -9,4 +9,4 @@ export interface ContainsDfdLabels extends SModelElementImpl { export function containsDfdLabels(element: T): element is T & ContainsDfdLabels { return element.features?.has(containsDfdLabelFeature) ?? false; -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/labels/labelTypeEditorUi.css b/frontend/webEditor/src/labels/labelTypeEditorUi.css index 754617f8..f1ac3397 100644 --- a/frontend/webEditor/src/labels/labelTypeEditorUi.css +++ b/frontend/webEditor/src/labels/labelTypeEditorUi.css @@ -31,7 +31,8 @@ font-size: 12pt; } - .label-type-values, .label-type-value-add { + .label-type-values, + .label-type-value-add { margin-left: 10px; } } diff --git a/frontend/webEditor/src/labels/registryCommands.ts b/frontend/webEditor/src/labels/registryCommands.ts index 27ea6cd3..c59eed93 100644 --- a/frontend/webEditor/src/labels/registryCommands.ts +++ b/frontend/webEditor/src/labels/registryCommands.ts @@ -7,101 +7,103 @@ import { inject } from "inversify"; // TODO: readd abstract class LabelCommand implements Command { - constructor(protected labelTypeRegistry: LabelTypeRegistry) {} - abstract execute(context: CommandExecutionContext): CommandReturn - abstract undo(context: CommandExecutionContext): CommandReturn - abstract redo(context: CommandExecutionContext): CommandReturn + abstract execute(context: CommandExecutionContext): CommandReturn; + abstract undo(context: CommandExecutionContext): CommandReturn; + abstract redo(context: CommandExecutionContext): CommandReturn; addLabelType() { - return this.labelTypeRegistry.registerLabelType('').id + return this.labelTypeRegistry.registerLabelType("").id; } deleteLabelType(id: string, root: SParentElementImpl) { - this.labelTypeRegistry.unregisterLabelType(id) - this.removeLabelAssignments(root, (a) => a.labelTypeId === id) + this.labelTypeRegistry.unregisterLabelType(id); + this.removeLabelAssignments(root, (a) => a.labelTypeId === id); } addLabelTypeValue(typeId: string) { - return this.labelTypeRegistry.registerLabelTypeValue(typeId, "").id + return this.labelTypeRegistry.registerLabelTypeValue(typeId, "").id; } deleteLabelTypeValue(typeId: string, valueId: string, root: SParentElementImpl) { - this.labelTypeRegistry.unregisterLabelTypeValue(typeId, valueId) - this.removeLabelAssignments(root, (a) => a.labelTypeId === typeId && a.labelTypeValueId === valueId) + this.labelTypeRegistry.unregisterLabelTypeValue(typeId, valueId); + this.removeLabelAssignments(root, (a) => a.labelTypeId === typeId && a.labelTypeValueId === valueId); } removeLabelAssignments(node: SParentElementImpl, filter: (s: LabelAssignment) => boolean) { if (node instanceof DfdNodeImpl) { - node.labels = node.labels.filter(filter) + node.labels = node.labels.filter(filter); } for (const child of node.children) { - this.removeLabelAssignments(child, filter) + this.removeLabelAssignments(child, filter); } } } export namespace AddLabelTypeAction { - export const KIND = 'add-label-type' + export const KIND = "add-label-type"; export function create(): Action { - return { kind: KIND} + return { kind: KIND }; } } export class AddLabelTypeCommand extends LabelCommand { - static readonly KIND = AddLabelTypeAction.KIND - private addedId?: string + static readonly KIND = AddLabelTypeAction.KIND; + private addedId?: string; constructor(@inject(TYPES.Action) _: Action, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry) { - super(labelTypeRegistry) + super(labelTypeRegistry); } execute(context: CommandExecutionContext): CommandReturn { - this.addedId = this.addLabelType() - return context.root + this.addedId = this.addLabelType(); + return context.root; } undo(context: CommandExecutionContext): CommandReturn { - this.deleteLabelType(this.addedId!, context.root) - return context.root + this.deleteLabelType(this.addedId!, context.root); + return context.root; } redo(context: CommandExecutionContext): CommandReturn { - this.addedId = this.addLabelType() - return context.root + this.addedId = this.addLabelType(); + return context.root; } } interface AddLabelTypeValueAction extends Action { - typeId: string + typeId: string; } namespace AddLabelTypeValueAction { - export const KIND = 'add-label-type-value' + export const KIND = "add-label-type-value"; export function create(typeId: string): AddLabelTypeValueAction { return { kind: KIND, - typeId - } + typeId, + }; } } export class AddLabelTypeValueCommand extends LabelCommand { - static readonly KIND = AddLabelTypeValueAction.KIND - private addedId?: string - - constructor(@inject(TYPES.Action) private action: AddLabelTypeValueAction, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry) { - super(labelTypeRegistry) + static readonly KIND = AddLabelTypeValueAction.KIND; + private addedId?: string; + + constructor( + @inject(TYPES.Action) private action: AddLabelTypeValueAction, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + ) { + super(labelTypeRegistry); } execute(context: CommandExecutionContext): CommandReturn { - this.addedId = this.addLabelTypeValue(this.action.typeId) - return context.root + this.addedId = this.addLabelTypeValue(this.action.typeId); + return context.root; } undo(context: CommandExecutionContext): CommandReturn { - this.deleteLabelTypeValue(this.action.typeId, this.addedId!, context.root) - return context.root + this.deleteLabelTypeValue(this.action.typeId, this.addedId!, context.root); + return context.root; } redo(context: CommandExecutionContext): CommandReturn { - this.addedId = this.addLabelTypeValue(this.action.typeId) - return context.root + this.addedId = this.addLabelTypeValue(this.action.typeId); + return context.root; } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/languages/tokenize.ts b/frontend/webEditor/src/languages/tokenize.ts index f8fc9f9e..fe8b16c7 100644 --- a/frontend/webEditor/src/languages/tokenize.ts +++ b/frontend/webEditor/src/languages/tokenize.ts @@ -21,7 +21,7 @@ export function tokenize(text: string[]): Token[] { for (const [lineNumber, line] of text.entries()) { const lineTokens = line.split(/(\s+)/); let column = 0; - for (let i = 0; i < lineTokens.length; i ++) { + for (let i = 0; i < lineTokens.length; i++) { const token = lineTokens[i]; if (!token.match(/\s+/) && token.length > 0) { tokens.push({ @@ -36,11 +36,10 @@ export function tokenize(text: string[]): Token[] { tokens.push({ text: "", line: lineNumber + 1, - column: column + 1 - }) + column: column + 1, + }); } } - return tokens; } diff --git a/frontend/webEditor/src/layout/command.ts b/frontend/webEditor/src/layout/command.ts index d97c1905..36775bfa 100644 --- a/frontend/webEditor/src/layout/command.ts +++ b/frontend/webEditor/src/layout/command.ts @@ -29,17 +29,17 @@ export class LayoutModelCommand extends Command { constructor( @inject(TYPES.Action) private readonly action: LayoutModelAction, @inject(TYPES.IModelLayoutEngine) private readonly layoutEngine: IModelLayoutEngine, - @inject(DfdLayoutConfigurator) private readonly configurator: DfdLayoutConfigurator , - @inject(LoadingIndicator) private readonly loadingIndicator: LoadingIndicator + @inject(DfdLayoutConfigurator) private readonly configurator: DfdLayoutConfigurator, + @inject(LoadingIndicator) private readonly loadingIndicator: LoadingIndicator, ) { super(); } async execute(context: CommandExecutionContext): Promise { this.loadingIndicator.showIndicator("Layouting..."); - this.oldRoot = context.root + this.oldRoot = context.root; - this.configurator.method = this.action.layoutMethod + this.configurator.method = this.action.layoutMethod; // Layouting is normally done on the graph schema. // This is not viable for us because the dfd nodes have a dynamically computed size. // This is only available on loaded classes of the elements, not the json schema. @@ -55,10 +55,10 @@ export class LayoutModelCommand extends Command { } undo(context: CommandExecutionContext): SModelRootImpl { - return this.oldRoot ?? context.root + return this.oldRoot ?? context.root; } redo(context: CommandExecutionContext): SModelRootImpl { - return this.newModel ?? context.root + return this.newModel ?? context.root; } } diff --git a/frontend/webEditor/src/layout/layouter.ts b/frontend/webEditor/src/layout/layouter.ts index 08e31228..735b8ba9 100644 --- a/frontend/webEditor/src/layout/layouter.ts +++ b/frontend/webEditor/src/layout/layouter.ts @@ -14,14 +14,13 @@ import { LayoutMethod } from "./layoutMethod"; import { calculateTextSize } from "../utils/TextSize"; export class DfdLayoutConfigurator extends DefaultLayoutConfigurator { - - private static _method: LayoutMethod = LayoutMethod.LINES + private static _method: LayoutMethod = LayoutMethod.LINES; set method(method: LayoutMethod) { - DfdLayoutConfigurator._method = method + DfdLayoutConfigurator._method = method; } get method() { - return DfdLayoutConfigurator._method + return DfdLayoutConfigurator._method; } protected override graphOptions(): LayoutOptions { diff --git a/frontend/webEditor/src/loadingIndicator/di.config.ts b/frontend/webEditor/src/loadingIndicator/di.config.ts index ac47fd7f..b720040c 100644 --- a/frontend/webEditor/src/loadingIndicator/di.config.ts +++ b/frontend/webEditor/src/loadingIndicator/di.config.ts @@ -4,7 +4,7 @@ import { TYPES } from "sprotty"; import { EDITOR_TYPES } from "../editorTypes"; export const loadingIndicatorModule = new ContainerModule((bind) => { - bind(LoadingIndicator).toSelf().inSingletonScope(); - bind(TYPES.IUIExtension).toService(LoadingIndicator); - bind(EDITOR_TYPES.DefaultUIElement).toService(LoadingIndicator); -}) \ No newline at end of file + bind(LoadingIndicator).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(LoadingIndicator); + bind(EDITOR_TYPES.DefaultUIElement).toService(LoadingIndicator); +}); diff --git a/frontend/webEditor/src/loadingIndicator/loadingIndicator.css b/frontend/webEditor/src/loadingIndicator/loadingIndicator.css index 73da5e65..068ef24c 100644 --- a/frontend/webEditor/src/loadingIndicator/loadingIndicator.css +++ b/frontend/webEditor/src/loadingIndicator/loadingIndicator.css @@ -1,29 +1,29 @@ #loading-indicator-wrapper { - position: fixed; - top: 0; - left: 0; - bottom: 0; - right: 0; - z-index: 9999; - width: 100vw; - height: 100vh; + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 9999; + width: 100vw; + height: 100vh; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 20px; - font-size: xx-large; - font-weight: bold; - color: white; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 20px; + font-size: xx-large; + font-weight: bold; + color: white; - background-color: rgba(0, 0, 0, 0.8); + background-color: rgba(0, 0, 0, 0.8); } #turning-circle { - border: 20px solid white; - border-top: 20px solid #3498db; - border-radius: 9999px; - width: 100px; - height: 100px; - animation: spin 2s linear infinite; -} \ No newline at end of file + border: 20px solid white; + border-top: 20px solid #3498db; + border-radius: 9999px; + width: 100px; + height: 100px; + animation: spin 2s linear infinite; +} diff --git a/frontend/webEditor/src/loadingIndicator/loadingIndicator.ts b/frontend/webEditor/src/loadingIndicator/loadingIndicator.ts index c75c74a1..eb8d3b8d 100644 --- a/frontend/webEditor/src/loadingIndicator/loadingIndicator.ts +++ b/frontend/webEditor/src/loadingIndicator/loadingIndicator.ts @@ -32,7 +32,7 @@ export class LoadingIndicator extends AbstractUIExtension { public showIndicator(text?: string) { this.waitTimeout = setTimeout(() => { if (!this.waitTimeout) { - return + return; } if (this.loadingIndicatorWrapper) { this.loadingIndicatorWrapper.style.display = "flex"; @@ -42,8 +42,7 @@ export class LoadingIndicator extends AbstractUIExtension { this.loadingIndicatorWrapper.focus(); this.waitTimeout = undefined; } - }, 200) - + }, 200); } public hideIndicator() { @@ -55,4 +54,4 @@ export class LoadingIndicator extends AbstractUIExtension { this.loadingIndicatorWrapper.style.display = "none"; } } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/serialize/ModelFactory.ts b/frontend/webEditor/src/serialize/ModelFactory.ts index 06d9ad05..86311e35 100644 --- a/frontend/webEditor/src/serialize/ModelFactory.ts +++ b/frontend/webEditor/src/serialize/ModelFactory.ts @@ -10,16 +10,12 @@ export class DfdModelFactory extends SModelFactory { if (schema instanceof SModelElementImpl) { return super.createElement(schema, parent); } - if ( - schema.type === "node:storage" || - schema.type === "node:function" || - schema.type === "node:input-output" - ) { + if (schema.type === "node:storage" || schema.type === "node:function" || schema.type === "node:input-output") { const dfdSchema = schema as DfdNode; schema.children = schema.children ?? []; for (const port of dfdSchema.ports) { if ("features" in port) { - delete port.features + delete port.features; } } schema.children.push(...dfdSchema.ports, { @@ -30,8 +26,8 @@ export class DfdModelFactory extends SModelFactory { } if (schema.type === "edge:arrow") { - const dfdSchema = schema as ArrowEdge - schema.children = schema.children ?? [] + const dfdSchema = schema as ArrowEdge; + schema.children = schema.children ?? []; schema.children.push({ type: "label:filled-background", text: dfdSchema.text ?? "", @@ -40,54 +36,46 @@ export class DfdModelFactory extends SModelFactory { position: 0.5, side: "on", rotate: false, - } - } as SLabel) + }, + } as SLabel); } return super.createElement(schema, parent); - } override createSchema(element: SModelElementImpl): SModelElement { const schema = super.createSchema(element); - if ( - schema.type === "node:storage" || - schema.type === "node:function" || - schema.type === "node:input-output" - ) { + if (schema.type === "node:storage" || schema.type === "node:function" || schema.type === "node:input-output") { const dfdSchema = schema as DfdNode; - const ports = dfdSchema.children?.filter( - (child) => - getBasicType(child) === 'port' - ) ?? []; - dfdSchema.ports = ports + const ports = dfdSchema.children?.filter((child) => getBasicType(child) === "port") ?? []; + dfdSchema.ports = ports; - const labelValue = schema.children?.find( - (child) => child.type === "label:positional" - ) as SLabel | undefined; + const labelValue = schema.children?.find((child) => child.type === "label:positional") as + | SLabel + | undefined; if (labelValue) { dfdSchema.text = labelValue.text; - } + } - dfdSchema.children = [] - return dfdSchema + dfdSchema.children = []; + return dfdSchema; } if (schema.type === "edge:arrow") { - const dfdSchema = schema as ArrowEdge + const dfdSchema = schema as ArrowEdge; - const labelValue = schema.children?.find( - (child) => child.type === "label:filled-background" - ) as SLabel | undefined; + const labelValue = schema.children?.find((child) => child.type === "label:filled-background") as + | SLabel + | undefined; if (labelValue) { dfdSchema.text = labelValue.text; - } + } - dfdSchema.children = [] - return dfdSchema + dfdSchema.children = []; + return dfdSchema; } return schema; diff --git a/frontend/webEditor/src/serialize/SavedDiagram.ts b/frontend/webEditor/src/serialize/SavedDiagram.ts index 48ddb801..623b8752 100644 --- a/frontend/webEditor/src/serialize/SavedDiagram.ts +++ b/frontend/webEditor/src/serialize/SavedDiagram.ts @@ -10,4 +10,4 @@ export interface SavedDiagram { mode?: EditorMode; version: number; } -export const CURRENT_VERSION = 1; \ No newline at end of file +export const CURRENT_VERSION = 1; diff --git a/frontend/webEditor/src/serialize/analyze.ts b/frontend/webEditor/src/serialize/analyze.ts index 0deb0c2b..6b949abe 100644 --- a/frontend/webEditor/src/serialize/analyze.ts +++ b/frontend/webEditor/src/serialize/analyze.ts @@ -30,20 +30,27 @@ export class AnalyzeCommand extends LoadJsonCommand { @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, - @inject(LoadingIndicator) loadingIndicator: LoadingIndicator + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, ) { - super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName, loadingIndicator); + super( + logger, + labelTypeRegistry, + constraintRegistry, + editorModeController, + actionDispatcher, + fileName, + loadingIndicator, + ); } protected async getFile(context: CommandExecutionContext): Promise | undefined> { const savedDiagram = { - model: context.modelFactory.createSchema(context.root), - labelTypes: this.labelTypeRegistry.getLabelTypes(), - constraints: this.constraintRegistry.getConstraintList(), - mode: this.editorModeController.get(), - version: CURRENT_VERSION - } - return await this.dfdWebSocket.requestDiagram("Json:" + JSON.stringify(savedDiagram)) + model: context.modelFactory.createSchema(context.root), + labelTypes: this.labelTypeRegistry.getLabelTypes(), + constraints: this.constraintRegistry.getConstraintList(), + mode: this.editorModeController.get(), + version: CURRENT_VERSION, + }; + return await this.dfdWebSocket.requestDiagram("Json:" + JSON.stringify(savedDiagram)); } - -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/serialize/di.config.ts b/frontend/webEditor/src/serialize/di.config.ts index b9361e4d..2e34a49c 100644 --- a/frontend/webEditor/src/serialize/di.config.ts +++ b/frontend/webEditor/src/serialize/di.config.ts @@ -15,9 +15,9 @@ export const serializeModule = new ContainerModule((bind, unbind, isBound, rebin configureCommand(context, LoadJsonFileCommand); configureCommand(context, LoadDfdAndDdFileCommand); configureCommand(context, LoadPalladioFileCommand); - configureCommand(context, SaveJsonFileCommand) - configureCommand(context, SaveDfdAndDdFileCommand) - configureCommand(context, AnalyzeCommand) - + configureCommand(context, SaveJsonFileCommand); + configureCommand(context, SaveDfdAndDdFileCommand); + configureCommand(context, AnalyzeCommand); + rebind(TYPES.IModelFactory).to(DfdModelFactory); -}) \ No newline at end of file +}); diff --git a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts index ce4bbc0e..3e5c889b 100644 --- a/frontend/webEditor/src/serialize/loadDefaultDiagram.ts +++ b/frontend/webEditor/src/serialize/loadDefaultDiagram.ts @@ -1,5 +1,5 @@ import { FileData, LoadJsonCommand } from "./loadJson"; -import defaultDiagram from './defaultDiagram.json' +import defaultDiagram from "./defaultDiagram.json"; import { SavedDiagram } from "./SavedDiagram"; import { Action } from "sprotty-protocol"; import { inject } from "inversify"; @@ -30,15 +30,23 @@ export class LoadDefaultDiagramCommand extends LoadJsonCommand { @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(FileName) fileName: FileName, - @inject(LoadingIndicator) loadingIndicator: LoadingIndicator + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, ) { - super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName, loadingIndicator); + super( + logger, + labelTypeRegistry, + constraintRegistry, + editorModeController, + actionDispatcher, + fileName, + loadingIndicator, + ); } protected async getFile(): Promise | undefined> { return { fileName: "diagram.json", - content: defaultDiagram as SavedDiagram - } + content: defaultDiagram as SavedDiagram, + }; } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts index 23a3233e..e1831bb2 100644 --- a/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/loadDfdAndDdFile.ts @@ -27,14 +27,22 @@ export class LoadDfdAndDdFileCommand extends LoadJsonCommand { @inject(TYPES.Action) _: Action, @inject(TYPES.ILogger) logger: ILogger, @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, - @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, @inject(SETTINGS.Mode) editorModeController: EditorModeController, @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, - @inject(LoadingIndicator) loadingIndicator: LoadingIndicator + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, ) { - super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName, loadingIndicator); + super( + logger, + labelTypeRegistry, + constraintRegistry, + editorModeController, + actionDispatcher, + fileName, + loadingIndicator, + ); } protected async getFile(): Promise | undefined> { @@ -48,9 +56,11 @@ export class LoadDfdAndDdFileCommand extends LoadJsonCommand { const oldFileName = this.fileName.getName(); this.fileName.setName(files[0].fileName); - return this.dfdWebSocket.requestDiagram("DFD:" + dataflowFileContent + "\n:DD:\n" + dictionaryFileContent).catch((e) => { - this.fileName.setName(oldFileName); - throw e; - }) + return this.dfdWebSocket + .requestDiagram("DFD:" + dataflowFileContent + "\n:DD:\n" + dictionaryFileContent) + .catch((e) => { + this.fileName.setName(oldFileName); + throw e; + }); } } diff --git a/frontend/webEditor/src/serialize/loadJson.ts b/frontend/webEditor/src/serialize/loadJson.ts index e5a0860f..70e37c9f 100644 --- a/frontend/webEditor/src/serialize/loadJson.ts +++ b/frontend/webEditor/src/serialize/loadJson.ts @@ -1,4 +1,12 @@ -import { ActionDispatcher, Command, CommandExecutionContext, CommandReturn, EMPTY_ROOT, ILogger, SModelRootImpl } from "sprotty"; +import { + ActionDispatcher, + Command, + CommandExecutionContext, + CommandReturn, + EMPTY_ROOT, + ILogger, + SModelRootImpl, +} from "sprotty"; import { SavedDiagram } from "./SavedDiagram"; import { Action, SModelElement, SModelRoot } from "sprotty-protocol"; import { LabelTypeRegistry } from "../labels/LabelTypeRegistry"; @@ -39,7 +47,7 @@ export abstract class LoadJsonCommand extends Command { protected editorModeController: EditorModeController, private actionDispatcher: ActionDispatcher, protected fileName: FileName, - private loadingIndicator: LoadingIndicator + private loadingIndicator: LoadingIndicator, ) { super(); } @@ -52,7 +60,7 @@ export abstract class LoadJsonCommand extends Command { this.file = await this.getFile(context).catch(() => undefined); if (!this.file) { - this.loadingIndicator.hide() + this.loadingIndicator.hide(); return context.root; } @@ -81,26 +89,26 @@ export abstract class LoadJsonCommand extends Command { } this.logger.info(this, "Editor mode loaded successfully"); - this.oldConstrains = this.constraintRegistry.getConstraintList() - const newConstraints = this.file.content.constraints + this.oldConstrains = this.constraintRegistry.getConstraintList(); + const newConstraints = this.file.content.constraints; if (newConstraints) { - this.constraintRegistry.setConstraintsFromArray(newConstraints) + this.constraintRegistry.setConstraintsFromArray(newConstraints); } else { - this.constraintRegistry.clearConstraints() + this.constraintRegistry.clearConstraints(); } // TODO: post load actions like layout - this.actionDispatcher.dispatch(DefaultFitToScreenAction.create(this.newRoot)) + this.actionDispatcher.dispatch(DefaultFitToScreenAction.create(this.newRoot)); this.oldFileName = this.fileName.getName(); this.fileName.setName(this.file.fileName); - this.loadingIndicator.hide() + this.loadingIndicator.hide(); return this.newRoot; } catch (error) { this.logger.error(this, "Error loading model", error); this.newRoot = this.oldRoot; - this.loadingIndicator.hide() + this.loadingIndicator.hide(); return this.oldRoot; } } @@ -124,12 +132,12 @@ export abstract class LoadJsonCommand extends Command { } if (this.oldConstrains) { - this.constraintRegistry.setConstraintsFromArray(this.oldConstrains) + this.constraintRegistry.setConstraintsFromArray(this.oldConstrains); } - this.fileName.setName(this.oldFileName ?? 'diagram'); + this.fileName.setName(this.oldFileName ?? "diagram"); - this.loadingIndicator.hide() + this.loadingIndicator.hide(); return this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); } @@ -152,16 +160,16 @@ export abstract class LoadJsonCommand extends Command { } this.logger.info(this, "Editor mode loaded successfully"); - const newConstraints = this.file?.content.constraints + const newConstraints = this.file?.content.constraints; if (newConstraints) { - this.constraintRegistry.setConstraintsFromArray(newConstraints) + this.constraintRegistry.setConstraintsFromArray(newConstraints); } else { - this.constraintRegistry.clearConstraints() + this.constraintRegistry.clearConstraints(); } - this.fileName.setName(this.file?.fileName ?? 'diagram'); + this.fileName.setName(this.file?.fileName ?? "diagram"); - this.loadingIndicator.hide() + this.loadingIndicator.hide(); return this.newRoot ?? this.oldRoot ?? context.modelFactory.createRoot(EMPTY_ROOT); } diff --git a/frontend/webEditor/src/serialize/loadJsonFile.ts b/frontend/webEditor/src/serialize/loadJsonFile.ts index ea1ad532..438b24cd 100644 --- a/frontend/webEditor/src/serialize/loadJsonFile.ts +++ b/frontend/webEditor/src/serialize/loadJsonFile.ts @@ -19,35 +19,41 @@ export namespace LoadJsonFileAction { } } - export class LoadJsonFileCommand extends LoadJsonCommand { - static readonly KIND = LoadJsonFileAction.KIND; - - constructor( - @inject(TYPES.Action) _: Action, - @inject(TYPES.ILogger) logger: ILogger, - @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, - @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, - @inject(SETTINGS.Mode) editorModeController: EditorModeController, - @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, - @inject(FileName) fileName: FileName, - @inject(LoadingIndicator) loadingIndicator: LoadingIndicator -) { - super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName, loadingIndicator); -} - - protected async getFile(): Promise | undefined> { - const file = await chooseFile(["application/json"]) - if (!file) { - return undefined + static readonly KIND = LoadJsonFileAction.KIND; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(TYPES.ILogger) logger: ILogger, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, + @inject(FileName) fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super( + logger, + labelTypeRegistry, + constraintRegistry, + editorModeController, + actionDispatcher, + fileName, + loadingIndicator, + ); } - this.fileName.setName(file.fileName) + protected async getFile(): Promise | undefined> { + const file = await chooseFile(["application/json"]); + if (!file) { + return undefined; + } - return { - fileName: file.fileName, - content: JSON.parse(file.content) as SavedDiagram - } - } + this.fileName.setName(file.fileName); -} \ No newline at end of file + return { + fileName: file.fileName, + content: JSON.parse(file.content) as SavedDiagram, + }; + } +} diff --git a/frontend/webEditor/src/serialize/loadPalladioFile.ts b/frontend/webEditor/src/serialize/loadPalladioFile.ts index 4acca8c3..d9576cc9 100644 --- a/frontend/webEditor/src/serialize/loadPalladioFile.ts +++ b/frontend/webEditor/src/serialize/loadPalladioFile.ts @@ -22,7 +22,15 @@ export namespace LoadPalladioFileAction { export class LoadPalladioFileCommand extends LoadJsonCommand { static readonly KIND = LoadPalladioFileAction.KIND; - private static readonly FILE_ENDINGS = [".pddc", ".allocation", ".nodecharacteristics", ".repository", ".resourceenvironment", ".system", ".usagemodel"]; + private static readonly FILE_ENDINGS = [ + ".pddc", + ".allocation", + ".nodecharacteristics", + ".repository", + ".resourceenvironment", + ".system", + ".usagemodel", + ]; constructor( @inject(TYPES.Action) _: Action, @@ -33,27 +41,40 @@ export class LoadPalladioFileCommand extends LoadJsonCommand { @inject(TYPES.IActionDispatcher) actionDispatcher: ActionDispatcher, @inject(FileName) fileName: FileName, @inject(DfdWebSocket) private dfdWebSocket: DfdWebSocket, - @inject(LoadingIndicator) loadingIndicator: LoadingIndicator + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, ) { - super(logger, labelTypeRegistry, constraintRegistry, editorModeController, actionDispatcher, fileName, loadingIndicator); + super( + logger, + labelTypeRegistry, + constraintRegistry, + editorModeController, + actionDispatcher, + fileName, + loadingIndicator, + ); } protected async getFile(): Promise | undefined> { - const files = await chooseFiles(LoadPalladioFileCommand.FILE_ENDINGS, LoadPalladioFileCommand.FILE_ENDINGS.length); - + const files = await chooseFiles( + LoadPalladioFileCommand.FILE_ENDINGS, + LoadPalladioFileCommand.FILE_ENDINGS.length, + ); + if ( - LoadPalladioFileCommand.FILE_ENDINGS.some(ending => - !files.find(file => file.fileName.endsWith(ending)) - ) + LoadPalladioFileCommand.FILE_ENDINGS.some((ending) => !files.find((file) => file.fileName.endsWith(ending))) ) { - throw new Error("Please select one file of each required type: .pddc, .allocation, .nodecharacteristics, .repository, .resourceenvironment, .system, .usagemodel"); + throw new Error( + "Please select one file of each required type: .pddc, .allocation, .nodecharacteristics, .repository, .resourceenvironment, .system, .usagemodel", + ); } const oldFileName = this.fileName.getName(); - this.fileName.setName(files[0].fileName) + this.fileName.setName(files[0].fileName); - return this.dfdWebSocket.requestDiagram(files.map((f) => `${f.fileName}:${f.content}`).join("---FILE---")).catch((e) => { - this.fileName.setName(oldFileName); - throw e; - }); + return this.dfdWebSocket + .requestDiagram(files.map((f) => `${f.fileName}:${f.content}`).join("---FILE---")) + .catch((e) => { + this.fileName.setName(oldFileName); + throw e; + }); } } diff --git a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts index 3b1bd5bc..67ec2a09 100644 --- a/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts +++ b/frontend/webEditor/src/serialize/saveDfdAndDdFile.ts @@ -12,45 +12,47 @@ import { ConstraintRegistry } from "../constraint/constraintRegistry"; import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export namespace SaveDfdAndDdFileAction { - export const KIND = 'saveDfdAndDdFile' - export function create(): Action { - return { kind: KIND } - } + export const KIND = "saveDfdAndDdFile"; + export function create(): Action { + return { kind: KIND }; + } } export class SaveDfdAndDdFileCommand extends SaveFileCommand { - - static readonly KIND = SaveDfdAndDdFileAction.KIND; - private static readonly CLOSING_TAG = ""; - - constructor( - @inject(TYPES.Action) _: Action, - @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, - @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, - @inject(SETTINGS.Mode) editorModeController: EditorModeController, - @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, - @inject(FileName) private readonly fileName: FileName, - @inject(LoadingIndicator) loadingIndicator: LoadingIndicator - ) { - super(labelTypeRegistry, constraintRegistry, editorModeController, loadingIndicator); - } - - async getFiles(context: CommandExecutionContext): Promise[]> { - const savedDiagram = this.createSavedDiagram(context); - - const response = await this.dfdWebSocket.sendMessage("Json2DFD:" + JSON.stringify(savedDiagram)); - const endIndex = response.indexOf(SaveDfdAndDdFileCommand.CLOSING_TAG) + SaveDfdAndDdFileCommand.CLOSING_TAG.length; - const dfdContent = response.substring(0, endIndex).trim(); - const ddContent = response.substring(endIndex).trim(); - - const fileName = this.fileName.getName(); - return Promise.resolve([{ - fileName: fileName + ".dataflowdiagram", - content: dfdContent - }, { - fileName: fileName + ".datadictionary", - content: ddContent - }]); - } - -} \ No newline at end of file + static readonly KIND = SaveDfdAndDdFileAction.KIND; + private static readonly CLOSING_TAG = ""; + + constructor( + @inject(TYPES.Action) _: Action, + @inject(LabelTypeRegistry) labelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(DfdWebSocket) private readonly dfdWebSocket: DfdWebSocket, + @inject(FileName) private readonly fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super(labelTypeRegistry, constraintRegistry, editorModeController, loadingIndicator); + } + + async getFiles(context: CommandExecutionContext): Promise[]> { + const savedDiagram = this.createSavedDiagram(context); + + const response = await this.dfdWebSocket.sendMessage("Json2DFD:" + JSON.stringify(savedDiagram)); + const endIndex = + response.indexOf(SaveDfdAndDdFileCommand.CLOSING_TAG) + SaveDfdAndDdFileCommand.CLOSING_TAG.length; + const dfdContent = response.substring(0, endIndex).trim(); + const ddContent = response.substring(endIndex).trim(); + + const fileName = this.fileName.getName(); + return Promise.resolve([ + { + fileName: fileName + ".dataflowdiagram", + content: dfdContent, + }, + { + fileName: fileName + ".datadictionary", + content: ddContent, + }, + ]); + } +} diff --git a/frontend/webEditor/src/serialize/saveJsonFile.ts b/frontend/webEditor/src/serialize/saveJsonFile.ts index 4cba3db7..c71bd7be 100644 --- a/frontend/webEditor/src/serialize/saveJsonFile.ts +++ b/frontend/webEditor/src/serialize/saveJsonFile.ts @@ -11,32 +11,31 @@ import { ConstraintRegistry } from "../constraint/constraintRegistry"; import { LoadingIndicator } from "../loadingIndicator/loadingIndicator"; export namespace SaveJsonFileAction { - export const KIND = 'saveJsonFile' - export function create(): Action { - return { kind: KIND } - } + export const KIND = "saveJsonFile"; + export function create(): Action { + return { kind: KIND }; + } } export class SaveJsonFileCommand extends SaveFileCommand { - static readonly KIND = SaveJsonFileAction.KIND; + static readonly KIND = SaveJsonFileAction.KIND; - constructor( - @inject(TYPES.Action) _: Action, - @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, - @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, - @inject(SETTINGS.Mode) editorModeController: EditorModeController, - @inject(FileName) private readonly fileName: FileName, - @inject(LoadingIndicator) loadingIndicator: LoadingIndicator - ) { - super(LabelTypeRegistry, constraintRegistry, editorModeController, loadingIndicator); - } + constructor( + @inject(TYPES.Action) _: Action, + @inject(LabelTypeRegistry) LabelTypeRegistry: LabelTypeRegistry, + @inject(ConstraintRegistry) constraintRegistry: ConstraintRegistry, + @inject(SETTINGS.Mode) editorModeController: EditorModeController, + @inject(FileName) private readonly fileName: FileName, + @inject(LoadingIndicator) loadingIndicator: LoadingIndicator, + ) { + super(LabelTypeRegistry, constraintRegistry, editorModeController, loadingIndicator); + } - getFiles(context: CommandExecutionContext): Promise[]> { - const fileData: FileData = { - fileName: this.fileName.getName() + ".json", - content: JSON.stringify(this.createSavedDiagram(context)) - }; - return Promise.resolve([fileData]); - } - -} \ No newline at end of file + getFiles(context: CommandExecutionContext): Promise[]> { + const fileData: FileData = { + fileName: this.fileName.getName() + ".json", + content: JSON.stringify(this.createSavedDiagram(context)), + }; + return Promise.resolve([fileData]); + } +} diff --git a/frontend/webEditor/src/serialize/savedDiagramCreator.ts b/frontend/webEditor/src/serialize/savedDiagramCreator.ts index 713a4002..c4823075 100644 --- a/frontend/webEditor/src/serialize/savedDiagramCreator.ts +++ b/frontend/webEditor/src/serialize/savedDiagramCreator.ts @@ -5,25 +5,23 @@ import { EditorModeController } from "../settings/editorMode"; import { ConstraintRegistry } from "../constraint/constraintRegistry"; export abstract class SavedDiagramCreatorCommand extends Command { + constructor( + private readonly labelTypeRegistry: LabelTypeRegistry, + private readonly constraintRegistry: ConstraintRegistry, + private readonly editorModeController: EditorModeController, + ) { + super(); + } - constructor( - private readonly labelTypeRegistry: LabelTypeRegistry, - private readonly constraintRegistry: ConstraintRegistry, - private readonly editorModeController: EditorModeController - ) { - super() - } - - protected createSavedDiagram(context: CommandExecutionContext): SavedDiagram { - const schema = context.modelFactory.createSchema(context.root); + protected createSavedDiagram(context: CommandExecutionContext): SavedDiagram { + const schema = context.modelFactory.createSchema(context.root); - return { - model: schema, - labelTypes: this.labelTypeRegistry.getLabelTypes(), - constraints: this.constraintRegistry.getConstraintList(), - mode: this.editorModeController.get(), - version: CURRENT_VERSION + return { + model: schema, + labelTypes: this.labelTypeRegistry.getLabelTypes(), + constraints: this.constraintRegistry.getConstraintList(), + mode: this.editorModeController.get(), + version: CURRENT_VERSION, + }; } - } - -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/settings/Settings.ts b/frontend/webEditor/src/settings/Settings.ts index 1decf1c3..2bba96f6 100644 --- a/frontend/webEditor/src/settings/Settings.ts +++ b/frontend/webEditor/src/settings/Settings.ts @@ -1,12 +1,12 @@ -import { BoolSettingsValue } from "./SettingsValue" +import { BoolSettingsValue } from "./SettingsValue"; export const SETTINGS = { - Theme: Symbol("Theme"), - Mode: Symbol("EditorMode"), - HideEdgeNames: Symbol("HideEdgeNames"), - SimplifyNodeNames: Symbol("SimplifyNodeNames"), - ShownLabels: Symbol("ShownLabels"), -} + Theme: Symbol("Theme"), + Mode: Symbol("EditorMode"), + HideEdgeNames: Symbol("HideEdgeNames"), + SimplifyNodeNames: Symbol("SimplifyNodeNames"), + ShownLabels: Symbol("ShownLabels"), +}; -export type SimplifyNodeNames = BoolSettingsValue -export type HideEdgeNames = BoolSettingsValue \ No newline at end of file +export type SimplifyNodeNames = BoolSettingsValue; +export type HideEdgeNames = BoolSettingsValue; diff --git a/frontend/webEditor/src/settings/SettingsUi.ts b/frontend/webEditor/src/settings/SettingsUi.ts index 02f0e6e9..70f6194a 100644 --- a/frontend/webEditor/src/settings/SettingsUi.ts +++ b/frontend/webEditor/src/settings/SettingsUi.ts @@ -16,8 +16,9 @@ export class SettingsUI extends AccordionUiExtension { @inject(SETTINGS.ShownLabels) private readonly shownLabels: ShownLabelsValue, @inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, - @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController) { - super('right', 'up') + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, + ) { + super("right", "up"); } id(): string { @@ -29,39 +30,48 @@ export class SettingsUI extends AccordionUiExtension { } protected initializeHidableContent(contentElement: HTMLElement): void { - const grid = document.createElement('div'); - grid.id = 'settings-content' + const grid = document.createElement("div"); + grid.id = "settings-content"; contentElement.appendChild(grid); - this.addDropDown(grid, "Theme", this.themeManager, [Theme.SYSTEM_DEFAULT, Theme.LIGHT, Theme.DARK]) - this.addDropDown(grid, "Shown Labels", this.shownLabels, [ShownLabels.INCOMING, ShownLabels.OUTGOING, ShownLabels.ALL]) + this.addDropDown(grid, "Theme", this.themeManager, [Theme.SYSTEM_DEFAULT, Theme.LIGHT, Theme.DARK]); + this.addDropDown(grid, "Shown Labels", this.shownLabels, [ + ShownLabels.INCOMING, + ShownLabels.OUTGOING, + ShownLabels.ALL, + ]); this.addBooleanSwitch(grid, "Hide Edge Names", this.hideEdgeNames); this.addBooleanSwitch(grid, "Simplify Node Names", this.simplifyNodeNames); - this.addSwitch(grid, "Read Only", this.editorModeController, {true: "view", false: "edit"}); + this.addSwitch(grid, "Read Only", this.editorModeController, { true: "view", false: "edit" }); } protected initializeHeaderContent(headerElement: HTMLElement): void { - headerElement.classList.add('settings-accordion-icon'); - headerElement.innerText = 'Settings' + headerElement.classList.add("settings-accordion-icon"); + headerElement.innerText = "Settings"; } private addBooleanSwitch(container: HTMLElement, title: string, value: SettingsValue): void { - this.addSwitch(container, title, value, {true: true, false: false}); + this.addSwitch(container, title, value, { true: true, false: false }); } - private addSwitch(container: HTMLElement, title: string, value: SettingsValue, map: {'true':T, 'false': T}): void { + private addSwitch( + container: HTMLElement, + title: string, + value: SettingsValue, + map: { true: T; false: T }, + ): void { const inversedMap = { [map.true.toString()]: true, - [map.false.toString()]: false + [map.false.toString()]: false, }; const textLabel = document.createElement("label"); textLabel.textContent = title; - textLabel.htmlFor = `setting-${title.toLowerCase().replace(/\s+/g, '-')}`; + textLabel.htmlFor = `setting-${title.toLowerCase().replace(/\s+/g, "-")}`; const switchLabel = document.createElement("label"); switchLabel.classList.add("switch"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; - checkbox.id = `setting-${title.toLowerCase().replace(/\s+/g, '-')}`; + checkbox.id = `setting-${title.toLowerCase().replace(/\s+/g, "-")}`; checkbox.checked = inversedMap[value.get().toString()]; switchLabel.appendChild(checkbox); const sliderSpan = document.createElement("span"); @@ -72,39 +82,44 @@ export class SettingsUI extends AccordionUiExtension { container.appendChild(switchLabel); switchLabel.addEventListener("change", () => { - value.set(map[checkbox.checked ? 'true' : 'false']); + value.set(map[checkbox.checked ? "true" : "false"]); }); value.registerListener((newValue) => { checkbox.checked = inversedMap[newValue.toString()]; }); } - private addDropDown(container: HTMLElement, title: string, value: SettingsValue, values: T[]) { + private addDropDown( + container: HTMLElement, + title: string, + value: SettingsValue, + values: T[], + ) { const textLabel = document.createElement("label"); textLabel.textContent = title; - textLabel.htmlFor = `setting-${title.toLowerCase().replace(/\s+/g, '-')}`; + textLabel.htmlFor = `setting-${title.toLowerCase().replace(/\s+/g, "-")}`; - const dropDown = document.createElement('select') + const dropDown = document.createElement("select"); for (const v of values) { - const option = document.createElement('option') - option.value = v.toString() - option.innerText = v.toString() - dropDown.appendChild(option) + const option = document.createElement("option"); + option.value = v.toString(); + option.innerText = v.toString(); + dropDown.appendChild(option); } - dropDown.value = value.get().toString() + dropDown.value = value.get().toString(); dropDown.onchange = () => { - const newValue = values.find(v => v.toString() === dropDown.value) + const newValue = values.find((v) => v.toString() === dropDown.value); if (newValue) { - value.set(newValue) + value.set(newValue); } - } + }; - container.appendChild(textLabel) - container.appendChild(dropDown) + container.appendChild(textLabel); + container.appendChild(dropDown); } } interface ToString { - toString: () => string -} \ No newline at end of file + toString: () => string; +} diff --git a/frontend/webEditor/src/settings/SettingsValue.ts b/frontend/webEditor/src/settings/SettingsValue.ts index c3f961c3..7a24289b 100644 --- a/frontend/webEditor/src/settings/SettingsValue.ts +++ b/frontend/webEditor/src/settings/SettingsValue.ts @@ -1,31 +1,30 @@ export class SettingsValue { - private value: T; - private listeners: Array<(newValue: T) => void> = []; - - constructor(initialValue: T) { - this.value = initialValue; - } + private value: T; + private listeners: Array<(newValue: T) => void> = []; - get(): T { - return this.value; - } + constructor(initialValue: T) { + this.value = initialValue; + } - set(newValue: T): void { - const oldValue = this.value; - this.value = newValue; - if (oldValue !== newValue) { - this.listeners.forEach(listener => listener(newValue)); - } - } + get(): T { + return this.value; + } - registerListener(listener: (newValue: T) => void): void { - this.listeners.push(listener); - } + set(newValue: T): void { + const oldValue = this.value; + this.value = newValue; + if (oldValue !== newValue) { + this.listeners.forEach((listener) => listener(newValue)); + } + } + registerListener(listener: (newValue: T) => void): void { + this.listeners.push(listener); + } } export class BoolSettingsValue extends SettingsValue { - constructor(initialValue: boolean = false) { - super(initialValue); - } -} \ No newline at end of file + constructor(initialValue: boolean = false) { + super(initialValue); + } +} diff --git a/frontend/webEditor/src/settings/ShownLabels.ts b/frontend/webEditor/src/settings/ShownLabels.ts index 57235398..3f0f595b 100644 --- a/frontend/webEditor/src/settings/ShownLabels.ts +++ b/frontend/webEditor/src/settings/ShownLabels.ts @@ -3,11 +3,11 @@ import { SettingsValue } from "./SettingsValue"; export enum ShownLabels { INCOMING = "Incoming", OUTGOING = "Outgoing", - ALL = "All" + ALL = "All", } export class ShownLabelsValue extends SettingsValue { constructor() { - super(ShownLabels.ALL) + super(ShownLabels.ALL); } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/settings/Theme.ts b/frontend/webEditor/src/settings/Theme.ts index 4ef7dfde..687c6f0b 100644 --- a/frontend/webEditor/src/settings/Theme.ts +++ b/frontend/webEditor/src/settings/Theme.ts @@ -1,52 +1,52 @@ import { SettingsValue } from "./SettingsValue"; export enum Theme { - LIGHT = "Light", + LIGHT = "Light", DARK = "Dark", SYSTEM_DEFAULT = "System Default", } -export type ApplyableTheme = Theme.LIGHT | Theme.DARK +export type ApplyableTheme = Theme.LIGHT | Theme.DARK; export class ThemeManager extends SettingsValue { - private static SYSTEM_DEFAULT: ApplyableTheme = + private static SYSTEM_DEFAULT: ApplyableTheme = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? Theme.DARK : Theme.LIGHT; public static readonly LOCAL_STORAGE_KEY = "dfdwebeditor:theme"; constructor() { - super((localStorage.getItem(ThemeManager.LOCAL_STORAGE_KEY) ?? ThemeManager.SYSTEM_DEFAULT) as Theme) + super((localStorage.getItem(ThemeManager.LOCAL_STORAGE_KEY) ?? ThemeManager.SYSTEM_DEFAULT) as Theme); } getTheme(): ApplyableTheme { - const value = this.get() - if (value === Theme.SYSTEM_DEFAULT) { - return ThemeManager.SYSTEM_DEFAULT - } - return value + const value = this.get(); + if (value === Theme.SYSTEM_DEFAULT) { + return ThemeManager.SYSTEM_DEFAULT; + } + return value; } } -export const ThemeSwitchable = Symbol('ThemeSwitchable') +export const ThemeSwitchable = Symbol("ThemeSwitchable"); export interface ThemeSwitchable { - switchTheme: (newTheme: ApplyableTheme) => void + switchTheme: (newTheme: ApplyableTheme) => void; } export function registerThemeSwitch(themeManager: ThemeManager, switchables: ThemeSwitchable[]) { - themeManager.registerListener(() => { - setTheme(themeManager, switchables) - }) - setTheme(themeManager, switchables) + themeManager.registerListener(() => { + setTheme(themeManager, switchables); + }); + setTheme(themeManager, switchables); } function setTheme(themeManager: ThemeManager, switchables: ThemeSwitchable[]) { - const rootElement = document.querySelector(":root") as HTMLElement; + const rootElement = document.querySelector(":root") as HTMLElement; const sprottyElement = document.querySelector("#sprotty") as HTMLElement; const value = themeManager.getTheme() === Theme.DARK ? "dark" : "light"; rootElement.setAttribute("data-theme", value); sprottyElement.setAttribute("data-theme", value); - localStorage.setItem(ThemeManager.LOCAL_STORAGE_KEY, themeManager.get()) + localStorage.setItem(ThemeManager.LOCAL_STORAGE_KEY, themeManager.get()); - switchables.forEach(s => s.switchTheme(themeManager.getTheme())) -} \ No newline at end of file + switchables.forEach((s) => s.switchTheme(themeManager.getTheme())); +} diff --git a/frontend/webEditor/src/settings/di.config.ts b/frontend/webEditor/src/settings/di.config.ts index 15d2a94b..a10e86e4 100644 --- a/frontend/webEditor/src/settings/di.config.ts +++ b/frontend/webEditor/src/settings/di.config.ts @@ -11,18 +11,18 @@ import { HideEdgeNamesCommand } from "./hideEdgeNames"; import { NodeNameRegistry, SimplifyNodeNamesCommand } from "./simplifyNodeNames"; export const settingsModule = new ContainerModule((bind, _, isBound) => { - bind(SettingsUI).toSelf().inSingletonScope(); - bind(EDITOR_TYPES.DefaultUIElement).toService(SettingsUI) - bind(TYPES.IUIExtension).toService(SettingsUI); + bind(SettingsUI).toSelf().inSingletonScope(); + bind(EDITOR_TYPES.DefaultUIElement).toService(SettingsUI); + bind(TYPES.IUIExtension).toService(SettingsUI); - bind(SETTINGS.Theme).to(ThemeManager).inSingletonScope() - bind(SETTINGS.HideEdgeNames).to(BoolSettingsValue).inSingletonScope(); - bind(SETTINGS.SimplifyNodeNames).to(BoolSettingsValue).inSingletonScope(); - bind(SETTINGS.Mode).to(EditorModeController).inSingletonScope(); - bind(SETTINGS.ShownLabels).to(ShownLabelsValue).inSingletonScope() + bind(SETTINGS.Theme).to(ThemeManager).inSingletonScope(); + bind(SETTINGS.HideEdgeNames).to(BoolSettingsValue).inSingletonScope(); + bind(SETTINGS.SimplifyNodeNames).to(BoolSettingsValue).inSingletonScope(); + bind(SETTINGS.Mode).to(EditorModeController).inSingletonScope(); + bind(SETTINGS.ShownLabels).to(ShownLabelsValue).inSingletonScope(); - const context = {bind, isBound} - configureCommand(context, HideEdgeNamesCommand) - configureCommand(context, SimplifyNodeNamesCommand) - bind(NodeNameRegistry).toSelf().inSingletonScope(); -}) \ No newline at end of file + const context = { bind, isBound }; + configureCommand(context, HideEdgeNamesCommand); + configureCommand(context, SimplifyNodeNamesCommand); + bind(NodeNameRegistry).toSelf().inSingletonScope(); +}); diff --git a/frontend/webEditor/src/settings/editorMode.ts b/frontend/webEditor/src/settings/editorMode.ts index 10532758..db4a19f0 100644 --- a/frontend/webEditor/src/settings/editorMode.ts +++ b/frontend/webEditor/src/settings/editorMode.ts @@ -2,18 +2,16 @@ import { SettingsValue } from "./SettingsValue"; export type EditorMode = "edit" | "view"; - export class EditorModeController extends SettingsValue { + constructor() { + super("edit"); + } - constructor() { - super("edit"); - } - - setDefault(): void { - this.set("edit"); - } + setDefault(): void { + this.set("edit"); + } - isReadOnly(): boolean { - return this.get() !== "edit"; - } -} \ No newline at end of file + isReadOnly(): boolean { + return this.get() !== "edit"; + } +} diff --git a/frontend/webEditor/src/settings/hideEdgeNames.ts b/frontend/webEditor/src/settings/hideEdgeNames.ts index 6e7eff55..f183d0c7 100644 --- a/frontend/webEditor/src/settings/hideEdgeNames.ts +++ b/frontend/webEditor/src/settings/hideEdgeNames.ts @@ -2,24 +2,24 @@ import { Command, CommandExecutionContext, CommandReturn } from "sprotty"; import { Action } from "sprotty-protocol"; export namespace HideEdgeNamesAction { - export const KIND = 'hide-edge-names' - export function create(): Action { - return { - kind: KIND + export const KIND = "hide-edge-names"; + export function create(): Action { + return { + kind: KIND, + }; } - } } export class HideEdgeNamesCommand extends Command { - static readonly KIND = HideEdgeNamesAction.KIND - - execute(context: CommandExecutionContext): CommandReturn { - return context.root - } - undo(context: CommandExecutionContext): CommandReturn { - return context.root - } - redo(context: CommandExecutionContext): CommandReturn { - return context.root - } -} \ No newline at end of file + static readonly KIND = HideEdgeNamesAction.KIND; + + execute(context: CommandExecutionContext): CommandReturn { + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } +} diff --git a/frontend/webEditor/src/settings/initialize.ts b/frontend/webEditor/src/settings/initialize.ts index 8b7e1f0b..fcead901 100644 --- a/frontend/webEditor/src/settings/initialize.ts +++ b/frontend/webEditor/src/settings/initialize.ts @@ -5,34 +5,34 @@ import { HideEdgeNamesAction } from "./hideEdgeNames"; import { SimplifyNodeNamesAction } from "./simplifyNodeNames"; export function linkReadOnly( - editorModeController: EditorModeController, - simplifyNodeNames: SimplifyNodeNames, - hideEdgeNames: HideEdgeNames + editorModeController: EditorModeController, + simplifyNodeNames: SimplifyNodeNames, + hideEdgeNames: HideEdgeNames, ): void { - editorModeController.registerListener(() => { - if(!editorModeController.isReadOnly()) { - simplifyNodeNames.set(false); - hideEdgeNames.set(false); - } - }); + editorModeController.registerListener(() => { + if (!editorModeController.isReadOnly()) { + simplifyNodeNames.set(false); + hideEdgeNames.set(false); + } + }); - simplifyNodeNames.registerListener((newValue) => { - if(newValue) { - editorModeController.set("view"); - } - }); - hideEdgeNames.registerListener((newValue) => { - if(newValue) { - editorModeController.set("view"); - } - }); + simplifyNodeNames.registerListener((newValue) => { + if (newValue) { + editorModeController.set("view"); + } + }); + hideEdgeNames.registerListener((newValue) => { + if (newValue) { + editorModeController.set("view"); + } + }); } export function addCommands( - actionDispatcher: IActionDispatcher, - simplifyNodeNames: SimplifyNodeNames, - hideEdgeNames: HideEdgeNames + actionDispatcher: IActionDispatcher, + simplifyNodeNames: SimplifyNodeNames, + hideEdgeNames: HideEdgeNames, ) { - hideEdgeNames.registerListener(() => actionDispatcher.dispatch(HideEdgeNamesAction.create())) - simplifyNodeNames.registerListener(() => actionDispatcher.dispatch(SimplifyNodeNamesAction.create())) -} \ No newline at end of file + hideEdgeNames.registerListener(() => actionDispatcher.dispatch(HideEdgeNamesAction.create())); + simplifyNodeNames.registerListener(() => actionDispatcher.dispatch(SimplifyNodeNamesAction.create())); +} diff --git a/frontend/webEditor/src/settings/simplifyNodeNames.ts b/frontend/webEditor/src/settings/simplifyNodeNames.ts index 9ed763a2..d8ce802c 100644 --- a/frontend/webEditor/src/settings/simplifyNodeNames.ts +++ b/frontend/webEditor/src/settings/simplifyNodeNames.ts @@ -6,71 +6,77 @@ import { SETTINGS, SimplifyNodeNames } from "./Settings"; @injectable() export class NodeNameRegistry { - private plainNames: Map - private anonymousNames: Map - private nextNummber = 1 + private plainNames: Map; + private anonymousNames: Map; + private nextNummber = 1; - constructor() { - this.plainNames = new Map() - this.anonymousNames = new Map() - } - - - public setPlainName(node: DfdNodeImpl) { - if (node.editableLabel && this.plainNames.has(node.id)) { - node.editableLabel.text = this.plainNames.get(node.id)! + constructor() { + this.plainNames = new Map(); + this.anonymousNames = new Map(); } - } - public setAnonymousName(node: DfdNodeImpl) { - if (node instanceof DfdNodeImpl && node.editableLabel) { - this.plainNames.set(node.id, node.editableLabel.text) - } - if (!this.anonymousNames.has(node.id)) { - this.anonymousNames.set(node.id, this.nextNummber) - this.nextNummber++ + public setPlainName(node: DfdNodeImpl) { + if (node.editableLabel && this.plainNames.has(node.id)) { + node.editableLabel.text = this.plainNames.get(node.id)!; + } } - if (node.editableLabel) { - node.editableLabel.text = this.anonymousNames.get(node.id)!.toString() + + public setAnonymousName(node: DfdNodeImpl) { + if (node instanceof DfdNodeImpl && node.editableLabel) { + this.plainNames.set(node.id, node.editableLabel.text); + } + if (!this.anonymousNames.has(node.id)) { + this.anonymousNames.set(node.id, this.nextNummber); + this.nextNummber++; + } + if (node.editableLabel) { + node.editableLabel.text = this.anonymousNames.get(node.id)!.toString(); + } } - } } - export namespace SimplifyNodeNamesAction { - export const KIND = 'simplify-node-names' - export function create(): Action { - return { - kind: KIND + export const KIND = "simplify-node-names"; + export function create(): Action { + return { + kind: KIND, + }; } - } } export class SimplifyNodeNamesCommand extends Command { - static readonly KIND = SimplifyNodeNamesAction.KIND + static readonly KIND = SimplifyNodeNamesAction.KIND; - constructor(@inject(TYPES.Action) _: Action, @inject(NodeNameRegistry) private readonly nodeNameRegistry: NodeNameRegistry, @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames) { - super() - } - - execute(context: CommandExecutionContext): CommandReturn { - this.iterate(context.root, (n) => this.simplifyNodeNames.get() ? this.nodeNameRegistry.setAnonymousName(n) : this.nodeNameRegistry.setPlainName(n)) - return context.root - } - undo(context: CommandExecutionContext): CommandReturn { - return context.root - } - redo(context: CommandExecutionContext): CommandReturn { - return context.root - } + constructor( + @inject(TYPES.Action) _: Action, + @inject(NodeNameRegistry) private readonly nodeNameRegistry: NodeNameRegistry, + @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, + ) { + super(); + } - iterate(node: SParentElementImpl, f: (n: DfdNodeImpl) => void) { - if (node instanceof DfdNodeImpl) { - f(node) + execute(context: CommandExecutionContext): CommandReturn { + this.iterate(context.root, (n) => + this.simplifyNodeNames.get() + ? this.nodeNameRegistry.setAnonymousName(n) + : this.nodeNameRegistry.setPlainName(n), + ); + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root; } - for (const child of node.children) { - this.iterate(child, f) + iterate(node: SParentElementImpl, f: (n: DfdNodeImpl) => void) { + if (node instanceof DfdNodeImpl) { + f(node); + } + + for (const child of node.children) { + this.iterate(child, f); + } } - } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts b/frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts index a226cdd0..a2e05c70 100644 --- a/frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts +++ b/frontend/webEditor/src/startUpAgent/LoadDefaultDiagram.ts @@ -7,7 +7,6 @@ export class LoadDefaultDiagramStartUpAgent implements IStartUpAgent { constructor(@inject(TYPES.IActionDispatcher) private actionDispatcher: ActionDispatcher) {} run(): void { - this.actionDispatcher.dispatch(LoadDefaultDiagramAction.create()) + this.actionDispatcher.dispatch(LoadDefaultDiagramAction.create()); } - -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts b/frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts index 54d59a32..07ed36fb 100644 --- a/frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts +++ b/frontend/webEditor/src/startUpAgent/LoadDefaultUiExtensions.ts @@ -16,4 +16,4 @@ export class LoadDefaultUiExtensionsStartUpAgent implements IStartUpAgent { ); this.actionDispatcher.dispatchAll(uiVisibilityActions); } -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/startUpAgent/StartUpAgent.ts b/frontend/webEditor/src/startUpAgent/StartUpAgent.ts index 5127a482..3a77c8ce 100644 --- a/frontend/webEditor/src/startUpAgent/StartUpAgent.ts +++ b/frontend/webEditor/src/startUpAgent/StartUpAgent.ts @@ -2,4 +2,4 @@ export interface IStartUpAgent { run(): void; } -export const StartUpAgent = Symbol('StartUpAgent') \ No newline at end of file +export const StartUpAgent = Symbol("StartUpAgent"); diff --git a/frontend/webEditor/src/startUpAgent/di.config.ts b/frontend/webEditor/src/startUpAgent/di.config.ts index 4b1f6acd..47281130 100644 --- a/frontend/webEditor/src/startUpAgent/di.config.ts +++ b/frontend/webEditor/src/startUpAgent/di.config.ts @@ -6,8 +6,8 @@ import { WebSocketConnectStartUpAgent } from "./webSocketConnect"; import { SettingsInitStartUpAgent } from "./settingsInit"; export const startUpAgentModule = new ContainerModule((bind) => { - bind(StartUpAgent).to(LoadDefaultUiExtensionsStartUpAgent) - bind(StartUpAgent).to(LoadDefaultDiagramStartUpAgent) - bind(StartUpAgent).to(WebSocketConnectStartUpAgent) - bind(StartUpAgent).to(SettingsInitStartUpAgent) -}) \ No newline at end of file + bind(StartUpAgent).to(LoadDefaultUiExtensionsStartUpAgent); + bind(StartUpAgent).to(LoadDefaultDiagramStartUpAgent); + bind(StartUpAgent).to(WebSocketConnectStartUpAgent); + bind(StartUpAgent).to(SettingsInitStartUpAgent); +}); diff --git a/frontend/webEditor/src/startUpAgent/settingsInit.ts b/frontend/webEditor/src/startUpAgent/settingsInit.ts index ef70edc8..7df99711 100644 --- a/frontend/webEditor/src/startUpAgent/settingsInit.ts +++ b/frontend/webEditor/src/startUpAgent/settingsInit.ts @@ -7,14 +7,18 @@ import { registerThemeSwitch, ThemeManager, ThemeSwitchable } from "../settings/ import { ActionDispatcher, TYPES } from "sprotty"; export class SettingsInitStartUpAgent implements IStartUpAgent { - constructor(@inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, @inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, + constructor( + @inject(SETTINGS.Theme) private readonly themeManager: ThemeManager, + @inject(SETTINGS.HideEdgeNames) private readonly hideEdgeNames: HideEdgeNames, @inject(SETTINGS.SimplifyNodeNames) private readonly simplifyNodeNames: SimplifyNodeNames, - @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, @multiInject(ThemeSwitchable) private readonly switchables: ThemeSwitchable[], @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: ActionDispatcher) {} + @inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController, + @multiInject(ThemeSwitchable) private readonly switchables: ThemeSwitchable[], + @inject(TYPES.IActionDispatcher) private readonly actionDispatcher: ActionDispatcher, + ) {} run(): void { - linkReadOnly(this.editorModeController, this.simplifyNodeNames, this.hideEdgeNames); - registerThemeSwitch(this.themeManager, this.switchables) - addCommands(this.actionDispatcher, this.simplifyNodeNames, this.hideEdgeNames) + linkReadOnly(this.editorModeController, this.simplifyNodeNames, this.hideEdgeNames); + registerThemeSwitch(this.themeManager, this.switchables); + addCommands(this.actionDispatcher, this.simplifyNodeNames, this.hideEdgeNames); } - -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/startUpAgent/webSocketConnect.ts b/frontend/webEditor/src/startUpAgent/webSocketConnect.ts index e25b9e0a..1e234f9b 100644 --- a/frontend/webEditor/src/startUpAgent/webSocketConnect.ts +++ b/frontend/webEditor/src/startUpAgent/webSocketConnect.ts @@ -6,7 +6,5 @@ export class WebSocketConnectStartUpAgent implements IStartUpAgent { // eslint-disable-next-line @typescript-eslint/no-unused-vars constructor(@inject(DfdWebSocket) _: DfdWebSocket) {} - run(): void { - } - -} \ No newline at end of file + run(): void {} +} diff --git a/frontend/webEditor/src/toolPalette/nodeCreationTool.ts b/frontend/webEditor/src/toolPalette/nodeCreationTool.ts index f99e429b..afe47537 100644 --- a/frontend/webEditor/src/toolPalette/nodeCreationTool.ts +++ b/frontend/webEditor/src/toolPalette/nodeCreationTool.ts @@ -20,7 +20,7 @@ export class NodeCreationTool extends CreationTool { type: this.elementType, text: defaultTextCapitalized, ports: [], - labels: [] + labels: [], } as SNode; } } diff --git a/frontend/webEditor/src/utils/UiElementFactory.ts b/frontend/webEditor/src/utils/UiElementFactory.ts index 3e4741b8..2572d4fb 100644 --- a/frontend/webEditor/src/utils/UiElementFactory.ts +++ b/frontend/webEditor/src/utils/UiElementFactory.ts @@ -1,28 +1,26 @@ -import './baseUiElements.css' +import "./baseUiElements.css"; export class UiElementFactory { - private constructor() {} public static buildDeleteButton() { - const button = document.createElement('button'); - button.classList.add('delete-button') - const symbol = document.createElement('span'); - symbol.classList.add('codicon', 'codicon-trash') - button.appendChild(symbol) - return button + const button = document.createElement("button"); + button.classList.add("delete-button"); + const symbol = document.createElement("span"); + symbol.classList.add("codicon", "codicon-trash"); + button.appendChild(symbol); + return button; } public static buildAddButton(text: string) { - const button = document.createElement('button'); - button.classList.add('add-button') - const symbol = document.createElement('span'); - symbol.classList.add('codicon', 'codicon-add') - button.appendChild(symbol) - const textHolder = document.createElement('span'); - textHolder.innerText = text - button.appendChild(textHolder) - return button + const button = document.createElement("button"); + button.classList.add("add-button"); + const symbol = document.createElement("span"); + symbol.classList.add("codicon", "codicon-add"); + button.appendChild(symbol); + const textHolder = document.createElement("span"); + textHolder.innerText = text; + button.appendChild(textHolder); + return button; } } - diff --git a/frontend/webEditor/src/utils/baseUiElements.css b/frontend/webEditor/src/utils/baseUiElements.css index 8d1c7e73..c5c01b8d 100644 --- a/frontend/webEditor/src/utils/baseUiElements.css +++ b/frontend/webEditor/src/utils/baseUiElements.css @@ -1,6 +1,7 @@ -button.delete-button, button.add-button { +button.delete-button, +button.add-button { background-color: transparent; border: none; cursor: pointer; padding: 0; -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/utils/idGenerator.ts b/frontend/webEditor/src/utils/idGenerator.ts index 4c89dedd..e982b020 100644 --- a/frontend/webEditor/src/utils/idGenerator.ts +++ b/frontend/webEditor/src/utils/idGenerator.ts @@ -1,3 +1,3 @@ export function generateRandomSprottyId(): string { return Math.random().toString(36).substring(7); -} \ No newline at end of file +} diff --git a/frontend/webEditor/src/vite-env.d.ts b/frontend/webEditor/src/vite-env.d.ts index 357d6d1d..39fc679a 100644 --- a/frontend/webEditor/src/vite-env.d.ts +++ b/frontend/webEditor/src/vite-env.d.ts @@ -1,3 +1,3 @@ /// -declare module '*.css'; \ No newline at end of file +declare module "*.css"; diff --git a/frontend/webEditor/src/webSocket/di.config.ts b/frontend/webEditor/src/webSocket/di.config.ts index 82c9bd07..f1be5b84 100644 --- a/frontend/webEditor/src/webSocket/di.config.ts +++ b/frontend/webEditor/src/webSocket/di.config.ts @@ -2,6 +2,5 @@ import { ContainerModule } from "inversify"; import { DfdWebSocket } from "./webSocket"; export const webSocketModule = new ContainerModule((bind) => { - bind(DfdWebSocket).toSelf().inSingletonScope(); - -}); \ No newline at end of file + bind(DfdWebSocket).toSelf().inSingletonScope(); +}); diff --git a/frontend/webEditor/src/webSocket/webSocket.ts b/frontend/webEditor/src/webSocket/webSocket.ts index 49f14bc2..8811c528 100644 --- a/frontend/webEditor/src/webSocket/webSocket.ts +++ b/frontend/webEditor/src/webSocket/webSocket.ts @@ -4,92 +4,93 @@ import { FileName } from "../fileName/fileName"; @injectable() export class DfdWebSocket { + private webSocket?: WebSocket; + private webSocketId = -1; + private lastRequest: { + resolve?: (v: string) => void; + reject?: (e: Error) => void; + } = {}; + private static readonly WS_URL = "wss://websocket.dataflowanalysis.org/events/"; - private webSocket?: WebSocket - private webSocketId = -1 - private lastRequest: { - resolve?: (v: string) => void - reject?: (e: Error) => void - } = {} - private static readonly WS_URL = "wss://websocket.dataflowanalysis.org/events/" + constructor( + @inject(TYPES.ILogger) private readonly logger: ILogger, + @inject(FileName) private readonly fileName: FileName, + ) { + this.init(); + } - constructor(@inject(TYPES.ILogger) private readonly logger: ILogger, @inject(FileName) private readonly fileName: FileName) { - this.init() - } + private init() { + this.webSocket = new WebSocket(DfdWebSocket.WS_URL); - private init() { - this.webSocket = new WebSocket(DfdWebSocket.WS_URL) + this.webSocket.onopen = () => { + this.logger.log(this, "WebSocket connection established."); + }; - this.webSocket.onopen = () => { - this.logger.log(this, "WebSocket connection established.") - } + this.webSocket.onclose = () => { + this.logger.log(this, "WebSocket connection closed. Reconnecting..."); + this.reject(new Error("WebSocket connection closed")); + this.init(); + }; + this.webSocket.onerror = () => { + this.logger.log(this, "WebSocket error occurred."); + this.reject(new Error("WebSocket error occurred")); + this.init(); + }; - this.webSocket.onclose = () => { - this.logger.log(this, "WebSocket connection closed. Reconnecting...") - this.reject(new Error("WebSocket connection closed")) - this.init() - } - this.webSocket.onerror = () => { - this.logger.log(this, "WebSocket error occurred.") - this.reject(new Error("WebSocket error occurred")) - this.init() - } + this.webSocket.onmessage = (event) => { + const message = event.data as string; + this.logger.log(this, "WebSocket message received: " + message); - this.webSocket.onmessage = (event) => { - const message = event.data as string - this.logger.log(this, "WebSocket message received: " + message) - - if (message.startsWith("Error:")) { - this.reject(new Error(message)) - } + if (message.startsWith("Error:")) { + this.reject(new Error(message)); + } - if (message.startsWith("ID assigned:")) { - const parts = message.split(":") - this.webSocketId = parseInt(parts[1].trim()) - this.logger.log(this, "WebSocket ID assigned: " + this.webSocketId) - return - } + if (message.startsWith("ID assigned:")) { + const parts = message.split(":"); + this.webSocketId = parseInt(parts[1].trim()); + this.logger.log(this, "WebSocket ID assigned: " + this.webSocketId); + return; + } - if (this.lastRequest.resolve) { - this.lastRequest.resolve(message) - this.lastRequest.resolve = undefined - this.lastRequest.reject = undefined - } else { - this.logger.log(this, "No pending request to resolve.") - } + if (this.lastRequest.resolve) { + this.lastRequest.resolve(message); + this.lastRequest.resolve = undefined; + this.lastRequest.reject = undefined; + } else { + this.logger.log(this, "No pending request to resolve."); + } + }; } - } - private reject(error: Error) { - if (this.lastRequest.reject) { - this.lastRequest.reject(error) - this.lastRequest.resolve = undefined - this.lastRequest.reject = undefined + private reject(error: Error) { + if (this.lastRequest.reject) { + this.lastRequest.reject(error); + this.lastRequest.resolve = undefined; + this.lastRequest.reject = undefined; + } } - } - public async requestDiagram(message: string) { - const result = await this.sendMessage(message) - const name = result.split(":")[0] - const diagramMessage = result.replace(name + ":", "") - return { - fileName: name, - content: JSON.parse(diagramMessage) + public async requestDiagram(message: string) { + const result = await this.sendMessage(message); + const name = result.split(":")[0]; + const diagramMessage = result.replace(name + ":", ""); + return { + fileName: name, + content: JSON.parse(diagramMessage), + }; } - } - public sendMessage(message: string): Promise { - const result = new Promise((resolve, reject) => { - this.lastRequest.resolve = resolve - this.lastRequest.reject = reject - }) - if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) { - this.reject(new Error("WebSocket is not connected")) - return result - } - - this.webSocket.send(this.webSocketId + ":" + this.fileName.getName() + ":" + message) - return result - } + public sendMessage(message: string): Promise { + const result = new Promise((resolve, reject) => { + this.lastRequest.resolve = resolve; + this.lastRequest.reject = reject; + }); + if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) { + this.reject(new Error("WebSocket is not connected")); + return result; + } -} \ No newline at end of file + this.webSocket.send(this.webSocketId + ":" + this.fileName.getName() + ":" + message); + return result; + } +} From 30f8c5f2f9f8251d5294545218dd6c6527a3828b Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Tue, 2 Dec 2025 14:02:23 +0100 Subject: [PATCH 38/41] fix build --- frontend/webEditor/.husky/pre-commit | 4 +- frontend/webEditor/package.json | 82 +++++++++---------- .../webEditor/src/commandPalette/di.config.ts | 4 +- .../webEditor/src/constraint/di.config.ts | 2 +- .../webEditor/src/labels/LabelTypeEditorUi.ts | 4 +- 5 files changed, 48 insertions(+), 48 deletions(-) diff --git a/frontend/webEditor/.husky/pre-commit b/frontend/webEditor/.husky/pre-commit index 55cbc310..516f6fc2 100644 --- a/frontend/webEditor/.husky/pre-commit +++ b/frontend/webEditor/.husky/pre-commit @@ -2,8 +2,8 @@ set -e # Only run when WebEditor files are staged (optional guard) -if git diff --cached --name-only --diff-filter=ACMRT | grep -q '^Frontend/WebEditor/'; then +if git diff --cached --name-only --diff-filter=ACMRT | grep -q '^frontend/webEditor/'; then REPO_ROOT="$(git rev-parse --show-toplevel)" - cd "$REPO_ROOT/Frontend/WebEditor" + cd "$REPO_ROOT/frontend/webEditor" npx lint-staged fi \ No newline at end of file diff --git a/frontend/webEditor/package.json b/frontend/webEditor/package.json index 75129e2f..5f09ab6c 100644 --- a/frontend/webEditor/package.json +++ b/frontend/webEditor/package.json @@ -1,43 +1,43 @@ { - "name": "data-flow-analysis-web-editor", - "version": "0.0.0", - "private": true, - "repository": { - "type": "git", - "url": "https://github.com/DataFlowAnalysis/OnlineEditor.git" - }, - "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.35.0", - "@fortawesome/fontawesome-free": "^7.0.0", - "@vscode/codicons": "^0.0.39", - "eslint": "^9.32.0", - "eslint-config-prettier": "^10.1.8", - "husky": "^9.1.7", - "inversify": "^6.2.2", - "lint-staged": "^16.1.6", - "monaco-editor": "^0.52.2", - "prettier": "^3.6.2", - "reflect-metadata": "^0.2.2", - "sprotty": "^1.4.0", - "sprotty-elk": "^1.4.0", - "sprotty-protocol": "^1.4.0", - "typescript": "^5.8.3", - "typescript-eslint": "^8.44.0", - "vite": "^7.1.7" - }, - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "format": "prettier --write \"./**/*.{html,css,ts,tsx,json}\"", - "lint": "eslint --max-warnings 0 --no-warn-ignored", - "prepare": "husky" - }, - "lint-staged": { - "*.{html,css,ts,tsx,json}": [ - "npm run lint", - "npm run format" - ] - } + "name": "data-flow-analysis-web-editor", + "version": "0.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/DataFlowAnalysis/OnlineEditor.git" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.35.0", + "@fortawesome/fontawesome-free": "^7.0.0", + "@vscode/codicons": "^0.0.39", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", + "husky": "^9.1.7", + "inversify": "^6.2.2", + "lint-staged": "^16.1.6", + "monaco-editor": "^0.52.2", + "prettier": "^3.6.2", + "reflect-metadata": "^0.2.2", + "sprotty": "^1.4.0", + "sprotty-elk": "^1.4.0", + "sprotty-protocol": "^1.4.0", + "typescript": "^5.8.3", + "typescript-eslint": "^8.44.0", + "vite": "^7.1.7" + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "format": "prettier --write \"./**/*.{html,css,ts,tsx,json}\"", + "lint": "eslint --max-warnings 0 --no-warn-ignored", + "prepare": "husky" + }, + "lint-staged": { + "*.{html,css,ts,tsx,json}": [ + "npm run lint", + "npm run format" + ] + } } diff --git a/frontend/webEditor/src/commandPalette/di.config.ts b/frontend/webEditor/src/commandPalette/di.config.ts index ba48c8e5..e8f536bd 100644 --- a/frontend/webEditor/src/commandPalette/di.config.ts +++ b/frontend/webEditor/src/commandPalette/di.config.ts @@ -1,7 +1,7 @@ import { ContainerModule } from "inversify"; import { CommandPalette, TYPES } from "sprotty"; -import { WebEditorCommandPalette } from "./CommandPalette"; -import { WebEditorCommandPaletteActionProvider } from "./CommandPaletteProvider"; +import { WebEditorCommandPaletteActionProvider } from "./commandPaletteProvider"; +import { WebEditorCommandPalette } from "./commandPalette"; export const commandPaletteModule = new ContainerModule((bind, _, __, rebind) => { rebind(CommandPalette).to(WebEditorCommandPalette).inSingletonScope(); diff --git a/frontend/webEditor/src/constraint/di.config.ts b/frontend/webEditor/src/constraint/di.config.ts index a79e4235..4f0d664d 100644 --- a/frontend/webEditor/src/constraint/di.config.ts +++ b/frontend/webEditor/src/constraint/di.config.ts @@ -7,7 +7,7 @@ import { ThemeSwitchable } from "../settings/Theme"; import { TFGManager } from "./tfgManager"; import { SelectConstraintsCommand } from "./selection"; -export const constraintModule = new ContainerModule((bind, unbind, isBound) => { +export const constraintModule = new ContainerModule((bind, _, isBound) => { bind(ConstraintRegistry).toSelf().inSingletonScope(); bind(ConstraintMenu).toSelf().inSingletonScope(); diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts index afe51f2a..46970e3f 100644 --- a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -87,7 +87,7 @@ export class LabelTypeEditorUi extends AccordionUiExtension { nameInput.value = labelType.name; nameInput.placeholder = "Label Type Name"; - nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput); + nameInput.oninput = (e: Event) => this.onInputHandler(e as InputEvent, nameInput); dynamicallySetInputSize(nameInput); setTimeout(() => dynamicallySetInputSize(nameInput), 0); nameInput.onchange = () => { @@ -136,7 +136,7 @@ export class LabelTypeEditorUi extends AccordionUiExtension { nameInput.value = value.text; nameInput.placeholder = "Value"; - nameInput.oninput = (e: InputEvent) => this.onInputHandler(e, nameInput); + nameInput.oninput = (e: Event) => this.onInputHandler(e as InputEvent, nameInput); nameInput.style.width = "0px"; setTimeout(() => dynamicallySetInputSize(nameInput), 0); From 1c1db6818f350c0e570f2c5097f3c41a2a114dfc Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 4 Dec 2025 10:22:14 +0100 Subject: [PATCH 39/41] rename labels --- frontend/webEditor/src/assignment/language.ts | 24 ++---- .../src/constraint/constraintRegistry.ts | 1 + frontend/webEditor/src/constraint/language.ts | 16 ++++ .../webEditor/src/labels/LabelTypeEditorUi.ts | 16 ++++ frontend/webEditor/src/labels/di.config.ts | 2 + .../webEditor/src/labels/renameCommand.ts | 74 ++++++++++++++++ frontend/webEditor/src/languages/replace.ts | 84 +++++++++++++++++++ frontend/webEditor/src/languages/tokenize.ts | 1 - frontend/webEditor/src/languages/words.ts | 21 ++++- 9 files changed, 221 insertions(+), 18 deletions(-) create mode 100644 frontend/webEditor/src/labels/renameCommand.ts create mode 100644 frontend/webEditor/src/languages/replace.ts diff --git a/frontend/webEditor/src/assignment/language.ts b/frontend/webEditor/src/assignment/language.ts index 0d89f92a..c623393f 100644 --- a/frontend/webEditor/src/assignment/language.ts +++ b/frontend/webEditor/src/assignment/language.ts @@ -5,6 +5,7 @@ import { LanguageTreeNode } from "../languages/tokenize"; import { ConstantWord, ListWord, Word } from "../languages/words"; import { DfdNodeImpl } from "../diagram/nodes/common"; import { WordCompletion } from "../languages/autocomplete"; +import { ReplacementData } from "../languages/replace"; export const ASSIGNMENT_LANGUAGE_ID = "dfd-assignment-language"; @@ -202,13 +203,13 @@ class LabelWord implements Word { return []; } - /* - replaceWord(text: string, replacement: ReplacementData) { - if (replacement.type == "Label" && text == replacement.old) { + + replace(text: string, replacement: ReplacementData) { + if (replacement.type == "label" && text == replacement.old) { return replacement.replacement; } return text; - }*/ + } } class InputWord extends InputAwareWord implements Word { @@ -227,13 +228,6 @@ class InputWord extends InputAwareWord implements Word { } return [`Unknown input "${word}"`]; } - - /*replaceWord(text: string, replacement: ReplacementData) { - if (replacement.type == "Input" && text == replacement.old) { - return replacement.replacement; - } - return text; - }*/ } class InputLabelWord implements Word { @@ -275,15 +269,13 @@ class InputLabelWord implements Word { return [...inputErrors, ...labelErrors]; } - /*replaceWord(text: string, replacement: ReplacementData) { + replaceWord(text: string, replacement: ReplacementData) { const [input, label] = this.getParts(text); - if (replacement.type == "Input" && input === replacement.old) { - return replacement.replacement + (label ? "." + label : ""); - } else if (replacement.type == "Label" && label === replacement.old) { + if (replacement.type == "label" && label === replacement.old) { return input + "." + replacement.replacement; } return text; - }*/ + } private getParts(text: string): [string, string] | [string, undefined] { if (text.includes(".")) { diff --git a/frontend/webEditor/src/constraint/constraintRegistry.ts b/frontend/webEditor/src/constraint/constraintRegistry.ts index 3b773635..2324c318 100644 --- a/frontend/webEditor/src/constraint/constraintRegistry.ts +++ b/frontend/webEditor/src/constraint/constraintRegistry.ts @@ -13,6 +13,7 @@ export class ConstraintRegistry { public setConstraints(constraints: string[]): void { this.constraints = this.splitIntoConstraintTexts(constraints).map((c) => this.mapToConstraint(c)); + this.constraintListChanged(); } public setConstraintsFromArray(constraints: Constraint[]): void { diff --git a/frontend/webEditor/src/constraint/language.ts b/frontend/webEditor/src/constraint/language.ts index 8746b318..e91785be 100644 --- a/frontend/webEditor/src/constraint/language.ts +++ b/frontend/webEditor/src/constraint/language.ts @@ -6,6 +6,7 @@ import { SModelRoot } from "sprotty-protocol"; import { ArrowEdge } from "../diagram/edges/ArrowEdge"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; import { WordCompletion } from "../languages/autocomplete"; +import { ReplacementData } from "../languages/replace"; export const DSL_LANGUAGE_ID = "dfd-constraint"; @@ -288,6 +289,13 @@ export namespace ConstraintDslTreeBuilder { return []; } + + replace(text: string, replacement: ReplacementData) { + if (replacement.type == "label" && text == replacement.old) { + return replacement.replacement; + } + return text; + } } class NameWord implements Word { @@ -378,5 +386,13 @@ export namespace ConstraintDslTreeBuilder { return []; } + + replace(text: string, replacement: ReplacementData) { + if (!this.characteristicSelectorData.replace) { + return text + } + const parts = text.split(',') + return parts.map(p => this.characteristicSelectorData.replace(p, replacement)).join(',') + } } } diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts index 46970e3f..3c27e028 100644 --- a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -11,6 +11,7 @@ import { AddLabelAssignmentAction } from "./assignmentCommand"; import { IActionDispatcher, TYPES } from "sprotty"; import { SETTINGS } from "../settings/Settings"; import { EditorModeController } from "../settings/editorMode"; +import { ReplaceAction } from "./renameCommand"; export class LabelTypeEditorUi extends AccordionUiExtension { static readonly ID = "label-type-editor-ui"; @@ -91,7 +92,16 @@ export class LabelTypeEditorUi extends AccordionUiExtension { dynamicallySetInputSize(nameInput); setTimeout(() => dynamicallySetInputSize(nameInput), 0); nameInput.onchange = () => { + if (this.editorModeController.isReadOnly()) { + return; + } + const replacements = labelType.values.map(t => ({ + old: `${labelType.name}.${t}`, + replacement: `${nameInput.value}.${t}`, + type: 'label' + })) this.labelTypeRegistry.updateLabelTypeName(labelType.id, nameInput.value); + this.actionDispatcher.dispatch(ReplaceAction.create(replacements)) }; nameInput.onfocus = () => { if (this.editorModeController.isReadOnly()) { @@ -144,7 +154,13 @@ export class LabelTypeEditorUi extends AccordionUiExtension { if (this.editorModeController.isReadOnly()) { return; } + const replacements = [{ + old: `${labelType.name}.${value.text}`, + replacement: `${labelType.name}.${nameInput.value}`, + type: 'label' + }] this.labelTypeRegistry.updateLabelTypeValueText(labelType.id, value.id, nameInput.value); + this.actionDispatcher.dispatch(ReplaceAction.create(replacements)) }; deleteButton.onclick = () => { diff --git a/frontend/webEditor/src/labels/di.config.ts b/frontend/webEditor/src/labels/di.config.ts index 066837e7..2ceda7fc 100644 --- a/frontend/webEditor/src/labels/di.config.ts +++ b/frontend/webEditor/src/labels/di.config.ts @@ -5,6 +5,7 @@ import { configureCommand, TYPES } from "sprotty"; import { EDITOR_TYPES } from "../editorTypes"; import { LabelAssignmentCommand } from "./assignmentCommand"; import { DfdLabelMouseDropListener } from "./dragAndDrop"; +import { ReplaceCommand } from "./renameCommand"; export const labelModule = new ContainerModule((bind, _, isBound) => { bind(LabelTypeRegistry).toSelf().inSingletonScope(); @@ -14,5 +15,6 @@ export const labelModule = new ContainerModule((bind, _, isBound) => { bind(EDITOR_TYPES.DefaultUIElement).to(LabelTypeEditorUi); configureCommand({ bind, isBound }, LabelAssignmentCommand); + configureCommand({ bind, isBound }, ReplaceCommand); bind(TYPES.MouseListener).to(DfdLabelMouseDropListener); }); diff --git a/frontend/webEditor/src/labels/renameCommand.ts b/frontend/webEditor/src/labels/renameCommand.ts new file mode 100644 index 00000000..67d1d092 --- /dev/null +++ b/frontend/webEditor/src/labels/renameCommand.ts @@ -0,0 +1,74 @@ +import { Command, CommandExecutionContext, CommandReturn, LocalModelSource, SParentElementImpl, TYPES } from "sprotty"; +import { Action } from "sprotty-protocol"; +import { ConstraintRegistry } from "../constraint/constraintRegistry"; +import { inject } from "inversify"; +import { replace, ReplacementData } from "../languages/replace"; +import { DfdOutputPortImpl } from "../diagram/ports/DfdOutputPort"; +import { LabelTypeRegistry } from "./LabelTypeRegistry"; +import { AssignmentLanguageTreeBuilder } from "../assignment/language"; +import { ConstraintDslTreeBuilder } from "../constraint/language"; + +interface ReplaceAction extends Action { + replacements: ReplacementData[]; +} + +export namespace ReplaceAction { + export const KIND = "replace-action"; + export function create(replacements: ReplacementData[]) { + return { + kind: KIND, + replacements, + }; + } +} + +export class ReplaceCommand extends Command { + static readonly KIND = ReplaceAction.KIND; + + constructor( + @inject(TYPES.Action) private readonly action: ReplaceAction, + @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, + @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, + @inject(TYPES.ModelSource) private readonly localModelSource: LocalModelSource + ) { + super(); + } + + execute(context: CommandExecutionContext): CommandReturn { + this.iterateForPorts(context.root); + for (const replacement of this.action.replacements) { + this.constraintRegistry.setConstraints( + replace( + this.constraintRegistry.getConstraintsAsText().split('\n'), + ConstraintDslTreeBuilder.buildTree(this.localModelSource, this.labelTypeRegistry), + replacement, + ), + ); + } + return context.root; + } + undo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + redo(context: CommandExecutionContext): CommandReturn { + return context.root; + } + + iterateForPorts(element: SParentElementImpl) { + if (element instanceof DfdOutputPortImpl) { + for (const replacement of this.action.replacements) { + element.setBehavior( + replace( + element.getBehavior().split("\n"), + AssignmentLanguageTreeBuilder.buildTree(element, this.labelTypeRegistry), + replacement, + ).join("\n"), + ); + } + } + + for (const child of element.children) { + this.iterateForPorts(child); + } + } +} diff --git a/frontend/webEditor/src/languages/replace.ts b/frontend/webEditor/src/languages/replace.ts new file mode 100644 index 00000000..95d38c88 --- /dev/null +++ b/frontend/webEditor/src/languages/replace.ts @@ -0,0 +1,84 @@ +import { LanguageTreeNode, Token, tokenize } from "./tokenize"; +import { VerifyWord } from "./verify"; + +export interface ReplaceableWord { + replace?: (text: string, replacement: ReplacementData) => string; +} + +export interface ReplacementData { + old: string; + replacement: string; + type: string; +} + +export interface ReplacedToken extends Token { + newText: string; +} + +export function replace( + lines: string[], + tree: LanguageTreeNode[], + replacement: ReplacementData, +): string[] { + const tokens = tokenize(lines); + const replaced = replaceTokens(tokens, tree, tree, 0, replacement); + for (let i = 0; i < tokens.length; i++) { + replaceToken(i) + } + return lines + + function replaceToken(index: number) { + const token = replaced[index]; + const lengthDiff = token.newText.length - token.text.length + const lineIndex = token.line - 1; + const line = lines[lineIndex]; + const before = line.substring(0, token.column - 1); + const after = line.substring(token.column - 1 + token.text.length); + lines[lineIndex] = before + token.newText + after; + let i = index + 1; + // adjust the column of all following tokens on the same line + while (i < tokens.length && tokens[i].line == token.line) { + replaced[i].column += lengthDiff; + i++; + } + } +} + +function replaceTokens( + tokens: Token[], + tree: LanguageTreeNode[], + roots: LanguageTreeNode[], + index: number, + replacement: ReplacementData, + skipStartCheck = false, +): ReplacedToken[] { + if (index >= tokens.length) { + return []; + } + // check for new start + if (!skipStartCheck && tokens[index].column == 1) { + const matchesAnyRoot = roots.some((n) => n.word.verify(tokens[index].text).length === 0); + if (matchesAnyRoot) { + return replaceTokens(tokens, roots, roots, index, replacement, true); + } + } + let newText = tokens[index].text; + for (const n of tree) { + if (n.word.replace) { + newText = n.word.replace(newText, replacement); + } + } + return [ + { + ...tokens[index], + newText, + }, + ...replaceTokens( + tokens, + tree.flatMap((n) => n.children), + roots, + index + 1, + replacement, + ), + ]; +} diff --git a/frontend/webEditor/src/languages/tokenize.ts b/frontend/webEditor/src/languages/tokenize.ts index fe8b16c7..8669c4bb 100644 --- a/frontend/webEditor/src/languages/tokenize.ts +++ b/frontend/webEditor/src/languages/tokenize.ts @@ -2,7 +2,6 @@ export interface Token { text: string; line: number; column: number; - whiteSpaceAfter?: string; } export interface LanguageTreeNode { diff --git a/frontend/webEditor/src/languages/words.ts b/frontend/webEditor/src/languages/words.ts index 2de55da3..61eded2e 100644 --- a/frontend/webEditor/src/languages/words.ts +++ b/frontend/webEditor/src/languages/words.ts @@ -1,8 +1,9 @@ import { CompletionWord, WordCompletion } from "./autocomplete"; +import { ReplaceableWord, ReplacementData } from "./replace"; import { VerifyWord } from "./verify"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; -export type Word = VerifyWord & CompletionWord; +export type Word = VerifyWord & CompletionWord & ReplaceableWord; export class ConstantWord implements Word { constructor(private readonly word: string) {} @@ -57,6 +58,16 @@ export class NegatableWord implements Word { } return this.word.completionOptions(part); } + + replace(text: string, replacement: ReplacementData): string { + if (!this.word.replace) { + return text + } + if (text.startsWith('!')) { + return this.replace(text.substring(1), replacement) + } + return this.word.replace(text, replacement) + } } export class ListWord implements Word { @@ -78,4 +89,12 @@ export class ListWord implements Word { return this.word.completionOptions(last); } + + replace(text: string, replacement: ReplacementData): string { + if (!this.word.replace) { + return text + } + const parts = text.split(',') + return parts.map(p => this.word.replace!(p, replacement)).join(',') + } } From f24deb0f9c535ee8aeae4d55d452bfdd5adcda1e Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 4 Dec 2025 10:22:40 +0100 Subject: [PATCH 40/41] remove temporary docs --- frontend/webEditor/dependencyGraph.md | 49 --------------------------- 1 file changed, 49 deletions(-) delete mode 100644 frontend/webEditor/dependencyGraph.md diff --git a/frontend/webEditor/dependencyGraph.md b/frontend/webEditor/dependencyGraph.md deleted file mode 100644 index 7783b09d..00000000 --- a/frontend/webEditor/dependencyGraph.md +++ /dev/null @@ -1,49 +0,0 @@ -```mermaid -stateDiagram-v2 - - helpUi --> accordionUiExtension - helpUi --> editorTypes - startUpAgent --> editorTypes - labels --> utils - labels --> editorTypes - labels --> accordionUiExtension - - serialize --> labels - serialize --> constraint - serialize --> editorMode - serialize --> commonModule: logger - - serialize --> editorMode - - diagram --> labels - - webSocket --> commonModule: logger - - commandPalette --> serialize - - serialize --> webSocket - - commandPalette --> fitToScreen - serialize --> fitToScreen - - layout --> fitToScreen - commandPalette --> layout - - startUpAgent --> webSocket - -%% [*] --> layout -%% [*] --> commonModule -%% [*] --> labels -%% [*] --> serialize -%% [*] --> editorMode -%% [*] --> diagram -%% [*] --> webSocket -%% [*] --> helpUi -%% [*] --> startUpAgent -%% [*] --> commandPalette - - classDef diLess font-style:italic,stroke:#0fa - class accordionUiExtension,editorTypes,utils,fitToScreen diLess -``` - -green packages do not export a module \ No newline at end of file From 64ddb6c8ba81680b2094093e59ef9f88f08b2dad Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 4 Dec 2025 10:25:45 +0100 Subject: [PATCH 41/41] update dependencies --- frontend/webEditor/package-lock.json | 430 ++++++------------ frontend/webEditor/package.json | 16 +- .../src/accordionUiExtension/accordion.css | 2 +- frontend/webEditor/src/assignment/language.ts | 2 +- frontend/webEditor/src/constraint/language.ts | 16 +- .../webEditor/src/labels/LabelTypeEditorUi.ts | 22 +- .../webEditor/src/labels/renameCommand.ts | 4 +- frontend/webEditor/src/languages/replace.ts | 6 +- frontend/webEditor/src/languages/words.ts | 14 +- 9 files changed, 184 insertions(+), 328 deletions(-) diff --git a/frontend/webEditor/package-lock.json b/frontend/webEditor/package-lock.json index b1bd0730..c0163abf 100644 --- a/frontend/webEditor/package-lock.json +++ b/frontend/webEditor/package-lock.json @@ -8,24 +8,24 @@ "name": "data-flow-analysis-web-editor", "version": "0.0.0", "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.35.0", + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.1", "@fortawesome/fontawesome-free": "^7.0.0", - "@vscode/codicons": "^0.0.39", - "eslint": "^9.32.0", + "@vscode/codicons": "^0.0.43", + "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", "inversify": "^6.2.2", - "lint-staged": "^16.1.6", + "lint-staged": "^16.2.7", "monaco-editor": "^0.52.2", - "prettier": "^3.6.2", + "prettier": "^3.7.4", "reflect-metadata": "^0.2.2", "sprotty": "^1.4.0", "sprotty-elk": "^1.4.0", "sprotty-protocol": "^1.4.0", "typescript": "^5.8.3", - "typescript-eslint": "^8.44.0", - "vite": "^7.1.7" + "typescript-eslint": "^8.48.1", + "vite": "^7.2.6" } }, "node_modules/@esbuild/aix-ppc64": { @@ -513,13 +513,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -528,22 +528,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", - "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -554,9 +554,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -566,7 +566,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -578,9 +578,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", - "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", "engines": { @@ -591,9 +591,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -601,13 +601,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -704,44 +704,6 @@ "reflect-metadata": "0.2.2" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.52.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", @@ -1065,17 +1027,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", - "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", + "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/type-utils": "8.46.0", - "@typescript-eslint/utils": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/type-utils": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1089,7 +1051,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.0", + "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1105,16 +1067,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz", - "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", + "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "engines": { @@ -1130,14 +1092,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz", - "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.0", - "@typescript-eslint/types": "^8.46.0", + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "engines": { @@ -1152,14 +1114,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz", - "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0" + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1170,9 +1132,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz", - "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", "dev": true, "license": "MIT", "engines": { @@ -1187,15 +1149,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz", - "integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0", - "@typescript-eslint/utils": "8.46.0", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1212,9 +1174,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", - "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", "dev": true, "license": "MIT", "engines": { @@ -1226,21 +1188,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz", - "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.0", - "@typescript-eslint/tsconfig-utils": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -1281,16 +1242,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz", - "integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0" + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1305,13 +1266,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz", - "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1323,9 +1284,9 @@ } }, "node_modules/@vscode/codicons": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.39.tgz", - "integrity": "sha512-gO09UZrOBONyzI8LWPRsCahnmUR16hkRQCJOSJzX8L4dC5aa6YGP4nS+gh5oSekMlM8LFJXMAgqBMGGiktdRJw==", + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.43.tgz", + "integrity": "sha512-8sf8WOBoZkyUi8ogCm5ycHJJGhwOEG3E9b64+JIx+m6bCExdkc30VwCwr94cXUU1opmRD0CTCWLcN46I8WLJIg==", "dev": true, "license": "CC-BY-4.0" }, @@ -1370,9 +1331,9 @@ } }, "node_modules/ansi-escapes": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", - "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1503,9 +1464,9 @@ } }, "node_modules/cli-truncate": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.0.tgz", - "integrity": "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", "dev": true, "license": "MIT", "dependencies": { @@ -1547,9 +1508,9 @@ "license": "MIT" }, "node_modules/commander": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", - "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", "dev": true, "license": "MIT", "engines": { @@ -1611,9 +1572,9 @@ "license": "EPL-2.0" }, "node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, @@ -1686,25 +1647,24 @@ } }, "node_modules/eslint": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", - "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.4.0", - "@eslint/core": "^0.16.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.37.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -1870,36 +1830,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1914,16 +1844,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2190,9 +2110,9 @@ "license": "ISC" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2248,16 +2168,16 @@ } }, "node_modules/lint-staged": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.3.tgz", - "integrity": "sha512-1OnJEESB9zZqsp61XHH2fvpS1es3hRCxMplF/AJUDa8Ho8VrscYDIuxGrj3m8KPXbcWZ8fT9XTMUhEQmOVKpKw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^14.0.1", - "listr2": "^9.0.4", + "commander": "^14.0.2", + "listr2": "^9.0.5", "micromatch": "^4.0.8", - "nano-spawn": "^1.0.3", + "nano-spawn": "^2.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" @@ -2273,9 +2193,9 @@ } }, "node_modules/listr2": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.4.tgz", - "integrity": "sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", "dependencies": { @@ -2333,16 +2253,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2398,9 +2308,9 @@ "license": "MIT" }, "node_modules/nano-spawn": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.3.tgz", - "integrity": "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", "dev": true, "license": "MIT", "engines": { @@ -2608,9 +2518,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", "bin": { @@ -2633,27 +2543,6 @@ "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -2688,17 +2577,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -2748,34 +2626,10 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -3085,16 +2939,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz", - "integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", + "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.0", - "@typescript-eslint/parser": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0", - "@typescript-eslint/utils": "8.46.0" + "@typescript-eslint/eslint-plugin": "8.48.1", + "@typescript-eslint/parser": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3119,9 +2973,9 @@ } }, "node_modules/vite": { - "version": "7.1.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", - "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/frontend/webEditor/package.json b/frontend/webEditor/package.json index 5f09ab6c..5e118773 100644 --- a/frontend/webEditor/package.json +++ b/frontend/webEditor/package.json @@ -7,24 +7,24 @@ "url": "https://github.com/DataFlowAnalysis/OnlineEditor.git" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.35.0", + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.1", "@fortawesome/fontawesome-free": "^7.0.0", - "@vscode/codicons": "^0.0.39", - "eslint": "^9.32.0", + "@vscode/codicons": "^0.0.43", + "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "husky": "^9.1.7", "inversify": "^6.2.2", - "lint-staged": "^16.1.6", + "lint-staged": "^16.2.7", "monaco-editor": "^0.52.2", - "prettier": "^3.6.2", + "prettier": "^3.7.4", "reflect-metadata": "^0.2.2", "sprotty": "^1.4.0", "sprotty-elk": "^1.4.0", "sprotty-protocol": "^1.4.0", "typescript": "^5.8.3", - "typescript-eslint": "^8.44.0", - "vite": "^7.1.7" + "typescript-eslint": "^8.48.1", + "vite": "^7.2.6" }, "scripts": { "dev": "vite", diff --git a/frontend/webEditor/src/accordionUiExtension/accordion.css b/frontend/webEditor/src/accordionUiExtension/accordion.css index 8bd64783..6446547b 100644 --- a/frontend/webEditor/src/accordionUiExtension/accordion.css +++ b/frontend/webEditor/src/accordionUiExtension/accordion.css @@ -21,7 +21,7 @@ transition: grid-template-rows 300ms ease, /* ease-out animation: https://cubic-bezier.com/#0,.7,.4,1 */ /* mirrored version of the curve above */ - grid-template-columns 300ms cubic-bezier(0, 0.7, 0.4, 1), + grid-template-columns 300ms cubic-bezier(0, 0.7, 0.4, 1), padding-top 300ms ease; /* space between accordion button and the content, otherwise they would be directly next to each other without any spacing */ diff --git a/frontend/webEditor/src/assignment/language.ts b/frontend/webEditor/src/assignment/language.ts index c623393f..90128c4c 100644 --- a/frontend/webEditor/src/assignment/language.ts +++ b/frontend/webEditor/src/assignment/language.ts @@ -203,7 +203,7 @@ class LabelWord implements Word { return []; } - + replace(text: string, replacement: ReplacementData) { if (replacement.type == "label" && text == replacement.old) { return replacement.replacement; diff --git a/frontend/webEditor/src/constraint/language.ts b/frontend/webEditor/src/constraint/language.ts index e91785be..38591e26 100644 --- a/frontend/webEditor/src/constraint/language.ts +++ b/frontend/webEditor/src/constraint/language.ts @@ -291,10 +291,10 @@ export namespace ConstraintDslTreeBuilder { } replace(text: string, replacement: ReplacementData) { - if (replacement.type == "label" && text == replacement.old) { - return replacement.replacement; - } - return text; + if (replacement.type == "label" && text == replacement.old) { + return replacement.replacement; + } + return text; } } @@ -389,10 +389,10 @@ export namespace ConstraintDslTreeBuilder { replace(text: string, replacement: ReplacementData) { if (!this.characteristicSelectorData.replace) { - return text + return text; } - const parts = text.split(',') - return parts.map(p => this.characteristicSelectorData.replace(p, replacement)).join(',') - } + const parts = text.split(","); + return parts.map((p) => this.characteristicSelectorData.replace(p, replacement)).join(","); + } } } diff --git a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts index 3c27e028..9f8af3b4 100644 --- a/frontend/webEditor/src/labels/LabelTypeEditorUi.ts +++ b/frontend/webEditor/src/labels/LabelTypeEditorUi.ts @@ -95,13 +95,13 @@ export class LabelTypeEditorUi extends AccordionUiExtension { if (this.editorModeController.isReadOnly()) { return; } - const replacements = labelType.values.map(t => ({ + const replacements = labelType.values.map((t) => ({ old: `${labelType.name}.${t}`, replacement: `${nameInput.value}.${t}`, - type: 'label' - })) + type: "label", + })); this.labelTypeRegistry.updateLabelTypeName(labelType.id, nameInput.value); - this.actionDispatcher.dispatch(ReplaceAction.create(replacements)) + this.actionDispatcher.dispatch(ReplaceAction.create(replacements)); }; nameInput.onfocus = () => { if (this.editorModeController.isReadOnly()) { @@ -154,13 +154,15 @@ export class LabelTypeEditorUi extends AccordionUiExtension { if (this.editorModeController.isReadOnly()) { return; } - const replacements = [{ - old: `${labelType.name}.${value.text}`, - replacement: `${labelType.name}.${nameInput.value}`, - type: 'label' - }] + const replacements = [ + { + old: `${labelType.name}.${value.text}`, + replacement: `${labelType.name}.${nameInput.value}`, + type: "label", + }, + ]; this.labelTypeRegistry.updateLabelTypeValueText(labelType.id, value.id, nameInput.value); - this.actionDispatcher.dispatch(ReplaceAction.create(replacements)) + this.actionDispatcher.dispatch(ReplaceAction.create(replacements)); }; deleteButton.onclick = () => { diff --git a/frontend/webEditor/src/labels/renameCommand.ts b/frontend/webEditor/src/labels/renameCommand.ts index 67d1d092..003d43b9 100644 --- a/frontend/webEditor/src/labels/renameCommand.ts +++ b/frontend/webEditor/src/labels/renameCommand.ts @@ -29,7 +29,7 @@ export class ReplaceCommand extends Command { @inject(TYPES.Action) private readonly action: ReplaceAction, @inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry, @inject(LabelTypeRegistry) private readonly labelTypeRegistry: LabelTypeRegistry, - @inject(TYPES.ModelSource) private readonly localModelSource: LocalModelSource + @inject(TYPES.ModelSource) private readonly localModelSource: LocalModelSource, ) { super(); } @@ -39,7 +39,7 @@ export class ReplaceCommand extends Command { for (const replacement of this.action.replacements) { this.constraintRegistry.setConstraints( replace( - this.constraintRegistry.getConstraintsAsText().split('\n'), + this.constraintRegistry.getConstraintsAsText().split("\n"), ConstraintDslTreeBuilder.buildTree(this.localModelSource, this.labelTypeRegistry), replacement, ), diff --git a/frontend/webEditor/src/languages/replace.ts b/frontend/webEditor/src/languages/replace.ts index 95d38c88..39a0d636 100644 --- a/frontend/webEditor/src/languages/replace.ts +++ b/frontend/webEditor/src/languages/replace.ts @@ -23,13 +23,13 @@ export function replace( const tokens = tokenize(lines); const replaced = replaceTokens(tokens, tree, tree, 0, replacement); for (let i = 0; i < tokens.length; i++) { - replaceToken(i) + replaceToken(i); } - return lines + return lines; function replaceToken(index: number) { const token = replaced[index]; - const lengthDiff = token.newText.length - token.text.length + const lengthDiff = token.newText.length - token.text.length; const lineIndex = token.line - 1; const line = lines[lineIndex]; const before = line.substring(0, token.column - 1); diff --git a/frontend/webEditor/src/languages/words.ts b/frontend/webEditor/src/languages/words.ts index 61eded2e..2b41ac48 100644 --- a/frontend/webEditor/src/languages/words.ts +++ b/frontend/webEditor/src/languages/words.ts @@ -61,12 +61,12 @@ export class NegatableWord implements Word { replace(text: string, replacement: ReplacementData): string { if (!this.word.replace) { - return text + return text; } - if (text.startsWith('!')) { - return this.replace(text.substring(1), replacement) + if (text.startsWith("!")) { + return this.replace(text.substring(1), replacement); } - return this.word.replace(text, replacement) + return this.word.replace(text, replacement); } } @@ -92,9 +92,9 @@ export class ListWord implements Word { replace(text: string, replacement: ReplacementData): string { if (!this.word.replace) { - return text + return text; } - const parts = text.split(',') - return parts.map(p => this.word.replace!(p, replacement)).join(',') + const parts = text.split(","); + return parts.map((p) => this.word.replace!(p, replacement)).join(","); } }