diff --git a/package-lock.json b/package-lock.json index 0553bd260..1fc2e4736 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,15 @@ "": { "name": "@code0-tech/pictor", "version": "0.0.0", + "dependencies": { + "@radix-ui/react-context-menu": "^2.2.16" + }, "devDependencies": { "@ariakit/react": "^0.4.17", "@babel/plugin-proposal-decorators": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", "@code0-tech/definition-reader": "^0.0.18", - "@code0-tech/sagittarius-graphql-types": "^0.0.0-c63274fdd34593b8aa24f9f977659fd3d6270150", + "@code0-tech/sagittarius-graphql-types": "^0.0.0-968478cecf351385c6c13c2be8aeae5c45da026c", "@dagrejs/dagre": "^2.0.0", "@mdx-js/react": "^3.1.1", "@radix-ui/react-checkbox": "^1.3.3", @@ -1232,9 +1235,9 @@ } }, "node_modules/@code0-tech/sagittarius-graphql-types": { - "version": "0.0.0-c63274fdd34593b8aa24f9f977659fd3d6270150", - "resolved": "https://registry.npmjs.org/@code0-tech/sagittarius-graphql-types/-/sagittarius-graphql-types-0.0.0-c63274fdd34593b8aa24f9f977659fd3d6270150.tgz", - "integrity": "sha512-6J0DC2OdNnZRcS7jJkwCIEv4PoQmpDMdR4FAdVKfrE08X4PZAD+csVf0zTVjmX82KFzVL0M46HB/6twpKCsRUg==", + "version": "0.0.0-968478cecf351385c6c13c2be8aeae5c45da026c", + "resolved": "https://registry.npmjs.org/@code0-tech/sagittarius-graphql-types/-/sagittarius-graphql-types-0.0.0-968478cecf351385c6c13c2be8aeae5c45da026c.tgz", + "integrity": "sha512-6ndUP/zKixZ/VI7E+u9YSkxkzkeM+MTBhehqv4NDBZgbczXp1hVDBdJ+YEnTd22mLCA0a7EhGGBbBvY3z9WlQw==", "dev": true }, "node_modules/@code0-tech/tucana": { @@ -1741,7 +1744,6 @@ "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": { "@floating-ui/utils": "^0.2.10" @@ -1751,7 +1753,6 @@ "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.3", @@ -1762,7 +1763,6 @@ "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.4" @@ -1776,7 +1776,6 @@ "version": "0.2.10", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "dev": true, "license": "MIT" }, "node_modules/@isaacs/balanced-match": { @@ -4093,14 +4092,12 @@ "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" }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" @@ -4155,7 +4152,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -4182,7 +4178,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4198,7 +4193,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4210,6 +4204,34 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", @@ -4251,7 +4273,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4267,7 +4288,6 @@ "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.3", @@ -4325,7 +4345,6 @@ "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": { "@types/react": "*", @@ -4341,7 +4360,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -4367,7 +4385,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -4386,7 +4403,6 @@ "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.3", @@ -4462,7 +4478,6 @@ "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": { "@floating-ui/react-dom": "^2.0.0", @@ -4495,7 +4510,6 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3", @@ -4520,7 +4534,6 @@ "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": { "@radix-ui/react-compose-refs": "1.1.2", @@ -4545,7 +4558,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.3" @@ -4602,7 +4614,6 @@ "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.3", @@ -4666,7 +4677,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -4807,7 +4817,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4823,7 +4832,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", @@ -4843,7 +4851,6 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -4862,7 +4869,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" @@ -4900,7 +4906,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4932,7 +4937,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.1" @@ -4951,7 +4955,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -4994,7 +4997,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "dev": true, "license": "MIT" }, "node_modules/@rolldown/pluginutils": { @@ -6360,7 +6362,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -7438,7 +7440,6 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -8536,7 +8537,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/d3-color": { @@ -8811,7 +8812,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "dev": true, "license": "MIT" }, "node_modules/diff": { @@ -9595,7 +9595,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13972,7 +13971,6 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -14015,7 +14013,6 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -14046,7 +14043,6 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", - "dev": true, "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -14072,7 +14068,6 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "dev": true, "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.2", @@ -14106,7 +14101,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "dev": true, "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", @@ -14659,7 +14653,6 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "dev": true, "license": "MIT" }, "node_modules/schema-utils": { @@ -15819,7 +15812,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD", "peer": true }, @@ -15960,7 +15952,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -15982,7 +15973,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "dev": true, "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", diff --git a/package.json b/package.json index 9948559a7..3e63523e8 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@babel/plugin-proposal-decorators": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", "@code0-tech/definition-reader": "^0.0.18", - "@code0-tech/sagittarius-graphql-types": "^0.0.0-c63274fdd34593b8aa24f9f977659fd3d6270150", + "@code0-tech/sagittarius-graphql-types": "^0.0.0-968478cecf351385c6c13c2be8aeae5c45da026c", "@dagrejs/dagre": "^2.0.0", "@mdx-js/react": "^3.1.1", "@radix-ui/react-checkbox": "^1.3.3", @@ -94,7 +94,7 @@ "types": "dist/index.d.ts", "peerDependencies": { "@ariakit/react": "^0.4.5", - "@code0-tech/sagittarius-graphql-types": "^0.0.0-c63274fdd34593b8aa24f9f977659fd3d6270150", + "@code0-tech/sagittarius-graphql-types": "^0.0.0-968478cecf351385c6c13c2be8aeae5c45da026c", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", @@ -118,5 +118,8 @@ }, "publishConfig": { "access": "public" + }, + "dependencies": { + "@radix-ui/react-context-menu": "^2.2.16" } } diff --git a/src/components/badge/Badge.style.scss b/src/components/badge/Badge.style.scss index d2564a488..1988ae48e 100644 --- a/src/components/badge/Badge.style.scss +++ b/src/components/badge/Badge.style.scss @@ -12,6 +12,12 @@ height: fit-content; vertical-align: middle; + @if (var(--badge-color)) { + background-color: var(--badge-color-background); + border: 1px solid var(--badge-color-border); + color: var(--badge-color); + } + & { @include helpers.fontStyle(); @include helpers.borderRadius() diff --git a/src/components/badge/Badge.tsx b/src/components/badge/Badge.tsx index 74b775ef0..f549bf7ed 100644 --- a/src/components/badge/Badge.tsx +++ b/src/components/badge/Badge.tsx @@ -3,18 +3,91 @@ import "./Badge.style.scss" import {Code0Component, Color} from "../../utils/types"; import {mergeCode0Props} from "../../utils/utils"; -export interface BadgeType extends Code0Component{ +export interface BadgeType extends Code0Component { children: React.ReactNode - //defaults to primary - color?: Color + // defaults to primary + color?: Color | string border?: boolean } +type RGBA = { + r: number + g: number + b: number + a: number +} + export const Badge: React.FC = (props) => { - + const {color = "primary", border = false, children, ...args} = props - - return - {children} - + + return ( + + {children} + + ) +} + +/* =========================== + Color utilities + =========================== */ + +const clamp01 = (v: number) => Math.min(Math.max(v, 0), 1) + +const parseCssColorToRgba = (color: string): RGBA => { + if (typeof document === "undefined") { + return {r: 0, g: 0, b: 0, a: 1} + } + + const el = document.createElement("span") + el.style.color = color + document.body.appendChild(el) + + const computed = getComputedStyle(el).color + document.body.removeChild(el) + + const match = computed.match( + /rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/ + ) + + if (!match) { + return {r: 0, g: 0, b: 0, a: 1} + } + + return { + r: Math.round(Number(match[1])), + g: Math.round(Number(match[2])), + b: Math.round(Number(match[3])), + a: match[4] !== undefined ? Number(match[4]) : 1, + } +} + +const mixColorRgb = (color: string, level: number) => { + const w = clamp01(level * 0.1) + + const c1 = parseCssColorToRgba(color) + const c2 = parseCssColorToRgba("#030014") + + const mix = (a: number, b: number) => + Math.round(a * (1 - w) + b * w) + + return `rgb(${mix(c1.r, c2.r)}, ${mix(c1.g, c2.g)}, ${mix(c1.b, c2.b)})` +} + +const withAlpha = (color: string, alpha: number) => { + const c = parseCssColorToRgba(color) + return `rgba(${c.r}, ${c.g}, ${c.b}, ${clamp01(alpha)})` } \ No newline at end of file diff --git a/src/components/button/Button.style.scss b/src/components/button/Button.style.scss index f4df1684a..5d4552979 100644 --- a/src/components/button/Button.style.scss +++ b/src/components/button/Button.style.scss @@ -34,7 +34,7 @@ } } - &:active, &:focus, &[aria-selected=true] { + &:active, &:focus, &[aria-selected=true], &[data-state=open] { @if ($color == variables.$primary) { border-color: rgba(variables.$secondary, 0.2); } @else { @@ -51,7 +51,7 @@ background: helpers.backgroundColor($color, 1.5); } - &:active, &:focus, &[aria-selected=true] { + &:active, &:focus, &[aria-selected=true], &[data-state=open] { background: helpers.backgroundColor($color, 2); } } diff --git a/src/components/context-menu/ContextMenu.stories.tsx b/src/components/context-menu/ContextMenu.stories.tsx new file mode 100644 index 000000000..620e2e858 --- /dev/null +++ b/src/components/context-menu/ContextMenu.stories.tsx @@ -0,0 +1,9 @@ +import {ContextMenu} from "./ContextMenu"; +import {Meta} from "@storybook/react-vite"; + +const meta: Meta = { + title: 'Components/ContextMenu', + component: ContextMenu, +} + +export default meta \ No newline at end of file diff --git a/src/components/context-menu/ContextMenu.style.scss b/src/components/context-menu/ContextMenu.style.scss new file mode 100644 index 000000000..feaaca5a3 --- /dev/null +++ b/src/components/context-menu/ContextMenu.style.scss @@ -0,0 +1,70 @@ +@use "../../styles/helpers"; +@use "../../styles/box"; +@use "../../styles/variables"; + +.context-menu { + + &__content, &__sub-content { + padding: variables.$xxs; + position: relative; + box-sizing: border-box; + z-index: 999; + + & { + @include helpers.borderRadius(); + } + } + + &__label { + text-transform: uppercase; + font-size: variables.$xs; + display: flex; + gap: variables.$xxs; + align-items: center; + padding: variables.$xxs variables.$xs; + color: helpers.color(); + + & { + @include helpers.fontStyle(); + } + } + + &__item, &__sub-trigger { + border-radius: variables.$borderRadius - variables.$xxs; + padding: variables.$xxs variables.$xs; + gap: variables.$xs; + cursor: pointer; + width: 100%; + display: flex; + align-items: center; + font-size: variables.$sm; + + & { + @include box.box(variables.$primary, variables.$white, variables.$primary); + @include helpers.noFocusStyle(); + @include helpers.fontStyle(); + @include helpers.disabled(); + border: none; + } + + &:focus, &[data-focus=true] { + @include box.box(variables.$white, variables.$white, variables.$white); + border: none; + width: 100%; + } + } + + &__separator { + border: none; + margin: variables.$xxs 0; + color: rgba(white, .1); + height: 1px; + background-color: rgba(white, .1); + } +} + +@each $name, $color in variables.$colors { + .context-menu__content--#{$name}, .context-menu__sub-content--#{$name} { + @include box.box($color); + } +} \ No newline at end of file diff --git a/src/components/context-menu/ContextMenu.tsx b/src/components/context-menu/ContextMenu.tsx new file mode 100644 index 000000000..db2cf08d7 --- /dev/null +++ b/src/components/context-menu/ContextMenu.tsx @@ -0,0 +1,117 @@ +"use client" + +import React from "react"; +import {Code0ComponentProps, Color, mergeCode0Props} from "../../utils"; +import * as Radix from "@radix-ui/react-context-menu"; +import "./ContextMenu.style.scss" +import {Card} from "../card/Card"; +import {Flex} from "../flex/Flex"; +import {Badge} from "../badge/Badge"; +import {IconArrowDown, IconArrowUp, IconCornerDownLeft} from "@tabler/icons-react"; +import {Spacing} from "../spacing/Spacing"; + +export type ContextMenuProps = Code0ComponentProps & Radix.ContextMenuProps +export type ContextMenuTriggerProps = Code0ComponentProps & Radix.ContextMenuTriggerProps +export type ContextMenuPortalProps = Code0ComponentProps & Radix.ContextMenuPortalProps +export type ContextMenuContentProps = Code0ComponentProps & Radix.ContextMenuContentProps & { + color?: Color +} +export type ContextMenuLabelProps = Code0ComponentProps & Radix.ContextMenuLabelProps +export type ContextMenuItemProps = Code0ComponentProps & Radix.ContextMenuItemProps +export type ContextMenuGroupProps = Code0ComponentProps & Radix.ContextMenuGroupProps +export type ContextMenuSubProps = Code0ComponentProps & Radix.ContextMenuSubProps +export type ContextMenuSubTriggerProps = Code0ComponentProps & Radix.ContextMenuSubTriggerProps +export type ContextMenuSubContentProps = Code0ComponentProps & Radix.ContextMenuSubContentProps & { + color?: Color +} +export type ContextMenuSeparatorProps = Code0ComponentProps & Radix.ContextMenuSeparatorProps +export type ContextMenuArrowProps = Code0ComponentProps & Radix.ContextMenuArrowProps + +export const ContextMenu: React.FC = (props) => { + return +} + +export const ContextMenuTrigger: React.FC = (props) => { + return +} + +export const ContextMenuPortal: React.FC = (props) => { + return +} + +export const ContextMenuContent: React.FC = (props) => { + return + + {props.children} + + + + + + + + + move + + + + + select + + + + +} + +export const ContextMenuLabel: React.FC = (props) => { + return +} + +export const ContextMenuItem: React.FC = (props) => { + return +} + +export const ContextMenuGroup: React.FC = (props) => { + return +} + +export const ContextMenuSub: React.FC = (props) => { + return +} + +export const ContextMenuSubTrigger: React.FC = (props) => { + return +} + +export const ContextMenuSubContent: React.FC = (props) => { + return + + {props.children} + + + + + + + + + move + + + + + select + + + + +} + +export const ContextMenuSeparator: React.FC = (props) => { + return +} + +export const ContextMenuArrow: React.FC = (props) => { + return +} \ No newline at end of file diff --git a/src/components/d-application/DApplication.service.ts b/src/components/d-application/DApplication.service.ts new file mode 100644 index 000000000..b43675f74 --- /dev/null +++ b/src/components/d-application/DApplication.service.ts @@ -0,0 +1,12 @@ +import {ReactiveArrayService} from "../../utils"; +import type { + Application, + ApplicationSettingsUpdateInput, + ApplicationSettingsUpdatePayload +} from "@code0-tech/sagittarius-graphql-types"; + +export abstract class DApplicationService extends ReactiveArrayService { + + abstract settingsUpdate(payload: ApplicationSettingsUpdateInput): Promise; + +} \ No newline at end of file diff --git a/src/components/d-flow/data-type/DFlowDataType.service.ts b/src/components/d-flow-data-type/DFlowDataType.service.ts similarity index 89% rename from src/components/d-flow/data-type/DFlowDataType.service.ts rename to src/components/d-flow-data-type/DFlowDataType.service.ts index 9864d9668..d294bf465 100644 --- a/src/components/d-flow/data-type/DFlowDataType.service.ts +++ b/src/components/d-flow-data-type/DFlowDataType.service.ts @@ -1,23 +1,21 @@ -import {ReactiveArrayService} from "../../../utils/reactiveArrayService"; +import {ReactiveArrayService} from "../../utils"; import {DataTypeView} from "./DFlowDataType.view"; -import {resolveType} from "../../../utils/generics"; +import {resolveType} from "../../utils/generics"; import type { DataTypeIdentifier, DataTypeRule, DataTypeRulesContainsKeyConfig, - DataTypeRulesInputTypesConfig, + DataTypeRulesInputTypesConfig, Flow, GenericMapper, LiteralValue, Maybe, NodeParameterValue, Scalars } from "@code0-tech/sagittarius-graphql-types"; -import {useValidateValue} from "./DFlowDataType.validation.value"; -import {FlowView} from "../DFlow.view"; +import {useValueValidation} from "../d-flow-validation/DValueValidation.hook"; export abstract class DFlowDataTypeReactiveService extends ReactiveArrayService { - //TODO: remove string because of sagittarius types update getDataType (type: DataTypeIdentifier): DataTypeView | undefined { if (!type) return undefined if ((type as DataTypeIdentifier).genericKey) return undefined @@ -28,7 +26,7 @@ export abstract class DFlowDataTypeReactiveService extends ReactiveArrayService< }); } - getDataTypeFromValue (value: NodeParameterValue, flow?: FlowView): DataTypeView | undefined { + getDataTypeFromValue (value: NodeParameterValue, flow?: Flow): DataTypeView | undefined { if (!value) return undefined @@ -43,15 +41,15 @@ export abstract class DFlowDataTypeReactiveService extends ReactiveArrayService< //TODO: performance here is bad const matchingDataTypes = this.values().filter(type => { if (type.identifier === "OBJECT") return false - if (value.__typename === "NodeFunction" && (type.variant != "NODE" || !flow)) return false - return useValidateValue(value, type, flow) + if (value.__typename === "NodeFunctionIdWrapper" && (type.variant != "NODE" || !flow)) return false + return useValueValidation(value, type, this, flow) }) return matchingDataTypes[matchingDataTypes.length - 1] } - getTypeFromValue (value: NodeParameterValue, flow?: FlowView): Maybe | undefined { + getTypeFromValue (value: NodeParameterValue, flow?: Flow): Maybe | undefined { if (!value) return undefined diff --git a/src/components/d-flow/data-type/DFlowDataType.view.ts b/src/components/d-flow-data-type/DFlowDataType.view.ts similarity index 73% rename from src/components/d-flow/data-type/DFlowDataType.view.ts rename to src/components/d-flow-data-type/DFlowDataType.view.ts index 51d536cce..6a65034af 100644 --- a/src/components/d-flow/data-type/DFlowDataType.view.ts +++ b/src/components/d-flow-data-type/DFlowDataType.view.ts @@ -5,13 +5,15 @@ import type { TranslationConnection } from "@code0-tech/sagittarius-graphql-types"; -/* - @todo is DataType castable to another DataType - */ + export class DataTypeView { + /** Name of the function */ + private readonly _aliases?: Maybe; /** Time when this DataType was created */ private readonly _createdAt?: Maybe; + /** Display message of the function */ + private readonly _displayMessages?: Maybe; /** Generic keys of the datatype */ private readonly _genericKeys?: Maybe>; /** Global ID of this DataType */ @@ -20,32 +22,41 @@ export class DataTypeView { private readonly _identifier?: Maybe; /** Names of the flow type setting */ private readonly _name?: Maybe; - /** The namespace where this datatype belongs to */ - private readonly _runtime?: Maybe; /** Rules of the datatype */ private readonly _rules?: Maybe; + /** The namespace where this datatype belongs to */ + private readonly _runtime?: Maybe; /** Time when this DataType was last updated */ private readonly _updatedAt?: Maybe; /** The type of the datatype */ private readonly _variant?: Maybe; constructor(dataType: DataType) { - this._id = dataType.id - this._createdAt = dataType.createdAt - this._updatedAt = dataType.updatedAt - this._identifier = dataType.identifier - this._name = dataType.name ?? undefined - this._runtime = dataType.runtime ?? undefined - this._variant = dataType.variant - this._genericKeys = dataType.genericKeys ?? undefined - this._rules = dataType.rules ?? undefined + this._aliases = dataType.aliases; + this._createdAt = dataType.createdAt; + this._displayMessages = dataType.displayMessages; + this._genericKeys = dataType.genericKeys; + this._id = dataType.id; + this._identifier = dataType.identifier; + this._name = dataType.name; + this._runtime = dataType.runtime; + this._rules = dataType.rules; + this._updatedAt = dataType.updatedAt; + this._variant = dataType.variant; + } + get aliases(): Maybe | undefined { + return this._aliases; } get createdAt(): Maybe | undefined { return this._createdAt; } + get displayMessages(): Maybe | undefined { + return this._displayMessages; + } + get genericKeys(): Maybe> | undefined { return this._genericKeys; } diff --git a/src/components/d-flow/data-type/index.ts b/src/components/d-flow-data-type/index.ts similarity index 100% rename from src/components/d-flow/data-type/index.ts rename to src/components/d-flow-data-type/index.ts diff --git a/src/components/d-flow/data-type/rules/DFlowDataTypeContainsKeyRule.ts b/src/components/d-flow-data-type/rules/DFlowDataTypeContainsKeyRule.ts similarity index 71% rename from src/components/d-flow/data-type/rules/DFlowDataTypeContainsKeyRule.ts rename to src/components/d-flow-data-type/rules/DFlowDataTypeContainsKeyRule.ts index 45b7e2b74..19185740e 100644 --- a/src/components/d-flow/data-type/rules/DFlowDataTypeContainsKeyRule.ts +++ b/src/components/d-flow-data-type/rules/DFlowDataTypeContainsKeyRule.ts @@ -2,17 +2,17 @@ import {DFlowDataTypeRule, genericMapping, staticImplements} from "./DFlowDataTy import {DFlowDataTypeReactiveService} from "../DFlowDataType.service"; import type { DataTypeRulesContainsKeyConfig, - GenericCombinationStrategyType, - GenericMapper, LiteralValue, + Flow, + GenericMapper, + LiteralValue, NodeParameterValue } from "@code0-tech/sagittarius-graphql-types"; -import {useValidateValue} from "../DFlowDataType.validation.value"; -import {FlowView} from "../../DFlow.view"; +import {useValueValidation} from "../../d-flow-validation/DValueValidation.hook"; @staticImplements() export class DFlowDataTypeContainsKeyRule { - public static validate(value: NodeParameterValue, config: DataTypeRulesContainsKeyConfig, generics?: Map, service?: DFlowDataTypeReactiveService, flow?: FlowView): boolean { + public static validate(value: NodeParameterValue, config: DataTypeRulesContainsKeyConfig, generics?: Map, service?: DFlowDataTypeReactiveService, flow?: Flow): boolean { const genericMapper = generics?.get(config?.dataTypeIdentifier?.genericKey!!) const genericTypes = generics?.get(config?.dataTypeIdentifier?.genericKey!!)?.sourceDataTypeIdentifiers @@ -33,7 +33,7 @@ export class DFlowDataTypeContainsKeyRule { //use generic given type for checking against value if (config?.dataTypeIdentifier?.genericKey && genericMapper && genericTypes) { const checkAllTypes: boolean[] = genericTypes.map(genericType => { - return useValidateValue((value as LiteralValue).value[(config?.key ?? "")], service?.getDataType(genericType)!!, flow, ((genericType.genericType)!!.genericMappers as GenericMapper[])) + return useValueValidation((value as LiteralValue).value[(config?.key ?? "")], service?.getDataType(genericType)!!, service!!, flow, ((genericType.genericType)!!.genericMappers as GenericMapper[])) }) const combination = checkAllTypes.length > 1 ? checkAllTypes.reduce((previousValue, currentValue, currentIndex) => { @@ -49,9 +49,9 @@ export class DFlowDataTypeContainsKeyRule { //normal datatype link if (config?.dataTypeIdentifier?.dataType) { - return ((config?.key ?? "") in value) && useValidateValue((value as LiteralValue).value[(config?.key ?? "")], service?.getDataType(config.dataTypeIdentifier)!!) + return ((config?.key ?? "") in value) && useValueValidation((value as LiteralValue).value[(config?.key ?? "")], service?.getDataType(config.dataTypeIdentifier)!!, service!!) } - return ((config?.key ?? "") in value) && useValidateValue((value as LiteralValue).value[(config?.key ?? "")], service?.getDataType(config.dataTypeIdentifier!!)!!, flow, genericMapping(config?.dataTypeIdentifier?.genericType?.genericMappers!!, generics)) + return ((config?.key ?? "") in value) && useValueValidation((value as LiteralValue).value[(config?.key ?? "")], service?.getDataType(config.dataTypeIdentifier!!)!!, service!!, flow, genericMapping(config?.dataTypeIdentifier?.genericType?.genericMappers!!, generics)) } } \ No newline at end of file diff --git a/src/components/d-flow/data-type/rules/DFlowDataTypeContainsTypeRule.ts b/src/components/d-flow-data-type/rules/DFlowDataTypeContainsTypeRule.ts similarity index 72% rename from src/components/d-flow/data-type/rules/DFlowDataTypeContainsTypeRule.ts rename to src/components/d-flow-data-type/rules/DFlowDataTypeContainsTypeRule.ts index 15fbaf8f9..e233d041a 100644 --- a/src/components/d-flow/data-type/rules/DFlowDataTypeContainsTypeRule.ts +++ b/src/components/d-flow-data-type/rules/DFlowDataTypeContainsTypeRule.ts @@ -2,18 +2,17 @@ import {DFlowDataTypeRule, genericMapping, staticImplements} from "./DFlowDataTy import {DFlowDataTypeReactiveService} from "../DFlowDataType.service"; import type { DataTypeRulesContainsKeyConfig, - GenericCombinationStrategyType, + Flow, GenericMapper, GenericType, LiteralValue, NodeParameterValue } from "@code0-tech/sagittarius-graphql-types"; -import {useValidateValue} from "../DFlowDataType.validation.value"; -import {FlowView} from "../../DFlow.view"; +import {useValueValidation} from "../../d-flow-validation/DValueValidation.hook"; @staticImplements() export class DFlowDataTypeContainsTypeRule { - public static validate(value: NodeParameterValue, config: DataTypeRulesContainsKeyConfig, generics?: Map, service?: DFlowDataTypeReactiveService, flow?: FlowView): boolean { + public static validate(value: NodeParameterValue, config: DataTypeRulesContainsKeyConfig, generics?: Map, service?: DFlowDataTypeReactiveService, flow?: Flow): boolean { const genericMapper = generics?.get(config?.dataTypeIdentifier?.genericKey!!) const genericTypes = generics?.get(config?.dataTypeIdentifier?.genericKey!!)?.sourceDataTypeIdentifiers @@ -37,9 +36,15 @@ export class DFlowDataTypeContainsTypeRule { const checkAllTypes: boolean[] = genericTypes.map(genericType => { return (value as LiteralValue).value.every((value1: any) => { if (genericType.genericType) { - return useValidateValue({__typename: "LiteralValue", value: value1}, service?.getDataType(genericType)!!, flow, ((genericType.genericType as GenericType)!!.genericMappers as GenericMapper[])) + return useValueValidation({ + __typename: "LiteralValue", + value: value1 + }, service?.getDataType(genericType)!!, service!!, flow, ((genericType.genericType as GenericType)!!.genericMappers as GenericMapper[])) } - return useValidateValue({__typename: "LiteralValue", value: value1}, service?.getDataType(genericType)!!, flow) + return useValueValidation({ + __typename: "LiteralValue", + value: value1 + }, service?.getDataType(genericType)!!, service!!, flow) }) }) @@ -54,10 +59,10 @@ export class DFlowDataTypeContainsTypeRule { //normal datatype link if (config?.dataTypeIdentifier?.dataType) { - return (value as LiteralValue).value.every((value1: any) => useValidateValue(value1, service?.getDataType(config.dataTypeIdentifier!!)!!)) + return (value as LiteralValue).value.every((value1: any) => useValueValidation(value1, service?.getDataType(config.dataTypeIdentifier!!)!!, service!!)) } - return (value as LiteralValue).value.every((value1: any) => useValidateValue(value1, service?.getDataType(config.dataTypeIdentifier!!)!!, flow, genericMapping((config.dataTypeIdentifier?.genericType as GenericType).genericMappers!!, generics))) + return (value as LiteralValue).value.every((value1: any) => useValueValidation(value1, service?.getDataType(config.dataTypeIdentifier!!)!!, service!!, flow, genericMapping((config.dataTypeIdentifier?.genericType as GenericType).genericMappers!!, generics))) } } \ No newline at end of file diff --git a/src/components/d-flow/data-type/rules/DFlowDataTypeItemOfCollectionRule.ts b/src/components/d-flow-data-type/rules/DFlowDataTypeItemOfCollectionRule.ts similarity index 100% rename from src/components/d-flow/data-type/rules/DFlowDataTypeItemOfCollectionRule.ts rename to src/components/d-flow-data-type/rules/DFlowDataTypeItemOfCollectionRule.ts diff --git a/src/components/d-flow/data-type/rules/DFlowDataTypeNumberRangeRule.ts b/src/components/d-flow-data-type/rules/DFlowDataTypeNumberRangeRule.ts similarity index 100% rename from src/components/d-flow/data-type/rules/DFlowDataTypeNumberRangeRule.ts rename to src/components/d-flow-data-type/rules/DFlowDataTypeNumberRangeRule.ts diff --git a/src/components/d-flow/data-type/rules/DFlowDataTypeParentRule.ts b/src/components/d-flow-data-type/rules/DFlowDataTypeParentRule.ts similarity index 53% rename from src/components/d-flow/data-type/rules/DFlowDataTypeParentRule.ts rename to src/components/d-flow-data-type/rules/DFlowDataTypeParentRule.ts index 295a331e5..f8882352e 100644 --- a/src/components/d-flow/data-type/rules/DFlowDataTypeParentRule.ts +++ b/src/components/d-flow-data-type/rules/DFlowDataTypeParentRule.ts @@ -1,9 +1,8 @@ import {DFlowDataTypeRule, staticImplements} from "./DFlowDataTypeRule"; import {DFlowDataTypeReactiveService} from "../DFlowDataType.service"; -import {replaceGenericKeysInType} from "../../../../utils/generics"; -import type {DataTypeIdentifier, GenericMapper, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; -import {useValidateValue} from "../DFlowDataType.validation.value"; -import {FlowView} from "../../DFlow.view"; +import {replaceGenericKeysInType} from "../../../utils/generics"; +import type {DataTypeIdentifier, Flow, GenericMapper, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; +import {useValueValidation} from "../../d-flow-validation/DValueValidation.hook"; export interface DFlowDataTypeParentRuleConfig { type: DataTypeIdentifier @@ -11,12 +10,12 @@ export interface DFlowDataTypeParentRuleConfig { @staticImplements() export class DFlowDataTypeParentRule { - public static validate(value: NodeParameterValue, config: DFlowDataTypeParentRuleConfig, generics?: Map, service?: DFlowDataTypeReactiveService, flow?: FlowView): boolean { + public static validate(value: NodeParameterValue, config: DFlowDataTypeParentRuleConfig, generics?: Map, service?: DFlowDataTypeReactiveService, flow?: Flow): boolean { const replacedType = generics ? replaceGenericKeysInType(config.type, generics) : config.type if (!service) return false - return useValidateValue(value, service.getDataType(replacedType)!!, flow, Array.from(generics!!, ([_, value]) => value)) + return useValueValidation(value, service.getDataType(replacedType)!!, service, flow, Array.from(generics!!, ([_, value]) => value)) } } \ No newline at end of file diff --git a/src/components/d-flow/data-type/rules/DFlowDataTypeRegexRule.ts b/src/components/d-flow-data-type/rules/DFlowDataTypeRegexRule.ts similarity index 100% rename from src/components/d-flow/data-type/rules/DFlowDataTypeRegexRule.ts rename to src/components/d-flow-data-type/rules/DFlowDataTypeRegexRule.ts diff --git a/src/components/d-flow/data-type/rules/DFlowDataTypeReturnTypeRule.ts b/src/components/d-flow-data-type/rules/DFlowDataTypeReturnTypeRule.ts similarity index 65% rename from src/components/d-flow/data-type/rules/DFlowDataTypeReturnTypeRule.ts rename to src/components/d-flow-data-type/rules/DFlowDataTypeReturnTypeRule.ts index 16c415f64..bdbe465bd 100644 --- a/src/components/d-flow/data-type/rules/DFlowDataTypeReturnTypeRule.ts +++ b/src/components/d-flow-data-type/rules/DFlowDataTypeReturnTypeRule.ts @@ -2,16 +2,16 @@ import {DFlowDataTypeRule, genericMapping, staticImplements} from "./DFlowDataTy import {DFlowDataTypeReactiveService} from "../DFlowDataType.service"; import type { DataTypeRulesReturnTypeConfig, - GenericCombinationStrategyType, + Flow, GenericMapper, GenericType, NodeFunction, + NodeFunctionIdWrapper, NodeParameterValue, ReferenceValue } from "@code0-tech/sagittarius-graphql-types"; -import {FlowView} from "../../DFlow.view"; -import {useValidateDataType} from "../DFlowDataType.validation.type"; -import {useValidateValue} from "../DFlowDataType.validation.value"; +import {useDataTypeValidation} from "../../d-flow-validation/DDataTypeValidation.hook"; +import {useValueValidation} from "../../d-flow-validation/DValueValidation.hook"; //TODO: simple use useReturnType function @staticImplements() @@ -21,19 +21,18 @@ export class DFlowDataTypeReturnTypeRule { config: DataTypeRulesReturnTypeConfig, generics?: Map, service?: DFlowDataTypeReactiveService, - flow?: FlowView + flow?: Flow ): boolean { const genericMapper = generics?.get(config?.dataTypeIdentifier?.genericKey!!) const genericTypes = generics?.get(config?.dataTypeIdentifier?.genericKey!!)?.sourceDataTypeIdentifiers const genericCombination = generics?.get(config?.dataTypeIdentifier?.genericKey!!)?.genericCombinationStrategies - if (value.__typename != "NodeFunction") return false + if (value.__typename != "NodeFunctionIdWrapper") return false const foundReturnFunction = findReturnNode(value, flow!!) if (!foundReturnFunction) return false - //TODO: only if its really a generic key if (config?.dataTypeIdentifier?.genericKey && !genericMapper && !service?.getDataType(config.dataTypeIdentifier)) return true if (!(service?.getDataType(config.dataTypeIdentifier!!) || genericMapper)) return false @@ -50,7 +49,7 @@ export class DFlowDataTypeReturnTypeRule { if (config?.dataTypeIdentifier?.genericKey && genericMapper && genericTypes) { const checkAllTypes: boolean[] = genericTypes.map(genericType => { - return useValidateDataType(service?.getDataType(genericType)!!, service?.getDataType((foundReturnFunction?.parameters?.nodes!![0]?.value!! as ReferenceValue).dataTypeIdentifier!!)!!) + return useDataTypeValidation(service?.getDataType(genericType)!!, service?.getDataType((foundReturnFunction?.parameters?.nodes!![0]?.value!! as ReferenceValue).dataTypeIdentifier!!)!!) }) return checkAllTypes.length > 1 ? checkAllTypes.reduce((previousValue, currentValue, currentIndex) => { @@ -63,10 +62,10 @@ export class DFlowDataTypeReturnTypeRule { } if (config?.dataTypeIdentifier?.dataType) { - return useValidateDataType(service?.getDataType(config.dataTypeIdentifier!!)!!, service?.getDataType(foundReturnFunction?.parameters?.nodes!![0]?.value?.dataTypeIdentifier!!)!!) + return useDataTypeValidation(service?.getDataType(config.dataTypeIdentifier!!)!!, service?.getDataType(foundReturnFunction?.parameters?.nodes!![0]?.value?.dataTypeIdentifier!!)!!) } - } else if (foundReturnFunction?.parameters?.nodes!![0]?.value?.__typename == "NodeFunction") { + } else if (foundReturnFunction?.parameters?.nodes!![0]?.value?.__typename == "NodeFunctionIdWrapper") { //TODO : allow function as return value } else { @@ -74,7 +73,7 @@ export class DFlowDataTypeReturnTypeRule { if (config?.dataTypeIdentifier?.genericKey && genericMapper && genericTypes) { const checkAllTypes: boolean[] = genericTypes.map(genericType => { - return useValidateValue(foundReturnFunction?.parameters?.nodes!![0]?.value!!, service?.getDataType(genericType)!!, flow, ((genericType.genericType as GenericType)!!.genericMappers as GenericMapper[])) + return useValueValidation(foundReturnFunction?.parameters?.nodes!![0]?.value!!, service?.getDataType(genericType)!!, service!!, flow, ((genericType.genericType as GenericType)!!.genericMappers as GenericMapper[])) }) return checkAllTypes.length > 1 ? checkAllTypes.reduce((previousValue, currentValue, currentIndex) => { @@ -87,10 +86,10 @@ export class DFlowDataTypeReturnTypeRule { } if (config?.dataTypeIdentifier?.dataType) { - return useValidateValue(foundReturnFunction?.parameters?.nodes!![0]?.value!!, service?.getDataType(config.dataTypeIdentifier!!)!!) + return useValueValidation(foundReturnFunction?.parameters?.nodes!![0]?.value!!, service?.getDataType(config.dataTypeIdentifier!!)!!, service!!) } - return useValidateValue(foundReturnFunction?.parameters?.nodes!![0]?.value!!, service?.getDataType(config.dataTypeIdentifier!!)!!, flow, genericMapping(config.dataTypeIdentifier?.genericType?.genericMappers!!, generics)) + return useValueValidation(foundReturnFunction?.parameters?.nodes!![0]?.value!!, service?.getDataType(config.dataTypeIdentifier!!)!!, service!!, flow, genericMapping(config.dataTypeIdentifier?.genericType?.genericMappers!!, generics)) } @@ -99,12 +98,17 @@ export class DFlowDataTypeReturnTypeRule { } } -const findReturnNode = (n: NodeFunction, flow: FlowView): NodeFunction | undefined => { +const findReturnNode = (n: NodeFunctionIdWrapper, flow: Flow): NodeFunction | undefined => { - if (n.functionDefinition?.runtimeFunctionDefinition?.identifier === 'RETURN') return n - if (flow.getNodeById(n.nextNodeId!!)) { - const found = findReturnNode(flow.getNodeById(n.nextNodeId!!)!!.json()!!, flow) + const node = flow.nodes?.nodes?.find(node => node?.id === n.id) as NodeFunction | undefined + if (node?.functionDefinition?.runtimeFunctionDefinition?.identifier === 'RETURN') return node + + if (node) { + const found = findReturnNode({ + id: node.nextNodeId, + __typename: "NodeFunctionIdWrapper" + }, flow) if (found) return found } diff --git a/src/components/d-flow/data-type/rules/DFlowDataTypeRule.ts b/src/components/d-flow-data-type/rules/DFlowDataTypeRule.ts similarity index 83% rename from src/components/d-flow/data-type/rules/DFlowDataTypeRule.ts rename to src/components/d-flow-data-type/rules/DFlowDataTypeRule.ts index 626fc1fc2..8959bebf3 100644 --- a/src/components/d-flow/data-type/rules/DFlowDataTypeRule.ts +++ b/src/components/d-flow-data-type/rules/DFlowDataTypeRule.ts @@ -1,9 +1,8 @@ import {DFlowDataTypeReactiveService} from "../DFlowDataType.service"; -import type {GenericMapper, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; -import {FlowView} from "../../DFlow.view"; +import type {Flow, GenericMapper, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; export interface DFlowDataTypeRule { - validate(value: NodeParameterValue, config: object, generics?: Map, service?: DFlowDataTypeReactiveService, flow?: FlowView): boolean + validate(value: NodeParameterValue, config: object, generics?: Map, service?: DFlowDataTypeReactiveService, flow?: Flow): boolean } export const staticImplements = () => { diff --git a/src/components/d-flow/data-type/rules/DFlowDataTypeRules.ts b/src/components/d-flow-data-type/rules/DFlowDataTypeRules.ts similarity index 100% rename from src/components/d-flow/data-type/rules/DFlowDataTypeRules.ts rename to src/components/d-flow-data-type/rules/DFlowDataTypeRules.ts diff --git a/src/components/d-flow/tab/DFlowTabDefault.tsx b/src/components/d-flow-file/DFlowTabDefault.tsx similarity index 70% rename from src/components/d-flow/tab/DFlowTabDefault.tsx rename to src/components/d-flow-file/DFlowTabDefault.tsx index b354d9962..7700be845 100644 --- a/src/components/d-flow/tab/DFlowTabDefault.tsx +++ b/src/components/d-flow-file/DFlowTabDefault.tsx @@ -1,27 +1,25 @@ import React from "react"; -import {NodeFunctionView} 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 "../suggestion/DFlowSuggestion.hook"; -import {DFlowSuggestionMenuFooter} from "../suggestion/DFlowSuggestionMenuFooter"; -import {toInputSuggestions} from "../suggestion/DFlowSuggestionMenu.util"; -import {DFlowReactiveService} from "../DFlow.service"; -import {DFlowSuggestion} from "../suggestion/DFlowSuggestion.view"; -import {ParameterDefinitionView} from "../function/DFlowFunction.view"; -import {Badge} from "../../badge/Badge"; +import {TextInput} from "../form"; +import {Flex} from "../flex/Flex"; +import {useService} from "../../utils"; +import {DFlowFunctionReactiveService} from "../d-flow-function"; +import {useSuggestions} from "../d-flow-suggestion/DFlowSuggestion.hook"; +import {DFlowSuggestionMenuFooter} from "../d-flow-suggestion/DFlowSuggestionMenuFooter"; +import {toInputSuggestions} from "../d-flow-suggestion/DFlowSuggestionMenu.util"; +import {DFlowReactiveService} from "../d-flow"; +import {DFlowSuggestion} from "../d-flow-suggestion"; +import {ParameterDefinitionView} from "../d-flow-function"; +import {Badge} from "../badge/Badge"; import type { LiteralValue, - NodeFunction, - NodeParameterValue, + NodeFunction, NodeParameterValue, ReferenceValue, Scalars } from "@code0-tech/sagittarius-graphql-types"; -import {InputSyntaxSegment} from "../../form/Input.syntax.hook"; +import {InputSyntaxSegment} from "../form/Input.syntax.hook"; export interface DFlowTabDefaultProps { - functionInstance: NodeFunctionView + node: NodeFunction flowId: Scalars["FlowID"]["output"] depthLevel?: number scopeLevel?: number[] @@ -30,10 +28,12 @@ export interface DFlowTabDefaultProps { export const DFlowTabDefault: React.FC = (props) => { - const {functionInstance, flowId, depthLevel, scopeLevel, nodeLevel} = props + const {node, flowId, depthLevel, scopeLevel, nodeLevel} = props const functionService = useService(DFlowFunctionReactiveService) const flowService = useService(DFlowReactiveService) - const definition = functionService.getById(functionInstance.functionDefinition?.id!!) + const [, startTransition] = React.useTransition() + + const definition = functionService.getById(node.functionDefinition?.id!!) const paramDefinitions = React.useMemo(() => { const map: Record = {} definition?.parameterDefinitions?.forEach(pd => { @@ -43,26 +43,30 @@ export const DFlowTabDefault: React.FC = (props) => { }, [definition?.parameterDefinitions]) const sortedParameters = React.useMemo(() => { - return [...(functionInstance.parameters || [])].sort((a, b) => a.id!!.localeCompare(b.id!!)) - }, [functionInstance.parameters]) + return [...(node.parameters?.nodes || [])].sort((a, b) => a!!.id!!.localeCompare(b?.id!!)) + }, [node.parameters]) const suggestionsById: Record = {} sortedParameters.forEach(parameter => { - const parameterDefinition = paramDefinitions[parameter.id!!] - suggestionsById[parameter.id!!] = useSuggestions(parameterDefinition?.dataTypeIdentifier!!, [], flowId, depthLevel, scopeLevel, nodeLevel) + const parameterDefinition = paramDefinitions[parameter?.id!!] + suggestionsById[parameter?.id!!] = useSuggestions(parameterDefinition?.dataTypeIdentifier!!, [], flowId, depthLevel, scopeLevel, nodeLevel) }) return {sortedParameters.map(parameter => { - const submitValue = (value: NodeParameterValue | undefined) => { - parameter.value = value - flowService.update() + if (!parameter) return null + + const submitValue = (value: NodeFunction | LiteralValue | ReferenceValue | undefined) => { + startTransition(async () => { + await flowService.setParameterValue(flowId, node.id!!, parameter.id!!, value) + }) + } const submitValueEvent = (event: any) => { try { - const value = JSON.parse(event.target.value) as NodeParameterValue + const value = JSON.parse(event.target.value) as NodeFunction | LiteralValue | ReferenceValue if (!value.__typename) { submitValue(value ? { __typename: "LiteralValue", @@ -84,10 +88,7 @@ export const DFlowTabDefault: React.FC = (props) => { const result = suggestionsById[parameter.id!!] const title = parameterDefinition?.names ? parameterDefinition?.names?.nodes!![0]?.content : parameterDefinition?.id const description = parameterDefinition?.descriptions ? parameterDefinition?.descriptions?.nodes!![0]?.content : JSON.stringify(parameterDefinition?.dataTypeIdentifier) - const defaultValue: string | undefined = parameter.value instanceof NodeFunctionView ? JSON.stringify({ - ...parameter.value.json(), - __typename: "NodeFunction" - }) : parameter.value?.__typename === "ReferenceValue" ? JSON.stringify(parameter.value) : parameter.value?.__typename === "LiteralValue" ? typeof parameter.value?.value === "object" ? JSON.stringify(parameter.value?.value) : parameter.value.value : "" + const defaultValue: string | undefined = parameter.value?.__typename === "LiteralValue" ? (typeof parameter.value?.value === "object" ? JSON.stringify(parameter.value?.value) : parameter.value.value) : JSON.stringify(parameter.value) return
= (props) => { try { const parsed = JSON.parse(textValue) as NodeParameterValue - if (parsed?.__typename === "NodeFunction") { - const def = functionService.getById((parsed as NodeFunction).functionDefinition?.id!!) + if (parsed?.__typename === "NodeFunctionIdWrapper") { + const node = flowService.getNodeById(flowId, parsed.id) + const functionDefinition = functionService.getById(node?.functionDefinition?.id) return buildBlockSegment( - {def?.names?.nodes!![0]?.content} + {functionDefinition?.names?.nodes!![0]?.content} ) } @@ -140,10 +142,11 @@ export const DFlowTabDefault: React.FC = (props) => { onSuggestionSelect={(suggestion) => { submitValue(suggestion.value) }} + //TODO: validation formValidation={{ setValue: () => {}, - valid: parameter.validationResults.length <= 0, - notValidMessage: parameter.validationResults.map(value => value.message.nodes!![0]?.content).join(", ") + valid: true, + notValidMessage: "" }} onBlur={submitValueEvent} onClear={submitValueEvent} diff --git a/src/components/d-flow/tab/DFlowTabTrigger.tsx b/src/components/d-flow-file/DFlowTabTrigger.tsx similarity index 69% rename from src/components/d-flow/tab/DFlowTabTrigger.tsx rename to src/components/d-flow-file/DFlowTabTrigger.tsx index 7e938fd5a..1e47f301f 100644 --- a/src/components/d-flow/tab/DFlowTabTrigger.tsx +++ b/src/components/d-flow-file/DFlowTabTrigger.tsx @@ -1,28 +1,31 @@ import React from "react"; -import {FlowView} 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 {DFlowSuggestion} from "../suggestion/DFlowSuggestion.view"; -import {useSuggestions} from "../suggestion/DFlowSuggestion.hook"; -import {DFlowSuggestionMenuFooter} from "../suggestion/DFlowSuggestionMenuFooter"; -import {toInputSuggestions} from "../suggestion/DFlowSuggestionMenu.util"; -import type {DataType, NodeParameterValue, Scalars} from "@code0-tech/sagittarius-graphql-types"; -import {DFlowInputDataType} from "../input/DFlowInputDataType"; +import {useService} from "../../utils"; +import {DFlowReactiveService} from "../d-flow"; +import {TextInput} from "../form"; +import {Flex} from "../flex/Flex"; +import {DFlowTypeReactiveService} from "../d-flow-type"; +import {DFlowSuggestion} from "../d-flow-suggestion"; +import {useSuggestions} from "../d-flow-suggestion/DFlowSuggestion.hook"; +import {DFlowSuggestionMenuFooter} from "../d-flow-suggestion/DFlowSuggestionMenuFooter"; +import {toInputSuggestions} from "../d-flow-suggestion/DFlowSuggestionMenu.util"; +import type {DataType, Flow, NodeParameterValue, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowInputDataType} from "../d-flow-input/DFlowInputDataType"; export interface DFlowTabTriggerProps { - instance: FlowView + instance: Flow } export const DFlowTabTrigger: React.FC = (props) => { const {instance} = props + const flowTypeService = useService(DFlowTypeReactiveService) const flowService = useService(DFlowReactiveService) + const [,startTransition] = React.useTransition() + const definition = flowTypeService.getById(instance.type?.id!!) + const suggestionsById: Record = {} definition?.flowTypeSettings?.forEach(settingDefinition => { suggestionsById[settingDefinition.identifier!!] = useSuggestions({dataType: settingDefinition.dataType}, [], instance.id, 0, [0], 0) @@ -35,7 +38,7 @@ export const DFlowTabTrigger: React.FC = (props) => { flowService.update() }} initialValue={instance.inputType || definition.inputType} blockingDataType={definition.inputType}/> : null} {definition?.flowTypeSettings?.map(settingDefinition => { - const setting = instance.settings?.find(s => s.flowSettingIdentifier == settingDefinition.identifier) + const setting = instance.settings?.nodes?.find(s => s?.flowSettingIdentifier == settingDefinition.identifier) const title = settingDefinition.names?.nodes!![0]?.content ?? "" const description = settingDefinition?.descriptions?.nodes!![0]?.content ?? "" const result = suggestionsById[settingDefinition.identifier!!] @@ -46,12 +49,14 @@ export const DFlowTabTrigger: React.FC = (props) => { const defaultValue = setting.value?.__typename === "LiteralValue" ? typeof setting?.value == "object" ? JSON.stringify(setting?.value) : setting?.value : typeof setting?.value == "object" ? JSON.stringify(setting?.value) : setting?.value const submitValue = (value: NodeParameterValue) => { - if (value.__typename == "LiteralValue") { - setting.value = value.value - } else { - setting.value = value - } - flowService.update() + startTransition(async () => { + if (value.__typename == "LiteralValue") { + await flowService.setSettingValue(props.instance.id, setting.id, value.value) + } else { + await flowService.setSettingValue(props.instance.id, setting.id, value) + } + }) + } const submitValueEvent = (event: any) => { diff --git a/src/components/d-flow-file/DFlowTabs.tsx b/src/components/d-flow-file/DFlowTabs.tsx new file mode 100644 index 000000000..ed953d1ca --- /dev/null +++ b/src/components/d-flow-file/DFlowTabs.tsx @@ -0,0 +1,200 @@ +import {useService, useStore} from "../../utils"; +import {FileTabsService} from "../file-tabs/FileTabs.service"; +import {FileTabs, FileTabsContent, FileTabsList, FileTabsTrigger} from "../file-tabs/FileTabs"; +import React from "react"; +import {Menu, MenuContent, MenuItem, MenuLabel, MenuPortal, MenuSeparator, MenuTrigger} from "../menu/Menu"; +import {Button} from "../button/Button"; +import {IconArrowDown, IconArrowUp, IconCornerDownLeft, IconDotsVertical, IconPlus} from "@tabler/icons-react"; +import {FileTabsView} from "../file-tabs/FileTabs.view"; +import {DLayout} from "../d-layout/DLayout"; +import {ButtonGroup} from "../button-group/ButtonGroup"; +import {Flow} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowReactiveService} from "../d-flow"; +import {DFlowTypeReactiveService} from "../d-flow-type"; +import {Card} from "../card/Card"; +import {Flex} from "../flex/Flex"; +import {Badge} from "../badge/Badge"; +import {Spacing} from "../spacing/Spacing"; + +export interface DFlowTabsProps { + flowId: Flow['id'] +} + +export const DFlowTabs: React.FC = (props) => { + + const {flowId} = props + + const fileTabsService = useService(FileTabsService) + const fileTabsStore = useStore(FileTabsService) + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + const flowTypeService = useService(DFlowTypeReactiveService) + const flowTypeStore = useStore(DFlowTypeReactiveService) + const id = React.useId() + + const flow = React.useMemo(() => flowService.getById(flowId), [flowStore]) + const flowType = React.useMemo(() => flowTypeService.getById(flow?.type?.id!!), [flowTypeStore, flow]) + const activeTabId = React.useMemo(() => { + return fileTabsStore.find((t: any) => (t as any).active)?.id ?? fileTabsService.getActiveTab()?.id; + }, [fileTabsStore, fileTabsService]) + + const triggerTab = React.useMemo(() => { + if (!flowType?.id) return undefined + return fileTabsStore.find((tab: FileTabsView) => tab.id === flowType.id) + }, [fileTabsStore, flowType]) + + const visibleTabs = React.useMemo(() => { + return fileTabsStore.filter((tab: FileTabsView) => tab.show) + }, [fileTabsStore, triggerTab]) + + const hiddenTabs = React.useMemo(() => { + return fileTabsStore.filter((tab: FileTabsView) => !tab.show && tab.id !== triggerTab?.id) + }, [fileTabsStore, triggerTab]) + + 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 + fileTabsService.update() + }} + > + + + + + + + + + Starting Node + {triggerTab && + fileTabsService.activateTab(triggerTab.id!!)}> + {triggerTab.children} + } + + Opened Nodes + {visibleTabs.map((tab: FileTabsView) => ( + { + fileTabsService.activateTab(tab.id!) + }}> + {tab.children} + + ))} + + Available Node + {hiddenTabs.map((tab: FileTabsView) => ( + { + fileTabsService.activateTab(tab.id!) + }}> + {tab.children} + + ))} + + + + + + + + + move + + + + + select + + + + + + + + + + + + + + + fileTabsService.clearAll()}>Close all tabs + fileTabsService.clearWithoutActive()}>Close other + tabs + + fileTabsService.clearLeft()}>Close all tabs to + left + fileTabsService.clearRight()}>Close all tabs to + right + + + + + + + + + move + + + + + select + + + + + + + + } + > + {visibleTabs.map((tab: FileTabsView, _: number) => { + return tab.show && { + fileTabsService.removeTabById(tab.id!!) + }} + > + {tab.children} + + })} + }> + {fileTabsStore.map((tab: FileTabsView) => ( + + {tab.content} + + ))} + + + ); + +} \ No newline at end of file diff --git a/src/components/d-flow-file/index.ts b/src/components/d-flow-file/index.ts new file mode 100644 index 000000000..e75776eff --- /dev/null +++ b/src/components/d-flow-file/index.ts @@ -0,0 +1 @@ +export * from "./DFlowTabs" \ No newline at end of file diff --git a/src/components/d-flow/folder/DFlowFolder.style.scss b/src/components/d-flow-folder/DFlowFolder.style.scss similarity index 83% rename from src/components/d-flow/folder/DFlowFolder.style.scss rename to src/components/d-flow-folder/DFlowFolder.style.scss index 0205beb6e..7da9d0801 100644 --- a/src/components/d-flow/folder/DFlowFolder.style.scss +++ b/src/components/d-flow-folder/DFlowFolder.style.scss @@ -1,6 +1,6 @@ -@use "../../../styles/helpers"; -@use "../../../styles/box"; -@use "../../../styles/variables"; +@use "../../styles/helpers"; +@use "../../styles/box"; +@use "../../styles/variables"; .d-folder { padding: variables.$xxs variables.$xs; @@ -9,6 +9,8 @@ flex-wrap: nowrap; gap: variables.$xxs; cursor: pointer; + align-items: center; + justify-content: space-between; font-size: variables.$sm; -webkit-touch-callout: none; @@ -27,6 +29,10 @@ background: variables.$primary; } + &[data-state=open] { + @include box.box(variables.$secondary); + border: none; + } &__icon, &__status, &__item-icon { color: variables.$info; @@ -53,7 +59,6 @@ position: absolute; content: ""; left: 5px; - } } @@ -76,5 +81,10 @@ @include helpers.fontStyle(); border: none; } + + &[data-state=open] { + @include box.box(variables.$secondary); + border: none; + } } } \ No newline at end of file diff --git a/src/components/d-flow-folder/DFlowFolder.tsx b/src/components/d-flow-folder/DFlowFolder.tsx new file mode 100644 index 000000000..559a67147 --- /dev/null +++ b/src/components/d-flow-folder/DFlowFolder.tsx @@ -0,0 +1,214 @@ +"use client" + +import "./DFlowFolder.style.scss" +import React from "react" +import {Code0Component, mergeCode0Props, useService, useStore} from "../../utils" +import {IconChevronDown, IconChevronRight, IconDots, IconFolder, IconFolderOpen} from "@tabler/icons-react" +import type {Flow, FlowType, Scalars} from "@code0-tech/sagittarius-graphql-types" +import {DFlowReactiveService} from "../d-flow" +import {ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "../scroll-area/ScrollArea" +import {Flex} from "../flex/Flex" +import {Text} from "../text/Text" +import {Button} from "../button/Button" +import {DFlowFolderContextMenu} from "./DFlowFolderContextMenu"; + + +export interface DFlowFolderProps { + activeFlowId: Scalars["FlowID"]["output"] + onRename?: (flow: Flow, newName: string) => void + onDelete?: (flow: Flow) => void + onCreate?: (name: string, type: FlowType['id']) => void +} + +export interface DFlowFolderGroupProps extends DFlowFolderProps, Omit, "controls"> { + name: string + children: React.ReactElement | React.ReactElement[] | React.ReactElement | React.ReactElement[] + defaultOpen?: boolean + flows: Flow[] +} + +export interface DFlowFolderItemProps extends DFlowFolderProps, Code0Component { + name: string + path: string + icon?: React.ReactNode + active?: boolean + flow: Flow +} + +export const DFlowFolder: React.FC = (props) => { + + const {activeFlowId} = props + + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + + type TreeNode = { + name: string + path: string + children: Record + flow?: Flow + } + + const normalizePath = (p: string) => + p.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean) + + const flows = React.useMemo(() => { + const raw = (flowService.values?.() ?? []) as Flow[] + return raw.filter(f => !!f?.name) + }, [flowStore]) + + const activePathSegments = React.useMemo(() => { + const active = flows.find(f => f.id === activeFlowId) + if (!active?.name) return [] + return normalizePath(active.name) + }, [flows, activeFlowId]) + + const tree = React.useMemo(() => { + const root: TreeNode = {name: "", path: "", children: {}} + for (const flow of flows) { + const segs = normalizePath(flow.name as string) + if (segs.length === 0) continue + + let cur = root + let acc = "" + for (let i = 0; i < segs.length; i++) { + const seg = segs[i] + acc = acc ? `${acc}/${seg}` : seg + + if (i === segs.length - 1) { + // leaf (Flow) + if (!cur.children[seg]) { + cur.children[seg] = { + name: seg, + path: acc, + children: {}, + flow + } + } else { + // falls es bereits einen Knoten gibt, hänge Flow an + cur.children[seg].flow = flow + } + } else { + // folder + if (!cur.children[seg]) { + cur.children[seg] = { + name: seg, + path: acc, + children: {} + } + } + cur = cur.children[seg] + } + } + } + return root + }, [flows]) + + const isPrefixOfActive = React.useCallback((nodePath: string) => { + if (!nodePath) return false + const segs = nodePath.split("/").filter(Boolean) + return segs.every((s, i) => activePathSegments[i] === s) + }, [activePathSegments]) + + const renderChildren = React.useCallback((childrenMap: Record) => { + const nodes = Object.values(childrenMap) + + const folders = nodes.filter(n => !n.flow) + const items = nodes.filter(n => !!n.flow) + + folders.sort((a, b) => a.name.localeCompare(b.name)) + items.sort((a, b) => a.name.localeCompare(b.name)) + + return ( + <> + {folders.map(folder => ( + value.flow!!)} + defaultOpen={isPrefixOfActive(folder.path)} + {...props} + > + {renderChildren(folder.children)} + + ))} + {items.map(item => ( + + ))} + + ) + }, [activeFlowId, isPrefixOfActive]) + + return ( + + +
+ {renderChildren(tree.children)} +
+
+ + + +
+ ) + +} + +export const DFlowFolderGroup: React.FC = (props) => { + + const {name, flows, defaultOpen = false, children, ...rest} = props + const [open, setOpen] = React.useState(defaultOpen) + + return <> + +
setOpen(prevState => !prevState)} {...mergeCode0Props(`d-folder`, rest)}> + + + {open ? : } + + {name} + + + + + +
+
+
+ {open ? children : null} +
+ +} + +export const DFlowFolderItem: React.FC = (props) => { + + const {name, path, flow, icon, active, ...rest} = props + + return +
+ {icon ? {icon} : null} + {name} +
+
+ +} diff --git a/src/components/d-flow-folder/DFlowFolderContextMenu.tsx b/src/components/d-flow-folder/DFlowFolderContextMenu.tsx new file mode 100644 index 000000000..7929ef6c9 --- /dev/null +++ b/src/components/d-flow-folder/DFlowFolderContextMenu.tsx @@ -0,0 +1,131 @@ +import {DFlowFolderProps} from "./DFlowFolder"; +import React from "react"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuPortal, + ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger +} from "../context-menu/ContextMenu"; +import {Flex} from "../flex/Flex"; +import {Text} from "../text/Text"; +import {IconChevronRight, IconEdit, IconTrash} from "@tabler/icons-react"; +import {Dialog, DialogClose, DialogContent, DialogPortal} from "../dialog/Dialog"; +import {Badge} from "../badge/Badge"; +import {Button} from "../button/Button"; +import {useService, useStore} from "../../utils"; +import {DFlowTypeReactiveService} from "../d-flow-type"; +import {DFlowFolderItemPathInput} from "./DFlowFolderItemPathInput"; +import {Flow, FlowType} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowFolderRenameDialog} from "./DFlowFolderRenameDialog"; +import {DFlowFolderCreateDialog} from "./DFlowFolderCreateDialog"; + +export interface DFlowFolderContextMenuGroupData { + name: string + flows: Flow[] + type: "group" +} + +export interface DFlowFolderContextMenuItemData { + name: string + flow: Flow + type: "item" +} + +export interface DFlowFolderContextMenuProps extends DFlowFolderProps { + children: React.ReactNode + contextData: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData +} + +export const DFlowFolderContextMenu: React.FC = (props) => { + + const {children} = props + + const flowTypeService = useService(DFlowTypeReactiveService) + const flowTypeStore = useStore(DFlowTypeReactiveService) + + const flowTypes = React.useMemo(() => flowTypeService.values(), [flowTypeStore]) + const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) + const [renameDialogOpen, setRenameDialogOpen] = React.useState(false) + const [createDialogOpen, setCreateDialogOpen] = React.useState(false) + const [flowTypeId, setFlowTypeId] = React.useState(null) + + return <> + setDeleteDialogOpen(open)}> + + + + {props.contextData.type == "item" ? "Are you sure you want to remove flow" : "Are you sure you want to remove folder"} {" "} + + {props.contextData.name} + {" "} + {props.contextData.type == "group" ? ", all flows and sub-folders inside " : ""}from the this + project? + + + + + + + + + + + + + + setRenameDialogOpen(open)} open={renameDialogOpen} {...props}/> + + setCreateDialogOpen(open)} open={createDialogOpen} {...props}/> + + + + {children} + + + + + + + New flow + + + + + {flowTypes.map(flowType => { + return { + setFlowTypeId(flowType.id) + setCreateDialogOpen(true) + }}> + {flowType.names?.nodes!![0]?.content ?? flowType.id} + + })} + + + setRenameDialogOpen(true)}> + + Rename + + setDeleteDialogOpen(true)}> + + Delete + + + + + + +} \ No newline at end of file diff --git a/src/components/d-flow-folder/DFlowFolderCreateDialog.tsx b/src/components/d-flow-folder/DFlowFolderCreateDialog.tsx new file mode 100644 index 000000000..e24027ed8 --- /dev/null +++ b/src/components/d-flow-folder/DFlowFolderCreateDialog.tsx @@ -0,0 +1,63 @@ +import {DFlowFolderProps} from "./DFlowFolder"; +import React from "react"; +import {TextInput, useForm} from "../form"; +import {Dialog, DialogClose, DialogContent, DialogPortal} from "../dialog/Dialog"; +import {Flex} from "../flex/Flex"; +import {Button} from "../button/Button"; +import {FlowType} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowFolderItemPathInput} from "./DFlowFolderItemPathInput"; + +export interface DFlowFolderCreateDialogProps extends DFlowFolderProps { + open?: boolean + onOpenChange?: (open: boolean) => void + flowTypeId: FlowType['id'] +} + +export const DFlowFolderCreateDialog: React.FC = (props) => { + const {open} = props + + const [createDialogOpen, setCreateDialogOpen] = React.useState(open) + const initialValues = React.useMemo(() => ({ + name: "" + }), []) + + React.useEffect(() => { + setCreateDialogOpen(open) + }, [open]) + + const [inputs, validate] = useForm({ + initialValues: initialValues, + validate: { + name: (value) => { + if (!value) return "Name is required" + return null + } + }, + onSubmit: (values) => { + props.onCreate?.(values.name, props.flowTypeId) + props.onOpenChange?.(false) + } + }) + + return { + props.onOpenChange?.(open) + }}> + + +
+ +
+ + + + + + +
+
+
+} \ No newline at end of file diff --git a/src/components/d-flow-folder/DFlowFolderItemPathInput.tsx b/src/components/d-flow-folder/DFlowFolderItemPathInput.tsx new file mode 100644 index 000000000..5753f83eb --- /dev/null +++ b/src/components/d-flow-folder/DFlowFolderItemPathInput.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import {InputSuggestion, TextInput, TextInputProps} from "../form"; +import {InputSyntaxSegment} from "../form/Input.syntax.hook"; +import {Badge} from "../badge/Badge"; + +export interface DFlowFolderItemPathInputProps extends TextInputProps { + +} + +export const DFlowFolderItemPathInput: React.FC = (props) => { + + const {...rest} = props + + const transformSyntax = ( + value?: string | null, + _: (InputSuggestion | any)[] = [], + ): InputSyntaxSegment[] => { + + const normalizePath = (s: string) => { + const r = [], b = "" + let buf = b + s.split("").forEach(c => + c === "/" + ? (buf && r.push(buf), buf = "", r.push("/")) + : buf += c + ) + buf && r.push(buf) + return r + }; + const splitValue = normalizePath(value ?? "") + let cursor = 0 + + return splitValue.map((value, index) => { + const segment = { + type: splitValue.length - 1 !== index ? "block" : "text", + start: cursor, + end: cursor + value.length, + visualLength: splitValue.length - 1 !== index ? 1 : value.length, + content: splitValue.length - 1 !== index ? {value} : value, + } + cursor += value.length + return [segment] + }).flat() as InputSyntaxSegment[] + + } + + return +} \ No newline at end of file diff --git a/src/components/d-flow-folder/DFlowFolderRenameDialog.tsx b/src/components/d-flow-folder/DFlowFolderRenameDialog.tsx new file mode 100644 index 000000000..f5354a15e --- /dev/null +++ b/src/components/d-flow-folder/DFlowFolderRenameDialog.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import {Dialog, DialogClose, DialogContent, DialogPortal} from "../dialog/Dialog"; +import {DFlowFolderItemPathInput} from "./DFlowFolderItemPathInput"; +import {Flex} from "../flex/Flex"; +import {Button} from "../button/Button"; +import {DFlowFolderContextMenuGroupData, DFlowFolderContextMenuItemData} from "./DFlowFolderContextMenu"; +import {DFlowFolderProps} from "./DFlowFolder"; +import {useForm} from "../form"; + +export interface DFlowFolderRenameDialogProps extends DFlowFolderProps { + contextData: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export const DFlowFolderRenameDialog: React.FC = (props) => { + const {open} = props + + const [renameDialogOpen, setRenameDialogOpen] = React.useState(open) + const initialValues = React.useMemo(() => ({ + path: props.contextData.name + }), []) + + React.useEffect(() => { + setRenameDialogOpen(open) + }, [open]) + + const [inputs, validate] = useForm({ + initialValues: initialValues, + validate: { + path: (value) => { + return null + } + }, + onSubmit: (values) => { + if (props.contextData.type === "item") { + props.onRename?.(props.contextData.flow, values.path) + } else if (props.contextData.type === "group") { + props.contextData.flows.forEach(flow => { + const newName = flow.name?.replace(props.contextData.name, values.path) ?? flow.name + props.onRename?.(flow, newName!) + }) + } + } + }) + + return { + props.onOpenChange?.(open) + setRenameDialogOpen(open) + }}> + + +
+ +
+ + + + + + + + +
+
+
+} \ No newline at end of file diff --git a/src/components/d-flow/folder/index.ts b/src/components/d-flow-folder/index.ts similarity index 100% rename from src/components/d-flow/folder/index.ts rename to src/components/d-flow-folder/index.ts diff --git a/src/components/d-flow/function/DFlowFunction.input.hook.ts b/src/components/d-flow-function/DFlowFunction.input.hook.ts similarity index 85% rename from src/components/d-flow/function/DFlowFunction.input.hook.ts rename to src/components/d-flow-function/DFlowFunction.input.hook.ts index 86bc6d79d..cdd55fb1d 100644 --- a/src/components/d-flow/function/DFlowFunction.input.hook.ts +++ b/src/components/d-flow-function/DFlowFunction.input.hook.ts @@ -1,6 +1,6 @@ import {FunctionDefinitionView} from "./DFlowFunction.view"; -import {DFlowDataTypeReactiveService} from "../data-type/DFlowDataType.service"; -import {replaceGenericKeysInType, resolveGenericKeys} from "../../../utils/generics"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import {replaceGenericKeysInType, resolveGenericKeys} from "../../utils/generics"; import type {DataTypeIdentifier, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; export const useInputType = ( diff --git a/src/components/d-flow/function/DFlowFunction.return.hook.ts b/src/components/d-flow-function/DFlowFunction.return.hook.ts similarity index 65% rename from src/components/d-flow/function/DFlowFunction.return.hook.ts rename to src/components/d-flow-function/DFlowFunction.return.hook.ts index 518e5e71b..f8a879854 100644 --- a/src/components/d-flow/function/DFlowFunction.return.hook.ts +++ b/src/components/d-flow-function/DFlowFunction.return.hook.ts @@ -1,17 +1,14 @@ import {FunctionDefinitionView} from "./DFlowFunction.view"; -import {DFlowDataTypeReactiveService} from "../data-type/DFlowDataType.service"; -import {replaceGenericKeysInType, resolveGenericKeys} from "../../../utils/generics"; -import {useService} from "../../../utils/contextStore"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import {replaceGenericKeysInType, resolveGenericKeys} from "../../utils/generics"; import type {DataTypeIdentifier, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; export const useReturnType = ( func: FunctionDefinitionView, values: NodeParameterValue[], - dataTypeService?: DFlowDataTypeReactiveService + dataTypeService: DFlowDataTypeReactiveService ): DataTypeIdentifier | null => { - dataTypeService = dataTypeService ?? useService(DFlowDataTypeReactiveService) - if (!func.returnType) return null const genericTypeMap = resolveGenericKeys(func, values, dataTypeService) diff --git a/src/components/d-flow/function/DFlowFunction.service.ts b/src/components/d-flow-function/DFlowFunction.service.ts similarity index 85% rename from src/components/d-flow/function/DFlowFunction.service.ts rename to src/components/d-flow-function/DFlowFunction.service.ts index ac58c9356..fa68fc8f3 100644 --- a/src/components/d-flow/function/DFlowFunction.service.ts +++ b/src/components/d-flow-function/DFlowFunction.service.ts @@ -1,4 +1,4 @@ -import {ReactiveArrayService} from "../../../utils/reactiveArrayService"; +import {ReactiveArrayService} from "../../utils"; import {FunctionDefinitionView} from "./DFlowFunction.view"; import type {FunctionDefinition} from "@code0-tech/sagittarius-graphql-types"; diff --git a/src/components/d-flow/function/DFlowFunction.view.ts b/src/components/d-flow-function/DFlowFunction.view.ts similarity index 85% rename from src/components/d-flow/function/DFlowFunction.view.ts rename to src/components/d-flow-function/DFlowFunction.view.ts index 857e1ad63..c8bcc0910 100644 --- a/src/components/d-flow/function/DFlowFunction.view.ts +++ b/src/components/d-flow-function/DFlowFunction.view.ts @@ -9,18 +9,24 @@ import type { export class FunctionDefinitionView { + /** Name of the function */ + private readonly _aliases?: Maybe; /** Time when this FunctionDefinition was created */ private readonly _createdAt?: Maybe; /** Deprecation message of the function */ private readonly _deprecationMessages?: Maybe; /** Description of the function */ private readonly _descriptions?: Maybe; + /** Display message of the function */ + private readonly _displayMessages?: Maybe; /** Documentation of the function */ private readonly _documentations?: Maybe; /** Generic keys of the function */ private readonly _genericKeys?: Maybe>; /** Global ID of this FunctionDefinition */ private readonly _id?: Maybe; + /** Identifier of the function */ + private readonly _identifier?: Maybe; /** Name of the function */ private readonly _names?: Maybe; /** Parameters of the function */ @@ -35,12 +41,15 @@ export class FunctionDefinitionView { private readonly _updatedAt?: Maybe; constructor(object: FunctionDefinition) { + this._aliases = object.aliases; this._createdAt = object.createdAt; this._deprecationMessages = object.deprecationMessages; this._descriptions = object.descriptions; + this._displayMessages = object.displayMessages; this._documentations = object.documentations; this._genericKeys = object.genericKeys; this._id = object.id; + this._identifier = object.identifier; this._names = object.names; this._parameterDefinitions = object.parameterDefinitions?.nodes?.map(definition => new ParameterDefinitionView(definition!!)) ?? undefined; this._returnType = object.returnType; @@ -50,6 +59,10 @@ export class FunctionDefinitionView { } + get aliases(): Maybe | undefined { + return this._aliases; + } + get createdAt(): Maybe | undefined { return this._createdAt; } @@ -62,6 +75,10 @@ export class FunctionDefinitionView { return this._descriptions; } + get displayMessages(): Maybe | undefined { + return this._displayMessages; + } + get documentations(): Maybe | undefined { return this._documentations; } @@ -74,6 +91,10 @@ export class FunctionDefinitionView { return this._id; } + get identifier(): Maybe | undefined { + return this._identifier; + } + get names(): Maybe | undefined { return this._names; } @@ -100,12 +121,15 @@ export class FunctionDefinitionView { json(): FunctionDefinition { return { + aliases: this._aliases, createdAt: this._createdAt, deprecationMessages: this._deprecationMessages, + displayMessages: this._displayMessages, descriptions: this._descriptions, documentations: this._documentations, genericKeys: this._genericKeys, id: this._id, + identifier: this._identifier, names: this._names, parameterDefinitions: this._parameterDefinitions ? { nodes: this._parameterDefinitions.map(definitionView => definitionView.json()!!) @@ -130,6 +154,8 @@ export class ParameterDefinitionView { private readonly _documentations?: Maybe; /** Global ID of this ParameterDefinition */ private readonly _id?: Maybe; + /** Identifier of the parameter */ + private readonly _identifier?: Maybe; /** Name of the parameter */ private readonly _names?: Maybe; /** Time when this ParameterDefinition was last updated */ @@ -141,6 +167,7 @@ export class ParameterDefinitionView { this._descriptions = object.descriptions; this._documentations = object.documentations; this._id = object.id; + this._identifier = object.identifier; this._names = object.names; this._updatedAt = object.updatedAt; } @@ -166,6 +193,10 @@ export class ParameterDefinitionView { return this._id; } + get identifier(): Maybe | undefined { + return this._identifier; + } + get names(): Maybe | undefined { return this._names; } diff --git a/src/components/d-flow/function/DFlowFunctionDefaultCard.style.scss b/src/components/d-flow-function/DFlowFunctionDefaultCard.style.scss similarity index 70% rename from src/components/d-flow/function/DFlowFunctionDefaultCard.style.scss rename to src/components/d-flow-function/DFlowFunctionDefaultCard.style.scss index 41ab7f275..423a399c9 100644 --- a/src/components/d-flow/function/DFlowFunctionDefaultCard.style.scss +++ b/src/components/d-flow-function/DFlowFunctionDefaultCard.style.scss @@ -1,6 +1,6 @@ -@use "../../../styles/variables"; -@use "../../../styles/helpers"; -@use "../../../styles/box"; +@use "../../styles/variables"; +@use "../../styles/helpers"; +@use "../../styles/box"; .d-flow-viewport-default-card { &__handle { @@ -23,6 +23,6 @@ } &--active { - box-shadow: 0 0 0 2px helpers.borderColor(variables.$info); + box-shadow: 0 0 0 1px rgba(variables.$info, .5); } } \ No newline at end of file diff --git a/src/components/d-flow-function/DFlowFunctionDefaultCard.tsx b/src/components/d-flow-function/DFlowFunctionDefaultCard.tsx new file mode 100644 index 000000000..8a0aac9f2 --- /dev/null +++ b/src/components/d-flow-function/DFlowFunctionDefaultCard.tsx @@ -0,0 +1,189 @@ +import {Code0Component, InspectionSeverity} from "../../utils"; +import {Handle, Node, NodeProps, Position, useReactFlow, useStore} from "@xyflow/react"; +import React, {memo} from "react"; +import {Card} from "../card/Card"; +import "./DFlowFunctionDefaultCard.style.scss"; +import {Flex} from "../flex/Flex"; +import {IconFile} from "@tabler/icons-react"; +import {Text} from "../text/Text"; +import {useService, useStore as usePictorStore} from "../../utils/contextStore"; +import {DFlowFunctionReactiveService} from "./DFlowFunction.service"; +import {useNodeValidation} from "../d-flow-validation/DNodeValidation.hook"; +import {DFlowReactiveService} from "../d-flow"; +import {FileTabsService} from "../file-tabs/FileTabs.service"; +import {DFlowTabDefault} from "../d-flow-file/DFlowTabDefault"; +import type {NodeFunction, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import {Badge} from "../badge/Badge"; +import {md5} from "js-md5"; + +export interface DFlowFunctionDefaultCardDataProps extends Omit, "scope"> { + nodeId: NodeFunction['id'] + flowId: Scalars["FlowID"]["output"] + isParameter: boolean + linkingId?: string + depth: number + scope: number[] + index: number +} + +// @ts-ignore +export type DFlowFunctionDefaultCardProps = NodeProps> + +export const DFlowFunctionDefaultCard: React.FC = memo((props) => { + const {data, id, width = 0, height = 0} = props + + const viewportWidth = useStore(s => s.width); + const viewportHeight = useStore(s => s.height); + const flowInstance = useReactFlow() + const fileTabsService = useService(FileTabsService) + const fileTabsStore = usePictorStore(FileTabsService) + const flowService = useService(DFlowReactiveService) + const flowStore = usePictorStore(DFlowReactiveService) + const functionService = useService(DFlowFunctionReactiveService) + const functionStore = usePictorStore(DFlowFunctionReactiveService) + + const node = React.useMemo(() => flowService.getNodeById(data.flowId, data.nodeId), [flowStore, data]) + const definition = React.useMemo(() => node ? functionService.getById(node.functionDefinition?.id!!) : undefined, [functionStore, data, node]) + const validation = useNodeValidation(data.nodeId, data.flowId) + const activeTabId = React.useMemo(() => { + return fileTabsStore.find((t: any) => (t as any).active)?.id + }, [fileTabsStore, fileTabsService]); + + 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; + }) + + const splitTemplate = (str: string) => + str + .split(/(\$\{[^}]+\})/) + .filter(Boolean) + .flatMap(part => + part.startsWith("${") + ? [part.slice(2, -1)] // variable name ohne ${} + : part.split(/(\s*,\s*)/) // Kommas einzeln extrahieren + .filter(Boolean) + .flatMap(p => p.trim() === "," ? [","] : p.trim() ? [p.trim()] : []) + ); + + const colorHash = md5(id) + const hashToHue = (md5: string): number => { + // nimm z.B. 8 Hex-Zeichen = 32 Bit + const int = parseInt(md5.slice(0, 8), 16) + return int % 360 + } + + const displayMessage = React.useMemo(() => splitTemplate(definition?.displayMessages?.nodes!![0]?.content ?? "").map(item => { + const param = node?.parameters?.nodes?.find(p => { + const parameterDefinition = definition?.parameterDefinitions?.find(pd => pd.id == p?.id) + return parameterDefinition?.identifier == item + }) + + if (param) { + switch (param?.value?.__typename) { + case "LiteralValue": + return + + {String(param?.value?.value)} + + + case "ReferenceValue": + return + + {String(param?.value.node)}-{String(param?.value.depth)}-{String(param?.value.scope)} + + + case "NodeFunctionIdWrapper": + const hash = md5(`${id}-param-${JSON.stringify(param)}`) + const node = flowService.getNodeById(props.data.flowId, param.value.id) + return + + {String(functionService.getById(node?.functionDefinition?.id)?.names?.nodes!![0]?.content)} + + + + } + return + + {item} + + + } + return " " + String(item) + " " + }), [flowStore, functionStore, data, definition]) + + React.useEffect(() => { + if (!node?.id) return + fileTabsService.registerTab({ + id: node.id, + active: false, + closeable: true, + children: <> + + {definition?.names?.nodes!![0]?.content} + , + content: + }) + }, [node?.id, definition, data]) + + return ( + v.type === InspectionSeverity.ERROR)?.length ?? 0) > 0 ? "error" : "primary"} + onClick={() => { + flowInstance.setViewport({ + x: (viewportWidth / 2) + (props.positionAbsoluteX * -1) - (width / 2), + y: (viewportHeight / 2) + (props.positionAbsoluteY * -1) - (height / 2), + zoom: 1 + }, { + duration: 250, + }) + fileTabsService.activateTab(node?.id!!) + }} style={{position: "relative"}}> + + + + {/* Ausgang */} + + + + {displayMessage} + + + ); +}) \ No newline at end of file diff --git a/src/components/d-flow-function/DFlowFunctionGroupCard.tsx b/src/components/d-flow-function/DFlowFunctionGroupCard.tsx new file mode 100644 index 000000000..4e533b9e6 --- /dev/null +++ b/src/components/d-flow-function/DFlowFunctionGroupCard.tsx @@ -0,0 +1,93 @@ +import React, {memo} from "react"; +import {Handle, Node, NodeProps, Position} from "@xyflow/react"; +import {Card} from "../card/Card"; +import {Code0Component} from "../../utils"; + +export interface DFlowFunctionGroupCardDataProps extends Omit, "scope"> { + isParameter: boolean + linkingId: string + flowId: string + depth: number + scope: number[] + color?: string +} + +// @ts-ignore +export type DFlowFunctionGroupCardProps = NodeProps> + +export const DFlowFunctionGroupCard: React.FC = memo((props) => { + const {data, id} = props + + return ( + + + + + ); +}); + +/* =========================== + Color utilities + =========================== */ + +const clamp01 = (v: number) => Math.min(Math.max(v, 0), 1) + +const parseCssColorToRgba = (color: string): any => { + if (typeof document === "undefined") { + return {r: 0, g: 0, b: 0, a: 1} + } + + const el = document.createElement("span") + el.style.color = color + document.body.appendChild(el) + + const computed = getComputedStyle(el).color + document.body.removeChild(el) + + const match = computed.match( + /rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/ + ) + + if (!match) { + return {r: 0, g: 0, b: 0, a: 1} + } + + return { + r: Math.round(Number(match[1])), + g: Math.round(Number(match[2])), + b: Math.round(Number(match[3])), + a: match[4] !== undefined ? Number(match[4]) : 1, + } +} + +const mixColorRgb = (color: string, level: number) => { + const w = clamp01(level * 0.1) + + const c1 = parseCssColorToRgba(color) + const c2 = parseCssColorToRgba("#030014") + + const mix = (a: number, b: number) => + Math.round(a * (1 - w) + b * w) + + return `rgb(${mix(c1.r, c2.r)}, ${mix(c1.g, c2.g)}, ${mix(c1.b, c2.b)})` +} + +const withAlpha = (color: string, alpha: number) => { + const c = parseCssColorToRgba(color) + return `rgba(${c.r}, ${c.g}, ${c.b}, ${clamp01(alpha)})` +} diff --git a/src/components/d-flow/function/DFlowFunctionSuggestionCard.tsx b/src/components/d-flow-function/DFlowFunctionSuggestionCard.tsx similarity index 60% rename from src/components/d-flow/function/DFlowFunctionSuggestionCard.tsx rename to src/components/d-flow-function/DFlowFunctionSuggestionCard.tsx index 5ad49e31f..aa7b05d9d 100644 --- a/src/components/d-flow/function/DFlowFunctionSuggestionCard.tsx +++ b/src/components/d-flow-function/DFlowFunctionSuggestionCard.tsx @@ -1,18 +1,16 @@ -import {Code0Component} from "../../../utils/types"; +import {Code0Component, useService} from "../../utils"; import {Handle, Node, NodeProps, Position} from "@xyflow/react"; import React, {memo} from "react"; -import {Button} from "../../button/Button"; +import {Button} from "../button/Button"; import {IconPlus} from "@tabler/icons-react"; -import {useSuggestions} from "../suggestion/DFlowSuggestion.hook"; -import {NodeFunctionView} from "../DFlow.view"; -import {useService} from "../../../utils/contextStore"; -import {DFlowReactiveService} from "../DFlow.service"; -import {DFlowSuggestionMenu} from "../suggestion/DFlowSuggestionMenu"; +import {useSuggestions} from "../d-flow-suggestion/DFlowSuggestion.hook"; +import {DFlowReactiveService} from "../d-flow"; +import {DFlowSuggestionMenu} from "../d-flow-suggestion/DFlowSuggestionMenu"; import type {Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; export interface DFlowFunctionSuggestionCardDataProps extends Code0Component { flowId: Flow['id'] - parentFunction?: NodeFunctionView + parentFunction?: NodeFunction } // @ts-ignore @@ -20,19 +18,17 @@ export type DFlowFunctionSuggestionCardProps = NodeProps = memo((props) => { + const [, startTransition] = React.useTransition() const result = useSuggestions(undefined, [], props.data.flowId, 0, [0], 0) const flowService = useService(DFlowReactiveService) const flow = flowService.getById(props.data.flowId) return { - const nodeFunction = new NodeFunctionView(suggestion.value as NodeFunction) - if (props.data.parentFunction) { - props.data.parentFunction.nextNodeId = nodeFunction.id!! - } else if (flow) { - flow.startingNodeId = nodeFunction.id!! - } - flow?.addNode(nodeFunction) - flowService.update() + startTransition(async () => { + if (suggestion.value.__typename === "NodeFunction") { + await flowService.addNextNodeById(flow?.id, props.data.parentFunction?.id ?? null, suggestion.value) + } + }) }} suggestions={result} triggerContent={ }/> diff --git a/src/components/d-flow/function/DFlowFunctionTriggerCard.tsx b/src/components/d-flow-function/DFlowFunctionTriggerCard.tsx similarity index 60% rename from src/components/d-flow/function/DFlowFunctionTriggerCard.tsx rename to src/components/d-flow-function/DFlowFunctionTriggerCard.tsx index 5ba496987..2516da84b 100644 --- a/src/components/d-flow/function/DFlowFunctionTriggerCard.tsx +++ b/src/components/d-flow-function/DFlowFunctionTriggerCard.tsx @@ -1,20 +1,20 @@ import React, {memo} from "react"; -import {Code0Component} from "../../../utils/types"; -import {FlowView} from "../DFlow.view"; +import {Code0Component} from "../../utils"; 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 {Flex} from "../../flex/Flex"; -import {IconBolt, IconChevronDown} from "@tabler/icons-react"; -import {Button} from "../../button/Button"; -import {DFlowTabTrigger} from "../tab/DFlowTabTrigger"; -import {DFlowTypeReactiveService} from "../type/DFlowType.service"; -import {Badge} from "../../badge/Badge"; +import {Text} from "../text/Text"; +import {useService} from "../../utils"; +import {FileTabsService} from "../file-tabs/FileTabs.service"; +import {Card} from "../card/Card"; +import {Flex} from "../flex/Flex"; +import {IconBolt, IconChevronDown, IconFile} from "@tabler/icons-react"; +import {Button} from "../button/Button"; +import {DFlowTabTrigger} from "../d-flow-file/DFlowTabTrigger"; +import {DFlowTypeReactiveService} from "../d-flow-type"; +import {Badge} from "../badge/Badge"; +import {Flow} from "@code0-tech/sagittarius-graphql-types"; export interface DFlowFunctionTriggerCardDataProps extends Omit, "scope"> { - instance: FlowView + instance: Flow } // @ts-ignore @@ -32,12 +32,26 @@ export const DFlowFunctionTriggerCard: React.FC = const viewportWidth = useStore(s => s.width) const viewportHeight = useStore(s => s.height) - return - START + React.useEffect(() => { + fileTabsService.registerTab({ + id: definition?.id!!, + active: true, + closeable: true, + children: <> + + {definition?.names?.nodes!![0]?.content} + , + content: , + show: true + }) + }, [definition?.id, data.instance, fileTabsService, definition?.names?.nodes]) + + return + Starting node { flowInstance.setViewport({ x: (viewportWidth / 2) + (props.positionAbsoluteX * -1) - (width / 2), @@ -46,13 +60,7 @@ export const DFlowFunctionTriggerCard: React.FC = }, { duration: 250, }) - fileTabsService.add({ - id: id, - active: true, - closeable: true, - children: {definition?.names?.nodes!![0]?.content}, - content: - }) + fileTabsService.activateTab(definition?.id!!) }}> diff --git a/src/components/d-flow/function/index.ts b/src/components/d-flow-function/index.ts similarity index 100% rename from src/components/d-flow/function/index.ts rename to src/components/d-flow-function/index.ts diff --git a/src/components/d-flow/input/DFlowInputDataType.style.scss b/src/components/d-flow-input/DFlowInputDataType.style.scss similarity index 81% rename from src/components/d-flow/input/DFlowInputDataType.style.scss rename to src/components/d-flow-input/DFlowInputDataType.style.scss index b730e58ac..327fc46b6 100644 --- a/src/components/d-flow/input/DFlowInputDataType.style.scss +++ b/src/components/d-flow-input/DFlowInputDataType.style.scss @@ -1,6 +1,6 @@ -@use "../../../styles/helpers"; -@use "../../../styles/variables"; -@use "../../../styles/box"; +@use "../../styles/helpers"; +@use "../../styles/variables"; +@use "../../styles/box"; .d-flow-viewport-data-type-input { padding: variables.$md; diff --git a/src/components/d-flow/input/DFlowInputDataType.tsx b/src/components/d-flow-input/DFlowInputDataType.tsx similarity index 97% rename from src/components/d-flow/input/DFlowInputDataType.tsx rename to src/components/d-flow-input/DFlowInputDataType.tsx index 194fb27cf..6d9a74616 100644 --- a/src/components/d-flow/input/DFlowInputDataType.tsx +++ b/src/components/d-flow-input/DFlowInputDataType.tsx @@ -1,5 +1,5 @@ import React from "react"; -import {ValidationProps} from "../../form/useForm"; +import {ValidationProps} from "../form"; import type { DataType, DataTypeIdentifier, @@ -13,25 +13,25 @@ import type { GenericMapper, GenericType } from "@code0-tech/sagittarius-graphql-types"; -import {InputMessage} from "../../form/InputMessage"; +import {InputMessage} from "../form"; import "./DFlowInputDataType.style.scss"; -import {TextInput} from "../../form/TextInput"; -import {Button} from "../../button/Button"; +import {TextInput} from "../form"; +import {Button} from "../button/Button"; import {IconSettings, IconTrash} from "@tabler/icons-react"; -import {Text} from "../../text/Text"; -import {Flex} from "../../flex/Flex"; -import {Badge} from "../../badge/Badge"; -import {InputLabel} from "../../form/InputLabel"; -import {useSuggestions} from "../suggestion/DFlowSuggestion.hook"; -import {DFlowSuggestionMenuFooter} from "../suggestion/DFlowSuggestionMenuFooter"; -import {toInputSuggestions} from "../suggestion/DFlowSuggestionMenu.util"; -import {DFlowSuggestionType} from "../suggestion/DFlowSuggestion.view"; -import {Menu, MenuPortal, MenuTrigger} from "../../menu/Menu"; +import {Text} from "../text/Text"; +import {Flex} from "../flex/Flex"; +import {Badge} from "../badge/Badge"; +import {InputLabel} from "../form"; +import {useSuggestions} from "../d-flow-suggestion/DFlowSuggestion.hook"; +import {DFlowSuggestionMenuFooter} from "../d-flow-suggestion/DFlowSuggestionMenuFooter"; +import {toInputSuggestions} from "../d-flow-suggestion/DFlowSuggestionMenu.util"; +import {DFlowSuggestionType} from "../d-flow-suggestion"; +import {Menu, MenuPortal, MenuTrigger} from "../menu/Menu"; import { InputSuggestion, InputSuggestionMenuContent, InputSuggestionMenuContentItems -} from "../../form/InputSuggestion"; +} from "../form"; const NON_TYPE_RULE_VARIANTS = new Set([ "ITEM_OF_COLLECTION" as DataTypeRulesVariant.ItemOfCollection, diff --git a/src/components/d-flow/export/DFlowExport.tsx b/src/components/d-flow-panel/DFlowExport.tsx similarity index 80% rename from src/components/d-flow/export/DFlowExport.tsx rename to src/components/d-flow-panel/DFlowExport.tsx index 3253216be..308155ec9 100644 --- a/src/components/d-flow/export/DFlowExport.tsx +++ b/src/components/d-flow-panel/DFlowExport.tsx @@ -3,9 +3,9 @@ import React from "react" import {Panel} from "@xyflow/react" import {Flow} from "@code0-tech/sagittarius-graphql-types" -import {useService, useStore} from "../../../utils" -import {DFlowReactiveService} from "../DFlow.service" -import {Button} from "../../button/Button" +import {useService, useStore} from "../../utils" +import {DFlowReactiveService} from "../d-flow" +import {Button} from "../button/Button" export interface DFlowExportProps { flowId: Flow['id'] @@ -23,11 +23,10 @@ export const DFlowExport: React.FC = (props) => { if (!flow) return // Hole JSON-Daten - const data = flow.jsonInput?.() + const data = flow if (!data) return - const json = - typeof data === "string" ? data : JSON.stringify(data, null, 2) + const json = JSON.stringify(data, null, 2) // Blob + Download-Link const blob = new Blob([json], { type: "application/json" }) diff --git a/src/components/d-flow/minimap/DFlowMiniMap.style.scss b/src/components/d-flow-panel/DFlowMiniMap.style.scss similarity index 70% rename from src/components/d-flow/minimap/DFlowMiniMap.style.scss rename to src/components/d-flow-panel/DFlowMiniMap.style.scss index 329c940e4..e2dfa31b3 100644 --- a/src/components/d-flow/minimap/DFlowMiniMap.style.scss +++ b/src/components/d-flow-panel/DFlowMiniMap.style.scss @@ -1,6 +1,6 @@ -@use "../../../styles/helpers"; -@use "../../../styles/box"; -@use "../../../styles/variables"; +@use "../../styles/helpers"; +@use "../../styles/box"; +@use "../../styles/variables"; .d-flow-viewport-mini-map { @@ -8,7 +8,7 @@ @include box.box(variables.$primary); @include helpers.borderRadius(); margin: 0; - width: 100%; + width: fit-content; } > svg { diff --git a/src/components/d-flow-panel/DFlowMiniMap.tsx b/src/components/d-flow-panel/DFlowMiniMap.tsx new file mode 100644 index 000000000..887e0d31c --- /dev/null +++ b/src/components/d-flow-panel/DFlowMiniMap.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import {MiniMap, Panel, useNodes} from "@xyflow/react"; +import {FLOW_EDGE_RAINBOW} from "../d-flow"; +import "./DFlowMiniMap.style.scss" + +export const DFlowMiniMap: 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-panel/DFlowPanelControl.tsx b/src/components/d-flow-panel/DFlowPanelControl.tsx new file mode 100644 index 000000000..34a38a298 --- /dev/null +++ b/src/components/d-flow-panel/DFlowPanelControl.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import {ButtonGroup} from "../button-group/ButtonGroup"; +import {Button} from "../button/Button"; +import {IconPlus, IconTrash} from "@tabler/icons-react"; +import {Panel} from "@xyflow/react"; +import {useService, useStore} from "../../utils"; +import {FileTabsService} from "../file-tabs/FileTabs.service"; +import {DFlowReactiveService} from "../d-flow"; +import {Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; +import {Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; +import {Text} from "../text/Text"; +import {Badge} from "../badge/Badge"; +import {DFlowSuggestionMenu} from "../d-flow-suggestion/DFlowSuggestionMenu"; +import {useSuggestions} from "../d-flow-suggestion/DFlowSuggestion.hook"; + +export interface DFlowPanelControlProps { + flowId: Flow['id'] +} + +export const DFlowPanelControl: React.FC = (props) => { + + //props + const {flowId} = props + + //services and stores + const fileTabsService = useService(FileTabsService) + const fileTabsStore = useStore(FileTabsService) + const flowService = useService(DFlowReactiveService) + const [, startTransition] = React.useTransition() + + //memoized values + const activeTab = React.useMemo(() => { + return fileTabsStore.find((t: any) => (t as any).active) + }, [fileTabsStore, fileTabsService]) + const result = useSuggestions(undefined, [], flowId, 0, [0], 0) + + //callbacks + const deleteActiveNode = React.useCallback(() => { + if (!activeTab) return + // @ts-ignore + startTransition(async () => { + await flowService.deleteNodeById((activeTab.content.props.flowId as Flow['id']), (activeTab.content.props.node.id as NodeFunction['id'])) + }) + fileTabsService.deleteById(activeTab.id) + }, [activeTab, flowService]) + + const addNodeToFlow = React.useCallback((suggestion: any) => { + if (flowId && suggestion.value.__typename === "NodeFunction" && "node" in activeTab.content.props) { + startTransition(async () => { + await flowService.addNextNodeById(flowId, (activeTab.content.props.node.id as NodeFunction['id']) ?? undefined, suggestion.value) + }) + } else { + startTransition(async () => { + await flowService.addNextNodeById(flowId, null, suggestion.value) + }) + } + }, [flowId, flowService, activeTab]) + + //TODO: Add execute flow button functionality + //TODO: disable button if active tab is the trigger node + return + + + + + + + + + To execute this flow you can call the following endpoint {" "}
+ + POST + + + localhost:6212/72hsa13/users/get + +
+ +
+
+
+ + + + + + + Select a node to delete it + + + + + + + Next node + + }/> +
+
+ +} \ No newline at end of file diff --git a/src/components/d-flow-panel/DFlowPanelLayout.tsx b/src/components/d-flow-panel/DFlowPanelLayout.tsx new file mode 100644 index 000000000..92086b350 --- /dev/null +++ b/src/components/d-flow-panel/DFlowPanelLayout.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import {SegmentedControl, SegmentedControlItem} from "../segmented-control/SegmentedControl"; +import {IconLayout, IconLayoutDistributeHorizontal, IconLayoutDistributeVertical} from "@tabler/icons-react"; +import {Panel} from "@xyflow/react"; +import {Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; +import {Text} from "../text/Text"; + +export interface DFlowPanelLayoutProps { + +} + +export const DFlowPanelLayout: React.FC = (props) => { + + const {} = props + + return + + + + + + + + + + + Vertical layout + + + + + + + + + + + + + Horizontal layout + + + + + + + + + + + + + Manual layout + + + + + +} \ No newline at end of file diff --git a/src/components/d-flow/control/DFlowControl.tsx b/src/components/d-flow-panel/DFlowPanelSize.tsx similarity index 51% rename from src/components/d-flow/control/DFlowControl.tsx rename to src/components/d-flow-panel/DFlowPanelSize.tsx index 974c04914..1242ef423 100644 --- a/src/components/d-flow/control/DFlowControl.tsx +++ b/src/components/d-flow-panel/DFlowPanelSize.tsx @@ -1,12 +1,13 @@ import React from "react"; import {Panel, useReactFlow, useViewport} from "@xyflow/react"; -import {ButtonGroup} from "../../button-group/ButtonGroup"; -import {Button} from "../../button/Button"; +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 {Badge} from "../badge/Badge"; +import {Flex} from "../flex/Flex"; +import {Text} from "../text/Text"; -export const DFlowControl: React.FC = () => { +export const DFlowPanelSize: React.FC = () => { const viewport = useViewport(); const reactFlow = useReactFlow(); @@ -31,14 +32,13 @@ export const DFlowControl: React.FC = () => { - - - + + + - {getCurrentZoomInPercent()}% + + {getCurrentZoomInPercent()}% + diff --git a/src/components/d-flow-panel/index.ts b/src/components/d-flow-panel/index.ts new file mode 100644 index 000000000..751fb9c31 --- /dev/null +++ b/src/components/d-flow-panel/index.ts @@ -0,0 +1,5 @@ +export * from "./DFlowPanelSize" +export * from "./DFlowPanelControl" +export * from "./DFlowExport" +export * from "./DFlowMiniMap" +export * from "./DFlowPanelLayout" \ No newline at end of file diff --git a/src/components/d-flow/suggestion/DFlowSuggestion.hook.tsx b/src/components/d-flow-suggestion/DFlowSuggestion.hook.tsx similarity index 51% rename from src/components/d-flow/suggestion/DFlowSuggestion.hook.tsx rename to src/components/d-flow-suggestion/DFlowSuggestion.hook.tsx index 8af6c93e1..35a5bbd39 100644 --- a/src/components/d-flow/suggestion/DFlowSuggestion.hook.tsx +++ b/src/components/d-flow-suggestion/DFlowSuggestion.hook.tsx @@ -1,22 +1,19 @@ -import {useService} from "../../../utils/contextStore"; -import {DFlowReactiveSuggestionService} from "./DFlowSuggestion.service"; -import {DFlowDataTypeReactiveService} from "../data-type/DFlowDataType.service"; -import {md5} from 'js-md5'; +import {useService} from "../../utils"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; import {DFlowSuggestion, DFlowSuggestionType} from "./DFlowSuggestion.view"; -import {DFlowFunctionReactiveService} from "../function/DFlowFunction.service"; -import {isMatchingType, replaceGenericsAndSortType, resolveType} from "../../../utils/generics"; -import {NodeFunctionView} from "../DFlow.view"; -import {DFlowReactiveService} from "../DFlow.service"; -import {useReturnType} from "../function/DFlowFunction.return.hook"; -import {useInputType} from "../function/DFlowFunction.input.hook"; +import {DFlowFunctionReactiveService} from "../d-flow-function"; +import {isMatchingType, replaceGenericsAndSortType, resolveType} from "../../utils/generics"; +import {DFlowReactiveService} from "../d-flow"; +import {useReturnType} from "../d-flow-function/DFlowFunction.return.hook"; +import {useInputType} from "../d-flow-function/DFlowFunction.input.hook"; import type { DataTypeIdentifier, DataTypeRulesInputTypeConfig, DataTypeRulesItemOfCollectionConfig, DataTypeRulesNumberRangeConfig, - DataTypeRulesVariant, - DataTypeVariant, Flow, + Flow, LiteralValue, Maybe, + NodeFunction, NodeFunctionIdWrapper, NodeParameter, NodeParameterValue, ReferenceValue @@ -35,109 +32,116 @@ export const useSuggestions = ( suggestionTypes: DFlowSuggestionType[] = [DFlowSuggestionType.REF_OBJECT, DFlowSuggestionType.VALUE, DFlowSuggestionType.FUNCTION, DFlowSuggestionType.FUNCTION_COMBINATION, DFlowSuggestionType.DATA_TYPE] ): DFlowSuggestion[] => { - const suggestionService = useService(DFlowReactiveSuggestionService) const dataTypeService = useService(DFlowDataTypeReactiveService) - const flowService = useService(DFlowReactiveService) const functionService = useService(DFlowFunctionReactiveService) - const flow = flowService.getById(flowId) const dataType = type ? dataTypeService?.getDataType(type) : undefined - if (!suggestionService || !dataTypeService) return [] - - const hashedType = type ? useTypeHash(type) : undefined const resolvedType = type ? replaceGenericsAndSortType(resolveType(type, dataTypeService), genericKeys) : undefined const state: DFlowSuggestion[] = [] - const cached = suggestionService.getSuggestionsByHash(hashedType || "") - if (cached.length <= 0) { - if (hashedType && dataType && suggestionTypes.includes(DFlowSuggestionType.VALUE)) { - //calculate VALUE - dataType.rules?.nodes?.forEach(rule => { - if (rule?.variant === "ITEM_OF_COLLECTION") { - (rule.config as DataTypeRulesItemOfCollectionConfig)!!.items?.forEach(value => { - const suggestion = new DFlowSuggestion(hashedType, [], { + if (dataType && suggestionTypes.includes(DFlowSuggestionType.VALUE)) { + //calculate VALUE + dataType.rules?.nodes?.forEach(rule => { + if (rule?.variant === "ITEM_OF_COLLECTION") { + (rule.config as DataTypeRulesItemOfCollectionConfig)!!.items?.forEach(value => { + const suggestion: DFlowSuggestion = { + path: [], + type: DFlowSuggestionType.VALUE, + displayText: [value.toString()], + value: { __typename: "LiteralValue", value: value - }, DFlowSuggestionType.VALUE, [value.toString()]) - suggestionService.add(suggestion) - state.push(suggestion) - }) - } else if (rule?.variant === "NUMBER_RANGE") { - const config: DataTypeRulesNumberRangeConfig = rule.config as DataTypeRulesNumberRangeConfig - const suggestion = new DFlowSuggestion(hashedType, [], { + }, + } + state.push(suggestion) + }) + } else if (rule?.variant === "NUMBER_RANGE") { + const config: DataTypeRulesNumberRangeConfig = rule.config as DataTypeRulesNumberRangeConfig + const suggestion: DFlowSuggestion = { + path: [], + type: DFlowSuggestionType.VALUE, + displayText: [config.from?.toString() ?? ""], + value: { __typename: "LiteralValue", value: config.from - }, DFlowSuggestionType.VALUE, [config.from?.toString() ?? ""]) - suggestionService.add(suggestion) - state.push(suggestion) + }, } - }) - } - - //TODO: need to validate given type - if (hashedType && dataType && dataType.variant === "DATA_TYPE" && suggestionTypes.includes(DFlowSuggestionType.DATA_TYPE)) { - dataTypeService.values().forEach(dataType => { - //TODO: need to wait for sagittarius update to support DataTypes as values - // @ts-ignore - const suggestion = new DFlowSuggestion(hashedType, [], dataType.json, DFlowSuggestionType.DATA_TYPE, [dataType.name?.nodes!![0]?.content]) - suggestionService.add(suggestion) state.push(suggestion) - }) - } + } + }) + } - if (suggestionTypes.includes(DFlowSuggestionType.FUNCTION_COMBINATION)) { - //calculate FUNCTION - //generics to be replaced with GENERIC todo is written on top - const matchingFunctions = functionService.values().filter(funcDefinition => { - if (!type || !resolvedType || !hashedType) return true - if (funcDefinition.runtimeFunctionDefinition?.identifier == "RETURN" && type) return false - if (dataType?.variant === "NODE") return true - if (!funcDefinition.returnType) return false - if (!funcDefinition.genericKeys) return false - const resolvedReturnType = replaceGenericsAndSortType(resolveType(funcDefinition.returnType, dataTypeService), funcDefinition.genericKeys) - return isMatchingType(resolvedType, resolvedReturnType) - }).sort((a, b) => { - const [rA, pA, fA] = a.runtimeFunctionDefinition!!.identifier!!.split("::"); - const [rB, pB, fB] = b.runtimeFunctionDefinition!!.identifier!!.split("::"); - - // Erst runtime vergleichen - const runtimeCmp = rA.localeCompare(rB); - if (runtimeCmp !== 0) return runtimeCmp; - - // Dann package vergleichen - const packageCmp = pA.localeCompare(pB); - if (packageCmp !== 0) return packageCmp; - - // Dann function name - return fA.localeCompare(fB); - }) - - matchingFunctions.forEach(funcDefinition => { - const nodeFunctionSuggestion: NodeParameterValue = { - __typename: "NodeFunction", - //TODO: generate unique id - id: `gid://sagittarius/NodeFunction/${(flow?.nodes?.length ?? 0) + 1}`, - functionDefinition: { - id: funcDefinition.id, - runtimeFunctionDefinition: funcDefinition.runtimeFunctionDefinition - }, - parameters: { - nodes: (funcDefinition.parameterDefinitions?.map(definition => { - return { - id: definition.id, - runtimeParameter: { - id: definition.id - } + //TODO: need to validate given type + if (dataType && dataType.variant === "DATA_TYPE" && suggestionTypes.includes(DFlowSuggestionType.DATA_TYPE)) { + dataTypeService.values().forEach(dataType => { + //TODO: need to wait for sagittarius update to support DataTypes as values + const suggestion: DFlowSuggestion = { + path: [], + type: DFlowSuggestionType.DATA_TYPE, + displayText: [dataType.name?.nodes!![0]?.content!], + /*@ts-ignore*/ + value: dataType.json, + } + state.push(suggestion) + }) + } + + + if (suggestionTypes.includes(DFlowSuggestionType.FUNCTION_COMBINATION)) { + //calculate FUNCTION + //generics to be replaced with GENERIC todo is written on top + const matchingFunctions = functionService.values().filter(funcDefinition => { + if (!type || !resolvedType) return true + if (funcDefinition.runtimeFunctionDefinition?.identifier == "RETURN" && type) return false + if (dataType?.variant === "NODE") return true + if (!funcDefinition.returnType) return false + if (!funcDefinition.genericKeys) return false + const resolvedReturnType = replaceGenericsAndSortType(resolveType(funcDefinition.returnType, dataTypeService), funcDefinition.genericKeys) + return isMatchingType(resolvedType, resolvedReturnType) + }).sort((a, b) => { + const [rA, pA, fA] = a.runtimeFunctionDefinition!!.identifier!!.split("::"); + const [rB, pB, fB] = b.runtimeFunctionDefinition!!.identifier!!.split("::"); + + // Erst runtime vergleichen + const runtimeCmp = rA.localeCompare(rB); + if (runtimeCmp !== 0) return runtimeCmp; + + // Dann package vergleichen + const packageCmp = pA.localeCompare(pB); + if (packageCmp !== 0) return packageCmp; + + // Dann function name + return fA.localeCompare(fB); + }) + + matchingFunctions.forEach(funcDefinition => { + const nodeFunctionSuggestion: LiteralValue | ReferenceValue | NodeFunction = { + __typename: "NodeFunction", + id: `gid://sagittarius/NodeFunction/1`, + functionDefinition: { + id: funcDefinition.id, + runtimeFunctionDefinition: funcDefinition.runtimeFunctionDefinition + }, + parameters: { + nodes: (funcDefinition.parameterDefinitions?.map(definition => { + return { + id: definition.id, + runtimeParameter: { + id: definition.id } - }) ?? []) as Maybe>> - } + } + }) ?? []) as Maybe>> } - const suggestion = new DFlowSuggestion(hashedType || "", [], nodeFunctionSuggestion, DFlowSuggestionType.FUNCTION, [funcDefinition.names?.nodes!![0]?.content as string]) - state.push(suggestion) - }) - } - + } + const suggestion: DFlowSuggestion = { + path: [], + type: DFlowSuggestionType.FUNCTION, + displayText: [funcDefinition.names?.nodes!![0]?.content as string], + value: nodeFunctionSuggestion, + } + state.push(suggestion) + }) } if (suggestionTypes.includes(DFlowSuggestionType.REF_OBJECT)) { @@ -153,43 +157,18 @@ export const useSuggestions = ( const resolvedRefObjectType = replaceGenericsAndSortType(resolveType(value.dataTypeIdentifier!!, dataTypeService), []) if (!isMatchingType(resolvedType, resolvedRefObjectType)) return - const suggestion = new DFlowSuggestion(hashedType || "", [], value as ReferenceValue, DFlowSuggestionType.REF_OBJECT, [`${value.depth}-${value.scope}-${value.node || ''}`]) + const suggestion: DFlowSuggestion = { + path: [], + type: DFlowSuggestionType.REF_OBJECT, + displayText: [`${value.depth}-${value.scope}-${value.node || ''}`], + value: value as ReferenceValue, + } state.push(suggestion) }) } - return [...state, ...suggestionService.getSuggestionsByHash(hashedType || "")].sort() - -} - -/** - * React hook that produces a stable MD5 hash for a given Type, deeply resolving: - * - All type aliases (via DFlowDataTypeReactiveService) at any level of nesting - * - All occurrences of any provided generic_keys (can be any string) as "GENERIC" - * - All generic_mapper/type fields, rules/config.type fields, and parent fields, recursively - * - All object keys sorted for stable, order-independent hashing - * - * This ensures semantically equivalent types—regardless of alias, generic key naming, or structural nesting— - * always produce the same hash, making this suitable for caching, deduplication, and fast comparison. - * - * @param type The Type to hash (either a string or a GenericType object) - * @param generic_keys (optional) Array of string keys that should be normalized as generics (can be any string) - * @returns MD5 hash string if type/service available, otherwise undefined - */ -export const useTypeHash = (type: DataTypeIdentifier, generic_keys?: string[]): string | undefined => { - const dataTypeService = useService(DFlowDataTypeReactiveService) - if (!type || !dataTypeService) return undefined - - - // 1. Expand all aliases and deeply unify generics - const expandedType = resolveType(type, dataTypeService) - // 2. Replace generics and sort keys for canonicalization - const canonical = replaceGenericsAndSortType(expandedType, generic_keys) - // 3. Stable stringification for MD5 - const stableString = JSON.stringify(canonical) + return state.sort() - // 4. MD5 hash - return md5(stableString) } /** @@ -231,13 +210,13 @@ export const useRefObjects = (flowId: Flow['id']): Array => { * `scopePath` is the full scope path (e.g., [0], [0,2], [0,2,4], ...). */ const traverse = ( - fn: NodeFunctionView | undefined, + node: NodeFunctionIdWrapper | NodeFunction | undefined, depth: number, scopePath: number[] ) => { - if (!fn) return; + if (!node) return; - let current: NodeFunctionView | undefined = fn; + let current: NodeFunction | undefined = node.__typename === "NodeFunctionIdWrapper" ? flowService.getNodeById(flowId, node.id) : node as NodeFunction; while (current) { const def = functionService.getById(current.functionDefinition?.id!!); @@ -256,13 +235,13 @@ export const useRefObjects = (flowId: Flow['id']): Array => { pType.rules?.nodes?.filter((r) => r?.variant === "INPUT_TYPES") ?? []; if (inputTypeRules.length) { - const paramInstance = current.parameters.find((p) => p.id === pDef.id); + const paramInstance = current.parameters?.nodes?.find((p) => p?.id === pDef.id); const rawValue = paramInstance?.value; const valuesArray = rawValue !== undefined - ? rawValue instanceof NodeFunctionView - ? [rawValue.json()!!] - : [rawValue] + ? rawValue?.__typename === "NodeFunctionIdWrapper" + ? [rawValue!!] + : [rawValue!!] : []; for (const rule of inputTypeRules) { @@ -285,10 +264,10 @@ export const useRefObjects = (flowId: Flow['id']): Array => { // 2) Return type (main output of the current node) { const paramValues = - current.parameters?.map((p) => p.value).filter((v) => v !== undefined) ?? []; + current.parameters?.nodes?.map((p) => p?.value).filter((v) => v !== undefined) ?? []; const resolvedReturnType = useReturnType( def, - paramValues.map((v) => (v instanceof NodeFunctionView ? v.json()!! : v)), + paramValues as NodeParameterValue[], dataTypeService ); if (resolvedReturnType) { @@ -307,9 +286,9 @@ export const useRefObjects = (flowId: Flow['id']): Array => { for (const pDef of def.parameterDefinitions) { const pType = dataTypeService.getDataType(pDef.dataTypeIdentifier!!); if (pType?.variant === "NODE") { - const paramInstance = current.parameters.find((p) => p.id === pDef.id); - if (paramInstance?.value && paramInstance.value instanceof NodeFunctionView) { - const childFn = paramInstance.value as NodeFunctionView; + const paramInstance = current.parameters?.nodes?.find((p) => p?.id === pDef.id); + if (paramInstance?.value && paramInstance.value.__typename === "NodeFunctionIdWrapper") { + const childFn = paramInstance.value as NodeFunctionIdWrapper; // New group: extend the scope path with a fresh id; increase depth by 1. const childScopePath = [...scopePath, nextGroupId()]; @@ -317,21 +296,21 @@ export const useRefObjects = (flowId: Flow['id']): Array => { } } else { // Functions passed as NON-NODE parameters: same depth and same scope path. - const paramInstance = current.parameters.find((p) => p.id === pDef.id); - if (paramInstance?.value && paramInstance.value instanceof NodeFunctionView) { - traverse(paramInstance.value as NodeFunctionView, depth, scopePath); + const paramInstance = current.parameters?.nodes?.find((p) => p?.id === pDef.id); + if (paramInstance?.value && paramInstance.value.__typename === "NodeFunctionIdWrapper") { + traverse(paramInstance.value as NodeFunctionIdWrapper, depth, scopePath); } } } } // 4) Continue the linear chain in the same lane/scope. - current = flow.getNodeById(current.nextNodeId!!); + current = flowService.getNodeById(flow.id, current.nextNodeId) } }; // Root lane: depth 0, scope path [0]; node starts at 1 on the first visited node. - traverse(flow.getNodeById(flow.startingNodeId), 0, [0]); + traverse(flowService.getNodeById(flow.id, flow.startingNodeId), 0, [0]); return refObjects; }; \ No newline at end of file diff --git a/src/components/d-flow-suggestion/DFlowSuggestion.view.ts b/src/components/d-flow-suggestion/DFlowSuggestion.view.ts new file mode 100644 index 000000000..ea1230b21 --- /dev/null +++ b/src/components/d-flow-suggestion/DFlowSuggestion.view.ts @@ -0,0 +1,21 @@ +import type { + LiteralValue, + NodeFunction, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types"; + +export enum DFlowSuggestionType { + REF_OBJECT, + VALUE, + FUNCTION, + FUNCTION_COMBINATION, + DATA_TYPE, +} + +export interface DFlowSuggestion { + + displayText: string[] + path: number[] + value: LiteralValue | ReferenceValue | NodeFunction + type: DFlowSuggestionType +} \ No newline at end of file diff --git a/src/components/d-flow/suggestion/DFlowSuggestionMenu.tsx b/src/components/d-flow-suggestion/DFlowSuggestionMenu.tsx similarity index 92% rename from src/components/d-flow/suggestion/DFlowSuggestionMenu.tsx rename to src/components/d-flow-suggestion/DFlowSuggestionMenu.tsx index 39d2406b2..c8f98e862 100644 --- a/src/components/d-flow/suggestion/DFlowSuggestionMenu.tsx +++ b/src/components/d-flow-suggestion/DFlowSuggestionMenu.tsx @@ -1,4 +1,4 @@ -import {Menu, MenuPortal, MenuSeparator, MenuTrigger} from "../../menu/Menu"; +import {Menu, MenuPortal, MenuSeparator, MenuTrigger} from "../menu/Menu"; import React from "react"; import {DFlowSuggestion} from "./DFlowSuggestion.view"; import {DFlowSuggestionMenuFooter} from "./DFlowSuggestionMenuFooter"; @@ -6,7 +6,7 @@ import { InputSuggestionMenuContent, InputSuggestionMenuContentItems, InputSuggestionMenuContentItemsHandle -} from "../../form/InputSuggestion"; +} from "../form"; import {toInputSuggestions} from "./DFlowSuggestionMenu.util"; import {DFlowSuggestionMenuSearchBar} from "./DFlowSuggestionMenuSearchBar"; import {useStoreApi} from "@xyflow/react"; @@ -25,6 +25,10 @@ export const DFlowSuggestionMenu: React.FC = (props) = const menuRef = React.useRef(null); // Ref to suggestion list const [stateSuggestions, setStateSuggestions] = React.useState(suggestions) + React.useEffect(() => { + setStateSuggestions(suggestions) + }, [suggestions]) + return { setTimeout(() => { flowStoreApi.setState({ @@ -38,7 +42,7 @@ export const DFlowSuggestionMenu: React.FC = (props) = {triggerContent} - + { if (event.key === "ArrowDown") { diff --git a/src/components/d-flow/suggestion/DFlowSuggestionMenu.util.tsx b/src/components/d-flow-suggestion/DFlowSuggestionMenu.util.tsx similarity index 95% rename from src/components/d-flow/suggestion/DFlowSuggestionMenu.util.tsx rename to src/components/d-flow-suggestion/DFlowSuggestionMenu.util.tsx index 9c55856c4..59f9440ec 100644 --- a/src/components/d-flow/suggestion/DFlowSuggestionMenu.util.tsx +++ b/src/components/d-flow-suggestion/DFlowSuggestionMenu.util.tsx @@ -1,8 +1,8 @@ import {DFlowSuggestion, DFlowSuggestionType} from "./DFlowSuggestion.view"; -import {InputSuggestion} from "../../form/InputSuggestion"; +import {InputSuggestion} from "../form"; import React from "react"; import {IconCircleDot, IconCirclesRelation, IconFileFunctionFilled} from "@tabler/icons-react"; -import {Text} from "../../text/Text"; +import {Text} from "../text/Text"; export const toInputSuggestions = (suggestions: DFlowSuggestion[]): InputSuggestion[] => { diff --git a/src/components/d-flow/suggestion/DFlowSuggestionMenuFooter.tsx b/src/components/d-flow-suggestion/DFlowSuggestionMenuFooter.tsx similarity index 94% rename from src/components/d-flow/suggestion/DFlowSuggestionMenuFooter.tsx rename to src/components/d-flow-suggestion/DFlowSuggestionMenuFooter.tsx index e4b5ec60e..b4c9dfd2d 100644 --- a/src/components/d-flow/suggestion/DFlowSuggestionMenuFooter.tsx +++ b/src/components/d-flow-suggestion/DFlowSuggestionMenuFooter.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import {MenuLabel} from "../../menu/Menu"; +import {MenuLabel} from "../menu/Menu"; import { IconArrowsShuffle, IconBulb, @@ -10,9 +10,9 @@ import { IconCornerDownLeft, IconFileFunctionFilled } from "@tabler/icons-react"; -import {Text} from "../../text/Text"; -import {Flex} from "../../flex/Flex"; -import {Tooltip, TooltipContent, TooltipPortal, TooltipTrigger} from "../../tooltip/Tooltip"; +import {Text} from "../text/Text"; +import {Flex} from "../flex/Flex"; +import {Tooltip, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; export const DFlowSuggestionMenuFooter: React.FC = () => { return diff --git a/src/components/d-flow/suggestion/DFlowSuggestionMenuSearchBar.tsx b/src/components/d-flow-suggestion/DFlowSuggestionMenuSearchBar.tsx similarity index 93% rename from src/components/d-flow/suggestion/DFlowSuggestionMenuSearchBar.tsx rename to src/components/d-flow-suggestion/DFlowSuggestionMenuSearchBar.tsx index 03d03e396..21064a221 100644 --- a/src/components/d-flow/suggestion/DFlowSuggestionMenuSearchBar.tsx +++ b/src/components/d-flow-suggestion/DFlowSuggestionMenuSearchBar.tsx @@ -1,7 +1,7 @@ import React from "react"; import {DFlowSuggestionSearchInput} from "./DFlowSuggestionSearchInput"; import {IconSearch} from "@tabler/icons-react"; -import {Code0Component} from "../../../utils/types"; +import {Code0Component} from "../../utils"; export interface DFlowSuggestionMenuSearchBarProps extends Code0Component { onType: (event: React.KeyboardEvent) => void diff --git a/src/components/d-flow/suggestion/DFlowSuggestionSearchInput.style.scss b/src/components/d-flow-suggestion/DFlowSuggestionSearchInput.style.scss similarity index 59% rename from src/components/d-flow/suggestion/DFlowSuggestionSearchInput.style.scss rename to src/components/d-flow-suggestion/DFlowSuggestionSearchInput.style.scss index dff4426a3..0a0bf28db 100644 --- a/src/components/d-flow/suggestion/DFlowSuggestionSearchInput.style.scss +++ b/src/components/d-flow-suggestion/DFlowSuggestionSearchInput.style.scss @@ -1,11 +1,10 @@ -@use "../../../styles/variables"; -@use "../../../styles/helpers"; +@use "../../styles/variables"; +@use "../../styles/helpers"; .d-flow-suggestion-search-input { - box-shadow: none !important; + border: none !important; background: none !important; - margin-left: -1 * variables.$xxs; - margin-right: -1 * variables.$xxs; + margin: -1 * variables.$xxs; padding-left: variables.$xxs; padding-right: variables.$xxs; border-radius: 0 !important; diff --git a/src/components/d-flow/suggestion/DFlowSuggestionSearchInput.tsx b/src/components/d-flow-suggestion/DFlowSuggestionSearchInput.tsx similarity index 88% rename from src/components/d-flow/suggestion/DFlowSuggestionSearchInput.tsx rename to src/components/d-flow-suggestion/DFlowSuggestionSearchInput.tsx index 83804ce1d..efd633002 100644 --- a/src/components/d-flow/suggestion/DFlowSuggestionSearchInput.tsx +++ b/src/components/d-flow-suggestion/DFlowSuggestionSearchInput.tsx @@ -1,9 +1,9 @@ -import {InputProps, Input} from "../../form"; +import {InputProps, Input} from "../form"; import React, {RefObject} from "react"; -import {Button} from "../../button/Button"; +import {Button} from "../button/Button"; import {IconX} from "@tabler/icons-react"; import "./DFlowSuggestionSearchInput.style.scss" -import {setElementKey} from "../../form/Input.utils"; +import {setElementKey} from "../form/Input.utils"; interface DFlowSuggestionSearchInputProps extends Omit, "wrapperComponent" | "type"> { //defaults to false diff --git a/src/components/d-flow-suggestion/index.ts b/src/components/d-flow-suggestion/index.ts new file mode 100644 index 000000000..1cc4a00dd --- /dev/null +++ b/src/components/d-flow-suggestion/index.ts @@ -0,0 +1 @@ +export * from "./DFlowSuggestion.view" \ No newline at end of file diff --git a/src/components/d-flow/type/DFlowType.service.ts b/src/components/d-flow-type/DFlowType.service.ts similarity index 82% rename from src/components/d-flow/type/DFlowType.service.ts rename to src/components/d-flow-type/DFlowType.service.ts index 3ff84466a..9d49db86e 100644 --- a/src/components/d-flow/type/DFlowType.service.ts +++ b/src/components/d-flow-type/DFlowType.service.ts @@ -1,4 +1,4 @@ -import {ReactiveArrayService} from "../../../utils/reactiveArrayService"; +import {ReactiveArrayService} from "../../utils"; import {FlowTypeView} from "./DFlowType.view"; import type {FlowType} from "@code0-tech/sagittarius-graphql-types"; diff --git a/src/components/d-flow/type/DFlowType.view.ts b/src/components/d-flow-type/DFlowType.view.ts similarity index 86% rename from src/components/d-flow/type/DFlowType.view.ts rename to src/components/d-flow-type/DFlowType.view.ts index efa9c09dc..f5b98b757 100644 --- a/src/components/d-flow/type/DFlowType.view.ts +++ b/src/components/d-flow-type/DFlowType.view.ts @@ -10,10 +10,14 @@ import type { export class FlowTypeView { + /** Name of the function */ + private readonly _aliases?: Maybe; /** Time when this FlowType was created */ private readonly _createdAt?: Maybe; /** Descriptions of the flow type */ private readonly _descriptions?: Maybe; + /** Display message of the function */ + private readonly _displayMessages?: Maybe; /** Editable status of the flow type */ private readonly _editable?: Maybe; /** Flow type settings of the flow type */ @@ -33,8 +37,10 @@ export class FlowTypeView { constructor(flowType: FlowType) { + this._aliases = flowType.aliases; this._createdAt = flowType.createdAt; this._descriptions = flowType.descriptions; + this._displayMessages = flowType.displayMessages; this._editable = flowType.editable; this._flowTypeSettings = flowType.flowTypeSettings; this._id = flowType.id; @@ -45,6 +51,9 @@ export class FlowTypeView { this._updatedAt = flowType.updatedAt; } + get aliases(): Maybe | undefined { + return this._aliases; + } get createdAt(): Maybe | undefined { return this._createdAt; @@ -54,6 +63,10 @@ export class FlowTypeView { return this._descriptions; } + get displayMessages(): Maybe | undefined { + return this._displayMessages; + } + get editable(): Maybe | undefined { return this._editable; } diff --git a/src/components/d-flow/type/index.ts b/src/components/d-flow-type/index.ts similarity index 100% rename from src/components/d-flow/type/index.ts rename to src/components/d-flow-type/index.ts diff --git a/src/components/d-flow/data-type/DFlowDataType.validation.type.ts b/src/components/d-flow-validation/DDataTypeValidation.hook.ts similarity index 92% rename from src/components/d-flow/data-type/DFlowDataType.validation.type.ts rename to src/components/d-flow-validation/DDataTypeValidation.hook.ts index 81163cb5d..090874ccb 100644 --- a/src/components/d-flow/data-type/DFlowDataType.validation.type.ts +++ b/src/components/d-flow-validation/DDataTypeValidation.hook.ts @@ -1,8 +1,8 @@ -import {DataTypeView} from "./DFlowDataType.view"; +import {DataTypeView} from "../d-flow-data-type"; -const IGNORE_ID_KEYS = ["id", "__typename", "createdAt", "updatedAt"]; +const IGNORE_ID_KEYS = ["id", "__typename", "createdAt", "updatedAt", "aliases", "displayMessages", "name", "runtime"]; -export const useValidateDataType = ( +export const useDataTypeValidation = ( firstDataType: DataTypeView, secondDataType: DataTypeView ) => { diff --git a/src/components/d-flow-validation/DFlowValidation.hook.ts b/src/components/d-flow-validation/DFlowValidation.hook.ts new file mode 100644 index 000000000..5d8d7518f --- /dev/null +++ b/src/components/d-flow-validation/DFlowValidation.hook.ts @@ -0,0 +1,20 @@ +import {ValidationResult} from "../../utils"; +import type {Flow} from "@code0-tech/sagittarius-graphql-types"; +import {useService, useStore} from "../../utils"; +import {DFlowReactiveService} from "../d-flow"; +import React from "react"; + +export const useFlowValidation = (flowId: Flow['id']): ValidationResult[] => { + + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + const flow = flowService.getById(flowId) + + //TODO: re-enable validations + const validations = React.useMemo(() => { + return [] //flow?.nodes?.nodes?.map(node => node?.parameters?.nodes?.map(parameter => parameter?.validationResults).flat() ?? []).flat() ?? [] + }, [flowStore]) + + return validations + +} \ No newline at end of file diff --git a/src/components/d-flow/validation/DFlowValidation.style.scss b/src/components/d-flow-validation/DFlowValidation.style.scss similarity index 61% rename from src/components/d-flow/validation/DFlowValidation.style.scss rename to src/components/d-flow-validation/DFlowValidation.style.scss index 577501148..6fdc2bddd 100644 --- a/src/components/d-flow/validation/DFlowValidation.style.scss +++ b/src/components/d-flow-validation/DFlowValidation.style.scss @@ -1,6 +1,6 @@ -@use "../../../styles/variables"; -@use "../../../styles/helpers"; -@use "../../../styles/box"; +@use "../../styles/variables"; +@use "../../styles/helpers"; +@use "../../styles/box"; .d-flow-viewport-validations { padding: variables.$xxs; diff --git a/src/components/d-flow/validation/DFlowValidation.tsx b/src/components/d-flow-validation/DFlowValidation.tsx similarity index 67% rename from src/components/d-flow/validation/DFlowValidation.tsx rename to src/components/d-flow-validation/DFlowValidation.tsx index 461a635f1..d4d0608bf 100644 --- a/src/components/d-flow/validation/DFlowValidation.tsx +++ b/src/components/d-flow-validation/DFlowValidation.tsx @@ -1,12 +1,13 @@ import {Panel} from "@xyflow/react"; import React from "react"; -import {useDFlowValidations} from "./DFlowValidation.hook"; import type {Scalars} from "@code0-tech/sagittarius-graphql-types"; -import {Flex} from "../../flex/Flex"; -import {InspectionSeverity} from "../../../utils/inspection"; -import {Badge} from "../../badge/Badge"; +import {Flex} from "../flex/Flex"; +import {InspectionSeverity, ValidationResult} from "../../utils"; +import {Badge} from "../badge/Badge"; import {IconAlertTriangle, IconExclamationCircle, IconMessageExclamation} from "@tabler/icons-react"; import "./DFlowValidation.style.scss" +import {Text} from "../text/Text"; +import {useFlowValidation} from "./DFlowValidation.hook"; export interface DFlowValidationProps { flowId: Scalars['FlowID']['output'] @@ -15,34 +16,34 @@ export interface DFlowValidationProps { export const DFlowValidation: React.FC = (props) => { const {flowId} = props - const validations = useDFlowValidations(flowId) + const validations: ValidationResult[] = useFlowValidation(flowId) return {(validations?.length ?? 0) > 0 ? (
- + {(validations?.filter(v => v.type === InspectionSeverity.ERROR)?.length ?? 0) > 0 ? ( - + - - {validations?.filter(v => v.type === InspectionSeverity.ERROR)?.length} + + {validations?.filter(v => v.type === InspectionSeverity.ERROR)?.length} ) : null} {(validations?.filter(v => v.type === InspectionSeverity.WARNING)?.length ?? 0) > 0 ? ( - + - + {validations?.filter(v => v.type === InspectionSeverity.WARNING)?.length} ) : null} {(validations?.filter(v => v.type === InspectionSeverity.GRAMMAR)?.length ?? 0) > 0 ? ( - + - + {validations?.filter(v => v.type === InspectionSeverity.GRAMMAR)?.length} diff --git a/src/components/d-flow-validation/DNodeValidation.hook.ts b/src/components/d-flow-validation/DNodeValidation.hook.ts new file mode 100644 index 000000000..c1259f65f --- /dev/null +++ b/src/components/d-flow-validation/DNodeValidation.hook.ts @@ -0,0 +1,144 @@ +import React from "react" +import type { + Flow, + Maybe, + NodeFunction, + NodeFunctionIdWrapper, + NodeParameterValue, + Scalars +} from "@code0-tech/sagittarius-graphql-types" +import {DataTypeView, DFlowDataTypeReactiveService} from "../d-flow-data-type" +import {InspectionSeverity, useService, useStore, ValidationResult} from "../../utils" +import { + GenericMap, + replaceGenericKeysInDataTypeObject, + replaceGenericKeysInType, + resolveGenericKeys +} from "../../utils/generics" +import {useReturnType} from "../d-flow-function/DFlowFunction.return.hook" +import {DFlowFunctionReactiveService} from "../d-flow-function" +import {useDataTypeValidation} from "./DDataTypeValidation.hook" +import {useValueValidation} from "./DValueValidation.hook" +import {DFlowReactiveService} from "../d-flow" + +const isReferenceOrNode = (value: NodeParameterValue) => + value.__typename === "ReferenceValue" || value.__typename === "NodeFunctionIdWrapper" + +const isNode = (value: NodeParameterValue) => + value.__typename === "NodeFunctionIdWrapper" + +const resolveDataTypeWithGenerics = ( + dataType: DataTypeView, + genericMap: GenericMap +) => + new DataTypeView( + replaceGenericKeysInDataTypeObject(dataType.json!, genericMap) + ) + +const errorResult = ( + parameterId: Maybe, + expected?: DataTypeView, + actual?: DataTypeView +): ValidationResult => ({ + parameterId, + type: InspectionSeverity.ERROR, + message: { + nodes: [{ + code: "en-US", + content: `Argument of type ${actual?.name?.nodes!![0]?.content} is not assignable to parameter of type ${expected?.name?.nodes!![0]?.content}` + }] + } +}) + +export const useNodeValidation = ( + nodeId: NodeFunction['id'], + flowId: Flow['id'] +): ValidationResult[] | null => { + + const functionService = useService(DFlowFunctionReactiveService) + const functionStore = useStore(DFlowFunctionReactiveService) + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + const dataTypeService = useService(DFlowDataTypeReactiveService) + const dataTypeStore = useStore(DFlowDataTypeReactiveService) + + const flow = flowService.getById(flowId) + const node = flowService.getNodeById(flowId, nodeId) + const values = node?.parameters?.nodes?.map(p => p?.value!!) ?? [] + const functionDefinition = functionService.getById(node?.functionDefinition?.id) + const parameters = functionDefinition?.parameterDefinitions ?? [] + const genericKeys = functionDefinition?.genericKeys ?? [] + const genericMap = React.useMemo( + () => resolveGenericKeys(functionDefinition!, values, dataTypeService, flow), + [functionDefinition, values, dataTypeService, dataTypeStore, flow, flowStore] + ) + + const resolveValueType = React.useCallback( + (value: NodeParameterValue, expectedDT?: DataTypeView) => { + if (isNode(value) && expectedDT?.variant !== "NODE") { + const node = flowService.getNodeById(flowId, (value as NodeFunctionIdWrapper).id) + const fn = functionService.getById(node?.functionDefinition?.id!!)!! + const params = node?.parameters?.nodes?.map(p => p?.value!!) ?? [] + return useReturnType(fn, params, dataTypeService) + } + return dataTypeService.getTypeFromValue(value, flow) + }, + [dataTypeService, flow, flowId, flowService, functionService] + ) + + return React.useMemo(() => { + const errors: ValidationResult[] = [] + + for (let i = 0; i < parameters.length; i++) { + const parameter = parameters[i] + const value = values[i] + if (!value) continue + + const expectedType = parameter.dataTypeIdentifier + const expectedDT = dataTypeService.getDataType(expectedType!!) + const valueType = resolveValueType(value, expectedDT) + const valueDT = dataTypeService.getDataType(valueType!!) + + if (!expectedDT || !valueDT) { + errors.push(errorResult(parameter.id!!, expectedDT, valueDT)) + continue + } + + const isGeneric = + !!expectedType?.genericType || + (!!expectedType?.genericKey && genericKeys.includes(expectedType.genericKey)) + + let isValid = true + + if (isGeneric) { + const resolvedExpectedDT = resolveDataTypeWithGenerics(expectedDT, genericMap) + + if (isReferenceOrNode(value)) { + const resolvedValueDT = resolveDataTypeWithGenerics(valueDT, genericMap) + isValid = useDataTypeValidation(resolvedExpectedDT, resolvedValueDT) + } else { + const resolvedType = replaceGenericKeysInType(expectedType, genericMap) + isValid = useValueValidation( + value, + resolvedExpectedDT, + dataTypeService, + flow, + resolvedType?.genericType?.genericMappers! + ) + } + } else { + if (isReferenceOrNode(value) && expectedDT.variant !== "NODE") { + isValid = useDataTypeValidation(expectedDT, valueDT) + } else { + isValid = useValueValidation(value, expectedDT, dataTypeService, flow) + } + } + + if (!isValid) { + errors.push(errorResult(parameter.id!!, expectedDT, valueDT)) + } + } + + return errors.length > 0 ? errors : null + }, [flow, node, values, functionDefinition, parameters, genericKeys, genericMap, resolveValueType, nodeId, flowId, functionStore, flowStore, dataTypeStore, dataTypeService]) +} \ No newline at end of file diff --git a/src/components/d-flow/data-type/DFlowDataType.validation.value.ts b/src/components/d-flow-validation/DValueValidation.hook.ts similarity index 50% rename from src/components/d-flow/data-type/DFlowDataType.validation.value.ts rename to src/components/d-flow-validation/DValueValidation.hook.ts index c871d1c57..4c288a067 100644 --- a/src/components/d-flow/data-type/DFlowDataType.validation.value.ts +++ b/src/components/d-flow-validation/DValueValidation.hook.ts @@ -1,18 +1,16 @@ -import type {GenericMapper, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; -import {RuleMap} from "./rules/DFlowDataTypeRules"; -import {DataTypeView} from "./DFlowDataType.view"; -import {useService} from "../../../utils/contextStore"; -import {DFlowDataTypeReactiveService} from "./DFlowDataType.service"; -import {FlowView} from "../DFlow.view"; +import type {Flow, GenericMapper, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; +import {RuleMap} from "../d-flow-data-type/rules/DFlowDataTypeRules"; +import {DataTypeView} from "../d-flow-data-type"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; -export const useValidateValue = ( +export const useValueValidation = ( value: NodeParameterValue, dataType: DataTypeView, - flow?: FlowView, + dataTypeService: DFlowDataTypeReactiveService, + flow?: Flow, generics?: GenericMapper[], ): boolean => { - const dataTypeService = useService(DFlowDataTypeReactiveService) const map = new Map(generics?.map(generic => [generic.target!!, generic])) return dataType.rules?.nodes?.every(rule => { diff --git a/src/components/d-flow/validation/index.ts b/src/components/d-flow-validation/index.ts similarity index 100% rename from src/components/d-flow/validation/index.ts rename to src/components/d-flow-validation/index.ts diff --git a/src/components/d-flow/DFlow.edges.hook.ts b/src/components/d-flow/DFlow.edges.hook.ts index eeb4654aa..3a7eab9a7 100644 --- a/src/components/d-flow/DFlow.edges.hook.ts +++ b/src/components/d-flow/DFlow.edges.hook.ts @@ -1,235 +1,220 @@ -import {useService, useStore} from "../../utils/contextStore"; +import {useService, useStore} from "../../utils"; 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 {DFlowFunctionReactiveService} from "../d-flow-function"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; import React from "react"; -import type {DataTypeIdentifier, DataTypeVariant, Flow, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import type {DataTypeIdentifier, Flow, NodeFunction, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import {md5} from "js-md5"; +import {DFlowEdgeDataProps} from "./DFlowEdge"; -// 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 + 'rgba(255, 255, 255, 0.25)', // rot ]; -export const useFlowEdges = (flowId: Flow['id']): Edge[] => { +// @ts-ignore +export const useFlowEdges = (flowId: Flow['id']): Edge[] => { const flowService = useService(DFlowReactiveService); - const functionService = useService(DFlowFunctionReactiveService); - const dataTypeService = useService(DFlowDataTypeReactiveService); - const flow = flowService.getById(flowId); const flowStore = useStore(DFlowReactiveService) + const functionService = useService(DFlowFunctionReactiveService); const functionStore = useStore(DFlowFunctionReactiveService) + const dataTypeService = useService(DFlowDataTypeReactiveService); const dataTypeStore = useStore(DFlowDataTypeReactiveService) - if (!flow) return []; + const flow = React.useMemo(() => flowService.getById(flowId), [flowId, flowStore]) - /* ------------------------------------------------------------------ */ - const edges: Edge[] = []; + return React.useMemo(() => { + if (!flow) return []; - /** merkt sich für jede Function-Card die Gruppen-IDs, - * **für die wirklich ein Funktions-Wert existiert** */ - const groupsWithValue = new Map(); + // @ts-ignore + const edges: Edge[] = []; - let idCounter = 0; // globale, fortlaufende Id-Vergabe + /** merkt sich für jede Function-Card die Gruppen-IDs, + * **für die wirklich ein Funktions-Wert existiert** */ + const groupsWithValue = new Map(); - const functionCache = new Map>(); - const dataTypeCache = new Map>(); + let idCounter = 0; // globale, fortlaufende Id-Vergabe - const getFunctionDefinitionCached = ( - id: Scalars['FunctionDefinitionID']['output'], - cache = functionCache, - ) => { - if (!cache.has(id)) { - cache.set(id, functionService.getById(id)); - } - return cache.get(id); - }; - - const getDataTypeCached = ( - type: DataTypeIdentifier, - 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.id}-${idCounter++}`; - - if (idCounter == 1) { - // erste Function-Card → Verbindung Trigger → Function - edges.push({ - id: `trigger-${fnId}-next`, - source: flow.id as string, // 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, - }); - } + const functionCache = new Map>(); + const dataTypeCache = new Map>(); - /* ------- 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, - }); + const getFunctionDefinitionCached = ( + id: Scalars['FunctionDefinitionID']['output'], + cache = functionCache, + ) => { + if (!cache.has(id)) { + cache.set(id, functionService.getById(id)); } - } - - /* ------- horizontale Kanten für Parameter -------------------- */ - fn.parameters?.forEach((param) => { - const val = param.value; - const def = getFunctionDefinitionCached(fn.functionDefinition?.id!!, fnCache) - ?.parameterDefinitions?.find(p => p.id === param.id); - const paramType = def?.dataTypeIdentifier; - const paramDT = paramType ? getDataTypeCached(paramType, dtCache) : undefined; + return cache.get(id); + }; + + const getDataTypeCached = ( + type: DataTypeIdentifier, + cache = dataTypeCache, + ) => { + if (!cache.has(type)) { + cache.set(type, dataTypeService.getDataType(type)); + } + return cache.get(type); + }; - if (!val) return + const traverse = ( + node: NodeFunction, + parentNode?: NodeFunction, + parentNodeId?: string, + level = 0, + fnCache = functionCache, + dtCache = dataTypeCache, + ): string => { - /* --- NODE-Parameter → Group-Card ------------------------- */ - if (paramDT?.variant === "NODE") { - const groupId = `${fnId}-group-${idCounter++}`; + const fnId = `${node.id}-${idCounter++}`; - /* Verbindung Gruppe → Function-Card (horizontal) */ + if (idCounter == 1) { edges.push({ - id: `${fnId}-${groupId}-param-${param.id}`, - source: fnId, // FunctionCard (Quelle) - target: groupId, // GroupCard (Ziel – hat Top: target) - deletable: false, - selectable: false, - label: def?.names?.nodes!![0]?.content ?? param.id, + id: `trigger-${fnId}-next`, + source: flow.id as string, + target: fnId, data: { color: FLOW_EDGE_RAINBOW[level % FLOW_EDGE_RAINBOW.length], - isParameter: false, + type: 'default', + flowId: flowId, + parentNodeId: parentNode?.id }, + deletable: false, + selectable: 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); + if (parentNodeId) { + const startGroups = groupsWithValue.get(parentNodeId) ?? []; + + if (startGroups.length > 0) { + startGroups.forEach((gId, idx) => edges.push({ + id: `${gId}-${fnId}-next-${idx}`, + source: gId, + target: fnId, + data: { + color: FLOW_EDGE_RAINBOW[level % FLOW_EDGE_RAINBOW.length], + type: 'default', + flowId: flowId, + parentNodeId: parentNode?.id + }, + deletable: false, + selectable: false, + })); + } else { + edges.push({ + id: `${parentNodeId}-${fnId}-next`, + source: parentNodeId, + target: fnId, + data: { + color: FLOW_EDGE_RAINBOW[level % FLOW_EDGE_RAINBOW.length], + type: 'default', + flowId: flowId, + parentNodeId: parentNode?.id + }, + deletable: false, + selectable: false, + }); + } + } - /* rekursiv Funktions-Ast innerhalb der Gruppe */ - traverse(param.value as NodeFunctionView, + node.parameters?.nodes?.forEach((param) => { + const val = param?.value; + const def = getFunctionDefinitionCached(node.functionDefinition?.id!!, fnCache) + ?.parameterDefinitions?.find(p => p.id === param?.id); + const paramType = def?.dataTypeIdentifier; + const paramDT = paramType ? getDataTypeCached(paramType, dtCache) : undefined; + + if (!val) return + + if (paramDT?.variant === "NODE") { + const groupId = `${fnId}-group-${idCounter++}`; + const hash = md5(`${fnId}-param-${JSON.stringify(param)}`) + const hashToHue = (md5: string): number => { + // nimm z.B. 8 Hex-Zeichen = 32 Bit + const int = parseInt(md5.slice(0, 8), 16) + return int % 360 + } + + edges.push({ + id: `${fnId}-${groupId}-param-${param.id}`, + source: fnId, + target: groupId, + deletable: false, + selectable: false, + animated: true, + label: def?.names?.nodes!![0]?.content ?? param.id, + data: { + color: `hsl(${hashToHue(hash)}, 100%, 72%)`, + type: 'group', + flowId: flowId, + parentNodeId: parentNode?.id + }, + }); + + + if (val && val.__typename === "NodeFunctionIdWrapper") { + + (groupsWithValue.get(fnId) ?? (groupsWithValue.set(fnId, []), + groupsWithValue.get(fnId)!)) + .push(groupId); + + traverse( + flowService.getNodeById(flowId, val.id)!, + node, + undefined, + level + 1, + fnCache, + dtCache + ); + } + } else if (val && val.__typename === "NodeFunctionIdWrapper") { + const subFnId = traverse( + flowService.getNodeById(flowId, val.id)!, + node, undefined, level + 1, fnCache, - dtCache); + dtCache + ); + + const hash = md5(`${fnId}-param-${JSON.stringify(param)}`) + const hashToHue = (md5: string): number => { + // nimm z.B. 8 Hex-Zeichen = 32 Bit + const int = parseInt(md5.slice(0, 8), 16) + return int % 360 + } + + edges.push({ + id: `${subFnId}-${fnId}-param-${param.id}`, + source: subFnId, + target: fnId, + targetHandle: `param-${param.id}`, + animated: true, + deletable: false, + selectable: false, + data: { + color: `hsl(${hashToHue(hash)}, 100%, 72%)`, + type: 'parameter', + flowId: flowId, + parentNodeId: parentNode?.id + }, + }); } - } - - /* --- 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.nextNodeId) { - traverse(flow.getNodeById(fn.nextNodeId!!)!!, 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, - }); + if (node.nextNodeId) { + traverse(flowService.getNodeById(flow.id!!, node.nextNodeId!!)!!, node, fnId, level, fnCache, dtCache); // gleiche Ebenentiefe } - } - return fnId; - }; + return fnId; + }; - if (flow.startingNodeId) { - traverse(flow.getNodeById(flow.startingNodeId)!!, undefined, 0, functionCache, dataTypeCache); - } + if (flow.startingNodeId) { + traverse(flowService.getNodeById(flow.id!!, flow.startingNodeId!!)!!, undefined, undefined, 0, functionCache, dataTypeCache); + } - return React.useMemo(() => edges, [flowStore, functionStore, dataTypeStore, edges]); + return edges + }, [flow, flowStore, functionStore, dataTypeStore]); }; \ No newline at end of file diff --git a/src/components/d-flow/DFlow.nodes.hook.ts b/src/components/d-flow/DFlow.nodes.hook.ts index 104d49120..ac7b82ab2 100644 --- a/src/components/d-flow/DFlow.nodes.hook.ts +++ b/src/components/d-flow/DFlow.nodes.hook.ts @@ -1,10 +1,15 @@ -import {useService} from "../../utils/contextStore"; +import {useService, useStore} from "../../utils"; 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 type {DataTypeIdentifier, DataTypeVariant, Flow, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowFunctionReactiveService} from "../d-flow-function"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import type {DataTypeIdentifier, Flow, NodeFunction, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import React from "react"; +import {DFlowFunctionDefaultCardDataProps} from "../d-flow-function/DFlowFunctionDefaultCard"; +import {DFlowFunctionSuggestionCardDataProps} from "../d-flow-function/DFlowFunctionSuggestionCard"; +import {DFlowFunctionTriggerCardDataProps} from "../d-flow-function/DFlowFunctionTriggerCard"; +import {DFlowFunctionGroupCardDataProps} from "../d-flow-function/DFlowFunctionGroupCard"; +import {md5} from "js-md5"; const packageNodes = new Map([ ['std', 'default'], @@ -90,165 +95,154 @@ const bestMatchValue = (map: Map, input: string): string => { return bestKey !== null ? map.get(bestKey)! : ""; }; -export const useFlowNodes = (flowId: Flow['id']): Node[] => { +// @ts-ignore +export const useFlowNodes = (flowId: Flow['id']): Node[] => { const flowService = useService(DFlowReactiveService); + const flowStore = useStore(DFlowReactiveService); const functionService = useService(DFlowFunctionReactiveService); + const functionStore = useStore(DFlowFunctionReactiveService); const dataTypeService = useService(DFlowDataTypeReactiveService); - const flow = flowService.getById(flowId); + const dataTypeStore = useStore(DFlowDataTypeReactiveService); - if (!flow) return []; + const flow = React.useMemo(() => flowService.getById(flowId), [flowId, flowStore]) - const nodes: Node[] = []; - let idCounter = 0; + return React.useMemo(() => { + if (!flow) return []; - const functionCache = new Map>(); - const dataTypeCache = new Map>(); + // @ts-ignore + const nodes: Node[] = []; + let idCounter = 0; - const getFunctionDefinitionCached = ( - id: Scalars['FunctionDefinitionID']['output'], - cache = functionCache, - ) => { - if (!cache.has(id)) { - cache.set(id, functionService.getById(id)); - } - return cache.get(id); - }; - - const getDataTypeCached = ( - type: DataTypeIdentifier, - 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, - flowId, - } - }) - - - const traverse = ( - fn: NodeFunctionView, - isParameter = false, - parentId?: string, - depth: number = 0, - scopePath: number[] = [0], - parentGroup?: string, - fnCache = functionCache, - dtCache = dataTypeCache, - ) => { - const id = `${fn.id}-${idCounter++}`; - const index = ++globalNodeIndex; // global node level + const functionCache = new Map>(); + const dataTypeCache = new Map>(); + + const getFunctionDefinitionCached = ( + id: Scalars['FunctionDefinitionID']['output'], + cache = functionCache, + ) => { + if (!cache.has(id)) { + cache.set(id, functionService.getById(id)); + } + return cache.get(id); + }; + + const getDataTypeCached = ( + type: DataTypeIdentifier, + 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, - type: bestMatchValue(packageNodes, fn.functionDefinition?.identifier!!), - position: { x: 0, y: 0 }, + id: `${flow.id}`, + type: "trigger", + position: {x: 0, y: 0}, draggable: false, - parentId: parentGroup, - extent: parentGroup ? "parent" : undefined, data: { - instance: fn, - isParameter, + instance: flow, flowId, - 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.nextNodeId && !isParameter) { + } + }) + + + const traverse = ( + node: NodeFunction, + isParameter = false, + parentId?: string, + depth: number = 0, + scopePath: number[] = [0], + parentGroup?: string, + fnCache = functionCache, + dtCache = dataTypeCache, + ) => { + const id = `${node.id}-${idCounter++}`; + const index = ++globalNodeIndex; // global node level + nodes.push({ - id: `${id}-suggestion`, - type: "suggestion", - position: { x: 0, y: 0 }, + id, + type: bestMatchValue(packageNodes, node.functionDefinition?.identifier!!), + position: {x: 0, y: 0}, draggable: false, - extent: parentGroup ? "parent" : undefined, parentId: parentGroup, + extent: parentGroup ? "parent" : undefined, data: { - flowId: flowId, - parentFunction: fn, + nodeId: node.id, + isParameter, + flowId: flowId!!, + 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 }, }); - } - const definition = getFunctionDefinitionCached(fn.functionDefinition?.id!!, fnCache); - - fn.parameters?.forEach((param) => { - const paramType = definition?.parameterDefinitions!!.find(p => p.id == param.runtimeParameter?.id)?.dataTypeIdentifier; - const paramDataType = paramType ? getDataTypeCached(paramType, dtCache) : undefined; - - if (paramDataType?.variant === "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, - flowId, - 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); + const definition = getFunctionDefinitionCached(node.functionDefinition?.id!!, fnCache); + + node.parameters?.nodes?.forEach((param) => { + const paramType = definition?.parameterDefinitions!!.find(p => p.id == param?.runtimeParameter?.id)?.dataTypeIdentifier; + const paramDataType = paramType ? getDataTypeCached(paramType, dtCache) : undefined; + + if (paramDataType?.variant === "NODE") { + if (param?.value && param.value.__typename === "NodeFunctionIdWrapper") { + const groupId = `${id}-group-${idCounter++}`; + + // New group: extend scope PATH with a fresh segment and increase depth. + const childScopePath = [...scopePath, nextScopeId()]; + + const hash = md5(`${id}-param-${JSON.stringify(param)}`) + const hashToHue = (md5: string): number => { + // nimm z.B. 8 Hex-Zeichen = 32 Bit + const int = parseInt(md5.slice(0, 8), 16) + return int % 360 + } + + nodes.push({ + id: groupId, + type: "group", + position: {x: 0, y: 0}, + draggable: false, + parentId: parentGroup, + extent: parentGroup ? "parent" : undefined, + data: { + isParameter: true, + linkingId: id, + flowId: flowId!!, + depth: depth + 1, + scope: childScopePath, + color: `hsl(${hashToHue(hash)}, 100%, 72%)`, + }, + }); + + // Child function inside the group uses the group's depth and scope PATH. + traverse(flowService.getNodeById(flowId, param.value.id)!, false, undefined, depth + 1, childScopePath, groupId, fnCache, dtCache); + } + } else if (param?.value && param.value.__typename === "NodeFunctionIdWrapper") { + // Functions passed as non-NODE parameters live in the same depth/scope PATH. + traverse(flowService.getNodeById(flowId, param.value.id)!, true, id, depth, scopePath, parentGroup, 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 (node.nextNodeId) { + // Linear chain continues in the same depth/scope PATH. + traverse(flowService.getNodeById(flow.id, node.nextNodeId)!!, false, undefined, depth, scopePath, parentGroup, fnCache, dtCache); } - }); + }; - if (fn.nextNodeId) { - // Linear chain continues in the same depth/scope PATH. - traverse(flow.getNodeById(fn.nextNodeId!!)!!, false, undefined, depth, scopePath, parentGroup, fnCache, dtCache); + // Root lane: depth 0, scope path [0] + if (flow.startingNodeId) { + traverse(flowService.getNodeById(flow.id, flow.startingNodeId)!!, false, undefined, 0, [0], undefined, functionCache, dataTypeCache); } - }; - - // Root lane: depth 0, scope path [0] - if (flow.startingNodeId) { - traverse(flow.getNodeById(flow.startingNodeId)!!, false, undefined, 0, [0], undefined, functionCache, dataTypeCache); - } else { - nodes.push({ - id: `${flow.id}-suggestion`, - type: "suggestion", - position: { x: 0, y: 0 }, - draggable: false, - extent: undefined, - data: { - flowId: flowId, - parentFunction: undefined, - }, - }); - } - return nodes; + return nodes; + }, [flow, flowStore, functionStore, dataTypeStore]) }; \ No newline at end of file diff --git a/src/components/d-flow/DFlow.service.ts b/src/components/d-flow/DFlow.service.ts index dc22ad765..775207835 100644 --- a/src/components/d-flow/DFlow.service.ts +++ b/src/components/d-flow/DFlow.service.ts @@ -1,25 +1,209 @@ -import {FlowView} from "./DFlow.view"; -import {ReactiveArrayService} from "../../utils/reactiveArrayService"; -import type { +import {ReactiveArrayService} from "../../utils"; +import { + DataTypeIdentifier, + DataTypeIdentifierInput, Flow, + FlowInput, + FlowSetting, + LiteralValue, NamespacesProjectsFlowsCreateInput, - NamespacesProjectsFlowsCreatePayload, NamespacesProjectsFlowsDeleteInput, - NamespacesProjectsFlowsDeletePayload + NamespacesProjectsFlowsCreatePayload, + NamespacesProjectsFlowsDeleteInput, + NamespacesProjectsFlowsDeletePayload, + NamespacesProjectsFlowsUpdateInput, + NamespacesProjectsFlowsUpdatePayload, + NodeFunction, + NodeFunctionIdWrapper, + NodeParameter, + ReferenceValue } from "@code0-tech/sagittarius-graphql-types"; -export abstract class DFlowReactiveService extends ReactiveArrayService { +export abstract class DFlowReactiveService extends ReactiveArrayService { - //TODO: inject UI error handler for toasts - //inject: namespaceId and projectId because the runtimes query needs it - - getById(id: Flow['id']): FlowView | undefined { + getById(id: Flow['id']): Flow | undefined { return this.values().find(value => value.id === id); } - /** Creates a new flow. */ + protected removeParameterNode(flow: Flow, parameter: NodeParameter): void { + if (parameter?.value?.__typename === "NodeFunctionIdWrapper") { + const parameterNode = flow?.nodes?.nodes?.find(n => n?.id === (parameter.value as NodeFunction)?.id) + if (parameterNode) { + flow!.nodes!.nodes = flow!.nodes!.nodes!.filter(n => n?.id !== (parameter.value as NodeFunction)?.id) + let nextNodeId = parameterNode.nextNodeId + while (nextNodeId) { + const nextNode = flow!.nodes!.nodes!.find(n => n?.id === nextNodeId) + if (nextNode) { + flow!.nodes!.nodes = flow!.nodes!.nodes!.filter(n => n?.id !== nextNodeId) + nextNodeId = nextNode.nextNodeId + } else { + nextNodeId = null + } + } + parameterNode.parameters?.nodes?.forEach(p => { + this.removeParameterNode(flow, p!!) + }) + } + } + } + + getNodeById(flowId: Flow['id'], nodeId: NodeFunction['id']): NodeFunction | undefined { + return this.getById(flowId)?.nodes?.nodes?.find(node => node?.id === nodeId)!! + } + + getPayloadById(flowId: Flow['id']): FlowInput | undefined { + const flow = this.getById(flowId) + + const getDataTypeIdentifierPayload = (identifier: DataTypeIdentifier): DataTypeIdentifierInput => { + return { + ...(identifier?.dataType ? { + dataTypeId: identifier?.dataType?.id + } : {}), + ...(identifier?.genericKey ? { + genericKey: identifier?.genericKey + } : {}), + ...(identifier?.genericType ? { + genericType: { + dataTypeId: identifier?.genericType?.dataType?.id!, + genericMappers: identifier?.genericType?.genericMappers?.map(genericMap => ({ + target: genericMap.target!, + sourceDataTypeIdentifiers: genericMap.sourceDataTypeIdentifiers?.map(getDataTypeIdentifierPayload) ?? [] + })) ?? [] + } + } : {}) + } + } + + return { + name: flow?.name!, + type: flow?.type?.id!, + settings: flow?.settings?.nodes?.map(setting => { + return { + flowSettingIdentifier: setting?.flowSettingIdentifier!, + value: setting?.value!, + } + }), + nodes: flow?.nodes?.nodes?.map(node => { + return { + id: node?.id!, + nextNodeId: node?.nextNodeId!, + parameters: node?.parameters?.nodes?.map(parameter => { + return { + runtimeParameterDefinitionId: parameter?.runtimeParameter?.id!, + value: parameter?.value?.__typename === "NodeFunctionIdWrapper" ? { + nodeFunctionId: parameter.value.id! + } : parameter?.value?.__typename === "LiteralValue" ? { + literalValue: parameter.value.value! + } : { + referenceValue: { + dataTypeIdentifier: getDataTypeIdentifierPayload((parameter?.value as ReferenceValue).dataTypeIdentifier!), + depth: (parameter?.value as ReferenceValue).depth!, + node: (parameter?.value as ReferenceValue).node!, + nodeFunctionId: (parameter?.value as ReferenceValue).nodeFunctionId!, + referencePath: (parameter?.value as ReferenceValue).referencePath!, + scope: (parameter?.value as ReferenceValue).scope!, + } + }, + } + }) ?? [], + runtimeFunctionId: node?.functionDefinition?.runtimeFunctionDefinition?.id! + } + }) ?? [], + startingNodeId: flow?.startingNodeId!, + } + } + + async deleteNodeById(flowId: Flow['id'], nodeId: NodeFunction['id']): Promise { + const flow = this.getById(flowId) + const node = this.getNodeById(flowId, nodeId) + const previousNodes = flow?.nodes?.nodes?.find(n => n?.nextNodeId === nodeId) + const index = this.values().findIndex(f => f.id === flowId) + if (!flow || !node) return + + flow.nodes!.nodes = flow.nodes!.nodes!.filter(n => n?.id !== nodeId) + node.parameters?.nodes?.forEach(p => this.removeParameterNode(flow, p!!)) + + if (previousNodes) { + previousNodes.nextNodeId = node.nextNodeId + } else { + flow.startingNodeId = node.nextNodeId ?? undefined + } + + this.set(index, flow) + } + + async addNextNodeById(flowId: Flow['id'], parentNodeId: NodeFunction['id'] | null, nextNode: NodeFunction): Promise { + + const flow = this.getById(flowId) + const index = this.values().findIndex(f => f.id === flowId) + const parentNode = parentNodeId ? this.getNodeById(flowId, parentNodeId) : undefined + + if (!flow || (parentNodeId && !parentNode)) return + + const nextNodeIndex: number = Math.max(0, ...flow.nodes?.nodes?.map(node => Number(node?.id?.match(/NodeFunction\/(\d+)$/)?.[1] ?? 0)) ?? [0]) + const nextNodeId: NodeFunction['id'] = `gid://sagittarius/NodeFunction/${nextNodeIndex + 1}` + const addingNode: NodeFunction = { + ...nextNode, + id: nextNodeId, + } + + if (parentNode && parentNode.nextNodeId) { + addingNode.nextNodeId = parentNode.nextNodeId + } else if (!parentNode && flow.startingNodeId) { + addingNode.nextNodeId = flow.startingNodeId + } + + flow.nodes?.nodes?.push(addingNode) + + if (parentNode) { + parentNode.nextNodeId = addingNode.id + } else { + flow.startingNodeId = addingNode.id + } + + this.set(index, flow) + } + + async setSettingValue(flowId: Flow['id'], settingId: FlowSetting['id'], value: FlowSetting['value']): Promise { + const flow = this.getById(flowId) + const index = this.values().findIndex(f => f.id === flowId) + if (!flow) return + const setting = flow.settings?.nodes?.find(s => s?.id === settingId) + if (!setting) return + setting.value = value + this.set(index, flow) + } + + async setParameterValue(flowId: Flow['id'], nodeId: NodeFunction['id'], parameterId: NodeParameter['id'], value?: LiteralValue | ReferenceValue | NodeFunction): Promise { + const flow = this.getById(flowId) + const index = this.values().findIndex(f => f.id === flowId) + if (!flow) return + const node = this.getNodeById(flowId, nodeId) + if (!node) return + const parameter = node.parameters?.nodes?.find(p => p?.id === parameterId) + if (!parameter) return + this.removeParameterNode(flow, parameter) + if (value?.__typename === "NodeFunction") { + const nextNodeIndex: number = Math.max(0, ...flow.nodes?.nodes?.map(node => Number(node?.id?.match(/NodeFunction\/(\d+)$/)?.[1] ?? 0)) ?? [0]) + const addingIdValue: NodeFunction = { + ...value, + id: `gid://sagittarius/NodeFunction/${nextNodeIndex + 1}` + } + flow.nodes?.nodes?.push(addingIdValue) + parameter.value = { + id: `gid://sagittarius/NodeFunction/${nextNodeIndex + 1}`, + __typename: "NodeFunctionIdWrapper" + } as NodeFunctionIdWrapper + } else { + parameter.value = value as LiteralValue | ReferenceValue + } + this.set(index, flow) + } + abstract flowCreate(payload: NamespacesProjectsFlowsCreateInput): Promise - /** Deletes a namespace project. */ + abstract flowDelete(payload: NamespacesProjectsFlowsDeleteInput): Promise + + abstract flowUpdate(payload: NamespacesProjectsFlowsUpdateInput): Promise } diff --git a/src/components/d-flow/DFlow.tsx b/src/components/d-flow/DFlow.tsx index aeb5f18ab..9d05bc81d 100644 --- a/src/components/d-flow/DFlow.tsx +++ b/src/components/d-flow/DFlow.tsx @@ -1,23 +1,30 @@ -import {Code0ComponentProps} from "../../utils/types"; +import {Code0ComponentProps, mergeCode0Props} from "../../utils"; import { + Background, + BackgroundVariant, Edge, Node, ReactFlow, - ReactFlowProps, ReactFlowProvider, useEdgesState, useNodesState, useUpdateNodeInternals } from "@xyflow/react"; import React from "react"; -import {mergeCode0Props} from "../../utils/utils"; import '@xyflow/react/dist/style.css'; import "./DFlow.style.scss" -import {DFlowFunctionDefaultCard} from "./function/DFlowFunctionDefaultCard"; -import {DFlowFunctionGroupCard} from "./function/DFlowFunctionGroupCard"; -import {DFlowFunctionSuggestionCard} from "./function/DFlowFunctionSuggestionCard"; -import {DFlowFunctionTriggerCard} from "./function/DFlowFunctionTriggerCard"; -import {DFlowEdge} from "./edge/DFlowEdge"; +import {DFlowFunctionDefaultCard} from "../d-flow-function/DFlowFunctionDefaultCard"; +import {DFlowFunctionGroupCard} from "../d-flow-function/DFlowFunctionGroupCard"; +import {DFlowFunctionSuggestionCard} from "../d-flow-function/DFlowFunctionSuggestionCard"; +import {DFlowFunctionTriggerCard} from "../d-flow-function/DFlowFunctionTriggerCard"; +import {DFlowEdge} from "./DFlowEdge"; +import {DFlowPanelSize} from "../d-flow-panel"; +import {DFlowValidation} from "../d-flow-validation"; +import {Flow} from "@code0-tech/sagittarius-graphql-types"; +import {useFlowNodes} from "./DFlow.nodes.hook"; +import {useFlowEdges} from "./DFlow.edges.hook"; +import {DFlowPanelControl} from "../d-flow-panel"; +import {DFlowPanelLayout} from "../d-flow-panel"; /** * Dynamically layouts a tree of nodes and their parameter nodes for a flow-based editor. @@ -31,11 +38,11 @@ import {DFlowEdge} from "./edge/DFlowEdge"; */ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { if (!dirtyIds || dirtyIds.size === 0) { - return {nodes}; + return {nodes} } /* Konstanten */ - const V = 75; // vertical gap Node ↕ Node - const H = 75; // horizontal gap Parent → Param + const V = 50; // vertical gap Node ↕ Node + const H = 50; // horizontal gap Parent → Param const PAD = 16; // inner padding einer Group (links+rechts / oben+unten) const EPS = 0.25; // Toleranz gegen Rundungsdrift @@ -44,72 +51,72 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { let changed = false; // Aktueller Arbeitsstand der Nodes (Styles werden in den Pässen fortgeschrieben) - const work = nodes.map(n => ({...n})); + const work = nodes.map(n => ({...n})) // Relationen einmalig ermitteln (IDs behalten) -------------------------------- - const rfKidIds = new Map(); - const paramIds = new Map(); + const rfKidIds = new Map() + const paramIds = new Map() for (const n of work) { const link = (n.data as any)?.linkingId; if (link) { - let arr = paramIds.get(link); + let arr = paramIds.get(link) if (!arr) { arr = []; - paramIds.set(link, arr); + paramIds.set(link, arr) } - arr.push(n.id); + arr.push(n.id) } if (n.parentId && !link) { - let arr = rfKidIds.get(n.parentId); + let arr = rfKidIds.get(n.parentId) if (!arr) { arr = []; - rfKidIds.set(n.parentId, arr); + rfKidIds.set(n.parentId, arr) } - arr.push(n.id); + arr.push(n.id) } } - const rfKids = new Map(); - const params = new Map(); + const rfKids = new Map() + const params = new Map() - const byId = new Map(work.map(n => [n.id, n])); + const byId = new Map(work.map(n => [n.id, n])) for (const [k, ids] of rfKidIds) { - const arr: Node[] = new Array(ids.length); + const arr: Node[] = new Array(ids.length) for (let i = 0; i < ids.length; i++) arr[i] = byId.get(ids[i])!; - rfKids.set(k, arr); + rfKids.set(k, arr) } for (const [k, ids] of paramIds) { - const arr: Node[] = new Array(ids.length); + const arr: Node[] = new Array(ids.length) for (let i = 0; i < ids.length; i++) arr[i] = byId.get(ids[i])!; - params.set(k, arr); + params.set(k, arr) } - type Size = { w: number; h: number }; - const baseSizes = new Map(); + type Size = { w: number; h: number } + const baseSizes = new Map() for (const n of work) { const styleW = typeof n.style?.width === 'number' ? n.style.width : undefined; const styleH = typeof n.style?.height === 'number' ? n.style.height : undefined; const mw = n.measured?.width && n.measured.width > 0 ? n.measured.width : undefined; const mh = n.measured?.height && n.measured.height > 0 ? n.measured.height : undefined; - baseSizes.set(n.id, {w: styleW ?? mw ?? 200, h: styleH ?? mh ?? 80}); + baseSizes.set(n.id, {w: styleW ?? mw ?? 200, h: styleH ?? mh ?? 80}) } - const sizeCache = new Map(); + const sizeCache = new Map() const size = (n: Node): Size => { if (sizeCache.has(n.id)) return sizeCache.get(n.id)!; if (n.type !== 'group') { const s = baseSizes.get(n.id)!; - sizeCache.set(n.id, s); + sizeCache.set(n.id, s) return s; } const styleW = typeof n.style?.width === 'number' ? n.style.width : undefined; const styleH = typeof n.style?.height === 'number' ? n.style.height : undefined; if (styleW !== undefined && styleH !== undefined) { - const s = {w: styleW, h: styleH}; - sizeCache.set(n.id, s); + const s = {w: styleW, h: styleH} + sizeCache.set(n.id, s) return s; } @@ -118,36 +125,36 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { let wMax = 0; let count = 0; for (const k of kids) { - const ks = size(k); + const ks = size(k) stackH += ks.h; if (ks.w > wMax) wMax = ks.w; count++; } - stackH += V * Math.max(0, count - 1); + stackH += V * Math.max(0, count - 1) const g = { w: wMax + 2 * PAD, h: (count ? stackH : 0) + 2 * PAD, - }; - sizeCache.set(n.id, g); + } + sizeCache.set(n.id, g) return g; - }; + } do { changed = false; pass++; - sizeCache.clear(); - for (const n of work) size(n); + sizeCache.clear() + for (const n of work) size(n) /* ---------- relatives Layout (Zentren in globalen Koordinaten) -------- */ - type Pos = { x: number; y: number }; - const rel = new Map(); + type Pos = { x: number; y: number } + const rel = new Map() // Merker: Unterkante je rechter Spalten-"Band", damit Parameter unterschiedlicher Parents // in derselben Spalte nicht kollidieren. - const columnBottom = new Map(); - const colKey = (x: number) => Math.round(x / 10); // 10px-Buckets gegen Floating-Drift + const columnBottom = new Map() + const colKey = (x: number) => Math.round(x / 10) // 10px-Buckets gegen Floating-Drift const layoutIter = (root: Node, cx: number, cy: number): number => { type Frame = { @@ -174,7 +181,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { kidIndex?: number; curY?: number; bottom?: number; - }; + } const stack: Frame[] = [{node: root, cx, cy, phase: 0}]; let returnBottom = 0; @@ -182,8 +189,8 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const f = stack[stack.length - 1]; switch (f.phase) { case 0: { - rel.set(f.node.id, {x: f.cx, y: f.cy}); - const {w, h} = size(f.node); + rel.set(f.node.id, {x: f.cx, y: f.cy}) + const {w, h} = size(f.node) f.w = w; f.h = h; @@ -191,16 +198,17 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const right: Node[] = []; const gParams: Node[] = []; for (const p of paramsOf) { - if (p.type === 'group') gParams.push(p); else right.push(p); + if (p.type === 'group') gParams.push(p) + else right.push(p) } - right.sort((a, b) => (+(a.data as any)?.paramIndex) - (+(b.data as any)?.paramIndex)); - gParams.sort((a, b) => (+(a.data as any)?.paramIndex) - (+(b.data as any)?.paramIndex)); + right.sort((a, b) => (+(a.data as any)?.paramIndex) - (+(b.data as any)?.paramIndex)) + gParams.sort((a, b) => (+(a.data as any)?.paramIndex) - (+(b.data as any)?.paramIndex)) f.right = right; f.gParams = gParams; let total = 0; for (const p of right) total += size(p).h; - total += V * Math.max(0, right.length - 1); + total += V * Math.max(0, right.length - 1) f.py = f.cy - total / 2; f.rightBottom = f.cy + h / 2; f.rightIndex = 0; @@ -210,10 +218,10 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { case 1: { if (f.rightIndex! < f.right!.length) { const p = f.right![f.rightIndex!]; - const ps = size(p); + const ps = size(p) const px = f.cx + f.w! / 2 + H + ps.w / 2; let pcy = f.py! + ps.h / 2; - const key = colKey(px); + const key = colKey(px) const occ = columnBottom.get(key) ?? Number.NEGATIVE_INFINITY; const minTop = occ + V; const desiredTop = pcy - ps.h / 2; @@ -223,19 +231,19 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { } f.childKey = key; f.childPs = ps; - stack.push({node: p, cx: px, cy: pcy, phase: 0}); + stack.push({node: p, cx: px, cy: pcy, phase: 0}) f.phase = 10; } else { - f.bottom = Math.max(f.cy + f.h! / 2, f.rightBottom!); + f.bottom = Math.max(f.cy + f.h! / 2, f.rightBottom!) f.phase = 2; } break; } case 10: { const subBottom = f.lastChildBottom!; - columnBottom.set(f.childKey!, Math.max(columnBottom.get(f.childKey!) ?? Number.NEGATIVE_INFINITY, subBottom)); - f.rightBottom = Math.max(f.rightBottom!, subBottom); - f.py = Math.max(f.py! + f.childPs!.h + V, subBottom + V); + columnBottom.set(f.childKey!, Math.max(columnBottom.get(f.childKey!) ?? Number.NEGATIVE_INFINITY, subBottom)) + f.rightBottom = Math.max(f.rightBottom!, subBottom) + f.py = Math.max(f.py! + f.childPs!.h + V, subBottom + V) f.rightIndex!++; f.phase = 1; break; @@ -245,11 +253,11 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const gSizes: Size[] = []; let rowW = 0; for (const g of f.gParams) { - const gs = size(g); - gSizes.push(gs); + const gs = size(g) + gSizes.push(gs) rowW += gs.w; } - rowW += H * (f.gParams.length - 1); + rowW += H * (f.gParams.length - 1) f.gSizes = gSizes; f.gx = f.cx - rowW / 2; f.gy = f.bottom! + V; @@ -268,7 +276,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const gcx = f.gx! + gs.w / 2; const gcy = f.gy! + gs.h / 2; f.gx! += gs.w + H; - stack.push({node: g, cx: gcx, cy: gcy, phase: 0}); + stack.push({node: g, cx: gcx, cy: gcy, phase: 0}) f.childPs = gs; f.phase = 30; } else { @@ -279,7 +287,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { } case 30: { const subBottom = f.lastChildBottom!; - f.rowBottom = Math.max(f.rowBottom!, subBottom); + f.rowBottom = Math.max(f.rowBottom!, subBottom) f.gIndex!++; f.phase = 3; break; @@ -289,7 +297,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const kidsAll = rfKids.get(f.node.id) ?? []; const kids: Node[] = []; for (const k of kidsAll) { - if (!(k.data as any)?.linkingId) kids.push(k); + if (!(k.data as any)?.linkingId) kids.push(k) } f.kids = kids; f.kidIndex = 0; @@ -303,14 +311,14 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { case 5: { if (f.kidIndex! < f.kids!.length) { const k = f.kids![f.kidIndex!]; - const ks = size(k); + const ks = size(k) const ky = f.curY! + ks.h / 2; - stack.push({node: k, cx: f.cx, cy: ky, phase: 0}); + stack.push({node: k, cx: f.cx, cy: ky, phase: 0}) f.childPs = ks; f.phase = 50; } else { const contentBottom = f.curY! - V; - f.bottom = Math.max(f.bottom!, contentBottom + PAD); + f.bottom = Math.max(f.bottom!, contentBottom + PAD) f.phase = 6; } break; @@ -335,26 +343,26 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { } return returnBottom; - }; + } /* Root-Nodes untereinander stapeln (nur echte Roots, keine Param-Nodes) */ let yCursor = 0; for (const r of work) { if (!(r.data as any)?.linkingId && !r.parentId) { - const b = layoutIter(r, 0, yCursor + size(r).h / 2); + const b = layoutIter(r, 0, yCursor + size(r).h / 2) yCursor = b + V; } } /* ---------- rel (Center) → abs (Top-Left) ----------------------------- */ - const absCenter = new Map(); - for (const n of work) absCenter.set(n.id, rel.get(n.id)!); + const absCenter = new Map() + for (const n of work) absCenter.set(n.id, rel.get(n.id)!) - const absTL_initial = new Map(); + const absTL_initial = new Map() for (const n of work) { - const {w, h} = size(n); + const {w, h} = size(n) const {x, y} = absCenter.get(n.id)!; - absTL_initial.set(n.id, {x: x - w / 2, y: y - h / 2}); + absTL_initial.set(n.id, {x: x - w / 2, y: y - h / 2}) } /* ---------- positions in RF-Koordinaten (Top-Left), ggf. relativ zu Parent */ @@ -367,39 +375,39 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { px -= pTL.x; py -= pTL.y; } - n.position = {x: px, y: py}; + n.position = {x: px, y: py} } - const posById = new Map(); - for (const n of work) posById.set(n.id, n); + const posById = new Map() + for (const n of work) posById.set(n.id, n) /* ---------- Bounding-Korrektur jeder Group ----------------------------- */ const depth = (g: Node) => { let d = 0, p: Node | undefined = g; while (p?.parentId) { d++; - p = posById.get(p.parentId); + p = posById.get(p.parentId) if (!p) break; } return d; - }; + } const groups: Node[] = []; for (const n of work) { - if (n.type === 'group') groups.push(n); + if (n.type === 'group') groups.push(n) } - groups.sort((a, b) => depth(b) - depth(a)); + groups.sort((a, b) => depth(b) - depth(a)) for (const g of groups) { const direct: Node[] = []; for (const k of work) { - if (k.parentId === g.id) direct.push(k); + if (k.parentId === g.id) direct.push(k) } if (!direct.length) { const gw = typeof g.style?.width === 'number' ? g.style.width : 2 * PAD; const gh = typeof g.style?.height === 'number' ? g.style.height : 2 * PAD; - g.style = {...(g.style as React.CSSProperties), width: gw, height: gh}; + g.style = {...(g.style as React.CSSProperties), width: gw, height: gh} continue; } @@ -407,8 +415,8 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const sw = typeof n.style?.width === 'number' ? n.style.width : undefined; const sh = typeof n.style?.height === 'number' ? n.style.height : undefined; const s = baseSizes.get(n.id)!; - return {w: sw ?? s.w, h: sh ?? s.h}; - }; + return {w: sw ?? s.w, h: sh ?? s.h} + } let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, @@ -416,7 +424,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { maxY = Number.NEGATIVE_INFINITY; for (const k of direct) { - const ks = childSize(k); + const ks = childSize(k) if (k.position.x < minX) minX = k.position.x; if (k.position.y < minY) minY = k.position.y; if (k.position.x + ks.w > maxX) maxX = k.position.x + ks.w; @@ -444,47 +452,47 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { changed = true; } - g.measured = {width: newW, height: newH}; - g.style = {...(g.style as React.CSSProperties), width: newW, height: newH}; - baseSizes.set(g.id, {w: newW, h: newH}); + g.measured = {width: newW, height: newH} + g.style = {...(g.style as React.CSSProperties), width: newW, height: newH} + baseSizes.set(g.id, {w: newW, h: newH}) } /* ---------- Param-Group-Row nach Bounding sauber zentrieren ----------- */ // WICHTIG: Größen-Cache invalidieren, da Group-Styles sich geändert haben - sizeCache.clear(); - for (const n of work) size(n); + sizeCache.clear() + for (const n of work) size(n) // Globale Center bleiben in rel; aber Top-Left muss mit NEUEN Größen berechnet werden - const absTL = new Map(); - const absCenterAfter = new Map(); + const absTL = new Map() + const absCenterAfter = new Map() for (const n of work) { - const s = size(n); + const s = size(n) const c = rel.get(n.id)!; // globales Center aus dem Layout-Durchlauf - absCenterAfter.set(n.id, c); - absTL.set(n.id, {x: c.x - s.w / 2, y: c.y - s.h / 2}); + absCenterAfter.set(n.id, c) + absTL.set(n.id, {x: c.x - s.w / 2, y: c.y - s.h / 2}) } for (const parent of work) { const pGroups: Node[] = []; const paramList = params.get(parent.id) ?? []; for (const p of paramList) { - if (p.type === 'group') pGroups.push(p); + if (p.type === 'group') pGroups.push(p) } if (!pGroups.length) continue; const ordered = pGroups.slice().sort((a, b) => (+((a.data as any)?.paramIndex) || 0) - (+((b.data as any)?.paramIndex) || 0) - ); + ) const widths: number[] = []; for (const g of ordered) { const gn = posById.get(g.id)!; const sw = typeof gn.style?.width === 'number' ? gn.style.width : undefined; - widths.push(sw ?? size(gn).w); + widths.push(sw ?? size(gn).w) } let rowW = 0; for (const w of widths) rowW += w; - rowW += H * (ordered.length - 1); + rowW += H * (ordered.length - 1) const pCenterX = absCenterAfter.get(parent.id)!.x; let gx = pCenterX - rowW / 2; @@ -492,20 +500,22 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { for (let i = 0; i < ordered.length; i++) { const g = ordered[i]; const gn = posById.get(g.id)!; - const containerTL = gn.parentId ? absTL.get(gn.parentId)! : {x: 0, y: 0}; + const containerTL = gn.parentId ? absTL.get(gn.parentId)! : {x: 0, y: 0} gn.position.x = gx - containerTL.x; gx += widths[i] + H; } } - } while (changed && pass < 5); + } while (changed && pass < 5) - return {nodes: work}; + return {nodes: work} } const getCachedLayoutElements = React.cache(getLayoutElements) -export type DFlowProps = Code0ComponentProps & ReactFlowProps +export interface DFlowProps extends Code0ComponentProps { + flowId: Flow['id'] +} export const DFlow: React.FC = (props) => { return @@ -514,45 +524,47 @@ export const DFlow: React.FC = (props) => { } const InternalDFlow: React.FC = (props) => { - const [nodes, setNodes] = useNodesState(props.nodes!!) - const [edges, setEdges, edgeChangeEvent] = useEdgesState(props.edges!!) - const updateNodeInternals = useUpdateNodeInternals() - - const revalidateHandles = React.useCallback((ids: string[]) => { - requestAnimationFrame(() => { - ids.forEach(id => updateNodeInternals(id)); - }); - }, [updateNodeInternals]) + const {flowId} = props const nodeTypes = { default: DFlowFunctionDefaultCard, group: DFlowFunctionGroupCard, suggestion: DFlowFunctionSuggestionCard, trigger: DFlowFunctionTriggerCard, - ...props.nodeTypes } const edgeTypes = { default: DFlowEdge, - ...props.edgeTypes } + const initialNodes = useFlowNodes(flowId) + const initialEdges = useFlowEdges(flowId) + const [nodes, setNodes] = useNodesState([]) + const [edges, setEdges, edgeChangeEvent] = useEdgesState([]) + const updateNodeInternals = useUpdateNodeInternals() + + const revalidateHandles = React.useCallback((ids: string[]) => { + requestAnimationFrame(() => { + ids.forEach(id => updateNodeInternals(id)) + }) + }, [updateNodeInternals]) + const nodeChangeEvent = React.useCallback((changes: any) => { const changedIds: string[] = Array.from(new Set( changes .filter((c: any) => c.type === 'dimensions' || c.type === 'position') .map((c: any) => c.id) - )); + )) - const dimensionMap = new Map(); + const dimensionMap = new Map() changes .filter((c: any) => c.type === 'dimensions') - .forEach((c: any) => dimensionMap.set(c.id, c.dimensions)); + .forEach((c: any) => dimensionMap.set(c.id, c.dimensions)) setNodes(prevNodes => { const localNodes = prevNodes.map(node => { if (!dimensionMap.has(node.id)) return node; - const dims = dimensionMap.get(node.id) || {}; + const dims = dimensionMap.get(node.id) || {} return { ...node, measured: { @@ -560,17 +572,17 @@ const InternalDFlow: React.FC = (props) => { height: dims.height ?? node.measured?.height ?? 0, } } as Node; - }); + }) - const layouted = getCachedLayoutElements(localNodes, new Set(changedIds)); + const layouted = getCachedLayoutElements(localNodes, new Set(changedIds)) return layouted.nodes as Node[]; - }); + }) - revalidateHandles(changedIds); - }, [revalidateHandles]); + revalidateHandles(changedIds) + }, [revalidateHandles]) React.useEffect(() => { - const localNodes = props.nodes!!.map(value => { + const localNodes = initialNodes.map(value => { const nodeEls = !value.measured ? document.querySelectorAll("[data-id='" + value.id + "']") : []; return { ...value, @@ -578,16 +590,16 @@ const InternalDFlow: React.FC = (props) => { width: value.measured?.width ?? (nodeEls[0] as any)?.clientWidth ?? 0, height: value.measured?.height ?? (nodeEls[0] as any)?.clientHeight ?? 0, } - } as Node; - }); + } as unknown as Node + }) const layouted = getCachedLayoutElements(localNodes, new Set(localNodes.map(n => n.id))) setNodes(layouted.nodes as Node[]) - setEdges(props.edges as Edge[]) + setEdges(initialEdges as Edge[]) revalidateHandles((layouted.nodes as Node[]).map(n => n.id)) - }, [props.nodes, props.edges, revalidateHandles]) + }, [initialNodes, initialEdges, revalidateHandles]) return ( = (props) => { onInit={(rf) => rf.fitView()} onNodesChange={nodeChangeEvent} onEdgesChange={edgeChangeEvent} + fitView {...mergeCode0Props("flow", props)} nodes={nodes} edges={edges} - /> + > + + + + + + ) } \ No newline at end of file diff --git a/src/components/d-flow/DFlow.view.ts b/src/components/d-flow/DFlow.view.ts deleted file mode 100644 index 74409f2d0..000000000 --- a/src/components/d-flow/DFlow.view.ts +++ /dev/null @@ -1,405 +0,0 @@ -import type { - DataType, - Flow, FlowInput, - FlowSetting, FlowSettingInput, - FlowType, FunctionDefinition, - LiteralValue, - Maybe, - NodeFunction, NodeFunctionInput, - NodeParameter, NodeParameterInput, - NodeParameterValue, - ReferenceValue, - RuntimeParameterDefinition, - Scalars -} from "@code0-tech/sagittarius-graphql-types"; -import {ValidationResult} from "../../utils"; - -export class FlowView { - - /** Time when this Flow was created */ - private readonly _createdAt?: Maybe; - /** Global ID of this Flow */ - private readonly _id?: Maybe; - /** The input data type of the flow */ - private _inputType?: Maybe; - /** Nodes of the flow */ - private _nodes?: NodeFunctionView[]; - /** The return data type of the flow */ - private readonly _returnType?: Maybe; - /** The settings of the flow */ - private readonly _settings?: FlowSettingView[]; - /** The ID of the starting node of the flow */ - private _startingNodeId?: Maybe; - /** The flow type of the flow */ - private readonly _type?: Maybe; - /** Time when this Flow was last updated */ - private readonly _updatedAt?: Maybe; - /** Name of the flow */ - private _name?: Maybe; - - constructor(flow: Flow) { - - this._createdAt = flow.createdAt - this._id = flow.id - this._inputType = flow.inputType - this._nodes = flow.nodes?.nodes?.map(node => new NodeFunctionView(node!!)) - this._returnType = flow.returnType - this._settings = flow.settings?.nodes?.map(setting => new FlowSettingView(setting!!)) - this._startingNodeId = flow.startingNodeId - this._type = flow.type - this._updatedAt = flow.updatedAt - this._name = flow.name - - } - - get createdAt(): Maybe | undefined { - return this._createdAt; - } - - get id(): Maybe | undefined { - return this._id; - } - - get inputType(): Maybe | undefined { - return this._inputType; - } - - get nodes(): NodeFunctionView[] | undefined { - return this._nodes; - } - - get returnType(): Maybe | undefined { - return this._returnType; - } - - get settings(): FlowSettingView[] | undefined { - return this._settings; - } - - get startingNodeId(): Maybe | undefined { - return this._startingNodeId; - } - - get type(): Maybe | undefined { - return this._type; - } - - get updatedAt(): Maybe | undefined { - return this._updatedAt; - } - - get name(): Maybe | undefined { - return this._name; - } - - set inputType(value: Maybe) { - this._inputType = value; - } - - set startingNodeId(value: Maybe) { - this._startingNodeId = value; - } - - set name(value: Maybe) { - this._name = value; - } - - addNode(node: NodeFunctionView): void { - if (!this._nodes) { - this._nodes = []; - } - this._nodes.push(node); - } - - updateNode(updatedNode: NodeFunctionView): void { - if (!this._nodes) { - return; - } - this._nodes = this._nodes.map(node => { - if (node.id === updatedNode.id) { - return updatedNode; - } - return node; - }); - } - - removeNode(nodeId: Scalars['NodeFunctionID']['output']): void { - if (!this._nodes) { - return; - } - this._nodes = this._nodes.filter(node => node.id !== nodeId); - } - - getNodeById(nodeId: Scalars['NodeFunctionID']['output']): NodeFunctionView | undefined { - if (!this._nodes) { - return undefined; - } - return this._nodes.find(node => node.id === nodeId); - } - - json(): Flow { - return { - __typename: "Flow", - createdAt: this._createdAt, - id: this._id, - inputType: this._inputType, - nodes: this._nodes ? { - nodes: this._nodes.map(node => node.json()!!) - } : undefined, - returnType: this._returnType, - settings: this._settings ? { - nodes: this._settings.map(setting => setting.json()!!) - } : undefined, - startingNodeId: this._startingNodeId, - type: this._type, - updatedAt: this._updatedAt, - name: this._name, - } - } - - jsonInput(): FlowInput { - return { - name: this._name, - nodes: this._nodes?.map(node => node.jsonInput()), - settings: this._settings?.map(setting => setting.jsonInput()), - startingNodeId: this._startingNodeId, - type: `gid://sagittarius/FlowType/1` - - } - } -} - -export class NodeFunctionView { - - - /** Time when this NodeFunction was created */ - private readonly _createdAt?: Maybe; - /** Global ID of this NodeFunction */ - private readonly _id?: Maybe; - /** The ID of the next Node Function in the flow */ - private _nextNodeId?: Maybe; - /** The parameters of the Node Function */ - private readonly _parameters?: NodeParameterView[]; - /** The definition of the Node Function */ - private readonly _functionDefinition?: Maybe; - /** Time when this NodeFunction was last updated */ - private readonly _updatedAt?: Maybe; - - - constructor(nodeFunction: NodeFunction) { - this._createdAt = nodeFunction.createdAt - this._id = nodeFunction.id - this._nextNodeId = nodeFunction.nextNodeId - this._functionDefinition = nodeFunction.functionDefinition - this._updatedAt = nodeFunction.updatedAt - this._parameters = nodeFunction.parameters ? nodeFunction.parameters.nodes?.map(param => new NodeParameterView(param!!)) : undefined - } - - get createdAt(): Maybe | undefined { - return this._createdAt; - } - - get id(): Maybe | undefined { - return this._id; - } - - get nextNodeId(): Maybe | undefined { - return this._nextNodeId; - } - - get parameters(): NodeParameterView[] | undefined { - return this._parameters; - } - - get functionDefinition(): Maybe | undefined { - return this._functionDefinition; - } - - get updatedAt(): Maybe | undefined { - return this._updatedAt; - } - - set nextNodeId(value: Maybe) { - this._nextNodeId = value; - } - - deleteNextNode() { - this._nextNodeId = undefined; - } - - json(): NodeFunction | undefined { - return { - __typename: "NodeFunction", - createdAt: this._createdAt, - id: this._id, - nextNodeId: this._nextNodeId, - parameters: this._parameters ? { - nodes: this._parameters.map(param => param.json()!!) - } : undefined, - functionDefinition: this._functionDefinition, - updatedAt: this._updatedAt, - } - } - - jsonInput(): NodeFunctionInput { - return { - nextNodeId: this._nextNodeId, - id: this._id, - parameters: this._parameters ? this._parameters.map(param => param.jsonInput()) : undefined, - runtimeFunctionId: this._functionDefinition?.runtimeFunctionDefinition?.id - } - } -} - -export class NodeParameterView { - - /** Time when this NodeParameter was created */ - private readonly _createdAt?: Maybe; - /** Global ID of this NodeParameter */ - private readonly _id?: Maybe; - /** The definition of the parameter */ - private readonly _runtimeParameter?: Maybe; - /** Time when this NodeParameter was last updated */ - private readonly _updatedAt?: Maybe; - /** The value of the parameter */ - private _value?: LiteralValue | ReferenceValue | NodeFunctionView; - - private _validationResults: ValidationResult[] - - constructor(nodeParameter: NodeParameter) { - this._createdAt = nodeParameter.createdAt - this._id = nodeParameter.id - this._runtimeParameter = nodeParameter.runtimeParameter - this._updatedAt = nodeParameter.updatedAt - if (nodeParameter.value?.__typename === "NodeFunction") { - this._value = new NodeFunctionView(nodeParameter.value as NodeFunction); - } else { - this._value = nodeParameter.value as LiteralValue | ReferenceValue; - } - this._validationResults = [] - - } - - get createdAt(): Maybe | undefined { - return this._createdAt; - } - - get id(): Maybe | undefined { - return this._id; - } - - get runtimeParameter(): Maybe | undefined { - return this._runtimeParameter; - } - - get updatedAt(): Maybe | undefined { - return this._updatedAt; - } - - get value(): LiteralValue | ReferenceValue | NodeFunctionView | undefined { - return this._value; - } - - get validationResults(): ValidationResult[] { - return this._validationResults; - } - - set validationResults(value: ValidationResult[]) { - this._validationResults = value; - } - - set value(value: NodeParameterValue | undefined) { - if (value?.__typename === "NodeFunction") { - this._value = new NodeFunctionView(value as NodeFunction); - } else { - this._value = value as LiteralValue | ReferenceValue; - } - } - - json(): NodeParameter | undefined { - return { - __typename: "NodeParameter", - createdAt: this._createdAt, - id: this._id, - runtimeParameter: this._runtimeParameter, - updatedAt: this._updatedAt, - value: this._value instanceof NodeFunctionView ? this._value.json() : this._value, - } - } - - jsonInput(): NodeParameterInput { - return { - value: this._value instanceof NodeFunctionView ? { - functionValue: this._value.jsonInput() - } : this._value?.__typename === "ReferenceValue" ? { - referenceValue: this._value as ReferenceValue - } : { - literalValue: this._value as LiteralValue - }, - runtimeParameterDefinitionId: this.runtimeParameter?.id - } - } -} - -export class FlowSettingView { - - private readonly _createdAt?: Maybe; - /** The identifier of the flow setting */ - private readonly _flowSettingIdentifier?: Maybe; - /** Global ID of this FlowSetting */ - private readonly _id?: Maybe; - /** Time when this FlowSetting was last updated */ - private readonly _updatedAt?: Maybe; - /** The value of the flow setting */ - private _value?: Maybe; - - constructor(flowSetting: FlowSetting) { - this._createdAt = flowSetting.createdAt - this._flowSettingIdentifier = flowSetting.flowSettingIdentifier - this._id = flowSetting.id - this._value = flowSetting.value - this._updatedAt = flowSetting.updatedAt - } - - get createdAt(): Maybe | undefined { - return this._createdAt; - } - - get flowSettingIdentifier(): Maybe | undefined { - return this._flowSettingIdentifier; - } - - get id(): Maybe | undefined { - return this._id; - } - - get value(): Maybe | undefined { - return this._value; - } - - get updatedAt(): Maybe | undefined { - return this._updatedAt; - } - - set value(value: Maybe) { - this._value = value; - } - - json(): FlowSetting { - return { - __typename: "FlowSetting", - createdAt: this._createdAt, - flowSettingIdentifier: this._flowSettingIdentifier, - id: this._id, - value: this._value, - updatedAt: this._updatedAt - } - } - - jsonInput(): FlowSettingInput { - return { - value: this.value, - flowSettingIdentifier: this.flowSettingIdentifier - } - } -} \ No newline at end of file diff --git a/src/components/d-flow/DFlowEdge.tsx b/src/components/d-flow/DFlowEdge.tsx new file mode 100644 index 000000000..f16687169 --- /dev/null +++ b/src/components/d-flow/DFlowEdge.tsx @@ -0,0 +1,88 @@ +import {Code0Component} from "../../utils"; +import {BaseEdge, Edge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, Position} from "@xyflow/react"; +import React, {memo} from "react"; +import {Badge} from "../badge/Badge"; +import {Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; + +export interface DFlowEdgeDataProps extends Code0Component { + //some data we will use + color?: string + type: 'parameter' | 'suggestion' | 'group' | 'default' + parentNodeId?: NodeFunction['id'] | null + flowId: Flow['id'] +} + +// @ts-ignore +export type DFlowEdgeProps = EdgeProps> + +export const DFlowEdge: React.FC = memo((props) => { + const { + sourceX, + sourceY, + targetX, + targetY, + id, + data, + label, + style, + ...rest + } = props + + const [edgePath, labelX, labelY] = getSmoothStepPath({ + sourceX, + sourceY, + sourcePosition: data?.type === "parameter" ? Position.Left : Position.Bottom, + targetX, + targetY, + targetPosition: data?.type === "parameter" ? Position.Right : Position.Top, + borderRadius: 16, + stepPosition: 0.5, + }) + const color = data?.color ?? "#ffffff" + const gradientId = `dflow-edge-gradient-${id}` + + return ( + <> + {/* Gradient-Definition für genau diese Edge */} + + + {/* Start: volle Farbe */} + + {/* Ende: gleiche Farbe, aber transparent */} + + + + + + + {label ? ( + +
+ + {label} + +
+
+ ) : null} + + ) +}) \ No newline at end of file diff --git a/src/components/d-flow/control/index.ts b/src/components/d-flow/control/index.ts deleted file mode 100644 index 4b6220afc..000000000 --- a/src/components/d-flow/control/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./DFlowControl" \ No newline at end of file diff --git a/src/components/d-flow/edge/DFlowEdge.tsx b/src/components/d-flow/edge/DFlowEdge.tsx deleted file mode 100644 index b42109152..000000000 --- a/src/components/d-flow/edge/DFlowEdge.tsx +++ /dev/null @@ -1,50 +0,0 @@ -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 DFlowEdgeDataProps extends Code0Component { - //some data we will use - color?: string - isParameter?: boolean - isSuggestion?: boolean -} - -// @ts-ignore -export type DFlowEdgeProps = EdgeProps> - -export const DFlowEdge: 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/folder/DFlowFolder.tsx b/src/components/d-flow/folder/DFlowFolder.tsx deleted file mode 100644 index 3ac4ef9a0..000000000 --- a/src/components/d-flow/folder/DFlowFolder.tsx +++ /dev/null @@ -1,184 +0,0 @@ -"use client" - -import "./DFlowFolder.style.scss" -import React from "react"; -import {Code0Component} from "../../../utils/types"; -import {mergeCode0Props} from "../../../utils/utils"; -import {IconChevronDown, IconChevronRight, IconFolder} from "@tabler/icons-react"; -import type {Scalars} from "@code0-tech/sagittarius-graphql-types"; -import {useService, useStore} from "../../../utils/contextStore"; -import {DFlowReactiveService} from "../DFlow.service"; -import {ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "../../scroll-area/ScrollArea"; -import {FlowView} from "../DFlow.view"; - - -export interface DFlowFolderProps { - flowId: Scalars["FlowID"]["output"] -} - -export interface DFlowFolderGroupProps extends Omit, "controls"> { - name: string - children: React.ReactElement | React.ReactElement[] | React.ReactElement | React.ReactElement[] - //defaults to false - defaultOpen?: boolean -} - -export interface DFlowFolderItemProps extends Code0Component { - name: string - icon?: React.ReactNode - //defaults to false - active?: boolean -} - -export const DFlowFolder: React.FC = (props) => { - - const { flowId } = props; - - const flowService = useService(DFlowReactiveService); - const flowStore = useStore(DFlowReactiveService) - - type TreeNode = { - name: string; - path: string; - children: Record; - flow?: FlowView; - }; - - const normalizePath = (p: string) => - p.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean); - - const flows = React.useMemo(() => { - const raw = (flowService.values?.() ?? []) as FlowView[]; - return raw.filter(f => !!f?.name); - }, [flowStore]); - - const activePathSegments = React.useMemo(() => { - const active = flows.find(f => f.id === flowId); - if (!active?.name) return []; - return normalizePath(active.name); - }, [flows, flowId]); - - const tree = React.useMemo(() => { - const root: TreeNode = { name: "", path: "", children: {} }; - for (const flow of flows) { - const segs = normalizePath(flow.name as string); - if (segs.length === 0) continue; - - let cur = root; - let acc = ""; - for (let i = 0; i < segs.length; i++) { - const seg = segs[i]; - acc = acc ? `${acc}/${seg}` : seg; - - if (i === segs.length - 1) { - // leaf (Flow) - if (!cur.children[seg]) { - cur.children[seg] = { - name: seg, - path: acc, - children: {}, - flow - }; - } else { - // falls es bereits einen Knoten gibt, hänge Flow an - cur.children[seg].flow = flow; - } - } else { - // folder - if (!cur.children[seg]) { - cur.children[seg] = { - name: seg, - path: acc, - children: {} - }; - } - cur = cur.children[seg]; - } - } - } - return root; - }, [flows]); - - const isPrefixOfActive = React.useCallback((nodePath: string) => { - if (!nodePath) return false; - const segs = nodePath.split("/").filter(Boolean); - return segs.every((s, i) => activePathSegments[i] === s); - }, [activePathSegments]); - - const renderChildren = React.useCallback((childrenMap: Record) => { - const nodes = Object.values(childrenMap); - - const folders = nodes.filter(n => !n.flow); - const items = nodes.filter(n => !!n.flow); - - folders.sort((a, b) => a.name.localeCompare(b.name)); - items.sort((a, b) => a.name.localeCompare(b.name)); - - return ( - <> - {folders.map(folder => ( - - {renderChildren(folder.children)} - - ))} - {items.map(item => ( - - ))} - - ); - }, [flowId, isPrefixOfActive]); - - return ( - - -
- {renderChildren(tree.children)} -
-
- - - -
- ) - -} - -export const DFlowFolderGroup: React.FC = (props) => { - - const {name, defaultOpen = false, children, ...rest} = props - const [open, setOpen] = React.useState(defaultOpen); - - return
-
setOpen(prevState => !prevState)} {...mergeCode0Props(`d-folder`, rest)}> - setOpen(prevState => !prevState)} className={"d-folder__status"}> - {open ? : } - - - {name} -
-
- {open ? children : null} -
-
-} - -export const DFlowFolderItem: React.FC = (props) => { - - const {name, icon, active, ...rest} = props - - return
- {icon? {icon} : null} - {name} -
-} - diff --git a/src/components/d-flow/function/DFlowFunction.vaildation.hook.ts b/src/components/d-flow/function/DFlowFunction.vaildation.hook.ts deleted file mode 100644 index b72e1d307..000000000 --- a/src/components/d-flow/function/DFlowFunction.vaildation.hook.ts +++ /dev/null @@ -1,160 +0,0 @@ -import {FunctionDefinitionView} from "./DFlowFunction.view"; -import {DataTypeView} from "../data-type/DFlowDataType.view"; -import {DFlowDataTypeReactiveService} from "../data-type/DFlowDataType.service"; -import {InspectionSeverity, ValidationResult} from "../../../utils/inspection"; -import { - replaceGenericKeysInDataTypeObject, - replaceGenericKeysInType, - resolveGenericKeys -} from "../../../utils/generics"; -import {useReturnType} from "./DFlowFunction.return.hook"; -import {useService} from "../../../utils/contextStore"; -import {DFlowFunctionReactiveService} from "./DFlowFunction.service"; -import type {DataTypeVariant, Maybe, NodeFunction, NodeParameterValue, Scalars} from "@code0-tech/sagittarius-graphql-types"; -import {useValidateDataType} from "../data-type/DFlowDataType.validation.type"; -import {useValidateValue} from "../data-type/DFlowDataType.validation.value"; -import {DFlowReactiveService} from "../DFlow.service"; - - -/** - * Validates function parameter values against a function definition, resolving all generics. - * For each parameter, determines if the provided value is a valid match for the parameter's (possibly generic) type. - * Returns an array of ValidationResults (errors for each parameter, null entry for valid). - */ -export const useFunctionValidation = ( - func: FunctionDefinitionView, - values: NodeParameterValue[], - dataTypeService: DFlowDataTypeReactiveService, - flowId: Scalars['FlowID']['output'] -): ValidationResult[] | null => { - const functionService = useService(DFlowFunctionReactiveService) - const flowService = useService(DFlowReactiveService) - const flow = flowService.getById(flowId) - const genericTypeMap = resolveGenericKeys(func, values, dataTypeService, flow) - const parameters = func.parameterDefinitions ?? [] - const genericKeys = func.genericKeys ?? [] - const errors: ValidationResult[] = []; - - parameters.forEach((parameter, index) => { - const value = values[index] - if (!value) return; - const parameterType = parameter.dataTypeIdentifier - const parameterDataType = dataTypeService.getDataType(parameterType!!) - const valueType = value.__typename === "NodeFunction" && parameterDataType?.variant != "NODE" ? useReturnType(functionService.getById((value as NodeFunction).functionDefinition?.id!!)!!, (value as NodeFunction).parameters?.nodes?.map(p => p?.value!!)!!) : dataTypeService.getTypeFromValue(value, flow); - const valueDataType = dataTypeService.getDataType(valueType!!) - - // Check if the parameter is generic (by key or by structure) - const isParameterGeneric = (parameterDataType && parameterType?.genericType) || (parameterType?.genericKey && genericKeys.includes(parameterType.genericKey)) - - let isValid = true - - if (isParameterGeneric) { - if (valueType?.genericType && parameterDataType) { - if (value.__typename === "ReferenceValue" || value.__typename === "NodeFunction") { - const resolvedParameterDT = new DataTypeView( - replaceGenericKeysInDataTypeObject(parameterDataType.json!!, genericTypeMap) - ); - const resolvedValueDT = new DataTypeView( - replaceGenericKeysInDataTypeObject(valueDataType?.json!, genericTypeMap) - ); - - isValid = useValidateDataType(resolvedParameterDT, resolvedValueDT) - if (!isValid) { - errors.push(errorResult(parameter.id!!, parameterDataType, valueDataType)); - } - } else { - const replacedGenericType = replaceGenericKeysInType(parameterType, genericTypeMap) - - isValid = useValidateValue(value, parameterDataType, flow, replacedGenericType?.genericType?.genericMappers!!) - if (!isValid) { - errors.push(errorResult(parameter.id!!, parameterDataType, valueDataType)); - } - } - return; - } - if (parameterType?.genericKey && genericKeys.includes(parameterType?.genericKey)) { - if (value.__typename != "ReferenceValue") { - const replacedGenericType = replaceGenericKeysInType(parameterType, genericTypeMap) - isValid = useValidateValue(value, dataTypeService.getDataType(replacedGenericType)!!, flow, replacedGenericType.genericType?.genericMappers!!) - if (!isValid) { - errors.push(errorResult(parameter.id!!, parameterDataType, valueDataType)); - } - } - return; - } - if (valueDataType && parameterDataType && valueDataType.json && parameterDataType.json) { - if (value.__typename === "ReferenceValue" || value.__typename === "NodeFunction") { - const resolvedParameterDT = new DataTypeView( - replaceGenericKeysInDataTypeObject(parameterDataType.json, genericTypeMap) - ); - isValid = useValidateDataType(resolvedParameterDT, valueDataType) - if (!isValid) { - errors.push(errorResult(parameter.id!!, parameterDataType, valueDataType)); - } - } else { - const replacedGenericType = replaceGenericKeysInType(parameterType, genericTypeMap); - isValid = useValidateValue(value, dataTypeService.getDataType(replacedGenericType)!!, flow, replacedGenericType.genericType?.genericMappers!!) - if (!isValid) { - errors.push(errorResult(parameter.id!!, parameterDataType, valueDataType)); - } - } - return; - } - } - - // Non-generic parameter validation - if (parameterDataType) { - if (valueType?.genericType && parameterDataType) { - if (value.__typename === "ReferenceValue" || value.__typename === "NodeFunction") { - const resolvedValueDT = new DataTypeView( - replaceGenericKeysInDataTypeObject(valueDataType?.json!, genericTypeMap) - ); - isValid = useValidateDataType(parameterDataType, resolvedValueDT) - if (!isValid) { - errors.push(errorResult(parameter.id!!, parameterDataType, valueDataType)); - } - } else { - isValid = useValidateValue(value, parameterDataType) - if (!isValid) { - errors.push(errorResult(parameter.id!!, parameterDataType, valueDataType)); - } - } - return; - } - - if (valueDataType) { - if ((value.__typename === "ReferenceValue" || value.__typename === "NodeFunction") && parameterDataType.variant !== "NODE") { - isValid = useValidateDataType(parameterDataType, valueDataType) - if (!isValid) { - errors.push(errorResult(parameter.id!!, parameterDataType, valueDataType)); - } - } else { - isValid = useValidateValue(value, parameterDataType) - if (!isValid) { - errors.push(errorResult(parameter.id!!, parameterDataType, valueDataType)); - } - } - return; - } - } - // If nothing matches, treat as invalid - errors.push(errorResult(parameter.id!!, parameterDataType, valueDataType)); - }); - - return errors.length > 0 ? errors : null; -}; - -const errorResult = ( - parameterId: Maybe, - expectedType?: DataTypeView, - actualType?: DataTypeView, -): ValidationResult => ({ - parameterId, - type: InspectionSeverity.ERROR, - message: { - nodes: [{ - code: "en-US", - content: `Argument of type ${actualType?.name?.nodes!![0]?.content} is not assignable to parameter of type ${expectedType?.name?.nodes!![0]?.content}` - }] - } -}) \ No newline at end of file diff --git a/src/components/d-flow/function/DFlowFunctionDefaultCard.tsx b/src/components/d-flow/function/DFlowFunctionDefaultCard.tsx deleted file mode 100644 index babbbee96..000000000 --- a/src/components/d-flow/function/DFlowFunctionDefaultCard.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import {Code0Component} from "../../../utils/types"; -import {Handle, Node, NodeProps, Position, useReactFlow, useStore, useStoreApi} from "@xyflow/react"; -import {NodeFunctionView, NodeParameterView} from "../DFlow.view"; -import React, {memo} from "react"; -import {Card} from "../../card/Card"; -import "./DFlowFunctionDefaultCard.style.scss"; -import CardSection from "../../card/CardSection"; -import {Flex} from "../../flex/Flex"; -import { - IconAlertTriangle, - IconArrowRightCircle, IconChevronDown, IconChevronRight, 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 "./DFlowFunction.service"; -import {useFunctionValidation} from "./DFlowFunction.vaildation.hook"; -import {DFlowDataTypeReactiveService} from "../data-type/DFlowDataType.service"; -import {InspectionSeverity} from "../../../utils/inspection"; -import {DFlowReactiveService} from "../DFlow.service"; -import {DFlowSuggestionMenu} from "../suggestion/DFlowSuggestionMenu"; -import {useSuggestions} from "../suggestion/DFlowSuggestion.hook"; -import {FileTabsService} from "../../file-tabs/FileTabs.service"; -import {DFlowTabDefault} from "../tab/DFlowTabDefault"; -import type {DataTypeVariant, Maybe, Scalars} from "@code0-tech/sagittarius-graphql-types"; - -export interface DFlowFunctionDefaultCardDataProps extends Omit, "scope"> { - instance: NodeFunctionView - flowId: Scalars["FlowID"]["output"] - isParameter: boolean - depth: number - scope: number[] - index: number -} - -// @ts-ignore -export type DFlowFunctionDefaultCardProps = NodeProps> - -export const DFlowFunctionDefaultCard: 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.getById(data.instance.functionDefinition?.id!!) - //TODO: some problems with react memorization here, need to investigate and also with hook calling - const validation = useFunctionValidation(definition!!, data.instance.parameters!!.map(p => p.value!! instanceof NodeFunctionView ? p.value.json()!! : p.value!!), dataTypeService!!, props.data.flowId) - const edges = useStore(s => s.edges); - const width = props.width ?? 0 - const height = props.height ?? 0 - - data.instance.parameters?.forEach(parameter => { - const parameterDefinition = definition?.parameterDefinitions!!.find(p => p.id == parameter?.id) - parameter.validationResults = validation ? validation.filter(v => v.parameterId === parameterDefinition?.id) : [] - }) - - // Helper, ob zu diesem Parameter eine Edge existiert: - function isParamConnected(paramId: Maybe): 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" : "primary"} - 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: {definition?.names?.nodes!![0]?.content}, - content: - }) - }} style={{position: "relative"}}> - - - - - - {definition?.names?.nodes!![0]?.content} - - - { - 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?.parameterDefinitions!!.find(p => p.id == param.id) - const isNodeDataType = dataTypeService.getDataType(parameter?.dataTypeIdentifier!!)?.variant === "NODE"; - return (param.value instanceof NodeFunctionView && !isNodeDataType) || (!param.value) - }) ? ( - - {/* Dynamische Parameter-Eingänge (rechts), nur wenn wirklich verbunden */} - {data.instance.parameters?.map((param: NodeParameterView, index: number) => { - - - const parameter = definition?.parameterDefinitions!!.find(p => p.id == param.id) - const isNodeDataType = dataTypeService.getDataType(parameter?.dataTypeIdentifier!!)?.variant === "NODE"; - const result = useSuggestions(parameter?.dataTypeIdentifier ?? undefined, [], props.data.flowId, data.depth, data.scope, data.index) - - return (param.value instanceof NodeFunctionView && !isNodeDataType) || (!param.value) ? - - - {parameter?.names?.nodes!![0]?.content ?? 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/function/DFlowFunctionGroupCard.tsx b/src/components/d-flow/function/DFlowFunctionGroupCard.tsx deleted file mode 100644 index c2386e93d..000000000 --- a/src/components/d-flow/function/DFlowFunctionGroupCard.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, {memo} from "react"; -import {Handle, NodeProps, Position, useStore} from "@xyflow/react"; -import {FLOW_EDGE_RAINBOW} from "../DFlow.edges.hook"; -import {Card} from "../../card/Card"; - -export interface DFlowFunctionGroupCardProps extends NodeProps { -} - -export const DFlowFunctionGroupCard: 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/index.ts b/src/components/d-flow/index.ts index 3262b3d87..fb1e4db53 100644 --- a/src/components/d-flow/index.ts +++ b/src/components/d-flow/index.ts @@ -1,14 +1,4 @@ -export * from "./DFlow.view" +export * from "./DFlow" export * from "./DFlow.service" export * from "./DFlow.edges.hook" export * from "./DFlow.nodes.hook" -export * from "./DFlow" - -export * from "./control/index" -export * from "./data-type/index" -export * from "./folder/index" -export * from "./function/index" -export * from "./minimap/index" -export * from "./suggestion/index" -export * from "./type/index" -export * from "./validation/index" diff --git a/src/components/d-flow/minimap/DFlowMiniMap.tsx b/src/components/d-flow/minimap/DFlowMiniMap.tsx deleted file mode 100644 index 6560c81d6..000000000 --- a/src/components/d-flow/minimap/DFlowMiniMap.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from "react"; -import {MiniMap, useNodes} from "@xyflow/react"; -import {FLOW_EDGE_RAINBOW} from "../DFlow.edges.hook"; -import "./DFlowMiniMap.style.scss" - -export const DFlowMiniMap: 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/minimap/index.ts b/src/components/d-flow/minimap/index.ts deleted file mode 100644 index 3424a8552..000000000 --- a/src/components/d-flow/minimap/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./DFlowMiniMap" \ No newline at end of file diff --git a/src/components/d-flow/suggestion/DFlowSuggestion.service.ts b/src/components/d-flow/suggestion/DFlowSuggestion.service.ts deleted file mode 100644 index cf8f6931e..000000000 --- a/src/components/d-flow/suggestion/DFlowSuggestion.service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {ReactiveArrayService} from "../../../utils/reactiveArrayService"; -import {DFlowSuggestion} from "./DFlowSuggestion.view"; - -export abstract class DFlowReactiveSuggestionService extends ReactiveArrayService { - - - //get all suggestions with matching hash - public getSuggestionsByHash(hash: string): DFlowSuggestion[] { - const seen = new Set(); - return this.values() - .filter(suggestion => suggestion.hash === hash) - .filter(suggestion => { - const key = JSON.stringify({ - path: suggestion.path, - value: suggestion.value, - type: suggestion.type - }); - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - } -} \ No newline at end of file diff --git a/src/components/d-flow/suggestion/DFlowSuggestion.view.ts b/src/components/d-flow/suggestion/DFlowSuggestion.view.ts deleted file mode 100644 index 048fe7d4a..000000000 --- a/src/components/d-flow/suggestion/DFlowSuggestion.view.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type {NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; - -export enum DFlowSuggestionType { - REF_OBJECT, - VALUE, - FUNCTION, - FUNCTION_COMBINATION, - DATA_TYPE, -} - -export class DFlowSuggestion { - - public constructor( - private readonly _hash: string, - private readonly _path: number[], - private readonly _value: NodeParameterValue, - private readonly _type: DFlowSuggestionType, - private readonly _displayText: string[] - ){} - - get displayText(): string[] { - return this._displayText; - } - - get hash(): string { - return this._hash; - } - - get path(): number[] { - return this._path; - } - - get value(): NodeParameterValue { - return this._value; - } - - get type(): DFlowSuggestionType { - return this._type; - } -} \ No newline at end of file diff --git a/src/components/d-flow/suggestion/index.ts b/src/components/d-flow/suggestion/index.ts deleted file mode 100644 index 0e8be2302..000000000 --- a/src/components/d-flow/suggestion/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./DFlowSuggestion.service" -export * from "./DFlowSuggestion.view" \ No newline at end of file diff --git a/src/components/d-flow/tab/DFlowTabs.tsx b/src/components/d-flow/tab/DFlowTabs.tsx deleted file mode 100644 index c13c173a8..000000000 --- a/src/components/d-flow/tab/DFlowTabs.tsx +++ /dev/null @@ -1,110 +0,0 @@ -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 DFlowTabs = () => { - - 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/validation/DFlowValidation.hook.ts b/src/components/d-flow/validation/DFlowValidation.hook.ts deleted file mode 100644 index e99245edd..000000000 --- a/src/components/d-flow/validation/DFlowValidation.hook.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {ValidationResult} from "../../../utils/inspection"; -import type {Scalars} from "@code0-tech/sagittarius-graphql-types"; -import {useService, useStore} from "../../../utils/contextStore"; -import {DFlowReactiveService} from "../DFlow.service"; -import React from "react"; - -export const useDFlowValidations = (flowId: Scalars['FlowID']['output']): ValidationResult[] => { - - const flowService = useService(DFlowReactiveService) - const flowStore = useStore(DFlowReactiveService) - const flow = flowService.getById(flowId) - - const validations = React.useMemo(() => { - return flow?.nodes?.map(node => node.parameters?.map(parameter => parameter.validationResults).flat() ?? []).flat() ?? [] - }, [flowStore]) - - return validations - -} \ No newline at end of file diff --git a/src/components/d-layout/DLayout.style.scss b/src/components/d-layout/DLayout.style.scss index 43d619084..d8a20df41 100644 --- a/src/components/d-layout/DLayout.style.scss +++ b/src/components/d-layout/DLayout.style.scss @@ -15,7 +15,6 @@ flex-direction: column; width: 100%; height: 100%; - gap: variables.$xl; } .d-layout__top, @@ -28,19 +27,25 @@ flex: 1 1 auto; min-height: 0; width: 100%; - gap: variables.$xl; } .d-layout__left { - padding-right: variables.$xl; border-right: 1px solid helpers.borderColor(); } .d-layout__right { - padding-left: variables.$xl; border-left: 1px solid helpers.borderColor(); } +.d-layout__top { + border-bottom: 1px solid helpers.borderColor(); +} + +.d-layout__bottom { + border-top: 1px solid helpers.borderColor(); +} + + .d-layout__left, .d-layout__right { flex: 0 0 auto; diff --git a/src/components/d-resizable/DResizable.stories.tsx b/src/components/d-resizable/DResizable.stories.tsx index 5617480b9..43e3a64da 100644 --- a/src/components/d-resizable/DResizable.stories.tsx +++ b/src/components/d-resizable/DResizable.stories.tsx @@ -2,41 +2,31 @@ import {Meta} from "@storybook/react-vite"; import {DResizableHandle, DResizablePanel, DResizablePanelGroup} from "./DResizable"; import React from "react"; import {DFullScreen} from "../d-fullscreen/DFullScreen"; -import {Button} from "../button/Button"; -import {IconDatabase, IconHierarchy3, IconSettings, IconTicket} from "@tabler/icons-react"; -import {Flex} from "../flex/Flex"; -import {Tooltip, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; -import {FunctionDefinitionView} from "../d-flow/function/DFlowFunction.view"; -import {useReactiveArrayService} from "../../utils/reactiveArrayService"; +import {IconDatabase, IconFile, IconMessageChatbot} from "@tabler/icons-react"; +import {useReactiveArrayService} from "../../utils"; 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 {DFlowSuggestion} from "../d-flow/suggestion/DFlowSuggestion.view"; -import {DFlowReactiveSuggestionService} from "../d-flow/suggestion/DFlowSuggestion.service"; -import {FlowView} from "../d-flow/DFlow.view"; -import {ContextStoreProvider} from "../../utils/contextStore"; -import {DFlowTabs} from "../d-flow/tab/DFlowTabs"; -import {DFlowTypeReactiveService} from "../d-flow/type/DFlowType.service"; -import {FlowTypeView} from "../d-flow/type/DFlowType.view"; +import {ContextStoreProvider} from "../../utils"; +import {DFlowTabs} from "../d-flow-file"; import DataTypesData from "./data_types.json"; import FunctionsData from "./runtime_functions.json"; import FlowTypeData from "./flow_types.json"; -import {useFlowNodes} from "../d-flow/DFlow.nodes.hook"; -import {useFlowEdges} from "../d-flow/DFlow.edges.hook"; -import {DFlow} from "../d-flow/DFlow"; -import {Background, BackgroundVariant} from "@xyflow/react"; -import {DFlowControl} from "../d-flow/control/DFlowControl"; -import {DFlowValidation} from "../d-flow/validation/DFlowValidation"; +import {DFlow, DFlowReactiveService} from "../d-flow"; import {DLayout} from "../d-layout/DLayout"; -import {DFlowFolder} from "../d-flow/folder/DFlowFolder"; import { + Flow, NamespacesProjectsFlowsCreateInput, - NamespacesProjectsFlowsCreatePayload, NamespacesProjectsFlowsDeleteInput, NamespacesProjectsFlowsDeletePayload + NamespacesProjectsFlowsCreatePayload, + NamespacesProjectsFlowsDeleteInput, + NamespacesProjectsFlowsDeletePayload, NamespacesProjectsFlowsUpdateInput, NamespacesProjectsFlowsUpdatePayload } from "@code0-tech/sagittarius-graphql-types"; -import {DFlowExport} from "../d-flow/export/DFlowExport"; +import {Flex} from "../flex/Flex"; +import {Button} from "../button/Button"; +import {Text} from "../text/Text"; +import {DFlowFolder} from "../d-flow-folder"; +import {DataTypeView, DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import {DFlowFunctionReactiveService, FunctionDefinitionView} from "../d-flow-function"; +import {DFlowTypeReactiveService, FlowTypeView} from "../d-flow-type"; const meta: Meta = { title: "Dashboard Resizable", @@ -59,18 +49,20 @@ class DFlowReactiveServiceExtend extends DFlowReactiveService { flowDelete(payload: NamespacesProjectsFlowsDeleteInput): Promise { return Promise.resolve(undefined); } -} -class DFlowReactiveSuggestionServiceExtend extends DFlowReactiveSuggestionService {} + flowUpdate(payload: NamespacesProjectsFlowsUpdateInput): Promise { + return Promise.resolve(undefined); + } +} export const Dashboard = () => { - const [fileTabsStore, fileTabsService] = useReactiveArrayService(FileTabsService) + const [fileTabsStore, fileTabsService] = useReactiveArrayService(FileTabsService, []) // @ts-ignore const [dataTypeStore, dataTypeService] = useReactiveArrayService(DFlowDataTypeReactiveService, [...DataTypesData.map(data => new DataTypeView(data))]); // @ts-ignore const [functionStore, functionService] = useReactiveArrayService(DFlowFunctionReactiveService, [...FunctionsData.map(data => new FunctionDefinitionView(data))]); - const [flowStore, flowService] = useReactiveArrayService(DFlowReactiveServiceExtend, [new FlowView({ + const [flowStore, flowService] = useReactiveArrayService(DFlowReactiveServiceExtend, [{ id: "gid://sagittarius/Flow/1", type: { id: "gid://sagittarius/FlowType/867", @@ -84,103 +76,60 @@ export const Dashboard = () => { }, { flowSettingIdentifier: "HTTP_HOST", }] + }, + nodes: { + nodes: [] } - })]); - const [suggestionStore, suggestionService] = useReactiveArrayService(DFlowReactiveSuggestionServiceExtend); + }]); // @ts-ignore const [flowTypeStore, flowTypeService] = useReactiveArrayService(DFlowTypeReactiveService, [...FlowTypeData.map(data => new FlowTypeView(data))]); - return + const [show, setShow] = React.useState(false); + + return - - - - - - - - - All Flows - - - - - - - - - - Issue Management - - - - - - - - - - Database - - - + services={[[flowTypeStore, flowTypeService], [fileTabsStore, fileTabsService], [dataTypeStore, dataTypeService], [functionStore, functionService], [flowStore, flowService]]}> + + + + -
- - - - - - - Settings - - - -
-
}> - }> - - - - - - - - - - + } bottomContent={ + + + + + }> + + + + + + + + + {show && ( + <> + + + + + + )} + -} - - -const FlowExample = () => { - const initialNodes = useFlowNodes("gid://sagittarius/Flow/1") - const initialEdges = useFlowEdges("gid://sagittarius/Flow/1") - - return - - - - - {/**/} - } \ No newline at end of file diff --git a/src/components/d-resizable/DResizable.style.scss b/src/components/d-resizable/DResizable.style.scss index 8a545896f..1f983be83 100644 --- a/src/components/d-resizable/DResizable.style.scss +++ b/src/components/d-resizable/DResizable.style.scss @@ -13,7 +13,7 @@ & { @include box.box(variables.$primary); - @include helpers.borderRadius(); + border: none; } &:has(.d-resizable__panel) { @@ -29,8 +29,9 @@ justify-content: center; &[data-panel-group-direction=horizontal] { - width: 1rem; + width: 1px; height: 100%; + background: helpers.borderColor(); } &[data-panel-group-direction=vertical] { @@ -42,25 +43,14 @@ &__handle-bar { z-index: 1; position: relative; - display: block; + display: flex; padding: .25rem; + border-radius: variables.$xs; & { - @include box.box(variables.$white, variables.$white, variables.$white); - @include helpers.borderRadius(); + @include box.box(variables.$secondary); } } - &[data-panel-group-direction=horizontal] * &__handle-bar { - min-height: 3rem; - height: 15%; - } - - &[data-panel-group-direction=vertical] * &__handle-bar { - min-width: 3rem; - min-height: auto; - height: auto; - width: 15%; - } } \ No newline at end of file diff --git a/src/components/d-resizable/DResizable.tsx b/src/components/d-resizable/DResizable.tsx index 414d9698b..9ca07f4f3 100644 --- a/src/components/d-resizable/DResizable.tsx +++ b/src/components/d-resizable/DResizable.tsx @@ -12,6 +12,7 @@ import { PanelResizeHandleProps } from "react-resizable-panels"; import "./DResizable.style.scss" +import {IconFolder, IconGripVertical} from "@tabler/icons-react"; type DResizablePanelGroupProps = Code0ComponentProps & PanelGroupProps type DResizablePanelProps = Code0ComponentProps & PanelProps @@ -33,7 +34,9 @@ export const DResizableHandle: React.FC = (props) => { return -
+
+ +
} diff --git a/src/components/file-tabs/FileTabs.service.ts b/src/components/file-tabs/FileTabs.service.ts index be34863be..bee6bf65f 100644 --- a/src/components/file-tabs/FileTabs.service.ts +++ b/src/components/file-tabs/FileTabs.service.ts @@ -1,5 +1,6 @@ import {FileTabsView} from "./FileTabs.view"; import {ReactiveArrayService, ReactiveArrayStore} from "../../utils/reactiveArrayService"; +import {startTransition} from "react"; export class FileTabsService extends ReactiveArrayService { @@ -7,23 +8,38 @@ export class FileTabsService extends ReactiveArrayService { super(store); } + getById(id: string): FileTabsView | undefined { + return this.values().find((item: FileTabsView) => item.id === id) + } + public clearLeft(): void { const index = this.getActiveIndex() - this.access.setState(prevState => [...prevState.filter((_, index1) => { - return index1 >= index - })]) + this.access.getState().forEach((tab, indexTab) => { + if (indexTab >= index) return + this.removeTabById(tab.id!) + }) } public clearRight(): void { const index = this.getActiveIndex() - this.access.setState(prevState => [...prevState.filter((_, index1) => { - return index1 <= index - })]) + this.access.getState().forEach((tab, indexTab) => { + if (indexTab <= index) return + this.removeTabById(tab.id!) + }) } public clearWithoutActive(): void { - const tab = this.getActiveTab() - if (tab) this.access.setState(prevState => [tab]) + const index = this.getActiveIndex() + this.access.getState().forEach((tab, indexTab) => { + if (indexTab == index) return + this.removeTabById(tab.id!) + }) + } + + public clearAll(): void { + this.access.getState().forEach((tab) => { + this.removeTabById(tab.id!) + }) } public activateTab(id: string) { @@ -32,7 +48,26 @@ export class FileTabsService extends ReactiveArrayService { }) const tab = this.values().find((item: FileTabsView) => item.id === id); - if (tab) tab.active = true + if (tab) { + tab.active = true + tab.show = true + } + this.update() + } + + removeTabById(id: string) { + const tab = this.getById(id) + const index = this.values().findIndex((item: FileTabsView) => item.id === id) + if (!tab) return + if (tab.active && this.has(index - 1)) { + const previousTab = this.get(index - 1) + if (previousTab.show) this.activateTab(previousTab.id!!) + } else if (tab.active && this.has(index + 1)) { + const nextTab = this.get(index + 1) + if (nextTab.show) this.activateTab(nextTab.id!!) + } + tab.show = false + tab.active = false this.update() } @@ -46,27 +81,57 @@ export class FileTabsService extends ReactiveArrayService { } super.delete(index); + this.update() } - public add(value: FileTabsView) { - //if tab with id already exists, do not add it again and just activate the existing one + deleteById(id: string) { + const index = this.values().findIndex((item: FileTabsView) => item.id === id) - if (this.values().some(value1 => value1.id == value.id)) { - this.activateTab(value.id!!) - return + if (index !== -1) { + this.delete(index) } + this.update() + } + + registerTab(value: FileTabsView) { + const nextValue = {...value, show: value.show ?? false} + + startTransition(() => { + this.access.setState((prevState) => { + const existingIndex = prevState.findIndex((tab) => tab.id === nextValue.id) + + if (existingIndex !== -1) return prevState - if (value.active) { - this.values().forEach((item: FileTabsView) => { - item.active = false + return [...prevState, nextValue] }) - } - super.add(value); + }) + } + + public add(value: FileTabsView) { + const nextValue = {...value, show: value.show ?? true} + + startTransition(() => { + this.access.setState((prevState) => { + const existingIndex = prevState.findIndex((tab) => tab.id === nextValue.id) + const nextState = prevState.map((tab) => ({...tab, active: nextValue.active ? false : tab.active})) + + if (existingIndex !== -1) { + nextState[existingIndex] = { + ...nextState[existingIndex], + ...nextValue, + active: nextValue.active ?? nextState[existingIndex].active, + show: nextValue.show ?? nextState[existingIndex].show, + } + return nextState + } + + return [...nextState, nextValue] + }) + }) } public getActiveTab(): FileTabsView | undefined { - const values = [...this.values()] - return values.reverse().find((item: FileTabsView) => { + return this.values().find((item: FileTabsView) => { return item.active }) } diff --git a/src/components/file-tabs/FileTabs.style.scss b/src/components/file-tabs/FileTabs.style.scss index 9f4d31c94..37a090b41 100644 --- a/src/components/file-tabs/FileTabs.style.scss +++ b/src/components/file-tabs/FileTabs.style.scss @@ -16,38 +16,31 @@ align-items: center; justify-content: space-between; position: relative; - gap: variables.$xs; padding: variables.$xs; &-content { display: flex; + width: 100%; overflow-x: scroll; flex-wrap: nowrap; - gap: variables.$xxs; + gap: variables.$xs; -ms-overflow-style: none; /* Internet Explorer 10+ */ scrollbar-width: none; /* Firefox, Safari 18.2+, Chromium 121+ */ &::-webkit-scrollbar { display: none; /* Older Safari and Chromium */ } } - - &-controls { - display: flex; - flex-wrap: nowrap; - gap: variables.$xxs; - } } &__trigger { padding: variables.$xxs variables.$xs; display: flex; - gap: variables.$xs; + gap: variables.$xxs; align-items: center; text-wrap: nowrap; margin-bottom: 1px; cursor: pointer; - font-size: variables.$sm; & { @include box.box(variables.$primary); @@ -55,22 +48,25 @@ @include helpers.borderRadius(); @include helpers.fontStyle(); background: transparent; - box-shadow: none; + border: none; + &:after { + position: absolute; + content: ""; + height: 75%; + width: 100%; + left: variables.$xxs; + top: 50%; + transform: translateY(-50%); + border-right: 1px solid helpers.borderColor(); + z-index: -1; + } } &[data-state="active"] { - @include box.box(variables.$secondary); - - .file-tabs__trigger-icon { - opacity: 1; - } + @include box.boxActiveStyle(variables.$secondary); + border: none; } - &:hover, &:active { - .file-tabs__trigger-icon { - opacity: 1; - } - } } &__trigger-icon { @@ -78,19 +74,17 @@ align-items: center; justify-content: center; padding: .1rem; - opacity: 0; cursor: pointer; font-size: unset; & { - @include box.boxHover(variables.$primary); @include helpers.borderRadius(); } } &__content { - padding: variables.$xs + variables.$xs; + padding: variables.$xs; position: relative; } diff --git a/src/components/file-tabs/FileTabs.tsx b/src/components/file-tabs/FileTabs.tsx index bb0224cb3..223b6d20c 100644 --- a/src/components/file-tabs/FileTabs.tsx +++ b/src/components/file-tabs/FileTabs.tsx @@ -12,7 +12,7 @@ import { import {mergeCode0Props} from "../../utils/utils"; import {Code0ComponentProps} from "../../utils/types"; import "./FileTabs.style.scss" -import {IconX} from "@tabler/icons-react"; +import {IconFile, IconX} from "@tabler/icons-react"; import {ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "../scroll-area/ScrollArea"; type FileTabsProps = Code0ComponentProps & TabsProps @@ -38,14 +38,14 @@ export const FileTabsTrigger: React.FC = (props) => { data-value={props.value} {...mergeCode0Props("file-tabs__trigger", props) as FileTabsTriggerProps}> {props.children} {props.closable ?
- +
: null} } export const FileTabsContent: React.FC = ({children, ...props}) => { return - + {children} diff --git a/src/components/file-tabs/FileTabs.view.ts b/src/components/file-tabs/FileTabs.view.ts index 5071e0d85..f94c9fa61 100644 --- a/src/components/file-tabs/FileTabs.view.ts +++ b/src/components/file-tabs/FileTabs.view.ts @@ -7,4 +7,5 @@ export interface FileTabsView { content: React.ReactNode active: boolean, lastActive?: Date + show?: boolean } \ No newline at end of file diff --git a/src/components/form/Input.tsx b/src/components/form/Input.tsx index 51262c0d0..00d43f82b 100644 --- a/src/components/form/Input.tsx +++ b/src/components/form/Input.tsx @@ -94,6 +94,16 @@ const InputComponent = React.forwardRef>( ...rest } = props + const { + onFocus: userOnFocus, + onBlur: userOnBlur, + onKeyDown: userOnKeyDown, + onKeyDownCapture: userOnKeyDownCapture, + onChange: userOnChange, + onInput: userOnInput, + ...inputProps + } = rest + const inputRef = ref || useRef(null) const menuRef = useRef(null) const syntaxRef = useRef(null) @@ -113,6 +123,11 @@ const InputComponent = React.forwardRef>( const disabledOnValue = React.useMemo(() => disableOnValue(value), [value, disableOnValue]) + const mergedInputProps = React.useMemo( + () => ({...inputProps, onChange: userOnChange, onInput: userOnInput}), + [inputProps, userOnChange, userOnInput], + ) + const normalizeTextValue = React.useCallback((rawValue: any): string => { const normalized = rawValue ?? "" return typeof normalized === "string" ? normalized : String(normalized) @@ -162,14 +177,14 @@ const InputComponent = React.forwardRef>( if (!el || !formValidation?.setValue) return const handleChange = (event: any) => { - const nextValue = rest.type !== "checkbox" ? event.target.value : event.target.checked + const nextValue = inputProps.type !== "checkbox" ? event.target.value : event.target.checked const validationValue = validationUsesSuggestions ? activeSuggestionsRef.current : nextValue formValidation.setValue?.(validationValue) } el.addEventListener("change", handleChange) return () => el.removeEventListener("change", handleChange) - }, [formValidation?.setValue, inputRef, rest.type, validationUsesSuggestions]) + }, [formValidation?.setValue, inputProps.type, inputRef, validationUsesSuggestions]) useEffect(() => { if (!suggestions) return @@ -257,7 +272,7 @@ const InputComponent = React.forwardRef>( useEffect(() => { if (!formValidation?.setValue) return - const currentValue = rest.type !== "checkbox" + const currentValue = inputProps.type !== "checkbox" ? normalizeTextValue(inputRef.current?.value ?? value) : value @@ -269,7 +284,7 @@ const InputComponent = React.forwardRef>( lastValidationValueRef.current = validationValue formValidation.setValue(validationValue) - }, [activeSuggestions, formValidation?.setValue, inputRef, normalizeTextValue, rest.type, validationUsesSuggestions, value]) + }, [activeSuggestions, formValidation?.setValue, inputProps.type, inputRef, normalizeTextValue, validationUsesSuggestions, value]) const focusInputCaretAtEnd = React.useCallback(() => { setTimeout(() => { @@ -675,7 +690,8 @@ const InputComponent = React.forwardRef>( setOpen(false) } } - }, [handleAtomicDeletion, mapRawIndexToVisualIndex, mapVisualIndexToRawIndex, normalizeSelectionForAtomicBlocks, open, suggestions, syncSyntaxScroll, totalVisualLength, transformSyntaxWithAppliedParts, updateVisualSelectionRange]) + userOnKeyDown?.(event) + }, [handleAtomicDeletion, mapRawIndexToVisualIndex, mapVisualIndexToRawIndex, normalizeSelectionForAtomicBlocks, open, suggestions, syncSyntaxScroll, totalVisualLength, transformSyntaxWithAppliedParts, updateVisualSelectionRange, userOnKeyDown]) const handleKeyDownCapture = React.useCallback((event: React.KeyboardEvent) => { if (event.key === " " || event.code === "Space") { @@ -685,9 +701,10 @@ const InputComponent = React.forwardRef>( // @ts-ignore -- nativeEvent may not exist in synthetic typing but is present at runtime event.nativeEvent?.stopImmediatePropagation?.() } - }, []) + userOnKeyDownCapture?.(event) + }, [userOnKeyDownCapture]) - const handleFocus = React.useCallback(() => { + const handleFocus = React.useCallback((event: React.FocusEvent) => { setIsFocused(true) requestAnimationFrame(() => { updateVisualSelectionRange() @@ -697,13 +714,15 @@ const InputComponent = React.forwardRef>( if (suggestions && !open) { setOpen(true) } - }, [open, suggestions, syncSyntaxScroll, updateVisualSelectionRange]) + userOnFocus?.(event) + }, [open, suggestions, syncSyntaxScroll, updateVisualSelectionRange, userOnFocus]) - const handleBlur = React.useCallback(() => { + const handleBlur = React.useCallback((event: React.FocusEvent) => { setIsFocused(false) setVisualCaretIndex(null) setVisualSelectionRange(null) - }, []) + userOnBlur?.(event) + }, [userOnBlur]) useEffect(() => { const target = inputRef.current @@ -885,7 +904,7 @@ const InputComponent = React.forwardRef>( } - {...mergeCode0Props(`input__control ${transformSyntaxWithAppliedParts ? "input__control--syntax" : ""}`, rest)} + {...mergeCode0Props(`input__control ${transformSyntaxWithAppliedParts ? "input__control--syntax" : ""}`, mergedInputProps)} onFocus={handleFocus} onBlur={handleBlur} onKeyDownCapture={handleKeyDownCapture} @@ -923,7 +942,7 @@ const InputComponent = React.forwardRef>(
- ), [applySuggestionValue, availableSuggestions, disabledOnValue, focusInputCaretAtEnd, handleBlur, handleFocus, handleKeyDown, handleKeyDownCapture, inputRef, onSuggestionSelect, open, rest, suggestionsEmptyState, suggestionsFooter, suggestionsHeader, transformSyntaxWithAppliedParts]) + ), [applySuggestionValue, availableSuggestions, disabledOnValue, focusInputCaretAtEnd, handleBlur, handleFocus, handleKeyDown, handleKeyDownCapture, inputRef, mergedInputProps, onSuggestionSelect, open, suggestionsEmptyState, suggestionsFooter, suggestionsHeader, transformSyntaxWithAppliedParts]) return ( <> @@ -949,7 +968,7 @@ const InputComponent = React.forwardRef>( onBlur={handleBlur} onKeyDownCapture={handleKeyDownCapture} onKeyDown={handleKeyDown} - {...mergeCode0Props(`input__control ${transformSyntaxWithAppliedParts ? "input__control--syntax" : ""}`, rest)} + {...mergeCode0Props(`input__control ${transformSyntaxWithAppliedParts ? "input__control--syntax" : ""}`, mergedInputProps)} /> )} diff --git a/src/components/segmented-control/SegmentedControl.style.scss b/src/components/segmented-control/SegmentedControl.style.scss index ed2ef9494..66589ab6d 100644 --- a/src/components/segmented-control/SegmentedControl.style.scss +++ b/src/components/segmented-control/SegmentedControl.style.scss @@ -19,17 +19,18 @@ cursor: pointer; padding: variables.$xxs variables.$xs; + &[data-state="off"] { + background: transparent; + opacity: .6666666666; + } + & { @include box.box(); + @include helpers.disabled(); border-radius: variables.$borderRadius - variables.$xxs; border: none; } - &[data-state="off"] { - background: transparent; - opacity: .6666666666; - } - } } \ No newline at end of file diff --git a/src/components/segmented-control/SegmentedControl.tsx b/src/components/segmented-control/SegmentedControl.tsx index fb4254753..61b0a1405 100644 --- a/src/components/segmented-control/SegmentedControl.tsx +++ b/src/components/segmented-control/SegmentedControl.tsx @@ -1,7 +1,7 @@ import React from "react"; -import {Code0ComponentProps} from "../../utils/types"; +import {Code0ComponentProps} from "../../utils"; import {ToggleGroupSingleProps, ToggleGroupItemProps, Root, Item} from "@radix-ui/react-toggle-group"; -import {mergeCode0Props} from "../../utils/utils"; +import {mergeCode0Props} from "../../utils"; import "./SegmentedControl.style.scss" type SegmentedControlProps = Code0ComponentProps & ToggleGroupSingleProps diff --git a/src/index.ts b/src/index.ts index a2e806487..d95cf53e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,15 @@ export * from "./components/command/Command" export * from "./components/container/Container" export * from "./components/d-flow/index" +export * from "./components/d-flow-data-type/index" +export * from "./components/d-flow-file/index" +export * from "./components/d-flow-folder/index" +export * from "./components/d-flow-function/index" +export * from "./components/d-flow-panel/index" +export * from "./components/d-flow-suggestion/index" +export * from "./components/d-flow-type/index" +export * from "./components/d-flow-validation/index" + export * from "./components/d-fullscreen/DFullScreen" export * from "./components/d-layout/DLayout" export * from "./components/d-license/index" diff --git a/src/styles/_box.scss b/src/styles/_box.scss index 93e9d0528..0bbf47023 100644 --- a/src/styles/_box.scss +++ b/src/styles/_box.scss @@ -44,7 +44,7 @@ $borderColor: variables.$secondary ) { - &:active, &:focus, &[aria-selected=true] { + &:active, &:focus, &[aria-selected=true], &[data-state=open] { @include boxActiveStyle($background, $color, $borderColor); } } \ No newline at end of file diff --git a/src/utils/generics.ts b/src/utils/generics.ts index b68aec12c..0d53041ff 100644 --- a/src/utils/generics.ts +++ b/src/utils/generics.ts @@ -1,5 +1,5 @@ -import {FunctionDefinitionView} from "../components/d-flow/function/DFlowFunction.view"; -import {DFlowDataTypeReactiveService} from "../components/d-flow/data-type/DFlowDataType.service"; +import {FunctionDefinitionView} from "../components/d-flow-function/DFlowFunction.view"; +import {DFlowDataTypeReactiveService} from "../components/d-flow-data-type/DFlowDataType.service"; import type { DataType, DataTypeIdentifier, @@ -7,13 +7,12 @@ import type { DataTypeRuleConnection, DataTypeRulesConfig, DataTypeRulesVariant, - DataTypeVariant, + DataTypeVariant, Flow, GenericCombinationStrategyType, GenericMapper, GenericType, NodeParameterValue } from "@code0-tech/sagittarius-graphql-types"; -import {FlowView} from "../components/d-flow/DFlow.view"; const GENERIC_PLACEHOLDER = "GENERIC"; @@ -23,7 +22,7 @@ type GenericMappingResult = Record; type GenericReplacement = DataTypeIdentifier | GenericMapper; -type GenericMap = Map; +export type GenericMap = Map; const isPlainObject = (value: unknown): value is Record => { return typeof value === "object" && value !== null && !Array.isArray(value); @@ -375,12 +374,12 @@ export const resolveGenericKeys = ( func: FunctionDefinitionView, values: NodeParameterValue[], dataTypeService: DFlowDataTypeReactiveService, - flow?: FlowView + flow?: Flow ): GenericMap => { const genericMap: GenericMap = new Map(); - const genericKeys = func.genericKeys ?? []; + const genericKeys = func?.genericKeys ?? []; - if (!func.parameterDefinitions || genericKeys.length <= 0) return genericMap; + if (!func?.parameterDefinitions || genericKeys.length <= 0) return genericMap; const genericKeySet = new Set(genericKeys); diff --git a/src/utils/reactiveArrayService.ts b/src/utils/reactiveArrayService.ts index 094397f50..75ff05cfa 100644 --- a/src/utils/reactiveArrayService.ts +++ b/src/utils/reactiveArrayService.ts @@ -50,7 +50,7 @@ export class ReactiveArrayService> implements ArraySe update() { startTransition(() => { - this.access.setState(prev => prev.slice()); + this.access.setState(prev => [...prev]); }) }