`. Set the primary action button to `type="submit"` and handle `onSubmit` (preventing default) rather than only utilizing `onClick` on the button.
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 9702f9d7a..5bb22defc 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,22 +8,22 @@
"name": "pg-erd-cloud-frontend",
"version": "0.1.0",
"dependencies": {
- "@xyflow/react": "^12.11.2",
- "react": "^19.2.8",
- "react-dom": "^19.2.8"
+ "@xyflow/react": "^12.11.1",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.7"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
- "@testing-library/user-event": "^14.6.3",
- "@types/react": "^19.2.18",
- "@types/react-dom": "^19.2.4",
- "@vitest/coverage-v8": "^4.1.10",
- "fast-check": "^4.9.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/react": "^19.2.10",
+ "@types/react-dom": "^19.2.3",
+ "@vitest/coverage-v8": "^4.1.9",
+ "fast-check": "^4.8.0",
"jsdom": "^29.1.1",
"typescript": "^6.0.3",
- "vite": "^8.2.0",
- "vitest": "^4.1.10"
+ "vite": "^8.1.0",
+ "vitest": "^4.1.9"
},
"engines": {
"node": ">=26 <27"
@@ -103,6 +103,14 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/code-frame/node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
@@ -124,13 +132,13 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
- "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.8"
+ "@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -150,9 +158,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
- "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -187,9 +195,9 @@
}
},
"node_modules/@csstools/color-helpers": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz",
- "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
+ "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
"dev": true,
"funding": [
{
@@ -207,9 +215,9 @@
}
},
"node_modules/@csstools/css-calc": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz",
- "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
+ "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
"dev": true,
"funding": [
{
@@ -231,9 +239,9 @@
}
},
"node_modules/@csstools/css-color-parser": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz",
- "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==",
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz",
+ "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==",
"dev": true,
"funding": [
{
@@ -247,8 +255,8 @@
],
"license": "MIT",
"dependencies": {
- "@csstools/color-helpers": "^6.1.0",
- "@csstools/css-calc": "^3.3.0"
+ "@csstools/color-helpers": "^6.0.2",
+ "@csstools/css-calc": "^3.2.1"
},
"engines": {
"node": ">=20.19.0"
@@ -282,9 +290,9 @@
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz",
- "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz",
+ "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==",
"dev": true,
"funding": [
{
@@ -326,6 +334,40 @@
"node": ">=20.19.0"
}
},
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@exodus/bytes": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
@@ -372,10 +414,29 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
"node_modules/@oxc-project/types": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz",
- "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz",
+ "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -383,9 +444,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz",
- "integrity": "sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz",
+ "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==",
"cpu": [
"arm64"
],
@@ -400,9 +461,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.2.tgz",
- "integrity": "sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz",
+ "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==",
"cpu": [
"arm64"
],
@@ -417,9 +478,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.2.tgz",
- "integrity": "sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz",
+ "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==",
"cpu": [
"x64"
],
@@ -434,9 +495,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.2.tgz",
- "integrity": "sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz",
+ "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==",
"cpu": [
"x64"
],
@@ -451,9 +512,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.2.tgz",
- "integrity": "sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz",
+ "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==",
"cpu": [
"arm"
],
@@ -468,16 +529,13 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.2.tgz",
- "integrity": "sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz",
+ "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==",
"cpu": [
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -488,16 +546,13 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.2.tgz",
- "integrity": "sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz",
+ "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==",
"cpu": [
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -508,16 +563,13 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.2.tgz",
- "integrity": "sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz",
+ "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==",
"cpu": [
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -528,16 +580,13 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.2.tgz",
- "integrity": "sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz",
+ "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==",
"cpu": [
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -548,16 +597,13 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.2.tgz",
- "integrity": "sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz",
+ "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==",
"cpu": [
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -568,16 +614,13 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.2.tgz",
- "integrity": "sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz",
+ "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==",
"cpu": [
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -588,9 +631,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.2.tgz",
- "integrity": "sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz",
+ "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==",
"cpu": [
"arm64"
],
@@ -604,10 +647,29 @@
"node": "^20.19.0 || >=22.12.0"
}
},
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz",
+ "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
"node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.2.tgz",
- "integrity": "sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz",
+ "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==",
"cpu": [
"arm64"
],
@@ -622,9 +684,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.2.tgz",
- "integrity": "sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz",
+ "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==",
"cpu": [
"x64"
],
@@ -729,9 +791,9 @@
}
},
"node_modules/@testing-library/user-event": {
- "version": "14.6.3",
- "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz",
- "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==",
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -742,6 +804,17 @@
"@testing-library/dom": ">=7.21.4"
}
},
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -825,9 +898,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "19.2.18",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
- "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -835,9 +908,9 @@
}
},
"node_modules/@types/react-dom": {
- "version": "19.2.4",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
- "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peerDependencies": {
@@ -845,14 +918,14 @@
}
},
"node_modules/@vitest/coverage-v8": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
- "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz",
+ "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
- "@vitest/utils": "4.1.10",
+ "@vitest/utils": "4.1.9",
"ast-v8-to-istanbul": "^1.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
@@ -866,8 +939,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "@vitest/browser": "4.1.10",
- "vitest": "4.1.10"
+ "@vitest/browser": "4.1.9",
+ "vitest": "4.1.9"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -876,16 +949,16 @@
}
},
"node_modules/@vitest/expect": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
- "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.10",
- "@vitest/utils": "4.1.10",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -894,13 +967,13 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
- "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.10",
+ "@vitest/spy": "4.1.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -921,9 +994,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
- "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -934,13 +1007,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
- "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.10",
+ "@vitest/utils": "4.1.9",
"pathe": "^2.0.3"
},
"funding": {
@@ -948,14 +1021,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
- "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.10",
- "@vitest/utils": "4.1.10",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/utils": "4.1.9",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -964,9 +1037,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
- "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -974,13 +1047,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
- "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.10",
+ "@vitest/pretty-format": "4.1.9",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -989,12 +1062,12 @@
}
},
"node_modules/@xyflow/react": {
- "version": "12.11.2",
- "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.2.tgz",
- "integrity": "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==",
+ "version": "12.11.1",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.1.tgz",
+ "integrity": "sha512-L+zBoLGSXham0MnlY8QqjfR7/C5JNw0zxkaey5aZ5XmCgJBAdH4+WRIu8CR40d3l/BdU635V6YbhBK1jMo8/6Q==",
"license": "MIT",
"dependencies": {
- "@xyflow/system": "0.0.79",
+ "@xyflow/system": "0.0.78",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
@@ -1014,9 +1087,9 @@
}
},
"node_modules/@xyflow/system": {
- "version": "0.0.79",
- "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.79.tgz",
- "integrity": "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==",
+ "version": "0.0.78",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.78.tgz",
+ "integrity": "sha512-lY0z2qP33fUhTva9Vaxrk0lqZta2pkbxB1trHAx1omnJqRtPvDlAQYV2r5fhS6AdpkulYmbNW0svy+A4/t4B/g==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
@@ -1076,9 +1149,9 @@
}
},
"node_modules/ast-v8-to-istanbul": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz",
- "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz",
+ "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1087,13 +1160,6 @@
"js-tokens": "^10.0.0"
}
},
- "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
@@ -1323,9 +1389,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
- "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
"dev": true,
"license": "MIT"
},
@@ -1340,9 +1406,9 @@
}
},
"node_modules/expect-type": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
- "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -1350,9 +1416,9 @@
}
},
"node_modules/fast-check": {
- "version": "4.9.0",
- "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.9.0.tgz",
- "integrity": "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==",
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.8.0.tgz",
+ "integrity": "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==",
"dev": true,
"funding": [
{
@@ -1492,12 +1558,11 @@
}
},
"node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/jsdom": {
"version": "29.1.1",
@@ -1541,9 +1606,9 @@
}
},
"node_modules/lightningcss": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
- "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
@@ -1557,23 +1622,23 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
- "lightningcss-android-arm64": "1.33.0",
- "lightningcss-darwin-arm64": "1.33.0",
- "lightningcss-darwin-x64": "1.33.0",
- "lightningcss-freebsd-x64": "1.33.0",
- "lightningcss-linux-arm-gnueabihf": "1.33.0",
- "lightningcss-linux-arm64-gnu": "1.33.0",
- "lightningcss-linux-arm64-musl": "1.33.0",
- "lightningcss-linux-x64-gnu": "1.33.0",
- "lightningcss-linux-x64-musl": "1.33.0",
- "lightningcss-win32-arm64-msvc": "1.33.0",
- "lightningcss-win32-x64-msvc": "1.33.0"
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-android-arm64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
- "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
@@ -1592,9 +1657,9 @@
}
},
"node_modules/lightningcss-darwin-arm64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
- "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
@@ -1613,9 +1678,9 @@
}
},
"node_modules/lightningcss-darwin-x64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
- "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
@@ -1634,9 +1699,9 @@
}
},
"node_modules/lightningcss-freebsd-x64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
- "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
@@ -1655,9 +1720,9 @@
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
- "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
@@ -1676,16 +1741,13 @@
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
- "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1700,16 +1762,13 @@
}
},
"node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
- "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1724,16 +1783,13 @@
}
},
"node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
- "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"cpu": [
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1748,16 +1804,13 @@
}
},
"node_modules/lightningcss-linux-x64-musl": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
- "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"cpu": [
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1772,9 +1825,9 @@
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
- "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
@@ -1793,9 +1846,9 @@
}
},
"node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
- "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
@@ -1814,9 +1867,9 @@
}
},
"node_modules/lru-cache": {
- "version": "11.5.2",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
- "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "version": "11.5.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
+ "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
@@ -1845,14 +1898,14 @@
}
},
"node_modules/magicast": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz",
- "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==",
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
+ "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7",
+ "@babel/parser": "^7.29.3",
+ "@babel/types": "^7.29.0",
"source-map-js": "^1.2.1"
}
},
@@ -1890,9 +1943,9 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.17",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
- "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"dev": true,
"funding": [
{
@@ -1909,9 +1962,9 @@
}
},
"node_modules/obug": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
- "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
@@ -1950,9 +2003,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2018,9 +2071,9 @@
}
},
"node_modules/pure-rand": {
- "version": "8.4.2",
- "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz",
- "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==",
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.1.tgz",
+ "integrity": "sha512-c58R2+SPFcSIPXoU834QN/KPDDOSd8sXcSrqf6e83Me6Rrp1EYkxukkjXMVrKvKaADs1SOyNkWdfvLf6zY8qLQ==",
"dev": true,
"funding": [
{
@@ -2035,24 +2088,24 @@
"license": "MIT"
},
"node_modules/react": {
- "version": "19.2.8",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
- "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "19.2.8",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
- "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
- "react": "^19.2.8"
+ "react": "^19.2.7"
}
},
"node_modules/react-is": {
@@ -2088,13 +2141,13 @@
}
},
"node_modules/rolldown": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.2.tgz",
- "integrity": "sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz",
+ "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "=0.142.0",
+ "@oxc-project/types": "=0.137.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -2104,20 +2157,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.2.2",
- "@rolldown/binding-darwin-arm64": "1.2.2",
- "@rolldown/binding-darwin-x64": "1.2.2",
- "@rolldown/binding-freebsd-x64": "1.2.2",
- "@rolldown/binding-linux-arm-gnueabihf": "1.2.2",
- "@rolldown/binding-linux-arm64-gnu": "1.2.2",
- "@rolldown/binding-linux-arm64-musl": "1.2.2",
- "@rolldown/binding-linux-ppc64-gnu": "1.2.2",
- "@rolldown/binding-linux-s390x-gnu": "1.2.2",
- "@rolldown/binding-linux-x64-gnu": "1.2.2",
- "@rolldown/binding-linux-x64-musl": "1.2.2",
- "@rolldown/binding-openharmony-arm64": "1.2.2",
- "@rolldown/binding-win32-arm64-msvc": "1.2.2",
- "@rolldown/binding-win32-x64-msvc": "1.2.2"
+ "@rolldown/binding-android-arm64": "1.1.3",
+ "@rolldown/binding-darwin-arm64": "1.1.3",
+ "@rolldown/binding-darwin-x64": "1.1.3",
+ "@rolldown/binding-freebsd-x64": "1.1.3",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.3",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.3",
+ "@rolldown/binding-linux-arm64-musl": "1.1.3",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.3",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.3",
+ "@rolldown/binding-linux-x64-gnu": "1.1.3",
+ "@rolldown/binding-linux-x64-musl": "1.1.3",
+ "@rolldown/binding-openharmony-arm64": "1.1.3",
+ "@rolldown/binding-wasm32-wasi": "1.1.3",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.3",
+ "@rolldown/binding-win32-x64-msvc": "1.1.3"
}
},
"node_modules/saxes": {
@@ -2177,9 +2231,9 @@
"license": "MIT"
},
"node_modules/std-env": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
- "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
"dev": true,
"license": "MIT"
},
@@ -2224,9 +2278,9 @@
"license": "MIT"
},
"node_modules/tinyexec": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
- "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2251,9 +2305,9 @@
}
},
"node_modules/tinyrainbow": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
- "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2261,29 +2315,29 @@
}
},
"node_modules/tldts": {
- "version": "7.4.10",
- "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz",
- "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==",
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz",
+ "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tldts-core": "^7.4.10"
+ "tldts-core": "^7.4.3"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
- "version": "7.4.10",
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz",
- "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==",
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz",
+ "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==",
"dev": true,
"license": "MIT"
},
"node_modules/tough-cookie": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
- "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
+ "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -2306,6 +2360,14 @@
"node": ">=20"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
@@ -2321,9 +2383,9 @@
}
},
"node_modules/undici": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
- "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2340,16 +2402,16 @@
}
},
"node_modules/vite": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz",
- "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==",
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz",
+ "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "lightningcss": "^1.33.0",
- "picomatch": "^4.0.5",
- "postcss": "^8.5.23",
- "rolldown": "~1.2.0",
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.15",
+ "rolldown": "~1.1.2",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -2366,7 +2428,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.4.0",
+ "@vitejs/devtools": "^0.3.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
@@ -2418,19 +2480,19 @@
}
},
"node_modules/vitest": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
- "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.1.10",
- "@vitest/mocker": "4.1.10",
- "@vitest/pretty-format": "4.1.10",
- "@vitest/runner": "4.1.10",
- "@vitest/snapshot": "4.1.10",
- "@vitest/spy": "4.1.10",
- "@vitest/utils": "4.1.10",
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -2458,12 +2520,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.10",
- "@vitest/browser-preview": "4.1.10",
- "@vitest/browser-webdriverio": "4.1.10",
- "@vitest/coverage-istanbul": "4.1.10",
- "@vitest/coverage-v8": "4.1.10",
- "@vitest/ui": "4.1.10",
+ "@vitest/browser-playwright": "4.1.9",
+ "@vitest/browser-preview": "4.1.9",
+ "@vitest/browser-webdriverio": "4.1.9",
+ "@vitest/coverage-istanbul": "4.1.9",
+ "@vitest/coverage-v8": "4.1.9",
+ "@vitest/ui": "4.1.9",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
diff --git a/frontend/package.json b/frontend/package.json
index ddfe5ca45..adbea9fd0 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,22 +17,22 @@
"e2e": "echo 'No e2e framework configured'"
},
"dependencies": {
- "@xyflow/react": "^12.11.2",
- "react": "^19.2.8",
- "react-dom": "^19.2.8"
+ "@xyflow/react": "^12.11.1",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.7"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
- "@testing-library/user-event": "^14.6.3",
- "@types/react": "^19.2.18",
- "@types/react-dom": "^19.2.4",
- "@vitest/coverage-v8": "^4.1.10",
- "fast-check": "^4.9.0",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/react": "^19.2.10",
+ "@types/react-dom": "^19.2.3",
+ "@vitest/coverage-v8": "^4.1.9",
+ "fast-check": "^4.8.0",
"jsdom": "^29.1.1",
"typescript": "^6.0.3",
- "vite": "^8.2.0",
- "vitest": "^4.1.10"
+ "vite": "^8.1.0",
+ "vitest": "^4.1.9"
},
"overrides": {
"esbuild": "^0.25.0",
diff --git a/frontend/src/App.coverage.test.tsx b/frontend/src/App.coverage.test.tsx
index fa342e766..aa170db31 100644
--- a/frontend/src/App.coverage.test.tsx
+++ b/frontend/src/App.coverage.test.tsx
@@ -1,7 +1,13 @@
-import '@testing-library/jest-dom/vitest'
-import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import "@testing-library/jest-dom/vitest";
+import {
+ act,
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const api = vi.hoisted(() => ({
getMe: vi.fn(),
@@ -13,71 +19,112 @@ const api = vi.hoisted(() => ({
createSnapshot: vi.fn(),
getSnapshot: vi.fn(),
createShareLink: vi.fn(),
-}))
+}));
const exports = vi.hoisted(() => ({
downloadText: vi.fn(),
- exportDDL: vi.fn(() => 'DDL'),
- exportDiagramSvg: vi.fn(() => '
'),
- exportDictionaryCsv: vi.fn(() => 'csv'),
- exportDictionaryMarkdown: vi.fn(() => 'markdown'),
- exportPlantUml: vi.fn(() => '@startuml'),
- exportMermaid: vi.fn(() => 'graph TD'),
- exportDbml: vi.fn(() => 'Table users {}'),
+ exportDDL: vi.fn(() => "DDL"),
+ exportDiagramSvg: vi.fn(() => "
"),
+ exportDictionaryCsv: vi.fn(() => "csv"),
+ exportDictionaryMarkdown: vi.fn(() => "markdown"),
+ exportPlantUml: vi.fn(() => "@startuml"),
+ exportMermaid: vi.fn(() => "graph TD"),
+ exportDbml: vi.fn(() => "Table users {}"),
inferRelationships: vi.fn(),
-}))
+}));
-vi.mock('./api', () => api)
-vi.mock('./erd/export', () => ({
+vi.mock("./api", () => api);
+vi.mock("./erd/export", () => ({
downloadText: exports.downloadText,
exportDDL: exports.exportDDL,
exportDiagramSvg: exports.exportDiagramSvg,
exportDictionaryCsv: exports.exportDictionaryCsv,
exportDictionaryMarkdown: exports.exportDictionaryMarkdown,
exportPlantUml: exports.exportPlantUml,
-}))
-vi.mock('./erd/mermaid', () => ({ exportMermaid: exports.exportMermaid }))
-vi.mock('./erd/dbml', () => ({ exportDbml: exports.exportDbml }))
-vi.mock('./erd/autoInfer', () => ({ inferRelationships: exports.inferRelationships }))
-
-vi.mock('@xyflow/react', async () => {
- const React = await import('react')
+}));
+vi.mock("./erd/mermaid", () => ({ exportMermaid: exports.exportMermaid }));
+vi.mock("./erd/dbml", () => ({ exportDbml: exports.exportDbml }));
+vi.mock("./erd/autoInfer", () => ({
+ inferRelationships: exports.inferRelationships,
+}));
+
+vi.mock("@xyflow/react", async () => {
+ const React = await import("react");
const initialNode = {
- id: 'table-1',
- type: 'tableNode',
+ id: "table-1",
+ type: "tableNode",
position: { x: 5, y: 10 },
data: {
- title: 'public.users',
+ title: "public.users",
columns: [
- { column_name: 'id', data_type: 'bigint', is_not_null: true, is_pk: true },
- { column_name: 'email', data_type: 'text', is_not_null: false, is_pk: false },
+ {
+ column_name: "id",
+ data_type: "bigint",
+ is_not_null: true,
+ is_pk: true,
+ },
+ {
+ column_name: "email",
+ data_type: "text",
+ is_not_null: false,
+ is_pk: false,
+ },
],
badges: { pk: true, fk: false },
},
- }
+ };
const otherNode = {
...initialNode,
- id: 'table-2',
+ id: "table-2",
position: { x: 50, y: 100 },
- data: { ...initialNode.data, title: 'public.orders' },
- }
- const edge = { id: 'edge-1', source: 'table-1', target: 'table-2', label: 'fk_old' }
+ data: { ...initialNode.data, title: "public.orders" },
+ };
+ const edge = {
+ id: "edge-1",
+ source: "table-1",
+ target: "table-2",
+ label: "fk_old",
+ };
function ReactFlowMock(props: Record
) {
React.useEffect(() => {
- props.onInit?.({ fitView: vi.fn() })
- }, [props.onInit])
+ props.onInit?.({ fitView: vi.fn() });
+ }, [props.onInit]);
return (
{props.nodes.length}
{props.edges.length}
-
- )
+ );
}
return {
@@ -85,55 +132,97 @@ vi.mock('@xyflow/react', async () => {
Controls: () => ,
MiniMap: () => ,
Handle: () => ,
- Position: { Top: 'top', Left: 'left', Right: 'right', Bottom: 'bottom' },
+ Position: { Top: "top", Left: "left", Right: "right", Bottom: "bottom" },
ReactFlow: ReactFlowMock,
- ReactFlowProvider: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ ReactFlowProvider: ({ children }: { children: React.ReactNode }) => (
+ <>{children}>
+ ),
addEdge: (next: unknown, current: unknown[]) => [...current, next],
useNodesState: (initial: unknown[]) => {
- const [value, setValue] = React.useState(initial)
- return [value, setValue, vi.fn()]
+ const [value, setValue] = React.useState(initial);
+ return [value, setValue, vi.fn()];
},
useEdgesState: (initial: unknown[]) => {
- const [value, setValue] = React.useState(initial)
- return [value, setValue, vi.fn()]
+ const [value, setValue] = React.useState(initial);
+ return [value, setValue, vi.fn()];
},
__fixtures: { initialNode, otherNode, edge },
- }
-})
+ };
+});
-vi.mock('./erd/convert', async () => {
- const flow = (await import('@xyflow/react')) as any
+vi.mock("./erd/convert", async () => {
+ const flow = (await import("@xyflow/react")) as any;
return {
snapshotToGraph: vi.fn(() => ({
nodes: [flow.__fixtures.initialNode, flow.__fixtures.otherNode],
edges: [flow.__fixtures.edge],
})),
- }
-})
+ };
+});
-vi.mock('./components/modals', () => ({
+vi.mock("./components/modals", () => ({
AddTableModal: (props: any) => (
-
+
{props.isOpen ? (
<>
- props.setNewTableName('audit_log')} />
-
-
+ props.setNewTableName("audit_log")}
+ />
+
+
>
) : null}
),
EditEdgeModal: (props: any) => (
-
-
+
+
{props.editingEdge ? (
<>
- props.setRelLabel(' fk_new ')} />
-
-
-
+ props.setRelLabel(" fk_new ")}
+ />
+
+
+
>
) : null}
@@ -142,87 +231,267 @@ vi.mock('./components/modals', () => ({
{props.shareLinkUrl}
{props.shareLinkError}
-
+
{props.isOpen ? (
<>
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
>
) : null}
),
GroupModal: (props: any) => (
-
+
{props.isOpen ? (
<>
- props.setNewGroupName('Billing')} />
-
- props.onAssignBusinessGroup(props.nodes[0]?.id ?? 'missing-node', 'missing')} />
- props.onAssignBusinessGroup(props.nodes[0]?.id ?? 'missing-node', props.businessGroups[0]?.id ?? '')} />
- props.onDeleteBusinessGroup(props.businessGroups[0]?.id ?? 'missing')} />
-
+ props.setNewGroupName("Billing")}
+ />
+
+
+ props.onAssignBusinessGroup(
+ props.nodes[0]?.id ?? "missing-node",
+ "missing",
+ )
+ }
+ />
+
+ props.onAssignBusinessGroup(
+ props.nodes[0]?.id ?? "missing-node",
+ props.businessGroups[0]?.id ?? "",
+ )
+ }
+ />
+
+ props.onDeleteBusinessGroup(
+ props.businessGroups[0]?.id ?? "missing",
+ )
+ }
+ />
+
>
) : null}
),
CardinalityModal: (props: any) => {
const recommendation = {
- index_name: 'idx_users_email',
- columns: ['email'],
- access_method: 'btree',
+ index_name: "idx_users_email",
+ columns: ["email"],
+ access_method: "btree",
estimated_distinct: 50,
cardinality_ratio: 0.5,
- strength: 'recommended',
- reason: 'selective',
- source: 'cardinality-wizard',
- }
+ strength: "recommended",
+ reason: "selective",
+ source: "cardinality-wizard",
+ };
return (
{props.formatPercent(0.5)}
- {props.strengthLabel('recommended')}
- {props.strengthLabel('consider')}
- {props.strengthLabel('skip')}
- props.onApplyCardinalityRecommendation({ ...recommendation, strength: 'skip' })} />
+
+ {props.strengthLabel("recommended")}
+
+
+ {props.strengthLabel("consider")}
+
+
+ {props.strengthLabel("skip")}
+
+
+ props.onApplyCardinalityRecommendation({
+ ...recommendation,
+ strength: "skip",
+ })
+ }
+ />
{props.isOpen ? (
<>
- props.onCardinalityTableChange('missing')} />
- props.onCardinalityTableChange('table-2')} />
- props.onCardinalityColumnToggle('email', true)} />
- props.onCardinalityDistinctCountChange('email', 'bad')} />
- props.onCardinalityDistinctCountChange('email', '50')} />
- props.onApplyCardinalityRecommendation(recommendation)} />
- props.onApplyCardinalityRecommendation(recommendation)} />
- props.onApplyCardinalityRecommendation({ ...recommendation, columns: undefined })} />
- props.onApplyCardinalityRecommendation({ ...recommendation, index_name: '', columns: [], strength: 'consider' })} />
- props.onApplyCardinalityRecommendation({ ...recommendation, index_name: 'idx_users_second', columns: ['id'] })} />
+ props.onCardinalityTableChange("missing")}
+ />
+ props.onCardinalityTableChange("table-2")}
+ />
+ props.onCardinalityColumnToggle("email", true)}
+ />
+
+ props.onCardinalityDistinctCountChange("email", "bad")
+ }
+ />
+
+ props.onCardinalityDistinctCountChange("email", "50")
+ }
+ />
+
+ props.onApplyCardinalityRecommendation(recommendation)
+ }
+ />
+
+ props.onApplyCardinalityRecommendation(recommendation)
+ }
+ />
+
+ props.onApplyCardinalityRecommendation({
+ ...recommendation,
+ columns: undefined,
+ })
+ }
+ />
+
+ props.onApplyCardinalityRecommendation({
+ ...recommendation,
+ index_name: "",
+ columns: [],
+ strength: "consider",
+ })
+ }
+ />
+
+ props.onApplyCardinalityRecommendation({
+ ...recommendation,
+ index_name: "idx_users_second",
+ columns: ["id"],
+ })
+ }
+ />
{
- const clearButton = document.querySelector('[aria-label="모든 노드 지우기"]')
- clearButton?.click()
- props.onApplyCardinalityRecommendation(recommendation)
+ const clearButton = document.querySelector(
+ '[aria-label="모든 노드 지우기"]',
+ );
+ clearButton?.click();
+ props.onApplyCardinalityRecommendation(recommendation);
}}
/>
-
+
>
) : null}
- )
+ );
},
EditTableModal: (props: any) => (
-
-
+
+
{props.isOpen && props.editingNode ? (
<>
-
-
-
+
+
>
) : null}
),
-}))
+}));
-import App, { DiagramTable } from './App'
-import { snapshotToGraph } from './erd/convert'
+import App, { DiagramTable } from "./App";
+import { snapshotToGraph } from "./erd/convert";
const projects = [
- { project_space_uuid: 'p1', project_name: '' },
- { project_space_uuid: 'p2', project_name: 'HR' },
-]
-const connections = [{ db_connection_uuid: 'c1', conn_name: 'Warehouse' }]
+ { project_space_uuid: "p1", project_name: "" },
+ { project_space_uuid: "p2", project_name: "HR" },
+];
+const connections = [{ db_connection_uuid: "c1", conn_name: "Warehouse" }];
const snapshots = [
- { schema_snapshot_uuid: 's1', status: 'succeeded', schema_filter: 'billing' },
- { schema_snapshot_uuid: 's2', status: 'failed', schema_filter: null },
-]
+ { schema_snapshot_uuid: "s1", status: "succeeded", schema_filter: "billing" },
+ { schema_snapshot_uuid: "s2", status: "failed", schema_filter: null },
+];
beforeEach(() => {
- vi.clearAllMocks()
- api.getMe.mockResolvedValue({ subject: 'user', display_name: 'User', user_account_uuid: 'u' })
- api.listProjects.mockResolvedValue(projects)
- api.listConnections.mockResolvedValue(connections)
- api.listSnapshots.mockResolvedValue(snapshots)
- api.createProject.mockResolvedValue({ project_space_uuid: 'p3', project_name: 'New' })
- api.createConnection.mockResolvedValue({ db_connection_uuid: 'c2', conn_name: 'New DB' })
- api.createSnapshot.mockResolvedValue({ schema_snapshot_uuid: 's3', status: 'queued', schema_filter: 'public' })
+ vi.clearAllMocks();
+ api.getMe.mockResolvedValue({
+ subject: "user",
+ display_name: "User",
+ user_account_uuid: "u",
+ });
+ api.listProjects.mockResolvedValue(projects);
+ api.listConnections.mockResolvedValue(connections);
+ api.listSnapshots.mockResolvedValue(snapshots);
+ api.createProject.mockResolvedValue({
+ project_space_uuid: "p3",
+ project_name: "New",
+ });
+ api.createConnection.mockResolvedValue({
+ db_connection_uuid: "c2",
+ conn_name: "New DB",
+ });
+ api.createSnapshot.mockResolvedValue({
+ schema_snapshot_uuid: "s3",
+ status: "queued",
+ schema_filter: "public",
+ });
api.getSnapshot.mockResolvedValue({
- schema_snapshot_uuid: 's3',
- status: 'succeeded',
- schema_filter: 'public',
+ schema_snapshot_uuid: "s3",
+ status: "succeeded",
+ schema_filter: "public",
error_message: null,
snapshot_json: { relations: [], columns: [], pk_columns: [], fk_edges: [] },
- })
- api.createShareLink.mockResolvedValue({ url: 'http://localhost/api/share/one' })
+ });
+ api.createShareLink.mockResolvedValue({
+ url: "http://localhost/api/share/one",
+ });
exports.inferRelationships.mockReturnValue([
- { id: 'inferred', source: 'table-1', target: 'table-2', label: 'fk_inferred' },
- ])
- vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
- vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
- callback(0)
- return 1
- })
- Object.defineProperty(navigator, 'clipboard', {
+ {
+ id: "inferred",
+ source: "table-1",
+ target: "table-2",
+ label: "fk_inferred",
+ },
+ ]);
+ vi.stubGlobal(
+ "ResizeObserver",
+ class {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ },
+ );
+ vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
+ callback(0);
+ return 1;
+ });
+ Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText: vi.fn().mockResolvedValue(undefined) },
- })
-})
+ });
+});
afterEach(() => {
- cleanup()
- vi.useRealTimers()
- vi.unstubAllGlobals()
- vi.restoreAllMocks()
-})
+ cleanup();
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+});
async function renderReadyApp() {
- render()
- await screen.findByRole('heading', { name: '대시보드' })
-}
-
-function buildTestDsn(scheme: string, authority: string, path = '') {
- const pathSuffix = path ? `/${path}` : ''
- return [scheme, ':', '/', '/', authority, pathSuffix].join('')
+ render();
+ await screen.findByRole("heading", { name: "대시보드" });
}
function forceClick(button: HTMLButtonElement) {
- button.disabled = false
- button.removeAttribute('disabled')
- button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
+ button.disabled = false;
+ button.removeAttribute("disabled");
+ button.dispatchEvent(
+ new MouseEvent("click", { bubbles: true, cancelable: true }),
+ );
}
-describe('App orchestration coverage', () => {
- it('shows loading and explicit authentication failure', async () => {
- let rejectMe!: (reason?: unknown) => void
- api.getMe.mockReturnValueOnce(new Promise((_resolve, reject) => { rejectMe = reject }))
- render()
- expect(screen.getByText('Authenticating…')).toBeInTheDocument()
- await act(async () => rejectMe(new Error('denied')))
- expect(await screen.findByRole('heading', { name: 'Authentication required' })).toBeInTheDocument()
- expect(screen.getByRole('alert')).toHaveTextContent('denied')
- })
-
- it('navigates dashboard, project, and diagram states including empty/search branches', async () => {
- const user = userEvent.setup()
- await renderReadyApp()
- expect(screen.getAllByText('<Billing & Core>').length).toBeGreaterThan(0)
- fireEvent.click(screen.getByRole('button', { name: '전체 보기' }))
- expect(screen.getByRole('heading', { name: '프로젝트' })).toBeInTheDocument()
- fireEvent.click(screen.getAllByRole('button', { name: '열기' })[1]!)
- expect(screen.getByRole('heading', { name: '다이어그램' })).toBeInTheDocument()
-
- const searchInput = screen.getByLabelText('다이어그램 검색')
- // Wait for the snapshots to load before searching.
- await waitFor(() => expect(screen.queryByText('아직 다이어그램 스냅샷이 없습니다. 편집기에서 데이터베이스를 역공학해 시작하세요.')).not.toBeInTheDocument())
-
- const diagramSearchCallCounts = {
- listProjects: api.listProjects.mock.calls.length,
- listConnections: api.listConnections.mock.calls.length,
- listSnapshots: api.listSnapshots.mock.calls.length,
- getSnapshot: api.getSnapshot.mock.calls.length,
- }
- const diagramSearchLocation = window.location.href
-
- await user.clear(searchInput)
- await user.type(searchInput, 'no-match{Enter}')
-
- const searchForm = searchInput.closest('form')
- const searchSubmitEvent = new Event('submit', { cancelable: true, bubbles: true })
- if (searchForm) searchForm.dispatchEvent(searchSubmitEvent)
- expect(searchSubmitEvent.defaultPrevented).toBe(true)
-
- expect(screen.getByText('검색 결과가 없습니다.')).toBeInTheDocument()
- expect(api.listProjects).toHaveBeenCalledTimes(diagramSearchCallCounts.listProjects)
- expect(api.listConnections).toHaveBeenCalledTimes(diagramSearchCallCounts.listConnections)
- expect(api.listSnapshots).toHaveBeenCalledTimes(diagramSearchCallCounts.listSnapshots)
- expect(api.getSnapshot).toHaveBeenCalledTimes(diagramSearchCallCounts.getSnapshot)
- expect(window.location.href).toBe(diagramSearchLocation)
-
- await user.clear(searchInput)
- await user.type(searchInput, 'failed{Enter}')
- expect(screen.getByText('ERD_all_2')).toBeInTheDocument()
- expect(api.listProjects).toHaveBeenCalledTimes(diagramSearchCallCounts.listProjects)
- expect(api.listConnections).toHaveBeenCalledTimes(diagramSearchCallCounts.listConnections)
- expect(api.listSnapshots).toHaveBeenCalledTimes(diagramSearchCallCounts.listSnapshots)
- expect(api.getSnapshot).toHaveBeenCalledTimes(diagramSearchCallCounts.getSnapshot)
- expect(window.location.href).toBe(diagramSearchLocation)
- fireEvent.click(screen.getByRole('button', { name: '편집기 열기' }))
- expect(screen.getByRole('toolbar', { name: 'ERD 캔버스 도구' })).toBeInTheDocument()
-
- cleanup()
- api.listProjects.mockResolvedValueOnce([])
- await act(async () => render())
- await screen.findByText('아직 프로젝트가 없습니다. 편집기에서 프로젝트를 생성하세요.')
- fireEvent.click(screen.getByRole('button', { name: '전체 보기' }))
- expect(screen.getByText('프로젝트가 없습니다. 이름을 입력해 새 프로젝트를 만드세요.')).toBeInTheDocument()
- fireEvent.click(screen.getByRole('button', { name: '다이어그램' }))
- expect(screen.getByText('프로젝트를 선택하세요.')).toBeInTheDocument()
- })
-
- it('creates projects, validates and creates connections, and starts a snapshot', async () => {
- const user = userEvent.setup()
- await renderReadyApp()
-
- // 1. Projects view inline-create via Enter
- await user.click(screen.getByRole('button', { name: '프로젝트' }))
- const inlineInput = screen.getByLabelText('새 프로젝트 이름')
- await user.clear(inlineInput)
- // Assert invalid (whitespace only) does not submit
- vi.mocked(api.createProject).mockClear()
- await user.type(inlineInput, ' {Enter}')
- expect(api.createProject).not.toHaveBeenCalled()
- // Assert valid submits exactly once
- await user.clear(inlineInput)
- await user.type(inlineInput, 'InlineProject{Enter}')
- await waitFor(() => expect(api.createProject).toHaveBeenCalledTimes(1))
- expect(api.createProject).toHaveBeenCalledWith('InlineProject')
-
- // Navigate back to Editor view
- await user.click(screen.getByRole('button', { name: '편집기' }))
-
- // 2. Editor view New Project via Enter
- const newProjectInput = screen.getByLabelText('New project')
- await user.clear(newProjectInput)
- vi.mocked(api.createProject).mockClear()
- vi.mocked(api.createProject).mockResolvedValueOnce({ project_space_uuid: 'p4', project_name: 'New' })
- await user.type(newProjectInput, ' {Enter}')
- expect(api.createProject).not.toHaveBeenCalled()
- await user.clear(newProjectInput)
- await user.type(newProjectInput, 'New{Enter}')
- await waitFor(() => expect(api.createProject).toHaveBeenCalledTimes(1))
- expect(api.createProject).toHaveBeenCalledWith('New')
-
- const projectSelect = screen.getByLabelText('Project')
- const connectionName = screen.getByLabelText('New connection (DSN)')
- const dsn = screen.getByLabelText('Connection DSN')
- const saveConnection = screen.getByRole('button', { name: 'Save connection' })
- vi.mocked(api.createConnection).mockClear()
-
- fireEvent.change(projectSelect, { target: { value: '' } })
- expect(saveConnection).toBeDisabled()
- await user.type(dsn, '{Enter}')
- expect(api.createConnection).not.toHaveBeenCalled()
-
- fireEvent.change(projectSelect, { target: { value: 'p4' } })
- fireEvent.change(connectionName, { target: { value: ' ' } })
- fireEvent.change(dsn, { target: { value: ' ' } })
- expect(saveConnection).toBeDisabled()
- await user.type(dsn, '{Enter}')
- expect(api.createConnection).not.toHaveBeenCalled()
-
- fireEvent.change(connectionName, { target: { value: 'target-db' } })
- await user.clear(dsn)
- expect(saveConnection).toBeDisabled()
- await user.type(dsn, '{Enter}')
- expect(api.createConnection).not.toHaveBeenCalled()
-
- fireEvent.change(dsn, { target: { value: buildTestDsn('postgresql', '[') } })
- await user.type(dsn, '{Enter}')
- expect(screen.getByRole('alert')).toHaveTextContent('Connection DSN must use')
- expect(api.createConnection).not.toHaveBeenCalled()
-
- fireEvent.change(dsn, { target: { value: buildTestDsn('http', 'bad.example', 'db') } })
- await user.type(dsn, '{Enter}')
- expect(screen.getByRole('alert')).toHaveTextContent('Connection DSN must use')
- expect(api.createConnection).not.toHaveBeenCalled()
- expect(dsn).toHaveValue('')
-
- // 3. Editor view New Connection via Enter
- const validDsn = buildTestDsn('postgresql', 'db.example', 'test')
- vi.mocked(api.createConnection).mockClear()
- fireEvent.change(dsn, { target: { value: validDsn } })
- await user.type(dsn, '{Enter}')
- await waitFor(() => expect(api.createConnection).toHaveBeenCalledTimes(1))
- expect(api.createConnection).toHaveBeenCalledWith('p4', 'target-db', validDsn)
-
- fireEvent.change(screen.getByLabelText('Schema filter (optional)'), { target: { value: ' public ' } })
- fireEvent.click(screen.getByRole('button', { name: 'Reverse engineer → snapshot' }))
- await waitFor(() => expect(api.createSnapshot).toHaveBeenCalledWith('p4', 'c2', 'public'))
- expect(screen.getByText('스냅샷 생성 중...')).toBeInTheDocument()
- })
-
- it('submits canvas search with Enter without navigation or API calls', async () => {
- const user = userEvent.setup()
- await renderReadyApp()
- await user.click(screen.getByRole('button', { name: '편집기' }))
-
- const canvasSearchInput = screen.getByLabelText('테이블 또는 컬럼 검색')
- const callCounts = {
- listProjects: api.listProjects.mock.calls.length,
- listConnections: api.listConnections.mock.calls.length,
- listSnapshots: api.listSnapshots.mock.calls.length,
- getSnapshot: api.getSnapshot.mock.calls.length,
- }
- const locationBeforeSubmit = window.location.href
-
- await user.type(canvasSearchInput, 'users{Enter}')
-
- expect(canvasSearchInput).toHaveValue('users')
- expect(screen.getByText('0개 테이블 일치', { exact: false })).toBeInTheDocument()
- expect(api.listProjects).toHaveBeenCalledTimes(callCounts.listProjects)
- expect(api.listConnections).toHaveBeenCalledTimes(callCounts.listConnections)
- expect(api.listSnapshots).toHaveBeenCalledTimes(callCounts.listSnapshots)
- expect(api.getSnapshot).toHaveBeenCalledTimes(callCounts.getSnapshot)
- expect(window.location.href).toBe(locationBeforeSubmit)
- })
-
- it('polls a terminal snapshot, builds graph state, and exercises editor handlers', async () => {
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '다이어그램' }))
- const openButtons = await screen.findAllByRole('button', { name: '열기' })
- vi.useFakeTimers()
- fireEvent.click(openButtons[0]!)
+describe("App orchestration coverage", () => {
+ it("shows loading and explicit authentication failure", async () => {
+ let rejectMe!: (reason?: unknown) => void;
+ api.getMe.mockReturnValueOnce(
+ new Promise((_resolve, reject) => {
+ rejectMe = reject;
+ }),
+ );
+ render();
+ expect(screen.getByText("Authenticating…")).toBeInTheDocument();
+ await act(async () => rejectMe(new Error("denied")));
+ expect(
+ await screen.findByRole("heading", { name: "Authentication required" }),
+ ).toBeInTheDocument();
+ expect(screen.getByRole("alert")).toHaveTextContent("denied");
+ });
+
+ it("navigates dashboard, project, and diagram states including empty/search branches", async () => {
+ await renderReadyApp();
+ expect(
+ screen.getAllByText("<Billing & Core>").length,
+ ).toBeGreaterThan(0);
+ fireEvent.click(screen.getByRole("button", { name: "전체 보기" }));
+ expect(
+ screen.getByRole("heading", { name: "프로젝트" }),
+ ).toBeInTheDocument();
+ fireEvent.click(screen.getAllByRole("button", { name: "열기" })[1]!);
+ expect(
+ screen.getByRole("heading", { name: "다이어그램" }),
+ ).toBeInTheDocument();
+ fireEvent.change(screen.getByLabelText("다이어그램 검색"), {
+ target: { value: "no-match" },
+ });
+ expect(screen.getByText("검색 결과가 없습니다.")).toBeInTheDocument();
+ fireEvent.change(screen.getByLabelText("다이어그램 검색"), {
+ target: { value: "failed" },
+ });
+ expect(screen.getByText("ERD_all_2")).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "편집기 열기" }));
+ expect(
+ screen.getByRole("toolbar", { name: "ERD 캔버스 도구" }),
+ ).toBeInTheDocument();
+
+ cleanup();
+ api.listProjects.mockResolvedValueOnce([]);
+ await act(async () => render());
+ await screen.findByText(
+ "아직 프로젝트가 없습니다. 편집기에서 프로젝트를 생성하세요.",
+ );
+ fireEvent.click(screen.getByRole("button", { name: "전체 보기" }));
+ expect(
+ screen.getByText(
+ "프로젝트가 없습니다. 이름을 입력해 새 프로젝트를 만드세요.",
+ ),
+ ).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "다이어그램" }));
+ expect(screen.getByText("프로젝트를 선택하세요.")).toBeInTheDocument();
+ });
+
+ it("creates projects, validates and creates connections, and starts a snapshot", async () => {
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "편집기" }));
+
+ fireEvent.change(screen.getByLabelText("New project"), {
+ target: { value: " New " },
+ });
+ fireEvent.click(screen.getByRole("button", { name: "Create" }));
+ await waitFor(() => expect(api.createProject).toHaveBeenCalledWith("New"));
+
+ const dsn = screen.getByLabelText("Connection DSN");
+ fireEvent.change(dsn, { target: { value: "postgresql://[" } });
+ fireEvent.click(screen.getByRole("button", { name: "Save connection" }));
+ expect(screen.getByRole("alert")).toHaveTextContent(
+ "Connection DSN must use",
+ );
+ fireEvent.change(dsn, { target: { value: "http://bad.example/db" } });
+ fireEvent.click(screen.getByRole("button", { name: "Save connection" }));
+ expect(screen.getByRole("alert")).toHaveTextContent(
+ "Connection DSN must use",
+ );
+ expect(dsn).toHaveValue("");
+
+ fireEvent.change(dsn, {
+ target: { value: "postgresql://db.example/test" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: "Save connection" }));
+ await waitFor(() =>
+ expect(api.createConnection).toHaveBeenCalledWith(
+ "p3",
+ "target-db",
+ "postgresql://db.example/test",
+ ),
+ );
+
+ fireEvent.change(screen.getByLabelText("Schema filter (optional)"), {
+ target: { value: " public " },
+ });
+ fireEvent.click(
+ screen.getByRole("button", { name: "Reverse engineer → snapshot" }),
+ );
+ await waitFor(() =>
+ expect(api.createSnapshot).toHaveBeenCalledWith("p3", "c2", "public"),
+ );
+ expect(screen.getByText("스냅샷 생성 중...")).toBeInTheDocument();
+ });
+
+ it("polls a terminal snapshot, builds graph state, and exercises editor handlers", async () => {
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "다이어그램" }));
+ const openButtons = await screen.findAllByRole("button", { name: "열기" });
+ vi.useFakeTimers();
+ fireEvent.click(openButtons[0]!);
await act(async () => {
- vi.advanceTimersByTime(1000)
- await Promise.resolve()
- await Promise.resolve()
- })
- expect(api.getSnapshot).toHaveBeenCalledWith('s1')
- expect(screen.getByTestId('node-count')).toHaveTextContent('2')
-
- const canvasSearchInput = screen.getByLabelText('테이블 또는 컬럼 검색')
- fireEvent.change(canvasSearchInput, { target: { value: 'users' } })
- const canvasSearchForm = canvasSearchInput.closest('form')
- const canvasSubmitEvent = new Event('submit', { cancelable: true, bubbles: true })
- if (canvasSearchForm) canvasSearchForm.dispatchEvent(canvasSubmitEvent)
- expect(canvasSubmitEvent.defaultPrevented).toBe(true)
-
- expect(screen.getByText('1개 테이블 일치', { exact: false })).toBeInTheDocument()
- fireEvent.click(screen.getByRole('button', { name: 'ERD 자동 정렬' }))
+ vi.advanceTimersByTime(1000);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(api.getSnapshot).toHaveBeenCalledWith("s1");
+ expect(screen.getByTestId("node-count")).toHaveTextContent("2");
+
+ const canvasSearchInput = screen.getByLabelText("테이블 또는 컬럼 검색");
+ fireEvent.change(canvasSearchInput, { target: { value: "users" } });
+ const canvasSearchForm = canvasSearchInput.closest("form");
+ const canvasSubmitEvent = new Event("submit", {
+ cancelable: true,
+ bubbles: true,
+ });
+ if (canvasSearchForm) canvasSearchForm.dispatchEvent(canvasSubmitEvent);
+ expect(canvasSubmitEvent.defaultPrevented).toBe(true);
+ expect(
+ screen.getByText("1개 테이블 일치", { exact: false }),
+ ).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "ERD 자동 정렬" }));
await act(async () => {
- vi.runOnlyPendingTimers()
- await Promise.resolve()
- })
- expect(screen.getByText('정렬 완료', { exact: false })).toBeInTheDocument()
- fireEvent.click(screen.getByRole('button', { name: '정렬 되돌리기' }))
- expect(screen.getByText('되돌렸습니다', { exact: false })).toBeInTheDocument()
-
- fireEvent.click(screen.getByTestId('flow-connect'))
- fireEvent.click(screen.getByTestId('edge-label'))
- fireEvent.click(screen.getByTestId('edge-submit'))
- fireEvent.click(screen.getByTestId('flow-edge'))
- fireEvent.click(screen.getByTestId('edge-cancel'))
- fireEvent.click(screen.getByTestId('flow-edge-unlabeled'))
- fireEvent.click(screen.getByTestId('edge-cancel'))
- fireEvent.click(screen.getByTestId('flow-edge'))
- vi.spyOn(window, 'confirm').mockReturnValueOnce(false).mockReturnValueOnce(true)
- fireEvent.click(screen.getByTestId('edge-delete'))
- fireEvent.click(screen.getByTestId('edge-delete'))
-
- fireEvent.doubleClick(screen.getByTestId('flow-node'))
- fireEvent.submit(screen.getByTestId('table-empty-form'))
- fireEvent.click(screen.getByTestId('table-cancel'))
- fireEvent.doubleClick(screen.getByTestId('flow-node'))
- fireEvent.submit(screen.getByTestId('table-form'))
- fireEvent.doubleClick(screen.getByTestId('flow-node'))
- fireEvent.click(screen.getByTestId('table-cancel'))
- fireEvent.doubleClick(screen.getByTestId('flow-node'))
- vi.spyOn(window, 'confirm').mockReturnValueOnce(false).mockReturnValueOnce(true)
- fireEvent.click(screen.getByTestId('table-delete'))
- fireEvent.click(screen.getByTestId('table-delete'))
- })
-
- it('adds nodes and exercises groups, cardinality, exports, inference, and clearing', async () => {
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '편집기' }))
- fireEvent.click(screen.getAllByRole('button', { name: '테이블 추가' })[0]!)
- fireEvent.click(screen.getByTestId('add-name'))
- fireEvent.click(screen.getByTestId('add-submit'))
- expect(screen.getByTestId('node-count')).toHaveTextContent('1')
-
- fireEvent.click(screen.getByRole('button', { name: '업무 그룹' }))
- fireEvent.click(screen.getByTestId('group-create-guard'))
- fireEvent.click(screen.getByTestId('group-name'))
- fireEvent.click(screen.getByTestId('group-create'))
- fireEvent.click(screen.getByTestId('group-assign-missing'))
- fireEvent.click(screen.getByTestId('group-assign'))
- vi.spyOn(window, 'confirm').mockReturnValueOnce(false).mockReturnValueOnce(true)
- fireEvent.click(screen.getByTestId('group-delete'))
- fireEvent.click(screen.getByTestId('group-delete'))
- fireEvent.click(screen.getByTestId('group-close'))
-
- fireEvent.click(screen.getByRole('button', { name: '인덱스 카디널리티 계산' }))
- expect(screen.getByTestId('card-format')).toHaveTextContent('50%')
- expect(screen.getByTestId('card-strength-recommended')).toHaveTextContent('추천')
- expect(screen.getByTestId('card-strength-consider')).toHaveTextContent('검토')
- expect(screen.getByTestId('card-strength-skip')).toHaveTextContent('보류')
- fireEvent.click(screen.getByTestId('card-table-missing'))
- fireEvent.click(screen.getByTestId('card-table'))
- fireEvent.click(screen.getByTestId('card-toggle'))
- fireEvent.click(screen.getByTestId('card-distinct-invalid'))
- fireEvent.click(screen.getByTestId('card-distinct'))
- fireEvent.click(screen.getByTestId('card-apply'))
- fireEvent.click(screen.getByTestId('card-apply-duplicate'))
- fireEvent.click(screen.getByTestId('card-apply-no-columns'))
- fireEvent.click(screen.getByTestId('card-apply-empty'))
- fireEvent.click(screen.getByTestId('card-apply-second'))
- fireEvent.click(screen.getByTestId('card-close'))
-
- fireEvent.click(screen.getByRole('button', { name: 'DDL 내보내기' }))
- for (const id of ['export-copy-ddl', 'export-svg', 'export-uml', 'export-mermaid', 'export-dbml', 'export-csv', 'export-md']) {
- fireEvent.click(screen.getByTestId(id))
+ vi.runOnlyPendingTimers();
+ await Promise.resolve();
+ });
+ expect(screen.getByText("정렬 완료", { exact: false })).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "정렬 되돌리기" }));
+ expect(
+ screen.getByText("되돌렸습니다", { exact: false }),
+ ).toBeInTheDocument();
+
+ fireEvent.click(screen.getByTestId("flow-connect"));
+ fireEvent.click(screen.getByTestId("edge-label"));
+ fireEvent.click(screen.getByTestId("edge-submit"));
+ fireEvent.click(screen.getByTestId("flow-edge"));
+ fireEvent.click(screen.getByTestId("edge-cancel"));
+ fireEvent.click(screen.getByTestId("flow-edge-unlabeled"));
+ fireEvent.click(screen.getByTestId("edge-cancel"));
+ fireEvent.click(screen.getByTestId("flow-edge"));
+ vi.spyOn(window, "confirm")
+ .mockReturnValueOnce(false)
+ .mockReturnValueOnce(true);
+ fireEvent.click(screen.getByTestId("edge-delete"));
+ fireEvent.click(screen.getByTestId("edge-delete"));
+
+ fireEvent.doubleClick(screen.getByTestId("flow-node"));
+ fireEvent.submit(screen.getByTestId("table-empty-form"));
+ fireEvent.click(screen.getByTestId("table-cancel"));
+ fireEvent.doubleClick(screen.getByTestId("flow-node"));
+ fireEvent.submit(screen.getByTestId("table-form"));
+ fireEvent.doubleClick(screen.getByTestId("flow-node"));
+ fireEvent.click(screen.getByTestId("table-cancel"));
+ fireEvent.doubleClick(screen.getByTestId("flow-node"));
+ vi.spyOn(window, "confirm")
+ .mockReturnValueOnce(false)
+ .mockReturnValueOnce(true);
+ fireEvent.click(screen.getByTestId("table-delete"));
+ fireEvent.click(screen.getByTestId("table-delete"));
+ });
+
+ it("adds nodes and exercises groups, cardinality, exports, inference, and clearing", async () => {
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "편집기" }));
+ fireEvent.click(screen.getAllByRole("button", { name: "테이블 추가" })[0]!);
+ fireEvent.click(screen.getByTestId("add-name"));
+ fireEvent.click(screen.getByTestId("add-submit"));
+ expect(screen.getByTestId("node-count")).toHaveTextContent("1");
+
+ fireEvent.click(screen.getByRole("button", { name: "업무 그룹" }));
+ fireEvent.click(screen.getByTestId("group-create-guard"));
+ fireEvent.click(screen.getByTestId("group-name"));
+ fireEvent.click(screen.getByTestId("group-create"));
+ fireEvent.click(screen.getByTestId("group-assign-missing"));
+ fireEvent.click(screen.getByTestId("group-assign"));
+ vi.spyOn(window, "confirm")
+ .mockReturnValueOnce(false)
+ .mockReturnValueOnce(true);
+ fireEvent.click(screen.getByTestId("group-delete"));
+ fireEvent.click(screen.getByTestId("group-delete"));
+ fireEvent.click(screen.getByTestId("group-close"));
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "인덱스 카디널리티 계산" }),
+ );
+ expect(screen.getByTestId("card-format")).toHaveTextContent("50%");
+ expect(screen.getByTestId("card-strength-recommended")).toHaveTextContent(
+ "추천",
+ );
+ expect(screen.getByTestId("card-strength-consider")).toHaveTextContent(
+ "검토",
+ );
+ expect(screen.getByTestId("card-strength-skip")).toHaveTextContent("보류");
+ fireEvent.click(screen.getByTestId("card-table-missing"));
+ fireEvent.click(screen.getByTestId("card-table"));
+ fireEvent.click(screen.getByTestId("card-toggle"));
+ fireEvent.click(screen.getByTestId("card-distinct-invalid"));
+ fireEvent.click(screen.getByTestId("card-distinct"));
+ fireEvent.click(screen.getByTestId("card-apply"));
+ fireEvent.click(screen.getByTestId("card-apply-duplicate"));
+ fireEvent.click(screen.getByTestId("card-apply-no-columns"));
+ fireEvent.click(screen.getByTestId("card-apply-empty"));
+ fireEvent.click(screen.getByTestId("card-apply-second"));
+ fireEvent.click(screen.getByTestId("card-close"));
+
+ fireEvent.click(screen.getByRole("button", { name: "DDL 내보내기" }));
+ for (const id of [
+ "export-copy-ddl",
+ "export-svg",
+ "export-uml",
+ "export-mermaid",
+ "export-dbml",
+ "export-csv",
+ "export-md",
+ ]) {
+ fireEvent.click(screen.getByTestId(id));
}
- fireEvent.click(screen.getByTestId('share-create'))
- await waitFor(() => expect(screen.getByTestId('share-url')).toHaveTextContent('/api/share/one'))
- fireEvent.click(screen.getByTestId('share-copy'))
- fireEvent.click(screen.getByTestId('export-close'))
- expect(exports.downloadText).toHaveBeenCalledTimes(6)
-
- fireEvent.click(screen.getByRole('button', { name: '관계 자동 추론' }))
- expect(exports.inferRelationships).toHaveBeenCalled()
- exports.inferRelationships.mockReturnValueOnce([])
- fireEvent.click(screen.getByRole('button', { name: '관계 자동 추론' }))
- vi.spyOn(window, 'confirm').mockReturnValueOnce(false).mockReturnValueOnce(true)
- fireEvent.click(screen.getByRole('button', { name: '모든 노드 지우기' }))
- fireEvent.click(screen.getByRole('button', { name: '모든 노드 지우기' }))
- expect(screen.getByText('ERD 캔버스가 비어 있습니다')).toBeInTheDocument()
- })
-
- it('covers guarded editor actions, navigation callbacks, and form selectors', async () => {
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '편집기' }))
-
- vi.spyOn(window, 'confirm').mockReturnValue(true)
+ fireEvent.click(screen.getByTestId("share-create"));
+ await waitFor(() =>
+ expect(screen.getByTestId("share-url")).toHaveTextContent(
+ "/api/share/one",
+ ),
+ );
+ fireEvent.click(screen.getByTestId("share-copy"));
+ fireEvent.click(screen.getByTestId("export-close"));
+ expect(exports.downloadText).toHaveBeenCalledTimes(6);
+
+ fireEvent.click(screen.getByRole("button", { name: "관계 자동 추론" }));
+ expect(exports.inferRelationships).toHaveBeenCalled();
+ exports.inferRelationships.mockReturnValueOnce([]);
+ fireEvent.click(screen.getByRole("button", { name: "관계 자동 추론" }));
+ vi.spyOn(window, "confirm")
+ .mockReturnValueOnce(false)
+ .mockReturnValueOnce(true);
+ fireEvent.click(screen.getByRole("button", { name: "모든 노드 지우기" }));
+ fireEvent.click(screen.getByRole("button", { name: "모든 노드 지우기" }));
+ expect(screen.getByText("ERD 캔버스가 비어 있습니다")).toBeInTheDocument();
+ });
+
+ it("covers guarded editor actions, navigation callbacks, and form selectors", async () => {
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "편집기" }));
+
+ vi.spyOn(window, "confirm").mockReturnValue(true);
for (const id of [
- 'edge-guard-submit',
- 'edge-guard-delete',
- 'share-copy-guard',
- 'table-delete-guard',
- 'add-guard',
- 'group-create-guard',
- 'card-skip-guard',
+ "edge-guard-submit",
+ "edge-guard-delete",
+ "share-copy-guard",
+ "table-delete-guard",
+ "add-guard",
+ "group-create-guard",
+ "card-skip-guard",
]) {
- fireEvent.click(screen.getByTestId(id))
+ fireEvent.click(screen.getByTestId(id));
}
- fireEvent.submit(screen.getByTestId('table-submit-guard'))
+ fireEvent.submit(screen.getByTestId("table-submit-guard"));
for (const name of [
- 'ERD 자동 정렬',
- '정렬 되돌리기',
- '업무 그룹',
- '인덱스 카디널리티 계산',
+ "ERD 자동 정렬",
+ "정렬 되돌리기",
+ "업무 그룹",
+ "인덱스 카디널리티 계산",
]) {
- const button = screen.getByRole('button', { name }) as HTMLButtonElement
- forceClick(button)
+ const button = screen.getByRole("button", { name }) as HTMLButtonElement;
+ forceClick(button);
}
- fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' }))
- fireEvent.click(screen.getByTestId('export-close'))
-
- fireEvent.change(screen.getByLabelText('Project'), { target: { value: 'p2' } })
- fireEvent.change(screen.getByLabelText('Connection'), { target: { value: 'c1' } })
- fireEvent.change(screen.getByLabelText('New connection (DSN)'), { target: { value: 'Analytics' } })
-
- fireEvent.change(screen.getByLabelText('New project'), { target: { value: ' ' } })
- forceClick(screen.getByRole('button', { name: 'Create' }))
- fireEvent.change(screen.getByLabelText('New project'), { target: { value: 'demo' } })
-
- fireEvent.change(screen.getByLabelText('Project'), { target: { value: '' } })
- forceClick(screen.getByRole('button', { name: 'Save connection' }))
- fireEvent.change(screen.getByLabelText('Project'), { target: { value: 'p1' } })
- fireEvent.change(screen.getByLabelText('New connection (DSN)'), { target: { value: ' ' } })
- forceClick(screen.getByRole('button', { name: 'Save connection' }))
- fireEvent.change(screen.getByLabelText('Connection'), { target: { value: '' } })
- forceClick(screen.getByRole('button', { name: 'Reverse engineer → snapshot' }))
- fireEvent.click(screen.getAllByRole('button', { name: '테이블 추가' })[0]!)
- fireEvent.click(screen.getByTestId('add-cancel'))
-
- fireEvent.click(screen.getByRole('button', { name: '대시보드' }))
- fireEvent.click(screen.getByRole('button', { name: /Billing.*다이어그램 보기/ }))
- expect(screen.getByRole('heading', { name: '다이어그램' })).toBeInTheDocument()
- fireEvent.click(screen.getByRole('button', { name: '대시보드' }))
- await waitFor(() => expect(screen.getAllByRole('button', { name: '열기' }).length).toBeGreaterThan(0))
- fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!)
- expect(screen.getByRole('toolbar', { name: 'ERD 캔버스 도구' })).toBeInTheDocument()
- fireEvent.click(screen.getByRole('button', { name: '대시보드' }))
- fireEvent.click(screen.getByRole('button', { name: '편집기로 이동' }))
- expect(screen.getByRole('toolbar', { name: 'ERD 캔버스 도구' })).toBeInTheDocument()
- fireEvent.click(screen.getByRole('button', { name: '대시보드' }))
- fireEvent.click(screen.getByRole('button', { name: '새 모델링' }))
- expect(screen.getByRole('toolbar', { name: 'ERD 캔버스 도구' })).toBeInTheDocument()
-
- fireEvent.click(screen.getByRole('button', { name: '프로젝트' }))
- fireEvent.change(screen.getByLabelText('새 프로젝트 이름'), { target: { value: 'Roadmap' } })
- fireEvent.click(screen.getByRole('button', { name: '새 프로젝트' }))
- await waitFor(() => expect(api.createProject).toHaveBeenCalledWith('Roadmap'))
- fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!)
- expect(screen.getByRole('heading', { name: '다이어그램' })).toBeInTheDocument()
- fireEvent.click(screen.getByRole('button', { name: '대시보드' }))
- fireEvent.click(screen.getByRole('button', { name: '목록 보기' }))
- })
-
- it('clears replacement copy timers and pending timers during close and unmount', async () => {
- vi.useFakeTimers()
+ fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" }));
+ fireEvent.click(screen.getByTestId("export-close"));
+
+ fireEvent.change(screen.getByLabelText("Project"), {
+ target: { value: "p2" },
+ });
+ fireEvent.change(screen.getByLabelText("Connection"), {
+ target: { value: "c1" },
+ });
+ fireEvent.change(screen.getByLabelText("New connection (DSN)"), {
+ target: { value: "Analytics" },
+ });
+
+ fireEvent.change(screen.getByLabelText("New project"), {
+ target: { value: " " },
+ });
+ forceClick(screen.getByRole("button", { name: "Create" }));
+ fireEvent.change(screen.getByLabelText("New project"), {
+ target: { value: "demo" },
+ });
+
+ fireEvent.change(screen.getByLabelText("Project"), {
+ target: { value: "" },
+ });
+ forceClick(screen.getByRole("button", { name: "Save connection" }));
+ fireEvent.change(screen.getByLabelText("Project"), {
+ target: { value: "p1" },
+ });
+ fireEvent.change(screen.getByLabelText("New connection (DSN)"), {
+ target: { value: " " },
+ });
+ forceClick(screen.getByRole("button", { name: "Save connection" }));
+ fireEvent.change(screen.getByLabelText("Connection"), {
+ target: { value: "" },
+ });
+ forceClick(
+ screen.getByRole("button", { name: "Reverse engineer → snapshot" }),
+ );
+ fireEvent.click(screen.getAllByRole("button", { name: "테이블 추가" })[0]!);
+ fireEvent.click(screen.getByTestId("add-cancel"));
+
+ fireEvent.click(screen.getByRole("button", { name: "대시보드" }));
+ fireEvent.click(
+ screen.getByRole("button", { name: /Billing.*다이어그램 보기/ }),
+ );
+ expect(
+ screen.getByRole("heading", { name: "다이어그램" }),
+ ).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "대시보드" }));
+ await waitFor(() =>
+ expect(
+ screen.getAllByRole("button", { name: "열기" }).length,
+ ).toBeGreaterThan(0),
+ );
+ fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!);
+ expect(
+ screen.getByRole("toolbar", { name: "ERD 캔버스 도구" }),
+ ).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "대시보드" }));
+ fireEvent.click(screen.getByRole("button", { name: "편집기로 이동" }));
+ expect(
+ screen.getByRole("toolbar", { name: "ERD 캔버스 도구" }),
+ ).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "대시보드" }));
+ fireEvent.click(screen.getByRole("button", { name: "새 모델링" }));
+ expect(
+ screen.getByRole("toolbar", { name: "ERD 캔버스 도구" }),
+ ).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole("button", { name: "프로젝트" }));
+ fireEvent.change(screen.getByLabelText("새 프로젝트 이름"), {
+ target: { value: "Roadmap" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: "새 프로젝트" }));
+ await waitFor(() =>
+ expect(api.createProject).toHaveBeenCalledWith("Roadmap"),
+ );
+ fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!);
+ expect(
+ screen.getByRole("heading", { name: "다이어그램" }),
+ ).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "대시보드" }));
+ fireEvent.click(screen.getByRole("button", { name: "목록 보기" }));
+ });
+
+ it("clears replacement copy timers and pending timers during close and unmount", async () => {
+ vi.useFakeTimers();
+ await act(async () => {
+ render();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ fireEvent.click(screen.getByRole("button", { name: "편집기" }));
+ fireEvent.click(screen.getAllByRole("button", { name: "테이블 추가" })[0]!);
+ fireEvent.click(screen.getByTestId("add-name"));
+ fireEvent.click(screen.getByTestId("add-submit"));
+ fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" }));
+ fireEvent.click(screen.getByTestId("export-copy-ddl"));
+ fireEvent.click(screen.getByTestId("export-copy-ddl"));
+ fireEvent.click(screen.getByTestId("share-create"));
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ fireEvent.click(screen.getByTestId("share-copy"));
+ await act(async () => {
+ await Promise.resolve();
+ });
+ fireEvent.click(screen.getByTestId("share-copy"));
await act(async () => {
- render()
- await Promise.resolve()
- await Promise.resolve()
- })
- fireEvent.click(screen.getByRole('button', { name: '편집기' }))
- fireEvent.click(screen.getAllByRole('button', { name: '테이블 추가' })[0]!)
- fireEvent.click(screen.getByTestId('add-name'))
- fireEvent.click(screen.getByTestId('add-submit'))
- fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' }))
- fireEvent.click(screen.getByTestId('export-copy-ddl'))
- fireEvent.click(screen.getByTestId('export-copy-ddl'))
- fireEvent.click(screen.getByTestId('share-create'))
+ await Promise.resolve();
+ });
await act(async () => {
- await Promise.resolve()
- await Promise.resolve()
- })
- fireEvent.click(screen.getByTestId('share-copy'))
- await act(async () => { await Promise.resolve() })
- fireEvent.click(screen.getByTestId('share-copy'))
- await act(async () => { await Promise.resolve() })
+ vi.advanceTimersByTime(2000);
+ await Promise.resolve();
+ });
+ fireEvent.click(screen.getByTestId("export-close"));
+
+ fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" }));
+ fireEvent.click(screen.getByTestId("export-copy-ddl"));
+ fireEvent.click(screen.getByTestId("share-create"));
await act(async () => {
- vi.advanceTimersByTime(2000)
- await Promise.resolve()
- })
- fireEvent.click(screen.getByTestId('export-close'))
-
- fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' }))
- fireEvent.click(screen.getByTestId('export-copy-ddl'))
- fireEvent.click(screen.getByTestId('share-create'))
- await act(async () => { await Promise.resolve() })
- fireEvent.click(screen.getByTestId('share-copy'))
- await act(async () => { await Promise.resolve() })
- fireEvent.click(screen.getByTestId('export-close'))
-
- fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' }))
- fireEvent.click(screen.getByTestId('export-copy-ddl'))
- fireEvent.click(screen.getByTestId('share-create'))
- await act(async () => { await Promise.resolve() })
- fireEvent.click(screen.getByTestId('share-copy'))
- await act(async () => { await Promise.resolve() })
- cleanup()
- })
-
- it('ignores authentication completions after unmount', async () => {
- let resolveMe!: (value: any) => void
- api.getMe.mockReturnValueOnce(new Promise((resolve) => { resolveMe = resolve }))
- render()
- cleanup()
- await act(async () => resolveMe({ subject: 'late', display_name: 'Late' }))
-
- let rejectMe!: (reason: unknown) => void
- api.getMe.mockReturnValueOnce(new Promise((_resolve, reject) => { rejectMe = reject }))
- render()
- cleanup()
- await act(async () => rejectMe(new Error('late failure')))
- })
-
- it('logs auto-layout failures and preserves nodes added after the undo snapshot', async () => {
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '다이어그램' }))
- const openButtons = await screen.findAllByRole('button', { name: '열기' })
- vi.useFakeTimers()
- fireEvent.click(openButtons[0]!)
+ await Promise.resolve();
+ });
+ fireEvent.click(screen.getByTestId("share-copy"));
await act(async () => {
- vi.advanceTimersByTime(1000)
- await Promise.resolve()
- await Promise.resolve()
- })
- vi.useRealTimers()
-
- const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
- vi.stubGlobal('requestAnimationFrame', () => { throw new Error('frame unavailable') })
- fireEvent.click(screen.getByRole('button', { name: 'ERD 자동 정렬' }))
- await waitFor(() => expect(screen.getByText('정렬에 실패했습니다. 다시 시도해 주세요.', { exact: false })).toBeInTheDocument())
- expect(consoleError).toHaveBeenCalled()
-
- vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { callback(0); return 1 })
- fireEvent.click(screen.getByRole('button', { name: 'ERD 자동 정렬' }))
- await screen.findByText('정렬 완료', { exact: false })
- fireEvent.click(screen.getAllByRole('button', { name: '테이블 추가' })[0]!)
- fireEvent.click(screen.getByTestId('add-name'))
- fireEvent.click(screen.getByTestId('add-submit'))
- fireEvent.click(screen.getByRole('button', { name: '정렬 되돌리기' }))
- expect(screen.getByTestId('node-count')).toHaveTextContent('3')
- })
-
- it('shows terminal refresh failures from the polling loop', async () => {
+ await Promise.resolve();
+ });
+ fireEvent.click(screen.getByTestId("export-close"));
+
+ fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" }));
+ fireEvent.click(screen.getByTestId("export-copy-ddl"));
+ fireEvent.click(screen.getByTestId("share-create"));
+ await act(async () => {
+ await Promise.resolve();
+ });
+ fireEvent.click(screen.getByTestId("share-copy"));
+ await act(async () => {
+ await Promise.resolve();
+ });
+ cleanup();
+ });
+
+ it("ignores authentication completions after unmount", async () => {
+ let resolveMe!: (value: any) => void;
+ api.getMe.mockReturnValueOnce(
+ new Promise((resolve) => {
+ resolveMe = resolve;
+ }),
+ );
+ render();
+ cleanup();
+ await act(async () => resolveMe({ subject: "late", display_name: "Late" }));
+
+ let rejectMe!: (reason: unknown) => void;
+ api.getMe.mockReturnValueOnce(
+ new Promise((_resolve, reject) => {
+ rejectMe = reject;
+ }),
+ );
+ render();
+ cleanup();
+ await act(async () => rejectMe(new Error("late failure")));
+ });
+
+ it("logs auto-layout failures and preserves nodes added after the undo snapshot", async () => {
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "다이어그램" }));
+ vi.useFakeTimers();
+ fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!);
+ await act(async () => {
+ vi.advanceTimersByTime(1000);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ vi.useRealTimers();
+
+ const consoleError = vi
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
+ vi.stubGlobal("requestAnimationFrame", () => {
+ throw new Error("frame unavailable");
+ });
+ fireEvent.click(screen.getByRole("button", { name: "ERD 자동 정렬" }));
+ await waitFor(() =>
+ expect(
+ screen.getByText("정렬에 실패했습니다. 다시 시도해 주세요.", {
+ exact: false,
+ }),
+ ).toBeInTheDocument(),
+ );
+ expect(consoleError).toHaveBeenCalled();
+
+ vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
+ callback(0);
+ return 1;
+ });
+ fireEvent.click(screen.getByRole("button", { name: "ERD 자동 정렬" }));
+ await screen.findByText("정렬 완료", { exact: false });
+ fireEvent.click(screen.getAllByRole("button", { name: "테이블 추가" })[0]!);
+ fireEvent.click(screen.getByTestId("add-name"));
+ fireEvent.click(screen.getByTestId("add-submit"));
+ fireEvent.click(screen.getByRole("button", { name: "정렬 되돌리기" }));
+ expect(screen.getByTestId("node-count")).toHaveTextContent("3");
+ });
+
+ it("shows terminal refresh failures from the polling loop", async () => {
api.listSnapshots
.mockResolvedValueOnce(snapshots)
- .mockRejectedValueOnce(new Error('terminal refresh down'))
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '다이어그램' }))
- const openButtons = await screen.findAllByRole('button', { name: '열기' })
- vi.useFakeTimers()
- fireEvent.click(openButtons[0]!)
+ .mockRejectedValueOnce(new Error("terminal refresh down"));
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "다이어그램" }));
+ vi.useFakeTimers();
+ fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!);
await act(async () => {
- vi.advanceTimersByTime(1000)
- await Promise.resolve()
- await Promise.resolve()
- })
- expect(screen.getByRole('alert')).toHaveTextContent('terminal refresh down')
- })
-
- it('ignores stale project metadata failures after changing projects', async () => {
- let rejectConnections!: (reason: unknown) => void
- let rejectSnapshots!: (reason: unknown) => void
+ vi.advanceTimersByTime(1000);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByRole("alert")).toHaveTextContent(
+ "terminal refresh down",
+ );
+ });
+
+ it("ignores stale project metadata failures after changing projects", async () => {
+ let rejectConnections!: (reason: unknown) => void;
+ let rejectSnapshots!: (reason: unknown) => void;
api.listConnections
- .mockReturnValueOnce(new Promise((_resolve, reject) => { rejectConnections = reject }))
- .mockResolvedValueOnce(connections)
+ .mockReturnValueOnce(
+ new Promise((_resolve, reject) => {
+ rejectConnections = reject;
+ }),
+ )
+ .mockResolvedValueOnce(connections);
api.listSnapshots
- .mockReturnValueOnce(new Promise((_resolve, reject) => { rejectSnapshots = reject }))
- .mockResolvedValueOnce(snapshots)
- await renderReadyApp()
-
- // Clear out initial errors before test acts
- await waitFor(() => expect(screen.queryByText(/stale/)).not.toBeInTheDocument())
-
- fireEvent.click(screen.getByRole('button', { name: '편집기' }))
- fireEvent.change(screen.getByLabelText('Project'), { target: { value: 'p2' } })
+ .mockReturnValueOnce(
+ new Promise((_resolve, reject) => {
+ rejectSnapshots = reject;
+ }),
+ )
+ .mockResolvedValueOnce(snapshots);
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "편집기" }));
+ fireEvent.change(screen.getByLabelText("Project"), {
+ target: { value: "p2" },
+ });
await act(async () => {
- // Catching these rejections inside the act stops the unhandled rejection in vitest
- rejectConnections(new Error('stale connections'))
- rejectSnapshots(new Error('stale snapshots'))
- await Promise.resolve()
- })
- expect(screen.queryByText(/stale (connections|snapshots)/)).not.toBeInTheDocument()
- })
-
- it('renders snapshot failures and polls without a selected project', async () => {
+ rejectConnections(new Error("stale connections"));
+ rejectSnapshots(new Error("stale snapshots"));
+ await Promise.resolve();
+ });
+ expect(
+ screen.queryByText(/stale (connections|snapshots)/),
+ ).not.toBeInTheDocument();
+ });
+
+ it("renders snapshot failures and polls without a selected project", async () => {
api.getSnapshot.mockResolvedValue({
- schema_snapshot_uuid: 's3',
- status: 'failed',
+ schema_snapshot_uuid: "s3",
+ status: "failed",
schema_filter: null,
- error_message: 'database rejected snapshot',
+ error_message: "database rejected snapshot",
snapshot_json: null,
- })
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '편집기' }))
- fireEvent.change(screen.getByLabelText('Connection DSN'), { target: { value: buildTestDsn('postgresql', 'db.example', 'test') } })
- fireEvent.click(screen.getByRole('button', { name: 'Save connection' }))
- await waitFor(() => expect(api.createConnection).toHaveBeenCalled())
- fireEvent.click(screen.getByRole('button', { name: 'Reverse engineer → snapshot' }))
- await waitFor(() => expect(api.createSnapshot).toHaveBeenCalledWith('p1', 'c2', undefined))
-
- vi.useFakeTimers()
- fireEvent.change(screen.getByLabelText('Project'), { target: { value: '' } })
+ });
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "편집기" }));
+ fireEvent.change(screen.getByLabelText("Connection DSN"), {
+ target: { value: "postgresql://db.example/test" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: "Save connection" }));
+ await waitFor(() => expect(api.createConnection).toHaveBeenCalled());
+ fireEvent.click(
+ screen.getByRole("button", { name: "Reverse engineer → snapshot" }),
+ );
+ await waitFor(() =>
+ expect(api.createSnapshot).toHaveBeenCalledWith("p1", "c2", undefined),
+ );
+
+ vi.useFakeTimers();
+ fireEvent.change(screen.getByLabelText("Project"), {
+ target: { value: "" },
+ });
await act(async () => {
- vi.advanceTimersByTime(1000)
- await Promise.resolve()
- await Promise.resolve()
- })
- expect(screen.getByRole('alert')).toHaveTextContent('database rejected snapshot')
- })
-
- it('renders user identity fallbacks and a diagram list without a project label', async () => {
- api.getMe.mockResolvedValueOnce({ subject: 'subject-only', display_name: null })
- await renderReadyApp()
- expect(screen.getByText('subject-only')).toBeInTheDocument()
-
- cleanup()
- api.getMe.mockResolvedValueOnce({ subject: '', display_name: null })
- await renderReadyApp()
- expect(screen.getByText('인증 필요')).toBeInTheDocument()
-
- cleanup()
- const onOpenEditor = vi.fn()
+ vi.advanceTimersByTime(1000);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByRole("alert")).toHaveTextContent(
+ "database rejected snapshot",
+ );
+ });
+
+ it("renders user identity fallbacks and a diagram list without a project label", async () => {
+ api.getMe.mockResolvedValueOnce({
+ subject: "subject-only",
+ display_name: null,
+ });
+ await renderReadyApp();
+ expect(screen.getByText("subject-only")).toBeInTheDocument();
+
+ cleanup();
+ api.getMe.mockResolvedValueOnce({ subject: "", display_name: null });
+ await renderReadyApp();
+ expect(screen.getByText("인증 필요")).toBeInTheDocument();
+
+ cleanup();
+ const onOpenEditor = vi.fn();
render(
,
- )
- expect(screen.getAllByText('현재 프로젝트')).toHaveLength(2)
- fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!)
- expect(onOpenEditor).toHaveBeenCalledWith('s1')
- })
-
- it('ignores duplicate share creation while a request is pending', async () => {
- let resolveShare!: (value: { url: string }) => void
- api.createShareLink.mockReturnValueOnce(new Promise((resolve) => { resolveShare = resolve }))
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '편집기' }))
- fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' }))
- fireEvent.click(screen.getByTestId('share-create'))
- fireEvent.click(screen.getByTestId('share-create'))
- expect(api.createShareLink).toHaveBeenCalledTimes(1)
- await act(async () => resolveShare({ url: 'http://localhost/api/share/done' }))
- })
-
- it('preserves positions across graph refresh and applies recommendations with sibling nodes', async () => {
- let pollCount = 0
+ );
+ expect(screen.getAllByText("현재 프로젝트")).toHaveLength(2);
+ fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!);
+ expect(onOpenEditor).toHaveBeenCalledWith("s1");
+ });
+
+ it("ignores duplicate share creation while a request is pending", async () => {
+ let resolveShare!: (value: { url: string }) => void;
+ api.createShareLink.mockReturnValueOnce(
+ new Promise((resolve) => {
+ resolveShare = resolve;
+ }),
+ );
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "편집기" }));
+ fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" }));
+ fireEvent.click(screen.getByTestId("share-create"));
+ fireEvent.click(screen.getByTestId("share-create"));
+ expect(api.createShareLink).toHaveBeenCalledTimes(1);
+ await act(async () =>
+ resolveShare({ url: "http://localhost/api/share/done" }),
+ );
+ });
+
+ it("preserves positions across graph refresh and applies recommendations with sibling nodes", async () => {
+ let pollCount = 0;
api.getSnapshot.mockImplementation(async () => ({
- schema_snapshot_uuid: 's3',
- status: pollCount++ === 0 ? 'running' : 'succeeded',
- schema_filter: 'public',
+ schema_snapshot_uuid: "s3",
+ status: pollCount++ === 0 ? "running" : "succeeded",
+ schema_filter: "public",
error_message: null,
- snapshot_json: { relations: [], columns: [], pk_columns: [], fk_edges: [] },
- }))
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '전체 보기' }))
- fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!)
- vi.useFakeTimers()
- fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!)
+ snapshot_json: {
+ relations: [],
+ columns: [],
+ pk_columns: [],
+ fk_edges: [],
+ },
+ }));
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "다이어그램" }));
+ vi.useFakeTimers();
+ fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!);
await act(async () => {
- vi.advanceTimersByTime(1000)
- await Promise.resolve()
- await Promise.resolve()
- })
- expect(screen.getByTestId('node-count')).toHaveTextContent('2')
+ vi.advanceTimersByTime(1000);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByTestId("node-count")).toHaveTextContent("2");
await act(async () => {
- vi.advanceTimersByTime(1000)
- await Promise.resolve()
- await Promise.resolve()
- })
- vi.useRealTimers()
- expect(screen.getByTestId('node-count')).toHaveTextContent('2')
-
- fireEvent.click(screen.getByRole('button', { name: '업무 그룹' }))
- fireEvent.click(screen.getByTestId('group-name'))
- fireEvent.click(screen.getByTestId('group-create'))
- fireEvent.click(screen.getByTestId('group-assign'))
- vi.spyOn(window, 'confirm').mockReturnValue(true)
- fireEvent.click(screen.getByTestId('group-delete'))
- fireEvent.click(screen.getByTestId('group-close'))
-
- fireEvent.click(screen.getByRole('button', { name: '인덱스 카디널리티 계산' }))
- fireEvent.click(screen.getByTestId('card-table'))
- fireEvent.click(screen.getByTestId('card-apply'))
- fireEvent.click(screen.getByTestId('card-clear-apply'))
- })
-
- it('falls back to node ids when auto-layout receives legacy nodes without titles', async () => {
+ vi.advanceTimersByTime(1000);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ vi.useRealTimers();
+ expect(screen.getByTestId("node-count")).toHaveTextContent("2");
+
+ fireEvent.click(screen.getByRole("button", { name: "업무 그룹" }));
+ fireEvent.click(screen.getByTestId("group-name"));
+ fireEvent.click(screen.getByTestId("group-create"));
+ fireEvent.click(screen.getByTestId("group-assign"));
+ vi.spyOn(window, "confirm").mockReturnValue(true);
+ fireEvent.click(screen.getByTestId("group-delete"));
+ fireEvent.click(screen.getByTestId("group-close"));
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "인덱스 카디널리티 계산" }),
+ );
+ fireEvent.click(screen.getByTestId("card-table"));
+ fireEvent.click(screen.getByTestId("card-apply"));
+ fireEvent.click(screen.getByTestId("card-clear-apply"));
+ });
+
+ it("falls back to node ids when auto-layout receives legacy nodes without titles", async () => {
vi.mocked(snapshotToGraph).mockReturnValueOnce({
nodes: [
- { id: 'z-node', type: 'tableNode', position: { x: 0, y: 0 }, data: { columns: [], badges: { pk: false, fk: false } } },
- { id: 'a-node', type: 'tableNode', position: { x: 1, y: 1 }, data: { columns: [], badges: { pk: false, fk: false } } },
+ {
+ id: "z-node",
+ type: "tableNode",
+ position: { x: 0, y: 0 },
+ data: { columns: [], badges: { pk: false, fk: false } },
+ },
+ {
+ id: "a-node",
+ type: "tableNode",
+ position: { x: 1, y: 1 },
+ data: { columns: [], badges: { pk: false, fk: false } },
+ },
] as any,
edges: [],
- })
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '다이어그램' }))
- vi.useFakeTimers()
- fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!)
+ });
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "다이어그램" }));
+ vi.useFakeTimers();
+ fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!);
await act(async () => {
- vi.advanceTimersByTime(1000)
- await Promise.resolve()
- await Promise.resolve()
- })
- vi.useRealTimers()
- fireEvent.click(screen.getByRole('button', { name: 'ERD 자동 정렬' }))
- await screen.findByText('정렬 완료', { exact: false })
- })
-
- it('reports API effect failures, snapshot polling failures, share failures, and clipboard failures', async () => {
- api.listConnections.mockRejectedValueOnce(new Error('connections down'))
- api.listSnapshots.mockRejectedValueOnce(new Error('snapshots down'))
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '편집기' }))
- expect(await screen.findByRole('alert')).toHaveTextContent(/down/)
- fireEvent.click(screen.getAllByRole('button', { name: '테이블 추가' })[0]!)
- fireEvent.click(screen.getByTestId('add-name'))
- fireEvent.click(screen.getByTestId('add-submit'))
- fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' }))
- api.createShareLink.mockRejectedValueOnce(new Error('share down'))
- fireEvent.click(screen.getByTestId('share-create'))
- await waitFor(() => expect(screen.getByTestId('share-error')).toHaveTextContent('share down'))
-
- api.createShareLink.mockResolvedValueOnce({ url: 'http://localhost/api/share/fail-copy' })
- fireEvent.click(screen.getByTestId('share-create'))
- await waitFor(() => expect(screen.getByTestId('share-url')).toHaveTextContent('fail-copy'))
- vi.mocked(navigator.clipboard.writeText).mockRejectedValueOnce(new Error('copy down'))
- fireEvent.click(screen.getByTestId('share-copy'))
- await waitFor(() => expect(screen.getByTestId('share-error')).toHaveTextContent('복사에 실패'))
-
- cleanup()
- vi.useRealTimers()
- api.listConnections.mockResolvedValue(connections)
- api.listSnapshots.mockResolvedValue(snapshots)
- await renderReadyApp()
- fireEvent.click(screen.getByRole('button', { name: '다이어그램' }))
- await waitFor(() => expect(screen.getAllByRole('button', { name: '열기' }).length).toBeGreaterThan(0))
- vi.useFakeTimers()
- api.getSnapshot.mockRejectedValueOnce(new Error('poll down'))
- fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!)
+ vi.advanceTimersByTime(1000);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ vi.useRealTimers();
+ fireEvent.click(screen.getByRole("button", { name: "ERD 자동 정렬" }));
+ await screen.findByText("정렬 완료", { exact: false });
+ });
+
+ it("reports API effect failures, snapshot polling failures, share failures, and clipboard failures", async () => {
+ api.listConnections.mockRejectedValueOnce(new Error("connections down"));
+ api.listSnapshots.mockRejectedValueOnce(new Error("snapshots down"));
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "편집기" }));
+ expect(await screen.findByRole("alert")).toHaveTextContent(/down/);
+ fireEvent.click(screen.getAllByRole("button", { name: "테이블 추가" })[0]!);
+ fireEvent.click(screen.getByTestId("add-name"));
+ fireEvent.click(screen.getByTestId("add-submit"));
+ fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" }));
+ api.createShareLink.mockRejectedValueOnce(new Error("share down"));
+ fireEvent.click(screen.getByTestId("share-create"));
+ await waitFor(() =>
+ expect(screen.getByTestId("share-error")).toHaveTextContent("share down"),
+ );
+
+ api.createShareLink.mockResolvedValueOnce({
+ url: "http://localhost/api/share/fail-copy",
+ });
+ fireEvent.click(screen.getByTestId("share-create"));
+ await waitFor(() =>
+ expect(screen.getByTestId("share-url")).toHaveTextContent("fail-copy"),
+ );
+ vi.mocked(navigator.clipboard.writeText).mockRejectedValueOnce(
+ new Error("copy down"),
+ );
+ fireEvent.click(screen.getByTestId("share-copy"));
+ await waitFor(() =>
+ expect(screen.getByTestId("share-error")).toHaveTextContent(
+ "복사에 실패",
+ ),
+ );
+
+ cleanup();
+ vi.useRealTimers();
+ api.listConnections.mockResolvedValue(connections);
+ api.listSnapshots.mockResolvedValue(snapshots);
+ await renderReadyApp();
+ fireEvent.click(screen.getByRole("button", { name: "다이어그램" }));
+ await waitFor(() =>
+ expect(
+ screen.getAllByRole("button", { name: "열기" }).length,
+ ).toBeGreaterThan(0),
+ );
+ vi.useFakeTimers();
+ api.getSnapshot.mockRejectedValueOnce(new Error("poll down"));
+ fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!);
await act(async () => {
- vi.advanceTimersByTime(1000)
- await Promise.resolve()
- })
- expect(screen.getByRole('alert')).toHaveTextContent('poll down')
- })
-})
+ vi.advanceTimersByTime(1000);
+ await Promise.resolve();
+ });
+ expect(screen.getByRole("alert")).toHaveTextContent("poll down");
+ });
+});
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 549d757da..278282dcd 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -72,7 +72,16 @@ const TERMINAL_SNAPSHOT_STATUSES = new Set([
"not_found",
]);
-const SUPPORTED_DSN_PROTOCOLS = new Set(["postgres:", "postgresql:", "snowflake:"]);
+const SUPPORTED_DSN_PROTOCOLS = new Set([
+ "postgres:",
+ "postgresql:",
+ "snowflake:",
+]);
+
+function sanitizeCssClass(str: unknown): string {
+ if (typeof str !== "string") return "";
+ return str.replace(/[^a-zA-Z0-9-_]/g, "");
+}
function sanitizeHtml(str: string | null | undefined): string {
if (!str) return "";
@@ -84,11 +93,6 @@ function sanitizeHtml(str: string | null | undefined): string {
.replace(/'/g, "'");
}
-function sanitizeCssClass(str: unknown): string {
- if (typeof str !== "string") return "";
- return str.replace(/[^a-zA-Z0-9-_]/g, "");
-}
-
type CurrentUser = {
subject: string;
display_name: string | null;
@@ -173,7 +177,9 @@ export default function App() {
const [shareLinkError, setShareLinkError] = useState(null);
const [editingEdge, setEditingEdge] = useState(null);
- const [editingNode, setEditingNode] = useState | null>(null);
+ const [editingNode, setEditingNode] = useState | null>(
+ null,
+ );
const [isEditTableModalOpen, setIsEditTableModalOpen] = useState(false);
const [isAddTableModalOpen, setIsAddTableModalOpen] = useState(false);
const [newTableName, setNewTableName] = useState("");
@@ -318,7 +324,11 @@ export default function App() {
getSnapshot(snapshotId)
.then((s) => {
setSnapshot(s);
- if (s.status === "succeeded" || s.status === "failed" || s.status === "not_found") {
+ if (
+ s.status === "succeeded" ||
+ s.status === "failed" ||
+ s.status === "not_found"
+ ) {
clearInterval(timer);
if (selectedProjectId) {
listSnapshots(selectedProjectId)
@@ -372,9 +382,7 @@ export default function App() {
// ⚡ Bolt: Removed nodesById Map creation inside useMemo which iterates over all nodes and allocates memory.
// Using nodes.find() for single lookups is O(N) but avoids Map construction overhead, providing ~10x speedup and reducing GC pressure.
const cardinalityNode = useMemo(() => {
- return (
- nodes.find((n) => n.id === cardinalityTableId) ?? nodes[0] ?? null
- );
+ return nodes.find((n) => n.id === cardinalityTableId) ?? nodes[0] ?? null;
}, [cardinalityTableId, nodes]);
const cardinalityColumns = useMemo(() => {
if (!cardinalityNode) return [];
@@ -385,11 +393,7 @@ export default function App() {
cardinalityDistinctCounts[column.column_name] ?? "",
),
}));
- }, [
- cardinalityColumnSelections,
- cardinalityDistinctCounts,
- cardinalityNode,
- ]);
+ }, [cardinalityColumnSelections, cardinalityDistinctCounts, cardinalityNode]);
const cardinalityRecommendations = useMemo(
() =>
buildIndexRecommendations({
@@ -397,7 +401,11 @@ export default function App() {
rowCount: cardinalityRowCountNumber,
columns: cardinalityColumns,
}),
- [cardinalityColumns, cardinalityNode?.data.title, cardinalityRowCountNumber],
+ [
+ cardinalityColumns,
+ cardinalityNode?.data.title,
+ cardinalityRowCountNumber,
+ ],
);
const appliedCardinalityIndexes = useMemo(
() => cardinalityNode?.data.indexes ?? [],
@@ -409,7 +417,8 @@ export default function App() {
const columns = new Set();
for (const index of appliedCardinalityIndexes) {
if (index.index_name) names.add(index.index_name);
- if (index.columns && index.columns.length > 0) columns.add(index.columns.join(","));
+ if (index.columns && index.columns.length > 0)
+ columns.add(index.columns.join(","));
}
return { names, columns };
}, [appliedCardinalityIndexes]);
@@ -508,11 +517,14 @@ export default function App() {
}
}
- const onNodeDoubleClick = useCallback((event: React.MouseEvent, node: Node) => {
- event.preventDefault();
- setEditingNode(node as Node);
- setIsEditTableModalOpen(true);
- }, []);
+ const onNodeDoubleClick = useCallback(
+ (event: React.MouseEvent, node: Node) => {
+ event.preventDefault();
+ setEditingNode(node as Node);
+ setIsEditTableModalOpen(true);
+ },
+ [],
+ );
const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {
event.preventDefault();
@@ -627,7 +639,11 @@ export default function App() {
}
function onDownloadMermaid() {
- downloadText("pg-erd-diagram.mermaid", exportMermaid(nodes, edges), "text/plain");
+ downloadText(
+ "pg-erd-diagram.mermaid",
+ exportMermaid(nodes, edges),
+ "text/plain",
+ );
}
function onDownloadDbml() {
@@ -635,7 +651,11 @@ export default function App() {
}
function onDownloadPrisma() {
- downloadText("pg-erd-diagram.prisma", exportPrisma(nodes, edges), "text/plain");
+ downloadText(
+ "pg-erd-diagram.prisma",
+ exportPrisma(nodes, edges),
+ "text/plain",
+ );
}
function onExportDictionaryCsv() {
@@ -701,10 +721,7 @@ export default function App() {
}));
}
- function onCardinalityDistinctCountChange(
- columnName: string,
- value: string,
- ) {
+ function onCardinalityDistinctCountChange(columnName: string, value: string) {
if (!/^\d*$/.test(value)) return;
setCardinalityDistinctCounts((prev) => ({
...prev,
@@ -734,7 +751,8 @@ export default function App() {
const recColumns = recommendation.columns?.join(",") ?? "";
if (
- (recommendation.index_name && appliedIndexNames.has(recommendation.index_name)) ||
+ (recommendation.index_name &&
+ appliedIndexNames.has(recommendation.index_name)) ||
(recColumns && appliedColumns.has(recColumns))
) {
return currentNodes;
@@ -811,7 +829,12 @@ export default function App() {
}
function onDeleteBusinessGroup(groupId: string) {
- if (!window.confirm("이 그룹을 삭제하면 포함된 모든 테이블에서 그룹 지정이 해제됩니다. 정말로 삭제하시겠습니까?")) return;
+ if (
+ !window.confirm(
+ "이 그룹을 삭제하면 포함된 모든 테이블에서 그룹 지정이 해제됩니다. 정말로 삭제하시겠습니까?",
+ )
+ )
+ return;
setBusinessGroups((groups) =>
groups.filter((group) => group.id !== groupId),
);
@@ -830,7 +853,6 @@ export default function App() {
);
}
-
function onDeleteTable() {
if (!editingNode) return;
if (!window.confirm("정말로 이 테이블을 삭제하시겠습니까?")) return;
@@ -839,7 +861,11 @@ export default function App() {
setNodes((nds) => nds.filter((n) => n.id !== editingNode.id));
// Remove connected edges
- setEdges((eds) => eds.filter((e) => e.source !== editingNode.id && e.target !== editingNode.id));
+ setEdges((eds) =>
+ eds.filter(
+ (e) => e.source !== editingNode.id && e.target !== editingNode.id,
+ ),
+ );
setIsEditTableModalOpen(false);
setEditingNode(null);
@@ -856,7 +882,9 @@ export default function App() {
if (!title.trim()) return;
// Parse columns from formData
- const updatedColumns: Array["data"]["columns"][number]> = [];
+ const updatedColumns: Array<
+ Node["data"]["columns"][number]
+ > = [];
for (let i = 0; i < editingNode.data.columns.length; i++) {
const colName = formData.get(`col_name_${i}`) as string;
if (colName === null) continue; // Deleted column
@@ -886,13 +914,13 @@ export default function App() {
columns: updatedColumns,
badges: {
...n.data.badges,
- pk: updatedColumns.some(c => c.is_pk)
- }
- }
+ pk: updatedColumns.some((c) => c.is_pk),
+ },
+ },
};
}
return n;
- })
+ }),
);
setIsEditTableModalOpen(false);
@@ -904,7 +932,6 @@ export default function App() {
setEditingNode(null);
}
-
function onAddTableSubmit() {
if (!newTableName.trim()) return;
const newId = `new_table_${Date.now()}`;
@@ -969,7 +996,9 @@ export default function App() {
/* v8 ignore next -- the save control is disabled until both fields are present */
if (!nextConnectionName || !connectionDsn) return;
if (!isSupportedConnectionDsn(connectionDsn)) {
- setError("Connection DSN must use postgresql://, postgres://, or snowflake:// with a host.");
+ setError(
+ "Connection DSN must use postgresql://, postgres://, or snowflake:// with a host.",
+ );
dsnInput.value = "";
setIsDsnPresent(false);
return;
@@ -1011,12 +1040,7 @@ export default function App() {
if (isAuthLoading) {
return (
-
+
pg-erd-cloud
Authenticating…
@@ -1025,7 +1049,8 @@ export default function App() {
if (!me) {
/* v8 ignore next -- the loaded unauthenticated state always records its rejection */
- const authGateMessage = authError ?? "Sign in before managing database metadata.";
+ const authGateMessage =
+ authError ?? "Sign in before managing database metadata.";
return (
Authentication required
@@ -1050,7 +1075,11 @@ export default function App() {
setActiveView(item.id)}
aria-current={activeView === item.id ? "page" : undefined}
>
@@ -1061,156 +1090,175 @@ export default function App() {
{activeView === "editor" ? (
<>
+
+
+
+
+
+
-
-
-
-
+ {connections.map((c) => (
+
+ {c.conn_name}
+
+ ))}
+
+
+
+
+
+
+
+ setSchemaFilter(e.target.value)}
+ placeholder="public"
+ />
+
-
-
-
- {createProjectHint ? (
-
- {createProjectHint}
-
- ) : null}
-
-
-
-
-
-
-
-
-
-
-
-
-
- setSchemaFilter(e.target.value)}
- placeholder="public"
- />
-
-
-
- {isCreatingSnapshot ? "Starting…" : "Reverse engineer → snapshot"}
-
- {createSnapshotHint ? (
-
- {createSnapshotHint}
-
- ) : null}
-
-
- Snapshot: {snapshot?.status || "—"}
- {snapshot?.error_message ? (
-
- {String(snapshot.error_message)}
+ {createSnapshotHint ? (
+
+ {createSnapshotHint}
+
+ ) : null}
+
+
+ Snapshot: {snapshot?.status || "—"}
+ {snapshot?.error_message ? (
+
+ {String(snapshot.error_message)}
+
+ ) : null}
- ) : null}
-
- {error ? (
-
- {error}
-
- ) : null}
+ {error ? (
+
+ {error}
+
+ ) : null}
>
) : (
@@ -1231,7 +1279,10 @@ export default function App() {
{activeView === "dashboard" ? (
-
+
대시보드
@@ -1257,7 +1308,10 @@ export default function App() {
-
+
최근 프로젝트
setActiveView("projects")}>
@@ -1283,11 +1337,16 @@ export default function App() {
))}
) : (
- 아직 프로젝트가 없습니다. 편집기에서 프로젝트를 생성하세요.
+
+ 아직 프로젝트가 없습니다. 편집기에서 프로젝트를 생성하세요.
+
)}
-
+
최근 다이어그램
setActiveView("diagrams")}>
@@ -1296,7 +1355,9 @@ export default function App() {
{
setSnapshotId(id);
setSnapshot(null);
@@ -1310,13 +1371,23 @@ export default function App() {
프로젝트
-
프로젝트를 선택하면 해당 다이어그램 목록을 볼 수 있습니다.
+
+ 프로젝트를 선택하면 해당 다이어그램 목록을 볼 수 있습니다.
+
-
-
+
이름
연결
동작
{projects.map((project) => (
-
-
{sanitizeHtml(project.project_name)}
-
{project.project_space_uuid === selectedProjectId ? connections.length : "선택 후 표시"}
+
+
+ {sanitizeHtml(project.project_name)}
+
+
+ {project.project_space_uuid === selectedProjectId
+ ? connections.length
+ : "선택 후 표시"}
+
))}
{!projects.length ? (
- 프로젝트가 없습니다. 이름을 입력해 새 프로젝트를 만드세요.
+
+ 프로젝트가 없습니다. 이름을 입력해 새 프로젝트를 만드세요.
+
) : null}
@@ -1359,7 +1445,11 @@ export default function App() {
다이어그램
-
{selectedProject ? `${selectedProject.project_name} 프로젝트의 스냅샷` : "프로젝트를 선택하세요."}
+
+ {selectedProject
+ ? `${selectedProject.project_name} 프로젝트의 스냅샷`
+ : "프로젝트를 선택하세요."}
+
setActiveView("editor")}>
편집기 열기
@@ -1373,7 +1463,9 @@ export default function App() {
placeholder="다이어그램 검색"
type="search"
value={diagramSearch}
- onChange={(event) => setDiagramSearch(event.currentTarget.value)}
+ onChange={(event) =>
+ setDiagramSearch(event.currentTarget.value)
+ }
/>
@@ -1390,306 +1482,324 @@ export default function App() {
) : (
-
-
-
- {isLayouting ? "…" : "↔"}
-
-
- ↶
-
-
- 🪄
-
-
- 🗑️
-
-
- +
-
-
- ◇
-
-
- #
-
-
- SQL
-
-
- ↗
-
-
e.preventDefault()}>
+
+
+
+ {isLayouting ? "…" : "↔"}
+
+
+ ↶
+
+
+ 🪄
+
+
+ 🗑️
+
+
+ +
+
+
+ ◇
+
+
+ #
+
+
+ SQL
+
+
+ ↗
+
+
+ IMG
+
+
+ UML
+
+
+ {"{}"}
+
+
+ {[layoutMessage, nodeSearchStatus].filter(Boolean).join(" ")}
+
+
+
+
{
+ reactFlowRef.current = instance;
+ }}
>
- IMG
-
-
+
+
+
+
+ {nodes.length === 0 && (
+
+ {isSnapshotPending ? (
+ <>
+
+
스냅샷 생성 중...
+
+ 데이터베이스에서 스키마를 가져오고 있습니다. 잠시만
+ 기다려주세요.
+
+ >
+ ) : (
+ <>
+
+
+ ERD 캔버스가 비어 있습니다
+
+
+ 작업 패널에서 스냅샷을 생성하거나 상단의{" "}
+ 테이블 추가 버튼을 눌러 시작하세요.
+
+
+ + 테이블 추가
+
+ >
+ )}
+
+ )}
+
+
0}
+ hasDictionaryExport={nodes.length > 0}
+ hasDiagramExport={nodes.length > 0}
+ shareLinkUrl={shareLinkUrl}
+ isCreatingShareLink={isCreatingShareLink}
+ isShareLinkCopied={isShareLinkCopied}
+ shareLinkError={shareLinkError}
+ canCreateShareLink={Boolean(selectedProjectId)}
+ onCloseExport={onCloseExport}
+ onCopyExportDdl={onCopyExportDdl}
+ onDownloadSvg={onDownloadSvg}
+ onDownloadUml={onDownloadUml}
+ onDownloadMermaid={onDownloadMermaid}
+ onExportDictionaryCsv={onExportDictionaryCsv}
+ onExportDictionaryMarkdown={onExportDictionaryMarkdown}
+ onDownloadDbml={onDownloadDbml}
+ onDownloadPrisma={onDownloadPrisma}
+ onCreateShareLink={onCreateShareLink}
+ onCopyShareLink={onCopyShareLink}
+ />
+
+
+
+
+
+
- UML
-
-
- {"{}"}
-
-
- {[layoutMessage, nodeSearchStatus].filter(Boolean).join(" ")}
-
-
+ parsePositiveInteger={parsePositiveInteger}
+ calculateCardinalityRatio={calculateCardinalityRatio}
+ formatPercent={formatPercent}
+ strengthLabel={strengthLabel}
+ />
- {
- reactFlowRef.current = instance;
- }}
- >
-
-
-
-
-
- {nodes.length === 0 && (
-
- {isSnapshotPending ? (
- <>
-
-
스냅샷 생성 중...
-
- 데이터베이스에서 스키마를 가져오고 있습니다. 잠시만 기다려주세요.
-
- >
- ) : (
- <>
-
-
ERD 캔버스가 비어 있습니다
-
- 작업 패널에서 스냅샷을 생성하거나 상단의 테이블 추가 버튼을 눌러 시작하세요.
-
-
- + 테이블 추가
-
- >
- )}
-
- )}
-
- 0}
- hasDictionaryExport={nodes.length > 0}
- hasDiagramExport={nodes.length > 0}
- shareLinkUrl={shareLinkUrl}
- isCreatingShareLink={isCreatingShareLink}
- isShareLinkCopied={isShareLinkCopied}
- shareLinkError={shareLinkError}
- canCreateShareLink={Boolean(selectedProjectId)}
- onCloseExport={onCloseExport}
- onCopyExportDdl={onCopyExportDdl}
- onDownloadSvg={onDownloadSvg}
- onDownloadUml={onDownloadUml}
- onDownloadMermaid={onDownloadMermaid}
- onExportDictionaryCsv={onExportDictionaryCsv}
- onExportDictionaryMarkdown={onExportDictionaryMarkdown}
- onDownloadDbml={onDownloadDbml}
- onDownloadPrisma={onDownloadPrisma}
- onCreateShareLink={onCreateShareLink}
- onCopyShareLink={onCopyShareLink}
- />
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
)}
@@ -1740,11 +1850,17 @@ export function DiagramTable({
동작
{rows.map(({ item, name }) => (
-
+
{name}
{selectedProjectName || "현재 프로젝트"}
-
+
{sanitizeHtml(item.status)}
From b48be1f8df946e400fad574086654a2a4aa5e29f Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 5 Aug 2026 02:54:19 +0000
Subject: [PATCH 41/43] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20form=20wr?=
=?UTF-8?q?appers=20for=20native=20keyboard=20submission?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/package-lock.json | 22 ++++++++++++++++------
frontend/package.json | 3 ++-
2 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 5bb22defc..96e871fdd 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10,7 +10,8 @@
"dependencies": {
"@xyflow/react": "^12.11.1",
"react": "^19.2.4",
- "react-dom": "^19.2.7"
+ "react-dom": "^19.2.7",
+ "undici": "^8.10.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
@@ -1605,6 +1606,16 @@
}
}
},
+ "node_modules/jsdom/node_modules/undici": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -2383,13 +2394,12 @@
}
},
"node_modules/undici": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
- "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
- "dev": true,
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz",
+ "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==",
"license": "MIT",
"engines": {
- "node": ">=20.18.1"
+ "node": ">=22.19.0"
}
},
"node_modules/use-sync-external-store": {
diff --git a/frontend/package.json b/frontend/package.json
index adbea9fd0..a4f89cd14 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -19,7 +19,8 @@
"dependencies": {
"@xyflow/react": "^12.11.1",
"react": "^19.2.4",
- "react-dom": "^19.2.7"
+ "react-dom": "^19.2.7",
+ "undici": "^8.10.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
From 19bab91c2d72e1f43fea99f96636ea4fcd64cab7 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 5 Aug 2026 03:50:01 +0000
Subject: [PATCH 42/43] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20form=20wr?=
=?UTF-8?q?appers=20for=20native=20keyboard=20submission?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/package.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/package.json b/frontend/package.json
index a4f89cd14..ba14cd262 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -19,8 +19,7 @@
"dependencies": {
"@xyflow/react": "^12.11.1",
"react": "^19.2.4",
- "react-dom": "^19.2.7",
- "undici": "^8.10.0"
+ "react-dom": "^19.2.7"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
@@ -33,7 +32,8 @@
"jsdom": "^29.1.1",
"typescript": "^6.0.3",
"vite": "^8.1.0",
- "vitest": "^4.1.9"
+ "vitest": "^4.1.9",
+ "undici": "^8.10.0"
},
"overrides": {
"esbuild": "^0.25.0",
From cf597e69a255d0497b1f233baeaa9b47422bd412 Mon Sep 17 00:00:00 2001
From: seonghobae <8172694+seonghobae@users.noreply.github.com>
Date: Wed, 5 Aug 2026 12:32:58 +0000
Subject: [PATCH 43/43] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20form=20wr?=
=?UTF-8?q?appers=20for=20native=20keyboard=20submission?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit