diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d805f8c..2562b185 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,16 +30,16 @@ importers: specifier: ^1.1.8 version: 1.1.8 '@frankframework/doc-library-core': - specifier: ^1.0.3 + specifier: ^1.0.6 version: 1.0.6 '@frankframework/doc-library-react': - specifier: ^1.0.3 + specifier: ^1.0.4 version: 1.0.4(react@19.2.7) '@mdx-js/rollup': specifier: ^3.1.1 - version: 3.1.1(rollup@4.60.2) + version: 3.1.1(rollup@4.60.2)(supports-color@8.1.1) '@xyflow/react': - specifier: ^12.10.2 + specifier: ^12.11.2 version: 12.11.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) allotment: specifier: ^1.20.5 @@ -50,9 +50,6 @@ importers: dagre: specifier: ^0.8.5 version: 0.8.5 - dotenv: - specifier: ^17.4.2 - version: 17.4.2 fflate: specifier: ^0.8.3 version: 0.8.3 @@ -66,36 +63,36 @@ importers: specifier: ^1.1.1 version: 1.1.1 react: - specifier: ^19.2.5 + specifier: ^19.2.7 version: 19.2.7 react-complex-tree: - specifier: ^2.6.1 + specifier: ^2.6.2 version: 2.6.2(react@19.2.7) react-dom: - specifier: ^19.2.5 + specifier: ^19.2.7 version: 19.2.7(react@19.2.7) react-router: - specifier: ^8.0.1 + specifier: ^8.2.0 version: 8.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) remark-gfm: specifier: ^4.0.1 - version: 4.0.1 + version: 4.0.1(supports-color@8.1.1) sax-ts: specifier: ^1.2.13 version: 1.2.13 tailwind-merge: - specifier: ^3.5.0 + specifier: ^3.6.0 version: 3.6.0 xmllint-wasm: specifier: ^5.2.0 - version: 5.2.0(@types/node@20.19.43) + version: 5.2.0(@types/node@26.1.1) zustand: - specifier: ^5.0.12 + specifier: ^5.0.14 version: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) '@mdx-js/react': specifier: ^3.1.1 version: 3.1.1(@types/react@19.2.17)(react@19.2.7) @@ -104,10 +101,10 @@ importers: version: 3.4.2(prettier@3.9.5) '@stylistic/eslint-plugin': specifier: ^5.10.0 - version: 5.10.0(eslint@10.7.0(jiti@2.7.0)) + version: 5.10.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.3.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -118,50 +115,50 @@ importers: specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) '@types/mdx': - specifier: ^2.0.13 + specifier: ^2.0.14 version: 2.0.14 '@types/node': - specifier: ^20.19.39 - version: 20.19.43 + specifier: ^26.1.1 + version: 26.1.1 '@types/react': - specifier: ^19.2.14 + specifier: ^19.2.17 version: 19.2.17 '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) '@typescript-eslint/eslint-plugin': specifier: ^8.64.0 - version: 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + version: 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^8.64.0 - version: 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + version: 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) '@vitejs/plugin-react': - specifier: ^6.0.2 - version: 6.0.3(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)) + specifier: ^6.0.3 + version: 6.0.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/ui': - specifier: ^4.1.5 + specifier: ^4.1.10 version: 4.1.10(vitest@4.1.10) eslint: specifier: ^10.7.0 - version: 10.7.0(jiti@2.7.0) + version: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-prettier: - specifier: ^5.5.5 - version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5) + specifier: ^5.5.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)))(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.5) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@10.7.0(jiti@2.7.0)) + version: 7.37.5(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-react-hooks: specifier: ^7.1.1 - version: 7.1.1(eslint@10.7.0(jiti@2.7.0)) + version: 7.1.1(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) eslint-plugin-sonarjs: specifier: ^4.2.0 - version: 4.2.0(eslint@10.7.0(jiti@2.7.0)) + version: 4.2.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-unicorn: specifier: ^72.0.0 - version: 72.0.0(eslint@10.7.0(jiti@2.7.0)) + version: 72.0.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) jsdom: specifier: ^29.1.1 version: 29.1.1 @@ -173,7 +170,7 @@ importers: version: 0.8.1(prettier@3.9.5) react-router-devtools: specifier: ^6.2.1 - version: 6.2.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(csstype@3.2.3)(react-dom@19.2.7(react@19.2.7))(react-router@8.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(solid-js@1.9.12)(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)) + version: 6.2.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(csstype@3.2.3)(react-dom@19.2.7(react@19.2.7))(react-router@8.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(solid-js@1.9.12)(supports-color@8.1.1)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) tailwindcss: specifier: ^4.3.3 version: 4.3.3 @@ -182,19 +179,19 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.64.0 - version: 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + version: 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) vite: specifier: ^8.1.5 - version: 8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0) + version: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) vite-plugin-checker: specifier: ^0.14.4 - version: 0.14.4(eslint@10.7.0(jiti@2.7.0))(optionator@0.9.4)(typescript@5.9.3)(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)) + version: 0.14.4(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(optionator@0.9.4)(typescript@5.9.3)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) vite-plugin-svgr: specifier: ^5.2.0 - version: 5.2.0(rollup@4.60.2)(typescript@5.9.3)(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)) + version: 5.2.0(rollup@4.60.2)(supports-color@8.1.1)(typescript@5.9.3)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) vitest: specifier: ^4.1.5 - version: 4.1.10(@types/node@20.19.43)(@vitest/ui@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.1)(@vitest/ui@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) src/main/frontend/cypress: devDependencies: @@ -203,7 +200,7 @@ importers: version: 15.18.0 cypress-multi-reporters: specifier: ^2.0.5 - version: 2.0.5(mocha@11.7.6) + version: 2.0.5(mocha@11.7.6)(supports-color@8.1.1) mocha: specifier: ^11.7.6 version: 11.7.6 @@ -1266,8 +1263,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@20.19.43': - resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} @@ -1966,10 +1963,6 @@ packages: dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - dotenv@17.4.2: - resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} - engines: {node: '>=12'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -4031,8 +4024,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} undici@7.28.0: resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} @@ -4429,16 +4422,16 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.7': + '@babel/core@7.29.7(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 @@ -4467,19 +4460,19 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-module-imports@7.29.7': + '@babel/helper-module-imports@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -4506,7 +4499,7 @@ snapshots: '@babel/parser': 7.29.7 '@babel/types': 7.29.7 - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.7(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 @@ -4614,14 +4607,14 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))': dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5': + '@eslint/config-array@0.23.5(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 3.0.5 debug: 4.4.3(supports-color@8.1.1) @@ -4642,9 +4635,9 @@ snapshots: mdn-data: 2.28.1 source-map-js: 1.2.1 - '@eslint/js@10.0.1(eslint@10.7.0(jiti@2.7.0))': + '@eslint/js@10.0.1(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))': optionalDependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) '@eslint/object-schema@3.0.5': {} @@ -4717,7 +4710,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mdx-js/mdx@3.1.1': + '@mdx-js/mdx@3.1.1(supports-color@8.1.1)': dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 @@ -4729,14 +4722,14 @@ snapshots: estree-util-is-identifier-name: 3.0.0 estree-util-scope: 1.0.0 estree-walker: 3.0.3 - hast-util-to-jsx-runtime: 2.3.6 + hast-util-to-jsx-runtime: 2.3.6(supports-color@8.1.1) markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 recma-jsx: 1.0.1(acorn@8.17.0) recma-stringify: 1.0.0 - rehype-recma: 1.0.0 - remark-mdx: 3.1.1 - remark-parse: 11.0.0 + rehype-recma: 1.0.0(supports-color@8.1.1) + remark-mdx: 3.1.1(supports-color@8.1.1) + remark-parse: 11.0.0(supports-color@8.1.1) remark-rehype: 11.1.2 source-map: 0.7.6 unified: 11.0.5 @@ -4753,9 +4746,9 @@ snapshots: '@types/react': 19.2.17 react: 19.2.7 - '@mdx-js/rollup@3.1.1(rollup@4.60.2)': + '@mdx-js/rollup@3.1.1(rollup@4.60.2)(supports-color@8.1.1)': dependencies: - '@mdx-js/mdx': 3.1.1 + '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) '@rollup/pluginutils': 5.4.0(rollup@4.60.2) rollup: 4.60.2 source-map: 0.7.6 @@ -5080,64 +5073,64 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@10.7.0(jiti@2.7.0))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) '@typescript-eslint/types': 8.64.0 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 picomatch: 4.0.5 - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-preset@8.1.0(@babel/core@7.29.7)': + '@svgr/babel-preset@8.1.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.7) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) - '@svgr/core@8.1.0(typescript@5.9.3)': + '@svgr/core@8.1.0(supports-color@8.1.1)(typescript@5.9.3)': dependencies: - '@babel/core': 7.29.7 - '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7(supports-color@8.1.1)) camelcase: 6.3.0 cosmiconfig: 8.3.6(typescript@5.9.3) snake-case: 3.0.4 @@ -5150,11 +5143,11 @@ snapshots: '@babel/types': 7.29.7 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(supports-color@8.1.1)(typescript@5.9.3))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7) - '@svgr/core': 8.1.0(typescript@5.9.3) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/core': 8.1.0(supports-color@8.1.1)(typescript@5.9.3) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: @@ -5221,12 +5214,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) '@tanstack/devtools-client@0.0.5': dependencies: @@ -5261,19 +5254,19 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-vite@0.4.1(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0))': + '@tanstack/devtools-vite@0.4.1(supports-color@8.1.1)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 '@tanstack/devtools-client': 0.0.5 '@tanstack/devtools-event-bus': 0.4.0 chalk: 5.6.2 launch-editor: 2.14.1 picomatch: 4.0.5 - vite: 8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - bufferutil - supports-color @@ -5407,9 +5400,9 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@20.19.43': + '@types/node@26.1.1': dependencies: - undici-types: 6.21.0 + undici-types: 8.3.0 '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: @@ -5432,15 +5425,15 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/type-utils': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.64.0 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -5448,19 +5441,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.64.0 '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.64.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.64.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.64.0(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3) '@typescript-eslint/types': 8.64.0 @@ -5478,13 +5471,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -5492,9 +5485,9 @@ snapshots: '@typescript-eslint/types@8.64.0': {} - '@typescript-eslint/typescript-estree@8.64.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.64.0(supports-color@8.1.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.64.0(typescript@5.9.3) + '@typescript-eslint/project-service': 8.64.0(supports-color@8.1.1)(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3) '@typescript-eslint/types': 8.64.0 '@typescript-eslint/visitor-keys': 8.64.0 @@ -5507,13 +5500,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/utils@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) '@typescript-eslint/scope-manager': 8.64.0 '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@8.1.1)(typescript@5.9.3) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5525,10 +5518,10 @@ snapshots: '@ungap/structured-clone@1.3.3': {} - '@vitejs/plugin-react@6.0.3(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) '@vitest/expect@4.1.10': dependencies: @@ -5539,13 +5532,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.10': dependencies: @@ -5574,7 +5567,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vitest: 4.1.10(@types/node@20.19.43)(@vitest/ui@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@26.1.1)(@vitest/ui@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/utils@4.1.10': dependencies: @@ -5954,7 +5947,7 @@ snapshots: csstype@3.2.3: {} - cypress-multi-reporters@2.0.5(mocha@11.7.6): + cypress-multi-reporters@2.0.5(mocha@11.7.6)(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) lodash: 4.18.1 @@ -6160,8 +6153,6 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 - dotenv@17.4.2: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6341,31 +6332,31 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)): + eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)))(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.5): dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) prettier: 3.9.5 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + eslint-config-prettier: 10.1.8(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) - eslint-plugin-react-hooks@7.1.1(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-react-hooks@7.1.1(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/parser': 7.29.7 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) hermes-parser: 0.25.1 zod: 4.4.3 zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-react@7.37.5(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -6373,7 +6364,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.4.0 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) estraverse: 5.3.0 hasown: 2.0.4 jsx-ast-utils: 3.3.5 @@ -6387,12 +6378,12 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-sonarjs@4.2.0(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-sonarjs@4.2.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) functional-red-black-tree: 1.0.1 globals: 17.7.0 jsx-ast-utils-x: 0.1.0 @@ -6404,9 +6395,9 @@ snapshots: typescript: 5.9.3 yaml: 2.9.0 - eslint-plugin-unicorn@72.0.0(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-unicorn@72.0.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) '@eslint/css-tree': 4.0.4 browserslist: 4.28.6 change-case: 5.4.4 @@ -6414,7 +6405,7 @@ snapshots: core-js-compat: 3.49.0 detect-indent: 7.0.2 entities: 4.5.0 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) find-up-simple: 1.0.1 globals: 17.7.0 indent-string: 5.0.0 @@ -6441,11 +6432,11 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.7.0(jiti@2.7.0): + eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 + '@eslint/config-array': 0.23.5(supports-color@8.1.1) '@eslint/config-helpers': 0.6.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 @@ -6770,7 +6761,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-to-estree@3.1.3: + hast-util-to-estree@3.1.3(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 @@ -6780,9 +6771,9 @@ snapshots: estree-util-attach-comments: 3.0.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -6791,7 +6782,7 @@ snapshots: transitivePeerDependencies: - supports-color - hast-util-to-jsx-runtime@2.3.6: + hast-util-to-jsx-runtime@2.3.6(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/hast': 3.0.5 @@ -6800,9 +6791,9 @@ snapshots: devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -7280,14 +7271,14 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.3: + mdast-util-from-markdown@2.0.3(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.2 + micromark: 4.0.2(supports-color@8.1.1) micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-decode-string: 2.0.1 micromark-util-normalize-identifier: 2.0.1 @@ -7305,67 +7296,67 @@ snapshots: mdast-util-find-and-replace: 3.0.2 micromark-util-character: 2.1.1 - mdast-util-gfm-footnote@2.1.0: + mdast-util-gfm-footnote@2.1.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: - supports-color - mdast-util-gfm-strikethrough@2.0.0: + mdast-util-gfm-strikethrough@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-table@2.0.0: + mdast-util-gfm-table@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-task-list-item@2.0.0: + mdast-util-gfm-task-list-item@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm@3.1.0: + mdast-util-gfm@3.1.0(supports-color@8.1.1): dependencies: - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-gfm-footnote: 2.1.0(supports-color@8.1.1) + mdast-util-gfm-strikethrough: 2.0.0(supports-color@8.1.1) + mdast-util-gfm-table: 2.0.0(supports-color@8.1.1) + mdast-util-gfm-task-list-item: 2.0.0(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-expression@2.0.1: + mdast-util-mdx-expression@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-jsx@3.2.0: + mdast-util-mdx-jsx@3.2.0(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 @@ -7373,7 +7364,7 @@ snapshots: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -7382,23 +7373,23 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdx@3.0.0: + mdast-util-mdx@3.0.0(supports-color@8.1.1): dependencies: - mdast-util-from-markdown: 2.0.3 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdxjs-esm@2.0.1: + mdast-util-mdxjs-esm@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -7684,7 +7675,7 @@ snapshots: micromark-util-types@2.0.2: {} - micromark@4.0.2: + micromark@4.0.2(supports-color@8.1.1): dependencies: '@types/debug': 4.1.13 debug: 4.4.3(supports-color@8.1.1) @@ -8068,17 +8059,17 @@ snapshots: react-lifecycles-compat@3.0.4: {} - react-router-devtools@6.2.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(csstype@3.2.3)(react-dom@19.2.7(react@19.2.7))(react-router@8.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(solid-js@1.9.12)(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)): + react-router-devtools@6.2.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(csstype@3.2.3)(react-dom@19.2.7(react@19.2.7))(react-router@8.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(solid-js@1.9.12)(supports-color@8.1.1)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 '@radix-ui/react-accordion': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/devtools-client': 0.0.5 '@tanstack/devtools-event-client': 0.4.4 - '@tanstack/devtools-vite': 0.4.1(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)) + '@tanstack/devtools-vite': 0.4.1(supports-color@8.1.1)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) '@tanstack/react-devtools': 0.9.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(csstype@3.2.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(solid-js@1.9.12) '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) @@ -8092,7 +8083,7 @@ snapshots: react-hotkeys-hook: 5.3.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-router: 8.2.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-tooltip: 5.30.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - vite: 8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) optionalDependencies: '@biomejs/cli-darwin-arm64': 2.5.4 '@rollup/rollup-darwin-arm64': 4.62.2 @@ -8194,36 +8185,36 @@ snapshots: dependencies: jsesc: 3.1.0 - rehype-recma@1.0.0: + rehype-recma@1.0.0(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/hast': 3.0.5 - hast-util-to-estree: 3.1.3 + hast-util-to-estree: 3.1.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - remark-gfm@4.0.1: + remark-gfm@4.0.1(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 + mdast-util-gfm: 3.1.0(supports-color@8.1.1) micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 + remark-parse: 11.0.0(supports-color@8.1.1) remark-stringify: 11.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-mdx@3.1.1: + remark-mdx@3.1.1(supports-color@8.1.1): dependencies: - mdast-util-mdx: 3.0.0 + mdast-util-mdx: 3.0.0(supports-color@8.1.1) micromark-extension-mdxjs: 3.0.0 transitivePeerDependencies: - supports-color - remark-parse@11.0.0: + remark-parse@11.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -8745,13 +8736,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3): + typescript-eslint@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@5.9.3) - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@8.1.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -8765,7 +8756,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.21.0: {} + undici-types@8.3.0: {} undici@7.28.0: {} @@ -8849,7 +8840,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-checker@0.14.4(eslint@10.7.0(jiti@2.7.0))(optionator@0.9.4)(typescript@5.9.3)(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)): + vite-plugin-checker@0.14.4(eslint@10.7.0(jiti@2.7.0)(supports-color@8.1.1))(optionator@0.9.4)(typescript@5.9.3)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@babel/code-frame': 7.29.7 chokidar: 5.0.0 @@ -8858,24 +8849,24 @@ snapshots: picomatch: 4.0.5 proper-lockfile: 4.1.2 tiny-invariant: 1.3.3 - vite: 8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) optionalDependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.7.0(jiti@2.7.0)(supports-color@8.1.1) optionator: 0.9.4 typescript: 5.9.3 - vite-plugin-svgr@5.2.0(rollup@4.60.2)(typescript@5.9.3)(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)): + vite-plugin-svgr@5.2.0(rollup@4.60.2)(supports-color@8.1.1)(typescript@5.9.3)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@rollup/pluginutils': 5.4.0(rollup@4.60.2) - '@svgr/core': 8.1.0(typescript@5.9.3) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - vite: 8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0) + '@svgr/core': 8.1.0(supports-color@8.1.1)(typescript@5.9.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(supports-color@8.1.1)(typescript@5.9.3))(supports-color@8.1.1) + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - rollup - supports-color - typescript - vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0): + vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 @@ -8883,15 +8874,15 @@ snapshots: rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 20.19.43 + '@types/node': 26.1.1 fsevents: 2.3.3 jiti: 2.7.0 yaml: 2.9.0 - vitest@4.1.10(@types/node@20.19.43)(@vitest/ui@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.10(@types/node@26.1.1)(@vitest/ui@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -8908,10 +8899,10 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@20.19.43)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 20.19.43 + '@types/node': 26.1.1 '@vitest/ui': 4.1.10(vitest@4.1.10) jsdom: 29.1.1 transitivePeerDependencies: @@ -9019,9 +9010,9 @@ snapshots: xmlchars@2.2.0: {} - xmllint-wasm@5.2.0(@types/node@20.19.43): + xmllint-wasm@5.2.0(@types/node@26.1.1): dependencies: - '@types/node': 20.19.43 + '@types/node': 26.1.1 y18n@5.0.8: {} diff --git a/src/main/frontend/app/components/file-structure/use-file-tree-context-menu.ts b/src/main/frontend/app/components/file-structure/use-file-tree-context-menu.ts index b71d1b69..63574055 100644 --- a/src/main/frontend/app/components/file-structure/use-file-tree-context-menu.ts +++ b/src/main/frontend/app/components/file-structure/use-file-tree-context-menu.ts @@ -233,7 +233,7 @@ export function useFileTreeContextMenu({ patterns: menu.isFolder ? FOLDER_OR_ADAPTER_NAME_PATTERNS : FILE_NAME_PATTERNS, }) }, - [projectName, dataProvider, closeContextMenu, showErrorToast, onAfterRename], + [projectName, dataProvider, closeContextMenu, showErrorToast, onAfterRename, logApiError], ) const handleDelete = useCallback( diff --git a/src/main/frontend/app/components/flow/create-node-modal.tsx b/src/main/frontend/app/components/flow/create-node-modal.tsx index faf6e3b0..2d970c0c 100644 --- a/src/main/frontend/app/components/flow/create-node-modal.tsx +++ b/src/main/frontend/app/components/flow/create-node-modal.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState, type ChangeEvent, type JSX } from 'react' import { useSubmitOnEnter } from '~/hooks/use-submit-on-enter' -import type { FlowNode } from '~/routes/studio/canvas/flow' +import type { FlowNode } from '~/routes/studio/canvas-flow/canvas-flow' import useFlowStore from '~/stores/flow-store' import useNodeContextStore, { type NodeContextStore } from '~/stores/node-context-store' import { useFFDoc } from '@frankframework/doc-library-react' diff --git a/src/main/frontend/app/components/sidebars-layout/sidebar-layout-store.ts b/src/main/frontend/app/components/sidebars-layout/sidebar-layout-store.ts index f6d2e72a..16d86ae3 100644 --- a/src/main/frontend/app/components/sidebars-layout/sidebar-layout-store.ts +++ b/src/main/frontend/app/components/sidebars-layout/sidebar-layout-store.ts @@ -33,10 +33,10 @@ export const useSidebarStore = create()( get, ): { instances: Record - initializeInstance: (name: string, defaultVisible?: VisibilityState | undefined) => unknown - toggleSidebar: (name: string, side: SidebarSide) => unknown - setSizes: (name: string, sizes: number[]) => unknown - setVisible: (name: string, side: SidebarSide, value: boolean) => unknown + initializeInstance: (name: string, defaultVisible?: VisibilityState | undefined) => void + toggleSidebar: (name: string, side: SidebarSide) => void + setSizes: (name: string, sizes: number[]) => void + setVisible: (name: string, side: SidebarSide, value: boolean) => void getSizes: (name: string) => number[] | undefined getVisibility: (name: string) => VisibilityState | undefined } => ({ @@ -69,7 +69,8 @@ export const useSidebarStore = create()( if (!instance) return state const newVisible = [...instance.visible] as VisibilityState - newVisible[side] = !Object.hasOwn(newVisible, side) + const currentValue = newVisible[side] + newVisible[side] = !currentValue return updateInstanceState(state, name, { visible: newVisible }) }), diff --git a/src/main/frontend/app/main.tsx b/src/main/frontend/app/main.tsx index 143ecf8d..76828569 100644 --- a/src/main/frontend/app/main.tsx +++ b/src/main/frontend/app/main.tsx @@ -5,9 +5,6 @@ import { ToastsContainer } from '~/components/toast/toast' import { useTheme } from '~/hooks/use-theme' import { useProjectStore } from '~/stores/project-store' import type { ConfigurationProject } from '~/types/project.types' -import dotenv from 'dotenv' - -dotenv.config({ quiet: true }) import { router } from './router' import 'allotment/dist/style.css' diff --git a/src/main/frontend/app/routes/projectlanding/project-landing.tsx b/src/main/frontend/app/routes/projectlanding/project-landing.tsx index 8e59cfb6..7fe6f03c 100644 --- a/src/main/frontend/app/routes/projectlanding/project-landing.tsx +++ b/src/main/frontend/app/routes/projectlanding/project-landing.tsx @@ -82,6 +82,7 @@ export default function ProjectLanding(): React.JSX.Element { setIsDiscovering(true) fetchInstanceConfigurations() .then((ffInstance): void => { + if (!ffInstance) return setFFInstanceName(ffInstance.name) setFFConfiguration(ffInstance.configurations) }) diff --git a/src/main/frontend/app/routes/studio/canvas/flow.tsx b/src/main/frontend/app/routes/studio/canvas-flow/canvas-flow.tsx similarity index 72% rename from src/main/frontend/app/routes/studio/canvas/flow.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/canvas-flow.tsx index b18f2147..955988a4 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.tsx +++ b/src/main/frontend/app/routes/studio/canvas-flow/canvas-flow.tsx @@ -1,4 +1,3 @@ -/* eslint-disable */ import { addEdge, Background, @@ -7,11 +6,11 @@ import { ControlButton, Controls, type Edge, + type FinalConnectionState, + type InternalNode, type Node, - type OnConnectEnd, - type OnConnectStart, + type OnConnectStartParams, ReactFlow, - ReactFlowProvider, useNodesInitialized, useReactFlow, useUpdateNodeInternals, @@ -20,22 +19,22 @@ import Dagre from '@dagrejs/dagre' import { useNavigate } from 'react-router' import { SaveStatusIndicator } from '~/components/save-status-indicator' import useToasts from '~/components/toast/use-toasts' +import { convertAdapterXmlToJson, getAdapterFromConfiguration } from '~/routes/studio/xml-to-json-parser' import { useSaveStatusStore } from '~/stores/save-status-store' import CodeIcon from '/icons/solar/Code.svg?react' import '@xyflow/react/dist/style.css' -import FrankNodeComponent, { type FrankNodeType } from '~/routes/studio/canvas/nodetypes/frank-node' -import FrankEdgeComponent from '~/routes/studio/canvas/edgetypes/frank-edge' -import ExitNodeComponent, { type ExitNode } from '~/routes/studio/canvas/nodetypes/exit-node' -import GroupNodeComponent, { type GroupNode } from '~/routes/studio/canvas/nodetypes/group-node' -import useFlowStore, { type FlowState, isStickyNote } from '~/stores/flow-store' +import FrankNodeComponent, { type FrankNodeType } from '~/routes/studio/canvas-flow/nodetypes/frank-node' +import FrankEdgeComponent from '~/routes/studio/canvas-flow/edgetypes/frank-edge' +import ExitNodeComponent, { type ExitNode } from '~/routes/studio/canvas-flow/nodetypes/exit-node' +import GroupNodeComponent, { type GroupNode } from '~/routes/studio/canvas-flow/nodetypes/group-node' +import useFlowStore, { isStickyNote } from '~/stores/flow-store' import { useShallow } from 'zustand/react/shallow' -import { FlowConfig } from '~/routes/studio/canvas/flow.config' +import { FlowConfig } from '~/routes/studio/canvas-flow/flow.config' import { getElementTypeFromName } from '~/routes/studio/node-translator-module' -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { NodeContextMenuContext, useNodeContextMenu } from './node-context-menu-context' -import StickyNoteComponent, { type StickyNote } from '~/routes/studio/canvas/nodetypes/sticky-note' +import { type JSX, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useNodeContextMenu } from './node-context-menu-context' +import StickyNoteComponent, { type StickyNote } from '~/routes/studio/canvas-flow/nodetypes/sticky-note' import useTabStore, { type TabData } from '~/stores/tab-store' -import { convertAdapterXmlToJson, getAdapterFromConfiguration } from '~/routes/studio/xml-to-json-parser' import { exportFlowToXml, replaceAdapterInXml } from '~/routes/studio/flow-to-xml-parser' import useNodeContextStore from '~/stores/node-context-store' import CreateNodeModal from '~/components/flow/create-node-modal' @@ -56,28 +55,28 @@ import LightbulbIcon from '/icons/solar/Lightbulb.svg?react' import CanvasContextMenu from '~/components/flow/canvas-context-menu' import { SidebarSide, useSidebarStore } from '~/components/sidebars-layout/sidebar-layout-store' import { openInEditorAtElement } from '~/actions/navigationActions' -import HandleMenu from '~/routes/studio/canvas/nodetypes/components/handle-menu' +import HandleMenu from '~/routes/studio/canvas-flow/nodetypes/components/handle-menu' import IconLabelButton from '~/components/inputs/icon-label-button' export type FlowNode = FrankNodeType | ExitNode | StickyNote | GroupNode | Node -const selector = (state: FlowState) => ({ - nodes: state.nodes, - edges: state.edges, - onNodesChange: state.onNodesChange, - onEdgesChange: state.onEdgesChange, - onConnect: state.onConnect, - onReconnect: state.onReconnect, -}) - const STICKY_SNAP_DISTANCE = 60 +const nodeTypes = { + frankNode: FrankNodeComponent, + exitNode: ExitNodeComponent, + stickyNote: StickyNoteComponent, + groupNode: GroupNodeComponent, +} +const edgeTypes = { frankEdge: FrankEdgeComponent } -const getStickyCenter = (sticky: StickyNote) => ({ - x: sticky.position.x + (sticky.measured?.width ?? FlowConfig.STICKY_NOTE_DEFAULT_WIDTH) / 2, - y: sticky.position.y + (sticky.measured?.height ?? FlowConfig.STICKY_NOTE_DEFAULT_HEIGHT) / 2, -}) +function getStickyCenter(sticky: StickyNote): { x: number; y: number } { + return { + x: sticky.position.x + (sticky.measured?.width ?? FlowConfig.STICKY_NOTE_DEFAULT_WIDTH) / 2, + y: sticky.position.y + (sticky.measured?.height ?? FlowConfig.STICKY_NOTE_DEFAULT_HEIGHT) / 2, + } +} -const isWithinSnapDistance = (sticky: StickyNote, frankNode: FlowNode) => { +function isWithinSnapDistance(sticky: StickyNote, frankNode: FlowNode): boolean { const center = getStickyCenter(sticky) return ( center.x >= frankNode.position.x - STICKY_SNAP_DISTANCE && @@ -88,30 +87,25 @@ const isWithinSnapDistance = (sticky: StickyNote, frankNode: FlowNode) => { ) } -const distanceToFrankNode = (sticky: StickyNote, frankNode: FlowNode) => { +function distanceToFrankNode(sticky: StickyNote, frankNode: FlowNode): number { const center = getStickyCenter(sticky) const dx = center.x - (frankNode.position.x + (frankNode.measured?.width ?? FlowConfig.NODE_DEFAULT_WIDTH) / 2) const dy = center.y - (frankNode.position.y + (frankNode.measured?.height ?? FlowConfig.NODE_MIN_HEIGHT) / 2) return Math.hypot(dx, dy) } -const isFrankNode = (node: FlowNode): node is FrankNodeType => node.type === 'frankNode' || node.type === 'exitNode' +function isFrankNode(node: FlowNode): node is FrankNodeType { + return node.type === 'frankNode' || node.type === 'exitNode' +} -const findNearestFrankNode = (sticky: StickyNote, candidates: FlowNode[]) => - candidates - .filter((n) => (n.type === 'frankNode' || n.type === 'exitNode') && isWithinSnapDistance(sticky, n)) - .reduce((best, n) => { - if (best === null) return n - return distanceToFrankNode(sticky, n) < distanceToFrankNode(sticky, best) ? n : best +function findNearestFrankNode(sticky: StickyNote, candidates: FlowNode[]): FlowNode | null { + return candidates + .filter((node) => (node.type === 'frankNode' || node.type === 'exitNode') && isWithinSnapDistance(sticky, node)) + .reduce((best, node) => { + if (best === null) return node + return distanceToFrankNode(sticky, node) < distanceToFrankNode(sticky, best) ? node : best }, null) - -const nodeTypes = { - frankNode: FrankNodeComponent, - exitNode: ExitNodeComponent, - stickyNote: StickyNoteComponent, - groupNode: GroupNodeComponent, } -const edgeTypes = { frankEdge: FrankEdgeComponent } function computeAbsoluteNodePosition( targetNode: FlowNode, @@ -148,7 +142,7 @@ function computeNodeCenteredViewport( const zoomToFitWidth = (canvasWidth * viewportPadding) / nodeWidth const zoomToFitHeight = (canvasHeight * viewportPadding) / nodeHeight - const zoom = Math.max(minZoom, Math.min(maxZoom, Math.min(zoomToFitWidth, zoomToFitHeight))) + const zoom = Math.max(minZoom, Math.min(maxZoom, zoomToFitWidth, zoomToFitHeight)) return { x: canvasWidth / 2 - nodeCenterX * zoom, @@ -157,9 +151,112 @@ function computeNodeCenteredViewport( } } -function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { +function stripMeasuredDimensions(nodes: FlowNode[]): FlowNode[] { + return nodes.map((node) => { + if (!('measured' in node)) return node + const { measured: _measured, ...nodeWithoutMeasured } = node as FlowNode & { measured?: unknown } + return nodeWithoutMeasured as FlowNode + }) +} + +export default function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }): JSX.Element { + /* Hooks */ + const showNodeContextMenu = useNodeContextMenu() + const navigate = useNavigate() + const { elements } = useFFDoc() + const updateNodeInternals = useUpdateNodeInternals() + const reactFlow = useReactFlow() + const nodesInitialized = useNodesInitialized() + const { showErrorToast, logApiError, showWarningToast } = useToasts() + useShortcut({ + 'studio.copy': () => copySelection(), + 'studio.paste': () => pasteSelection(), + 'studio.cut': () => cutSelection(), + 'studio.undo': () => undo(), + 'studio.redo': () => redo(), + 'studio.redo-alt': () => redo(), + 'studio.group': () => handleGrouping(), + 'studio.ungroup': () => handleUngroup(), + 'studio.hide': () => toggleSelectedHidden(), + 'studio.save': () => void saveFlow(), + 'studio.close-context': () => closeEditNodeContextOnEscape(), + 'studio.delete': () => deleteSelection(), + 'studio.show-in-editor': () => showSelectedNodeInEditor(), + }) + + /* useState */ + const [loading, setLoading] = useState(false) + const [showCreateNodeModal, setShowCreateNodeModal] = useState(false) + const [edgeDropPositions, setEdgeDropPositions] = useState<{ x: number; y: number } | null>(null) + const [edgeDropHandleType, setEdgeDropHandleType] = useState(null) + const [pendingCompactConnection, setPendingCompactConnection] = useState<{ + connection: Connection + sourceNodeSubtype: string + position: { x: number; y: number } + } | null>(null) + const [pendingEdgeDrop, setPendingEdgeDrop] = useState<{ + position: { x: number; y: number } + sourceNodeSubtype: string + } | null>(null) + const [contextMenu, setContextMenu] = useState<{ x: number; y: number; flowPos: { x: number; y: number } } | null>( + null, + ) + + /* useRef */ + + const showNodeContextMenuRef = useRef(showNodeContextMenu) + const clipboardRef = useRef<{ + nodes: FlowNode[] + edges: Edge[] + } | null>(null) + const autoSaveTimerRef = useRef | null>(null) + const isLoadingTabRef = useRef(false) + const pendingFitViewRef = useRef(null) + const reactFlowRef = useRef(reactFlow) + const canvasRef = useRef(null) + const fitAfterLayoutRef = useRef<{ id: string }[] | null>(null) + const pendingInitialRelayoutRef = useRef<{ pendingSelection: { subtype: string; name: string } | null } | null>(null) + const sourceInfoReference = useRef<{ + nodeId: string | null + handleId: string | null + handleType: 'source' | 'target' | null + }>({ nodeId: null, handleId: null, handleType: null }) + + /* useStore */ + + const { project } = useProjectStore() + const { activeTab, getTab, setTabData } = useTabStore() + const { + nodes, + edges, + viewport, + future, + onNodesChange, + onEdgesChange, + onConnect, + onReconnect, + undo, + redo, + getNextNodeId, + setNodesHiddenForwards, + setNodesWithoutHistory, + setViewport, + setHistory, + setFuture, + setEdges, + setNodes, + setStickyAttachment, + deleteChild, + deleteNode, + history: flowHistory, + resetStore: resetFlowStore, + addHandle: addFlowHandle, + } = useFlowStore() + const { setSaving, setSaved, setIdle } = useSaveStatusStore() + const autosaveEnabled = useSettingsStore((state) => state.general.autoSave.enabled) + const autosaveDelay = useSettingsStore((state) => state.general.autoSave.delayMs) const hoveredNodeId = useNodeContextStore((state) => state.hoveredNodeId) const showAllForwards = useNodeContextStore((state) => state.showAllForwards) const setHoveredNodeId = useNodeContextStore((state) => state.setHoveredNodeId) @@ -200,87 +297,8 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { setSelectedGroupId: store.setSelectedGroupId, })), ) - const { elements } = useFFDoc() - const elementsRef = useRef(elements) - const showNodeContextMenuRef = useRef(showNodeContextMenu) - const navigate = useNavigate() - const { showErrorToast, logApiError } = useToasts() - useEffect(() => { - elementsRef.current = elements - }, [elements]) - - useEffect(() => { - showNodeContextMenuRef.current = showNodeContextMenu - }, [showNodeContextMenu]) - - const [showCreateNodeModal, setShowCreateNodeModal] = useState(false) - const [edgeDropPositions, setEdgeDropPositions] = useState<{ x: number; y: number } | null>(null) - const [pendingCompactConnection, setPendingCompactConnection] = useState<{ - connection: Connection - sourceNodeSubtype: string - position: { x: number; y: number } - } | null>(null) - const [pendingEdgeDrop, setPendingEdgeDrop] = useState<{ - position: { x: number; y: number } - sourceNodeSubtype: string - } | null>(null) - - const [edgeDropHandleType, setEdgeDropHandleType] = useState(null) - - const clipboardRef = useRef<{ - nodes: FlowNode[] - edges: Edge[] - } | null>(null) - - const { setSaving, setSaved, setIdle } = useSaveStatusStore() - const [contextMenu, setContextMenu] = useState<{ x: number; y: number; flowPos: { x: number; y: number } } | null>( - null, - ) - const autoSaveTimerRef = useRef | null>(null) - const isLoadingTabRef = useRef(false) - const pendingFitViewRef = useRef(null) - - const updateNodeInternals = useUpdateNodeInternals() - const reactFlow = useReactFlow() - const reactFlowRef = useRef(reactFlow) - reactFlowRef.current = reactFlow - const nodesInitialized = useNodesInitialized() - const canvasRef = useRef(null) - const fitAfterLayoutRef = useRef<{ id: string }[] | null>(null) - const pendingInitialRelayoutRef = useRef<{ pendingSelection: { subtype: string; name: string } | null } | null>(null) - const [relayoutNonce, setRelayoutNonce] = useState(0) - const loadedTabIdRef = useRef(null) - - const applySelectionToNodes = useCallback((pendingSelection: { subtype: string; name: string }) => { - const currentNodes = useFlowStore.getState().nodes - const nodeToSelect = currentNodes.find( - (node): node is FrankNodeType => - isFrankNode(node) && node.data.subtype === pendingSelection.subtype && node.data.name === pendingSelection.name, - ) - - if (!nodeToSelect) return - - useFlowStore.getState().setNodes( - currentNodes.map((node) => ({ - ...node, - selected: node.id === nodeToSelect.id, - })), - ) - - pendingFitViewRef.current = nodeToSelect.id - - const nodeContextStore = useNodeContextStore.getState() - nodeContextStore.setParentId(null) - nodeContextStore.setChildParentId(null) - nodeContextStore.setNodeId(+nodeToSelect.id) - nodeContextStore.setAttributes(elementsRef.current?.[nodeToSelect.data.subtype]?.attributes) - nodeContextStore.setEditingSubtype(nodeToSelect.data.subtype) - nodeContextStore.setIsEditing(true) - showNodeContextMenuRef.current(true) - }, []) - - const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onReconnect } = useFlowStore(useShallow(selector)) + /* useMemo */ const hiddenForwardNodeIds = useMemo(() => { const ids = new Set() @@ -311,7 +329,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { const displayEdges = useMemo(() => { if (hiddenForwardNodeIds.size === 0) return edges - const isHiddenAndNotRevealed = (nodeId: string) => + const isHiddenAndNotRevealed = (nodeId: string): boolean => hiddenForwardNodeIds.has(nodeId) && !revealedHiddenIds.has(nodeId) return edges.map((edge) => { @@ -330,102 +348,118 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { }) }, [nodes, hiddenForwardNodeIds, revealedHiddenIds]) - const saveFlow = useCallback(async () => { - const { nodes: flowNodes, edges: flowEdges, viewport: flowViewport } = useFlowStore.getState() - const flowData = { nodes: flowNodes, edges: flowEdges, viewport: flowViewport } - const currentProject = useProjectStore.getState().project - const activeTabKey = useTabStore.getState().activeTab - const tabData = useTabStore.getState().getTab(activeTabKey) - const configurationPath = tabData?.configurationPath - const adapterName = tabData?.name - const adapterPosition = tabData?.adapterPosition + /* useCallback */ - if (!configurationPath || !adapterName || !currentProject) return + const saveFlowToTab = useCallback( + (tabId: string) => { + const tabData = getTab(tabId) + if (!tabData) return - setSaving() - try { - const fullConfigXml = await fetchConfigurationFileCached(currentProject.name, configurationPath) - const configDoc = new DOMParser().parseFromString(fullConfigXml, 'text/xml') - const allAdapters = [...configDoc.querySelectorAll('Adapter, adapter')] + setTabData(tabId, { + ...tabData, + flowJson: { + nodes, + edges, + viewport, + }, + history: flowHistory, + future, + }) + }, + [edges, flowHistory, future, getTab, nodes, setTabData, viewport], + ) - const existingAdapter = - adapterPosition === undefined - ? (allAdapters.find((a) => a.getAttribute('name') === adapterName) ?? null) - : (allAdapters[adapterPosition] ?? null) + const loadFromApi = useCallback( + async (tab: TabData, pendingSelection: { subtype: string; name: string } | null) => { + if (!project) return - if (!existingAdapter) { - throw new Error(`Could not find adapter "${adapterName}" at position ${adapterPosition} in configuration`) - } + const adapter = await getAdapterFromConfiguration( + project.name, + tab.configurationPath, + tab.name!, + tab.adapterPosition, + ) + if (!adapter) return - const existingAdapterXml = new XMLSerializer().serializeToString(existingAdapter) + const adapterJson = await convertAdapterXmlToJson(adapter) + setEdges(adapterJson.edges) + setNodes(adapterJson.nodes) + setHistory([]) + setFuture([]) + pendingInitialRelayoutRef.current = { pendingSelection } + }, + [project, setEdges, setFuture, setHistory, setNodes], + ) - const newAdapterXml = await exportFlowToXml( - flowData, - currentProject.name, - configurationPath, - adapterName, - existingAdapterXml, + const applySelectionToNodes = useCallback( + (pendingSelection: { subtype: string; name: string }) => { + const currentNodes = nodes + const nodeToSelect = currentNodes.find( + (node): node is FrankNodeType => + isFrankNode(node) && + node.data.subtype === pendingSelection.subtype && + node.data.name === pendingSelection.name, ) - const adapterIndex = allAdapters.indexOf(existingAdapter) - if (adapterIndex === -1) showErrorToast('Could not determine adapter position for replacement') + if (!nodeToSelect) return - const updatedConfigXml = replaceAdapterInXml(fullConfigXml, adapterIndex, newAdapterXml.trim()) - - await saveConfigurationFile(currentProject.name, configurationPath, updatedConfigXml, true) - clearConfigurationFileCache(currentProject.name, configurationPath) - useEditorTabStore.getState().refreshAllTabs() - if (currentProject.isGitRepository) await refreshOpenDiffs(currentProject.name) + setNodes( + currentNodes.map((node) => ({ + ...node, + selected: node.id === nodeToSelect.id, + })), + ) - const tabData = useTabStore.getState().getTab(activeTabKey) + pendingFitViewRef.current = nodeToSelect.id - if (tabData) { - const { nodes: savedNodes, edges: savedEdges, viewport: savedViewport } = useFlowStore.getState() - useTabStore.getState().setTabData(activeTabKey, { - ...tabData, - flowJson: { nodes: savedNodes, edges: savedEdges, viewport: savedViewport }, - }) - } + const nodeContextStore = useNodeContextStore.getState() + nodeContextStore.setParentId(null) + nodeContextStore.setChildParentId(null) + nodeContextStore.setNodeId(+nodeToSelect.id) + nodeContextStore.setAttributes(elements?.[nodeToSelect.data.subtype]?.attributes) + nodeContextStore.setEditingSubtype(nodeToSelect.data.subtype) + nodeContextStore.setIsEditing(true) + showNodeContextMenuRef.current(true) + }, + [elements, nodes, setNodes], + ) - setSaved() - } catch (error) { - logApiError('Failed to save XML', error as Error) - setIdle() - } - }, []) + const computeAdapterCenteredViewport = useCallback( + (nodes: Node[], canvasWidth: number, canvasHeight: number): { x: number; y: number; zoom: number } => { + const layoutNodes = nodes.filter((node) => node.type === 'frankNode' || node.type === 'exitNode') + if (layoutNodes.length === 0) return { x: 0, y: 0, zoom: 1 } - const autosaveEnabled = useSettingsStore((s) => s.general.autoSave.enabled) - const autosaveDelay = useSettingsStore((s) => s.general.autoSave.delayMs) + const boundsLeft = Math.min(...layoutNodes.map((node) => node.position.x)) + const boundsTop = Math.min(...layoutNodes.map((node) => node.position.y)) + const boundsRight = Math.max( + ...layoutNodes.map((node) => node.position.x + (node.measured?.width ?? FlowConfig.NODE_DEFAULT_WIDTH)), + ) + const boundsBottom = Math.max( + ...layoutNodes.map((node) => node.position.y + (node.measured?.height ?? FlowConfig.NODE_MIN_HEIGHT)), + ) - const scheduleAutoSave = useCallback(() => { - if (!autosaveEnabled) return - if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current) - autoSaveTimerRef.current = setTimeout(() => { - autoSaveTimerRef.current = null - saveFlow() - }, autosaveDelay) - }, [saveFlow, autosaveEnabled, autosaveDelay]) + const boundsWidth = boundsRight - boundsLeft + const boundsHeight = boundsBottom - boundsTop + const boundsCenterX = boundsLeft + boundsWidth / 2 + const boundsCenterY = boundsTop + boundsHeight / 2 - useEffect(() => { - return () => { - if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current) - } - }, []) + const viewportPadding = 0.85 + const minZoom = 0.2 + const maxZoom = 1.5 + const verticalOffset = 40 - useEffect(() => { - if (nodes.length > 0 && !isLoadingTabRef.current) { - scheduleAutoSave() - } - }, [nodes, edges, scheduleAutoSave]) + const zoomToFitWidth = (canvasWidth * viewportPadding) / Math.max(boundsWidth, 1) + const zoomToFitHeight = (canvasHeight * viewportPadding) / Math.max(boundsHeight, 1) + const zoom = Math.max(minZoom, Math.min(maxZoom, zoomToFitWidth, zoomToFitHeight)) - useEffect(() => { - if (!fitAfterLayoutRef.current) return - const nodeIds = fitAfterLayoutRef.current - fitAfterLayoutRef.current = null - requestAnimationFrame(() => { - reactFlowRef.current?.fitView({ nodes: nodeIds, padding: 0.15, duration: 300 }) - }) - }, [nodes]) + return { + x: canvasWidth / 2 - boundsCenterX * zoom, + y: canvasHeight / 2 - boundsCenterY * zoom - verticalOffset, + zoom, + } + }, + [], + ) const waitForStableCanvasDimensions = useCallback((onStable: (canvasWidth: number, canvasHeight: number) => void) => { let previousWidth = -1 @@ -436,7 +470,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { const REQUIRED_STABLE_FRAMES = 3 const MINIMUM_TOTAL_FRAMES = 5 - const checkDimensions = () => { + const checkDimensions = (): void => { const currentWidth = canvasRef.current?.clientWidth ?? 0 const currentHeight = canvasRef.current?.clientHeight ?? 0 @@ -466,96 +500,173 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { } return } + } + + requestAnimationFrame(checkDimensions) + }, []) + + const restoreFlowFromTab = useCallback( + (tab: TabData, options: { skipViewport: boolean; forceRemeasure: boolean }) => { + const flowJson = tab.flowJson + + if (flowJson) { + let nodes = Array.isArray(flowJson.nodes) ? flowJson.nodes : [] + if (options.forceRemeasure) { + nodes = stripMeasuredDimensions(nodes) + } + setNodes(nodes) + setEdges(Array.isArray(flowJson.edges) ? flowJson.edges : []) + + if (!options.skipViewport) { + const savedViewport = flowJson.viewport as { x: number; y: number; zoom: number } | undefined + const targetViewport = savedViewport ?? { x: 0, y: 0, zoom: 1 } + setViewport(targetViewport) + requestAnimationFrame(() => reactFlowRef.current?.setViewport(targetViewport)) + } + + setHistory(tab.history ?? []) + setFuture(tab.future ?? []) + } else { + resetFlowStore() + } + }, + [resetFlowStore, setEdges, setFuture, setHistory, setNodes, setViewport], + ) + + const loadFromCache = useCallback( + (tab: TabData, pendingSelection: { subtype: string; name: string } | null, recenter: boolean) => { + const hasPendingSelection = !!pendingSelection + restoreFlowFromTab(tab, { + skipViewport: hasPendingSelection || recenter, + forceRemeasure: hasPendingSelection, + }) + if (pendingSelection) { + applySelectionToNodes(pendingSelection) + } else if (recenter) { + const cachedNodes = [...nodes] + waitForStableCanvasDimensions((canvasWidth, canvasHeight) => { + const viewport = computeAdapterCenteredViewport(cachedNodes, canvasWidth, canvasHeight) + setViewport(viewport) + reactFlowRef.current?.setViewport(viewport) + }) + } + }, + [ + applySelectionToNodes, + computeAdapterCenteredViewport, + nodes, + restoreFlowFromTab, + setViewport, + waitForStableCanvasDimensions, + ], + ) + + const loadFlowFromTab = useCallback( + async (tabData: TabData) => { + const pendingSelection = tabData.pendingNodeSelection ?? null + const recenter = tabData.pendingRecenter ?? false + + isLoadingTabRef.current = true + setLoading(true) + + if (pendingSelection || recenter) { + setTabData(activeTab, { ...tabData, pendingNodeSelection: null, pendingRecenter: null }) + } - requestAnimationFrame(checkDimensions) - } + try { + const hasCachedFlow = tabData.flowJson && Object.keys(tabData.flowJson).length > 0 + if (hasCachedFlow) { + loadFromCache(tabData, pendingSelection, recenter) + } else if (tabData.configurationPath && tabData.name) { + await loadFromApi(tabData, pendingSelection) + } + } catch (error) { + logApiError('Error loading tab flow:', error as Error) + } finally { + isLoadingTabRef.current = false + setLoading(false) + } + }, + [activeTab, loadFromApi, loadFromCache, logApiError, setTabData], + ) - requestAnimationFrame(checkDimensions) - }, []) + const saveFlow = useCallback(async () => { + const tabData = getTab(activeTab) + const configurationPath = tabData?.configurationPath + const adapterName = tabData?.name + const currentProject = useProjectStore.getState().project + if (!configurationPath || !adapterName || !currentProject) return - useEffect(() => { - const nodeId = pendingFitViewRef.current - if (!nodeId) return + const flowData = { nodes, edges, viewport } + const adapterPosition = tabData?.adapterPosition - const targetNode = nodes.find((node) => node.id === nodeId) - if (!targetNode?.measured?.width || !targetNode?.measured?.height) return - if (!targetNode.selected) return + setSaving() + const allAdapters: Element[] = [] + const fullConfigXml = await fetchConfigurationFileCached(currentProject.name, configurationPath) + try { + const configDoc = new DOMParser().parseFromString(fullConfigXml, 'text/xml') + allAdapters.push(...configDoc.querySelectorAll('Adapter, adapter')) + } catch (error) { + logApiError('Failed to parse configuration XML', error as Error) + } - pendingFitViewRef.current = null + const existingAdapter = + adapterPosition === undefined + ? (allAdapters.find((element) => element.getAttribute('name') === adapterName) ?? null) + : (allAdapters[adapterPosition] ?? null) - waitForStableCanvasDimensions((canvasWidth, canvasHeight) => { - const reactFlowInstance = reactFlowRef.current - if (!reactFlowInstance) return + if (!existingAdapter) { + throw new Error(`Could not find adapter "${adapterName}" at position ${adapterPosition} in configuration`) + } - const allNodes = useFlowStore.getState().nodes - const node = allNodes.find((node) => node.id === nodeId) - if (!node?.measured?.width || !node?.measured?.height) return + try { + const existingAdapterXml = new XMLSerializer().serializeToString(existingAdapter) + const newAdapterXml = await exportFlowToXml( + flowData, + currentProject.name, + configurationPath, + adapterName, + existingAdapterXml, + ) - const { absoluteX, absoluteY } = computeAbsoluteNodePosition(node, allNodes) + const adapterIndex = allAdapters.indexOf(existingAdapter) + if (adapterIndex === -1) showErrorToast('Could not determine adapter position for replacement') - const viewport = computeNodeCenteredViewport( - absoluteX, - absoluteY, - node.measured.width, - node.measured.height, - canvasWidth, - canvasHeight, - ) + const updatedConfigXml = replaceAdapterInXml(fullConfigXml, adapterIndex, newAdapterXml.trim()) - reactFlowInstance.setViewport(viewport, { duration: 400 }) - }) - }, [nodes, waitForStableCanvasDimensions]) + await saveConfigurationFile(currentProject.name, configurationPath, updatedConfigXml, true) + clearConfigurationFileCache(currentProject.name, configurationPath) + useEditorTabStore.getState().refreshAllTabs() + if (currentProject.isGitRepository) await refreshOpenDiffs(currentProject.name) - useEffect(() => { - useNodeContextStore.getState().registerSaveFlow(async () => { - if (autoSaveTimerRef.current) { - clearTimeout(autoSaveTimerRef.current) - autoSaveTimerRef.current = null + if (tabData) { + setTabData(activeTab, { + ...tabData, + flowJson: { nodes, edges, viewport }, + }) } - await saveFlow() - }) - return () => useNodeContextStore.getState().registerSaveFlow(null) - }, [saveFlow]) - - const sourceInfoReference = useRef<{ - nodeId: string | null - handleId: string | null - handleType: 'source' | 'target' | null - }>({ nodeId: null, handleId: null, handleType: null }) - const handleConnectStart: OnConnectStart = (_, params) => { - sourceInfoReference.current = { - nodeId: params.nodeId, - handleId: params.handleId, - handleType: params.handleType, + setSaved() + } catch (error) { + logApiError('Failed to save XML', error as Error) + setIdle() } - } + }, [activeTab, edges, getTab, logApiError, nodes, setIdle, setSaved, setSaving, setTabData, showErrorToast, viewport]) - const handleConnectEnd: OnConnectEnd = (event, connectionState) => { - const mouseEvent = event as MouseEvent - if (!connectionState.isValid) { - const zoom = reactFlow.getZoom() - if (zoom < FlowConfig.ZOOM_THRESHOLD && sourceInfoReference.current.handleType === 'source') { - const { nodes } = useFlowStore.getState() - const sourceNode = nodes.find((node) => node.id === sourceInfoReference.current.nodeId) - if (sourceNode && isFrankNode(sourceNode)) { - setPendingEdgeDrop({ - position: { x: mouseEvent.clientX, y: mouseEvent.clientY }, - sourceNodeSubtype: sourceNode.data.subtype, - }) - return - } - } - handleEdgeDropOnCanvas(mouseEvent.clientX, mouseEvent.clientY) - } - } + const scheduleAutoSave = useCallback(() => { + if (!autosaveEnabled) return + if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current) + autoSaveTimerRef.current = setTimeout(() => { + autoSaveTimerRef.current = null + saveFlow() + }, autosaveDelay) + }, [saveFlow, autosaveEnabled, autosaveDelay]) const handleConnect = useCallback( (connection: Connection) => { const zoom = reactFlow.getZoom() if (zoom < FlowConfig.ZOOM_THRESHOLD && connection.source) { - const { nodes } = useFlowStore.getState() const sourceNode = nodes.find((node) => node.id === connection.source) if (sourceNode && isFrankNode(sourceNode)) { @@ -578,14 +689,12 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { onConnect(connection) }, - [onConnect, reactFlow], + [nodes, onConnect, reactFlow], ) const handleCompactHandleSelect = useCallback( (type: string) => { if (!pendingCompactConnection) return - - const { nodes } = useFlowStore.getState() const sourceNode = nodes.find((node) => node.id === pendingCompactConnection.connection.source) if (!sourceNode || !isFrankNode(sourceNode)) { @@ -602,7 +711,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { }) } else { const newIndex = sourceNode.data.sourceHandles.length + 1 - useFlowStore.getState().addHandle(pendingCompactConnection.connection.source!, { type, index: newIndex }) + addFlowHandle(pendingCompactConnection.connection.source, { type, index: newIndex }) onConnect({ ...pendingCompactConnection.connection, sourceHandle: newIndex.toString(), @@ -611,17 +720,9 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { setPendingCompactConnection(null) }, - [pendingCompactConnection, onConnect], + [pendingCompactConnection, nodes, onConnect, addFlowHandle], ) - const handleEdgeDropOnCanvas = (x: number, y: number) => { - const { screenToFlowPosition } = reactFlow - const flowPositions = screenToFlowPosition({ x: x, y: y }) - - setEdgeDropPositions(flowPositions) - setShowCreateNodeModal(true) - } - const handleEdgeDropHandleSelect = useCallback( (type: string) => { if (!pendingEdgeDrop) return @@ -634,43 +735,6 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { [pendingEdgeDrop, reactFlow], ) - const computeAdapterCenteredViewport = useCallback( - (nodes: Node[], canvasWidth: number, canvasHeight: number): { x: number; y: number; zoom: number } => { - const layoutNodes = nodes.filter((node) => node.type === 'frankNode' || node.type === 'exitNode') - if (layoutNodes.length === 0) return { x: 0, y: 0, zoom: 1 } - - const boundsLeft = Math.min(...layoutNodes.map((node) => node.position.x)) - const boundsTop = Math.min(...layoutNodes.map((node) => node.position.y)) - const boundsRight = Math.max( - ...layoutNodes.map((node) => node.position.x + (node.measured?.width ?? FlowConfig.NODE_DEFAULT_WIDTH)), - ) - const boundsBottom = Math.max( - ...layoutNodes.map((node) => node.position.y + (node.measured?.height ?? FlowConfig.NODE_MIN_HEIGHT)), - ) - - const boundsWidth = boundsRight - boundsLeft - const boundsHeight = boundsBottom - boundsTop - const boundsCenterX = boundsLeft + boundsWidth / 2 - const boundsCenterY = boundsTop + boundsHeight / 2 - - const viewportPadding = 0.85 - const minZoom = 0.2 - const maxZoom = 1.5 - const verticalOffset = 40 - - const zoomToFitWidth = (canvasWidth * viewportPadding) / Math.max(boundsWidth, 1) - const zoomToFitHeight = (canvasHeight * viewportPadding) / Math.max(boundsHeight, 1) - const zoom = Math.max(minZoom, Math.min(maxZoom, Math.min(zoomToFitWidth, zoomToFitHeight))) - - return { - x: canvasWidth / 2 - boundsCenterX * zoom, - y: canvasHeight / 2 - boundsCenterY * zoom - verticalOffset, - zoom, - } - }, - [], - ) - const layoutGraph = useCallback((nodes: Node[], edges: Edge[], direction: 'TB' | 'LR' = 'LR'): Node[] => { const dagreGraph = new Dagre.graphlib.Graph() dagreGraph.setDefaultEdgeLabel(() => ({})) @@ -682,12 +746,13 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { const layoutableIds = new Set() for (const node of nodes) { - if ((node.type === 'frankNode' || node.type === 'exitNode') && node.position.x === 0 && node.position.y === 0) { - const width = node.measured?.width ?? FlowConfig.NODE_DEFAULT_WIDTH - const height = node.measured?.height ?? FlowConfig.NODE_MIN_HEIGHT - dagreGraph.setNode(node.id, { width: width, height: height }) - layoutableIds.add(node.id) - } + if (!((node.type === 'frankNode' || node.type === 'exitNode') && node.position.x === 0 && node.position.y === 0)) + continue + + const width = node.measured?.width ?? FlowConfig.NODE_DEFAULT_WIDTH + const height = node.measured?.height ?? FlowConfig.NODE_MIN_HEIGHT + dagreGraph.setNode(node.id, { width: width, height: height }) + layoutableIds.add(node.id) } for (const edge of edges) { @@ -718,11 +783,10 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { }, []) const handleAutoLayout = useCallback(() => { - const flowStore = useFlowStore.getState() - const resetNodes = flowStore.nodes.map((node) => + const resetNodes = nodes.map((node) => node.type === 'frankNode' || node.type === 'exitNode' ? { ...node, position: { x: 0, y: 0 } } : node, ) - const laidOut = layoutGraph(resetNodes, flowStore.edges, 'LR') + const laidOut = layoutGraph(resetNodes, edges, 'LR') const nodeIds = laidOut .filter((node) => node.type === 'frankNode' || node.type === 'exitNode') @@ -731,48 +795,14 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { if (nodeIds.length === 0) return fitAfterLayoutRef.current = nodeIds - flowStore.setNodes(laidOut) - }, [layoutGraph]) - - useEffect(() => { - if (!nodesInitialized || !pendingInitialRelayoutRef.current) return - - const { pendingSelection } = pendingInitialRelayoutRef.current - pendingInitialRelayoutRef.current = null - - const flowStore = useFlowStore.getState() - const nodesWithResetPositions = flowStore.nodes.map((node) => - node.type === 'frankNode' || node.type === 'exitNode' ? { ...node, position: { x: 0, y: 0 } } : node, - ) - const laidOutNodes = layoutGraph(nodesWithResetPositions, flowStore.edges, 'LR') - flowStore.setNodesWithoutHistory(laidOutNodes) - - if (pendingSelection) { - applySelectionToNodes(pendingSelection) - } else { - waitForStableCanvasDimensions((canvasWidth, canvasHeight) => { - const freshViewport = computeAdapterCenteredViewport(laidOutNodes, canvasWidth, canvasHeight) - useFlowStore.getState().setViewport(freshViewport) - reactFlowRef.current?.setViewport(freshViewport) - }) - } - - flowStore.setHistory([]) - flowStore.setFuture([]) - }, [ - nodesInitialized, - relayoutNonce, - layoutGraph, - waitForStableCanvasDimensions, - computeAdapterCenteredViewport, - applySelectionToNodes, - ]) + setNodes(laidOut) + }, [edges, layoutGraph, nodes, setNodes]) const getFullySelectedGroupIds = useCallback( - (parentIds: (string | undefined)[], selectedNodes: FlowNode[]) => { + (parentIds: string[], selectedNodes: FlowNode[]) => { return parentIds.filter((parentId) => { const children = nodes.filter((node) => node.parentId === parentId) - return children.every((child) => selectedNodes.some((sn) => sn.id === child.id)) + return children.every((child) => selectedNodes.some((selectedNode) => selectedNode.id === child.id)) }) }, [nodes], @@ -782,46 +812,40 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { return selectedNodes.every((node) => node.parentId && node.parentId === selectedNodes[0].parentId) }, []) - const degroupNodes = useCallback( - (selectedNodes: FlowNode[], parentId: string | undefined, allNodes: FlowNode[]): FlowNode[] => { - const groupNode = allNodes.find((node) => node.id === parentId) - if (!groupNode) return allNodes + const degroupNodes = useCallback((selectedNodes: FlowNode[], parentId: string, allNodes: FlowNode[]): FlowNode[] => { + const groupNode = allNodes.find((node) => node.id === parentId) + if (!groupNode) return allNodes - const groupX = groupNode.position.x - const groupY = groupNode.position.y + const groupX = groupNode.position.x + const groupY = groupNode.position.y + const ungroupedNodes = [] - return allNodes - .map((node) => { - if (node.id === parentId) { - return null - } - - if (selectedNodes.includes(node) && node.parentId === parentId) { - return { - ...node, - parentId: undefined, - extent: undefined, - position: { - x: node.position.x + groupX, - y: node.position.y + groupY, - }, - } - } + for (const node of allNodes) { + if (node.id === parentId) continue + if (!selectedNodes.includes(node) && node.parentId !== parentId) ungroupedNodes.push(node) - return node - }) - .filter((node): node is FlowNode => node !== null) - }, - [], - ) + const ungroupedNode = { + ...node, + position: { + x: node.position.x + groupX, + y: node.position.y + groupY, + }, + parentId: undefined, + extent: undefined, + } + ungroupedNodes.push(ungroupedNode) + } + return ungroupedNodes + }, []) const handleDegroupSingleGroup = useCallback( (selectedNodes: FlowNode[]) => { - const parentId = selectedNodes[0].parentId! + const parentId = selectedNodes[0].parentId + if (!parentId) return const updatedNodes = degroupNodes(selectedNodes, parentId, nodes) - useFlowStore.getState().setNodes(updatedNodes) + setNodes(updatedNodes) }, - [nodes, degroupNodes], + [degroupNodes, nodes, setNodes], ) const shouldMergeUngroupedIntoGroup = useCallback( @@ -838,24 +862,65 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { [nodes], ) + const groupNodes = useCallback( + (nodesToGroup: FlowNode[], currentNodes: FlowNode[]) => { + const minX = Math.min(...nodesToGroup.map((node) => node.position.x)) + const minY = Math.min(...nodesToGroup.map((node) => node.position.y)) + const maxX = Math.max(...nodesToGroup.map((node) => node.position.x + (node.measured?.width ?? 0))) + const maxY = Math.max(...nodesToGroup.map((node) => node.position.y + (node.measured?.height ?? 0))) + + const padding = 30 + const width = maxX - minX + padding * 2 + const height = maxY - minY + padding * 2 + + const newGroupId = getNextNodeId() + + const groupNode: FlowNode = { + id: newGroupId, + position: { x: minX - padding, y: minY - padding }, + type: 'groupNode', + data: { label: 'New Group', width: width, height: height }, + dragHandle: '.drag-handle', + selectable: true, + } + + const updatedSelectedNodes: FlowNode[] = nodesToGroup.map((node) => ({ + ...node, + position: { + x: node.position.x - minX + padding, + y: node.position.y - minY + padding, + }, + parentId: newGroupId, + extent: 'parent', + selected: true, + })) + + const allNodes = [...currentNodes.filter((node) => !node.selected), groupNode, ...updatedSelectedNodes] + + setNodes(allNodes) + }, + [getNextNodeId, setNodes], + ) + const handleMergeUngroupedIntoGroup = useCallback( (selectedNodes: FlowNode[]) => { const parentId = selectedNodes.find((n) => n.parentId)?.parentId + if (!parentId) return const updatedNodes = degroupNodes(selectedNodes, parentId, nodes) const updatedSelectedNodes = updatedNodes.filter((node) => selectedNodes.some((selectedNode) => selectedNode.id === node.id), ) groupNodes(updatedSelectedNodes, updatedNodes) }, - [nodes, degroupNodes], + [degroupNodes, nodes, groupNodes], ) const handleMultiGroupMerge = useCallback( - (groupIds: (string | undefined)[], selectedNodes: FlowNode[]) => { + (groupIds: string[], selectedNodes: FlowNode[]) => { let updatedNodes = [...nodes] for (const parentId of groupIds) { const groupChildren = updatedNodes.filter((node) => node.parentId === parentId) - updatedNodes = degroupNodes(groupChildren, parentId!, updatedNodes) + updatedNodes = degroupNodes(groupChildren, parentId, updatedNodes) } const degroupedSelectedNodes = updatedNodes.filter((node) => @@ -864,14 +929,14 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { groupNodes(degroupedSelectedNodes, updatedNodes) }, - [nodes, degroupNodes], + [nodes, groupNodes, degroupNodes], ) const handleGrouping = useCallback(() => { const selectedNodes = nodes.filter((node) => node.selected) if (selectedNodes.length < 2) return - const parentIds = [...new Set(selectedNodes.map((node) => node.parentId).filter(Boolean))] + const parentIds = [...new Set(selectedNodes.map((node) => node.parentId).filter(Boolean))] as string[] const fullySelectedGroupIds = getFullySelectedGroupIds(parentIds, selectedNodes) if (fullySelectedGroupIds.length > 1) { @@ -892,57 +957,60 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { showNodeContextMenu(true) }, [ nodes, - allSelectedInSameGroup, getFullySelectedGroupIds, - handleMergeUngroupedIntoGroup, - handleMultiGroupMerge, + allSelectedInSameGroup, shouldMergeUngroupedIntoGroup, + groupNodes, showNodeContextMenu, + handleMultiGroupMerge, + handleMergeUngroupedIntoGroup, ]) const copySelection = useCallback(() => { - const selectedNodes = nodes.filter((n) => n.selected) + const selectedNodes = nodes.filter((node) => node.selected) if (selectedNodes.length === 0) return - const selectedNodeIds = new Set(selectedNodes.map((n) => n.id)) - const selectedEdges = edges.filter((e) => selectedNodeIds.has(e.source) && selectedNodeIds.has(e.target)) + const selectedNodeIds = new Set(selectedNodes.map((node) => node.id)) + const selectedEdges = edges.filter((edge) => selectedNodeIds.has(edge.source) && selectedNodeIds.has(edge.target)) const data = { nodes: selectedNodes, edges: selectedEdges } clipboardRef.current = data - navigator.clipboard.writeText(JSON.stringify(data)).catch(() => { - // clipboard write failed, ignore - }) - }, [nodes, edges]) - - const applyClipboardData = useCallback((clipboard: { nodes: FlowNode[]; edges: Edge[] }) => { - const flowStore = useFlowStore.getState() - const idMap = new Map() - const generateId = () => flowStore.getNextNodeId().toString() - - const newNodes: FlowNode[] = clipboard.nodes.map((node) => { - const cloned = cloneWithRemappedIds(node, idMap, generateId) - const remappedParentId = node.parentId ? idMap.get(node.parentId) : undefined - return { - ...cloned, - position: { - x: node.position.x + FlowConfig.COPY_PASTE_OFFSET, - y: node.position.y + FlowConfig.COPY_PASTE_OFFSET, - }, - parentId: remappedParentId, - extent: remappedParentId ? 'parent' : undefined, - selected: true, - } - }) + navigator.clipboard + .writeText(JSON.stringify(data)) + .catch(() => showWarningToast('Copy/paste may not work in this browser.', 'Failed to copy')) + }, [nodes, edges, showWarningToast]) + + const applyClipboardData = useCallback( + (clipboard: { nodes: FlowNode[]; edges: Edge[] }) => { + const idMap = new Map() + const generateId = (): string => getNextNodeId().toString() + + const newNodes: FlowNode[] = clipboard.nodes.map((node) => { + const cloned = cloneWithRemappedIds(node, idMap, generateId) + const remappedParentId = node.parentId ? idMap.get(node.parentId) : undefined + return { + ...cloned, + position: { + x: node.position.x + FlowConfig.COPY_PASTE_OFFSET, + y: node.position.y + FlowConfig.COPY_PASTE_OFFSET, + }, + parentId: remappedParentId, + extent: remappedParentId ? 'parent' : undefined, + selected: true, + } + }) - const newEdges: Edge[] = clipboard.edges.map((edge) => cloneWithRemappedIds(edge, idMap, generateId)) + const newEdges: Edge[] = clipboard.edges.map((edge) => cloneWithRemappedIds(edge, idMap, generateId)) - const deselectedNodes = flowStore.nodes.map((n) => ({ ...n, selected: false })) - const deselectedEdges = flowStore.edges.map((e) => ({ ...e, selected: false })) + const deselectedNodes = nodes.map((node) => ({ ...node, selected: false })) + const deselectedEdges = edges.map((edge) => ({ ...edge, selected: false })) - flowStore.setNodes([...deselectedNodes, ...newNodes]) - flowStore.setEdges([...deselectedEdges, ...newEdges]) - }, []) + setNodes([...deselectedNodes, ...newNodes]) + setEdges([...deselectedEdges, ...newEdges]) + }, + [edges, getNextNodeId, nodes, setEdges, setNodes], + ) const pasteSelection = useCallback(() => { navigator.clipboard @@ -955,41 +1023,22 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { return } } catch { - // text is not valid JSON, fall through to clipboard ref - } - - if (clipboardRef.current) applyClipboardData(clipboardRef.current) - }) - .catch(() => { - if (clipboardRef.current) applyClipboardData(clipboardRef.current) - }) - }, [applyClipboardData]) - - function closeEditNodeContextOnEscape(): void { - const { isNewNode, nodeId, parentId } = useNodeContextStore.getState() - - if (isNewNode) { - if (parentId) { - useFlowStore.getState().deleteChild(parentId, nodeId.toString()) - } else { - useFlowStore.getState().deleteNode(nodeId.toString()) - } - useNodeContextStore.getState().setIsNewNode(false) - } + // text is not valid JSON, fall through to clipboard ref + } - showNodeContextMenu(false) - setIsEditing(false) - setIsMultiSelect(false) - setParentId(null) - setChildParentId(null) - } + if (clipboardRef.current) applyClipboardData(clipboardRef.current) + }) + .catch(() => { + if (clipboardRef.current) applyClipboardData(clipboardRef.current) + }) + }, [applyClipboardData]) const deleteSelection = useCallback((): boolean => { if (isEditing) return false const { parentId: storeParentId, nodeId: storeNodeId } = useNodeContextStore.getState() if (storeParentId !== null) { - useFlowStore.getState().deleteChild(storeParentId, storeNodeId.toString()) + deleteChild(storeParentId, storeNodeId.toString()) useNodeContextStore.getState().setParentId(null) useNodeContextStore.getState().setChildParentId(null) useNodeContextStore.getState().setNodeId(0) @@ -997,9 +1046,8 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { return true } - const { nodes, edges, setNodes, setEdges } = useFlowStore.getState() - const selectedNodeIds = new Set(nodes.filter((n) => n.selected).map((n) => n.id)) - const hasSelection = selectedNodeIds.size > 0 || edges.some((e) => e.selected) + const selectedNodeIds = new Set(nodes.filter((node) => node.selected).map((n) => n.id)) + const hasSelection = selectedNodeIds.size > 0 || edges.some((edge) => edge.selected) if (!hasSelection) return false const { selectedStickyId } = useNodeContextStore.getState() @@ -1008,10 +1056,12 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { showNodeContextMenu(false) } - setNodes(nodes.filter((n) => !n.selected)) - setEdges(edges.filter((e) => !e.selected && !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target))) + setNodes(nodes.filter((node) => !node.selected)) + setEdges( + edges.filter((edge) => !edge.selected && !selectedNodeIds.has(edge.source) && !selectedNodeIds.has(edge.target)), + ) return true - }, [isEditing, showNodeContextMenu]) + }, [deleteChild, edges, isEditing, nodes, setEdges, setNodes, showNodeContextMenu]) const toggleSelectedHidden = useCallback(() => { const selected = useFlowStore @@ -1020,38 +1070,23 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { if (selected.length === 0) return false const shouldHide = selected.some((node) => !node.data.hiddenForwards) - useFlowStore.getState().setNodesHiddenForwards( + setNodesHiddenForwards( selected.map((node) => node.id), shouldHide, ) - }, []) + }, [setNodesHiddenForwards]) - useShortcut({ - 'studio.copy': () => copySelection(), - 'studio.paste': () => pasteSelection(), - 'studio.cut': () => cutSelection(), - 'studio.undo': () => useFlowStore.getState().undo(), - 'studio.redo': () => useFlowStore.getState().redo(), - 'studio.redo-alt': () => useFlowStore.getState().redo(), - 'studio.group': () => handleGrouping(), - 'studio.ungroup': () => handleUngroup(), - 'studio.hide': () => toggleSelectedHidden(), - 'studio.save': () => void saveFlow(), - 'studio.close-context': () => closeEditNodeContextOnEscape(), - 'studio.delete': () => deleteSelection(), - 'studio.show-in-editor': () => showSelectedNodeInEditor(), - }) + const handleNodeDragStop = useCallback( + (_event: MouseEvent | TouchEvent, node: FlowNode) => { + if (!isStickyNote(node)) return + const nearest = findNearestFrankNode(node as StickyNote, nodes) + if (!nearest) return - const handleNodeDragStop = useCallback((_event: MouseEvent | TouchEvent, node: FlowNode) => { - if (!isStickyNote(node)) return - - const flowStore = useFlowStore.getState() - const nearest = findNearestFrankNode(node as StickyNote, flowStore.nodes) - if (!nearest) return - - flowStore.setStickyAttachment(node.id, nearest.id) - void useNodeContextStore.getState().saveFlow?.() - }, []) + setStickyAttachment(node.id, nearest.id) + void useNodeContextStore.getState().saveFlow?.() + }, + [nodes, setStickyAttachment], + ) const lookupFrankElement = useCallback( (subtype: string) => { @@ -1091,7 +1126,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { const handleNodeClick = useCallback( (event: React.MouseEvent, node: FlowNode) => { - if (event.shiftKey || event.ctrlKey || event.metaKey || isDirty) return + if (isDirty || event.shiftKey || event.ctrlKey || event.metaKey) return if (node.type === 'stickyNote') { setSelectedStickyId(node.id) @@ -1227,43 +1262,6 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { ], ) - const groupNodes = (nodesToGroup: FlowNode[], currentNodes: FlowNode[]) => { - const minX = Math.min(...nodesToGroup.map((node) => node.position.x)) - const minY = Math.min(...nodesToGroup.map((node) => node.position.y)) - const maxX = Math.max(...nodesToGroup.map((node) => node.position.x + (node.measured?.width ?? 0))) - const maxY = Math.max(...nodesToGroup.map((node) => node.position.y + (node.measured?.height ?? 0))) - - const padding = 30 - const width = maxX - minX + padding * 2 - const height = maxY - minY + padding * 2 - - const newGroupId = useFlowStore.getState().getNextNodeId() - - const groupNode: FlowNode = { - id: newGroupId, - position: { x: minX - padding, y: minY - padding }, - type: 'groupNode', - data: { label: 'New Group', width: width, height: height }, - dragHandle: '.drag-handle', - selectable: true, - } - - const updatedSelectedNodes: FlowNode[] = nodesToGroup.map((node) => ({ - ...node, - position: { - x: node.position.x - minX + padding, - y: node.position.y - minY + padding, - }, - parentId: newGroupId, - extent: 'parent', - selected: true, - })) - - const allNodes = [...currentNodes.filter((node) => !node.selected), groupNode, ...updatedSelectedNodes] - - useFlowStore.getState().setNodes(allNodes) - } - const onDragOver = useCallback( (event: React.DragEvent) => { event.preventDefault() @@ -1272,7 +1270,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { [allowedOnCanvas], ) - const onDrop = (event: React.DragEvent) => { + const onDrop = (event: React.DragEvent): void => { event.preventDefault() setDraggedName(null) setParentId(null) @@ -1297,7 +1295,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { addNodeAtPosition(position, parsedData.name) } - const onDragEnd = () => { + const onDragEnd = (): void => { setDraggedName(null) setParentId(null) } @@ -1306,7 +1304,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { position: { x: number; y: number }, elementName: string, sourceInfo?: { nodeId: string | null; handleId: string | null; handleType: 'source' | 'target' | null }, - ) { + ): void { showNodeContextMenu(true) setIsNewNode(true) setEditingSubtype(elementName) @@ -1344,7 +1342,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { if (sourceInfo?.nodeId && sourceInfo.handleType === 'source') { const sourceNode = flowStore.nodes.find((node) => node.id === sourceInfo.nodeId) - if (reactFlow.getZoom() < FlowConfig.ZOOM_THRESHOLD && sourceNode && isFrankNode(sourceNode)) { + if (sourceNode && reactFlow.getZoom() < FlowConfig.ZOOM_THRESHOLD && isFrankNode(sourceNode)) { if (edgeDropHandleType) { const existingHandle = sourceNode.data.sourceHandles.find((handle) => handle.type === edgeDropHandleType) if (existingHandle) { @@ -1400,10 +1398,9 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { const addStickyNote = useCallback( (flowPos: { x: number; y: number }) => { - const flowStore = useFlowStore.getState() - const newId = flowStore.getNextNodeId() + const newId = getNextNodeId() - const deselectedNodes = flowStore.nodes.map((node) => ({ + const deselectedNodes = nodes.map((node) => ({ ...node, selected: false, })) @@ -1420,37 +1417,35 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { }, } - flowStore.setNodes([...deselectedNodes, stickyNote]) + setNodes([...deselectedNodes, stickyNote]) setSelectedStickyId(newId) showNodeContextMenu(true) }, - [setSelectedStickyId, showNodeContextMenu], + [getNextNodeId, nodes, setNodes, setSelectedStickyId, showNodeContextMenu], ) const cutSelection = useCallback(() => { copySelection() - const flowStore = useFlowStore.getState() - const selectedNodes = flowStore.nodes.filter((n) => n.selected) + const selectedNodes = nodes.filter((node) => node.selected) if (selectedNodes.length === 0) return - const selectedNodeIds = new Set(selectedNodes.map((n) => n.id)) - const remainingNodes = flowStore.nodes.filter((n) => !selectedNodeIds.has(n.id)) - const remainingEdges = flowStore.edges.filter( - (e) => !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target), + const selectedNodeIds = new Set(selectedNodes.map((node) => node.id)) + const remainingNodes = nodes.filter((node) => !selectedNodeIds.has(node.id)) + const remainingEdges = edges.filter( + (edge) => !selectedNodeIds.has(edge.source) && !selectedNodeIds.has(edge.target), ) - flowStore.setNodes(remainingNodes) - flowStore.setEdges(remainingEdges) - }, [copySelection]) + setNodes(remainingNodes) + setEdges(remainingEdges) + }, [copySelection, edges, nodes, setEdges, setNodes]) const handleUngroup = useCallback(() => { - const flowStore = useFlowStore.getState() - const selectedNodes = flowStore.nodes.filter((n) => n.selected) + const selectedNodes = nodes.filter((node) => node.selected) if (selectedNodes.length === 0) return - const selectedGroupNodes = selectedNodes.filter((n) => n.type === 'groupNode') + const selectedGroupNodes = selectedNodes.filter((node) => node.type === 'groupNode') if (selectedGroupNodes.length > 0) { - let updatedNodes = [...flowStore.nodes] + let updatedNodes = [...nodes] for (const groupNode of selectedGroupNodes) { const groupId = groupNode.id @@ -1458,20 +1453,17 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { updatedNodes = degroupNodes(children, groupId, updatedNodes) } - flowStore.setNodes(updatedNodes) + setNodes(updatedNodes) return } if (!allSelectedInSameGroup(selectedNodes)) return handleDegroupSingleGroup(selectedNodes) - }, [allSelectedInSameGroup, handleDegroupSingleGroup, degroupNodes]) + }, [nodes, allSelectedInSameGroup, handleDegroupSingleGroup, setNodes, degroupNodes]) const showSelectedNodeInEditor = useCallback(() => { - const flowStore = useFlowStore.getState() - const selectedFrankNodes = flowStore.nodes.filter( - (node) => node.selected && node.type === 'frankNode', - ) as FrankNodeType[] + const selectedFrankNodes = nodes.filter((node) => node.selected && node.type === 'frankNode') as FrankNodeType[] if (selectedFrankNodes.length !== 1) return const { data: nodeData } = selectedFrankNodes[0] @@ -1483,7 +1475,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { name: nodeData.name, filepath: tabData.configurationPath, }) - }, [navigate]) + }, [navigate, nodes]) const handleRightMouseButtonClick = useCallback( (event: React.MouseEvent) => { @@ -1495,169 +1487,177 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { }, [reactFlow], ) - - useEffect(() => { - function stripMeasuredDimensions(nodes: FlowNode[]): FlowNode[] { - return nodes.map((node) => { - if (!('measured' in node)) return node - const { measured: _measured, ...nodeWithoutMeasured } = node as FlowNode & { measured?: unknown } - return nodeWithoutMeasured as FlowNode - }) - } - - function restoreFlowFromTab(tab: TabData, options: { skipViewport: boolean; forceRemeasure: boolean }) { - const flowStore = useFlowStore.getState() - const flowJson = tab.flowJson - - if (flowJson) { - let nodes = Array.isArray(flowJson.nodes) ? flowJson.nodes : [] - if (options.forceRemeasure) { - nodes = stripMeasuredDimensions(nodes) - } - flowStore.setNodes(nodes) - flowStore.setEdges(Array.isArray(flowJson.edges) ? flowJson.edges : []) - - if (!options.skipViewport) { - const savedViewport = flowJson.viewport as { x: number; y: number; zoom: number } | undefined - const targetViewport = savedViewport ?? { x: 0, y: 0, zoom: 1 } - flowStore.setViewport(targetViewport) - requestAnimationFrame(() => reactFlowRef.current?.setViewport(targetViewport)) + useCallback( + ({ + sourceNode, + sourceNodeId, + newId, + position, + }: { + sourceNode: FrankNodeType + sourceNodeId: string + newId: string + position: { x: number; y: number } + }) => { + if (edgeDropHandleType) { + const existingHandle = sourceNode.data.sourceHandles.find((handle) => handle.type === edgeDropHandleType) + if (existingHandle) { + onConnect({ + source: sourceNodeId, + sourceHandle: existingHandle.index.toString(), + target: newId.toString(), + targetHandle: null, + }) + } else { + const newIndex = sourceNode.data.sourceHandles.length + 1 + addFlowHandle(sourceNodeId, { type: edgeDropHandleType, index: newIndex }) + onConnect({ + source: sourceNodeId, + sourceHandle: newIndex.toString(), + target: newId.toString(), + targetHandle: null, + }) } - - flowStore.setHistory(tab.history ?? []) - flowStore.setFuture(tab.future ?? []) + setEdgeDropHandleType(null) } else { - clearFlow() + setPendingCompactConnection({ + connection: { + source: sourceNodeId, + sourceHandle: null, + target: newId.toString(), + targetHandle: null, + }, + sourceNodeSubtype: sourceNode.data.subtype, + position: reactFlow.flowToScreenPosition(position), + }) } - } + }, + [addFlowHandle, edgeDropHandleType, onConnect, reactFlow], + ) + /* useEffect */ - function clearFlow() { - const flowStore = useFlowStore.getState() - flowStore.resetStore() - } + useEffect(() => { + if (!activeTab) return + const tabData = getTab(activeTab) + if (!tabData) return - function loadFromCache( - tab: TabData, - pendingSelection: { subtype: string; name: string } | null, - recenter: boolean, - ) { - const hasPendingSelection = !!pendingSelection - restoreFlowFromTab(tab, { - skipViewport: hasPendingSelection || recenter, - forceRemeasure: hasPendingSelection, - }) - if (pendingSelection) { - applySelectionToNodes(pendingSelection) - } else if (recenter) { - const cachedNodes = useFlowStore.getState().nodes - waitForStableCanvasDimensions((canvasWidth, canvasHeight) => { - const viewport = computeAdapterCenteredViewport(cachedNodes, canvasWidth, canvasHeight) - useFlowStore.getState().setViewport(viewport) - reactFlowRef.current?.setViewport(viewport) - }) - } - } + const unsubscribe = useTabStore.subscribe( + (state) => state.activeTab, + async (newTab, oldTab) => { + if (!newTab) { + resetFlowStore() + return + } + if (oldTab) saveFlowToTab(oldTab) + const activeTab = getTab(newTab) + if (activeTab) await loadFlowFromTab(activeTab) + }, + ) - async function loadFromApi(tab: TabData, pendingSelection: { subtype: string; name: string } | null) { - const flowStore = useFlowStore.getState() - const currentProject = useProjectStore.getState().project - if (!currentProject) return + return (): void => unsubscribe() + }, [activeTab, getTab, loadFlowFromTab, resetFlowStore, saveFlowToTab]) - const adapter = await getAdapterFromConfiguration( - currentProject.name, - tab.configurationPath, - tab.name!, - tab.adapterPosition, - ) - if (!adapter) return - const adapterJson = await convertAdapterXmlToJson(adapter) + useEffect(() => { + return (): void => { + if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current) + } + }, []) - flowStore.setEdges(adapterJson.edges) - flowStore.setNodes(adapterJson.nodes) - flowStore.setHistory([]) - flowStore.setFuture([]) - pendingInitialRelayoutRef.current = { pendingSelection } - setRelayoutNonce((nonce) => nonce + 1) + useEffect(() => { + if (nodes.length > 0 && !isLoadingTabRef.current) { + scheduleAutoSave() } + }, [nodes, edges, scheduleAutoSave]) - async function loadFlowFromTab(tab: TabData) { - const tabId = useTabStore.getState().activeTab - const pendingSelection = tab.pendingNodeSelection ?? null - const recenter = tab.pendingRecenter ?? false + useEffect(() => { + if (!fitAfterLayoutRef.current) return + const nodeIds = fitAfterLayoutRef.current + fitAfterLayoutRef.current = null + requestAnimationFrame(() => { + reactFlowRef.current?.fitView({ nodes: nodeIds, padding: 0.15, duration: 300 }) + }) + }, [nodes]) - if (tabId === loadedTabIdRef.current && !pendingSelection && !recenter) return - loadedTabIdRef.current = tabId + useEffect(() => { + const nodeId = pendingFitViewRef.current + if (!nodeId) return - isLoadingTabRef.current = true - setLoading(true) + const targetNode = nodes.find((node) => node.id === nodeId) + if (!targetNode?.measured?.width || !targetNode?.measured?.height) return + if (!targetNode.selected) return - if (pendingSelection || recenter) { - useTabStore.getState().setTabData(tabId, { ...tab, pendingNodeSelection: null, pendingRecenter: null }) - } + pendingFitViewRef.current = null - try { - const hasCachedFlow = tab.flowJson && Object.keys(tab.flowJson).length > 0 - if (hasCachedFlow) { - loadFromCache(tab, pendingSelection, recenter) - } else if (tab.configurationPath && tab.name) { - await loadFromApi(tab, pendingSelection) - } - } catch (error) { - logApiError('Error loading tab flow:', error as Error) - } finally { - isLoadingTabRef.current = false - setLoading(false) - } - } + waitForStableCanvasDimensions((canvasWidth, canvasHeight) => { + const reactFlowInstance = reactFlowRef.current + if (!reactFlowInstance) return - function saveFlowToTab(tabId: string) { - const tabStore = useTabStore.getState() - const flowStore = useFlowStore.getState() + const node = nodes.find((node) => node.id === nodeId) + if (!node?.measured?.width || !node?.measured?.height) return - const tabData = tabStore.getTab(tabId) - if (!tabData) return + const { absoluteX, absoluteY } = computeAbsoluteNodePosition(node, nodes) - tabStore.setTabData(tabId, { - ...tabData, - flowJson: { - nodes: flowStore.nodes, - edges: flowStore.edges, - viewport: flowStore.viewport, - }, - history: flowStore.history, - future: flowStore.future, - }) - } + const viewport = computeNodeCenteredViewport( + absoluteX, + absoluteY, + node.measured.width, + node.measured.height, + canvasWidth, + canvasHeight, + ) - const tabStore = useTabStore.getState() - const currentActiveTabKey = tabStore.activeTab + reactFlowInstance.setViewport(viewport, { duration: 400 }) + }) + }, [nodes, waitForStableCanvasDimensions]) - if (currentActiveTabKey) { - const activeTab = tabStore.getTab(currentActiveTabKey) - if (activeTab) { - loadFlowFromTab(activeTab) + useEffect(() => { + useNodeContextStore.getState().registerSaveFlow(async () => { + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current) + autoSaveTimerRef.current = null } - } - - const unsubscribe = useTabStore.subscribe( - (state) => state.activeTab, - async (newTab, oldTab) => { - if (!newTab) { - clearFlow() - return - } + await saveFlow() + }) + return (): void => useNodeContextStore.getState().registerSaveFlow(null) + }, [saveFlow]) - if (oldTab) saveFlowToTab(oldTab) + useEffect(() => { + if (!nodesInitialized || !pendingInitialRelayoutRef.current) return - const activeTab = useTabStore.getState().getTab(newTab) - if (!activeTab) return + const { pendingSelection } = pendingInitialRelayoutRef.current + pendingInitialRelayoutRef.current = null - await loadFlowFromTab(activeTab) - }, + const nodesWithResetPositions = nodes.map((node) => + node.type === 'frankNode' || node.type === 'exitNode' ? { ...node, position: { x: 0, y: 0 } } : node, ) + const laidOutNodes = layoutGraph(nodesWithResetPositions, edges, 'LR') + setNodesWithoutHistory(laidOutNodes) + + if (pendingSelection) { + applySelectionToNodes(pendingSelection) + } else { + waitForStableCanvasDimensions((canvasWidth, canvasHeight) => { + const freshViewport = computeAdapterCenteredViewport(laidOutNodes, canvasWidth, canvasHeight) + setViewport(freshViewport) + reactFlowRef.current?.setViewport(freshViewport) + }) + } - return () => unsubscribe() - }, [layoutGraph, computeAdapterCenteredViewport, applySelectionToNodes, waitForStableCanvasDimensions]) + setHistory([]) + setFuture([]) + }, [ + nodesInitialized, + layoutGraph, + waitForStableCanvasDimensions, + computeAdapterCenteredViewport, + applySelectionToNodes, + nodes, + edges, + setNodesWithoutHistory, + setHistory, + setFuture, + setViewport, + ]) useEffect(() => { const unsub = useFlowStore.subscribe( @@ -1666,26 +1666,26 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { const { selectedStickyId, setSelectedStickyId, selectedGroupId, setSelectedGroupId, setIsEditing } = useNodeContextStore.getState() - if (selectedStickyId && !nodes.some((node) => node.id === selectedStickyId)) { + if (selectedStickyId && nodes.every((node) => node.id !== selectedStickyId)) { setSelectedStickyId(null) showNodeContextMenu(false) setIsEditing(false) } - if (selectedGroupId && !nodes.some((node) => node.id === selectedGroupId)) { + if (selectedGroupId && nodes.every((node) => node.id !== selectedGroupId)) { setSelectedGroupId(null) showNodeContextMenu(false) } }, ) - return () => unsub() + return (): void => unsub() }, [showNodeContextMenu]) useEffect(() => { const unsub = useFlowStore.subscribe( (state) => state.nodes, // selector: subscribe only to nodes (newNodes, oldNodes) => { - if (!reactFlowRef.current || !oldNodes) return + if (!oldNodes || !reactFlowRef.current) return // Compare old vs new node data for (const newNode of newNodes) { @@ -1699,9 +1699,72 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { }, ) - return () => unsub() + return (): void => unsub() }, [updateNodeInternals]) + /* TODO ????? */ + /* + * useEffect(() => { + * showNodeContextMenuRef.current = showNodeContextMenu + * }, [showNodeContextMenu]) + * reactFlowRef.current = reactFlow + */ + + /* Functions */ + + function handleConnectStart(_: MouseEvent | TouchEvent, parameters: OnConnectStartParams): void { + sourceInfoReference.current = { + nodeId: parameters.nodeId, + handleId: parameters.handleId, + handleType: parameters.handleType, + } + } + + function handleConnectEnd(event: MouseEvent | TouchEvent, connectionState: FinalConnectionState): void { + const mouseEvent = event as MouseEvent + if (!connectionState.isValid) { + const zoom = reactFlow.getZoom() + if (zoom < FlowConfig.ZOOM_THRESHOLD && sourceInfoReference.current.handleType === 'source') { + const sourceNode = nodes.find((node) => node.id === sourceInfoReference.current.nodeId) + if (sourceNode && isFrankNode(sourceNode)) { + setPendingEdgeDrop({ + position: { x: mouseEvent.clientX, y: mouseEvent.clientY }, + sourceNodeSubtype: sourceNode.data.subtype, + }) + return + } + } + handleEdgeDropOnCanvas(mouseEvent.clientX, mouseEvent.clientY) + } + } + + function handleEdgeDropOnCanvas(x: number, y: number): void { + const { screenToFlowPosition } = reactFlow + const flowPositions = screenToFlowPosition({ x: x, y: y }) + + setEdgeDropPositions(flowPositions) + setShowCreateNodeModal(true) + } + + function closeEditNodeContextOnEscape(): void { + const { isNewNode, nodeId, parentId } = useNodeContextStore.getState() + + if (isNewNode) { + if (parentId) { + deleteChild(parentId, nodeId.toString()) + } else { + deleteNode(nodeId.toString()) + } + useNodeContextStore.getState().setIsNewNode(false) + } + + showNodeContextMenu(false) + setIsEditing(false) + setIsMultiSelect(false) + setParentId(null) + setChildParentId(null) + } + return (
void }) { { - useFlowStore.getState().setViewport(viewPort) + onViewportChange={(viewport_) => { + setViewport(viewport_) }} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} @@ -1876,16 +1939,3 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) {
) } - -export default function Flow({ - showNodeContextMenu, - onOpenInEditor, -}: Readonly<{ showNodeContextMenu: (b: boolean) => void; onOpenInEditor: () => void }>) { - return ( - - - - - - ) -} diff --git a/src/main/frontend/app/routes/studio/canvas/edges.ts b/src/main/frontend/app/routes/studio/canvas-flow/edges.ts similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/edges.ts rename to src/main/frontend/app/routes/studio/canvas-flow/edges.ts diff --git a/src/main/frontend/app/routes/studio/canvas/edgetypes/edge-label.tsx b/src/main/frontend/app/routes/studio/canvas-flow/edgetypes/edge-label.tsx similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/edgetypes/edge-label.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/edgetypes/edge-label.tsx diff --git a/src/main/frontend/app/routes/studio/canvas/edgetypes/frank-edge.tsx b/src/main/frontend/app/routes/studio/canvas-flow/edgetypes/frank-edge.tsx similarity index 99% rename from src/main/frontend/app/routes/studio/canvas/edgetypes/frank-edge.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/edgetypes/frank-edge.tsx index aa52e602..ad880540 100644 --- a/src/main/frontend/app/routes/studio/canvas/edgetypes/frank-edge.tsx +++ b/src/main/frontend/app/routes/studio/canvas-flow/edgetypes/frank-edge.tsx @@ -1,7 +1,7 @@ import { BaseEdge, EdgeLabelRenderer, getBezierPath, type Position, useReactFlow, useStore } from '@xyflow/react' import { type JSX, type MouseEvent, useEffect, useRef, useState } from 'react' import useFlowStore from '~/stores/flow-store' -import { FlowConfig, getCompactLabelScale } from '~/routes/studio/canvas/flow.config' +import { FlowConfig, getCompactLabelScale } from '~/routes/studio/canvas-flow/flow.config' import { getEdgeLabelPositions } from '~/utils/edge-label-utils' import EdgeLabel from './edge-label' diff --git a/src/main/frontend/app/routes/studio/canvas/flow.config.ts b/src/main/frontend/app/routes/studio/canvas-flow/flow.config.ts similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/flow.config.ts rename to src/main/frontend/app/routes/studio/canvas-flow/flow.config.ts diff --git a/src/main/frontend/app/routes/studio/canvas/node-context-menu-context.ts b/src/main/frontend/app/routes/studio/canvas-flow/node-context-menu-context.ts similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/node-context-menu-context.ts rename to src/main/frontend/app/routes/studio/canvas-flow/node-context-menu-context.ts diff --git a/src/main/frontend/app/routes/studio/canvas/nodes.ts b/src/main/frontend/app/routes/studio/canvas-flow/nodes.ts similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/nodes.ts rename to src/main/frontend/app/routes/studio/canvas-flow/nodes.ts diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/child-node.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/child-node.tsx similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/child-node.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/child-node.tsx diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/deprecated-popover.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/deprecated-popover.tsx similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/components/deprecated-popover.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/deprecated-popover.tsx diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle-menu-item.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/handle-menu-item.tsx similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle-menu-item.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/handle-menu-item.tsx diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle-menu.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/handle-menu.tsx similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle-menu.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/handle-menu.tsx diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/handle.tsx similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/handle.tsx diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/missing-requirements.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/missing-requirements.tsx similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/components/missing-requirements.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/missing-requirements.tsx diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/node-children-container.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/node-children-container.tsx similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/components/node-children-container.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/node-children-container.tsx diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/node-header.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/node-header.tsx similarity index 100% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/components/node-header.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/components/node-header.tsx diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/exit-node.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/exit-node.tsx similarity index 94% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/exit-node.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/exit-node.tsx index 5f6b0fa5..852bf9f8 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/exit-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/exit-node.tsx @@ -8,8 +8,8 @@ import { useUpdateNodeInternals, } from '@xyflow/react' import { type JSX, useEffect } from 'react' -import { ResizeIcon } from '~/routes/studio/canvas/nodetypes/frank-node' -import { FlowConfig } from '~/routes/studio/canvas/flow.config' +import { ResizeIcon } from '~/routes/studio/canvas-flow/nodetypes/frank-node' +import { FlowConfig } from '~/routes/studio/canvas-flow/flow.config' import { useSettingsStore } from '~/stores/settings-store' import { NodeHeader } from './components/node-header' import ZoomedOutNode from './zoomed-out-node' diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/frank-node.tsx similarity index 99% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/frank-node.tsx index 92d0d46d..2ad22b18 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/frank-node.tsx @@ -16,9 +16,9 @@ import DangerIcon from '../../../../../icons/solar/Danger Triangle.svg?react' import { type JSX, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useShallow } from 'zustand/react/shallow' import useFlowStore, { isExitNode, isFrankNode } from '~/stores/flow-store' -import { CustomHandle } from '~/routes/studio/canvas/nodetypes/components/handle' -import { FlowConfig } from '~/routes/studio/canvas/flow.config' -import { useNodeContextMenu } from '~/routes/studio/canvas/node-context-menu-context' +import { CustomHandle } from '~/routes/studio/canvas-flow/nodetypes/components/handle' +import { FlowConfig } from '~/routes/studio/canvas-flow/flow.config' +import { useNodeContextMenu } from '~/routes/studio/canvas-flow/node-context-menu-context' import useNodeContextStore from '~/stores/node-context-store' import { getElementTypeFromName } from '~/routes/studio/node-translator-module' import { useSettingsStore } from '~/stores/settings-store' diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/group-node.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/group-node.tsx similarity index 97% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/group-node.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/group-node.tsx index 981fbb12..c7a786a7 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/group-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/group-node.tsx @@ -1,6 +1,6 @@ import { type Node, type NodeProps, NodeResizeControl, type ResizeDragEvent, type ResizeParams } from '@xyflow/react' import { type JSX, useState } from 'react' -import { ResizeIcon } from '~/routes/studio/canvas/nodetypes/frank-node' +import { ResizeIcon } from '~/routes/studio/canvas-flow/nodetypes/frank-node' export const GROUP_COLORS = [ { label: 'Blue', value: 'var(--group-color-blue)' }, diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/sticky-note.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/sticky-note.tsx similarity index 96% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/sticky-note.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/sticky-note.tsx index 51c8a8dd..646cd870 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/sticky-note.tsx +++ b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/sticky-note.tsx @@ -1,9 +1,9 @@ import { type Node, type NodeProps, NodeResizeControl, useUpdateNodeInternals } from '@xyflow/react' -import { FlowConfig } from '~/routes/studio/canvas/flow.config' -import { ResizeIcon } from '~/routes/studio/canvas/nodetypes/frank-node' +import { FlowConfig } from '~/routes/studio/canvas-flow/flow.config' +import { ResizeIcon } from '~/routes/studio/canvas-flow/nodetypes/frank-node' import { type JSX, useEffect, useLayoutEffect, useRef, useState } from 'react' import useFlowStore from '~/stores/flow-store' -import { useNodeContextMenu } from '~/routes/studio/canvas/node-context-menu-context' +import { useNodeContextMenu } from '~/routes/studio/canvas-flow/node-context-menu-context' import useNodeContextStore from '~/stores/node-context-store' export const STICKY_NOTE_COLORS = [ diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/zoomed-out-node.tsx b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/zoomed-out-node.tsx similarity index 99% rename from src/main/frontend/app/routes/studio/canvas/nodetypes/zoomed-out-node.tsx rename to src/main/frontend/app/routes/studio/canvas-flow/nodetypes/zoomed-out-node.tsx index ab430fff..34d56471 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/zoomed-out-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas-flow/nodetypes/zoomed-out-node.tsx @@ -1,6 +1,6 @@ import { Handle, Position, useStore } from '@xyflow/react' import type { JSX } from 'react' -import { FlowConfig, getCompactLabelScale } from '~/routes/studio/canvas/flow.config' +import { FlowConfig, getCompactLabelScale } from '~/routes/studio/canvas-flow/flow.config' const COMPACT_INITIALS_BOX_SIZE = 160 const COMPACT_PADDING_TOP = 8 diff --git a/src/main/frontend/app/routes/studio/context/context-input.tsx b/src/main/frontend/app/routes/studio/context/context-input.tsx index 5734973d..b7f4b1e7 100644 --- a/src/main/frontend/app/routes/studio/context/context-input.tsx +++ b/src/main/frontend/app/routes/studio/context/context-input.tsx @@ -6,7 +6,7 @@ import { useJavadocTransform } from '@frankframework/doc-library-react' import ContextInputField from './context-input-field' import type { Attribute, Elements } from '@frankframework/doc-library-core' import type { DeprecatedInfo } from './deprecated-list-popover' -import { DeprecatedPopover } from '../canvas/nodetypes/components/deprecated-popover' +import { DeprecatedPopover } from '~/routes/studio/canvas-flow/nodetypes/components/deprecated-popover' export type ContextInputProperties = { id: string diff --git a/src/main/frontend/app/routes/studio/context/group-context.tsx b/src/main/frontend/app/routes/studio/context/group-context.tsx index 06e5e095..abfff8da 100644 --- a/src/main/frontend/app/routes/studio/context/group-context.tsx +++ b/src/main/frontend/app/routes/studio/context/group-context.tsx @@ -1,7 +1,7 @@ import type { JSX } from 'react' -import type { FlowNode } from '~/routes/studio/canvas/flow' +import type { FlowNode } from '~/routes/studio/canvas-flow/canvas-flow' import useFlowStore, { isGroupNode } from '~/stores/flow-store' -import { GROUP_COLORS, GROUP_DEFAULT_COLOR } from '~/routes/studio/canvas/nodetypes/group-node' +import { GROUP_COLORS, GROUP_DEFAULT_COLOR } from '~/routes/studio/canvas-flow/nodetypes/group-node' import { ALL_SHORTCUTS, formatShortcutParts, type Platform, useShortcutStore } from '~/stores/shortcut-store' import Button from '~/components/inputs/button' import Input from '~/components/inputs/input' diff --git a/src/main/frontend/app/routes/studio/context/sticky-note-context.tsx b/src/main/frontend/app/routes/studio/context/sticky-note-context.tsx index bead120f..23a2c5d5 100644 --- a/src/main/frontend/app/routes/studio/context/sticky-note-context.tsx +++ b/src/main/frontend/app/routes/studio/context/sticky-note-context.tsx @@ -1,8 +1,8 @@ import type { JSX } from 'react' -import type { FlowNode } from '~/routes/studio/canvas/flow' -import type { FrankNodeType } from '~/routes/studio/canvas/nodetypes/frank-node' +import type { FlowNode } from '~/routes/studio/canvas-flow/canvas-flow' +import type { FrankNodeType } from '~/routes/studio/canvas-flow/nodetypes/frank-node' import useFlowStore, { isFrankNode, isStickyNote } from '~/stores/flow-store' -import { STICKY_NOTE_COLORS } from '~/routes/studio/canvas/nodetypes/sticky-note' +import { STICKY_NOTE_COLORS } from '~/routes/studio/canvas-flow/nodetypes/sticky-note' import { useShallow } from 'zustand/react/shallow' import Dropdown from '~/components/inputs/dropdown' diff --git a/src/main/frontend/app/routes/studio/flow-to-xml-parser.ts b/src/main/frontend/app/routes/studio/flow-to-xml-parser.ts index bd16d5ce..e5a19fa0 100644 --- a/src/main/frontend/app/routes/studio/flow-to-xml-parser.ts +++ b/src/main/frontend/app/routes/studio/flow-to-xml-parser.ts @@ -1,10 +1,10 @@ -import type { FlowNode } from '~/routes/studio/canvas/flow' +import type { FlowNode } from '~/routes/studio/canvas-flow/canvas-flow' import type { Edge } from '@xyflow/react' -import type { ChildNode } from './canvas/nodetypes/child-node' +import type { ChildNode } from '~/routes/studio/canvas-flow/nodetypes/child-node' import { getAdapter } from '~/services/adapter-service' -import { FlowConfig } from './canvas/flow.config' +import { FlowConfig } from '~/routes/studio/canvas-flow/flow.config' import { isGroupNode, isFrankNode, isStickyNote } from '~/stores/flow-store' -import type { GroupNode } from './canvas/nodetypes/group-node' +import type { GroupNode } from '~/routes/studio/canvas-flow/nodetypes/group-node' type ReactFlowJson = { nodes: FlowNode[] diff --git a/src/main/frontend/app/routes/studio/right-panel-content.tsx b/src/main/frontend/app/routes/studio/right-panel-content.tsx new file mode 100644 index 00000000..5349f5b7 --- /dev/null +++ b/src/main/frontend/app/routes/studio/right-panel-content.tsx @@ -0,0 +1,120 @@ +import type { JSX } from 'react' +import { useShallow } from 'zustand/react/shallow' +import Button from '~/components/inputs/button' +import type { StickyNote } from '~/routes/studio/canvas-flow/nodetypes/sticky-note' +import GroupContext from '~/routes/studio/context/group-context' +import NodeContext from '~/routes/studio/context/node-context' +import StickyNoteContext from '~/routes/studio/context/sticky-note-context' +import StudioContext from '~/routes/studio/context/studio-context' +import useFlowStore, { isStickyNote } from '~/stores/flow-store' +import { ALL_SHORTCUTS, formatShortcutParts, useShortcutStore } from '~/stores/shortcut-store' + +type RightPanelProps = { + isMultiSelect: boolean + selectedStickyId: string | null + selectedGroupId: string | null + showNodeContext: boolean + nodeId: number + editingSubtype: string | null + onShowNodeContext: (visible: boolean) => void +} + +export default function RightPanelContent({ + isMultiSelect, + selectedStickyId, + selectedGroupId, + showNodeContext, + nodeId, + onShowNodeContext, +}: RightPanelProps): JSX.Element { + const showPalette = !isMultiSelect && !selectedStickyId && !selectedGroupId && !showNodeContext + + return ( + <> + {isMultiSelect && } + {!isMultiSelect && selectedStickyId && } + {!isMultiSelect && !selectedStickyId && selectedGroupId && } + {!isMultiSelect && !selectedStickyId && !selectedGroupId && showNodeContext && ( +
+ + +
+ )} + +
+ +
+ + ) +} + +function MultiSelectPanel(): JSX.Element { + const { allInSameGroup, groupId } = useFlowStore( + useShallow((state) => { + const selected = state.nodes.filter((node) => node.selected) + if (selected.length < 2) return { allInSameGroup: false, groupId: null } + + const content = selected.filter((node) => node.type === 'frankNode' || node.type === 'exitNode') + if (content.length < 2) return { allInSameGroup: false, groupId: null } + + const firstParent = content[0].parentId + const allSame = Boolean(firstParent) && content.every((node) => node.parentId === firstParent) + return { allInSameGroup: allSame, groupId: allSame ? (firstParent ?? null) : null } + }), + ) + + const { platform, shortcuts } = useShortcutStore() + + if (allInSameGroup && groupId) { + return + } + const groupDefinition = ALL_SHORTCUTS.find((shortCut) => shortCut.id === 'studio.group')! + const groupParts = formatShortcutParts(groupDefinition, platform) + const triggerGroup = (): boolean | null => shortcuts.get('studio.group')?.handler?.() ?? null + + return ( +
+ +
+ ) +} + +function AttachedNotesPanel({ nodeId }: { nodeId: number }): JSX.Element | null { + const attachedNotes = useFlowStore( + useShallow((state) => + state.nodes.filter( + (node) => isStickyNote(node) && (node as StickyNote).data.attachedToNodeId === nodeId.toString(), + ), + ), + ) as StickyNote[] + + if (attachedNotes.length === 0) return null + + return ( +
+
+ {attachedNotes.length === 1 ? 'Note' : 'Notes'} +
+
+ {attachedNotes.map((note) => ( +
+ {note.data.content || Empty note} +
+ ))} +
+
+ ) +} diff --git a/src/main/frontend/app/routes/studio/studio.tsx b/src/main/frontend/app/routes/studio/studio.tsx index 0164aa96..02afa6bb 100644 --- a/src/main/frontend/app/routes/studio/studio.tsx +++ b/src/main/frontend/app/routes/studio/studio.tsx @@ -1,11 +1,12 @@ +import { ReactFlowProvider } from '@xyflow/react' import { type JSX, useCallback, useEffect, useState } from 'react' import { useNavigate } from 'react-router' import StudioTabs from '~/components/tabs/studio-tabs' import StudioFileStructure from '~/components/file-structure/studio-file-structure' -import StudioContext from '~/routes/studio/context/studio-context' -import Flow, { type FlowNode } from '~/routes/studio/canvas/flow' -import NodeContext from '~/routes/studio/context/node-context' -import StickyNoteContext from '~/routes/studio/context/sticky-note-context' +import FlowCanvas from '~/routes/studio/canvas-flow/canvas-flow' +import { NodeContextMenuContext } from '~/routes/studio/canvas-flow/node-context-menu-context' +import RightPanelContent from '~/routes/studio/right-panel-content' +import useFlowStore from '~/stores/flow-store' import useNodeContextStore from '~/stores/node-context-store' import SidebarContentClose from '~/components/sidebars-layout/sidebar-content-close' import SidebarHeader from '~/components/sidebars-layout/sidebar-header' @@ -15,21 +16,6 @@ import useTabStore from '~/stores/tab-store' import { useShallow } from 'zustand/react/shallow' import { openInEditor } from '~/actions/navigationActions' import { getBaseName } from '~/utils/path-utils' -import Button from '~/components/inputs/button' -import useFlowStore, { isStickyNote } from '~/stores/flow-store' -import type { StickyNote } from '~/routes/studio/canvas/nodetypes/sticky-note' -import { ALL_SHORTCUTS, formatShortcutParts, type Platform, useShortcutStore } from '~/stores/shortcut-store' -import GroupContext from '~/routes/studio/context/group-context' - -type RightPanelProperties = { - isMultiSelect: boolean - selectedStickyId: string | null - selectedGroupId: string | null - showNodeContext: boolean - nodeId: number - editingSubtype: string | null - onShowNodeContext: (visible: boolean) => void -} function getRightPanelTitle( isMultiSelect: boolean, @@ -46,111 +32,8 @@ function getRightPanelTitle( return 'Palette' } -function MultiSelectPanel(): JSX.Element { - const { allInSameGroup, groupId } = useFlowStore( - useShallow((state): { allInSameGroup: boolean; groupId: string | null } => { - const selected = state.nodes.filter((node): boolean | undefined => node.selected) - if (selected.length < 2) return { allInSameGroup: false, groupId: null } - - const content = selected.filter((node): boolean => node.type === 'frankNode' || node.type === 'exitNode') - if (content.length < 2) return { allInSameGroup: false, groupId: null } - - const firstParent = content[0].parentId - const allSame = Boolean(firstParent) && content.every((node): boolean => node.parentId === firstParent) - return { allInSameGroup: allSame, groupId: allSame ? (firstParent ?? null) : null } - }), - ) - - const platform = useShortcutStore((shortcut): Platform => shortcut.platform) - if (allInSameGroup && groupId) { - return - } - - const groupDefinition = ALL_SHORTCUTS.find((shortCut): boolean => shortCut.id === 'studio.group')! - const groupParts = formatShortcutParts(groupDefinition, platform) - const triggerGroup = (): boolean | void | undefined => - useShortcutStore.getState().shortcuts.get('studio.group')?.handler?.() - - return ( -
- -
- ) -} - -function AttachedNotesPanel({ nodeId }: { nodeId: number }): JSX.Element | null { - const attachedNotes = useFlowStore( - useShallow((state): FlowNode[] => - state.nodes.filter( - (node): boolean => isStickyNote(node) && (node as StickyNote).data.attachedToNodeId === nodeId.toString(), - ), - ), - ) as StickyNote[] - - if (attachedNotes.length === 0) return null - - return ( -
-
- {attachedNotes.length === 1 ? 'Note' : 'Notes'} -
-
- {attachedNotes.map((note): JSX.Element => ( -
- {note.data.content || Empty note} -
- ))} -
-
- ) -} - -function RightPanelContent({ - isMultiSelect, - selectedStickyId, - selectedGroupId, - showNodeContext, - nodeId, - onShowNodeContext, -}: RightPanelProperties): JSX.Element { - const showPalette = !isMultiSelect && !selectedStickyId && !selectedGroupId && !showNodeContext - - return ( - <> - {isMultiSelect && } - {!isMultiSelect && selectedStickyId && } - {!isMultiSelect && !selectedStickyId && selectedGroupId && } - {!isMultiSelect && !selectedStickyId && !selectedGroupId && showNodeContext && ( -
- - -
- )} - -
- -
- - ) -} - export default function Studio(): JSX.Element { - const setVisibility = useSidebarStore( - (state): ((name: string, side: SidebarSide, value: boolean) => void) => state.setVisible, - ) + const setVisibility = useSidebarStore((state) => state.setVisible) const [showNodeContext, setShowNodeContext] = useState(false) const { nodeId, editingSubtype, isMultiSelect, selectedStickyId, selectedGroupId } = useNodeContextStore( useShallow( @@ -180,9 +63,9 @@ export default function Studio(): JSX.Element { const activeStickyId = stickyNodeExists ? selectedStickyId : null const { activeTab, activeTabPath } = useTabStore( - useShallow((state): { activeTab: string; activeTabPath: string | undefined } => ({ + useShallow((state): { activeTab: string; activeTabPath: string | null } => ({ activeTab: state.activeTab, - activeTabPath: state.activeTab ? state.tabs[state.activeTab]?.configurationPath : undefined, + activeTabPath: state.activeTab ? state.tabs[state.activeTab]?.configurationPath : null, })), ) @@ -248,9 +131,11 @@ export default function Studio(): JSX.Element { {activeTab ? ( - <> - - + + + + + ) : (

No adapter selected

diff --git a/src/main/frontend/app/routes/studio/xml-to-json-parser.ts b/src/main/frontend/app/routes/studio/xml-to-json-parser.ts index 5cdc4b43..038ba022 100644 --- a/src/main/frontend/app/routes/studio/xml-to-json-parser.ts +++ b/src/main/frontend/app/routes/studio/xml-to-json-parser.ts @@ -1,14 +1,14 @@ -import type { FlowNode } from '~/routes/studio/canvas/flow' -import type { ChildNode } from '~/routes/studio/canvas/nodetypes/child-node' -import type { ExitNode } from '~/routes/studio/canvas/nodetypes/exit-node' -import type { FrankNodeType } from '~/routes/studio/canvas/nodetypes/frank-node' +import type { FlowNode } from '~/routes/studio/canvas-flow/canvas-flow' +import type { ChildNode } from '~/routes/studio/canvas-flow/nodetypes/child-node' +import type { ExitNode } from '~/routes/studio/canvas-flow/nodetypes/exit-node' +import type { FrankNodeType } from '~/routes/studio/canvas-flow/nodetypes/frank-node' import { getElementTypeFromName } from '~/routes/studio/node-translator-module' import { fetchConfigurationFileCached } from '~/services/configuration-file-service' import { isFrankNode } from '~/stores/flow-store' import { translateElementFromOldToNewFormat } from '~/utils/flow-utils' -import { FlowConfig } from './canvas/flow.config' -import type { GroupNode } from './canvas/nodetypes/group-node' -import type { StickyNote } from './canvas/nodetypes/sticky-note' +import { FlowConfig } from '~/routes/studio/canvas-flow/flow.config' +import type { GroupNode } from '~/routes/studio/canvas-flow/nodetypes/group-node' +import type { StickyNote } from '~/routes/studio/canvas-flow/nodetypes/sticky-note' const FLOW_WIDTH_ATTRIBUTE = 'flow:width' const FLOW_HEIGHT_ATTRIBUTE = 'flow:height' diff --git a/src/main/frontend/app/services/frank-framework-service.ts b/src/main/frontend/app/services/frank-framework-service.ts index 89d72cdc..a05eedc6 100644 --- a/src/main/frontend/app/services/frank-framework-service.ts +++ b/src/main/frontend/app/services/frank-framework-service.ts @@ -13,6 +13,6 @@ export type FFConfiguration = { filename?: string } -export async function fetchInstanceConfigurations(signal?: AbortSignal): Promise { +export async function fetchInstanceConfigurations(signal?: AbortSignal): Promise { return apiFetch('/projects/configurations', { signal }) } diff --git a/src/main/frontend/app/stores/child-utilities.ts b/src/main/frontend/app/stores/child-utilities.ts index 1b5dbce8..f042e26f 100644 --- a/src/main/frontend/app/stores/child-utilities.ts +++ b/src/main/frontend/app/stores/child-utilities.ts @@ -1,4 +1,4 @@ -import type { ChildNode } from '~/routes/studio/canvas/nodetypes/child-node' +import type { ChildNode } from '~/routes/studio/canvas-flow/nodetypes/child-node' export function addChildRecursive(children: ChildNode[], targetId: string, newChild: ChildNode): ChildNode[] { return children.map((child): ChildNode => { diff --git a/src/main/frontend/app/stores/flow-store.ts b/src/main/frontend/app/stores/flow-store.ts index 86c9b60c..1e6b1696 100644 --- a/src/main/frontend/app/stores/flow-store.ts +++ b/src/main/frontend/app/stores/flow-store.ts @@ -10,18 +10,18 @@ import { type OnReconnect, } from '@xyflow/react' -import { initialNodes } from '~/routes/studio/canvas/nodes' -import { initialEdges } from '~/routes/studio/canvas/edges' -import type { FlowNode } from '~/routes/studio/canvas/flow' -import type { FrankNodeType } from '~/routes/studio/canvas/nodetypes/frank-node' -import type { ExitNode } from '~/routes/studio/canvas/nodetypes/exit-node' -import type { StickyNote } from '~/routes/studio/canvas/nodetypes/sticky-note' -import type { ChildNode } from '~/routes/studio/canvas/nodetypes/child-node' +import { initialNodes } from '~/routes/studio/canvas-flow/nodes' +import { initialEdges } from '~/routes/studio/canvas-flow/edges' +import type { FlowNode } from '~/routes/studio/canvas-flow/canvas-flow' +import type { FrankNodeType } from '~/routes/studio/canvas-flow/nodetypes/frank-node' +import type { ExitNode } from '~/routes/studio/canvas-flow/nodetypes/exit-node' +import type { StickyNote } from '~/routes/studio/canvas-flow/nodetypes/sticky-note' +import type { ChildNode } from '~/routes/studio/canvas-flow/nodetypes/child-node' import { addChildRecursive, deleteChildRecursive, updateChildRecursive } from './child-utilities' -import { FlowConfig } from '~/routes/studio/canvas/flow.config' +import { FlowConfig } from '~/routes/studio/canvas-flow/flow.config' import { subscribeWithSelector } from 'zustand/middleware' import { getEdgeLabelFromHandle } from '~/utils/flow-utils' -import type { GroupNode } from '~/routes/studio/canvas/nodetypes/group-node' +import type { GroupNode } from '~/routes/studio/canvas-flow/nodetypes/group-node' export type FlowSnapshot = { nodes: FlowNode[] diff --git a/src/main/frontend/app/utils/flow-utils.ts b/src/main/frontend/app/utils/flow-utils.ts index 82672590..a9f59783 100644 --- a/src/main/frontend/app/utils/flow-utils.ts +++ b/src/main/frontend/app/utils/flow-utils.ts @@ -1,4 +1,4 @@ -import type { FlowNode } from '~/routes/studio/canvas/flow' +import type { FlowNode } from '~/routes/studio/canvas-flow/canvas-flow' const REFERENCE_KEYS = new Set(['source', 'target', 'parentId']) diff --git a/src/main/frontend/app/utils/xsd-utils.ts b/src/main/frontend/app/utils/xsd-utils.ts index 2347f7c2..ec297911 100644 --- a/src/main/frontend/app/utils/xsd-utils.ts +++ b/src/main/frontend/app/utils/xsd-utils.ts @@ -1,4 +1,4 @@ -import { type ChildNode } from '~/routes/studio/canvas/nodetypes/child-node' +import { type ChildNode } from '~/routes/studio/canvas-flow/nodetypes/child-node' export function parseXsd(xsdString: string): Document { const parser = new DOMParser() @@ -161,7 +161,8 @@ export function getFirstLevelElementsForType(document: Document, typeName: strin const typeNode = getComplexTypeByName(document, typeName) if (!typeNode) return [] - const results = extractElement(typeNode) + const results = extractElement(document, typeNode) + return [...results] } @@ -178,16 +179,16 @@ export function getElementRequirements(document: Document, elementName: string): return extractRequirements(document, typeNode) } -function extractElement(node: Element, visitedGroups = new Set()): Set { +function extractElement(document: Document, node: Element, visitedGroups = new Set()): Set { let results = new Set() for (const child of node.children) { - const childResults = extractChild(child, visitedGroups) + const childResults = extractChild(child, document, visitedGroups) if (childResults) results = new Set([...results, ...childResults]) } return results } -function extractChild(child: Element, visitedGroups: Set): Set | null { +function extractChild(child: Element, document: Document, visitedGroups: Set): Set | null { const tag = child.localName switch (tag) { @@ -207,13 +208,13 @@ function extractChild(child: Element, visitedGroups: Set): Set | const groupDefinition = getGroupByName(document, reference) if (!groupDefinition) break - return extractElement(groupDefinition, visitedGroups) + return extractElement(document, groupDefinition, visitedGroups) } case 'sequence': case 'choice': case 'all': { - return extractElement(child, visitedGroups) + return extractElement(document, child, visitedGroups) } } return null diff --git a/src/main/frontend/package.json b/src/main/frontend/package.json index 9d350fe7..597de0c5 100644 --- a/src/main/frontend/package.json +++ b/src/main/frontend/package.json @@ -15,27 +15,26 @@ }, "dependencies": { "@dagrejs/dagre": "^1.1.8", - "@frankframework/doc-library-core": "^1.0.3", - "@frankframework/doc-library-react": "^1.0.3", + "@frankframework/doc-library-core": "^1.0.6", + "@frankframework/doc-library-react": "^1.0.4", "@mdx-js/rollup": "^3.1.1", - "@xyflow/react": "^12.10.2", + "@xyflow/react": "^12.11.2", "allotment": "^1.20.5", "clsx": "^2.1.1", "dagre": "^0.8.5", - "dotenv": "^17.4.2", "fflate": "^0.8.3", "isbot": "^5.2.1", "monaco-editor": "0.55.1", "monaco-xsd-code-completion": "^1.1.1", - "react": "^19.2.5", - "react-complex-tree": "^2.6.1", - "react-dom": "^19.2.5", - "react-router": "^8.0.1", + "react": "^19.2.7", + "react-complex-tree": "^2.6.2", + "react-dom": "^19.2.7", + "react-router": "^8.2.0", "remark-gfm": "^4.0.1", "sax-ts": "^1.2.13", - "tailwind-merge": "^3.5.0", + "tailwind-merge": "^3.6.0", "xmllint-wasm": "^5.2.0", - "zustand": "^5.0.12" + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -46,17 +45,17 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", - "@types/mdx": "^2.0.13", - "@types/node": "^20.19.39", - "@types/react": "^19.2.14", + "@types/mdx": "^2.0.14", + "@types/node": "^26.1.1", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.64.0", "@typescript-eslint/parser": "^8.64.0", - "@vitejs/plugin-react": "^6.0.2", - "@vitest/ui": "^4.1.5", + "@vitejs/plugin-react": "^6.0.3", + "@vitest/ui": "^4.1.10", "eslint": "^10.7.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-sonarjs": "^4.2.0", @@ -73,5 +72,5 @@ "vite-plugin-svgr": "^5.2.0", "vitest": "^4.1.5" }, - "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be" + "packageManager": "pnpm@11.17.0+sha512.cca3cea332ad254bb84145f966d19f4879615210346fc92c79a047f23a0d7b3cca3c3792f0076ba1f1831d277efbcf0a9119b31a9a60eca7fb3d6231f331ef72" } diff --git a/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java b/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java index 969fc705..85cc5ce3 100644 --- a/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java +++ b/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java @@ -74,9 +74,9 @@ public ResponseEntity createProject(@RequestBody Config @PostMapping("/clone") public ResponseEntity cloneProject(@RequestBody ConfigurationProjectCloneDTO configurationProjectCloneDTO) throws IOException { ConfigurationProject configurationProject = configurationProjectService.cloneAndOpenProject( - configurationProjectCloneDTO.repoUrl(), - configurationProjectCloneDTO.localPath(), - configurationProjectCloneDTO.token() + configurationProjectCloneDTO.repoUrl(), + configurationProjectCloneDTO.localPath(), + configurationProjectCloneDTO.token() ); recentProjectsService.addRecentProject(configurationProject.getName(), configurationProject.getRootPath()); return ResponseEntity.ok(configurationProjectService.toDto(configurationProject)); @@ -128,7 +128,7 @@ public ResponseEntity importProject( @GetMapping("/configurations") public Map getFrameworkConfigurations() throws ApiException { if (session.getMemberTarget() == null) { - throw new ApiException("No cluster member target found for the current session.", HttpStatus.NOT_FOUND); + throw new ApiException("No cluster member target found for the current session.", HttpStatus.NO_CONTENT); } RequestMessageBuilder builder = RequestMessageBuilder.create(BusTopic.CONFIGURATION, BusAction.FIND);