From 46b45b98a05cde98c19265743963389e349722be Mon Sep 17 00:00:00 2001 From: Oliver Dudgeon <22367286+OliverDudgeon@users.noreply.github.com> Date: Sat, 15 Mar 2025 23:38:00 +0000 Subject: [PATCH 1/3] refactor: replace react-hook-form with tanstack form --- package.json | 2 +- pnpm-lock.yaml | 254 ++++++++++++++++++++++-- src/features/SDFViewer/ConfigEditor.tsx | 200 +++++++++++-------- 3 files changed, 359 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index 231efd1d2..869331475 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@squonk/mui-theme": "5.0.0", "@squonk/sdf-parser": "1.3.0", "@tanstack/match-sorter-utils": "8.19.4", + "@tanstack/react-form": "^1.0.5", "@tanstack/react-query": "5.71.1", "@tanstack/react-query-devtools": "5.71.2", "@tanstack/react-table": "8.21.2", @@ -93,7 +94,6 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-dropzone": "14.3.8", - "react-hook-form": "7.54.2", "react-plotly.js": "2.6.0", "react-use-websocket": "^4.13.0", "sharp": "0.33.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a58481b6d..b57386b23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: '@tanstack/match-sorter-utils': specifier: 8.19.4 version: 8.19.4 + '@tanstack/react-form': + specifier: ^1.0.5 + version: 1.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.2) '@tanstack/react-query': specifier: 5.71.1 version: 5.71.1(react@19.1.0) @@ -185,9 +188,6 @@ importers: react-dropzone: specifier: 14.3.8 version: 14.3.8(react@19.1.0) - react-hook-form: - specifier: 7.54.2 - version: 7.54.2(react@19.1.0) react-plotly.js: specifier: 2.6.0 version: 2.6.0(plotly.js@2.35.3(mapbox-gl@1.13.3)(webpack@5.97.1))(react@19.1.0) @@ -1396,6 +1396,44 @@ packages: react-redux: optional: true + '@remix-run/node@2.16.0': + resolution: {integrity: sha512-9yYBYCHYO1+bIScGAtOy5/r4BoTS8E5lpQmjWP99UxSCSiKHPEO76V9Z8mmmarTNis/FPN+sUwfmbQWNHLA2vw==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@remix-run/router@1.23.0': + resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} + engines: {node: '>=14.0.0'} + + '@remix-run/server-runtime@2.16.0': + resolution: {integrity: sha512-gbuc4slxPi+pT47MrUYprX/wCuDlYL6H3LHZSvimWO1kDCBt8oefHzdHDPjLi4B1xzqXZomswTbuJzpZ7xRRTg==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@remix-run/web-blob@3.1.0': + resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} + + '@remix-run/web-fetch@4.4.2': + resolution: {integrity: sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==} + engines: {node: ^10.17 || >=12.3} + + '@remix-run/web-file@3.1.0': + resolution: {integrity: sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==} + + '@remix-run/web-form-data@3.1.0': + resolution: {integrity: sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==} + + '@remix-run/web-stream@1.1.0': + resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==} + '@rjsf/core@5.24.8': resolution: {integrity: sha512-7YdOMl0UylldYR75rnKoQF5etibSwPQHA3rY8LEo28xyYek81ESGuqPTs37P14doBpGRL+SobggR76tYhDYO7A==} engines: {node: '>=14'} @@ -1601,6 +1639,9 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tanstack/form-core@1.0.5': + resolution: {integrity: sha512-jLwdYKFTfdf9DcNLi9QS6sdHV3NsEsfWR0LHc5qosFfGS8WXQ/EAm8yr88jZyV4gvEO8XE8W2Sf7i4VAZnxxCQ==} + '@tanstack/match-sorter-utils@8.19.4': resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} @@ -1611,6 +1652,18 @@ packages: '@tanstack/query-devtools@5.71.2': resolution: {integrity: sha512-cg2TExgXM1TL3f+BPf6R7iXsPZnrj16HvqBT88xKr1hduXkccs/5EHf2X3zSNwB0Y9XfowSQB8dcGa2qKYv5ZA==} + '@tanstack/react-form@1.0.5': + resolution: {integrity: sha512-4e9Au0s2Uk7f2nlY0olte5s8k8vZHLjmqvRoVyw+rjzt0clDnryNJu3ZHuOaUcyG0vOG/Ame84q2voyImBKzFQ==} + peerDependencies: + '@tanstack/react-start': ^1.112.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + vinxi: ^0.5.0 + peerDependenciesMeta: + '@tanstack/react-start': + optional: true + vinxi: + optional: true + '@tanstack/react-query-devtools@5.71.2': resolution: {integrity: sha512-HQNJxTLELzGYBdSdhLToVzyrVJAA01OP7ntNj20sWjzUb+jBYKWbp2TqHopOaRbIOyACu+PMNUb7Xqjk1jfReQ==} peerDependencies: @@ -1622,6 +1675,12 @@ packages: peerDependencies: react: ^18 || ^19 + '@tanstack/react-store@0.7.0': + resolution: {integrity: sha512-S/Rq17HaGOk+tQHV/yrePMnG1xbsKZIl/VsNWnNXt4XW+tTY8dTlvpJH2ZQ3GRALsusG5K6Q3unAGJ2pd9W/Ng==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-table@8.21.2': resolution: {integrity: sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==} engines: {node: '>=12'} @@ -1629,6 +1688,9 @@ packages: react: '>=16.8' react-dom: '>=16.8' + '@tanstack/store@0.7.0': + resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} + '@tanstack/table-core@8.21.2': resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==} engines: {node: '>=12'} @@ -1660,6 +1722,9 @@ packages: '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1921,6 +1986,9 @@ packages: cpu: [x64] os: [win32] + '@web3-storage/multipart-parser@1.0.0': + resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -1972,10 +2040,17 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@zxing/text-encoding@0.9.0': + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + abs-svg-path@0.1.1: resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} @@ -2427,6 +2502,14 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -2677,6 +2760,10 @@ packages: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} + data-uri-to-buffer@3.0.1: + resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} + engines: {node: '>= 6'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -2728,6 +2815,9 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decode-formdata@0.8.0: + resolution: {integrity: sha512-iUzDgnWsw5ToSkFY7VPFA5Gfph6ROoOxOB7Ybna4miUSzLZ4KaSJk6IAB2AdW6+C9vCVWhjjNA4gjT6wF3eZHQ==} + decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -3113,6 +3203,10 @@ packages: event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -4393,6 +4487,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -4950,12 +5048,6 @@ packages: react-fast-compare@2.0.4: resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} - react-hook-form@7.54.2: - resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==} - engines: {node: '>=18.0.0'} - peerDependencies: - react: ^16.8.0 || ^17 || ^18 || ^19 - react-intersection-observer@8.34.0: resolution: {integrity: sha512-TYKh52Zc0Uptp5/b4N91XydfSGKubEhgZRtcg1rhTKABXijc4Sdr1uTp5lJ8TN27jwUsdXxjHXtHa0kPj704sw==} peerDependencies: @@ -5298,6 +5390,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -5422,6 +5517,9 @@ packages: stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + stream-slice@0.1.2: + resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -5677,6 +5775,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -5774,6 +5875,10 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + undici@6.21.2: + resolution: {integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==} + engines: {node: '>=18.17'} + unified-engine@11.2.2: resolution: {integrity: sha512-15g/gWE7qQl9tQ3nAEbMd5h9HV1EACtFs6N9xaRBZICoCwnNGbal1kOs++ICf4aiTdItZxU2s/kYWhW7htlqJg==} @@ -5849,6 +5954,11 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5917,6 +6027,9 @@ packages: weak-map@1.0.8: resolution: {integrity: sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==} + web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -7387,6 +7500,60 @@ snapshots: react: 19.1.0 react-redux: 7.2.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@remix-run/node@2.16.0(typescript@5.8.2)': + dependencies: + '@remix-run/server-runtime': 2.16.0(typescript@5.8.2) + '@remix-run/web-fetch': 4.4.2 + '@web3-storage/multipart-parser': 1.0.0 + cookie-signature: 1.2.2 + source-map-support: 0.5.21 + stream-slice: 0.1.2 + undici: 6.21.2 + optionalDependencies: + typescript: 5.8.2 + + '@remix-run/router@1.23.0': {} + + '@remix-run/server-runtime@2.16.0(typescript@5.8.2)': + dependencies: + '@remix-run/router': 1.23.0 + '@types/cookie': 0.6.0 + '@web3-storage/multipart-parser': 1.0.0 + cookie: 0.6.0 + set-cookie-parser: 2.7.1 + source-map: 0.7.4 + turbo-stream: 2.4.0 + optionalDependencies: + typescript: 5.8.2 + + '@remix-run/web-blob@3.1.0': + dependencies: + '@remix-run/web-stream': 1.1.0 + web-encoding: 1.1.5 + + '@remix-run/web-fetch@4.4.2': + dependencies: + '@remix-run/web-blob': 3.1.0 + '@remix-run/web-file': 3.1.0 + '@remix-run/web-form-data': 3.1.0 + '@remix-run/web-stream': 1.1.0 + '@web3-storage/multipart-parser': 1.0.0 + abort-controller: 3.0.0 + data-uri-to-buffer: 3.0.1 + mrmime: 1.0.1 + + '@remix-run/web-file@3.1.0': + dependencies: + '@remix-run/web-blob': 3.1.0 + + '@remix-run/web-form-data@3.1.0': + dependencies: + web-encoding: 1.1.5 + + '@remix-run/web-stream@1.1.0': + dependencies: + web-streams-polyfill: 3.3.3 + '@rjsf/core@5.24.8(@rjsf/utils@5.24.8(react@19.1.0))(react@19.1.0)': dependencies: '@rjsf/utils': 5.24.8(react@19.1.0) @@ -7681,6 +7848,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@tanstack/form-core@1.0.5': + dependencies: + '@tanstack/store': 0.7.0 + '@tanstack/match-sorter-utils@8.19.4': dependencies: remove-accents: 0.5.0 @@ -7689,6 +7860,17 @@ snapshots: '@tanstack/query-devtools@5.71.2': {} + '@tanstack/react-form@1.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.2)': + dependencies: + '@remix-run/node': 2.16.0(typescript@5.8.2) + '@tanstack/form-core': 1.0.5 + '@tanstack/react-store': 0.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + decode-formdata: 0.8.0 + react: 19.1.0 + transitivePeerDependencies: + - react-dom + - typescript + '@tanstack/react-query-devtools@5.71.2(@tanstack/react-query@5.71.1(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/query-devtools': 5.71.2 @@ -7700,12 +7882,21 @@ snapshots: '@tanstack/query-core': 5.71.1 react: 19.1.0 + '@tanstack/react-store@0.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/store': 0.7.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + use-sync-external-store: 1.4.0(react@19.1.0) + '@tanstack/react-table@8.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/table-core': 8.21.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + '@tanstack/store@0.7.0': {} + '@tanstack/table-core@8.21.2': {} '@turf/area@7.2.0': @@ -7756,6 +7947,8 @@ snapshots: dependencies: '@types/node': 22.9.0 + '@types/cookie@0.6.0': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -8034,6 +8227,8 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.3.3': optional: true + '@web3-storage/multipart-parser@1.0.0': {} + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -8114,8 +8309,15 @@ snapshots: '@xtuc/long@4.2.2': {} + '@zxing/text-encoding@0.9.0': + optional: true + abbrev@2.0.0: {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + abs-svg-path@0.1.1: {} acorn-import-attributes@1.9.5(acorn@8.14.1): @@ -8586,6 +8788,10 @@ snapshots: convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.6.0: {} + cookie@0.7.2: {} core-js-compat@3.41.0: @@ -8864,6 +9070,8 @@ snapshots: es5-ext: 0.10.64 type: 2.7.3 + data-uri-to-buffer@3.0.1: {} + data-uri-to-buffer@4.0.1: {} data-view-buffer@1.0.2: @@ -8902,6 +9110,8 @@ snapshots: decamelize@1.2.0: {} + decode-formdata@0.8.0: {} + decode-named-character-reference@1.0.2: dependencies: character-entities: 2.0.2 @@ -9465,6 +9675,8 @@ snapshots: d: 1.0.2 es5-ext: 0.10.64 + event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} @@ -11160,6 +11372,8 @@ snapshots: mri@1.2.0: {} + mrmime@1.0.1: {} + mrmime@2.0.0: {} ms@2.0.0: {} @@ -11758,10 +11972,6 @@ snapshots: react-fast-compare@2.0.4: {} - react-hook-form@7.54.2(react@19.1.0): - dependencies: - react: 19.1.0 - react-intersection-observer@8.34.0(react@19.1.0): dependencies: react: 19.1.0 @@ -12206,6 +12416,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -12367,6 +12579,8 @@ snapshots: stream-shift@1.0.3: {} + stream-slice@0.1.2: {} + streamsearch@1.1.0: {} string-argv@0.3.2: {} @@ -12629,6 +12843,8 @@ snapshots: tslib@2.8.1: {} + turbo-stream@2.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -12726,6 +12942,8 @@ snapshots: undici-types@6.20.0: {} + undici@6.21.2: {} + unified-engine@11.2.2: dependencies: '@types/concat-stream': 2.0.3 @@ -12859,6 +13077,10 @@ snapshots: react-dom: 19.1.0(react@19.1.0) resize-observer-polyfill: 1.5.1 + use-sync-external-store@1.4.0(react@19.1.0): + dependencies: + react: 19.1.0 + util-deprecate@1.0.2: {} util@0.12.5: @@ -12948,6 +13170,12 @@ snapshots: weak-map@1.0.8: {} + web-encoding@1.1.5: + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + web-streams-polyfill@3.3.3: {} webgl-context@2.2.0: diff --git a/src/features/SDFViewer/ConfigEditor.tsx b/src/features/SDFViewer/ConfigEditor.tsx index 1770560aa..03d575ec5 100644 --- a/src/features/SDFViewer/ConfigEditor.tsx +++ b/src/features/SDFViewer/ConfigEditor.tsx @@ -1,7 +1,7 @@ import { Fragment } from "react"; -import { Controller, type SubmitHandler, useForm } from "react-hook-form"; import { Alert, Box, Button, Checkbox, MenuItem, TextField, Typography } from "@mui/material"; +import { type Updater, useForm } from "@tanstack/react-form"; import { type SDFViewerConfig } from "../../utils/api/sdfViewer"; import { type JSON_SCHEMA_TYPE, JSON_SCHEMA_TYPES } from "../../utils/app/jsonSchema"; @@ -36,6 +36,19 @@ const getDefault = (field: string, dtype: JSON_SCHEMA_TYPE) => ({ sort: "ASC", }); +const getStep = (_field: string, type: JSON_SCHEMA_TYPE) => { + switch (type) { + case "number": + return 0.1; + case "integer": + return 1; + default: + return undefined; + } +}; + +const getIsNumeric = (type: JSON_SCHEMA_TYPE) => type === "number" || type === "integer"; + export const ConfigEditor = ({ schema, config, onChange }: ConfigEditorProps) => { const { fields } = schema; @@ -45,37 +58,24 @@ export const ConfigEditor = ({ schema, config, onChange }: ConfigEditorProps) => !fieldsInConfig.includes(field) && (config[field] = getDefault(field, fields[field].type)), ); - const { control, register, watch, handleSubmit } = useForm({ + const form = useForm({ defaultValues: config, + onSubmit: ({ value }) => { + onChange(value); + }, }); if (Object.values(fields).length === 0) { return No fields found in schema; } - const getStep = (field: string) => { - const type = watch(field).dtype; - switch (type) { - case "number": - return 0.1; - case "integer": - return 1; - default: - return undefined; - } - }; - - const getIsNumeric = (key: string) => - watch(key).dtype === "number" || watch(key).dtype === "integer"; - - // data isn't really of type SDFViewerConfig, inputs give string values instead of numbers, but - // our Infinity defaults are numbers - const onSubmit: SubmitHandler = (data) => onChange(data); - - // const onSubmit: SubmitHandler = (data) => console.log(data); - return ( -
void handleSubmit(onSubmit, (errors) => console.log(errors))(event)}> + { + e.preventDefault(); + void form.handleSubmit(); + }} + > Field name @@ -99,74 +99,108 @@ export const ConfigEditor = ({ schema, config, onChange }: ConfigEditorProps) => Sort - {Object.entries(fields).map(([key], index) => ( + {Object.entries(fields).map(([key,], index) => ( // eslint-disable-next-line react/no-array-index-key {key} - - {JSON_SCHEMA_TYPES.map((type) => ( - - {type} - - ))} - - ( + + + {(field) => ( + field.handleChange(e.target.value as Updater)} + > + {JSON_SCHEMA_TYPES.map((type) => ( + + {type} + + ))} + + )} + + + + {(field) => ( field.onChange(e.target.checked)} + checked={Boolean(field.state.value)} + onChange={(e) => field.handleChange(e.target.checked)} /> )} - /> - ( + + + + {(field) => ( field.onChange(e.target.checked)} + checked={Boolean(field.state.value)} + onChange={(e) => field.handleChange(e.target.checked)} /> )} - /> - - - - - ASC - - - DESC - - + + + + {(field) => ( + state.values[key].dtype}> + {(currentType) => ( + { + const value = e.target.value === "" ? "" : Number(e.target.value); + field.handleChange(value as Updater); + }} + /> + )} + + )} + + + + {(field) => ( + state.values[key].dtype}> + {(currentType) => ( + { + const value = e.target.value === "" ? "" : Number(e.target.value); + field.handleChange(value as Updater); + }} + /> + )} + + )} + + + + {(field) => ( + field.handleChange(e.target.value as Updater)} + > + + ASC + + + DESC + + + )} + ))} - -
- ); - }} - + + + {(field) => ( + 0} + helperText={field.state.meta.errors.map((error) => error?.message)[0]} + label="Name" + sx={{ maxWidth: 150 }} + value={field.state.value} + onBlur={field.handleBlur} + onChange={(e) => field.handleChange(e.target.value)} + /> + )} + + + + {(field) => ( + 0} + helperText={field.state.meta.errors.map((error) => error?.message)[0]} + label="Allowance" + slotProps={{ htmlInput: { min: 1 } }} + sx={{ maxWidth: 100 }} + type="number" + value={field.state.value} + onBlur={field.handleBlur} + onChange={(e) => field.handleChange(Number(e.target.value))} + /> + )} + + + {!!cost && Cost: {formatCoins(cost * form.state.values.allowance)}} + + + ); }; diff --git a/src/components/LowerCaseTextField.tsx b/src/components/LowerCaseTextField.tsx deleted file mode 100644 index 51c3e253b..000000000 --- a/src/components/LowerCaseTextField.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { type ChangeEvent, useCallback } from "react"; - -import { TextField as MuiTextField } from "@mui/material"; -import { fieldToTextField, type TextFieldProps } from "formik-mui"; - -/** - * Formik binding for a Mui TextField forcing the typed text to lowercase - */ -export const LowerCaseTextField = (props: TextFieldProps) => { - const { - form: { setFieldValue }, - field: { name }, - } = props; - - const onChange = useCallback( - (event: ChangeEvent) => { - const { value } = event.target; - void setFieldValue(name, value ? value.toLowerCase() : ""); - }, - [setFieldValue, name], - ); - - return ; -}; diff --git a/src/components/labels/NewLabelButton.tsx b/src/components/labels/NewLabelButton.tsx index 1a218e2ae..f13abd718 100644 --- a/src/components/labels/NewLabelButton.tsx +++ b/src/components/labels/NewLabelButton.tsx @@ -3,16 +3,14 @@ import { getGetDatasetsQueryKey } from "@squonk/data-manager-client/dataset"; import { useAddMetadata } from "@squonk/data-manager-client/metadata"; import { AddCircleOutlineRounded as AddCircleOutlineRoundedIcon } from "@mui/icons-material"; -import { Box, Button, IconButton, Popover, Tooltip } from "@mui/material"; +import { Box, Button, IconButton, Popover, TextField, Tooltip } from "@mui/material"; +import { useForm } from "@tanstack/react-form"; import { useQueryClient } from "@tanstack/react-query"; -import { Field, Form, Formik } from "formik"; -import { TextField } from "formik-mui"; import { bindPopover, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; -import * as yup from "yup"; +import { z } from "zod"; import { type TableDataset } from "../../features/DatasetsTable"; import { useEnqueueError } from "../../hooks/useEnqueueStackError"; -import { LowerCaseTextField } from "../LowerCaseTextField"; export interface NewLabelButtonProps { /** @@ -28,6 +26,45 @@ export const NewLabelButton = ({ datasetId }: NewLabelButtonProps) => { const popupState = usePopupState({ variant: "popover", popupId: `add-label-${datasetId}` }); + // Define Zod schema for validation + const labelSchema = z.object({ + label: z.string().trim().min(1, "A label name is required"), + value: z.string(), + }); + + const form = useForm({ + defaultValues: { + label: "", + value: "", + }, + validators: { + onChange: labelSchema, + }, + onSubmit: async ({ value }) => { + try { + await addAnnotations({ + datasetId, + data: { + labels: JSON.stringify([ + { + type: "LabelAnnotation", + label: value.label.trim().toLowerCase(), + value: value.value.trim(), + active: true, + }, + ]), + }, + }); + await queryClient.invalidateQueries({ queryKey: getGetDatasetsQueryKey() }); + form.reset(); + } catch (error) { + enqueueError(error); + } finally { + popupState.close(); + } + }, + }); + return ( <> @@ -42,47 +79,43 @@ export const NewLabelButton = ({ datasetId }: NewLabelButtonProps) => { anchorOrigin={{ vertical: "top", horizontal: "center" }} transformOrigin={{ vertical: "bottom", horizontal: "center" }} > - { - try { - await addAnnotations({ - datasetId, - data: { - labels: JSON.stringify([ - { - type: "LabelAnnotation", - label: label.trim().toLowerCase(), - value: value.trim(), - active: true, - }, - ]), - }, - }); - await queryClient.invalidateQueries({ queryKey: getGetDatasetsQueryKey() }); - } catch (error) { - enqueueError(error); - } finally { - popupState.close(); - } +
{ + e.preventDefault(); + void form.handleSubmit(); }} > - {({ submitForm, isSubmitting, isValid }) => ( - - - - - - -
- )} -
+ + + {(field) => ( + 0} + helperText={field.state.meta.errors.map((error) => error?.message)[0]} + label="Name" + value={field.state.value} + onBlur={field.handleBlur} + onChange={(e) => field.handleChange(e.target.value.toLowerCase())} + /> + )} + + + {(field) => ( + 0} + helperText={field.state.meta.errors.map((error) => error?.message)[0]} + label="Value" + value={field.state.value} + onBlur={field.handleBlur} + onChange={(e) => field.handleChange(e.target.value)} + /> + )} + + + + ); diff --git a/src/components/modals/FormModalWrapper.tsx b/src/components/modals/FormModalWrapper.tsx new file mode 100644 index 000000000..911edc21d --- /dev/null +++ b/src/components/modals/FormModalWrapper.tsx @@ -0,0 +1,95 @@ +import { type ReactNode } from "react"; + +import { type DialogProps } from "@mui/material"; + +import { ModalWrapper } from "./ModalWrapper"; +import { type BaseModalWrapperProps } from "./types"; + +export interface FormModalWrapperProps extends BaseModalWrapperProps { + /** + * ID given to the aria properties. Required for ensure good accessibility. + */ + id: string; + /** + * Title text of the modal + */ + title: string; + /** + * Text displayed in the submit button + */ + submitText: string; + /** + * Text displayed in the close button. Defaults to "Close" + */ + closeText?: string; + /** + * Whether the modal is open + */ + open: boolean; + /** + * Called when a close action is initiated. E.g. close button, submit or click-away + */ + onClose: () => void; + /** + * Props forwarded to the MuiDialog component + */ + DialogProps?: Partial; + /** + * The Tanstack Form instance + */ + form: { + handleSubmit: () => Promise; + reset: () => void; + state: { + canSubmit: boolean; + isSubmitting?: boolean; + }; + }; + /** + * Children can be a function that receives the form instance + */ + children: ReactNode; +} + +/** + * Reusable Modal/Dialog component with Tanstack Form integration for easy Modal forms + * Unlike the {@link ModalWrapper} component, the submit button is always required + */ +export const FormModalWrapper = ({ + id, + title, + submitText, + closeText, + open, + onClose, + children, + DialogProps, + form, +}: FormModalWrapperProps) => { + return ( +
{ + e.preventDefault(); + void form.handleSubmit(); + }} + > + form.reset(), + ...DialogProps, + }} + id={id} + open={open} + submitDisabled={!form.state.canSubmit || !!form.state.isSubmitting} + submitText={submitText} + title={title} + onClose={onClose} + onSubmit={() => void form.handleSubmit()} + > + {children} + +
+ ); +}; diff --git a/src/components/modals/FormikModalWrapper.tsx b/src/components/modals/FormikModalWrapper.tsx deleted file mode 100644 index 01b0105f1..000000000 --- a/src/components/modals/FormikModalWrapper.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { type DialogProps } from "@mui/material"; -import { Form, Formik, type FormikConfig, type FormikValues } from "formik"; - -import { ModalWrapper } from "./ModalWrapper"; -import { type BaseModalWrapperProps } from "./types"; - -export interface FormikModalWrapperProps extends BaseModalWrapperProps { - /** - * ID given to the aria properties. Required for ensure good accessibility. - */ - id: string; - /** - * Title text of the modal - */ - title: string; - /** - * Text displayed in the submit button - */ - submitText: string; - /** - * Text displayed in the close button. Defaults to "Close" - */ - closeText?: string; - /** - * Whether the modal is open - */ - open: boolean; - /** - * Called when a close action is initiated. E.g. close button, submit or click-away - */ - onClose: () => void; - /** - * Props forwarded to the MuiDialog component - */ - DialogProps?: Partial; -} - -/** - * Reusable Modal/Dialog component with Formik integration for easy Modal forms - * Unlike the {@link ModalWrapper} component, the submit button is always required - * - * @privateRemarks - * - * We use a function here instead of arrow functions as generics work better with them - */ -export const FormikModalWrapper = ({ - id, - title, - submitText, - closeText, - open, - onClose, - children, - DialogProps, - ...formikProps -}: FormikConfig & FormikModalWrapperProps) => { - return ( - - {({ submitForm, isSubmitting, isValid, resetForm, ...rest }) => ( -
- resetForm(), ...DialogProps }} - id={id} - open={open} - submitDisabled={isSubmitting || !isValid} - submitText={submitText} - title={title} - onClose={onClose} - onSubmit={() => void submitForm()} - > - {typeof children === "function" - ? children({ isValid, isSubmitting, submitForm, resetForm, ...rest }) - : children} - -
- )} -
- ); -}; diff --git a/src/components/products/AdjustProjectProduct.tsx b/src/components/products/AdjustProjectProduct.tsx index 206bf50e2..1304cf243 100644 --- a/src/components/products/AdjustProjectProduct.tsx +++ b/src/components/products/AdjustProjectProduct.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { type ProductDetail } from "@squonk/account-server-client"; import { @@ -8,76 +8,130 @@ import { } from "@squonk/account-server-client/product"; import { Edit as EditIcon } from "@mui/icons-material"; -import { Box, IconButton } from "@mui/material"; +import { Box, IconButton, TextField } from "@mui/material"; +import { useForm } from "@tanstack/react-form"; import { useQueryClient } from "@tanstack/react-query"; -import { Field } from "formik"; -import { TextField } from "formik-mui"; +import { z } from "zod"; import { useEnqueueError } from "../../hooks/useEnqueueStackError"; import { useGetStorageCost } from "../../hooks/useGetStorageCost"; import { formatCoins } from "../../utils/app/coins"; -import { FormikModalWrapper } from "../modals/FormikModalWrapper"; +import { FormModalWrapper } from "../modals/FormModalWrapper"; export interface AdjustProjectProductProps { product: ProductDetail; allowance: number; } +// Define Zod schema for validation +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), + allowance: z.number().min(1, "Allowance must be at least 1"), +}); + export const AdjustProjectProduct = ({ product, allowance }: AdjustProjectProductProps) => { const [open, setOpen] = useState(false); + const [currentAllowance, setCurrentAllowance] = useState(allowance); + const cost = useGetStorageCost(); const { mutateAsync: adjustProduct } = usePatchProduct(); const { enqueueError, enqueueSnackbar } = useEnqueueError(); const queryClient = useQueryClient(); - const initialValues = { name: product.name, allowance }; + const form = useForm({ + defaultValues: { name: product.name, allowance }, + validators: { onChange: formSchema }, + onSubmit: (values) => { + return adjustProduct({ + productId: product.id, + data: values.value, + }) + .then(() => { + return Promise.allSettled([ + queryClient.invalidateQueries({ queryKey: getGetProductsQueryKey() }), + queryClient.invalidateQueries({ queryKey: getGetProductQueryKey(product.id) }), + ]); + }) + .then(() => { + enqueueSnackbar("Updated product", { variant: "success" }); + setOpen(false); + return {}; + }) + .catch((error) => { + enqueueError(error); + return {}; + }); + }, + }); + + const formWrapper = { + handleSubmit: () => form.handleSubmit(), + reset: () => { + form.reset(); + setCurrentAllowance(allowance); + }, + state: { + canSubmit: form.state.canSubmit, + isSubmitting: form.state.isSubmitting, + }, + }; + + useEffect(() => { + if (!open) { + setCurrentAllowance(allowance); + } + }, [open, allowance]); return ( <> setOpen(true)}> - setOpen(false)} - onSubmit={async (values) => { - try { - await adjustProduct({ productId: product.id, data: values }); - await Promise.allSettled([ - queryClient.invalidateQueries({ queryKey: getGetProductsQueryKey() }), - queryClient.invalidateQueries({ queryKey: getGetProductQueryKey(product.id) }), - ]); - enqueueSnackbar("Updated product", { variant: "success" }); - } catch (error) { - enqueueError(error); - } - }} > - {({ values }) => ( - - - - {!!cost && Cost: {formatCoins(cost * values.allowance).slice(1)}C} - - )} - + + + {(field) => ( + 0} + helperText={field.state.meta.errors[0]?.message} + label="Name" + sx={{ maxWidth: 150 }} + value={field.state.value} + onBlur={field.handleBlur} + onChange={(e) => field.handleChange(e.target.value)} + /> + )} + + + {(field) => ( + 0} + helperText={field.state.meta.errors[0]?.message} + label="Allowance" + slotProps={{ htmlInput: { min: 1 } }} + sx={{ maxWidth: 100 }} + type="number" + value={field.state.value} + onBlur={field.handleBlur} + onChange={(e) => { + const newValue = Number(e.target.value); + field.handleChange(newValue); + setCurrentAllowance(newValue); + }} + /> + )} + + {!!cost && Cost: {formatCoins(cost * currentAllowance).slice(1)}C} + + ); }; diff --git a/src/components/projects/CreateProject/CreateProjectForm.tsx b/src/components/projects/CreateProject/CreateProjectForm.tsx index ce49b5b84..a9dffdcab 100644 --- a/src/components/projects/CreateProject/CreateProjectForm.tsx +++ b/src/components/projects/CreateProject/CreateProjectForm.tsx @@ -22,29 +22,26 @@ import { FormControl, FormLabel, MenuItem, + TextField, Tooltip, Typography, useTheme, } from "@mui/material"; +import { useForm } from "@tanstack/react-form"; import { useQueryClient } from "@tanstack/react-query"; -import { Field, Form, Formik, type FormikConfig } from "formik"; -import { TextField } from "formik-mui"; -import * as yup from "yup"; +import { z } from "zod"; -import { type Resolve } from "../../../../types"; import { useCurrentProjectId } from "../../../hooks/projectHooks"; import { useEnqueueError } from "../../../hooks/useEnqueueStackError"; import { formatTierString } from "../../../utils/app/products"; import { getErrorMessage } from "../../../utils/next/orvalError"; -import { FormikModalWrapper, type FormikModalWrapperProps } from "../../modals/FormikModalWrapper"; +import { FormModalWrapper, type FormModalWrapperProps } from "../../modals/FormModalWrapper"; import { PrivacyToggle } from "./PrivacyToggle"; const PROJECT_SUB = ProductDetailType.DATA_MANAGER_PROJECT_TIER_SUBSCRIPTION; export interface CreateProjectFormProps { - modal?: Resolve< - Pick - >; + modal?: Pick; unitId: UnitAllDetail["id"] | (() => Promise); defaultPrivacy: UnitAllDetailDefaultProductPrivacy; product?: ProductDetail; @@ -57,8 +54,6 @@ export interface Values { isPrivate: boolean; } -type ProjectFormikProps = FormikConfig; - const isPrivateDefaultValues: Record = { ALWAYS_PRIVATE: true, ALWAYS_PUBLIC: false, @@ -66,6 +61,16 @@ const isPrivateDefaultValues: Record { + const create = async (values: Values, productId?: ProductDetail["id"]) => { try { if (!productId) { const { id } = await createProduct({ unitId: typeof unitId === "function" ? await unitId() : unitId, data: { - name: projectName, + name: values.projectName, type: PROJECT_SUB, - flavour: flavour as UnitProductPostBodyBodyFlavour, + flavour: values.flavour as UnitProductPostBodyBodyFlavour, }, }); productId = id; } const { project_id } = await createProject({ - data: { name: projectName, tier_product_id: productId, private: isPrivate }, + data: { + name: values.projectName, + tier_product_id: productId, + private: values.isPrivate, + }, }); enqueueSnackbar("Project created"); @@ -138,27 +145,23 @@ export const CreateProjectForm = ({ } }; - const handleSubmit: ProjectFormikProps["onSubmit"] = async (values, { setSubmitting }) => { - try { - await create(values, product?.id); - modal?.onClose(); - } catch (error) { - enqueueError(error); - } finally { - setSubmitting(false); - } - }; + const form = useForm({ + defaultValues: initialValues as z.infer, + onSubmit: async ({ value }) => { + try { + await create(value, product?.id); + modal?.onClose(); + } catch (error) { + enqueueError(error); + } + }, + validators: { + onChange: formSchema, + }, + }); - const children: ProjectFormikProps["children"] = ({ - submitForm, - isSubmitting, - isValid, - handleChange, - setFieldValue, - values, - touched, - }) => ( -
+ const formContent = () => ( +
- + + {(field) => ( + 0} + helperText={field.state.meta.errors.map((error) => error?.message)[0]} + label="Project Name" + name="projectName" + value={field.state.value} + onChange={(e) => field.handleChange(e.target.value)} + /> + )} + {isError ? ( {getErrorMessage(error)} ) : ( - { - handleChange(event); - if (event.target.value === ProductDetailFlavour.EVALUATION) { - void setFieldValue("isPrivate", false); - } else if (!touched.isPrivate) { - void setFieldValue("isPrivate", isPrivateDefaultValues[defaultPrivacy]); - } - }} - > - {isLoading ? ( - Loading - ) : ( - productTypes?.map((product) => { - return ( - - {formatTierString(product.flavour ?? "Unknown Flavour")} - - ); - }) + + {(field) => ( + 0} + helperText={field.state.meta.errors.map((error) => error?.message)[0]} + label="Tier" + name="flavour" + value={field.state.value} + onChange={(e) => { + field.handleChange(e.target.value); + // Set isPrivate to false for evaluation tier + if (e.target.value === ProductDetailFlavour.EVALUATION) { + form.setFieldValue("isPrivate", false); + } else { + // Otherwise set to default value + form.setFieldValue("isPrivate", isPrivateDefaultValues[defaultPrivacy]); + } + }} + > + {isLoading ? ( + Loading + ) : ( + productTypes?.map((product) => { + return ( + + {formatTierString(product.flavour ?? "Unknown Flavour")} + + ); + }) + )} + )} - + )} {/* Span to prevent forward ref warning, probably fixed in react 19 */} - + + {(field) => ( + + )} + {!modal && ( - )} - +
); - const commonProps: ProjectFormikProps = { - validateOnMount: true, - initialValues, - validationSchema: yup.object().shape({ - projectName: yup - .string() - .required("A project name is required") - .matches(/^[A-Za-z0-9-_.][A-Za-z0-9-_. ]*[A-Za-z0-9-_.]$/u), - flavour: yup.string().required("A tier is required"), - }), - onSubmit: handleSubmit, - }; - return modal ? ( - - {children} - + + {formContent()} + ) : ( - {children} +
{ + e.preventDefault(); + void form.handleSubmit(); + }} + > + {formContent()} +
); }; diff --git a/src/components/projects/CreateProject/PrivacyToggle.tsx b/src/components/projects/CreateProject/PrivacyToggle.tsx index 9c2c1c805..fd89e1dfd 100644 --- a/src/components/projects/CreateProject/PrivacyToggle.tsx +++ b/src/components/projects/CreateProject/PrivacyToggle.tsx @@ -3,11 +3,9 @@ import { UnitAllDetailDefaultProductPrivacy, } from "@squonk/account-server-client"; -import { FormControlLabel } from "@mui/material"; -import { Field } from "formik"; -import { Checkbox } from "formik-mui"; +import { Checkbox, FormControlLabel } from "@mui/material"; -export interface PrivacySwitchProps { +export interface PrivacyToggleProps { /** * Selected flavour of the product */ @@ -16,9 +14,18 @@ export interface PrivacySwitchProps { * Default privacy of the product given by the unit */ defaultPrivacy: UnitAllDetailDefaultProductPrivacy; + /** + * Field object from Tanstack Form + */ + field: { + state: { + value: boolean; + }; + handleChange: (value: boolean) => void; + }; } -export const PrivacyToggle = ({ flavour, defaultPrivacy }: PrivacySwitchProps) => { +export const PrivacyToggle = ({ flavour, defaultPrivacy, field }: PrivacyToggleProps) => { // Disable the switch if the product is an evaluation product or if the default privacy is set to // always private or always public const isDisabled = @@ -28,7 +35,14 @@ export const PrivacyToggle = ({ flavour, defaultPrivacy }: PrivacySwitchProps) = return ( } + control={ + field.handleChange(e.target.checked)} + /> + } disabled={isDisabled} label="Private" labelPlacement="start" diff --git a/src/features/DatasetsTable/DatasetDetails/VersionActionsSection/AttachDatasetListItem/AttachDatasetListItem.tsx b/src/features/DatasetsTable/DatasetDetails/VersionActionsSection/AttachDatasetListItem/AttachDatasetListItem.tsx index ae5fce9ab..21dab5687 100644 --- a/src/features/DatasetsTable/DatasetDetails/VersionActionsSection/AttachDatasetListItem/AttachDatasetListItem.tsx +++ b/src/features/DatasetsTable/DatasetDetails/VersionActionsSection/AttachDatasetListItem/AttachDatasetListItem.tsx @@ -9,18 +9,20 @@ import { useGetFileTypes } from "@squonk/data-manager-client/type"; import { AttachFileRounded as AttachFileRoundedIcon } from "@mui/icons-material"; import { Alert, + Checkbox, FormControl, + FormControlLabel, FormGroup, ListItemButton, ListItemText, MenuItem, + TextField, } from "@mui/material"; +import { useForm } from "@tanstack/react-form"; import { useQueryClient } from "@tanstack/react-query"; -import { Field } from "formik"; -import { CheckboxWithLabel, TextField } from "formik-mui"; -import * as yup from "yup"; +import { z } from "zod"; -import { FormikModalWrapper } from "../../../../../components/modals/FormikModalWrapper"; +import { FormModalWrapper } from "../../../../../components/modals/FormModalWrapper"; import { useEnqueueError } from "../../../../../hooks/useEnqueueStackError"; import { useKeycloakUser } from "../../../../../hooks/useKeycloakUser"; import { getErrorMessage } from "../../../../../utils/next/orvalError"; @@ -37,13 +39,19 @@ export interface AttachDatasetListItemProps { version: DatasetVersionSummary; } -interface FormState { - project: string; - type: string; - path: string; - isImmutable: boolean; - isCompress: boolean; -} +// Define schema for validation +const schema = z.object({ + project: z.string().min(1, "A project is required"), + type: z.string().min(1, "A file type is required"), + path: z + .string() + .regex(/^\/([A-z0-9-_+]+\/)*([A-z0-9]+)$/gmu, "Invalid Path") + .or(z.literal("")), + isImmutable: z.boolean(), + isCompress: z.boolean(), +}); + +type FormType = z.infer; /** * MuiListItem with a click action that opens a modal allowing a dataset to be attached to a project @@ -68,18 +76,69 @@ export const AttachDatasetListItem = ({ datasetId, version }: AttachDatasetListI const { data: typesData, isLoading: isTypesLoading } = useGetFileTypes(); const types = typesData?.types; - const initialValues: FormState = { - project: projects?.[0]?.project_id ?? "", - type: version.type, - path: "", - isImmutable: true, - isCompress: false, - }; - const projectNames = useGetAttachedProjectsNames(projectIds, projectsData?.projects); const { enqueueError, enqueueSnackbar } = useEnqueueError(); + const form = useForm({ + defaultValues: { + project: projects?.[0]?.project_id ?? "", + type: version.type, + path: "", + isImmutable: true, + isCompress: false, + } as FormType, + validators: { onChange: schema }, + onSubmit: async (values) => { + const { project, type, path, isImmutable, isCompress } = values.value; + const resolvedPath = path || "/"; + + try { + await attachFile({ + data: { + dataset_version: version.version, + dataset_id: datasetId, + project_id: project, + immutable: isImmutable, + compress: isCompress, + as_type: type, + path: resolvedPath, + }, + }); + + await Promise.allSettled([ + // Ensure the views showing project files is updated to include the new addition + queryClient.invalidateQueries({ + queryKey: getGetFilesQueryKey({ project_id: project, path: resolvedPath }), + }), + // Ensure that the dataset's details display the project's name in the used in projects + // field + queryClient.invalidateQueries({ queryKey: getGetDatasetsQueryKey() }), + ]); + + enqueueSnackbar("The dataset was successfully attached to your project", { + variant: "success", + }); + setOpen(false); + return {}; + } catch (error) { + enqueueError(error); + return {}; + } + }, + }); + + const formWrapper = { + handleSubmit: () => form.handleSubmit(), + reset: () => { + form.reset(); + }, + state: { + canSubmit: form.state.canSubmit, + isSubmitting: form.state.isSubmitting, + }, + }; + return ( <> - setOpen(false)} - onSubmit={async ({ project, type, path, isImmutable, isCompress }, { setSubmitting }) => { - const resolvedPath = path || "/"; - try { - await attachFile({ - data: { - dataset_version: version.version, - dataset_id: datasetId, - project_id: project, - immutable: isImmutable, - compress: isCompress, - as_type: type, - path: resolvedPath, - }, - }); - - await Promise.allSettled([ - // Ensure the views showing project files is updated to include the new addition - queryClient.invalidateQueries({ - queryKey: getGetFilesQueryKey({ project_id: project, path: resolvedPath }), - }), - // Ensure that the dataset's details display the project's name in the used in projects - // field - queryClient.invalidateQueries({ queryKey: getGetDatasetsQueryKey() }), - ]); - - enqueueSnackbar("The dataset was successfully attached to your project", { - variant: "success", - }); - - setOpen(false); - } catch (error) { - enqueueError(error); - } finally { - setSubmitting(false); - } - }} > - - {(projects ?? []).map((project) => ( - - {project.name} - - ))} - + + {(field) => ( + field.handleChange(e.target.value)} + > + {(projects ?? []).map((project) => ( + + {project.name} + + ))} + + )} + - - {(types ?? []) - .sort((a, b) => a.mime.localeCompare(b.mime)) // Sort alphabetically - .map((type) => ( - - {type.mime} - - ))} - + + {(field) => ( + field.handleChange(e.target.value)} + > + {(types ?? []) + .sort((a, b) => a.mime.localeCompare(b.mime)) // Sort alphabetically + .map((type) => ( + + {type.mime} + + ))} + + )} + - - + + {(field) => ( + field.handleChange(e.target.checked)} + /> + } + label="Immutable" + /> + )} + + + {(field) => ( + field.handleChange(e.target.checked)} + /> + } + label="Compress" + /> + )} + - + + {(field) => ( + field.handleChange(e.target.value)} + /> + )} + {!!errorMessage && ( @@ -211,7 +270,7 @@ export const AttachDatasetListItem = ({ datasetId, version }: AttachDatasetListI Error: {errorMessage} )} - + ); }; diff --git a/src/features/ProjectTable/buttons/RenameButton.tsx b/src/features/ProjectTable/buttons/RenameButton.tsx index 5fc30c475..ef3c29095 100644 --- a/src/features/ProjectTable/buttons/RenameButton.tsx +++ b/src/features/ProjectTable/buttons/RenameButton.tsx @@ -1,12 +1,11 @@ import { useState } from "react"; import DriveFileRenameOutlineRoundedIcon from "@mui/icons-material/DriveFileRenameOutlineRounded"; -import { Box, IconButton, type IconButtonProps, Tooltip } from "@mui/material"; -import { Field } from "formik"; -import { TextField } from "formik-mui"; -import * as yup from "yup"; +import { Box, IconButton, type IconButtonProps, TextField, Tooltip } from "@mui/material"; +import { useForm } from "@tanstack/react-form"; +import { z } from "zod"; -import { FormikModalWrapper } from "../../../components/modals/FormikModalWrapper"; +import { FormModalWrapper } from "../../../components/modals/FormModalWrapper"; import { PATH_PATTERN, type ProjectObject, @@ -22,10 +21,41 @@ export interface RenameButtonProps extends Omit { export const RenameButton = ({ projectId, type, path, ...buttonProps }: RenameButtonProps) => { const [open, setOpen] = useState(false); - const initialValues = { dstPath: path }; - const { handleMove } = useMoveProjectObject(projectId, path, type, () => setOpen(false)); + // Define validation schema + const schema = z.object({ + dstPath: z + .string() + .min(1, "A destination path is required") + .max(255, "Path cannot exceed 255 characters") + .refine((val) => PATH_PATTERN.test(val), { + message: "The path is invalid. It should not start or end with a slash.", + }), + }); + + const form = useForm({ + defaultValues: { + dstPath: path, + }, + validators: { + onChange: schema, + }, + onSubmit: ({ value }) => { + handleMove(value.dstPath, { onSettled: () => form.reset() }); + return {}; + }, + }); + + const formWrapper = { + handleSubmit: () => form.handleSubmit(), + reset: () => form.reset(), + state: { + canSubmit: form.state.canSubmit, + isSubmitting: form.state.isSubmitting, + }, + }; + return ( <> @@ -33,41 +63,31 @@ export const RenameButton = ({ projectId, type, path, ...buttonProps }: RenameBu - setOpen(false)} - onSubmit={({ dstPath }, { resetForm }) => { - handleMove(dstPath, { onSettled: () => resetForm() }); - }} > - + + {(field) => ( + 0} + helperText={field.state.meta.errors[0]?.message ?? ""} + label="Destination Path" + sx={{ "& input": { width: "100vw" } }} + value={field.state.value} + onChange={(e) => field.handleChange(e.target.value)} + /> + )} + - + ); }; diff --git a/src/features/userSettings/UserSettingsContent/ContextSection/contextActions/CreateOrganisationListItem.tsx b/src/features/userSettings/UserSettingsContent/ContextSection/contextActions/CreateOrganisationListItem.tsx index fddcf2aa8..ece9dec75 100644 --- a/src/features/userSettings/UserSettingsContent/ContextSection/contextActions/CreateOrganisationListItem.tsx +++ b/src/features/userSettings/UserSettingsContent/ContextSection/contextActions/CreateOrganisationListItem.tsx @@ -9,11 +9,16 @@ import { } from "@squonk/account-server-client/organisation"; import { CreateNewFolder } from "@mui/icons-material"; -import { Grid2 as Grid, ListItemButton, ListItemIcon, ListItemText } from "@mui/material"; +import { + Grid2 as Grid, + ListItemButton, + ListItemIcon, + ListItemText, + TextField, +} from "@mui/material"; +import { useForm } from "@tanstack/react-form"; import { useQueryClient } from "@tanstack/react-query"; -import { Field, Form, Formik } from "formik"; -import { TextField } from "formik-mui"; -import * as yup from "yup"; +import { z } from "zod"; import { ModalWrapper } from "../../../../../components/modals/ModalWrapper"; import { useEnqueueError } from "../../../../../hooks/useEnqueueStackError"; @@ -63,6 +68,37 @@ export const CreateOrganisationListItem = () => { void changeContext(organisationId); }; + // Define Zod schema for validation + const orgSchema = z.object({ + name: z + .string() + .min(2, "The name is too short") + .refine((name) => !organisations?.map((org) => org.name).includes(name), { + message: "The name is already used for an organisation", + }), + owner: z.string().min(1, "The username for the owner is required"), + }); + + const form = useForm({ + defaultValues: { + name: "", + owner: user.username ?? "", + } as z.infer, + validators: { + onChange: orgSchema, + }, + onSubmit: async ({ value }) => { + try { + await create(value.name, value.owner); + form.reset(); + } catch (error) { + enqueueError(error); + } finally { + setOpen(false); + } + }, + }); + return ( <> setOpen(true)}> @@ -79,76 +115,52 @@ export const CreateOrganisationListItem = () => { - !organisations?.map((organisation) => organisation.name).includes(name), - ) - .min(2, "The name is too short"), - owner: yup - .string() - .required("The username for the owner is required") - .test( - "does-not-exist-already", - "The name is already used for an organisation", - (name) => !organisations?.map((organisation) => organisation.name).includes(name), - ), - })} - onSubmit={async ({ name, owner }, { setSubmitting, resetForm }) => { - try { - await create(name, owner); - resetForm(); - } catch (error) { - enqueueError(error); - } finally { - setOpen(false); - setSubmitting(false); - } - }} + setOpen(false)} + onSubmit={() => void form.handleSubmit()} > - {({ submitForm, isSubmitting, isValid }) => ( - setOpen(false)} - onSubmit={() => void submitForm()} - > -
- - - - - - - - -
-
- )} -
+ + + + {(field) => ( + 0} + helperText={field.state.meta.errors.map((error) => error?.message)[0]} + label="Organisation Name" + value={field.state.value} + onBlur={field.handleBlur} + onChange={(e) => field.handleChange(e.target.value)} + /> + )} + + + + + {(field) => ( + 0} + helperText={field.state.meta.errors.map((error) => error?.message)[0]} + label="Owner (username)" + value={field.state.value} + onBlur={field.handleBlur} + onChange={(e) => field.handleChange(e.target.value)} + /> + )} + + + + ); }; diff --git a/src/features/userSettings/UserSettingsContent/ContextSection/contextActions/CreateUnitListItem.tsx b/src/features/userSettings/UserSettingsContent/ContextSection/contextActions/CreateUnitListItem.tsx index aae313c82..161e6671f 100644 --- a/src/features/userSettings/UserSettingsContent/ContextSection/contextActions/CreateUnitListItem.tsx +++ b/src/features/userSettings/UserSettingsContent/ContextSection/contextActions/CreateUnitListItem.tsx @@ -10,11 +10,16 @@ import { } from "@squonk/account-server-client/unit"; import { CreateNewFolder } from "@mui/icons-material"; -import { Grid2 as Grid, ListItemButton, ListItemIcon, ListItemText } from "@mui/material"; +import { + Grid2 as Grid, + ListItemButton, + ListItemIcon, + ListItemText, + TextField, +} from "@mui/material"; +import { useForm } from "@tanstack/react-form"; import { useQueryClient } from "@tanstack/react-query"; -import { Field, Form, Formik } from "formik"; -import { TextField } from "formik-mui"; -import * as yup from "yup"; +import { z } from "zod"; import { ModalWrapper } from "../../../../../components/modals/ModalWrapper"; import { useEnqueueError } from "../../../../../hooks/useEnqueueStackError"; @@ -71,6 +76,35 @@ export const CreateUnitListItem = () => { } }; + // Define Zod schema for validation + const unitSchema = z.object({ + name: z + .string() + .min(2, "The name is too short") + .refine((name) => !units?.map((unit) => unit.name).includes(name), { + message: "The name is already used for a unit", + }), + }); + + const form = useForm({ + defaultValues: { + name: "", + } as z.infer, + validators: { + onChange: unitSchema, + }, + onSubmit: async ({ value }) => { + try { + await create(value.name); + form.reset(); + } catch (error) { + enqueueError(error); + } finally { + setOpen(false); + } + }, + }); + return ( <> setOpen(true)}> @@ -83,53 +117,42 @@ export const CreateUnitListItem = () => { - !units?.map((unit) => unit.name).includes(name), - ) - .min(2, "The name is too short"), - })} - onSubmit={async ({ name }, { setSubmitting, resetForm }) => { - try { - await create(name); - resetForm(); - } catch (error) { - enqueueError(error); - } finally { - setOpen(false); - setSubmitting(false); - } - }} + setOpen(false)} + onSubmit={() => void form.handleSubmit()} > - {({ submitForm, isSubmitting, isValid }) => ( - setOpen(false)} - onSubmit={() => void submitForm()} - > -
- - - - - -
-
- )} -
+
{ + e.preventDefault(); + void form.handleSubmit(); + }} + > + + + + {(field) => ( + 0} + helperText={field.state.meta.errors.map((error) => error?.message)[0]} + label="Unit Name" + value={field.state.value} + onBlur={field.handleBlur} + onChange={(e) => field.handleChange(e.target.value)} + /> + )} + + + +
+ ); }; From 8fcb304afeeee10e4536238ad06abac56c01f012 Mon Sep 17 00:00:00 2001 From: Oliver Dudgeon <22367286+OliverDudgeon@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:32:40 +0100 Subject: [PATCH 3/3] chore: update dependencies --- package.json | 12 +-- pnpm-lock.yaml | 220 ++++++------------------------------------------- 2 files changed, 30 insertions(+), 202 deletions(-) diff --git a/package.json b/package.json index 8e6b3d5f3..95a5e257a 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@auth0/nextjs-auth0": "3.7.0", - "@bufbuild/protobuf": "^2.2.5", + "@bufbuild/protobuf": "2.2.5", "@emotion/cache": "11.14.0", "@emotion/react": "11.14.0", "@emotion/server": "11.11.0", @@ -57,7 +57,7 @@ "@squonk/mui-theme": "5.0.0", "@squonk/sdf-parser": "1.3.0", "@tanstack/match-sorter-utils": "8.19.4", - "@tanstack/react-form": "1.1.0", + "@tanstack/react-form": "1.6.3", "@tanstack/react-query": "5.71.1", "@tanstack/react-query-devtools": "5.71.2", "@tanstack/react-table": "8.21.2", @@ -92,15 +92,15 @@ "react-dom": "19.1.0", "react-dropzone": "14.3.8", "react-plotly.js": "2.6.0", - "react-use-websocket": "^4.13.0", + "react-use-websocket": "4.13.0", "sharp": "0.33.5", "typescript": "5.8.2", "use-immer": "0.11.0", - "zod": "^3.24.2" + "zod": "3.24.3" }, "devDependencies": { - "@bufbuild/buf": "^1.52.1", - "@bufbuild/protoc-gen-es": "^2.2.5", + "@bufbuild/buf": "1.52.1", + "@bufbuild/protoc-gen-es": "2.2.5", "@next/bundle-analyzer": "15.2.4", "@playwright/test": "1.51.1", "@squonk/eslint-config": "2.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53db904d1..8c166e017 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,7 @@ importers: specifier: 3.7.0 version: 3.7.0(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.83.1)) '@bufbuild/protobuf': - specifier: ^2.2.5 + specifier: 2.2.5 version: 2.2.5 '@emotion/cache': specifier: 11.14.0 @@ -78,8 +78,8 @@ importers: specifier: 8.19.4 version: 8.19.4 '@tanstack/react-form': - specifier: 1.1.0 - version: 1.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.2) + specifier: 1.6.3 + version: 1.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-query': specifier: 5.71.1 version: 5.71.1(react@19.1.0) @@ -183,7 +183,7 @@ importers: specifier: 2.6.0 version: 2.6.0(plotly.js@2.35.3(mapbox-gl@1.13.3)(webpack@5.97.1))(react@19.1.0) react-use-websocket: - specifier: ^4.13.0 + specifier: 4.13.0 version: 4.13.0 sharp: specifier: 0.33.5 @@ -195,14 +195,14 @@ importers: specifier: 0.11.0 version: 0.11.0(immer@10.1.1)(react@19.1.0) zod: - specifier: ^3.24.2 + specifier: 3.24.3 version: 3.24.3 devDependencies: '@bufbuild/buf': - specifier: ^1.52.1 + specifier: 1.52.1 version: 1.52.1 '@bufbuild/protoc-gen-es': - specifier: ^2.2.5 + specifier: 2.2.5 version: 2.2.5(@bufbuild/protobuf@2.2.5) '@next/bundle-analyzer': specifier: 15.2.4 @@ -1387,44 +1387,6 @@ packages: react-redux: optional: true - '@remix-run/node@2.16.0': - resolution: {integrity: sha512-9yYBYCHYO1+bIScGAtOy5/r4BoTS8E5lpQmjWP99UxSCSiKHPEO76V9Z8mmmarTNis/FPN+sUwfmbQWNHLA2vw==} - engines: {node: '>=18.0.0'} - peerDependencies: - typescript: ^5.1.0 - peerDependenciesMeta: - typescript: - optional: true - - '@remix-run/router@1.23.0': - resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} - engines: {node: '>=14.0.0'} - - '@remix-run/server-runtime@2.16.0': - resolution: {integrity: sha512-gbuc4slxPi+pT47MrUYprX/wCuDlYL6H3LHZSvimWO1kDCBt8oefHzdHDPjLi4B1xzqXZomswTbuJzpZ7xRRTg==} - engines: {node: '>=18.0.0'} - peerDependencies: - typescript: ^5.1.0 - peerDependenciesMeta: - typescript: - optional: true - - '@remix-run/web-blob@3.1.0': - resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} - - '@remix-run/web-fetch@4.4.2': - resolution: {integrity: sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==} - engines: {node: ^10.17 || >=12.3} - - '@remix-run/web-file@3.1.0': - resolution: {integrity: sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==} - - '@remix-run/web-form-data@3.1.0': - resolution: {integrity: sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==} - - '@remix-run/web-stream@1.1.0': - resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==} - '@rjsf/core@5.24.8': resolution: {integrity: sha512-7YdOMl0UylldYR75rnKoQF5etibSwPQHA3rY8LEo28xyYek81ESGuqPTs37P14doBpGRL+SobggR76tYhDYO7A==} engines: {node: '>=14'} @@ -1630,8 +1592,8 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@tanstack/form-core@1.1.0': - resolution: {integrity: sha512-RfRv+grnTzQhG6UyjhA+t9zyx0mSjaowyVzuo2qb4NR2Pc73zoNA5WKEAUhjwONlXrLSSs0h9LE0vJDbk8kYrQ==} + '@tanstack/form-core@1.6.3': + resolution: {integrity: sha512-0m+F3vOplArF5yDzBOFpMeM+3cGjgRmtVgfi3z5+Bzcc4F8AkXngRay+PmzY0z8K3Trz7EAqmJWMB9T+LlZWoA==} '@tanstack/match-sorter-utils@8.19.4': resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} @@ -1643,8 +1605,8 @@ packages: '@tanstack/query-devtools@5.71.2': resolution: {integrity: sha512-cg2TExgXM1TL3f+BPf6R7iXsPZnrj16HvqBT88xKr1hduXkccs/5EHf2X3zSNwB0Y9XfowSQB8dcGa2qKYv5ZA==} - '@tanstack/react-form@1.1.0': - resolution: {integrity: sha512-vvFM2T4pDO1+96LUaSprTqZq2e0JFIGV1Qpr7PVHaFJlSqunmmVX0uiW6tW4xfCesUodSjW0evgPNVO+FSsu5Q==} + '@tanstack/react-form@1.6.3': + resolution: {integrity: sha512-JuooyQ1bqIn3zC0wWCJB4n1UTyFZAF1Z4IWduUt6l356CoT78KTD56umRYLv3CirJqvYoa9kWOQKyjrVTmTNqw==} peerDependencies: '@tanstack/react-start': ^1.112.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1713,9 +1675,6 @@ packages: '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1977,9 +1936,6 @@ packages: cpu: [x64] os: [win32] - '@web3-storage/multipart-parser@1.0.0': - resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} - '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -2031,17 +1987,10 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@zxing/text-encoding@0.9.0': - resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} - abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - abs-svg-path@0.1.1: resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} @@ -2493,14 +2442,6 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} - cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -2751,10 +2692,6 @@ packages: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} - data-uri-to-buffer@3.0.1: - resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} - engines: {node: '>= 6'} - data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -2806,8 +2743,8 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - decode-formdata@0.8.0: - resolution: {integrity: sha512-iUzDgnWsw5ToSkFY7VPFA5Gfph6ROoOxOB7Ybna4miUSzLZ4KaSJk6IAB2AdW6+C9vCVWhjjNA4gjT6wF3eZHQ==} + decode-formdata@0.9.0: + resolution: {integrity: sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw==} decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -2852,6 +2789,9 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -3190,10 +3130,6 @@ packages: event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -4448,10 +4384,6 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} - mrmime@1.0.1: - resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} - engines: {node: '>=10'} - mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -5345,9 +5277,6 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} - set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -5472,9 +5401,6 @@ packages: stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} - stream-slice@0.1.2: - resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} - streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -5721,9 +5647,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - turbo-stream@2.4.0: - resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -5817,10 +5740,6 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - undici@6.21.2: - resolution: {integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==} - engines: {node: '>=18.17'} - unified-engine@11.2.2: resolution: {integrity: sha512-15g/gWE7qQl9tQ3nAEbMd5h9HV1EACtFs6N9xaRBZICoCwnNGbal1kOs++ICf4aiTdItZxU2s/kYWhW7htlqJg==} @@ -5969,9 +5888,6 @@ packages: weak-map@1.0.8: resolution: {integrity: sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==} - web-encoding@1.1.5: - resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} - web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -7442,60 +7358,6 @@ snapshots: react: 19.1.0 react-redux: 7.2.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@remix-run/node@2.16.0(typescript@5.8.2)': - dependencies: - '@remix-run/server-runtime': 2.16.0(typescript@5.8.2) - '@remix-run/web-fetch': 4.4.2 - '@web3-storage/multipart-parser': 1.0.0 - cookie-signature: 1.2.2 - source-map-support: 0.5.21 - stream-slice: 0.1.2 - undici: 6.21.2 - optionalDependencies: - typescript: 5.8.2 - - '@remix-run/router@1.23.0': {} - - '@remix-run/server-runtime@2.16.0(typescript@5.8.2)': - dependencies: - '@remix-run/router': 1.23.0 - '@types/cookie': 0.6.0 - '@web3-storage/multipart-parser': 1.0.0 - cookie: 0.6.0 - set-cookie-parser: 2.7.1 - source-map: 0.7.4 - turbo-stream: 2.4.0 - optionalDependencies: - typescript: 5.8.2 - - '@remix-run/web-blob@3.1.0': - dependencies: - '@remix-run/web-stream': 1.1.0 - web-encoding: 1.1.5 - - '@remix-run/web-fetch@4.4.2': - dependencies: - '@remix-run/web-blob': 3.1.0 - '@remix-run/web-file': 3.1.0 - '@remix-run/web-form-data': 3.1.0 - '@remix-run/web-stream': 1.1.0 - '@web3-storage/multipart-parser': 1.0.0 - abort-controller: 3.0.0 - data-uri-to-buffer: 3.0.1 - mrmime: 1.0.1 - - '@remix-run/web-file@3.1.0': - dependencies: - '@remix-run/web-blob': 3.1.0 - - '@remix-run/web-form-data@3.1.0': - dependencies: - web-encoding: 1.1.5 - - '@remix-run/web-stream@1.1.0': - dependencies: - web-streams-polyfill: 3.3.3 - '@rjsf/core@5.24.8(@rjsf/utils@5.24.8(react@19.1.0))(react@19.1.0)': dependencies: '@rjsf/utils': 5.24.8(react@19.1.0) @@ -7790,7 +7652,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@tanstack/form-core@1.1.0': + '@tanstack/form-core@1.6.3': dependencies: '@tanstack/store': 0.7.0 @@ -7802,16 +7664,15 @@ snapshots: '@tanstack/query-devtools@5.71.2': {} - '@tanstack/react-form@1.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.2)': + '@tanstack/react-form@1.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@remix-run/node': 2.16.0(typescript@5.8.2) - '@tanstack/form-core': 1.1.0 + '@tanstack/form-core': 1.6.3 '@tanstack/react-store': 0.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - decode-formdata: 0.8.0 + decode-formdata: 0.9.0 + devalue: 5.1.1 react: 19.1.0 transitivePeerDependencies: - react-dom - - typescript '@tanstack/react-query-devtools@5.71.2(@tanstack/react-query@5.71.1(react@19.1.0))(react@19.1.0)': dependencies: @@ -7889,8 +7750,6 @@ snapshots: dependencies: '@types/node': 22.9.0 - '@types/cookie@0.6.0': {} - '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -8169,8 +8028,6 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.3.3': optional: true - '@web3-storage/multipart-parser@1.0.0': {} - '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -8251,15 +8108,8 @@ snapshots: '@xtuc/long@4.2.2': {} - '@zxing/text-encoding@0.9.0': - optional: true - abbrev@2.0.0: {} - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - abs-svg-path@0.1.1: {} acorn-import-attributes@1.9.5(acorn@8.14.1): @@ -8730,10 +8580,6 @@ snapshots: convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} - - cookie@0.6.0: {} - cookie@0.7.2: {} core-js-compat@3.41.0: @@ -9012,8 +8858,6 @@ snapshots: es5-ext: 0.10.64 type: 2.7.3 - data-uri-to-buffer@3.0.1: {} - data-uri-to-buffer@4.0.1: {} data-view-buffer@1.0.2: @@ -9052,7 +8896,7 @@ snapshots: decamelize@1.2.0: {} - decode-formdata@0.8.0: {} + decode-formdata@0.9.0: {} decode-named-character-reference@1.0.2: dependencies: @@ -9091,6 +8935,8 @@ snapshots: detect-libc@2.0.3: {} + devalue@5.1.1: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -9615,8 +9461,6 @@ snapshots: d: 1.0.2 es5-ext: 0.10.64 - event-target-shim@5.0.1: {} - eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} @@ -11281,8 +11125,6 @@ snapshots: mri@1.2.0: {} - mrmime@1.0.1: {} - mrmime@2.0.0: {} ms@2.0.0: {} @@ -12321,8 +12163,6 @@ snapshots: set-blocking@2.0.0: {} - set-cookie-parser@2.7.1: {} - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -12484,8 +12324,6 @@ snapshots: stream-shift@1.0.3: {} - stream-slice@0.1.2: {} - streamsearch@1.1.0: {} string-argv@0.3.2: {} @@ -12742,8 +12580,6 @@ snapshots: tslib@2.8.1: {} - turbo-stream@2.4.0: {} - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -12839,8 +12675,6 @@ snapshots: undici-types@6.20.0: {} - undici@6.21.2: {} - unified-engine@11.2.2: dependencies: '@types/concat-stream': 2.0.3 @@ -13067,12 +12901,6 @@ snapshots: weak-map@1.0.8: {} - web-encoding@1.1.5: - dependencies: - util: 0.12.5 - optionalDependencies: - '@zxing/text-encoding': 0.9.0 - web-streams-polyfill@3.3.3: {} webgl-context@2.2.0: