diff --git a/.storybook/global.scss b/.storybook/global.scss index 47271789..13a9520a 100644 --- a/.storybook/global.scss +++ b/.storybook/global.scss @@ -4,10 +4,7 @@ html { } .sb-show-main { - background: url(https://applescoop.org/image/wallpapers/mac/windows-10-stock-hd-cells-glowing-dark-22-09-2024-1727068449-hd-wallpaper.jpeg); - transition: background-color 0.3s; - background-position: center; - background-size: cover; + background: #030014 !important; } .sb-main-fullscreen { diff --git a/package-lock.json b/package-lock.json index 76a6e43e..cf21cf0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,15 +11,17 @@ "@ariakit/react": "^0.4.17", "@babel/plugin-proposal-decorators": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", + "@code0-tech/sagittarius-graphql-types": "^0.0.0-6c84ca6a692d3341e857f740343aead437ab4427", + "@dagrejs/dagre": "^1.1.5", "@mdx-js/react": "^3.1.0", - "@radix-ui/react-checkbox": "^1.3.2", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-one-time-password-field": "^0.1.7", - "@radix-ui/react-radio-group": "^1.3.7", - "@radix-ui/react-scroll-area": "^1.2.9", - "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-toggle-group": "^1.1.10", - "@radix-ui/react-tooltip": "^1.2.7", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-one-time-password-field": "^0.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-terser": "^0.4.4", @@ -41,6 +43,7 @@ "@tabler/icons-react": "3.34.1", "@types/react": "^19.1.8", "@vitejs/plugin-react": "^4.6.0", + "@xyflow/react": "^12.8.2", "axe-playwright": "^2.1.0", "babel-loader": "^9.2.1", "babel-plugin-react-compiler": "^19.1.0-rc.2", @@ -70,13 +73,16 @@ }, "peerDependencies": { "@ariakit/react": "^0.4.5", - "@radix-ui/react-checkbox": "^1.3.2", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-one-time-password-field": "^0.1.7", - "@radix-ui/react-radio-group": "^1.3.7", - "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-one-time-password-field": "^0.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", "@tabler/icons-react": "^3.5.0", + "@xyflow/react": "^12.8.2", "js-md5": "^0.8.3", "merge-props": "^6.0.0", "overlap-area": "^1.1.0", @@ -2274,6 +2280,12 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@code0-tech/sagittarius-graphql-types": { + "version": "0.0.0-6c84ca6a692d3341e857f740343aead437ab4427", + "resolved": "https://registry.npmjs.org/@code0-tech/sagittarius-graphql-types/-/sagittarius-graphql-types-0.0.0-6c84ca6a692d3341e857f740343aead437ab4427.tgz", + "integrity": "sha512-TxZN7tdN5cds0ExzJlY22GnE/tUagP6ke1okeWMkCM/cpPCd4l5gw2NiOpyJh/giP4tDZyz1h8hTopSQMbxPfQ==", + "dev": true + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2302,6 +2314,26 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@dagrejs/dagre": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.5.tgz", + "integrity": "sha512-Ghgrh08s12DCL5SeiR6AoyE80mQELTWhJBRmXfFoqDiFkR458vPEdgTbbjA0T+9ETNxUblnD0QW55tfdvi5pjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dagrejs/graphlib": "2.2.4" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz", + "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">17.0.0" + } + }, "node_modules/@daybrush/utils": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/@daybrush/utils/-/utils-1.13.0.tgz", @@ -2709,9 +2741,9 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", - "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2719,24 +2751,24 @@ } }, "node_modules/@floating-ui/dom": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", - "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.2", + "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", - "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.2" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -3886,9 +3918,9 @@ "license": "MIT" }, "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "dev": true, "license": "MIT" }, @@ -3917,16 +3949,16 @@ } }, "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", - "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", @@ -4023,13 +4055,13 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", - "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", @@ -4051,17 +4083,17 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", - "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, @@ -4081,9 +4113,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4142,26 +4174,26 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", - "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", @@ -4183,20 +4215,20 @@ } }, "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.7.tgz", - "integrity": "sha512-w1vm7AGI8tNXVovOK7TYQHrAGpRF7qQL+ENpT1a743De5Zmay2RbWGKAiYDKIyIuqptns+znCKwNztE2xl1n0Q==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", "dev": true, "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", @@ -4218,9 +4250,9 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "dev": true, "license": "MIT", "dependencies": { @@ -4276,9 +4308,9 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4325,19 +4357,19 @@ } }, "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz", - "integrity": "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" @@ -4358,13 +4390,13 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -4390,18 +4422,18 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz", - "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", "dev": true, "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -4441,19 +4473,19 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", - "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { @@ -4472,13 +4504,13 @@ } }, "node_modules/@radix-ui/react-toggle": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.9.tgz", - "integrity": "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, @@ -4498,18 +4530,18 @@ } }, "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.10.tgz", - "integrity": "sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", - "@radix-ui/react-toggle": "1.1.9", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { @@ -4528,20 +4560,20 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", - "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", @@ -6560,6 +6592,61 @@ "@types/node": "*" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/doctrine": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", @@ -7116,6 +7203,40 @@ "dev": true, "peer": true }, + "node_modules/@xyflow/react": { + "version": "12.8.4", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz", + "integrity": "sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.68", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.68", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.68.tgz", + "integrity": "sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -8036,6 +8157,13 @@ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "dev": true, + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -8538,6 +8666,120 @@ "node": ">=0.8" } }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -17284,6 +17526,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index af17f19f..42d58895 100644 --- a/package.json +++ b/package.json @@ -21,15 +21,17 @@ "@ariakit/react": "^0.4.17", "@babel/plugin-proposal-decorators": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", + "@code0-tech/sagittarius-graphql-types": "^0.0.0-6c84ca6a692d3341e857f740343aead437ab4427", + "@dagrejs/dagre": "^1.1.5", "@mdx-js/react": "^3.1.0", - "@radix-ui/react-checkbox": "^1.3.2", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-one-time-password-field": "^0.1.7", - "@radix-ui/react-radio-group": "^1.3.7", - "@radix-ui/react-scroll-area": "^1.2.9", - "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-toggle-group": "^1.1.10", - "@radix-ui/react-tooltip": "^1.2.7", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-one-time-password-field": "^0.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-terser": "^0.4.4", @@ -51,6 +53,7 @@ "@tabler/icons-react": "3.34.1", "@types/react": "^19.1.8", "@vitejs/plugin-react": "^4.6.0", + "@xyflow/react": "^12.8.2", "axe-playwright": "^2.1.0", "babel-loader": "^9.2.1", "babel-plugin-react-compiler": "^19.1.0-rc.2", @@ -85,13 +88,16 @@ "types": "dist/index.d.ts", "peerDependencies": { "@ariakit/react": "^0.4.5", - "@radix-ui/react-checkbox": "^1.3.2", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-one-time-password-field": "^0.1.7", - "@radix-ui/react-radio-group": "^1.3.7", - "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-one-time-password-field": "^0.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", "@tabler/icons-react": "^3.5.0", + "@xyflow/react": "^12.8.2", "js-md5": "^0.8.3", "merge-props": "^6.0.0", "overlap-area": "^1.1.0", diff --git a/src/components/badge/Badge.style.scss b/src/components/badge/Badge.style.scss index 207fc066..233a9d90 100644 --- a/src/components/badge/Badge.style.scss +++ b/src/components/badge/Badge.style.scss @@ -7,6 +7,7 @@ display: inline-flex; font-size: variables.$xs; width: fit-content; + align-items: center; & { @include helpers.fontStyle(); diff --git a/src/components/button/Button.style.scss b/src/components/button/Button.style.scss index 0f285887..c64607ff 100644 --- a/src/components/button/Button.style.scss +++ b/src/components/button/Button.style.scss @@ -25,6 +25,7 @@ @include box.boxActive($color, variables.$white, variables.$white); &.button--outlined { + background: transparent; //@include helpers.glassBox($color, variables.$white, $color); } diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index 298ea00d..947475cc 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -19,7 +19,7 @@ export interface ButtonProps extends Code0Component { const Button: React.FC = React.forwardRef((props, ref) => { - const {children, variant = "outlined", color = "secondary", active = false, disabled = false, ...args} = props + const {children, variant = "normal", color = "secondary", active = false, disabled = false, ...args} = props return ) + + + return +}) \ No newline at end of file diff --git a/src/components/d-flow/type/DFlowType.data.ts b/src/components/d-flow/type/DFlowType.data.ts index b40b5ee4..03a9c77e 100644 --- a/src/components/d-flow/type/DFlowType.data.ts +++ b/src/components/d-flow/type/DFlowType.data.ts @@ -1,5 +1,6 @@ import {FlowType} from "./DFlowType.view"; -import {EDataType, EDataTypeRuleType} from "../data-type/DFlowDataType.view"; +import {EDataType} from "../data-type/DFlowDataType.view"; +import {EDataTypeRuleType} from "../data-type/rules/DFlowDataTypeRules"; export const REST_FLOW_TYPE: FlowType = { flow_type_id: "REST", diff --git a/src/components/d-flow/type/DFlowType.service.ts b/src/components/d-flow/type/DFlowType.service.ts index e69de29b..1653c659 100644 --- a/src/components/d-flow/type/DFlowType.service.ts +++ b/src/components/d-flow/type/DFlowType.service.ts @@ -0,0 +1,18 @@ +import {ReactiveArrayService, ReactiveArrayStore} from "../../../utils/reactiveArrayService"; +import {FlowTypeView} from "./DFlowType.view"; + +export interface DFlowTypeService { + getById(id: string): FlowTypeView | undefined +} + +export class DFlowTypeReactiveService extends ReactiveArrayService implements DFlowTypeService { + + constructor(store: ReactiveArrayStore) { + super(store); + } + + getById(id: string): FlowTypeView | undefined { + return this.values().find(value => value.id === id); + } + +} \ No newline at end of file diff --git a/src/components/d-flow/type/DFlowType.view.ts b/src/components/d-flow/type/DFlowType.view.ts index 0eac4235..f5e1faf8 100644 --- a/src/components/d-flow/type/DFlowType.view.ts +++ b/src/components/d-flow/type/DFlowType.view.ts @@ -1,22 +1,66 @@ -import {Translation} from "../../../utils/translation"; -import {DataTypeObject} from "../data-type/DFlowDataType.view"; - -export interface FlowType { - flow_type_id: string - name: Translation[] - description: Translation[] - documentation?: Translation[] //as markdown - settings: FlowTypeSetting[] - input_type?: DataTypeObject // data type - return_type?: DataTypeObject -} - -export interface FlowTypeSetting { - flow_definition_settings_id: string - key: string - name: Translation[] - unique: boolean - description: Translation[] - type: string // data type id - default_value?: object +import {DataType, FlowType, Maybe, Scalars, TranslationConnection} from "@code0-tech/sagittarius-graphql-types"; + + +export class FlowTypeView { + + private readonly _id: Scalars['TypesFlowTypeID']['output'] + private readonly _identifier: Scalars['String']['output'] + private readonly _createdAt: Scalars['Time']['output'] + private readonly _updatedAt: Scalars['Time']['output'] + private readonly _descriptions: Maybe | undefined + private readonly _names: Maybe | undefined + private readonly _editable: Scalars['Boolean']['output'] + private readonly _inputType: Maybe | undefined + private readonly _returnType: Maybe | undefined + //TODO: settings but their is a problem with type in settings + + + constructor(flowType: FlowType) { + this._id = flowType.id + this._identifier = flowType.identifier + this._createdAt = flowType.createdAt + this._updatedAt = flowType.updatedAt + this._descriptions = flowType.descriptions ?? undefined + this._names = flowType.names ?? undefined + this._editable = flowType.editable + this._inputType = flowType.inputType ?? undefined + this._returnType = flowType.returnType ?? undefined + } + + + get id(): Scalars["TypesFlowTypeID"]["output"] { + return this._id; + } + + get identifier(): Scalars["String"]["output"] { + return this._identifier; + } + + get createdAt(): Scalars["Time"]["output"] { + return this._createdAt; + } + + get updatedAt(): Scalars["Time"]["output"] { + return this._updatedAt; + } + + get descriptions(): Maybe | undefined { + return this._descriptions; + } + + get names(): Maybe | undefined { + return this._names; + } + + get editable(): Scalars["Boolean"]["output"] { + return this._editable; + } + + get inputType(): Maybe | undefined { + return this._inputType; + } + + get returnType(): Maybe | undefined { + return this._returnType; + } } \ No newline at end of file diff --git a/src/components/d-flow/viewport/DFlowViewport.edges.hook.ts b/src/components/d-flow/viewport/DFlowViewport.edges.hook.ts new file mode 100644 index 00000000..31e3a357 --- /dev/null +++ b/src/components/d-flow/viewport/DFlowViewport.edges.hook.ts @@ -0,0 +1,234 @@ +import {useService, useStore} from "../../../utils/contextStore"; +import {DFlowReactiveService} from "../DFlow.service"; +import {Edge} from "@xyflow/react"; +import {NodeFunctionView} from "../DFlow.view"; +import {DFlowFunctionReactiveService} from "../function/DFlowFunction.service"; +import {DFlowDataTypeReactiveService} from "../data-type/DFlowDataType.service"; +import {EDataType, Type} from "../data-type/DFlowDataType.view"; +import React, {memo} from "react"; + +// Deine Primärfarbe als Start, danach harmonisch verteilt +export const FLOW_EDGE_RAINBOW: string[] = [ + '#70ffb2', // 0 – Primary (Grün) + '#70e2ff', // 1 – Cyan + '#709aff', // 2 – Blau + '#a170ff', // 3 – Violett + '#f170ff', // 4 – Magenta + '#ff70b5', // 5 – Pink/Rot + '#ff7070', // 6 – Orange-Rot + '#fff170', // 7 – Gelb +]; + +export const useFlowViewportEdges = (flowId: string): Edge[] => { + const flowService = useService(DFlowReactiveService); + const functionService = useService(DFlowFunctionReactiveService); + const dataTypeService = useService(DFlowDataTypeReactiveService); + const flow = flowService.getById(flowId); + const flowStore = useStore(DFlowReactiveService) + const functionStore = useStore(DFlowFunctionReactiveService) + const dataTypeStore = useStore(DFlowDataTypeReactiveService) + + if (!flow) return []; + + /* ------------------------------------------------------------------ */ + const edges: Edge[] = []; + + /** merkt sich für jede Function-Card die Gruppen-IDs, + * **für die wirklich ein Funktions-Wert existiert** */ + const groupsWithValue = new Map(); + + let idCounter = 0; // globale, fortlaufende Id-Vergabe + + const functionCache = new Map>(); + const dataTypeCache = new Map>(); + + const getFunctionDefinitionCached = ( + id: string, + cache = functionCache, + ) => { + if (!cache.has(id)) { + cache.set(id, functionService.getFunctionDefinition(id)); + } + return cache.get(id); + }; + + const getDataTypeCached = ( + type: Type, + cache = dataTypeCache, + ) => { + if (!cache.has(type)) { + cache.set(type, dataTypeService.getDataType(type)); + } + return cache.get(type); + }; + + /* ------------------------------------------------------------------ */ + const traverse = ( + fn: NodeFunctionView, + parentFnId?: string, // Id der *Function-Card* des Aufrufers + level = 0, // Tiefe ⇒ Farbe aus dem Rainbow-Array, + fnCache = functionCache, + dtCache = dataTypeCache, + ): string => { + + /* ------- Id der aktuellen Function-Card im Diagramm ---------- */ + const fnId = `${fn.runtime_id}-${idCounter++}`; + + if (idCounter == 1) { + // erste Function-Card → Verbindung Trigger → Function + edges.push({ + id: `trigger-${fnId}-next`, + source: flow.id, // Handle-Bottom des Trigger-Nodes + target: fnId, // Handle-Top der Function-Card + data: { + color: FLOW_EDGE_RAINBOW[level % FLOW_EDGE_RAINBOW.length], + isParameter: false + }, + deletable: false, + selectable: false, + }); + } + + /* ------- vertikale Kante (nextNode) -------------------------- */ + if (parentFnId) { + const startGroups = groupsWithValue.get(parentFnId) ?? []; + + if (startGroups.length > 0) { + startGroups.forEach((gId, idx) => edges.push({ + id: `${gId}-${fnId}-next-${idx}`, + source: gId, // Handle-Bottom der Group-Card + target: fnId, // Handle-Top der Function-Card + data: { + color: FLOW_EDGE_RAINBOW[level % FLOW_EDGE_RAINBOW.length], + isParameter: false + }, + deletable: false, + selectable: false, + })); + } else { + edges.push({ + id: `${parentFnId}-${fnId}-next`, + source: parentFnId, // Handle-Bottom der Function-Card + target: fnId, // Handle-Top der Function-Card + data: { + color: FLOW_EDGE_RAINBOW[level % FLOW_EDGE_RAINBOW.length], + isParameter: false + }, + deletable: false, + selectable: false, + }); + } + } + + /* ------- horizontale Kanten für Parameter -------------------- */ + fn.parameters?.forEach((param) => { + const val = param.value; + const def = getFunctionDefinitionCached(fn.id, fnCache) + ?.parameters?.find(p => p.parameter_id === param.id); + const paramType = def?.type; + const paramDT = paramType ? getDataTypeCached(paramType, dtCache) : undefined; + + if (!val) return + + /* --- NODE-Parameter → Group-Card ------------------------- */ + if (paramDT?.type === EDataType.NODE) { + const groupId = `${fnId}-group-${idCounter++}`; + + /* Verbindung Gruppe → Function-Card (horizontal) */ + edges.push({ + id: `${fnId}-${groupId}-param-${param.id}`, + source: fnId, // FunctionCard (Quelle) + target: groupId, // GroupCard (Ziel – hat Top: target) + deletable: false, + selectable: false, + label: param.id, + data: { + color: FLOW_EDGE_RAINBOW[level % FLOW_EDGE_RAINBOW.length], + isParameter: false, + }, + }); + + /* existiert ein Funktions-Wert für dieses Param-Feld? */ + if (val && val instanceof NodeFunctionView) { + /* merken: diese Group-Card besitzt Content – das ist + später Startpunkt der next-Kante */ + (groupsWithValue.get(fnId) ?? (groupsWithValue.set(fnId, []), + groupsWithValue.get(fnId)!)) + .push(groupId); + + /* rekursiv Funktions-Ast innerhalb der Gruppe */ + traverse(param.value as NodeFunctionView, + undefined, + level + 1, + fnCache, + dtCache); + } + } + + /* --- anderer Parameter, der selbst eine Function hält ---- */ + else if (val && val instanceof NodeFunctionView) { + const subFnId = traverse(param.value as NodeFunctionView, + undefined, + level + 1, + fnCache, + dtCache); + + edges.push({ + id: `${subFnId}-${fnId}-param-${param.id}`, + source: subFnId, + target: fnId, + targetHandle: `param-${param.id}`, + animated: true, + deletable: false, + selectable: false, + data: { + color: FLOW_EDGE_RAINBOW[(level + 1) % FLOW_EDGE_RAINBOW.length], + isParameter: true + }, + }); + } + }); + + /* ------- Rekursion auf nextNode ------------------------------ */ + if (fn.nextNode) { + traverse(fn.nextNode, fnId, level, fnCache, dtCache); // gleiche Ebenentiefe + } else { + // letzte Function-Card im Ast → Add-new-node wie *normale* nextNode behandeln + const suggestionNodeId = `${fnId}-suggestion`; + const startGroups = groupsWithValue.get(fnId) ?? []; + + if (startGroups.length > 0) { + startGroups.forEach((gId, idx) => edges.push({ + id: `${gId}-${suggestionNodeId}-next-${idx}`, + source: gId, // wie bei echter nextNode von Group-Card starten + target: suggestionNodeId, // Ziel ist die Suggestion-Card + data: { + color: FLOW_EDGE_RAINBOW[level % FLOW_EDGE_RAINBOW.length], + isSuggestion: true, + }, + deletable: false, + selectable: false, + })); + } else { + edges.push({ + id: `${fnId}-${suggestionNodeId}-next`, + source: fnId, // Handle-Bottom der Function-Card + target: suggestionNodeId, // Handle-Top der Suggestion-Card + data: { + color: FLOW_EDGE_RAINBOW[level % FLOW_EDGE_RAINBOW.length], + isSuggestion: true, + }, + deletable: false, + selectable: false, + }); + } + } + + return fnId; + }; + + /* ------------------------------------------------------------------ */ + traverse(flow.startingNode, undefined, 0, functionCache, dataTypeCache); + + return React.useMemo(() => edges, [flowStore, functionStore, dataTypeStore, edges]); +}; \ No newline at end of file diff --git a/src/components/d-flow/viewport/DFlowViewport.nodes.hook.ts b/src/components/d-flow/viewport/DFlowViewport.nodes.hook.ts new file mode 100644 index 00000000..d82b1d85 --- /dev/null +++ b/src/components/d-flow/viewport/DFlowViewport.nodes.hook.ts @@ -0,0 +1,236 @@ +import {useService} from "../../../utils/contextStore"; +import {DFlowReactiveService} from "../DFlow.service"; +import {NodeFunctionView} from "../DFlow.view"; +import {Node} from "@xyflow/react"; +import {DFlowFunctionReactiveService} from "../function/DFlowFunction.service"; +import {DFlowDataTypeReactiveService} from "../data-type/DFlowDataType.service"; +import {EDataType, Type} from "../data-type/DFlowDataType.view"; + +const packageNodes = new Map([ + ['std', 'default'], +]); + +/** + * Returns the value of the best-matching key from a Map. + * + * Matching priority: + * 1) Exact match + * 2) Longest prefix match (with bonus if the prefix ends at a token boundary) + * 3) Largest common prefix length (with small boundary bonus) + * + * Token boundaries are characters in /[:._\-\/\s]+/ (e.g., "::", ".", "_", "-", "/", whitespace). + * + * Performance: + * - O(N * M), where N = number of keys, M = average key length (string comparisons only). + * + */ +const bestMatchValue = (map: Map, input: string): string => { + if (!input || map.size === 0) return "" + + const SEP = /[:._\-\/\s]+/; + const normInput = input.trim().toLowerCase(); + + let bestKey: string | null = null; + let bestScore = -Infinity; + + for (const [key, value] of map.entries()) { + const normKey = key.trim().toLowerCase(); + + // (1) Exact match → immediately return (strongest possible score) + if (normInput === normKey) { + return value; + } + + let score = 0; + + // (2) Prefix match + if (normInput.startsWith(normKey)) { + score = 2000 + normKey.length * 2; + + // Bonus if the prefix ends at a clean token boundary (or equals the whole input) + const boundaryChar = normInput.charAt(normKey.length); // '' if out of range + if (boundaryChar === "" || SEP.test(boundaryChar)) { + score += 200; + } + } else { + // (3) Largest common prefix (LCP) + const max = Math.min(normInput.length, normKey.length); + let lcp = 0; + while (lcp < max && normInput.charCodeAt(lcp) === normKey.charCodeAt(lcp)) { + lcp++; + } + if (lcp > 0) { + score = 1000 + lcp; + + // Small bonus if LCP ends at a boundary on either side + const inBoundaryChar = normInput.charAt(lcp); + const keyBoundaryChar = normKey.charAt(lcp); + if ( + inBoundaryChar === "" || + SEP.test(inBoundaryChar) || + keyBoundaryChar === "" || + SEP.test(keyBoundaryChar) + ) { + score += 50; + } + } + } + + // Best candidate so far? Tie-breaker favors longer key (more specific) + if (score > bestScore) { + bestScore = score; + bestKey = key; + } else if (score === bestScore && bestKey !== null && key.length > bestKey.length) { + bestKey = key; + } else if (score === bestScore && bestKey === null) { + bestKey = key; + } + } + + return bestKey !== null ? map.get(bestKey)! : ""; +}; + +export const useFlowViewportNodes = (flowId: string): Node[] => { + const flowService = useService(DFlowReactiveService); + const functionService = useService(DFlowFunctionReactiveService); + const dataTypeService = useService(DFlowDataTypeReactiveService); + const flow = flowService.getById(flowId); + + if (!flow) return []; + + const nodes: Node[] = []; + let idCounter = 0; + + const functionCache = new Map>(); + const dataTypeCache = new Map>(); + + const getFunctionDefinitionCached = ( + id: string, + cache = functionCache, + ) => { + if (!cache.has(id)) { + cache.set(id, functionService.getFunctionDefinition(id)); + } + return cache.get(id); + }; + + const getDataTypeCached = ( + type: Type, + cache = dataTypeCache, + ) => { + if (!cache.has(type)) { + cache.set(type, dataTypeService.getDataType(type)); + } + return cache.get(type); + }; + + // Global, strictly increasing group-id used to build the scope PATH ([0], [0,1], [0,2], [0,2,3], ...) + let globalScopeId = 0; + const nextScopeId = () => ++globalScopeId; + + // Global, strictly increasing node index across the entire flow (only real nodes) + let globalNodeIndex = 0; + + //trigger node + nodes.push({ + id: `${flow.id}`, + type: "trigger", + position: { x: 0, y: 0 }, + draggable: false, + data: { + instance: flow + } + }) + + const traverse = ( + fn: NodeFunctionView, + isParameter = false, + parentId?: string, + depth: number = 0, + scopePath: number[] = [0], + parentGroup?: string, + fnCache = functionCache, + dtCache = dataTypeCache, + ) => { + const id = `${fn.runtime_id}-${idCounter++}`; + const index = ++globalNodeIndex; // global node level + + nodes.push({ + id, + type: bestMatchValue(packageNodes, fn.runtime_id), + position: { x: 0, y: 0 }, + draggable: false, + parentId: parentGroup, + extent: parentGroup ? "parent" : undefined, + data: { + instance: fn, + isParameter, + linkingId: isParameter ? parentId : undefined, + scope: scopePath, // scope is now a PATH (number[]) + depth, // structural depth (0 at root, +1 per group) + index, // global node level + }, + }); + + if (!fn.nextNode && !isParameter) { + nodes.push({ + id: `${id}-suggestion`, + type: "suggestion", + position: { x: 0, y: 0 }, + draggable: false, + extent: parentGroup ? "parent" : undefined, + parentId: parentGroup, + data: { + flowId: flowId, + parentFunction: fn, + }, + }); + } + + const definition = getFunctionDefinitionCached(fn.id, fnCache); + + fn.parameters?.forEach((param) => { + const paramType = definition?.parameters!!.find(p => p.parameter_id == param.id)?.type; + const paramDataType = paramType ? getDataTypeCached(paramType, dtCache) : undefined; + + if (paramDataType?.type === EDataType.NODE) { + if (param.value && param.value instanceof NodeFunctionView) { + const groupId = `${id}-group-${idCounter++}`; + + // New group: extend scope PATH with a fresh segment and increase depth. + const childScopePath = [...scopePath, nextScopeId()]; + + nodes.push({ + id: groupId, + type: "group", + position: { x: 0, y: 0 }, + draggable: false, + parentId: parentGroup, + extent: parentGroup ? "parent" : undefined, + data: { + isParameter: true, + linkingId: id, + depth: depth + 1, + scope: childScopePath, + }, + }); + + // Child function inside the group uses the group's depth and scope PATH. + traverse(param.value as NodeFunctionView, false, undefined, depth + 1, childScopePath, groupId, fnCache, dtCache); + } + } else if (param.value && param.value instanceof NodeFunctionView) { + // Functions passed as non-NODE parameters live in the same depth/scope PATH. + traverse(param.value as NodeFunctionView, true, id, depth, scopePath, parentGroup, fnCache, dtCache); + } + }); + + if (fn.nextNode) { + // Linear chain continues in the same depth/scope PATH. + traverse(fn.nextNode, false, undefined, depth, scopePath, parentGroup, fnCache, dtCache); + } + }; + + // Root lane: depth 0, scope path [0] + traverse(flow.startingNode, false, undefined, 0, [0], undefined, functionCache, dataTypeCache); + return nodes; +}; \ No newline at end of file diff --git a/src/components/d-flow/viewport/DFlowViewportControls.tsx b/src/components/d-flow/viewport/DFlowViewportControls.tsx new file mode 100644 index 00000000..c8c91851 --- /dev/null +++ b/src/components/d-flow/viewport/DFlowViewportControls.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import {Panel, useReactFlow, useViewport} from "@xyflow/react"; +import ButtonGroup from "../../button-group/ButtonGroup"; +import Button from "../../button/Button"; +import {IconFocusCentered, IconMinus, IconPlus} from "@tabler/icons-react"; +import Badge from "../../badge/Badge"; +import Flex from "../../flex/Flex"; +import {DFlowViewportMiniMap} from "./DFlowViewportMiniMap"; + +export const DFlowViewportControls: React.FC = () => { + + const viewport = useViewport(); + const reactFlow = useReactFlow(); + + const zoomIn = () => { + reactFlow.zoomIn() + } + + const zoomOut = () => { + reactFlow.zoomOut() + } + + const center = () => { + reactFlow.fitView() + } + + const getCurrentZoomInPercent = () => { + return Math.round(viewport.zoom * 100); + } + + return + + + + + + + + {getCurrentZoomInPercent()}% + + + + +} \ No newline at end of file diff --git a/src/components/d-flow/viewport/DFlowViewportEdge.tsx b/src/components/d-flow/viewport/DFlowViewportEdge.tsx new file mode 100644 index 00000000..5f85659b --- /dev/null +++ b/src/components/d-flow/viewport/DFlowViewportEdge.tsx @@ -0,0 +1,50 @@ +import {Code0Component} from "../../../utils/types"; +import {BaseEdge, Edge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, Position} from "@xyflow/react"; +import React, {memo} from "react"; +import Badge from "../../badge/Badge"; + +export interface DFlowViewportEdgeDataProps extends Code0Component { + //some data we will use + color?: string + isParameter?: boolean + isSuggestion?: boolean +} + +// @ts-ignore +export type DFlowViewportEdgeProps = EdgeProps> + +export const DFlowViewportEdge: React.FC = memo((props) => { + + const {sourceX, sourceY, targetX, targetY, id, data, ...rest} = props + + const [edgePath, labelX, labelY] = getSmoothStepPath({ + sourceX, + sourceY, + sourcePosition: data?.isParameter ? Position.Left : Position.Bottom, + targetX, + targetY, + targetPosition: data?.isParameter ? Position.Right : Position.Top, + borderRadius: 16, + centerY: data?.isSuggestion ? targetY - 37.5 : targetY - 37.5 + }) + + return <> + + {props.label ? ( + +
+ + {props.label} + +
+
+ ) : null} + + + +}) \ No newline at end of file diff --git a/src/components/d-flow/viewport/DFlowViewportMiniMap.style.scss b/src/components/d-flow/viewport/DFlowViewportMiniMap.style.scss new file mode 100644 index 00000000..329c940e --- /dev/null +++ b/src/components/d-flow/viewport/DFlowViewportMiniMap.style.scss @@ -0,0 +1,21 @@ +@use "../../../styles/helpers"; +@use "../../../styles/box"; +@use "../../../styles/variables"; + +.d-flow-viewport-mini-map { + + & { + @include box.box(variables.$primary); + @include helpers.borderRadius(); + margin: 0; + width: 100%; + } + + > svg { + @include helpers.borderRadius(); + } + + * .react-flow__minimap-mask { + fill: rgba(variables.$secondary, 0.1); + } +} \ No newline at end of file diff --git a/src/components/d-flow/viewport/DFlowViewportMiniMap.tsx b/src/components/d-flow/viewport/DFlowViewportMiniMap.tsx new file mode 100644 index 00000000..7c1f2119 --- /dev/null +++ b/src/components/d-flow/viewport/DFlowViewportMiniMap.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import {MiniMap, useNodes} from "@xyflow/react"; +import {FLOW_EDGE_RAINBOW} from "./DFlowViewport.edges.hook"; +import "./DFlowViewportMiniMap.style.scss" + +export const DFlowViewportMiniMap: React.FC = (props) => { + + const nodes = useNodes(); + + return { + + const node = nodes.find(node => node.id === props1.id) + if (!node) return null + + if (node.type == "suggestion") return null + + if (node.type == "group") { + + const depth = (node.data as any)?.depth ?? 0; + const color = FLOW_EDGE_RAINBOW[depth % FLOW_EDGE_RAINBOW.length]; + + return + } + + return + + }}/> + +} \ No newline at end of file diff --git a/src/components/d-flow/viewport/cards/DFlowViewportDefaultCard.style.scss b/src/components/d-flow/viewport/cards/DFlowViewportDefaultCard.style.scss new file mode 100644 index 00000000..63c55e1e --- /dev/null +++ b/src/components/d-flow/viewport/cards/DFlowViewportDefaultCard.style.scss @@ -0,0 +1,28 @@ +@use "../../../../styles/variables"; +@use "../../../../styles/helpers"; +@use "../../../../styles/box"; + +.d-flow-viewport-default-card { + &__handle { + border: none !important; + padding: 1px; + z-index: -1; + opacity: 0; + } + + &__inspection { + top: 0; + transform: translateY(-50%); + padding: variables.$xxs; + + & { + @include box.box(variables.$primary); + @include helpers.borderRadius(); + position: absolute; + } + } + + &--active { + box-shadow: inset 0 0 0 1px helpers.borderColor(variables.$info), 0 0 0 3px helpers.borderColor(variables.$info); + } +} \ No newline at end of file diff --git a/src/components/d-flow/viewport/cards/DFlowViewportDefaultCard.tsx b/src/components/d-flow/viewport/cards/DFlowViewportDefaultCard.tsx new file mode 100644 index 00000000..82222d6f --- /dev/null +++ b/src/components/d-flow/viewport/cards/DFlowViewportDefaultCard.tsx @@ -0,0 +1,242 @@ +import {Code0Component} from "../../../../utils/types"; +import {Handle, Node, NodeProps, Position, useReactFlow, useStore, useStoreApi} from "@xyflow/react"; +import {NodeFunctionView, NodeFunctionParameter} from "../../DFlow.view"; +import React, {memo} from "react"; +import Card from "../../../card/Card"; +import "./DFlowViewportDefaultCard.style.scss"; +import CardSection from "../../../card/CardSection"; +import Flex from "../../../flex/Flex"; +import { + IconAlertTriangle, + IconArrowRightCircle, IconCopy, + IconDots, + IconExclamationCircle, + IconFileLambdaFilled, + IconLayoutNavbarCollapseFilled, + IconMessageExclamation, + IconTrash +} from "@tabler/icons-react"; +import Text from "../../../text/Text"; +import Button from "../../../button/Button"; +import {Menu, MenuContent, MenuItem, MenuLabel, MenuPortal, MenuTrigger} from "../../../menu/Menu"; +import Badge from "../../../badge/Badge"; +import {useService} from "../../../../utils/contextStore"; +import {DFlowFunctionReactiveService} from "../../function/DFlowFunction.service"; +import {useFunctionValidation} from "../../function/DFlowFunction.vaildation.hook"; +import {DFlowDataTypeReactiveService} from "../../data-type/DFlowDataType.service"; +import {InspectionSeverity} from "../../../../utils/inspection"; +import {EDataType} from "../../data-type/DFlowDataType.view"; +import {DFlowReactiveService} from "../../DFlow.service"; +import {DFlowSuggestionMenu} from "../../suggestions/DFlowSuggestionMenu"; +import {useSuggestions} from "../../suggestions/DFlowSuggestion.hook"; +import {FileTabsService} from "../../../file-tabs/FileTabs.service"; +import {DFlowViewportDefaultTabContent} from "../file-tabs/DFlowViewportDefaultTabContent"; + +export interface DFlowViewportDefaultCardDataProps extends Omit, "scope"> { + instance: NodeFunctionView + isParameter: boolean + depth: number + scope: number[] + index: number +} + +// @ts-ignore +export type DFlowViewportDefaultCardProps = NodeProps> + +export const DFlowViewportDefaultCard: React.FC = memo((props) => { + const {data, id} = props; + const viewportWidth = useStore(s => s.width); + const viewportHeight = useStore(s => s.height); + const flowInstance = useReactFlow() + const flowStoreApi = useStoreApi() + const fileTabsService = useService(FileTabsService) + const flowService = useService(DFlowReactiveService) + const functionService = useService(DFlowFunctionReactiveService) + const dataTypeService = useService(DFlowDataTypeReactiveService) + const definition = functionService.getFunctionDefinition(data.instance.id) + const validation = useFunctionValidation(definition!!, data.instance.parameters!!.map(p => p.value!! instanceof NodeFunctionView ? p.value.json : p.value!!), useService(DFlowDataTypeReactiveService)!!) + const edges = useStore(s => s.edges); + const width = props.width ?? 0 + const height = props.height ?? 0 + + // Helper, ob zu diesem Parameter eine Edge existiert: + function isParamConnected(paramId: string): boolean { + return edges.some(e => + e.target === id && + e.targetHandle === `param-${paramId}` + ); + } + + const firstItem = useStore((s) => { + const children = s.nodes.filter((n) => n.parentId === props.parentId); + let start: any | undefined = undefined; + children.forEach((n) => { + const idx = (n.data as any)?.index ?? Infinity; + const startIdx = (start?.data as any)?.index ?? Infinity; + if (!start || idx < startIdx) { + start = n; + } + }); + return start; + }) + + return ( + v.type === InspectionSeverity.ERROR)?.length ?? 0) > 0 ? "error" : "secondary"} + onClick={() => { + flowInstance.setViewport({ + x: (viewportWidth / 2) + (props.positionAbsoluteX * -1) - (width / 2), + y: (viewportHeight / 2) + (props.positionAbsoluteY * -1) - (height / 2), + zoom: 1 + }, { + duration: 250, + }) + fileTabsService.add({ + id: id, + active: true, + closeable: true, + children: {data.instance.id}, + content: + }) + }} style={{position: "relative"}}> + + + + + + {data.instance.id} + + + { + setTimeout(() => { + flowStoreApi.setState({ + nodesDraggable: !event, + nodesConnectable: !event, + elementsSelectable: !event, + }); + }, 250) // Timeout to ensure the menu is fully opened before changing the state + }}> + + + + + + Actions + { + data.instance.deleteNextNode() + flowService.update() + }}> Delete node + Copy node + + + + + + + + + + + {(validation?.length ?? 0) > 0 ? ( +
+ + {(validation?.filter(v => v.type === InspectionSeverity.ERROR)?.length ?? 0) > 0 ? ( + + + + {validation?.filter(v => v.type === InspectionSeverity.ERROR)?.length} + + + ) : null} + + {(validation?.filter(v => v.type === InspectionSeverity.WARNING)?.length ?? 0) > 0 ? ( + + + + {validation?.filter(v => v.type === InspectionSeverity.WARNING)?.length} + + + ) : null} + + {(validation?.filter(v => v.type === InspectionSeverity.GRAMMAR)?.length ?? 0) > 0 ? ( + + + + {validation?.filter(v => v.type === InspectionSeverity.GRAMMAR)?.length} + + + ) : null} + +
+ ) : null} + + {data.instance.parameters?.some(param => { + const parameter = definition?.parameters!!.find(p => p.parameter_id == param.id) + const isNodeDataType = dataTypeService.getDataType(parameter!!.type)?.type === EDataType.NODE; + return (param.value instanceof NodeFunctionView && !isNodeDataType) || (!param.value) + }) ? ( + + {/* Dynamische Parameter-Eingänge (rechts), nur wenn wirklich verbunden */} + {data.instance.parameters?.map((param: NodeFunctionParameter, index: number) => { + + + const parameter = definition?.parameters!!.find(p => p.parameter_id == param.id) + const isNodeDataType = dataTypeService.getDataType(parameter!!.type)?.type === EDataType.NODE; + const result = useSuggestions(parameter?.type ?? undefined, [], "some_database_id", data.depth, data.scope, data.index) + + return (param.value instanceof NodeFunctionView && !isNodeDataType) || (!param.value) ? + + {param.id} + {!param.value ? ( + { + param.value = suggestion.value + flowService.update() + }} suggestions={result} triggerContent={}/> + ) : null} + : null + })} + + ) : null} + + {/* Ausgang */} + +
+ ); +}) \ No newline at end of file diff --git a/src/components/d-flow/viewport/cards/DFlowViewportGroupCard.tsx b/src/components/d-flow/viewport/cards/DFlowViewportGroupCard.tsx new file mode 100644 index 00000000..b36c2e71 --- /dev/null +++ b/src/components/d-flow/viewport/cards/DFlowViewportGroupCard.tsx @@ -0,0 +1,61 @@ +import React, {memo} from "react"; +import {Handle, NodeProps, Position, useStore} from "@xyflow/react"; +import {FLOW_EDGE_RAINBOW} from "../DFlowViewport.edges.hook"; +import Card from "../../../card/Card"; + +export interface DFlowViewportGroupCardProps extends NodeProps { +} + +export const DFlowViewportGroupCard: React.FC = memo((props) => { + const {data, id} = props + const depth = (data as any)?.depth ?? 0; + const color = FLOW_EDGE_RAINBOW[depth % FLOW_EDGE_RAINBOW.length]; + + // Align handles with the first node inside this group + const handleLeft = useStore((s) => { + const children = s.nodes.filter((n) => n.parentId === id); + let start: any | undefined = undefined; + children.forEach((n) => { + const idx = (n.data as any)?.index ?? Infinity; + const startIdx = (start?.data as any)?.index ?? Infinity; + if (!start || idx < startIdx) { + start = n; + } + }); + if (start) { + const width = start.measured.width ?? 0; + return start.position.x + width / 2; + } + return undefined; + }) + + return ( + + + + + ); +}); + +const withAlpha = (hex: string, alpha: number) => { + const h = hex.replace('#', ''); + const r = parseInt(h.length === 3 ? h[0] + h[0] : h.slice(0, 2), 16); + const g = parseInt(h.length === 3 ? h[1] + h[1] : h.slice(2, 4), 16); + const b = parseInt(h.length === 3 ? h[2] + h[2] : h.slice(4, 6), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +}; diff --git a/src/components/d-flow/viewport/cards/DFlowViewportSuggestionCard.tsx b/src/components/d-flow/viewport/cards/DFlowViewportSuggestionCard.tsx new file mode 100644 index 00000000..8808d389 --- /dev/null +++ b/src/components/d-flow/viewport/cards/DFlowViewportSuggestionCard.tsx @@ -0,0 +1,43 @@ +import {Code0Component} from "../../../../utils/types"; +import {Handle, Node, NodeProps, Position} from "@xyflow/react"; +import React, {memo} from "react"; +import Button from "../../../button/Button"; +import {IconPlus} from "@tabler/icons-react"; +import {useSuggestions} from "../../suggestions/DFlowSuggestion.hook"; +import {NodeFunctionView, NodeFunctionObject} from "../../DFlow.view"; +import {useService} from "../../../../utils/contextStore"; +import {DFlowReactiveService} from "../../DFlow.service"; +import {DFlowSuggestionMenu} from "../../suggestions/DFlowSuggestionMenu"; + +export interface DFlowViewportSuggestionCardDataProps extends Code0Component { + flowId: string + parentFunction: NodeFunctionView +} + +// @ts-ignore +export type DFlowViewportSuggestionCardProps = NodeProps> + +export const DFlowViewportSuggestionCard: React.FC = memo((props) => { + + const result = useSuggestions(undefined, [], props.data.flowId, 0, [0], 0) + const flowService = useService(DFlowReactiveService) + + return { + props.data.parentFunction.nextNode = new NodeFunctionView(suggestion.value as NodeFunctionObject) + flowService.update() + }} suggestions={result} triggerContent={ + + }/> + +}) \ No newline at end of file diff --git a/src/components/d-flow/viewport/cards/DFlowViewportTriggerCard.tsx b/src/components/d-flow/viewport/cards/DFlowViewportTriggerCard.tsx new file mode 100644 index 00000000..5da0b4cf --- /dev/null +++ b/src/components/d-flow/viewport/cards/DFlowViewportTriggerCard.tsx @@ -0,0 +1,84 @@ +import React, {memo} from "react"; +import {Code0Component} from "../../../../utils/types"; +import {FlowView} from "../../DFlow.view"; +import {Handle, Node, NodeProps, Position, useReactFlow, useStore} from "@xyflow/react"; +import Text from "../../../text/Text"; +import {useService} from "../../../../utils/contextStore"; +import {FileTabsService} from "../../../file-tabs/FileTabs.service"; +import Card from "../../../card/Card"; +import CardSection from "../../../card/CardSection"; +import Flex from "../../../flex/Flex"; +import {IconBolt, IconLayoutNavbarCollapseFilled} from "@tabler/icons-react"; +import Button from "../../../button/Button"; +import Badge from "../../../badge/Badge"; +import {DFlowViewportTriggerTabContent} from "../file-tabs/DFlowViewportTriggerTabContent"; + +export interface DFlowViewportTriggerCardDataProps extends Omit, "scope"> { + instance: FlowView +} + +// @ts-ignore +export type DFlowViewportTriggerCardProps = NodeProps> + +export const DFlowViewportTriggerCard: React.FC = memo((props) => { + + const {data, id} = props + const fileTabsService = useService(FileTabsService) + const flowInstance = useReactFlow() + const width = props.width ?? 0 + const height = props.height ?? 0 + const viewportWidth = useStore(s => s.width) + const viewportHeight = useStore(s => s.height) + + return { + flowInstance.setViewport({ + x: (viewportWidth / 2) + (props.positionAbsoluteX * -1) - (width / 2), + y: (viewportHeight / 2) + (props.positionAbsoluteY * -1) - (height / 2), + zoom: 1 + }, { + duration: 250, + }) + fileTabsService.add({ + id: id, + active: true, + closeable: true, + children: {data.instance.id}, + content: + }) + }}> + + + + + Trigger + + + + + + + + {data.instance.id} + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy. + + + + {/* Ausgang */} + + + + +}) \ No newline at end of file diff --git a/src/components/d-flow/viewport/file-tabs/DFlowViewportDefaultTabContent.tsx b/src/components/d-flow/viewport/file-tabs/DFlowViewportDefaultTabContent.tsx new file mode 100644 index 00000000..4f1f42cf --- /dev/null +++ b/src/components/d-flow/viewport/file-tabs/DFlowViewportDefaultTabContent.tsx @@ -0,0 +1,126 @@ +import React from "react"; +import {isNodeFunctionObject, NodeFunctionView, NodeFunctionObject} from "../../DFlow.view"; +import TextInput from "../../../form/TextInput"; +import Flex from "../../../flex/Flex"; +import {useService} from "../../../../utils/contextStore"; +import {DFlowFunctionReactiveService} from "../../function/DFlowFunction.service"; +import {useSuggestions} from "../../suggestions/DFlowSuggestion.hook"; +import {DFlowSuggestionMenuFooter} from "../../suggestions/DFlowSuggestionMenuFooter"; +import {toInputSuggestions} from "../../suggestions/DFlowSuggestionMenu.util"; +import {isRefObject, RefObject, Value} from "../../data-type/DFlowDataType.view"; +import {DFlowReactiveService} from "../../DFlow.service"; +import {DFlowSuggestion} from "../../suggestions/DFlowSuggestion.view"; +import {ParameterDefinition} from "../../function/DFlowFunction.view"; +import Badge from "../../../badge/Badge"; +import {DFlowDataTypeReactiveService} from "../../data-type/DFlowDataType.service"; +import {useReturnType} from "../../function/DFlowFunction.return.hook"; +import {resolveGenericKeys} from "../../../../utils/generics"; + +export interface DFlowViewportFileTabsContentProps { + functionInstance: NodeFunctionView + depthLevel?: number + scopeLevel?: number[] + nodeLevel?: number +} + +export const DFlowViewportDefaultTabContent: React.FC = (props) => { + + const {functionInstance, depthLevel, scopeLevel, nodeLevel} = props + const functionService = useService(DFlowFunctionReactiveService) + const dataTypeService = useService(DFlowDataTypeReactiveService) + const flowService = useService(DFlowReactiveService) + const definition = functionService.getFunctionDefinition(functionInstance.id) + const paramDefinitions = React.useMemo(() => { + const map: Record = {} + definition?.parameters?.forEach(pd => { + map[pd.parameter_id] = pd + }) + return map + }, [definition?.parameters]) + + const sortedParameters = React.useMemo(() => { + return [...(functionInstance.parameters || [])].sort((a, b) => a.id.localeCompare(b.id)) + }, [functionInstance.parameters]) + + const suggestionsById: Record = {} + sortedParameters.forEach(parameter => { + const parameterDefinition = paramDefinitions[parameter.id] + suggestionsById[parameter.id] = useSuggestions(parameterDefinition?.type, [], "some_database_id", depthLevel, scopeLevel, nodeLevel) + }) + + const returnType = useReturnType(definition!!, sortedParameters.map(p => p.value as Value), dataTypeService) + const genericTypeMap = resolveGenericKeys(definition!!, sortedParameters.map(p => p.value as Value), dataTypeService) + return + {sortedParameters.map(parameter => { + + const submitValue = (value: Value) => { + parameter.value = value + flowService.update() + } + + const submitValueEvent = (event: any) => { + try { + const value = JSON.parse(event.target.value) + submitValue(value) + } catch (e) { + // @ts-ignore + submitValue(event.target.value == "" ? undefined : event.target.value) + } + } + + const parameterDefinition = paramDefinitions[parameter.id] + const result = suggestionsById[parameter.id] + const title = parameterDefinition?.name ? parameterDefinition?.name[0]?.text : parameterDefinition!!.parameter_id + const description = parameterDefinition?.description ? parameterDefinition?.description[0]?.text : JSON.stringify(parameterDefinition!!.type) + const defaultValue = parameter.value instanceof NodeFunctionView ? JSON.stringify(parameter.value.json) : typeof parameter.value == "object" || typeof parameter.value == "boolean" ? JSON.stringify(parameter.value) : parameter.value + + + return
+ {JSON.stringify(dataTypeService.getTypeFromValue(parameter.value as Value))} + { + try { + if (!value) return value + if (isNodeFunctionObject(JSON.parse(value) as NodeFunctionObject)) { + return {(JSON.parse(value) as NodeFunctionObject).function.function_id} + } + if (isRefObject(JSON.parse(value))) { + const refObject = JSON.parse(value) as RefObject + return {refObject.depth}-{refObject.scope}-{refObject.node}-{JSON.stringify(refObject.type)} + } + } catch (e) { + } + return value + }} + disableOnValue={value => { + if (!value) return false + try { + return isNodeFunctionObject(JSON.parse(value) as NodeFunctionObject) || isRefObject(JSON.parse(value)) + } catch (e) { + } + return false + }} + defaultValue={defaultValue} + onSuggestionSelect={(suggestion) => { + submitValue(suggestion.value) + }} + onBlur={submitValueEvent} + onClear={submitValueEvent} + suggestionsFooter={} + suggestions={toInputSuggestions(result)} + + /> +
+ })} + {JSON.stringify(returnType)} +
+
+ {JSON.stringify(genericTypeMap)} +
+ +} \ No newline at end of file diff --git a/src/components/d-flow/viewport/file-tabs/DFlowViewportTabs.tsx b/src/components/d-flow/viewport/file-tabs/DFlowViewportTabs.tsx new file mode 100644 index 00000000..ad2b81b4 --- /dev/null +++ b/src/components/d-flow/viewport/file-tabs/DFlowViewportTabs.tsx @@ -0,0 +1,110 @@ +import {useService, useStore} from "../../../../utils/contextStore"; +import {FileTabsService} from "../../../file-tabs/FileTabs.service"; +import {FileTabs, FileTabsContent, FileTabsList, FileTabsTrigger} from "../../../file-tabs/FileTabs"; +import React from "react"; +import {Menu, MenuContent, MenuItem, MenuPortal, MenuSeparator, MenuTrigger} from "../../../menu/Menu"; +import Button from "../../../button/Button"; +import {IconChevronDown, IconDotsVertical} from "@tabler/icons-react"; +import {FileTabsView} from "../../../file-tabs/FileTabs.view"; + +export const DFlowViewportTabs = () => { + + const fileTabsService = useService(FileTabsService) + const fileTabsStore = useStore(FileTabsService) + const id = React.useId() + + const activeTabId = React.useMemo(() => { + return fileTabsStore.find((t: any) => (t as any).active)?.id ?? fileTabsService.getActiveTab()?.id; + }, [fileTabsStore, fileTabsService]); + + React.useEffect(() => { + setTimeout(() => { + const parent = document.querySelector("[data-id=" + '"' + id + '"' + "]") as HTMLDivElement + const tabList = parent.querySelector(".file-tabs__list-content") as HTMLDivElement + const trigger = tabList.querySelector("[data-value=" + '"' + fileTabsService.getActiveTab()?.id + '"' + "]") as HTMLDivElement + + if (tabList && trigger) { + const offset = (trigger.offsetLeft + (trigger.offsetWidth / 2)) - (tabList.offsetWidth / 2) + tabList.scrollLeft = 0 //reset to 0 + tabList.scrollBy({ + left: offset, + behavior: 'smooth' + }); + } + }, 0) + }, [activeTabId, id]) + + return ( + { + fileTabsService.activateTab(value); // mutieren reicht; kein .update() nötig, wenn setState benutzt wird + }} + > + + + + + + + + {fileTabsStore.map((tab: FileTabsView) => ( + { + fileTabsService.activateTab(tab.id!) + }}> + {tab.children} + + ))} + + + + + + + + + + + fileTabsService.clear()}>Close all tabs + fileTabsService.clearWithoutActive()}>Close other + tabs + + fileTabsService.clearLeft()}>Close all tabs to + left + fileTabsService.clearRight()}>Close all tabs to + right + + + + + } + > + {fileTabsStore.map((tab: FileTabsView, index: number) => ( + fileTabsService.delete(index)} + > + {tab.children} + + ))} + + + {fileTabsStore.map((tab: FileTabsView) => ( + + {tab.content} + + ))} + + ); + +} \ No newline at end of file diff --git a/src/components/d-flow/viewport/file-tabs/DFlowViewportTriggerTabContent.tsx b/src/components/d-flow/viewport/file-tabs/DFlowViewportTriggerTabContent.tsx new file mode 100644 index 00000000..bd7f2ddb --- /dev/null +++ b/src/components/d-flow/viewport/file-tabs/DFlowViewportTriggerTabContent.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import {FlowView, NodeFunctionView} from "../../DFlow.view"; +import {useService} from "../../../../utils/contextStore"; +import {DFlowReactiveService} from "../../DFlow.service"; +import TextInput from "../../../form/TextInput"; +import Flex from "../../../flex/Flex"; +import {DFlowTypeReactiveService} from "../../type/DFlowType.service"; +import {FlowTypeSetting} from "../../type/DFlowType.view"; +import {DFlowSuggestion} from "../../suggestions/DFlowSuggestion.view"; +import {useSuggestions} from "../../suggestions/DFlowSuggestion.hook"; +import {DFlowSuggestionMenuFooter} from "../../suggestions/DFlowSuggestionMenuFooter"; +import {toInputSuggestions} from "../../suggestions/DFlowSuggestionMenu.util"; + +export interface DFlowViewportTriggerTabContentProps { + instance: FlowView +} + +export const DFlowViewportTriggerTabContent: React.FC = (props) => { + + const {instance} = props + const flowService = useService(DFlowReactiveService) + const flowTypeService = useService(DFlowTypeReactiveService) + const definition = flowTypeService.getById(instance.type) + + const flowTypeSettingsDefinition = React.useMemo(() => { + const map: Record = {} + definition?.settings.forEach(def => { + map[def.flow_definition_settings_id] = def + }) + return map + }, [definition?.settings]) + + const suggestionsById: Record = {} + instance?.settings?.forEach(setting => { + const settingDefinition = flowTypeSettingsDefinition[setting.definition.setting_id] + suggestionsById[setting.definition.setting_id] = useSuggestions(settingDefinition.type, [], "some_database_id", 0, [0], 0) + }) + + return + {instance.settings?.map(setting => { + + const settingsDefinition = flowTypeSettingsDefinition[setting.definition.setting_id] + const title = settingsDefinition?.name ? settingsDefinition.name[0]?.text : setting.definition.setting_id + const description = settingsDefinition?.description ? settingsDefinition?.description[0]?.text : JSON.stringify(settingsDefinition!!.type) + const result = suggestionsById[setting.definition.setting_id] + const defaultValue = setting.value instanceof NodeFunctionView ? JSON.stringify(setting.value.json) : typeof setting.value == "object" || typeof setting.value == "boolean" ? JSON.stringify(setting.value) : setting.value + + return
+ } + suggestions={toInputSuggestions(result)}/> +
+ })} +
+} \ No newline at end of file diff --git a/src/components/d-resizable/DResizable.stories.tsx b/src/components/d-resizable/DResizable.stories.tsx index 46c1057b..1a2fd9e8 100644 --- a/src/components/d-resizable/DResizable.stories.tsx +++ b/src/components/d-resizable/DResizable.stories.tsx @@ -4,18 +4,39 @@ import React from "react"; import DFullScreen from "../d-fullscreen/DFullScreen"; import DFolder from "../d-folder/DFolder"; import Button from "../button/Button"; -import { - IconDatabase, - IconFileFilled, - IconHierarchy3, - IconSettings, - IconTicket -} from "@tabler/icons-react"; +import {IconDatabase, IconFileFilled, IconHierarchy3, IconSettings, IconTicket} from "@tabler/icons-react"; import Flex from "../flex/Flex"; -import {ExampleFileTabs} from "../file-tabs/FileTabs.stories"; import {ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "../scroll-area/ScrollArea"; import {Tooltip, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; -import {ExampleFlowLine} from "../flow-line/FlowLines.stories"; +import {useFlowViewportNodes} from "../d-flow/viewport/DFlowViewport.nodes.hook"; +import {useFlowViewportEdges} from "../d-flow/viewport/DFlowViewport.edges.hook"; +import {DFlowViewportDefaultCard} from "../d-flow/viewport/cards/DFlowViewportDefaultCard"; +import {DFlowViewportGroupCard} from "../d-flow/viewport/cards/DFlowViewportGroupCard"; +import {DFlowViewportSuggestionCard} from "../d-flow/viewport/cards/DFlowViewportSuggestionCard"; +import {DFlowViewportEdge} from "../d-flow/viewport/DFlowViewportEdge"; +import {DFlow} from "../d-flow/DFlow"; +import {Background, BackgroundVariant} from "@xyflow/react"; +import {DFlowViewportControls} from "../d-flow/viewport/DFlowViewportControls"; +import {FunctionDefinitionView} from "../d-flow/function/DFlowFunction.view"; +import {functionData} from "../d-flow/function/DFlowFunction.data"; +import {useReactiveArrayService} from "../../utils/reactiveArrayService"; +import {FileTabsView} from "../file-tabs/FileTabs.view"; +import {FileTabsService} from "../file-tabs/FileTabs.service"; +import {DataTypeView} from "../d-flow/data-type/DFlowDataType.view"; +import {DFlowDataTypeReactiveService} from "../d-flow/data-type/DFlowDataType.service"; +import {DFlowFunctionReactiveService} from "../d-flow/function/DFlowFunction.service"; +import {DFlowReactiveService} from "../d-flow/DFlow.service"; +import {flow1} from "../d-flow/DFlow.data"; +import {DFlowSuggestion} from "../d-flow/suggestions/DFlowSuggestion.view"; +import {DFlowReactiveSuggestionService} from "../d-flow/suggestions/DFlowSuggestion.service"; +import {dataTypes} from "../d-flow/data-type/DFlowDataType.data"; +import {FlowView} from "../d-flow/DFlow.view"; +import {ContextStoreProvider} from "../../utils/contextStore"; +import {DFlowViewportTabs} from "../d-flow/viewport/file-tabs/DFlowViewportTabs"; +import {DFlowViewportTriggerCard} from "../d-flow/viewport/cards/DFlowViewportTriggerCard"; +import {FlowType} from "../d-flow/type/DFlowType.view"; +import {DFlowTypeReactiveService} from "../d-flow/type/DFlowType.service"; +import {REST_FLOW_TYPE} from "../d-flow/type/DFlowType.data"; const meta: Meta = { title: "Dashboard Resizable", @@ -32,13 +53,23 @@ export default meta export const Dashboard = () => { + const [fileTabsStore, fileTabsService] = useReactiveArrayService(FileTabsService) + const [dataTypeStore, dataTypeService] = useReactiveArrayService(DFlowDataTypeReactiveService, (service) => { + return dataTypes.map(dataType => (new DataTypeView(dataType, service))) + }) + const [functionStore, functionService] = useReactiveArrayService(DFlowFunctionReactiveService, functionData.map((fd) => new FunctionDefinitionView(fd))); + const [flowStore, flowService] = useReactiveArrayService(DFlowReactiveService, [new FlowView(flow1)]); + const [suggestionStore, suggestionService] = useReactiveArrayService(DFlowReactiveSuggestionService); + const [flowTypeStore, flowTypeService] = useReactiveArrayService(DFlowTypeReactiveService, [REST_FLOW_TYPE]); + return - @@ -50,7 +81,8 @@ export const Dashboard = () => { - @@ -76,7 +108,8 @@ export const Dashboard = () => {
- @@ -91,7 +124,7 @@ export const Dashboard = () => { {[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1].map((_, index) => { - return + return } name={"Google Cloud Flows"}/> @@ -114,13 +147,16 @@ export const Dashboard = () => { }}>
- - - - - - - + + + + + + + + +
@@ -128,4 +164,19 @@ export const Dashboard = () => { +} + +const FlowExample = () => { + const initialNodes = useFlowViewportNodes("some_database_id") + const initialEdges = useFlowViewportEdges("some_database_id") + + return + + + {/**/} + } \ No newline at end of file diff --git a/src/components/file-tabs/FileTabs.service.ts b/src/components/file-tabs/FileTabs.service.ts index 6dd80ddb..be34863b 100644 --- a/src/components/file-tabs/FileTabs.service.ts +++ b/src/components/file-tabs/FileTabs.service.ts @@ -9,21 +9,21 @@ export class FileTabsService extends ReactiveArrayService { public clearLeft(): void { const index = this.getActiveIndex() - this.store[1](prevState => [...prevState.filter((_, index1) => { + this.access.setState(prevState => [...prevState.filter((_, index1) => { return index1 >= index })]) } public clearRight(): void { const index = this.getActiveIndex() - this.store[1](prevState => [...prevState.filter((_, index1) => { + this.access.setState(prevState => [...prevState.filter((_, index1) => { return index1 <= index })]) } public clearWithoutActive(): void { const tab = this.getActiveTab() - if (tab) this.store[1](prevState => [tab]) + if (tab) this.access.setState(prevState => [tab]) } public activateTab(id: string) { @@ -33,6 +33,7 @@ export class FileTabsService extends ReactiveArrayService { const tab = this.values().find((item: FileTabsView) => item.id === id); if (tab) tab.active = true + this.update() } public delete(index: number) { @@ -48,6 +49,13 @@ export class FileTabsService extends ReactiveArrayService { } public add(value: FileTabsView) { + //if tab with id already exists, do not add it again and just activate the existing one + + if (this.values().some(value1 => value1.id == value.id)) { + this.activateTab(value.id!!) + return + } + if (value.active) { this.values().forEach((item: FileTabsView) => { item.active = false diff --git a/src/components/file-tabs/FileTabs.stories.tsx b/src/components/file-tabs/FileTabs.stories.tsx index f99ee69c..933f24ea 100644 --- a/src/components/file-tabs/FileTabs.stories.tsx +++ b/src/components/file-tabs/FileTabs.stories.tsx @@ -3,7 +3,7 @@ import {FileTabs, FileTabsContent, FileTabsList, FileTabsTrigger} from "./FileTa import React from "react"; import {IconChevronDown, IconDotsVertical, IconFileLambdaFilled} from "@tabler/icons-react"; import Flex from "../flex/Flex"; -import {createReactiveArrayService} from "../../utils/reactiveArrayService"; +import {useReactiveArrayService} from "../../utils/reactiveArrayService"; import {FileTabsView} from "./FileTabs.view"; import {FileTabsService} from "./FileTabs.service"; import Button from "../button/Button"; @@ -17,7 +17,7 @@ export default { export const ExampleFileTabs = () => { - const [store, service] = createReactiveArrayService(FileTabsService) + const [store, service] = useReactiveArrayService(FileTabsService) const id = React.useId() React.useEffect(() => { diff --git a/src/components/form/Input.stories.tsx b/src/components/form/Input.stories.tsx index 039cb8cc..f7699425 100644 --- a/src/components/form/Input.stories.tsx +++ b/src/components/form/Input.stories.tsx @@ -49,35 +49,93 @@ export const Login = () => { dolore magna aliquyam erat, sed diam voluptua.
-
- } - {...inputs.getInputProps("email")} - /> -
- } - {...inputs.getInputProps("password")} - /> -
-
} + {...inputs.getInputProps("email")} + /> +
+ } + {...inputs.getInputProps("password")} + /> +
+
+ + + + Login with Passkeys + -
+ - +
} @@ -128,34 +186,34 @@ export const RadioExample = () => { return - - - - - -
-
- + + + + + +
+
+ -
- +
+
} @@ -223,23 +281,23 @@ export const Switch = () => { return - -
-
- + +
+
+ -
- +
+
} diff --git a/src/components/form/Input.style.scss b/src/components/form/Input.style.scss index bd57d487..cdc1e972 100644 --- a/src/components/form/Input.style.scss +++ b/src/components/form/Input.style.scss @@ -23,6 +23,22 @@ @include box.boxActiveStyle(variables.$primary, variables.$white, variables.$white); } + &__syntax { + position: absolute; + z-index: -1; + overflow: hidden; + font-size: variables.$sm; + color: rgba(variables.$white, .5); + white-space: nowrap; + padding: $padding $padding; + box-sizing: border-box; + top: 0; + left: 0; + width: 100%; + height: 100%; + + } + &__left, &__right { display: flex; align-items: stretch; @@ -73,11 +89,16 @@ width: 100%; box-shadow: none; font-size: variables.$sm; + box-sizing: border-box; color: rgba(variables.$white, .5); & { @include helpers.fontStyle(); } + + &--syntax { + color: transparent; + } } diff --git a/src/components/form/Input.tsx b/src/components/form/Input.tsx index 4e28db93..71d7578d 100644 --- a/src/components/form/Input.tsx +++ b/src/components/form/Input.tsx @@ -1,95 +1,244 @@ +/** + * Input.tsx + * + * A highly customizable and accessible input component with extended features, + * including dynamic suggestion rendering, validation handling, and structural + * decoration options. Designed to integrate seamlessly with complex forms, + * this component provides robust interaction patterns and user guidance. + */ + +import React, {ForwardRefExoticComponent, LegacyRef, RefObject, useEffect, useMemo, useRef, useState} from "react"; + import {Code0Component} from "../../utils/types"; -import React, {LegacyRef, RefObject, useEffect} from "react"; import {ValidationProps} from "./useForm"; import {mergeCode0Props} from "../../utils/utils"; -import "./Input.style.scss" + +import "./Input.style.scss"; + import InputLabel from "./InputLabel"; import InputDescription from "./InputDescription"; import InputMessage from "./InputMessage"; -type Code0Input = Omit, "defaultValue">, "left">, "right">, "title"> - -export interface InputProps extends Code0Input, ValidationProps { - - wrapperComponent?: Code0Component - right?: React.ReactNode | React.ReactElement | React.ReactElement[] - left?: React.ReactNode | React.ReactElement | React.ReactElement[] - leftType?: "action" | "placeholder" | "icon" - rightType?: "action" | "placeholder" | "icon" - title?: React.ReactNode | React.ReactElement - description?: React.ReactNode | React.ReactElement -} - -export const setElementKey = (element: HTMLElement, key: string, value: any, event: string) => { - const valueSetter = Object.getOwnPropertyDescriptor(element, key)?.set; +import {Menu, MenuPortal, MenuTrigger} from "../menu/Menu"; +import { + InputSuggestion, + InputSuggestionMenuContent, + InputSuggestionMenuContentItems, + InputSuggestionMenuContentItemsHandle +} from "./InputSuggestion"; + +// Programmatically set a property (like 'value') and dispatch an event (like 'change') +export const setElementKey = ( + element: HTMLElement, + key: string, + value: any, + event: string +) => { + const valueSetter = Object.getOwnPropertyDescriptor(element, key)?.set; // Try direct setter const prototype = Object.getPrototypeOf(element); - const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, key)?.set; + const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, key)?.set; // Fallback to prototype if (valueSetter && valueSetter !== prototypeValueSetter) { - prototypeValueSetter?.call(element, value); + prototypeValueSetter?.call(element, value); // Use prototype's setter if overridden } else { - valueSetter?.call(element, value); + valueSetter?.call(element, value); // Use direct setter } - element.dispatchEvent(new Event(event, { bubbles: true })); -} - - -const Input: React.ForwardRefExoticComponent> = React.forwardRef((props: InputProps, ref: RefObject) => { + element.dispatchEvent(new Event(event, {bubbles: true})); // Fire change/input event +}; - ref = ref || React.useRef(null) +// Base input props without layout-specific keys +export type Code0Input = Omit< + Omit, "left">, "right">, + "title" +>; - const { - wrapperComponent = {}, - title, - description, - disabled = false, - left, - right, - leftType = "icon", - rightType = "action", - formValidation = { - valid: true, - notValidMessage: null, - setValue: null - }, - ...rest - } = props - - useEffect(() => { - if (!ref) return - if (!ref.current) return - if (!formValidation) return - if (!formValidation.setValue) return - - // @ts-ignore - ref.current.addEventListener("change", ev => formValidation.setValue(rest.type != "checkbox" ? ev.target.value : ev.target.checked)) - }, [ref]) - - - return <> - - {!!title ? : null} - {!!description ? : null} - -
- - {!!left ?
- {left} -
: null} - - | undefined} {...mergeCode0Props("input__control", rest)}/> - - {!!right ?
- {right} -
: null} +// Input component props definition +export interface InputProps extends Code0Input, ValidationProps { -
+ suggestions?: InputSuggestion[] // Optional suggestions shown in dropdown + suggestionsHeader?: React.ReactNode // Custom header above suggestions + suggestionsFooter?: React.ReactNode // Custom footer below suggestions + onSuggestionSelect?: (suggestion: InputSuggestion) => void // Callback when a suggestion is selected + transformValue?: (value: T) => React.ReactNode | T // Optional value transformation function + disableOnValue?: (value: T) => boolean + + wrapperComponent?: Code0Component // Props for the wrapping div + right?: React.ReactNode // Right-side icon or element + left?: React.ReactNode // Left-side icon or element + leftType?: "action" | "placeholder" | "icon" // Visual type for left slot + rightType?: "action" | "placeholder" | "icon" // Visual type for right slot + title?: React.ReactNode // Input label + description?: React.ReactNode // Label description below title - {!formValidation?.valid && formValidation?.notValidMessage ? - : null} - -}) +} +const Input: ForwardRefExoticComponent> = React.forwardRef( + (props: InputProps, ref: RefObject) => { + const inputRef = ref || useRef(null); // External ref or fallback internal ref + const menuRef = useRef(null); // Ref to suggestion list + const [open, setOpen] = useState(false); // Dropdown open state + const shouldPreventCloseRef = useRef(true); // Controls if dropdown should stay open on click + const [value, setValue] = useState(props.defaultValue || props.initialValue || props.placeholder) + + const { + wrapperComponent = {}, // Default empty wrapper props + title, // Optional input label + description, // Optional description below label + disabled = false, // Input disabled state + left, // Left element (icon/button) + right, // Right element (icon/button) + leftType = "icon", // Visual hint for left + rightType = "action", // Visual hint for right + formValidation = {valid: true, notValidMessage: null, setValue: null}, // Validation config + suggestions, // Optional suggestions array + suggestionsHeader, // Optional header above suggestion list + suggestionsFooter, // Optional footer below suggestion list + onSuggestionSelect = () => { + }, // Callback for suggestion selection, + disableOnValue = () => false, + ...rest // Remaining native input props + } = props; + + // Sync input value changes to external validation state + useEffect(() => { + const el = inputRef.current; + if (!el || !formValidation?.setValue) return; + + const handleChange = (event: any) => { + const value = rest.type !== "checkbox" ? event.target.value : event.target.checked; // Support checkbox + formValidation.setValue?.(value); // Push value to form context + }; + + el.addEventListener("change", handleChange); // Native listener + return () => el.removeEventListener("change", handleChange); // Cleanup + }, [formValidation?.setValue]); + + // Manage click-outside logic for dropdown + useEffect(() => { + if (!suggestions) return; + + const handlePointerDown = (event: PointerEvent) => { + shouldPreventCloseRef.current = !!inputRef.current?.contains(event.target as Node); // Stay open if click is inside + }; + + document.addEventListener("pointerdown", handlePointerDown, true); + return () => document.removeEventListener("pointerdown", handlePointerDown, true); + }, [suggestions]); + + const disabledOnValue = React.useMemo(() => disableOnValue(value), [value, disableOnValue]) + + useEffect(() => { + if (!inputRef.current) return + inputRef.current.addEventListener("change", (e: any) => { + if (disabledOnValue) return + setValue(e.target.value) + }) + inputRef.current.addEventListener("input", (e: any) => { + if (disabledOnValue) return + setValue(e.target.value) + }) + }, [inputRef, disabledOnValue]) + + + const syntax = React.useMemo(() => { + return props.transformValue ? ( +
+ {props.transformValue(value)} {/* Render transformed value */} +
+ ) : null + }, [props.transformValue, value]) + + // Render suggestion menu dropdown + const suggestionMenu = useMemo(() => ( + { + if (!nextOpen && shouldPreventCloseRef.current) { // Prevent close if internal click + shouldPreventCloseRef.current = false; + return; + } + + setOpen(nextOpen); + + if (nextOpen) { + setTimeout(() => inputRef.current?.focus(), 0); // Refocus input on open + } + }} + > + + } // Cast for TS compatibility + {...mergeCode0Props(`input__control ${props.transformValue ? "input__control--syntax" : ""}`, rest)} + onFocus={() => !open && setOpen(true)} // Open on focus + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + menuRef.current?.focusFirstItem(); // Navigate down + } else if (e.key === "ArrowUp") { + e.preventDefault(); + menuRef.current?.focusLastItem(); // Navigate up + } + }} + disabled={disabled || disabledOnValue} + /> + + + + {suggestionsHeader} {/* Custom content above suggestions */} + { + // Update value and dispatch event + setElementKey(ref.current, "value", typeof value == "object" ? JSON.stringify(suggestion.value) : suggestion.value, "change"); + onSuggestionSelect(suggestion) + setOpen(false) + }} + /> + {suggestionsFooter} {/* Custom content below suggestions */} + + + + ), [open, suggestions, suggestionsHeader, suggestionsFooter]); + + return ( + <> + {title && {title}} {/* Optional label */} + {description && {description}} {/* Optional description */} + +
+ {left &&
{left}
} {/* Left element */} + + {suggestions ? ( + suggestionMenu // If suggestions exist, render dropdown version + ) : ( + } + {...mergeCode0Props(`input__control ${props.transformValue ? "input__control--syntax" : ""}`, rest)} // Basic input styling and props + /> + )} + + {syntax} + + {right && +
{right}
} {/* Right element */} +
+ + {!formValidation?.valid && formValidation?.notValidMessage && ( + {formValidation.notValidMessage} // Show validation error + )} + + ); + } +); -export default Input \ No newline at end of file +export default Input; \ No newline at end of file diff --git a/src/components/form/InputSuggestion.tsx b/src/components/form/InputSuggestion.tsx new file mode 100644 index 00000000..20dd1481 --- /dev/null +++ b/src/components/form/InputSuggestion.tsx @@ -0,0 +1,68 @@ +import {MenuContent, MenuContentProps, MenuItem} from "../menu/Menu"; +import React from "react"; +import {ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "../scroll-area/ScrollArea"; + +export interface InputSuggestion { + children: React.ReactNode + value: any + ref?: any +} + +export type InputSuggestionMenuContentProps = MenuContentProps + +export interface InputSuggestionMenuContentItemsProps { + suggestions?: InputSuggestion[] + onSuggestionSelect?: (suggestion: InputSuggestion) => void +} + +export interface InputSuggestionMenuContentItemsHandle { + focusFirstItem: () => void + focusLastItem: () => void +} + +export const InputSuggestionMenuContent: React.FC = React.forwardRef((props, ref) => { + + const {children, ...rest} = props + const localRef = React.useRef(null) + + // @ts-ignore + return e.stopPropagation()} + onInteractOutside={(event) => event.target instanceof HTMLInputElement && event.preventDefault()} + onCloseAutoFocus={(event) => event.preventDefault()} + align={"start"} + sideOffset={8} + {...rest} > + {children} + + +}) + + +export const InputSuggestionMenuContentItems: React.FC = React.forwardRef((props, ref) => { + + const {suggestions, onSuggestionSelect = () => {}, ...rest} = props + const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]) + + React.useImperativeHandle(ref, () => ({ + focusFirstItem: () => itemRefs.current[0]?.focus(), + focusLastItem: () => itemRefs.current.at(-1)?.focus(), + }), []) + + // @ts-ignore + return + + {suggestions?.map((suggestion, i) => { + // @ts-ignore + return setTimeout(() => onSuggestionSelect(suggestion), 0)} ref={el => itemRefs.current[i] = el}> + {suggestion.children} + + })} + + + + + + +}) \ No newline at end of file diff --git a/src/components/form/TextInput.tsx b/src/components/form/TextInput.tsx index 74e5dbc6..1e90ee08 100644 --- a/src/components/form/TextInput.tsx +++ b/src/components/form/TextInput.tsx @@ -7,6 +7,7 @@ import Button from "../button/Button"; interface TextInputProps extends Omit, "wrapperComponent" | "type"> { //defaults to false clearable?: boolean + onClear?: (event: Event) => void } const TextInput: React.ForwardRefExoticComponent = React.forwardRef((props, ref: RefObject) => { @@ -19,8 +20,9 @@ const TextInput: React.ForwardRefExoticComponent = React.forward ...rest } = props - const toClearable = () => { + const toClearable = (event: Event) => { if (ref.current) setElementKey(ref.current, "value", "", "change") + if (props.onClear) props.onClear(event) } const rightAction = [right] diff --git a/src/components/menu/Menu.style.scss b/src/components/menu/Menu.style.scss index 8bb947ee..adebd665 100644 --- a/src/components/menu/Menu.style.scss +++ b/src/components/menu/Menu.style.scss @@ -3,7 +3,7 @@ @use "../../styles/variables"; .menu { - z-index: 100; + z-index: 999; &__content, &__sub-content { padding: variables.$xxs; @@ -19,7 +19,9 @@ &__label { text-transform: uppercase; font-size: variables.$xs; - display: block; + display: flex; + gap: variables.$xxs; + align-items: center; padding: variables.$xxs variables.$xs; color: helpers.color(); diff --git a/src/components/segmented-control/SegmentedControl.style.scss b/src/components/segmented-control/SegmentedControl.style.scss index d3fdc49f..f4349fa7 100644 --- a/src/components/segmented-control/SegmentedControl.style.scss +++ b/src/components/segmented-control/SegmentedControl.style.scss @@ -21,7 +21,7 @@ & { @include box.box(); - @include helpers.borderRadius(); + border-radius: variables.$borderRadius - variables.$xxs; @include helpers.fontStyle(); font-size: variables.$sm; box-shadow: none; diff --git a/src/components/tooltip/Tooltip.style.scss b/src/components/tooltip/Tooltip.style.scss index 604f8bb0..7baca7b2 100644 --- a/src/components/tooltip/Tooltip.style.scss +++ b/src/components/tooltip/Tooltip.style.scss @@ -6,6 +6,7 @@ &__content { + z-index: 999; padding: variables.$xxs variables.$xs; & { diff --git a/src/styles/_helpers.scss b/src/styles/_helpers.scss index e840dbf5..2654370a 100644 --- a/src/styles/_helpers.scss +++ b/src/styles/_helpers.scss @@ -22,8 +22,10 @@ } @function borderColor($color: variables.$secondary) { - $mixedBorderWhite: color.mix($color, variables.$white, 25%); - @return rgba($mixedBorderWhite, .1); + @if($color == variables.$secondary) { + @return rgba($color, .1); + } + @return rgba($color, .25); } @function color($color: variables.$white, $hierarchy: variables.$hierarchyTertiary) { diff --git a/src/utils/generics.ts b/src/utils/generics.ts index a8dcd860..dfcd3f0a 100644 --- a/src/utils/generics.ts +++ b/src/utils/generics.ts @@ -1,18 +1,10 @@ -import { - DataTypeObject, - DataTypeRuleObject, - EDataTypeRuleType, - GenericCombinationStrategy, - GenericMapper, - GenericType, - Type, - Value -} from "../components/d-flow/data-type/DFlowDataType.view"; -import {FunctionDefinition} from "../components/d-flow/function/DFlowFunction.view"; +import {FunctionDefinitionView} from "../components/d-flow/function/DFlowFunction.view"; import {DFlowDataTypeReactiveService, DFlowDataTypeService} from "../components/d-flow/data-type/DFlowDataType.service"; import { DFlowDataTypeItemOfCollectionRuleConfig } from "../components/d-flow/data-type/rules/DFlowDataTypeItemOfCollectionRule"; +import {EDataTypeRuleType} from "../components/d-flow/data-type/rules/DFlowDataTypeRules"; +import {DataTypeVariant} from "@code0-tech/sagittarius-graphql-types"; /** * Resolves concrete type mappings for generic keys in a generic type system. @@ -346,7 +338,7 @@ export const replaceGenericKeysInDataTypeObject = ( * @returns Map from generic key to its resolved Type or GenericMapper */ export const resolveGenericKeys = ( - func: FunctionDefinition, + func: FunctionDefinitionView, values: Value[], dataTypeService: DFlowDataTypeService ): Map => { @@ -549,73 +541,108 @@ export function isMatchingType( } /** - * Recursively expands all type aliases (e.g. "NUMBER_ARRAY" → ARRAY). - * Ensures every alias in the tree is fully expanded. + * Recursively expands all type aliases + * - ARRAY aliases expand their inner type recursively (e.g. NUMBER_ARRAY → ARRAY). + * - OBJECT aliases expand to OBJECT (e.g. TEST_OBJECT → OBJECT). * * @param type The Type or type alias to expand * @param service Data type service (for lookup) * @returns Deeply expanded Type object (with aliases replaced) */ export const resolveType = (type: Type, service: DFlowDataTypeReactiveService): Type => { - // Alias (string): try to expand via CONTAINS_TYPE recursively + // --- Case 1: Alias string --- if (typeof type === "string") { const dt = service.getDataType(type) - if ( - dt && - dt.rules && - dt.rules.some(r => r.type === EDataTypeRuleType.CONTAINS_TYPE && "type" in r.config) - ) { - // Find most generic DataType with matching type and generics - const genericDT = service.values().find( - dt2 => dt2.type === dt.type && dt2.genericKeys && dt2.genericKeys.length > 0 - ) - if (genericDT && genericDT.genericKeys!!.length) { - const rule = dt.rules.find(r => r.type === EDataTypeRuleType.CONTAINS_TYPE && "type" in r.config) - if (rule) { - // Recursively expand inner type - //@ts-ignore - const expandedInner = resolveType(rule.config.type, service) + + if (dt && dt.rules) { + // --- ARRAY alias (e.g. NUMBER_ARRAY) --- + if ( + dt.type === DataTypeVariant.Array && + dt.rules.some(r => r.type === EDataTypeRuleType.CONTAINS_TYPE && "type" in r.config) + ) { + const genericDT = service.values().find( + dt2 => dt2.type === DataTypeVariant.Array && dt2.genericKeys && dt2.genericKeys.length > 0 + ) + if (genericDT?.genericKeys && genericDT.genericKeys.length > 0) { + const rule = dt.rules.find(r => r.type === EDataTypeRuleType.CONTAINS_TYPE && "type" in r.config) + if (rule) { + //@ts-ignore + const expandedInner = resolveType(rule.config.type, service) + const [genericKey] = genericDT.genericKeys + if (!genericKey) return type + return { + type: genericDT.id, + generic_mapper: [{ + types: [expandedInner], + generic_target: genericKey + }] + } + } + } + } + + // --- OBJECT alias (e.g. TEST_OBJECT) --- + if (dt.type === DataTypeVariant.Object) { + const genericDT = service.values().find( + dt2 => dt2.type === DataTypeVariant.Object && dt2.genericKeys && dt2.genericKeys.length > 0 + ) + if (genericDT?.genericKeys && genericDT.genericKeys.length > 0) { + const [genericKey] = genericDT.genericKeys + if (!genericKey) return type return { type: genericDT.id, generic_mapper: [{ - types: [expandedInner], - generic_target: "GENERIC" + types: [type], // keep alias itself as Type ("TEST_OBJECT") + generic_target: genericKey }] } } } } - // Not an alias or cannot expand further + + // Fallback: primitive or unknown alias → keep as string return type } - // If already a GenericType object: expand all generic_mapper[].types recursively + + // --- Case 2: Already a GenericType --- if (typeof type === "object" && type !== null) { - const result: any = {} - for (const key of Object.keys(type)) { - if (key === "generic_mapper" && Array.isArray(type[key])) { - result[key] = type[key].map((gm: any) => ({ - ...gm, - types: Array.isArray(gm.types) - ? gm.types.map((t: any) => resolveType(t, service)) - : resolveType(gm.types, service), - generic_target: gm.generic_target // Normalize all generic_target to "GENERIC" - })) - } else if (key === "type") { - // Always expand nested type to primitive string (no nested objects) - if (typeof type[key] === "object") { - result[key] = (type[key] as any).type || type[key] - } else { - result[key] = type[key] + const result: GenericType = { type: type.type } + + if (Array.isArray(type.generic_mapper)) { + result.generic_mapper = type.generic_mapper.map(gm => ({ + ...gm, + types: gm.types.map(t => resolveType(t, service)) + })) + } + + const dataType = service.getDataType(type.type) + if (dataType?.genericKeys && dataType.genericKeys.length > 0) { + const baseTypeId = DataTypeVariant[dataType.type] + if (typeof baseTypeId === "string") { + const baseType = service.getDataType(baseTypeId) + const baseGenericKeys = baseType?.genericKeys + + if ( + baseType && + baseGenericKeys && + baseGenericKeys.length > 0 && + baseType.id !== dataType.id + ) { + return { + type: baseType.id, + generic_mapper: [{ + types: [result], + generic_target: baseGenericKeys[0] + }] + } } - } else { - // Recursively handle all other fields - //@ts-ignore - result[key] = resolveType(type[key], service) } } + return result } - // Primitive (number/boolean): just return as-is + + // --- Case 3: Primitive (shouldn't happen often) --- return type } diff --git a/src/utils/inspection.ts b/src/utils/inspection.ts index e2184b6a..02f37d10 100644 --- a/src/utils/inspection.ts +++ b/src/utils/inspection.ts @@ -1,6 +1,6 @@ import {Translation} from "./translation"; -export const enum InspectionSeverity { +export enum InspectionSeverity { TYPO, GRAMMAR, WEAK, diff --git a/src/utils/reactiveArrayService.ts b/src/utils/reactiveArrayService.ts index c59c9bde..ccccfbcc 100644 --- a/src/utils/reactiveArrayService.ts +++ b/src/utils/reactiveArrayService.ts @@ -1,71 +1,90 @@ +// reactiveArrayService.ts import React from "react"; -import {ArrayService} from "./arrayService"; +import { ArrayService } from "./arrayService"; -/** - * using a React ref as a store in combination with a map - * to be able to access and modify the store easily - */ -export type ReactiveArrayStore = [T[], React.Dispatch>] +// Zugriffstyp: aktueller State via Getter, Updates via setState +export type ReactiveArrayStore = { + getState: () => T[]; + setState: React.Dispatch>; +}; export class ReactiveArrayService implements ArrayService { - - protected readonly store: ReactiveArrayStore - - constructor(store: ReactiveArrayStore) { - this.store = store + protected readonly access: ReactiveArrayStore; + constructor(access: ReactiveArrayStore) { + this.access = access; } - public delete(index: number) { - this.store[1](prevState => { - const newState = prevState.filter((value, index1) => index1 !== index); - return [ - ...newState, - ] - }) + delete(index: number) { + this.access.setState(prev => prev.filter((_, i) => i !== index)); } - public add(value: T) { - this.store[1](prevState => { - return [ - ...prevState, - value - ] - }) + add(value: T) { + this.access.setState(prev => [...prev, value]); } - public set(index: number, value: T) { - this.store[1](prevState => { - prevState[index] = value - return [ - ...prevState - ] - }) + set(index: number, value: T) { + this.access.setState(prev => { + const next = prev.slice(); + next[index] = value; + return next; + }); } - public has(index: number) { - return !!this.store[0][index] + has(index: number) { + const arr = this.access.getState(); + return index >= 0 && index < arr.length; } - public get(index: number) { - return this.store[0][index] + get(index: number) { + return this.access.getState()[index]; } - public values() { - return this.store[0] + values() { + return this.access.getState(); } - public update() { - this.store[1](prevState => [...prevState]) + update() { + this.access.setState(prev => prev.slice()); } - clear(): void { - this.store[1](_ => []) + clear() { + this.access.setState(() => []); } - } -// @ts-ignore -export const createReactiveArrayService = >(service: typeof T, callback?: (store: ReactiveArrayStore) => T, initial?: K[]): [K[] | undefined, T] => { - const store = React.useState(initial ?? []) - return [store[0], (callback ? callback(store) : new service(store)) as T] +// Hilfstyp für den Konstruktor des Service +type ArrayServiceCtor> = + new (access: ReactiveArrayStore) => S; + +// initial kann Array oder Callback sein +type InitialArg> = K[] | ((svc: S) => K[]); + +export function useReactiveArrayService>( + Ctor: ArrayServiceCtor, + initial: InitialArg = [] +): [K[], S] { + // 1) State + Ref + const [state, setState] = React.useState( + Array.isArray(initial) ? initial : [] // Platzhalter; Callback folgt in Effect + ); + const ref = React.useRef(state); + React.useEffect(() => { ref.current = state; }, [state]); + + // 2) stabiler Getter + const getState = React.useCallback(() => ref.current, []); + + // 3) Service erstellen (stabil über getState/setState) + const service = React.useMemo(() => new Ctor({ getState, setState }), [Ctor, getState, setState]); + + // 4) Falls initial ein Callback ist, einmalig ausführen, sobald Service existiert + React.useEffect(() => { + if (typeof initial === "function") { + // Nur einmal setzen (überschreibt ggf. den Platzhalter) + setState((initial as (svc: S) => K[])(service)); + } + // absichtlich keine Abhängigkeit auf `state` – nur einmal ausführen + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [service]); + + return [state, service]; } \ No newline at end of file