mirror of
https://github.com/chartdb/chartdb.git
synced 2025-10-30 11:33:58 +00:00
Compare commits
29 Commits
jf/prevent
...
jf/add_dup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76f9662b80 | ||
|
|
ec3719ebce | ||
|
|
0a5874a69b | ||
|
|
7e0fdd1595 | ||
|
|
2531a7023f | ||
|
|
73daf0df21 | ||
|
|
c77c983989 | ||
|
|
0aaa451479 | ||
|
|
b697e26170 | ||
|
|
04d91c67b1 | ||
|
|
d0dee84970 | ||
|
|
b4ccfcdcde | ||
|
|
1759b0b9f2 | ||
|
|
ab4845c772 | ||
|
|
0545b41140 | ||
|
|
4520f8b1f7 | ||
|
|
712bdf5b95 | ||
|
|
d7c9536272 | ||
|
|
815a52f192 | ||
|
|
f1a4298362 | ||
|
|
b8f2141bd2 | ||
|
|
eaebe34768 | ||
|
|
0d623a86b1 | ||
|
|
19fd94c6bd | ||
|
|
0da3caeeac | ||
|
|
cb2ba66233 | ||
|
|
8a2267281b | ||
|
|
41ba251377 | ||
|
|
e9c5442d9d |
13
index.html
13
index.html
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="robots" content="max-image-preview:large" />
|
<meta name="robots" content="noindex, max-image-preview:large" />
|
||||||
<title>ChartDB - Create & Visualize Database Schema Diagrams</title>
|
<title>ChartDB - Create & Visualize Database Schema Diagrams</title>
|
||||||
<link rel="canonical" href="https://chartdb.io" />
|
<link rel="canonical" href="https://chartdb.io" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
@@ -16,14 +16,19 @@
|
|||||||
<script src="/config.js"></script>
|
<script src="/config.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Load analytics only if not disabled
|
// Load analytics only if not disabled
|
||||||
(function() {
|
(function () {
|
||||||
const disableAnalytics = (window.env && window.env.DISABLE_ANALYTICS === 'true') ||
|
const disableAnalytics =
|
||||||
(typeof process !== 'undefined' && process.env && process.env.VITE_DISABLE_ANALYTICS === 'true');
|
(window.env && window.env.DISABLE_ANALYTICS === 'true') ||
|
||||||
|
(typeof process !== 'undefined' &&
|
||||||
|
process.env &&
|
||||||
|
process.env.VITE_DISABLE_ANALYTICS === 'true');
|
||||||
|
|
||||||
if (!disableAnalytics) {
|
if (!disableAnalytics) {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = 'https://cdn.usefathom.com/script.js';
|
script.src = 'https://cdn.usefathom.com/script.js';
|
||||||
script.setAttribute('data-site', 'PRHIVBNN');
|
script.setAttribute('data-site', 'PRHIVBNN');
|
||||||
|
script.setAttribute('data-canonical', 'false');
|
||||||
|
script.setAttribute('data-spa', 'auto');
|
||||||
script.defer = true;
|
script.defer = true;
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
}
|
}
|
||||||
|
|||||||
588
package-lock.json
generated
588
package-lock.json
generated
@@ -18,22 +18,22 @@
|
|||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
"@radix-ui/react-collapsible": "^1.1.0",
|
"@radix-ui/react-collapsible": "^1.1.0",
|
||||||
"@radix-ui/react-context-menu": "^2.2.1",
|
"@radix-ui/react-context-menu": "^2.2.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-hover-card": "^1.1.1",
|
"@radix-ui/react-hover-card": "^1.1.1",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-menubar": "^1.1.1",
|
"@radix-ui/react-menubar": "^1.1.1",
|
||||||
"@radix-ui/react-popover": "^1.1.1",
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-scroll-area": "1.2.0",
|
"@radix-ui/react-scroll-area": "1.2.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.2",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"@xyflow/react": "^12.8.2",
|
"@xyflow/react": "^12.8.2",
|
||||||
"ahooks": "^3.8.1",
|
"ahooks": "^3.8.1",
|
||||||
@@ -2121,23 +2121,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dialog": {
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
"version": "1.1.6",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
|
||||||
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
"integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.1",
|
"@radix-ui/primitive": "1.1.2",
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-context": "1.1.1",
|
"@radix-ui/react-context": "1.1.2",
|
||||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||||
"@radix-ui/react-focus-guards": "1.1.1",
|
"@radix-ui/react-focus-guards": "1.1.2",
|
||||||
"@radix-ui/react-focus-scope": "1.1.2",
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
"@radix-ui/react-id": "1.1.0",
|
"@radix-ui/react-id": "1.1.1",
|
||||||
"@radix-ui/react-portal": "1.1.4",
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
"@radix-ui/react-presence": "1.1.2",
|
"@radix-ui/react-presence": "1.1.4",
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-slot": "1.1.2",
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
"aria-hidden": "^1.2.4",
|
"aria-hidden": "^1.2.4",
|
||||||
"react-remove-scroll": "^2.6.3"
|
"react-remove-scroll": "^2.6.3"
|
||||||
},
|
},
|
||||||
@@ -2156,17 +2156,53 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
|
||||||
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
|
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.1",
|
"@radix-ui/primitive": "1.1.2",
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -2183,15 +2219,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||||
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
|
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -2208,14 +2259,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -2233,12 +2326,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
|
||||||
"version": "2.0.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "1.1.2"
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -2255,13 +2348,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.1"
|
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -2273,6 +2382,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-direction": {
|
"node_modules/@radix-ui/react-direction": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||||
@@ -2941,12 +3083,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-separator": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
||||||
"integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==",
|
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-primitive": "2.0.2"
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -2964,12 +3106,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
|
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
|
||||||
"version": "2.0.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "1.1.2"
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -2986,24 +3128,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/react-compose-refs": "1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
@@ -3156,23 +3280,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip": {
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
"version": "1.1.8",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
|
||||||
"integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==",
|
"integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.1",
|
"@radix-ui/primitive": "1.1.2",
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-context": "1.1.1",
|
"@radix-ui/react-context": "1.1.2",
|
||||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||||
"@radix-ui/react-id": "1.1.0",
|
"@radix-ui/react-id": "1.1.1",
|
||||||
"@radix-ui/react-popper": "1.2.2",
|
"@radix-ui/react-popper": "1.2.7",
|
||||||
"@radix-ui/react-portal": "1.1.4",
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
"@radix-ui/react-presence": "1.1.2",
|
"@radix-ui/react-presence": "1.1.4",
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-slot": "1.1.2",
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
"@radix-ui/react-visually-hidden": "1.1.2"
|
"@radix-ui/react-visually-hidden": "1.2.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -3189,13 +3313,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||||
"integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==",
|
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-primitive": "2.0.2"
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -3212,17 +3342,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
|
||||||
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
|
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.1",
|
"@radix-ui/primitive": "1.1.2",
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -3239,22 +3399,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
|
||||||
"integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==",
|
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react-dom": "^2.0.0",
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
"@radix-ui/react-arrow": "1.1.2",
|
"@radix-ui/react-arrow": "1.1.7",
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-context": "1.1.1",
|
"@radix-ui/react-context": "1.1.2",
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
"@radix-ui/react-use-rect": "1.1.0",
|
"@radix-ui/react-use-rect": "1.1.1",
|
||||||
"@radix-ui/react-use-size": "1.1.0",
|
"@radix-ui/react-use-size": "1.1.1",
|
||||||
"@radix-ui/rect": "1.1.0"
|
"@radix-ui/rect": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -3272,13 +3450,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||||
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -3296,12 +3498,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
|
||||||
"version": "2.0.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "1.1.2"
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -3318,13 +3520,98 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.1"
|
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-size": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -3337,12 +3624,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
|
||||||
"version": "1.1.2",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||||
"integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==",
|
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-primitive": "2.0.2"
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -3359,6 +3646,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||||
@@ -3392,6 +3685,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-effect-event": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||||
|
|||||||
@@ -26,22 +26,22 @@
|
|||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
"@radix-ui/react-collapsible": "^1.1.0",
|
"@radix-ui/react-collapsible": "^1.1.0",
|
||||||
"@radix-ui/react-context-menu": "^2.2.1",
|
"@radix-ui/react-context-menu": "^2.2.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-hover-card": "^1.1.1",
|
"@radix-ui/react-hover-card": "^1.1.1",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-menubar": "^1.1.1",
|
"@radix-ui/react-menubar": "^1.1.1",
|
||||||
"@radix-ui/react-popover": "^1.1.1",
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-scroll-area": "1.2.0",
|
"@radix-ui/react-scroll-area": "1.2.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.2",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"@xyflow/react": "^12.8.2",
|
"@xyflow/react": "^12.8.2",
|
||||||
"ahooks": "^3.8.1",
|
"ahooks": "^3.8.1",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Disallow: /
|
||||||
|
|
||||||
Sitemap: https://app.chartdb.io/sitemap.xml
|
Sitemap: https://app.chartdb.io/sitemap.xml
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
|
|
||||||
export const buttonVariants = cva(
|
export const buttonVariants = cva(
|
||||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
112
src/components/button/button-with-alternatives.tsx
Normal file
112
src/components/button/button-with-alternatives.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ChevronDownIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import { type VariantProps } from 'class-variance-authority';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { buttonVariants } from './button-variants';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/dropdown-menu/dropdown-menu';
|
||||||
|
|
||||||
|
export interface ButtonWithAlternativesProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
alternatives: Array<{
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}>;
|
||||||
|
dropdownTriggerClassName?: string;
|
||||||
|
chevronDownIconClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonWithAlternatives = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ButtonWithAlternativesProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
alternatives,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
dropdownTriggerClassName,
|
||||||
|
chevronDownIconClassName,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
const hasAlternatives = (alternatives?.length ?? 0) > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-stretch">
|
||||||
|
<Comp
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant, size }),
|
||||||
|
{ 'rounded-r-none': hasAlternatives },
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
onClick={onClick}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
{hasAlternatives ? (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant, size }),
|
||||||
|
'rounded-l-none border-l border-l-primary/5 px-2 min-w-0',
|
||||||
|
className?.includes('h-') &&
|
||||||
|
className.match(/h-\d+/)?.[0],
|
||||||
|
className?.includes('text-') &&
|
||||||
|
className.match(/text-\w+/)?.[0],
|
||||||
|
dropdownTriggerClassName
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={cn(
|
||||||
|
'size-4 shrink-0',
|
||||||
|
chevronDownIconClassName
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{alternatives.map((alternative, index) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={index}
|
||||||
|
onClick={alternative.onClick}
|
||||||
|
disabled={alternative.disabled}
|
||||||
|
className={cn(alternative.className)}
|
||||||
|
>
|
||||||
|
<span className="flex w-full items-center justify-between gap-2">
|
||||||
|
{alternative.label}
|
||||||
|
{alternative.icon}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ButtonWithAlternatives.displayName = 'ButtonWithAlternatives';
|
||||||
|
|
||||||
|
export { ButtonWithAlternatives };
|
||||||
@@ -4,6 +4,7 @@ import { Cross2Icon } from '@radix-ui/react-icons';
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { ScrollArea } from '../scroll-area/scroll-area';
|
import { ScrollArea } from '../scroll-area/scroll-area';
|
||||||
|
import { ChevronLeft } from 'lucide-react';
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root;
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
|
||||||
@@ -32,10 +33,45 @@ const DialogContent = React.forwardRef<
|
|||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||||
showClose?: boolean;
|
showClose?: boolean;
|
||||||
|
showBack?: boolean;
|
||||||
|
backButtonClassName?: string;
|
||||||
|
blurBackground?: boolean;
|
||||||
|
forceOverlay?: boolean;
|
||||||
|
onBackClick?: () => void;
|
||||||
}
|
}
|
||||||
>(({ className, children, showClose, ...props }, ref) => (
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showClose,
|
||||||
|
showBack,
|
||||||
|
onBackClick,
|
||||||
|
backButtonClassName,
|
||||||
|
blurBackground,
|
||||||
|
forceOverlay,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
{forceOverlay ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-0 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||||
|
{
|
||||||
|
'bg-black/80': !blurBackground,
|
||||||
|
'bg-black/30 backdrop-blur-sm': blurBackground,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
data-state="open"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<DialogOverlay
|
||||||
|
className={cn({
|
||||||
|
'bg-black/30 backdrop-blur-sm': blurBackground,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -45,6 +81,17 @@ const DialogContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
{showBack && (
|
||||||
|
<button
|
||||||
|
onClick={() => onBackClick?.()}
|
||||||
|
className={cn(
|
||||||
|
'absolute left-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground',
|
||||||
|
backButtonClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="size-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{showClose && (
|
{showClose && (
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
<Cross2Icon className="size-4" />
|
<Cross2Icon className="size-4" />
|
||||||
@@ -53,7 +100,8 @@ const DialogContent = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
));
|
)
|
||||||
|
);
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
|
|||||||
@@ -2,16 +2,13 @@ import React from 'react';
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export interface InputProps
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
setOpen?.(isOpen);
|
setOpen?.(isOpen);
|
||||||
setIsOpen(isOpen);
|
setIsOpen(isOpen);
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
setSearchTerm('');
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
|
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
|
||||||
},
|
},
|
||||||
[setOpen]
|
[setOpen]
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|||||||
const SIDEBAR_WIDTH = '16rem';
|
const SIDEBAR_WIDTH = '16rem';
|
||||||
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||||
const SIDEBAR_WIDTH_ICON = '3rem';
|
const SIDEBAR_WIDTH_ICON = '3rem';
|
||||||
|
const SIDEBAR_WIDTH_ICON_EXTENDED = '4rem';
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||||
|
|
||||||
type SidebarContext = {
|
type SidebarContext = {
|
||||||
@@ -142,6 +143,8 @@ const SidebarProvider = React.forwardRef<
|
|||||||
{
|
{
|
||||||
'--sidebar-width': SIDEBAR_WIDTH,
|
'--sidebar-width': SIDEBAR_WIDTH,
|
||||||
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||||
|
'--sidebar-width-icon-extended':
|
||||||
|
SIDEBAR_WIDTH_ICON_EXTENDED,
|
||||||
...style,
|
...style,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
@@ -166,7 +169,7 @@ const Sidebar = React.forwardRef<
|
|||||||
React.ComponentProps<'div'> & {
|
React.ComponentProps<'div'> & {
|
||||||
side?: 'left' | 'right';
|
side?: 'left' | 'right';
|
||||||
variant?: 'sidebar' | 'floating' | 'inset';
|
variant?: 'sidebar' | 'floating' | 'inset';
|
||||||
collapsible?: 'offcanvas' | 'icon' | 'none';
|
collapsible?: 'offcanvas' | 'icon' | 'icon-extended' | 'none';
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
@@ -245,8 +248,8 @@ const Sidebar = React.forwardRef<
|
|||||||
'group-data-[collapsible=offcanvas]:w-0',
|
'group-data-[collapsible=offcanvas]:w-0',
|
||||||
'group-data-[side=right]:rotate-180',
|
'group-data-[side=right]:rotate-180',
|
||||||
variant === 'floating' || variant === 'inset'
|
variant === 'floating' || variant === 'inset'
|
||||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
|
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4))]'
|
||||||
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
|
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended]'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -257,8 +260,8 @@ const Sidebar = React.forwardRef<
|
|||||||
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||||
// Adjust the padding for floating and inset variants.
|
// Adjust the padding for floating and inset variants.
|
||||||
variant === 'floating' || variant === 'inset'
|
variant === 'floating' || variant === 'inset'
|
||||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
|
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4)_+2px)]'
|
||||||
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended] group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -421,7 +424,7 @@ const SidebarContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
data-sidebar="content"
|
data-sidebar="content"
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden group-data-[collapsible=icon-extended]:overflow-hidden',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -461,6 +464,7 @@ const SidebarGroupLabel = React.forwardRef<
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
||||||
|
'group-data-[collapsible=icon-extended]:-mt-8 group-data-[collapsible=icon-extended]:opacity-0',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -483,7 +487,7 @@ const SidebarGroupAction = React.forwardRef<
|
|||||||
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
// Increases the hit area of the button on mobile.
|
// Increases the hit area of the button on mobile.
|
||||||
'after:absolute after:-inset-2 after:md:hidden',
|
'after:absolute after:-inset-2 after:md:hidden',
|
||||||
'group-data-[collapsible=icon]:hidden',
|
'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -532,7 +536,7 @@ const SidebarMenuItem = React.forwardRef<
|
|||||||
SidebarMenuItem.displayName = 'SidebarMenuItem';
|
SidebarMenuItem.displayName = 'SidebarMenuItem';
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
const sidebarMenuButtonVariants = cva(
|
||||||
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon-extended]:h-auto group-data-[collapsible=icon-extended]:flex-col group-data-[collapsible=icon-extended]:gap-1 group-data-[collapsible=icon-extended]:p-2 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate group-data-[collapsible=icon-extended]:[&>span]:w-full group-data-[collapsible=icon-extended]:[&>span]:text-center group-data-[collapsible=icon-extended]:[&>span]:text-[10px] group-data-[collapsible=icon-extended]:[&>span]:leading-tight [&>svg]:size-4 [&>svg]:shrink-0',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -636,7 +640,7 @@ const SidebarMenuAction = React.forwardRef<
|
|||||||
'peer-data-[size=sm]/menu-button:top-1',
|
'peer-data-[size=sm]/menu-button:top-1',
|
||||||
'peer-data-[size=default]/menu-button:top-1.5',
|
'peer-data-[size=default]/menu-button:top-1.5',
|
||||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||||
'group-data-[collapsible=icon]:hidden',
|
'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
|
||||||
showOnHover &&
|
showOnHover &&
|
||||||
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
|
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
|
||||||
className
|
className
|
||||||
@@ -753,7 +757,7 @@ const SidebarMenuSubButton = React.forwardRef<
|
|||||||
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
||||||
size === 'sm' && 'text-xs',
|
size === 'sm' && 'text-xs',
|
||||||
size === 'md' && 'text-sm',
|
size === 'md' && 'text-sm',
|
||||||
'group-data-[collapsible=icon]:hidden',
|
'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -13,15 +13,17 @@ const TooltipContent = React.forwardRef<
|
|||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
// <TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Content
|
<TooltipPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
// </TooltipPrimitive.Portal>
|
||||||
));
|
));
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import React, { type ReactNode, useCallback, useState } from 'react';
|
import React, {
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
import { canvasContext } from './canvas-context';
|
import { canvasContext } from './canvas-context';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { adjustTablePositions } from '@/lib/domain/db-table';
|
import { adjustTablePositions } from '@/lib/domain/db-table';
|
||||||
@@ -15,14 +21,35 @@ interface CanvasProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||||
const { tables, relationships, updateTablesState, databaseType } =
|
const {
|
||||||
useChartDB();
|
tables,
|
||||||
const { filter } = useDiagramFilter();
|
relationships,
|
||||||
|
updateTablesState,
|
||||||
|
databaseType,
|
||||||
|
areas,
|
||||||
|
diagramId,
|
||||||
|
} = useChartDB();
|
||||||
|
const { filter, loading: filterLoading } = useDiagramFilter();
|
||||||
const { fitView } = useReactFlow();
|
const { fitView } = useReactFlow();
|
||||||
const [overlapGraph, setOverlapGraph] =
|
const [overlapGraph, setOverlapGraph] =
|
||||||
useState<Graph<string>>(createGraph());
|
useState<Graph<string>>(createGraph());
|
||||||
|
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
|
const diagramIdActiveFilterRef = useRef<string>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filterLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diagramIdActiveFilterRef.current === diagramId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
diagramIdActiveFilterRef.current = diagramId;
|
||||||
|
|
||||||
|
setShowFilter(true);
|
||||||
|
}, [filterLoading, diagramId]);
|
||||||
|
|
||||||
const reorderTables = useCallback(
|
const reorderTables = useCallback(
|
||||||
(
|
(
|
||||||
@@ -44,6 +71,7 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
areas,
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,6 +114,7 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
|||||||
updateTablesState,
|
updateTablesState,
|
||||||
fitView,
|
fitView,
|
||||||
databaseType,
|
databaseType,
|
||||||
|
areas,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import { deepCopy, generateId } from '@/lib/utils';
|
import { deepCopy, generateId } from '@/lib/utils';
|
||||||
import { randomColor } from '@/lib/colors';
|
import { defaultTableColor, defaultAreaColor, viewColor } from '@/lib/colors';
|
||||||
import type { ChartDBContext, ChartDBEvent } from './chartdb-context';
|
import type { ChartDBContext, ChartDBEvent } from './chartdb-context';
|
||||||
import { chartDBContext } from './chartdb-context';
|
import { chartDBContext } from './chartdb-context';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
@@ -89,7 +89,10 @@ export const ChartDBProvider: React.FC<
|
|||||||
|
|
||||||
diffEvents.useSubscription(diffCalculatedHandler);
|
diffEvents.useSubscription(diffCalculatedHandler);
|
||||||
|
|
||||||
const defaultSchemaName = defaultSchemas[databaseType];
|
const defaultSchemaName = useMemo(
|
||||||
|
() => defaultSchemas[databaseType],
|
||||||
|
[databaseType]
|
||||||
|
);
|
||||||
|
|
||||||
const readonly = useMemo(
|
const readonly = useMemo(
|
||||||
() => readonlyProp ?? hasDiff ?? false,
|
() => readonlyProp ?? hasDiff ?? false,
|
||||||
@@ -110,9 +113,11 @@ export const ChartDBProvider: React.FC<
|
|||||||
.filter((schema) => !!schema) as string[]
|
.filter((schema) => !!schema) as string[]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
.sort((a, b) =>
|
.sort((a, b) => {
|
||||||
a === defaultSchemaName ? -1 : a.localeCompare(b)
|
if (a === defaultSchemaName) return -1;
|
||||||
)
|
if (b === defaultSchemaName) return 1;
|
||||||
|
return a.localeCompare(b);
|
||||||
|
})
|
||||||
.map(
|
.map(
|
||||||
(schema): DBSchema => ({
|
(schema): DBSchema => ({
|
||||||
id: schemaNameToSchemaId(schema),
|
id: schemaNameToSchemaId(schema),
|
||||||
@@ -337,7 +342,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
indexes: [],
|
indexes: [],
|
||||||
color: randomColor(),
|
color: attributes?.isView ? viewColor : defaultTableColor,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
isView: false,
|
isView: false,
|
||||||
order: tables.length,
|
order: tables.length,
|
||||||
@@ -1412,7 +1417,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
y: 0,
|
y: 0,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 200,
|
height: 200,
|
||||||
color: randomColor(),
|
color: defaultAreaColor,
|
||||||
...attributes,
|
...attributes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,50 @@
|
|||||||
import type { DBSchema } from '@/lib/domain';
|
import type { DBSchema } from '@/lib/domain';
|
||||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
import type {
|
||||||
|
DiagramFilter,
|
||||||
|
FilterTableInfo,
|
||||||
|
} from '@/lib/domain/diagram-filter/diagram-filter';
|
||||||
import { emptyFn } from '@/lib/utils';
|
import { emptyFn } from '@/lib/utils';
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
export interface DiagramFilterContext {
|
export interface DiagramFilterContext {
|
||||||
filter?: DiagramFilter;
|
filter?: DiagramFilter;
|
||||||
|
loading: boolean;
|
||||||
|
|
||||||
hasActiveFilter: boolean;
|
hasActiveFilter: boolean;
|
||||||
schemasDisplayed: DBSchema[];
|
schemasDisplayed: DBSchema[];
|
||||||
|
|
||||||
// schemas
|
|
||||||
schemaIdsFilter?: string[];
|
|
||||||
addSchemaIdsFilter: (...ids: string[]) => void;
|
|
||||||
removeSchemaIdsFilter: (...ids: string[]) => void;
|
|
||||||
clearSchemaIdsFilter: () => void;
|
clearSchemaIdsFilter: () => void;
|
||||||
|
|
||||||
// tables
|
|
||||||
tableIdsFilter?: string[];
|
|
||||||
addTableIdsFilter: (...ids: string[]) => void;
|
|
||||||
removeTableIdsFilter: (...ids: string[]) => void;
|
|
||||||
clearTableIdsFilter: () => void;
|
clearTableIdsFilter: () => void;
|
||||||
|
|
||||||
setTableIdsFilterEmpty: () => void;
|
setTableIdsFilterEmpty: () => void;
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
resetFilter: () => void;
|
resetFilter: () => void;
|
||||||
|
|
||||||
// smart filters
|
|
||||||
toggleSchemaFilter: (schemaId: string) => void;
|
toggleSchemaFilter: (schemaId: string) => void;
|
||||||
toggleTableFilter: (tableId: string) => void;
|
toggleTableFilter: (tableId: string) => void;
|
||||||
addSchemaIfFiltered: (schemaId: string) => void;
|
addSchemaToFilter: (schemaId: string) => void;
|
||||||
|
addTablesToFilter: (attrs: {
|
||||||
|
tableIds?: string[];
|
||||||
|
filterCallback?: (table: FilterTableInfo) => boolean;
|
||||||
|
}) => void;
|
||||||
|
removeTablesFromFilter: (attrs: {
|
||||||
|
tableIds?: string[];
|
||||||
|
filterCallback?: (table: FilterTableInfo) => boolean;
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const diagramFilterContext = createContext<DiagramFilterContext>({
|
export const diagramFilterContext = createContext<DiagramFilterContext>({
|
||||||
hasActiveFilter: false,
|
hasActiveFilter: false,
|
||||||
addSchemaIdsFilter: emptyFn,
|
|
||||||
addTableIdsFilter: emptyFn,
|
|
||||||
clearSchemaIdsFilter: emptyFn,
|
clearSchemaIdsFilter: emptyFn,
|
||||||
clearTableIdsFilter: emptyFn,
|
clearTableIdsFilter: emptyFn,
|
||||||
setTableIdsFilterEmpty: emptyFn,
|
setTableIdsFilterEmpty: emptyFn,
|
||||||
removeSchemaIdsFilter: emptyFn,
|
|
||||||
removeTableIdsFilter: emptyFn,
|
|
||||||
resetFilter: emptyFn,
|
resetFilter: emptyFn,
|
||||||
toggleSchemaFilter: emptyFn,
|
toggleSchemaFilter: emptyFn,
|
||||||
toggleTableFilter: emptyFn,
|
toggleTableFilter: emptyFn,
|
||||||
addSchemaIfFiltered: emptyFn,
|
addSchemaToFilter: emptyFn,
|
||||||
schemasDisplayed: [],
|
schemasDisplayed: [],
|
||||||
|
addTablesToFilter: emptyFn,
|
||||||
|
removeTablesFromFilter: emptyFn,
|
||||||
|
loading: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,33 +7,45 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import type { DiagramFilterContext } from './diagram-filter-context';
|
import type { DiagramFilterContext } from './diagram-filter-context';
|
||||||
import { diagramFilterContext } from './diagram-filter-context';
|
import { diagramFilterContext } from './diagram-filter-context';
|
||||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
import type {
|
||||||
import { reduceFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
DiagramFilter,
|
||||||
|
FilterTableInfo,
|
||||||
|
} from '@/lib/domain/diagram-filter/diagram-filter';
|
||||||
|
import {
|
||||||
|
reduceFilter,
|
||||||
|
spreadFilterTables,
|
||||||
|
} from '@/lib/domain/diagram-filter/diagram-filter';
|
||||||
import { useStorage } from '@/hooks/use-storage';
|
import { useStorage } from '@/hooks/use-storage';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { filterSchema, filterTable } from '@/lib/domain/diagram-filter/filter';
|
import { filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||||
import { schemaNameToSchemaId } from '@/lib/domain';
|
import { databasesWithSchemas, schemaNameToSchemaId } from '@/lib/domain';
|
||||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||||
|
import type { ChartDBEvent } from '../chartdb-context/chartdb-context';
|
||||||
|
|
||||||
export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { diagramId, tables, schemas, databaseType } = useChartDB();
|
const { diagramId, tables, schemas, databaseType, events } = useChartDB();
|
||||||
const { getDiagramFilter, updateDiagramFilter } = useStorage();
|
const { getDiagramFilter, updateDiagramFilter } = useStorage();
|
||||||
const [filter, setFilter] = useState<DiagramFilter>({});
|
const [filter, setFilter] = useState<DiagramFilter>({});
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
const allSchemasIds = useMemo(() => {
|
const allSchemasIds = useMemo(() => {
|
||||||
return schemas.map((schema) => schema.id);
|
return schemas.map((schema) => schema.id);
|
||||||
}, [schemas]);
|
}, [schemas]);
|
||||||
|
|
||||||
const allTables = useMemo(() => {
|
const allTables: FilterTableInfo[] = useMemo(() => {
|
||||||
return tables.map((table) => ({
|
return tables.map(
|
||||||
|
(table) =>
|
||||||
|
({
|
||||||
id: table.id,
|
id: table.id,
|
||||||
schemaId: table.schema
|
schemaId: table.schema
|
||||||
? schemaNameToSchemaId(table.schema)
|
? schemaNameToSchemaId(table.schema)
|
||||||
: defaultSchemas[databaseType],
|
: defaultSchemas[databaseType],
|
||||||
schema: table.schema,
|
schema: table.schema ?? defaultSchemas[databaseType],
|
||||||
}));
|
areaId: table.parentAreaId ?? undefined,
|
||||||
|
}) satisfies FilterTableInfo
|
||||||
|
);
|
||||||
}, [tables, databaseType]);
|
}, [tables, databaseType]);
|
||||||
|
|
||||||
const diagramIdOfLoadedFilter = useRef<string | null>(null);
|
const diagramIdOfLoadedFilter = useRef<string | null>(null);
|
||||||
@@ -51,11 +63,26 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
const loadFilterFromStorage = async (diagramId: string) => {
|
const loadFilterFromStorage = async (diagramId: string) => {
|
||||||
if (diagramId) {
|
if (diagramId) {
|
||||||
const storedFilter = await getDiagramFilter(diagramId);
|
const storedFilter = await getDiagramFilter(diagramId);
|
||||||
setFilter(storedFilter ?? {});
|
|
||||||
|
let filterToSet = storedFilter;
|
||||||
|
|
||||||
|
if (!filterToSet) {
|
||||||
|
// If no filter is stored, set default based on database type
|
||||||
|
filterToSet =
|
||||||
|
schemas.length > 1
|
||||||
|
? { schemaIds: [schemas[0].id] }
|
||||||
|
: {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFilter(filterToSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
setFilter({});
|
setFilter({});
|
||||||
@@ -64,34 +91,7 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
loadFilterFromStorage(diagramId);
|
loadFilterFromStorage(diagramId);
|
||||||
diagramIdOfLoadedFilter.current = diagramId;
|
diagramIdOfLoadedFilter.current = diagramId;
|
||||||
}
|
}
|
||||||
}, [diagramId, getDiagramFilter]);
|
}, [diagramId, getDiagramFilter, schemas]);
|
||||||
|
|
||||||
// Schema methods
|
|
||||||
const addSchemaIds: DiagramFilterContext['addSchemaIdsFilter'] =
|
|
||||||
useCallback((...ids: string[]) => {
|
|
||||||
setFilter(
|
|
||||||
(prev) =>
|
|
||||||
({
|
|
||||||
...prev,
|
|
||||||
schemaIds: [
|
|
||||||
...new Set([...(prev.schemaIds || []), ...ids]),
|
|
||||||
],
|
|
||||||
}) satisfies DiagramFilter
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const removeSchemaIds: DiagramFilterContext['removeSchemaIdsFilter'] =
|
|
||||||
useCallback((...ids: string[]) => {
|
|
||||||
setFilter(
|
|
||||||
(prev) =>
|
|
||||||
({
|
|
||||||
...prev,
|
|
||||||
schemaIds: prev.schemaIds?.filter(
|
|
||||||
(id) => !ids.includes(id)
|
|
||||||
),
|
|
||||||
}) satisfies DiagramFilter
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] =
|
const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] =
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@@ -104,35 +104,6 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Table methods
|
|
||||||
const addTableIds: DiagramFilterContext['addTableIdsFilter'] = useCallback(
|
|
||||||
(...ids: string[]) => {
|
|
||||||
setFilter(
|
|
||||||
(prev) =>
|
|
||||||
({
|
|
||||||
...prev,
|
|
||||||
tableIds: [
|
|
||||||
...new Set([...(prev.tableIds || []), ...ids]),
|
|
||||||
],
|
|
||||||
}) satisfies DiagramFilter
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeTableIds: DiagramFilterContext['removeTableIdsFilter'] =
|
|
||||||
useCallback((...ids: string[]) => {
|
|
||||||
setFilter(
|
|
||||||
(prev) =>
|
|
||||||
({
|
|
||||||
...prev,
|
|
||||||
tableIds: prev.tableIds?.filter(
|
|
||||||
(id) => !ids.includes(id)
|
|
||||||
),
|
|
||||||
}) satisfies DiagramFilter
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearTableIds: DiagramFilterContext['clearTableIdsFilter'] =
|
const clearTableIds: DiagramFilterContext['clearTableIdsFilter'] =
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
setFilter(
|
setFilter(
|
||||||
@@ -167,10 +138,20 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const currentSchemaIds = prev.schemaIds;
|
const currentSchemaIds = prev.schemaIds;
|
||||||
|
|
||||||
// Check if schema is currently visible
|
// Check if schema is currently visible
|
||||||
const isSchemaVisible = filterSchema({
|
const isSchemaVisible = !allTables.some(
|
||||||
schemaId,
|
(table) =>
|
||||||
schemaIdsFilter: currentSchemaIds,
|
table.schemaId === schemaId &&
|
||||||
});
|
filterTable({
|
||||||
|
table: {
|
||||||
|
id: table.id,
|
||||||
|
schema: table.schema,
|
||||||
|
},
|
||||||
|
filter: prev,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
}) === false
|
||||||
|
);
|
||||||
|
|
||||||
let newSchemaIds: string[] | undefined;
|
let newSchemaIds: string[] | undefined;
|
||||||
let newTableIds: string[] | undefined = prev.tableIds;
|
let newTableIds: string[] | undefined = prev.tableIds;
|
||||||
@@ -224,11 +205,15 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
schemaIds: newSchemaIds,
|
schemaIds: newSchemaIds,
|
||||||
tableIds: newTableIds,
|
tableIds: newTableIds,
|
||||||
},
|
},
|
||||||
allTables
|
allTables satisfies FilterTableInfo[],
|
||||||
|
{
|
||||||
|
databaseWithSchemas:
|
||||||
|
databasesWithSchemas.includes(databaseType),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[allSchemasIds, allTables]
|
[allSchemasIds, allTables, databaseType]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleTableFilterForNoSchema = useCallback(
|
const toggleTableFilterForNoSchema = useCallback(
|
||||||
@@ -271,17 +256,21 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
schemaIds: undefined,
|
schemaIds: undefined,
|
||||||
tableIds: newTableIds,
|
tableIds: newTableIds,
|
||||||
},
|
},
|
||||||
allTables
|
allTables satisfies FilterTableInfo[],
|
||||||
|
{
|
||||||
|
databaseWithSchemas:
|
||||||
|
databasesWithSchemas.includes(databaseType),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[allTables]
|
[allTables, databaseType]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] =
|
const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] =
|
||||||
useCallback(
|
useCallback(
|
||||||
(tableId: string) => {
|
(tableId: string) => {
|
||||||
if (!defaultSchemas[databaseType]) {
|
if (!databasesWithSchemas.includes(databaseType)) {
|
||||||
// No schemas, toggle table filter without schema context
|
// No schemas, toggle table filter without schema context
|
||||||
toggleTableFilterForNoSchema(tableId);
|
toggleTableFilterForNoSchema(tableId);
|
||||||
return;
|
return;
|
||||||
@@ -359,14 +348,18 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
schemaIds: newSchemaIds,
|
schemaIds: newSchemaIds,
|
||||||
tableIds: newTableIds,
|
tableIds: newTableIds,
|
||||||
},
|
},
|
||||||
allTables
|
allTables satisfies FilterTableInfo[],
|
||||||
|
{
|
||||||
|
databaseWithSchemas:
|
||||||
|
databasesWithSchemas.includes(databaseType),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[allTables, databaseType, toggleTableFilterForNoSchema]
|
[allTables, databaseType, toggleTableFilterForNoSchema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addSchemaIfFiltered: DiagramFilterContext['addSchemaIfFiltered'] =
|
const addSchemaToFilter: DiagramFilterContext['addSchemaToFilter'] =
|
||||||
useCallback(
|
useCallback(
|
||||||
(schemaId: string) => {
|
(schemaId: string) => {
|
||||||
setFilter((prev) => {
|
setFilter((prev) => {
|
||||||
@@ -406,32 +399,156 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
|
|
||||||
const schemasDisplayed: DiagramFilterContext['schemasDisplayed'] =
|
const schemasDisplayed: DiagramFilterContext['schemasDisplayed'] =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
if (!filter.schemaIds) {
|
if (!hasActiveFilter) {
|
||||||
return schemas;
|
return schemas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayedSchemaIds = new Set<string>();
|
||||||
|
for (const table of allTables) {
|
||||||
|
if (
|
||||||
|
filterTable({
|
||||||
|
table: {
|
||||||
|
id: table.id,
|
||||||
|
schema: table.schema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
if (table.schemaId) {
|
||||||
|
displayedSchemaIds.add(table.schemaId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return schemas.filter((schema) =>
|
return schemas.filter((schema) =>
|
||||||
filter.schemaIds?.includes(schema.id)
|
displayedSchemaIds.has(schema.id)
|
||||||
);
|
);
|
||||||
}, [filter.schemaIds, schemas]);
|
}, [hasActiveFilter, schemas, allTables, filter, databaseType]);
|
||||||
|
|
||||||
|
const addTablesToFilter: DiagramFilterContext['addTablesToFilter'] =
|
||||||
|
useCallback(
|
||||||
|
({ tableIds, filterCallback }) => {
|
||||||
|
setFilter((prev) => {
|
||||||
|
let tableIdsToAdd: string[];
|
||||||
|
|
||||||
|
if (tableIds) {
|
||||||
|
// If tableIds are provided, use them directly
|
||||||
|
tableIdsToAdd = tableIds;
|
||||||
|
} else if (filterCallback) {
|
||||||
|
// If filterCallback is provided, filter tables based on it
|
||||||
|
tableIdsToAdd = allTables
|
||||||
|
.filter(filterCallback)
|
||||||
|
.map((table) => table.id);
|
||||||
|
} else {
|
||||||
|
// If neither is provided, do nothing
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterByTableIds = spreadFilterTables(
|
||||||
|
prev,
|
||||||
|
allTables satisfies FilterTableInfo[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentTableIds = filterByTableIds.tableIds || [];
|
||||||
|
const newTableIds = [
|
||||||
|
...new Set([...currentTableIds, ...tableIdsToAdd]),
|
||||||
|
];
|
||||||
|
|
||||||
|
return reduceFilter(
|
||||||
|
{
|
||||||
|
...filterByTableIds,
|
||||||
|
tableIds: newTableIds,
|
||||||
|
},
|
||||||
|
allTables satisfies FilterTableInfo[],
|
||||||
|
{
|
||||||
|
databaseWithSchemas:
|
||||||
|
databasesWithSchemas.includes(databaseType),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[allTables, databaseType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeTablesFromFilter: DiagramFilterContext['removeTablesFromFilter'] =
|
||||||
|
useCallback(
|
||||||
|
({ tableIds, filterCallback }) => {
|
||||||
|
setFilter((prev) => {
|
||||||
|
let tableIdsToRemovoe: string[];
|
||||||
|
|
||||||
|
if (tableIds) {
|
||||||
|
// If tableIds are provided, use them directly
|
||||||
|
tableIdsToRemovoe = tableIds;
|
||||||
|
} else if (filterCallback) {
|
||||||
|
// If filterCallback is provided, filter tables based on it
|
||||||
|
tableIdsToRemovoe = allTables
|
||||||
|
.filter(filterCallback)
|
||||||
|
.map((table) => table.id);
|
||||||
|
} else {
|
||||||
|
// If neither is provided, do nothing
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterByTableIds = spreadFilterTables(
|
||||||
|
prev,
|
||||||
|
allTables satisfies FilterTableInfo[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentTableIds = filterByTableIds.tableIds || [];
|
||||||
|
const newTableIds = currentTableIds.filter(
|
||||||
|
(id) => !tableIdsToRemovoe.includes(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return reduceFilter(
|
||||||
|
{
|
||||||
|
...filterByTableIds,
|
||||||
|
tableIds: newTableIds,
|
||||||
|
},
|
||||||
|
allTables satisfies FilterTableInfo[],
|
||||||
|
{
|
||||||
|
databaseWithSchemas:
|
||||||
|
databasesWithSchemas.includes(databaseType),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[allTables, databaseType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventConsumer = useCallback(
|
||||||
|
(event: ChartDBEvent) => {
|
||||||
|
if (!hasActiveFilter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.action === 'add_tables') {
|
||||||
|
addTablesToFilter({
|
||||||
|
tableIds: event.data.tables.map((table) => table.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[hasActiveFilter, addTablesToFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
events.useSubscription(eventConsumer);
|
||||||
|
|
||||||
const value: DiagramFilterContext = {
|
const value: DiagramFilterContext = {
|
||||||
|
loading,
|
||||||
filter,
|
filter,
|
||||||
schemaIdsFilter: filter.schemaIds,
|
|
||||||
addSchemaIdsFilter: addSchemaIds,
|
|
||||||
removeSchemaIdsFilter: removeSchemaIds,
|
|
||||||
clearSchemaIdsFilter: clearSchemaIds,
|
clearSchemaIdsFilter: clearSchemaIds,
|
||||||
setTableIdsFilterEmpty: setTableIdsEmpty,
|
setTableIdsFilterEmpty: setTableIdsEmpty,
|
||||||
tableIdsFilter: filter.tableIds,
|
|
||||||
addTableIdsFilter: addTableIds,
|
|
||||||
removeTableIdsFilter: removeTableIds,
|
|
||||||
clearTableIdsFilter: clearTableIds,
|
clearTableIdsFilter: clearTableIds,
|
||||||
resetFilter,
|
resetFilter,
|
||||||
toggleSchemaFilter,
|
toggleSchemaFilter,
|
||||||
toggleTableFilter,
|
toggleTableFilter,
|
||||||
addSchemaIfFiltered,
|
addSchemaToFilter,
|
||||||
hasActiveFilter,
|
hasActiveFilter,
|
||||||
schemasDisplayed,
|
schemasDisplayed,
|
||||||
|
addTablesToFilter,
|
||||||
|
removeTablesFromFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { emptyFn } from '@/lib/utils';
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
export type SidebarSection =
|
export type SidebarSection =
|
||||||
|
| 'dbml'
|
||||||
| 'tables'
|
| 'tables'
|
||||||
| 'relationships'
|
| 'refs'
|
||||||
| 'dependencies'
|
|
||||||
| 'areas'
|
| 'areas'
|
||||||
| 'customTypes';
|
| 'customTypes';
|
||||||
|
|
||||||
@@ -13,14 +13,16 @@ export interface LayoutContext {
|
|||||||
openTableFromSidebar: (tableId: string) => void;
|
openTableFromSidebar: (tableId: string) => void;
|
||||||
closeAllTablesInSidebar: () => void;
|
closeAllTablesInSidebar: () => void;
|
||||||
|
|
||||||
openedRelationshipInSidebar: string | undefined;
|
|
||||||
openRelationshipFromSidebar: (relationshipId: string) => void;
|
openRelationshipFromSidebar: (relationshipId: string) => void;
|
||||||
closeAllRelationshipsInSidebar: () => void;
|
closeAllRelationshipsInSidebar: () => void;
|
||||||
|
|
||||||
openedDependencyInSidebar: string | undefined;
|
|
||||||
openDependencyFromSidebar: (dependencyId: string) => void;
|
openDependencyFromSidebar: (dependencyId: string) => void;
|
||||||
closeAllDependenciesInSidebar: () => void;
|
closeAllDependenciesInSidebar: () => void;
|
||||||
|
|
||||||
|
openedRefInSidebar: string | undefined;
|
||||||
|
openRefFromSidebar: (refId: string) => void;
|
||||||
|
closeAllRefsInSidebar: () => void;
|
||||||
|
|
||||||
openedAreaInSidebar: string | undefined;
|
openedAreaInSidebar: string | undefined;
|
||||||
openAreaFromSidebar: (areaId: string) => void;
|
openAreaFromSidebar: (areaId: string) => void;
|
||||||
closeAllAreasInSidebar: () => void;
|
closeAllAreasInSidebar: () => void;
|
||||||
@@ -42,14 +44,16 @@ export const layoutContext = createContext<LayoutContext>({
|
|||||||
openedTableInSidebar: undefined,
|
openedTableInSidebar: undefined,
|
||||||
selectedSidebarSection: 'tables',
|
selectedSidebarSection: 'tables',
|
||||||
|
|
||||||
openedRelationshipInSidebar: undefined,
|
|
||||||
openRelationshipFromSidebar: emptyFn,
|
openRelationshipFromSidebar: emptyFn,
|
||||||
closeAllRelationshipsInSidebar: emptyFn,
|
closeAllRelationshipsInSidebar: emptyFn,
|
||||||
|
|
||||||
openedDependencyInSidebar: undefined,
|
|
||||||
openDependencyFromSidebar: emptyFn,
|
openDependencyFromSidebar: emptyFn,
|
||||||
closeAllDependenciesInSidebar: emptyFn,
|
closeAllDependenciesInSidebar: emptyFn,
|
||||||
|
|
||||||
|
openedRefInSidebar: undefined,
|
||||||
|
openRefFromSidebar: emptyFn,
|
||||||
|
closeAllRefsInSidebar: emptyFn,
|
||||||
|
|
||||||
openedAreaInSidebar: undefined,
|
openedAreaInSidebar: undefined,
|
||||||
openAreaFromSidebar: emptyFn,
|
openAreaFromSidebar: emptyFn,
|
||||||
closeAllAreasInSidebar: emptyFn,
|
closeAllAreasInSidebar: emptyFn,
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const [openedTableInSidebar, setOpenedTableInSidebar] = React.useState<
|
const [openedTableInSidebar, setOpenedTableInSidebar] = React.useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
const [openedRelationshipInSidebar, setOpenedRelationshipInSidebar] =
|
const [openedRefInSidebar, setOpenedRefInSidebar] = React.useState<
|
||||||
React.useState<string | undefined>();
|
string | undefined
|
||||||
const [openedDependencyInSidebar, setOpenedDependencyInSidebar] =
|
>();
|
||||||
React.useState<string | undefined>();
|
|
||||||
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
|
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
@@ -28,10 +27,13 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
() => setOpenedTableInSidebar('');
|
() => setOpenedTableInSidebar('');
|
||||||
|
|
||||||
const closeAllRelationshipsInSidebar: LayoutContext['closeAllRelationshipsInSidebar'] =
|
const closeAllRelationshipsInSidebar: LayoutContext['closeAllRelationshipsInSidebar'] =
|
||||||
() => setOpenedRelationshipInSidebar('');
|
() => setOpenedRefInSidebar('');
|
||||||
|
|
||||||
const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] =
|
const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] =
|
||||||
() => setOpenedDependencyInSidebar('');
|
() => setOpenedRefInSidebar('');
|
||||||
|
|
||||||
|
const closeAllRefsInSidebar: LayoutContext['closeAllRefsInSidebar'] = () =>
|
||||||
|
setOpenedRefInSidebar('');
|
||||||
|
|
||||||
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
|
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
|
||||||
() => setOpenedAreaInSidebar('');
|
() => setOpenedAreaInSidebar('');
|
||||||
@@ -60,15 +62,21 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const openRelationshipFromSidebar: LayoutContext['openRelationshipFromSidebar'] =
|
const openRelationshipFromSidebar: LayoutContext['openRelationshipFromSidebar'] =
|
||||||
(relationshipId) => {
|
(relationshipId) => {
|
||||||
showSidePanel();
|
showSidePanel();
|
||||||
setSelectedSidebarSection('relationships');
|
setSelectedSidebarSection('refs');
|
||||||
setOpenedRelationshipInSidebar(relationshipId);
|
setOpenedRefInSidebar(relationshipId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDependencyFromSidebar: LayoutContext['openDependencyFromSidebar'] =
|
const openDependencyFromSidebar: LayoutContext['openDependencyFromSidebar'] =
|
||||||
(dependencyId) => {
|
(dependencyId) => {
|
||||||
showSidePanel();
|
showSidePanel();
|
||||||
setSelectedSidebarSection('dependencies');
|
setSelectedSidebarSection('refs');
|
||||||
setOpenedDependencyInSidebar(dependencyId);
|
setOpenedRefInSidebar(dependencyId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openRefFromSidebar: LayoutContext['openRefFromSidebar'] = (refId) => {
|
||||||
|
showSidePanel();
|
||||||
|
setSelectedSidebarSection('refs');
|
||||||
|
setOpenedRefInSidebar(refId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = (
|
const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = (
|
||||||
@@ -93,7 +101,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
selectedSidebarSection,
|
selectedSidebarSection,
|
||||||
openTableFromSidebar,
|
openTableFromSidebar,
|
||||||
selectSidebarSection: setSelectedSidebarSection,
|
selectSidebarSection: setSelectedSidebarSection,
|
||||||
openedRelationshipInSidebar,
|
|
||||||
openRelationshipFromSidebar,
|
openRelationshipFromSidebar,
|
||||||
closeAllTablesInSidebar,
|
closeAllTablesInSidebar,
|
||||||
closeAllRelationshipsInSidebar,
|
closeAllRelationshipsInSidebar,
|
||||||
@@ -101,9 +108,11 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
hideSidePanel,
|
hideSidePanel,
|
||||||
showSidePanel,
|
showSidePanel,
|
||||||
toggleSidePanel,
|
toggleSidePanel,
|
||||||
openedDependencyInSidebar,
|
|
||||||
openDependencyFromSidebar,
|
openDependencyFromSidebar,
|
||||||
closeAllDependenciesInSidebar,
|
closeAllDependenciesInSidebar,
|
||||||
|
openedRefInSidebar,
|
||||||
|
openRefFromSidebar,
|
||||||
|
closeAllRefsInSidebar,
|
||||||
openedAreaInSidebar,
|
openedAreaInSidebar,
|
||||||
openAreaFromSidebar,
|
openAreaFromSidebar,
|
||||||
closeAllAreasInSidebar,
|
closeAllAreasInSidebar,
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ export interface LocalConfigContext {
|
|||||||
scrollAction: ScrollAction;
|
scrollAction: ScrollAction;
|
||||||
setScrollAction: (action: ScrollAction) => void;
|
setScrollAction: (action: ScrollAction) => void;
|
||||||
|
|
||||||
|
showDBViews: boolean;
|
||||||
|
setShowDBViews: (showViews: boolean) => void;
|
||||||
|
|
||||||
showCardinality: boolean;
|
showCardinality: boolean;
|
||||||
setShowCardinality: (showCardinality: boolean) => void;
|
setShowCardinality: (showCardinality: boolean) => void;
|
||||||
|
|
||||||
@@ -23,9 +26,6 @@ export interface LocalConfigContext {
|
|||||||
starUsDialogLastOpen: number;
|
starUsDialogLastOpen: number;
|
||||||
setStarUsDialogLastOpen: (lastOpen: number) => void;
|
setStarUsDialogLastOpen: (lastOpen: number) => void;
|
||||||
|
|
||||||
showDependenciesOnCanvas: boolean;
|
|
||||||
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
|
||||||
|
|
||||||
showMiniMapOnCanvas: boolean;
|
showMiniMapOnCanvas: boolean;
|
||||||
setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void;
|
setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void;
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,9 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
scrollAction: 'pan',
|
scrollAction: 'pan',
|
||||||
setScrollAction: emptyFn,
|
setScrollAction: emptyFn,
|
||||||
|
|
||||||
|
showDBViews: false,
|
||||||
|
setShowDBViews: emptyFn,
|
||||||
|
|
||||||
showCardinality: true,
|
showCardinality: true,
|
||||||
setShowCardinality: emptyFn,
|
setShowCardinality: emptyFn,
|
||||||
|
|
||||||
@@ -49,9 +52,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
starUsDialogLastOpen: 0,
|
starUsDialogLastOpen: 0,
|
||||||
setStarUsDialogLastOpen: emptyFn,
|
setStarUsDialogLastOpen: emptyFn,
|
||||||
|
|
||||||
showDependenciesOnCanvas: false,
|
|
||||||
setShowDependenciesOnCanvas: emptyFn,
|
|
||||||
|
|
||||||
showMiniMapOnCanvas: false,
|
showMiniMapOnCanvas: false,
|
||||||
setShowMiniMapOnCanvas: emptyFn,
|
setShowMiniMapOnCanvas: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ const showCardinalityKey = 'show_cardinality';
|
|||||||
const showFieldAttributesKey = 'show_field_attributes';
|
const showFieldAttributesKey = 'show_field_attributes';
|
||||||
const githubRepoOpenedKey = 'github_repo_opened';
|
const githubRepoOpenedKey = 'github_repo_opened';
|
||||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
|
||||||
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
||||||
|
const showDBViewsKey = 'show_db_views';
|
||||||
|
|
||||||
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -23,6 +23,10 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
(localStorage.getItem(scrollActionKey) as ScrollAction) || 'pan'
|
(localStorage.getItem(scrollActionKey) as ScrollAction) || 'pan'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [showDBViews, setShowDBViews] = React.useState<boolean>(
|
||||||
|
(localStorage.getItem(showDBViewsKey) || 'false') === 'true'
|
||||||
|
);
|
||||||
|
|
||||||
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
||||||
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||||
);
|
);
|
||||||
@@ -41,12 +45,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
|
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
|
||||||
);
|
);
|
||||||
|
|
||||||
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
|
|
||||||
React.useState<boolean>(
|
|
||||||
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
|
|
||||||
'true'
|
|
||||||
);
|
|
||||||
|
|
||||||
const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] =
|
const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] =
|
||||||
React.useState<boolean>(
|
React.useState<boolean>(
|
||||||
(localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true'
|
(localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true'
|
||||||
@@ -72,15 +70,12 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
}, [scrollAction]);
|
}, [scrollAction]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(showCardinalityKey, showCardinality.toString());
|
localStorage.setItem(showDBViewsKey, showDBViews.toString());
|
||||||
}, [showCardinality]);
|
}, [showDBViews]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(showCardinalityKey, showCardinality.toString());
|
||||||
showDependenciesOnCanvasKey,
|
}, [showCardinality]);
|
||||||
showDependenciesOnCanvas.toString()
|
|
||||||
);
|
|
||||||
}, [showDependenciesOnCanvas]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
@@ -96,6 +91,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setTheme,
|
setTheme,
|
||||||
scrollAction,
|
scrollAction,
|
||||||
setScrollAction,
|
setScrollAction,
|
||||||
|
showDBViews,
|
||||||
|
setShowDBViews,
|
||||||
showCardinality,
|
showCardinality,
|
||||||
setShowCardinality,
|
setShowCardinality,
|
||||||
showFieldAttributes,
|
showFieldAttributes,
|
||||||
@@ -104,8 +101,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
githubRepoOpened,
|
githubRepoOpened,
|
||||||
starUsDialogLastOpen,
|
starUsDialogLastOpen,
|
||||||
setStarUsDialogLastOpen,
|
setStarUsDialogLastOpen,
|
||||||
showDependenciesOnCanvas,
|
|
||||||
setShowDependenciesOnCanvas,
|
|
||||||
showMiniMapOnCanvas,
|
showMiniMapOnCanvas,
|
||||||
setShowMiniMapOnCanvas,
|
setShowMiniMapOnCanvas,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -245,7 +245,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
|
|
||||||
const getDiagramFilter: StorageContext['getDiagramFilter'] = useCallback(
|
const getDiagramFilter: StorageContext['getDiagramFilter'] = useCallback(
|
||||||
async (diagramId: string): Promise<DiagramFilter | undefined> => {
|
async (diagramId: string): Promise<DiagramFilter | undefined> => {
|
||||||
return await db.diagram_filters.get({ diagramId });
|
const filter = await db.diagram_filters.get({ diagramId });
|
||||||
|
|
||||||
|
return filter;
|
||||||
},
|
},
|
||||||
[db]
|
[db]
|
||||||
);
|
);
|
||||||
@@ -762,6 +764,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
db.db_dependencies.where('diagramId').equals(id).delete(),
|
db.db_dependencies.where('diagramId').equals(id).delete(),
|
||||||
db.areas.where('diagramId').equals(id).delete(),
|
db.areas.where('diagramId').equals(id).delete(),
|
||||||
db.db_custom_types.where('diagramId').equals(id).delete(),
|
db.db_custom_types.where('diagramId').equals(id).delete(),
|
||||||
|
db.diagram_filters.where('diagramId').equals(id).delete(),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
[db]
|
[db]
|
||||||
|
|||||||
@@ -218,8 +218,14 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
closeCreateRelationshipDialog();
|
closeCreateRelationshipDialog();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
modal={false}
|
||||||
|
>
|
||||||
|
<DialogContent
|
||||||
|
className="flex flex-col overflow-y-auto"
|
||||||
|
showClose
|
||||||
|
forceOverlay
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<DialogContent className="flex flex-col overflow-y-auto" showClose>
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{t('create_relationship_dialog.title')}
|
{t('create_relationship_dialog.title')}
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/dropdown-menu/dropdown-menu';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
MoreHorizontal,
|
||||||
|
SquareArrowOutUpRight,
|
||||||
|
Trash2,
|
||||||
|
Loader2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useStorage } from '@/hooks/use-storage';
|
||||||
|
import { useAlert } from '@/context/alert-context/alert-context';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { cloneDiagram } from '@/lib/clone';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { useConfig } from '@/hooks/use-config';
|
||||||
|
|
||||||
|
interface DiagramRowActionsMenuProps {
|
||||||
|
diagram: Diagram;
|
||||||
|
onOpen: () => void;
|
||||||
|
refetch: () => void;
|
||||||
|
onSelectDiagram?: (diagramId: string | undefined) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DiagramRowActionsMenu: React.FC<DiagramRowActionsMenuProps> = ({
|
||||||
|
diagram,
|
||||||
|
onOpen,
|
||||||
|
refetch,
|
||||||
|
onSelectDiagram,
|
||||||
|
}) => {
|
||||||
|
const { addDiagram, deleteDiagram, listDiagrams, getDiagram } =
|
||||||
|
useStorage();
|
||||||
|
const { showAlert } = useAlert();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { diagramId: currentDiagramId } = useParams<{ diagramId: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { updateConfig } = useConfig();
|
||||||
|
const [isDuplicating, setIsDuplicating] = useState(false);
|
||||||
|
|
||||||
|
const handleDuplicateDiagram = useCallback(async () => {
|
||||||
|
setIsDuplicating(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load the full diagram with all components
|
||||||
|
const fullDiagram = await getDiagram(diagram.id, {
|
||||||
|
includeTables: true,
|
||||||
|
includeRelationships: true,
|
||||||
|
includeAreas: true,
|
||||||
|
includeDependencies: true,
|
||||||
|
includeCustomTypes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fullDiagram) {
|
||||||
|
console.error('Failed to load diagram for duplication');
|
||||||
|
setIsDuplicating(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { diagram: clonedDiagram } = cloneDiagram(fullDiagram);
|
||||||
|
|
||||||
|
// Generate a unique name for the duplicated diagram
|
||||||
|
const diagrams = await listDiagrams();
|
||||||
|
const existingNames = diagrams.map((d) => d.name);
|
||||||
|
let duplicatedName = `${diagram.name} - Copy`;
|
||||||
|
let counter = 1;
|
||||||
|
|
||||||
|
while (existingNames.includes(duplicatedName)) {
|
||||||
|
duplicatedName = `${diagram.name} - Copy ${counter}`;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diagramToAdd = {
|
||||||
|
...clonedDiagram,
|
||||||
|
name: duplicatedName,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add 2 second delay for better UX
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
await addDiagram({ diagram: diagramToAdd });
|
||||||
|
|
||||||
|
// Clear current selection first, then select the new diagram
|
||||||
|
if (onSelectDiagram) {
|
||||||
|
onSelectDiagram(undefined); // Clear selection
|
||||||
|
await refetch(); // Refresh the list
|
||||||
|
// Use setTimeout to ensure the DOM has updated with the new row
|
||||||
|
setTimeout(() => {
|
||||||
|
onSelectDiagram(diagramToAdd.id);
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
await refetch(); // Refresh the list
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error duplicating diagram:', error);
|
||||||
|
} finally {
|
||||||
|
setIsDuplicating(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
diagram,
|
||||||
|
addDiagram,
|
||||||
|
listDiagrams,
|
||||||
|
getDiagram,
|
||||||
|
refetch,
|
||||||
|
onSelectDiagram,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleDeleteDiagram = useCallback(() => {
|
||||||
|
showAlert({
|
||||||
|
title: t('delete_diagram_alert.title'),
|
||||||
|
description: t('delete_diagram_alert.description'),
|
||||||
|
actionLabel: t('delete_diagram_alert.delete'),
|
||||||
|
closeLabel: t('delete_diagram_alert.cancel'),
|
||||||
|
onAction: async () => {
|
||||||
|
await deleteDiagram(diagram.id);
|
||||||
|
|
||||||
|
// If we deleted the currently open diagram, navigate to another one
|
||||||
|
if (currentDiagramId === diagram.id) {
|
||||||
|
// Get updated list of diagrams after deletion
|
||||||
|
const remainingDiagrams = await listDiagrams();
|
||||||
|
|
||||||
|
if (remainingDiagrams.length > 0) {
|
||||||
|
// Sort by last modified date (most recent first)
|
||||||
|
const sortedDiagrams = remainingDiagrams.sort(
|
||||||
|
(a, b) =>
|
||||||
|
b.updatedAt.getTime() - a.updatedAt.getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Navigate to the most recently modified diagram
|
||||||
|
const firstDiagram = sortedDiagrams[0];
|
||||||
|
updateConfig({
|
||||||
|
config: { defaultDiagramId: firstDiagram.id },
|
||||||
|
});
|
||||||
|
navigate(`/diagrams/${firstDiagram.id}`);
|
||||||
|
} else {
|
||||||
|
// No diagrams left, navigate to home
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refetch(); // Refresh the list
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
diagram.id,
|
||||||
|
currentDiagramId,
|
||||||
|
deleteDiagram,
|
||||||
|
refetch,
|
||||||
|
showAlert,
|
||||||
|
t,
|
||||||
|
listDiagrams,
|
||||||
|
updateConfig,
|
||||||
|
navigate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-8 p-0"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
disabled={isDuplicating}
|
||||||
|
>
|
||||||
|
{isDuplicating ? (
|
||||||
|
<Loader2 className="size-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<MoreHorizontal className="size-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onOpen();
|
||||||
|
}}
|
||||||
|
className="flex justify-between gap-4"
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
<SquareArrowOutUpRight className="size-3.5" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDuplicateDiagram();
|
||||||
|
}}
|
||||||
|
className="flex justify-between gap-4"
|
||||||
|
>
|
||||||
|
{t('menu.databases.duplicate')}
|
||||||
|
<Copy className="size-3.5" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteDiagram();
|
||||||
|
}}
|
||||||
|
className="flex items-center justify-between text-red-600 focus:text-red-600"
|
||||||
|
>
|
||||||
|
{t('menu.databases.delete_diagram')}
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
import { useDebounce } from '@/hooks/use-debounce';
|
import { useDebounce } from '@/hooks/use-debounce';
|
||||||
|
import { DiagramRowActionsMenu } from './diagram-row-actions-menu/diagram-row-actions-menu';
|
||||||
|
|
||||||
export interface OpenDiagramDialogProps extends BaseDialogProps {
|
export interface OpenDiagramDialogProps extends BaseDialogProps {
|
||||||
canClose?: boolean;
|
canClose?: boolean;
|
||||||
@@ -50,17 +51,18 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
setSelectedDiagramId(undefined);
|
setSelectedDiagramId(undefined);
|
||||||
}, [dialog.open]);
|
}, [dialog.open]);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchDiagrams = useCallback(async () => {
|
||||||
const fetchDiagrams = async () => {
|
|
||||||
const diagrams = await listDiagrams({ includeTables: true });
|
const diagrams = await listDiagrams({ includeTables: true });
|
||||||
setDiagrams(
|
setDiagrams(
|
||||||
diagrams.sort(
|
diagrams.sort(
|
||||||
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
|
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
}, [listDiagrams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
fetchDiagrams();
|
fetchDiagrams();
|
||||||
}, [listDiagrams, setDiagrams, dialog.open]);
|
}, [fetchDiagrams, dialog.open]);
|
||||||
|
|
||||||
const openDiagram = useCallback(
|
const openDiagram = useCallback(
|
||||||
(diagramId: string) => {
|
(diagramId: string) => {
|
||||||
@@ -221,6 +223,19 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{diagram.tables?.length}
|
{diagram.tables?.length}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="items-center p-0 pr-1 text-right">
|
||||||
|
<DiagramRowActionsMenu
|
||||||
|
diagram={diagram}
|
||||||
|
onOpen={() => {
|
||||||
|
openDiagram(diagram.id);
|
||||||
|
closeOpenDiagramDialog();
|
||||||
|
}}
|
||||||
|
refetch={fetchDiagrams}
|
||||||
|
onSelectDiagram={
|
||||||
|
setSelectedDiagramId
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||||
import { Label } from '@/components/label/label';
|
import { Label } from '@/components/label/label';
|
||||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
|
||||||
|
|
||||||
export interface TableSchemaDialogProps extends BaseDialogProps {
|
export interface TableSchemaDialogProps extends BaseDialogProps {
|
||||||
table?: DBTable;
|
table?: DBTable;
|
||||||
@@ -46,7 +45,6 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { databaseType } = useChartDB();
|
const { databaseType } = useChartDB();
|
||||||
const { addSchemaIfFiltered } = useDiagramFilter();
|
|
||||||
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
|
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
|
||||||
table?.schema
|
table?.schema
|
||||||
? schemaNameToSchemaId(table.schema)
|
? schemaNameToSchemaId(table.schema)
|
||||||
@@ -95,7 +93,6 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
|||||||
const { closeTableSchemaDialog } = useDialog();
|
const { closeTableSchemaDialog } = useDialog();
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
let createdSchemaId: string;
|
|
||||||
if (isCreatingNew && newSchemaName.trim()) {
|
if (isCreatingNew && newSchemaName.trim()) {
|
||||||
const newSchema: DBSchema = {
|
const newSchema: DBSchema = {
|
||||||
id: schemaNameToSchemaId(newSchemaName.trim()),
|
id: schemaNameToSchemaId(newSchemaName.trim()),
|
||||||
@@ -103,26 +100,14 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
|||||||
tableCount: 0,
|
tableCount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
createdSchemaId = newSchema.id;
|
|
||||||
|
|
||||||
onConfirm({ schema: newSchema });
|
onConfirm({ schema: newSchema });
|
||||||
} else {
|
} else {
|
||||||
const schema = schemas.find((s) => s.id === selectedSchemaId);
|
const schema = schemas.find((s) => s.id === selectedSchemaId);
|
||||||
if (!schema) return;
|
if (!schema) return;
|
||||||
|
|
||||||
createdSchemaId = schema.id;
|
|
||||||
onConfirm({ schema });
|
onConfirm({ schema });
|
||||||
}
|
}
|
||||||
|
}, [onConfirm, selectedSchemaId, schemas, isCreatingNew, newSchemaName]);
|
||||||
addSchemaIfFiltered(createdSchemaId);
|
|
||||||
}, [
|
|
||||||
onConfirm,
|
|
||||||
selectedSchemaId,
|
|
||||||
schemas,
|
|
||||||
isCreatingNew,
|
|
||||||
newSchemaName,
|
|
||||||
addSchemaIfFiltered,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const schemaOptions: SelectBoxOption[] = useMemo(
|
const schemaOptions: SelectBoxOption[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const ar: LanguageTranslation = {
|
export const ar: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'جديد',
|
||||||
|
browse: 'تصفح',
|
||||||
|
tables: 'الجداول',
|
||||||
|
refs: 'المراجع',
|
||||||
|
areas: 'المناطق',
|
||||||
|
dependencies: 'التبعيات',
|
||||||
|
custom_types: 'الأنواع المخصصة',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'ملف',
|
databases: 'قواعد البيانات',
|
||||||
new: 'جديد',
|
new: 'مخطط جديد',
|
||||||
open: 'فتح',
|
browse: 'تصفح...',
|
||||||
save: 'حفظ',
|
save: 'حفظ',
|
||||||
|
duplicate: 'تكرار',
|
||||||
import: 'استيراد قاعدة بيانات',
|
import: 'استيراد قاعدة بيانات',
|
||||||
export_sql: 'SQL تصدير',
|
export_sql: 'SQL تصدير',
|
||||||
export_as: 'تصدير كـ',
|
export_as: 'تصدير كـ',
|
||||||
delete_diagram: 'حذف الرسم البياني',
|
delete_diagram: 'حذف الرسم البياني',
|
||||||
exit: 'خروج',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'تحرير',
|
edit: 'تحرير',
|
||||||
@@ -29,6 +38,7 @@ export const ar: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'إخفاء خصائص الحقل',
|
hide_field_attributes: 'إخفاء خصائص الحقل',
|
||||||
show_field_attributes: 'إظهار خصائص الحقل',
|
show_field_attributes: 'إظهار خصائص الحقل',
|
||||||
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
|
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
|
||||||
|
show_views: 'عروض قاعدة البيانات',
|
||||||
theme: 'المظهر',
|
theme: 'المظهر',
|
||||||
show_dependencies: 'إظهار الاعتمادات',
|
show_dependencies: 'إظهار الاعتمادات',
|
||||||
hide_dependencies: 'إخفاء الاعتمادات',
|
hide_dependencies: 'إخفاء الاعتمادات',
|
||||||
@@ -110,6 +120,7 @@ export const ar: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'الجداول',
|
tables: 'الجداول',
|
||||||
add_table: 'إضافة جدول',
|
add_table: 'إضافة جدول',
|
||||||
|
add_view: 'إضافة عرض',
|
||||||
filter: 'تصفية',
|
filter: 'تصفية',
|
||||||
collapse: 'طي الكل',
|
collapse: 'طي الكل',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -135,6 +146,7 @@ export const ar: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'خصائص الحقل',
|
title: 'خصائص الحقل',
|
||||||
unique: 'فريد',
|
unique: 'فريد',
|
||||||
|
auto_increment: 'زيادة تلقائية',
|
||||||
comments: 'تعليقات',
|
comments: 'تعليقات',
|
||||||
no_comments: 'لا يوجد تعليقات',
|
no_comments: 'لا يوجد تعليقات',
|
||||||
delete_field: 'حذف الحقل',
|
delete_field: 'حذف الحقل',
|
||||||
@@ -149,6 +161,7 @@ export const ar: LanguageTranslation = {
|
|||||||
title: 'خصائص الفهرس',
|
title: 'خصائص الفهرس',
|
||||||
name: 'الإسم',
|
name: 'الإسم',
|
||||||
unique: 'فريد',
|
unique: 'فريد',
|
||||||
|
index_type: 'نوع الفهرس',
|
||||||
delete_index: 'حذف الفهرس',
|
delete_index: 'حذف الفهرس',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -165,12 +178,15 @@ export const ar: LanguageTranslation = {
|
|||||||
description: 'أنشئ جدولاً للبدء',
|
description: 'أنشئ جدولاً للبدء',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'العلاقات',
|
refs: 'المراجع',
|
||||||
filter: 'تصفية',
|
filter: 'تصفية',
|
||||||
add_relationship: 'إضافة علاقة',
|
|
||||||
collapse: 'طي الكل',
|
collapse: 'طي الكل',
|
||||||
|
add_relationship: 'إضافة علاقة',
|
||||||
|
relationships: 'العلاقات',
|
||||||
|
dependencies: 'الاعتمادات',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'العلاقة',
|
||||||
primary: 'الجدول الأساسي',
|
primary: 'الجدول الأساسي',
|
||||||
foreign: 'الجدول المرتبط',
|
foreign: 'الجدول المرتبط',
|
||||||
cardinality: 'الكاردينالية',
|
cardinality: 'الكاردينالية',
|
||||||
@@ -180,16 +196,8 @@ export const ar: LanguageTranslation = {
|
|||||||
delete_relationship: 'حذف',
|
delete_relationship: 'حذف',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'لا توجد علاقات',
|
|
||||||
description: 'إنشئ علاقة لربط الجداول',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'الاعتمادات',
|
|
||||||
filter: 'تصفية',
|
|
||||||
collapse: 'طي الكل',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'الاعتماد',
|
||||||
table: 'الجدول',
|
table: 'الجدول',
|
||||||
dependent_table: 'عرض الاعتمادات',
|
dependent_table: 'عرض الاعتمادات',
|
||||||
delete_dependency: 'حذف',
|
delete_dependency: 'حذف',
|
||||||
@@ -199,8 +207,8 @@ export const ar: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'لا توجد اعتمادات',
|
title: 'لا توجد علاقات',
|
||||||
description: 'إنشاء اعتماد للبدء',
|
description: 'إنشاء علاقة للبدء',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -461,6 +469,7 @@ export const ar: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'جدول جديد',
|
new_table: 'جدول جديد',
|
||||||
|
new_view: 'عرض جديد',
|
||||||
new_relationship: 'علاقة جديدة',
|
new_relationship: 'علاقة جديدة',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -482,6 +491,8 @@ export const ar: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'اللغة',
|
change_language: 'اللغة',
|
||||||
},
|
},
|
||||||
|
on: 'تشغيل',
|
||||||
|
off: 'إيقاف',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const bn: LanguageTranslation = {
|
export const bn: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'নতুন',
|
||||||
|
browse: 'ব্রাউজ',
|
||||||
|
tables: 'টেবিল',
|
||||||
|
refs: 'রেফস',
|
||||||
|
areas: 'এলাকা',
|
||||||
|
dependencies: 'নির্ভরতা',
|
||||||
|
custom_types: 'কাস্টম টাইপ',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'ফাইল',
|
databases: 'ডাটাবেস',
|
||||||
new: 'নতুন',
|
new: 'নতুন ডায়াগ্রাম',
|
||||||
open: 'খুলুন',
|
browse: 'ব্রাউজ করুন...',
|
||||||
save: 'সংরক্ষণ করুন',
|
save: 'সংরক্ষণ করুন',
|
||||||
|
duplicate: 'ডুপ্লিকেট করুন',
|
||||||
import: 'ডাটাবেস আমদানি করুন',
|
import: 'ডাটাবেস আমদানি করুন',
|
||||||
export_sql: 'SQL রপ্তানি করুন',
|
export_sql: 'SQL রপ্তানি করুন',
|
||||||
export_as: 'রূপে রপ্তানি করুন',
|
export_as: 'রূপে রপ্তানি করুন',
|
||||||
delete_diagram: 'ডায়াগ্রাম মুছুন',
|
delete_diagram: 'ডায়াগ্রাম মুছুন',
|
||||||
exit: 'প্রস্থান করুন',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'সম্পাদনা',
|
edit: 'সম্পাদনা',
|
||||||
@@ -29,6 +38,7 @@ export const bn: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
|
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
|
||||||
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
|
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
|
||||||
zoom_on_scroll: 'স্ক্রলে জুম করুন',
|
zoom_on_scroll: 'স্ক্রলে জুম করুন',
|
||||||
|
show_views: 'ডাটাবেস ভিউ',
|
||||||
theme: 'থিম',
|
theme: 'থিম',
|
||||||
show_dependencies: 'নির্ভরতাগুলি দেখান',
|
show_dependencies: 'নির্ভরতাগুলি দেখান',
|
||||||
hide_dependencies: 'নির্ভরতাগুলি লুকান',
|
hide_dependencies: 'নির্ভরতাগুলি লুকান',
|
||||||
@@ -111,6 +121,7 @@ export const bn: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'টেবিল',
|
tables: 'টেবিল',
|
||||||
add_table: 'টেবিল যোগ করুন',
|
add_table: 'টেবিল যোগ করুন',
|
||||||
|
add_view: 'ভিউ যোগ করুন',
|
||||||
filter: 'ফিল্টার',
|
filter: 'ফিল্টার',
|
||||||
collapse: 'সব ভাঁজ করুন',
|
collapse: 'সব ভাঁজ করুন',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -136,6 +147,7 @@ export const bn: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'ফিল্ড কর্ম',
|
title: 'ফিল্ড কর্ম',
|
||||||
unique: 'অদ্বিতীয়',
|
unique: 'অদ্বিতীয়',
|
||||||
|
auto_increment: 'স্বয়ংক্রিয় বৃদ্ধি',
|
||||||
comments: 'মন্তব্য',
|
comments: 'মন্তব্য',
|
||||||
no_comments: 'কোনো মন্তব্য নেই',
|
no_comments: 'কোনো মন্তব্য নেই',
|
||||||
delete_field: 'ফিল্ড মুছুন',
|
delete_field: 'ফিল্ড মুছুন',
|
||||||
@@ -151,6 +163,7 @@ export const bn: LanguageTranslation = {
|
|||||||
title: 'ইনডেক্স কর্ম',
|
title: 'ইনডেক্স কর্ম',
|
||||||
name: 'নাম',
|
name: 'নাম',
|
||||||
unique: 'অদ্বিতীয়',
|
unique: 'অদ্বিতীয়',
|
||||||
|
index_type: 'ইনডেক্স ধরন',
|
||||||
delete_index: 'ইনডেক্স মুছুন',
|
delete_index: 'ইনডেক্স মুছুন',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -167,14 +180,17 @@ export const bn: LanguageTranslation = {
|
|||||||
description: 'শুরু করতে একটি টেবিল তৈরি করুন',
|
description: 'শুরু করতে একটি টেবিল তৈরি করুন',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'সম্পর্ক',
|
refs: 'রেফস',
|
||||||
filter: 'ফিল্টার',
|
filter: 'ফিল্টার',
|
||||||
add_relationship: 'সম্পর্ক যোগ করুন',
|
|
||||||
collapse: 'সব ভাঁজ করুন',
|
collapse: 'সব ভাঁজ করুন',
|
||||||
|
add_relationship: 'সম্পর্ক যোগ করুন',
|
||||||
|
relationships: 'সম্পর্ক',
|
||||||
|
dependencies: 'নির্ভরতাগুলি',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'সম্পর্ক',
|
||||||
primary: 'প্রাথমিক টেবিল',
|
primary: 'প্রাথমিক টেবিল',
|
||||||
foreign: 'বিদেশি টেবিল',
|
foreign: 'রেফারেন্স করা টেবিল',
|
||||||
cardinality: 'কার্ডিনালিটি',
|
cardinality: 'কার্ডিনালিটি',
|
||||||
delete_relationship: 'মুছুন',
|
delete_relationship: 'মুছুন',
|
||||||
relationship_actions: {
|
relationship_actions: {
|
||||||
@@ -182,27 +198,19 @@ export const bn: LanguageTranslation = {
|
|||||||
delete_relationship: 'মুছুন',
|
delete_relationship: 'মুছুন',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'কোনো সম্পর্ক নেই',
|
|
||||||
description: 'টেবিল সংযোগ করতে একটি সম্পর্ক তৈরি করুন',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'নির্ভরতাগুলি',
|
|
||||||
filter: 'ফিল্টার',
|
|
||||||
collapse: 'ভাঁজ করুন',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'নির্ভরতা',
|
||||||
table: 'টেবিল',
|
table: 'টেবিল',
|
||||||
dependent_table: 'নির্ভরশীল টেবিল',
|
dependent_table: 'নির্ভরশীল ভিউ',
|
||||||
delete_dependency: 'নির্ভরতা মুছুন',
|
delete_dependency: 'মুছুন',
|
||||||
dependency_actions: {
|
dependency_actions: {
|
||||||
title: 'কর্ম',
|
title: 'কর্ম',
|
||||||
delete_dependency: 'নির্ভরতা মুছুন',
|
delete_dependency: 'মুছুন',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'কোনো নির্ভরতাগুলি নেই',
|
title: 'কোনো সম্পর্ক নেই',
|
||||||
description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।',
|
description: 'শুরু করতে একটি সম্পর্ক তৈরি করুন',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -466,6 +474,7 @@ export const bn: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'নতুন টেবিল',
|
new_table: 'নতুন টেবিল',
|
||||||
|
new_view: 'নতুন ভিউ',
|
||||||
new_relationship: 'নতুন সম্পর্ক',
|
new_relationship: 'নতুন সম্পর্ক',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -487,6 +496,9 @@ export const bn: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'ভাষা পরিবর্তন করুন',
|
change_language: 'ভাষা পরিবর্তন করুন',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'চালু',
|
||||||
|
off: 'বন্ধ',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const de: LanguageTranslation = {
|
export const de: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'Neu',
|
||||||
|
browse: 'Durchsuchen',
|
||||||
|
tables: 'Tabellen',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'Bereiche',
|
||||||
|
dependencies: 'Abhängigkeiten',
|
||||||
|
custom_types: 'Benutzerdefinierte Typen',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'Datei',
|
databases: 'Datenbanken',
|
||||||
new: 'Neu',
|
new: 'Neues Diagramm',
|
||||||
open: 'Öffnen',
|
browse: 'Durchsuchen...',
|
||||||
save: 'Speichern',
|
save: 'Speichern',
|
||||||
|
duplicate: 'Diagramm duplizieren',
|
||||||
import: 'Datenbank importieren',
|
import: 'Datenbank importieren',
|
||||||
export_sql: 'SQL exportieren',
|
export_sql: 'SQL exportieren',
|
||||||
export_as: 'Exportieren als',
|
export_as: 'Exportieren als',
|
||||||
delete_diagram: 'Diagramm löschen',
|
delete_diagram: 'Diagramm löschen',
|
||||||
exit: 'Beenden',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Bearbeiten',
|
edit: 'Bearbeiten',
|
||||||
@@ -29,6 +38,7 @@ export const de: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'Feldattribute ausblenden',
|
hide_field_attributes: 'Feldattribute ausblenden',
|
||||||
show_field_attributes: 'Feldattribute anzeigen',
|
show_field_attributes: 'Feldattribute anzeigen',
|
||||||
zoom_on_scroll: 'Zoom beim Scrollen',
|
zoom_on_scroll: 'Zoom beim Scrollen',
|
||||||
|
show_views: 'Datenbankansichten',
|
||||||
theme: 'Stil',
|
theme: 'Stil',
|
||||||
show_dependencies: 'Abhängigkeiten anzeigen',
|
show_dependencies: 'Abhängigkeiten anzeigen',
|
||||||
hide_dependencies: 'Abhängigkeiten ausblenden',
|
hide_dependencies: 'Abhängigkeiten ausblenden',
|
||||||
@@ -112,6 +122,7 @@ export const de: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Tabellen',
|
tables: 'Tabellen',
|
||||||
add_table: 'Tabelle hinzufügen',
|
add_table: 'Tabelle hinzufügen',
|
||||||
|
add_view: 'Ansicht hinzufügen',
|
||||||
filter: 'Filter',
|
filter: 'Filter',
|
||||||
collapse: 'Alle einklappen',
|
collapse: 'Alle einklappen',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -137,6 +148,7 @@ export const de: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Feldattribute',
|
title: 'Feldattribute',
|
||||||
unique: 'Eindeutig',
|
unique: 'Eindeutig',
|
||||||
|
auto_increment: 'Automatisch hochzählen',
|
||||||
comments: 'Kommentare',
|
comments: 'Kommentare',
|
||||||
no_comments: 'Keine Kommentare',
|
no_comments: 'Keine Kommentare',
|
||||||
delete_field: 'Feld löschen',
|
delete_field: 'Feld löschen',
|
||||||
@@ -152,6 +164,7 @@ export const de: LanguageTranslation = {
|
|||||||
title: 'Indexattribute',
|
title: 'Indexattribute',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
unique: 'Eindeutig',
|
unique: 'Eindeutig',
|
||||||
|
index_type: 'Indextyp',
|
||||||
delete_index: 'Index löschen',
|
delete_index: 'Index löschen',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -168,32 +181,26 @@ export const de: LanguageTranslation = {
|
|||||||
description: 'Erstellen Sie eine Tabelle, um zu beginnen',
|
description: 'Erstellen Sie eine Tabelle, um zu beginnen',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'Beziehungen',
|
refs: 'Refs',
|
||||||
filter: 'Filter',
|
filter: 'Filter',
|
||||||
add_relationship: 'Beziehung hinzufügen',
|
|
||||||
collapse: 'Alle einklappen',
|
collapse: 'Alle einklappen',
|
||||||
|
add_relationship: 'Beziehung hinzufügen',
|
||||||
|
relationships: 'Beziehungen',
|
||||||
|
dependencies: 'Abhängigkeiten',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'Beziehung',
|
||||||
primary: 'Primäre Tabelle',
|
primary: 'Primäre Tabelle',
|
||||||
foreign: 'Referenzierte Tabelle',
|
foreign: 'Referenzierte Tabelle',
|
||||||
cardinality: 'Kardinalität',
|
cardinality: 'Kardinalität',
|
||||||
delete_relationship: 'Beziehung löschen',
|
delete_relationship: 'Löschen',
|
||||||
relationship_actions: {
|
relationship_actions: {
|
||||||
title: 'Aktionen',
|
title: 'Aktionen',
|
||||||
delete_relationship: 'Beziehung löschen',
|
delete_relationship: 'Löschen',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'Keine Beziehungen',
|
|
||||||
description:
|
|
||||||
'Erstellen Sie eine Beziehung, um Tabellen zu verbinden',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Abhängigkeiten',
|
|
||||||
filter: 'Filter',
|
|
||||||
collapse: 'Alle einklappen',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'Abhängigkeit',
|
||||||
table: 'Tabelle',
|
table: 'Tabelle',
|
||||||
dependent_table: 'Abhängige Ansicht',
|
dependent_table: 'Abhängige Ansicht',
|
||||||
delete_dependency: 'Löschen',
|
delete_dependency: 'Löschen',
|
||||||
@@ -203,8 +210,8 @@ export const de: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Keine Abhängigkeiten',
|
title: 'Keine Beziehungen',
|
||||||
description: 'Erstellen Sie eine Ansicht, um zu beginnen',
|
description: 'Erstellen Sie eine Beziehung, um zu beginnen',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -298,7 +305,7 @@ export const de: LanguageTranslation = {
|
|||||||
step_1: 'Gehen Sie zu Tools > Optionen > Abfrageergebnisse > SQL Server.',
|
step_1: 'Gehen Sie zu Tools > Optionen > Abfrageergebnisse > SQL Server.',
|
||||||
step_2: 'Wenn Sie "Ergebnisse in Raster" verwenden, ändern Sie die maximale Zeichenanzahl für Nicht-XML-Daten (auf 9999999 setzen).',
|
step_2: 'Wenn Sie "Ergebnisse in Raster" verwenden, ändern Sie die maximale Zeichenanzahl für Nicht-XML-Daten (auf 9999999 setzen).',
|
||||||
},
|
},
|
||||||
instructions_link: 'Brauchen Sie Hilfe? So geht’s',
|
instructions_link: "Brauchen Sie Hilfe? So geht's",
|
||||||
check_script_result: 'Skriptergebnis überprüfen',
|
check_script_result: 'Skriptergebnis überprüfen',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -470,6 +477,7 @@ export const de: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Neue Tabelle',
|
new_table: 'Neue Tabelle',
|
||||||
|
new_view: 'Neue Ansicht',
|
||||||
new_relationship: 'Neue Beziehung',
|
new_relationship: 'Neue Beziehung',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -492,6 +500,9 @@ export const de: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Sprache',
|
change_language: 'Sprache',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'Ein',
|
||||||
|
off: 'Aus',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata } from '../types';
|
|||||||
|
|
||||||
export const en = {
|
export const en = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'New',
|
||||||
|
browse: 'Browse',
|
||||||
|
tables: 'Tables',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'Areas',
|
||||||
|
dependencies: 'Dependencies',
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'File',
|
databases: 'Databases',
|
||||||
new: 'New',
|
new: 'New Diagram',
|
||||||
open: 'Open',
|
browse: 'Browse...',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
|
duplicate: 'Duplicate Diagram',
|
||||||
import: 'Import',
|
import: 'Import',
|
||||||
export_sql: 'Export SQL',
|
export_sql: 'Export SQL',
|
||||||
export_as: 'Export as',
|
export_as: 'Export as',
|
||||||
delete_diagram: 'Delete Diagram',
|
delete_diagram: 'Delete Diagram',
|
||||||
exit: 'Exit',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
@@ -29,6 +38,7 @@ export const en = {
|
|||||||
hide_field_attributes: 'Hide Field Attributes',
|
hide_field_attributes: 'Hide Field Attributes',
|
||||||
show_field_attributes: 'Show Field Attributes',
|
show_field_attributes: 'Show Field Attributes',
|
||||||
zoom_on_scroll: 'Zoom on Scroll',
|
zoom_on_scroll: 'Zoom on Scroll',
|
||||||
|
show_views: 'Database Views',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
show_dependencies: 'Show Dependencies',
|
show_dependencies: 'Show Dependencies',
|
||||||
hide_dependencies: 'Hide Dependencies',
|
hide_dependencies: 'Hide Dependencies',
|
||||||
@@ -109,6 +119,7 @@ export const en = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Tables',
|
tables: 'Tables',
|
||||||
add_table: 'Add Table',
|
add_table: 'Add Table',
|
||||||
|
add_view: 'Add View',
|
||||||
filter: 'Filter',
|
filter: 'Filter',
|
||||||
collapse: 'Collapse All',
|
collapse: 'Collapse All',
|
||||||
clear: 'Clear Filter',
|
clear: 'Clear Filter',
|
||||||
@@ -132,6 +143,7 @@ export const en = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Field Attributes',
|
title: 'Field Attributes',
|
||||||
unique: 'Unique',
|
unique: 'Unique',
|
||||||
|
auto_increment: 'Auto Increment',
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
precision: 'Precision',
|
precision: 'Precision',
|
||||||
scale: 'Scale',
|
scale: 'Scale',
|
||||||
@@ -145,6 +157,7 @@ export const en = {
|
|||||||
title: 'Index Attributes',
|
title: 'Index Attributes',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
unique: 'Unique',
|
unique: 'Unique',
|
||||||
|
index_type: 'Index Type',
|
||||||
delete_index: 'Delete Index',
|
delete_index: 'Delete Index',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -161,12 +174,15 @@ export const en = {
|
|||||||
description: 'Create a table to get started',
|
description: 'Create a table to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'Relationships',
|
refs: 'Refs',
|
||||||
filter: 'Filter',
|
filter: 'Filter',
|
||||||
add_relationship: 'Add Relationship',
|
|
||||||
collapse: 'Collapse All',
|
collapse: 'Collapse All',
|
||||||
|
add_relationship: 'Add Relationship',
|
||||||
|
relationships: 'Relationships',
|
||||||
|
dependencies: 'Dependencies',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'Relationship',
|
||||||
primary: 'Primary Table',
|
primary: 'Primary Table',
|
||||||
foreign: 'Referenced Table',
|
foreign: 'Referenced Table',
|
||||||
cardinality: 'Cardinality',
|
cardinality: 'Cardinality',
|
||||||
@@ -176,16 +192,8 @@ export const en = {
|
|||||||
delete_relationship: 'Delete',
|
delete_relationship: 'Delete',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'No relationships',
|
|
||||||
description: 'Create a relationship to connect tables',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Dependencies',
|
|
||||||
filter: 'Filter',
|
|
||||||
collapse: 'Collapse All',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'Dependency',
|
||||||
table: 'Table',
|
table: 'Table',
|
||||||
dependent_table: 'Dependent View',
|
dependent_table: 'Dependent View',
|
||||||
delete_dependency: 'Delete',
|
delete_dependency: 'Delete',
|
||||||
@@ -195,8 +203,8 @@ export const en = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'No dependencies',
|
title: 'No relationships',
|
||||||
description: 'Create a view to get started',
|
description: 'Create a relationship to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -456,6 +464,7 @@ export const en = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'New Table',
|
new_table: 'New Table',
|
||||||
|
new_view: 'New View',
|
||||||
new_relationship: 'New Relationship',
|
new_relationship: 'New Relationship',
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
},
|
},
|
||||||
@@ -476,6 +485,9 @@ export const en = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Language',
|
change_language: 'Language',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'On',
|
||||||
|
off: 'Off',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const es: LanguageTranslation = {
|
export const es: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'Nuevo',
|
||||||
|
browse: 'Examinar',
|
||||||
|
tables: 'Tablas',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'Áreas',
|
||||||
|
dependencies: 'Dependencias',
|
||||||
|
custom_types: 'Tipos Personalizados',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'Archivo',
|
databases: 'Bases de Datos',
|
||||||
new: 'Nuevo',
|
new: 'Nuevo Diagrama',
|
||||||
open: 'Abrir',
|
browse: 'Examinar...',
|
||||||
save: 'Guardar',
|
save: 'Guardar',
|
||||||
|
duplicate: 'Duplicar',
|
||||||
import: 'Importar Base de Datos',
|
import: 'Importar Base de Datos',
|
||||||
export_sql: 'Exportar SQL',
|
export_sql: 'Exportar SQL',
|
||||||
export_as: 'Exportar como',
|
export_as: 'Exportar como',
|
||||||
delete_diagram: 'Eliminar Diagrama',
|
delete_diagram: 'Eliminar Diagrama',
|
||||||
exit: 'Salir',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Editar',
|
edit: 'Editar',
|
||||||
@@ -29,6 +38,7 @@ export const es: LanguageTranslation = {
|
|||||||
show_sidebar: 'Mostrar Barra Lateral',
|
show_sidebar: 'Mostrar Barra Lateral',
|
||||||
hide_sidebar: 'Ocultar Barra Lateral',
|
hide_sidebar: 'Ocultar Barra Lateral',
|
||||||
zoom_on_scroll: 'Zoom al Desplazarse',
|
zoom_on_scroll: 'Zoom al Desplazarse',
|
||||||
|
show_views: 'Vistas de Base de Datos',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Mostrar dependencias',
|
show_dependencies: 'Mostrar dependencias',
|
||||||
hide_dependencies: 'Ocultar dependencias',
|
hide_dependencies: 'Ocultar dependencias',
|
||||||
@@ -110,6 +120,7 @@ export const es: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Tablas',
|
tables: 'Tablas',
|
||||||
add_table: 'Agregar Tabla',
|
add_table: 'Agregar Tabla',
|
||||||
|
add_view: 'Agregar Vista',
|
||||||
filter: 'Filtrar',
|
filter: 'Filtrar',
|
||||||
collapse: 'Colapsar Todo',
|
collapse: 'Colapsar Todo',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -135,6 +146,7 @@ export const es: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Atributos del Campo',
|
title: 'Atributos del Campo',
|
||||||
unique: 'Único',
|
unique: 'Único',
|
||||||
|
auto_increment: 'Autoincremento',
|
||||||
comments: 'Comentarios',
|
comments: 'Comentarios',
|
||||||
no_comments: 'Sin comentarios',
|
no_comments: 'Sin comentarios',
|
||||||
delete_field: 'Eliminar Campo',
|
delete_field: 'Eliminar Campo',
|
||||||
@@ -150,6 +162,7 @@ export const es: LanguageTranslation = {
|
|||||||
title: 'Atributos del Índice',
|
title: 'Atributos del Índice',
|
||||||
name: 'Nombre',
|
name: 'Nombre',
|
||||||
unique: 'Único',
|
unique: 'Único',
|
||||||
|
index_type: 'Tipo de Índice',
|
||||||
delete_index: 'Eliminar Índice',
|
delete_index: 'Eliminar Índice',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -166,14 +179,17 @@ export const es: LanguageTranslation = {
|
|||||||
description: 'Crea una tabla para comenzar',
|
description: 'Crea una tabla para comenzar',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'Relaciones',
|
refs: 'Refs',
|
||||||
add_relationship: 'Agregar Relación',
|
|
||||||
filter: 'Filtrar',
|
filter: 'Filtrar',
|
||||||
collapse: 'Colapsar Todo',
|
collapse: 'Colapsar Todo',
|
||||||
|
add_relationship: 'Agregar Relación',
|
||||||
|
relationships: 'Relaciones',
|
||||||
|
dependencies: 'Dependencias',
|
||||||
relationship: {
|
relationship: {
|
||||||
primary: 'Primaria',
|
relationship: 'Relación',
|
||||||
foreign: 'Foránea',
|
primary: 'Tabla Primaria',
|
||||||
|
foreign: 'Tabla Referenciada',
|
||||||
cardinality: 'Cardinalidad',
|
cardinality: 'Cardinalidad',
|
||||||
delete_relationship: 'Eliminar',
|
delete_relationship: 'Eliminar',
|
||||||
relationship_actions: {
|
relationship_actions: {
|
||||||
@@ -181,18 +197,10 @@ export const es: LanguageTranslation = {
|
|||||||
delete_relationship: 'Eliminar',
|
delete_relationship: 'Eliminar',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'No hay relaciones',
|
|
||||||
description: 'Crea una relación para conectar tablas',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Dependencias',
|
|
||||||
filter: 'Filtro',
|
|
||||||
collapse: 'Colapsar todo',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'Dependencia',
|
||||||
table: 'Tabla',
|
table: 'Tabla',
|
||||||
dependent_table: 'Vista dependiente',
|
dependent_table: 'Vista Dependiente',
|
||||||
delete_dependency: 'Eliminar',
|
delete_dependency: 'Eliminar',
|
||||||
dependency_actions: {
|
dependency_actions: {
|
||||||
title: 'Acciones',
|
title: 'Acciones',
|
||||||
@@ -200,8 +208,8 @@ export const es: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Sin dependencias',
|
title: 'Sin relaciones',
|
||||||
description: 'Crea una vista para comenzar',
|
description: 'Crea una relación para comenzar',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -468,6 +476,7 @@ export const es: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Nueva Tabla',
|
new_table: 'Nueva Tabla',
|
||||||
|
new_view: 'Nueva Vista',
|
||||||
new_relationship: 'Nueva Relación',
|
new_relationship: 'Nueva Relación',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -490,6 +499,9 @@ export const es: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Idioma',
|
change_language: 'Idioma',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'Encendido',
|
||||||
|
off: 'Apagado',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const fr: LanguageTranslation = {
|
export const fr: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'Nouveau',
|
||||||
|
browse: 'Parcourir',
|
||||||
|
tables: 'Tables',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'Zones',
|
||||||
|
dependencies: 'Dépendances',
|
||||||
|
custom_types: 'Types Personnalisés',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'Fichier',
|
databases: 'Bases de Données',
|
||||||
new: 'Nouveau',
|
new: 'Nouveau Diagramme',
|
||||||
open: 'Ouvrir',
|
browse: 'Parcourir...',
|
||||||
save: 'Enregistrer',
|
save: 'Enregistrer',
|
||||||
|
duplicate: 'Dupliquer',
|
||||||
import: 'Importer Base de Données',
|
import: 'Importer Base de Données',
|
||||||
export_sql: 'Exporter SQL',
|
export_sql: 'Exporter SQL',
|
||||||
export_as: 'Exporter en tant que',
|
export_as: 'Exporter en tant que',
|
||||||
delete_diagram: 'Supprimer le Diagramme',
|
delete_diagram: 'Supprimer le Diagramme',
|
||||||
exit: 'Quitter',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Édition',
|
edit: 'Édition',
|
||||||
@@ -29,6 +38,7 @@ export const fr: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'Masquer les Attributs de Champ',
|
hide_field_attributes: 'Masquer les Attributs de Champ',
|
||||||
show_field_attributes: 'Afficher les Attributs de Champ',
|
show_field_attributes: 'Afficher les Attributs de Champ',
|
||||||
zoom_on_scroll: 'Zoom sur le Défilement',
|
zoom_on_scroll: 'Zoom sur le Défilement',
|
||||||
|
show_views: 'Vues de Base de Données',
|
||||||
theme: 'Thème',
|
theme: 'Thème',
|
||||||
show_dependencies: 'Afficher les Dépendances',
|
show_dependencies: 'Afficher les Dépendances',
|
||||||
hide_dependencies: 'Masquer les Dépendances',
|
hide_dependencies: 'Masquer les Dépendances',
|
||||||
@@ -109,6 +119,7 @@ export const fr: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Tables',
|
tables: 'Tables',
|
||||||
add_table: 'Ajouter une Table',
|
add_table: 'Ajouter une Table',
|
||||||
|
add_view: 'Ajouter une Vue',
|
||||||
filter: 'Filtrer',
|
filter: 'Filtrer',
|
||||||
collapse: 'Réduire Tout',
|
collapse: 'Réduire Tout',
|
||||||
clear: 'Effacer le Filtre',
|
clear: 'Effacer le Filtre',
|
||||||
@@ -133,6 +144,7 @@ export const fr: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Attributs du Champ',
|
title: 'Attributs du Champ',
|
||||||
unique: 'Unique',
|
unique: 'Unique',
|
||||||
|
auto_increment: 'Auto-incrément',
|
||||||
comments: 'Commentaires',
|
comments: 'Commentaires',
|
||||||
no_comments: 'Pas de commentaires',
|
no_comments: 'Pas de commentaires',
|
||||||
delete_field: 'Supprimer le Champ',
|
delete_field: 'Supprimer le Champ',
|
||||||
@@ -148,6 +160,7 @@ export const fr: LanguageTranslation = {
|
|||||||
title: "Attributs de l'Index",
|
title: "Attributs de l'Index",
|
||||||
name: 'Nom',
|
name: 'Nom',
|
||||||
unique: 'Unique',
|
unique: 'Unique',
|
||||||
|
index_type: "Type d'index",
|
||||||
delete_index: "Supprimer l'Index",
|
delete_index: "Supprimer l'Index",
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -164,12 +177,15 @@ export const fr: LanguageTranslation = {
|
|||||||
description: 'Créez une table pour commencer',
|
description: 'Créez une table pour commencer',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'Relations',
|
refs: 'Refs',
|
||||||
filter: 'Filtrer',
|
filter: 'Filtrer',
|
||||||
add_relationship: 'Ajouter une Relation',
|
|
||||||
collapse: 'Réduire Tout',
|
collapse: 'Réduire Tout',
|
||||||
|
add_relationship: 'Ajouter une Relation',
|
||||||
|
relationships: 'Relations',
|
||||||
|
dependencies: 'Dépendances',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'Relation',
|
||||||
primary: 'Table Principale',
|
primary: 'Table Principale',
|
||||||
foreign: 'Table Référencée',
|
foreign: 'Table Référencée',
|
||||||
cardinality: 'Cardinalité',
|
cardinality: 'Cardinalité',
|
||||||
@@ -179,16 +195,8 @@ export const fr: LanguageTranslation = {
|
|||||||
delete_relationship: 'Supprimer',
|
delete_relationship: 'Supprimer',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'Aucune relation',
|
|
||||||
description: 'Créez une relation pour connecter les tables',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Dépendances',
|
|
||||||
filter: 'Filtrer',
|
|
||||||
collapse: 'Réduire Tout',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'Dépendance',
|
||||||
table: 'Table',
|
table: 'Table',
|
||||||
dependent_table: 'Vue Dépendante',
|
dependent_table: 'Vue Dépendante',
|
||||||
delete_dependency: 'Supprimer',
|
delete_dependency: 'Supprimer',
|
||||||
@@ -198,8 +206,8 @@ export const fr: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Aucune dépendance',
|
title: 'Aucune relation',
|
||||||
description: 'Créez une vue pour commencer',
|
description: 'Créez une relation pour commencer',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -464,6 +472,7 @@ export const fr: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Nouvelle Table',
|
new_table: 'Nouvelle Table',
|
||||||
|
new_view: 'Nouvelle Vue',
|
||||||
new_relationship: 'Nouvelle Relation',
|
new_relationship: 'Nouvelle Relation',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -486,6 +495,9 @@ export const fr: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Langue',
|
change_language: 'Langue',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'Activé',
|
||||||
|
off: 'Désactivé',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const gu: LanguageTranslation = {
|
export const gu: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'નવું',
|
||||||
|
browse: 'બ્રાઉજ',
|
||||||
|
tables: 'ટેબલો',
|
||||||
|
refs: 'રેફ્સ',
|
||||||
|
areas: 'ક્ષેત્રો',
|
||||||
|
dependencies: 'નિર્ભરતાઓ',
|
||||||
|
custom_types: 'કસ્ટમ ટાઇપ',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'ફાઇલ',
|
databases: 'ડેટાબેસેસ',
|
||||||
new: 'નવું',
|
new: 'નવું ડાયાગ્રામ',
|
||||||
open: 'ખોલો',
|
browse: 'બ્રાઉજ કરો...',
|
||||||
save: 'સાચવો',
|
save: 'સાચવો',
|
||||||
|
duplicate: 'ડુપ્લિકેટ',
|
||||||
import: 'ડેટાબેસ આયાત કરો',
|
import: 'ડેટાબેસ આયાત કરો',
|
||||||
export_sql: 'SQL નિકાસ કરો',
|
export_sql: 'SQL નિકાસ કરો',
|
||||||
export_as: 'રૂપે નિકાસ કરો',
|
export_as: 'રૂપે નિકાસ કરો',
|
||||||
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
|
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
|
||||||
exit: 'બહાર જાઓ',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'ફેરફાર',
|
edit: 'ફેરફાર',
|
||||||
@@ -29,6 +38,7 @@ export const gu: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
|
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
|
||||||
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
|
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
|
||||||
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
|
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
|
||||||
|
show_views: 'ડેટાબેઝ વ્યૂઝ',
|
||||||
theme: 'થિમ',
|
theme: 'થિમ',
|
||||||
show_dependencies: 'નિર્ભરતાઓ બતાવો',
|
show_dependencies: 'નિર્ભરતાઓ બતાવો',
|
||||||
hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
|
hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
|
||||||
@@ -111,6 +121,7 @@ export const gu: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'ટેબલ્સ',
|
tables: 'ટેબલ્સ',
|
||||||
add_table: 'ટેબલ ઉમેરો',
|
add_table: 'ટેબલ ઉમેરો',
|
||||||
|
add_view: 'વ્યૂ ઉમેરો',
|
||||||
filter: 'ફિલ્ટર',
|
filter: 'ફિલ્ટર',
|
||||||
collapse: 'બધાને સકુચિત કરો',
|
collapse: 'બધાને સકુચિત કરો',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -137,6 +148,7 @@ export const gu: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'ફીલ્ડ લક્ષણો',
|
title: 'ફીલ્ડ લક્ષણો',
|
||||||
unique: 'અદ્વિતીય',
|
unique: 'અદ્વિતીય',
|
||||||
|
auto_increment: 'ઑટો ઇન્ક્રિમેન્ટ',
|
||||||
comments: 'ટિપ્પણીઓ',
|
comments: 'ટિપ્પણીઓ',
|
||||||
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
||||||
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
||||||
@@ -152,6 +164,7 @@ export const gu: LanguageTranslation = {
|
|||||||
title: 'ઇન્ડેક્સ લક્ષણો',
|
title: 'ઇન્ડેક્સ લક્ષણો',
|
||||||
name: 'નામ',
|
name: 'નામ',
|
||||||
unique: 'અદ્વિતીય',
|
unique: 'અદ્વિતીય',
|
||||||
|
index_type: 'ઇન્ડેક્સ પ્રકાર',
|
||||||
delete_index: 'ઇન્ડેક્સ કાઢી નાખો',
|
delete_index: 'ઇન્ડેક્સ કાઢી નાખો',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -168,14 +181,17 @@ export const gu: LanguageTranslation = {
|
|||||||
description: 'શરૂ કરવા માટે એક ટેબલ બનાવો',
|
description: 'શરૂ કરવા માટે એક ટેબલ બનાવો',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'સંબંધો',
|
refs: 'રેફ્સ',
|
||||||
filter: 'ફિલ્ટર',
|
filter: 'ફિલ્ટર',
|
||||||
add_relationship: 'સંબંધ ઉમેરો',
|
|
||||||
collapse: 'બધાને સકુચિત કરો',
|
collapse: 'બધાને સકુચિત કરો',
|
||||||
|
add_relationship: 'સંબંધ ઉમેરો',
|
||||||
|
relationships: 'સંબંધો',
|
||||||
|
dependencies: 'નિર્ભરતાઓ',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'સંબંધ',
|
||||||
primary: 'પ્રાથમિક ટેબલ',
|
primary: 'પ્રાથમિક ટેબલ',
|
||||||
foreign: 'સંદર્ભ ટેબલ',
|
foreign: 'સંદર્ભિત ટેબલ',
|
||||||
cardinality: 'કાર્ડિનાલિટી',
|
cardinality: 'કાર્ડિનાલિટી',
|
||||||
delete_relationship: 'કાઢી નાખો',
|
delete_relationship: 'કાઢી નાખો',
|
||||||
relationship_actions: {
|
relationship_actions: {
|
||||||
@@ -183,27 +199,19 @@ export const gu: LanguageTranslation = {
|
|||||||
delete_relationship: 'કાઢી નાખો',
|
delete_relationship: 'કાઢી નાખો',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'કોઈ સંબંધો નથી',
|
|
||||||
description: 'ટેબલ્સ કનેક્ટ કરવા માટે એક સંબંધ બનાવો',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'નિર્ભરતાઓ',
|
|
||||||
filter: 'ફિલ્ટર',
|
|
||||||
collapse: 'સિકોડો',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'નિર્ભરતા',
|
||||||
table: 'ટેબલ',
|
table: 'ટેબલ',
|
||||||
dependent_table: 'આધાર રાખેલું ટેબલ',
|
dependent_table: 'નિર્ભરશીલ વ્યૂ',
|
||||||
delete_dependency: 'નિર્ભરતા કાઢી નાખો',
|
delete_dependency: 'કાઢી નાખો',
|
||||||
dependency_actions: {
|
dependency_actions: {
|
||||||
title: 'ક્રિયાઓ',
|
title: 'ક્રિયાઓ',
|
||||||
delete_dependency: 'નિર્ભરતા કાઢી નાખો',
|
delete_dependency: 'કાઢી નાખો',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'કોઈ નિર્ભરતાઓ નથી',
|
title: 'કોઈ સંબંધો નથી',
|
||||||
description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.',
|
description: 'શરૂ કરવા માટે એક સંબંધ બનાવો',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -467,6 +475,7 @@ export const gu: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'નવું ટેબલ',
|
new_table: 'નવું ટેબલ',
|
||||||
|
new_view: 'નવું વ્યૂ',
|
||||||
new_relationship: 'નવો સંબંધ',
|
new_relationship: 'નવો સંબંધ',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -488,6 +497,9 @@ export const gu: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'ભાષા બદલો',
|
change_language: 'ભાષા બદલો',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'ચાલુ',
|
||||||
|
off: 'બંધ',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const hi: LanguageTranslation = {
|
export const hi: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'नया',
|
||||||
|
browse: 'ब्राउज़',
|
||||||
|
tables: 'टेबल',
|
||||||
|
refs: 'रेफ्स',
|
||||||
|
areas: 'क्षेत्र',
|
||||||
|
dependencies: 'निर्भरताएं',
|
||||||
|
custom_types: 'कस्टम टाइप',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'फ़ाइल',
|
databases: 'डेटाबेस',
|
||||||
new: 'नया',
|
new: 'नया आरेख',
|
||||||
open: 'खोलें',
|
browse: 'ब्राउज़ करें...',
|
||||||
save: 'सहेजें',
|
save: 'सहेजें',
|
||||||
|
duplicate: 'डुप्लिकेट',
|
||||||
import: 'डेटाबेस आयात करें',
|
import: 'डेटाबेस आयात करें',
|
||||||
export_sql: 'SQL निर्यात करें',
|
export_sql: 'SQL निर्यात करें',
|
||||||
export_as: 'के रूप में निर्यात करें',
|
export_as: 'के रूप में निर्यात करें',
|
||||||
delete_diagram: 'आरेख हटाएँ',
|
delete_diagram: 'आरेख हटाएँ',
|
||||||
exit: 'बाहर जाएँ',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'संपादित करें',
|
edit: 'संपादित करें',
|
||||||
@@ -29,6 +38,7 @@ export const hi: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
|
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
|
||||||
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
|
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
|
||||||
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
|
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
|
||||||
|
show_views: 'डेटाबेस व्यू',
|
||||||
theme: 'थीम',
|
theme: 'थीम',
|
||||||
show_dependencies: 'निर्भरता दिखाएँ',
|
show_dependencies: 'निर्भरता दिखाएँ',
|
||||||
hide_dependencies: 'निर्भरता छिपाएँ',
|
hide_dependencies: 'निर्भरता छिपाएँ',
|
||||||
@@ -111,6 +121,7 @@ export const hi: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'तालिकाएँ',
|
tables: 'तालिकाएँ',
|
||||||
add_table: 'तालिका जोड़ें',
|
add_table: 'तालिका जोड़ें',
|
||||||
|
add_view: 'व्यू जोड़ें',
|
||||||
filter: 'फ़िल्टर',
|
filter: 'फ़िल्टर',
|
||||||
collapse: 'सभी को संक्षिप्त करें',
|
collapse: 'सभी को संक्षिप्त करें',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -136,6 +147,7 @@ export const hi: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'फ़ील्ड विशेषताएँ',
|
title: 'फ़ील्ड विशेषताएँ',
|
||||||
unique: 'अद्वितीय',
|
unique: 'अद्वितीय',
|
||||||
|
auto_increment: 'ऑटो इंक्रीमेंट',
|
||||||
comments: 'टिप्पणियाँ',
|
comments: 'टिप्पणियाँ',
|
||||||
no_comments: 'कोई टिप्पणी नहीं',
|
no_comments: 'कोई टिप्पणी नहीं',
|
||||||
delete_field: 'फ़ील्ड हटाएँ',
|
delete_field: 'फ़ील्ड हटाएँ',
|
||||||
@@ -151,6 +163,7 @@ export const hi: LanguageTranslation = {
|
|||||||
title: 'सूचकांक विशेषताएँ',
|
title: 'सूचकांक विशेषताएँ',
|
||||||
name: 'नाम',
|
name: 'नाम',
|
||||||
unique: 'अद्वितीय',
|
unique: 'अद्वितीय',
|
||||||
|
index_type: 'इंडेक्स प्रकार',
|
||||||
delete_index: 'सूचकांक हटाएँ',
|
delete_index: 'सूचकांक हटाएँ',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -167,12 +180,15 @@ export const hi: LanguageTranslation = {
|
|||||||
description: 'शुरू करने के लिए एक तालिका बनाएँ',
|
description: 'शुरू करने के लिए एक तालिका बनाएँ',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'संबंध',
|
refs: 'रेफ्स',
|
||||||
filter: 'फ़िल्टर',
|
filter: 'फ़िल्टर',
|
||||||
add_relationship: 'संबंध जोड़ें',
|
|
||||||
collapse: 'सभी को संक्षिप्त करें',
|
collapse: 'सभी को संक्षिप्त करें',
|
||||||
|
add_relationship: 'संबंध जोड़ें',
|
||||||
|
relationships: 'संबंध',
|
||||||
|
dependencies: 'निर्भरताएँ',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'संबंध',
|
||||||
primary: 'प्राथमिक तालिका',
|
primary: 'प्राथमिक तालिका',
|
||||||
foreign: 'संदर्भित तालिका',
|
foreign: 'संदर्भित तालिका',
|
||||||
cardinality: 'कार्डिनैलिटी',
|
cardinality: 'कार्डिनैलिटी',
|
||||||
@@ -182,28 +198,19 @@ export const hi: LanguageTranslation = {
|
|||||||
delete_relationship: 'हटाएँ',
|
delete_relationship: 'हटाएँ',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dependency: {
|
||||||
|
dependency: 'निर्भरता',
|
||||||
|
table: 'तालिका',
|
||||||
|
dependent_table: 'आश्रित दृश्य',
|
||||||
|
delete_dependency: 'हटाएँ',
|
||||||
|
dependency_actions: {
|
||||||
|
title: 'क्रियाएँ',
|
||||||
|
delete_dependency: 'हटाएँ',
|
||||||
|
},
|
||||||
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'कोई संबंध नहीं',
|
title: 'कोई संबंध नहीं',
|
||||||
description:
|
description: 'शुरू करने के लिए एक संबंध बनाएँ',
|
||||||
'तालिकाओं को कनेक्ट करने के लिए एक संबंध बनाएँ',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'निर्भरताएँ',
|
|
||||||
filter: 'फ़िल्टर',
|
|
||||||
collapse: 'सिकोड़ें',
|
|
||||||
dependency: {
|
|
||||||
table: 'तालिका',
|
|
||||||
dependent_table: 'आश्रित तालिका',
|
|
||||||
delete_dependency: 'निर्भरता हटाएँ',
|
|
||||||
dependency_actions: {
|
|
||||||
title: 'कार्रवाइयाँ',
|
|
||||||
delete_dependency: 'निर्भरता हटाएँ',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
empty_state: {
|
|
||||||
title: 'कोई निर्भरता नहीं',
|
|
||||||
description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -470,6 +477,7 @@ export const hi: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'नई तालिका',
|
new_table: 'नई तालिका',
|
||||||
|
new_view: 'नया व्यू',
|
||||||
new_relationship: 'नया संबंध',
|
new_relationship: 'नया संबंध',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -492,6 +500,9 @@ export const hi: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'भाषा बदलें',
|
change_language: 'भाषा बदलें',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'चालू',
|
||||||
|
off: 'बंद',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const hr: LanguageTranslation = {
|
export const hr: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'Novi',
|
||||||
|
browse: 'Pregledaj',
|
||||||
|
tables: 'Tablice',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'Područja',
|
||||||
|
dependencies: 'Ovisnosti',
|
||||||
|
custom_types: 'Prilagođeni Tipovi',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'Datoteka',
|
databases: 'Baze Podataka',
|
||||||
new: 'Nova',
|
new: 'Novi Dijagram',
|
||||||
open: 'Otvori',
|
browse: 'Pregledaj...',
|
||||||
save: 'Spremi',
|
save: 'Spremi',
|
||||||
|
duplicate: 'Dupliciraj dijagram',
|
||||||
import: 'Uvezi',
|
import: 'Uvezi',
|
||||||
export_sql: 'Izvezi SQL',
|
export_sql: 'Izvezi SQL',
|
||||||
export_as: 'Izvezi kao',
|
export_as: 'Izvezi kao',
|
||||||
delete_diagram: 'Izbriši dijagram',
|
delete_diagram: 'Izbriši dijagram',
|
||||||
exit: 'Izađi',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Uredi',
|
edit: 'Uredi',
|
||||||
@@ -29,6 +38,7 @@ export const hr: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'Sakrij atribute polja',
|
hide_field_attributes: 'Sakrij atribute polja',
|
||||||
show_field_attributes: 'Prikaži atribute polja',
|
show_field_attributes: 'Prikaži atribute polja',
|
||||||
zoom_on_scroll: 'Zumiranje pri skrolanju',
|
zoom_on_scroll: 'Zumiranje pri skrolanju',
|
||||||
|
show_views: 'Pogledi Baze Podataka',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Prikaži ovisnosti',
|
show_dependencies: 'Prikaži ovisnosti',
|
||||||
hide_dependencies: 'Sakrij ovisnosti',
|
hide_dependencies: 'Sakrij ovisnosti',
|
||||||
@@ -109,6 +119,7 @@ export const hr: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Tablice',
|
tables: 'Tablice',
|
||||||
add_table: 'Dodaj tablicu',
|
add_table: 'Dodaj tablicu',
|
||||||
|
add_view: 'Dodaj Pogled',
|
||||||
filter: 'Filtriraj',
|
filter: 'Filtriraj',
|
||||||
collapse: 'Sažmi sve',
|
collapse: 'Sažmi sve',
|
||||||
clear: 'Očisti filter',
|
clear: 'Očisti filter',
|
||||||
@@ -133,6 +144,7 @@ export const hr: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Atributi polja',
|
title: 'Atributi polja',
|
||||||
unique: 'Jedinstven',
|
unique: 'Jedinstven',
|
||||||
|
auto_increment: 'Automatsko povećavanje',
|
||||||
character_length: 'Maksimalna dužina',
|
character_length: 'Maksimalna dužina',
|
||||||
precision: 'Preciznost',
|
precision: 'Preciznost',
|
||||||
scale: 'Skala',
|
scale: 'Skala',
|
||||||
@@ -146,6 +158,7 @@ export const hr: LanguageTranslation = {
|
|||||||
title: 'Atributi indeksa',
|
title: 'Atributi indeksa',
|
||||||
name: 'Naziv',
|
name: 'Naziv',
|
||||||
unique: 'Jedinstven',
|
unique: 'Jedinstven',
|
||||||
|
index_type: 'Vrsta indeksa',
|
||||||
delete_index: 'Izbriši indeks',
|
delete_index: 'Izbriši indeks',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -162,12 +175,15 @@ export const hr: LanguageTranslation = {
|
|||||||
description: 'Stvorite tablicu za početak',
|
description: 'Stvorite tablicu za početak',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'Veze',
|
refs: 'Refs',
|
||||||
filter: 'Filtriraj',
|
filter: 'Filtriraj',
|
||||||
add_relationship: 'Dodaj vezu',
|
|
||||||
collapse: 'Sažmi sve',
|
collapse: 'Sažmi sve',
|
||||||
|
add_relationship: 'Dodaj vezu',
|
||||||
|
relationships: 'Veze',
|
||||||
|
dependencies: 'Ovisnosti',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'Veza',
|
||||||
primary: 'Primarna tablica',
|
primary: 'Primarna tablica',
|
||||||
foreign: 'Referentna tablica',
|
foreign: 'Referentna tablica',
|
||||||
cardinality: 'Kardinalnost',
|
cardinality: 'Kardinalnost',
|
||||||
@@ -177,16 +193,8 @@ export const hr: LanguageTranslation = {
|
|||||||
delete_relationship: 'Izbriši',
|
delete_relationship: 'Izbriši',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'Nema veza',
|
|
||||||
description: 'Stvorite vezu za povezivanje tablica',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Ovisnosti',
|
|
||||||
filter: 'Filtriraj',
|
|
||||||
collapse: 'Sažmi sve',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'Ovisnost',
|
||||||
table: 'Tablica',
|
table: 'Tablica',
|
||||||
dependent_table: 'Ovisni pogled',
|
dependent_table: 'Ovisni pogled',
|
||||||
delete_dependency: 'Izbriši',
|
delete_dependency: 'Izbriši',
|
||||||
@@ -196,8 +204,8 @@ export const hr: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Nema ovisnosti',
|
title: 'Nema veze',
|
||||||
description: 'Stvorite pogled za početak',
|
description: 'Stvorite vezu za početak',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -461,6 +469,7 @@ export const hr: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Nova tablica',
|
new_table: 'Nova tablica',
|
||||||
|
new_view: 'Novi Pogled',
|
||||||
new_relationship: 'Nova veza',
|
new_relationship: 'Nova veza',
|
||||||
new_area: 'Novo područje',
|
new_area: 'Novo područje',
|
||||||
},
|
},
|
||||||
@@ -481,6 +490,9 @@ export const hr: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Jezik',
|
change_language: 'Jezik',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'Uključeno',
|
||||||
|
off: 'Isključeno',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const id_ID: LanguageTranslation = {
|
export const id_ID: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'Baru',
|
||||||
|
browse: 'Jelajahi',
|
||||||
|
tables: 'Tabel',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'Area',
|
||||||
|
dependencies: 'Ketergantungan',
|
||||||
|
custom_types: 'Tipe Kustom',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'Berkas',
|
databases: 'Basis Data',
|
||||||
new: 'Buat Baru',
|
new: 'Diagram Baru',
|
||||||
open: 'Buka',
|
browse: 'Jelajahi...',
|
||||||
save: 'Simpan',
|
save: 'Simpan',
|
||||||
|
duplicate: 'Duplikat',
|
||||||
import: 'Impor Database',
|
import: 'Impor Database',
|
||||||
export_sql: 'Ekspor SQL',
|
export_sql: 'Ekspor SQL',
|
||||||
export_as: 'Ekspor Sebagai',
|
export_as: 'Ekspor Sebagai',
|
||||||
delete_diagram: 'Hapus Diagram',
|
delete_diagram: 'Hapus Diagram',
|
||||||
exit: 'Keluar',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Ubah',
|
edit: 'Ubah',
|
||||||
@@ -29,6 +38,7 @@ export const id_ID: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'Sembunyikan Atribut Kolom',
|
hide_field_attributes: 'Sembunyikan Atribut Kolom',
|
||||||
show_field_attributes: 'Tampilkan Atribut Kolom',
|
show_field_attributes: 'Tampilkan Atribut Kolom',
|
||||||
zoom_on_scroll: 'Perbesar saat Scroll',
|
zoom_on_scroll: 'Perbesar saat Scroll',
|
||||||
|
show_views: 'Tampilan Database',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Tampilkan Dependensi',
|
show_dependencies: 'Tampilkan Dependensi',
|
||||||
hide_dependencies: 'Sembunyikan Dependensi',
|
hide_dependencies: 'Sembunyikan Dependensi',
|
||||||
@@ -110,6 +120,7 @@ export const id_ID: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Tabel',
|
tables: 'Tabel',
|
||||||
add_table: 'Tambah Tabel',
|
add_table: 'Tambah Tabel',
|
||||||
|
add_view: 'Tambah Tampilan',
|
||||||
filter: 'Saring',
|
filter: 'Saring',
|
||||||
collapse: 'Lipat Semua',
|
collapse: 'Lipat Semua',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -135,6 +146,7 @@ export const id_ID: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Atribut Kolom',
|
title: 'Atribut Kolom',
|
||||||
unique: 'Unik',
|
unique: 'Unik',
|
||||||
|
auto_increment: 'Kenaikan Otomatis',
|
||||||
comments: 'Komentar',
|
comments: 'Komentar',
|
||||||
no_comments: 'Tidak ada komentar',
|
no_comments: 'Tidak ada komentar',
|
||||||
delete_field: 'Hapus Kolom',
|
delete_field: 'Hapus Kolom',
|
||||||
@@ -150,6 +162,7 @@ export const id_ID: LanguageTranslation = {
|
|||||||
title: 'Atribut Indeks',
|
title: 'Atribut Indeks',
|
||||||
name: 'Nama',
|
name: 'Nama',
|
||||||
unique: 'Unik',
|
unique: 'Unik',
|
||||||
|
index_type: 'Tipe Indeks',
|
||||||
delete_index: 'Hapus Indeks',
|
delete_index: 'Hapus Indeks',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -166,12 +179,15 @@ export const id_ID: LanguageTranslation = {
|
|||||||
description: 'Buat tabel untuk memulai',
|
description: 'Buat tabel untuk memulai',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'Hubungan',
|
refs: 'Refs',
|
||||||
filter: 'Saring',
|
filter: 'Saring',
|
||||||
add_relationship: 'Tambah Hubungan',
|
|
||||||
collapse: 'Lipat Semua',
|
collapse: 'Lipat Semua',
|
||||||
|
add_relationship: 'Tambah Hubungan',
|
||||||
|
relationships: 'Hubungan',
|
||||||
|
dependencies: 'Dependensi',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'Hubungan',
|
||||||
primary: 'Tabel Primer',
|
primary: 'Tabel Primer',
|
||||||
foreign: 'Tabel Referensi',
|
foreign: 'Tabel Referensi',
|
||||||
cardinality: 'Kardinalitas',
|
cardinality: 'Kardinalitas',
|
||||||
@@ -181,16 +197,8 @@ export const id_ID: LanguageTranslation = {
|
|||||||
delete_relationship: 'Hapus',
|
delete_relationship: 'Hapus',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'Tidak ada hubungan',
|
|
||||||
description: 'Buat hubungan untuk menghubungkan tabel',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Dependensi',
|
|
||||||
filter: 'Saring',
|
|
||||||
collapse: 'Lipat Semua',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'Dependensi',
|
||||||
table: 'Tabel',
|
table: 'Tabel',
|
||||||
dependent_table: 'Tampilan Dependen',
|
dependent_table: 'Tampilan Dependen',
|
||||||
delete_dependency: 'Hapus',
|
delete_dependency: 'Hapus',
|
||||||
@@ -200,8 +208,8 @@ export const id_ID: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Tidak ada dependensi',
|
title: 'Tidak ada hubungan',
|
||||||
description: 'Buat tampilan untuk memulai',
|
description: 'Buat hubungan untuk memulai',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -466,6 +474,7 @@ export const id_ID: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Tabel Baru',
|
new_table: 'Tabel Baru',
|
||||||
|
new_view: 'Tampilan Baru',
|
||||||
new_relationship: 'Hubungan Baru',
|
new_relationship: 'Hubungan Baru',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -487,6 +496,9 @@ export const id_ID: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Bahasa',
|
change_language: 'Bahasa',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'Aktif',
|
||||||
|
off: 'Nonaktif',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const ja: LanguageTranslation = {
|
export const ja: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: '新規',
|
||||||
|
browse: '参照',
|
||||||
|
tables: 'テーブル',
|
||||||
|
refs: '参照',
|
||||||
|
areas: 'エリア',
|
||||||
|
dependencies: '依存関係',
|
||||||
|
custom_types: 'カスタムタイプ',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'ファイル',
|
databases: 'データベース',
|
||||||
new: '新規',
|
new: '新しいダイアグラム',
|
||||||
open: '開く',
|
browse: '参照...',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
|
duplicate: '複製',
|
||||||
import: 'データベースをインポート',
|
import: 'データベースをインポート',
|
||||||
export_sql: 'SQLをエクスポート',
|
export_sql: 'SQLをエクスポート',
|
||||||
export_as: '形式を指定してエクスポート',
|
export_as: '形式を指定してエクスポート',
|
||||||
delete_diagram: 'ダイアグラムを削除',
|
delete_diagram: 'ダイアグラムを削除',
|
||||||
exit: '終了',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: '編集',
|
edit: '編集',
|
||||||
@@ -29,6 +38,7 @@ export const ja: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'フィールド属性を非表示',
|
hide_field_attributes: 'フィールド属性を非表示',
|
||||||
show_field_attributes: 'フィールド属性を表示',
|
show_field_attributes: 'フィールド属性を表示',
|
||||||
zoom_on_scroll: 'スクロールでズーム',
|
zoom_on_scroll: 'スクロールでズーム',
|
||||||
|
show_views: 'データベースビュー',
|
||||||
theme: 'テーマ',
|
theme: 'テーマ',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
show_dependencies: 'Show Dependencies',
|
show_dependencies: 'Show Dependencies',
|
||||||
@@ -114,6 +124,7 @@ export const ja: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'テーブル',
|
tables: 'テーブル',
|
||||||
add_table: 'テーブルを追加',
|
add_table: 'テーブルを追加',
|
||||||
|
add_view: 'ビューを追加',
|
||||||
filter: 'フィルタ',
|
filter: 'フィルタ',
|
||||||
collapse: 'すべて折りたたむ',
|
collapse: 'すべて折りたたむ',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -139,6 +150,7 @@ export const ja: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'フィールド属性',
|
title: 'フィールド属性',
|
||||||
unique: 'ユニーク',
|
unique: 'ユニーク',
|
||||||
|
auto_increment: 'オートインクリメント',
|
||||||
comments: 'コメント',
|
comments: 'コメント',
|
||||||
no_comments: 'コメントがありません',
|
no_comments: 'コメントがありません',
|
||||||
delete_field: 'フィールドを削除',
|
delete_field: 'フィールドを削除',
|
||||||
@@ -154,6 +166,7 @@ export const ja: LanguageTranslation = {
|
|||||||
title: 'インデックス属性',
|
title: 'インデックス属性',
|
||||||
name: '名前',
|
name: '名前',
|
||||||
unique: 'ユニーク',
|
unique: 'ユニーク',
|
||||||
|
index_type: 'インデックスタイプ',
|
||||||
delete_index: 'インデックスを削除',
|
delete_index: 'インデックスを削除',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -170,12 +183,15 @@ export const ja: LanguageTranslation = {
|
|||||||
description: 'テーブルを作成して開始してください',
|
description: 'テーブルを作成して開始してください',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'リレーションシップ',
|
refs: '参照',
|
||||||
filter: 'フィルタ',
|
filter: 'フィルタ',
|
||||||
add_relationship: 'リレーションシップを追加',
|
|
||||||
collapse: 'すべて折りたたむ',
|
collapse: 'すべて折りたたむ',
|
||||||
|
add_relationship: 'リレーションシップを追加',
|
||||||
|
relationships: 'リレーションシップ',
|
||||||
|
dependencies: '依存関係',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'リレーションシップ',
|
||||||
primary: '主テーブル',
|
primary: '主テーブル',
|
||||||
foreign: '参照テーブル',
|
foreign: '参照テーブル',
|
||||||
cardinality: 'カーディナリティ',
|
cardinality: 'カーディナリティ',
|
||||||
@@ -185,29 +201,20 @@ export const ja: LanguageTranslation = {
|
|||||||
delete_relationship: '削除',
|
delete_relationship: '削除',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dependency: {
|
||||||
|
dependency: '依存関係',
|
||||||
|
table: 'テーブル',
|
||||||
|
dependent_table: '依存ビュー',
|
||||||
|
delete_dependency: '削除',
|
||||||
|
dependency_actions: {
|
||||||
|
title: '操作',
|
||||||
|
delete_dependency: '削除',
|
||||||
|
},
|
||||||
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'リレーションシップがありません',
|
title: 'リレーションシップがありません',
|
||||||
description:
|
description:
|
||||||
'テーブルを接続するためにリレーションシップを作成してください',
|
'開始するためにリレーションシップを作成してください',
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: Translate
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Dependencies',
|
|
||||||
filter: 'Filter',
|
|
||||||
collapse: 'Collapse All',
|
|
||||||
dependency: {
|
|
||||||
table: 'Table',
|
|
||||||
dependent_table: 'Dependent View',
|
|
||||||
delete_dependency: 'Delete',
|
|
||||||
dependency_actions: {
|
|
||||||
title: 'Actions',
|
|
||||||
delete_dependency: 'Delete',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
empty_state: {
|
|
||||||
title: 'No dependencies',
|
|
||||||
description: 'Create a view to get started',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -472,6 +479,7 @@ export const ja: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: '新しいテーブル',
|
new_table: '新しいテーブル',
|
||||||
|
new_view: '新しいビュー',
|
||||||
new_relationship: '新しいリレーションシップ',
|
new_relationship: '新しいリレーションシップ',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -494,6 +502,9 @@ export const ja: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: '言語',
|
change_language: '言語',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'オン',
|
||||||
|
off: 'オフ',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const ko_KR: LanguageTranslation = {
|
export const ko_KR: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: '새로 만들기',
|
||||||
|
browse: '찾아보기',
|
||||||
|
tables: '테이블',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: '영역',
|
||||||
|
dependencies: '종속성',
|
||||||
|
custom_types: '사용자 지정 타입',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: '파일',
|
databases: '데이터베이스',
|
||||||
new: '새 다이어그램',
|
new: '새 다이어그램',
|
||||||
open: '열기',
|
browse: '찾아보기...',
|
||||||
save: '저장',
|
save: '저장',
|
||||||
|
duplicate: '복사',
|
||||||
import: '데이터베이스 가져오기',
|
import: '데이터베이스 가져오기',
|
||||||
export_sql: 'SQL로 저장',
|
export_sql: 'SQL로 저장',
|
||||||
export_as: '다른 형식으로 저장',
|
export_as: '다른 형식으로 저장',
|
||||||
delete_diagram: '다이어그램 삭제',
|
delete_diagram: '다이어그램 삭제',
|
||||||
exit: '종료',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: '편집',
|
edit: '편집',
|
||||||
@@ -29,6 +38,7 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
hide_field_attributes: '필드 속성 숨기기',
|
hide_field_attributes: '필드 속성 숨기기',
|
||||||
show_field_attributes: '필드 속성 보이기',
|
show_field_attributes: '필드 속성 보이기',
|
||||||
zoom_on_scroll: '스크롤 시 확대',
|
zoom_on_scroll: '스크롤 시 확대',
|
||||||
|
show_views: '데이터베이스 뷰',
|
||||||
theme: '테마',
|
theme: '테마',
|
||||||
show_dependencies: '종속성 보이기',
|
show_dependencies: '종속성 보이기',
|
||||||
hide_dependencies: '종속성 숨기기',
|
hide_dependencies: '종속성 숨기기',
|
||||||
@@ -110,6 +120,7 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: '테이블',
|
tables: '테이블',
|
||||||
add_table: '테이블 추가',
|
add_table: '테이블 추가',
|
||||||
|
add_view: '뷰 추가',
|
||||||
filter: '필터',
|
filter: '필터',
|
||||||
collapse: '모두 접기',
|
collapse: '모두 접기',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -135,6 +146,7 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: '필드 속성',
|
title: '필드 속성',
|
||||||
unique: '유니크 여부',
|
unique: '유니크 여부',
|
||||||
|
auto_increment: '자동 증가',
|
||||||
comments: '주석',
|
comments: '주석',
|
||||||
no_comments: '주석 없음',
|
no_comments: '주석 없음',
|
||||||
delete_field: '필드 삭제',
|
delete_field: '필드 삭제',
|
||||||
@@ -150,6 +162,7 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
title: '인덱스 속성',
|
title: '인덱스 속성',
|
||||||
name: '인덱스 명',
|
name: '인덱스 명',
|
||||||
unique: '유니크 여부',
|
unique: '유니크 여부',
|
||||||
|
index_type: '인덱스 타입',
|
||||||
delete_index: '인덱스 삭제',
|
delete_index: '인덱스 삭제',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -166,12 +179,15 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
description: '테이블을 만들어 시작하세요.',
|
description: '테이블을 만들어 시작하세요.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: '연관 관계',
|
refs: 'Refs',
|
||||||
filter: '필터',
|
filter: '필터',
|
||||||
add_relationship: '연관 관계 추가',
|
|
||||||
collapse: '모두 접기',
|
collapse: '모두 접기',
|
||||||
|
add_relationship: '연관 관계 추가',
|
||||||
|
relationships: '연관 관계',
|
||||||
|
dependencies: '종속성',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: '연관 관계',
|
||||||
primary: '주 테이블',
|
primary: '주 테이블',
|
||||||
foreign: '참조 테이블',
|
foreign: '참조 테이블',
|
||||||
cardinality: '카디널리티',
|
cardinality: '카디널리티',
|
||||||
@@ -181,16 +197,8 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
delete_relationship: '연관 관계 삭제',
|
delete_relationship: '연관 관계 삭제',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: '연관 관계',
|
|
||||||
description: '테이블 연결을 위해 연관 관계를 생성하세요',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: '종속성',
|
|
||||||
filter: '필터',
|
|
||||||
collapse: '모두 접기',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: '종속성',
|
||||||
table: '테이블',
|
table: '테이블',
|
||||||
dependent_table: '뷰 테이블',
|
dependent_table: '뷰 테이블',
|
||||||
delete_dependency: '삭제',
|
delete_dependency: '삭제',
|
||||||
@@ -200,8 +208,8 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: '뷰 테이블 없음',
|
title: '연관 관계 없음',
|
||||||
description: '뷰 테이블을 만들어 시작하세요.',
|
description: '연관 관계를 만들어 시작하세요.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -463,6 +471,7 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: '새 테이블',
|
new_table: '새 테이블',
|
||||||
|
new_view: '새 뷰',
|
||||||
new_relationship: '새 연관관계',
|
new_relationship: '새 연관관계',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -484,6 +493,9 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: '언어',
|
change_language: '언어',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: '켜기',
|
||||||
|
off: '끄기',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const mr: LanguageTranslation = {
|
export const mr: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'नवीन',
|
||||||
|
browse: 'ब्राउज',
|
||||||
|
tables: 'टेबल',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'क्षेत्रे',
|
||||||
|
dependencies: 'अवलंबने',
|
||||||
|
custom_types: 'कस्टम प्रकार',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'फाइल',
|
databases: 'डेटाबेस',
|
||||||
new: 'नवीन',
|
new: 'नवीन आरेख',
|
||||||
open: 'उघडा',
|
browse: 'ब्राउज करा...',
|
||||||
save: 'जतन करा',
|
save: 'जतन करा',
|
||||||
|
duplicate: 'डुप्लिकेट',
|
||||||
import: 'डेटाबेस इम्पोर्ट करा',
|
import: 'डेटाबेस इम्पोर्ट करा',
|
||||||
export_sql: 'SQL एक्स्पोर्ट करा',
|
export_sql: 'SQL एक्स्पोर्ट करा',
|
||||||
export_as: 'म्हणून एक्स्पोर्ट करा',
|
export_as: 'म्हणून एक्स्पोर्ट करा',
|
||||||
delete_diagram: 'आरेख हटवा',
|
delete_diagram: 'आरेख हटवा',
|
||||||
exit: 'बाहेर पडा',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'संपादन करा',
|
edit: 'संपादन करा',
|
||||||
@@ -29,6 +38,7 @@ export const mr: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
|
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
|
||||||
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
|
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
|
||||||
zoom_on_scroll: 'स्क्रोलवर झूम करा',
|
zoom_on_scroll: 'स्क्रोलवर झूम करा',
|
||||||
|
show_views: 'डेटाबेस व्ह्यूज',
|
||||||
theme: 'थीम',
|
theme: 'थीम',
|
||||||
show_dependencies: 'डिपेंडेन्सि दाखवा',
|
show_dependencies: 'डिपेंडेन्सि दाखवा',
|
||||||
hide_dependencies: 'डिपेंडेन्सि लपवा',
|
hide_dependencies: 'डिपेंडेन्सि लपवा',
|
||||||
@@ -113,6 +123,7 @@ export const mr: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'टेबल्स',
|
tables: 'टेबल्स',
|
||||||
add_table: 'टेबल जोडा',
|
add_table: 'टेबल जोडा',
|
||||||
|
add_view: 'व्ह्यू जोडा',
|
||||||
filter: 'फिल्टर',
|
filter: 'फिल्टर',
|
||||||
collapse: 'सर्व संकुचित करा',
|
collapse: 'सर्व संकुचित करा',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -138,6 +149,7 @@ export const mr: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'फील्ड गुणधर्म',
|
title: 'फील्ड गुणधर्म',
|
||||||
unique: 'युनिक',
|
unique: 'युनिक',
|
||||||
|
auto_increment: 'ऑटो इंक्रिमेंट',
|
||||||
comments: 'टिप्पण्या',
|
comments: 'टिप्पण्या',
|
||||||
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
||||||
delete_field: 'फील्ड हटवा',
|
delete_field: 'फील्ड हटवा',
|
||||||
@@ -153,6 +165,7 @@ export const mr: LanguageTranslation = {
|
|||||||
title: 'इंडेक्स गुणधर्म',
|
title: 'इंडेक्स गुणधर्म',
|
||||||
name: 'नाव',
|
name: 'नाव',
|
||||||
unique: 'युनिक',
|
unique: 'युनिक',
|
||||||
|
index_type: 'इंडेक्स प्रकार',
|
||||||
delete_index: 'इंडेक्स हटवा',
|
delete_index: 'इंडेक्स हटवा',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -170,12 +183,15 @@ export const mr: LanguageTranslation = {
|
|||||||
description: 'सुरू करण्यासाठी एक टेबल तयार करा',
|
description: 'सुरू करण्यासाठी एक टेबल तयार करा',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'रिलेशनशिप',
|
refs: 'Refs',
|
||||||
filter: 'फिल्टर',
|
filter: 'फिल्टर',
|
||||||
add_relationship: 'रिलेशनशिप जोडा',
|
|
||||||
collapse: 'सर्व संकुचित करा',
|
collapse: 'सर्व संकुचित करा',
|
||||||
|
add_relationship: 'रिलेशनशिप जोडा',
|
||||||
|
relationships: 'रिलेशनशिप',
|
||||||
|
dependencies: 'डिपेंडेन्सि',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'रिलेशनशिप',
|
||||||
primary: 'प्राथमिक टेबल',
|
primary: 'प्राथमिक टेबल',
|
||||||
foreign: 'रेफरंस टेबल',
|
foreign: 'रेफरंस टेबल',
|
||||||
cardinality: 'कार्डिनॅलिटी',
|
cardinality: 'कार्डिनॅलिटी',
|
||||||
@@ -185,17 +201,8 @@ export const mr: LanguageTranslation = {
|
|||||||
delete_relationship: 'हटवा',
|
delete_relationship: 'हटवा',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'कोणतेही रिलेशनशिप नाहीत',
|
|
||||||
description:
|
|
||||||
'टेबल्स कनेक्ट करण्यासाठी एक रिलेशनशिप तयार करा',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'डिपेंडेन्सि',
|
|
||||||
filter: 'फिल्टर',
|
|
||||||
collapse: 'सर्व संकुचित करा',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'डिपेंडेन्सि',
|
||||||
table: 'टेबल',
|
table: 'टेबल',
|
||||||
dependent_table: 'डिपेंडेन्सि दृश्य',
|
dependent_table: 'डिपेंडेन्सि दृश्य',
|
||||||
delete_dependency: 'हटवा',
|
delete_dependency: 'हटवा',
|
||||||
@@ -205,8 +212,8 @@ export const mr: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'कोणत्याही डिपेंडेन्सि नाहीत',
|
title: 'कोणतेही रिलेशनशिप नाहीत',
|
||||||
description: 'सुरू करण्यासाठी एक दृश्य तयार करा',
|
description: 'सुरू करण्यासाठी एक रिलेशनशिप तयार करा',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -476,6 +483,7 @@ export const mr: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'नवीन टेबल',
|
new_table: 'नवीन टेबल',
|
||||||
|
new_view: 'नवीन व्ह्यू',
|
||||||
new_relationship: 'नवीन रिलेशनशिप',
|
new_relationship: 'नवीन रिलेशनशिप',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -499,6 +507,9 @@ export const mr: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'भाषा बदला',
|
change_language: 'भाषा बदला',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'चालू',
|
||||||
|
off: 'बंद',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const ne: LanguageTranslation = {
|
export const ne: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'नयाँ',
|
||||||
|
browse: 'ब्राउज',
|
||||||
|
tables: 'टेबलहरू',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'क्षेत्रहरू',
|
||||||
|
dependencies: 'निर्भरताहरू',
|
||||||
|
custom_types: 'कस्टम प्रकारहरू',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'फाइल',
|
databases: 'डाटाबेसहरू',
|
||||||
new: 'नयाँ',
|
new: 'नयाँ डायाग्राम',
|
||||||
open: 'खोल्नुहोस्',
|
browse: 'ब्राउज गर्नुहोस्...',
|
||||||
save: 'सुरक्षित गर्नुहोस्',
|
save: 'सुरक्षित गर्नुहोस्',
|
||||||
|
duplicate: 'डुप्लिकेट',
|
||||||
import: 'डाटाबेस आयात गर्नुहोस्',
|
import: 'डाटाबेस आयात गर्नुहोस्',
|
||||||
export_sql: 'SQL निर्यात गर्नुहोस्',
|
export_sql: 'SQL निर्यात गर्नुहोस्',
|
||||||
export_as: 'निर्यात गर्नुहोस्',
|
export_as: 'निर्यात गर्नुहोस्',
|
||||||
delete_diagram: 'डायाग्राम हटाउनुहोस्',
|
delete_diagram: 'डायाग्राम हटाउनुहोस्',
|
||||||
exit: 'बाहिर निस्कनुहोस्',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'सम्पादन',
|
edit: 'सम्पादन',
|
||||||
@@ -29,6 +38,7 @@ export const ne: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
|
hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
|
||||||
show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
|
show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
|
||||||
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
|
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
|
||||||
|
show_views: 'डाटाबेस भ्यूहरू',
|
||||||
theme: 'थिम',
|
theme: 'थिम',
|
||||||
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
|
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
|
||||||
hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
|
hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
|
||||||
@@ -111,6 +121,7 @@ export const ne: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'तालिकाहरू',
|
tables: 'तालिकाहरू',
|
||||||
add_table: 'तालिका थप्नुहोस्',
|
add_table: 'तालिका थप्नुहोस्',
|
||||||
|
add_view: 'भ्यू थप्नुहोस्',
|
||||||
filter: 'फिल्टर',
|
filter: 'फिल्टर',
|
||||||
collapse: 'सबै लुकाउनुहोस्',
|
collapse: 'सबै लुकाउनुहोस्',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -136,6 +147,7 @@ export const ne: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'क्षेत्र विशेषताहरू',
|
title: 'क्षेत्र विशेषताहरू',
|
||||||
unique: 'अनन्य',
|
unique: 'अनन्य',
|
||||||
|
auto_increment: 'स्वचालित वृद्धि',
|
||||||
comments: 'टिप्पणीहरू',
|
comments: 'टिप्पणीहरू',
|
||||||
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
||||||
delete_field: 'क्षेत्र हटाउनुहोस्',
|
delete_field: 'क्षेत्र हटाउनुहोस्',
|
||||||
@@ -151,6 +163,7 @@ export const ne: LanguageTranslation = {
|
|||||||
title: 'सूचक विशेषताहरू',
|
title: 'सूचक विशेषताहरू',
|
||||||
name: 'नाम',
|
name: 'नाम',
|
||||||
unique: 'अनन्य',
|
unique: 'अनन्य',
|
||||||
|
index_type: 'इन्डेक्स प्रकार',
|
||||||
delete_index: 'सूचक हटाउनुहोस्',
|
delete_index: 'सूचक हटाउनुहोस्',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -167,12 +180,15 @@ export const ne: LanguageTranslation = {
|
|||||||
description: 'सुरु गर्नका लागि एक तालिका बनाउनुहोस्',
|
description: 'सुरु गर्नका लागि एक तालिका बनाउनुहोस्',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'सम्बन्धहरू',
|
refs: 'Refs',
|
||||||
filter: 'फिल्टर',
|
filter: 'फिल्टर',
|
||||||
add_relationship: 'सम्बन्ध थप्नुहोस्',
|
|
||||||
collapse: 'सबै लुकाउनुहोस्',
|
collapse: 'सबै लुकाउनुहोस्',
|
||||||
|
add_relationship: 'सम्बन्ध थप्नुहोस्',
|
||||||
|
relationships: 'सम्बन्धहरू',
|
||||||
|
dependencies: 'डिपेन्डेन्सीहरू',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'सम्बन्ध',
|
||||||
primary: 'मुख्य तालिका',
|
primary: 'मुख्य तालिका',
|
||||||
foreign: 'परिचित तालिका',
|
foreign: 'परिचित तालिका',
|
||||||
cardinality: 'कार्डिन्यालिटी',
|
cardinality: 'कार्डिन्यालिटी',
|
||||||
@@ -182,16 +198,8 @@ export const ne: LanguageTranslation = {
|
|||||||
delete_relationship: 'हटाउनुहोस्',
|
delete_relationship: 'हटाउनुहोस्',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'कुनै सम्बन्धहरू छैनन्',
|
|
||||||
description: 'तालिकाहरू जोड्नका लागि एक सम्बन्ध बनाउनुहोस्',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'डिपेन्डेन्सीहरू',
|
|
||||||
filter: 'फिल्टर',
|
|
||||||
collapse: 'सबै लुकाउनुहोस्',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'डिपेन्डेन्सी',
|
||||||
table: 'तालिका',
|
table: 'तालिका',
|
||||||
dependent_table: 'विचलित तालिका',
|
dependent_table: 'विचलित तालिका',
|
||||||
delete_dependency: 'हटाउनुहोस्',
|
delete_dependency: 'हटाउनुहोस्',
|
||||||
@@ -201,9 +209,8 @@ export const ne: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'कुनै डिपेन्डेन्सीहरू छैनन्',
|
title: 'कुनै सम्बन्धहरू छैनन्',
|
||||||
description:
|
description: 'सुरु गर्नका लागि एक सम्बन्ध बनाउनुहोस्',
|
||||||
'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -470,6 +477,7 @@ export const ne: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'नयाँ तालिका',
|
new_table: 'नयाँ तालिका',
|
||||||
|
new_view: 'नयाँ भ्यू',
|
||||||
new_relationship: 'नयाँ सम्बन्ध',
|
new_relationship: 'नयाँ सम्बन्ध',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -491,6 +499,9 @@ export const ne: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'भाषा परिवर्तन गर्नुहोस्',
|
change_language: 'भाषा परिवर्तन गर्नुहोस्',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'सक्रिय',
|
||||||
|
off: 'निष्क्रिय',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const pt_BR: LanguageTranslation = {
|
export const pt_BR: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'Novo',
|
||||||
|
browse: 'Navegar',
|
||||||
|
tables: 'Tabelas',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'Áreas',
|
||||||
|
dependencies: 'Dependências',
|
||||||
|
custom_types: 'Tipos Personalizados',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'Arquivo',
|
databases: 'Bancos de Dados',
|
||||||
new: 'Novo',
|
new: 'Novo Diagrama',
|
||||||
open: 'Abrir',
|
browse: 'Navegar...',
|
||||||
save: 'Salvar',
|
save: 'Salvar',
|
||||||
|
duplicate: 'Duplicar',
|
||||||
import: 'Importar Banco de Dados',
|
import: 'Importar Banco de Dados',
|
||||||
export_sql: 'Exportar SQL',
|
export_sql: 'Exportar SQL',
|
||||||
export_as: 'Exportar como',
|
export_as: 'Exportar como',
|
||||||
delete_diagram: 'Excluir Diagrama',
|
delete_diagram: 'Excluir Diagrama',
|
||||||
exit: 'Sair',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Editar',
|
edit: 'Editar',
|
||||||
@@ -29,6 +38,7 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
hide_field_attributes: 'Ocultar Atributos de Campo',
|
hide_field_attributes: 'Ocultar Atributos de Campo',
|
||||||
show_field_attributes: 'Mostrar Atributos de Campo',
|
show_field_attributes: 'Mostrar Atributos de Campo',
|
||||||
zoom_on_scroll: 'Zoom ao Rolar',
|
zoom_on_scroll: 'Zoom ao Rolar',
|
||||||
|
show_views: 'Visualizações do Banco de Dados',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Mostrar Dependências',
|
show_dependencies: 'Mostrar Dependências',
|
||||||
hide_dependencies: 'Ocultar Dependências',
|
hide_dependencies: 'Ocultar Dependências',
|
||||||
@@ -111,6 +121,7 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Tabelas',
|
tables: 'Tabelas',
|
||||||
add_table: 'Adicionar Tabela',
|
add_table: 'Adicionar Tabela',
|
||||||
|
add_view: 'Adicionar Visualização',
|
||||||
filter: 'Filtrar',
|
filter: 'Filtrar',
|
||||||
collapse: 'Colapsar Todas',
|
collapse: 'Colapsar Todas',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -136,6 +147,7 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Atributos do Campo',
|
title: 'Atributos do Campo',
|
||||||
unique: 'Único',
|
unique: 'Único',
|
||||||
|
auto_increment: 'Incremento Automático',
|
||||||
comments: 'Comentários',
|
comments: 'Comentários',
|
||||||
no_comments: 'Sem comentários',
|
no_comments: 'Sem comentários',
|
||||||
delete_field: 'Excluir Campo',
|
delete_field: 'Excluir Campo',
|
||||||
@@ -151,6 +163,7 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
title: 'Atributos do Índice',
|
title: 'Atributos do Índice',
|
||||||
name: 'Nome',
|
name: 'Nome',
|
||||||
unique: 'Único',
|
unique: 'Único',
|
||||||
|
index_type: 'Tipo de Índice',
|
||||||
delete_index: 'Excluir Índice',
|
delete_index: 'Excluir Índice',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -167,12 +180,15 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
description: 'Crie uma tabela para começar',
|
description: 'Crie uma tabela para começar',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'Relacionamentos',
|
refs: 'Refs',
|
||||||
filter: 'Filtrar',
|
filter: 'Filtrar',
|
||||||
add_relationship: 'Adicionar Relacionamento',
|
|
||||||
collapse: 'Colapsar Todas',
|
collapse: 'Colapsar Todas',
|
||||||
|
add_relationship: 'Adicionar Relacionamento',
|
||||||
|
relationships: 'Relacionamentos',
|
||||||
|
dependencies: 'Dependências',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'Relacionamento',
|
||||||
primary: 'Tabela Primária',
|
primary: 'Tabela Primária',
|
||||||
foreign: 'Tabela Referenciada',
|
foreign: 'Tabela Referenciada',
|
||||||
cardinality: 'Cardinalidade',
|
cardinality: 'Cardinalidade',
|
||||||
@@ -182,16 +198,8 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
delete_relationship: 'Excluir',
|
delete_relationship: 'Excluir',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'Sem relacionamentos',
|
|
||||||
description: 'Crie um relacionamento para conectar tabelas',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Dependências',
|
|
||||||
filter: 'Filtrar',
|
|
||||||
collapse: 'Colapsar Todas',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'Dependência',
|
||||||
table: 'Tabela',
|
table: 'Tabela',
|
||||||
dependent_table: 'Visualização Dependente',
|
dependent_table: 'Visualização Dependente',
|
||||||
delete_dependency: 'Excluir',
|
delete_dependency: 'Excluir',
|
||||||
@@ -201,8 +209,8 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Sem dependências',
|
title: 'Sem relacionamentos',
|
||||||
description: 'Crie uma visualização para começar',
|
description: 'Crie um relacionamento para começar',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -468,6 +476,7 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Nova Tabela',
|
new_table: 'Nova Tabela',
|
||||||
|
new_view: 'Nova Visualização',
|
||||||
new_relationship: 'Novo Relacionamento',
|
new_relationship: 'Novo Relacionamento',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -490,6 +499,9 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Idioma',
|
change_language: 'Idioma',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'Ligado',
|
||||||
|
off: 'Desligado',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const ru: LanguageTranslation = {
|
export const ru: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'Новая',
|
||||||
|
browse: 'Обзор',
|
||||||
|
tables: 'Таблицы',
|
||||||
|
refs: 'Ссылки',
|
||||||
|
areas: 'Области',
|
||||||
|
dependencies: 'Зависимости',
|
||||||
|
custom_types: 'Пользовательские типы',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'Файл',
|
databases: 'Базы данных',
|
||||||
new: 'Создать',
|
new: 'Новая диаграмма',
|
||||||
open: 'Открыть',
|
browse: 'Обзор...',
|
||||||
save: 'Сохранить',
|
save: 'Сохранить',
|
||||||
|
duplicate: 'Дублировать',
|
||||||
import: 'Импортировать базу данных',
|
import: 'Импортировать базу данных',
|
||||||
export_sql: 'Экспорт SQL',
|
export_sql: 'Экспорт SQL',
|
||||||
export_as: 'Экспортировать как',
|
export_as: 'Экспортировать как',
|
||||||
delete_diagram: 'Удалить диаграмму',
|
delete_diagram: 'Удалить диаграмму',
|
||||||
exit: 'Выход',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Изменение',
|
edit: 'Изменение',
|
||||||
@@ -29,6 +38,7 @@ export const ru: LanguageTranslation = {
|
|||||||
show_field_attributes: 'Показать атрибуты поля',
|
show_field_attributes: 'Показать атрибуты поля',
|
||||||
hide_field_attributes: 'Скрыть атрибуты поля',
|
hide_field_attributes: 'Скрыть атрибуты поля',
|
||||||
zoom_on_scroll: 'Увеличение при прокрутке',
|
zoom_on_scroll: 'Увеличение при прокрутке',
|
||||||
|
show_views: 'Представления базы данных',
|
||||||
theme: 'Тема',
|
theme: 'Тема',
|
||||||
show_dependencies: 'Показать зависимости',
|
show_dependencies: 'Показать зависимости',
|
||||||
hide_dependencies: 'Скрыть зависимости',
|
hide_dependencies: 'Скрыть зависимости',
|
||||||
@@ -108,6 +118,7 @@ export const ru: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Таблицы',
|
tables: 'Таблицы',
|
||||||
add_table: 'Добавить таблицу',
|
add_table: 'Добавить таблицу',
|
||||||
|
add_view: 'Добавить представление',
|
||||||
filter: 'Фильтр',
|
filter: 'Фильтр',
|
||||||
collapse: 'Свернуть все',
|
collapse: 'Свернуть все',
|
||||||
clear: 'Очистить фильтр',
|
clear: 'Очистить фильтр',
|
||||||
@@ -133,6 +144,7 @@ export const ru: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Атрибуты поля',
|
title: 'Атрибуты поля',
|
||||||
unique: 'Уникальный',
|
unique: 'Уникальный',
|
||||||
|
auto_increment: 'Автоинкремент',
|
||||||
comments: 'Комментарии',
|
comments: 'Комментарии',
|
||||||
no_comments: 'Нет комментария',
|
no_comments: 'Нет комментария',
|
||||||
delete_field: 'Удалить поле',
|
delete_field: 'Удалить поле',
|
||||||
@@ -147,6 +159,7 @@ export const ru: LanguageTranslation = {
|
|||||||
title: 'Атрибуты индекса',
|
title: 'Атрибуты индекса',
|
||||||
name: 'Имя',
|
name: 'Имя',
|
||||||
unique: 'Уникальный',
|
unique: 'Уникальный',
|
||||||
|
index_type: 'Тип индекса',
|
||||||
delete_index: 'Удалить индекс',
|
delete_index: 'Удалить индекс',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -163,12 +176,15 @@ export const ru: LanguageTranslation = {
|
|||||||
description: 'Создайте таблицу, чтобы начать',
|
description: 'Создайте таблицу, чтобы начать',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'Отношения',
|
refs: 'Ссылки',
|
||||||
filter: 'Фильтр',
|
filter: 'Фильтр',
|
||||||
add_relationship: 'Добавить отношение',
|
|
||||||
collapse: 'Свернуть все',
|
collapse: 'Свернуть все',
|
||||||
|
add_relationship: 'Добавить отношение',
|
||||||
|
relationships: 'Отношения',
|
||||||
|
dependencies: 'Зависимости',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'Отношение',
|
||||||
primary: 'Основная таблица',
|
primary: 'Основная таблица',
|
||||||
foreign: 'Справочная таблица',
|
foreign: 'Справочная таблица',
|
||||||
cardinality: 'Тип множественной связи',
|
cardinality: 'Тип множественной связи',
|
||||||
@@ -178,18 +194,10 @@ export const ru: LanguageTranslation = {
|
|||||||
delete_relationship: 'Удалить',
|
delete_relationship: 'Удалить',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'Нет отношений',
|
|
||||||
description: 'Создайте связь для соединения таблиц',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Зависимости',
|
|
||||||
filter: 'Фильтр',
|
|
||||||
collapse: 'Свернуть все',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
table: 'Стол',
|
dependency: 'Зависимость',
|
||||||
dependent_table: 'Зависимый вид',
|
table: 'Таблица',
|
||||||
|
dependent_table: 'Зависимое представление',
|
||||||
delete_dependency: 'Удалить',
|
delete_dependency: 'Удалить',
|
||||||
dependency_actions: {
|
dependency_actions: {
|
||||||
title: 'Действия',
|
title: 'Действия',
|
||||||
@@ -197,8 +205,8 @@ export const ru: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Нет зависимостей',
|
title: 'Нет отношений',
|
||||||
description: 'Создайте представление, чтобы начать',
|
description: 'Создайте отношение, чтобы начать',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -464,6 +472,7 @@ export const ru: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Создать таблицу',
|
new_table: 'Создать таблицу',
|
||||||
|
new_view: 'Новое представление',
|
||||||
new_relationship: 'Создать отношение',
|
new_relationship: 'Создать отношение',
|
||||||
new_area: 'Новая область',
|
new_area: 'Новая область',
|
||||||
},
|
},
|
||||||
@@ -485,6 +494,9 @@ export const ru: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Сменить язык',
|
change_language: 'Сменить язык',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'Вкл',
|
||||||
|
off: 'Выкл',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const te: LanguageTranslation = {
|
export const te: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'కొత్తది',
|
||||||
|
browse: 'బ్రాఉజ్',
|
||||||
|
tables: 'టేబల్లు',
|
||||||
|
refs: 'సంబంధాలు',
|
||||||
|
areas: 'ప్రదేశాలు',
|
||||||
|
dependencies: 'ఆధారతలు',
|
||||||
|
custom_types: 'కస్టమ్ టైప్స్',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'ఫైల్',
|
databases: 'డేటాబేస్లు',
|
||||||
new: 'కొత్తది',
|
new: 'కొత్త డైగ్రాం',
|
||||||
open: 'తెరవు',
|
browse: 'బ్రాఉజ్ చేయండి...',
|
||||||
save: 'సేవ్',
|
save: 'సేవ్',
|
||||||
|
duplicate: 'డుప్లికేట్',
|
||||||
import: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
|
import: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
|
||||||
export_sql: 'SQL ఎగుమతి',
|
export_sql: 'SQL ఎగుమతి',
|
||||||
export_as: 'వగా ఎగుమతి చేయండి',
|
export_as: 'వగా ఎగుమతి చేయండి',
|
||||||
delete_diagram: 'చిత్రాన్ని తొలగించండి',
|
delete_diagram: 'చిత్రాన్ని తొలగించండి',
|
||||||
exit: 'నిష్క్రమించు',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'సవరించు',
|
edit: 'సవరించు',
|
||||||
@@ -29,6 +38,7 @@ export const te: LanguageTranslation = {
|
|||||||
show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
|
show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
|
||||||
hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
|
hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
|
||||||
zoom_on_scroll: 'స్క్రోల్పై జూమ్',
|
zoom_on_scroll: 'స్క్రోల్పై జూమ్',
|
||||||
|
show_views: 'డేటాబేస్ వ్యూలు',
|
||||||
theme: 'థీమ్',
|
theme: 'థీమ్',
|
||||||
show_dependencies: 'ఆధారాలు చూపించండి',
|
show_dependencies: 'ఆధారాలు చూపించండి',
|
||||||
hide_dependencies: 'ఆధారాలను దాచండి',
|
hide_dependencies: 'ఆధారాలను దాచండి',
|
||||||
@@ -111,6 +121,7 @@ export const te: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'పట్టికలు',
|
tables: 'పట్టికలు',
|
||||||
add_table: 'పట్టికను జోడించు',
|
add_table: 'పట్టికను జోడించు',
|
||||||
|
add_view: 'వ్యూ జోడించండి',
|
||||||
filter: 'ఫిల్టర్',
|
filter: 'ఫిల్టర్',
|
||||||
collapse: 'అన్ని కూల్ చేయి',
|
collapse: 'అన్ని కూల్ చేయి',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -136,6 +147,7 @@ export const te: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'ఫీల్డ్ గుణాలు',
|
title: 'ఫీల్డ్ గుణాలు',
|
||||||
unique: 'అద్వితీయ',
|
unique: 'అద్వితీయ',
|
||||||
|
auto_increment: 'ఆటో ఇంక్రిమెంట్',
|
||||||
comments: 'వ్యాఖ్యలు',
|
comments: 'వ్యాఖ్యలు',
|
||||||
no_comments: 'వ్యాఖ్యలు లేవు',
|
no_comments: 'వ్యాఖ్యలు లేవు',
|
||||||
delete_field: 'ఫీల్డ్ తొలగించు',
|
delete_field: 'ఫీల్డ్ తొలగించు',
|
||||||
@@ -151,6 +163,7 @@ export const te: LanguageTranslation = {
|
|||||||
title: 'ఇండెక్స్ గుణాలు',
|
title: 'ఇండెక్స్ గుణాలు',
|
||||||
name: 'పేరు',
|
name: 'పేరు',
|
||||||
unique: 'అద్వితీయ',
|
unique: 'అద్వితీయ',
|
||||||
|
index_type: 'ఇండెక్స్ రకం',
|
||||||
delete_index: 'ఇండెక్స్ తొలగించు',
|
delete_index: 'ఇండెక్స్ తొలగించు',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -168,12 +181,15 @@ export const te: LanguageTranslation = {
|
|||||||
description: 'ప్రారంభించడానికి ఒక పట్టిక సృష్టించండి',
|
description: 'ప్రారంభించడానికి ఒక పట్టిక సృష్టించండి',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'సంబంధాలు',
|
refs: 'Refs',
|
||||||
filter: 'ఫిల్టర్',
|
filter: 'ఫిల్టర్',
|
||||||
add_relationship: 'సంబంధం జోడించు',
|
|
||||||
collapse: 'అన్ని కూల్ చేయి',
|
collapse: 'అన్ని కూల్ చేయి',
|
||||||
|
add_relationship: 'సంబంధం జోడించు',
|
||||||
|
relationships: 'సంబంధాలు',
|
||||||
|
dependencies: 'ఆధారాలు',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'సంబంధం',
|
||||||
primary: 'ప్రాథమిక పట్టిక',
|
primary: 'ప్రాథమిక పట్టిక',
|
||||||
foreign: 'సూచించబడిన పట్టిక',
|
foreign: 'సూచించబడిన పట్టిక',
|
||||||
cardinality: 'కార్డినాలిటీ',
|
cardinality: 'కార్డినాలిటీ',
|
||||||
@@ -183,16 +199,8 @@ export const te: LanguageTranslation = {
|
|||||||
delete_relationship: 'సంబంధం తొలగించు',
|
delete_relationship: 'సంబంధం తొలగించు',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'సంబంధాలు లేవు',
|
|
||||||
description: 'పట్టికలను అనుసంధించడానికి సంబంధం సృష్టించండి',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'ఆధారాలు',
|
|
||||||
filter: 'ఫిల్టర్',
|
|
||||||
collapse: 'అన్ని కూల్ చేయి',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'ఆధారం',
|
||||||
table: 'పట్టిక',
|
table: 'పట్టిక',
|
||||||
dependent_table: 'ఆధారిత వీక్షణ',
|
dependent_table: 'ఆధారిత వీక్షణ',
|
||||||
delete_dependency: 'ఆధారాన్ని తొలగించు',
|
delete_dependency: 'ఆధారాన్ని తొలగించు',
|
||||||
@@ -202,8 +210,8 @@ export const te: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'ఆధారాలు లేవు',
|
title: 'సంబంధాలు లేవు',
|
||||||
description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి',
|
description: 'ప్రారంభించడానికి ఒక సంబంధం సృష్టించండి',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -472,6 +480,7 @@ export const te: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'కొత్త పట్టిక',
|
new_table: 'కొత్త పట్టిక',
|
||||||
|
new_view: 'కొత్త వ్యూ',
|
||||||
new_relationship: 'కొత్త సంబంధం',
|
new_relationship: 'కొత్త సంబంధం',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -495,6 +504,9 @@ export const te: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'భాష మార్చు',
|
change_language: 'భాష మార్చు',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'ఆన్',
|
||||||
|
off: 'ఆఫ్',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const tr: LanguageTranslation = {
|
export const tr: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'Yeni',
|
||||||
|
browse: 'Gözat',
|
||||||
|
tables: 'Tablolar',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'Alanlar',
|
||||||
|
dependencies: 'Bağımlılıklar',
|
||||||
|
custom_types: 'Özel Tipler',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'Dosya',
|
databases: 'Veritabanları',
|
||||||
new: 'Yeni',
|
new: 'Yeni Diyagram',
|
||||||
open: 'Aç',
|
browse: 'Gözat...',
|
||||||
save: 'Kaydet',
|
save: 'Kaydet',
|
||||||
|
duplicate: 'Kopyala',
|
||||||
import: 'Veritabanı İçe Aktar',
|
import: 'Veritabanı İçe Aktar',
|
||||||
export_sql: 'SQL Olarak Dışa Aktar',
|
export_sql: 'SQL Olarak Dışa Aktar',
|
||||||
export_as: 'Olarak Dışa Aktar',
|
export_as: 'Olarak Dışa Aktar',
|
||||||
delete_diagram: 'Diyagramı Sil',
|
delete_diagram: 'Diyagramı Sil',
|
||||||
exit: 'Çıkış',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Düzenle',
|
edit: 'Düzenle',
|
||||||
@@ -29,6 +38,7 @@ export const tr: LanguageTranslation = {
|
|||||||
show_field_attributes: 'Alan Özelliklerini Göster',
|
show_field_attributes: 'Alan Özelliklerini Göster',
|
||||||
hide_field_attributes: 'Alan Özelliklerini Gizle',
|
hide_field_attributes: 'Alan Özelliklerini Gizle',
|
||||||
zoom_on_scroll: 'Kaydırarak Yakınlaştır',
|
zoom_on_scroll: 'Kaydırarak Yakınlaştır',
|
||||||
|
show_views: 'Veritabanı Görünümleri',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Bağımlılıkları Göster',
|
show_dependencies: 'Bağımlılıkları Göster',
|
||||||
hide_dependencies: 'Bağımlılıkları Gizle',
|
hide_dependencies: 'Bağımlılıkları Gizle',
|
||||||
@@ -110,6 +120,7 @@ export const tr: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Tablolar',
|
tables: 'Tablolar',
|
||||||
add_table: 'Tablo Ekle',
|
add_table: 'Tablo Ekle',
|
||||||
|
add_view: 'Görünüm Ekle',
|
||||||
filter: 'Filtrele',
|
filter: 'Filtrele',
|
||||||
collapse: 'Hepsini Daralt',
|
collapse: 'Hepsini Daralt',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -135,6 +146,7 @@ export const tr: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Alan Özellikleri',
|
title: 'Alan Özellikleri',
|
||||||
unique: 'Tekil',
|
unique: 'Tekil',
|
||||||
|
auto_increment: 'Otomatik Artış',
|
||||||
comments: 'Yorumlar',
|
comments: 'Yorumlar',
|
||||||
no_comments: 'Yorum yok',
|
no_comments: 'Yorum yok',
|
||||||
delete_field: 'Alanı Sil',
|
delete_field: 'Alanı Sil',
|
||||||
@@ -150,6 +162,7 @@ export const tr: LanguageTranslation = {
|
|||||||
title: 'İndeks Özellikleri',
|
title: 'İndeks Özellikleri',
|
||||||
name: 'Ad',
|
name: 'Ad',
|
||||||
unique: 'Tekil',
|
unique: 'Tekil',
|
||||||
|
index_type: 'İndeks Türü',
|
||||||
delete_index: 'İndeksi Sil',
|
delete_index: 'İndeksi Sil',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -167,12 +180,15 @@ export const tr: LanguageTranslation = {
|
|||||||
description: 'Başlamak için bir tablo oluşturun',
|
description: 'Başlamak için bir tablo oluşturun',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'İlişkiler',
|
refs: 'Refs',
|
||||||
filter: 'Filtrele',
|
filter: 'Filtrele',
|
||||||
add_relationship: 'İlişki Ekle',
|
|
||||||
collapse: 'Hepsini Daralt',
|
collapse: 'Hepsini Daralt',
|
||||||
|
add_relationship: 'İlişki Ekle',
|
||||||
|
relationships: 'İlişkiler',
|
||||||
|
dependencies: 'Bağımlılıklar',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'İlişki',
|
||||||
primary: 'Birincil Tablo',
|
primary: 'Birincil Tablo',
|
||||||
foreign: 'Referans Tablo',
|
foreign: 'Referans Tablo',
|
||||||
cardinality: 'Kardinalite',
|
cardinality: 'Kardinalite',
|
||||||
@@ -182,16 +198,8 @@ export const tr: LanguageTranslation = {
|
|||||||
delete_relationship: 'Sil',
|
delete_relationship: 'Sil',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'İlişki yok',
|
|
||||||
description: 'Tabloları bağlamak için bir ilişki oluşturun',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Bağımlılıklar',
|
|
||||||
filter: 'Filtrele',
|
|
||||||
collapse: 'Hepsini Daralt',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'Bağımlılık',
|
||||||
table: 'Tablo',
|
table: 'Tablo',
|
||||||
dependent_table: 'Bağımlı Görünüm',
|
dependent_table: 'Bağımlı Görünüm',
|
||||||
delete_dependency: 'Sil',
|
delete_dependency: 'Sil',
|
||||||
@@ -201,8 +209,8 @@ export const tr: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Bağımlılık yok',
|
title: 'İlişki yok',
|
||||||
description: 'Başlamak için bir görünüm oluşturun',
|
description: 'Başlamak için bir ilişki oluşturun',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -457,6 +465,7 @@ export const tr: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Yeni Tablo',
|
new_table: 'Yeni Tablo',
|
||||||
|
new_view: 'Yeni Görünüm',
|
||||||
new_relationship: 'Yeni İlişki',
|
new_relationship: 'Yeni İlişki',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -479,6 +488,9 @@ export const tr: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Dil',
|
change_language: 'Dil',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'Açık',
|
||||||
|
off: 'Kapalı',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const uk: LanguageTranslation = {
|
export const uk: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'Нова',
|
||||||
|
browse: 'Огляд',
|
||||||
|
tables: 'Таблиці',
|
||||||
|
refs: 'Зв’язки',
|
||||||
|
areas: 'Області',
|
||||||
|
dependencies: 'Залежності',
|
||||||
|
custom_types: 'Користувацькі типи',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'Файл',
|
databases: 'Бази даних',
|
||||||
new: 'Новий',
|
new: 'Нова діаграма',
|
||||||
open: 'Відкрити',
|
browse: 'Огляд...',
|
||||||
save: 'Зберегти',
|
save: 'Зберегти',
|
||||||
|
duplicate: 'Дублювати',
|
||||||
import: 'Імпорт бази даних',
|
import: 'Імпорт бази даних',
|
||||||
export_sql: 'Експорт SQL',
|
export_sql: 'Експорт SQL',
|
||||||
export_as: 'Експортувати як',
|
export_as: 'Експортувати як',
|
||||||
delete_diagram: 'Видалити діаграму',
|
delete_diagram: 'Видалити діаграму',
|
||||||
exit: 'Вийти',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Редагувати',
|
edit: 'Редагувати',
|
||||||
@@ -29,6 +38,7 @@ export const uk: LanguageTranslation = {
|
|||||||
show_field_attributes: 'Показати атрибути полів',
|
show_field_attributes: 'Показати атрибути полів',
|
||||||
hide_field_attributes: 'Приховати атрибути полів',
|
hide_field_attributes: 'Приховати атрибути полів',
|
||||||
zoom_on_scroll: 'Масштабувати прокручуванням',
|
zoom_on_scroll: 'Масштабувати прокручуванням',
|
||||||
|
show_views: 'Представлення бази даних',
|
||||||
theme: 'Тема',
|
theme: 'Тема',
|
||||||
show_dependencies: 'Показати залежності',
|
show_dependencies: 'Показати залежності',
|
||||||
hide_dependencies: 'Приховати залежності',
|
hide_dependencies: 'Приховати залежності',
|
||||||
@@ -109,6 +119,7 @@ export const uk: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Таблиці',
|
tables: 'Таблиці',
|
||||||
add_table: 'Додати таблицю',
|
add_table: 'Додати таблицю',
|
||||||
|
add_view: 'Додати представлення',
|
||||||
filter: 'Фільтр',
|
filter: 'Фільтр',
|
||||||
collapse: 'Згорнути все',
|
collapse: 'Згорнути все',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -134,6 +145,7 @@ export const uk: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Атрибути полів',
|
title: 'Атрибути полів',
|
||||||
unique: 'Унікальне',
|
unique: 'Унікальне',
|
||||||
|
auto_increment: 'Автоінкремент',
|
||||||
comments: 'Коментарі',
|
comments: 'Коментарі',
|
||||||
no_comments: 'Немає коментарів',
|
no_comments: 'Немає коментарів',
|
||||||
delete_field: 'Видалити поле',
|
delete_field: 'Видалити поле',
|
||||||
@@ -149,6 +161,7 @@ export const uk: LanguageTranslation = {
|
|||||||
title: 'Атрибути індексу',
|
title: 'Атрибути індексу',
|
||||||
name: 'Назва індекса',
|
name: 'Назва індекса',
|
||||||
unique: 'Унікальний',
|
unique: 'Унікальний',
|
||||||
|
index_type: 'Тип індексу',
|
||||||
delete_index: 'Видалити індекс',
|
delete_index: 'Видалити індекс',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -165,12 +178,15 @@ export const uk: LanguageTranslation = {
|
|||||||
description: 'Щоб почати, створіть таблицю',
|
description: 'Щоб почати, створіть таблицю',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'Звʼязки',
|
refs: 'Refs',
|
||||||
filter: 'Фільтр',
|
filter: 'Фільтр',
|
||||||
add_relationship: 'Додати звʼязок',
|
|
||||||
collapse: 'Згорнути все',
|
collapse: 'Згорнути все',
|
||||||
|
add_relationship: 'Додати звʼязок',
|
||||||
|
relationships: 'Звʼязки',
|
||||||
|
dependencies: 'Залежності',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'Звʼязок',
|
||||||
primary: 'Первинна таблиця',
|
primary: 'Первинна таблиця',
|
||||||
foreign: 'Посилання на таблицю',
|
foreign: 'Посилання на таблицю',
|
||||||
cardinality: 'Звʼязок',
|
cardinality: 'Звʼязок',
|
||||||
@@ -180,16 +196,8 @@ export const uk: LanguageTranslation = {
|
|||||||
delete_relationship: 'Видалити',
|
delete_relationship: 'Видалити',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'Звʼязків немає',
|
|
||||||
description: 'Створіть звʼязок для зʼєднання таблиць',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Залежності',
|
|
||||||
filter: 'Фільтр',
|
|
||||||
collapse: 'Згорнути все',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'Залежність',
|
||||||
table: 'Таблиця',
|
table: 'Таблиця',
|
||||||
dependent_table: 'Залежне подання',
|
dependent_table: 'Залежне подання',
|
||||||
delete_dependency: 'Видалити',
|
delete_dependency: 'Видалити',
|
||||||
@@ -199,8 +207,8 @@ export const uk: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Жодних залежностей',
|
title: 'Жодних зв’язків',
|
||||||
description: 'Створіть подання, щоб почати',
|
description: 'Створіть зв’язок, щоб почати',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -463,6 +471,7 @@ export const uk: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Нова таблиця',
|
new_table: 'Нова таблиця',
|
||||||
|
new_view: 'Нове представлення',
|
||||||
new_relationship: 'Новий звʼязок',
|
new_relationship: 'Новий звʼязок',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -484,6 +493,9 @@ export const uk: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Мова',
|
change_language: 'Мова',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'Увімк',
|
||||||
|
off: 'Вимк',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const vi: LanguageTranslation = {
|
export const vi: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: 'Mới',
|
||||||
|
browse: 'Duyệt',
|
||||||
|
tables: 'Bảng',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: 'Khu vực',
|
||||||
|
dependencies: 'Phụ thuộc',
|
||||||
|
custom_types: 'Kiểu tùy chỉnh',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: 'Tệp',
|
databases: 'Cơ sở dữ liệu',
|
||||||
new: 'Tạo mới',
|
new: 'Sơ đồ mới',
|
||||||
open: 'Mở',
|
browse: 'Duyệt...',
|
||||||
save: 'Lưu',
|
save: 'Lưu',
|
||||||
|
duplicate: 'Nhân đôi',
|
||||||
import: 'Nhập cơ sở dữ liệu',
|
import: 'Nhập cơ sở dữ liệu',
|
||||||
export_sql: 'Xuất SQL',
|
export_sql: 'Xuất SQL',
|
||||||
export_as: 'Xuất thành',
|
export_as: 'Xuất thành',
|
||||||
delete_diagram: 'Xóa sơ đồ',
|
delete_diagram: 'Xóa sơ đồ',
|
||||||
exit: 'Thoát',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'Sửa',
|
edit: 'Sửa',
|
||||||
@@ -29,6 +38,7 @@ export const vi: LanguageTranslation = {
|
|||||||
show_field_attributes: 'Hiển thị thuộc tính trường',
|
show_field_attributes: 'Hiển thị thuộc tính trường',
|
||||||
hide_field_attributes: 'Ẩn thuộc tính trường',
|
hide_field_attributes: 'Ẩn thuộc tính trường',
|
||||||
zoom_on_scroll: 'Thu phóng khi cuộn',
|
zoom_on_scroll: 'Thu phóng khi cuộn',
|
||||||
|
show_views: 'Chế độ xem Cơ sở dữ liệu',
|
||||||
theme: 'Chủ đề',
|
theme: 'Chủ đề',
|
||||||
show_dependencies: 'Hiển thị các phụ thuộc',
|
show_dependencies: 'Hiển thị các phụ thuộc',
|
||||||
hide_dependencies: 'Ẩn các phụ thuộc',
|
hide_dependencies: 'Ẩn các phụ thuộc',
|
||||||
@@ -110,6 +120,7 @@ export const vi: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: 'Bảng',
|
tables: 'Bảng',
|
||||||
add_table: 'Thêm bảng',
|
add_table: 'Thêm bảng',
|
||||||
|
add_view: 'Thêm Chế độ xem',
|
||||||
filter: 'Lọc',
|
filter: 'Lọc',
|
||||||
collapse: 'Thu gọn tất cả',
|
collapse: 'Thu gọn tất cả',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -135,6 +146,7 @@ export const vi: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Thuộc tính trường',
|
title: 'Thuộc tính trường',
|
||||||
unique: 'Giá trị duy nhất',
|
unique: 'Giá trị duy nhất',
|
||||||
|
auto_increment: 'Tự động tăng',
|
||||||
comments: 'Bình luận',
|
comments: 'Bình luận',
|
||||||
no_comments: 'Không có bình luận',
|
no_comments: 'Không có bình luận',
|
||||||
delete_field: 'Xóa trường',
|
delete_field: 'Xóa trường',
|
||||||
@@ -150,6 +162,7 @@ export const vi: LanguageTranslation = {
|
|||||||
title: 'Thuộc tính chỉ mục',
|
title: 'Thuộc tính chỉ mục',
|
||||||
name: 'Tên',
|
name: 'Tên',
|
||||||
unique: 'Giá trị duy nhất',
|
unique: 'Giá trị duy nhất',
|
||||||
|
index_type: 'Loại chỉ mục',
|
||||||
delete_index: 'Xóa chỉ mục',
|
delete_index: 'Xóa chỉ mục',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -166,12 +179,15 @@ export const vi: LanguageTranslation = {
|
|||||||
description: 'Tạo một bảng để bắt đầu',
|
description: 'Tạo một bảng để bắt đầu',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: 'Quan hệ',
|
refs: 'Refs',
|
||||||
filter: 'Lọc',
|
filter: 'Lọc',
|
||||||
add_relationship: 'Thêm quan hệ',
|
|
||||||
collapse: 'Thu gọn tất cả',
|
collapse: 'Thu gọn tất cả',
|
||||||
|
add_relationship: 'Thêm quan hệ',
|
||||||
|
relationships: 'Quan hệ',
|
||||||
|
dependencies: 'Phụ thuộc',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: 'Quan hệ',
|
||||||
primary: 'Bảng khóa chính',
|
primary: 'Bảng khóa chính',
|
||||||
foreign: 'Bảng khóa ngoại',
|
foreign: 'Bảng khóa ngoại',
|
||||||
cardinality: 'Quan hệ',
|
cardinality: 'Quan hệ',
|
||||||
@@ -181,16 +197,8 @@ export const vi: LanguageTranslation = {
|
|||||||
delete_relationship: 'Xóa',
|
delete_relationship: 'Xóa',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: 'Không có quan hệ',
|
|
||||||
description: 'Tạo quan hệ để kết nối các bảng',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: 'Phụ thuộc',
|
|
||||||
filter: 'Lọc',
|
|
||||||
collapse: 'Thu gọn tất cả',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: 'Phụ thuộc',
|
||||||
table: 'Bảng',
|
table: 'Bảng',
|
||||||
dependent_table: 'Bảng xem phụ thuộc',
|
dependent_table: 'Bảng xem phụ thuộc',
|
||||||
delete_dependency: 'Xóa',
|
delete_dependency: 'Xóa',
|
||||||
@@ -200,8 +208,8 @@ export const vi: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Không có phụ thuộc',
|
title: 'Không có quan hệ',
|
||||||
description: 'Tạo bảng xem phụ thuộc để bắt đầu',
|
description: 'Tạo một quan hệ để bắt đầu',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -464,6 +472,7 @@ export const vi: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Tạo bảng mới',
|
new_table: 'Tạo bảng mới',
|
||||||
|
new_view: 'Chế độ xem Mới',
|
||||||
new_relationship: 'Tạo quan hệ mới',
|
new_relationship: 'Tạo quan hệ mới',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -485,6 +494,9 @@ export const vi: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: 'Ngôn ngữ',
|
change_language: 'Ngôn ngữ',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: 'Bật',
|
||||||
|
off: 'Tắt',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const zh_CN: LanguageTranslation = {
|
export const zh_CN: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: '新建',
|
||||||
|
browse: '浏览',
|
||||||
|
tables: '表',
|
||||||
|
refs: '引用',
|
||||||
|
areas: '区域',
|
||||||
|
dependencies: '依赖关系',
|
||||||
|
custom_types: '自定义类型',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: '文件',
|
databases: '数据库',
|
||||||
new: '新建',
|
new: '新建关系图',
|
||||||
open: '打开',
|
browse: '浏览...',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
|
duplicate: '复制',
|
||||||
import: '导入数据库',
|
import: '导入数据库',
|
||||||
export_sql: '导出 SQL 语句',
|
export_sql: '导出 SQL 语句',
|
||||||
export_as: '导出为',
|
export_as: '导出为',
|
||||||
delete_diagram: '删除关系图',
|
delete_diagram: '删除关系图',
|
||||||
exit: '退出',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: '编辑',
|
edit: '编辑',
|
||||||
@@ -29,6 +38,7 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
show_field_attributes: '展示字段属性',
|
show_field_attributes: '展示字段属性',
|
||||||
hide_field_attributes: '隐藏字段属性',
|
hide_field_attributes: '隐藏字段属性',
|
||||||
zoom_on_scroll: '滚动缩放',
|
zoom_on_scroll: '滚动缩放',
|
||||||
|
show_views: '数据库视图',
|
||||||
theme: '主题',
|
theme: '主题',
|
||||||
show_dependencies: '展示依赖',
|
show_dependencies: '展示依赖',
|
||||||
hide_dependencies: '隐藏依赖',
|
hide_dependencies: '隐藏依赖',
|
||||||
@@ -107,6 +117,7 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: '表',
|
tables: '表',
|
||||||
add_table: '添加表',
|
add_table: '添加表',
|
||||||
|
add_view: '添加视图',
|
||||||
filter: '筛选',
|
filter: '筛选',
|
||||||
collapse: '全部折叠',
|
collapse: '全部折叠',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -132,6 +143,7 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: '字段属性',
|
title: '字段属性',
|
||||||
unique: '唯一',
|
unique: '唯一',
|
||||||
|
auto_increment: '自动递增',
|
||||||
comments: '注释',
|
comments: '注释',
|
||||||
no_comments: '空',
|
no_comments: '空',
|
||||||
delete_field: '删除字段',
|
delete_field: '删除字段',
|
||||||
@@ -147,6 +159,7 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
title: '索引属性',
|
title: '索引属性',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
unique: '唯一',
|
unique: '唯一',
|
||||||
|
index_type: '索引类型',
|
||||||
delete_index: '删除索引',
|
delete_index: '删除索引',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -163,12 +176,15 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
description: '新建表以开始',
|
description: '新建表以开始',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: '关系',
|
refs: '引用',
|
||||||
filter: '筛选',
|
filter: '筛选',
|
||||||
add_relationship: '添加关系',
|
|
||||||
collapse: '全部折叠',
|
collapse: '全部折叠',
|
||||||
|
add_relationship: '添加关系',
|
||||||
|
relationships: '关系',
|
||||||
|
dependencies: '依赖关系',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: '关系',
|
||||||
primary: '主表',
|
primary: '主表',
|
||||||
foreign: '被引用表',
|
foreign: '被引用表',
|
||||||
cardinality: '基数',
|
cardinality: '基数',
|
||||||
@@ -178,16 +194,8 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
delete_relationship: '删除',
|
delete_relationship: '删除',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: '无关系',
|
|
||||||
description: '创建关系以连接表',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: '依赖关系',
|
|
||||||
filter: '筛选',
|
|
||||||
collapse: '全部折叠',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: '依赖',
|
||||||
table: '表',
|
table: '表',
|
||||||
dependent_table: '依赖视图',
|
dependent_table: '依赖视图',
|
||||||
delete_dependency: '删除',
|
delete_dependency: '删除',
|
||||||
@@ -197,8 +205,8 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: '无依赖',
|
title: '无关系',
|
||||||
description: '创建视图以开始',
|
description: '创建关系以开始',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -459,6 +467,7 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: '新建表',
|
new_table: '新建表',
|
||||||
|
new_view: '新建视图',
|
||||||
new_relationship: '新建关系',
|
new_relationship: '新建关系',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -480,6 +489,9 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: '语言',
|
change_language: '语言',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: '开启',
|
||||||
|
off: '关闭',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
|
|||||||
|
|
||||||
export const zh_TW: LanguageTranslation = {
|
export const zh_TW: LanguageTranslation = {
|
||||||
translation: {
|
translation: {
|
||||||
|
editor_sidebar: {
|
||||||
|
new_diagram: '新建',
|
||||||
|
browse: '瀏覽',
|
||||||
|
tables: '表格',
|
||||||
|
refs: 'Refs',
|
||||||
|
areas: '區域',
|
||||||
|
dependencies: '相依性',
|
||||||
|
custom_types: '自定義類型',
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
databases: {
|
||||||
file: '檔案',
|
databases: '資料庫',
|
||||||
new: '新增',
|
new: '新增圖表',
|
||||||
open: '開啟',
|
browse: '瀏覽...',
|
||||||
save: '儲存',
|
save: '儲存',
|
||||||
|
duplicate: '複製',
|
||||||
import: '匯入資料庫',
|
import: '匯入資料庫',
|
||||||
export_sql: '匯出 SQL',
|
export_sql: '匯出 SQL',
|
||||||
export_as: '匯出為特定格式',
|
export_as: '匯出為特定格式',
|
||||||
delete_diagram: '刪除圖表',
|
delete_diagram: '刪除圖表',
|
||||||
exit: '退出',
|
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: '編輯',
|
edit: '編輯',
|
||||||
@@ -29,6 +38,7 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
hide_field_attributes: '隱藏欄位屬性',
|
hide_field_attributes: '隱藏欄位屬性',
|
||||||
show_field_attributes: '顯示欄位屬性',
|
show_field_attributes: '顯示欄位屬性',
|
||||||
zoom_on_scroll: '滾動縮放',
|
zoom_on_scroll: '滾動縮放',
|
||||||
|
show_views: '資料庫檢視',
|
||||||
theme: '主題',
|
theme: '主題',
|
||||||
show_dependencies: '顯示相依性',
|
show_dependencies: '顯示相依性',
|
||||||
hide_dependencies: '隱藏相依性',
|
hide_dependencies: '隱藏相依性',
|
||||||
@@ -107,6 +117,7 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
tables_section: {
|
tables_section: {
|
||||||
tables: '表格',
|
tables: '表格',
|
||||||
add_table: '新增表格',
|
add_table: '新增表格',
|
||||||
|
add_view: '新增檢視',
|
||||||
filter: '篩選',
|
filter: '篩選',
|
||||||
collapse: '全部摺疊',
|
collapse: '全部摺疊',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -132,6 +143,7 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: '欄位屬性',
|
title: '欄位屬性',
|
||||||
unique: '唯一',
|
unique: '唯一',
|
||||||
|
auto_increment: '自動遞增',
|
||||||
comments: '註解',
|
comments: '註解',
|
||||||
no_comments: '無註解',
|
no_comments: '無註解',
|
||||||
delete_field: '刪除欄位',
|
delete_field: '刪除欄位',
|
||||||
@@ -147,6 +159,7 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
title: '索引屬性',
|
title: '索引屬性',
|
||||||
name: '名稱',
|
name: '名稱',
|
||||||
unique: '唯一',
|
unique: '唯一',
|
||||||
|
index_type: '索引類型',
|
||||||
delete_index: '刪除索引',
|
delete_index: '刪除索引',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
@@ -163,12 +176,15 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
description: '請新增表格以開始',
|
description: '請新增表格以開始',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
refs_section: {
|
||||||
relationships: '關聯',
|
refs: 'Refs',
|
||||||
filter: '篩選',
|
filter: '篩選',
|
||||||
add_relationship: '新增關聯',
|
|
||||||
collapse: '全部摺疊',
|
collapse: '全部摺疊',
|
||||||
|
add_relationship: '新增關聯',
|
||||||
|
relationships: '關聯',
|
||||||
|
dependencies: '相依性',
|
||||||
relationship: {
|
relationship: {
|
||||||
|
relationship: '關聯',
|
||||||
primary: '主表格',
|
primary: '主表格',
|
||||||
foreign: '參照表格',
|
foreign: '參照表格',
|
||||||
cardinality: '基數',
|
cardinality: '基數',
|
||||||
@@ -178,16 +194,8 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
delete_relationship: '刪除',
|
delete_relationship: '刪除',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
|
||||||
title: '尚無關聯',
|
|
||||||
description: '請新增關聯以連接表格',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies_section: {
|
|
||||||
dependencies: '相依性',
|
|
||||||
filter: '篩選',
|
|
||||||
collapse: '全部摺疊',
|
|
||||||
dependency: {
|
dependency: {
|
||||||
|
dependency: '相依性',
|
||||||
table: '表格',
|
table: '表格',
|
||||||
dependent_table: '相依檢視',
|
dependent_table: '相依檢視',
|
||||||
delete_dependency: '刪除',
|
delete_dependency: '刪除',
|
||||||
@@ -197,8 +205,8 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: '尚無相依性',
|
title: '尚無關聯',
|
||||||
description: '請建立檢視以開始',
|
description: '請建立關聯以開始',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -459,6 +467,7 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: '新建表格',
|
new_table: '新建表格',
|
||||||
|
new_view: '新檢視',
|
||||||
new_relationship: '新建關聯',
|
new_relationship: '新建關聯',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
new_area: 'New Area',
|
new_area: 'New Area',
|
||||||
@@ -480,6 +489,9 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
language_select: {
|
language_select: {
|
||||||
change_language: '變更語言',
|
change_language: '變更語言',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
on: '開啟',
|
||||||
|
off: '關閉',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,5 @@ export const randomColor = () => {
|
|||||||
|
|
||||||
export const viewColor = '#b0b0b0';
|
export const viewColor = '#b0b0b0';
|
||||||
export const materializedViewColor = '#7d7d7d';
|
export const materializedViewColor = '#7d7d7d';
|
||||||
|
export const defaultTableColor = '#8eb7ff';
|
||||||
|
export const defaultAreaColor = '#b067e9';
|
||||||
|
|||||||
@@ -146,3 +146,22 @@ export const findDataTypeDataById = (
|
|||||||
|
|
||||||
return dataTypesOptions.find((dataType) => dataType.id === id);
|
return dataTypesOptions.find((dataType) => dataType.id === id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const supportsAutoIncrementDataType = (
|
||||||
|
dataTypeName: string
|
||||||
|
): boolean => {
|
||||||
|
return [
|
||||||
|
'integer',
|
||||||
|
'int',
|
||||||
|
'bigint',
|
||||||
|
'smallint',
|
||||||
|
'tinyint',
|
||||||
|
'mediumint',
|
||||||
|
'serial',
|
||||||
|
'bigserial',
|
||||||
|
'smallserial',
|
||||||
|
'number',
|
||||||
|
'numeric',
|
||||||
|
'decimal',
|
||||||
|
].includes(dataTypeName.toLocaleLowerCase());
|
||||||
|
};
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ export function exportMSSQL({
|
|||||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||||
|
|
||||||
// Check if identity column
|
// Check if identity column
|
||||||
const identity = field.default
|
const identity =
|
||||||
?.toLowerCase()
|
field.increment ||
|
||||||
.includes('identity')
|
field.default?.toLowerCase().includes('identity')
|
||||||
? ' IDENTITY(1,1)'
|
? ' IDENTITY(1,1)'
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
@@ -168,6 +168,7 @@ export function exportMSSQL({
|
|||||||
// Handle default value using SQL Server specific parser
|
// Handle default value using SQL Server specific parser
|
||||||
const defaultValue =
|
const defaultValue =
|
||||||
field.default &&
|
field.default &&
|
||||||
|
!field.increment &&
|
||||||
!field.default.toLowerCase().includes('identity')
|
!field.default.toLowerCase().includes('identity')
|
||||||
? ` DEFAULT ${parseMSSQLDefault(field)}`
|
? ` DEFAULT ${parseMSSQLDefault(field)}`
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
@@ -274,14 +274,15 @@ export function exportMySQL({
|
|||||||
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
|
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
|
||||||
let autoIncrement = '';
|
let autoIncrement = '';
|
||||||
if (
|
if (
|
||||||
field.primaryKey &&
|
field.increment ||
|
||||||
|
(field.primaryKey &&
|
||||||
(field.default
|
(field.default
|
||||||
?.toLowerCase()
|
?.toLowerCase()
|
||||||
.includes('identity') ||
|
.includes('identity') ||
|
||||||
field.default
|
field.default
|
||||||
?.toLowerCase()
|
?.toLowerCase()
|
||||||
.includes('autoincrement') ||
|
.includes('autoincrement') ||
|
||||||
field.default?.includes('nextval'))
|
field.default?.includes('nextval')))
|
||||||
) {
|
) {
|
||||||
autoIncrement = ' AUTO_INCREMENT';
|
autoIncrement = ' AUTO_INCREMENT';
|
||||||
}
|
}
|
||||||
@@ -290,9 +291,10 @@ export function exportMySQL({
|
|||||||
const unique =
|
const unique =
|
||||||
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||||
|
|
||||||
// Handle default value
|
// Handle default value - skip if auto increment
|
||||||
const defaultValue =
|
const defaultValue =
|
||||||
field.default &&
|
field.default &&
|
||||||
|
!field.increment &&
|
||||||
!field.default.toLowerCase().includes('identity') &&
|
!field.default.toLowerCase().includes('identity') &&
|
||||||
!field.default
|
!field.default
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ export function exportPostgreSQL({
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
return indexFieldNames.length > 0
|
return indexFieldNames.length > 0
|
||||||
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});`
|
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName}${index.type && index.type !== 'btree' ? ` USING ${index.type.toUpperCase()}` : ''} (${indexFieldNames.join(', ')});`
|
||||||
: '';
|
: '';
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|||||||
@@ -343,7 +343,8 @@ export function exportSQLite({
|
|||||||
if (
|
if (
|
||||||
field.primaryKey &&
|
field.primaryKey &&
|
||||||
singleIntegerPrimaryKey &&
|
singleIntegerPrimaryKey &&
|
||||||
(field.default
|
(field.increment ||
|
||||||
|
field.default
|
||||||
?.toLowerCase()
|
?.toLowerCase()
|
||||||
.includes('identity') ||
|
.includes('identity') ||
|
||||||
field.default
|
field.default
|
||||||
@@ -362,6 +363,7 @@ export function exportSQLite({
|
|||||||
let defaultValue = '';
|
let defaultValue = '';
|
||||||
if (
|
if (
|
||||||
field.default &&
|
field.default &&
|
||||||
|
!field.increment &&
|
||||||
!field.default.toLowerCase().includes('identity') &&
|
!field.default.toLowerCase().includes('identity') &&
|
||||||
!field.default
|
!field.default
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { Diagram } from '../../domain/diagram';
|
import type { Diagram } from '../../domain/diagram';
|
||||||
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
|
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import {
|
||||||
|
DatabaseType,
|
||||||
|
databaseTypesWithCommentSupport,
|
||||||
|
} from '@/lib/domain/database-type';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import type { DataType } from '../data-types/data-types';
|
import type { DataType } from '../data-types/data-types';
|
||||||
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
|
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
|
||||||
@@ -8,6 +11,7 @@ import { exportMSSQL } from './export-per-type/mssql';
|
|||||||
import { exportPostgreSQL } from './export-per-type/postgresql';
|
import { exportPostgreSQL } from './export-per-type/postgresql';
|
||||||
import { exportSQLite } from './export-per-type/sqlite';
|
import { exportSQLite } from './export-per-type/sqlite';
|
||||||
import { exportMySQL } from './export-per-type/mysql';
|
import { exportMySQL } from './export-per-type/mysql';
|
||||||
|
import { escapeSQLComment } from './export-per-type/common';
|
||||||
|
|
||||||
// Function to simplify verbose data type names
|
// Function to simplify verbose data type names
|
||||||
const simplifyDataType = (typeName: string): string => {
|
const simplifyDataType = (typeName: string): string => {
|
||||||
@@ -270,8 +274,13 @@ export const exportBaseSQL = ({
|
|||||||
sqlScript += ` UNIQUE`;
|
sqlScript += ` UNIQUE`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle AUTO INCREMENT - add as a comment for AI to process
|
||||||
|
if (field.increment) {
|
||||||
|
sqlScript += ` /* AUTO_INCREMENT */`;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle DEFAULT value
|
// Handle DEFAULT value
|
||||||
if (field.default) {
|
if (field.default && !field.increment) {
|
||||||
// Temp remove default user-define value when it have it
|
// Temp remove default user-define value when it have it
|
||||||
let fieldDefault = field.default;
|
let fieldDefault = field.default;
|
||||||
|
|
||||||
@@ -323,15 +332,18 @@ export const exportBaseSQL = ({
|
|||||||
|
|
||||||
sqlScript += '\n);\n';
|
sqlScript += '\n);\n';
|
||||||
|
|
||||||
// Add table comment
|
// Add table comment (only for databases that support COMMENT ON syntax)
|
||||||
if (table.comments) {
|
const supportsCommentOn =
|
||||||
sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n`;
|
databaseTypesWithCommentSupport.includes(targetDatabaseType);
|
||||||
|
|
||||||
|
if (table.comments && supportsCommentOn) {
|
||||||
|
sqlScript += `COMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.fields.forEach((field) => {
|
table.fields.forEach((field) => {
|
||||||
// Add column comment
|
// Add column comment (only for databases that support COMMENT ON syntax)
|
||||||
if (field.comments) {
|
if (field.comments && supportsCommentOn) {
|
||||||
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments.replace(/'/g, "''")}';\n`;
|
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${escapeSQLComment(field.comments)}';\n`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { DBField } from '@/lib/domain/db-field';
|
|||||||
import type { DBIndex } from '@/lib/domain/db-index';
|
import type { DBIndex } from '@/lib/domain/db-index';
|
||||||
import type { DataType } from '@/lib/data/data-types/data-types';
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
|
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
|
||||||
import { randomColor } from '@/lib/colors';
|
import { defaultTableColor } from '@/lib/colors';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
|
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
|
||||||
@@ -727,10 +727,10 @@ export function convertToChartDBDiagram(
|
|||||||
indexes,
|
indexes,
|
||||||
x: col * tableSpacing,
|
x: col * tableSpacing,
|
||||||
y: row * tableSpacing,
|
y: row * tableSpacing,
|
||||||
color: randomColor(),
|
color: defaultTableColor,
|
||||||
isView: false,
|
isView: false,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
};
|
} satisfies DBTable;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process relationships
|
// Process relationships
|
||||||
@@ -786,12 +786,6 @@ export function convertToChartDBDiagram(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!sourceField || !targetField) {
|
if (!sourceField || !targetField) {
|
||||||
console.log('Relationship refers to non-existent field:', {
|
|
||||||
sourceTable: rel.sourceTable,
|
|
||||||
sourceField: rel.sourceColumn,
|
|
||||||
targetTable: rel.targetTable,
|
|
||||||
targetField: rel.targetColumn,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,11 +203,6 @@ export function findTableWithSchemaSupport(
|
|||||||
// If still not found with schema, try any match on the table name
|
// If still not found with schema, try any match on the table name
|
||||||
if (!table) {
|
if (!table) {
|
||||||
table = tables.find((t) => t.name === tableName);
|
table = tables.find((t) => t.name === tableName);
|
||||||
if (table) {
|
|
||||||
console.log(
|
|
||||||
`Found table ${tableName} without schema match, source schema: ${effectiveSchema}, table schema: ${table.schema}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return table;
|
return table;
|
||||||
@@ -235,11 +230,7 @@ export function getTableIdWithSchemaSupport(
|
|||||||
// If still not found with schema, try without schema
|
// If still not found with schema, try without schema
|
||||||
if (!tableId) {
|
if (!tableId) {
|
||||||
tableId = tableMap[tableName];
|
tableId = tableMap[tableName];
|
||||||
if (tableId) {
|
if (!tableId) {
|
||||||
console.log(
|
|
||||||
`Found table ID for ${tableName} without schema match, source schema: ${effectiveSchema}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.warn(
|
console.warn(
|
||||||
`No table ID found for ${tableName} with schema ${effectiveSchema}`
|
`No table ID found for ${tableName} with schema ${effectiveSchema}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,573 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { fromSQLServer } from '../sqlserver';
|
||||||
|
|
||||||
|
describe('SQL Server Multi-Schema Database Tests', () => {
|
||||||
|
it('should parse a fantasy-themed multi-schema database with cross-schema relationships', async () => {
|
||||||
|
const sql = `
|
||||||
|
-- =============================================
|
||||||
|
-- Magical Realm Multi-Schema Database
|
||||||
|
-- A comprehensive fantasy database with multiple schemas
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Create schemas
|
||||||
|
CREATE SCHEMA [realm];
|
||||||
|
CREATE SCHEMA [academy];
|
||||||
|
CREATE SCHEMA [treasury];
|
||||||
|
CREATE SCHEMA [combat];
|
||||||
|
CREATE SCHEMA [marketplace];
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- REALM Schema - Core realm entities
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [realm].[kingdoms] (
|
||||||
|
[kingdom_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[kingdom_name] NVARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
[ruler_name] NVARCHAR(100) NOT NULL,
|
||||||
|
[founding_date] DATE NOT NULL,
|
||||||
|
[capital_city] NVARCHAR(100),
|
||||||
|
[population] BIGINT,
|
||||||
|
[treasury_gold] DECIMAL(18, 2) DEFAULT 10000.00
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [realm].[cities] (
|
||||||
|
[city_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[city_name] NVARCHAR(100) NOT NULL,
|
||||||
|
[kingdom_id] BIGINT NOT NULL,
|
||||||
|
[population] INT,
|
||||||
|
[has_walls] BIT DEFAULT 0,
|
||||||
|
[has_academy] BIT DEFAULT 0,
|
||||||
|
[has_marketplace] BIT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [realm].[guilds] (
|
||||||
|
[guild_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[guild_name] NVARCHAR(100) NOT NULL,
|
||||||
|
[guild_type] NVARCHAR(50) NOT NULL, -- 'Mages', 'Warriors', 'Thieves', 'Merchants'
|
||||||
|
[headquarters_city_id] BIGINT NOT NULL,
|
||||||
|
[founding_year] INT,
|
||||||
|
[member_count] INT DEFAULT 0,
|
||||||
|
[guild_master] NVARCHAR(100)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- ACADEMY Schema - Educational institutions
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [academy].[schools] (
|
||||||
|
[school_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[school_name] NVARCHAR(150) NOT NULL,
|
||||||
|
[city_id] BIGINT NOT NULL,
|
||||||
|
[specialization] NVARCHAR(100), -- 'Elemental Magic', 'Necromancy', 'Healing', 'Alchemy'
|
||||||
|
[founded_year] INT,
|
||||||
|
[tuition_gold] DECIMAL(10, 2),
|
||||||
|
[headmaster] NVARCHAR(100)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [academy].[students] (
|
||||||
|
[student_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[first_name] NVARCHAR(50) NOT NULL,
|
||||||
|
[last_name] NVARCHAR(50) NOT NULL,
|
||||||
|
[school_id] BIGINT NOT NULL,
|
||||||
|
[enrollment_date] DATE NOT NULL,
|
||||||
|
[graduation_date] DATE NULL,
|
||||||
|
[major_discipline] NVARCHAR(100),
|
||||||
|
[home_kingdom_id] BIGINT NOT NULL,
|
||||||
|
[sponsor_guild_id] BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [academy].[courses] (
|
||||||
|
[course_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[course_name] NVARCHAR(200) NOT NULL,
|
||||||
|
[school_id] BIGINT NOT NULL,
|
||||||
|
[credit_hours] INT,
|
||||||
|
[difficulty_level] INT CHECK (difficulty_level BETWEEN 1 AND 10),
|
||||||
|
[prerequisites] NVARCHAR(MAX),
|
||||||
|
[professor_name] NVARCHAR(100)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [academy].[enrollments] (
|
||||||
|
[enrollment_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[student_id] BIGINT NOT NULL,
|
||||||
|
[course_id] BIGINT NOT NULL,
|
||||||
|
[enrollment_date] DATE NOT NULL,
|
||||||
|
[grade] NVARCHAR(2),
|
||||||
|
[completed] BIT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- TREASURY Schema - Financial entities
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [treasury].[currencies] (
|
||||||
|
[currency_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[currency_name] NVARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
[symbol] NVARCHAR(10),
|
||||||
|
[gold_exchange_rate] DECIMAL(10, 4) NOT NULL,
|
||||||
|
[issuing_kingdom_id] BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [treasury].[banks] (
|
||||||
|
[bank_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[bank_name] NVARCHAR(100) NOT NULL,
|
||||||
|
[headquarters_city_id] BIGINT NOT NULL,
|
||||||
|
[total_deposits] DECIMAL(18, 2) DEFAULT 0,
|
||||||
|
[vault_security_level] INT CHECK (vault_security_level BETWEEN 1 AND 10),
|
||||||
|
[founding_date] DATE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [treasury].[accounts] (
|
||||||
|
[account_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[account_number] NVARCHAR(20) NOT NULL UNIQUE,
|
||||||
|
[bank_id] BIGINT NOT NULL,
|
||||||
|
[owner_type] NVARCHAR(20) NOT NULL, -- 'Student', 'Guild', 'Kingdom', 'Merchant'
|
||||||
|
[owner_id] BIGINT NOT NULL,
|
||||||
|
[balance] DECIMAL(18, 2) DEFAULT 0,
|
||||||
|
[currency_id] BIGINT NOT NULL,
|
||||||
|
[opened_date] DATE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [treasury].[transactions] (
|
||||||
|
[transaction_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[from_account_id] BIGINT NULL,
|
||||||
|
[to_account_id] BIGINT NULL,
|
||||||
|
[amount] DECIMAL(18, 2) NOT NULL,
|
||||||
|
[currency_id] BIGINT NOT NULL,
|
||||||
|
[transaction_date] DATETIME NOT NULL,
|
||||||
|
[description] NVARCHAR(500),
|
||||||
|
[transaction_type] NVARCHAR(50) -- 'Deposit', 'Withdrawal', 'Transfer', 'Payment'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- COMBAT Schema - Battle and warrior entities
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [combat].[warriors] (
|
||||||
|
[warrior_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[warrior_name] NVARCHAR(100) NOT NULL,
|
||||||
|
[class] NVARCHAR(50) NOT NULL, -- 'Knight', 'Archer', 'Mage', 'Barbarian'
|
||||||
|
[level] INT DEFAULT 1,
|
||||||
|
[experience_points] BIGINT DEFAULT 0,
|
||||||
|
[guild_id] BIGINT NULL,
|
||||||
|
[home_city_id] BIGINT NOT NULL,
|
||||||
|
[strength] INT,
|
||||||
|
[agility] INT,
|
||||||
|
[intelligence] INT,
|
||||||
|
[current_hp] INT,
|
||||||
|
[max_hp] INT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [combat].[weapons] (
|
||||||
|
[weapon_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[weapon_name] NVARCHAR(100) NOT NULL,
|
||||||
|
[weapon_type] NVARCHAR(50), -- 'Sword', 'Bow', 'Staff', 'Axe'
|
||||||
|
[damage] INT,
|
||||||
|
[durability] INT,
|
||||||
|
[enchantment_level] INT DEFAULT 0,
|
||||||
|
[market_value] DECIMAL(10, 2),
|
||||||
|
[owner_warrior_id] BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [combat].[battles] (
|
||||||
|
[battle_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[battle_name] NVARCHAR(200),
|
||||||
|
[battle_date] DATETIME NOT NULL,
|
||||||
|
[location_city_id] BIGINT NOT NULL,
|
||||||
|
[victor_warrior_id] BIGINT NULL,
|
||||||
|
[total_participants] INT,
|
||||||
|
[battle_type] NVARCHAR(50) -- 'Duel', 'Tournament', 'War', 'Training'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [combat].[battle_participants] (
|
||||||
|
[participant_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[battle_id] BIGINT NOT NULL,
|
||||||
|
[warrior_id] BIGINT NOT NULL,
|
||||||
|
[damage_dealt] INT DEFAULT 0,
|
||||||
|
[damage_received] INT DEFAULT 0,
|
||||||
|
[survived] BIT DEFAULT 1,
|
||||||
|
[rewards_earned] DECIMAL(10, 2) DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- MARKETPLACE Schema - Commerce entities
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [marketplace].[merchants] (
|
||||||
|
[merchant_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[merchant_name] NVARCHAR(100) NOT NULL,
|
||||||
|
[shop_name] NVARCHAR(150),
|
||||||
|
[city_id] BIGINT NOT NULL,
|
||||||
|
[specialization] NVARCHAR(100), -- 'Weapons', 'Potions', 'Scrolls', 'Artifacts'
|
||||||
|
[reputation_score] INT DEFAULT 50,
|
||||||
|
[bank_account_id] BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [marketplace].[items] (
|
||||||
|
[item_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[item_name] NVARCHAR(150) NOT NULL,
|
||||||
|
[item_type] NVARCHAR(50),
|
||||||
|
[base_price] DECIMAL(10, 2),
|
||||||
|
[rarity] NVARCHAR(20), -- 'Common', 'Uncommon', 'Rare', 'Epic', 'Legendary'
|
||||||
|
[merchant_id] BIGINT NOT NULL,
|
||||||
|
[stock_quantity] INT DEFAULT 0,
|
||||||
|
[magical_properties] NVARCHAR(MAX)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [marketplace].[trade_routes] (
|
||||||
|
[route_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[from_city_id] BIGINT NOT NULL,
|
||||||
|
[to_city_id] BIGINT NOT NULL,
|
||||||
|
[distance_leagues] INT,
|
||||||
|
[travel_days] INT,
|
||||||
|
[danger_level] INT CHECK (danger_level BETWEEN 1 AND 10),
|
||||||
|
[toll_cost] DECIMAL(10, 2),
|
||||||
|
[controlled_by_guild_id] BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [marketplace].[transactions] (
|
||||||
|
[transaction_id] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[buyer_type] NVARCHAR(20), -- 'Warrior', 'Student', 'Merchant'
|
||||||
|
[buyer_id] BIGINT NOT NULL,
|
||||||
|
[merchant_id] BIGINT NOT NULL,
|
||||||
|
[item_id] BIGINT NOT NULL,
|
||||||
|
[quantity] INT NOT NULL,
|
||||||
|
[total_price] DECIMAL(10, 2) NOT NULL,
|
||||||
|
[transaction_date] DATETIME NOT NULL,
|
||||||
|
[payment_account_id] BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Foreign Key Constraints - Cross-Schema Relationships
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Realm schema relationships
|
||||||
|
ALTER TABLE [realm].[cities] ADD CONSTRAINT [FK_Cities_Kingdoms]
|
||||||
|
FOREIGN KEY ([kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [realm].[guilds] ADD CONSTRAINT [FK_Guilds_Cities]
|
||||||
|
FOREIGN KEY ([headquarters_city_id]) REFERENCES [realm].[cities]([city_id]);
|
||||||
|
|
||||||
|
-- Academy schema relationships (references realm schema)
|
||||||
|
ALTER TABLE [academy].[schools] ADD CONSTRAINT [FK_Schools_Cities]
|
||||||
|
FOREIGN KEY ([city_id]) REFERENCES [realm].[cities]([city_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Schools]
|
||||||
|
FOREIGN KEY ([school_id]) REFERENCES [academy].[schools]([school_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Kingdoms]
|
||||||
|
FOREIGN KEY ([home_kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [academy].[students] ADD CONSTRAINT [FK_Students_Guilds]
|
||||||
|
FOREIGN KEY ([sponsor_guild_id]) REFERENCES [realm].[guilds]([guild_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [academy].[courses] ADD CONSTRAINT [FK_Courses_Schools]
|
||||||
|
FOREIGN KEY ([school_id]) REFERENCES [academy].[schools]([school_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [academy].[enrollments] ADD CONSTRAINT [FK_Enrollments_Students]
|
||||||
|
FOREIGN KEY ([student_id]) REFERENCES [academy].[students]([student_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [academy].[enrollments] ADD CONSTRAINT [FK_Enrollments_Courses]
|
||||||
|
FOREIGN KEY ([course_id]) REFERENCES [academy].[courses]([course_id]);
|
||||||
|
|
||||||
|
-- Treasury schema relationships (references realm schema)
|
||||||
|
ALTER TABLE [treasury].[currencies] ADD CONSTRAINT [FK_Currencies_Kingdoms]
|
||||||
|
FOREIGN KEY ([issuing_kingdom_id]) REFERENCES [realm].[kingdoms]([kingdom_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [treasury].[banks] ADD CONSTRAINT [FK_Banks_Cities]
|
||||||
|
FOREIGN KEY ([headquarters_city_id]) REFERENCES [realm].[cities]([city_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [treasury].[accounts] ADD CONSTRAINT [FK_Accounts_Banks]
|
||||||
|
FOREIGN KEY ([bank_id]) REFERENCES [treasury].[banks]([bank_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [treasury].[accounts] ADD CONSTRAINT [FK_Accounts_Currencies]
|
||||||
|
FOREIGN KEY ([currency_id]) REFERENCES [treasury].[currencies]([currency_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_FromAccount]
|
||||||
|
FOREIGN KEY ([from_account_id]) REFERENCES [treasury].[accounts]([account_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_ToAccount]
|
||||||
|
FOREIGN KEY ([to_account_id]) REFERENCES [treasury].[accounts]([account_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [treasury].[transactions] ADD CONSTRAINT [FK_Transactions_Currency]
|
||||||
|
FOREIGN KEY ([currency_id]) REFERENCES [treasury].[currencies]([currency_id]);
|
||||||
|
|
||||||
|
-- Combat schema relationships (references realm and combat schemas)
|
||||||
|
ALTER TABLE [combat].[warriors] ADD CONSTRAINT [FK_Warriors_Guilds]
|
||||||
|
FOREIGN KEY ([guild_id]) REFERENCES [realm].[guilds]([guild_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [combat].[warriors] ADD CONSTRAINT [FK_Warriors_Cities]
|
||||||
|
FOREIGN KEY ([home_city_id]) REFERENCES [realm].[cities]([city_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [combat].[weapons] ADD CONSTRAINT [FK_Weapons_Warriors]
|
||||||
|
FOREIGN KEY ([owner_warrior_id]) REFERENCES [combat].[warriors]([warrior_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [combat].[battles] ADD CONSTRAINT [FK_Battles_Cities]
|
||||||
|
FOREIGN KEY ([location_city_id]) REFERENCES [realm].[cities]([city_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [combat].[battles] ADD CONSTRAINT [FK_Battles_VictorWarrior]
|
||||||
|
FOREIGN KEY ([victor_warrior_id]) REFERENCES [combat].[warriors]([warrior_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [combat].[battle_participants] ADD CONSTRAINT [FK_BattleParticipants_Battles]
|
||||||
|
FOREIGN KEY ([battle_id]) REFERENCES [combat].[battles]([battle_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [combat].[battle_participants] ADD CONSTRAINT [FK_BattleParticipants_Warriors]
|
||||||
|
FOREIGN KEY ([warrior_id]) REFERENCES [combat].[warriors]([warrior_id]);
|
||||||
|
|
||||||
|
-- Marketplace schema relationships (references multiple schemas)
|
||||||
|
ALTER TABLE [marketplace].[merchants] ADD CONSTRAINT [FK_Merchants_Cities]
|
||||||
|
FOREIGN KEY ([city_id]) REFERENCES [realm].[cities]([city_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [marketplace].[merchants] ADD CONSTRAINT [FK_Merchants_BankAccounts]
|
||||||
|
FOREIGN KEY ([bank_account_id]) REFERENCES [treasury].[accounts]([account_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [marketplace].[items] ADD CONSTRAINT [FK_Items_Merchants]
|
||||||
|
FOREIGN KEY ([merchant_id]) REFERENCES [marketplace].[merchants]([merchant_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_FromCity]
|
||||||
|
FOREIGN KEY ([from_city_id]) REFERENCES [realm].[cities]([city_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_ToCity]
|
||||||
|
FOREIGN KEY ([to_city_id]) REFERENCES [realm].[cities]([city_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [marketplace].[trade_routes] ADD CONSTRAINT [FK_TradeRoutes_Guilds]
|
||||||
|
FOREIGN KEY ([controlled_by_guild_id]) REFERENCES [realm].[guilds]([guild_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_Merchants]
|
||||||
|
FOREIGN KEY ([merchant_id]) REFERENCES [marketplace].[merchants]([merchant_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_Items]
|
||||||
|
FOREIGN KEY ([item_id]) REFERENCES [marketplace].[items]([item_id]);
|
||||||
|
|
||||||
|
ALTER TABLE [marketplace].[transactions] ADD CONSTRAINT [FK_MarketTransactions_PaymentAccount]
|
||||||
|
FOREIGN KEY ([payment_account_id]) REFERENCES [treasury].[accounts]([account_id]);
|
||||||
|
|
||||||
|
-- Note: Testing table reference without schema prefix defaults to dbo schema
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await fromSQLServer(sql);
|
||||||
|
|
||||||
|
// Verify all schemas are recognized
|
||||||
|
const schemas = new Set(result.tables.map((t) => t.schema));
|
||||||
|
expect(schemas.has('realm')).toBe(true);
|
||||||
|
expect(schemas.has('academy')).toBe(true);
|
||||||
|
expect(schemas.has('treasury')).toBe(true);
|
||||||
|
expect(schemas.has('combat')).toBe(true);
|
||||||
|
expect(schemas.has('marketplace')).toBe(true);
|
||||||
|
|
||||||
|
// Verify table count per schema
|
||||||
|
const tablesBySchema = {
|
||||||
|
realm: result.tables.filter((t) => t.schema === 'realm').length,
|
||||||
|
academy: result.tables.filter((t) => t.schema === 'academy').length,
|
||||||
|
treasury: result.tables.filter((t) => t.schema === 'treasury')
|
||||||
|
.length,
|
||||||
|
combat: result.tables.filter((t) => t.schema === 'combat').length,
|
||||||
|
marketplace: result.tables.filter((t) => t.schema === 'marketplace')
|
||||||
|
.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(tablesBySchema.realm).toBe(3); // kingdoms, cities, guilds
|
||||||
|
expect(tablesBySchema.academy).toBe(4); // schools, students, courses, enrollments
|
||||||
|
expect(tablesBySchema.treasury).toBe(4); // currencies, banks, accounts, transactions
|
||||||
|
expect(tablesBySchema.combat).toBe(4); // warriors, weapons, battles, battle_participants
|
||||||
|
expect(tablesBySchema.marketplace).toBe(4); // merchants, items, trade_routes, transactions
|
||||||
|
|
||||||
|
// Total tables should be 19
|
||||||
|
expect(result.tables.length).toBe(19);
|
||||||
|
|
||||||
|
// Debug: log which relationships are missing
|
||||||
|
const expectedRelationshipNames = [
|
||||||
|
'FK_Cities_Kingdoms',
|
||||||
|
'FK_Guilds_Cities',
|
||||||
|
'FK_Schools_Cities',
|
||||||
|
'FK_Students_Schools',
|
||||||
|
'FK_Students_Kingdoms',
|
||||||
|
'FK_Students_Guilds',
|
||||||
|
'FK_Courses_Schools',
|
||||||
|
'FK_Enrollments_Students',
|
||||||
|
'FK_Enrollments_Courses',
|
||||||
|
'FK_Currencies_Kingdoms',
|
||||||
|
'FK_Banks_Cities',
|
||||||
|
'FK_Accounts_Banks',
|
||||||
|
'FK_Accounts_Currencies',
|
||||||
|
'FK_Transactions_FromAccount',
|
||||||
|
'FK_Transactions_ToAccount',
|
||||||
|
'FK_Transactions_Currency',
|
||||||
|
'FK_Warriors_Guilds',
|
||||||
|
'FK_Warriors_Cities',
|
||||||
|
'FK_Weapons_Warriors',
|
||||||
|
'FK_Battles_Cities',
|
||||||
|
'FK_Battles_VictorWarrior',
|
||||||
|
'FK_BattleParticipants_Battles',
|
||||||
|
'FK_BattleParticipants_Warriors',
|
||||||
|
'FK_Merchants_Cities',
|
||||||
|
'FK_Merchants_BankAccounts',
|
||||||
|
'FK_Items_Merchants',
|
||||||
|
'FK_TradeRoutes_FromCity',
|
||||||
|
'FK_TradeRoutes_ToCity',
|
||||||
|
'FK_TradeRoutes_Guilds',
|
||||||
|
'FK_MarketTransactions_Merchants',
|
||||||
|
'FK_MarketTransactions_Items',
|
||||||
|
'FK_MarketTransactions_PaymentAccount',
|
||||||
|
];
|
||||||
|
|
||||||
|
const foundRelationshipNames = result.relationships.map((r) => r.name);
|
||||||
|
const missingRelationships = expectedRelationshipNames.filter(
|
||||||
|
(name) => !foundRelationshipNames.includes(name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingRelationships.length > 0) {
|
||||||
|
console.log('Missing relationships:', missingRelationships);
|
||||||
|
console.log('Found relationships:', foundRelationshipNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify relationships count - we have 32 working relationships
|
||||||
|
expect(result.relationships.length).toBe(32);
|
||||||
|
|
||||||
|
// Verify some specific cross-schema relationships
|
||||||
|
const crossSchemaRelationships = result.relationships.filter(
|
||||||
|
(r) => r.sourceSchema !== r.targetSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(crossSchemaRelationships.length).toBeGreaterThan(10); // Many cross-schema relationships
|
||||||
|
|
||||||
|
// Check specific cross-schema relationships exist
|
||||||
|
const schoolsToCities = result.relationships.find(
|
||||||
|
(r) =>
|
||||||
|
r.sourceTable === 'schools' &&
|
||||||
|
r.sourceSchema === 'academy' &&
|
||||||
|
r.targetTable === 'cities' &&
|
||||||
|
r.targetSchema === 'realm'
|
||||||
|
);
|
||||||
|
expect(schoolsToCities).toBeDefined();
|
||||||
|
expect(schoolsToCities?.name).toBe('FK_Schools_Cities');
|
||||||
|
|
||||||
|
const studentsToKingdoms = result.relationships.find(
|
||||||
|
(r) =>
|
||||||
|
r.sourceTable === 'students' &&
|
||||||
|
r.sourceSchema === 'academy' &&
|
||||||
|
r.targetTable === 'kingdoms' &&
|
||||||
|
r.targetSchema === 'realm'
|
||||||
|
);
|
||||||
|
expect(studentsToKingdoms).toBeDefined();
|
||||||
|
expect(studentsToKingdoms?.name).toBe('FK_Students_Kingdoms');
|
||||||
|
|
||||||
|
const warriorsToGuilds = result.relationships.find(
|
||||||
|
(r) =>
|
||||||
|
r.sourceTable === 'warriors' &&
|
||||||
|
r.sourceSchema === 'combat' &&
|
||||||
|
r.targetTable === 'guilds' &&
|
||||||
|
r.targetSchema === 'realm'
|
||||||
|
);
|
||||||
|
expect(warriorsToGuilds).toBeDefined();
|
||||||
|
expect(warriorsToGuilds?.name).toBe('FK_Warriors_Guilds');
|
||||||
|
|
||||||
|
const merchantsToAccounts = result.relationships.find(
|
||||||
|
(r) =>
|
||||||
|
r.sourceTable === 'merchants' &&
|
||||||
|
r.sourceSchema === 'marketplace' &&
|
||||||
|
r.targetTable === 'accounts' &&
|
||||||
|
r.targetSchema === 'treasury'
|
||||||
|
);
|
||||||
|
expect(merchantsToAccounts).toBeDefined();
|
||||||
|
expect(merchantsToAccounts?.name).toBe('FK_Merchants_BankAccounts');
|
||||||
|
|
||||||
|
// Verify all relationships have valid source and target table IDs
|
||||||
|
const validRelationships = result.relationships.filter(
|
||||||
|
(r) => r.sourceTableId && r.targetTableId
|
||||||
|
);
|
||||||
|
expect(validRelationships.length).toBe(result.relationships.length);
|
||||||
|
|
||||||
|
// Check that table IDs are properly linked
|
||||||
|
for (const rel of result.relationships) {
|
||||||
|
const sourceTable = result.tables.find(
|
||||||
|
(t) =>
|
||||||
|
t.name === rel.sourceTable && t.schema === rel.sourceSchema
|
||||||
|
);
|
||||||
|
const targetTable = result.tables.find(
|
||||||
|
(t) =>
|
||||||
|
t.name === rel.targetTable && t.schema === rel.targetSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceTable).toBeDefined();
|
||||||
|
expect(targetTable).toBeDefined();
|
||||||
|
expect(rel.sourceTableId).toBe(sourceTable?.id);
|
||||||
|
expect(rel.targetTableId).toBe(targetTable?.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test relationships within the same schema
|
||||||
|
const withinSchemaRels = result.relationships.filter(
|
||||||
|
(r) => r.sourceSchema === r.targetSchema
|
||||||
|
);
|
||||||
|
expect(withinSchemaRels.length).toBeGreaterThan(10);
|
||||||
|
|
||||||
|
// Verify specific within-schema relationship
|
||||||
|
const citiesToKingdoms = result.relationships.find(
|
||||||
|
(r) =>
|
||||||
|
r.sourceTable === 'cities' &&
|
||||||
|
r.targetTable === 'kingdoms' &&
|
||||||
|
r.sourceSchema === 'realm' &&
|
||||||
|
r.targetSchema === 'realm'
|
||||||
|
);
|
||||||
|
expect(citiesToKingdoms).toBeDefined();
|
||||||
|
|
||||||
|
console.log('Multi-schema test results:');
|
||||||
|
console.log('Total schemas:', schemas.size);
|
||||||
|
console.log('Total tables:', result.tables.length);
|
||||||
|
console.log('Total relationships:', result.relationships.length);
|
||||||
|
console.log(
|
||||||
|
'Cross-schema relationships:',
|
||||||
|
crossSchemaRelationships.length
|
||||||
|
);
|
||||||
|
console.log('Within-schema relationships:', withinSchemaRels.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed schema notation formats', async () => {
|
||||||
|
const sql = `
|
||||||
|
-- Mix of different schema notation styles
|
||||||
|
CREATE TABLE [dbo].[table1] (
|
||||||
|
[id] INT PRIMARY KEY,
|
||||||
|
[name] NVARCHAR(50)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE table2 (
|
||||||
|
id INT PRIMARY KEY,
|
||||||
|
table1_id INT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [schema1].[table3] (
|
||||||
|
[id] INT PRIMARY KEY,
|
||||||
|
[value] DECIMAL(10,2)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Different ALTER TABLE formats
|
||||||
|
ALTER TABLE [dbo].[table1] ADD CONSTRAINT [FK1]
|
||||||
|
FOREIGN KEY ([id]) REFERENCES [schema1].[table3]([id]);
|
||||||
|
|
||||||
|
ALTER TABLE table2 ADD CONSTRAINT FK2
|
||||||
|
FOREIGN KEY (table1_id) REFERENCES [dbo].[table1](id);
|
||||||
|
|
||||||
|
ALTER TABLE [schema1].[table3] ADD CONSTRAINT [FK3]
|
||||||
|
FOREIGN KEY ([id]) REFERENCES table2(id);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await fromSQLServer(sql);
|
||||||
|
|
||||||
|
expect(result.tables.length).toBe(3);
|
||||||
|
expect(result.relationships.length).toBe(3);
|
||||||
|
|
||||||
|
// Verify schemas are correctly assigned
|
||||||
|
const table1 = result.tables.find((t) => t.name === 'table1');
|
||||||
|
const table2 = result.tables.find((t) => t.name === 'table2');
|
||||||
|
const table3 = result.tables.find((t) => t.name === 'table3');
|
||||||
|
|
||||||
|
expect(table1?.schema).toBe('dbo');
|
||||||
|
expect(table2?.schema).toBe('dbo');
|
||||||
|
expect(table3?.schema).toBe('schema1');
|
||||||
|
|
||||||
|
// Verify all relationships are properly linked
|
||||||
|
for (const rel of result.relationships) {
|
||||||
|
expect(rel.sourceTableId).toBeTruthy();
|
||||||
|
expect(rel.targetTableId).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,704 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { fromSQLServer } from '../sqlserver';
|
||||||
|
|
||||||
|
describe('SQL Server Single-Schema Database Tests', () => {
|
||||||
|
it('should parse a comprehensive fantasy-themed single-schema database with many foreign key relationships', async () => {
|
||||||
|
// This test simulates a complex single-schema database similar to real-world scenarios
|
||||||
|
// It tests the fix for parsing ALTER TABLE ADD CONSTRAINT statements without schema prefixes
|
||||||
|
const sql = `
|
||||||
|
-- =============================================
|
||||||
|
-- Enchanted Kingdom Management System
|
||||||
|
-- A comprehensive fantasy database using single schema (dbo)
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Core Kingdom Tables
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [Kingdoms] (
|
||||||
|
[KingdomID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[KingdomName] NVARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
[FoundedYear] INT NOT NULL,
|
||||||
|
[CurrentRuler] NVARCHAR(100) NOT NULL,
|
||||||
|
[TreasuryGold] DECIMAL(18, 2) DEFAULT 100000.00,
|
||||||
|
[Population] BIGINT DEFAULT 0,
|
||||||
|
[MilitaryStrength] INT DEFAULT 100
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Regions] (
|
||||||
|
[RegionID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[RegionName] NVARCHAR(100) NOT NULL,
|
||||||
|
[KingdomID] BIGINT NOT NULL,
|
||||||
|
[Terrain] NVARCHAR(50), -- 'Mountains', 'Forest', 'Plains', 'Desert', 'Swamp'
|
||||||
|
[Population] INT DEFAULT 0,
|
||||||
|
[TaxRate] DECIMAL(5, 2) DEFAULT 10.00
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Cities] (
|
||||||
|
[CityID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[CityName] NVARCHAR(100) NOT NULL,
|
||||||
|
[RegionID] BIGINT NOT NULL,
|
||||||
|
[Population] INT DEFAULT 1000,
|
||||||
|
[HasWalls] BIT DEFAULT 0,
|
||||||
|
[HasMarket] BIT DEFAULT 1,
|
||||||
|
[DefenseRating] INT DEFAULT 5
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Castles] (
|
||||||
|
[CastleID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[CastleName] NVARCHAR(100) NOT NULL,
|
||||||
|
[CityID] BIGINT NOT NULL,
|
||||||
|
[GarrisonSize] INT DEFAULT 50,
|
||||||
|
[TowerCount] INT DEFAULT 4,
|
||||||
|
[MoatDepth] DECIMAL(5, 2) DEFAULT 3.00
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Character Management Tables
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [CharacterClasses] (
|
||||||
|
[ClassID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[ClassName] NVARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
[ClassType] NVARCHAR(30), -- 'Warrior', 'Mage', 'Rogue', 'Cleric'
|
||||||
|
[BaseHealth] INT DEFAULT 100,
|
||||||
|
[BaseMana] INT DEFAULT 50,
|
||||||
|
[BaseStrength] INT DEFAULT 10,
|
||||||
|
[BaseIntelligence] INT DEFAULT 10
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Characters] (
|
||||||
|
[CharacterID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[CharacterName] NVARCHAR(100) NOT NULL,
|
||||||
|
[ClassID] BIGINT NOT NULL,
|
||||||
|
[Level] INT DEFAULT 1,
|
||||||
|
[Experience] BIGINT DEFAULT 0,
|
||||||
|
[CurrentHealth] INT DEFAULT 100,
|
||||||
|
[CurrentMana] INT DEFAULT 50,
|
||||||
|
[HomeCityID] BIGINT NOT NULL,
|
||||||
|
[Gold] DECIMAL(10, 2) DEFAULT 100.00,
|
||||||
|
[CreatedDate] DATE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [CharacterSkills] (
|
||||||
|
[SkillID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[SkillName] NVARCHAR(100) NOT NULL,
|
||||||
|
[RequiredClassID] BIGINT NULL,
|
||||||
|
[RequiredLevel] INT DEFAULT 1,
|
||||||
|
[ManaCost] INT DEFAULT 10,
|
||||||
|
[Cooldown] INT DEFAULT 0,
|
||||||
|
[Damage] INT DEFAULT 0,
|
||||||
|
[Description] NVARCHAR(MAX)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [CharacterSkillMapping] (
|
||||||
|
[MappingID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[CharacterID] BIGINT NOT NULL,
|
||||||
|
[SkillID] BIGINT NOT NULL,
|
||||||
|
[SkillLevel] INT DEFAULT 1,
|
||||||
|
[LastUsed] DATETIME NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Guild System Tables
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [GuildTypes] (
|
||||||
|
[GuildTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[TypeName] NVARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
[Description] NVARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Guilds] (
|
||||||
|
[GuildID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[GuildName] NVARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
[GuildTypeID] BIGINT NOT NULL,
|
||||||
|
[HeadquartersCityID] BIGINT NOT NULL,
|
||||||
|
[FoundedDate] DATE NOT NULL,
|
||||||
|
[GuildMasterID] BIGINT NULL,
|
||||||
|
[MemberCount] INT DEFAULT 0,
|
||||||
|
[GuildBank] DECIMAL(18, 2) DEFAULT 0.00,
|
||||||
|
[Reputation] INT DEFAULT 50
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [GuildMembers] (
|
||||||
|
[MembershipID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[GuildID] BIGINT NOT NULL,
|
||||||
|
[CharacterID] BIGINT NOT NULL,
|
||||||
|
[JoinDate] DATE NOT NULL,
|
||||||
|
[Rank] NVARCHAR(50) DEFAULT 'Member',
|
||||||
|
[ContributionPoints] INT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [GuildQuests] (
|
||||||
|
[QuestID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[QuestName] NVARCHAR(200) NOT NULL,
|
||||||
|
[GuildID] BIGINT NOT NULL,
|
||||||
|
[RequiredLevel] INT DEFAULT 1,
|
||||||
|
[RewardGold] DECIMAL(10, 2) DEFAULT 100.00,
|
||||||
|
[RewardExperience] INT DEFAULT 100,
|
||||||
|
[QuestGiverID] BIGINT NULL,
|
||||||
|
[Status] NVARCHAR(20) DEFAULT 'Available'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Item and Inventory Tables
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [ItemCategories] (
|
||||||
|
[CategoryID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[CategoryName] NVARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
[Description] NVARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Items] (
|
||||||
|
[ItemID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[ItemName] NVARCHAR(150) NOT NULL,
|
||||||
|
[CategoryID] BIGINT NOT NULL,
|
||||||
|
[Rarity] NVARCHAR(20), -- 'Common', 'Uncommon', 'Rare', 'Epic', 'Legendary'
|
||||||
|
[BaseValue] DECIMAL(10, 2) DEFAULT 1.00,
|
||||||
|
[Weight] DECIMAL(5, 2) DEFAULT 1.00,
|
||||||
|
[Stackable] BIT DEFAULT 1,
|
||||||
|
[MaxStack] INT DEFAULT 99,
|
||||||
|
[RequiredLevel] INT DEFAULT 1,
|
||||||
|
[RequiredClassID] BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Weapons] (
|
||||||
|
[WeaponID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[ItemID] BIGINT NOT NULL UNIQUE,
|
||||||
|
[WeaponType] NVARCHAR(50), -- 'Sword', 'Axe', 'Bow', 'Staff', 'Dagger'
|
||||||
|
[MinDamage] INT DEFAULT 1,
|
||||||
|
[MaxDamage] INT DEFAULT 10,
|
||||||
|
[AttackSpeed] DECIMAL(3, 2) DEFAULT 1.00,
|
||||||
|
[Durability] INT DEFAULT 100,
|
||||||
|
[EnchantmentSlots] INT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Armor] (
|
||||||
|
[ArmorID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[ItemID] BIGINT NOT NULL UNIQUE,
|
||||||
|
[ArmorType] NVARCHAR(50), -- 'Helmet', 'Chest', 'Legs', 'Boots', 'Gloves'
|
||||||
|
[DefenseValue] INT DEFAULT 1,
|
||||||
|
[MagicResistance] INT DEFAULT 0,
|
||||||
|
[Durability] INT DEFAULT 100,
|
||||||
|
[SetBonusID] BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [CharacterInventory] (
|
||||||
|
[InventoryID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[CharacterID] BIGINT NOT NULL,
|
||||||
|
[ItemID] BIGINT NOT NULL,
|
||||||
|
[Quantity] INT DEFAULT 1,
|
||||||
|
[IsEquipped] BIT DEFAULT 0,
|
||||||
|
[SlotPosition] INT NULL,
|
||||||
|
[AcquiredDate] DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Magic System Tables
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [MagicSchools] (
|
||||||
|
[SchoolID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[SchoolName] NVARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
[Element] NVARCHAR(30), -- 'Fire', 'Water', 'Earth', 'Air', 'Light', 'Dark'
|
||||||
|
[Description] NVARCHAR(MAX)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Spells] (
|
||||||
|
[SpellID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[SpellName] NVARCHAR(100) NOT NULL,
|
||||||
|
[SchoolID] BIGINT NOT NULL,
|
||||||
|
[SpellLevel] INT DEFAULT 1,
|
||||||
|
[ManaCost] INT DEFAULT 10,
|
||||||
|
[CastTime] DECIMAL(3, 1) DEFAULT 1.0,
|
||||||
|
[Range] INT DEFAULT 10,
|
||||||
|
[AreaOfEffect] INT DEFAULT 0,
|
||||||
|
[BaseDamage] INT DEFAULT 0,
|
||||||
|
[Description] NVARCHAR(MAX)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [SpellBooks] (
|
||||||
|
[SpellBookID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[CharacterID] BIGINT NOT NULL,
|
||||||
|
[SpellID] BIGINT NOT NULL,
|
||||||
|
[DateLearned] DATE NOT NULL,
|
||||||
|
[MasteryLevel] INT DEFAULT 1,
|
||||||
|
[TimesUsed] INT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Enchantments] (
|
||||||
|
[EnchantmentID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[EnchantmentName] NVARCHAR(100) NOT NULL,
|
||||||
|
[RequiredSpellID] BIGINT NULL,
|
||||||
|
[BonusType] NVARCHAR(50), -- 'Damage', 'Defense', 'Speed', 'Magic'
|
||||||
|
[BonusValue] INT DEFAULT 1,
|
||||||
|
[Duration] INT NULL, -- NULL for permanent
|
||||||
|
[Cost] DECIMAL(10, 2) DEFAULT 100.00
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [ItemEnchantments] (
|
||||||
|
[ItemEnchantmentID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[ItemID] BIGINT NOT NULL,
|
||||||
|
[EnchantmentID] BIGINT NOT NULL,
|
||||||
|
[AppliedByCharacterID] BIGINT NOT NULL,
|
||||||
|
[AppliedDate] DATETIME NOT NULL,
|
||||||
|
[ExpiryDate] DATETIME NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Quest and Achievement Tables
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [QuestLines] (
|
||||||
|
[QuestLineID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[QuestLineName] NVARCHAR(200) NOT NULL,
|
||||||
|
[MinLevel] INT DEFAULT 1,
|
||||||
|
[MaxLevel] INT DEFAULT 100,
|
||||||
|
[TotalQuests] INT DEFAULT 1,
|
||||||
|
[FinalRewardItemID] BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Quests] (
|
||||||
|
[QuestID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[QuestName] NVARCHAR(200) NOT NULL,
|
||||||
|
[QuestLineID] BIGINT NULL,
|
||||||
|
[QuestGiverNPCID] BIGINT NULL,
|
||||||
|
[RequiredLevel] INT DEFAULT 1,
|
||||||
|
[RequiredQuestID] BIGINT NULL, -- Prerequisite quest
|
||||||
|
[ObjectiveType] NVARCHAR(50), -- 'Kill', 'Collect', 'Deliver', 'Explore'
|
||||||
|
[ObjectiveCount] INT DEFAULT 1,
|
||||||
|
[RewardGold] DECIMAL(10, 2) DEFAULT 10.00,
|
||||||
|
[RewardExperience] INT DEFAULT 100,
|
||||||
|
[RewardItemID] BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [CharacterQuests] (
|
||||||
|
[CharacterQuestID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[CharacterID] BIGINT NOT NULL,
|
||||||
|
[QuestID] BIGINT NOT NULL,
|
||||||
|
[StartDate] DATETIME NOT NULL,
|
||||||
|
[CompletedDate] DATETIME NULL,
|
||||||
|
[CurrentProgress] INT DEFAULT 0,
|
||||||
|
[Status] NVARCHAR(20) DEFAULT 'Active' -- 'Active', 'Completed', 'Failed', 'Abandoned'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Achievements] (
|
||||||
|
[AchievementID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[AchievementName] NVARCHAR(100) NOT NULL,
|
||||||
|
[Description] NVARCHAR(500),
|
||||||
|
[Points] INT DEFAULT 10,
|
||||||
|
[Category] NVARCHAR(50),
|
||||||
|
[RequiredCount] INT DEFAULT 1,
|
||||||
|
[RewardTitle] NVARCHAR(100) NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [CharacterAchievements] (
|
||||||
|
[CharacterAchievementID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[CharacterID] BIGINT NOT NULL,
|
||||||
|
[AchievementID] BIGINT NOT NULL,
|
||||||
|
[EarnedDate] DATETIME NOT NULL,
|
||||||
|
[Progress] INT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- NPC and Monster Tables
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [NPCTypes] (
|
||||||
|
[NPCTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[TypeName] NVARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
[IsFriendly] BIT DEFAULT 1,
|
||||||
|
[CanTrade] BIT DEFAULT 0,
|
||||||
|
[CanGiveQuests] BIT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [NPCs] (
|
||||||
|
[NPCID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[NPCName] NVARCHAR(100) NOT NULL,
|
||||||
|
[NPCTypeID] BIGINT NOT NULL,
|
||||||
|
[LocationCityID] BIGINT NOT NULL,
|
||||||
|
[Health] INT DEFAULT 100,
|
||||||
|
[Level] INT DEFAULT 1,
|
||||||
|
[DialogueText] NVARCHAR(MAX),
|
||||||
|
[RespawnTime] INT DEFAULT 300 -- seconds
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Monsters] (
|
||||||
|
[MonsterID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[MonsterName] NVARCHAR(100) NOT NULL,
|
||||||
|
[MonsterType] NVARCHAR(50), -- 'Beast', 'Undead', 'Dragon', 'Elemental', 'Demon'
|
||||||
|
[Level] INT DEFAULT 1,
|
||||||
|
[Health] INT DEFAULT 100,
|
||||||
|
[Damage] INT DEFAULT 10,
|
||||||
|
[Defense] INT DEFAULT 5,
|
||||||
|
[ExperienceReward] INT DEFAULT 50,
|
||||||
|
[GoldDrop] DECIMAL(10, 2) DEFAULT 5.00,
|
||||||
|
[SpawnRegionID] BIGINT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [MonsterLoot] (
|
||||||
|
[LootID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[MonsterID] BIGINT NOT NULL,
|
||||||
|
[ItemID] BIGINT NOT NULL,
|
||||||
|
[DropChance] DECIMAL(5, 2) DEFAULT 10.00, -- percentage
|
||||||
|
[MinQuantity] INT DEFAULT 1,
|
||||||
|
[MaxQuantity] INT DEFAULT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Combat and PvP Tables
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [BattleTypes] (
|
||||||
|
[BattleTypeID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[TypeName] NVARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
[MinParticipants] INT DEFAULT 2,
|
||||||
|
[MaxParticipants] INT DEFAULT 2,
|
||||||
|
[AllowTeams] BIT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Battles] (
|
||||||
|
[BattleID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[BattleTypeID] BIGINT NOT NULL,
|
||||||
|
[StartTime] DATETIME NOT NULL,
|
||||||
|
[EndTime] DATETIME NULL,
|
||||||
|
[LocationCityID] BIGINT NOT NULL,
|
||||||
|
[WinnerCharacterID] BIGINT NULL,
|
||||||
|
[TotalDamageDealt] BIGINT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [BattleParticipants] (
|
||||||
|
[ParticipantID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[BattleID] BIGINT NOT NULL,
|
||||||
|
[CharacterID] BIGINT NOT NULL,
|
||||||
|
[TeamNumber] INT DEFAULT 0,
|
||||||
|
[DamageDealt] INT DEFAULT 0,
|
||||||
|
[DamageTaken] INT DEFAULT 0,
|
||||||
|
[HealingDone] INT DEFAULT 0,
|
||||||
|
[KillCount] INT DEFAULT 0,
|
||||||
|
[DeathCount] INT DEFAULT 0,
|
||||||
|
[FinalPlacement] INT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Economy Tables
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE [Currencies] (
|
||||||
|
[CurrencyID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[CurrencyName] NVARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
[ExchangeRate] DECIMAL(10, 4) DEFAULT 1.0000, -- relative to gold
|
||||||
|
[IssuingKingdomID] BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [MarketListings] (
|
||||||
|
[ListingID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[SellerCharacterID] BIGINT NOT NULL,
|
||||||
|
[ItemID] BIGINT NOT NULL,
|
||||||
|
[Quantity] INT DEFAULT 1,
|
||||||
|
[PricePerUnit] DECIMAL(10, 2) NOT NULL,
|
||||||
|
[CurrencyID] BIGINT NOT NULL,
|
||||||
|
[ListedDate] DATETIME NOT NULL,
|
||||||
|
[ExpiryDate] DATETIME NOT NULL,
|
||||||
|
[Status] NVARCHAR(20) DEFAULT 'Active'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [Transactions] (
|
||||||
|
[TransactionID] BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
[BuyerCharacterID] BIGINT NOT NULL,
|
||||||
|
[SellerCharacterID] BIGINT NOT NULL,
|
||||||
|
[ItemID] BIGINT NOT NULL,
|
||||||
|
[Quantity] INT DEFAULT 1,
|
||||||
|
[TotalPrice] DECIMAL(10, 2) NOT NULL,
|
||||||
|
[CurrencyID] BIGINT NOT NULL,
|
||||||
|
[TransactionDate] DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Foreign Key Constraints (Without Schema Prefix)
|
||||||
|
-- Testing the fix for single-schema foreign key parsing
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Kingdom Relationships
|
||||||
|
ALTER TABLE [Regions] ADD CONSTRAINT [FK_Regions_Kingdoms]
|
||||||
|
FOREIGN KEY ([KingdomID]) REFERENCES [Kingdoms]([KingdomID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Cities] ADD CONSTRAINT [FK_Cities_Regions]
|
||||||
|
FOREIGN KEY ([RegionID]) REFERENCES [Regions]([RegionID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Castles] ADD CONSTRAINT [FK_Castles_Cities]
|
||||||
|
FOREIGN KEY ([CityID]) REFERENCES [Cities]([CityID]);
|
||||||
|
|
||||||
|
-- Character Relationships
|
||||||
|
ALTER TABLE [Characters] ADD CONSTRAINT [FK_Characters_Classes]
|
||||||
|
FOREIGN KEY ([ClassID]) REFERENCES [CharacterClasses]([ClassID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Characters] ADD CONSTRAINT [FK_Characters_Cities]
|
||||||
|
FOREIGN KEY ([HomeCityID]) REFERENCES [Cities]([CityID]);
|
||||||
|
|
||||||
|
ALTER TABLE [CharacterSkills] ADD CONSTRAINT [FK_CharacterSkills_Classes]
|
||||||
|
FOREIGN KEY ([RequiredClassID]) REFERENCES [CharacterClasses]([ClassID]);
|
||||||
|
|
||||||
|
ALTER TABLE [CharacterSkillMapping] ADD CONSTRAINT [FK_SkillMapping_Characters]
|
||||||
|
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [CharacterSkillMapping] ADD CONSTRAINT [FK_SkillMapping_Skills]
|
||||||
|
FOREIGN KEY ([SkillID]) REFERENCES [CharacterSkills]([SkillID]);
|
||||||
|
|
||||||
|
-- Guild Relationships
|
||||||
|
ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_GuildTypes]
|
||||||
|
FOREIGN KEY ([GuildTypeID]) REFERENCES [GuildTypes]([GuildTypeID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_Cities]
|
||||||
|
FOREIGN KEY ([HeadquartersCityID]) REFERENCES [Cities]([CityID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Guilds] ADD CONSTRAINT [FK_Guilds_GuildMaster]
|
||||||
|
FOREIGN KEY ([GuildMasterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [GuildMembers] ADD CONSTRAINT [FK_GuildMembers_Guilds]
|
||||||
|
FOREIGN KEY ([GuildID]) REFERENCES [Guilds]([GuildID]);
|
||||||
|
|
||||||
|
ALTER TABLE [GuildMembers] ADD CONSTRAINT [FK_GuildMembers_Characters]
|
||||||
|
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [GuildQuests] ADD CONSTRAINT [FK_GuildQuests_Guilds]
|
||||||
|
FOREIGN KEY ([GuildID]) REFERENCES [Guilds]([GuildID]);
|
||||||
|
|
||||||
|
ALTER TABLE [GuildQuests] ADD CONSTRAINT [FK_GuildQuests_QuestGiver]
|
||||||
|
FOREIGN KEY ([QuestGiverID]) REFERENCES [NPCs]([NPCID]);
|
||||||
|
|
||||||
|
-- Item Relationships
|
||||||
|
ALTER TABLE [Items] ADD CONSTRAINT [FK_Items_Categories]
|
||||||
|
FOREIGN KEY ([CategoryID]) REFERENCES [ItemCategories]([CategoryID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Items] ADD CONSTRAINT [FK_Items_RequiredClass]
|
||||||
|
FOREIGN KEY ([RequiredClassID]) REFERENCES [CharacterClasses]([ClassID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Weapons] ADD CONSTRAINT [FK_Weapons_Items]
|
||||||
|
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Armor] ADD CONSTRAINT [FK_Armor_Items]
|
||||||
|
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
|
||||||
|
|
||||||
|
ALTER TABLE [CharacterInventory] ADD CONSTRAINT [FK_Inventory_Characters]
|
||||||
|
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [CharacterInventory] ADD CONSTRAINT [FK_Inventory_Items]
|
||||||
|
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
|
||||||
|
|
||||||
|
-- Magic Relationships
|
||||||
|
ALTER TABLE [Spells] ADD CONSTRAINT [FK_Spells_Schools]
|
||||||
|
FOREIGN KEY ([SchoolID]) REFERENCES [MagicSchools]([SchoolID]);
|
||||||
|
|
||||||
|
ALTER TABLE [SpellBooks] ADD CONSTRAINT [FK_SpellBooks_Characters]
|
||||||
|
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [SpellBooks] ADD CONSTRAINT [FK_SpellBooks_Spells]
|
||||||
|
FOREIGN KEY ([SpellID]) REFERENCES [Spells]([SpellID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Enchantments] ADD CONSTRAINT [FK_Enchantments_Spells]
|
||||||
|
FOREIGN KEY ([RequiredSpellID]) REFERENCES [Spells]([SpellID]);
|
||||||
|
|
||||||
|
ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Items]
|
||||||
|
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
|
||||||
|
|
||||||
|
ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Enchantments]
|
||||||
|
FOREIGN KEY ([EnchantmentID]) REFERENCES [Enchantments]([EnchantmentID]);
|
||||||
|
|
||||||
|
ALTER TABLE [ItemEnchantments] ADD CONSTRAINT [FK_ItemEnchantments_Characters]
|
||||||
|
FOREIGN KEY ([AppliedByCharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
-- Quest Relationships
|
||||||
|
ALTER TABLE [QuestLines] ADD CONSTRAINT [FK_QuestLines_FinalReward]
|
||||||
|
FOREIGN KEY ([FinalRewardItemID]) REFERENCES [Items]([ItemID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_QuestLines]
|
||||||
|
FOREIGN KEY ([QuestLineID]) REFERENCES [QuestLines]([QuestLineID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_QuestGiver]
|
||||||
|
FOREIGN KEY ([QuestGiverNPCID]) REFERENCES [NPCs]([NPCID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_Prerequisites]
|
||||||
|
FOREIGN KEY ([RequiredQuestID]) REFERENCES [Quests]([QuestID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Quests] ADD CONSTRAINT [FK_Quests_RewardItem]
|
||||||
|
FOREIGN KEY ([RewardItemID]) REFERENCES [Items]([ItemID]);
|
||||||
|
|
||||||
|
ALTER TABLE [CharacterQuests] ADD CONSTRAINT [FK_CharacterQuests_Characters]
|
||||||
|
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [CharacterQuests] ADD CONSTRAINT [FK_CharacterQuests_Quests]
|
||||||
|
FOREIGN KEY ([QuestID]) REFERENCES [Quests]([QuestID]);
|
||||||
|
|
||||||
|
ALTER TABLE [CharacterAchievements] ADD CONSTRAINT [FK_CharAchievements_Characters]
|
||||||
|
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [CharacterAchievements] ADD CONSTRAINT [FK_CharAchievements_Achievements]
|
||||||
|
FOREIGN KEY ([AchievementID]) REFERENCES [Achievements]([AchievementID]);
|
||||||
|
|
||||||
|
-- NPC and Monster Relationships
|
||||||
|
ALTER TABLE [NPCs] ADD CONSTRAINT [FK_NPCs_Types]
|
||||||
|
FOREIGN KEY ([NPCTypeID]) REFERENCES [NPCTypes]([NPCTypeID]);
|
||||||
|
|
||||||
|
ALTER TABLE [NPCs] ADD CONSTRAINT [FK_NPCs_Cities]
|
||||||
|
FOREIGN KEY ([LocationCityID]) REFERENCES [Cities]([CityID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Monsters] ADD CONSTRAINT [FK_Monsters_Regions]
|
||||||
|
FOREIGN KEY ([SpawnRegionID]) REFERENCES [Regions]([RegionID]);
|
||||||
|
|
||||||
|
ALTER TABLE [MonsterLoot] ADD CONSTRAINT [FK_MonsterLoot_Monsters]
|
||||||
|
FOREIGN KEY ([MonsterID]) REFERENCES [Monsters]([MonsterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [MonsterLoot] ADD CONSTRAINT [FK_MonsterLoot_Items]
|
||||||
|
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
|
||||||
|
|
||||||
|
-- Battle Relationships
|
||||||
|
ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Types]
|
||||||
|
FOREIGN KEY ([BattleTypeID]) REFERENCES [BattleTypes]([BattleTypeID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Cities]
|
||||||
|
FOREIGN KEY ([LocationCityID]) REFERENCES [Cities]([CityID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Battles] ADD CONSTRAINT [FK_Battles_Winner]
|
||||||
|
FOREIGN KEY ([WinnerCharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [BattleParticipants] ADD CONSTRAINT [FK_BattleParticipants_Battles]
|
||||||
|
FOREIGN KEY ([BattleID]) REFERENCES [Battles]([BattleID]);
|
||||||
|
|
||||||
|
ALTER TABLE [BattleParticipants] ADD CONSTRAINT [FK_BattleParticipants_Characters]
|
||||||
|
FOREIGN KEY ([CharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
-- Economy Relationships
|
||||||
|
ALTER TABLE [Currencies] ADD CONSTRAINT [FK_Currencies_Kingdoms]
|
||||||
|
FOREIGN KEY ([IssuingKingdomID]) REFERENCES [Kingdoms]([KingdomID]);
|
||||||
|
|
||||||
|
ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Seller]
|
||||||
|
FOREIGN KEY ([SellerCharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Items]
|
||||||
|
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
|
||||||
|
|
||||||
|
ALTER TABLE [MarketListings] ADD CONSTRAINT [FK_MarketListings_Currency]
|
||||||
|
FOREIGN KEY ([CurrencyID]) REFERENCES [Currencies]([CurrencyID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Buyer]
|
||||||
|
FOREIGN KEY ([BuyerCharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Seller]
|
||||||
|
FOREIGN KEY ([SellerCharacterID]) REFERENCES [Characters]([CharacterID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Items]
|
||||||
|
FOREIGN KEY ([ItemID]) REFERENCES [Items]([ItemID]);
|
||||||
|
|
||||||
|
ALTER TABLE [Transactions] ADD CONSTRAINT [FK_Transactions_Currency]
|
||||||
|
FOREIGN KEY ([CurrencyID]) REFERENCES [Currencies]([CurrencyID]);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await fromSQLServer(sql);
|
||||||
|
|
||||||
|
// Debug: log table names to see what's parsed
|
||||||
|
console.log('Tables found:', result.tables.length);
|
||||||
|
console.log(
|
||||||
|
'Table names:',
|
||||||
|
result.tables.map((t) => t.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify correct number of tables
|
||||||
|
expect(result.tables.length).toBe(37); // Actually 37 tables after counting
|
||||||
|
|
||||||
|
// Verify all tables use default 'dbo' schema
|
||||||
|
const schemas = new Set(result.tables.map((t) => t.schema));
|
||||||
|
expect(schemas.size).toBe(1);
|
||||||
|
expect(schemas.has('dbo')).toBe(true);
|
||||||
|
|
||||||
|
// Verify correct number of relationships
|
||||||
|
console.log('Relationships found:', result.relationships.length);
|
||||||
|
expect(result.relationships.length).toBe(55); // 55 foreign key relationships that can be parsed
|
||||||
|
|
||||||
|
// Verify all relationships have valid source and target table IDs
|
||||||
|
const validRelationships = result.relationships.filter(
|
||||||
|
(r) => r.sourceTableId && r.targetTableId
|
||||||
|
);
|
||||||
|
expect(validRelationships.length).toBe(result.relationships.length);
|
||||||
|
|
||||||
|
// Check specific table names exist
|
||||||
|
const tableNames = result.tables.map((t) => t.name);
|
||||||
|
expect(tableNames).toContain('Kingdoms');
|
||||||
|
expect(tableNames).toContain('Characters');
|
||||||
|
expect(tableNames).toContain('Guilds');
|
||||||
|
expect(tableNames).toContain('Items');
|
||||||
|
expect(tableNames).toContain('Spells');
|
||||||
|
expect(tableNames).toContain('Quests');
|
||||||
|
expect(tableNames).toContain('Battles');
|
||||||
|
expect(tableNames).toContain('Monsters');
|
||||||
|
|
||||||
|
// Verify some specific relationships exist and are properly linked
|
||||||
|
const characterToClass = result.relationships.find(
|
||||||
|
(r) => r.name === 'FK_Characters_Classes'
|
||||||
|
);
|
||||||
|
expect(characterToClass).toBeDefined();
|
||||||
|
expect(characterToClass?.sourceTable).toBe('Characters');
|
||||||
|
expect(characterToClass?.targetTable).toBe('CharacterClasses');
|
||||||
|
expect(characterToClass?.sourceColumn).toBe('ClassID');
|
||||||
|
expect(characterToClass?.targetColumn).toBe('ClassID');
|
||||||
|
|
||||||
|
const guildsToCity = result.relationships.find(
|
||||||
|
(r) => r.name === 'FK_Guilds_Cities'
|
||||||
|
);
|
||||||
|
expect(guildsToCity).toBeDefined();
|
||||||
|
expect(guildsToCity?.sourceTable).toBe('Guilds');
|
||||||
|
expect(guildsToCity?.targetTable).toBe('Cities');
|
||||||
|
|
||||||
|
const inventoryToItems = result.relationships.find(
|
||||||
|
(r) => r.name === 'FK_Inventory_Items'
|
||||||
|
);
|
||||||
|
expect(inventoryToItems).toBeDefined();
|
||||||
|
expect(inventoryToItems?.sourceTable).toBe('CharacterInventory');
|
||||||
|
expect(inventoryToItems?.targetTable).toBe('Items');
|
||||||
|
|
||||||
|
// Check self-referencing relationship
|
||||||
|
const questPrerequisite = result.relationships.find(
|
||||||
|
(r) => r.name === 'FK_Quests_Prerequisites'
|
||||||
|
);
|
||||||
|
expect(questPrerequisite).toBeDefined();
|
||||||
|
expect(questPrerequisite?.sourceTable).toBe('Quests');
|
||||||
|
expect(questPrerequisite?.targetTable).toBe('Quests');
|
||||||
|
|
||||||
|
// Verify table IDs are correctly linked in relationships
|
||||||
|
for (const rel of result.relationships) {
|
||||||
|
const sourceTable = result.tables.find(
|
||||||
|
(t) =>
|
||||||
|
t.name === rel.sourceTable && t.schema === rel.sourceSchema
|
||||||
|
);
|
||||||
|
const targetTable = result.tables.find(
|
||||||
|
(t) =>
|
||||||
|
t.name === rel.targetTable && t.schema === rel.targetSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sourceTable).toBeDefined();
|
||||||
|
expect(targetTable).toBeDefined();
|
||||||
|
expect(rel.sourceTableId).toBe(sourceTable?.id);
|
||||||
|
expect(rel.targetTableId).toBe(targetTable?.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Single-schema test results:');
|
||||||
|
console.log('Total tables:', result.tables.length);
|
||||||
|
console.log('Total relationships:', result.relationships.length);
|
||||||
|
console.log(
|
||||||
|
'All relationships properly linked:',
|
||||||
|
validRelationships.length === result.relationships.length
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sample of relationship names for verification
|
||||||
|
const sampleRelationships = result.relationships
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((r) => ({
|
||||||
|
name: r.name,
|
||||||
|
source: `${r.sourceTable}.${r.sourceColumn}`,
|
||||||
|
target: `${r.targetTable}.${r.targetColumn}`,
|
||||||
|
}));
|
||||||
|
console.log('Sample relationships:', sampleRelationships);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -162,15 +162,36 @@ function parseAlterTableAddConstraint(statements: string[]): SQLForeignKey[] {
|
|||||||
if (match) {
|
if (match) {
|
||||||
const [
|
const [
|
||||||
,
|
,
|
||||||
sourceSchema = 'dbo',
|
sourceSchemaOrTable,
|
||||||
sourceTable,
|
sourceTableIfSchema,
|
||||||
constraintName,
|
constraintName,
|
||||||
sourceColumn,
|
sourceColumn,
|
||||||
targetSchema = 'dbo',
|
targetSchemaOrTable,
|
||||||
targetTable,
|
targetTableIfSchema,
|
||||||
targetColumn,
|
targetColumn,
|
||||||
] = match;
|
] = match;
|
||||||
|
|
||||||
|
// Handle both schema.table and just table formats
|
||||||
|
let sourceSchema = 'dbo';
|
||||||
|
let sourceTable = '';
|
||||||
|
let targetSchema = 'dbo';
|
||||||
|
let targetTable = '';
|
||||||
|
|
||||||
|
// If second group is empty, first group is the table name
|
||||||
|
if (!sourceTableIfSchema) {
|
||||||
|
sourceTable = sourceSchemaOrTable;
|
||||||
|
} else {
|
||||||
|
sourceSchema = sourceSchemaOrTable;
|
||||||
|
sourceTable = sourceTableIfSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetTableIfSchema) {
|
||||||
|
targetTable = targetSchemaOrTable;
|
||||||
|
} else {
|
||||||
|
targetSchema = targetSchemaOrTable;
|
||||||
|
targetTable = targetTableIfSchema;
|
||||||
|
}
|
||||||
|
|
||||||
fkData.push({
|
fkData.push({
|
||||||
name: constraintName,
|
name: constraintName,
|
||||||
sourceTable: sourceTable,
|
sourceTable: sourceTable,
|
||||||
|
|||||||
@@ -226,6 +226,16 @@ const updateTables = ({
|
|||||||
const targetKey = createObjectKeyFromTable(targetTable);
|
const targetKey = createObjectKeyFromTable(targetTable);
|
||||||
let sourceTable = sourceTablesByKey.get(targetKey);
|
let sourceTable = sourceTablesByKey.get(targetKey);
|
||||||
|
|
||||||
|
// If no match and target has a schema, try without schema
|
||||||
|
if (!sourceTable && targetTable.schema) {
|
||||||
|
const noSchemaKey = createObjectKeyFromTable({
|
||||||
|
...targetTable,
|
||||||
|
schema: undefined,
|
||||||
|
});
|
||||||
|
sourceTable = sourceTablesByKey.get(noSchemaKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no match, try with default schema
|
||||||
if (!sourceTable && defaultDatabaseSchema) {
|
if (!sourceTable && defaultDatabaseSchema) {
|
||||||
if (!targetTable.schema) {
|
if (!targetTable.schema) {
|
||||||
// If target table has no schema, try matching with default schema
|
// If target table has no schema, try matching with default schema
|
||||||
@@ -235,12 +245,7 @@ const updateTables = ({
|
|||||||
});
|
});
|
||||||
sourceTable = sourceTablesByKey.get(defaultKey);
|
sourceTable = sourceTablesByKey.get(defaultKey);
|
||||||
} else if (targetTable.schema === defaultDatabaseSchema) {
|
} else if (targetTable.schema === defaultDatabaseSchema) {
|
||||||
// If target table's schema matches default, try matching without schema
|
// Already tried without schema above
|
||||||
const noSchemaKey = createObjectKeyFromTable({
|
|
||||||
...targetTable,
|
|
||||||
schema: undefined,
|
|
||||||
});
|
|
||||||
sourceTable = sourceTablesByKey.get(noSchemaKey);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,17 +93,38 @@ ALTER TABLE wizard_spellbooks ADD CONSTRAINT fk_mentor FOREIGN KEY (owner_id) RE
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should comment out self-referential foreign keys', () => {
|
it('should preserve valid self-referential foreign keys but filter invalid ones', () => {
|
||||||
const sql = `ALTER TABLE quest_prerequisites ADD CONSTRAINT fk_quest_prereq FOREIGN KEY (quest_id) REFERENCES quest_prerequisites (quest_id);
|
const sql = `-- Valid self-references (different fields)
|
||||||
ALTER TABLE spell_components ADD CONSTRAINT fk_component_substitute FOREIGN KEY (substitute_id) REFERENCES spell_components (id);
|
ALTER TABLE spell_components ADD CONSTRAINT fk_component_substitute FOREIGN KEY (substitute_id) REFERENCES spell_components (id);
|
||||||
ALTER TABLE guild_hierarchy ADD CONSTRAINT fk_parent_guild FOREIGN KEY (parent_guild_id) REFERENCES guild_hierarchy (guild_id);`;
|
ALTER TABLE guild_hierarchy ADD CONSTRAINT fk_parent_guild FOREIGN KEY (parent_guild_id) REFERENCES guild_hierarchy (guild_id);
|
||||||
|
ALTER TABLE "finance"."general_ledger" ADD CONSTRAINT fk_reversal FOREIGN KEY("reversal_id") REFERENCES "finance"."general_ledger"("ledger_id");
|
||||||
|
|
||||||
|
-- Invalid self-references (same field referencing itself)
|
||||||
|
ALTER TABLE quest_prerequisites ADD CONSTRAINT fk_quest_prereq FOREIGN KEY (quest_id) REFERENCES quest_prerequisites (quest_id);
|
||||||
|
ALTER TABLE "finance"."general_ledger" ADD CONSTRAINT fk_ledger_self FOREIGN KEY("ledger_id") REFERENCES "finance"."general_ledger"("ledger_id");
|
||||||
|
ALTER TABLE wizards ADD CONSTRAINT fk_wizard_self FOREIGN KEY (id) REFERENCES wizards (id);`;
|
||||||
|
|
||||||
const sanitized = sanitizeSQLforDBML(sql);
|
const sanitized = sanitizeSQLforDBML(sql);
|
||||||
|
|
||||||
// Self-referential constraints should be commented out
|
// Valid self-referential constraints should be preserved
|
||||||
|
expect(sanitized).toContain(
|
||||||
|
'ALTER TABLE spell_components ADD CONSTRAINT'
|
||||||
|
);
|
||||||
|
expect(sanitized).toContain(
|
||||||
|
'ALTER TABLE guild_hierarchy ADD CONSTRAINT'
|
||||||
|
);
|
||||||
|
expect(sanitized).toMatch(
|
||||||
|
/ALTER TABLE "finance"\."general_ledger".*fk_reversal.*FOREIGN KEY\("reversal_id"\)/
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invalid self-referential constraints (same field to itself) should be commented out
|
||||||
expect(sanitized).toContain('-- ALTER TABLE quest_prerequisites');
|
expect(sanitized).toContain('-- ALTER TABLE quest_prerequisites');
|
||||||
expect(sanitized).toContain('-- ALTER TABLE spell_components');
|
expect(sanitized).toMatch(
|
||||||
expect(sanitized).toContain('-- ALTER TABLE guild_hierarchy');
|
/-- ALTER TABLE "finance"\."general_ledger".*fk_ledger_self.*FOREIGN KEY\("ledger_id"\).*REFERENCES.*\("ledger_id"\)/
|
||||||
|
);
|
||||||
|
expect(sanitized).toContain(
|
||||||
|
'-- ALTER TABLE wizards ADD CONSTRAINT fk_wizard_self'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not comment out normal foreign keys', () => {
|
it('should not comment out normal foreign keys', () => {
|
||||||
@@ -246,7 +267,11 @@ ALTER TABLE spell_component_links ADD CONSTRAINT fk_creator FOREIGN KEY (link_id
|
|||||||
expect(sanitized).toContain("DEFAULT 'F'");
|
expect(sanitized).toContain("DEFAULT 'F'");
|
||||||
expect(sanitized).toContain("DEFAULT 'NOW'"); // NOW is quoted as a single word
|
expect(sanitized).toContain("DEFAULT 'NOW'"); // NOW is quoted as a single word
|
||||||
expect(sanitized).toContain('(matrix_pattern)'); // Deduplicated
|
expect(sanitized).toContain('(matrix_pattern)'); // Deduplicated
|
||||||
|
// Valid self-referencing relationships (different fields) are preserved
|
||||||
expect(sanitized).toContain(
|
expect(sanitized).toContain(
|
||||||
|
'ALTER TABLE spell_matrices ADD CONSTRAINT fk_self_ref'
|
||||||
|
);
|
||||||
|
expect(sanitized).not.toContain(
|
||||||
'-- ALTER TABLE spell_matrices ADD CONSTRAINT fk_self_ref'
|
'-- ALTER TABLE spell_matrices ADD CONSTRAINT fk_self_ref'
|
||||||
);
|
);
|
||||||
expect(sanitized).toContain(
|
expect(sanitized).toContain(
|
||||||
|
|||||||
172
src/lib/dbml/dbml-export/__tests__/dbml-self-referencing.test.ts
Normal file
172
src/lib/dbml/dbml-export/__tests__/dbml-self-referencing.test.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { generateDBMLFromDiagram } from '../dbml-export';
|
||||||
|
import { importDBMLToDiagram } from '../../dbml-import/dbml-import';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
|
||||||
|
describe('DBML Self-Referencing Relationships', () => {
|
||||||
|
it('should preserve self-referencing relationships in DBML export', async () => {
|
||||||
|
// Create a DBML with self-referencing relationship (general_ledger example)
|
||||||
|
const inputDBML = `
|
||||||
|
Table "finance"."general_ledger" {
|
||||||
|
"ledger_id" bigint [pk]
|
||||||
|
"account_name" varchar(100)
|
||||||
|
"amount" decimal(10,2)
|
||||||
|
"reversal_id" bigint [ref: > "finance"."general_ledger"."ledger_id"]
|
||||||
|
"created_at" timestamp
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Import the DBML
|
||||||
|
const diagram = await importDBMLToDiagram(inputDBML, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the relationship was imported
|
||||||
|
expect(diagram.relationships).toBeDefined();
|
||||||
|
expect(diagram.relationships?.length).toBe(1);
|
||||||
|
|
||||||
|
// Verify it's a self-referencing relationship
|
||||||
|
const relationship = diagram.relationships![0];
|
||||||
|
expect(relationship.sourceTableId).toBe(relationship.targetTableId);
|
||||||
|
|
||||||
|
// Export back to DBML
|
||||||
|
const exportResult = generateDBMLFromDiagram(diagram);
|
||||||
|
|
||||||
|
// Check inline format
|
||||||
|
expect(exportResult.inlineDbml).toContain('reversal_id');
|
||||||
|
// The DBML parser correctly interprets FK as: target < source
|
||||||
|
expect(exportResult.inlineDbml).toMatch(
|
||||||
|
/ref:\s*<\s*"finance"\."general_ledger"\."ledger_id"/
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check standard format
|
||||||
|
expect(exportResult.standardDbml).toContain('Ref ');
|
||||||
|
expect(exportResult.standardDbml).toMatch(
|
||||||
|
/"finance"\."general_ledger"\."ledger_id"\s*<\s*"finance"\."general_ledger"\."reversal_id"/
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'✅ Self-referencing relationship preserved in DBML export'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle self-referencing relationships in employee hierarchy', async () => {
|
||||||
|
// Create an employee table with manager relationship
|
||||||
|
const inputDBML = `
|
||||||
|
Table "employees" {
|
||||||
|
"id" int [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"manager_id" int [ref: > "employees"."id"]
|
||||||
|
"department" varchar(50)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(inputDBML, {
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the relationship
|
||||||
|
expect(diagram.relationships?.length).toBe(1);
|
||||||
|
const rel = diagram.relationships![0];
|
||||||
|
expect(rel.sourceTableId).toBe(rel.targetTableId);
|
||||||
|
|
||||||
|
// Export and verify
|
||||||
|
const exportResult = generateDBMLFromDiagram(diagram);
|
||||||
|
|
||||||
|
// Check that the self-reference is preserved
|
||||||
|
expect(exportResult.inlineDbml).toContain('manager_id');
|
||||||
|
// The DBML parser correctly interprets FK as: target < source
|
||||||
|
expect(exportResult.inlineDbml).toMatch(/ref:\s*<\s*"employees"\."id"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple self-referencing relationships', async () => {
|
||||||
|
// Create a category table with parent-child relationships
|
||||||
|
const inputDBML = `
|
||||||
|
Table "categories" {
|
||||||
|
"id" int [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"parent_id" int [ref: > "categories"."id"]
|
||||||
|
"related_id" int [ref: > "categories"."id"]
|
||||||
|
"description" text
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(inputDBML, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should have 2 self-referencing relationships
|
||||||
|
expect(diagram.relationships?.length).toBe(2);
|
||||||
|
|
||||||
|
// Both should be self-referencing
|
||||||
|
diagram.relationships?.forEach((rel) => {
|
||||||
|
expect(rel.sourceTableId).toBe(rel.targetTableId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export and verify both relationships are preserved
|
||||||
|
const exportResult = generateDBMLFromDiagram(diagram);
|
||||||
|
|
||||||
|
expect(exportResult.inlineDbml).toContain('parent_id');
|
||||||
|
expect(exportResult.inlineDbml).toContain('related_id');
|
||||||
|
|
||||||
|
// Count the number of ref: statements
|
||||||
|
// The DBML parser correctly interprets FK as: target < source
|
||||||
|
const refMatches = exportResult.inlineDbml.match(/ref:\s*</g);
|
||||||
|
expect(refMatches?.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle self-referencing with schemas', async () => {
|
||||||
|
// Test with explicit schema names
|
||||||
|
const inputDBML = `
|
||||||
|
Table "hr"."staff" {
|
||||||
|
"staff_id" int [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"supervisor_id" int [ref: > "hr"."staff"."staff_id"]
|
||||||
|
"level" int
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(inputDBML, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(diagram.relationships?.length).toBe(1);
|
||||||
|
|
||||||
|
const exportResult = generateDBMLFromDiagram(diagram);
|
||||||
|
|
||||||
|
// Should preserve the schema in the reference
|
||||||
|
// The DBML parser correctly interprets FK as: target < source
|
||||||
|
expect(exportResult.inlineDbml).toMatch(
|
||||||
|
/ref:\s*<\s*"hr"\."staff"\."staff_id"/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle circular references in graph structures', async () => {
|
||||||
|
// Create a node table for graph structures
|
||||||
|
const inputDBML = `
|
||||||
|
Table "graph_nodes" {
|
||||||
|
"node_id" bigint [pk]
|
||||||
|
"value" varchar(100)
|
||||||
|
"next_node_id" bigint [ref: > "graph_nodes"."node_id"]
|
||||||
|
"prev_node_id" bigint [ref: > "graph_nodes"."node_id"]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(inputDBML, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should have 2 self-referencing relationships
|
||||||
|
expect(diagram.relationships?.length).toBe(2);
|
||||||
|
|
||||||
|
const exportResult = generateDBMLFromDiagram(diagram);
|
||||||
|
|
||||||
|
// Both references should be preserved
|
||||||
|
expect(exportResult.inlineDbml).toContain('next_node_id');
|
||||||
|
expect(exportResult.inlineDbml).toContain('prev_node_id');
|
||||||
|
|
||||||
|
// Verify no lines are commented out
|
||||||
|
expect(exportResult.standardDbml).not.toContain('-- ALTER TABLE');
|
||||||
|
expect(exportResult.inlineDbml).not.toContain('-- ALTER TABLE');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -155,14 +155,25 @@ export const sanitizeSQLforDBML = (sql: string): string => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Comment out self-referencing foreign keys to prevent "Two endpoints are the same" error
|
// Comment out invalid self-referencing foreign keys where the same field references itself
|
||||||
// Example: ALTER TABLE public.class ADD CONSTRAINT ... FOREIGN KEY (class_id) REFERENCES public.class (class_id);
|
// Example: ALTER TABLE table ADD CONSTRAINT ... FOREIGN KEY (field_a) REFERENCES table (field_a);
|
||||||
|
// But keep valid self-references like: FOREIGN KEY (field_a) REFERENCES table (field_b);
|
||||||
const lines = sanitized.split('\n');
|
const lines = sanitized.split('\n');
|
||||||
const processedLines = lines.map((line) => {
|
const processedLines = lines.map((line) => {
|
||||||
|
// Match pattern: ALTER TABLE [schema.]table ADD CONSTRAINT ... FOREIGN KEY(field) REFERENCES [schema.]table(field)
|
||||||
|
// Capture the table name, source field, and target field
|
||||||
const selfRefFKPattern =
|
const selfRefFKPattern =
|
||||||
/ALTER\s+TABLE\s+(?:\S+\.)?(\S+)\s+ADD\s+CONSTRAINT\s+\S+\s+FOREIGN\s+KEY\s*\([^)]+\)\s+REFERENCES\s+(?:\S+\.)?\1\s*\([^)]+\)\s*;/i;
|
/ALTER\s+TABLE\s+(?:["[]?(\S+?)[\]"]?\.)?["[]?(\S+?)[\]"]?\s+ADD\s+CONSTRAINT\s+\S+\s+FOREIGN\s+KEY\s*\(["[]?([^)"]+)[\]"]?\)\s+REFERENCES\s+(?:["[]?\S+?[\]"]?\.)?"?[[]?\2[\]]?"?\s*\(["[]?([^)"]+)[\]"]?\)\s*;/i;
|
||||||
if (selfRefFKPattern.test(line)) {
|
const match = selfRefFKPattern.exec(line);
|
||||||
return `-- ${line}`; // Comment out the line
|
|
||||||
|
if (match) {
|
||||||
|
const sourceField = match[3].trim();
|
||||||
|
const targetField = match[4].trim();
|
||||||
|
|
||||||
|
// Only comment out if source and target fields are the same
|
||||||
|
if (sourceField === targetField) {
|
||||||
|
return `-- ${line}`; // Comment out invalid self-reference
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
});
|
});
|
||||||
@@ -491,9 +502,21 @@ const convertToInlineRefs = (dbml: string): string => {
|
|||||||
return cleanedDbml;
|
return cleanedDbml;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Function to check for DBML reserved keywords
|
||||||
|
const isDBMLKeyword = (name: string): boolean => {
|
||||||
|
const keywords = new Set([
|
||||||
|
'YES',
|
||||||
|
'NO',
|
||||||
|
'TRUE',
|
||||||
|
'FALSE',
|
||||||
|
'NULL', // DBML reserved keywords (boolean literals)
|
||||||
|
]);
|
||||||
|
return keywords.has(name.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
// Function to check for SQL keywords (add more if needed)
|
// Function to check for SQL keywords (add more if needed)
|
||||||
const isSQLKeyword = (name: string): boolean => {
|
const isSQLKeyword = (name: string): boolean => {
|
||||||
const keywords = new Set(['CASE', 'ORDER', 'GROUP', 'FROM', 'TO', 'USER']); // Add common keywords
|
const keywords = new Set(['CASE', 'ORDER', 'GROUP', 'FROM', 'TO', 'USER']); // Common SQL keywords
|
||||||
return keywords.has(name.toUpperCase());
|
return keywords.has(name.toUpperCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -758,6 +781,8 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
|||||||
const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
|
const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
|
||||||
|
|
||||||
// --- Final sanitization and renaming pass ---
|
// --- Final sanitization and renaming pass ---
|
||||||
|
// Only rename keywords for PostgreSQL/SQLite
|
||||||
|
// For other databases, we'll wrap problematic names in quotes instead
|
||||||
const shouldRenameKeywords =
|
const shouldRenameKeywords =
|
||||||
diagram.databaseType === DatabaseType.POSTGRESQL ||
|
diagram.databaseType === DatabaseType.POSTGRESQL ||
|
||||||
diagram.databaseType === DatabaseType.SQLITE;
|
diagram.databaseType === DatabaseType.SQLITE;
|
||||||
@@ -777,14 +802,21 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
|||||||
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
|
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename table if SQL keyword (PostgreSQL only)
|
// Rename table if it's a keyword (PostgreSQL/SQLite only)
|
||||||
if (shouldRenameKeywords && isSQLKeyword(originalName)) {
|
if (
|
||||||
|
shouldRenameKeywords &&
|
||||||
|
(isDBMLKeyword(originalName) || isSQLKeyword(originalName))
|
||||||
|
) {
|
||||||
const newName = `${originalName}_table`;
|
const newName = `${originalName}_table`;
|
||||||
sqlRenamedTables.set(newName, originalName);
|
sqlRenamedTables.set(newName, originalName);
|
||||||
safeTableName = /[^\w]/.test(newName)
|
safeTableName = /[^\w]/.test(newName)
|
||||||
? `"${newName.replace(/"/g, '\\"')}"`
|
? `"${newName.replace(/"/g, '\\"')}"`
|
||||||
: newName;
|
: newName;
|
||||||
}
|
}
|
||||||
|
// For other databases, just quote DBML keywords
|
||||||
|
else if (!shouldRenameKeywords && isDBMLKeyword(originalName)) {
|
||||||
|
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
const fieldNameCounts = new Map<string, number>();
|
const fieldNameCounts = new Map<string, number>();
|
||||||
const processedFields = table.fields.map((field) => {
|
const processedFields = table.fields.map((field) => {
|
||||||
@@ -811,8 +843,11 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
|||||||
name: finalSafeName,
|
name: finalSafeName,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rename field if SQL keyword (PostgreSQL only)
|
// Rename field if it's a keyword (PostgreSQL/SQLite only)
|
||||||
if (shouldRenameKeywords && isSQLKeyword(field.name)) {
|
if (
|
||||||
|
shouldRenameKeywords &&
|
||||||
|
(isDBMLKeyword(field.name) || isSQLKeyword(field.name))
|
||||||
|
) {
|
||||||
const newFieldName = `${field.name}_field`;
|
const newFieldName = `${field.name}_field`;
|
||||||
fieldRenames.push({
|
fieldRenames.push({
|
||||||
table: safeTableName,
|
table: safeTableName,
|
||||||
@@ -823,6 +858,10 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
|||||||
? `"${newFieldName.replace(/"/g, '\\"')}"`
|
? `"${newFieldName.replace(/"/g, '\\"')}"`
|
||||||
: newFieldName;
|
: newFieldName;
|
||||||
}
|
}
|
||||||
|
// For other databases, just quote DBML keywords
|
||||||
|
else if (!shouldRenameKeywords && isDBMLKeyword(field.name)) {
|
||||||
|
sanitizedField.name = `"${field.name.replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
return sanitizedField;
|
return sanitizedField;
|
||||||
});
|
});
|
||||||
@@ -875,8 +914,11 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
|||||||
|
|
||||||
baseScript = sanitizeSQLforDBML(baseScript);
|
baseScript = sanitizeSQLforDBML(baseScript);
|
||||||
|
|
||||||
// Append comments for renamed tables and fields (PostgreSQL only)
|
// Append comments for renamed tables and fields (PostgreSQL/SQLite only)
|
||||||
if (shouldRenameKeywords) {
|
if (
|
||||||
|
shouldRenameKeywords &&
|
||||||
|
(sqlRenamedTables.size > 0 || fieldRenames.length > 0)
|
||||||
|
) {
|
||||||
baseScript = appendRenameComments(
|
baseScript = appendRenameComments(
|
||||||
baseScript,
|
baseScript,
|
||||||
sqlRenamedTables,
|
sqlRenamedTables,
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { importDBMLToDiagram } from '../dbml-import';
|
||||||
|
import { generateDBMLFromDiagram } from '../../dbml-export/dbml-export';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
|
||||||
|
describe('DBML Character Varying Length Preservation', () => {
|
||||||
|
it('should preserve character varying length when quoted', async () => {
|
||||||
|
const inputDBML = `
|
||||||
|
Table "finance"."general_ledger" {
|
||||||
|
"ledger_id" integer [pk]
|
||||||
|
"currency_code" "character varying(3)"
|
||||||
|
"reference_number" "character varying(50)"
|
||||||
|
"description" text
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(inputDBML, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that the lengths were captured
|
||||||
|
const table = diagram.tables?.find((t) => t.name === 'general_ledger');
|
||||||
|
expect(table).toBeDefined();
|
||||||
|
|
||||||
|
const currencyField = table?.fields.find(
|
||||||
|
(f) => f.name === 'currency_code'
|
||||||
|
);
|
||||||
|
const referenceField = table?.fields.find(
|
||||||
|
(f) => f.name === 'reference_number'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(currencyField?.characterMaximumLength).toBe('3');
|
||||||
|
expect(referenceField?.characterMaximumLength).toBe('50');
|
||||||
|
|
||||||
|
// Export and verify lengths are preserved
|
||||||
|
const exportResult = generateDBMLFromDiagram(diagram);
|
||||||
|
|
||||||
|
// Should contain the character varying with lengths
|
||||||
|
expect(exportResult.inlineDbml).toMatch(
|
||||||
|
/"currency_code".*(?:character varying|varchar)\(3\)/
|
||||||
|
);
|
||||||
|
expect(exportResult.inlineDbml).toMatch(
|
||||||
|
/"reference_number".*(?:character varying|varchar)\(50\)/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve varchar length without quotes', async () => {
|
||||||
|
const inputDBML = `
|
||||||
|
Table "users" {
|
||||||
|
"id" int [pk]
|
||||||
|
"username" varchar(100)
|
||||||
|
"email" varchar(255)
|
||||||
|
"bio" text
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(inputDBML, {
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = diagram.tables?.find((t) => t.name === 'users');
|
||||||
|
expect(table).toBeDefined();
|
||||||
|
|
||||||
|
const usernameField = table?.fields.find((f) => f.name === 'username');
|
||||||
|
const emailField = table?.fields.find((f) => f.name === 'email');
|
||||||
|
|
||||||
|
expect(usernameField?.characterMaximumLength).toBe('100');
|
||||||
|
expect(emailField?.characterMaximumLength).toBe('255');
|
||||||
|
|
||||||
|
// Export and verify
|
||||||
|
const exportResult = generateDBMLFromDiagram(diagram);
|
||||||
|
expect(exportResult.inlineDbml).toContain('varchar(100)');
|
||||||
|
expect(exportResult.inlineDbml).toContain('varchar(255)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex quoted types with schema and length', async () => {
|
||||||
|
const inputDBML = `
|
||||||
|
Enum "public"."transaction_type" {
|
||||||
|
"debit"
|
||||||
|
"credit"
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "finance"."general_ledger" {
|
||||||
|
"ledger_id" integer [pk, not null]
|
||||||
|
"transaction_date" date [not null]
|
||||||
|
"account_id" integer
|
||||||
|
"transaction_type" transaction_type
|
||||||
|
"amount" numeric(15,2) [not null]
|
||||||
|
"currency_code" "character varying(3)"
|
||||||
|
"exchange_rate" numeric(10,6)
|
||||||
|
"reference_number" "character varying(50)"
|
||||||
|
"description" text
|
||||||
|
"posted_by" integer
|
||||||
|
"posting_date" timestamp
|
||||||
|
"is_reversed" boolean
|
||||||
|
"reversal_id" integer [ref: < "finance"."general_ledger"."ledger_id"]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(inputDBML, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = diagram.tables?.find((t) => t.name === 'general_ledger');
|
||||||
|
expect(table).toBeDefined();
|
||||||
|
|
||||||
|
// Check all field types are preserved
|
||||||
|
const currencyField = table?.fields.find(
|
||||||
|
(f) => f.name === 'currency_code'
|
||||||
|
);
|
||||||
|
const referenceField = table?.fields.find(
|
||||||
|
(f) => f.name === 'reference_number'
|
||||||
|
);
|
||||||
|
const amountField = table?.fields.find((f) => f.name === 'amount');
|
||||||
|
const exchangeRateField = table?.fields.find(
|
||||||
|
(f) => f.name === 'exchange_rate'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(currencyField?.characterMaximumLength).toBe('3');
|
||||||
|
expect(referenceField?.characterMaximumLength).toBe('50');
|
||||||
|
expect(amountField?.precision).toBe(15);
|
||||||
|
expect(amountField?.scale).toBe(2);
|
||||||
|
expect(exchangeRateField?.precision).toBe(10);
|
||||||
|
expect(exchangeRateField?.scale).toBe(6);
|
||||||
|
|
||||||
|
// Export and verify all types are preserved correctly
|
||||||
|
const exportResult = generateDBMLFromDiagram(diagram);
|
||||||
|
|
||||||
|
// Check that numeric types have their precision/scale
|
||||||
|
expect(exportResult.inlineDbml).toMatch(/numeric\(15,\s*2\)/);
|
||||||
|
expect(exportResult.inlineDbml).toMatch(/numeric\(10,\s*6\)/);
|
||||||
|
|
||||||
|
// Check that character varying has lengths
|
||||||
|
expect(exportResult.inlineDbml).toMatch(
|
||||||
|
/(?:character varying|varchar)\(3\)/
|
||||||
|
);
|
||||||
|
expect(exportResult.inlineDbml).toMatch(
|
||||||
|
/(?:character varying|varchar)\(50\)/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle char types with length', async () => {
|
||||||
|
const inputDBML = `
|
||||||
|
Table "products" {
|
||||||
|
"product_code" char(5) [pk]
|
||||||
|
"category" "char(2)"
|
||||||
|
"status" character(1)
|
||||||
|
"description" varchar
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(inputDBML, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = diagram.tables?.find((t) => t.name === 'products');
|
||||||
|
|
||||||
|
const productCodeField = table?.fields.find(
|
||||||
|
(f) => f.name === 'product_code'
|
||||||
|
);
|
||||||
|
const categoryField = table?.fields.find((f) => f.name === 'category');
|
||||||
|
const statusField = table?.fields.find((f) => f.name === 'status');
|
||||||
|
const descriptionField = table?.fields.find(
|
||||||
|
(f) => f.name === 'description'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(productCodeField?.characterMaximumLength).toBe('5');
|
||||||
|
expect(categoryField?.characterMaximumLength).toBe('2');
|
||||||
|
expect(statusField?.characterMaximumLength).toBe('1');
|
||||||
|
expect(descriptionField?.characterMaximumLength).toBeUndefined(); // varchar without length
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -817,8 +817,9 @@ Table admin.users {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Verify fields reference correct enums
|
// Verify fields reference correct enums
|
||||||
|
// Note: 'public' schema is converted to empty string
|
||||||
const publicUsersTable = diagram.tables?.find(
|
const publicUsersTable = diagram.tables?.find(
|
||||||
(t) => t.name === 'users' && t.schema === 'public'
|
(t) => t.name === 'users' && t.schema === ''
|
||||||
);
|
);
|
||||||
const adminUsersTable = diagram.tables?.find(
|
const adminUsersTable = diagram.tables?.find(
|
||||||
(t) => t.name === 'users' && t.schema === 'admin'
|
(t) => t.name === 'users' && t.schema === 'admin'
|
||||||
@@ -1075,8 +1076,9 @@ Table "public_3"."comments" {
|
|||||||
// Verify tables
|
// Verify tables
|
||||||
expect(diagram.tables).toHaveLength(3);
|
expect(diagram.tables).toHaveLength(3);
|
||||||
|
|
||||||
|
// Note: 'public' schema is converted to empty string
|
||||||
const usersTable = diagram.tables?.find(
|
const usersTable = diagram.tables?.find(
|
||||||
(t) => t.name === 'users' && t.schema === 'public'
|
(t) => t.name === 'users' && t.schema === ''
|
||||||
);
|
);
|
||||||
const postsTable = diagram.tables?.find(
|
const postsTable = diagram.tables?.find(
|
||||||
(t) => t.name === 'posts' && t.schema === 'public_2'
|
(t) => t.name === 'posts' && t.schema === 'public_2'
|
||||||
|
|||||||
437
src/lib/dbml/dbml-import/__tests__/dbml-schema-handling.test.ts
Normal file
437
src/lib/dbml/dbml-import/__tests__/dbml-schema-handling.test.ts
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { importDBMLToDiagram } from '../dbml-import';
|
||||||
|
import { generateDBMLFromDiagram } from '../../dbml-export/dbml-export';
|
||||||
|
import { applyDBMLChanges } from '../../apply-dbml/apply-dbml';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
|
||||||
|
describe('DBML Schema Handling - Fantasy Realm Database', () => {
|
||||||
|
describe('MySQL - No Schema Support', () => {
|
||||||
|
it('should not add public schema for MySQL databases', async () => {
|
||||||
|
// Fantasy realm DBML with tables that would typically get 'public' schema
|
||||||
|
const dbmlContent = `
|
||||||
|
Table "wizards" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"magic_level" int
|
||||||
|
"Yes" varchar(10) // Reserved DBML keyword
|
||||||
|
"No" varchar(10) // Reserved DBML keyword
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "dragons" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"treasure_count" int
|
||||||
|
"is_friendly" boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "spells" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"spell_name" varchar(200)
|
||||||
|
"wizard_id" bigint
|
||||||
|
"power_level" int
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref: "spells"."wizard_id" > "wizards"."id"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(dbmlContent, {
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify no 'public' schema was added
|
||||||
|
expect(diagram.tables).toBeDefined();
|
||||||
|
diagram.tables?.forEach((table) => {
|
||||||
|
expect(table.schema).toBe('');
|
||||||
|
console.log(
|
||||||
|
`✓ Table "${table.name}" has no schema (MySQL behavior)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check specific tables
|
||||||
|
const wizardsTable = diagram.tables?.find(
|
||||||
|
(t) => t.name === 'wizards'
|
||||||
|
);
|
||||||
|
expect(wizardsTable).toBeDefined();
|
||||||
|
expect(wizardsTable?.schema).toBe('');
|
||||||
|
|
||||||
|
// Check that reserved keywords are preserved as field names
|
||||||
|
const yesField = wizardsTable?.fields.find((f) => f.name === 'Yes');
|
||||||
|
const noField = wizardsTable?.fields.find((f) => f.name === 'No');
|
||||||
|
expect(yesField).toBeDefined();
|
||||||
|
expect(noField).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve IDs when re-importing DBML (no false changes)', async () => {
|
||||||
|
// Create initial diagram
|
||||||
|
const initialDBML = `
|
||||||
|
Table "kingdoms" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"ruler" varchar(100)
|
||||||
|
"Yes" varchar(10) // Acceptance status
|
||||||
|
"No" varchar(10) // Rejection status
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "knights" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"kingdom_id" bigint
|
||||||
|
"honor_points" int
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref: "knights"."kingdom_id" > "kingdoms"."id"
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Import initial DBML
|
||||||
|
const sourceDiagram = await importDBMLToDiagram(initialDBML, {
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export to DBML
|
||||||
|
const exported = generateDBMLFromDiagram(sourceDiagram);
|
||||||
|
|
||||||
|
// Re-import the exported DBML (simulating edit mode)
|
||||||
|
const reimportedDiagram = await importDBMLToDiagram(
|
||||||
|
exported.inlineDbml,
|
||||||
|
{
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply DBML changes (should preserve IDs)
|
||||||
|
const targetDiagram: Diagram = {
|
||||||
|
...sourceDiagram,
|
||||||
|
tables: reimportedDiagram.tables,
|
||||||
|
relationships: reimportedDiagram.relationships,
|
||||||
|
customTypes: reimportedDiagram.customTypes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resultDiagram = applyDBMLChanges({
|
||||||
|
sourceDiagram,
|
||||||
|
targetDiagram,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify IDs are preserved
|
||||||
|
expect(resultDiagram.tables?.length).toBe(
|
||||||
|
sourceDiagram.tables?.length
|
||||||
|
);
|
||||||
|
|
||||||
|
sourceDiagram.tables?.forEach((sourceTable, idx) => {
|
||||||
|
const resultTable = resultDiagram.tables?.[idx];
|
||||||
|
expect(resultTable?.id).toBe(sourceTable.id);
|
||||||
|
expect(resultTable?.name).toBe(sourceTable.name);
|
||||||
|
|
||||||
|
// Check field IDs are preserved
|
||||||
|
sourceTable.fields.forEach((sourceField, fieldIdx) => {
|
||||||
|
const resultField = resultTable?.fields[fieldIdx];
|
||||||
|
expect(resultField?.id).toBe(sourceField.id);
|
||||||
|
expect(resultField?.name).toBe(sourceField.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✓ All IDs preserved after DBML round-trip');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PostgreSQL - Schema Support', () => {
|
||||||
|
it('should handle schemas correctly for PostgreSQL', async () => {
|
||||||
|
// Fantasy realm with multiple schemas
|
||||||
|
const dbmlContent = `
|
||||||
|
Table "public"."heroes" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"class" varchar(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "private"."secret_quests" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"quest_name" varchar(200)
|
||||||
|
"hero_id" bigint
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "artifacts" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"power" int
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref: "private"."secret_quests"."hero_id" > "public"."heroes"."id"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(dbmlContent, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check schemas are preserved correctly
|
||||||
|
const heroesTable = diagram.tables?.find(
|
||||||
|
(t) => t.name === 'heroes'
|
||||||
|
);
|
||||||
|
expect(heroesTable?.schema).toBe(''); // 'public' should be converted to empty
|
||||||
|
|
||||||
|
const secretQuestsTable = diagram.tables?.find(
|
||||||
|
(t) => t.name === 'secret_quests'
|
||||||
|
);
|
||||||
|
expect(secretQuestsTable?.schema).toBe('private'); // Other schemas preserved
|
||||||
|
|
||||||
|
const artifactsTable = diagram.tables?.find(
|
||||||
|
(t) => t.name === 'artifacts'
|
||||||
|
);
|
||||||
|
expect(artifactsTable?.schema).toBe(''); // No schema = empty string
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rename reserved keywords for PostgreSQL', async () => {
|
||||||
|
const dbmlContent = `
|
||||||
|
Table "magic_items" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"Order" int // SQL keyword
|
||||||
|
"Yes" varchar(10) // DBML keyword
|
||||||
|
"No" varchar(10) // DBML keyword
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(dbmlContent, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const exported = generateDBMLFromDiagram(diagram);
|
||||||
|
|
||||||
|
// For PostgreSQL, keywords should be renamed in export
|
||||||
|
expect(exported.standardDbml).toContain('Order_field');
|
||||||
|
expect(exported.standardDbml).toContain('Yes_field');
|
||||||
|
expect(exported.standardDbml).toContain('No_field');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Public Schema Handling - The Core Fix', () => {
|
||||||
|
it('should strip public schema for MySQL to prevent ID mismatch', async () => {
|
||||||
|
// This test verifies the core fix - that 'public' schema is converted to empty string
|
||||||
|
const dbmlWithPublicSchema = `
|
||||||
|
Table "public"."enchanted_items" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"item_name" varchar(100)
|
||||||
|
"power" int
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "public"."spell_books" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"title" varchar(200)
|
||||||
|
"author" varchar(100)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mysqlDiagram = await importDBMLToDiagram(
|
||||||
|
dbmlWithPublicSchema,
|
||||||
|
{
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// For MySQL, 'public' schema should be stripped
|
||||||
|
mysqlDiagram.tables?.forEach((table) => {
|
||||||
|
expect(table.schema).toBe('');
|
||||||
|
console.log(
|
||||||
|
`✓ MySQL: Table "${table.name}" has no schema (public was stripped)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now test with PostgreSQL - public should also be stripped (it's the default)
|
||||||
|
const pgDiagram = await importDBMLToDiagram(dbmlWithPublicSchema, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
pgDiagram.tables?.forEach((table) => {
|
||||||
|
expect(table.schema).toBe('');
|
||||||
|
console.log(
|
||||||
|
`✓ PostgreSQL: Table "${table.name}" has no schema (public is default)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve non-public schemas', async () => {
|
||||||
|
const dbmlWithCustomSchema = `
|
||||||
|
Table "fantasy"."magic_users" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"class" varchar(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "adventure"."quests" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"title" varchar(200)
|
||||||
|
"reward" int
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(dbmlWithCustomSchema, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Non-public schemas should be preserved
|
||||||
|
const magicTable = diagram.tables?.find(
|
||||||
|
(t) => t.name === 'magic_users'
|
||||||
|
);
|
||||||
|
const questTable = diagram.tables?.find((t) => t.name === 'quests');
|
||||||
|
|
||||||
|
expect(magicTable?.schema).toBe('fantasy');
|
||||||
|
expect(questTable?.schema).toBe('adventure');
|
||||||
|
console.log('✓ Custom schemas preserved correctly');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases - The Dungeon of Bugs', () => {
|
||||||
|
it('should handle tables with names that need quoting', async () => {
|
||||||
|
const dbmlContent = `
|
||||||
|
Table "dragons_lair" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"treasure_amount" decimal
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "wizard_tower" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"floor_count" int
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "quest_log" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"quest_name" varchar(200)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(dbmlContent, {
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tables should be imported correctly
|
||||||
|
expect(diagram.tables?.length).toBe(3);
|
||||||
|
expect(
|
||||||
|
diagram.tables?.find((t) => t.name === 'dragons_lair')
|
||||||
|
).toBeDefined();
|
||||||
|
expect(
|
||||||
|
diagram.tables?.find((t) => t.name === 'wizard_tower')
|
||||||
|
).toBeDefined();
|
||||||
|
expect(
|
||||||
|
diagram.tables?.find((t) => t.name === 'quest_log')
|
||||||
|
).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle the Work_Order_Page_Debug case with Yes/No fields', async () => {
|
||||||
|
// This is the exact case that was causing the original bug
|
||||||
|
const dbmlContent = `
|
||||||
|
Table "Work_Order_Page_Debug" {
|
||||||
|
"ID" bigint [pk, not null]
|
||||||
|
"Work_Order_For" varchar(255)
|
||||||
|
"Quan_to_Make" int
|
||||||
|
"Text_Gen" text
|
||||||
|
"Gen_Info" text
|
||||||
|
"Yes" varchar(255)
|
||||||
|
"No" varchar(255)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diagram = await importDBMLToDiagram(dbmlContent, {
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = diagram.tables?.find(
|
||||||
|
(t) => t.name === 'Work_Order_Page_Debug'
|
||||||
|
);
|
||||||
|
expect(table).toBeDefined();
|
||||||
|
|
||||||
|
// Check Yes and No fields are preserved
|
||||||
|
const yesField = table?.fields.find((f) => f.name === 'Yes');
|
||||||
|
const noField = table?.fields.find((f) => f.name === 'No');
|
||||||
|
|
||||||
|
expect(yesField).toBeDefined();
|
||||||
|
expect(noField).toBeDefined();
|
||||||
|
expect(yesField?.name).toBe('Yes');
|
||||||
|
expect(noField?.name).toBe('No');
|
||||||
|
|
||||||
|
// Export and verify it doesn't cause errors
|
||||||
|
const exported = generateDBMLFromDiagram(diagram);
|
||||||
|
expect(exported.standardDbml).toContain('"Yes"');
|
||||||
|
expect(exported.standardDbml).toContain('"No"');
|
||||||
|
|
||||||
|
// Re-import should work without errors
|
||||||
|
const reimported = await importDBMLToDiagram(exported.inlineDbml, {
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(reimported.tables?.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Round-trip Testing - The Eternal Cycle', () => {
|
||||||
|
it('should maintain data integrity through multiple import/export cycles', async () => {
|
||||||
|
const originalDBML = `
|
||||||
|
Table "guild_members" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"name" varchar(100)
|
||||||
|
"level" int
|
||||||
|
"Yes" varchar(10) // Active status
|
||||||
|
"No" varchar(10) // Inactive status
|
||||||
|
"Order" int // SQL keyword - rank order
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "guild_quests" {
|
||||||
|
"id" bigint [pk]
|
||||||
|
"quest_name" varchar(200)
|
||||||
|
"assigned_to" bigint
|
||||||
|
"difficulty" int
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref: "guild_quests"."assigned_to" > "guild_members"."id"
|
||||||
|
`;
|
||||||
|
|
||||||
|
let currentDiagram = await importDBMLToDiagram(originalDBML, {
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store original IDs
|
||||||
|
const originalTableIds = currentDiagram.tables?.map((t) => ({
|
||||||
|
name: t.name,
|
||||||
|
id: t.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Perform 3 round-trips
|
||||||
|
for (let cycle = 1; cycle <= 3; cycle++) {
|
||||||
|
console.log(`🔄 Round-trip cycle ${cycle}`);
|
||||||
|
|
||||||
|
// Export
|
||||||
|
const exported = generateDBMLFromDiagram(currentDiagram);
|
||||||
|
|
||||||
|
// Re-import
|
||||||
|
const reimported = await importDBMLToDiagram(
|
||||||
|
exported.inlineDbml,
|
||||||
|
{
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply changes
|
||||||
|
const targetDiagram: Diagram = {
|
||||||
|
...currentDiagram,
|
||||||
|
tables: reimported.tables,
|
||||||
|
relationships: reimported.relationships,
|
||||||
|
customTypes: reimported.customTypes,
|
||||||
|
};
|
||||||
|
|
||||||
|
currentDiagram = applyDBMLChanges({
|
||||||
|
sourceDiagram: currentDiagram,
|
||||||
|
targetDiagram,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify IDs are still the same as original
|
||||||
|
originalTableIds?.forEach((original) => {
|
||||||
|
const currentTable = currentDiagram.tables?.find(
|
||||||
|
(t) => t.name === original.name
|
||||||
|
);
|
||||||
|
expect(currentTable?.id).toBe(original.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✓ Data integrity maintained through 3 cycles');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,7 @@ import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
|
|||||||
import type { DBField } from '@/lib/domain/db-field';
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
import type { DataTypeData } from '@/lib/data/data-types/data-types';
|
import type { DataTypeData } from '@/lib/data/data-types/data-types';
|
||||||
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
|
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
|
||||||
import { randomColor } from '@/lib/colors';
|
import { defaultTableColor } from '@/lib/colors';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import type Field from '@dbml/core/types/model_structure/field';
|
import type Field from '@dbml/core/types/model_structure/field';
|
||||||
import type { DBIndex } from '@/lib/domain';
|
import type { DBIndex } from '@/lib/domain';
|
||||||
@@ -246,21 +246,47 @@ export const importDBMLToDiagram = async (
|
|||||||
field: Field,
|
field: Field,
|
||||||
enums: DBMLEnum[]
|
enums: DBMLEnum[]
|
||||||
): Partial<DBMLField> => {
|
): Partial<DBMLField> => {
|
||||||
if (!field.type || !field.type.args) {
|
// First check if the type name itself contains the length (e.g., "character varying(50)")
|
||||||
return {};
|
const typeName = field.type.type_name;
|
||||||
|
let extractedArgs: string[] | undefined;
|
||||||
|
|
||||||
|
// Check for types with embedded length like "character varying(50)" or varchar(255)
|
||||||
|
const typeWithLengthMatch = typeName.match(/^(.+?)\(([^)]+)\)$/);
|
||||||
|
if (typeWithLengthMatch) {
|
||||||
|
// Extract the args from the type name itself
|
||||||
|
extractedArgs = typeWithLengthMatch[2]
|
||||||
|
.split(',')
|
||||||
|
.map((arg: string) => arg.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = field.type.args.split(',') as string[];
|
// Use extracted args or fall back to field.type.args
|
||||||
|
const args =
|
||||||
|
extractedArgs ||
|
||||||
|
(field.type.args ? field.type.args.split(',') : undefined);
|
||||||
|
|
||||||
|
if (!args || args.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const dataType = mapDBMLTypeToDataType(field.type.type_name, {
|
const dataType = mapDBMLTypeToDataType(field.type.type_name, {
|
||||||
...options,
|
...options,
|
||||||
enums,
|
enums,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dataType.fieldAttributes?.hasCharMaxLength) {
|
// Check if this is a character type that should have a max length
|
||||||
const charMaxLength = args?.[0];
|
const baseTypeName = typeName
|
||||||
|
.replace(/\(.*\)/, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/['"]/g, '');
|
||||||
|
const isCharType =
|
||||||
|
baseTypeName.includes('char') ||
|
||||||
|
baseTypeName.includes('varchar') ||
|
||||||
|
baseTypeName === 'text' ||
|
||||||
|
baseTypeName === 'string';
|
||||||
|
|
||||||
|
if (isCharType && args[0]) {
|
||||||
return {
|
return {
|
||||||
characterMaximumLength: charMaxLength,
|
characterMaximumLength: args[0],
|
||||||
};
|
};
|
||||||
} else if (
|
} else if (
|
||||||
dataType.fieldAttributes?.precision &&
|
dataType.fieldAttributes?.precision &&
|
||||||
@@ -500,18 +526,20 @@ export const importDBMLToDiagram = async (
|
|||||||
name: table.name.replace(/['"]/g, ''),
|
name: table.name.replace(/['"]/g, ''),
|
||||||
schema:
|
schema:
|
||||||
typeof table.schema === 'string'
|
typeof table.schema === 'string'
|
||||||
? table.schema
|
? table.schema === 'public'
|
||||||
|
? ''
|
||||||
|
: table.schema
|
||||||
: table.schema?.name || '',
|
: table.schema?.name || '',
|
||||||
order: index,
|
order: index,
|
||||||
fields,
|
fields,
|
||||||
indexes,
|
indexes,
|
||||||
x: col * tableSpacing,
|
x: col * tableSpacing,
|
||||||
y: row * tableSpacing,
|
y: row * tableSpacing,
|
||||||
color: randomColor(),
|
color: defaultTableColor,
|
||||||
isView: false,
|
isView: false,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
comments: tableComment,
|
comments: tableComment,
|
||||||
} as DBTable;
|
} satisfies DBTable;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create relationships using the refs
|
// Create relationships using the refs
|
||||||
|
|||||||
@@ -9,3 +9,9 @@ export enum DatabaseType {
|
|||||||
COCKROACHDB = 'cockroachdb',
|
COCKROACHDB = 'cockroachdb',
|
||||||
ORACLE = 'oracle',
|
ORACLE = 'oracle',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const databaseTypesWithCommentSupport: DatabaseType[] = [
|
||||||
|
DatabaseType.POSTGRESQL,
|
||||||
|
DatabaseType.COCKROACHDB,
|
||||||
|
DatabaseType.ORACLE,
|
||||||
|
];
|
||||||
|
|||||||
@@ -2,6 +2,25 @@ import { z } from 'zod';
|
|||||||
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
|
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
|
||||||
import { generateId } from '../utils';
|
import { generateId } from '../utils';
|
||||||
import type { DBField } from './db-field';
|
import type { DBField } from './db-field';
|
||||||
|
import { DatabaseType } from './database-type';
|
||||||
|
|
||||||
|
export const INDEX_TYPES = [
|
||||||
|
'btree',
|
||||||
|
'hash',
|
||||||
|
'gist',
|
||||||
|
'gin',
|
||||||
|
'spgist',
|
||||||
|
'brin',
|
||||||
|
// sql server
|
||||||
|
'nonclustered',
|
||||||
|
'clustered',
|
||||||
|
'xml',
|
||||||
|
'fulltext',
|
||||||
|
'spatial',
|
||||||
|
'hash',
|
||||||
|
'index',
|
||||||
|
] as const;
|
||||||
|
export type IndexType = (typeof INDEX_TYPES)[number];
|
||||||
|
|
||||||
export interface DBIndex {
|
export interface DBIndex {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -9,6 +28,7 @@ export interface DBIndex {
|
|||||||
unique: boolean;
|
unique: boolean;
|
||||||
fieldIds: string[];
|
fieldIds: string[];
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
type?: IndexType | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
|
export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
|
||||||
@@ -17,6 +37,7 @@ export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
|
|||||||
unique: z.boolean(),
|
unique: z.boolean(),
|
||||||
fieldIds: z.array(z.string()),
|
fieldIds: z.array(z.string()),
|
||||||
createdAt: z.number(),
|
createdAt: z.number(),
|
||||||
|
type: z.enum(INDEX_TYPES).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createIndexesFromMetadata = ({
|
export const createIndexesFromMetadata = ({
|
||||||
@@ -36,5 +57,10 @@ export const createIndexesFromMetadata = ({
|
|||||||
.map((c) => fields.find((f) => f.name === c.name)?.id)
|
.map((c) => fields.find((f) => f.name === c.name)?.id)
|
||||||
.filter((id): id is string => id !== undefined),
|
.filter((id): id is string => id !== undefined),
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
type: idx.index_type?.toLowerCase() as IndexType,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const databaseIndexTypes: { [key in DatabaseType]?: IndexType[] } = {
|
||||||
|
[DatabaseType.POSTGRESQL]: ['btree', 'hash'],
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import {
|
|||||||
} from './db-field';
|
} from './db-field';
|
||||||
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
|
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
|
||||||
import { createAggregatedIndexes } from '../data/import-metadata/metadata-types/index-info';
|
import { createAggregatedIndexes } from '../data/import-metadata/metadata-types/index-info';
|
||||||
import { materializedViewColor, viewColor, randomColor } from '@/lib/colors';
|
import {
|
||||||
|
materializedViewColor,
|
||||||
|
viewColor,
|
||||||
|
defaultTableColor,
|
||||||
|
} from '@/lib/colors';
|
||||||
import type { DBRelationship } from './db-relationship';
|
import type { DBRelationship } from './db-relationship';
|
||||||
import {
|
import {
|
||||||
decodeBase64ToUtf16LE,
|
decodeBase64ToUtf16LE,
|
||||||
@@ -22,6 +26,7 @@ import { schemaNameToDomainSchemaName } from './db-schema';
|
|||||||
import { DatabaseType } from './database-type';
|
import { DatabaseType } from './database-type';
|
||||||
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
|
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import type { Area } from './area';
|
||||||
|
|
||||||
export const MAX_TABLE_SIZE = 450;
|
export const MAX_TABLE_SIZE = 450;
|
||||||
export const MID_TABLE_SIZE = 337;
|
export const MID_TABLE_SIZE = 337;
|
||||||
@@ -224,7 +229,7 @@ export const createTablesFromMetadata = ({
|
|||||||
? materializedViewColor
|
? materializedViewColor
|
||||||
: isView
|
: isView
|
||||||
? viewColor
|
? viewColor
|
||||||
: randomColor(),
|
: defaultTableColor,
|
||||||
isView: isView,
|
isView: isView,
|
||||||
isMaterializedView: isMaterializedView,
|
isMaterializedView: isMaterializedView,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
@@ -235,89 +240,170 @@ export const createTablesFromMetadata = ({
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Simple grid-based positioning for large databases
|
|
||||||
const adjustTablePositionsSimple = (
|
|
||||||
tables: DBTable[],
|
|
||||||
mode: 'all' | 'perSchema' = 'all'
|
|
||||||
): DBTable[] => {
|
|
||||||
const TABLES_PER_ROW = 20;
|
|
||||||
const TABLE_WIDTH = 250;
|
|
||||||
const TABLE_HEIGHT = 350;
|
|
||||||
const GAP_X = 50;
|
|
||||||
const GAP_Y = 50;
|
|
||||||
const START_X = 100;
|
|
||||||
const START_Y = 100;
|
|
||||||
|
|
||||||
if (mode === 'perSchema') {
|
|
||||||
// Group tables by schema for better organization
|
|
||||||
const tablesBySchema = new Map<string, DBTable[]>();
|
|
||||||
tables.forEach((table) => {
|
|
||||||
const schema = table.schema || 'default';
|
|
||||||
if (!tablesBySchema.has(schema)) {
|
|
||||||
tablesBySchema.set(schema, []);
|
|
||||||
}
|
|
||||||
tablesBySchema.get(schema)!.push(table);
|
|
||||||
});
|
|
||||||
|
|
||||||
const result: DBTable[] = [];
|
|
||||||
let currentSchemaOffset = 0;
|
|
||||||
|
|
||||||
// Position each schema's tables in its own section
|
|
||||||
tablesBySchema.forEach((schemaTables) => {
|
|
||||||
schemaTables.forEach((table, index) => {
|
|
||||||
const row = Math.floor(index / TABLES_PER_ROW);
|
|
||||||
const col = index % TABLES_PER_ROW;
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
...table,
|
|
||||||
x: START_X + col * (TABLE_WIDTH + GAP_X),
|
|
||||||
y:
|
|
||||||
START_Y +
|
|
||||||
currentSchemaOffset +
|
|
||||||
row * (TABLE_HEIGHT + GAP_Y),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add extra spacing between schemas
|
|
||||||
const schemaRows = Math.ceil(schemaTables.length / TABLES_PER_ROW);
|
|
||||||
currentSchemaOffset += schemaRows * (TABLE_HEIGHT + GAP_Y) + 200;
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple mode - just arrange all tables in a grid
|
|
||||||
return tables.map((table, index) => {
|
|
||||||
const row = Math.floor(index / TABLES_PER_ROW);
|
|
||||||
const col = index % TABLES_PER_ROW;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...table,
|
|
||||||
x: START_X + col * (TABLE_WIDTH + GAP_X),
|
|
||||||
y: START_Y + row * (TABLE_HEIGHT + GAP_Y),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const adjustTablePositions = ({
|
export const adjustTablePositions = ({
|
||||||
relationships: inputRelationships,
|
relationships: inputRelationships,
|
||||||
tables: inputTables,
|
tables: inputTables,
|
||||||
|
areas: inputAreas = [],
|
||||||
mode = 'all',
|
mode = 'all',
|
||||||
}: {
|
}: {
|
||||||
tables: DBTable[];
|
tables: DBTable[];
|
||||||
relationships: DBRelationship[];
|
relationships: DBRelationship[];
|
||||||
|
areas?: Area[];
|
||||||
mode?: 'all' | 'perSchema';
|
mode?: 'all' | 'perSchema';
|
||||||
}): DBTable[] => {
|
}): DBTable[] => {
|
||||||
// For large databases, use simple grid layout for better performance
|
// Deep copy inputs for manipulation
|
||||||
if (inputTables.length > 200) {
|
|
||||||
const result = adjustTablePositionsSimple(inputTables, mode);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For smaller databases, use the existing complex algorithm
|
|
||||||
const tables = deepCopy(inputTables);
|
const tables = deepCopy(inputTables);
|
||||||
const relationships = deepCopy(inputRelationships);
|
const relationships = deepCopy(inputRelationships);
|
||||||
|
const areas = deepCopy(inputAreas);
|
||||||
|
|
||||||
|
// If there are no areas, fall back to the original algorithm
|
||||||
|
if (areas.length === 0) {
|
||||||
|
return adjustTablePositionsWithoutAreas(tables, relationships, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group tables by their parent area
|
||||||
|
const tablesByArea = new Map<string | null, DBTable[]>();
|
||||||
|
|
||||||
|
// Initialize with empty arrays for all areas
|
||||||
|
areas.forEach((area) => {
|
||||||
|
tablesByArea.set(area.id, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also create a group for tables without areas
|
||||||
|
tablesByArea.set(null, []);
|
||||||
|
|
||||||
|
// Group tables
|
||||||
|
tables.forEach((table) => {
|
||||||
|
const areaId = table.parentAreaId || null;
|
||||||
|
if (areaId && tablesByArea.has(areaId)) {
|
||||||
|
tablesByArea.get(areaId)!.push(table);
|
||||||
|
} else {
|
||||||
|
// If the area doesn't exist or table has no area, put it in the null group
|
||||||
|
tablesByArea.get(null)!.push(table);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check and adjust tables within each area
|
||||||
|
areas.forEach((area) => {
|
||||||
|
const tablesInArea = tablesByArea.get(area.id) || [];
|
||||||
|
if (tablesInArea.length === 0) return;
|
||||||
|
|
||||||
|
// Only reposition tables that are outside their area bounds
|
||||||
|
const tablesToReposition = tablesInArea.filter((table) => {
|
||||||
|
return !isTableInsideArea(table, area);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tablesToReposition.length > 0) {
|
||||||
|
// Create a sub-graph of relationships for tables that need repositioning
|
||||||
|
const areaRelationships = relationships.filter((rel) => {
|
||||||
|
const sourceNeedsReposition = tablesToReposition.some(
|
||||||
|
(t) => t.id === rel.sourceTableId
|
||||||
|
);
|
||||||
|
const targetNeedsReposition = tablesToReposition.some(
|
||||||
|
(t) => t.id === rel.targetTableId
|
||||||
|
);
|
||||||
|
return sourceNeedsReposition && targetNeedsReposition;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Position only tables that are outside the area bounds
|
||||||
|
positionTablesWithinArea(
|
||||||
|
tablesToReposition,
|
||||||
|
areaRelationships,
|
||||||
|
area
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Tables already inside the area keep their positions
|
||||||
|
});
|
||||||
|
|
||||||
|
// Position free tables (those not in any area)
|
||||||
|
const freeTables = tablesByArea.get(null) || [];
|
||||||
|
if (freeTables.length > 0) {
|
||||||
|
// Create a sub-graph of relationships for free tables
|
||||||
|
const freeRelationships = relationships.filter((rel) => {
|
||||||
|
const sourceIsFree = freeTables.some(
|
||||||
|
(t) => t.id === rel.sourceTableId
|
||||||
|
);
|
||||||
|
const targetIsFree = freeTables.some(
|
||||||
|
(t) => t.id === rel.targetTableId
|
||||||
|
);
|
||||||
|
return sourceIsFree && targetIsFree;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use the original algorithm for free tables with area avoidance
|
||||||
|
adjustTablePositionsWithoutAreas(
|
||||||
|
freeTables,
|
||||||
|
freeRelationships,
|
||||||
|
mode,
|
||||||
|
areas
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tables;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to check if a table is inside an area
|
||||||
|
function isTableInsideArea(table: DBTable, area: Area): boolean {
|
||||||
|
const tableDimensions = getTableDimensions(table);
|
||||||
|
const padding = 20; // Same padding as used in positioning
|
||||||
|
|
||||||
|
return (
|
||||||
|
table.x >= area.x + padding &&
|
||||||
|
table.x + tableDimensions.width <= area.x + area.width - padding &&
|
||||||
|
table.y >= area.y + padding &&
|
||||||
|
table.y + tableDimensions.height <= area.y + area.height - padding
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to position tables within an area
|
||||||
|
function positionTablesWithinArea(
|
||||||
|
tables: DBTable[],
|
||||||
|
_relationships: DBRelationship[],
|
||||||
|
area: Area
|
||||||
|
) {
|
||||||
|
if (tables.length === 0) return;
|
||||||
|
|
||||||
|
const padding = 20; // Padding from area edges
|
||||||
|
const gapX = 50;
|
||||||
|
const gapY = 50;
|
||||||
|
|
||||||
|
// Available space within the area
|
||||||
|
const availableWidth = area.width - 2 * padding;
|
||||||
|
const availableHeight = area.height - 2 * padding;
|
||||||
|
|
||||||
|
// Simple grid layout within the area
|
||||||
|
const cols = Math.max(1, Math.floor(availableWidth / 250));
|
||||||
|
const rows = Math.ceil(tables.length / cols);
|
||||||
|
|
||||||
|
const cellWidth = availableWidth / cols;
|
||||||
|
const cellHeight = availableHeight / Math.max(rows, 1);
|
||||||
|
|
||||||
|
tables.forEach((table, index) => {
|
||||||
|
const col = index % cols;
|
||||||
|
const row = Math.floor(index / cols);
|
||||||
|
|
||||||
|
// Position relative to area
|
||||||
|
table.x = area.x + padding + col * cellWidth + gapX / 2;
|
||||||
|
table.y = area.y + padding + row * cellHeight + gapY / 2;
|
||||||
|
|
||||||
|
// Ensure table stays within area bounds
|
||||||
|
const tableDimensions = getTableDimensions(table);
|
||||||
|
const maxX = area.x + area.width - padding - tableDimensions.width;
|
||||||
|
const maxY = area.y + area.height - padding - tableDimensions.height;
|
||||||
|
|
||||||
|
table.x = Math.min(table.x, maxX);
|
||||||
|
table.y = Math.min(table.y, maxY);
|
||||||
|
table.x = Math.max(table.x, area.x + padding);
|
||||||
|
table.y = Math.max(table.y, area.y + padding);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original algorithm with area avoidance
|
||||||
|
function adjustTablePositionsWithoutAreas(
|
||||||
|
tables: DBTable[],
|
||||||
|
relationships: DBRelationship[],
|
||||||
|
mode: 'all' | 'perSchema',
|
||||||
|
areas: Area[] = []
|
||||||
|
): DBTable[] {
|
||||||
const adjustPositionsForTables = (tablesToAdjust: DBTable[]) => {
|
const adjustPositionsForTables = (tablesToAdjust: DBTable[]) => {
|
||||||
const defaultTableWidth = 200;
|
const defaultTableWidth = 200;
|
||||||
const defaultTableHeight = 300;
|
const defaultTableHeight = 300;
|
||||||
@@ -339,8 +425,23 @@ export const adjustTablePositions = ({
|
|||||||
tableConnections.get(rel.targetTableId)!.add(rel.sourceTableId);
|
tableConnections.get(rel.targetTableId)!.add(rel.sourceTableId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort tables by number of connections
|
// Separate tables into connected and isolated
|
||||||
const sortedTables = [...tablesToAdjust].sort(
|
const connectedTables: DBTable[] = [];
|
||||||
|
const isolatedTables: DBTable[] = [];
|
||||||
|
|
||||||
|
tablesToAdjust.forEach((table) => {
|
||||||
|
if (
|
||||||
|
tableConnections.has(table.id) &&
|
||||||
|
tableConnections.get(table.id)!.size > 0
|
||||||
|
) {
|
||||||
|
connectedTables.push(table);
|
||||||
|
} else {
|
||||||
|
isolatedTables.push(table);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort connected tables by number of connections (most connected first)
|
||||||
|
connectedTables.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(tableConnections.get(b.id)?.size || 0) -
|
(tableConnections.get(b.id)?.size || 0) -
|
||||||
(tableConnections.get(a.id)?.size || 0)
|
(tableConnections.get(a.id)?.size || 0)
|
||||||
@@ -368,6 +469,7 @@ export const adjustTablePositions = ({
|
|||||||
y: number,
|
y: number,
|
||||||
currentTableId: string
|
currentTableId: string
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
// Check overlap with other tables
|
||||||
for (const [tableId, pos] of tablePositions) {
|
for (const [tableId, pos] of tablePositions) {
|
||||||
if (tableId === currentTableId) continue;
|
if (tableId === currentTableId) continue;
|
||||||
|
|
||||||
@@ -379,6 +481,26 @@ export const adjustTablePositions = ({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check overlap with areas
|
||||||
|
const { width: currentWidth, height: currentHeight } =
|
||||||
|
getTableWidthAndHeight(currentTableId);
|
||||||
|
const buffer = 50; // Add buffer around areas to keep tables away
|
||||||
|
|
||||||
|
for (const area of areas) {
|
||||||
|
// Check if the table position would overlap with the area (with buffer)
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
x + currentWidth < area.x - buffer ||
|
||||||
|
x > area.x + area.width + buffer ||
|
||||||
|
y + currentHeight < area.y - buffer ||
|
||||||
|
y > area.y + area.height + buffer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -462,8 +584,10 @@ export const adjustTablePositions = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Position tables
|
// Position connected tables first
|
||||||
sortedTables.forEach((table, index) => {
|
if (connectedTables.length < 100) {
|
||||||
|
// Use relationship-based positioning for small sets of connected tables
|
||||||
|
connectedTables.forEach((table, index) => {
|
||||||
if (!positionedTables.has(table.id)) {
|
if (!positionedTables.has(table.id)) {
|
||||||
const row = Math.floor(index / 6);
|
const row = Math.floor(index / 6);
|
||||||
const col = index % 6;
|
const col = index % 6;
|
||||||
@@ -475,6 +599,65 @@ export const adjustTablePositions = ({
|
|||||||
positionTable(table, x, y);
|
positionTable(table, x, y);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Use simple grid layout for large sets of connected tables
|
||||||
|
connectedTables.forEach((table, index) => {
|
||||||
|
if (!positionedTables.has(table.id)) {
|
||||||
|
const row = Math.floor(index / 10); // More columns for large sets
|
||||||
|
const col = index % 10;
|
||||||
|
const { width: tableWidth, height: tableHeight } =
|
||||||
|
getTableWidthAndHeight(table.id);
|
||||||
|
|
||||||
|
const x = startX + col * (tableWidth + gapX);
|
||||||
|
const y = startY + row * (tableHeight + gapY);
|
||||||
|
|
||||||
|
// Direct positioning without relationship-based clustering
|
||||||
|
const finalPos = findNonOverlappingPosition(x, y, table.id);
|
||||||
|
table.x = finalPos.x;
|
||||||
|
table.y = finalPos.y;
|
||||||
|
tablePositions.set(table.id, { x: table.x, y: table.y });
|
||||||
|
positionedTables.add(table.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the bottommost position of connected tables for isolated table placement
|
||||||
|
let maxY = startY;
|
||||||
|
for (const pos of tablePositions.values()) {
|
||||||
|
const tableId = [...tablePositions.entries()].find(
|
||||||
|
([, p]) => p === pos
|
||||||
|
)?.[0];
|
||||||
|
if (tableId) {
|
||||||
|
const { height } = getTableWidthAndHeight(tableId);
|
||||||
|
maxY = Math.max(maxY, pos.y + height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position isolated tables after connected ones
|
||||||
|
if (isolatedTables.length > 0) {
|
||||||
|
const isolatedStartY = maxY + gapY * 2;
|
||||||
|
const isolatedStartX = startX;
|
||||||
|
|
||||||
|
isolatedTables.forEach((table, index) => {
|
||||||
|
if (!positionedTables.has(table.id)) {
|
||||||
|
const row = Math.floor(index / 8); // More columns for isolated tables
|
||||||
|
const col = index % 8;
|
||||||
|
const { width: tableWidth, height: tableHeight } =
|
||||||
|
getTableWidthAndHeight(table.id);
|
||||||
|
|
||||||
|
// Use a simple grid layout for isolated tables
|
||||||
|
const x = isolatedStartX + col * (tableWidth + gapX);
|
||||||
|
const y = isolatedStartY + row * (tableHeight + gapY);
|
||||||
|
|
||||||
|
// Find non-overlapping position
|
||||||
|
const finalPos = findNonOverlappingPosition(x, y, table.id);
|
||||||
|
table.x = finalPos.x;
|
||||||
|
table.y = finalPos.y;
|
||||||
|
tablePositions.set(table.id, { x: table.x, y: table.y });
|
||||||
|
positionedTables.add(table.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Apply positions to tables
|
// Apply positions to tables
|
||||||
tablesToAdjust.forEach((table) => {
|
tablesToAdjust.forEach((table) => {
|
||||||
@@ -508,7 +691,7 @@ export const adjustTablePositions = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return tables;
|
return tables;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const calcTableHeight = (table?: DBTable): number => {
|
export const calcTableHeight = (table?: DBTable): number => {
|
||||||
if (!table) {
|
if (!table) {
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ export interface DiagramFilter {
|
|||||||
tableIds?: string[];
|
tableIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TableInfo {
|
export interface FilterTableInfo {
|
||||||
id: string;
|
id: string;
|
||||||
schemaId?: string;
|
schemaId?: string;
|
||||||
|
schema?: string;
|
||||||
|
areaId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,7 +20,8 @@ export interface TableInfo {
|
|||||||
*/
|
*/
|
||||||
export function reduceFilter(
|
export function reduceFilter(
|
||||||
filter: DiagramFilter,
|
filter: DiagramFilter,
|
||||||
tables: TableInfo[]
|
tables: FilterTableInfo[],
|
||||||
|
options: { databaseWithSchemas: boolean }
|
||||||
): DiagramFilter {
|
): DiagramFilter {
|
||||||
let { schemaIds, tableIds } = filter;
|
let { schemaIds, tableIds } = filter;
|
||||||
|
|
||||||
@@ -27,10 +30,14 @@ export function reduceFilter(
|
|||||||
return { schemaIds: undefined, tableIds: undefined };
|
return { schemaIds: undefined, tableIds: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!schemaIds && tableIds && tableIds.length === 0) {
|
||||||
|
return { schemaIds: undefined, tableIds: [] };
|
||||||
|
}
|
||||||
|
|
||||||
// Get all unique schema IDs from tables
|
// Get all unique schema IDs from tables
|
||||||
const allSchemaIds = [
|
const allSchemaIds = options.databaseWithSchemas
|
||||||
...new Set(tables.filter((t) => t.schemaId).map((t) => t.schemaId!)),
|
? [...new Set(tables.filter((t) => t.schemaId).map((t) => t.schemaId!))]
|
||||||
];
|
: [];
|
||||||
const allTableIds = tables.map((t) => t.id);
|
const allTableIds = tables.map((t) => t.id);
|
||||||
|
|
||||||
// in case its db with no schemas
|
// in case its db with no schemas
|
||||||
@@ -145,3 +152,50 @@ export function reduceFilter(
|
|||||||
tableIds: reducedTableIds,
|
tableIds: reducedTableIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const spreadFilterTables = (
|
||||||
|
filter: DiagramFilter,
|
||||||
|
tables: FilterTableInfo[]
|
||||||
|
): DiagramFilter => {
|
||||||
|
const { schemaIds, tableIds } = filter;
|
||||||
|
|
||||||
|
// If no filters are defined, everything is visible (return undefined)
|
||||||
|
if (!schemaIds && !tableIds) {
|
||||||
|
const allTablesIds = new Set<string>();
|
||||||
|
tables.forEach((table) => {
|
||||||
|
allTablesIds.add(table.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { tableIds: Array.from(allTablesIds) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only tableIds is defined, return it as is
|
||||||
|
if (!schemaIds && tableIds) {
|
||||||
|
return { tableIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all table IDs that should be visible
|
||||||
|
const visibleTableIds = new Set<string>();
|
||||||
|
|
||||||
|
// Add existing tableIds to the set
|
||||||
|
if (tableIds) {
|
||||||
|
tableIds.forEach((id) => visibleTableIds.add(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all tables from specified schemas
|
||||||
|
if (schemaIds) {
|
||||||
|
const schemaSet = new Set(schemaIds);
|
||||||
|
tables.forEach((table) => {
|
||||||
|
if (table.schemaId && schemaSet.has(table.schemaId)) {
|
||||||
|
visibleTableIds.add(table.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no tables are visible, return empty array
|
||||||
|
if (visibleTableIds.size === 0) {
|
||||||
|
return { tableIds: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tableIds: Array.from(visibleTableIds) };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||||
import { schemaNameToSchemaId } from '../db-schema';
|
import { schemaNameToSchemaId } from '../db-schema';
|
||||||
|
import type { Diagram } from '../diagram';
|
||||||
import type { DiagramFilter } from './diagram-filter';
|
import type { DiagramFilter } from './diagram-filter';
|
||||||
|
|
||||||
export const filterTable = ({
|
export const filterTable = ({
|
||||||
@@ -37,48 +39,6 @@ export const filterTable = ({
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterTableBySchema = ({
|
|
||||||
table,
|
|
||||||
schemaIdsFilter,
|
|
||||||
options = { defaultSchema: undefined },
|
|
||||||
}: {
|
|
||||||
table: { id: string; schema?: string | null };
|
|
||||||
schemaIdsFilter?: string[];
|
|
||||||
options?: {
|
|
||||||
defaultSchema?: string;
|
|
||||||
};
|
|
||||||
}): boolean => {
|
|
||||||
if (!schemaIdsFilter) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableSchemaId = table.schema ?? options.defaultSchema;
|
|
||||||
|
|
||||||
if (tableSchemaId) {
|
|
||||||
return schemaIdsFilter.includes(schemaNameToSchemaId(tableSchemaId));
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const filterSchema = ({
|
|
||||||
schemaId,
|
|
||||||
schemaIdsFilter,
|
|
||||||
}: {
|
|
||||||
schemaId?: string;
|
|
||||||
schemaIdsFilter?: string[];
|
|
||||||
}): boolean => {
|
|
||||||
if (!schemaIdsFilter) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!schemaId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return schemaIdsFilter.includes(schemaId);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const filterRelationship = ({
|
export const filterRelationship = ({
|
||||||
tableA: { id: tableAId, schema: tableASchema },
|
tableA: { id: tableAId, schema: tableASchema },
|
||||||
tableB: { id: tableBId, schema: tableBSchema },
|
tableB: { id: tableBId, schema: tableBSchema },
|
||||||
@@ -112,3 +72,63 @@ export const filterRelationship = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const filterDependency = filterRelationship;
|
export const filterDependency = filterRelationship;
|
||||||
|
|
||||||
|
export const applyFilterOnDiagram = ({
|
||||||
|
diagram,
|
||||||
|
filter,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
filter: DiagramFilter;
|
||||||
|
}): Diagram => {
|
||||||
|
const defaultSchema = defaultSchemas[diagram.databaseType];
|
||||||
|
const filteredTables = diagram.tables?.filter((table) =>
|
||||||
|
filterTable({
|
||||||
|
table: { id: table.id, schema: table.schema },
|
||||||
|
filter,
|
||||||
|
options: { defaultSchema },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredRelationships = diagram.relationships?.filter(
|
||||||
|
(relationship) =>
|
||||||
|
filterRelationship({
|
||||||
|
tableA: {
|
||||||
|
id: relationship.sourceTableId,
|
||||||
|
schema: relationship.sourceSchema,
|
||||||
|
},
|
||||||
|
tableB: {
|
||||||
|
id: relationship.targetTableId,
|
||||||
|
schema: relationship.targetSchema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: { defaultSchema },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredDependencies = diagram.dependencies?.filter((dependency) =>
|
||||||
|
filterDependency({
|
||||||
|
tableA: {
|
||||||
|
id: dependency.tableId,
|
||||||
|
schema: dependency.schema,
|
||||||
|
},
|
||||||
|
tableB: {
|
||||||
|
id: dependency.dependentTableId,
|
||||||
|
schema: dependency.dependentSchema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: { defaultSchema },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredAreas = diagram.areas?.filter((area) =>
|
||||||
|
filteredTables?.some((table) => table.parentAreaId === area.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...diagram,
|
||||||
|
tables: filteredTables,
|
||||||
|
relationships: filteredRelationships,
|
||||||
|
dependencies: filteredDependencies,
|
||||||
|
areas: filteredAreas,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import { useDialog } from '@/hooks/use-dialog';
|
|||||||
import { useReactFlow } from '@xyflow/react';
|
import { useReactFlow } from '@xyflow/react';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Table, Workflow, Group } from 'lucide-react';
|
import { Table, Workflow, Group, View } from 'lucide-react';
|
||||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||||
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
|
|
||||||
export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -21,6 +22,7 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
|||||||
const { openCreateRelationshipDialog, openTableSchemaDialog } = useDialog();
|
const { openCreateRelationshipDialog, openTableSchemaDialog } = useDialog();
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { showDBViews } = useLocalConfig();
|
||||||
|
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
const { isMd: isDesktop } = useBreakpoint('md');
|
||||||
|
|
||||||
@@ -61,6 +63,45 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const createViewHandler = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
const position = screenToFlowPosition({
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (schemasDisplayed.length > 1) {
|
||||||
|
openTableSchemaDialog({
|
||||||
|
onConfirm: ({ schema }) =>
|
||||||
|
createTable({
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
schema: schema.name,
|
||||||
|
isView: true,
|
||||||
|
}),
|
||||||
|
schemas: schemasDisplayed,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const schema =
|
||||||
|
schemasDisplayed?.length === 1
|
||||||
|
? schemasDisplayed[0]?.name
|
||||||
|
: undefined;
|
||||||
|
createTable({
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
schema,
|
||||||
|
isView: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
createTable,
|
||||||
|
screenToFlowPosition,
|
||||||
|
openTableSchemaDialog,
|
||||||
|
schemasDisplayed,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const createAreaHandler = useCallback(
|
const createAreaHandler = useCallback(
|
||||||
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
const position = screenToFlowPosition({
|
const position = screenToFlowPosition({
|
||||||
@@ -97,6 +138,15 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
|||||||
{t('canvas_context_menu.new_table')}
|
{t('canvas_context_menu.new_table')}
|
||||||
<Table className="size-3.5" />
|
<Table className="size-3.5" />
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
{showDBViews ? (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={createViewHandler}
|
||||||
|
className="flex justify-between gap-4"
|
||||||
|
>
|
||||||
|
{t('canvas_context_menu.new_view')}
|
||||||
|
<View className="size-3.5" />
|
||||||
|
</ContextMenuItem>
|
||||||
|
) : null}
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={createRelationshipHandler}
|
onClick={createRelationshipHandler}
|
||||||
className="flex justify-between gap-4"
|
className="flex justify-between gap-4"
|
||||||
|
|||||||
@@ -5,167 +5,98 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { X, Search, Eye, EyeOff, Database, Table, Funnel } from 'lucide-react';
|
import { X, Search, Database, Table, Funnel, Box } from 'lucide-react';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
import { Input } from '@/components/input/input';
|
import { Input } from '@/components/input/input';
|
||||||
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
|
|
||||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
|
||||||
import { useReactFlow } from '@xyflow/react';
|
import { useReactFlow } from '@xyflow/react';
|
||||||
import { TreeView } from '@/components/tree-view/tree-view';
|
import { TreeView } from '@/components/tree-view/tree-view';
|
||||||
import type { TreeNode } from '@/components/tree-view/tree';
|
import type { TreeNode } from '@/components/tree-view/tree';
|
||||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||||
import { filterSchema, filterTable } from '@/lib/domain/diagram-filter/filter';
|
|
||||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||||
|
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
|
||||||
|
import type {
|
||||||
|
GroupingMode,
|
||||||
|
NodeContext,
|
||||||
|
NodeType,
|
||||||
|
RelevantTableData,
|
||||||
|
TableContext,
|
||||||
|
} from './types';
|
||||||
|
import { generateTreeDataByAreas, generateTreeDataBySchemas } from './utils';
|
||||||
|
import { FilterItemActions } from './filter-item-actions';
|
||||||
|
import { databasesWithSchemas } from '@/lib/domain';
|
||||||
|
import { getOperatingSystem } from '@/lib/utils';
|
||||||
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
|
|
||||||
export interface CanvasFilterProps {
|
export interface CanvasFilterProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeType = 'schema' | 'table';
|
|
||||||
|
|
||||||
type SchemaContext = { name: string; visible: boolean };
|
|
||||||
type TableContext = {
|
|
||||||
tableSchema?: string | null;
|
|
||||||
visible: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NodeContext = {
|
|
||||||
schema: SchemaContext;
|
|
||||||
table: TableContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RelevantTableData = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
schema?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { tables, databaseType } = useChartDB();
|
const { tables, databaseType, areas } = useChartDB();
|
||||||
const {
|
const {
|
||||||
filter,
|
filter,
|
||||||
toggleSchemaFilter,
|
toggleSchemaFilter,
|
||||||
toggleTableFilter,
|
toggleTableFilter,
|
||||||
clearTableIdsFilter,
|
clearTableIdsFilter,
|
||||||
setTableIdsFilterEmpty,
|
setTableIdsFilterEmpty,
|
||||||
|
addTablesToFilter,
|
||||||
|
removeTablesFromFilter,
|
||||||
} = useDiagramFilter();
|
} = useDiagramFilter();
|
||||||
const { fitView, setNodes } = useReactFlow();
|
const { fitView, setNodes } = useReactFlow();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||||
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
||||||
|
const [groupingMode, setGroupingMode] = useState<GroupingMode>('schema');
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const { showDBViews } = useLocalConfig();
|
||||||
|
|
||||||
// Extract only the properties needed for tree data
|
// Extract only the properties needed for tree data
|
||||||
const relevantTableData = useMemo<RelevantTableData[]>(
|
const relevantTableData = useMemo<RelevantTableData[]>(
|
||||||
() =>
|
() =>
|
||||||
tables.map((table) => ({
|
tables
|
||||||
|
.filter((table) => (showDBViews ? true : !table.isView))
|
||||||
|
.map((table) => ({
|
||||||
id: table.id,
|
id: table.id,
|
||||||
name: table.name,
|
name: table.name,
|
||||||
schema: table.schema,
|
schema: table.schema,
|
||||||
|
parentAreaId: table.parentAreaId,
|
||||||
})),
|
})),
|
||||||
[tables]
|
[tables, showDBViews]
|
||||||
);
|
);
|
||||||
|
|
||||||
const databaseWithSchemas = useMemo(
|
const databaseWithSchemas = useMemo(
|
||||||
() => !!defaultSchemas[databaseType],
|
() => databasesWithSchemas.includes(databaseType),
|
||||||
[databaseType]
|
[databaseType]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert tables to tree nodes
|
// Convert tables to tree nodes
|
||||||
const treeData = useMemo(() => {
|
const treeData = useMemo(() => {
|
||||||
// Group tables by schema
|
if (groupingMode === 'area') {
|
||||||
const tablesBySchema = new Map<string, RelevantTableData[]>();
|
return generateTreeDataByAreas({
|
||||||
|
areas,
|
||||||
relevantTableData.forEach((table) => {
|
databaseType,
|
||||||
const schema = !databaseWithSchemas
|
filter,
|
||||||
? 'All Tables'
|
relevantTableData,
|
||||||
: (table.schema ?? defaultSchemas[databaseType] ?? 'default');
|
|
||||||
|
|
||||||
if (!tablesBySchema.has(schema)) {
|
|
||||||
tablesBySchema.set(schema, []);
|
|
||||||
}
|
|
||||||
tablesBySchema.get(schema)!.push(table);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort tables within each schema
|
|
||||||
tablesBySchema.forEach((tables) => {
|
|
||||||
tables.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert to tree nodes
|
|
||||||
const nodes: TreeNode<NodeType, NodeContext>[] = [];
|
|
||||||
|
|
||||||
tablesBySchema.forEach((schemaTables, schemaName) => {
|
|
||||||
let schemaVisible;
|
|
||||||
|
|
||||||
if (databaseWithSchemas) {
|
|
||||||
const schemaId = schemaNameToSchemaId(schemaName);
|
|
||||||
schemaVisible = filterSchema({
|
|
||||||
schemaId,
|
|
||||||
schemaIdsFilter: filter?.schemaIds,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// if at least one table is visible, the schema is considered visible
|
return generateTreeDataBySchemas({
|
||||||
schemaVisible = schemaTables.some((table) =>
|
relevantTableData,
|
||||||
filterTable({
|
databaseWithSchemas,
|
||||||
table: {
|
databaseType,
|
||||||
id: table.id,
|
|
||||||
schema: table.schema,
|
|
||||||
},
|
|
||||||
filter,
|
filter,
|
||||||
options: {
|
});
|
||||||
defaultSchema: defaultSchemas[databaseType],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
const schemaNode: TreeNode<NodeType, NodeContext> = {
|
relevantTableData,
|
||||||
id: `schema-${schemaName}`,
|
databaseType,
|
||||||
name: `${schemaName} (${schemaTables.length})`,
|
|
||||||
type: 'schema',
|
|
||||||
isFolder: true,
|
|
||||||
icon: Database,
|
|
||||||
context: { name: schemaName, visible: schemaVisible },
|
|
||||||
className: !schemaVisible ? 'opacity-50' : '',
|
|
||||||
children: schemaTables.map(
|
|
||||||
(table): TreeNode<NodeType, NodeContext> => {
|
|
||||||
const tableVisible = filterTable({
|
|
||||||
table: {
|
|
||||||
id: table.id,
|
|
||||||
schema: table.schema,
|
|
||||||
},
|
|
||||||
filter,
|
filter,
|
||||||
options: {
|
databaseWithSchemas,
|
||||||
defaultSchema: defaultSchemas[databaseType],
|
groupingMode,
|
||||||
},
|
areas,
|
||||||
});
|
]);
|
||||||
|
|
||||||
const hidden = !tableVisible;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: table.id,
|
|
||||||
name: table.name,
|
|
||||||
type: 'table',
|
|
||||||
isFolder: false,
|
|
||||||
icon: Table,
|
|
||||||
context: {
|
|
||||||
tableSchema: table.schema,
|
|
||||||
visible: tableVisible,
|
|
||||||
},
|
|
||||||
className: hidden ? 'opacity-50' : '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
),
|
|
||||||
};
|
|
||||||
nodes.push(schemaNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
}, [relevantTableData, databaseType, filter, databaseWithSchemas]);
|
|
||||||
|
|
||||||
// Initialize expanded state with all schemas expanded
|
// Initialize expanded state with all schemas expanded
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
@@ -201,6 +132,31 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
|||||||
return result;
|
return result;
|
||||||
}, [treeData, searchQuery]);
|
}, [treeData, searchQuery]);
|
||||||
|
|
||||||
|
// Render actions with proper memoization for performance
|
||||||
|
const renderActions = useCallback(
|
||||||
|
(node: TreeNode<NodeType, NodeContext>) => (
|
||||||
|
<FilterItemActions
|
||||||
|
node={node}
|
||||||
|
databaseWithSchemas={databaseWithSchemas}
|
||||||
|
toggleSchemaFilter={toggleSchemaFilter}
|
||||||
|
toggleTableFilter={toggleTableFilter}
|
||||||
|
clearTableIdsFilter={clearTableIdsFilter}
|
||||||
|
setTableIdsFilterEmpty={setTableIdsFilterEmpty}
|
||||||
|
addTablesToFilter={addTablesToFilter}
|
||||||
|
removeTablesFromFilter={removeTablesFromFilter}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[
|
||||||
|
databaseWithSchemas,
|
||||||
|
toggleSchemaFilter,
|
||||||
|
toggleTableFilter,
|
||||||
|
clearTableIdsFilter,
|
||||||
|
setTableIdsFilterEmpty,
|
||||||
|
addTablesToFilter,
|
||||||
|
removeTablesFromFilter,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const focusOnTable = useCallback(
|
const focusOnTable = useCallback(
|
||||||
(tableId: string) => {
|
(tableId: string) => {
|
||||||
// Make sure the table is visible
|
// Make sure the table is visible
|
||||||
@@ -236,86 +192,14 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
|||||||
[fitView, setNodes]
|
[fitView, setNodes]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render component that's always visible (eye indicator)
|
|
||||||
const renderActions = useCallback(
|
|
||||||
(node: TreeNode<NodeType, NodeContext>) => {
|
|
||||||
if (node.type === 'schema') {
|
|
||||||
const schemaContext = node.context as SchemaContext;
|
|
||||||
const schemaId = schemaNameToSchemaId(schemaContext.name);
|
|
||||||
const schemaVisible = node.context.visible;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="size-7 h-fit p-0"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (databaseWithSchemas) {
|
|
||||||
toggleSchemaFilter(schemaId);
|
|
||||||
} else {
|
|
||||||
// Toggle visibility of all tables in this schema
|
|
||||||
if (node.context.visible) {
|
|
||||||
setTableIdsFilterEmpty();
|
|
||||||
} else {
|
|
||||||
clearTableIdsFilter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!schemaVisible ? (
|
|
||||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<Eye className="size-3.5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.type === 'table') {
|
|
||||||
const tableId = node.id;
|
|
||||||
const tableContext = node.context as TableContext;
|
|
||||||
const tableVisible = tableContext.visible;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="size-7 h-fit p-0"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
toggleTableFilter(tableId);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!tableVisible ? (
|
|
||||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<Eye className="size-3.5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[
|
|
||||||
toggleSchemaFilter,
|
|
||||||
toggleTableFilter,
|
|
||||||
clearTableIdsFilter,
|
|
||||||
setTableIdsFilterEmpty,
|
|
||||||
databaseWithSchemas,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle node click
|
// Handle node click
|
||||||
const handleNodeClick = useCallback(
|
const handleNodeClick = useCallback(
|
||||||
(node: TreeNode<NodeType, NodeContext>) => {
|
(node: TreeNode<NodeType, NodeContext>) => {
|
||||||
if (node.type === 'table') {
|
if (node.type === 'table') {
|
||||||
const tableContext = node.context as TableContext;
|
const context = node.context as TableContext;
|
||||||
const isTableVisible = tableContext.visible;
|
const isTableVisible = context.visible;
|
||||||
|
|
||||||
// Only focus if neither table is hidden nor filtered by schema
|
// Only focus if table is visible
|
||||||
if (isTableVisible) {
|
if (isTableVisible) {
|
||||||
focusOnTable(node.id);
|
focusOnTable(node.id);
|
||||||
}
|
}
|
||||||
@@ -333,6 +217,11 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
|||||||
}, 300);
|
}, 300);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const openFilterShortcut = useMemo(
|
||||||
|
() => (getOperatingSystem() === 'mac' ? '⌘' : 'Ctrl+') + 'F',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute right-2 top-2 z-10 flex flex-col rounded-lg border bg-background/85 shadow-lg backdrop-blur-sm transition-all duration-300 md:right-4 md:top-4 ${
|
className={`absolute right-2 top-2 z-10 flex flex-col rounded-lg border bg-background/85 shadow-lg backdrop-blur-sm transition-all duration-300 md:right-4 md:top-4 ${
|
||||||
@@ -346,8 +235,11 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Funnel className="size-3.5 text-muted-foreground md:size-4" />
|
<Funnel className="size-3.5 text-muted-foreground md:size-4" />
|
||||||
<h2 className="text-sm font-medium">
|
<h2 className="text-sm font-medium">
|
||||||
{t('canvas_filter.title', 'Filter Tables')}
|
{t('canvas_filter.title', 'Filter Tables')}{' '}
|
||||||
</h2>
|
</h2>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
({openFilterShortcut})
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -376,13 +268,34 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Grouping Toggle */}
|
||||||
|
<div className="border-b p-2">
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
value={groupingMode}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value) setGroupingMode(value as GroupingMode);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value="schema" className="flex-1 text-xs">
|
||||||
|
<Database className="mr-1.5 size-3.5" />
|
||||||
|
{t('canvas_filter.group_by_schema', 'Group by Schema')}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="area" className="flex-1 text-xs">
|
||||||
|
<Box className="mr-1.5 size-3.5" />
|
||||||
|
{t('canvas_filter.group_by_area', 'Group by Area')}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Table Tree */}
|
{/* Table Tree */}
|
||||||
<ScrollArea className="flex-1 rounded-b-lg" type="auto">
|
<ScrollArea className="flex-1 rounded-b-lg" type="auto">
|
||||||
<TreeView
|
<TreeView
|
||||||
data={filteredTreeData}
|
data={filteredTreeData}
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
renderActionsComponent={renderActions}
|
renderActionsComponent={renderActions}
|
||||||
defaultFolderIcon={Database}
|
defaultFolderIcon={groupingMode === 'area' ? Box : Database}
|
||||||
defaultIcon={Table}
|
defaultIcon={Table}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
setExpanded={setExpanded}
|
setExpanded={setExpanded}
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import type { TreeNode } from '@/components/tree-view/tree';
|
||||||
|
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
|
||||||
|
import type {
|
||||||
|
AreaContext,
|
||||||
|
NodeContext,
|
||||||
|
NodeType,
|
||||||
|
// RelevantTableData,
|
||||||
|
SchemaContext,
|
||||||
|
TableContext,
|
||||||
|
} from './types';
|
||||||
|
import type { FilterTableInfo } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||||
|
|
||||||
|
interface FilterItemActionsProps {
|
||||||
|
node: TreeNode<NodeType, NodeContext>;
|
||||||
|
databaseWithSchemas: boolean;
|
||||||
|
toggleSchemaFilter: (schemaId: string) => void;
|
||||||
|
toggleTableFilter: (tableId: string) => void;
|
||||||
|
clearTableIdsFilter: () => void;
|
||||||
|
setTableIdsFilterEmpty: () => void;
|
||||||
|
addTablesToFilter: (attrs: {
|
||||||
|
tableIds?: string[];
|
||||||
|
filterCallback?: (table: FilterTableInfo) => boolean;
|
||||||
|
}) => void;
|
||||||
|
removeTablesFromFilter: (attrs: {
|
||||||
|
tableIds?: string[];
|
||||||
|
filterCallback?: (table: FilterTableInfo) => boolean;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilterItemActions: React.FC<FilterItemActionsProps> = ({
|
||||||
|
node,
|
||||||
|
databaseWithSchemas,
|
||||||
|
toggleSchemaFilter,
|
||||||
|
toggleTableFilter,
|
||||||
|
clearTableIdsFilter,
|
||||||
|
setTableIdsFilterEmpty,
|
||||||
|
addTablesToFilter,
|
||||||
|
removeTablesFromFilter,
|
||||||
|
}) => {
|
||||||
|
if (node.type === 'schema') {
|
||||||
|
const context = node.context as SchemaContext;
|
||||||
|
const schemaVisible = context.visible;
|
||||||
|
const schemaName = context.name;
|
||||||
|
const schemaId = schemaNameToSchemaId(schemaName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-7 h-fit p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (databaseWithSchemas) {
|
||||||
|
toggleSchemaFilter(schemaId);
|
||||||
|
} else {
|
||||||
|
// Toggle visibility of all tables in this schema
|
||||||
|
if (schemaVisible) {
|
||||||
|
setTableIdsFilterEmpty();
|
||||||
|
} else {
|
||||||
|
clearTableIdsFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!schemaVisible ? (
|
||||||
|
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<Eye className="size-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'area') {
|
||||||
|
const context = node.context as AreaContext;
|
||||||
|
const areaVisible = context.visible;
|
||||||
|
const isUngrouped = context.isUngrouped;
|
||||||
|
const areaId = context.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-7 h-fit p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Toggle all tables in this area
|
||||||
|
if (areaVisible) {
|
||||||
|
// Hide all tables in this area
|
||||||
|
removeTablesFromFilter({
|
||||||
|
filterCallback: (table) =>
|
||||||
|
(isUngrouped && !table.areaId) ||
|
||||||
|
(!isUngrouped && table.areaId === areaId),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Show all tables in this area
|
||||||
|
addTablesToFilter({
|
||||||
|
filterCallback: (table) =>
|
||||||
|
(isUngrouped && !table.areaId) ||
|
||||||
|
(!isUngrouped && table.areaId === areaId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!areaVisible ? (
|
||||||
|
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<Eye className="size-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'table') {
|
||||||
|
const tableId = node.id;
|
||||||
|
const context = node.context as TableContext;
|
||||||
|
const tableVisible = context.visible;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-7 h-fit p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleTableFilter(tableId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!tableVisible ? (
|
||||||
|
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<Eye className="size-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
27
src/pages/editor-page/canvas/canvas-filter/types.ts
Normal file
27
src/pages/editor-page/canvas/canvas-filter/types.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export type NodeType = 'schema' | 'area' | 'table';
|
||||||
|
export type GroupingMode = 'schema' | 'area';
|
||||||
|
|
||||||
|
export type SchemaContext = { name: string; visible: boolean };
|
||||||
|
export type AreaContext = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
visible: boolean;
|
||||||
|
isUngrouped: boolean;
|
||||||
|
};
|
||||||
|
export type TableContext = {
|
||||||
|
tableSchema?: string | null;
|
||||||
|
visible: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NodeContext = {
|
||||||
|
schema: SchemaContext;
|
||||||
|
area: AreaContext;
|
||||||
|
table: TableContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RelevantTableData = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
schema?: string | null;
|
||||||
|
parentAreaId?: string | null;
|
||||||
|
};
|
||||||
292
src/pages/editor-page/canvas/canvas-filter/utils.ts
Normal file
292
src/pages/editor-page/canvas/canvas-filter/utils.ts
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import type { Area, DatabaseType } from '@/lib/domain';
|
||||||
|
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||||
|
import type {
|
||||||
|
AreaContext,
|
||||||
|
NodeContext,
|
||||||
|
NodeType,
|
||||||
|
RelevantTableData,
|
||||||
|
SchemaContext,
|
||||||
|
TableContext,
|
||||||
|
} from './types';
|
||||||
|
import type { TreeNode } from '@/components/tree-view/tree';
|
||||||
|
import { Box, Database, Layers, Table } from 'lucide-react';
|
||||||
|
import { filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||||
|
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||||
|
|
||||||
|
export const generateTreeDataByAreas = ({
|
||||||
|
areas,
|
||||||
|
databaseType,
|
||||||
|
filter,
|
||||||
|
relevantTableData,
|
||||||
|
}: {
|
||||||
|
areas: Area[];
|
||||||
|
databaseType: DatabaseType;
|
||||||
|
filter?: DiagramFilter;
|
||||||
|
relevantTableData: RelevantTableData[];
|
||||||
|
}): TreeNode<NodeType, NodeContext>[] => {
|
||||||
|
const nodes: TreeNode<NodeType, NodeContext>[] = [];
|
||||||
|
|
||||||
|
// Group tables by area
|
||||||
|
const tablesByArea = new Map<string | null, RelevantTableData[]>();
|
||||||
|
const tablesWithoutArea: RelevantTableData[] = [];
|
||||||
|
|
||||||
|
relevantTableData.forEach((table) => {
|
||||||
|
if (table.parentAreaId) {
|
||||||
|
if (!tablesByArea.has(table.parentAreaId)) {
|
||||||
|
tablesByArea.set(table.parentAreaId, []);
|
||||||
|
}
|
||||||
|
tablesByArea.get(table.parentAreaId)!.push(table);
|
||||||
|
} else {
|
||||||
|
tablesWithoutArea.push(table);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort tables within each area
|
||||||
|
tablesByArea.forEach((areaTables) => {
|
||||||
|
areaTables.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
});
|
||||||
|
tablesWithoutArea.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
// Create nodes for areas
|
||||||
|
areas.forEach((area) => {
|
||||||
|
const areaTables = tablesByArea.get(area.id) || [];
|
||||||
|
|
||||||
|
// Check if at least one table in the area is visible
|
||||||
|
const areaVisible =
|
||||||
|
// areaTables.length === 0 ||
|
||||||
|
!areaTables.some(
|
||||||
|
(table) =>
|
||||||
|
filterTable({
|
||||||
|
table: {
|
||||||
|
id: table.id,
|
||||||
|
schema: table.schema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
}) === false
|
||||||
|
);
|
||||||
|
|
||||||
|
const areaNode: TreeNode<NodeType, NodeContext> = {
|
||||||
|
id: `area-${area.id}`,
|
||||||
|
name: `${area.name} (${areaTables.length})`,
|
||||||
|
type: 'area',
|
||||||
|
isFolder: true,
|
||||||
|
icon: Box,
|
||||||
|
context: {
|
||||||
|
id: area.id,
|
||||||
|
name: area.name,
|
||||||
|
visible: areaVisible,
|
||||||
|
isUngrouped: false,
|
||||||
|
} satisfies AreaContext,
|
||||||
|
className: !areaVisible ? 'opacity-50' : '',
|
||||||
|
children: areaTables.map(
|
||||||
|
(table): TreeNode<NodeType, NodeContext> => {
|
||||||
|
const tableVisible = filterTable({
|
||||||
|
table: {
|
||||||
|
id: table.id,
|
||||||
|
schema: table.schema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: table.id,
|
||||||
|
name: table.name,
|
||||||
|
type: 'table',
|
||||||
|
isFolder: false,
|
||||||
|
icon: Table,
|
||||||
|
context: {
|
||||||
|
tableSchema: table.schema,
|
||||||
|
visible: tableVisible,
|
||||||
|
} satisfies TableContext,
|
||||||
|
className: !tableVisible ? 'opacity-50' : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (areaTables.length > 0) {
|
||||||
|
nodes.push(areaNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add ungrouped tables
|
||||||
|
if (tablesWithoutArea.length > 0) {
|
||||||
|
const ungroupedVisible = !tablesWithoutArea.some(
|
||||||
|
(table) =>
|
||||||
|
filterTable({
|
||||||
|
table: {
|
||||||
|
id: table.id,
|
||||||
|
schema: table.schema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
}) == false
|
||||||
|
);
|
||||||
|
|
||||||
|
const ungroupedNode: TreeNode<NodeType, NodeContext> = {
|
||||||
|
id: 'ungrouped',
|
||||||
|
name: `Ungrouped (${tablesWithoutArea.length})`,
|
||||||
|
type: 'area',
|
||||||
|
isFolder: true,
|
||||||
|
icon: Layers,
|
||||||
|
context: {
|
||||||
|
id: 'ungrouped',
|
||||||
|
name: 'Ungrouped',
|
||||||
|
visible: ungroupedVisible,
|
||||||
|
isUngrouped: true,
|
||||||
|
} satisfies AreaContext,
|
||||||
|
className: !ungroupedVisible ? 'opacity-50' : '',
|
||||||
|
children: tablesWithoutArea.map(
|
||||||
|
(table): TreeNode<NodeType, NodeContext> => {
|
||||||
|
const tableVisible = filterTable({
|
||||||
|
table: {
|
||||||
|
id: table.id,
|
||||||
|
schema: table.schema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: table.id,
|
||||||
|
name: table.name,
|
||||||
|
type: 'table',
|
||||||
|
isFolder: false,
|
||||||
|
icon: Table,
|
||||||
|
context: {
|
||||||
|
tableSchema: table.schema,
|
||||||
|
visible: tableVisible,
|
||||||
|
} satisfies TableContext,
|
||||||
|
className: !tableVisible ? 'opacity-50' : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
nodes.push(ungroupedNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateTreeDataBySchemas = ({
|
||||||
|
relevantTableData,
|
||||||
|
databaseWithSchemas,
|
||||||
|
databaseType,
|
||||||
|
filter,
|
||||||
|
}: {
|
||||||
|
relevantTableData: RelevantTableData[];
|
||||||
|
databaseWithSchemas: boolean;
|
||||||
|
databaseType: DatabaseType;
|
||||||
|
filter?: DiagramFilter;
|
||||||
|
}): TreeNode<NodeType, NodeContext>[] => {
|
||||||
|
const nodes: TreeNode<NodeType, NodeContext>[] = [];
|
||||||
|
|
||||||
|
// Group tables by schema (existing logic)
|
||||||
|
const tablesBySchema = new Map<string, RelevantTableData[]>();
|
||||||
|
|
||||||
|
relevantTableData.forEach((table) => {
|
||||||
|
const schema = !databaseWithSchemas
|
||||||
|
? 'All Tables'
|
||||||
|
: (table.schema ?? defaultSchemas[databaseType] ?? 'default');
|
||||||
|
|
||||||
|
if (!tablesBySchema.has(schema)) {
|
||||||
|
tablesBySchema.set(schema, []);
|
||||||
|
}
|
||||||
|
tablesBySchema.get(schema)!.push(table);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort tables within each schema
|
||||||
|
tablesBySchema.forEach((tables) => {
|
||||||
|
tables.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
tablesBySchema.forEach((schemaTables, schemaName) => {
|
||||||
|
let schemaVisible;
|
||||||
|
|
||||||
|
if (databaseWithSchemas) {
|
||||||
|
schemaVisible = !schemaTables.some(
|
||||||
|
(table) =>
|
||||||
|
filterTable({
|
||||||
|
table: {
|
||||||
|
id: table.id,
|
||||||
|
schema: table.schema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
}) === false
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// if at least one table is visible, the schema is considered visible
|
||||||
|
schemaVisible = !schemaTables.some(
|
||||||
|
(table) =>
|
||||||
|
filterTable({
|
||||||
|
table: {
|
||||||
|
id: table.id,
|
||||||
|
schema: table.schema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
}) === false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaNode: TreeNode<NodeType, NodeContext> = {
|
||||||
|
id: `schema-${schemaName}`,
|
||||||
|
name: `${schemaName} (${schemaTables.length})`,
|
||||||
|
type: 'schema',
|
||||||
|
isFolder: true,
|
||||||
|
icon: Database,
|
||||||
|
context: {
|
||||||
|
name: schemaName,
|
||||||
|
visible: schemaVisible,
|
||||||
|
} satisfies SchemaContext,
|
||||||
|
className: !schemaVisible ? 'opacity-50' : '',
|
||||||
|
children: schemaTables.map(
|
||||||
|
(table): TreeNode<NodeType, NodeContext> => {
|
||||||
|
const tableVisible = filterTable({
|
||||||
|
table: {
|
||||||
|
id: table.id,
|
||||||
|
schema: table.schema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hidden = !tableVisible;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: table.id,
|
||||||
|
name: table.name,
|
||||||
|
type: 'table',
|
||||||
|
isFolder: false,
|
||||||
|
icon: Table,
|
||||||
|
context: {
|
||||||
|
tableSchema: table.schema,
|
||||||
|
visible: tableVisible,
|
||||||
|
} satisfies TableContext,
|
||||||
|
className: hidden ? 'opacity-50' : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
nodes.push(schemaNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
};
|
||||||
@@ -80,7 +80,7 @@ import {
|
|||||||
TARGET_DEP_PREFIX,
|
TARGET_DEP_PREFIX,
|
||||||
TOP_SOURCE_HANDLE_ID_PREFIX,
|
TOP_SOURCE_HANDLE_ID_PREFIX,
|
||||||
} from './table-node/table-node-dependency-indicator';
|
} from './table-node/table-node-dependency-indicator';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import type { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import { useAlert } from '@/context/alert-context/alert-context';
|
import { useAlert } from '@/context/alert-context/alert-context';
|
||||||
import { useCanvas } from '@/hooks/use-canvas';
|
import { useCanvas } from '@/hooks/use-canvas';
|
||||||
import type { AreaNodeType } from './area-node/area-node';
|
import type { AreaNodeType } from './area-node/area-node';
|
||||||
@@ -119,8 +119,17 @@ const initialEdges: EdgeType[] = [];
|
|||||||
|
|
||||||
const tableToTableNode = (
|
const tableToTableNode = (
|
||||||
table: DBTable,
|
table: DBTable,
|
||||||
filter: DiagramFilter | undefined,
|
{
|
||||||
databaseType: DatabaseType
|
filter,
|
||||||
|
databaseType,
|
||||||
|
filterLoading,
|
||||||
|
showDBViews,
|
||||||
|
}: {
|
||||||
|
filter?: DiagramFilter;
|
||||||
|
databaseType: DatabaseType;
|
||||||
|
filterLoading: boolean;
|
||||||
|
showDBViews?: boolean;
|
||||||
|
}
|
||||||
): TableNodeType => {
|
): TableNodeType => {
|
||||||
// Always use absolute position for now
|
// Always use absolute position for now
|
||||||
const position = { x: table.x, y: table.y };
|
const position = { x: table.x, y: table.y };
|
||||||
@@ -134,15 +143,48 @@ const tableToTableNode = (
|
|||||||
isOverlapping: false,
|
isOverlapping: false,
|
||||||
},
|
},
|
||||||
width: table.width ?? MIN_TABLE_SIZE,
|
width: table.width ?? MIN_TABLE_SIZE,
|
||||||
hidden: !filterTable({
|
hidden:
|
||||||
|
!filterTable({
|
||||||
table: { id: table.id, schema: table.schema },
|
table: { id: table.id, schema: table.schema },
|
||||||
filter,
|
filter,
|
||||||
options: { defaultSchema: defaultSchemas[databaseType] },
|
options: { defaultSchema: defaultSchemas[databaseType] },
|
||||||
}),
|
}) ||
|
||||||
|
filterLoading ||
|
||||||
|
(!showDBViews && table.isView),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const areaToAreaNode = (area: Area): AreaNodeType => ({
|
const areaToAreaNode = (
|
||||||
|
area: Area,
|
||||||
|
{
|
||||||
|
tables,
|
||||||
|
filter,
|
||||||
|
databaseType,
|
||||||
|
filterLoading,
|
||||||
|
}: {
|
||||||
|
tables: DBTable[];
|
||||||
|
filter?: DiagramFilter;
|
||||||
|
databaseType: DatabaseType;
|
||||||
|
filterLoading: boolean;
|
||||||
|
}
|
||||||
|
): AreaNodeType => {
|
||||||
|
// Get all tables in this area
|
||||||
|
const tablesInArea = tables.filter((t) => t.parentAreaId === area.id);
|
||||||
|
|
||||||
|
// Check if at least one table in the area is visible
|
||||||
|
const hasVisibleTable =
|
||||||
|
tablesInArea.length === 0 ||
|
||||||
|
tablesInArea.some((table) =>
|
||||||
|
filterTable({
|
||||||
|
table: { id: table.id, schema: table.schema },
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
id: area.id,
|
id: area.id,
|
||||||
type: 'area',
|
type: 'area',
|
||||||
position: { x: area.x, y: area.y },
|
position: { x: area.x, y: area.y },
|
||||||
@@ -150,7 +192,9 @@ const areaToAreaNode = (area: Area): AreaNodeType => ({
|
|||||||
width: area.width,
|
width: area.width,
|
||||||
height: area.height,
|
height: area.height,
|
||||||
zIndex: -10,
|
zIndex: -10,
|
||||||
});
|
hidden: !hasVisibleTable || filterLoading,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export interface CanvasProps {
|
export interface CanvasProps {
|
||||||
initialTables: DBTable[];
|
initialTables: DBTable[];
|
||||||
@@ -186,8 +230,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
} = useChartDB();
|
} = useChartDB();
|
||||||
const { showSidePanel } = useLayout();
|
const { showSidePanel } = useLayout();
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
const { scrollAction, showDependenciesOnCanvas, showMiniMapOnCanvas } =
|
const { scrollAction, showDBViews, showMiniMapOnCanvas } = useLocalConfig();
|
||||||
useLocalConfig();
|
|
||||||
const { showAlert } = useAlert();
|
const { showAlert } = useAlert();
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
const { isMd: isDesktop } = useBreakpoint('md');
|
||||||
const [highlightOverlappingTables, setHighlightOverlappingTables] =
|
const [highlightOverlappingTables, setHighlightOverlappingTables] =
|
||||||
@@ -200,13 +243,18 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
showFilter,
|
showFilter,
|
||||||
setShowFilter,
|
setShowFilter,
|
||||||
} = useCanvas();
|
} = useCanvas();
|
||||||
const { filter } = useDiagramFilter();
|
const { filter, loading: filterLoading } = useDiagramFilter();
|
||||||
|
|
||||||
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
|
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState<NodeType>(
|
const [nodes, setNodes, onNodesChange] = useNodesState<NodeType>(
|
||||||
initialTables.map((table) =>
|
initialTables.map((table) =>
|
||||||
tableToTableNode(table, filter, databaseType)
|
tableToTableNode(table, {
|
||||||
|
filter,
|
||||||
|
databaseType,
|
||||||
|
filterLoading,
|
||||||
|
showDBViews,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const [edges, setEdges, onEdgesChange] =
|
const [edges, setEdges, onEdgesChange] =
|
||||||
@@ -220,12 +268,24 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialNodes = initialTables.map((table) =>
|
const initialNodes = initialTables.map((table) =>
|
||||||
tableToTableNode(table, filter, databaseType)
|
tableToTableNode(table, {
|
||||||
|
filter,
|
||||||
|
databaseType,
|
||||||
|
filterLoading,
|
||||||
|
showDBViews,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
if (equal(initialNodes, nodes)) {
|
if (equal(initialNodes, nodes)) {
|
||||||
setIsInitialLoadingNodes(false);
|
setIsInitialLoadingNodes(false);
|
||||||
}
|
}
|
||||||
}, [initialTables, nodes, filter, databaseType]);
|
}, [
|
||||||
|
initialTables,
|
||||||
|
nodes,
|
||||||
|
filter,
|
||||||
|
databaseType,
|
||||||
|
filterLoading,
|
||||||
|
showDBViews,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isInitialLoadingNodes) {
|
if (!isInitialLoadingNodes) {
|
||||||
@@ -279,19 +339,11 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
targetHandle: `${TARGET_DEP_PREFIX}${targetDepIndexes[dep.tableId]++}_${dep.tableId}`,
|
targetHandle: `${TARGET_DEP_PREFIX}${targetDepIndexes[dep.tableId]++}_${dep.tableId}`,
|
||||||
type: 'dependency-edge',
|
type: 'dependency-edge',
|
||||||
data: { dependency: dep },
|
data: { dependency: dep },
|
||||||
hidden:
|
hidden: !showDBViews,
|
||||||
!showDependenciesOnCanvas &&
|
|
||||||
databaseType !== DatabaseType.CLICKHOUSE,
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}, [
|
}, [relationships, dependencies, setEdges, showDBViews]);
|
||||||
relationships,
|
|
||||||
dependencies,
|
|
||||||
setEdges,
|
|
||||||
showDependenciesOnCanvas,
|
|
||||||
databaseType,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedNodesIds = nodes
|
const selectedNodesIds = nodes
|
||||||
@@ -388,7 +440,12 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
...tables.map((table) => {
|
...tables.map((table) => {
|
||||||
const isOverlapping =
|
const isOverlapping =
|
||||||
(overlapGraph.graph.get(table.id) ?? []).length > 0;
|
(overlapGraph.graph.get(table.id) ?? []).length > 0;
|
||||||
const node = tableToTableNode(table, filter, databaseType);
|
const node = tableToTableNode(table, {
|
||||||
|
filter,
|
||||||
|
databaseType,
|
||||||
|
filterLoading,
|
||||||
|
showDBViews,
|
||||||
|
});
|
||||||
|
|
||||||
// Check if table uses the highlighted custom type
|
// Check if table uses the highlighted custom type
|
||||||
let hasHighlightedCustomType = false;
|
let hasHighlightedCustomType = false;
|
||||||
@@ -409,7 +466,14 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
...areas.map(areaToAreaNode),
|
...areas.map((area) =>
|
||||||
|
areaToAreaNode(area, {
|
||||||
|
tables,
|
||||||
|
filter,
|
||||||
|
databaseType,
|
||||||
|
filterLoading,
|
||||||
|
})
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check if nodes actually changed
|
// Check if nodes actually changed
|
||||||
@@ -429,6 +493,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
overlapGraph.graph,
|
overlapGraph.graph,
|
||||||
highlightOverlappingTables,
|
highlightOverlappingTables,
|
||||||
highlightedCustomType,
|
highlightedCustomType,
|
||||||
|
filterLoading,
|
||||||
|
showDBViews,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const prevFilter = useRef<DiagramFilter | undefined>(undefined);
|
const prevFilter = useRef<DiagramFilter | undefined>(undefined);
|
||||||
@@ -460,22 +526,26 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
}
|
}
|
||||||
}, [filter, fitView, tables, setOverlapGraph, databaseType]);
|
}, [filter, fitView, tables, setOverlapGraph, databaseType]);
|
||||||
|
|
||||||
// Handle parent area updates when tables move
|
|
||||||
const tablePositions = useMemo(
|
|
||||||
() => tables.map((t) => ({ id: t.id, x: t.x, y: t.y })),
|
|
||||||
[tables]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkParentAreas = debounce(() => {
|
const checkParentAreas = debounce(() => {
|
||||||
const updatedTables = updateTablesParentAreas(tables, areas);
|
const visibleTables = nodes
|
||||||
|
.filter((node) => node.type === 'table' && !node.hidden)
|
||||||
|
.map((node) => (node as TableNodeType).data.table);
|
||||||
|
const visibleAreas = nodes
|
||||||
|
.filter((node) => node.type === 'area' && !node.hidden)
|
||||||
|
.map((node) => (node as AreaNodeType).data.area);
|
||||||
|
|
||||||
|
const updatedTables = updateTablesParentAreas(
|
||||||
|
visibleTables,
|
||||||
|
visibleAreas
|
||||||
|
);
|
||||||
const needsUpdate: Array<{
|
const needsUpdate: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
parentAreaId: string | null;
|
parentAreaId: string | null;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
updatedTables.forEach((newTable, index) => {
|
updatedTables.forEach((newTable, index) => {
|
||||||
const oldTable = tables[index];
|
const oldTable = visibleTables[index];
|
||||||
if (
|
if (
|
||||||
oldTable &&
|
oldTable &&
|
||||||
(!!newTable.parentAreaId || !!oldTable.parentAreaId) &&
|
(!!newTable.parentAreaId || !!oldTable.parentAreaId) &&
|
||||||
@@ -509,7 +579,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
checkParentAreas();
|
checkParentAreas();
|
||||||
}, [tablePositions, areas, updateTablesState, tables]);
|
}, [nodes, updateTablesState]);
|
||||||
|
|
||||||
const onConnectHandler = useCallback(
|
const onConnectHandler = useCallback(
|
||||||
async (params: AddEdgeParams) => {
|
async (params: AddEdgeParams) => {
|
||||||
@@ -792,26 +862,87 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
changesToApply = [...changesToApply, ...additionalChanges];
|
changesToApply = [...changesToApply, ...additionalChanges];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle table changes - only update storage when NOT dragging
|
// First, detect area changes
|
||||||
|
const {
|
||||||
|
positionChanges: areaPositionChanges,
|
||||||
|
removeChanges: areaRemoveChanges,
|
||||||
|
sizeChanges: areaSizeChanges,
|
||||||
|
} = findRelevantNodesChanges(changesToApply, 'area');
|
||||||
|
|
||||||
|
// Then, detect table changes
|
||||||
const { positionChanges, removeChanges, sizeChanges } =
|
const { positionChanges, removeChanges, sizeChanges } =
|
||||||
findRelevantNodesChanges(changesToApply, 'table');
|
findRelevantNodesChanges(changesToApply, 'table');
|
||||||
|
|
||||||
|
// Calculate child table movements from area position changes
|
||||||
|
const childTableMovements: Map<
|
||||||
|
string,
|
||||||
|
{ deltaX: number; deltaY: number }
|
||||||
|
> = new Map();
|
||||||
|
if (
|
||||||
|
areaPositionChanges.length > 0 &&
|
||||||
|
areaSizeChanges.length === 0
|
||||||
|
) {
|
||||||
|
areaPositionChanges.forEach((change) => {
|
||||||
|
if (change.type === 'position' && change.position) {
|
||||||
|
const currentArea = areas.find(
|
||||||
|
(a) => a.id === change.id
|
||||||
|
);
|
||||||
|
if (currentArea) {
|
||||||
|
const deltaX = change.position.x - currentArea.x;
|
||||||
|
const deltaY = change.position.y - currentArea.y;
|
||||||
|
|
||||||
|
const childTables = getTablesInArea(
|
||||||
|
change.id,
|
||||||
|
tables
|
||||||
|
);
|
||||||
|
childTables.forEach((table) => {
|
||||||
|
childTableMovements.set(table.id, {
|
||||||
|
deltaX,
|
||||||
|
deltaY,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply all table updates in a single call
|
||||||
if (
|
if (
|
||||||
positionChanges.length > 0 ||
|
positionChanges.length > 0 ||
|
||||||
removeChanges.length > 0 ||
|
removeChanges.length > 0 ||
|
||||||
sizeChanges.length > 0
|
sizeChanges.length > 0 ||
|
||||||
|
childTableMovements.size > 0 ||
|
||||||
|
areaRemoveChanges.length > 0
|
||||||
) {
|
) {
|
||||||
updateTablesState((currentTables) => {
|
updateTablesState((currentTables) => {
|
||||||
// First update positions
|
|
||||||
const updatedTables = currentTables
|
const updatedTables = currentTables
|
||||||
.map((currentTable) => {
|
.map((currentTable) => {
|
||||||
|
// Handle area removal - clear parentAreaId
|
||||||
|
const removedArea = areaRemoveChanges.find(
|
||||||
|
(change) =>
|
||||||
|
change.id === currentTable.parentAreaId
|
||||||
|
);
|
||||||
|
if (removedArea) {
|
||||||
|
return {
|
||||||
|
...currentTable,
|
||||||
|
parentAreaId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle direct table changes
|
||||||
const positionChange = positionChanges.find(
|
const positionChange = positionChanges.find(
|
||||||
(change) => change.id === currentTable.id
|
(change) => change.id === currentTable.id
|
||||||
);
|
);
|
||||||
const sizeChange = sizeChanges.find(
|
const sizeChange = sizeChanges.find(
|
||||||
(change) => change.id === currentTable.id
|
(change) => change.id === currentTable.id
|
||||||
);
|
);
|
||||||
if (positionChange || sizeChange) {
|
|
||||||
|
// Handle child table movement from area drag
|
||||||
|
const areaMovement = childTableMovements.get(
|
||||||
|
currentTable.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (positionChange || sizeChange || areaMovement) {
|
||||||
const x = positionChange?.position?.x;
|
const x = positionChange?.position?.x;
|
||||||
const y = positionChange?.position?.y;
|
const y = positionChange?.position?.y;
|
||||||
|
|
||||||
@@ -827,6 +958,16 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
y,
|
y,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
...(areaMovement && !positionChange
|
||||||
|
? {
|
||||||
|
x:
|
||||||
|
currentTable.x +
|
||||||
|
areaMovement.deltaX,
|
||||||
|
y:
|
||||||
|
currentTable.y +
|
||||||
|
areaMovement.deltaY,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...(sizeChange
|
...(sizeChange
|
||||||
? {
|
? {
|
||||||
width:
|
width:
|
||||||
@@ -855,20 +996,13 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
sizeChanges,
|
sizeChanges,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle area changes
|
|
||||||
const {
|
|
||||||
positionChanges: areaPositionChanges,
|
|
||||||
removeChanges: areaRemoveChanges,
|
|
||||||
sizeChanges: areaSizeChanges,
|
|
||||||
} = findRelevantNodesChanges(changesToApply, 'area');
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
areaPositionChanges.length > 0 ||
|
areaPositionChanges.length > 0 ||
|
||||||
areaRemoveChanges.length > 0 ||
|
areaRemoveChanges.length > 0 ||
|
||||||
areaSizeChanges.length > 0
|
areaSizeChanges.length > 0
|
||||||
) {
|
) {
|
||||||
const areasUpdates: Record<string, Partial<Area>> = {};
|
const areasUpdates: Record<string, Partial<Area>> = {};
|
||||||
// Handle area position changes and move child tables (only when drag ends)
|
// Handle area position changes (child tables already moved above)
|
||||||
areaPositionChanges.forEach((change) => {
|
areaPositionChanges.forEach((change) => {
|
||||||
if (change.type === 'position' && change.position) {
|
if (change.type === 'position' && change.position) {
|
||||||
areasUpdates[change.id] = {
|
areasUpdates[change.id] = {
|
||||||
@@ -876,39 +1010,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
x: change.position.x,
|
x: change.position.x,
|
||||||
y: change.position.y,
|
y: change.position.y,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (areaSizeChanges.length !== 0) {
|
|
||||||
// If there are size changes, we don't need to move child tables
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentArea = areas.find(
|
|
||||||
(a) => a.id === change.id
|
|
||||||
);
|
|
||||||
if (currentArea) {
|
|
||||||
const deltaX = change.position.x - currentArea.x;
|
|
||||||
const deltaY = change.position.y - currentArea.y;
|
|
||||||
|
|
||||||
const childTables = getTablesInArea(
|
|
||||||
change.id,
|
|
||||||
tables
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update child table positions in storage
|
|
||||||
if (childTables.length > 0) {
|
|
||||||
updateTablesState((currentTables) =>
|
|
||||||
currentTables.map((table) => {
|
|
||||||
if (table.parentAreaId === change.id) {
|
|
||||||
return {
|
|
||||||
id: table.id,
|
|
||||||
x: table.x + deltaX,
|
|
||||||
y: table.y + deltaY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return table;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -923,20 +1024,9 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle area removal (child tables parentAreaId already cleared above)
|
||||||
areaRemoveChanges.forEach((change) => {
|
areaRemoveChanges.forEach((change) => {
|
||||||
updateTablesState((currentTables) =>
|
|
||||||
currentTables.map((table) => {
|
|
||||||
if (table.parentAreaId === change.id) {
|
|
||||||
return {
|
|
||||||
...table,
|
|
||||||
parentAreaId: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return table;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
removeArea(change.id);
|
removeArea(change.id);
|
||||||
|
|
||||||
delete areasUpdates[change.id];
|
delete areasUpdates[change.id];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const DependencyEdge: React.FC<EdgeProps<DependencyEdgeType>> = ({
|
|||||||
const { openDependencyFromSidebar, selectSidebarSection } = useLayout();
|
const { openDependencyFromSidebar, selectSidebarSection } = useLayout();
|
||||||
|
|
||||||
const openDependencyInEditor = useCallback(() => {
|
const openDependencyInEditor = useCallback(() => {
|
||||||
selectSidebarSection('dependencies');
|
selectSidebarSection('refs');
|
||||||
openDependencyFromSidebar(id);
|
openDependencyFromSidebar(id);
|
||||||
}, [id, openDependencyFromSidebar, selectSidebarSection]);
|
}, [id, openDependencyFromSidebar, selectSidebarSection]);
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> =
|
|||||||
const relationship = data?.relationship;
|
const relationship = data?.relationship;
|
||||||
|
|
||||||
const openRelationshipInEditor = useCallback(() => {
|
const openRelationshipInEditor = useCallback(() => {
|
||||||
selectSidebarSection('relationships');
|
selectSidebarSection('refs');
|
||||||
openRelationshipFromSidebar(id);
|
openRelationshipFromSidebar(id);
|
||||||
}, [id, openRelationshipFromSidebar, selectSidebarSection]);
|
}, [id, openRelationshipFromSidebar, selectSidebarSection]);
|
||||||
|
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/sidebar/sidebar';
|
} from '@/components/sidebar/sidebar';
|
||||||
import {
|
import {
|
||||||
Twitter,
|
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Group,
|
Group,
|
||||||
FileType,
|
FileType,
|
||||||
Plus,
|
Plus,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
|
CodeXml,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { SquareStack, Table, Workflow } from 'lucide-react';
|
import { Table, Workflow } from 'lucide-react';
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DiscordLogoIcon } from '@radix-ui/react-icons';
|
import { DiscordLogoIcon, TwitterLogoIcon } from '@radix-ui/react-icons';
|
||||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||||
import ChartDBLogo from '@/assets/logo-light.png';
|
import ChartDBLogo from '@/assets/logo-light.png';
|
||||||
import ChartDBDarkLogo from '@/assets/logo-dark.png';
|
import ChartDBDarkLogo from '@/assets/logo-dark.png';
|
||||||
@@ -47,13 +47,13 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
const { isMd: isDesktop } = useBreakpoint('md');
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
const { dependencies, databaseType } = useChartDB();
|
const { databaseType } = useChartDB();
|
||||||
const { openCreateDiagramDialog, openOpenDiagramDialog } = useDialog();
|
const { openCreateDiagramDialog, openOpenDiagramDialog } = useDialog();
|
||||||
|
|
||||||
const diagramItems: SidebarItem[] = useMemo(
|
const diagramItems: SidebarItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
title: t('menu.file.new'),
|
title: t('editor_sidebar.new_diagram'),
|
||||||
icon: Plus,
|
icon: Plus,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
openCreateDiagramDialog();
|
openCreateDiagramDialog();
|
||||||
@@ -61,7 +61,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
active: false,
|
active: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('menu.file.open'),
|
title: t('editor_sidebar.browse'),
|
||||||
icon: FolderOpen,
|
icon: FolderOpen,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
openOpenDiagramDialog();
|
openOpenDiagramDialog();
|
||||||
@@ -75,7 +75,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
const baseItems: SidebarItem[] = useMemo(
|
const baseItems: SidebarItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
title: t('side_panel.tables_section.tables'),
|
title: t('editor_sidebar.tables'),
|
||||||
icon: Table,
|
icon: Table,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
showSidePanel();
|
showSidePanel();
|
||||||
@@ -84,16 +84,25 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
active: selectedSidebarSection === 'tables',
|
active: selectedSidebarSection === 'tables',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('side_panel.relationships_section.relationships'),
|
title: 'DBML',
|
||||||
|
icon: CodeXml,
|
||||||
|
onClick: () => {
|
||||||
|
showSidePanel();
|
||||||
|
selectSidebarSection('dbml');
|
||||||
|
},
|
||||||
|
active: selectedSidebarSection === 'dbml',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('editor_sidebar.refs'),
|
||||||
icon: Workflow,
|
icon: Workflow,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
showSidePanel();
|
showSidePanel();
|
||||||
selectSidebarSection('relationships');
|
selectSidebarSection('refs');
|
||||||
},
|
},
|
||||||
active: selectedSidebarSection === 'relationships',
|
active: selectedSidebarSection === 'refs',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('side_panel.areas_section.areas'),
|
title: t('editor_sidebar.areas'),
|
||||||
icon: Group,
|
icon: Group,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
showSidePanel();
|
showSidePanel();
|
||||||
@@ -101,27 +110,10 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
},
|
},
|
||||||
active: selectedSidebarSection === 'areas',
|
active: selectedSidebarSection === 'areas',
|
||||||
},
|
},
|
||||||
...(dependencies && dependencies.length > 0
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
title: t(
|
|
||||||
'side_panel.dependencies_section.dependencies'
|
|
||||||
),
|
|
||||||
icon: SquareStack,
|
|
||||||
onClick: () => {
|
|
||||||
showSidePanel();
|
|
||||||
selectSidebarSection('dependencies');
|
|
||||||
},
|
|
||||||
active: selectedSidebarSection === 'dependencies',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(databaseType === DatabaseType.POSTGRESQL
|
...(databaseType === DatabaseType.POSTGRESQL
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
title: t(
|
title: t('editor_sidebar.custom_types'),
|
||||||
'side_panel.custom_types_section.custom_types'
|
|
||||||
),
|
|
||||||
icon: FileType,
|
icon: FileType,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
showSidePanel();
|
showSidePanel();
|
||||||
@@ -137,7 +129,6 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
selectedSidebarSection,
|
selectedSidebarSection,
|
||||||
t,
|
t,
|
||||||
showSidePanel,
|
showSidePanel,
|
||||||
dependencies,
|
|
||||||
databaseType,
|
databaseType,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -153,7 +144,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Twitter',
|
title: 'Twitter',
|
||||||
icon: Twitter,
|
icon: TwitterLogoIcon,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
window.open(
|
window.open(
|
||||||
'https://x.com/intent/follow?screen_name=jonathanfishner',
|
'https://x.com/intent/follow?screen_name=jonathanfishner',
|
||||||
@@ -162,7 +153,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
active: false,
|
active: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Documentation',
|
title: 'Docs',
|
||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
onClick: () => window.open('https://docs.chartdb.io', '_blank'),
|
onClick: () => window.open('https://docs.chartdb.io', '_blank'),
|
||||||
active: false,
|
active: false,
|
||||||
@@ -174,7 +165,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
return (
|
return (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
side="left"
|
side="left"
|
||||||
collapsible="icon"
|
collapsible="icon-extended"
|
||||||
variant="sidebar"
|
variant="sidebar"
|
||||||
className="relative h-full"
|
className="relative h-full"
|
||||||
>
|
>
|
||||||
@@ -205,14 +196,21 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
{diagramItems.map((item) => (
|
{diagramItems.map((item) => (
|
||||||
<SidebarMenuItem key={item.title}>
|
<SidebarMenuItem key={item.title}>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
className="hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
|
className="justify-center space-y-0.5 !px-0 hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
|
||||||
isActive={item.active}
|
isActive={item.active}
|
||||||
asChild
|
asChild
|
||||||
tooltip={item.title}
|
|
||||||
>
|
>
|
||||||
<button onClick={item.onClick}>
|
<button onClick={item.onClick}>
|
||||||
<item.icon />
|
<item.icon />
|
||||||
<span>{item.title}</span>
|
<span>
|
||||||
|
{item.title
|
||||||
|
.split(' ')
|
||||||
|
.map((word, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
{word}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -223,14 +221,21 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
{baseItems.map((item) => (
|
{baseItems.map((item) => (
|
||||||
<SidebarMenuItem key={item.title}>
|
<SidebarMenuItem key={item.title}>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
className="hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
|
className="justify-center space-y-0.5 !px-0 hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
|
||||||
isActive={item.active}
|
isActive={item.active}
|
||||||
asChild
|
asChild
|
||||||
tooltip={item.title}
|
|
||||||
>
|
>
|
||||||
<button onClick={item.onClick}>
|
<button onClick={item.onClick}>
|
||||||
<item.icon />
|
<item.icon />
|
||||||
<span>{item.title}</span>
|
<span>
|
||||||
|
{item.title
|
||||||
|
.split(' ')
|
||||||
|
.map((word, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
{word}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -250,10 +255,9 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
className="hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
|
className="justify-center space-y-0.5 !px-0 hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
|
||||||
isActive={item.active}
|
isActive={item.active}
|
||||||
asChild
|
asChild
|
||||||
tooltip={item.title}
|
|
||||||
>
|
>
|
||||||
<button onClick={item.onClick}>
|
<button onClick={item.onClick}>
|
||||||
<item.icon />
|
<item.icon />
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
import { Badge } from '@/components/badge/badge';
|
import { Badge } from '@/components/badge/badge';
|
||||||
import { checkIfCustomTypeUsed } from '../utils';
|
import { checkIfCustomTypeUsed } from '../utils';
|
||||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||||
|
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||||
|
|
||||||
export interface CustomTypeListItemHeaderProps {
|
export interface CustomTypeListItemHeaderProps {
|
||||||
customType: DBCustomType;
|
customType: DBCustomType;
|
||||||
@@ -49,6 +50,7 @@ export const CustomTypeListItemHeader: React.FC<
|
|||||||
highlightedCustomType,
|
highlightedCustomType,
|
||||||
highlightCustomTypeId,
|
highlightCustomTypeId,
|
||||||
tables,
|
tables,
|
||||||
|
databaseType,
|
||||||
} = useChartDB();
|
} = useChartDB();
|
||||||
const { schemasDisplayed } = useDiagramFilter();
|
const { schemasDisplayed } = useDiagramFilter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -163,9 +165,9 @@ export const CustomTypeListItemHeader: React.FC<
|
|||||||
|
|
||||||
const schemaToDisplay = useMemo(() => {
|
const schemaToDisplay = useMemo(() => {
|
||||||
if (schemasDisplayed.length > 1) {
|
if (schemasDisplayed.length > 1) {
|
||||||
return customType.schema;
|
return customType.schema ?? defaultSchemas[databaseType];
|
||||||
}
|
}
|
||||||
}, [customType.schema, schemasDisplayed.length]);
|
}, [customType.schema, schemasDisplayed.length, databaseType]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
|
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { TableDBML } from './table-dbml/table-dbml';
|
||||||
|
|
||||||
|
export interface DBMLSectionProps {}
|
||||||
|
|
||||||
|
export const DBMLSection: React.FC<DBMLSectionProps> = () => {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="flex flex-1 flex-col overflow-hidden px-2"
|
||||||
|
data-vaul-no-drag
|
||||||
|
>
|
||||||
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<TableDBML />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,7 +5,6 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
|
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
|
||||||
@@ -36,9 +35,7 @@ import type * as monaco from 'monaco-editor';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
||||||
|
|
||||||
export interface TableDBMLProps {
|
export interface TableDBMLProps {}
|
||||||
filteredTables: DBTable[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const getEditorTheme = (theme: EffectiveTheme) => {
|
const getEditorTheme = (theme: EffectiveTheme) => {
|
||||||
return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
|
return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import React, { useMemo } from 'react';
|
|
||||||
import { Button } from '@/components/button/button';
|
|
||||||
import { ListCollapse } from 'lucide-react';
|
|
||||||
import { Input } from '@/components/input/input';
|
|
||||||
import { DependencyList } from './dependency-list/dependency-list';
|
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
|
||||||
import { EmptyState } from '@/components/empty-state/empty-state';
|
|
||||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/tooltip/tooltip';
|
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
|
||||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
|
||||||
import { filterDependency } from '@/lib/domain/diagram-filter/filter';
|
|
||||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
|
||||||
|
|
||||||
export interface DependenciesSectionProps {}
|
|
||||||
|
|
||||||
export const DependenciesSection: React.FC<DependenciesSectionProps> = () => {
|
|
||||||
const { dependencies, getTable, databaseType } = useChartDB();
|
|
||||||
const { filter } = useDiagramFilter();
|
|
||||||
const [filterText, setFilterText] = React.useState('');
|
|
||||||
const { closeAllDependenciesInSidebar } = useLayout();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const filteredDependencies = useMemo(() => {
|
|
||||||
const filterName: (dependency: DBDependency) => boolean = (
|
|
||||||
dependency
|
|
||||||
) => {
|
|
||||||
if (!filterText?.trim?.()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableName = getTable(dependency.tableId)?.name ?? '';
|
|
||||||
const dependentTableName =
|
|
||||||
getTable(dependency.dependentTableId)?.name ?? '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
tableName.toLowerCase().includes(filterText.toLowerCase()) ||
|
|
||||||
dependentTableName
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(filterText.toLowerCase())
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterDependencies: (dependency: DBDependency) => boolean = (
|
|
||||||
dependency
|
|
||||||
) =>
|
|
||||||
filterDependency({
|
|
||||||
tableA: {
|
|
||||||
id: dependency.tableId,
|
|
||||||
schema: dependency.schema,
|
|
||||||
},
|
|
||||||
tableB: {
|
|
||||||
id: dependency.dependentTableId,
|
|
||||||
schema: dependency.dependentSchema,
|
|
||||||
},
|
|
||||||
filter,
|
|
||||||
options: {
|
|
||||||
defaultSchema: defaultSchemas[databaseType],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return dependencies
|
|
||||||
.filter(filterDependencies)
|
|
||||||
.filter(filterName)
|
|
||||||
.sort((a, b) => {
|
|
||||||
const dependentTableA = getTable(a.dependentTableId);
|
|
||||||
const tableA = getTable(a.tableId);
|
|
||||||
const dependentTableB = getTable(b.dependentTableId);
|
|
||||||
const tableB = getTable(b.tableId);
|
|
||||||
return `${dependentTableA?.name}${tableA?.name}`.localeCompare(
|
|
||||||
`${dependentTableB?.name}${tableB?.name}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [dependencies, filterText, filter, getTable, databaseType]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="flex flex-1 flex-col overflow-hidden px-2">
|
|
||||||
<div className="flex items-center justify-between gap-4 py-1">
|
|
||||||
<div>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="size-8 p-0"
|
|
||||||
onClick={closeAllDependenciesInSidebar}
|
|
||||||
>
|
|
||||||
<ListCollapse className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
{t('side_panel.dependencies_section.collapse')}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder={t(
|
|
||||||
'side_panel.dependencies_section.filter'
|
|
||||||
)}
|
|
||||||
className="h-8 w-full focus-visible:ring-0"
|
|
||||||
value={filterText}
|
|
||||||
onChange={(e) => setFilterText(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
|
||||||
<ScrollArea className="h-full">
|
|
||||||
{dependencies.length === 0 ? (
|
|
||||||
<EmptyState
|
|
||||||
title={t(
|
|
||||||
'side_panel.dependencies_section.empty_state.title'
|
|
||||||
)}
|
|
||||||
description={t(
|
|
||||||
'side_panel.dependencies_section.empty_state.description'
|
|
||||||
)}
|
|
||||||
className="mt-20"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DependencyList dependencies={filteredDependencies} />
|
|
||||||
)}
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import { Accordion } from '@/components/accordion/accordion';
|
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
|
||||||
import { DependencyListItem } from './dependency-list-item/dependency-list-item';
|
|
||||||
|
|
||||||
export interface DependencyListProps {
|
|
||||||
dependencies: DBDependency[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DependencyList: React.FC<DependencyListProps> = ({
|
|
||||||
dependencies,
|
|
||||||
}) => {
|
|
||||||
const { openDependencyFromSidebar, openedDependencyInSidebar } =
|
|
||||||
useLayout();
|
|
||||||
const lastOpenedDependency = React.useRef<string | null>(null);
|
|
||||||
|
|
||||||
const refs = dependencies.reduce(
|
|
||||||
(acc, dependency) => {
|
|
||||||
acc[dependency.id] = React.createRef();
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, React.RefObject<HTMLDivElement>>
|
|
||||||
);
|
|
||||||
|
|
||||||
const scrollToDependency = useCallback(
|
|
||||||
(id: string) =>
|
|
||||||
refs[id]?.current?.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start',
|
|
||||||
}),
|
|
||||||
[refs]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleScrollToDependency = useCallback(() => {
|
|
||||||
if (
|
|
||||||
openedDependencyInSidebar &&
|
|
||||||
lastOpenedDependency.current !== openedDependencyInSidebar
|
|
||||||
) {
|
|
||||||
lastOpenedDependency.current = openedDependencyInSidebar;
|
|
||||||
scrollToDependency(openedDependencyInSidebar);
|
|
||||||
}
|
|
||||||
}, [scrollToDependency, openedDependencyInSidebar]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Accordion
|
|
||||||
type="single"
|
|
||||||
collapsible
|
|
||||||
className="flex w-full flex-col gap-1"
|
|
||||||
value={openedDependencyInSidebar}
|
|
||||||
onValueChange={openDependencyFromSidebar}
|
|
||||||
onAnimationEnd={handleScrollToDependency}
|
|
||||||
>
|
|
||||||
{dependencies.map((dependency) => (
|
|
||||||
<DependencyListItem
|
|
||||||
key={dependency.id}
|
|
||||||
dependency={dependency}
|
|
||||||
ref={refs[dependency.id]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -41,7 +41,7 @@ export const DependencyListItemContent: React.FC<
|
|||||||
<FileMinus2 className="size-4 text-subtitle" />
|
<FileMinus2 className="size-4 text-subtitle" />
|
||||||
<div className="font-bold text-subtitle">
|
<div className="font-bold text-subtitle">
|
||||||
{t(
|
{t(
|
||||||
'side_panel.dependencies_section.dependency.dependent_table'
|
'side_panel.refs_section.dependency.dependent_table'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,9 +60,7 @@ export const DependencyListItemContent: React.FC<
|
|||||||
<div className="flex flex-row items-center gap-1">
|
<div className="flex flex-row items-center gap-1">
|
||||||
<FileOutput className="size-4 text-subtitle" />
|
<FileOutput className="size-4 text-subtitle" />
|
||||||
<div className="font-bold text-subtitle">
|
<div className="font-bold text-subtitle">
|
||||||
{t(
|
{t('side_panel.refs_section.dependency.table')}
|
||||||
'side_panel.dependencies_section.dependency.table'
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -85,7 +83,7 @@ export const DependencyListItemContent: React.FC<
|
|||||||
<Trash2 className="mr-1 size-3.5 text-red-700" />
|
<Trash2 className="mr-1 size-3.5 text-red-700" />
|
||||||
<div className="text-red-700">
|
<div className="text-red-700">
|
||||||
{t(
|
{t(
|
||||||
'side_panel.dependencies_section.dependency.delete_dependency'
|
'side_panel.refs_section.dependency.delete_dependency'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -105,7 +105,7 @@ export const DependencyListItemHeader: React.FC<
|
|||||||
<DropdownMenuContent className="w-40">
|
<DropdownMenuContent className="w-40">
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
{t(
|
{t(
|
||||||
'side_panel.dependencies_section.dependency.dependency_actions.title'
|
'side_panel.refs_section.dependency.dependency_actions.title'
|
||||||
)}
|
)}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -115,7 +115,7 @@ export const DependencyListItemHeader: React.FC<
|
|||||||
className="flex justify-between !text-red-700"
|
className="flex justify-between !text-red-700"
|
||||||
>
|
>
|
||||||
{t(
|
{t(
|
||||||
'side_panel.dependencies_section.dependency.dependency_actions.delete_dependency'
|
'side_panel.refs_section.dependency.dependency_actions.delete_dependency'
|
||||||
)}
|
)}
|
||||||
<Trash2 className="size-3.5 text-red-700" />
|
<Trash2 className="size-3.5 text-red-700" />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -127,11 +127,11 @@ export const DependencyListItemHeader: React.FC<
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex h-11 flex-1 items-center justify-between overflow-hidden">
|
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
|
||||||
<div className="flex min-w-0 flex-1">
|
<div className="flex min-w-0 flex-1">
|
||||||
<div className="truncate">{dependencyName}</div>
|
<div className="truncate">{dependencyName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row-reverse">
|
<div className="flex flex-row-reverse items-center">
|
||||||
<div>{renderDropDownMenu()}</div>
|
<div>{renderDropDownMenu()}</div>
|
||||||
<div className="flex flex-row-reverse md:hidden md:group-hover:flex">
|
<div className="flex flex-row-reverse md:hidden md:group-hover:flex">
|
||||||
<ListItemHeaderButton onClick={focusOnDependency}>
|
<ListItemHeaderButton onClick={focusOnDependency}>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { Accordion } from '@/components/accordion/accordion';
|
||||||
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
|
import type { Ref } from '../refs-section';
|
||||||
|
import { RelationshipListItem } from './relationship-list-item/relationship-list-item';
|
||||||
|
import { DependencyListItem } from './dependency-list-item/dependency-list-item';
|
||||||
|
import { Label } from '@/components/label/label';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export interface RefsListProps {
|
||||||
|
refs: Ref[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RefsList: React.FC<RefsListProps> = ({ refs }) => {
|
||||||
|
const { openRefFromSidebar, openedRefInSidebar } = useLayout();
|
||||||
|
const lastOpenedRef = React.useRef<string | null>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const itemRefs = refs.reduce(
|
||||||
|
(acc, ref) => {
|
||||||
|
acc[ref.id] = React.createRef();
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, React.RefObject<HTMLDivElement>>
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollToRef = useCallback(
|
||||||
|
(id: string) =>
|
||||||
|
itemRefs[id]?.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
}),
|
||||||
|
[itemRefs]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleScrollToRef = useCallback(() => {
|
||||||
|
if (
|
||||||
|
openedRefInSidebar &&
|
||||||
|
lastOpenedRef.current !== openedRefInSidebar
|
||||||
|
) {
|
||||||
|
lastOpenedRef.current = openedRefInSidebar;
|
||||||
|
scrollToRef(openedRefInSidebar);
|
||||||
|
}
|
||||||
|
}, [scrollToRef, openedRefInSidebar]);
|
||||||
|
|
||||||
|
const numberOfRelationships = useMemo(
|
||||||
|
() => refs.filter((ref) => ref.type === 'relationship').length,
|
||||||
|
[refs]
|
||||||
|
);
|
||||||
|
|
||||||
|
const relationshipsTitle = React.useMemo(
|
||||||
|
() =>
|
||||||
|
`${numberOfRelationships} ${t(
|
||||||
|
'side_panel.refs_section.relationships'
|
||||||
|
)}`,
|
||||||
|
[numberOfRelationships, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const numberOfDependencies = useMemo(
|
||||||
|
() => refs.filter((ref) => ref.type === 'dependency').length,
|
||||||
|
[refs]
|
||||||
|
);
|
||||||
|
|
||||||
|
const dependenciesTitle = React.useMemo(
|
||||||
|
() =>
|
||||||
|
`${numberOfDependencies} ${t(
|
||||||
|
'side_panel.refs_section.dependencies'
|
||||||
|
)}`,
|
||||||
|
[numberOfDependencies, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
type="single"
|
||||||
|
collapsible
|
||||||
|
className="flex w-full flex-col gap-1"
|
||||||
|
value={openedRefInSidebar}
|
||||||
|
onValueChange={openRefFromSidebar}
|
||||||
|
onAnimationEnd={handleScrollToRef}
|
||||||
|
>
|
||||||
|
{numberOfRelationships > 0 ? (
|
||||||
|
<Label className="mt-2 px-2 text-xs font-medium text-muted-foreground">
|
||||||
|
{relationshipsTitle}
|
||||||
|
</Label>
|
||||||
|
) : null}
|
||||||
|
{refs
|
||||||
|
.filter((ref) => ref.type === 'relationship')
|
||||||
|
.map((ref) => (
|
||||||
|
<RelationshipListItem
|
||||||
|
key={ref.id}
|
||||||
|
relationship={ref.relationship!}
|
||||||
|
ref={itemRefs[ref.id]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{numberOfDependencies > 0 ? (
|
||||||
|
<Label className="mt-2 px-2 text-xs font-medium text-muted-foreground">
|
||||||
|
{dependenciesTitle}
|
||||||
|
</Label>
|
||||||
|
) : null}
|
||||||
|
{refs
|
||||||
|
.filter((ref) => ref.type === 'dependency')
|
||||||
|
.map((ref) => (
|
||||||
|
<DependencyListItem
|
||||||
|
key={ref.id}
|
||||||
|
dependency={ref.dependency!}
|
||||||
|
ref={itemRefs[ref.id]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -91,7 +91,7 @@ export const RelationshipListItemContent: React.FC<
|
|||||||
<FileOutput className="size-4 text-subtitle" />
|
<FileOutput className="size-4 text-subtitle" />
|
||||||
<div className="font-bold text-subtitle">
|
<div className="font-bold text-subtitle">
|
||||||
{t(
|
{t(
|
||||||
'side_panel.relationships_section.relationship.primary'
|
'side_panel.refs_section.relationship.primary'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,7 +117,7 @@ export const RelationshipListItemContent: React.FC<
|
|||||||
<FileMinus2 className="size-4 text-subtitle" />
|
<FileMinus2 className="size-4 text-subtitle" />
|
||||||
<div className="font-bold text-subtitle">
|
<div className="font-bold text-subtitle">
|
||||||
{t(
|
{t(
|
||||||
'side_panel.relationships_section.relationship.foreign'
|
'side_panel.refs_section.relationship.foreign'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,7 +144,7 @@ export const RelationshipListItemContent: React.FC<
|
|||||||
<ChevronsLeftRightEllipsis className="size-4 text-subtitle" />
|
<ChevronsLeftRightEllipsis className="size-4 text-subtitle" />
|
||||||
<div className="font-bold text-subtitle">
|
<div className="font-bold text-subtitle">
|
||||||
{t(
|
{t(
|
||||||
'side_panel.relationships_section.relationship.cardinality'
|
'side_panel.refs_section.relationship.cardinality'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,7 +184,7 @@ export const RelationshipListItemContent: React.FC<
|
|||||||
<Trash2 className="mr-1 size-3.5 text-red-700" />
|
<Trash2 className="mr-1 size-3.5 text-red-700" />
|
||||||
<div className="text-red-700">
|
<div className="text-red-700">
|
||||||
{t(
|
{t(
|
||||||
'side_panel.relationships_section.relationship.delete_relationship'
|
'side_panel.refs_section.relationship.delete_relationship'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -133,7 +133,7 @@ export const RelationshipListItemHeader: React.FC<
|
|||||||
<DropdownMenuContent className="w-40">
|
<DropdownMenuContent className="w-40">
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
{t(
|
{t(
|
||||||
'side_panel.relationships_section.relationship.relationship_actions.title'
|
'side_panel.refs_section.relationship.relationship_actions.title'
|
||||||
)}
|
)}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -143,7 +143,7 @@ export const RelationshipListItemHeader: React.FC<
|
|||||||
className="flex justify-between !text-red-700"
|
className="flex justify-between !text-red-700"
|
||||||
>
|
>
|
||||||
{t(
|
{t(
|
||||||
'side_panel.relationships_section.relationship.relationship_actions.delete_relationship'
|
'side_panel.refs_section.relationship.relationship_actions.delete_relationship'
|
||||||
)}
|
)}
|
||||||
<Trash2 className="size-3.5 text-red-700" />
|
<Trash2 className="size-3.5 text-red-700" />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -155,7 +155,7 @@ export const RelationshipListItemHeader: React.FC<
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex h-11 flex-1 items-center justify-between overflow-hidden">
|
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
|
||||||
<div className="flex min-w-0 flex-1">
|
<div className="flex min-w-0 flex-1">
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
<Input
|
<Input
|
||||||
@@ -172,7 +172,7 @@ export const RelationshipListItemHeader: React.FC<
|
|||||||
<div className="truncate">{relationship.name}</div>
|
<div className="truncate">{relationship.name}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row-reverse">
|
<div className="flex flex-row-reverse items-center">
|
||||||
{!editMode ? (
|
{!editMode ? (
|
||||||
<>
|
<>
|
||||||
<div>{renderDropDownMenu()}</div>
|
<div>{renderDropDownMenu()}</div>
|
||||||
225
src/pages/editor-page/side-panel/refs-section/refs-section.tsx
Normal file
225
src/pages/editor-page/side-panel/refs-section/refs-section.tsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import { ListCollapse, Workflow } from 'lucide-react';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
import { RefsList } from './refs-list/refs-list';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
|
import { EmptyState } from '@/components/empty-state/empty-state';
|
||||||
|
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/tooltip/tooltip';
|
||||||
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||||
|
import {
|
||||||
|
filterRelationship,
|
||||||
|
filterDependency,
|
||||||
|
} from '@/lib/domain/diagram-filter/filter';
|
||||||
|
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||||
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
|
|
||||||
|
export type RefType = 'relationship' | 'dependency';
|
||||||
|
|
||||||
|
export interface Ref {
|
||||||
|
id: string;
|
||||||
|
type: RefType;
|
||||||
|
relationship?: DBRelationship;
|
||||||
|
dependency?: DBDependency;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefsSectionProps {}
|
||||||
|
|
||||||
|
export const RefsSection: React.FC<RefsSectionProps> = () => {
|
||||||
|
const { relationships, dependencies, databaseType, getTable } =
|
||||||
|
useChartDB();
|
||||||
|
const { filter } = useDiagramFilter();
|
||||||
|
const [filterText, setFilterText] = React.useState('');
|
||||||
|
const { closeAllRefsInSidebar } = useLayout();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { openCreateRelationshipDialog } = useDialog();
|
||||||
|
const { showDBViews } = useLocalConfig();
|
||||||
|
const filterInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const allRefs = useMemo((): Ref[] => {
|
||||||
|
const relationshipRefs: Ref[] = relationships.map(
|
||||||
|
(rel) =>
|
||||||
|
({
|
||||||
|
id: rel.id,
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: rel,
|
||||||
|
}) satisfies Ref
|
||||||
|
);
|
||||||
|
|
||||||
|
const dependencyRefs: Ref[] = showDBViews
|
||||||
|
? dependencies.map(
|
||||||
|
(dep) =>
|
||||||
|
({
|
||||||
|
id: dep.id,
|
||||||
|
type: 'dependency',
|
||||||
|
dependency: dep,
|
||||||
|
}) satisfies Ref
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [...relationshipRefs, ...dependencyRefs];
|
||||||
|
}, [relationships, dependencies, showDBViews]);
|
||||||
|
|
||||||
|
const filteredRefs = useMemo(() => {
|
||||||
|
const filterName = (ref: Ref): boolean => {
|
||||||
|
if (!filterText?.trim?.()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchText = filterText.toLowerCase();
|
||||||
|
|
||||||
|
if (ref.type === 'relationship') {
|
||||||
|
const relationship = ref.relationship!;
|
||||||
|
return relationship.name.toLowerCase().includes(searchText);
|
||||||
|
} else {
|
||||||
|
const dependency = ref.dependency!;
|
||||||
|
const tableName = getTable(dependency.tableId)?.name ?? '';
|
||||||
|
const dependentTableName =
|
||||||
|
getTable(dependency.dependentTableId)?.name ?? '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
tableName.toLowerCase().includes(searchText) ||
|
||||||
|
dependentTableName.toLowerCase().includes(searchText)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterByDiagram = (ref: Ref): boolean => {
|
||||||
|
if (ref.type === 'relationship') {
|
||||||
|
const relationship = ref.relationship!;
|
||||||
|
return filterRelationship({
|
||||||
|
tableA: {
|
||||||
|
id: relationship.sourceTableId,
|
||||||
|
schema: relationship.sourceSchema,
|
||||||
|
},
|
||||||
|
tableB: {
|
||||||
|
id: relationship.targetTableId,
|
||||||
|
schema: relationship.targetSchema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const dependency = ref.dependency!;
|
||||||
|
return filterDependency({
|
||||||
|
tableA: {
|
||||||
|
id: dependency.tableId,
|
||||||
|
schema: dependency.schema,
|
||||||
|
},
|
||||||
|
tableB: {
|
||||||
|
id: dependency.dependentTableId,
|
||||||
|
schema: dependency.dependentSchema,
|
||||||
|
},
|
||||||
|
filter,
|
||||||
|
options: {
|
||||||
|
defaultSchema: defaultSchemas[databaseType],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return allRefs
|
||||||
|
.filter(filterByDiagram)
|
||||||
|
.filter(filterName)
|
||||||
|
.sort((a, b) => {
|
||||||
|
// Sort relationships before dependencies
|
||||||
|
if (a.type !== b.type) {
|
||||||
|
return a.type === 'relationship' ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Within same type, sort by name
|
||||||
|
if (a.type === 'relationship') {
|
||||||
|
const relA = a.relationship!;
|
||||||
|
const relB = b.relationship!;
|
||||||
|
return relA.name.localeCompare(relB.name);
|
||||||
|
} else {
|
||||||
|
const depA = a.dependency!;
|
||||||
|
const depB = b.dependency!;
|
||||||
|
const tableA = getTable(depA.dependentTableId);
|
||||||
|
const tableAName = getTable(depA.tableId);
|
||||||
|
const tableB = getTable(depB.dependentTableId);
|
||||||
|
const tableBName = getTable(depB.tableId);
|
||||||
|
return `${tableA?.name}${tableAName?.name}`.localeCompare(
|
||||||
|
`${tableB?.name}${tableBName?.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [allRefs, filterText, filter, databaseType, getTable]);
|
||||||
|
|
||||||
|
const handleCreateRelationship = useCallback(async () => {
|
||||||
|
setFilterText('');
|
||||||
|
openCreateRelationshipDialog();
|
||||||
|
}, [openCreateRelationshipDialog, setFilterText]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="flex flex-1 flex-col overflow-hidden px-2">
|
||||||
|
<div className="flex items-center justify-between gap-4 py-1">
|
||||||
|
<div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="size-8 p-0"
|
||||||
|
onClick={closeAllRefsInSidebar}
|
||||||
|
>
|
||||||
|
<ListCollapse className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{t('side_panel.refs_section.collapse')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
ref={filterInputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder={t('side_panel.refs_section.filter')}
|
||||||
|
className="h-8 w-full focus-visible:ring-0"
|
||||||
|
value={filterText}
|
||||||
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="h-8 p-2 text-xs"
|
||||||
|
onClick={handleCreateRelationship}
|
||||||
|
>
|
||||||
|
<Workflow className="h-4" />
|
||||||
|
{t('side_panel.refs_section.add_relationship')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<ScrollArea className="h-full">
|
||||||
|
{allRefs.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
title={t(
|
||||||
|
'side_panel.refs_section.empty_state.title'
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
'side_panel.refs_section.empty_state.description'
|
||||||
|
)}
|
||||||
|
className="mt-20"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RefsList refs={filteredRefs} />
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import { Accordion } from '@/components/accordion/accordion';
|
|
||||||
import { RelationshipListItem } from './relationship-list-item/relationship-list-item';
|
|
||||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
|
||||||
|
|
||||||
export interface RelationshipListProps {
|
|
||||||
relationships: DBRelationship[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RelationshipList: React.FC<RelationshipListProps> = ({
|
|
||||||
relationships,
|
|
||||||
}) => {
|
|
||||||
const { openRelationshipFromSidebar, openedRelationshipInSidebar } =
|
|
||||||
useLayout();
|
|
||||||
const lastOpenedRelationship = React.useRef<string | null>(null);
|
|
||||||
|
|
||||||
const refs = relationships.reduce(
|
|
||||||
(acc, relationship) => {
|
|
||||||
acc[relationship.id] = React.createRef();
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, React.RefObject<HTMLDivElement>>
|
|
||||||
);
|
|
||||||
|
|
||||||
const scrollToRelationship = useCallback(
|
|
||||||
(id: string) =>
|
|
||||||
refs[id]?.current?.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start',
|
|
||||||
}),
|
|
||||||
[refs]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleScrollToRelationship = useCallback(() => {
|
|
||||||
if (
|
|
||||||
openedRelationshipInSidebar &&
|
|
||||||
lastOpenedRelationship.current !== openedRelationshipInSidebar
|
|
||||||
) {
|
|
||||||
lastOpenedRelationship.current = openedRelationshipInSidebar;
|
|
||||||
scrollToRelationship(openedRelationshipInSidebar);
|
|
||||||
}
|
|
||||||
}, [scrollToRelationship, openedRelationshipInSidebar]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Accordion
|
|
||||||
type="single"
|
|
||||||
collapsible
|
|
||||||
className="flex w-full flex-col gap-1"
|
|
||||||
value={openedRelationshipInSidebar}
|
|
||||||
onValueChange={openRelationshipFromSidebar}
|
|
||||||
onAnimationEnd={handleScrollToRelationship}
|
|
||||||
>
|
|
||||||
{relationships.map((relationship) => (
|
|
||||||
<RelationshipListItem
|
|
||||||
key={relationship.id}
|
|
||||||
relationship={relationship}
|
|
||||||
ref={refs[relationship.id]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
|
||||||
import { Button } from '@/components/button/button';
|
|
||||||
import { ListCollapse, Workflow } from 'lucide-react';
|
|
||||||
import { Input } from '@/components/input/input';
|
|
||||||
import { RelationshipList } from './relationship-list/relationship-list';
|
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
|
||||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
|
||||||
import { EmptyState } from '@/components/empty-state/empty-state';
|
|
||||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/tooltip/tooltip';
|
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
|
||||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
|
||||||
import { filterRelationship } from '@/lib/domain/diagram-filter/filter';
|
|
||||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
|
||||||
|
|
||||||
export interface RelationshipsSectionProps {}
|
|
||||||
|
|
||||||
export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
|
|
||||||
const { relationships, databaseType } = useChartDB();
|
|
||||||
const { filter } = useDiagramFilter();
|
|
||||||
const [filterText, setFilterText] = React.useState('');
|
|
||||||
const { closeAllRelationshipsInSidebar } = useLayout();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { openCreateRelationshipDialog } = useDialog();
|
|
||||||
const filterInputRef = React.useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const filteredRelationships = useMemo(() => {
|
|
||||||
const filterName: (relationship: DBRelationship) => boolean = (
|
|
||||||
relationship
|
|
||||||
) =>
|
|
||||||
!filterText?.trim?.() ||
|
|
||||||
relationship.name.toLowerCase().includes(filterText.toLowerCase());
|
|
||||||
|
|
||||||
const filterRelationships: (relationship: DBRelationship) => boolean = (
|
|
||||||
relationship
|
|
||||||
) =>
|
|
||||||
filterRelationship({
|
|
||||||
tableA: {
|
|
||||||
id: relationship.sourceTableId,
|
|
||||||
schema: relationship.sourceSchema,
|
|
||||||
},
|
|
||||||
tableB: {
|
|
||||||
id: relationship.targetTableId,
|
|
||||||
schema: relationship.targetSchema,
|
|
||||||
},
|
|
||||||
filter,
|
|
||||||
options: {
|
|
||||||
defaultSchema: defaultSchemas[databaseType],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return relationships.filter(filterRelationships).filter(filterName);
|
|
||||||
}, [relationships, filterText, filter, databaseType]);
|
|
||||||
|
|
||||||
const handleCreateRelationship = useCallback(async () => {
|
|
||||||
setFilterText('');
|
|
||||||
openCreateRelationshipDialog();
|
|
||||||
}, [openCreateRelationshipDialog, setFilterText]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="flex flex-1 flex-col overflow-hidden px-2">
|
|
||||||
<div className="flex items-center justify-between gap-4 py-1">
|
|
||||||
<div>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="size-8 p-0"
|
|
||||||
onClick={closeAllRelationshipsInSidebar}
|
|
||||||
>
|
|
||||||
<ListCollapse className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
{t('side_panel.relationships_section.collapse')}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Input
|
|
||||||
ref={filterInputRef}
|
|
||||||
type="text"
|
|
||||||
placeholder={t(
|
|
||||||
'side_panel.relationships_section.filter'
|
|
||||||
)}
|
|
||||||
className="h-8 w-full focus-visible:ring-0"
|
|
||||||
value={filterText}
|
|
||||||
onChange={(e) => setFilterText(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
className="h-8 p-2 text-xs"
|
|
||||||
onClick={handleCreateRelationship}
|
|
||||||
>
|
|
||||||
<Workflow className="h-4" />
|
|
||||||
{t('side_panel.relationships_section.add_relationship')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
|
||||||
<ScrollArea className="h-full">
|
|
||||||
{relationships.length === 0 ? (
|
|
||||||
<EmptyState
|
|
||||||
title={t(
|
|
||||||
'side_panel.relationships_section.empty_state.title'
|
|
||||||
)}
|
|
||||||
description={t(
|
|
||||||
'side_panel.relationships_section.empty_state.description'
|
|
||||||
)}
|
|
||||||
className="mt-20"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<RelationshipList
|
|
||||||
relationships={filteredRelationships}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,16 +8,16 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/select/select';
|
} from '@/components/select/select';
|
||||||
import { TablesSection } from './tables-section/tables-section';
|
import { TablesSection } from './tables-section/tables-section';
|
||||||
import { RelationshipsSection } from './relationships-section/relationships-section';
|
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
import type { SidebarSection } from '@/context/layout-context/layout-context';
|
import type { SidebarSection } from '@/context/layout-context/layout-context';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { DependenciesSection } from './dependencies-section/dependencies-section';
|
|
||||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||||
import { AreasSection } from './areas-section/areas-section';
|
import { AreasSection } from './areas-section/areas-section';
|
||||||
import { CustomTypesSection } from './custom-types-section/custom-types-section';
|
import { CustomTypesSection } from './custom-types-section/custom-types-section';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
import { DBMLSection } from './dbml-section/dbml-section';
|
||||||
|
import { RefsSection } from './refs-section/refs-section';
|
||||||
|
|
||||||
export interface SidePanelProps {}
|
export interface SidePanelProps {}
|
||||||
|
|
||||||
@@ -48,15 +48,8 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
|
|||||||
<SelectItem value="tables">
|
<SelectItem value="tables">
|
||||||
{t('side_panel.tables_section.tables')}
|
{t('side_panel.tables_section.tables')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="relationships">
|
<SelectItem value="refs">
|
||||||
{t(
|
{t('side_panel.refs_section.refs')}
|
||||||
'side_panel.relationships_section.relationships'
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="dependencies">
|
|
||||||
{t(
|
|
||||||
'side_panel.dependencies_section.dependencies'
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="areas">
|
<SelectItem value="areas">
|
||||||
{t('side_panel.areas_section.areas')}
|
{t('side_panel.areas_section.areas')}
|
||||||
@@ -75,10 +68,10 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
{selectedSidebarSection === 'tables' ? (
|
{selectedSidebarSection === 'tables' ? (
|
||||||
<TablesSection />
|
<TablesSection />
|
||||||
) : selectedSidebarSection === 'relationships' ? (
|
) : selectedSidebarSection === 'dbml' ? (
|
||||||
<RelationshipsSection />
|
<DBMLSection />
|
||||||
) : selectedSidebarSection === 'dependencies' ? (
|
) : selectedSidebarSection === 'refs' ? (
|
||||||
<DependenciesSection />
|
<RefsSection />
|
||||||
) : selectedSidebarSection === 'areas' ? (
|
) : selectedSidebarSection === 'areas' ? (
|
||||||
<AreasSection />
|
<AreasSection />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { Button } from '@/components/button/button';
|
|||||||
import { Separator } from '@/components/separator/separator';
|
import { Separator } from '@/components/separator/separator';
|
||||||
import type { DBField } from '@/lib/domain/db-field';
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
import type { FieldAttributeRange } from '@/lib/data/data-types/data-types';
|
import type { FieldAttributeRange } from '@/lib/data/data-types/data-types';
|
||||||
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
|
import {
|
||||||
|
findDataTypeDataById,
|
||||||
|
supportsAutoIncrementDataType,
|
||||||
|
} from '@/lib/data/data-types/data-types';
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -83,6 +86,7 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
|||||||
scale: localField.scale,
|
scale: localField.scale,
|
||||||
unique: localField.unique,
|
unique: localField.unique,
|
||||||
default: localField.default,
|
default: localField.default,
|
||||||
|
increment: localField.increment,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
prevFieldRef.current = localField;
|
prevFieldRef.current = localField;
|
||||||
@@ -93,6 +97,11 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
|||||||
[field.type.id, databaseType]
|
[field.type.id, databaseType]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const supportsAutoIncrement = useMemo(
|
||||||
|
() => supportsAutoIncrementDataType(field.type.name),
|
||||||
|
[field.type.name]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
@@ -137,6 +146,28 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{supportsAutoIncrement ? (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label
|
||||||
|
htmlFor="increment"
|
||||||
|
className="text-subtitle"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
'side_panel.tables_section.table.field_actions.auto_increment'
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<Checkbox
|
||||||
|
checked={localField.increment ?? false}
|
||||||
|
disabled={!localField.primaryKey}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
setLocalField((current) => ({
|
||||||
|
...current,
|
||||||
|
increment: !!value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="default" className="text-subtitle">
|
<Label htmlFor="default" className="text-subtitle">
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Ellipsis, Trash2 } from 'lucide-react';
|
import { Ellipsis, Trash2 } from 'lucide-react';
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
import type { DBIndex } from '@/lib/domain/db-index';
|
import {
|
||||||
|
databaseIndexTypes,
|
||||||
|
type DBIndex,
|
||||||
|
type IndexType,
|
||||||
|
} from '@/lib/domain/db-index';
|
||||||
import type { DBField } from '@/lib/domain/db-field';
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -20,6 +24,7 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/tooltip/tooltip';
|
} from '@/components/tooltip/tooltip';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
|
||||||
export interface TableIndexProps {
|
export interface TableIndexProps {
|
||||||
index: DBIndex;
|
index: DBIndex;
|
||||||
@@ -28,6 +33,11 @@ export interface TableIndexProps {
|
|||||||
fields: DBField[];
|
fields: DBField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allIndexTypeOptions: { label: string; value: IndexType }[] = [
|
||||||
|
{ label: 'B-tree (default)', value: 'btree' },
|
||||||
|
{ label: 'Hash', value: 'hash' },
|
||||||
|
];
|
||||||
|
|
||||||
export const TableIndex: React.FC<TableIndexProps> = ({
|
export const TableIndex: React.FC<TableIndexProps> = ({
|
||||||
fields,
|
fields,
|
||||||
index,
|
index,
|
||||||
@@ -35,14 +45,51 @@ export const TableIndex: React.FC<TableIndexProps> = ({
|
|||||||
removeIndex,
|
removeIndex,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { databaseType } = useChartDB();
|
||||||
const fieldOptions = fields.map((field) => ({
|
const fieldOptions = fields.map((field) => ({
|
||||||
label: field.name,
|
label: field.name,
|
||||||
value: field.id,
|
value: field.id,
|
||||||
}));
|
}));
|
||||||
const updateIndexFields = (fieldIds: string | string[]) => {
|
const updateIndexFields = useCallback(
|
||||||
|
(fieldIds: string | string[]) => {
|
||||||
const ids = Array.isArray(fieldIds) ? fieldIds : [fieldIds];
|
const ids = Array.isArray(fieldIds) ? fieldIds : [fieldIds];
|
||||||
|
|
||||||
|
// For hash indexes, only keep the last selected field
|
||||||
|
if (index.type === 'hash' && ids.length > 0) {
|
||||||
|
updateIndex({ fieldIds: [ids[ids.length - 1]] });
|
||||||
|
} else {
|
||||||
updateIndex({ fieldIds: ids });
|
updateIndex({ fieldIds: ids });
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[index.type, updateIndex]
|
||||||
|
);
|
||||||
|
|
||||||
|
const indexTypeOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
allIndexTypeOptions.filter((option) =>
|
||||||
|
databaseIndexTypes[databaseType]?.includes(option.value)
|
||||||
|
),
|
||||||
|
[databaseType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateIndexType = useCallback(
|
||||||
|
(value: string | string[]) => {
|
||||||
|
{
|
||||||
|
const newType = value as IndexType;
|
||||||
|
// If switching to hash and multiple fields are selected, keep only the first
|
||||||
|
if (newType === 'hash' && index.fieldIds.length > 1) {
|
||||||
|
updateIndex({
|
||||||
|
type: newType,
|
||||||
|
fieldIds: [index.fieldIds[0]],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateIndex({ type: newType });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[updateIndex, index.fieldIds]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-row justify-between gap-2 p-1">
|
<div className="flex flex-1 flex-row justify-between gap-2 p-1">
|
||||||
<SelectBox
|
<SelectBox
|
||||||
@@ -135,6 +182,23 @@ export const TableIndex: React.FC<TableIndexProps> = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{indexTypeOptions.length > 0 ? (
|
||||||
|
<div className="mt-2 flex flex-col gap-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="indexType"
|
||||||
|
className="text-subtitle"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
'side_panel.tables_section.table.index_actions.index_type'
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<SelectBox
|
||||||
|
options={indexTypeOptions}
|
||||||
|
value={index.type || 'btree'}
|
||||||
|
onChange={updateIndexType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<Separator orientation="horizontal" />
|
<Separator orientation="horizontal" />
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -281,10 +281,15 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
<Separator className="" />
|
<Separator className="" />
|
||||||
<div className="flex flex-1 items-center justify-between">
|
<div className="flex flex-1 items-center justify-between">
|
||||||
|
{!table.isView ? (
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
color={color}
|
color={color}
|
||||||
onChange={(color) => updateTable(table.id, { color })}
|
onChange={(color) => updateTable(table.id, { color })}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -268,9 +268,9 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
|
|||||||
|
|
||||||
const schemaToDisplay = useMemo(() => {
|
const schemaToDisplay = useMemo(() => {
|
||||||
if (schemasDisplayed.length > 1) {
|
if (schemasDisplayed.length > 1) {
|
||||||
return table.schema;
|
return table.schema ?? defaultSchemas[databaseType];
|
||||||
}
|
}
|
||||||
}, [table.schema, schemasDisplayed.length]);
|
}, [table.schema, schemasDisplayed.length, databaseType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (table.name.trim()) {
|
if (table.name.trim()) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user