diff --git a/docs/source/user/api/apollon-editor.d.ts b/docs/source/user/api/apollon-editor.d.ts index af3174211..09b2cbdc5 100644 --- a/docs/source/user/api/apollon-editor.d.ts +++ b/docs/source/user/api/apollon-editor.d.ts @@ -1,9 +1,11 @@ import 'pepjs'; import { DeepPartial } from 'redux'; import { Styles } from './components/theme/styles'; +import { Patch } from './services/patcher'; import { Locale } from './services/editor/editor-types'; import * as Apollon from './typings'; import { UMLDiagramType, UMLModel } from './typings'; +import { UMLModelCompat } from './compat'; export declare class ApollonEditor { private container; private options; @@ -16,7 +18,7 @@ export declare class ApollonEditor { * Sets a model as the current model of the Apollon Editor * @param model valid Apollon Editor Model */ - set model(model: Apollon.UMLModel); + set model(model: UMLModelCompat); /** * Sets the diagram type of the current Apollon Editor. This changes the selection of elements the user can chose from on the sidebar. * @param diagramType the new diagram type @@ -39,6 +41,7 @@ export declare class ApollonEditor { private currentModelState?; private assessments; private application; + private patcher; private selectionSubscribers; private assessmentSubscribers; private modelSubscribers; @@ -103,6 +106,23 @@ export declare class ApollonEditor { * @param subscriptionId subscription identifier */ unsubscribeFromDiscreteModelChange(subscriptionId: number): void; + /** + * Register callback which is executed when the model changes, receiving the changes to the model + * in [JSONPatch](http://jsonpatch.com/) format. + * @param callback function which is called when the model changes + * @return returns the subscription identifier which can be used to unsubscribe + */ + subscribeToModelChangePatches(callback: (patch: Patch) => void): number; + /** + * Remove model change subscription, so that the corresponding callback is no longer executed when the model is changed. + * @param subscriptionId subscription identifier + */ + unsubscribeFromModelChangePatches(subscriptionId: number): void; + /** + * Imports a patch into the current model. + * @param patch changes to be applied to the model, in [JSONPatch](http://jsonpatch.com/) format. + */ + importPatch(patch: Patch): void; /** * Register callback which is executed when an error occurs in the editor. Apollon will try to recreate the latest working state when an error occurs, so that it is less visible to user / less interrupting. * A registered callback would be called anyway, giving the full error, so that the application which uses Apollon can decide what to do next. @@ -110,6 +130,25 @@ export declare class ApollonEditor { * @return returns the subscription identifier which can be used to unsubscribe */ subscribeToApollonErrors(callback: (error: Error) => void): number; + /** + * Displays given elements and relationships as selected or deselected by + * a given remote selector, identified by a name and a color. + * @param selectorName name of the remote selector + * @param selectorColor color of the remote selector + * @param select ids of elements and relationships to be selected + * @param deselect ids of elements and relationships to be deselected + */ + remoteSelect(selectorName: string, selectorColor: string, select: string[], deselect?: string[]): void; + /** + * Allows a given set of remote selectors for remotely selecting and deselecting + * elements and relationships, removing all other selectors. This won't have an effect + * on future remote selections. + * @param allowedSelectors allowed remote selectors + */ + pruneRemoteSelectors(allowedSelectors: { + name: string; + color: string; + }[]): void; /** * Removes error subscription, so that the corresponding callback is no longer executed when an error occurs. * @param subscriptionId subscription identifier diff --git a/docs/source/user/api/typings.d.ts b/docs/source/user/api/typings.d.ts index dfd97db7f..835d2223e 100644 --- a/docs/source/user/api/typings.d.ts +++ b/docs/source/user/api/typings.d.ts @@ -1,7 +1,6 @@ import { DeepPartial } from 'redux'; import { Styles } from './components/theme/styles'; import { UMLDiagramType } from './packages/diagram-type'; -import { UMLElementSelectorType } from './packages/uml-element-selector-type'; import { UMLElementType } from './packages/uml-element-type'; import { UMLRelationshipType } from './packages/uml-relationship-type'; import { ApollonMode, Locale } from './services/editor/editor-types'; @@ -62,7 +61,6 @@ export type UMLModelElement = { strokeColor?: string; textColor?: string; assessmentNote?: string; - selectedBy?: UMLElementSelectorType[]; }; export type UMLElement = UMLModelElement & { type: UMLElementType; diff --git a/package-lock.json b/package-lock.json index a318a381e..184148e10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ls1intum/apollon", - "version": "3.0.6", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ls1intum/apollon", - "version": "3.0.6", + "version": "3.2.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -29,25 +29,25 @@ "devDependencies": { "@stylelint/postcss-css-in-js": "0.38.0", "@testing-library/jest-dom": "6.1.4", - "@testing-library/react": "14.0.0", - "@types/jest": "29.5.7", - "@types/react": "18.2.35", - "@types/react-color": "3.0.9", - "@types/react-dom": "18.2.14", - "@types/react-redux": "7.1.28", - "@types/redux-mock-store": "1.0.5", - "@types/styled-components": "5.1.29", - "@types/uuid": "9.0.6", - "@typescript-eslint/eslint-plugin": "6.9.1", - "@typescript-eslint/parser": "6.9.1", + "@testing-library/react": "14.1.2", + "@types/jest": "29.5.10", + "@types/react": "18.2.38", + "@types/react-color": "3.0.10", + "@types/react-dom": "18.2.17", + "@types/react-redux": "7.1.31", + "@types/redux-mock-store": "1.0.6", + "@types/styled-components": "5.1.32", + "@types/uuid": "9.0.7", + "@typescript-eslint/eslint-plugin": "6.12.0", + "@typescript-eslint/parser": "6.12.0", "circular-dependency-plugin": "5.2.2", "copy-webpack-plugin": "11.0.0", "css-loader": "6.8.1", - "cypress": "13.4.0", + "cypress": "13.6.0", "cypress-real-events": "1.11.0", - "eslint": "8.53.0", + "eslint": "8.54.0", "eslint-config-prettier": "9.0.0", - "eslint-plugin-jsdoc": "46.8.2", + "eslint-plugin-jsdoc": "46.9.0", "eslint-plugin-prefer-arrow": "1.2.3", "eslint-plugin-prettier": "5.0.1", "eslint-plugin-react": "7.33.2", @@ -58,26 +58,29 @@ "jest-environment-jsdom": "29.7.0", "jest-html-reporter": "3.10.2", "jest-styled-components": "7.2.0", - "lint-staged": "15.0.2", + "lint-staged": "15.1.0", "pinst": "3.0.0", "postcss": "8.4.31", "postcss-syntax": "0.36.2", - "prettier": "3.0.3", + "prettier": "3.1.0", "redux-mock-store": "1.5.4", "sleep-promise": "9.1.0", - "start-server-and-test": "2.0.2", + "start-server-and-test": "2.0.3", "style-loader": "3.3.3", "stylelint": "15.11.0", "stylelint-config-recommended": "13.0.0", "stylelint-config-styled-components": "0.1.1", "stylelint-processor-styled-components": "1.10.0", "ts-jest": "29.1.1", - "ts-loader": "9.5.0", - "typescript": "5.2.2", + "ts-loader": "9.5.1", + "typescript": "5.3.2", "webpack": "5.89.0", "webpack-cli": "5.1.4", "webpack-dev-server": "4.15.1", "webpack-merge": "5.10.0" + }, + "engines": { + "node": ">=18.17.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -772,12 +775,12 @@ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.40.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", - "integrity": "sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", + "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", "dev": true, "dependencies": { - "comment-parser": "1.4.0", + "comment-parser": "1.4.1", "esquery": "^1.5.0", "jsdoc-type-pratt-parser": "~4.0.0" }, @@ -860,9 +863,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2117,9 +2120,9 @@ } }, "node_modules/@testing-library/react": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", - "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.2.tgz", + "integrity": "sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -2342,9 +2345,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.7", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.7.tgz", - "integrity": "sha512-HLyetab6KVPSiF+7pFcUyMeLsx25LDNDemw9mGsJBkai/oouwrjTycocSDYopMEwFhN2Y4s9oPyOCZNofgSt2g==", + "version": "29.5.10", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", + "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -2436,9 +2439,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.35", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.35.tgz", - "integrity": "sha512-LG3xpFZ++rTndV+/XFyX5vUP7NI9yxyk+MQvBDq+CVs8I9DLSc3Ymwb1Vmw5YDoeNeHN4PDZa3HylMKJYT9PNQ==", + "version": "18.2.38", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.38.tgz", + "integrity": "sha512-cBBXHzuPtQK6wNthuVMV6IjHAFkdl/FOPFIlkd81/Cd1+IqkHu/A+w4g43kaQQoYHik/ruaQBDL72HyCy1vuMw==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2446,9 +2449,9 @@ } }, "node_modules/@types/react-color": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.9.tgz", - "integrity": "sha512-Ojyc6jySSKvM6UYQrZxaYe0JZXtgHHXwR2q9H4MhcNCswFdeZH1owYZCvPtdHtMOfh7t8h1fY0Gd0nvU1JGDkQ==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.10.tgz", + "integrity": "sha512-6K5BAn3zyd8lW8UbckIAVeXGxR82Za9jyGD2DBEynsa7fKaguLDVtjfypzs7fgEV7bULgs7uhds8A8v1wABTvQ==", "dev": true, "dependencies": { "@types/react": "*", @@ -2456,18 +2459,18 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.14", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.14.tgz", - "integrity": "sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==", + "version": "18.2.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", + "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", "devOptional": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-redux": { - "version": "7.1.28", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.28.tgz", - "integrity": "sha512-EQr7cChVzVUuqbA+J8ArWK1H0hLAHKOs21SIMrskKZ3nHNeE+LFYA+IsoZGhVOT8Ktjn3M20v4rnZKN3fLbypw==", + "version": "7.1.31", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.31.tgz", + "integrity": "sha512-merF9AH72krBUekQY6uObXnMsEo1xTeZy9NONNRnqSwvwVe3HtLeRvNIPaKmPDIOWPsSFE51rc2WGpPMqmuCWg==", "dev": true, "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", @@ -2486,9 +2489,9 @@ } }, "node_modules/@types/redux-mock-store": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.5.tgz", - "integrity": "sha512-RU3IuL6ZVx/Ove0qxPz0Lcaiog2YIVIO2iyEg6xN0EqDGYn60cVqVKjo8mvoIsgjH1SepJoYpDZ2fGzFoY4YlQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.6.tgz", + "integrity": "sha512-eg5RDfhJTXuoJjOMyXiJbaDb1B8tfTaJixscmu+jOusj6adGC0Krntz09Tf4gJgXeCqCrM5bBMd+B7ez0izcAQ==", "dev": true, "dependencies": { "redux": "^4.0.5" @@ -2506,9 +2509,9 @@ "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==" }, "node_modules/@types/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/send": { @@ -2569,9 +2572,9 @@ "dev": true }, "node_modules/@types/styled-components": { - "version": "5.1.29", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.29.tgz", - "integrity": "sha512-5h/ah9PAblggQ6Laa4peplT4iY5ddA8qM1LMD4HzwToUWs3hftfy0fayeRgbtH1JZUdw5CCaowmz7Lnb8SjIxQ==", + "version": "5.1.32", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.32.tgz", + "integrity": "sha512-DqVpl8R0vbhVSop4120UHtGrFmHuPeoDwF4hDT0kPJTY8ty0SI38RV3VhCMsWigMUXG+kCXu7vMRqMFNy6eQgA==", "dev": true, "dependencies": { "@types/hoist-non-react-statics": "*", @@ -2591,9 +2594,9 @@ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, "node_modules/@types/uuid": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.6.tgz", - "integrity": "sha512-BT2Krtx4xaO6iwzwMFUYvWBWkV2pr37zD68Vmp1CDV196MzczBRxuEpD6Pr395HAgebC/co7hOphs53r8V7jew==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", "dev": true }, "node_modules/@types/ws": { @@ -2631,16 +2634,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz", - "integrity": "sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz", + "integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.9.1", - "@typescript-eslint/type-utils": "6.9.1", - "@typescript-eslint/utils": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1", + "@typescript-eslint/scope-manager": "6.12.0", + "@typescript-eslint/type-utils": "6.12.0", + "@typescript-eslint/utils": "6.12.0", + "@typescript-eslint/visitor-keys": "6.12.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -2666,15 +2669,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz", - "integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz", + "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.9.1", - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/typescript-estree": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1", + "@typescript-eslint/scope-manager": "6.12.0", + "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/typescript-estree": "6.12.0", + "@typescript-eslint/visitor-keys": "6.12.0", "debug": "^4.3.4" }, "engines": { @@ -2694,13 +2697,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz", - "integrity": "sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz", + "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1" + "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/visitor-keys": "6.12.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2711,13 +2714,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz", - "integrity": "sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz", + "integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.9.1", - "@typescript-eslint/utils": "6.9.1", + "@typescript-eslint/typescript-estree": "6.12.0", + "@typescript-eslint/utils": "6.12.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -2738,9 +2741,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.1.tgz", - "integrity": "sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz", + "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2751,13 +2754,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz", - "integrity": "sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz", + "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1", + "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/visitor-keys": "6.12.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2778,17 +2781,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.1.tgz", - "integrity": "sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz", + "integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.9.1", - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/typescript-estree": "6.9.1", + "@typescript-eslint/scope-manager": "6.12.0", + "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/typescript-estree": "6.12.0", "semver": "^7.5.4" }, "engines": { @@ -2803,12 +2806,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz", - "integrity": "sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz", + "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.9.1", + "@typescript-eslint/types": "6.12.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -3514,13 +3517,14 @@ "dev": true }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dev": true, "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/axios/node_modules/form-data": { @@ -3537,6 +3541,12 @@ "node": ">= 6" } }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4446,9 +4456,9 @@ } }, "node_modules/comment-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", - "integrity": "sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true, "engines": { "node": ">= 12.0.0" @@ -4904,9 +4914,9 @@ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/cypress": { - "version": "13.4.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.4.0.tgz", - "integrity": "sha512-KeWNC9xSHG/ewZURVbaQsBQg2mOKw4XhjJZFKjWbEjgZCdxpPXLpJnfq5Jns1Gvnjp6AlnIfpZfWFlDgVKXdWQ==", + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz", + "integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -5994,15 +6004,15 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/js": "8.54.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -6061,14 +6071,14 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "46.8.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.2.tgz", - "integrity": "sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==", + "version": "46.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.9.0.tgz", + "integrity": "sha512-UQuEtbqLNkPf5Nr/6PPRCtr9xypXY+g8y/Q7gPa0YK7eDhh0y2lWprXRnaYbW7ACgIUvpDKy9X2bZqxtGzBG9Q==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.40.1", + "@es-joy/jsdoccomment": "~0.41.0", "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.0", + "comment-parser": "1.4.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.5.0", @@ -10860,9 +10870,9 @@ "dev": true }, "node_modules/lint-staged": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.0.2.tgz", - "integrity": "sha512-vnEy7pFTHyVuDmCAIFKR5QDO8XLVlPFQQyujQ/STOxe40ICWqJ6knS2wSJ/ffX/Lw0rz83luRDh+ET7toN+rOw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.1.0.tgz", + "integrity": "sha512-ZPKXWHVlL7uwVpy8OZ7YQjYDAuO5X4kMh0XgZvPNxLcCCngd0PO5jKQyy3+s4TL2EnHoIXIzP1422f/l3nZKMw==", "dev": true, "dependencies": { "chalk": "5.3.0", @@ -10874,7 +10884,7 @@ "micromatch": "4.0.5", "pidtree": "0.6.0", "string-argv": "0.3.2", - "yaml": "2.3.3" + "yaml": "2.3.4" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -12776,9 +12786,9 @@ } }, "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -14313,9 +14323,9 @@ } }, "node_modules/start-server-and-test": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.2.tgz", - "integrity": "sha512-4sGS2QmETUwqeBUqtTLP7OqXp3PdDnevaWlPlrFQgn8+7uCgVg4Do7/H/ZhAAVyvnL3DqKyANhnLgcgxrjhrMA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.3.tgz", + "integrity": "sha512-QsVObjfjFZKJE6CS6bSKNwWZCKBG6975/jKRPPGFfFh+yOQglSeGXiNWjzgQNXdphcBI9nXbyso9tPfX4YAUhg==", "dev": true, "dependencies": { "arg": "^5.0.2", @@ -14325,7 +14335,7 @@ "execa": "5.1.1", "lazy-ass": "1.6.0", "ps-tree": "1.2.0", - "wait-on": "7.1.0" + "wait-on": "7.2.0" }, "bin": { "server-test": "src/bin/start.js", @@ -15286,9 +15296,9 @@ } }, "node_modules/ts-loader": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.0.tgz", - "integrity": "sha512-LLlB/pkB4q9mW2yLdFMnK3dEHbrBjeZTYguaaIfusyojBgAGf5kF+O6KcWqiGzWqHk0LBsoolrp4VftEURhybg==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", "dev": true, "dependencies": { "chalk": "^4.1.0", @@ -15519,9 +15529,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -15743,12 +15753,12 @@ } }, "node_modules/wait-on": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.1.0.tgz", - "integrity": "sha512-U7TF/OYYzAg+OoiT/B8opvN48UHt0QYMi4aD3PjRFpybQ+o6czQF8Ig3SKCCMJdxpBrCalIJ4O00FBof27Fu9Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", + "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", "dev": true, "dependencies": { - "axios": "^0.27.2", + "axios": "^1.6.1", "joi": "^17.11.0", "lodash": "^4.17.21", "minimist": "^1.2.8", @@ -16368,9 +16378,9 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz", - "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "dev": true, "engines": { "node": ">= 14" diff --git a/package.json b/package.json index a92313cca..987c95081 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ls1intum/apollon", - "version": "3.0.6", + "version": "3.2.0", "description": "A UML diagram editor.", "keywords": [], "homepage": "https://github.com/ls1intum/apollon#readme", @@ -16,6 +16,9 @@ "typings": "lib/es6/index.d.ts", "repository": "github:ls1intum/apollon", "sideEffects": false, + "engines": { + "node": ">=18.17.0" + }, "scripts": { "postinstall": "husky install", "prepublishOnly": "pinst --disable", @@ -76,25 +79,25 @@ "devDependencies": { "@stylelint/postcss-css-in-js": "0.38.0", "@testing-library/jest-dom": "6.1.4", - "@testing-library/react": "14.0.0", - "@types/jest": "29.5.7", - "@types/react": "18.2.35", - "@types/react-color": "3.0.9", - "@types/react-dom": "18.2.14", - "@types/react-redux": "7.1.28", - "@types/redux-mock-store": "1.0.5", - "@types/styled-components": "5.1.29", - "@types/uuid": "9.0.6", - "@typescript-eslint/eslint-plugin": "6.9.1", - "@typescript-eslint/parser": "6.9.1", + "@testing-library/react": "14.1.2", + "@types/jest": "29.5.10", + "@types/react": "18.2.38", + "@types/react-color": "3.0.10", + "@types/react-dom": "18.2.17", + "@types/react-redux": "7.1.31", + "@types/redux-mock-store": "1.0.6", + "@types/styled-components": "5.1.32", + "@types/uuid": "9.0.7", + "@typescript-eslint/eslint-plugin": "6.12.0", + "@typescript-eslint/parser": "6.12.0", "circular-dependency-plugin": "5.2.2", "copy-webpack-plugin": "11.0.0", "css-loader": "6.8.1", - "cypress": "13.4.0", + "cypress": "13.6.0", "cypress-real-events": "1.11.0", - "eslint": "8.53.0", + "eslint": "8.54.0", "eslint-config-prettier": "9.0.0", - "eslint-plugin-jsdoc": "46.8.2", + "eslint-plugin-jsdoc": "46.9.0", "eslint-plugin-prefer-arrow": "1.2.3", "eslint-plugin-prettier": "5.0.1", "eslint-plugin-react": "7.33.2", @@ -105,22 +108,22 @@ "jest-environment-jsdom": "29.7.0", "jest-html-reporter": "3.10.2", "jest-styled-components": "7.2.0", - "lint-staged": "15.0.2", + "lint-staged": "15.1.0", "pinst": "3.0.0", "postcss": "8.4.31", "postcss-syntax": "0.36.2", - "prettier": "3.0.3", + "prettier": "3.1.0", "redux-mock-store": "1.5.4", "sleep-promise": "9.1.0", - "start-server-and-test": "2.0.2", + "start-server-and-test": "2.0.3", "style-loader": "3.3.3", "stylelint": "15.11.0", "stylelint-config-recommended": "13.0.0", "stylelint-config-styled-components": "0.1.1", "stylelint-processor-styled-components": "1.10.0", "ts-jest": "29.1.1", - "ts-loader": "9.5.0", - "typescript": "5.2.2", + "ts-loader": "9.5.1", + "typescript": "5.3.2", "webpack": "5.89.0", "webpack-cli": "5.1.4", "webpack-dev-server": "4.15.1", diff --git a/src/main/apollon-editor.ts b/src/main/apollon-editor.ts index 70fe69f74..b532e4435 100644 --- a/src/main/apollon-editor.ts +++ b/src/main/apollon-editor.ts @@ -20,6 +20,7 @@ import { debounce } from './utils/debounce'; import { delay } from './utils/delay'; import { ErrorBoundary } from './components/controls/error-boundary/ErrorBoundary'; import { replaceColorVariables } from './utils/replace-color-variables'; +import { UMLModelCompat } from './compat'; export class ApollonEditor { private ensureInitialized() { @@ -46,7 +47,7 @@ export class ApollonEditor { * Sets a model as the current model of the Apollon Editor * @param model valid Apollon Editor Model */ - set model(model: Apollon.UMLModel) { + set model(model: UMLModelCompat) { this.ensureInitialized(); const state: PartialModelState = { ...ModelState.fromModel(model), @@ -330,6 +331,30 @@ export class ApollonEditor { return id; } + /** + * Displays given elements and relationships as selected or deselected by + * a given remote selector, identified by a name and a color. + * @param selectorName name of the remote selector + * @param selectorColor color of the remote selector + * @param select ids of elements and relationships to be selected + * @param deselect ids of elements and relationships to be deselected + */ + remoteSelect(selectorName: string, selectorColor: string, select: string[], deselect?: string[]): void { + this.store?.dispatch( + UMLElementRepository.remoteSelectDeselect({ name: selectorName, color: selectorColor }, select, deselect || []), + ); + } + + /** + * Allows a given set of remote selectors for remotely selecting and deselecting + * elements and relationships, removing all other selectors. This won't have an effect + * on future remote selections. + * @param allowedSelectors allowed remote selectors + */ + pruneRemoteSelectors(allowedSelectors: { name: string; color: string }[]): void { + this.store?.dispatch(UMLElementRepository.pruneRemoteSelectors(allowedSelectors)); + } + /** * Removes error subscription, so that the corresponding callback is no longer executed when an error occurs. * @param subscriptionId subscription identifier diff --git a/src/main/compat/helpers.ts b/src/main/compat/helpers.ts new file mode 100644 index 000000000..d91331286 --- /dev/null +++ b/src/main/compat/helpers.ts @@ -0,0 +1,160 @@ +import { Assessment, UMLElement, UMLRelationship } from '../typings'; +import { UMLModelCompat } from './typings'; +import { + isV2, + findElement as findElementV2, + addOrUpdateElement as addOrUpdateElementV2, + findRelationship as findRelationshipV2, + addOrUpdateRelationship as addOrUpdateRelationshipV2, + findAssessment as findAssessmentV2, + addOrUpdateAssessment as addOrUpdateAssessmentV2, + isInteractiveElement as isInteractiveElementV2, + setInteractiveElement as setInteractiveElementV2, + isInteractiveRelationship as isInteractiveRelationshipV2, + setInteractiveRelationship as setInteractiveRelationshipV2, +} from './v2'; + +/** + * + * Finds an element in the model by id + * + * @param {UMLModelCompat} model the model to search + * @param {string} id the id of the element to find + * @returns {UMLElement | undefined} the element or undefined if not found + */ +export function findElement(model: UMLModelCompat, id: string): UMLElement | undefined { + if (isV2(model)) { + return findElementV2(model, id); + } else { + return model.elements[id]; + } +} + +/** + * + * Adds given element to given model. If element with same id already exists, it will be replaced. + * + * @param {UMLModelCompat} model the model to update + * @param {UMLElement} element the element to add or update + */ +export function addOrUpdateElement(model: UMLModelCompat, element: UMLElement): void { + if (isV2(model)) { + return addOrUpdateElementV2(model, element); + } else { + model.elements[element.id] = element; + } +} + +/** + * + * Finds a relationship in the model by id + * + * @param {UMLModelCompat} model the model to search + * @param {string} id the id of the relationship to find + * @returns {UMLRelationship | undefined} the relationship or undefined if not found + */ +export function findRelationship(model: UMLModelCompat, id: string): UMLRelationship | undefined { + if (isV2(model)) { + return findRelationshipV2(model, id); + } else { + return model.relationships[id]; + } +} + +/** + * + * Adds given relationship to given model. If relationship with same id already exists, it will be replaced. + * + * @param {UMLModelCompat} model the model to update + * @param {UMLRelationship} relationship the relationship to add or update + */ +export function addOrUpdateRelationship(model: UMLModelCompat, relationship: UMLRelationship): void { + if (isV2(model)) { + return addOrUpdateRelationshipV2(model, relationship); + } else { + model.relationships[relationship.id] = relationship; + } +} + +/** + * + * Finds an assessment in the model by id + * + * @param {UMLModelCompat} model the model to search + * @param {string} id the id of the assessment to find + * @returns {Assessment | undefined} the assessment or undefined if not found + */ +export function findAssessment(model: UMLModelCompat, id: string): Assessment | undefined { + if (isV2(model)) { + return findAssessmentV2(model, id); + } else { + return model.assessments[id]; + } +} + +/** + * + * Adds given assessment to given model. If assessment with same id already exists, it will be replaced. + * + * @param {UMLModelCompat} model the model to update + * @param {Assessment} assessment the assessment to add or update + */ +export function addOrUpdateAssessment(model: UMLModelCompat, assessment: Assessment): void { + if (isV2(model)) { + return addOrUpdateAssessmentV2(model, assessment); + } else { + model.assessments[assessment.modelElementId] = assessment; + } +} + +/** + * @returns true if the element is interactive, false otherwise. + */ +export function isInteractiveElement(model: UMLModelCompat, id: string): boolean { + if (isV2(model)) { + return isInteractiveElementV2(model, id); + } else { + return !!model.interactive.elements[id]; + } +} + +/** + * Sets the interactive state of the element. + * + * @param {UMLModelCompat} model the model to update + * @param {string} id the id of the element to set interactive state + * @param {boolean} interactive the interactive state to set + */ +export function setInteractiveElement(model: UMLModelCompat, id: string, interactive: boolean): void { + if (isV2(model)) { + return setInteractiveElementV2(model, id, interactive); + } else { + model.interactive.elements[id] = interactive; + } +} + +/** + * @returns true if the relationship is interactive, false otherwise. + */ +export function isInteractiveRelationship(model: UMLModelCompat, id: string): boolean { + if (isV2(model)) { + return isInteractiveRelationshipV2(model, id); + } else { + return !!model.interactive.relationships[id]; + } +} + +/** + * Sets the interactive state of the relationship. + * + * @param {UMLModelCompat} model the model to update + * @param {string} id the id of the relationship to set interactive state + * @param {boolean} interactive the interactive state to set + */ +export function setInteractiveRelationship(model: UMLModelCompat, id: string, interactive: boolean): void { + if (isV2(model)) { + return setInteractiveRelationshipV2(model, id, interactive); + } else { + model.interactive.relationships[id] = interactive; + } +} diff --git a/src/main/compat/index.ts b/src/main/compat/index.ts index db4123ba8..ea8063959 100644 --- a/src/main/compat/index.ts +++ b/src/main/compat/index.ts @@ -1,24 +1,6 @@ import { UMLModel } from '../typings'; -import { isV2, UMLModelCompat as UMLModelCompatV2, v2ModeltoV3Model } from './v2'; - -/** - * - * Represents all model versions that can be converted to the latest version. - * - */ -/* - * - * HINT for future maintainers: - * - * this should always be the union of compatible model versions, i.e. - * if the model version is moved to V4, while support for V2 is still required, - * this should look like the following: - * - * ```ts - * export type UMLModelCompat = UMLModelCompatV2 | UMLModelCompatV3; - * ``` - */ -export type UMLModelCompat = UMLModelCompatV2; +import { UMLModelCompat } from './typings'; +import { isV2, v2ModeltoV3Model } from './v2'; /** * @@ -35,3 +17,6 @@ export function backwardsCompatibleModel(model: UMLModelCompat): UMLModel { return model; } } + +export type { UMLModelCompat } from './typings'; +export * from './helpers'; diff --git a/src/main/compat/typings.ts b/src/main/compat/typings.ts new file mode 100644 index 000000000..52573ed69 --- /dev/null +++ b/src/main/compat/typings.ts @@ -0,0 +1,18 @@ +import { UMLModelCompat as UMLModelCompatV2 } from './v2'; + +/* + * + * HINT for future maintainers: + * + * this should always be the union of compatible model versions, i.e. + * if the model version is moved to V4, while support for V2 is still required, + * this should look like the following: + * + * ```ts + * export type UMLModelCompat = UMLModelCompatV2 | UMLModelCompatV3; + * ``` + */ +/** + * Represents the union of all compatible model versions. + */ +export type UMLModelCompat = UMLModelCompatV2; diff --git a/src/main/compat/v2/helpers.ts b/src/main/compat/v2/helpers.ts new file mode 100644 index 000000000..3eddfc139 --- /dev/null +++ b/src/main/compat/v2/helpers.ts @@ -0,0 +1,145 @@ +import { Assessment, UMLElement, UMLRelationship } from '../../typings'; +import { v2RelationshipToV3Relationship, v3RelaionshipToV2Relationship } from './transform'; +import { UMLModelV2 } from './typings'; + +/** + * + * Finds an element in the model by id + * + * @param {UMLModelV2} model the model to search + * @param {string} id the id of the element to find + * @returns {UMLElement | undefined} the element or undefined if not found + */ +export function findElement(model: UMLModelV2, id: string): UMLElement | undefined { + return model.elements.find((element) => element.id === id); +} + +/** + * + * Adds given element to given model. If element with same id already exists, it will be replaced. + * + * @param {UMLModelV2} model the model to update + * @param {UMLElement} element the element to add or update + */ +export function addOrUpdateElement(model: UMLModelV2, element: UMLElement): void { + const priorIndex = model.elements.findIndex((e) => e.id === element.id); + if (priorIndex >= 0) { + model.elements[priorIndex] = element; + } else { + model.elements.push(element); + } +} + +/** + * + * Finds a relationship in the model by id + * + * @param {UMLModelV2} model the model to search + * @param {string} id the id of the relationship to find + * @returns {UMLRelationship | undefined} the relationship or undefined if not found + */ +export function findRelationship(model: UMLModelV2, id: string): UMLRelationship | undefined { + const candidate = model.relationships.find((relationship) => relationship.id === id); + return candidate && v2RelationshipToV3Relationship(candidate); +} + +/** + * + * Adds given relationship to given model. If relationship with same id already exists, it will be replaced. + * + * @param {UMLModelV2} model the model to update + * @param {UMLRelationship} relationship the relationship to add or update + */ +export function addOrUpdateRelationship(model: UMLModelV2, relationship: UMLRelationship): void { + const v2rel = v3RelaionshipToV2Relationship(relationship); + const priorIndex = model.relationships.findIndex((r) => r.id === relationship.id); + if (priorIndex >= 0) { + model.relationships[priorIndex] = v2rel; + } else { + model.relationships.push(v2rel); + } +} + +/** + * + * Finds an assessment in the model by id + * + * @param {UMLModelV2} model the model to search + * @param {string} id the id of the assessment to find + * @returns {Assessment | undefined} the assessment or undefined if not found + */ +export function findAssessment(model: UMLModelV2, id: string): Assessment | undefined { + return model.assessments.find((assessment) => assessment.modelElementId === id); +} + +/** + * + * Adds given assessment to given model. If assessment with same id already exists, it will be replaced. + * + * @param {UMLModelV2} model the model to update + * @param {Assessment} assessment the assessment to add or update + */ +export function addOrUpdateAssessment(model: UMLModelV2, assessment: Assessment): void { + const priorIndex = model.assessments.findIndex((a) => a.modelElementId === assessment.modelElementId); + if (priorIndex >= 0) { + model.assessments[priorIndex] = assessment; + } else { + model.assessments.push(assessment); + } +} + +/** + * @returns true if given element is interactive, false otherwise + */ +export function isInteractiveElement(model: UMLModelV2, id: string): boolean { + return model.interactive.elements.includes(id); +} + +/** + * + * Sets given element interactive state to given value. + * + * @param {UMLModelV2} model the model to update + * @param {string} id the id of the element to update + * @param {boolean} interactive the interactive state to set + */ +export function setInteractiveElement(model: UMLModelV2, id: string, interactive: boolean): void { + if (interactive) { + if (!isInteractiveElement(model, id)) { + model.interactive.elements.push(id); + } + } else { + const index = model.interactive.elements.indexOf(id); + if (index >= 0) { + model.interactive.elements.splice(index, 1); + } + } +} + +/** + * @returns true if given relationship is interactive, false otherwise + */ +export function isInteractiveRelationship(model: UMLModelV2, id: string): boolean { + return model.interactive.relationships.includes(id); +} + +/** + * + * Sets given relationship interactive state to given value. + * + * @param {UMLModelV2} model the model to update + * @param {string} id the id of the relationship to update + * @param {boolean} interactive the interactive state to set + */ +export function setInteractiveRelationship(model: UMLModelV2, id: string, interactive: boolean): void { + if (interactive) { + if (!isInteractiveRelationship(model, id)) { + model.interactive.relationships.push(id); + } + } else { + const index = model.interactive.relationships.indexOf(id); + if (index >= 0) { + model.interactive.relationships.splice(index, 1); + } + } +} diff --git a/src/main/compat/v2/index.ts b/src/main/compat/v2/index.ts index c4682c131..38fdbf018 100644 --- a/src/main/compat/v2/index.ts +++ b/src/main/compat/v2/index.ts @@ -1,2 +1,3 @@ export * from './typings'; export * from './transform'; +export * from './helpers'; diff --git a/src/main/compat/v2/transform.ts b/src/main/compat/v2/transform.ts index a463c552f..3b6f0c0ac 100644 --- a/src/main/compat/v2/transform.ts +++ b/src/main/compat/v2/transform.ts @@ -1,5 +1,41 @@ -import { UMLCommunicationLink, UMLModel } from '../../typings'; -import { isCommunicationLink, UMLModelV2 } from './typings'; +import { UMLCommunicationLink, UMLModel, UMLRelationship } from '../../typings'; +import { isCommunicationLink, UMLCommunicationLinkV2, UMLModelV2, UMLRelationshipV2 } from './typings'; + +/** + * + * Converts a v2 relationshuip to a v3 relationship. + * + * @param {UMLRelationshipV2} relationship to convert + * @returns {UMLRelationship} the converted relationship + */ +export function v2RelationshipToV3Relationship(relationship: UMLRelationshipV2): UMLRelationship { + if (isCommunicationLink(relationship)) { + return { + ...relationship, + messages: relationship.messages.reduce((acc, val) => ({ ...acc, [val.id]: val }), {}), + } as UMLCommunicationLink; + } else { + return relationship; + } +} + +/** + * + * Converts a v3 relationship to a v2 relationship. + * + * @param {UMLRelationship} relationship to convert + * @returns {UMLRelationshipV2} the converted relationship + */ +export function v3RelaionshipToV2Relationship(relationship: UMLRelationship): UMLRelationshipV2 { + if (isCommunicationLink(relationship)) { + return { + ...relationship, + messages: Object.values(relationship.messages), + } as UMLCommunicationLinkV2; + } else { + return relationship; + } +} /** * @@ -20,16 +56,7 @@ export function v2ModeltoV3Model(model: UMLModelV2): UMLModel { version: '3.0.0', elements: elements.reduce((acc, val) => ({ ...acc, [val.id]: val }), {}), relationships: relationships - .map((relationship) => { - if (isCommunicationLink(relationship)) { - return { - ...relationship, - messages: relationship.messages.reduce((acc, val) => ({ ...acc, [val.id]: val }), {}), - } as UMLCommunicationLink; - } else { - return relationship; - } - }) + .map(v2RelationshipToV3Relationship) .reduce((acc, val) => ({ ...acc, [val.id]: val }), {}), assessments: assessments.reduce((acc, val) => ({ ...acc, [val.modelElementId]: val }), {}), interactive: { diff --git a/src/main/compat/v2/typings.ts b/src/main/compat/v2/typings.ts index 902be4c2c..11d083602 100644 --- a/src/main/compat/v2/typings.ts +++ b/src/main/compat/v2/typings.ts @@ -23,6 +23,13 @@ export type SelectionV2 = { relationships: string[]; }; +/** + * + * Represents the relationship type in V2 schema. + * + */ +export type UMLRelationshipV2 = UMLRelationship | UMLCommunicationLinkV2; + /** * * Represents the V2 model. @@ -40,10 +47,17 @@ export type UMLModelV2 = { size: { width: number; height: number }; elements: UMLElement[]; interactive: SelectionV2; - relationships: (UMLRelationship | UMLCommunicationLinkV2)[]; + relationships: UMLRelationshipV2[]; assessments: Assessment[]; }; +/** + * + * Represents a relationship compatible with either V2 or latest version. + * + */ +export type UMLRelationshipCompat = UMLRelationship | UMLRelationshipV2; + /** * * Represents a model compatible with either V2 or latest version. diff --git a/src/main/components/canvas/editor.tsx b/src/main/components/canvas/editor.tsx index 54fd6dd31..6b7f07645 100644 --- a/src/main/components/canvas/editor.tsx +++ b/src/main/components/canvas/editor.tsx @@ -164,14 +164,14 @@ class EditorComponent extends Component { touch.clientX * scale < clientRect.x + SCROLL_BORDER ? -SCROLL_DISTANCE : touch.clientX * scale > clientRect.x + clientRect.width - SCROLL_BORDER - ? SCROLL_DISTANCE - : 0; + ? SCROLL_DISTANCE + : 0; const scrollVertically = touch.clientY * scale < clientRect.y + SCROLL_BORDER ? -SCROLL_DISTANCE : touch.clientY * scale > clientRect.y + clientRect.height - SCROLL_BORDER - ? SCROLL_DISTANCE - : 0; + ? SCROLL_DISTANCE + : 0; this.editor.current.scrollBy(scrollHorizontally, scrollVertically); if (this.props.moving) { this.props.move({ x: scrollHorizontally, y: scrollVertically }, this.props.moving); diff --git a/src/main/components/controls/popover/popover-styles.ts b/src/main/components/controls/popover/popover-styles.ts index dd9ed91f3..a07acdffd 100644 --- a/src/main/components/controls/popover/popover-styles.ts +++ b/src/main/components/controls/popover/popover-styles.ts @@ -173,11 +173,11 @@ export const Arrow = styled.div` ? props.alignment === 'start' ? 'left: 0.3em;' : props.alignment === 'end' - ? 'right: 0.3em;' - : 'left: 50%; transform: translate(-50%, 0);' + ? 'right: 0.3em;' + : 'left: 50%; transform: translate(-50%, 0);' : props.alignment === 'start' - ? 'top: 0.3em;' - : props.alignment === 'end' - ? 'bottom: 0.3em;' - : 'top: 50%; transform: translate(0, -50%);'} + ? 'top: 0.3em;' + : props.alignment === 'end' + ? 'bottom: 0.3em;' + : 'top: 50%; transform: translate(0, -50%);'} `; diff --git a/src/main/components/store/model-state.ts b/src/main/components/store/model-state.ts index f99d8687e..858fa2551 100644 --- a/src/main/components/store/model-state.ts +++ b/src/main/components/store/model-state.ts @@ -27,6 +27,7 @@ import { UMLDiagram } from '../../services/uml-diagram/uml-diagram'; import { CopyState } from '../../services/copypaste/copy-types'; import { LastActionState } from '../../services/last-action/last-action-types'; import { arrayToInclusionMap, inclusionMapToArray } from './util'; +import { RemoteSelectionState } from '../../services/uml-element/remote-selectable/remote-selectable-types'; export type PartialModelState = Omit, 'editor'> & { editor?: Partial }; @@ -35,6 +36,7 @@ export interface ModelState { diagram: UMLDiagramState; hovered: HoverableState; selected: SelectableState; + remoteSelection: RemoteSelectionState; moving: MovableState; resizing: ResizableState; connecting: ConnectableState; diff --git a/src/main/components/uml-element/canvas-element.tsx b/src/main/components/uml-element/canvas-element.tsx index dac51641d..a2fb3f91b 100644 --- a/src/main/components/uml-element/canvas-element.tsx +++ b/src/main/components/uml-element/canvas-element.tsx @@ -10,6 +10,7 @@ import { UMLElementRepository } from '../../services/uml-element/uml-element-rep import { ModelState } from '../store/model-state'; import { withTheme, withThemeProps } from '../theme/styles'; import { UMLElementComponentProps } from './uml-element-component-props'; +import { UMLElementSelectorType } from '../../packages/uml-element-selector-type'; const STROKE = 5; @@ -19,6 +20,7 @@ type OwnProps = { child?: ComponentClass } & UMLElemen type StateProps = { hovered: boolean; selected: boolean; + remoteSelectors: UMLElementSelectorType[]; moving: boolean; interactive: boolean; interactable: boolean; @@ -36,6 +38,7 @@ const enhance = compose>( (state, props) => ({ hovered: state.hovered[0] === props.id, selected: state.selected.includes(props.id), + remoteSelectors: state.remoteSelection[props.id] || [], moving: state.moving.includes(props.id), interactive: state.interactive.includes(props.id), interactable: state.editor.view === ApollonView.Exporting || state.editor.view === ApollonView.Highlight, @@ -51,6 +54,7 @@ class CanvasElementComponent extends Component { const { hovered, selected, + remoteSelectors, moving, interactive, interactable, @@ -72,14 +76,13 @@ class CanvasElementComponent extends Component { interactable && interactive ? theme.interactive.normal : interactable && hovered - ? theme.interactive.hovered - : element.highlight - ? element.highlight - : element.fillColor - ? element.fillColor - : theme.color.background; + ? theme.interactive.hovered + : element.highlight + ? element.highlight + : element.fillColor + ? element.fillColor + : theme.color.background; - const selectedByList = element.selectedBy || []; return ( { pointerEvents="none" /> )} - {selectedByList.length > 0 && ( + {remoteSelectors.length > 0 && ( - {selectedByList.map((selectedBy, index) => { + {remoteSelectors.map((selectedBy, index) => { const indicatorPosition = 'translate(' + (element.bounds.width + STROKE) + ' ' + index * 32 + ')'; return ( diff --git a/src/main/components/uml-element/canvas-relationship.tsx b/src/main/components/uml-element/canvas-relationship.tsx index 0cde1c676..925a3d795 100644 --- a/src/main/components/uml-element/canvas-relationship.tsx +++ b/src/main/components/uml-element/canvas-relationship.tsx @@ -14,12 +14,14 @@ import { getClientEventCoordinates } from '../../utils/touch-event'; import { ModelState } from '../store/model-state'; import { withTheme, withThemeProps } from '../theme/styles'; import { UMLElementComponentProps } from './uml-element-component-props'; +import { UMLElementSelectorType } from '../../packages/uml-element-selector-type'; type OwnProps = UMLElementComponentProps & SVGProps; type StateProps = { hovered: boolean; selected: boolean; + remoteSelectors: UMLElementSelectorType[]; interactive: boolean; interactable: boolean; reconnecting: boolean; @@ -55,6 +57,7 @@ const enhance = compose>( (state, props) => ({ hovered: state.hovered[0] === props.id, selected: state.selected.includes(props.id), + remoteSelectors: state.remoteSelection[props.id] || [], interactive: state.interactive.includes(props.id), interactable: state.editor.view === ApollonView.Exporting || state.editor.view === ApollonView.Highlight, reconnecting: !!state.reconnecting[props.id], @@ -77,6 +80,7 @@ export class CanvasRelationshipComponent extends Component { const { hovered, selected, + remoteSelectors, interactive, interactable, reconnecting, @@ -112,12 +116,12 @@ export class CanvasRelationshipComponent extends Component { interactable && interactive ? theme.interactive.normal : interactable && hovered - ? theme.interactive.hovered - : hovered || selected - ? 'rgba(0, 100, 255, 0.2)' - : relationship.highlight - ? relationship.highlight - : 'rgba(0, 100, 255, 0)'; + ? theme.interactive.hovered + : hovered || selected + ? 'rgba(0, 100, 255, 0.2)' + : relationship.highlight + ? relationship.highlight + : 'rgba(0, 100, 255, 0)'; return ( { pointerEvents={disabled ? 'none' : 'stroke'} > + {remoteSelectors.length > 0 && + remoteSelectors.map((selector) => ( + + ))} {children} {midPoints.map((point, index) => { diff --git a/src/main/components/uml-element/connectable/connectable.tsx b/src/main/components/uml-element/connectable/connectable.tsx index 6776029fa..6cfbd42e4 100644 --- a/src/main/components/uml-element/connectable/connectable.tsx +++ b/src/main/components/uml-element/connectable/connectable.tsx @@ -99,10 +99,10 @@ const Handle = styled((props) => { direction === Direction.Up || direction === Direction.Topright || direction === Direction.Topleft ? 0 : direction === Direction.Right || direction === Direction.Upright || direction === Direction.Downright - ? 90 - : direction === Direction.Down || direction === Direction.Bottomright || direction === Direction.Bottomleft - ? 180 - : -90, + ? 90 + : direction === Direction.Down || direction === Direction.Bottomright || direction === Direction.Bottomleft + ? 180 + : -90, }))<{ rotate: number }>` cursor: crosshair; pointer-events: all; diff --git a/src/main/index.ts b/src/main/index.ts index eee928fe6..e46ddcabf 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,6 @@ export * from './typings'; export * from './apollon-editor'; +export * from './compat/helpers'; export type { Patch } from './services/patcher'; +export type { UMLModelCompat } from './compat'; diff --git a/src/main/packages/uml-communication-diagram/uml-communication-link/uml-communication-link-component.tsx b/src/main/packages/uml-communication-diagram/uml-communication-link/uml-communication-link-component.tsx index 28e65bbfb..594925b2b 100644 --- a/src/main/packages/uml-communication-diagram/uml-communication-link/uml-communication-link-component.tsx +++ b/src/main/packages/uml-communication-diagram/uml-communication-link/uml-communication-link-component.tsx @@ -35,8 +35,8 @@ export const UMLCommunicationLinkComponent: FunctionComponent = ({ elemen ? Direction.Left : Direction.Right : norm.y > 0 - ? Direction.Up - : Direction.Down; + ? Direction.Up + : Direction.Down; position = path[index].add(norm.scale(distance)); break; } diff --git a/src/main/packages/uml-communication-diagram/uml-communication-link/uml-communication-link.ts b/src/main/packages/uml-communication-diagram/uml-communication-link/uml-communication-link.ts index 11283dda8..eefe9bd64 100644 --- a/src/main/packages/uml-communication-diagram/uml-communication-link/uml-communication-link.ts +++ b/src/main/packages/uml-communication-diagram/uml-communication-link/uml-communication-link.ts @@ -77,8 +77,8 @@ export class UMLCommunicationLink extends UMLRelationship implements IUMLCommuni ? Direction.Left : Direction.Right : norm.y > 0 - ? Direction.Up - : Direction.Down; + ? Direction.Up + : Direction.Down; messagePosition = path[index].add(norm.scale(distance)); break; } @@ -157,15 +157,15 @@ export class UMLCommunicationLink extends UMLRelationship implements IUMLCommuni arrowDirection === Direction.Left ? messagePosition.y - arrowSize.height : arrowDirection === Direction.Right - ? messagePosition.y + Text.size(canvas, messages[0].name).height + arrowSize.height - : messagePosition.y; + ? messagePosition.y + Text.size(canvas, messages[0].name).height + arrowSize.height + : messagePosition.y; const x = arrowDirection === Direction.Up ? messagePosition.x + arrowSize.width : arrowDirection === Direction.Down - ? messagePosition.x - arrowSize.width - : messagePosition.x; + ? messagePosition.x - arrowSize.width + : messagePosition.x; for (const message of messages) { const messageSize = Text.size(canvas, message.name); diff --git a/src/main/packages/uml-element-selector-type.ts b/src/main/packages/uml-element-selector-type.ts index 3e8d20ef9..f96117409 100644 --- a/src/main/packages/uml-element-selector-type.ts +++ b/src/main/packages/uml-element-selector-type.ts @@ -1,5 +1,4 @@ export type UMLElementSelectorType = { - elementId: string; name: string; color: string; }; diff --git a/src/main/services/actions.ts b/src/main/services/actions.ts index 2ea92a7c3..4d6a2a3ea 100644 --- a/src/main/services/actions.ts +++ b/src/main/services/actions.ts @@ -11,6 +11,7 @@ import { MovingActions } from './uml-element/movable/moving-types'; import { ResizableActions } from './uml-element/resizable/resizable-types'; import { ResizingActions } from './uml-element/resizable/resizing-types'; import { SelectableActions } from './uml-element/selectable/selectable-types'; +import { RemoteSelectionActions } from './uml-element/remote-selectable/remote-selectable-types'; import { UMLElementActions } from './uml-element/uml-element-types'; import { UpdatableActions } from './uml-element/updatable/updatable-types'; import { ReconnectableActions } from './uml-relationship/reconnectable/reconnectable-types'; @@ -36,6 +37,7 @@ export type Actions = | ResizableActions | ResizingActions | SelectableActions + | RemoteSelectionActions | UpdatableActions | AssessmentActions | UndoActions diff --git a/src/main/services/reducer.ts b/src/main/services/reducer.ts index c641b849f..0c848226d 100644 --- a/src/main/services/reducer.ts +++ b/src/main/services/reducer.ts @@ -21,6 +21,7 @@ import { ReconnectableReducer } from './uml-relationship/reconnectable/reconnect import { UMLRelationshipReducer } from './uml-relationship/uml-relationship-reducer'; import { CopyReducer } from './copypaste/copy-reducer'; import { LastActionReducer } from './last-action/last-action-reducer'; +import { RemoteSelectionReducer } from './uml-element/remote-selectable/remote-selection-reducer'; const reduce = (intial: S, ...reducerList: Reducer[]): Reducer => @@ -40,6 +41,7 @@ export const reducers: ReducersMapObject = { updating: UpdatableReducer, copy: CopyReducer, lastAction: LastActionReducer, + remoteSelection: RemoteSelectionReducer, elements: reduce( {}, UMLContainerReducer, diff --git a/src/main/services/uml-element/remote-selectable/remote-selectable-types.ts b/src/main/services/uml-element/remote-selectable/remote-selectable-types.ts new file mode 100644 index 000000000..242d69e10 --- /dev/null +++ b/src/main/services/uml-element/remote-selectable/remote-selectable-types.ts @@ -0,0 +1,37 @@ +import { UMLElementSelectorType } from '../../../packages/uml-element-selector-type'; +import { Action } from '../../../utils/actions/actions'; +import { UMLElementState } from '../uml-element-types'; + +export const enum RemoteSelectionActionTypes { + SELECTION_CHANGE = '@@element/remote-selection/CHANGE', + PRUNE_SELECTORS = '@@element/remote-selection/PRUNE_SELECTORS', +} + +export const enum RemoteSelectionChangeTypes { + SELECT = '@@element/remote-selection/SELECT', + DESELECT = '@@element/remote-selection/DESELECT', +} + +export interface RemoteSelectionChange { + type: RemoteSelectionChangeTypes.SELECT | RemoteSelectionChangeTypes.DESELECT; + id: string; +} + +export type RemoteSelectionChangeAction = Action & { + payload: { + changes: RemoteSelectionChange[]; + selector: UMLElementSelectorType; + }; +}; + +export type RemoteSelectionPruneSelectorsAction = Action & { + payload: { + allowedSelectors: UMLElementSelectorType[]; + }; +}; + +export type RemoteSelectionState = { + [id: string]: UMLElementSelectorType[]; +}; + +export type RemoteSelectionActions = RemoteSelectionChangeAction | RemoteSelectionPruneSelectorsAction; diff --git a/src/main/services/uml-element/remote-selectable/remote-selection-reducer.ts b/src/main/services/uml-element/remote-selectable/remote-selection-reducer.ts new file mode 100644 index 000000000..c3974624b --- /dev/null +++ b/src/main/services/uml-element/remote-selectable/remote-selection-reducer.ts @@ -0,0 +1,50 @@ +import { Reducer } from 'redux'; +import { + RemoteSelectionActionTypes, + RemoteSelectionChangeTypes, + RemoteSelectionState, +} from './remote-selectable-types'; +import { Actions } from '../../actions'; +import { UMLElementSelectorType } from '../../../packages/uml-element-selector-type'; + +const sameSelector = (a: UMLElementSelectorType, b: UMLElementSelectorType) => { + return a.name === b.name && a.color === b.color; +}; + +export const RemoteSelectionReducer: Reducer = (state = {}, action) => { + switch (action.type) { + case RemoteSelectionActionTypes.SELECTION_CHANGE: + const { payload } = action; + const { selector, changes } = payload; + + return changes.reduce((selection, change) => { + const { id } = change; + const selectors: UMLElementSelectorType[] = [...(selection[id] ?? [])]; + + if (change.type === RemoteSelectionChangeTypes.SELECT && !selectors.some((s) => sameSelector(s, selector))) { + selectors.push(selector); + } else if (change.type === RemoteSelectionChangeTypes.DESELECT) { + const index = selectors.findIndex((s) => sameSelector(s, selector)); + if (index >= 0) { + selectors.splice(index, 1); + } + } + + return { + ...selection, + [id]: selectors, + }; + }, state); + + case RemoteSelectionActionTypes.PRUNE_SELECTORS: + const { allowedSelectors } = action.payload; + + return Object.fromEntries( + Object.entries(state).map(([id, selectors]) => { + return [id, selectors.filter((s) => allowedSelectors.some((selector) => sameSelector(s, selector)))]; + }), + ); + } + + return state; +}; diff --git a/src/main/services/uml-element/remote-selectable/remote-selection-repository.ts b/src/main/services/uml-element/remote-selectable/remote-selection-repository.ts new file mode 100644 index 000000000..f1f61e9ca --- /dev/null +++ b/src/main/services/uml-element/remote-selectable/remote-selection-repository.ts @@ -0,0 +1,52 @@ +import { UMLElementSelectorType } from '../../../packages/uml-element-selector-type'; +import { + RemoteSelectionActionTypes, + RemoteSelectionChangeTypes, + RemoteSelectionChange, + RemoteSelectionChangeAction, + RemoteSelectionPruneSelectorsAction, +} from './remote-selectable-types'; + +export const RemoteSelectable = { + remoteSelectionChange: ( + selector: UMLElementSelectorType, + changes: RemoteSelectionChange[], + ): RemoteSelectionChangeAction => ({ + type: RemoteSelectionActionTypes.SELECTION_CHANGE, + payload: { + selector, + changes, + }, + undoable: false, + }), + + remoteSelect: (selector: UMLElementSelectorType, ids: string[]): RemoteSelectionChangeAction => + RemoteSelectable.remoteSelectionChange( + selector, + ids.map((id) => ({ type: RemoteSelectionChangeTypes.SELECT, id })), + ), + + remoteDeselect: (selector: UMLElementSelectorType, ids: string[]): RemoteSelectionChangeAction => + RemoteSelectable.remoteSelectionChange( + selector, + ids.map((id) => ({ type: RemoteSelectionChangeTypes.DESELECT, id })), + ), + + remoteSelectDeselect: ( + selector: UMLElementSelectorType, + select: string[], + deselect: string[], + ): RemoteSelectionChangeAction => + RemoteSelectable.remoteSelectionChange(selector, [ + ...select.map((id) => ({ type: RemoteSelectionChangeTypes.SELECT, id })), + ...deselect.map((id) => ({ type: RemoteSelectionChangeTypes.DESELECT, id })), + ]), + + pruneRemoteSelectors: (allowedSelectors: UMLElementSelectorType[]): RemoteSelectionPruneSelectorsAction => ({ + type: RemoteSelectionActionTypes.PRUNE_SELECTORS, + payload: { + allowedSelectors, + }, + undoable: false, + }), +}; diff --git a/src/main/services/uml-element/uml-element-repository.ts b/src/main/services/uml-element/uml-element-repository.ts index b07438800..2e1af06cc 100644 --- a/src/main/services/uml-element/uml-element-repository.ts +++ b/src/main/services/uml-element/uml-element-repository.ts @@ -4,6 +4,7 @@ import { Interactable } from './interactable/interactable-repository'; import { Movable } from './movable/movable-repository'; import { Resizable } from './resizable/resizable-repository'; import { Selectable } from './selectable/selectable-repository'; +import { RemoteSelectable } from './remote-selectable/remote-selection-repository'; import { UMLElementCommonRepository } from './uml-element-common-repository'; import { Updatable } from './updatable/updatable-repository'; @@ -11,6 +12,7 @@ export const UMLElementRepository = { ...UMLElementCommonRepository, ...Hoverable, ...Selectable, + ...RemoteSelectable, ...Movable, ...Resizable, ...Connectable, diff --git a/src/main/services/uml-element/uml-element.ts b/src/main/services/uml-element/uml-element.ts index 11189385d..2d3d6aa45 100644 --- a/src/main/services/uml-element/uml-element.ts +++ b/src/main/services/uml-element/uml-element.ts @@ -33,7 +33,6 @@ export interface IUMLElement { textColor?: string; /** Note to show for element's assessment */ assessmentNote?: string; - selectedBy?: UMLElementSelectorType[]; isManuallyLayouted?: boolean; } @@ -91,7 +90,6 @@ export abstract class UMLElement implements IUMLElement, ILayoutable { strokeColor?: string; textColor?: string; assessmentNote?: string; - selectedBy?: UMLElementSelectorType[]; resizeFrom: ResizeFrom = ResizeFrom.BOTTOMRIGHT; constructor(values?: DeepPartial) { @@ -123,7 +121,6 @@ export abstract class UMLElement implements IUMLElement, ILayoutable { strokeColor: this.strokeColor, textColor: this.textColor, assessmentNote: this.assessmentNote, - selectedBy: this.selectedBy, }; } @@ -139,7 +136,6 @@ export abstract class UMLElement implements IUMLElement, ILayoutable { this.strokeColor = values.strokeColor; this.textColor = values.textColor; this.assessmentNote = values.assessmentNote; - this.selectedBy = values.selectedBy; } abstract render(canvas: ILayer): ILayoutable[]; diff --git a/src/main/typings.ts b/src/main/typings.ts index affd001b8..61f3c0bdb 100644 --- a/src/main/typings.ts +++ b/src/main/typings.ts @@ -58,7 +58,6 @@ export type UMLModelElement = { strokeColor?: string; textColor?: string; assessmentNote?: string; - selectedBy?: UMLElementSelectorType[]; }; export type UMLElement = UMLModelElement & { diff --git a/src/tests/unit/apollon-editor-test.tsx b/src/tests/unit/apollon-editor-test.tsx index 6c740d98b..c359f216a 100644 --- a/src/tests/unit/apollon-editor-test.tsx +++ b/src/tests/unit/apollon-editor-test.tsx @@ -441,6 +441,47 @@ describe('test apollon editor ', () => { }, 500); }, 500); }); + + it('remoteSelection.', () => { + const state = ModelState.fromModel(testClassDiagram as any); + const elements = Object.keys(state.elements!).map((id) => state.elements![id]); + const store = getRealStore(state, elements); + // inject store + Object.defineProperty(editor, 'store', { value: store }); + + const elA = 'c10b995a-036c-4e9e-aa67-0570ada5cb6a'; + const elB = '4d3509e-0dce-458b-bf62-f3555497a5a4'; + const john = { name: 'john', color: 'red' }; + const jane = { name: 'jane', color: 'blue' }; + + act(() => { + editor.remoteSelect(john.name, john.color, [elA]); + }); + + expect(store.getState().remoteSelection[elA]).toEqual([john]); + + act(() => { + editor.remoteSelect(jane.name, jane.color, [elA, elB]); + }); + + expect(store.getState().remoteSelection[elA]).toEqual([john, jane]); + expect(store.getState().remoteSelection[elB]).toEqual([jane]); + + act(() => { + editor.remoteSelect(john.name, john.color, [elB], [elA]); + }); + + expect(store.getState().remoteSelection[elA]).toEqual([jane]); + expect(store.getState().remoteSelection[elB]).toEqual([jane, john]); + + act(() => { + editor.pruneRemoteSelectors([john]); + }); + + expect(store.getState().remoteSelection[elA]).toEqual([]); + expect(store.getState().remoteSelection[elB]).toEqual([john]); + }); + it('set type to UseCaseDiagram', () => { act(() => { editor.type = UMLDiagramType.UseCaseDiagram; diff --git a/src/tests/unit/compat/v2/helper-test.ts b/src/tests/unit/compat/v2/helper-test.ts new file mode 100644 index 000000000..8b2e62401 --- /dev/null +++ b/src/tests/unit/compat/v2/helper-test.ts @@ -0,0 +1,183 @@ +import { deepClone } from 'fast-json-patch'; + +import { + UMLModelCompat, + findElement, + addOrUpdateElement, + findRelationship, + addOrUpdateRelationship, + findAssessment, + addOrUpdateAssessment, + isInteractiveElement, + setInteractiveElement, + isInteractiveRelationship, + setInteractiveRelationship, +} from '../../../../main/compat'; +import { Assessment, UMLElement, UMLModel, UMLRelationship } from '../../../../main'; + +import diagram from '../../test-resources/class-diagram-2.json'; +import diagramV2 from '../../test-resources/class-diagram-2-v2.json'; +import { Direction } from '../../../../main/services/uml-element/uml-element-port'; + +describe('test compat helpers for modifying diagrams.', () => { + let model: UMLModel; + let modelV2: UMLModelCompat; + + const packageId = 'c10b995a-036c-4e9e-aa67-0570ada5cb6a'; + const class1Id = '04d3509e-0dce-458b-bf62-f3555497a5a4'; + const class2Id = '9eadc4f6-caa0-4835-a24c-71c0c1ccbc39'; + const relId = 'f5c4e20d-8347-4136-bc02-b7a016e017f5'; + + beforeEach(() => { + model = deepClone(diagram); + modelV2 = deepClone(diagramV2); + }); + + test('can find elements.', () => { + expect(findElement(model, class1Id)?.owner).toEqual(packageId); + expect(findElement(modelV2, class1Id)?.owner).toEqual(packageId); + expect(findElement(model, 'non-existing-id')).toBeUndefined(); + expect(findElement(modelV2, 'non-existing-id')).toBeUndefined(); + }); + + test('can find relationships.', () => { + expect(findRelationship(model, relId)?.source.element).toEqual(class2Id); + expect(findRelationship(modelV2, relId)?.target.element).toEqual(class1Id); + expect(findRelationship(model, 'non-existing-id')).toBeUndefined(); + expect(findRelationship(modelV2, 'non-existing-id')).toBeUndefined(); + }); + + test('can find assessments.', () => { + expect(findAssessment(model, class1Id)?.score).toBe(10); + expect(findAssessment(modelV2, class1Id)?.score).toBe(10); + expect(findAssessment(model, 'non-existing-id')).toBeUndefined(); + expect(findAssessment(modelV2, 'non-existing-id')).toBeUndefined(); + }); + + test('can update existing elements or add new elements.', () => { + const newClass: UMLElement = { + id: 'new-class-id', + type: 'Class', + name: 'New Class', + owner: packageId, + bounds: { x: 0, y: 0, width: 100, height: 100 }, + }; + const newClass2: UMLElement = { + id: 'new-class-id', + type: 'Class', + name: 'New Class 2', + owner: packageId, + bounds: { x: 100, y: 100, width: 100, height: 100 }, + }; + + addOrUpdateElement(model, newClass); + addOrUpdateElement(modelV2, newClass); + + expect(findElement(model, newClass.id)).toEqual(newClass); + expect(findElement(modelV2, newClass.id)).toEqual(newClass); + + addOrUpdateElement(model, newClass2); + addOrUpdateElement(modelV2, newClass2); + + expect(findElement(model, newClass.id)).toEqual(newClass2); + expect(findElement(modelV2, newClass.id)).toEqual(newClass2); + }); + + test('can update existing relationships or add new relationships.', () => { + const newRel: UMLRelationship = { + id: 'new-rel-id', + owner: null, + type: 'ClassBidirectional', + source: { element: class1Id, direction: Direction.Up }, + target: { element: class2Id, direction: Direction.Down }, + path: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + bounds: { x: 0, y: 0, width: 100, height: 100 }, + name: 'New Relationship', + }; + const newRel2: UMLRelationship = { + id: 'new-rel-id', + owner: null, + type: 'ClassBidirectional', + source: { element: class2Id, direction: Direction.Up }, + target: { element: packageId, direction: Direction.Down }, + path: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + bounds: { x: 0, y: 0, width: 100, height: 100 }, + name: 'New Relationship 2', + }; + + addOrUpdateRelationship(model, newRel); + addOrUpdateRelationship(modelV2, newRel); + + expect(findRelationship(model, newRel.id)).toEqual(newRel); + expect(findRelationship(modelV2, newRel.id)).toEqual(newRel); + + addOrUpdateRelationship(model, newRel2); + addOrUpdateRelationship(modelV2, newRel2); + + expect(findRelationship(model, newRel.id)).toEqual(newRel2); + expect(findRelationship(modelV2, newRel.id)).toEqual(newRel2); + }); + + test('can update existing assessments or add new assessments.', () => { + const newAssessment: Assessment = { + modelElementId: class1Id, + elementType: 'Class', + score: 20, + }; + + addOrUpdateAssessment(model, newAssessment); + addOrUpdateAssessment(modelV2, newAssessment); + + expect(findAssessment(model, class1Id)?.score).toBe(20); + expect(findAssessment(modelV2, class1Id)?.score).toBe(20); + + const newAssessment2: Assessment = { + modelElementId: class2Id, + elementType: 'Class', + score: 30, + }; + + addOrUpdateAssessment(model, newAssessment2); + addOrUpdateAssessment(modelV2, newAssessment2); + + expect(findAssessment(model, class2Id)?.score).toBe(30); + expect(findAssessment(modelV2, class2Id)?.score).toBe(30); + }); + + test('can check and set element and relationship interactive state.', () => { + expect(isInteractiveElement(model, packageId)).toBe(true); + expect(isInteractiveElement(modelV2, packageId)).toBe(true); + expect(isInteractiveElement(model, class1Id)).toBe(false); + expect(isInteractiveElement(modelV2, class1Id)).toBe(false); + expect(isInteractiveElement(model, 'non-existing-id')).toBe(false); + expect(isInteractiveElement(modelV2, 'non-existing-id')).toBe(false); + expect(isInteractiveRelationship(model, relId)).toBe(false); + expect(isInteractiveRelationship(modelV2, relId)).toBe(false); + + setInteractiveElement(model, packageId, false); + setInteractiveElement(modelV2, packageId, false); + setInteractiveElement(model, class1Id, true); + setInteractiveElement(modelV2, class1Id, true); + setInteractiveRelationship(model, relId, true); + setInteractiveRelationship(modelV2, relId, true); + + expect(isInteractiveElement(model, packageId)).toBe(false); + expect(isInteractiveElement(modelV2, packageId)).toBe(false); + expect(isInteractiveElement(model, class1Id)).toBe(true); + expect(isInteractiveElement(modelV2, class1Id)).toBe(true); + expect(isInteractiveRelationship(model, relId)).toBe(true); + expect(isInteractiveRelationship(modelV2, relId)).toBe(true); + + setInteractiveRelationship(model, relId, false); + setInteractiveRelationship(modelV2, relId, false); + + expect(isInteractiveElement(model, relId)).toBe(false); + expect(isInteractiveElement(modelV2, relId)).toBe(false); + }); +}); diff --git a/src/tests/unit/services/uml-element/remote-selectable/remote-selectable-service-test.ts b/src/tests/unit/services/uml-element/remote-selectable/remote-selectable-service-test.ts new file mode 100644 index 000000000..6d4998901 --- /dev/null +++ b/src/tests/unit/services/uml-element/remote-selectable/remote-selectable-service-test.ts @@ -0,0 +1,32 @@ +import { getRealStore } from '../../../test-utils/test-utils'; +import { UMLClass } from '../../../../../main/packages/uml-class-diagram/uml-class/uml-class'; +import { RemoteSelectable } from '../../../../../main/services/uml-element/remote-selectable/remote-selection-repository'; + +describe('test redux state upon changing remote selection.', () => { + test('elements can be selected.', () => { + const classA = new UMLClass({ name: 'ClassA' }); + const classB = new UMLClass({ name: 'ClassB' }); + + const john = { name: 'John', color: 'red' }; + const jane = { name: 'Jane', color: 'blue' }; + + const store = getRealStore({}, [classA, classB]); + + expect(store.getState().remoteSelection).toEqual({}); + + store.dispatch(RemoteSelectable.remoteSelect(john, [classA.id])); + expect(store.getState().remoteSelection).toEqual({ [classA.id]: [john] }); + + store.dispatch(RemoteSelectable.remoteSelect(jane, [classA.id, classB.id])); + expect(store.getState().remoteSelection).toEqual({ [classA.id]: [john, jane], [classB.id]: [jane] }); + + store.dispatch(RemoteSelectable.remoteDeselect(jane, [classA.id])); + expect(store.getState().remoteSelection).toEqual({ [classA.id]: [john], [classB.id]: [jane] }); + + store.dispatch(RemoteSelectable.remoteSelectDeselect(john, [classB.id], [classA.id])); + expect(store.getState().remoteSelection).toEqual({ [classA.id]: [], [classB.id]: [jane, john] }); + + store.dispatch(RemoteSelectable.pruneRemoteSelectors([jane])); + expect(store.getState().remoteSelection).toEqual({ [classA.id]: [], [classB.id]: [jane] }); + }); +}); diff --git a/src/tests/unit/test-resources/class-diagram-2-v2.json b/src/tests/unit/test-resources/class-diagram-2-v2.json new file mode 100644 index 000000000..ffc7c86db --- /dev/null +++ b/src/tests/unit/test-resources/class-diagram-2-v2.json @@ -0,0 +1,97 @@ +{ + "version": "2.0.0", + "type": "ClassDiagram", + "size": { "width": 860, "height": 260 }, + "interactive": { "elements": ["c10b995a-036c-4e9e-aa67-0570ada5cb6a"], "relationships": [] }, + "elements": [ + { + "id": "c10b995a-036c-4e9e-aa67-0570ada5cb6a", + "name": "Package", + "type": "Package", + "owner": null, + "bounds": { "x": 0, "y": 0, "width": 350, "height": 220 } + }, + { + "id": "04d3509e-0dce-458b-bf62-f3555497a5a4", + "name": "Class", + "type": "Class", + "owner": "c10b995a-036c-4e9e-aa67-0570ada5cb6a", + "bounds": { "x": 80, "y": 70, "width": 200, "height": 100 }, + "attributes": ["90f94404-1fc6-4121-97ed-6b2c6d57d525"], + "methods": ["12d8bb82-e4e5-4505-a0eb-48ddab0fc0a0"] + }, + { + "id": "90f94404-1fc6-4121-97ed-6b2c6d57d525", + "name": "+ attribute: Type", + "type": "ClassAttribute", + "owner": "04d3509e-0dce-458b-bf62-f3555497a5a4", + "bounds": { "x": 80, "y": 110, "width": 200, "height": 30 } + }, + { + "id": "12d8bb82-e4e5-4505-a0eb-48ddab0fc0a0", + "name": "+ method()", + "type": "ClassMethod", + "owner": "04d3509e-0dce-458b-bf62-f3555497a5a4", + "bounds": { "x": 80, "y": 140, "width": 200, "height": 30 } + }, + { + "id": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39", + "name": "Class", + "type": "Class", + "owner": null, + "bounds": { "x": 620, "y": 90, "width": 200, "height": 100 }, + "attributes": ["dbd4193a-4483-43df-8934-77192be006c2"], + "methods": ["e7ef41ee-290e-4df2-a535-199f1c5521fd"] + }, + { + "id": "dbd4193a-4483-43df-8934-77192be006c2", + "name": "+ attribute: Type", + "type": "ClassAttribute", + "owner": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39", + "bounds": { "x": 620, "y": 130, "width": 200, "height": 30 } + }, + { + "id": "e7ef41ee-290e-4df2-a535-199f1c5521fd", + "name": "+ method()", + "type": "ClassMethod", + "owner": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39", + "bounds": { "x": 620, "y": 160, "width": 200, "height": 30 } + } + ], + "relationships": [ + { + "id": "f5c4e20d-8347-4136-bc02-b7a016e017f5", + "name": "", + "type": "ClassBidirectional", + "owner": null, + "bounds": { "x": 280, "y": 130, "width": 340, "height": 1 }, + "path": [ + { "x": 340, "y": 0 }, + { "x": 0, "y": 0 } + ], + "source": { + "direction": "Left", + "element": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39", + "multiplicity": "", + "role": "" + }, + "target": { + "direction": "Right", + "element": "04d3509e-0dce-458b-bf62-f3555497a5a4", + "multiplicity": "", + "role": "" + }, + "isManuallyLayouted": false + } + ], + "assessments": [ + { + "modelElementId": "04d3509e-0dce-458b-bf62-f3555497a5a4", + "elementType": "Class", + "score": 10, + "feedback": "good", + "label": "Class", + "labelColor": "blue" + } + ] +} diff --git a/src/tests/unit/test-resources/class-diagram-2.json b/src/tests/unit/test-resources/class-diagram-2.json new file mode 100644 index 000000000..d947d0fd1 --- /dev/null +++ b/src/tests/unit/test-resources/class-diagram-2.json @@ -0,0 +1,102 @@ +{ + "version": "3.0.0", + "type": "ClassDiagram", + "size": { "width": 860, "height": 260 }, + "interactive": { + "elements": { + "c10b995a-036c-4e9e-aa67-0570ada5cb6a": true + }, + "relationships": {} + }, + "elements": { + "c10b995a-036c-4e9e-aa67-0570ada5cb6a": { + "id": "c10b995a-036c-4e9e-aa67-0570ada5cb6a", + "name": "Package", + "type": "Package", + "owner": null, + "bounds": { "x": 0, "y": 0, "width": 350, "height": 220 } + }, + "04d3509e-0dce-458b-bf62-f3555497a5a4": { + "id": "04d3509e-0dce-458b-bf62-f3555497a5a4", + "name": "Class", + "type": "Class", + "owner": "c10b995a-036c-4e9e-aa67-0570ada5cb6a", + "bounds": { "x": 80, "y": 70, "width": 200, "height": 100 }, + "attributes": ["90f94404-1fc6-4121-97ed-6b2c6d57d525"], + "methods": ["12d8bb82-e4e5-4505-a0eb-48ddab0fc0a0"] + }, + "90f94404-1fc6-4121-97ed-6b2c6d57d525": { + "id": "90f94404-1fc6-4121-97ed-6b2c6d57d525", + "name": "+ attribute: Type", + "type": "ClassAttribute", + "owner": "04d3509e-0dce-458b-bf62-f3555497a5a4", + "bounds": { "x": 80, "y": 110, "width": 200, "height": 30 } + }, + "12d8bb82-e4e5-4505-a0eb-48ddab0fc0a0": { + "id": "12d8bb82-e4e5-4505-a0eb-48ddab0fc0a0", + "name": "+ method()", + "type": "ClassMethod", + "owner": "04d3509e-0dce-458b-bf62-f3555497a5a4", + "bounds": { "x": 80, "y": 140, "width": 200, "height": 30 } + }, + "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39": { + "id": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39", + "name": "Class", + "type": "Class", + "owner": null, + "bounds": { "x": 620, "y": 90, "width": 200, "height": 100 }, + "attributes": ["dbd4193a-4483-43df-8934-77192be006c2"], + "methods": ["e7ef41ee-290e-4df2-a535-199f1c5521fd"] + }, + "dbd4193a-4483-43df-8934-77192be006c2": { + "id": "dbd4193a-4483-43df-8934-77192be006c2", + "name": "+ attribute: Type", + "type": "ClassAttribute", + "owner": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39", + "bounds": { "x": 620, "y": 130, "width": 200, "height": 30 } + }, + "e7ef41ee-290e-4df2-a535-199f1c5521fd": { + "id": "e7ef41ee-290e-4df2-a535-199f1c5521fd", + "name": "+ method()", + "type": "ClassMethod", + "owner": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39", + "bounds": { "x": 620, "y": 160, "width": 200, "height": 30 } + } + }, + "relationships": { + "f5c4e20d-8347-4136-bc02-b7a016e017f5": { + "id": "f5c4e20d-8347-4136-bc02-b7a016e017f5", + "name": "", + "type": "ClassBidirectional", + "owner": null, + "bounds": { "x": 280, "y": 130, "width": 340, "height": 1 }, + "path": [ + { "x": 340, "y": 0 }, + { "x": 0, "y": 0 } + ], + "source": { + "direction": "Left", + "element": "9eadc4f6-caa0-4835-a24c-71c0c1ccbc39", + "multiplicity": "", + "role": "" + }, + "target": { + "direction": "Right", + "element": "04d3509e-0dce-458b-bf62-f3555497a5a4", + "multiplicity": "", + "role": "" + }, + "isManuallyLayouted": false + } + }, + "assessments": { + "04d3509e-0dce-458b-bf62-f3555497a5a4": { + "modelElementId": "04d3509e-0dce-458b-bf62-f3555497a5a4", + "elementType": "Class", + "score": 10, + "feedback": "good", + "label": "Class", + "labelColor": "blue" + } + } +} diff --git a/src/tests/unit/test-utils/test-utils.tsx b/src/tests/unit/test-utils/test-utils.tsx index 590d17024..33506fc9b 100644 --- a/src/tests/unit/test-utils/test-utils.tsx +++ b/src/tests/unit/test-utils/test-utils.tsx @@ -34,6 +34,7 @@ const createModelStateFromPartialModelState = ( reconnecting: partialModelState?.reconnecting ? partialModelState.reconnecting : {}, resizing: partialModelState?.resizing ? partialModelState.resizing : [], selected: partialModelState?.selected ? partialModelState.selected : [], + remoteSelection: partialModelState?.remoteSelection ? partialModelState.remoteSelection : {}, updating: partialModelState?.updating ? partialModelState.updating : [], hovered: partialModelState?.hovered ? partialModelState.hovered : [], editor: {