-
-
+
+
Accent glow
diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/error-message.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/error-message.tsx
index f0a1fb64a9..9bc97d9970 100644
--- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/error-message.tsx
+++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/error-message.tsx
@@ -14,7 +14,6 @@ const ErrorMessage = ({
errorMsg,
}: ErrorMessageProps) => {
return (
- // eslint-disable-next-line tailwindcss/migration-from-tailwind-2
=16.0.0}
- '@eslint-react/ast@2.8.1':
- resolution: {integrity: sha512-4D442lxeFvvd9PMvBbA621rfz/Ne8Kod8RW0/FLKO0vx+IOxm74pP6be1uU56rqL9TvoIHxjclBjfgXplEF+Yw==}
+ '@eslint-react/ast@2.9.4':
+ resolution: {integrity: sha512-WI9iq5ePTlcWo0xhSs4wxLUC6u4QuBmQkKeSiXexkEO8C2p8QE7ECNIXhRVkYs3p3AKH5xTez9V8C/CBIGxeXA==}
engines: {node: '>=20.19.0'}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@eslint-react/core@2.8.1':
- resolution: {integrity: sha512-zF73p8blyuX+zrfgyTtpKesichYzK+G54TEjFWtzagWIbnqQjtVscebL/eGep72oWzAOd5B04ACBvJ2hW4fp5g==}
+ '@eslint-react/core@2.9.4':
+ resolution: {integrity: sha512-Ob+Dip1vyR9ch9XL7LUAsGXc0UUf9Kuzn9BEiwOLT7l+cF91ieKeCvIzNPp0LmTuanPfQweJ9iDT9i295SqBZA==}
engines: {node: '>=20.19.0'}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@eslint-react/eff@2.8.1':
- resolution: {integrity: sha512-ZASOs8oTZJSiu1giue7V87GEKQvlKLfGfLppal6Rl+aKnfIEz+vartmjpH12pkFQZ9ESRyHzYbU533S6pEDoNg==}
+ '@eslint-react/eff@2.9.4':
+ resolution: {integrity: sha512-7AOmozmfa0HgXY9O+J+iX3ciZfViz+W+jhRe2y0YqqkDR7PwV2huzhk/Bxq6sRzzf2uFHqoh/AQNZUhRJ3A05A==}
engines: {node: '>=20.19.0'}
- '@eslint-react/eslint-plugin@2.8.1':
- resolution: {integrity: sha512-ob+SSDnTPnA5dhiWWJLfyHRLEzWnjilCsohgo5s9PPKF5b5bjxG+c/rwqhQwT3M9Ey83mGNdkrLzt00SOfr4pw==}
+ '@eslint-react/eslint-plugin@2.9.4':
+ resolution: {integrity: sha512-B1LOEUBuT4L7EmY3E9F7+K8Jdr9nAzx66USz4uWEtg8ZMn82E2O5TzOBPw6eeL0O9BoyLBoslZotXNQVazR2dA==}
engines: {node: '>=20.19.0'}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@eslint-react/shared@2.8.1':
- resolution: {integrity: sha512-NDmJBiMiPDXR6qeZzYOtiILHxWjYwBHxquQ/bMQkWcWK+1qF5LeD8UTRcWtBpZoMPi3sNBWwR3k2Sc5HWZpJ7g==}
+ '@eslint-react/shared@2.9.4':
+ resolution: {integrity: sha512-PU7C4JzDZ6OffAWD+HwJdvzGSho25UPYJRyb4wZ/pDaI8QPTDj8AtKWKK69SEOQl2ic89ht1upjQX+jrXhN15w==}
engines: {node: '>=20.19.0'}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@eslint-react/var@2.8.1':
- resolution: {integrity: sha512-iHIdEBz6kgW4dEFdhEjpy9SEQ6+d4RYg+WBzHg5J5ktT2xSQFi77Dq6Wtemik6QvvAPnYLRseQxgW+m+1rQlfA==}
+ '@eslint-react/var@2.9.4':
+ resolution: {integrity: sha512-Qiih6hT+D2vZmCbAGUooReKlqXjtb/g3SzYj2zNlci6YcWxsQB/pqhR0ayU2AOdW6U9YdeCCfPIwBBQ4AEpyBA==}
engines: {node: '>=20.19.0'}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -1208,6 +1208,10 @@ packages:
resolution: {integrity: sha512-r18fEAj9uCk+VjzGt2thsbOmychS+4kxI14spVNibUO2vqKX7obOG+ymZljAwuPZl+S3clPGwCwTDtrdqTiY6Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ '@eslint/css-tree@3.6.8':
+ resolution: {integrity: sha512-s0f40zY7dlMp8i0Jf0u6l/aSswS0WRAgkhgETgiCJRcxIWb4S/Sp9uScKHWbkM3BnoFLbJbmOYk5AZUDFVxaLA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
'@eslint/eslintrc@3.3.3':
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2867,10 +2871,14 @@ packages:
peerDependencies:
solid-js: 1.9.11
- '@tanstack/eslint-plugin-query@5.91.3':
- resolution: {integrity: sha512-5GMGZMYFK9dOvjpdedjJs4hU40EdPuO2AjzObQzP7eOSsikunCfrXaU3oNGXSsvoU9ve1Z1xQZZuDyPi0C1M7Q==}
+ '@tanstack/eslint-plugin-query@5.91.4':
+ resolution: {integrity: sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ==}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
+ typescript: ^5.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
'@tanstack/form-core@1.24.3':
resolution: {integrity: sha512-e+HzSD49NWr4aIqJWtPPzmi+/phBJAP3nSPN8dvxwmJWqAxuB/cH138EcmCFf3+oA7j3BXvwvTY0I+8UweGPjQ==}
@@ -3378,6 +3386,11 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+ '@valibot/to-json-schema@1.5.0':
+ resolution: {integrity: sha512-GE7DmSr1C2UCWPiV0upRH6mv0cCPsqYGs819fb6srCS1tWhyXrkGGe+zxUiwzn/L1BOfADH4sNjY/YHCuP8phQ==}
+ peerDependencies:
+ valibot: ^1.2.0
+
'@vitejs/plugin-react@5.1.2':
resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -4443,6 +4456,19 @@ packages:
peerDependencies:
eslint: '*'
+ eslint-plugin-better-tailwindcss@4.1.1:
+ resolution: {integrity: sha512-ctw461TGJi8iM0P01mNVjSW7jeUAdyUgmrrd59np5/VxqX50nayMbwKZkfmjWpP1PWOqlh4CSMOH/WW6ICWmJw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
+ oxlint: ^1.35.0
+ tailwindcss: ^3.3.0 || ^4.1.17
+ peerDependenciesMeta:
+ eslint:
+ optional: true
+ oxlint:
+ optional: true
+
eslint-plugin-command@3.4.0:
resolution: {integrity: sha512-EW4eg/a7TKEhG0s5IEti72kh3YOTlnhfFNuctq5WnB1fst37/IHTd5OkD+vnlRf3opTvUcSRihAateP6bT5ZcA==}
peerDependencies:
@@ -4493,15 +4519,15 @@ packages:
peerDependencies:
eslint: ^9.0.0
- eslint-plugin-react-dom@2.8.1:
- resolution: {integrity: sha512-VAVs3cp/0XTxdjTeLePtZVadj+om+N1VNVy7hyzSPACfh5ncAicC0zOIc5MB15KUWCj8PoG/ZnVny0YqeubgRg==}
+ eslint-plugin-react-dom@2.9.4:
+ resolution: {integrity: sha512-lRa3iN082cX3HRKdbKSESmlj+z4zMR10DughwagV7h+IOd3O07UGnYQhenH08GMSyLy1f2D6QJmKBLGbx2p20g==}
engines: {node: '>=20.19.0'}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- eslint-plugin-react-hooks-extra@2.8.1:
- resolution: {integrity: sha512-YeZLGzcib6UxlY7Gf+3zz8Mfl7u+OoVj3MukGaTuU6zkm1XQMI8/k4o16bKHuWtUauhn7Udl1bLAWfLgQM5UFw==}
+ eslint-plugin-react-hooks-extra@2.9.4:
+ resolution: {integrity: sha512-8hQArFHpXubT+i++8TwIL24vQ5b/ZcnVT3EFOSvy1TdBZw8NqrcFNBVqywQ6YUWX0utuPiTQgeJB0qnBF7gx4g==}
engines: {node: '>=20.19.0'}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -4513,27 +4539,34 @@ packages:
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
- eslint-plugin-react-naming-convention@2.8.1:
- resolution: {integrity: sha512-fVj+hSzIe2I6HyPTf1nccMBXq72c4jbM3gk0T+szo/wewEF8/LgenjfquJoxHPpheb1fujFgdlo5HBhsilAX7Q==}
+ eslint-plugin-react-naming-convention@2.9.4:
+ resolution: {integrity: sha512-Ow9ikJ49tDjeTaO2wfUYlSlVBsbG8AZVqoVFu4HH69FZe6I5LEdjZf/gdXnN2W+/JAy7Ru5vYQ8H8LU3tTZERg==}
engines: {node: '>=20.19.0'}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- eslint-plugin-react-refresh@0.4.26:
- resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==}
+ eslint-plugin-react-refresh@0.5.0:
+ resolution: {integrity: sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA==}
peerDependencies:
- eslint: '>=8.40'
+ eslint: '>=9'
- eslint-plugin-react-web-api@2.8.1:
- resolution: {integrity: sha512-NYsZKW1aJZ2XZuYTPzbwYLShvGcuXKRV/5TW61VO56gik/btil4Snt5UtyxshHbvT/zXx/Z+QsHul51/XM4/Qw==}
+ eslint-plugin-react-rsc@2.9.4:
+ resolution: {integrity: sha512-RwBYSLkcGXQV6SQYABdHLrafUmpfdPBYsAa/kvg6smqEn+/vPKSk0I+uAuzkmiw4y4KXW94Q9rlIdJlzOMdJfQ==}
engines: {node: '>=20.19.0'}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- eslint-plugin-react-x@2.8.1:
- resolution: {integrity: sha512-4IpCMrsb63AVEa9diOApIm+T3wUGIzK+EB5vyYocO31YYPJ16+R7Fh4lV3S3fOuX1+aQ+Ad4SE0cYuZ2pF2Tlg==}
+ eslint-plugin-react-web-api@2.9.4:
+ resolution: {integrity: sha512-/k++qhGoYtMNZrsQT+M08fCGi/VurL1fE/LNiz2fMwOIU7KjXD9N0kGWPFdIAISnYXGzOg53O5WW/mnNR78emQ==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ eslint-plugin-react-x@2.9.4:
+ resolution: {integrity: sha512-a078MHeM/FdjRu3KJsFX+PCHewZyC77EjAO7QstL/vvwjsFae3PCWMZ8Q4b+mzUsT4FkFxi5mEW43ZHksPWDFw==}
engines: {node: '>=20.19.0'}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -4550,17 +4583,11 @@ packages:
peerDependencies:
eslint: ^8.0.0 || ^9.0.0
- eslint-plugin-storybook@10.2.1:
- resolution: {integrity: sha512-5+V+dlzTuZfNKUD8hPbLvCVtggcWfI2lDGTpiq0AENrHeAgcztj17wwDva96lbg/sAG20uX71l8HQo3s/GmpHw==}
+ eslint-plugin-storybook@10.2.6:
+ resolution: {integrity: sha512-Ykf0hDS97oJlQel21WG+SYtGnzFkkSfifupJ92NQtMMSMLXsWm4P0x8ZQqu9/EQa+dUkGoj9EWyNmmbB/54uhA==}
peerDependencies:
eslint: '>=8'
- storybook: ^10.2.1
-
- eslint-plugin-tailwindcss@3.18.2:
- resolution: {integrity: sha512-QbkMLDC/OkkjFQ1iz/5jkMdHfiMu/uwujUHLAJK5iwNHD8RTxVTlsUezE0toTZ6VhybNBsk+gYGPDq2agfeRNA==}
- engines: {node: '>=18.12.0'}
- peerDependencies:
- tailwindcss: ^3.4.0
+ storybook: ^10.2.6
eslint-plugin-toml@1.0.3:
resolution: {integrity: sha512-GlCBX+R313RvFY2Tj0ZmvzCEv8FDp1z2itvTFTV4bW/Bkbl3xEp9inWNsRWH3SiDUlxo8Pew31ILEp/3J0WxaA==}
@@ -5585,6 +5612,9 @@ packages:
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
+ mdn-data@2.23.0:
+ resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==}
+
memoize-one@5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
@@ -6850,11 +6880,15 @@ packages:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
- tailwind-merge@2.6.0:
- resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
+ tailwind-csstree@0.1.4:
+ resolution: {integrity: sha512-FzD187HuFIZEyeR7Xy6sJbJll2d4SybS90satC8SKIuaNRC05CxMvdzN7BUsfDQffcnabckRM5OIcfArjsZ0mg==}
+ engines: {node: '>=18.18'}
- tailwindcss@3.4.18:
- resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==}
+ tailwind-merge@2.6.1:
+ resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==}
+
+ tailwindcss@3.4.19:
+ resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
engines: {node: '>=14.0.0'}
hasBin: true
@@ -7003,6 +7037,10 @@ packages:
typescript:
optional: true
+ tsconfig-paths-webpack-plugin@4.2.0:
+ resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==}
+ engines: {node: '>=10.13.0'}
+
tsconfig-paths@4.2.0:
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
engines: {node: '>=6'}
@@ -7171,6 +7209,14 @@ packages:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
+ valibot@1.2.0:
+ resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==}
+ peerDependencies:
+ typescript: '>=5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
@@ -7694,7 +7740,7 @@ snapshots:
idb: 8.0.3
tslib: 2.8.1
- '@antfu/eslint-config@7.2.0(@eslint-react/eslint-plugin@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.1.6)(@vue/compiler-sfc@3.5.27)(eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)(vitest@4.0.17)':
+ '@antfu/eslint-config@7.2.0(@eslint-react/eslint-plugin@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.1.6)(@vue/compiler-sfc@3.5.27)(eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.0(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)(vitest@4.0.17)':
dependencies:
'@antfu/install-pkg': 1.1.0
'@clack/prompts': 0.11.0
@@ -7734,10 +7780,10 @@ snapshots:
vue-eslint-parser: 10.2.0(eslint@9.39.2(jiti@1.21.7))
yaml-eslint-parser: 2.0.0
optionalDependencies:
- '@eslint-react/eslint-plugin': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/eslint-plugin': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@next/eslint-plugin-next': 16.1.6
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@1.21.7))
- eslint-plugin-react-refresh: 0.4.26(eslint@9.39.2(jiti@1.21.7))
+ eslint-plugin-react-refresh: 0.5.0(eslint@9.39.2(jiti@1.21.7))
transitivePeerDependencies:
- '@eslint/json'
- '@vue/compiler-sfc'
@@ -8137,9 +8183,9 @@ snapshots:
'@eslint-community/regexpp@4.12.2': {}
- '@eslint-react/ast@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
+ '@eslint-react/ast@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
- '@eslint-react/eff': 2.8.1
+ '@eslint-react/eff': 2.9.4
'@typescript-eslint/types': 8.54.0
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
@@ -8149,12 +8195,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@eslint-react/core@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
+ '@eslint-react/core@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
- '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/eff': 2.8.1
- '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/eff': 2.9.4
+ '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.54.0
'@typescript-eslint/types': 8.54.0
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
@@ -8164,30 +8210,31 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@eslint-react/eff@2.8.1': {}
+ '@eslint-react/eff@2.9.4': {}
- '@eslint-react/eslint-plugin@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
+ '@eslint-react/eslint-plugin@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
- '@eslint-react/eff': 2.8.1
- '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/eff': 2.9.4
+ '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.54.0
'@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/types': 8.54.0
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
eslint: 9.39.2(jiti@1.21.7)
- eslint-plugin-react-dom: 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- eslint-plugin-react-hooks-extra: 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- eslint-plugin-react-naming-convention: 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- eslint-plugin-react-web-api: 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- eslint-plugin-react-x: 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ eslint-plugin-react-dom: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ eslint-plugin-react-hooks-extra: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ eslint-plugin-react-naming-convention: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ eslint-plugin-react-rsc: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ eslint-plugin-react-web-api: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ eslint-plugin-react-x: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@eslint-react/shared@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
+ '@eslint-react/shared@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
- '@eslint-react/eff': 2.8.1
+ '@eslint-react/eff': 2.9.4
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
eslint: 9.39.2(jiti@1.21.7)
ts-pattern: 5.9.0
@@ -8196,11 +8243,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@eslint-react/var@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
+ '@eslint-react/var@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
- '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/eff': 2.8.1
- '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/eff': 2.9.4
+ '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.54.0
'@typescript-eslint/types': 8.54.0
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
@@ -8258,6 +8305,11 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
+ '@eslint/css-tree@3.6.8':
+ dependencies:
+ mdn-data: 2.23.0
+ source-map-js: 1.2.1
+
'@eslint/eslintrc@3.3.3':
dependencies:
ajv: 6.12.6
@@ -9902,10 +9954,10 @@ snapshots:
dependencies:
'@swc/counter': 0.1.3
- '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))':
+ '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
postcss-selector-parser: 6.0.10
- tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2)
+ tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
'@tanstack/devtools-client@0.0.5':
dependencies:
@@ -9957,13 +10009,14 @@ snapshots:
- csstype
- utf-8-validate
- '@tanstack/eslint-plugin-query@5.91.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
+ '@tanstack/eslint-plugin-query@5.91.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
eslint: 9.39.2(jiti@1.21.7)
+ optionalDependencies:
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- - typescript
'@tanstack/form-core@1.24.3':
dependencies:
@@ -10593,6 +10646,10 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
+ '@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3))':
+ dependencies:
+ valibot: 1.2.0(typescript@5.9.3)
+
'@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@18.15.0)(jiti@1.21.7)(sass@1.93.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@babel/core': 7.28.6
@@ -11747,6 +11804,22 @@ snapshots:
dependencies:
eslint: 9.39.2(jiti@1.21.7)
+ eslint-plugin-better-tailwindcss@4.1.1(eslint@9.39.2(jiti@1.21.7))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))(typescript@5.9.3):
+ dependencies:
+ '@eslint/css-tree': 3.6.8
+ '@valibot/to-json-schema': 1.5.0(valibot@1.2.0(typescript@5.9.3))
+ enhanced-resolve: 5.18.4
+ jiti: 2.6.1
+ synckit: 0.11.12
+ tailwind-csstree: 0.1.4
+ tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
+ tsconfig-paths-webpack-plugin: 4.2.0
+ valibot: 1.2.0(typescript@5.9.3)
+ optionalDependencies:
+ eslint: 9.39.2(jiti@1.21.7)
+ transitivePeerDependencies:
+ - typescript
+
eslint-plugin-command@3.4.0(eslint@9.39.2(jiti@1.21.7)):
dependencies:
'@es-joy/jsdoccomment': 0.78.0
@@ -11835,37 +11908,35 @@ snapshots:
yaml: 2.8.2
yaml-eslint-parser: 2.0.0
- eslint-plugin-react-dom@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
+ eslint-plugin-react-dom@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
dependencies:
- '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/core': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/eff': 2.8.1
- '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/core': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/eff': 2.9.4
+ '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.54.0
'@typescript-eslint/types': 8.54.0
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
compare-versions: 6.1.1
eslint: 9.39.2(jiti@1.21.7)
- string-ts: 2.3.1
ts-pattern: 5.9.0
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- eslint-plugin-react-hooks-extra@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
+ eslint-plugin-react-hooks-extra@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
dependencies:
- '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/core': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/eff': 2.8.1
- '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/core': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/eff': 2.9.4
+ '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.54.0
'@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/types': 8.54.0
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
eslint: 9.39.2(jiti@1.21.7)
- string-ts: 2.3.1
ts-pattern: 5.9.0
typescript: 5.9.3
transitivePeerDependencies:
@@ -11882,13 +11953,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-react-naming-convention@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
+ eslint-plugin-react-naming-convention@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
dependencies:
- '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/core': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/eff': 2.8.1
- '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/core': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/eff': 2.9.4
+ '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.54.0
'@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/types': 8.54.0
@@ -11901,35 +11972,47 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@1.21.7)):
+ eslint-plugin-react-refresh@0.5.0(eslint@9.39.2(jiti@1.21.7)):
dependencies:
eslint: 9.39.2(jiti@1.21.7)
- eslint-plugin-react-web-api@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
+ eslint-plugin-react-rsc@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
dependencies:
- '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/core': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/eff': 2.8.1
- '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.54.0
+ '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/types': 8.54.0
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- birecord: 0.1.1
eslint: 9.39.2(jiti@1.21.7)
- string-ts: 2.3.1
ts-pattern: 5.9.0
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- eslint-plugin-react-x@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
+ eslint-plugin-react-web-api@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
dependencies:
- '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/core': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/eff': 2.8.1
- '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/core': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/eff': 2.9.4
+ '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.54.0
+ '@typescript-eslint/types': 8.54.0
+ '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ birecord: 0.1.1
+ eslint: 9.39.2(jiti@1.21.7)
+ ts-pattern: 5.9.0
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-react-x@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
+ dependencies:
+ '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/core': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/eff': 2.9.4
+ '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.54.0
'@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/types': 8.54.0
@@ -11937,7 +12020,6 @@ snapshots:
compare-versions: 6.1.1
eslint: 9.39.2(jiti@1.21.7)
is-immutable-type: 5.0.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
- string-ts: 2.3.1
ts-api-utils: 2.4.0(typescript@5.9.3)
ts-pattern: 5.9.0
typescript: 5.9.3
@@ -11969,21 +12051,15 @@ snapshots:
semver: 7.7.3
typescript: 5.9.3
- eslint-plugin-storybook@10.2.1(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.0(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3):
+ eslint-plugin-storybook@10.2.6(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.0(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3):
dependencies:
- '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
eslint: 9.39.2(jiti@1.21.7)
storybook: 10.2.0(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- supports-color
- typescript
- eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)):
- dependencies:
- fast-glob: 3.3.3
- postcss: 8.5.6
- tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2)
-
eslint-plugin-toml@1.0.3(eslint@9.39.2(jiti@1.21.7)):
dependencies:
'@eslint/core': 1.0.1
@@ -13250,6 +13326,8 @@ snapshots:
mdn-data@2.12.2: {}
+ mdn-data@2.23.0: {}
+
memoize-one@5.2.1: {}
merge-stream@2.0.0: {}
@@ -14863,9 +14941,11 @@ snapshots:
tagged-tag@1.0.0: {}
- tailwind-merge@2.6.0: {}
+ tailwind-csstree@0.1.4: {}
- tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2):
+ tailwind-merge@2.6.1: {}
+
+ tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@@ -15020,6 +15100,13 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
+ tsconfig-paths-webpack-plugin@4.2.0:
+ dependencies:
+ chalk: 4.1.2
+ enhanced-resolve: 5.18.4
+ tapable: 2.3.0
+ tsconfig-paths: 4.2.0
+
tsconfig-paths@4.2.0:
dependencies:
json5: 2.2.3
@@ -15185,6 +15272,10 @@ snapshots:
uuid@11.1.0: {}
+ valibot@1.2.0(typescript@5.9.3):
+ optionalDependencies:
+ typescript: 5.9.3
+
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3
diff --git a/web/utils/classnames.spec.ts b/web/utils/classnames.spec.ts
deleted file mode 100644
index 1b8f487856..0000000000
--- a/web/utils/classnames.spec.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-/**
- * Test suite for the classnames utility function
- * This utility combines the classnames library with tailwind-merge
- * to handle conditional CSS classes and merge conflicting Tailwind classes
- */
-import { cn } from './classnames'
-
-describe('classnames', () => {
- /**
- * Tests basic classnames library features:
- * - String concatenation
- * - Array handling
- * - Falsy value filtering
- * - Object-based conditional classes
- */
- it('classnames libs feature', () => {
- expect(cn('foo')).toBe('foo')
- expect(cn('foo', 'bar')).toBe('foo bar')
- expect(cn(['foo', 'bar'])).toBe('foo bar')
-
- expect(cn(undefined)).toBe('')
- expect(cn(null)).toBe('')
- expect(cn(false)).toBe('')
-
- expect(cn({
- foo: true,
- bar: false,
- baz: true,
- })).toBe('foo baz')
- })
-
- /**
- * Tests tailwind-merge functionality:
- * - Conflicting class resolution (last one wins)
- * - Modifier handling (hover, focus, etc.)
- * - Important prefix (!)
- * - Custom color classes
- * - Arbitrary values
- */
- it('tailwind-merge', () => {
- /* eslint-disable tailwindcss/classnames-order */
- expect(cn('p-0')).toBe('p-0')
- expect(cn('text-right text-center text-left')).toBe('text-left')
- expect(cn('pl-4 p-8')).toBe('p-8')
- expect(cn('m-[2px] m-[4px]')).toBe('m-[4px]')
- expect(cn('m-1 m-[4px]')).toBe('m-[4px]')
- expect(cn('overflow-x-auto hover:overflow-x-hidden overflow-x-scroll')).toBe(
- 'hover:overflow-x-hidden overflow-x-scroll',
- )
- expect(cn('h-10 h-min')).toBe('h-min')
- expect(cn('bg-grey-5 bg-hotpink')).toBe('bg-hotpink')
-
- expect(cn('hover:block hover:inline')).toBe('hover:inline')
-
- expect(cn('font-medium !font-bold')).toBe('font-medium !font-bold')
- expect(cn('!font-medium !font-bold')).toBe('!font-bold')
-
- expect(cn('text-gray-100 text-primary-200')).toBe('text-primary-200')
- expect(cn('text-some-unknown-color text-components-input-bg-disabled text-primary-200')).toBe('text-primary-200')
- expect(cn('bg-some-unknown-color bg-components-input-bg-disabled bg-primary-200')).toBe('bg-primary-200')
-
- expect(cn('border-t border-white/10')).toBe('border-t border-white/10')
- expect(cn('border-t border-white')).toBe('border-t border-white')
- expect(cn('text-3.5xl text-black')).toBe('text-3.5xl text-black')
- })
-
- /**
- * Tests the integration of classnames and tailwind-merge:
- * - Object-based conditional classes with Tailwind conflict resolution
- */
- it('classnames combined with tailwind-merge', () => {
- expect(cn('text-right', {
- 'text-center': true,
- })).toBe('text-center')
-
- expect(cn('text-right', {
- 'text-center': false,
- })).toBe('text-right')
- })
-
- /**
- * Tests handling of multiple mixed argument types:
- * - Strings, arrays, and objects in a single call
- * - Tailwind merge working across different argument types
- */
- it('multiple mixed argument types', () => {
- expect(cn('foo', ['bar', 'baz'], { qux: true, quux: false })).toBe('foo bar baz qux')
- expect(cn('p-4', ['p-2', 'm-4'], { 'text-left': true, 'text-right': true })).toBe('p-2 m-4 text-right')
- })
-
- /**
- * Tests nested array handling:
- * - Deep array flattening
- * - Tailwind merge with nested structures
- */
- it('nested arrays', () => {
- expect(cn(['foo', ['bar', 'baz']])).toBe('foo bar baz')
- expect(cn(['p-4', ['p-2', 'text-center']])).toBe('p-2 text-center')
- })
-
- /**
- * Tests empty input handling:
- * - Empty strings, arrays, and objects
- * - Mixed empty and non-empty values
- */
- it('empty inputs', () => {
- expect(cn('')).toBe('')
- expect(cn([])).toBe('')
- expect(cn({})).toBe('')
- expect(cn('', [], {})).toBe('')
- expect(cn('foo', '', 'bar')).toBe('foo bar')
- })
-
- /**
- * Tests number input handling:
- * - Truthy numbers converted to strings
- * - Zero treated as falsy
- */
- it('numbers as inputs', () => {
- expect(cn(1)).toBe('1')
- expect(cn(0)).toBe('')
- expect(cn('foo', 1, 'bar')).toBe('foo 1 bar')
- })
-
- /**
- * Tests multiple object arguments:
- * - Object merging
- * - Tailwind conflict resolution across objects
- */
- it('multiple objects', () => {
- expect(cn({ foo: true }, { bar: true })).toBe('foo bar')
- expect(cn({ foo: true, bar: false }, { bar: true, baz: true })).toBe('foo bar baz')
- expect(cn({ 'p-4': true }, { 'p-2': true })).toBe('p-2')
- })
-
- /**
- * Tests complex edge cases:
- * - Mixed falsy values
- * - Nested arrays with falsy values
- * - Multiple conflicting Tailwind classes
- */
- it('complex edge cases', () => {
- expect(cn('foo', null, undefined, false, 'bar', 0, 1, '')).toBe('foo bar 1')
- expect(cn(['foo', null, ['bar', undefined, 'baz']])).toBe('foo bar baz')
- expect(cn('text-sm', { 'text-lg': false, 'text-xl': true }, 'text-2xl')).toBe('text-2xl')
- })
-
- /**
- * Tests important (!) modifier behavior:
- * - Important modifiers in objects
- * - Conflict resolution with important prefix
- */
- it('important modifier with objects', () => {
- expect(cn({ '!font-medium': true }, { '!font-bold': true })).toBe('!font-bold')
- expect(cn('font-normal', { '!font-bold': true })).toBe('font-normal !font-bold')
- })
-})
From e04f2a0786df994168618abe1832a8ae921f6227 Mon Sep 17 00:00:00 2001
From: Stream
Date: Thu, 5 Feb 2026 18:58:17 +0800
Subject: [PATCH 04/10] feat: use static manifest for pre-caching all plugin
manifests before checking updates (#31942)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
api/core/helper/marketplace.py | 54 +++++++-----
api/core/plugin/entities/marketplace.py | 14 +++-
api/schedule/check_upgradable_plugin_task.py | 24 ++++++
...ss_tenant_plugin_autoupgrade_check_task.py | 83 +++++--------------
4 files changed, 88 insertions(+), 87 deletions(-)
diff --git a/api/core/helper/marketplace.py b/api/core/helper/marketplace.py
index 25dc4ba9ed..d7b6e82062 100644
--- a/api/core/helper/marketplace.py
+++ b/api/core/helper/marketplace.py
@@ -6,7 +6,8 @@ from yarl import URL
from configs import dify_config
from core.helper.download import download_with_size_limit
-from core.plugin.entities.marketplace import MarketplacePluginDeclaration
+from core.plugin.entities.marketplace import MarketplacePluginDeclaration, MarketplacePluginSnapshot
+from extensions.ext_redis import redis_client
marketplace_api_url = URL(str(dify_config.MARKETPLACE_API_URL))
logger = logging.getLogger(__name__)
@@ -43,28 +44,37 @@ def batch_fetch_plugin_by_ids(plugin_ids: list[str]) -> list[dict]:
return data.get("data", {}).get("plugins", [])
-def batch_fetch_plugin_manifests_ignore_deserialization_error(
- plugin_ids: list[str],
-) -> Sequence[MarketplacePluginDeclaration]:
- if len(plugin_ids) == 0:
- return []
-
- url = str(marketplace_api_url / "api/v1/plugins/batch")
- response = httpx.post(url, json={"plugin_ids": plugin_ids}, headers={"X-Dify-Version": dify_config.project.version})
- response.raise_for_status()
- result: list[MarketplacePluginDeclaration] = []
- for plugin in response.json()["data"]["plugins"]:
- try:
- result.append(MarketplacePluginDeclaration.model_validate(plugin))
- except Exception:
- logger.exception(
- "Failed to deserialize marketplace plugin manifest for %s", plugin.get("plugin_id", "unknown")
- )
-
- return result
-
-
def record_install_plugin_event(plugin_unique_identifier: str):
url = str(marketplace_api_url / "api/v1/stats/plugins/install_count")
response = httpx.post(url, json={"unique_identifier": plugin_unique_identifier})
response.raise_for_status()
+
+
+def fetch_global_plugin_manifest(cache_key_prefix: str, cache_ttl: int) -> None:
+ """
+ Fetch all plugin manifests from marketplace and cache them in Redis.
+ This should be called once per check cycle to populate the instance-level cache.
+
+ Args:
+ cache_key_prefix: Redis key prefix for caching plugin manifests
+ cache_ttl: Cache TTL in seconds
+
+ Raises:
+ httpx.HTTPError: If the HTTP request fails
+ Exception: If any other error occurs during fetching or caching
+ """
+ url = str(marketplace_api_url / "api/v1/dist/plugins/manifest.json")
+ response = httpx.get(url, headers={"X-Dify-Version": dify_config.project.version}, timeout=30)
+ response.raise_for_status()
+
+ raw_json = response.json()
+ plugins_data = raw_json.get("plugins", [])
+
+ # Parse and cache all plugin snapshots
+ for plugin_data in plugins_data:
+ plugin_snapshot = MarketplacePluginSnapshot.model_validate(plugin_data)
+ redis_client.setex(
+ name=f"{cache_key_prefix}{plugin_snapshot.plugin_id}",
+ time=cache_ttl,
+ value=plugin_snapshot.model_dump_json(),
+ )
diff --git a/api/core/plugin/entities/marketplace.py b/api/core/plugin/entities/marketplace.py
index e0762619e6..cf1f7ff0dd 100644
--- a/api/core/plugin/entities/marketplace.py
+++ b/api/core/plugin/entities/marketplace.py
@@ -1,4 +1,4 @@
-from pydantic import BaseModel, Field, model_validator
+from pydantic import BaseModel, Field, computed_field, model_validator
from core.model_runtime.entities.provider_entities import ProviderEntity
from core.plugin.entities.endpoint import EndpointProviderDeclaration
@@ -48,3 +48,15 @@ class MarketplacePluginDeclaration(BaseModel):
if "tool" in data and not data["tool"]:
del data["tool"]
return data
+
+
+class MarketplacePluginSnapshot(BaseModel):
+ org: str
+ name: str
+ latest_version: str
+ latest_package_identifier: str
+ latest_package_url: str
+
+ @computed_field
+ def plugin_id(self) -> str:
+ return f"{self.org}/{self.name}"
diff --git a/api/schedule/check_upgradable_plugin_task.py b/api/schedule/check_upgradable_plugin_task.py
index e91ce07be3..13d2f24ca0 100644
--- a/api/schedule/check_upgradable_plugin_task.py
+++ b/api/schedule/check_upgradable_plugin_task.py
@@ -1,16 +1,24 @@
+import logging
import math
import time
import click
import app
+from core.helper.marketplace import fetch_global_plugin_manifest
from extensions.ext_database import db
from models.account import TenantPluginAutoUpgradeStrategy
from tasks import process_tenant_plugin_autoupgrade_check_task as check_task
+logger = logging.getLogger(__name__)
+
AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL = 15 * 60 # 15 minutes
MAX_CONCURRENT_CHECK_TASKS = 20
+# Import cache constants from the task module
+CACHE_REDIS_KEY_PREFIX = check_task.CACHE_REDIS_KEY_PREFIX
+CACHE_REDIS_TTL = check_task.CACHE_REDIS_TTL
+
@app.celery.task(queue="plugin")
def check_upgradable_plugin_task():
@@ -40,6 +48,22 @@ def check_upgradable_plugin_task():
) # make sure all strategies are checked in this interval
batch_interval_time = (AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL / batch_chunk_count) if batch_chunk_count > 0 else 0
+ if total_strategies == 0:
+ click.echo(click.style("no strategies to process, skipping plugin manifest fetch.", fg="green"))
+ return
+
+ # Fetch and cache all plugin manifests before processing tenants
+ # This reduces load on marketplace from 300k requests to 1 request per check cycle
+ logger.info("fetching global plugin manifest from marketplace")
+ try:
+ fetch_global_plugin_manifest(CACHE_REDIS_KEY_PREFIX, CACHE_REDIS_TTL)
+ logger.info("successfully fetched and cached global plugin manifest")
+ except Exception as e:
+ logger.exception("failed to fetch global plugin manifest")
+ click.echo(click.style(f"failed to fetch global plugin manifest: {e}", fg="red"))
+ click.echo(click.style("skipping plugin upgrade check for this cycle", fg="yellow"))
+ return
+
for i in range(0, total_strategies, MAX_CONCURRENT_CHECK_TASKS):
batch_strategies = strategies[i : i + MAX_CONCURRENT_CHECK_TASKS]
for strategy in batch_strategies:
diff --git a/api/tasks/process_tenant_plugin_autoupgrade_check_task.py b/api/tasks/process_tenant_plugin_autoupgrade_check_task.py
index b5e6508006..6ad04aab0d 100644
--- a/api/tasks/process_tenant_plugin_autoupgrade_check_task.py
+++ b/api/tasks/process_tenant_plugin_autoupgrade_check_task.py
@@ -6,8 +6,8 @@ import typing
import click
from celery import shared_task
-from core.helper import marketplace
-from core.helper.marketplace import MarketplacePluginDeclaration
+from core.helper.marketplace import record_install_plugin_event
+from core.plugin.entities.marketplace import MarketplacePluginSnapshot
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.impl.plugin import PluginInstaller
from extensions.ext_redis import redis_client
@@ -16,7 +16,7 @@ from models.account import TenantPluginAutoUpgradeStrategy
logger = logging.getLogger(__name__)
RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3
-CACHE_REDIS_KEY_PREFIX = "plugin_autoupgrade_check_task:cached_plugin_manifests:"
+CACHE_REDIS_KEY_PREFIX = "plugin_autoupgrade_check_task:cached_plugin_snapshot:"
CACHE_REDIS_TTL = 60 * 60 # 1 hour
@@ -25,11 +25,11 @@ def _get_redis_cache_key(plugin_id: str) -> str:
return f"{CACHE_REDIS_KEY_PREFIX}{plugin_id}"
-def _get_cached_manifest(plugin_id: str) -> typing.Union[MarketplacePluginDeclaration, None, bool]:
+def _get_cached_manifest(plugin_id: str) -> typing.Union[MarketplacePluginSnapshot, None, bool]:
"""
Get cached plugin manifest from Redis.
Returns:
- - MarketplacePluginDeclaration: if found in cache
+ - MarketplacePluginSnapshot: if found in cache
- None: if cached as not found (marketplace returned no result)
- False: if not in cache at all
"""
@@ -43,76 +43,31 @@ def _get_cached_manifest(plugin_id: str) -> typing.Union[MarketplacePluginDeclar
if cached_json is None:
return None
- return MarketplacePluginDeclaration.model_validate(cached_json)
+ return MarketplacePluginSnapshot.model_validate(cached_json)
except Exception:
logger.exception("Failed to get cached manifest for plugin %s", plugin_id)
return False
-def _set_cached_manifest(plugin_id: str, manifest: typing.Union[MarketplacePluginDeclaration, None]) -> None:
- """
- Cache plugin manifest in Redis.
- Args:
- plugin_id: The plugin ID
- manifest: The manifest to cache, or None if not found in marketplace
- """
- try:
- key = _get_redis_cache_key(plugin_id)
- if manifest is None:
- # Cache the fact that this plugin was not found
- redis_client.setex(key, CACHE_REDIS_TTL, json.dumps(None))
- else:
- # Cache the manifest data
- redis_client.setex(key, CACHE_REDIS_TTL, manifest.model_dump_json())
- except Exception:
- # If Redis fails, continue without caching
- # traceback.print_exc()
- logger.exception("Failed to set cached manifest for plugin %s", plugin_id)
-
-
def marketplace_batch_fetch_plugin_manifests(
plugin_ids_plain_list: list[str],
-) -> list[MarketplacePluginDeclaration]:
- """Fetch plugin manifests with Redis caching support."""
- cached_manifests: dict[str, typing.Union[MarketplacePluginDeclaration, None]] = {}
- not_cached_plugin_ids: list[str] = []
+) -> list[MarketplacePluginSnapshot]:
+ """
+ Fetch plugin manifests from Redis cache only.
+ This function assumes fetch_global_plugin_manifest() has been called
+ to pre-populate the cache with all marketplace plugins.
+ """
+ result: list[MarketplacePluginSnapshot] = []
# Check Redis cache for each plugin
for plugin_id in plugin_ids_plain_list:
cached_result = _get_cached_manifest(plugin_id)
- if cached_result is False:
- # Not in cache, need to fetch
- not_cached_plugin_ids.append(plugin_id)
- else:
- # Either found manifest or cached as None (not found in marketplace)
- # At this point, cached_result is either MarketplacePluginDeclaration or None
- if isinstance(cached_result, bool):
- # This should never happen due to the if condition above, but for type safety
- continue
- cached_manifests[plugin_id] = cached_result
+ if not isinstance(cached_result, MarketplacePluginSnapshot):
+ # cached_result is False (not in cache) or None (cached as not found)
+ logger.warning("plugin %s not found in cache, skipping", plugin_id)
+ continue
- # Fetch uncached plugins from marketplace
- if not_cached_plugin_ids:
- manifests = marketplace.batch_fetch_plugin_manifests_ignore_deserialization_error(not_cached_plugin_ids)
-
- # Cache the fetched manifests
- for manifest in manifests:
- cached_manifests[manifest.plugin_id] = manifest
- _set_cached_manifest(manifest.plugin_id, manifest)
-
- # Cache plugins that were not found in marketplace
- fetched_plugin_ids = {manifest.plugin_id for manifest in manifests}
- for plugin_id in not_cached_plugin_ids:
- if plugin_id not in fetched_plugin_ids:
- cached_manifests[plugin_id] = None
- _set_cached_manifest(plugin_id, None)
-
- # Build result list from cached manifests
- result: list[MarketplacePluginDeclaration] = []
- for plugin_id in plugin_ids_plain_list:
- cached_manifest: typing.Union[MarketplacePluginDeclaration, None] = cached_manifests.get(plugin_id)
- if cached_manifest is not None:
- result.append(cached_manifest)
+ result.append(cached_result)
return result
@@ -211,7 +166,7 @@ def process_tenant_plugin_autoupgrade_check_task(
# execute upgrade
new_unique_identifier = manifest.latest_package_identifier
- marketplace.record_install_plugin_event(new_unique_identifier)
+ record_install_plugin_event(new_unique_identifier)
click.echo(
click.style(
f"Upgrade plugin: {original_unique_identifier} -> {new_unique_identifier}",
From cb970e54dae86f8d288a69313aeb064f25095423 Mon Sep 17 00:00:00 2001
From: QuantumGhost
Date: Thu, 5 Feb 2026 19:05:09 +0800
Subject: [PATCH 05/10] perf(api): Optimize the response time of AppListApi
endpoint (#31999)
---
api/controllers/console/app/app.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py
index 8c371da596..91034f2d87 100644
--- a/api/controllers/console/app/app.py
+++ b/api/controllers/console/app/app.py
@@ -1,3 +1,4 @@
+import logging
import uuid
from datetime import datetime
from typing import Any, Literal, TypeAlias
@@ -54,6 +55,8 @@ ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "co
register_enum_models(console_ns, IconType)
+_logger = logging.getLogger(__name__)
+
class AppListQuery(BaseModel):
page: int = Field(default=1, ge=1, le=99999, description="Page number (1-99999)")
@@ -499,6 +502,7 @@ class AppListApi(Resource):
select(Workflow).where(
Workflow.version == Workflow.VERSION_DRAFT,
Workflow.app_id.in_(workflow_capable_app_ids),
+ Workflow.tenant_id == current_tenant_id,
)
)
.scalars()
@@ -510,12 +514,14 @@ class AppListApi(Resource):
NodeType.TRIGGER_PLUGIN,
}
for workflow in draft_workflows:
+ node_id = None
try:
- for _, node_data in workflow.walk_nodes():
+ for node_id, node_data in workflow.walk_nodes():
if node_data.get("type") in trigger_node_types:
draft_trigger_app_ids.add(str(workflow.app_id))
break
except Exception:
+ _logger.exception("error while walking nodes, workflow_id=%s, node_id=%s", workflow.id, node_id)
continue
for app in app_pagination.items:
From 095b3ee234ddbfdfb507267bb21b0cc4bb83255b Mon Sep 17 00:00:00 2001
From: 99
Date: Thu, 5 Feb 2026 21:44:31 +0800
Subject: [PATCH 06/10] chore: Remove redundant double space in variable type
description (core/variables/variables.py) (#32002)
---
api/core/variables/variables.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/core/variables/variables.py b/api/core/variables/variables.py
index a19c53918d..338d81df78 100644
--- a/api/core/variables/variables.py
+++ b/api/core/variables/variables.py
@@ -112,7 +112,7 @@ class ArrayBooleanVariable(ArrayBooleanSegment, ArrayVariable):
class RAGPipelineVariable(BaseModel):
belong_to_node_id: str = Field(description="belong to which node id, shared means public")
- type: str = Field(description="variable type, text-input, paragraph, select, number, file, file-list")
+ type: str = Field(description="variable type, text-input, paragraph, select, number, file, file-list")
label: str = Field(description="label")
description: str | None = Field(description="description", default="")
variable: str = Field(description="variable key", default="")
From 45164ce33e4608df77c7fb0ea8344bd45f9f48af Mon Sep 17 00:00:00 2001
From: 99
Date: Fri, 6 Feb 2026 10:37:26 +0800
Subject: [PATCH 07/10] refactor: strip external imports in workflow template
transform (#32017)
---
api/.importlinter | 1 -
api/core/app/workflow/node_factory.py | 5 +++++
.../template_transform/template_transform_node.py | 13 +++++++++----
.../template_transform_node_spec.py | 2 +-
4 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/api/.importlinter b/api/.importlinter
index 9dad254560..2a6bb66a95 100644
--- a/api/.importlinter
+++ b/api/.importlinter
@@ -136,7 +136,6 @@ ignore_imports =
core.workflow.nodes.llm.llm_utils -> models.provider
core.workflow.nodes.llm.llm_utils -> services.credit_pool_service
core.workflow.nodes.llm.node -> core.tools.signature
- core.workflow.nodes.template_transform.template_transform_node -> configs
core.workflow.nodes.tool.tool_node -> core.callback_handler.workflow_tool_callback_handler
core.workflow.nodes.tool.tool_node -> core.tools.tool_engine
core.workflow.nodes.tool.tool_node -> core.tools.tool_manager
diff --git a/api/core/app/workflow/node_factory.py b/api/core/app/workflow/node_factory.py
index a5773bbef8..6717be3ae6 100644
--- a/api/core/app/workflow/node_factory.py
+++ b/api/core/app/workflow/node_factory.py
@@ -47,6 +47,7 @@ class DifyNodeFactory(NodeFactory):
code_providers: Sequence[type[CodeNodeProvider]] | None = None,
code_limits: CodeNodeLimits | None = None,
template_renderer: Jinja2TemplateRenderer | None = None,
+ template_transform_max_output_length: int | None = None,
http_request_http_client: HttpClientProtocol | None = None,
http_request_tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager,
http_request_file_manager: FileManagerProtocol | None = None,
@@ -68,6 +69,9 @@ class DifyNodeFactory(NodeFactory):
max_object_array_length=dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH,
)
self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer()
+ self._template_transform_max_output_length = (
+ template_transform_max_output_length or dify_config.TEMPLATE_TRANSFORM_MAX_LENGTH
+ )
self._http_request_http_client = http_request_http_client or ssrf_proxy
self._http_request_tool_file_manager_factory = http_request_tool_file_manager_factory
self._http_request_file_manager = http_request_file_manager or file_manager
@@ -122,6 +126,7 @@ class DifyNodeFactory(NodeFactory):
graph_init_params=self.graph_init_params,
graph_runtime_state=self.graph_runtime_state,
template_renderer=self._template_renderer,
+ max_output_length=self._template_transform_max_output_length,
)
if node_type == NodeType.HTTP_REQUEST:
diff --git a/api/core/workflow/nodes/template_transform/template_transform_node.py b/api/core/workflow/nodes/template_transform/template_transform_node.py
index f7e0bccccf..3dc8afd9be 100644
--- a/api/core/workflow/nodes/template_transform/template_transform_node.py
+++ b/api/core/workflow/nodes/template_transform/template_transform_node.py
@@ -1,7 +1,6 @@
from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Any
-from configs import dify_config
from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus
from core.workflow.node_events import NodeRunResult
from core.workflow.nodes.base.node import Node
@@ -16,12 +15,13 @@ if TYPE_CHECKING:
from core.workflow.entities import GraphInitParams
from core.workflow.runtime import GraphRuntimeState
-MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH = dify_config.TEMPLATE_TRANSFORM_MAX_LENGTH
+DEFAULT_TEMPLATE_TRANSFORM_MAX_OUTPUT_LENGTH = 400_000
class TemplateTransformNode(Node[TemplateTransformNodeData]):
node_type = NodeType.TEMPLATE_TRANSFORM
_template_renderer: Jinja2TemplateRenderer
+ _max_output_length: int
def __init__(
self,
@@ -31,6 +31,7 @@ class TemplateTransformNode(Node[TemplateTransformNodeData]):
graph_runtime_state: "GraphRuntimeState",
*,
template_renderer: Jinja2TemplateRenderer | None = None,
+ max_output_length: int | None = None,
) -> None:
super().__init__(
id=id,
@@ -40,6 +41,10 @@ class TemplateTransformNode(Node[TemplateTransformNodeData]):
)
self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer()
+ if max_output_length is not None and max_output_length <= 0:
+ raise ValueError("max_output_length must be a positive integer")
+ self._max_output_length = max_output_length or DEFAULT_TEMPLATE_TRANSFORM_MAX_OUTPUT_LENGTH
+
@classmethod
def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]:
"""
@@ -69,11 +74,11 @@ class TemplateTransformNode(Node[TemplateTransformNodeData]):
except TemplateRenderError as e:
return NodeRunResult(inputs=variables, status=WorkflowNodeExecutionStatus.FAILED, error=str(e))
- if len(rendered) > MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH:
+ if len(rendered) > self._max_output_length:
return NodeRunResult(
inputs=variables,
status=WorkflowNodeExecutionStatus.FAILED,
- error=f"Output length exceeds {MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH} characters",
+ error=f"Output length exceeds {self._max_output_length} characters",
)
return NodeRunResult(
diff --git a/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py b/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py
index 66d6c3c56b..61bdcbd250 100644
--- a/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py
+++ b/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py
@@ -217,7 +217,6 @@ class TestTemplateTransformNode:
@patch(
"core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template"
)
- @patch("core.workflow.nodes.template_transform.template_transform_node.MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH", 10)
def test_run_output_length_exceeds_limit(
self, mock_execute, basic_node_data, mock_graph, mock_graph_runtime_state, graph_init_params
):
@@ -231,6 +230,7 @@ class TestTemplateTransformNode:
graph_init_params=graph_init_params,
graph=mock_graph,
graph_runtime_state=mock_graph_runtime_state,
+ max_output_length=10,
)
result = node._run()
From 59a9cbbf78c3bbe20d15108c5d68066132369ed4 Mon Sep 17 00:00:00 2001
From: Ryan
Date: Fri, 6 Feb 2026 10:46:50 +0800
Subject: [PATCH 08/10] chore: remove .codex/skills directory (#32022)
Co-authored-by: Longwei Liu
---
.codex/skills/component-refactoring | 1 -
.codex/skills/frontend-code-review | 1 -
.codex/skills/frontend-testing | 1 -
.codex/skills/orpc-contract-first | 1 -
4 files changed, 4 deletions(-)
delete mode 120000 .codex/skills/component-refactoring
delete mode 120000 .codex/skills/frontend-code-review
delete mode 120000 .codex/skills/frontend-testing
delete mode 120000 .codex/skills/orpc-contract-first
diff --git a/.codex/skills/component-refactoring b/.codex/skills/component-refactoring
deleted file mode 120000
index 53ae67e2f2..0000000000
--- a/.codex/skills/component-refactoring
+++ /dev/null
@@ -1 +0,0 @@
-../../.agents/skills/component-refactoring
\ No newline at end of file
diff --git a/.codex/skills/frontend-code-review b/.codex/skills/frontend-code-review
deleted file mode 120000
index 55654ffbd7..0000000000
--- a/.codex/skills/frontend-code-review
+++ /dev/null
@@ -1 +0,0 @@
-../../.agents/skills/frontend-code-review
\ No newline at end of file
diff --git a/.codex/skills/frontend-testing b/.codex/skills/frontend-testing
deleted file mode 120000
index 092cec7745..0000000000
--- a/.codex/skills/frontend-testing
+++ /dev/null
@@ -1 +0,0 @@
-../../.agents/skills/frontend-testing
\ No newline at end of file
diff --git a/.codex/skills/orpc-contract-first b/.codex/skills/orpc-contract-first
deleted file mode 120000
index da47b335c7..0000000000
--- a/.codex/skills/orpc-contract-first
+++ /dev/null
@@ -1 +0,0 @@
-../../.agents/skills/orpc-contract-first
\ No newline at end of file
From b24e6edada70f1711b745787919e5b113dd047ad Mon Sep 17 00:00:00 2001
From: wangxiaolei
Date: Fri, 6 Feb 2026 11:24:39 +0800
Subject: [PATCH 09/10] fix: fix agent node tool type is not right (#32008)
Infer real tool type via querying relevant database tables.
The root cause for incorrect `type` field is still not clear.
---
api/.importlinter | 2 +
api/core/workflow/nodes/agent/agent_node.py | 42 +++-
.../core/workflow/nodes/agent/__init__.py | 0
.../workflow/nodes/agent/test_agent_node.py | 197 ++++++++++++++++++
4 files changed, 239 insertions(+), 2 deletions(-)
create mode 100644 api/tests/unit_tests/core/workflow/nodes/agent/__init__.py
create mode 100644 api/tests/unit_tests/core/workflow/nodes/agent/test_agent_node.py
diff --git a/api/.importlinter b/api/.importlinter
index 2a6bb66a95..fb66df7334 100644
--- a/api/.importlinter
+++ b/api/.importlinter
@@ -102,6 +102,8 @@ forbidden_modules =
core.trigger
core.variables
ignore_imports =
+ core.workflow.nodes.agent.agent_node -> core.db.session_factory
+ core.workflow.nodes.agent.agent_node -> models.tools
core.workflow.nodes.loop.loop_node -> core.app.workflow.node_factory
core.workflow.graph_engine.command_channels.redis_channel -> extensions.ext_redis
core.workflow.workflow_entry -> core.app.workflow.layers.observability
diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py
index e195aebe6d..e64a83034c 100644
--- a/api/core/workflow/nodes/agent/agent_node.py
+++ b/api/core/workflow/nodes/agent/agent_node.py
@@ -2,7 +2,7 @@ from __future__ import annotations
import json
from collections.abc import Generator, Mapping, Sequence
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, Any, Union, cast
from packaging.version import Version
from pydantic import ValidationError
@@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
from core.agent.entities import AgentToolEntity
from core.agent.plugin_entities import AgentStrategyParameter
+from core.db.session_factory import session_factory
from core.file import File, FileTransferMethod
from core.memory.token_buffer_memory import TokenBufferMemory
from core.model_manager import ModelInstance, ModelManager
@@ -49,6 +50,12 @@ from factories import file_factory
from factories.agent_factory import get_plugin_agent_strategy
from models import ToolFile
from models.model import Conversation
+from models.tools import (
+ ApiToolProvider,
+ BuiltinToolProvider,
+ MCPToolProvider,
+ WorkflowToolProvider,
+)
from services.tools.builtin_tools_manage_service import BuiltinToolManageService
from .exc import (
@@ -259,7 +266,7 @@ class AgentNode(Node[AgentNodeData]):
value = cast(list[dict[str, Any]], value)
tool_value = []
for tool in value:
- provider_type = ToolProviderType(tool.get("type", ToolProviderType.BUILT_IN))
+ provider_type = self._infer_tool_provider_type(tool, self.tenant_id)
setting_params = tool.get("settings", {})
parameters = tool.get("parameters", {})
manual_input_params = [key for key, value in parameters.items() if value is not None]
@@ -748,3 +755,34 @@ class AgentNode(Node[AgentNodeData]):
llm_usage=llm_usage,
)
)
+
+ @staticmethod
+ def _infer_tool_provider_type(tool_config: dict[str, Any], tenant_id: str) -> ToolProviderType:
+ provider_type_str = tool_config.get("type")
+ if provider_type_str:
+ return ToolProviderType(provider_type_str)
+
+ provider_id = tool_config.get("provider_name")
+ if not provider_id:
+ return ToolProviderType.BUILT_IN
+
+ with session_factory.create_session() as session:
+ provider_map: dict[
+ type[Union[WorkflowToolProvider, MCPToolProvider, ApiToolProvider, BuiltinToolProvider]],
+ ToolProviderType,
+ ] = {
+ WorkflowToolProvider: ToolProviderType.WORKFLOW,
+ MCPToolProvider: ToolProviderType.MCP,
+ ApiToolProvider: ToolProviderType.API,
+ BuiltinToolProvider: ToolProviderType.BUILT_IN,
+ }
+
+ for provider_model, provider_type in provider_map.items():
+ stmt = select(provider_model).where(
+ provider_model.id == provider_id,
+ provider_model.tenant_id == tenant_id,
+ )
+ if session.scalar(stmt):
+ return provider_type
+
+ raise AgentNodeError(f"Tool provider with ID '{provider_id}' not found.")
diff --git a/api/tests/unit_tests/core/workflow/nodes/agent/__init__.py b/api/tests/unit_tests/core/workflow/nodes/agent/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api/tests/unit_tests/core/workflow/nodes/agent/test_agent_node.py b/api/tests/unit_tests/core/workflow/nodes/agent/test_agent_node.py
new file mode 100644
index 0000000000..a95892d0b6
--- /dev/null
+++ b/api/tests/unit_tests/core/workflow/nodes/agent/test_agent_node.py
@@ -0,0 +1,197 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from core.tools.entities.tool_entities import ToolProviderType
+from core.workflow.nodes.agent.agent_node import AgentNode
+
+
+class TestInferToolProviderType:
+ """Test cases for AgentNode._infer_tool_provider_type method."""
+
+ def test_infer_type_from_config_workflow(self):
+ """Test inferring workflow provider type from config."""
+ tool_config = {
+ "type": "workflow",
+ "provider_name": "workflow-provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ result = AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ assert result == ToolProviderType.WORKFLOW
+
+ def test_infer_type_from_config_builtin(self):
+ """Test inferring builtin provider type from config."""
+ tool_config = {
+ "type": "builtin",
+ "provider_name": "builtin-provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ result = AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ assert result == ToolProviderType.BUILT_IN
+
+ def test_infer_type_from_config_api(self):
+ """Test inferring API provider type from config."""
+ tool_config = {
+ "type": "api",
+ "provider_name": "api-provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ result = AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ assert result == ToolProviderType.API
+
+ def test_infer_type_from_config_mcp(self):
+ """Test inferring MCP provider type from config."""
+ tool_config = {
+ "type": "mcp",
+ "provider_name": "mcp-provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ result = AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ assert result == ToolProviderType.MCP
+
+ def test_infer_type_invalid_config_value_raises_error(self):
+ """Test that invalid type value in config raises ValueError."""
+ tool_config = {
+ "type": "invalid-type",
+ "provider_name": "workflow-provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ with pytest.raises(ValueError):
+ AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ def test_infer_workflow_type_from_database(self):
+ """Test inferring workflow provider type from database."""
+ tool_config = {
+ "provider_name": "workflow-provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ with patch("core.db.session_factory.session_factory.create_session") as mock_create_session:
+ mock_session = MagicMock()
+ mock_create_session.return_value.__enter__.return_value = mock_session
+
+ # First query (WorkflowToolProvider) returns a result
+ mock_session.scalar.return_value = True
+
+ result = AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ assert result == ToolProviderType.WORKFLOW
+ # Should only query once (after finding WorkflowToolProvider)
+ assert mock_session.scalar.call_count == 1
+
+ def test_infer_mcp_type_from_database(self):
+ """Test inferring MCP provider type from database."""
+ tool_config = {
+ "provider_name": "mcp-provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ with patch("core.db.session_factory.session_factory.create_session") as mock_create_session:
+ mock_session = MagicMock()
+ mock_create_session.return_value.__enter__.return_value = mock_session
+
+ # First query (WorkflowToolProvider) returns None
+ # Second query (MCPToolProvider) returns a result
+ mock_session.scalar.side_effect = [None, True]
+
+ result = AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ assert result == ToolProviderType.MCP
+ assert mock_session.scalar.call_count == 2
+
+ def test_infer_api_type_from_database(self):
+ """Test inferring API provider type from database."""
+ tool_config = {
+ "provider_name": "api-provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ with patch("core.db.session_factory.session_factory.create_session") as mock_create_session:
+ mock_session = MagicMock()
+ mock_create_session.return_value.__enter__.return_value = mock_session
+
+ # First query (WorkflowToolProvider) returns None
+ # Second query (MCPToolProvider) returns None
+ # Third query (ApiToolProvider) returns a result
+ mock_session.scalar.side_effect = [None, None, True]
+
+ result = AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ assert result == ToolProviderType.API
+ assert mock_session.scalar.call_count == 3
+
+ def test_infer_builtin_type_from_database(self):
+ """Test inferring builtin provider type from database."""
+ tool_config = {
+ "provider_name": "builtin-provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ with patch("core.db.session_factory.session_factory.create_session") as mock_create_session:
+ mock_session = MagicMock()
+ mock_create_session.return_value.__enter__.return_value = mock_session
+
+ # First three queries return None
+ # Fourth query (BuiltinToolProvider) returns a result
+ mock_session.scalar.side_effect = [None, None, None, True]
+
+ result = AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ assert result == ToolProviderType.BUILT_IN
+ assert mock_session.scalar.call_count == 4
+
+ def test_infer_type_default_when_not_found(self):
+ """Test raising AgentNodeError when provider is not found in database."""
+ tool_config = {
+ "provider_name": "unknown-provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ with patch("core.db.session_factory.session_factory.create_session") as mock_create_session:
+ mock_session = MagicMock()
+ mock_create_session.return_value.__enter__.return_value = mock_session
+
+ # All queries return None
+ mock_session.scalar.return_value = None
+
+ # Current implementation raises AgentNodeError when provider not found
+ from core.workflow.nodes.agent.exc import AgentNodeError
+
+ with pytest.raises(AgentNodeError, match="Tool provider with ID 'unknown-provider-id' not found"):
+ AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ def test_infer_type_default_when_no_provider_name(self):
+ """Test defaulting to BUILT_IN when provider_name is missing."""
+ tool_config = {}
+ tenant_id = "test-tenant"
+
+ result = AgentNode._infer_tool_provider_type(tool_config, tenant_id)
+
+ assert result == ToolProviderType.BUILT_IN
+
+ def test_infer_type_database_exception_propagates(self):
+ """Test that database exception propagates (current implementation doesn't catch it)."""
+ tool_config = {
+ "provider_name": "provider-id",
+ }
+ tenant_id = "test-tenant"
+
+ with patch("core.db.session_factory.session_factory.create_session") as mock_create_session:
+ mock_session = MagicMock()
+ mock_create_session.return_value.__enter__.return_value = mock_session
+
+ # Database query raises exception
+ mock_session.scalar.side_effect = Exception("Database error")
+
+ # Current implementation doesn't catch exceptions, so it propagates
+ with pytest.raises(Exception, match="Database error"):
+ AgentNode._infer_tool_provider_type(tool_config, tenant_id)
From d9530f7bb75c324ec17fae870d752209e1e86194 Mon Sep 17 00:00:00 2001
From: longbingljw
Date: Fri, 6 Feb 2026 13:01:31 +0900
Subject: [PATCH 10/10] fix: make `flask upgrade-db` fail on error (#32024)
---
api/commands.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/api/commands.py b/api/commands.py
index c4f2c9edbb..93855bc3b8 100644
--- a/api/commands.py
+++ b/api/commands.py
@@ -739,8 +739,10 @@ def upgrade_db():
click.echo(click.style("Database migration successful!", fg="green"))
- except Exception:
+ except Exception as e:
logger.exception("Failed to execute database migration")
+ click.echo(click.style(f"Database migration failed: {e}", fg="red"))
+ raise SystemExit(1)
finally:
lock.release()
else: