Compare commits

...

47 Commits

Author SHA1 Message Date
johnnyfish
76f9662b80 feat(open-diagram): add row menu options for open diagram dialog 2025-08-26 10:05:48 +03:00
Guy Ben-Aharon
ec3719ebce fix: merge relationship & dependency sections to ref section (#870)
* fix: merge relationship & dependency sections to ref section

* fix

* fix

* fix
2025-08-25 20:14:32 +03:00
Guy Ben-Aharon
0a5874a69b feat: support create views (#868)
* feat: support create views

* fix

* fix

* fix

* fix

* fix
2025-08-25 16:14:28 +03:00
Guy Ben-Aharon
7e0fdd1595 fix: open filter by default (#863) 2025-08-21 17:55:45 +03:00
Guy Ben-Aharon
2531a7023f fix: move dbml into sections menu (#862) 2025-08-21 14:48:47 +03:00
Guy Ben-Aharon
73daf0df21 fix: area filter logic (#861) 2025-08-20 15:21:48 +03:00
Jonathan Fishner
c77c983989 feat: add auto increment support for fields with database-specific export (#851)
* feat: add auto increment support for fields with database-specific export

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-19 11:39:26 +03:00
Jonathan Fishner
0aaa451479 fix: prevent false change detection in DBML editor by stripping public schema on import (#858)
* fix: prevent false change detection in DBML editor by stripping public schema on import

* fix(dbml): preserve self-referencing relationships and character varying lengths in DBML import/export
2025-08-18 21:39:24 +03:00
Guy Ben-Aharon
b697e26170 fix(canvas): delete table + area together bug (#859) 2025-08-18 20:56:32 +03:00
Jonathan Fishner
04d91c67b1 fix(sql-import): fix SQL Server foreign key parsing for tables without schema prefix (#857) 2025-08-18 19:13:46 +03:00
Guy Ben-Aharon
d0dee84970 fix(filter): filter toggle issues with no schemas dbs (#856) 2025-08-17 12:54:56 +03:00
Guy Ben-Aharon
b4ccfcdcde fix: set default filter only if has more than 1 schemas (#855) 2025-08-14 11:37:24 +03:00
Guy Ben-Aharon
1759b0b9f2 fix: show default schema first (#854) 2025-08-13 21:08:53 +03:00
Guy Ben-Aharon
ab4845c772 fix: initially show filter when filter active (#853) 2025-08-13 20:20:59 +03:00
Jonathan Fishner
0545b41140 fix: DBML export error with multi-line table comments for SQL Server (#852)
* fix: DBML export error with multi-line table comments for SQL Server

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-13 18:15:38 +03:00
Guy Ben-Aharon
4520f8b1f7 update index.html (#850) 2025-08-13 11:34:08 +03:00
Guy Ben-Aharon
712bdf5b95 fix: filter to default schema on load new diagram (#849) 2025-08-12 18:07:19 +03:00
Jonathan Fishner
d7c9536272 fix: reorder with areas (#846) 2025-08-12 16:25:56 +03:00
Guy Ben-Aharon
815a52f192 update index.html (#848) 2025-08-12 14:31:41 +03:00
Guy Ben-Aharon
f1a4298362 fix: remove unnecessary space (#845) 2025-08-12 11:08:37 +03:00
Guy Ben-Aharon
b8f2141bd2 fix(sidebar): add titles to sidebar (#844)
* update shadcn

* menu v1

* menu v2

* resize menu items

* fix

* fix
2025-08-11 17:38:40 +03:00
Guy Ben-Aharon
eaebe34768 fix(menu): clear file menu (#843) 2025-08-11 11:46:33 +03:00
Jonathan Fishner
0d623a86b1 feat(postgres): add support hash index types (#812)
* feat(postgres): add support for hash index type with single column constraint

* some fixes

* some fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-10 20:24:34 +03:00
Guy Ben-Aharon
19fd94c6bd fix(area filter): fix dragging tables over filtered areas (#842) 2025-08-10 15:26:47 +03:00
Guy Ben-Aharon
0da3caeeac fix(table colors): switch to default table color (#841) 2025-08-10 14:42:22 +03:00
Guy Ben-Aharon
cb2ba66233 fix(select-box): fix select box issue in dialog (#840) 2025-08-10 14:06:09 +03:00
Guy Ben-Aharon
8a2267281b alignment of node converters (#839) 2025-08-10 13:36:55 +03:00
Guy Ben-Aharon
41ba251377 fix: update filter on adding table (#838) 2025-08-10 11:04:52 +03:00
Jonathan Fishner
e9c5442d9d feat(filter): filter tables by areas (#836)
* feat: auto-hide/show areas based on table visibility in canvas filter

* fix build

* fix

* fix

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-10 10:13:28 +03:00
Guy Ben-Aharon
4f1d3295c0 fix(filters): refactor diagram filters - remove schema filter (#832)
* refactor(filters): refactor diagram filters

* replace old filters

* fix storage

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2025-08-07 14:55:35 +03:00
Jonathan Fishner
5936500ca0 fix: for sqlite import - add more types & include type parameters (#834)
* fix: for sqlite import - add more types

* fix: preserve original data types in SQLite export instead of converting to storage classes

* fix: include type parameters (length, precision, scale) in SQLite export
2025-08-07 14:50:55 +03:00
Jonathan Fishner
43fc1d7fc2 feat: include foreign keys inline in SQLite CREATE TABLE statements (#833) 2025-08-07 11:55:15 +03:00
Guy Ben-Aharon
8dfa7cc62e chore(main): release 1.14.0 (#758) 2025-08-05 12:24:38 +03:00
Guy Ben-Aharon
23e93bfd01 fix: area resizers size (#830) 2025-08-04 20:56:19 +03:00
Guy Ben-Aharon
16f9f4671e fix(dbml): dbml indentation (#829) 2025-08-04 20:32:48 +03:00
Guy Ben-Aharon
0c300e5e72 fix(dbml): fix schemas with same table names (#828) 2025-08-04 17:50:28 +03:00
Guy Ben-Aharon
b9a1e78b53 fix(dbml): import dbml notes (table + fields) (#827) 2025-08-04 12:43:02 +03:00
Guy Ben-Aharon
337f7cdab4 fix(dbml): dbml note syntax (#826) 2025-08-04 12:21:37 +03:00
Guy Ben-Aharon
1b0390f0b7 feat(dbml): Edit Diagram Directly from DBML (#819)
* initial dbml apply

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2025-08-04 11:35:34 +03:00
Guy Ben-Aharon
bc52933b58 fix: update relationship when table width changes via expand/shrink (#825) 2025-08-03 22:41:11 +03:00
Guy Ben-Aharon
2fdad2344c solve issue with multiple render of tables (#824) 2025-08-03 22:13:18 +03:00
Guy Ben-Aharon
0c7eaa2df2 fix: solve issue with multiple render of tables (#823) 2025-08-03 21:52:17 +03:00
Guy Ben-Aharon
a5f8e56b3c fix(dbml): support multiple relationships on same field in inline DBML (#822) 2025-08-03 12:04:05 +03:00
Guy Ben-Aharon
8ffde62c1a fix(readonly): fix zoom out on readonly (#818) 2025-07-31 15:27:38 +03:00
Guy Ben-Aharon
39247b77a2 feat: enhance primary key and unique field handling logic (#817) 2025-07-31 11:38:33 +03:00
Guy Ben-Aharon
984b2aeee2 fix(ui): reduce spacing between primary key icon and short field types (#816) 2025-07-31 11:04:48 +03:00
Guy Ben-Aharon
eed104be5b fix(dbml): fix dbml output format (#815) 2025-07-30 14:31:56 +03:00
133 changed files with 11260 additions and 3157 deletions

View File

@@ -1,5 +1,68 @@
# Changelog
## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)
### Features
* add floating "Show All" button when tables are out of view ([#787](https://github.com/chartdb/chartdb/issues/787)) ([bda150d](https://github.com/chartdb/chartdb/commit/bda150d4b6d6fb90beb423efba69349d21a037a5))
* add table selection for large database imports ([#776](https://github.com/chartdb/chartdb/issues/776)) ([0d9f57a](https://github.com/chartdb/chartdb/commit/0d9f57a9c969a67e350d6bf25e07c3a9ef5bba39))
* **canvas:** Add filter tables on canvas ([#774](https://github.com/chartdb/chartdb/issues/774)) ([dfbcf05](https://github.com/chartdb/chartdb/commit/dfbcf05b2f595f5b7b77dd61abf77e6e07acaf8f))
* **custom-types:** add highlight fields option for custom types ([#726](https://github.com/chartdb/chartdb/issues/726)) ([7e0483f](https://github.com/chartdb/chartdb/commit/7e0483f1a5512a6a737baf61caf7513e043f2e96))
* **datatypes:** Add decimal / numeric attribute support + organize field row ([#715](https://github.com/chartdb/chartdb/issues/715)) ([778f85d](https://github.com/chartdb/chartdb/commit/778f85d49214232a39710e47bb5d4ec41b75d427))
* **dbml:** Edit Diagram Directly from DBML ([#819](https://github.com/chartdb/chartdb/issues/819)) ([1b0390f](https://github.com/chartdb/chartdb/commit/1b0390f0b7652fe415540b7942cf53ec87143f08))
* **default value:** add default value option to table field settings ([#770](https://github.com/chartdb/chartdb/issues/770)) ([c9ea7da](https://github.com/chartdb/chartdb/commit/c9ea7da0923ff991cb936235674d9a52b8186137))
* enhance primary key and unique field handling logic ([#817](https://github.com/chartdb/chartdb/issues/817)) ([39247b7](https://github.com/chartdb/chartdb/commit/39247b77a299caa4f29ea434af3028155c6d37ed))
* implement area grouping with parent-child relationships ([#762](https://github.com/chartdb/chartdb/issues/762)) ([b35e175](https://github.com/chartdb/chartdb/commit/b35e17526b3c9b918928ae5f3f89711ea7b2529c))
* **schema:** support create new schema ([#801](https://github.com/chartdb/chartdb/issues/801)) ([867903c](https://github.com/chartdb/chartdb/commit/867903cd5f24d96ce1fe718dc9b562e2f2b75276))
### Bug Fixes
* add open and create diagram to side menu ([#757](https://github.com/chartdb/chartdb/issues/757)) ([67f5ac3](https://github.com/chartdb/chartdb/commit/67f5ac303ebf5ada97d5c80fb08a2815ca205a91))
* add PostgreSQL tests and fix parsing SQL ([#760](https://github.com/chartdb/chartdb/issues/760)) ([5d33740](https://github.com/chartdb/chartdb/commit/5d337409d64d1078b538350016982a98e684c06c))
* area resizers size ([#830](https://github.com/chartdb/chartdb/issues/830)) ([23e93bf](https://github.com/chartdb/chartdb/commit/23e93bfd01d741dd3d11aa5c479cef97e1a86fa6))
* **area:** redo/undo after dragging an area with tables ([#767](https://github.com/chartdb/chartdb/issues/767)) ([6af94af](https://github.com/chartdb/chartdb/commit/6af94afc56cf8987b8fc9e3f0a9bfa966de35408))
* **canvas filter:** improve scroller on canvas filter ([#799](https://github.com/chartdb/chartdb/issues/799)) ([6bea827](https://github.com/chartdb/chartdb/commit/6bea82729362a8c7b73dc089ddd9e52bae176aa2))
* **canvas:** fix filter eye button ([#780](https://github.com/chartdb/chartdb/issues/780)) ([b7dbe54](https://github.com/chartdb/chartdb/commit/b7dbe54c83c75cfe3c556f7a162055dcfe2de23d))
* clone of custom types ([#804](https://github.com/chartdb/chartdb/issues/804)) ([b30162d](https://github.com/chartdb/chartdb/commit/b30162d98bc659a61aae023cdeaead4ce25c7ae9))
* **cockroachdb:** support schema creation for cockroachdb ([#803](https://github.com/chartdb/chartdb/issues/803)) ([dba372d](https://github.com/chartdb/chartdb/commit/dba372d25a8c642baf8600d05aa154882729d446))
* **dbml actions:** set dbml tooltips side ([#798](https://github.com/chartdb/chartdb/issues/798)) ([a119854](https://github.com/chartdb/chartdb/commit/a119854da7c935eb595984ea9398e04136ce60c4))
* **dbml editor:** move tooltips button to be on the right ([#797](https://github.com/chartdb/chartdb/issues/797)) ([bfbfd7b](https://github.com/chartdb/chartdb/commit/bfbfd7b843f96c894b1966ad95393b866c927466))
* **dbml export:** fix handle tables with same name under different schemas ([#807](https://github.com/chartdb/chartdb/issues/807)) ([18e9142](https://github.com/chartdb/chartdb/commit/18e914242faccd6376fe5a7cd5a4478667f065ee))
* **dbml export:** handle tables with same name under different schemas ([#806](https://github.com/chartdb/chartdb/issues/806)) ([e68837a](https://github.com/chartdb/chartdb/commit/e68837a34aa635fb6fc02c7f1289495e5c448242))
* **dbml field comments:** support export field comments in dbml ([#796](https://github.com/chartdb/chartdb/issues/796)) ([0ca7008](https://github.com/chartdb/chartdb/commit/0ca700873577bbfbf1dd3f8088c258fc89b10c53))
* **dbml import:** fix dbml import types + schemas ([#808](https://github.com/chartdb/chartdb/issues/808)) ([00bd535](https://github.com/chartdb/chartdb/commit/00bd535b3c62d26d25a6276d52beb10e26afad76))
* **dbml-export:** merge field attributes into single brackets and fix schema syntax ([#790](https://github.com/chartdb/chartdb/issues/790)) ([309ee9c](https://github.com/chartdb/chartdb/commit/309ee9cb0ff1f5a68ed183e3919e1a11a8410909))
* **dbml-import:** handle unsupported DBML features and add comprehensive tests ([#766](https://github.com/chartdb/chartdb/issues/766)) ([22d46e1](https://github.com/chartdb/chartdb/commit/22d46e1e90729730cc25dd6961bfe8c3d2ae0c98))
* **dbml:** dbml indentation ([#829](https://github.com/chartdb/chartdb/issues/829)) ([16f9f46](https://github.com/chartdb/chartdb/commit/16f9f4671e011eb66ba9594bed47570eda3eed66))
* **dbml:** dbml note syntax ([#826](https://github.com/chartdb/chartdb/issues/826)) ([337f7cd](https://github.com/chartdb/chartdb/commit/337f7cdab4759d15cb4d25a8c0e9394e99ba33d4))
* **dbml:** fix dbml output format ([#815](https://github.com/chartdb/chartdb/issues/815)) ([eed104b](https://github.com/chartdb/chartdb/commit/eed104be5ba2b7d9940ffac38e7877722ad764fc))
* **dbml:** fix schemas with same table names ([#828](https://github.com/chartdb/chartdb/issues/828)) ([0c300e5](https://github.com/chartdb/chartdb/commit/0c300e5e72cc5ff22cac42f8dbaed167061157c6))
* **dbml:** import dbml notes (table + fields) ([#827](https://github.com/chartdb/chartdb/issues/827)) ([b9a1e78](https://github.com/chartdb/chartdb/commit/b9a1e78b53c932c0b1a12ee38b62494a5c2f9348))
* **dbml:** support multiple relationships on same field in inline DBML ([#822](https://github.com/chartdb/chartdb/issues/822)) ([a5f8e56](https://github.com/chartdb/chartdb/commit/a5f8e56b3ca97b851b6953481644d3a3ff7ce882))
* **dbml:** support spaces in names ([#794](https://github.com/chartdb/chartdb/issues/794)) ([8f27f10](https://github.com/chartdb/chartdb/commit/8f27f10dec96af400dc2c12a30b22b3a346803a9))
* fix hotkeys on form elements ([#778](https://github.com/chartdb/chartdb/issues/778)) ([43d1dff](https://github.com/chartdb/chartdb/commit/43d1dfff71f2b960358a79b0112b78d11df91fb7))
* fix screen freeze after schema select ([#800](https://github.com/chartdb/chartdb/issues/800)) ([8aeb1df](https://github.com/chartdb/chartdb/commit/8aeb1df0ad353c49e91243453f24bfa5921a89ab))
* **i18n:** add Croatian (hr) language support ([#802](https://github.com/chartdb/chartdb/issues/802)) ([2eb48e7](https://github.com/chartdb/chartdb/commit/2eb48e75d303d622f51327d22502a6f78e7fb32d))
* improve SQL export formatting and add schema-aware FK grouping ([#783](https://github.com/chartdb/chartdb/issues/783)) ([6df588f](https://github.com/chartdb/chartdb/commit/6df588f40e6e7066da6125413b94466429d48767))
* lost in canvas button animation ([#793](https://github.com/chartdb/chartdb/issues/793)) ([a93ec2c](https://github.com/chartdb/chartdb/commit/a93ec2cab906d0e4431d8d1668adcf2dbfc3c80f))
* **readonly:** fix zoom out on readonly ([#818](https://github.com/chartdb/chartdb/issues/818)) ([8ffde62](https://github.com/chartdb/chartdb/commit/8ffde62c1a00893c4bf6b4dd39068df530375416))
* remove error lag after autofix ([#764](https://github.com/chartdb/chartdb/issues/764)) ([bf32c08](https://github.com/chartdb/chartdb/commit/bf32c08d37c02ee6d7946a41633bb97b2271fcb7))
* remove unnecessary import ([#791](https://github.com/chartdb/chartdb/issues/791)) ([87836e5](https://github.com/chartdb/chartdb/commit/87836e53d145b825f9c4f80abca72f418df50e6c))
* **scroll:** disable scroll x behavior ([#795](https://github.com/chartdb/chartdb/issues/795)) ([4bc71c5](https://github.com/chartdb/chartdb/commit/4bc71c52ff5c462800d8530b72a5aadb7d7f85ed))
* set focus on filter search ([#775](https://github.com/chartdb/chartdb/issues/775)) ([9949a46](https://github.com/chartdb/chartdb/commit/9949a46ee3ba7f46a2ea7f2c0d7101cc9336df4f))
* solve issue with multiple render of tables ([#823](https://github.com/chartdb/chartdb/issues/823)) ([0c7eaa2](https://github.com/chartdb/chartdb/commit/0c7eaa2df20cfb6994b7e6251c760a2d4581c879))
* **sql-export:** escape newlines and quotes in multi-line comments ([#765](https://github.com/chartdb/chartdb/issues/765)) ([f7f9290](https://github.com/chartdb/chartdb/commit/f7f92903def84a94ac0c66f625f96a6681383945))
* **sql-server:** improvment for sql-server import via sql script ([#789](https://github.com/chartdb/chartdb/issues/789)) ([79b8855](https://github.com/chartdb/chartdb/commit/79b885502e3385e996a52093a3ccd5f6e469993a))
* **table-node:** fix comment icon on field ([#786](https://github.com/chartdb/chartdb/issues/786)) ([745bdee](https://github.com/chartdb/chartdb/commit/745bdee86d07f1e9c3a2d24237c48c25b9a8eeea))
* **table-node:** improve field spacing ([#785](https://github.com/chartdb/chartdb/issues/785)) ([08eb9cc](https://github.com/chartdb/chartdb/commit/08eb9cc55f0077f53afea6f9ce720341e1a583c2))
* **table-select:** add loading indication for import ([#782](https://github.com/chartdb/chartdb/issues/782)) ([b46ed58](https://github.com/chartdb/chartdb/commit/b46ed58dff1ec74579fb1544dba46b0f77730c52))
* **ui:** reduce spacing between primary key icon and short field types ([#816](https://github.com/chartdb/chartdb/issues/816)) ([984b2ae](https://github.com/chartdb/chartdb/commit/984b2aeee22c43cb9bda77df2c22087973079af4))
* update MariaDB database import smart query ([#792](https://github.com/chartdb/chartdb/issues/792)) ([386e40a](https://github.com/chartdb/chartdb/commit/386e40a0bf93d9aef1486bb1e729d8f485e675eb))
* update multiple schemas toast to require user action ([#771](https://github.com/chartdb/chartdb/issues/771)) ([f56fab9](https://github.com/chartdb/chartdb/commit/f56fab9876fb9fc46c6c708231324a90d8a7851d))
* update relationship when table width changes via expand/shrink ([#825](https://github.com/chartdb/chartdb/issues/825)) ([bc52933](https://github.com/chartdb/chartdb/commit/bc52933b58bfe6bc73779d9401128254cbf497d5))
## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)

View File

@@ -4,8 +4,9 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<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>
<link rel="canonical" href="https://chartdb.io" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
@@ -15,14 +16,19 @@
<script src="/config.js"></script>
<script>
// Load analytics only if not disabled
(function() {
const disableAnalytics = (window.env && window.env.DISABLE_ANALYTICS === 'true') ||
(typeof process !== 'undefined' && process.env && process.env.VITE_DISABLE_ANALYTICS === 'true');
(function () {
const disableAnalytics =
(window.env && window.env.DISABLE_ANALYTICS === 'true') ||
(typeof process !== 'undefined' &&
process.env &&
process.env.VITE_DISABLE_ANALYTICS === 'true');
if (!disableAnalytics) {
const script = document.createElement('script');
script.src = 'https://cdn.usefathom.com/script.js';
script.setAttribute('data-site', 'PRHIVBNN');
script.setAttribute('data-canonical', 'false');
script.setAttribute('data-spa', 'auto');
script.defer = true;
document.head.appendChild(script);
}

610
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.13.2",
"version": "1.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.13.2",
"version": "1.14.0",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",
@@ -18,24 +18,24 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@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-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-menubar": "^1.1.1",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "1.2.0",
"@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-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^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",
"@xyflow/react": "^12.3.1",
"@xyflow/react": "^12.8.2",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.1",
@@ -2121,23 +2121,23 @@
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
"integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"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": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0"
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2208,14 +2259,56 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"node_modules/@radix-ui/react-dialog/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-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
"@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-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": {
"@types/react": "*",
@@ -2233,12 +2326,12 @@
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2255,13 +2348,29 @@
}
}
},
"node_modules/@radix-ui/react-dialog/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==",
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"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",
"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": "*",
@@ -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": {
"version": "1.1.0",
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
"integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==",
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2964,12 +3106,12 @@
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@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": {
"version": "1.2.3",
"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": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
"integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==",
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
"integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.2",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.2"
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.7",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -3189,13 +3313,19 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
"integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==",
"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": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@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": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@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": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
"integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==",
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-rect": "1.1.0",
"@radix-ui/react-use-size": "1.1.0",
"@radix-ui/rect": "1.1.0"
"@radix-ui/react-arrow": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-rect": "1.1.1",
"@radix-ui/react-use-size": "1.1.1",
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -3272,13 +3450,37 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"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.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
"@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-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": {
"@types/react": "*",
@@ -3296,12 +3498,12 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -3318,13 +3520,98 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/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==",
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"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",
"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": {
"@types/react": "*",
@@ -3337,12 +3624,12 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
"integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@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": {
"version": "1.1.0",
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
@@ -4603,12 +4929,12 @@
"license": "MIT"
},
"node_modules/@xyflow/react": {
"version": "12.4.2",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.2.tgz",
"integrity": "sha512-AFJKVc/fCPtgSOnRst3xdYJwiEcUN9lDY7EO/YiRvFHYCJGgfzg+jpvZjkTOnBLGyrMJre9378pRxAc3fsR06A==",
"version": "12.8.2",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.2.tgz",
"integrity": "sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.50",
"@xyflow/system": "0.0.66",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
@@ -4618,16 +4944,18 @@
}
},
"node_modules/@xyflow/system": {
"version": "0.0.50",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.50.tgz",
"integrity": "sha512-HVUZd4LlY88XAaldFh2nwVxDOcdIBxGpQ5txzwfJPf+CAjj2BfYug1fHs2p4yS7YO8H6A3EFJQovBE8YuHkAdg==",
"version": "0.0.66",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.66.tgz",
"integrity": "sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.13.2",
"version": "1.14.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -26,24 +26,24 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@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-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-menubar": "^1.1.1",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "1.2.0",
"@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-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^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",
"@xyflow/react": "^12.3.1",
"@xyflow/react": "^12.8.2",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.1",

View File

@@ -1,4 +1,4 @@
User-agent: *
Allow: /
Disallow: /
Sitemap: https://app.chartdb.io/sitemap.xml

View File

@@ -1,7 +1,7 @@
import { cva } from 'class-variance-authority';
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: {
variant: {

View 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 };

View File

@@ -31,6 +31,7 @@ export interface CodeSnippetAction {
label: string;
icon: LucideIcon;
onClick: () => void;
className?: string;
}
export interface CodeSnippetProps {
@@ -44,6 +45,7 @@ export interface CodeSnippetProps {
editorProps?: React.ComponentProps<EditorType>;
actions?: CodeSnippetAction[];
actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left';
allowCopy?: boolean;
}
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
@@ -58,6 +60,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
editorProps,
actions,
actionsTooltipSide,
allowCopy = true,
}) => {
const { t } = useTranslation();
const monaco = useMonaco();
@@ -131,33 +134,37 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
<Suspense fallback={<Spinner />}>
{isComplete ? (
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent side={actionsTooltipSide}>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
{allowCopy ? (
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent
side={actionsTooltipSide}
>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
) : null}
{actions &&
actions.length > 0 &&
@@ -166,7 +173,10 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
className={cn(
'h-fit p-1.5',
action.className
)}
variant="outline"
onClick={action.onClick}
>

View File

@@ -0,0 +1,51 @@
import type { DBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
import * as monaco from 'monaco-editor';
export const highlightErrorLine = ({
error,
model,
editorDecorationsCollection,
}: {
error: DBMLError;
model?: monaco.editor.ITextModel | null;
editorDecorationsCollection:
| monaco.editor.IEditorDecorationsCollection
| undefined;
}) => {
if (!model) return;
if (!editorDecorationsCollection) return;
const decorations = [
{
range: new monaco.Range(
error.line,
1,
error.line,
model.getLineMaxColumn(error.line)
),
options: {
isWholeLine: true,
className: 'dbml-error-line',
glyphMarginClassName: 'dbml-error-glyph',
hoverMessage: { value: error.message },
overviewRuler: {
color: '#ff0000',
position: monaco.editor.OverviewRulerLane.Right,
darkColor: '#ff0000',
},
},
},
];
editorDecorationsCollection?.set(decorations);
};
export const clearErrorHighlight = (
editorDecorationsCollection:
| monaco.editor.IEditorDecorationsCollection
| undefined
) => {
if (editorDecorationsCollection) {
editorDecorationsCollection.clear();
}
};

View File

@@ -37,11 +37,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
const datatypePattern = dataTypesNames.join('|');
monaco.languages.setMonarchTokensProvider('dbml', {
keywords: ['Table', 'Ref', 'Indexes'],
keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
datatypes: dataTypesNames,
tokenizer: {
root: [
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
[
/\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
'keyword',
],
[/\[.*?\]/, 'annotation'],
[/'''/, 'string', '@tripleQuoteString'],
[/".*?"/, 'string'],

View File

@@ -4,6 +4,7 @@ import { Cross2Icon } from '@radix-ui/react-icons';
import { cn } from '@/lib/utils';
import { ScrollArea } from '../scroll-area/scroll-area';
import { ChevronLeft } from 'lucide-react';
const Dialog = DialogPrimitive.Root;
@@ -32,28 +33,75 @@ const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showClose?: boolean;
showBack?: boolean;
backButtonClassName?: string;
blurBackground?: boolean;
forceOverlay?: boolean;
onBackClick?: () => void;
}
>(({ className, children, showClose, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
{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">
<Cross2Icon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));
>(
(
{
className,
children,
showClose,
showBack,
onBackClick,
backButtonClassName,
blurBackground,
forceOverlay,
...props
},
ref
) => (
<DialogPortal>
{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
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{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 && (
<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" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
);
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({

View File

@@ -2,16 +2,13 @@ import React from 'react';
import { cn } from '@/lib/utils';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
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
)}
ref={ref}

View File

@@ -94,6 +94,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
setOpen?.(isOpen);
setIsOpen(isOpen);
if (isOpen) {
setSearchTerm('');
}
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
},
[setOpen]

View File

@@ -29,6 +29,7 @@ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_WIDTH_ICON_EXTENDED = '4rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
type SidebarContext = {
@@ -142,6 +143,8 @@ const SidebarProvider = React.forwardRef<
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
'--sidebar-width-icon-extended':
SIDEBAR_WIDTH_ICON_EXTENDED,
...style,
} as React.CSSProperties
}
@@ -166,7 +169,7 @@ const Sidebar = React.forwardRef<
React.ComponentProps<'div'> & {
side?: 'left' | 'right';
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-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
? '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-extended]:w-[--sidebar-width-icon-extended]'
)}
/>
<div
@@ -257,8 +260,8 @@ const Sidebar = React.forwardRef<
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
? '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-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended] group-data-[side=left]:border-r group-data-[side=right]:border-l',
className
)}
{...props}
@@ -421,7 +424,7 @@ const SidebarContent = React.forwardRef<
ref={ref}
data-sidebar="content"
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
)}
{...props}
@@ -461,6 +464,7 @@ const SidebarGroupLabel = React.forwardRef<
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',
'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
)}
{...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',
// Increases the hit area of the button on mobile.
'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
)}
{...props}
@@ -532,7 +536,7 @@ const SidebarMenuItem = React.forwardRef<
SidebarMenuItem.displayName = 'SidebarMenuItem';
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: {
variant: {
@@ -636,7 +640,7 @@ const SidebarMenuAction = React.forwardRef<
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.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 &&
'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
@@ -753,7 +757,7 @@ const SidebarMenuSubButton = React.forwardRef<
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
className
)}
{...props}

View File

@@ -13,15 +13,17 @@ const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
// <TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
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
)}
{...props}
/>
// </TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;

View File

@@ -1,27 +1,55 @@
import React, { type ReactNode, useCallback, useState } from 'react';
import React, {
type ReactNode,
useCallback,
useState,
useEffect,
useRef,
} from 'react';
import { canvasContext } from './canvas-context';
import { useChartDB } from '@/hooks/use-chartdb';
import {
adjustTablePositions,
shouldShowTablesBySchemaFilter,
} from '@/lib/domain/db-table';
import { adjustTablePositions } from '@/lib/domain/db-table';
import { useReactFlow } from '@xyflow/react';
import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils';
import type { Graph } from '@/lib/graph';
import { createGraph } from '@/lib/graph';
import { useDiagramFilter } from '../diagram-filter-context/use-diagram-filter';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
interface CanvasProviderProps {
children: ReactNode;
}
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const { tables, relationships, updateTablesState, filteredSchemas } =
useChartDB();
const {
tables,
relationships,
updateTablesState,
databaseType,
areas,
diagramId,
} = useChartDB();
const { filter, loading: filterLoading } = useDiagramFilter();
const { fitView } = useReactFlow();
const [overlapGraph, setOverlapGraph] =
useState<Graph<string>>(createGraph());
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(
(
@@ -32,9 +60,19 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const newTables = adjustTablePositions({
relationships,
tables: tables.filter((table) =>
shouldShowTablesBySchemaFilter(table, filteredSchemas)
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
),
mode: 'all', // Use 'all' mode for manual reordering
areas,
mode: 'all',
});
const updatedOverlapGraph = findOverlappingTables({
@@ -69,7 +107,15 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
});
}, 500);
},
[filteredSchemas, relationships, tables, updateTablesState, fitView]
[
filter,
relationships,
tables,
updateTablesState,
fitView,
databaseType,
areas,
]
);
return (

View File

@@ -81,9 +81,6 @@ export interface ChartDBContext {
highlightedCustomType?: DBCustomType;
highlightCustomTypeId: (id?: string) => void;
filteredSchemas?: string[];
filterSchemas: (schemaIds: string[]) => void;
// General operations
updateDiagramId: (id: string) => Promise<void>;
updateDiagramName: (
@@ -95,6 +92,10 @@ export interface ChartDBContext {
updateDiagramUpdatedAt: () => Promise<void>;
clearDiagramData: () => Promise<void>;
deleteDiagram: () => Promise<void>;
updateDiagramData: (
diagram: Diagram,
options?: { forceUpdateStorage?: boolean }
) => Promise<void>;
// Database type operations
updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
@@ -280,11 +281,6 @@ export interface ChartDBContext {
customType: Partial<DBCustomType>,
options?: { updateHistory: boolean }
) => Promise<void>;
// Filters
hiddenTableIds?: string[];
addHiddenTableId: (tableId: string) => Promise<void>;
removeHiddenTableId: (tableId: string) => Promise<void>;
}
export const chartDBContext = createContext<ChartDBContext>({
@@ -298,8 +294,6 @@ export const chartDBContext = createContext<ChartDBContext>({
customTypes: [],
schemas: [],
highlightCustomTypeId: emptyFn,
filteredSchemas: [],
filterSchemas: emptyFn,
currentDiagram: {
id: '',
name: '',
@@ -317,6 +311,7 @@ export const chartDBContext = createContext<ChartDBContext>({
loadDiagramFromData: emptyFn,
clearDiagramData: emptyFn,
deleteDiagram: emptyFn,
updateDiagramData: emptyFn,
// Database type operations
updateDatabaseType: emptyFn,
@@ -381,9 +376,4 @@ export const chartDBContext = createContext<ChartDBContext>({
removeCustomType: emptyFn,
removeCustomTypes: emptyFn,
updateCustomType: emptyFn,
// Filters
hiddenTableIds: [],
addHiddenTableId: emptyFn,
removeHiddenTableId: emptyFn,
});

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import type { DBTable } from '@/lib/domain/db-table';
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 { chartDBContext } from './chartdb-context';
import { DatabaseType } from '@/lib/domain/database-type';
@@ -17,7 +17,6 @@ import {
databasesWithSchemas,
schemaNameToSchemaId,
} from '@/lib/domain/db-schema';
import { useLocalConfig } from '@/hooks/use-local-config';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { useEventEmitter } from 'ahooks';
import type { DBDependency } from '@/lib/domain/db-dependency';
@@ -29,7 +28,6 @@ import {
DBCustomTypeKind,
type DBCustomType,
} from '@/lib/domain/db-custom-type';
import { useConfig } from '@/hooks/use-config';
export interface ChartDBProviderProps {
diagram?: Diagram;
@@ -40,16 +38,12 @@ export const ChartDBProvider: React.FC<
React.PropsWithChildren<ChartDBProviderProps>
> = ({ children, diagram, readonly: readonlyProp }) => {
const { hasDiff } = useDiff();
let db = useStorage();
const dbStorage = useStorage();
let db = dbStorage;
const events = useEventEmitter<ChartDBEvent>();
const { setSchemasFilter, schemasFilter } = useLocalConfig();
const { addUndoAction, resetRedoStack, resetUndoStack } =
useRedoUndoStack();
const {
getHiddenTablesForDiagram,
hideTableForDiagram,
unhideTableForDiagram,
} = useConfig();
const [diagramId, setDiagramId] = useState('');
const [diagramName, setDiagramName] = useState('');
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
@@ -71,7 +65,7 @@ export const ChartDBProvider: React.FC<
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
diagram?.customTypes ?? []
);
const [hiddenTableIds, setHiddenTableIds] = useState<string[]>([]);
const { events: diffEvents } = useDiff();
const [highlightedCustomTypeId, setHighlightedCustomTypeId] =
@@ -95,15 +89,10 @@ export const ChartDBProvider: React.FC<
diffEvents.useSubscription(diffCalculatedHandler);
// Sync hiddenTableIds with config
useEffect(() => {
if (diagramId) {
const hiddenTables = getHiddenTablesForDiagram(diagramId);
setHiddenTableIds(hiddenTables);
}
}, [diagramId, getHiddenTablesForDiagram]);
const defaultSchemaName = defaultSchemas[databaseType];
const defaultSchemaName = useMemo(
() => defaultSchemas[databaseType],
[databaseType]
);
const readonly = useMemo(
() => readonlyProp ?? hasDiff ?? false,
@@ -124,9 +113,11 @@ export const ChartDBProvider: React.FC<
.filter((schema) => !!schema) as string[]
),
]
.sort((a, b) =>
a === defaultSchemaName ? -1 : a.localeCompare(b)
)
.sort((a, b) => {
if (a === defaultSchemaName) return -1;
if (b === defaultSchemaName) return 1;
return a.localeCompare(b);
})
.map(
(schema): DBSchema => ({
id: schemaNameToSchemaId(schema),
@@ -140,34 +131,6 @@ export const ChartDBProvider: React.FC<
[tables, defaultSchemaName, databaseType]
);
const filterSchemas: ChartDBContext['filterSchemas'] = useCallback(
(schemaIds) => {
setSchemasFilter((prev) => ({
...prev,
[diagramId]: schemaIds,
}));
},
[diagramId, setSchemasFilter]
);
const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(() => {
if (schemas.length === 0) {
return undefined;
}
const schemasFilterFromCache =
(schemasFilter[diagramId] ?? []).length === 0
? undefined // in case of empty filter, skip cache
: schemasFilter[diagramId];
return (
schemasFilterFromCache ?? [
schemas.find((s) => s.name === defaultSchemaName)?.id ??
schemas[0]?.id,
]
);
}, [schemasFilter, diagramId, schemas, defaultSchemaName]);
const currentDiagram: Diagram = useMemo(
() => ({
id: diagramId,
@@ -379,7 +342,7 @@ export const ChartDBProvider: React.FC<
},
],
indexes: [],
color: randomColor(),
color: attributes?.isView ? viewColor : defaultTableColor,
createdAt: Date.now(),
isView: false,
order: tables.length,
@@ -1124,12 +1087,15 @@ export const ChartDBProvider: React.FC<
const sourceFieldName = sourceField?.name ?? '';
const targetTable = getTable(targetTableId);
const targetTableSchema = targetTable?.schema;
const relationship: DBRelationship = {
id: generateId(),
name: `${sourceTableName}_${sourceFieldName}_fk`,
sourceSchema: sourceTable?.schema,
sourceTableId,
targetSchema: sourceTable?.schema,
targetSchema: targetTableSchema,
targetTableId,
sourceFieldId,
targetFieldId,
@@ -1451,7 +1417,7 @@ export const ChartDBProvider: React.FC<
y: 0,
width: 300,
height: 200,
color: randomColor(),
color: defaultAreaColor,
...attributes,
};
@@ -1585,6 +1551,16 @@ export const ChartDBProvider: React.FC<
]
);
const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
async (diagram, options) => {
const st = options?.forceUpdateStorage ? dbStorage : db;
await st.deleteDiagram(diagram.id);
await st.addDiagram({ diagram });
loadDiagramFromData(diagram);
},
[db, dbStorage, loadDiagramFromData]
);
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
async (diagramId: string) => {
const diagram = await db.getDiagram(diagramId, {
@@ -1748,29 +1724,6 @@ export const ChartDBProvider: React.FC<
]
);
const addHiddenTableId: ChartDBContext['addHiddenTableId'] = useCallback(
async (tableId: string) => {
if (!hiddenTableIds.includes(tableId)) {
setHiddenTableIds((prev) => [...prev, tableId]);
await hideTableForDiagram(diagramId, tableId);
}
},
[hiddenTableIds, diagramId, hideTableForDiagram]
);
const removeHiddenTableId: ChartDBContext['removeHiddenTableId'] =
useCallback(
async (tableId: string) => {
if (hiddenTableIds.includes(tableId)) {
setHiddenTableIds((prev) =>
prev.filter((id) => id !== tableId)
);
await unhideTableForDiagram(diagramId, tableId);
}
},
[hiddenTableIds, diagramId, unhideTableForDiagram]
);
return (
<chartDBContext.Provider
value={{
@@ -1783,10 +1736,9 @@ export const ChartDBProvider: React.FC<
areas,
currentDiagram,
schemas,
filteredSchemas,
events,
readonly,
filterSchemas,
updateDiagramData,
updateDiagramId,
updateDiagramName,
loadDiagram,
@@ -1843,9 +1795,6 @@ export const ChartDBProvider: React.FC<
removeCustomType,
removeCustomTypes,
updateCustomType,
hiddenTableIds,
addHiddenTableId,
removeHiddenTableId,
highlightCustomTypeId,
highlightedCustomType,
}}

View File

@@ -8,23 +8,9 @@ export interface ConfigContext {
config?: Partial<ChartDBConfig>;
updateFn?: (config: ChartDBConfig) => ChartDBConfig;
}) => Promise<void>;
getHiddenTablesForDiagram: (diagramId: string) => string[];
setHiddenTablesForDiagram: (
diagramId: string,
hiddenTableIds: string[]
) => Promise<void>;
hideTableForDiagram: (diagramId: string, tableId: string) => Promise<void>;
unhideTableForDiagram: (
diagramId: string,
tableId: string
) => Promise<void>;
}
export const ConfigContext = createContext<ConfigContext>({
config: undefined,
updateConfig: emptyFn,
getHiddenTablesForDiagram: () => [],
setHiddenTablesForDiagram: emptyFn,
hideTableForDiagram: emptyFn,
unhideTableForDiagram: emptyFn,
});

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { ConfigContext } from './config-context';
import { useStorage } from '@/hooks/use-storage';
@@ -8,7 +8,7 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { getConfig, updateConfig: updateDataConfig } = useStorage();
const [config, setConfig] = React.useState<ChartDBConfig | undefined>();
const [config, setConfig] = useState<ChartDBConfig | undefined>();
useEffect(() => {
const loadConfig = async () => {
@@ -44,84 +44,11 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
return promise;
};
const getHiddenTablesForDiagram = (diagramId: string): string[] => {
return config?.hiddenTablesByDiagram?.[diagramId] ?? [];
};
const setHiddenTablesForDiagram = async (
diagramId: string,
hiddenTableIds: string[]
): Promise<void> => {
return updateConfig({
updateFn: (currentConfig) => ({
...currentConfig,
hiddenTablesByDiagram: {
...currentConfig.hiddenTablesByDiagram,
[diagramId]: hiddenTableIds,
},
}),
});
};
const hideTableForDiagram = async (
diagramId: string,
tableId: string
): Promise<void> => {
return updateConfig({
updateFn: (currentConfig) => {
const currentHiddenTables =
currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
if (currentHiddenTables.includes(tableId)) {
return currentConfig; // Already hidden, no change needed
}
return {
...currentConfig,
hiddenTablesByDiagram: {
...currentConfig.hiddenTablesByDiagram,
[diagramId]: [...currentHiddenTables, tableId],
},
};
},
});
};
const unhideTableForDiagram = async (
diagramId: string,
tableId: string
): Promise<void> => {
return updateConfig({
updateFn: (currentConfig) => {
const currentHiddenTables =
currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
const filteredTables = currentHiddenTables.filter(
(id) => id !== tableId
);
if (filteredTables.length === currentHiddenTables.length) {
return currentConfig; // Not hidden, no change needed
}
return {
...currentConfig,
hiddenTablesByDiagram: {
...currentConfig.hiddenTablesByDiagram,
[diagramId]: filteredTables,
},
};
},
});
};
return (
<ConfigContext.Provider
value={{
config,
updateConfig,
getHiddenTablesForDiagram,
setHiddenTablesForDiagram,
hideTableForDiagram,
unhideTableForDiagram,
}}
>
{children}

View File

@@ -0,0 +1,50 @@
import type { DBSchema } from '@/lib/domain';
import type {
DiagramFilter,
FilterTableInfo,
} from '@/lib/domain/diagram-filter/diagram-filter';
import { emptyFn } from '@/lib/utils';
import { createContext } from 'react';
export interface DiagramFilterContext {
filter?: DiagramFilter;
loading: boolean;
hasActiveFilter: boolean;
schemasDisplayed: DBSchema[];
clearSchemaIdsFilter: () => void;
clearTableIdsFilter: () => void;
setTableIdsFilterEmpty: () => void;
// reset
resetFilter: () => void;
toggleSchemaFilter: (schemaId: string) => void;
toggleTableFilter: (tableId: 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>({
hasActiveFilter: false,
clearSchemaIdsFilter: emptyFn,
clearTableIdsFilter: emptyFn,
setTableIdsFilterEmpty: emptyFn,
resetFilter: emptyFn,
toggleSchemaFilter: emptyFn,
toggleTableFilter: emptyFn,
addSchemaToFilter: emptyFn,
schemasDisplayed: [],
addTablesToFilter: emptyFn,
removeTablesFromFilter: emptyFn,
loading: false,
});

View File

@@ -0,0 +1,559 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type { DiagramFilterContext } from './diagram-filter-context';
import { diagramFilterContext } from './diagram-filter-context';
import type {
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 { useChartDB } from '@/hooks/use-chartdb';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { databasesWithSchemas, schemaNameToSchemaId } from '@/lib/domain';
import { defaultSchemas } from '@/lib/data/default-schemas';
import type { ChartDBEvent } from '../chartdb-context/chartdb-context';
export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { diagramId, tables, schemas, databaseType, events } = useChartDB();
const { getDiagramFilter, updateDiagramFilter } = useStorage();
const [filter, setFilter] = useState<DiagramFilter>({});
const [loading, setLoading] = useState<boolean>(true);
const allSchemasIds = useMemo(() => {
return schemas.map((schema) => schema.id);
}, [schemas]);
const allTables: FilterTableInfo[] = useMemo(() => {
return tables.map(
(table) =>
({
id: table.id,
schemaId: table.schema
? schemaNameToSchemaId(table.schema)
: defaultSchemas[databaseType],
schema: table.schema ?? defaultSchemas[databaseType],
areaId: table.parentAreaId ?? undefined,
}) satisfies FilterTableInfo
);
}, [tables, databaseType]);
const diagramIdOfLoadedFilter = useRef<string | null>(null);
useEffect(() => {
if (diagramId && diagramId === diagramIdOfLoadedFilter.current) {
updateDiagramFilter(diagramId, filter);
}
}, [diagramId, filter, updateDiagramFilter]);
// Reset filter when diagram changes
useEffect(() => {
if (diagramIdOfLoadedFilter.current === diagramId) {
// If the diagramId hasn't changed, do not reset the filter
return;
}
setLoading(true);
const loadFilterFromStorage = async (diagramId: string) => {
if (diagramId) {
const storedFilter = await getDiagramFilter(diagramId);
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({});
if (diagramId) {
loadFilterFromStorage(diagramId);
diagramIdOfLoadedFilter.current = diagramId;
}
}, [diagramId, getDiagramFilter, schemas]);
const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] =
useCallback(() => {
setFilter(
(prev) =>
({
...prev,
schemaIds: undefined,
}) satisfies DiagramFilter
);
}, []);
const clearTableIds: DiagramFilterContext['clearTableIdsFilter'] =
useCallback(() => {
setFilter(
(prev) =>
({
...prev,
tableIds: undefined,
}) satisfies DiagramFilter
);
}, []);
const setTableIdsEmpty: DiagramFilterContext['setTableIdsFilterEmpty'] =
useCallback(() => {
setFilter(
(prev) =>
({
...prev,
tableIds: [],
}) satisfies DiagramFilter
);
}, []);
// Reset filter
const resetFilter: DiagramFilterContext['resetFilter'] = useCallback(() => {
setFilter({});
}, []);
const toggleSchemaFilter: DiagramFilterContext['toggleSchemaFilter'] =
useCallback(
(schemaId: string) => {
setFilter((prev) => {
const currentSchemaIds = prev.schemaIds;
// Check if schema is currently visible
const isSchemaVisible = !allTables.some(
(table) =>
table.schemaId === schemaId &&
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter: prev,
options: {
defaultSchema: defaultSchemas[databaseType],
},
}) === false
);
let newSchemaIds: string[] | undefined;
let newTableIds: string[] | undefined = prev.tableIds;
if (isSchemaVisible) {
// Schema is visible, make it not visible
if (!currentSchemaIds) {
// All schemas are visible, create filter with all except this one
newSchemaIds = allSchemasIds.filter(
(id) => id !== schemaId
);
} else {
// Remove this schema from the filter
newSchemaIds = currentSchemaIds.filter(
(id) => id !== schemaId
);
}
// Remove tables from this schema from tableIds if present
if (prev.tableIds) {
const schemaTableIds = allTables
.filter((table) => table.schemaId === schemaId)
.map((table) => table.id);
newTableIds = prev.tableIds.filter(
(id) => !schemaTableIds.includes(id)
);
}
} else {
// Schema is not visible, make it visible
newSchemaIds = [
...new Set([...(currentSchemaIds || []), schemaId]),
];
// Add tables from this schema to tableIds if tableIds is defined
if (prev.tableIds) {
const schemaTableIds = allTables
.filter((table) => table.schemaId === schemaId)
.map((table) => table.id);
newTableIds = [
...new Set([
...prev.tableIds,
...schemaTableIds,
]),
];
}
}
// Use reduceFilter to optimize and handle edge cases
return reduceFilter(
{
schemaIds: newSchemaIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[],
{
databaseWithSchemas:
databasesWithSchemas.includes(databaseType),
}
);
});
},
[allSchemasIds, allTables, databaseType]
);
const toggleTableFilterForNoSchema = useCallback(
(tableId: string) => {
setFilter((prev) => {
const currentTableIds = prev.tableIds;
// Check if table is currently visible
const isTableVisible = filterTable({
table: { id: tableId, schema: undefined },
filter: prev,
options: { defaultSchema: undefined },
});
let newTableIds: string[] | undefined;
if (isTableVisible) {
// Table is visible, make it not visible
if (!currentTableIds) {
// All tables are visible, create filter with all except this one
newTableIds = allTables
.filter((t) => t.id !== tableId)
.map((t) => t.id);
} else {
// Remove this table from the filter
newTableIds = currentTableIds.filter(
(id) => id !== tableId
);
}
} else {
// Table is not visible, make it visible
newTableIds = [
...new Set([...(currentTableIds || []), tableId]),
];
}
// Use reduceFilter to optimize and handle edge cases
return reduceFilter(
{
schemaIds: undefined,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[],
{
databaseWithSchemas:
databasesWithSchemas.includes(databaseType),
}
);
});
},
[allTables, databaseType]
);
const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] =
useCallback(
(tableId: string) => {
if (!databasesWithSchemas.includes(databaseType)) {
// No schemas, toggle table filter without schema context
toggleTableFilterForNoSchema(tableId);
return;
}
setFilter((prev) => {
// Find the table in the tables list
const tableInfo = allTables.find((t) => t.id === tableId);
if (!tableInfo) {
return prev;
}
// Check if table is currently visible using filterTable
const isTableVisible = filterTable({
table: {
id: tableInfo.id,
schema: tableInfo.schema,
},
filter: prev,
options: {
defaultSchema: defaultSchemas[databaseType],
},
});
let newSchemaIds = prev.schemaIds;
let newTableIds = prev.tableIds;
if (isTableVisible) {
// Table is visible, make it not visible
// If the table is visible due to its schema being in schemaIds
if (
tableInfo?.schemaId &&
prev.schemaIds?.includes(tableInfo.schemaId)
) {
// Remove the schema from schemaIds and add all other tables from that schema to tableIds
newSchemaIds = prev.schemaIds.filter(
(id) => id !== tableInfo.schemaId
);
// Get all other tables from this schema (except the one being toggled)
const otherTablesFromSchema = allTables
.filter(
(t) =>
t.schemaId === tableInfo.schemaId &&
t.id !== tableId
)
.map((t) => t.id);
// Add these tables to tableIds
newTableIds = [
...(prev.tableIds || []),
...otherTablesFromSchema,
];
} else if (prev.tableIds?.includes(tableId)) {
// Table is visible because it's in tableIds, remove it
newTableIds = prev.tableIds.filter(
(id) => id !== tableId
);
} else if (!prev.tableIds && !prev.schemaIds) {
// No filters = all visible, create filter with all tables except this one
newTableIds = allTables
.filter((t) => t.id !== tableId)
.map((t) => t.id);
}
} else {
// Table is not visible, make it visible by adding to tableIds
newTableIds = [...(prev.tableIds || []), tableId];
}
// Use reduceFilter to optimize and handle edge cases
return reduceFilter(
{
schemaIds: newSchemaIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[],
{
databaseWithSchemas:
databasesWithSchemas.includes(databaseType),
}
);
});
},
[allTables, databaseType, toggleTableFilterForNoSchema]
);
const addSchemaToFilter: DiagramFilterContext['addSchemaToFilter'] =
useCallback(
(schemaId: string) => {
setFilter((prev) => {
const currentSchemaIds = prev.schemaIds;
if (!currentSchemaIds) {
// No schemas are filtered
return prev;
}
// If schema is already filtered, do nothing
if (currentSchemaIds.includes(schemaId)) {
return prev;
}
// Add schema to the filter
const newSchemaIds = [...currentSchemaIds, schemaId];
if (newSchemaIds.length === allSchemasIds.length) {
// All schemas are now filtered, set to undefined
return {
...prev,
schemaIds: undefined,
} satisfies DiagramFilter;
}
return {
...prev,
schemaIds: newSchemaIds,
} satisfies DiagramFilter;
});
},
[allSchemasIds.length]
);
const hasActiveFilter: boolean = useMemo(() => {
return !!filter.schemaIds || !!filter.tableIds;
}, [filter]);
const schemasDisplayed: DiagramFilterContext['schemasDisplayed'] =
useMemo(() => {
if (!hasActiveFilter) {
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) =>
displayedSchemaIds.has(schema.id)
);
}, [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 = {
loading,
filter,
clearSchemaIdsFilter: clearSchemaIds,
setTableIdsFilterEmpty: setTableIdsEmpty,
clearTableIdsFilter: clearTableIds,
resetFilter,
toggleSchemaFilter,
toggleTableFilter,
addSchemaToFilter,
hasActiveFilter,
schemasDisplayed,
addTablesToFilter,
removeTablesFromFilter,
};
return (
<diagramFilterContext.Provider value={value}>
{children}
</diagramFilterContext.Provider>
);
};

View File

@@ -0,0 +1,4 @@
import { useContext } from 'react';
import { diagramFilterContext } from './diagram-filter-context';
export const useDiagramFilter = () => useContext(diagramFilterContext);

View File

@@ -32,14 +32,20 @@ export interface DiffContext {
originalDiagram: Diagram | null;
diffMap: DiffMap;
hasDiff: boolean;
isSummaryOnly: boolean;
calculateDiff: ({
diagram,
newDiagram,
options,
}: {
diagram: Diagram;
newDiagram: Diagram;
options?: {
summaryOnly?: boolean;
};
}) => void;
resetDiff: () => void;
// table diff
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
@@ -60,6 +66,15 @@ export interface DiffContext {
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
getFieldNewPrimaryKey: ({ fieldId }: { fieldId: string }) => boolean | null;
getFieldNewNullable: ({ fieldId }: { fieldId: string }) => boolean | null;
getFieldNewCharacterMaximumLength: ({
fieldId,
}: {
fieldId: string;
}) => string | null;
getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null;
getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null;
// relationship diff
checkIfNewRelationship: ({

View File

@@ -32,6 +32,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
const [fieldsChanged, setFieldsChanged] = React.useState<
Map<string, boolean>
>(new Map<string, boolean>());
const [isSummaryOnly, setIsSummaryOnly] = React.useState<boolean>(false);
const events = useEventEmitter<DiffEvent>();
@@ -127,7 +128,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
);
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
({ diagram, newDiagram: newDiagramArg }) => {
({ diagram, newDiagram: newDiagramArg, options }) => {
const {
diffMap: newDiffs,
changedTables: newChangedTables,
@@ -139,6 +140,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
setFieldsChanged(newChangedFields);
setNewDiagram(newDiagramArg);
setOriginalDiagram(diagram);
setIsSummaryOnly(options?.summaryOnly ?? false);
events.emit({
action: 'diff_calculated',
@@ -305,6 +307,117 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
[diffMap]
);
const getFieldNewPrimaryKey = useCallback<
DiffContext['getFieldNewPrimaryKey']
>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'primaryKey',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as boolean;
}
}
return null;
},
[diffMap]
);
const getFieldNewNullable = useCallback<DiffContext['getFieldNewNullable']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'nullable',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as boolean;
}
}
return null;
},
[diffMap]
);
const getFieldNewCharacterMaximumLength = useCallback<
DiffContext['getFieldNewCharacterMaximumLength']
>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'characterMaximumLength',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as string;
}
}
return null;
},
[diffMap]
);
const getFieldNewScale = useCallback<DiffContext['getFieldNewScale']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'scale',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as number;
}
}
return null;
},
[diffMap]
);
const getFieldNewPrecision = useCallback<
DiffContext['getFieldNewPrecision']
>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'precision',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as number;
}
}
return null;
},
[diffMap]
);
const checkIfNewRelationship = useCallback<
DiffContext['checkIfNewRelationship']
>(
@@ -339,6 +452,15 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
[diffMap]
);
const resetDiff = useCallback<DiffContext['resetDiff']>(() => {
setDiffMap(new Map<string, ChartDBDiff>());
setTablesChanged(new Map<string, boolean>());
setFieldsChanged(new Map<string, boolean>());
setNewDiagram(null);
setOriginalDiagram(null);
setIsSummaryOnly(false);
}, []);
return (
<diffContext.Provider
value={{
@@ -346,8 +468,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
originalDiagram,
diffMap,
hasDiff: diffMap.size > 0,
isSummaryOnly,
calculateDiff,
resetDiff,
// table diff
getTableNewName,
@@ -362,6 +486,11 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
checkIfNewField,
getFieldNewName,
getFieldNewType,
getFieldNewPrimaryKey,
getFieldNewNullable,
getFieldNewCharacterMaximumLength,
getFieldNewScale,
getFieldNewPrecision,
// relationship diff
checkIfNewRelationship,

View File

@@ -2,9 +2,9 @@ import { emptyFn } from '@/lib/utils';
import { createContext } from 'react';
export type SidebarSection =
| 'dbml'
| 'tables'
| 'relationships'
| 'dependencies'
| 'refs'
| 'areas'
| 'customTypes';
@@ -13,14 +13,16 @@ export interface LayoutContext {
openTableFromSidebar: (tableId: string) => void;
closeAllTablesInSidebar: () => void;
openedRelationshipInSidebar: string | undefined;
openRelationshipFromSidebar: (relationshipId: string) => void;
closeAllRelationshipsInSidebar: () => void;
openedDependencyInSidebar: string | undefined;
openDependencyFromSidebar: (dependencyId: string) => void;
closeAllDependenciesInSidebar: () => void;
openedRefInSidebar: string | undefined;
openRefFromSidebar: (refId: string) => void;
closeAllRefsInSidebar: () => void;
openedAreaInSidebar: string | undefined;
openAreaFromSidebar: (areaId: string) => void;
closeAllAreasInSidebar: () => void;
@@ -36,24 +38,22 @@ export interface LayoutContext {
hideSidePanel: () => void;
showSidePanel: () => void;
toggleSidePanel: () => void;
isSelectSchemaOpen: boolean;
openSelectSchema: () => void;
closeSelectSchema: () => void;
}
export const layoutContext = createContext<LayoutContext>({
openedTableInSidebar: undefined,
selectedSidebarSection: 'tables',
openedRelationshipInSidebar: undefined,
openRelationshipFromSidebar: emptyFn,
closeAllRelationshipsInSidebar: emptyFn,
openedDependencyInSidebar: undefined,
openDependencyFromSidebar: emptyFn,
closeAllDependenciesInSidebar: emptyFn,
openedRefInSidebar: undefined,
openRefFromSidebar: emptyFn,
closeAllRefsInSidebar: emptyFn,
openedAreaInSidebar: undefined,
openAreaFromSidebar: emptyFn,
closeAllAreasInSidebar: emptyFn,
@@ -70,8 +70,4 @@ export const layoutContext = createContext<LayoutContext>({
hideSidePanel: emptyFn,
showSidePanel: emptyFn,
toggleSidePanel: emptyFn,
isSelectSchemaOpen: false,
openSelectSchema: emptyFn,
closeSelectSchema: emptyFn,
});

View File

@@ -10,10 +10,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
const [openedTableInSidebar, setOpenedTableInSidebar] = React.useState<
string | undefined
>();
const [openedRelationshipInSidebar, setOpenedRelationshipInSidebar] =
React.useState<string | undefined>();
const [openedDependencyInSidebar, setOpenedDependencyInSidebar] =
React.useState<string | undefined>();
const [openedRefInSidebar, setOpenedRefInSidebar] = React.useState<
string | undefined
>();
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
string | undefined
>();
@@ -23,17 +22,18 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
React.useState<SidebarSection>('tables');
const [isSidePanelShowed, setIsSidePanelShowed] =
React.useState<boolean>(isDesktop);
const [isSelectSchemaOpen, setIsSelectSchemaOpen] =
React.useState<boolean>(false);
const closeAllTablesInSidebar: LayoutContext['closeAllTablesInSidebar'] =
() => setOpenedTableInSidebar('');
const closeAllRelationshipsInSidebar: LayoutContext['closeAllRelationshipsInSidebar'] =
() => setOpenedRelationshipInSidebar('');
() => setOpenedRefInSidebar('');
const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] =
() => setOpenedDependencyInSidebar('');
() => setOpenedRefInSidebar('');
const closeAllRefsInSidebar: LayoutContext['closeAllRefsInSidebar'] = () =>
setOpenedRefInSidebar('');
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
() => setOpenedAreaInSidebar('');
@@ -62,17 +62,23 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
const openRelationshipFromSidebar: LayoutContext['openRelationshipFromSidebar'] =
(relationshipId) => {
showSidePanel();
setSelectedSidebarSection('relationships');
setOpenedRelationshipInSidebar(relationshipId);
setSelectedSidebarSection('refs');
setOpenedRefInSidebar(relationshipId);
};
const openDependencyFromSidebar: LayoutContext['openDependencyFromSidebar'] =
(dependencyId) => {
showSidePanel();
setSelectedSidebarSection('dependencies');
setOpenedDependencyInSidebar(dependencyId);
setSelectedSidebarSection('refs');
setOpenedRefInSidebar(dependencyId);
};
const openRefFromSidebar: LayoutContext['openRefFromSidebar'] = (refId) => {
showSidePanel();
setSelectedSidebarSection('refs');
setOpenedRefInSidebar(refId);
};
const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = (
areaId
) => {
@@ -88,11 +94,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
setOpenedTableInSidebar(customTypeId);
};
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
setIsSelectSchemaOpen(true);
const closeSelectSchema: LayoutContext['closeSelectSchema'] = () =>
setIsSelectSchemaOpen(false);
return (
<layoutContext.Provider
value={{
@@ -100,7 +101,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
selectedSidebarSection,
openTableFromSidebar,
selectSidebarSection: setSelectedSidebarSection,
openedRelationshipInSidebar,
openRelationshipFromSidebar,
closeAllTablesInSidebar,
closeAllRelationshipsInSidebar,
@@ -108,12 +108,11 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
hideSidePanel,
showSidePanel,
toggleSidePanel,
isSelectSchemaOpen,
openSelectSchema,
closeSelectSchema,
openedDependencyInSidebar,
openDependencyFromSidebar,
closeAllDependenciesInSidebar,
openedRefInSidebar,
openRefFromSidebar,
closeAllRefsInSidebar,
openedAreaInSidebar,
openAreaFromSidebar,
closeAllAreasInSidebar,

View File

@@ -4,8 +4,6 @@ import type { Theme } from '../theme-context/theme-context';
export type ScrollAction = 'pan' | 'zoom';
export type SchemasFilter = Record<string, string[]>;
export interface LocalConfigContext {
theme: Theme;
setTheme: (theme: Theme) => void;
@@ -13,8 +11,8 @@ export interface LocalConfigContext {
scrollAction: ScrollAction;
setScrollAction: (action: ScrollAction) => void;
schemasFilter: SchemasFilter;
setSchemasFilter: React.Dispatch<React.SetStateAction<SchemasFilter>>;
showDBViews: boolean;
setShowDBViews: (showViews: boolean) => void;
showCardinality: boolean;
setShowCardinality: (showCardinality: boolean) => void;
@@ -22,20 +20,12 @@ export interface LocalConfigContext {
showFieldAttributes: boolean;
setShowFieldAttributes: (showFieldAttributes: boolean) => void;
hideMultiSchemaNotification: boolean;
setHideMultiSchemaNotification: (
hideMultiSchemaNotification: boolean
) => void;
githubRepoOpened: boolean;
setGithubRepoOpened: (githubRepoOpened: boolean) => void;
starUsDialogLastOpen: number;
setStarUsDialogLastOpen: (lastOpen: number) => void;
showDependenciesOnCanvas: boolean;
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
showMiniMapOnCanvas: boolean;
setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void;
}
@@ -47,8 +37,8 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
scrollAction: 'pan',
setScrollAction: emptyFn,
schemasFilter: {},
setSchemasFilter: emptyFn,
showDBViews: false,
setShowDBViews: emptyFn,
showCardinality: true,
setShowCardinality: emptyFn,
@@ -56,18 +46,12 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
showFieldAttributes: true,
setShowFieldAttributes: emptyFn,
hideMultiSchemaNotification: false,
setHideMultiSchemaNotification: emptyFn,
githubRepoOpened: false,
setGithubRepoOpened: emptyFn,
starUsDialogLastOpen: 0,
setStarUsDialogLastOpen: emptyFn,
showDependenciesOnCanvas: false,
setShowDependenciesOnCanvas: emptyFn,
showMiniMapOnCanvas: false,
setShowMiniMapOnCanvas: emptyFn,
});

View File

@@ -1,18 +1,16 @@
import React, { useEffect } from 'react';
import type { SchemasFilter, ScrollAction } from './local-config-context';
import type { ScrollAction } from './local-config-context';
import { LocalConfigContext } from './local-config-context';
import type { Theme } from '../theme-context/theme-context';
const themeKey = 'theme';
const scrollActionKey = 'scroll_action';
const schemasFilterKey = 'schemas_filter';
const showCardinalityKey = 'show_cardinality';
const showFieldAttributesKey = 'show_field_attributes';
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
const githubRepoOpenedKey = 'github_repo_opened';
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
const showDBViewsKey = 'show_db_views';
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -25,10 +23,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
(localStorage.getItem(scrollActionKey) as ScrollAction) || 'pan'
);
const [schemasFilter, setSchemasFilter] = React.useState<SchemasFilter>(
JSON.parse(
localStorage.getItem(schemasFilterKey) || '{}'
) as SchemasFilter
const [showDBViews, setShowDBViews] = React.useState<boolean>(
(localStorage.getItem(showDBViewsKey) || 'false') === 'true'
);
const [showCardinality, setShowCardinality] = React.useState<boolean>(
@@ -40,12 +36,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
(localStorage.getItem(showFieldAttributesKey) || 'true') === 'true'
);
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
React.useState<boolean>(
(localStorage.getItem(hideMultiSchemaNotificationKey) ||
'false') === 'true'
);
const [githubRepoOpened, setGithubRepoOpened] = React.useState<boolean>(
(localStorage.getItem(githubRepoOpenedKey) || 'false') === 'true'
);
@@ -55,12 +45,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
);
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
React.useState<boolean>(
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
'true'
);
const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] =
React.useState<boolean>(
(localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true'
@@ -77,13 +61,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
}, [githubRepoOpened]);
useEffect(() => {
localStorage.setItem(
hideMultiSchemaNotificationKey,
hideMultiSchemaNotification.toString()
);
}, [hideMultiSchemaNotification]);
useEffect(() => {
localStorage.setItem(themeKey, theme);
}, [theme]);
@@ -93,20 +70,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
}, [scrollAction]);
useEffect(() => {
localStorage.setItem(schemasFilterKey, JSON.stringify(schemasFilter));
}, [schemasFilter]);
localStorage.setItem(showDBViewsKey, showDBViews.toString());
}, [showDBViews]);
useEffect(() => {
localStorage.setItem(showCardinalityKey, showCardinality.toString());
}, [showCardinality]);
useEffect(() => {
localStorage.setItem(
showDependenciesOnCanvasKey,
showDependenciesOnCanvas.toString()
);
}, [showDependenciesOnCanvas]);
useEffect(() => {
localStorage.setItem(
showMiniMapOnCanvasKey,
@@ -121,20 +91,16 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
setTheme,
scrollAction,
setScrollAction,
schemasFilter,
setSchemasFilter,
showDBViews,
setShowDBViews,
showCardinality,
setShowCardinality,
showFieldAttributes,
setShowFieldAttributes,
hideMultiSchemaNotification,
setHideMultiSchemaNotification,
setGithubRepoOpened,
githubRepoOpened,
starUsDialogLastOpen,
setStarUsDialogLastOpen,
showDependenciesOnCanvas,
setShowDependenciesOnCanvas,
showMiniMapOnCanvas,
setShowMiniMapOnCanvas,
}}

View File

@@ -7,12 +7,21 @@ import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
export interface StorageContext {
// Config operations
getConfig: () => Promise<ChartDBConfig | undefined>;
updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>;
// Diagram filter operations
getDiagramFilter: (diagramId: string) => Promise<DiagramFilter | undefined>;
updateDiagramFilter: (
diagramId: string,
filter: DiagramFilter
) => Promise<void>;
deleteDiagramFilter: (diagramId: string) => Promise<void>;
// Diagram operations
addDiagram: (params: { diagram: Diagram }) => Promise<void>;
listDiagrams: (options?: {
@@ -132,6 +141,10 @@ export const storageInitialValue: StorageContext = {
getConfig: emptyFn,
updateConfig: emptyFn,
getDiagramFilter: emptyFn,
updateDiagramFilter: emptyFn,
deleteDiagramFilter: emptyFn,
addDiagram: emptyFn,
listDiagrams: emptyFn,
getDiagram: emptyFn,

View File

@@ -10,6 +10,7 @@ import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -44,6 +45,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
ChartDBConfig & { id: number },
'id' // primary key "id" (for the typings only)
>;
diagram_filters: EntityTable<
DiagramFilter & { diagramId: string },
'diagramId' // primary key "id" (for the typings only)
>;
};
// Schema declaration:
@@ -190,6 +195,27 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
config: '++id, defaultDiagramId',
});
dexieDB
.version(12)
.stores({
diagrams:
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
db_tables:
'++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
db_relationships:
'++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
db_dependencies:
'++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
areas: '++id, diagramId, name, x, y, width, height, color',
db_custom_types:
'++id, diagramId, schema, type, kind, values, fields',
config: '++id, defaultDiagramId',
diagram_filters: 'diagramId, tableIds, schemasIds',
})
.upgrade((tx) => {
tx.table('config').clear();
});
dexieDB.on('ready', async () => {
const config = await dexieDB.config.get(1);
@@ -217,6 +243,34 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
[db]
);
const getDiagramFilter: StorageContext['getDiagramFilter'] = useCallback(
async (diagramId: string): Promise<DiagramFilter | undefined> => {
const filter = await db.diagram_filters.get({ diagramId });
return filter;
},
[db]
);
const updateDiagramFilter: StorageContext['updateDiagramFilter'] =
useCallback(
async (diagramId, filter): Promise<void> => {
await db.diagram_filters.put({
diagramId,
...filter,
});
},
[db]
);
const deleteDiagramFilter: StorageContext['deleteDiagramFilter'] =
useCallback(
async (diagramId: string): Promise<void> => {
await db.diagram_filters.where({ diagramId }).delete();
},
[db]
);
const addTable: StorageContext['addTable'] = useCallback(
async ({ diagramId, table }) => {
await db.db_tables.add({
@@ -710,6 +764,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
db.db_dependencies.where('diagramId').equals(id).delete(),
db.areas.where('diagramId').equals(id).delete(),
db.db_custom_types.where('diagramId').equals(id).delete(),
db.diagram_filters.where('diagramId').equals(id).delete(),
]);
},
[db]
@@ -756,6 +811,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
deleteCustomType,
listCustomTypes,
deleteDiagramCustomTypes,
getDiagramFilter,
updateDiagramFilter,
deleteDiagramFilter,
}}
>
{children}

View File

@@ -218,8 +218,14 @@ export const CreateRelationshipDialog: React.FC<
closeCreateRelationshipDialog();
}
}}
modal={false}
>
<DialogContent className="flex flex-col overflow-y-auto" showClose>
<DialogContent
className="flex flex-col overflow-y-auto"
showClose
forceOverlay
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>
{t('create_relationship_dialog.title')}

View File

@@ -20,12 +20,18 @@ import {
} from '@/lib/data/export-metadata/export-sql-script';
import { databaseTypeToLabelMap } from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type';
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
import { Annoyed, Sparkles } from 'lucide-react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
import type { Diagram } from '@/lib/domain/diagram';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
import {
filterDependency,
filterRelationship,
filterTable,
} from '@/lib/domain/diagram-filter/filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
export interface ExportSQLDialogProps extends BaseDialogProps {
targetDatabaseType: DatabaseType;
@@ -36,7 +42,8 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
targetDatabaseType,
}) => {
const { closeExportSQLDialog } = useDialog();
const { currentDiagram, filteredSchemas } = useChartDB();
const { currentDiagram } = useChartDB();
const { filter } = useDiagramFilter();
const { t } = useTranslation();
const [script, setScript] = React.useState<string>();
const [error, setError] = React.useState<boolean>(false);
@@ -48,7 +55,16 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
const filteredDiagram: Diagram = {
...currentDiagram,
tables: currentDiagram.tables?.filter((table) =>
shouldShowTablesBySchemaFilter(table, filteredSchemas)
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
),
relationships: currentDiagram.relationships?.filter((rel) => {
const sourceTable = currentDiagram.tables?.find(
@@ -60,11 +76,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
return (
sourceTable &&
targetTable &&
shouldShowTablesBySchemaFilter(
sourceTable,
filteredSchemas
) &&
shouldShowTablesBySchemaFilter(targetTable, filteredSchemas)
filterRelationship({
tableA: {
id: sourceTable.id,
schema: sourceTable.schema,
},
tableB: {
id: targetTable.id,
schema: targetTable.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
);
}),
dependencies: currentDiagram.dependencies?.filter((dep) => {
@@ -77,11 +102,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
return (
table &&
dependentTable &&
shouldShowTablesBySchemaFilter(table, filteredSchemas) &&
shouldShowTablesBySchemaFilter(
dependentTable,
filteredSchemas
)
filterDependency({
tableA: {
id: table.id,
schema: table.schema,
},
tableB: {
id: dependentTable.id,
schema: dependentTable.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[targetDatabaseType],
},
})
);
}),
};
@@ -101,7 +135,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
signal: abortControllerRef.current?.signal,
});
}
}, [targetDatabaseType, currentDiagram, filteredSchemas]);
}, [targetDatabaseType, currentDiagram, filter]);
useEffect(() => {
if (!dialog.open) {

View File

@@ -5,7 +5,7 @@ import React, {
Suspense,
useRef,
} from 'react';
import * as monaco from 'monaco-editor';
import type * as monaco from 'monaco-editor';
import { useDialog } from '@/hooks/use-dialog';
import {
Dialog,
@@ -36,45 +36,11 @@ import type { DBTable } from '@/lib/domain/db-table';
import { useToast } from '@/components/toast/use-toast';
import { Spinner } from '@/components/spinner/spinner';
import { debounce } from '@/lib/utils';
interface DBMLError {
message: string;
line: number;
column: number;
}
function parseDBMLError(error: unknown): DBMLError | null {
try {
if (typeof error === 'string') {
const parsed = JSON.parse(error);
if (parsed.diags?.[0]) {
const diag = parsed.diags[0];
return {
message: diag.message,
line: diag.location.start.line,
column: diag.location.start.column,
};
}
} else if (error && typeof error === 'object' && 'diags' in error) {
const parsed = error as {
diags: Array<{
message: string;
location: { start: { line: number; column: number } };
}>;
};
if (parsed.diags?.[0]) {
return {
message: parsed.diags[0].message,
line: parsed.diags[0].location.start.line,
column: parsed.diags[0].location.start.column,
};
}
}
} catch (e) {
console.error('Error parsing DBML error:', e);
}
return null;
}
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
import {
clearErrorHighlight,
highlightErrorLine,
} from '@/components/code-snippet/dbml/utils';
export interface ImportDBMLDialogProps extends BaseDialogProps {
withCreateEmptyDiagram?: boolean;
@@ -150,39 +116,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
}
}, [reorder, reorderTables]);
const highlightErrorLine = useCallback((error: DBMLError) => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;
const decorations = [
{
range: new monaco.Range(
error.line,
1,
error.line,
model.getLineMaxColumn(error.line)
),
options: {
isWholeLine: true,
className: 'dbml-error-line',
glyphMarginClassName: 'dbml-error-glyph',
hoverMessage: { value: error.message },
overviewRuler: {
color: '#ff0000',
position: monaco.editor.OverviewRulerLane.Right,
darkColor: '#ff0000',
},
},
},
];
decorationsCollection.current?.set(decorations);
}, []);
const clearDecorations = useCallback(() => {
decorationsCollection.current?.clear();
clearErrorHighlight(decorationsCollection.current);
}, []);
const validateDBML = useCallback(
@@ -205,7 +140,12 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
t('import_dbml_dialog.error.description') +
` (1 error found - in line ${parsedError.line})`
);
highlightErrorLine(parsedError);
highlightErrorLine({
error: parsedError,
model: editorRef.current?.getModel(),
editorDecorationsCollection:
decorationsCollection.current,
});
} else {
setErrorMessage(
e instanceof Error ? e.message : JSON.stringify(e)
@@ -213,7 +153,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
}
}
},
[clearDecorations, highlightErrorLine, t]
[clearDecorations, t]
);
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);

View File

@@ -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>
);
};

View File

@@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useDebounce } from '@/hooks/use-debounce';
import { DiagramRowActionsMenu } from './diagram-row-actions-menu/diagram-row-actions-menu';
export interface OpenDiagramDialogProps extends BaseDialogProps {
canClose?: boolean;
@@ -50,17 +51,18 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
setSelectedDiagramId(undefined);
}, [dialog.open]);
const fetchDiagrams = useCallback(async () => {
const diagrams = await listDiagrams({ includeTables: true });
setDiagrams(
diagrams.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
)
);
}, [listDiagrams]);
useEffect(() => {
const fetchDiagrams = async () => {
const diagrams = await listDiagrams({ includeTables: true });
setDiagrams(
diagrams.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
)
);
};
fetchDiagrams();
}, [listDiagrams, setDiagrams, dialog.open]);
}, [fetchDiagrams, dialog.open]);
const openDiagram = useCallback(
(diagramId: string) => {
@@ -221,6 +223,19 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
<TableCell className="text-center">
{diagram.tables?.length}
</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>
))}
</TableBody>

View File

@@ -44,7 +44,7 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
allowSchemaCreation = false,
}) => {
const { t } = useTranslation();
const { databaseType, filteredSchemas, filterSchemas } = useChartDB();
const { databaseType } = useChartDB();
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
table?.schema
? schemaNameToSchemaId(table.schema)
@@ -93,7 +93,6 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
const { closeTableSchemaDialog } = useDialog();
const handleConfirm = useCallback(() => {
let createdSchemaId: string;
if (isCreatingNew && newSchemaName.trim()) {
const newSchema: DBSchema = {
id: schemaNameToSchemaId(newSchemaName.trim()),
@@ -101,30 +100,14 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
tableCount: 0,
};
createdSchemaId = newSchema.id;
onConfirm({ schema: newSchema });
} else {
const schema = schemas.find((s) => s.id === selectedSchemaId);
if (!schema) return;
createdSchemaId = schema.id;
onConfirm({ schema });
}
filterSchemas([
...(filteredSchemas ?? schemas.map((s) => s.id)),
createdSchemaId,
]);
}, [
onConfirm,
selectedSchemaId,
schemas,
isCreatingNew,
newSchemaName,
filteredSchemas,
filterSchemas,
]);
}, [onConfirm, selectedSchemaId, schemas, isCreatingNew, newSchemaName]);
const schemaOptions: SelectBoxOption[] = useMemo(
() =>

View File

@@ -155,3 +155,29 @@
background-size: 650%;
}
}
/* Edit button emphasis animation */
@keyframes dbml_edit-button-emphasis {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
background-color: rgba(59, 130, 246, 0);
}
50% {
transform: scale(1.1);
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
background-color: rgba(59, 130, 246, 0.1);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
background-color: rgba(59, 130, 246, 0);
}
}
.dbml-edit-button-emphasis {
animation: dbml_edit-button-emphasis 0.6s ease-in-out;
animation-iteration-count: 1;
position: relative;
z-index: 10;
}

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ar: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'جديد',
browse: 'تصفح',
tables: 'الجداول',
refs: 'المراجع',
areas: 'المناطق',
dependencies: 'التبعيات',
custom_types: 'الأنواع المخصصة',
},
menu: {
file: {
file: 'ملف',
new: 'جديد',
open: 'فتح',
databases: {
databases: 'قواعد البيانات',
new: 'مخطط جديد',
browse: 'تصفح...',
save: 'حفظ',
duplicate: 'تكرار',
import: 'استيراد قاعدة بيانات',
export_sql: 'SQL تصدير',
export_as: 'تصدير كـ',
delete_diagram: 'حذف الرسم البياني',
exit: 'خروج',
},
edit: {
edit: 'تحرير',
@@ -29,6 +38,7 @@ export const ar: LanguageTranslation = {
hide_field_attributes: 'إخفاء خصائص الحقل',
show_field_attributes: 'إظهار خصائص الحقل',
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
show_views: 'عروض قاعدة البيانات',
theme: 'المظهر',
show_dependencies: 'إظهار الاعتمادات',
hide_dependencies: 'إخفاء الاعتمادات',
@@ -72,15 +82,6 @@ export const ar: LanguageTranslation = {
cancel: 'إلغاء',
},
multiple_schemas_alert: {
title: 'مخططات متعددة',
description:
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
// TODO: Translate
show_me: 'Show me',
none: 'لا شيء',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'فشل النسخ',
@@ -115,14 +116,11 @@ export const ar: LanguageTranslation = {
copied: '!تم النسخ',
side_panel: {
schema: ':المخطط',
filter_by_schema: 'تصفية حسب المخطط',
search_schema: '...بحث في المخطط',
no_schemas_found: '.لم يتم العثور على مخططات',
view_all_options: '...عرض جميع الخيارات',
tables_section: {
tables: 'الجداول',
add_table: 'إضافة جدول',
add_view: 'إضافة عرض',
filter: 'تصفية',
collapse: 'طي الكل',
// TODO: Translate
@@ -148,6 +146,7 @@ export const ar: LanguageTranslation = {
field_actions: {
title: 'خصائص الحقل',
unique: 'فريد',
auto_increment: 'زيادة تلقائية',
comments: 'تعليقات',
no_comments: 'لا يوجد تعليقات',
delete_field: 'حذف الحقل',
@@ -162,6 +161,7 @@ export const ar: LanguageTranslation = {
title: 'خصائص الفهرس',
name: 'الإسم',
unique: 'فريد',
index_type: 'نوع الفهرس',
delete_index: 'حذف الفهرس',
},
table_actions: {
@@ -178,12 +178,15 @@ export const ar: LanguageTranslation = {
description: 'أنشئ جدولاً للبدء',
},
},
relationships_section: {
relationships: 'العلاقات',
refs_section: {
refs: 'المراجع',
filter: 'تصفية',
add_relationship: 'إضافة علاقة',
collapse: 'طي الكل',
add_relationship: 'إضافة علاقة',
relationships: 'العلاقات',
dependencies: 'الاعتمادات',
relationship: {
relationship: 'العلاقة',
primary: 'الجدول الأساسي',
foreign: 'الجدول المرتبط',
cardinality: 'الكاردينالية',
@@ -193,16 +196,8 @@ export const ar: LanguageTranslation = {
delete_relationship: 'حذف',
},
},
empty_state: {
title: 'لا توجد علاقات',
description: 'إنشئ علاقة لربط الجداول',
},
},
dependencies_section: {
dependencies: 'الاعتمادات',
filter: 'تصفية',
collapse: 'طي الكل',
dependency: {
dependency: 'الاعتماد',
table: 'الجدول',
dependent_table: 'عرض الاعتمادات',
delete_dependency: 'حذف',
@@ -212,8 +207,8 @@ export const ar: LanguageTranslation = {
},
},
empty_state: {
title: 'لا توجد اعتمادات',
description: 'إنشاء اعتماد للبدء',
title: 'لا توجد علاقات',
description: 'إنشاء علاقة للبدء',
},
},
@@ -474,6 +469,7 @@ export const ar: LanguageTranslation = {
canvas_context_menu: {
new_table: 'جدول جديد',
new_view: 'عرض جديد',
new_relationship: 'علاقة جديدة',
// TODO: Translate
new_area: 'New Area',
@@ -495,6 +491,8 @@ export const ar: LanguageTranslation = {
language_select: {
change_language: 'اللغة',
},
on: 'تشغيل',
off: 'إيقاف',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const bn: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'নতুন',
browse: 'ব্রাউজ',
tables: 'টেবিল',
refs: 'রেফস',
areas: 'এলাকা',
dependencies: 'নির্ভরতা',
custom_types: 'কাস্টম টাইপ',
},
menu: {
file: {
file: 'ফাইল',
new: 'নতুন',
open: 'খুলুন',
databases: {
databases: 'ডাটাবেস',
new: 'নতুন ডায়াগ্রাম',
browse: 'ব্রাউজ করুন...',
save: 'সংরক্ষণ করুন',
duplicate: 'ডুপ্লিকেট করুন',
import: 'ডাটাবেস আমদানি করুন',
export_sql: 'SQL রপ্তানি করুন',
export_as: 'রূপে রপ্তানি করুন',
delete_diagram: 'ডায়াগ্রাম মুছুন',
exit: 'প্রস্থান করুন',
},
edit: {
edit: 'সম্পাদনা',
@@ -29,6 +38,7 @@ export const bn: LanguageTranslation = {
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
zoom_on_scroll: 'স্ক্রলে জুম করুন',
show_views: 'ডাটাবেস ভিউ',
theme: 'থিম',
show_dependencies: 'নির্ভরতাগুলি দেখান',
hide_dependencies: 'নির্ভরতাগুলি লুকান',
@@ -73,15 +83,6 @@ export const bn: LanguageTranslation = {
cancel: 'বাতিল করুন',
},
multiple_schemas_alert: {
title: 'বহু স্কিমা',
description:
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
// TODO: Translate
show_me: 'Show me',
none: 'কিছুই না',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'কপি ব্যর্থ হয়েছে',
@@ -116,14 +117,11 @@ export const bn: LanguageTranslation = {
copied: 'অনুলিপি সম্পন্ন!',
side_panel: {
schema: 'স্কিমা:',
filter_by_schema: 'স্কিমা দ্বারা ফিল্টার করুন',
search_schema: 'স্কিমা খুঁজুন...',
no_schemas_found: 'কোনো স্কিমা পাওয়া যায়নি।',
view_all_options: 'সমস্ত বিকল্প দেখুন...',
tables_section: {
tables: 'টেবিল',
add_table: 'টেবিল যোগ করুন',
add_view: 'ভিউ যোগ করুন',
filter: 'ফিল্টার',
collapse: 'সব ভাঁজ করুন',
// TODO: Translate
@@ -149,6 +147,7 @@ export const bn: LanguageTranslation = {
field_actions: {
title: 'ফিল্ড কর্ম',
unique: 'অদ্বিতীয়',
auto_increment: 'স্বয়ংক্রিয় বৃদ্ধি',
comments: 'মন্তব্য',
no_comments: 'কোনো মন্তব্য নেই',
delete_field: 'ফিল্ড মুছুন',
@@ -164,6 +163,7 @@ export const bn: LanguageTranslation = {
title: 'ইনডেক্স কর্ম',
name: 'নাম',
unique: 'অদ্বিতীয়',
index_type: 'ইনডেক্স ধরন',
delete_index: 'ইনডেক্স মুছুন',
},
table_actions: {
@@ -180,14 +180,17 @@ export const bn: LanguageTranslation = {
description: 'শুরু করতে একটি টেবিল তৈরি করুন',
},
},
relationships_section: {
relationships: 'সম্পর্ক',
refs_section: {
refs: 'রেফস',
filter: 'ফিল্টার',
add_relationship: 'সম্পর্ক যোগ করুন',
collapse: 'সব ভাঁজ করুন',
add_relationship: 'সম্পর্ক যোগ করুন',
relationships: 'সম্পর্ক',
dependencies: 'নির্ভরতাগুলি',
relationship: {
relationship: 'সম্পর্ক',
primary: 'প্রাথমিক টেবিল',
foreign: 'বিদেশি টেবিল',
foreign: 'রেফারেন্স করা টেবিল',
cardinality: 'কার্ডিনালিটি',
delete_relationship: 'মুছুন',
relationship_actions: {
@@ -195,27 +198,19 @@ export const bn: LanguageTranslation = {
delete_relationship: 'মুছুন',
},
},
empty_state: {
title: 'কোনো সম্পর্ক নেই',
description: 'টেবিল সংযোগ করতে একটি সম্পর্ক তৈরি করুন',
},
},
dependencies_section: {
dependencies: 'নির্ভরতাগুলি',
filter: 'ফিল্টার',
collapse: 'ভাঁজ করুন',
dependency: {
dependency: 'নির্ভরতা',
table: 'টেবিল',
dependent_table: 'নির্ভরশীল টেবিল',
delete_dependency: 'নির্ভরতা মুছুন',
dependent_table: 'নির্ভরশীল ভিউ',
delete_dependency: 'মুছুন',
dependency_actions: {
title: 'কর্ম',
delete_dependency: 'নির্ভরতা মুছুন',
delete_dependency: 'মুছুন',
},
},
empty_state: {
title: 'কোনো নির্ভরতাগুলি নেই',
description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।',
title: 'কোনো সম্পর্ক নেই',
description: 'শুরু করতে একটি সম্পর্ক তৈরি করুন',
},
},
@@ -479,6 +474,7 @@ export const bn: LanguageTranslation = {
canvas_context_menu: {
new_table: 'নতুন টেবিল',
new_view: 'নতুন ভিউ',
new_relationship: 'নতুন সম্পর্ক',
// TODO: Translate
new_area: 'New Area',
@@ -500,6 +496,9 @@ export const bn: LanguageTranslation = {
language_select: {
change_language: 'ভাষা পরিবর্তন করুন',
},
on: 'চালু',
off: 'বন্ধ',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const de: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Neu',
browse: 'Durchsuchen',
tables: 'Tabellen',
refs: 'Refs',
areas: 'Bereiche',
dependencies: 'Abhängigkeiten',
custom_types: 'Benutzerdefinierte Typen',
},
menu: {
file: {
file: 'Datei',
new: 'Neu',
open: 'Öffnen',
databases: {
databases: 'Datenbanken',
new: 'Neues Diagramm',
browse: 'Durchsuchen...',
save: 'Speichern',
duplicate: 'Diagramm duplizieren',
import: 'Datenbank importieren',
export_sql: 'SQL exportieren',
export_as: 'Exportieren als',
delete_diagram: 'Diagramm löschen',
exit: 'Beenden',
},
edit: {
edit: 'Bearbeiten',
@@ -29,6 +38,7 @@ export const de: LanguageTranslation = {
hide_field_attributes: 'Feldattribute ausblenden',
show_field_attributes: 'Feldattribute anzeigen',
zoom_on_scroll: 'Zoom beim Scrollen',
show_views: 'Datenbankansichten',
theme: 'Stil',
show_dependencies: 'Abhängigkeiten anzeigen',
hide_dependencies: 'Abhängigkeiten ausblenden',
@@ -73,15 +83,6 @@ export const de: LanguageTranslation = {
cancel: 'Abbrechen',
},
multiple_schemas_alert: {
title: 'Mehrere Schemas',
description:
'{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'Keine',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Kopieren fehlgeschlagen',
@@ -117,14 +118,11 @@ export const de: LanguageTranslation = {
copied: 'Kopiert!',
side_panel: {
schema: 'Schema:',
filter_by_schema: 'Nach Schema filtern',
search_schema: 'Schema suchen...',
no_schemas_found: 'Keine Schemas gefunden.',
view_all_options: 'Alle Optionen anzeigen...',
tables_section: {
tables: 'Tabellen',
add_table: 'Tabelle hinzufügen',
add_view: 'Ansicht hinzufügen',
filter: 'Filter',
collapse: 'Alle einklappen',
// TODO: Translate
@@ -150,6 +148,7 @@ export const de: LanguageTranslation = {
field_actions: {
title: 'Feldattribute',
unique: 'Eindeutig',
auto_increment: 'Automatisch hochzählen',
comments: 'Kommentare',
no_comments: 'Keine Kommentare',
delete_field: 'Feld löschen',
@@ -165,6 +164,7 @@ export const de: LanguageTranslation = {
title: 'Indexattribute',
name: 'Name',
unique: 'Eindeutig',
index_type: 'Indextyp',
delete_index: 'Index löschen',
},
table_actions: {
@@ -181,32 +181,26 @@ export const de: LanguageTranslation = {
description: 'Erstellen Sie eine Tabelle, um zu beginnen',
},
},
relationships_section: {
relationships: 'Beziehungen',
refs_section: {
refs: 'Refs',
filter: 'Filter',
add_relationship: 'Beziehung hinzufügen',
collapse: 'Alle einklappen',
add_relationship: 'Beziehung hinzufügen',
relationships: 'Beziehungen',
dependencies: 'Abhängigkeiten',
relationship: {
relationship: 'Beziehung',
primary: 'Primäre Tabelle',
foreign: 'Referenzierte Tabelle',
cardinality: 'Kardinalität',
delete_relationship: 'Beziehung löschen',
delete_relationship: 'Löschen',
relationship_actions: {
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: 'Abhängigkeit',
table: 'Tabelle',
dependent_table: 'Abhängige Ansicht',
delete_dependency: 'Löschen',
@@ -216,8 +210,8 @@ export const de: LanguageTranslation = {
},
},
empty_state: {
title: 'Keine Abhängigkeiten',
description: 'Erstellen Sie eine Ansicht, um zu beginnen',
title: 'Keine Beziehungen',
description: 'Erstellen Sie eine Beziehung, um zu beginnen',
},
},
@@ -311,7 +305,7 @@ export const de: LanguageTranslation = {
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).',
},
instructions_link: 'Brauchen Sie Hilfe? So gehts',
instructions_link: "Brauchen Sie Hilfe? So geht's",
check_script_result: 'Skriptergebnis überprüfen',
},
@@ -483,6 +477,7 @@ export const de: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Neue Tabelle',
new_view: 'Neue Ansicht',
new_relationship: 'Neue Beziehung',
// TODO: Translate
new_area: 'New Area',
@@ -505,6 +500,9 @@ export const de: LanguageTranslation = {
language_select: {
change_language: 'Sprache',
},
on: 'Ein',
off: 'Aus',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata } from '../types';
export const en = {
translation: {
editor_sidebar: {
new_diagram: 'New',
browse: 'Browse',
tables: 'Tables',
refs: 'Refs',
areas: 'Areas',
dependencies: 'Dependencies',
custom_types: 'Custom Types',
},
menu: {
file: {
file: 'File',
new: 'New',
open: 'Open',
databases: {
databases: 'Databases',
new: 'New Diagram',
browse: 'Browse...',
save: 'Save',
duplicate: 'Duplicate Diagram',
import: 'Import',
export_sql: 'Export SQL',
export_as: 'Export as',
delete_diagram: 'Delete Diagram',
exit: 'Exit',
},
edit: {
edit: 'Edit',
@@ -29,6 +38,7 @@ export const en = {
hide_field_attributes: 'Hide Field Attributes',
show_field_attributes: 'Show Field Attributes',
zoom_on_scroll: 'Zoom on Scroll',
show_views: 'Database Views',
theme: 'Theme',
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
@@ -71,14 +81,6 @@ export const en = {
cancel: 'Cancel',
},
multiple_schemas_alert: {
title: 'Multiple Schemas',
description:
'{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
show_me: 'Show me',
none: 'none',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Copy failed',
@@ -113,14 +115,11 @@ export const en = {
copied: 'Copied!',
side_panel: {
schema: 'Schema:',
filter_by_schema: 'Filter by schema',
search_schema: 'Search schema...',
no_schemas_found: 'No schemas found.',
view_all_options: 'View all Options...',
tables_section: {
tables: 'Tables',
add_table: 'Add Table',
add_view: 'Add View',
filter: 'Filter',
collapse: 'Collapse All',
clear: 'Clear Filter',
@@ -144,6 +143,7 @@ export const en = {
field_actions: {
title: 'Field Attributes',
unique: 'Unique',
auto_increment: 'Auto Increment',
character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
@@ -157,6 +157,7 @@ export const en = {
title: 'Index Attributes',
name: 'Name',
unique: 'Unique',
index_type: 'Index Type',
delete_index: 'Delete Index',
},
table_actions: {
@@ -173,12 +174,15 @@ export const en = {
description: 'Create a table to get started',
},
},
relationships_section: {
relationships: 'Relationships',
refs_section: {
refs: 'Refs',
filter: 'Filter',
add_relationship: 'Add Relationship',
collapse: 'Collapse All',
add_relationship: 'Add Relationship',
relationships: 'Relationships',
dependencies: 'Dependencies',
relationship: {
relationship: 'Relationship',
primary: 'Primary Table',
foreign: 'Referenced Table',
cardinality: 'Cardinality',
@@ -188,16 +192,8 @@ export const en = {
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',
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
@@ -207,8 +203,8 @@ export const en = {
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
title: 'No relationships',
description: 'Create a relationship to get started',
},
},
@@ -468,6 +464,7 @@ export const en = {
canvas_context_menu: {
new_table: 'New Table',
new_view: 'New View',
new_relationship: 'New Relationship',
new_area: 'New Area',
},
@@ -488,6 +485,9 @@ export const en = {
language_select: {
change_language: 'Language',
},
on: 'On',
off: 'Off',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const es: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Nuevo',
browse: 'Examinar',
tables: 'Tablas',
refs: 'Refs',
areas: 'Áreas',
dependencies: 'Dependencias',
custom_types: 'Tipos Personalizados',
},
menu: {
file: {
file: 'Archivo',
new: 'Nuevo',
open: 'Abrir',
databases: {
databases: 'Bases de Datos',
new: 'Nuevo Diagrama',
browse: 'Examinar...',
save: 'Guardar',
duplicate: 'Duplicar',
import: 'Importar Base de Datos',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Eliminar Diagrama',
exit: 'Salir',
},
edit: {
edit: 'Editar',
@@ -29,6 +38,7 @@ export const es: LanguageTranslation = {
show_sidebar: 'Mostrar Barra Lateral',
hide_sidebar: 'Ocultar Barra Lateral',
zoom_on_scroll: 'Zoom al Desplazarse',
show_views: 'Vistas de Base de Datos',
theme: 'Tema',
show_dependencies: 'Mostrar dependencias',
hide_dependencies: 'Ocultar dependencias',
@@ -106,14 +116,11 @@ export const es: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'Esquema:',
filter_by_schema: 'Filtrar por esquema',
search_schema: 'Buscar esquema...',
no_schemas_found: 'No se encontraron esquemas.',
view_all_options: 'Ver todas las opciones...',
tables_section: {
tables: 'Tablas',
add_table: 'Agregar Tabla',
add_view: 'Agregar Vista',
filter: 'Filtrar',
collapse: 'Colapsar Todo',
// TODO: Translate
@@ -139,6 +146,7 @@ export const es: LanguageTranslation = {
field_actions: {
title: 'Atributos del Campo',
unique: 'Único',
auto_increment: 'Autoincremento',
comments: 'Comentarios',
no_comments: 'Sin comentarios',
delete_field: 'Eliminar Campo',
@@ -154,6 +162,7 @@ export const es: LanguageTranslation = {
title: 'Atributos del Índice',
name: 'Nombre',
unique: 'Único',
index_type: 'Tipo de Índice',
delete_index: 'Eliminar Índice',
},
table_actions: {
@@ -170,14 +179,17 @@ export const es: LanguageTranslation = {
description: 'Crea una tabla para comenzar',
},
},
relationships_section: {
relationships: 'Relaciones',
add_relationship: 'Agregar Relación',
refs_section: {
refs: 'Refs',
filter: 'Filtrar',
collapse: 'Colapsar Todo',
add_relationship: 'Agregar Relación',
relationships: 'Relaciones',
dependencies: 'Dependencias',
relationship: {
primary: 'Primaria',
foreign: 'Foránea',
relationship: 'Relación',
primary: 'Tabla Primaria',
foreign: 'Tabla Referenciada',
cardinality: 'Cardinalidad',
delete_relationship: 'Eliminar',
relationship_actions: {
@@ -185,18 +197,10 @@ export const es: LanguageTranslation = {
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: 'Dependencia',
table: 'Tabla',
dependent_table: 'Vista dependiente',
dependent_table: 'Vista Dependiente',
delete_dependency: 'Eliminar',
dependency_actions: {
title: 'Acciones',
@@ -204,8 +208,8 @@ export const es: LanguageTranslation = {
},
},
empty_state: {
title: 'Sin dependencias',
description: 'Crea una vista para comenzar',
title: 'Sin relaciones',
description: 'Crea una relación para comenzar',
},
},
@@ -424,14 +428,6 @@ export const es: LanguageTranslation = {
confirm: '¡Claro!',
},
multiple_schemas_alert: {
title: 'Múltiples Esquemas',
description:
'{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'nada',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
@@ -480,6 +476,7 @@ export const es: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nueva Tabla',
new_view: 'Nueva Vista',
new_relationship: 'Nueva Relación',
// TODO: Translate
new_area: 'New Area',
@@ -502,6 +499,9 @@ export const es: LanguageTranslation = {
language_select: {
change_language: 'Idioma',
},
on: 'Encendido',
off: 'Apagado',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const fr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Nouveau',
browse: 'Parcourir',
tables: 'Tables',
refs: 'Refs',
areas: 'Zones',
dependencies: 'Dépendances',
custom_types: 'Types Personnalisés',
},
menu: {
file: {
file: 'Fichier',
new: 'Nouveau',
open: 'Ouvrir',
databases: {
databases: 'Bases de Données',
new: 'Nouveau Diagramme',
browse: 'Parcourir...',
save: 'Enregistrer',
duplicate: 'Dupliquer',
import: 'Importer Base de Données',
export_sql: 'Exporter SQL',
export_as: 'Exporter en tant que',
delete_diagram: 'Supprimer le Diagramme',
exit: 'Quitter',
},
edit: {
edit: 'Édition',
@@ -29,6 +38,7 @@ export const fr: LanguageTranslation = {
hide_field_attributes: 'Masquer les Attributs de Champ',
show_field_attributes: 'Afficher les Attributs de Champ',
zoom_on_scroll: 'Zoom sur le Défilement',
show_views: 'Vues de Base de Données',
theme: 'Thème',
show_dependencies: 'Afficher les Dépendances',
hide_dependencies: 'Masquer les Dépendances',
@@ -105,14 +115,11 @@ export const fr: LanguageTranslation = {
copied: 'Copié !',
side_panel: {
schema: 'Schéma:',
filter_by_schema: 'Filtrer par schéma',
search_schema: 'Rechercher un schéma...',
no_schemas_found: 'Aucun schéma trouvé.',
view_all_options: 'Voir toutes les Options...',
tables_section: {
tables: 'Tables',
add_table: 'Ajouter une Table',
add_view: 'Ajouter une Vue',
filter: 'Filtrer',
collapse: 'Réduire Tout',
clear: 'Effacer le Filtre',
@@ -137,6 +144,7 @@ export const fr: LanguageTranslation = {
field_actions: {
title: 'Attributs du Champ',
unique: 'Unique',
auto_increment: 'Auto-incrément',
comments: 'Commentaires',
no_comments: 'Pas de commentaires',
delete_field: 'Supprimer le Champ',
@@ -152,6 +160,7 @@ export const fr: LanguageTranslation = {
title: "Attributs de l'Index",
name: 'Nom',
unique: 'Unique',
index_type: "Type d'index",
delete_index: "Supprimer l'Index",
},
table_actions: {
@@ -168,12 +177,15 @@ export const fr: LanguageTranslation = {
description: 'Créez une table pour commencer',
},
},
relationships_section: {
relationships: 'Relations',
refs_section: {
refs: 'Refs',
filter: 'Filtrer',
add_relationship: 'Ajouter une Relation',
collapse: 'Réduire Tout',
add_relationship: 'Ajouter une Relation',
relationships: 'Relations',
dependencies: 'Dépendances',
relationship: {
relationship: 'Relation',
primary: 'Table Principale',
foreign: 'Table Référencée',
cardinality: 'Cardinalité',
@@ -183,16 +195,8 @@ export const fr: LanguageTranslation = {
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: 'Dépendance',
table: 'Table',
dependent_table: 'Vue Dépendante',
delete_dependency: 'Supprimer',
@@ -202,8 +206,8 @@ export const fr: LanguageTranslation = {
},
},
empty_state: {
title: 'Aucune dépendance',
description: 'Créez une vue pour commencer',
title: 'Aucune relation',
description: 'Créez une relation pour commencer',
},
},
@@ -357,15 +361,6 @@ export const fr: LanguageTranslation = {
transparent_description: 'Remove background color from image.',
},
multiple_schemas_alert: {
title: 'Schémas Multiples',
description:
'{{schemasCount}} schémas dans ce diagramme. Actuellement affiché(s) : {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'Aucun',
},
new_table_schema_dialog: {
title: 'Sélectionner un Schéma',
description:
@@ -477,6 +472,7 @@ export const fr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nouvelle Table',
new_view: 'Nouvelle Vue',
new_relationship: 'Nouvelle Relation',
// TODO: Translate
new_area: 'New Area',
@@ -499,6 +495,9 @@ export const fr: LanguageTranslation = {
language_select: {
change_language: 'Langue',
},
on: 'Activé',
off: 'Désactivé',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const gu: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'નવું',
browse: 'બ્રાઉજ',
tables: 'ટેબલો',
refs: 'રેફ્સ',
areas: 'ક્ષેત્રો',
dependencies: 'નિર્ભરતાઓ',
custom_types: 'કસ્ટમ ટાઇપ',
},
menu: {
file: {
file: 'ફાઇલ',
new: 'નવું',
open: 'ખોલો',
databases: {
databases: 'ડેટાબેસેસ',
new: 'નવું ડાયાગ્રામ',
browse: 'બ્રાઉજ કરો...',
save: 'સાચવો',
duplicate: 'ડુપ્લિકેટ',
import: 'ડેટાબેસ આયાત કરો',
export_sql: 'SQL નિકાસ કરો',
export_as: 'રૂપે નિકાસ કરો',
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
exit: 'બહાર જાઓ',
},
edit: {
edit: 'ફેરફાર',
@@ -29,6 +38,7 @@ export const gu: LanguageTranslation = {
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
show_views: 'ડેટાબેઝ વ્યૂઝ',
theme: 'થિમ',
show_dependencies: 'નિર્ભરતાઓ બતાવો',
hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
@@ -73,15 +83,6 @@ export const gu: LanguageTranslation = {
cancel: 'રદ કરો',
},
multiple_schemas_alert: {
title: 'કઈંક વધારે સ્કીમા',
description:
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'કઈ નહીં',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'નકલ નિષ્ફળ',
@@ -116,14 +117,11 @@ export const gu: LanguageTranslation = {
copied: 'નકલ થયું!',
side_panel: {
schema: 'સ્કીમા:',
filter_by_schema: 'સ્કીમા દ્વારા ફિલ્ટર કરો',
search_schema: 'સ્કીમા શોધો...',
no_schemas_found: 'કોઈ સ્કીમા મળ્યા નથી.',
view_all_options: 'બધા વિકલ્પો જુઓ...',
tables_section: {
tables: 'ટેબલ્સ',
add_table: 'ટેબલ ઉમેરો',
add_view: 'વ્યૂ ઉમેરો',
filter: 'ફિલ્ટર',
collapse: 'બધાને સકુચિત કરો',
// TODO: Translate
@@ -150,6 +148,7 @@ export const gu: LanguageTranslation = {
field_actions: {
title: 'ફીલ્ડ લક્ષણો',
unique: 'અદ્વિતીય',
auto_increment: 'ઑટો ઇન્ક્રિમેન્ટ',
comments: 'ટિપ્પણીઓ',
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
delete_field: 'ફીલ્ડ કાઢી નાખો',
@@ -165,6 +164,7 @@ export const gu: LanguageTranslation = {
title: 'ઇન્ડેક્સ લક્ષણો',
name: 'નામ',
unique: 'અદ્વિતીય',
index_type: 'ઇન્ડેક્સ પ્રકાર',
delete_index: 'ઇન્ડેક્સ કાઢી નાખો',
},
table_actions: {
@@ -181,14 +181,17 @@ export const gu: LanguageTranslation = {
description: 'શરૂ કરવા માટે એક ટેબલ બનાવો',
},
},
relationships_section: {
relationships: 'સંબંધો',
refs_section: {
refs: 'રેફ્સ',
filter: 'ફિલ્ટર',
add_relationship: 'સંબંધ ઉમેરો',
collapse: 'બધાને સકુચિત કરો',
add_relationship: 'સંબંધ ઉમેરો',
relationships: 'સંબંધો',
dependencies: 'નિર્ભરતાઓ',
relationship: {
relationship: 'સંબંધ',
primary: 'પ્રાથમિક ટેબલ',
foreign: 'સંદર્ભ ટેબલ',
foreign: 'સંદર્ભિત ટેબલ',
cardinality: 'કાર્ડિનાલિટી',
delete_relationship: 'કાઢી નાખો',
relationship_actions: {
@@ -196,27 +199,19 @@ export const gu: LanguageTranslation = {
delete_relationship: 'કાઢી નાખો',
},
},
empty_state: {
title: 'કોઈ સંબંધો નથી',
description: 'ટેબલ્સ કનેક્ટ કરવા માટે એક સંબંધ બનાવો',
},
},
dependencies_section: {
dependencies: 'નિર્ભરતાઓ',
filter: 'ફિલ્ટર',
collapse: 'સિકોડો',
dependency: {
dependency: 'નિર્ભરતા',
table: 'ટેબલ',
dependent_table: 'આધાર રાખેલું ટેબલ',
delete_dependency: 'નિર્ભરતા કાઢી નાખો',
dependent_table: 'નિર્ભરશીલ વ્યૂ',
delete_dependency: 'કાઢી નાખો',
dependency_actions: {
title: 'ક્રિયાઓ',
delete_dependency: 'નિર્ભરતા કાઢી નાખો',
delete_dependency: 'કાઢી નાખો',
},
},
empty_state: {
title: 'કોઈ નિર્ભરતાઓ નથી',
description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.',
title: 'કોઈ સંબંધો નથી',
description: 'શરૂ કરવા માટે એક સંબંધ બનાવો',
},
},
@@ -480,6 +475,7 @@ export const gu: LanguageTranslation = {
canvas_context_menu: {
new_table: 'નવું ટેબલ',
new_view: 'નવું વ્યૂ',
new_relationship: 'નવો સંબંધ',
// TODO: Translate
new_area: 'New Area',
@@ -501,6 +497,9 @@ export const gu: LanguageTranslation = {
language_select: {
change_language: 'ભાષા બદલો',
},
on: 'ચાલુ',
off: 'બંધ',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const hi: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नया',
browse: 'ब्राउज़',
tables: 'टेबल',
refs: 'रेफ्स',
areas: 'क्षेत्र',
dependencies: 'निर्भरताएं',
custom_types: 'कस्टम टाइप',
},
menu: {
file: {
file: 'फ़ाइल',
new: 'नया',
open: 'खोलें',
databases: {
databases: 'डेटाबेस',
new: 'नया आरेख',
browse: 'ब्राउज़ करें...',
save: 'सहेजें',
duplicate: 'डुप्लिकेट',
import: 'डेटाबेस आयात करें',
export_sql: 'SQL निर्यात करें',
export_as: 'के रूप में निर्यात करें',
delete_diagram: 'आरेख हटाएँ',
exit: 'बाहर जाएँ',
},
edit: {
edit: 'संपादित करें',
@@ -29,6 +38,7 @@ export const hi: LanguageTranslation = {
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
show_views: 'डेटाबेस व्यू',
theme: 'थीम',
show_dependencies: 'निर्भरता दिखाएँ',
hide_dependencies: 'निर्भरता छिपाएँ',
@@ -72,15 +82,6 @@ export const hi: LanguageTranslation = {
cancel: 'रद्द करें',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
// TODO: Translate
show_me: 'Show me',
none: 'कोई नहीं',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'कॉपी असफल',
@@ -116,14 +117,11 @@ export const hi: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारा फ़िल्टर करें',
search_schema: 'स्कीमा खोजें...',
no_schemas_found: 'कोई स्कीमा नहीं मिला।',
view_all_options: 'सभी विकल्प देखें...',
tables_section: {
tables: 'तालिकाएँ',
add_table: 'तालिका जोड़ें',
add_view: 'व्यू जोड़ें',
filter: 'फ़िल्टर',
collapse: 'सभी को संक्षिप्त करें',
// TODO: Translate
@@ -149,6 +147,7 @@ export const hi: LanguageTranslation = {
field_actions: {
title: 'फ़ील्ड विशेषताएँ',
unique: 'अद्वितीय',
auto_increment: 'ऑटो इंक्रीमेंट',
comments: 'टिप्पणियाँ',
no_comments: 'कोई टिप्पणी नहीं',
delete_field: 'फ़ील्ड हटाएँ',
@@ -164,6 +163,7 @@ export const hi: LanguageTranslation = {
title: 'सूचकांक विशेषताएँ',
name: 'नाम',
unique: 'अद्वितीय',
index_type: 'इंडेक्स प्रकार',
delete_index: 'सूचकांक हटाएँ',
},
table_actions: {
@@ -180,12 +180,15 @@ export const hi: LanguageTranslation = {
description: 'शुरू करने के लिए एक तालिका बनाएँ',
},
},
relationships_section: {
relationships: 'संबंध',
refs_section: {
refs: 'रेफ्स',
filter: 'फ़िल्टर',
add_relationship: 'संबंध जोड़ें',
collapse: 'सभी को संक्षिप्त करें',
add_relationship: 'संबंध जोड़ें',
relationships: 'संबंध',
dependencies: 'निर्भरताएँ',
relationship: {
relationship: 'संबंध',
primary: 'प्राथमिक तालिका',
foreign: 'संदर्भित तालिका',
cardinality: 'कार्डिनैलिटी',
@@ -195,28 +198,19 @@ export const hi: LanguageTranslation = {
delete_relationship: 'हटाएँ',
},
},
empty_state: {
title: 'कोई संबंध नहीं',
description:
'तालिकाओं को कनेक्ट करने के लिए एक संबंध बनाएँ',
},
},
dependencies_section: {
dependencies: 'निर्भरताएँ',
filter: 'फ़िल्टर',
collapse: 'सिकोड़ें',
dependency: {
dependency: 'निर्भरता',
table: 'तालिका',
dependent_table: 'आश्रित तालिका',
delete_dependency: 'निर्भरता हटाएँ',
dependent_table: 'आश्रित दृश्य',
delete_dependency: 'हटाएँ',
dependency_actions: {
title: 'कार्रवाइयाँ',
delete_dependency: 'निर्भरता हटाएँ',
title: 'क्रियाँ',
delete_dependency: 'हटाएँ',
},
},
empty_state: {
title: 'कोई निर्भरता नहीं',
description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।',
title: 'कोई संबंध नहीं',
description: 'शुरू करने के लिए एक संबंध बनाएँ',
},
},
@@ -483,6 +477,7 @@ export const hi: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नई तालिका',
new_view: 'नया व्यू',
new_relationship: 'नया संबंध',
// TODO: Translate
new_area: 'New Area',
@@ -505,6 +500,9 @@ export const hi: LanguageTranslation = {
language_select: {
change_language: 'भाषा बदलें',
},
on: 'चालू',
off: 'बंद',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const hr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Novi',
browse: 'Pregledaj',
tables: 'Tablice',
refs: 'Refs',
areas: 'Područja',
dependencies: 'Ovisnosti',
custom_types: 'Prilagođeni Tipovi',
},
menu: {
file: {
file: 'Datoteka',
new: 'Nova',
open: 'Otvori',
databases: {
databases: 'Baze Podataka',
new: 'Novi Dijagram',
browse: 'Pregledaj...',
save: 'Spremi',
duplicate: 'Dupliciraj dijagram',
import: 'Uvezi',
export_sql: 'Izvezi SQL',
export_as: 'Izvezi kao',
delete_diagram: 'Izbriši dijagram',
exit: 'Izađi',
},
edit: {
edit: 'Uredi',
@@ -29,6 +38,7 @@ export const hr: LanguageTranslation = {
hide_field_attributes: 'Sakrij atribute polja',
show_field_attributes: 'Prikaži atribute polja',
zoom_on_scroll: 'Zumiranje pri skrolanju',
show_views: 'Pogledi Baze Podataka',
theme: 'Tema',
show_dependencies: 'Prikaži ovisnosti',
hide_dependencies: 'Sakrij ovisnosti',
@@ -71,14 +81,6 @@ export const hr: LanguageTranslation = {
cancel: 'Odustani',
},
multiple_schemas_alert: {
title: 'Više shema',
description:
'{{schemasCount}} shema u ovom dijagramu. Trenutno prikazano: {{formattedSchemas}}.',
show_me: 'Prikaži mi',
none: 'nijedna',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Kopiranje neuspješno',
@@ -113,14 +115,11 @@ export const hr: LanguageTranslation = {
copied: 'Kopirano!',
side_panel: {
schema: 'Shema:',
filter_by_schema: 'Filtriraj po shemi',
search_schema: 'Pretraži shemu...',
no_schemas_found: 'Nema pronađenih shema.',
view_all_options: 'Prikaži sve opcije...',
tables_section: {
tables: 'Tablice',
add_table: 'Dodaj tablicu',
add_view: 'Dodaj Pogled',
filter: 'Filtriraj',
collapse: 'Sažmi sve',
clear: 'Očisti filter',
@@ -145,6 +144,7 @@ export const hr: LanguageTranslation = {
field_actions: {
title: 'Atributi polja',
unique: 'Jedinstven',
auto_increment: 'Automatsko povećavanje',
character_length: 'Maksimalna dužina',
precision: 'Preciznost',
scale: 'Skala',
@@ -158,6 +158,7 @@ export const hr: LanguageTranslation = {
title: 'Atributi indeksa',
name: 'Naziv',
unique: 'Jedinstven',
index_type: 'Vrsta indeksa',
delete_index: 'Izbriši indeks',
},
table_actions: {
@@ -174,12 +175,15 @@ export const hr: LanguageTranslation = {
description: 'Stvorite tablicu za početak',
},
},
relationships_section: {
relationships: 'Veze',
refs_section: {
refs: 'Refs',
filter: 'Filtriraj',
add_relationship: 'Dodaj vezu',
collapse: 'Sažmi sve',
add_relationship: 'Dodaj vezu',
relationships: 'Veze',
dependencies: 'Ovisnosti',
relationship: {
relationship: 'Veza',
primary: 'Primarna tablica',
foreign: 'Referentna tablica',
cardinality: 'Kardinalnost',
@@ -189,16 +193,8 @@ export const hr: LanguageTranslation = {
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: 'Ovisnost',
table: 'Tablica',
dependent_table: 'Ovisni pogled',
delete_dependency: 'Izbriši',
@@ -208,8 +204,8 @@ export const hr: LanguageTranslation = {
},
},
empty_state: {
title: 'Nema ovisnosti',
description: 'Stvorite pogled za početak',
title: 'Nema veze',
description: 'Stvorite vezu za početak',
},
},
@@ -473,6 +469,7 @@ export const hr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nova tablica',
new_view: 'Novi Pogled',
new_relationship: 'Nova veza',
new_area: 'Novo područje',
},
@@ -493,6 +490,9 @@ export const hr: LanguageTranslation = {
language_select: {
change_language: 'Jezik',
},
on: 'Uključeno',
off: 'Isključeno',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const id_ID: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Baru',
browse: 'Jelajahi',
tables: 'Tabel',
refs: 'Refs',
areas: 'Area',
dependencies: 'Ketergantungan',
custom_types: 'Tipe Kustom',
},
menu: {
file: {
file: 'Berkas',
new: 'Buat Baru',
open: 'Buka',
databases: {
databases: 'Basis Data',
new: 'Diagram Baru',
browse: 'Jelajahi...',
save: 'Simpan',
duplicate: 'Duplikat',
import: 'Impor Database',
export_sql: 'Ekspor SQL',
export_as: 'Ekspor Sebagai',
delete_diagram: 'Hapus Diagram',
exit: 'Keluar',
},
edit: {
edit: 'Ubah',
@@ -29,6 +38,7 @@ export const id_ID: LanguageTranslation = {
hide_field_attributes: 'Sembunyikan Atribut Kolom',
show_field_attributes: 'Tampilkan Atribut Kolom',
zoom_on_scroll: 'Perbesar saat Scroll',
show_views: 'Tampilan Database',
theme: 'Tema',
show_dependencies: 'Tampilkan Dependensi',
hide_dependencies: 'Sembunyikan Dependensi',
@@ -72,15 +82,6 @@ export const id_ID: LanguageTranslation = {
cancel: 'Batal',
},
multiple_schemas_alert: {
title: 'Schema Lebih dari satu',
description:
'{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'Tidak ada',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Gagal menyalin',
@@ -115,14 +116,11 @@ export const id_ID: LanguageTranslation = {
copied: 'Tersalin!',
side_panel: {
schema: 'Skema:',
filter_by_schema: 'Saring berdasarkan skema',
search_schema: 'Cari skema...',
no_schemas_found: 'Tidak ada skema yang ditemukan.',
view_all_options: 'Tampilkan Semua Pilihan...',
tables_section: {
tables: 'Tabel',
add_table: 'Tambah Tabel',
add_view: 'Tambah Tampilan',
filter: 'Saring',
collapse: 'Lipat Semua',
// TODO: Translate
@@ -148,6 +146,7 @@ export const id_ID: LanguageTranslation = {
field_actions: {
title: 'Atribut Kolom',
unique: 'Unik',
auto_increment: 'Kenaikan Otomatis',
comments: 'Komentar',
no_comments: 'Tidak ada komentar',
delete_field: 'Hapus Kolom',
@@ -163,6 +162,7 @@ export const id_ID: LanguageTranslation = {
title: 'Atribut Indeks',
name: 'Nama',
unique: 'Unik',
index_type: 'Tipe Indeks',
delete_index: 'Hapus Indeks',
},
table_actions: {
@@ -179,12 +179,15 @@ export const id_ID: LanguageTranslation = {
description: 'Buat tabel untuk memulai',
},
},
relationships_section: {
relationships: 'Hubungan',
refs_section: {
refs: 'Refs',
filter: 'Saring',
add_relationship: 'Tambah Hubungan',
collapse: 'Lipat Semua',
add_relationship: 'Tambah Hubungan',
relationships: 'Hubungan',
dependencies: 'Dependensi',
relationship: {
relationship: 'Hubungan',
primary: 'Tabel Primer',
foreign: 'Tabel Referensi',
cardinality: 'Kardinalitas',
@@ -194,16 +197,8 @@ export const id_ID: LanguageTranslation = {
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: 'Dependensi',
table: 'Tabel',
dependent_table: 'Tampilan Dependen',
delete_dependency: 'Hapus',
@@ -213,8 +208,8 @@ export const id_ID: LanguageTranslation = {
},
},
empty_state: {
title: 'Tidak ada dependensi',
description: 'Buat tampilan untuk memulai',
title: 'Tidak ada hubungan',
description: 'Buat hubungan untuk memulai',
},
},
@@ -479,6 +474,7 @@ export const id_ID: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Tabel Baru',
new_view: 'Tampilan Baru',
new_relationship: 'Hubungan Baru',
// TODO: Translate
new_area: 'New Area',
@@ -500,6 +496,9 @@ export const id_ID: LanguageTranslation = {
language_select: {
change_language: 'Bahasa',
},
on: 'Aktif',
off: 'Nonaktif',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ja: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '新規',
browse: '参照',
tables: 'テーブル',
refs: '参照',
areas: 'エリア',
dependencies: '依存関係',
custom_types: 'カスタムタイプ',
},
menu: {
file: {
file: 'ファイル',
new: '新',
open: '開く',
databases: {
databases: 'データベース',
new: '新しいダイアグラム',
browse: '参照...',
save: '保存',
duplicate: '複製',
import: 'データベースをインポート',
export_sql: 'SQLをエクスポート',
export_as: '形式を指定してエクスポート',
delete_diagram: 'ダイアグラムを削除',
exit: '終了',
},
edit: {
edit: '編集',
@@ -29,6 +38,7 @@ export const ja: LanguageTranslation = {
hide_field_attributes: 'フィールド属性を非表示',
show_field_attributes: 'フィールド属性を表示',
zoom_on_scroll: 'スクロールでズーム',
show_views: 'データベースビュー',
theme: 'テーマ',
// TODO: Translate
show_dependencies: 'Show Dependencies',
@@ -74,15 +84,6 @@ export const ja: LanguageTranslation = {
cancel: 'キャンセル',
},
multiple_schemas_alert: {
title: '複数のスキーマ',
description:
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
// TODO: Translate
show_me: 'Show me',
none: 'なし',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'コピー失敗',
@@ -119,14 +120,11 @@ export const ja: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'スキーマ:',
filter_by_schema: 'スキーマでフィルタ',
search_schema: 'スキーマを検索...',
no_schemas_found: 'スキーマが見つかりません。',
view_all_options: 'すべてのオプションを表示...',
tables_section: {
tables: 'テーブル',
add_table: 'テーブルを追加',
add_view: 'ビューを追加',
filter: 'フィルタ',
collapse: 'すべて折りたたむ',
// TODO: Translate
@@ -152,6 +150,7 @@ export const ja: LanguageTranslation = {
field_actions: {
title: 'フィールド属性',
unique: 'ユニーク',
auto_increment: 'オートインクリメント',
comments: 'コメント',
no_comments: 'コメントがありません',
delete_field: 'フィールドを削除',
@@ -167,6 +166,7 @@ export const ja: LanguageTranslation = {
title: 'インデックス属性',
name: '名前',
unique: 'ユニーク',
index_type: 'インデックスタイプ',
delete_index: 'インデックスを削除',
},
table_actions: {
@@ -183,12 +183,15 @@ export const ja: LanguageTranslation = {
description: 'テーブルを作成して開始してください',
},
},
relationships_section: {
relationships: 'リレーションシップ',
refs_section: {
refs: '参照',
filter: 'フィルタ',
add_relationship: 'リレーションシップを追加',
collapse: 'すべて折りたたむ',
add_relationship: 'リレーションシップを追加',
relationships: 'リレーションシップ',
dependencies: '依存関係',
relationship: {
relationship: 'リレーションシップ',
primary: '主テーブル',
foreign: '参照テーブル',
cardinality: 'カーディナリティ',
@@ -198,29 +201,20 @@ export const ja: LanguageTranslation = {
delete_relationship: '削除',
},
},
empty_state: {
title: 'リレーションシップがありません',
description:
'テーブルを接続するためにリレーションシップを作成してください',
},
},
// TODO: Translate
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
dependency: '依存関係',
table: 'テーブル',
dependent_table: '依存ビュー',
delete_dependency: '削除',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
title: '操作',
delete_dependency: '削除',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
title: 'リレーションシップがありません',
description:
'開始するためにリレーションシップを作成してください',
},
},
@@ -485,6 +479,7 @@ export const ja: LanguageTranslation = {
canvas_context_menu: {
new_table: '新しいテーブル',
new_view: '新しいビュー',
new_relationship: '新しいリレーションシップ',
// TODO: Translate
new_area: 'New Area',
@@ -507,6 +502,9 @@ export const ja: LanguageTranslation = {
language_select: {
change_language: '言語',
},
on: 'オン',
off: 'オフ',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ko_KR: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '새로 만들기',
browse: '찾아보기',
tables: '테이블',
refs: 'Refs',
areas: '영역',
dependencies: '종속성',
custom_types: '사용자 지정 타입',
},
menu: {
file: {
file: '파일',
databases: {
databases: '데이터베이스',
new: '새 다이어그램',
open: '열기',
browse: '찾아보기...',
save: '저장',
duplicate: '복사',
import: '데이터베이스 가져오기',
export_sql: 'SQL로 저장',
export_as: '다른 형식으로 저장',
delete_diagram: '다이어그램 삭제',
exit: '종료',
},
edit: {
edit: '편집',
@@ -29,6 +38,7 @@ export const ko_KR: LanguageTranslation = {
hide_field_attributes: '필드 속성 숨기기',
show_field_attributes: '필드 속성 보이기',
zoom_on_scroll: '스크롤 시 확대',
show_views: '데이터베이스 뷰',
theme: '테마',
show_dependencies: '종속성 보이기',
hide_dependencies: '종속성 숨기기',
@@ -72,15 +82,6 @@ export const ko_KR: LanguageTranslation = {
cancel: '취소',
},
multiple_schemas_alert: {
title: '다중 스키마',
description:
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: '없음',
},
copy_to_clipboard_toast: {
unsupported: {
title: '복사 실패',
@@ -115,14 +116,11 @@ export const ko_KR: LanguageTranslation = {
copied: '복사됨!',
side_panel: {
schema: '스키마:',
filter_by_schema: '스키마로 필터링',
search_schema: '스키마 검색...',
no_schemas_found: '스키마를 찾을 수 없습니다.',
view_all_options: '전체 옵션 보기...',
tables_section: {
tables: '테이블',
add_table: '테이블 추가',
add_view: '뷰 추가',
filter: '필터',
collapse: '모두 접기',
// TODO: Translate
@@ -148,6 +146,7 @@ export const ko_KR: LanguageTranslation = {
field_actions: {
title: '필드 속성',
unique: '유니크 여부',
auto_increment: '자동 증가',
comments: '주석',
no_comments: '주석 없음',
delete_field: '필드 삭제',
@@ -163,6 +162,7 @@ export const ko_KR: LanguageTranslation = {
title: '인덱스 속성',
name: '인덱스 명',
unique: '유니크 여부',
index_type: '인덱스 타입',
delete_index: '인덱스 삭제',
},
table_actions: {
@@ -179,12 +179,15 @@ export const ko_KR: LanguageTranslation = {
description: '테이블을 만들어 시작하세요.',
},
},
relationships_section: {
relationships: '연관 관계',
refs_section: {
refs: 'Refs',
filter: '필터',
add_relationship: '연관 관계 추가',
collapse: '모두 접기',
add_relationship: '연관 관계 추가',
relationships: '연관 관계',
dependencies: '종속성',
relationship: {
relationship: '연관 관계',
primary: '주 테이블',
foreign: '참조 테이블',
cardinality: '카디널리티',
@@ -194,16 +197,8 @@ export const ko_KR: LanguageTranslation = {
delete_relationship: '연관 관계 삭제',
},
},
empty_state: {
title: '연관 관계',
description: '테이블 연결을 위해 연관 관계를 생성하세요',
},
},
dependencies_section: {
dependencies: '종속성',
filter: '필터',
collapse: '모두 접기',
dependency: {
dependency: '종속성',
table: '테이블',
dependent_table: '뷰 테이블',
delete_dependency: '삭제',
@@ -213,8 +208,8 @@ export const ko_KR: LanguageTranslation = {
},
},
empty_state: {
title: '뷰 테이블 없음',
description: '뷰 테이블을 만들어 시작하세요.',
title: '연관 관계 없음',
description: '연관 관계를 만들어 시작하세요.',
},
},
@@ -476,6 +471,7 @@ export const ko_KR: LanguageTranslation = {
canvas_context_menu: {
new_table: '새 테이블',
new_view: '새 뷰',
new_relationship: '새 연관관계',
// TODO: Translate
new_area: 'New Area',
@@ -497,6 +493,9 @@ export const ko_KR: LanguageTranslation = {
language_select: {
change_language: '언어',
},
on: '켜기',
off: '끄기',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const mr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नवीन',
browse: 'ब्राउज',
tables: 'टेबल',
refs: 'Refs',
areas: 'क्षेत्रे',
dependencies: 'अवलंबने',
custom_types: 'कस्टम प्रकार',
},
menu: {
file: {
file: 'फाइल',
new: 'नवीन',
open: 'उघडा',
databases: {
databases: 'डेटाबेस',
new: 'नवीन आरेख',
browse: 'ब्राउज करा...',
save: 'जतन करा',
duplicate: 'डुप्लिकेट',
import: 'डेटाबेस इम्पोर्ट करा',
export_sql: 'SQL एक्स्पोर्ट करा',
export_as: 'म्हणून एक्स्पोर्ट करा',
delete_diagram: 'आरेख हटवा',
exit: 'बाहेर पडा',
},
edit: {
edit: 'संपादन करा',
@@ -29,6 +38,7 @@ export const mr: LanguageTranslation = {
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
zoom_on_scroll: 'स्क्रोलवर झूम करा',
show_views: 'डेटाबेस व्ह्यूज',
theme: 'थीम',
show_dependencies: 'डिपेंडेन्सि दाखवा',
hide_dependencies: 'डिपेंडेन्सि लपवा',
@@ -73,15 +83,6 @@ export const mr: LanguageTranslation = {
cancel: 'रद्द करा',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'काहीही नाही',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'कॉपी अयशस्वी',
@@ -118,14 +119,11 @@ export const mr: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारे फिल्टर करा',
search_schema: 'स्कीमा शोधा...',
no_schemas_found: 'कोणतेही स्कीमा सापडले नाहीत.',
view_all_options: 'सर्व पर्याय पहा...',
tables_section: {
tables: 'टेबल्स',
add_table: 'टेबल जोडा',
add_view: 'व्ह्यू जोडा',
filter: 'फिल्टर',
collapse: 'सर्व संकुचित करा',
// TODO: Translate
@@ -151,6 +149,7 @@ export const mr: LanguageTranslation = {
field_actions: {
title: 'फील्ड गुणधर्म',
unique: 'युनिक',
auto_increment: 'ऑटो इंक्रिमेंट',
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा',
@@ -166,6 +165,7 @@ export const mr: LanguageTranslation = {
title: 'इंडेक्स गुणधर्म',
name: 'नाव',
unique: 'युनिक',
index_type: 'इंडेक्स प्रकार',
delete_index: 'इंडेक्स हटवा',
},
table_actions: {
@@ -183,12 +183,15 @@ export const mr: LanguageTranslation = {
description: 'सुरू करण्यासाठी एक टेबल तयार करा',
},
},
relationships_section: {
relationships: 'रिलेशनशिप',
refs_section: {
refs: 'Refs',
filter: 'फिल्टर',
add_relationship: 'रिलेशनशिप जोडा',
collapse: 'सर्व संकुचित करा',
add_relationship: 'रिलेशनशिप जोडा',
relationships: 'रिलेशनशिप',
dependencies: 'डिपेंडेन्सि',
relationship: {
relationship: 'रिलेशनशिप',
primary: 'प्राथमिक टेबल',
foreign: 'रेफरंस टेबल',
cardinality: 'कार्डिनॅलिटी',
@@ -198,17 +201,8 @@ export const mr: LanguageTranslation = {
delete_relationship: 'हटवा',
},
},
empty_state: {
title: 'कोणतेही रिलेशनशिप नाहीत',
description:
'टेबल्स कनेक्ट करण्यासाठी एक रिलेशनशिप तयार करा',
},
},
dependencies_section: {
dependencies: 'डिपेंडेन्सि',
filter: 'फिल्टर',
collapse: 'सर्व संकुचित करा',
dependency: {
dependency: 'डिपेंडेन्सि',
table: 'टेबल',
dependent_table: 'डिपेंडेन्सि दृश्य',
delete_dependency: 'हटवा',
@@ -218,8 +212,8 @@ export const mr: LanguageTranslation = {
},
},
empty_state: {
title: 'कोणत्याही डिपेंडेन्सि नाहीत',
description: 'सुरू करण्यासाठी एक दृश्य तयार करा',
title: 'कोणतेही रिलेशनशिप नाहीत',
description: 'सुरू करण्यासाठी एक रिलेशनशिप तयार करा',
},
},
@@ -489,6 +483,7 @@ export const mr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नवीन टेबल',
new_view: 'नवीन व्ह्यू',
new_relationship: 'नवीन रिलेशनशिप',
// TODO: Translate
new_area: 'New Area',
@@ -512,6 +507,9 @@ export const mr: LanguageTranslation = {
language_select: {
change_language: 'भाषा बदला',
},
on: 'चालू',
off: 'बंद',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ne: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नयाँ',
browse: 'ब्राउज',
tables: 'टेबलहरू',
refs: 'Refs',
areas: 'क्षेत्रहरू',
dependencies: 'निर्भरताहरू',
custom_types: 'कस्टम प्रकारहरू',
},
menu: {
file: {
file: 'फाइल',
new: 'नयाँ',
open: 'खोल्नुहोस्',
databases: {
databases: 'डाटाबेसहरू',
new: 'नयाँ डायाग्राम',
browse: 'ब्राउज गर्नुहोस्...',
save: 'सुरक्षित गर्नुहोस्',
duplicate: 'डुप्लिकेट',
import: 'डाटाबेस आयात गर्नुहोस्',
export_sql: 'SQL निर्यात गर्नुहोस्',
export_as: 'निर्यात गर्नुहोस्',
delete_diagram: 'डायाग्राम हटाउनुहोस्',
exit: 'बाहिर निस्कनुहोस्',
},
edit: {
edit: 'सम्पादन',
@@ -29,6 +38,7 @@ export const ne: LanguageTranslation = {
hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
show_views: 'डाटाबेस भ्यूहरू',
theme: 'थिम',
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
@@ -73,15 +83,6 @@ export const ne: LanguageTranslation = {
cancel: 'रद्द गर्नुहोस्',
},
multiple_schemas_alert: {
title: 'विविध स्कीमहरू',
description:
'{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
// TODO: Translate
show_me: 'Show me',
none: 'कुनै पनि छैन',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'प्रतिलिपि असफल',
@@ -116,14 +117,11 @@ export const ne: LanguageTranslation = {
copied: 'प्रतिलिपि गरियो!',
side_panel: {
schema: 'स्कीम:',
filter_by_schema: 'स्कीम अनुसार फिल्टर गर्नुहोस्',
search_schema: 'स्कीम खोज्नुहोस्...',
no_schemas_found: 'कुनै स्कीमहरू फेला परेनन्',
view_all_options: 'सबै विकल्पहरू हेर्नुहोस्',
tables_section: {
tables: 'तालिकाहरू',
add_table: 'तालिका थप्नुहोस्',
add_view: 'भ्यू थप्नुहोस्',
filter: 'फिल्टर',
collapse: 'सबै लुकाउनुहोस्',
// TODO: Translate
@@ -149,6 +147,7 @@ export const ne: LanguageTranslation = {
field_actions: {
title: 'क्षेत्र विशेषताहरू',
unique: 'अनन्य',
auto_increment: 'स्वचालित वृद्धि',
comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्',
delete_field: 'क्षेत्र हटाउनुहोस्',
@@ -164,6 +163,7 @@ export const ne: LanguageTranslation = {
title: 'सूचक विशेषताहरू',
name: 'नाम',
unique: 'अनन्य',
index_type: 'इन्डेक्स प्रकार',
delete_index: 'सूचक हटाउनुहोस्',
},
table_actions: {
@@ -180,12 +180,15 @@ export const ne: LanguageTranslation = {
description: 'सुरु गर्नका लागि एक तालिका बनाउनुहोस्',
},
},
relationships_section: {
relationships: 'सम्बन्धहरू',
refs_section: {
refs: 'Refs',
filter: 'फिल्टर',
add_relationship: 'सम्बन्ध थप्नुहोस्',
collapse: 'सबै लुकाउनुहोस्',
add_relationship: 'सम्बन्ध थप्नुहोस्',
relationships: 'सम्बन्धहरू',
dependencies: 'डिपेन्डेन्सीहरू',
relationship: {
relationship: 'सम्बन्ध',
primary: 'मुख्य तालिका',
foreign: 'परिचित तालिका',
cardinality: 'कार्डिन्यालिटी',
@@ -195,16 +198,8 @@ export const ne: LanguageTranslation = {
delete_relationship: 'हटाउनुहोस्',
},
},
empty_state: {
title: 'कुनै सम्बन्धहरू छैनन्',
description: 'तालिकाहरू जोड्नका लागि एक सम्बन्ध बनाउनुहोस्',
},
},
dependencies_section: {
dependencies: 'डिपेन्डेन्सीहरू',
filter: 'फिल्टर',
collapse: 'सबै लुकाउनुहोस्',
dependency: {
dependency: 'डिपेन्डेन्सी',
table: 'तालिका',
dependent_table: 'विचलित तालिका',
delete_dependency: 'हटाउनुहोस्',
@@ -214,9 +209,8 @@ export const ne: LanguageTranslation = {
},
},
empty_state: {
title: 'कुनै डिपेन्डेन्सीहरू छैनन्',
description:
'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्',
title: 'कुनै सम्बन्धहरू छैनन्',
description: 'सुरु गर्नका लागि एक सम्बन्ध बनाउनुहोस्',
},
},
@@ -483,6 +477,7 @@ export const ne: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नयाँ तालिका',
new_view: 'नयाँ भ्यू',
new_relationship: 'नयाँ सम्बन्ध',
// TODO: Translate
new_area: 'New Area',
@@ -504,6 +499,9 @@ export const ne: LanguageTranslation = {
language_select: {
change_language: 'भाषा परिवर्तन गर्नुहोस्',
},
on: 'सक्रिय',
off: 'निष्क्रिय',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const pt_BR: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Novo',
browse: 'Navegar',
tables: 'Tabelas',
refs: 'Refs',
areas: 'Áreas',
dependencies: 'Dependências',
custom_types: 'Tipos Personalizados',
},
menu: {
file: {
file: 'Arquivo',
new: 'Novo',
open: 'Abrir',
databases: {
databases: 'Bancos de Dados',
new: 'Novo Diagrama',
browse: 'Navegar...',
save: 'Salvar',
duplicate: 'Duplicar',
import: 'Importar Banco de Dados',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Excluir Diagrama',
exit: 'Sair',
},
edit: {
edit: 'Editar',
@@ -29,6 +38,7 @@ export const pt_BR: LanguageTranslation = {
hide_field_attributes: 'Ocultar Atributos de Campo',
show_field_attributes: 'Mostrar Atributos de Campo',
zoom_on_scroll: 'Zoom ao Rolar',
show_views: 'Visualizações do Banco de Dados',
theme: 'Tema',
show_dependencies: 'Mostrar Dependências',
hide_dependencies: 'Ocultar Dependências',
@@ -73,15 +83,6 @@ export const pt_BR: LanguageTranslation = {
cancel: 'Cancelar',
},
multiple_schemas_alert: {
title: 'Múltiplos Esquemas',
description:
'{{schemasCount}} esquemas neste diagrama. Atualmente exibindo: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'nenhum',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Falha na cópia',
@@ -116,14 +117,11 @@ export const pt_BR: LanguageTranslation = {
copied: 'Copiado!',
side_panel: {
schema: 'Esquema:',
filter_by_schema: 'Filtrar por esquema',
search_schema: 'Buscar esquema...',
no_schemas_found: 'Nenhum esquema encontrado.',
view_all_options: 'Ver todas as Opções...',
tables_section: {
tables: 'Tabelas',
add_table: 'Adicionar Tabela',
add_view: 'Adicionar Visualização',
filter: 'Filtrar',
collapse: 'Colapsar Todas',
// TODO: Translate
@@ -149,6 +147,7 @@ export const pt_BR: LanguageTranslation = {
field_actions: {
title: 'Atributos do Campo',
unique: 'Único',
auto_increment: 'Incremento Automático',
comments: 'Comentários',
no_comments: 'Sem comentários',
delete_field: 'Excluir Campo',
@@ -164,6 +163,7 @@ export const pt_BR: LanguageTranslation = {
title: 'Atributos do Índice',
name: 'Nome',
unique: 'Único',
index_type: 'Tipo de Índice',
delete_index: 'Excluir Índice',
},
table_actions: {
@@ -180,12 +180,15 @@ export const pt_BR: LanguageTranslation = {
description: 'Crie uma tabela para começar',
},
},
relationships_section: {
relationships: 'Relacionamentos',
refs_section: {
refs: 'Refs',
filter: 'Filtrar',
add_relationship: 'Adicionar Relacionamento',
collapse: 'Colapsar Todas',
add_relationship: 'Adicionar Relacionamento',
relationships: 'Relacionamentos',
dependencies: 'Dependências',
relationship: {
relationship: 'Relacionamento',
primary: 'Tabela Primária',
foreign: 'Tabela Referenciada',
cardinality: 'Cardinalidade',
@@ -195,16 +198,8 @@ export const pt_BR: LanguageTranslation = {
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: 'Dependência',
table: 'Tabela',
dependent_table: 'Visualização Dependente',
delete_dependency: 'Excluir',
@@ -214,8 +209,8 @@ export const pt_BR: LanguageTranslation = {
},
},
empty_state: {
title: 'Sem dependências',
description: 'Crie uma visualização para começar',
title: 'Sem relacionamentos',
description: 'Crie um relacionamento para começar',
},
},
@@ -481,6 +476,7 @@ export const pt_BR: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nova Tabela',
new_view: 'Nova Visualização',
new_relationship: 'Novo Relacionamento',
// TODO: Translate
new_area: 'New Area',
@@ -503,6 +499,9 @@ export const pt_BR: LanguageTranslation = {
language_select: {
change_language: 'Idioma',
},
on: 'Ligado',
off: 'Desligado',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ru: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Новая',
browse: 'Обзор',
tables: 'Таблицы',
refs: 'Ссылки',
areas: 'Области',
dependencies: 'Зависимости',
custom_types: 'Пользовательские типы',
},
menu: {
file: {
file: 'Файл',
new: 'Создать',
open: 'Открыть',
databases: {
databases: 'Базы данных',
new: 'Новая диаграмма',
browse: 'Обзор...',
save: 'Сохранить',
duplicate: 'Дублировать',
import: 'Импортировать базу данных',
export_sql: 'Экспорт SQL',
export_as: 'Экспортировать как',
delete_diagram: 'Удалить диаграмму',
exit: 'Выход',
},
edit: {
edit: 'Изменение',
@@ -29,6 +38,7 @@ export const ru: LanguageTranslation = {
show_field_attributes: 'Показать атрибуты поля',
hide_field_attributes: 'Скрыть атрибуты поля',
zoom_on_scroll: 'Увеличение при прокрутке',
show_views: 'Представления базы данных',
theme: 'Тема',
show_dependencies: 'Показать зависимости',
hide_dependencies: 'Скрыть зависимости',
@@ -71,15 +81,6 @@ export const ru: LanguageTranslation = {
cancel: 'Отменить',
},
multiple_schemas_alert: {
title: 'Множественные схемы',
description:
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'никто',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Ошибка копирования',
@@ -113,14 +114,11 @@ export const ru: LanguageTranslation = {
show_less: 'Показать меньше',
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фильтр по схеме',
search_schema: 'Схема поиска...',
no_schemas_found: 'Схемы не найдены.',
view_all_options: 'Просмотреть все варианты...',
tables_section: {
tables: 'Таблицы',
add_table: 'Добавить таблицу',
add_view: 'Добавить представление',
filter: 'Фильтр',
collapse: 'Свернуть все',
clear: 'Очистить фильтр',
@@ -146,6 +144,7 @@ export const ru: LanguageTranslation = {
field_actions: {
title: 'Атрибуты поля',
unique: 'Уникальный',
auto_increment: 'Автоинкремент',
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
@@ -160,6 +159,7 @@ export const ru: LanguageTranslation = {
title: 'Атрибуты индекса',
name: 'Имя',
unique: 'Уникальный',
index_type: 'Тип индекса',
delete_index: 'Удалить индекс',
},
table_actions: {
@@ -176,12 +176,15 @@ export const ru: LanguageTranslation = {
description: 'Создайте таблицу, чтобы начать',
},
},
relationships_section: {
relationships: 'Отношения',
refs_section: {
refs: 'Ссылки',
filter: 'Фильтр',
add_relationship: 'Добавить отношение',
collapse: 'Свернуть все',
add_relationship: 'Добавить отношение',
relationships: 'Отношения',
dependencies: 'Зависимости',
relationship: {
relationship: 'Отношение',
primary: 'Основная таблица',
foreign: 'Справочная таблица',
cardinality: 'Тип множественной связи',
@@ -191,18 +194,10 @@ export const ru: LanguageTranslation = {
delete_relationship: 'Удалить',
},
},
empty_state: {
title: 'Нет отношений',
description: 'Создайте связь для соединения таблиц',
},
},
dependencies_section: {
dependencies: 'Зависимости',
filter: 'Фильтр',
collapse: 'Свернуть все',
dependency: {
table: 'Стол',
dependent_table: 'Зависимый вид',
dependency: 'Зависимость',
table: 'Таблица',
dependent_table: 'Зависимое представление',
delete_dependency: 'Удалить',
dependency_actions: {
title: 'Действия',
@@ -210,8 +205,8 @@ export const ru: LanguageTranslation = {
},
},
empty_state: {
title: 'Нет зависимостей',
description: 'Создайте представление, чтобы начать',
title: 'Нет отношений',
description: 'Создайте отношение, чтобы начать',
},
},
@@ -477,6 +472,7 @@ export const ru: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Создать таблицу',
new_view: 'Новое представление',
new_relationship: 'Создать отношение',
new_area: 'Новая область',
},
@@ -498,6 +494,9 @@ export const ru: LanguageTranslation = {
language_select: {
change_language: 'Сменить язык',
},
on: 'Вкл',
off: 'Выкл',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const te: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'కొత్తది',
browse: 'బ్రాఉజ్',
tables: 'టేబల్లు',
refs: 'సంబంధాలు',
areas: 'ప్రదేశాలు',
dependencies: 'ఆధారతలు',
custom_types: 'కస్టమ్ టైప్స్',
},
menu: {
file: {
file: 'ఫైల్',
new: 'కొత్తది',
open: 'తెరవు',
databases: {
databases: 'డేటాబేస్లు',
new: 'కొత్త డైగ్రాం',
browse: 'బ్రాఉజ్ చేయండి...',
save: 'సేవ్',
duplicate: 'డుప్లికేట్',
import: 'డేటాబేస్‌ను దిగుమతి చేసుకోండి',
export_sql: 'SQL ఎగుమతి',
export_as: 'వగా ఎగుమతి చేయండి',
delete_diagram: 'చిత్రాన్ని తొలగించండి',
exit: 'నిష్క్రమించు',
},
edit: {
edit: 'సవరించు',
@@ -29,6 +38,7 @@ export const te: LanguageTranslation = {
show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
zoom_on_scroll: 'స్క్రోల్‌పై జూమ్',
show_views: 'డేటాబేస్ వ్యూలు',
theme: 'థీమ్',
show_dependencies: 'ఆధారాలు చూపించండి',
hide_dependencies: 'ఆధారాలను దాచండి',
@@ -73,15 +83,6 @@ export const te: LanguageTranslation = {
cancel: 'రద్దు',
},
multiple_schemas_alert: {
title: 'బహుళ స్కీమాలు',
description:
'{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'ఎదరికాదు',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'కాపీ విఫలమైంది',
@@ -116,14 +117,11 @@ export const te: LanguageTranslation = {
copied: 'కాపీ చేయబడింది!',
side_panel: {
schema: 'స్కీమా:',
filter_by_schema: 'స్కీమా ద్వారా ఫిల్టర్ చేయండి',
search_schema: 'స్కీమా కోసం శోధించండి...',
no_schemas_found: 'ఏ స్కీమాలు కూడా కనుగొనబడలేదు.',
view_all_options: 'అన్ని ఎంపికలను చూడండి...',
tables_section: {
tables: 'పట్టికలు',
add_table: 'పట్టికను జోడించు',
add_view: 'వ్యూ జోడించండి',
filter: 'ఫిల్టర్',
collapse: 'అన్ని కూల్ చేయి',
// TODO: Translate
@@ -149,6 +147,7 @@ export const te: LanguageTranslation = {
field_actions: {
title: 'ఫీల్డ్ గుణాలు',
unique: 'అద్వితీయ',
auto_increment: 'ఆటో ఇంక్రిమెంట్',
comments: 'వ్యాఖ్యలు',
no_comments: 'వ్యాఖ్యలు లేవు',
delete_field: 'ఫీల్డ్ తొలగించు',
@@ -164,6 +163,7 @@ export const te: LanguageTranslation = {
title: 'ఇండెక్స్ గుణాలు',
name: 'పేరు',
unique: 'అద్వితీయ',
index_type: 'ఇండెక్స్ రకం',
delete_index: 'ఇండెక్స్ తొలగించు',
},
table_actions: {
@@ -181,12 +181,15 @@ export const te: LanguageTranslation = {
description: 'ప్రారంభించడానికి ఒక పట్టిక సృష్టించండి',
},
},
relationships_section: {
relationships: 'సంబంధాలు',
refs_section: {
refs: 'Refs',
filter: 'ఫిల్టర్',
add_relationship: 'సంబంధం జోడించు',
collapse: 'అన్ని కూల్ చేయి',
add_relationship: 'సంబంధం జోడించు',
relationships: 'సంబంధాలు',
dependencies: 'ఆధారాలు',
relationship: {
relationship: 'సంబంధం',
primary: 'ప్రాథమిక పట్టిక',
foreign: 'సూచించబడిన పట్టిక',
cardinality: 'కార్డినాలిటీ',
@@ -196,16 +199,8 @@ export const te: LanguageTranslation = {
delete_relationship: 'సంబంధం తొలగించు',
},
},
empty_state: {
title: 'సంబంధాలు లేవు',
description: 'పట్టికలను అనుసంధించడానికి సంబంధం సృష్టించండి',
},
},
dependencies_section: {
dependencies: 'ఆధారాలు',
filter: 'ఫిల్టర్',
collapse: 'అన్ని కూల్ చేయి',
dependency: {
dependency: 'ఆధారం',
table: 'పట్టిక',
dependent_table: 'ఆధారిత వీక్షణ',
delete_dependency: 'ఆధారాన్ని తొలగించు',
@@ -215,8 +210,8 @@ export const te: LanguageTranslation = {
},
},
empty_state: {
title: 'ఆధారాలు లేవు',
description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి',
title: 'సంబంధాలు లేవు',
description: 'ప్రారంభించడానికి ఒక సంబంధం సృష్టించండి',
},
},
@@ -485,6 +480,7 @@ export const te: LanguageTranslation = {
canvas_context_menu: {
new_table: 'కొత్త పట్టిక',
new_view: 'కొత్త వ్యూ',
new_relationship: 'కొత్త సంబంధం',
// TODO: Translate
new_area: 'New Area',
@@ -508,6 +504,9 @@ export const te: LanguageTranslation = {
language_select: {
change_language: 'భాష మార్చు',
},
on: 'ఆన్',
off: 'ఆఫ్',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const tr: LanguageTranslation = {
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: {
file: {
file: 'Dosya',
new: 'Yeni',
open: '',
databases: {
databases: 'Veritabanları',
new: 'Yeni Diyagram',
browse: 'Gözat...',
save: 'Kaydet',
duplicate: 'Kopyala',
import: 'Veritabanı İçe Aktar',
export_sql: 'SQL Olarak Dışa Aktar',
export_as: 'Olarak Dışa Aktar',
delete_diagram: 'Diyagramı Sil',
exit: ıkış',
},
edit: {
edit: 'Düzenle',
@@ -29,6 +38,7 @@ export const tr: LanguageTranslation = {
show_field_attributes: 'Alan Özelliklerini Göster',
hide_field_attributes: 'Alan Özelliklerini Gizle',
zoom_on_scroll: 'Kaydırarak Yakınlaştır',
show_views: 'Veritabanı Görünümleri',
theme: 'Tema',
show_dependencies: 'Bağımlılıkları Göster',
hide_dependencies: 'Bağımlılıkları Gizle',
@@ -73,15 +83,6 @@ export const tr: LanguageTranslation = {
cancel: 'İptal',
},
multiple_schemas_alert: {
title: 'Birden Fazla Şema',
description:
'Bu diyagramda {{schemasCount}} şema var. Şu anda görüntülenen: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'yok',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Kopyalama başarısız',
@@ -115,14 +116,11 @@ export const tr: LanguageTranslation = {
copy_to_clipboard: 'Panoya Kopyala',
copied: 'Kopyalandı!',
side_panel: {
schema: 'Şema:',
filter_by_schema: 'Şemaya Göre Filtrele',
search_schema: 'Şema ara...',
no_schemas_found: 'Şema bulunamadı.',
view_all_options: 'Tüm Seçenekleri Gör...',
tables_section: {
tables: 'Tablolar',
add_table: 'Tablo Ekle',
add_view: 'Görünüm Ekle',
filter: 'Filtrele',
collapse: 'Hepsini Daralt',
// TODO: Translate
@@ -148,6 +146,7 @@ export const tr: LanguageTranslation = {
field_actions: {
title: 'Alan Özellikleri',
unique: 'Tekil',
auto_increment: 'Otomatik Artış',
comments: 'Yorumlar',
no_comments: 'Yorum yok',
delete_field: 'Alanı Sil',
@@ -163,6 +162,7 @@ export const tr: LanguageTranslation = {
title: 'İndeks Özellikleri',
name: 'Ad',
unique: 'Tekil',
index_type: 'İndeks Türü',
delete_index: 'İndeksi Sil',
},
table_actions: {
@@ -180,12 +180,15 @@ export const tr: LanguageTranslation = {
description: 'Başlamak için bir tablo oluşturun',
},
},
relationships_section: {
relationships: 'İlişkiler',
refs_section: {
refs: 'Refs',
filter: 'Filtrele',
add_relationship: 'İlişki Ekle',
collapse: 'Hepsini Daralt',
add_relationship: 'İlişki Ekle',
relationships: 'İlişkiler',
dependencies: 'Bağımlılıklar',
relationship: {
relationship: 'İlişki',
primary: 'Birincil Tablo',
foreign: 'Referans Tablo',
cardinality: 'Kardinalite',
@@ -195,16 +198,8 @@ export const tr: LanguageTranslation = {
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: 'Bağımlılık',
table: 'Tablo',
dependent_table: 'Bağımlı Görünüm',
delete_dependency: 'Sil',
@@ -214,8 +209,8 @@ export const tr: LanguageTranslation = {
},
},
empty_state: {
title: 'Bağımlılık yok',
description: 'Başlamak için bir görünüm oluşturun',
title: 'İlişki yok',
description: 'Başlamak için bir ilişki oluşturun',
},
},
@@ -470,6 +465,7 @@ export const tr: LanguageTranslation = {
},
canvas_context_menu: {
new_table: 'Yeni Tablo',
new_view: 'Yeni Görünüm',
new_relationship: 'Yeni İlişki',
// TODO: Translate
new_area: 'New Area',
@@ -492,6 +488,9 @@ export const tr: LanguageTranslation = {
language_select: {
change_language: 'Dil',
},
on: 'Açık',
off: 'Kapalı',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const uk: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Нова',
browse: 'Огляд',
tables: 'Таблиці',
refs: 'Зв’язки',
areas: 'Області',
dependencies: 'Залежності',
custom_types: 'Користувацькі типи',
},
menu: {
file: {
file: 'Файл',
new: 'Новий',
open: 'Відкрити',
databases: {
databases: 'Бази даних',
new: 'Нова діаграма',
browse: 'Огляд...',
save: 'Зберегти',
duplicate: 'Дублювати',
import: 'Імпорт бази даних',
export_sql: 'Експорт SQL',
export_as: 'Експортувати як',
delete_diagram: 'Видалити діаграму',
exit: 'Вийти',
},
edit: {
edit: 'Редагувати',
@@ -29,6 +38,7 @@ export const uk: LanguageTranslation = {
show_field_attributes: 'Показати атрибути полів',
hide_field_attributes: 'Приховати атрибути полів',
zoom_on_scroll: 'Масштабувати прокручуванням',
show_views: 'Представлення бази даних',
theme: 'Тема',
show_dependencies: 'Показати залежності',
hide_dependencies: 'Приховати залежності',
@@ -71,15 +81,6 @@ export const uk: LanguageTranslation = {
cancel: 'Скасувати',
},
multiple_schemas_alert: {
title: 'Кілька схем',
description:
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'немає',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Помилка копіювання',
@@ -114,14 +115,11 @@ export const uk: LanguageTranslation = {
copied: 'Скопійовано!',
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фільтрувати за схемою',
search_schema: 'Пошук схеми…',
no_schemas_found: 'Схеми не знайдено.',
view_all_options: 'Переглянути всі параметри…',
tables_section: {
tables: 'Таблиці',
add_table: 'Додати таблицю',
add_view: 'Додати представлення',
filter: 'Фільтр',
collapse: 'Згорнути все',
// TODO: Translate
@@ -147,6 +145,7 @@ export const uk: LanguageTranslation = {
field_actions: {
title: 'Атрибути полів',
unique: 'Унікальне',
auto_increment: 'Автоінкремент',
comments: 'Коментарі',
no_comments: 'Немає коментарів',
delete_field: 'Видалити поле',
@@ -162,6 +161,7 @@ export const uk: LanguageTranslation = {
title: 'Атрибути індексу',
name: 'Назва індекса',
unique: 'Унікальний',
index_type: 'Тип індексу',
delete_index: 'Видалити індекс',
},
table_actions: {
@@ -178,12 +178,15 @@ export const uk: LanguageTranslation = {
description: 'Щоб почати, створіть таблицю',
},
},
relationships_section: {
relationships: 'Звʼязки',
refs_section: {
refs: 'Refs',
filter: 'Фільтр',
add_relationship: 'Додати звʼязок',
collapse: 'Згорнути все',
add_relationship: 'Додати звʼязок',
relationships: 'Звʼязки',
dependencies: 'Залежності',
relationship: {
relationship: 'Звʼязок',
primary: 'Первинна таблиця',
foreign: 'Посилання на таблицю',
cardinality: 'Звʼязок',
@@ -193,16 +196,8 @@ export const uk: LanguageTranslation = {
delete_relationship: 'Видалити',
},
},
empty_state: {
title: 'Звʼязків немає',
description: 'Створіть звʼязок для зʼєднання таблиць',
},
},
dependencies_section: {
dependencies: 'Залежності',
filter: 'Фільтр',
collapse: 'Згорнути все',
dependency: {
dependency: 'Залежність',
table: 'Таблиця',
dependent_table: 'Залежне подання',
delete_dependency: 'Видалити',
@@ -212,8 +207,8 @@ export const uk: LanguageTranslation = {
},
},
empty_state: {
title: 'Жодних залежностей',
description: 'Створіть подання, щоб почати',
title: 'Жодних зв’язків',
description: 'Створіть зв’язок, щоб почати',
},
},
@@ -476,6 +471,7 @@ export const uk: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Нова таблиця',
new_view: 'Нове представлення',
new_relationship: 'Новий звʼязок',
// TODO: Translate
new_area: 'New Area',
@@ -497,6 +493,9 @@ export const uk: LanguageTranslation = {
language_select: {
change_language: 'Мова',
},
on: 'Увімк',
off: 'Вимк',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const vi: LanguageTranslation = {
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: {
file: {
file: 'Tệp',
new: 'Tạo mới',
open: 'Mở',
databases: {
databases: 'Cơ sở dữ liệu',
new: 'Sơ đồ mới',
browse: 'Duyệt...',
save: 'Lưu',
duplicate: 'Nhân đôi',
import: 'Nhập cơ sở dữ liệu',
export_sql: 'Xuất SQL',
export_as: 'Xuất thành',
delete_diagram: 'Xóa sơ đồ',
exit: 'Thoát',
},
edit: {
edit: 'Sửa',
@@ -29,6 +38,7 @@ export const vi: LanguageTranslation = {
show_field_attributes: 'Hiển thị 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',
show_views: 'Chế độ xem Cơ sở dữ liệu',
theme: 'Chủ đề',
show_dependencies: 'Hiển thị các phụ thuộc',
hide_dependencies: 'Ẩn các phụ thuộc',
@@ -72,15 +82,6 @@ export const vi: LanguageTranslation = {
cancel: 'Hủy',
},
multiple_schemas_alert: {
title: 'Có nhiều lược đồ',
description:
'Có {{schemasCount}} lược đồ trong sơ đồ này. Hiện đang hiển thị: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'không có',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Sao chép thất bại',
@@ -115,14 +116,11 @@ export const vi: LanguageTranslation = {
copied: 'Đã sao chép!',
side_panel: {
schema: 'Lược đồ:',
filter_by_schema: 'Lọc bởi lược đồ',
search_schema: 'Tìm kiếm lược đồ...',
no_schemas_found: 'Không tìm thấy lược đồ.',
view_all_options: 'Xem tất cả tùy chọn...',
tables_section: {
tables: 'Bảng',
add_table: 'Thêm bảng',
add_view: 'Thêm Chế độ xem',
filter: 'Lọc',
collapse: 'Thu gọn tất cả',
// TODO: Translate
@@ -148,6 +146,7 @@ export const vi: LanguageTranslation = {
field_actions: {
title: 'Thuộc tính trường',
unique: 'Giá trị duy nhất',
auto_increment: 'Tự động tăng',
comments: 'Bình luận',
no_comments: 'Không có bình luận',
delete_field: 'Xóa trường',
@@ -163,6 +162,7 @@ export const vi: LanguageTranslation = {
title: 'Thuộc tính chỉ mục',
name: 'Tên',
unique: 'Giá trị duy nhất',
index_type: 'Loại chỉ mục',
delete_index: 'Xóa chỉ mục',
},
table_actions: {
@@ -179,12 +179,15 @@ export const vi: LanguageTranslation = {
description: 'Tạo một bảng để bắt đầu',
},
},
relationships_section: {
relationships: 'Quan hệ',
refs_section: {
refs: 'Refs',
filter: 'Lọc',
add_relationship: 'Thêm quan hệ',
collapse: 'Thu gọn tất cả',
add_relationship: 'Thêm quan hệ',
relationships: 'Quan hệ',
dependencies: 'Phụ thuộc',
relationship: {
relationship: 'Quan hệ',
primary: 'Bảng khóa chính',
foreign: 'Bảng khóa ngoại',
cardinality: 'Quan hệ',
@@ -194,16 +197,8 @@ export const vi: LanguageTranslation = {
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: 'Phụ thuộc',
table: 'Bảng',
dependent_table: 'Bảng xem phụ thuộc',
delete_dependency: 'Xóa',
@@ -213,8 +208,8 @@ export const vi: LanguageTranslation = {
},
},
empty_state: {
title: 'Không có phụ thuộc',
description: 'Tạo bảng xem phụ thuộc để bắt đầu',
title: 'Không có quan hệ',
description: 'Tạo một quan hệ để bắt đầu',
},
},
@@ -477,6 +472,7 @@ export const vi: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Tạo bảng mới',
new_view: 'Chế độ xem Mới',
new_relationship: 'Tạo quan hệ mới',
// TODO: Translate
new_area: 'New Area',
@@ -498,6 +494,9 @@ export const vi: LanguageTranslation = {
language_select: {
change_language: 'Ngôn ngữ',
},
on: 'Bật',
off: 'Tắt',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_CN: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '新建',
browse: '浏览',
tables: '表',
refs: '引用',
areas: '区域',
dependencies: '依赖关系',
custom_types: '自定义类型',
},
menu: {
file: {
file: '文件',
new: '新建',
open: '打开',
databases: {
databases: '数据库',
new: '新建关系图',
browse: '浏览...',
save: '保存',
duplicate: '复制',
import: '导入数据库',
export_sql: '导出 SQL 语句',
export_as: '导出为',
delete_diagram: '删除关系图',
exit: '退出',
},
edit: {
edit: '编辑',
@@ -29,6 +38,7 @@ export const zh_CN: LanguageTranslation = {
show_field_attributes: '展示字段属性',
hide_field_attributes: '隐藏字段属性',
zoom_on_scroll: '滚动缩放',
show_views: '数据库视图',
theme: '主题',
show_dependencies: '展示依赖',
hide_dependencies: '隐藏依赖',
@@ -69,15 +79,6 @@ export const zh_CN: LanguageTranslation = {
cancel: '取消',
},
multiple_schemas_alert: {
title: '多个模式',
description:
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
// TODO: Translate
show_me: 'Show me',
none: '无',
},
copy_to_clipboard_toast: {
unsupported: {
title: '复制失败',
@@ -112,14 +113,11 @@ export const zh_CN: LanguageTranslation = {
copied: '复制了!',
side_panel: {
schema: '模式:',
filter_by_schema: '按模式筛选',
search_schema: '搜索模式...',
no_schemas_found: '未找到模式。',
view_all_options: '查看所有选项...',
tables_section: {
tables: '表',
add_table: '添加表',
add_view: '添加视图',
filter: '筛选',
collapse: '全部折叠',
// TODO: Translate
@@ -145,6 +143,7 @@ export const zh_CN: LanguageTranslation = {
field_actions: {
title: '字段属性',
unique: '唯一',
auto_increment: '自动递增',
comments: '注释',
no_comments: '空',
delete_field: '删除字段',
@@ -160,6 +159,7 @@ export const zh_CN: LanguageTranslation = {
title: '索引属性',
name: '名称',
unique: '唯一',
index_type: '索引类型',
delete_index: '删除索引',
},
table_actions: {
@@ -176,12 +176,15 @@ export const zh_CN: LanguageTranslation = {
description: '新建表以开始',
},
},
relationships_section: {
relationships: '关系',
refs_section: {
refs: '引用',
filter: '筛选',
add_relationship: '添加关系',
collapse: '全部折叠',
add_relationship: '添加关系',
relationships: '关系',
dependencies: '依赖关系',
relationship: {
relationship: '关系',
primary: '主表',
foreign: '被引用表',
cardinality: '基数',
@@ -191,16 +194,8 @@ export const zh_CN: LanguageTranslation = {
delete_relationship: '删除',
},
},
empty_state: {
title: '无关系',
description: '创建关系以连接表',
},
},
dependencies_section: {
dependencies: '依赖关系',
filter: '筛选',
collapse: '全部折叠',
dependency: {
dependency: '依赖',
table: '表',
dependent_table: '依赖视图',
delete_dependency: '删除',
@@ -210,8 +205,8 @@ export const zh_CN: LanguageTranslation = {
},
},
empty_state: {
title: '无依赖',
description: '创建视图以开始',
title: '无关系',
description: '创建关系以开始',
},
},
@@ -472,6 +467,7 @@ export const zh_CN: LanguageTranslation = {
canvas_context_menu: {
new_table: '新建表',
new_view: '新建视图',
new_relationship: '新建关系',
// TODO: Translate
new_area: 'New Area',
@@ -493,6 +489,9 @@ export const zh_CN: LanguageTranslation = {
language_select: {
change_language: '语言',
},
on: '开启',
off: '关闭',
},
};

View File

@@ -2,17 +2,26 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_TW: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '新建',
browse: '瀏覽',
tables: '表格',
refs: 'Refs',
areas: '區域',
dependencies: '相依性',
custom_types: '自定義類型',
},
menu: {
file: {
file: '檔案',
new: '新增',
open: '開啟',
databases: {
databases: '資料庫',
new: '新增圖表',
browse: '瀏覽...',
save: '儲存',
duplicate: '複製',
import: '匯入資料庫',
export_sql: '匯出 SQL',
export_as: '匯出為特定格式',
delete_diagram: '刪除圖表',
exit: '退出',
},
edit: {
edit: '編輯',
@@ -29,6 +38,7 @@ export const zh_TW: LanguageTranslation = {
hide_field_attributes: '隱藏欄位屬性',
show_field_attributes: '顯示欄位屬性',
zoom_on_scroll: '滾動縮放',
show_views: '資料庫檢視',
theme: '主題',
show_dependencies: '顯示相依性',
hide_dependencies: '隱藏相依性',
@@ -69,15 +79,6 @@ export const zh_TW: LanguageTranslation = {
cancel: '取消',
},
multiple_schemas_alert: {
title: '多重 Schema',
description:
'此圖表中包含 {{schemasCount}} 個 Schema目前顯示{{formattedSchemas}}。',
// TODO: Translate
show_me: 'Show me',
none: '無',
},
copy_to_clipboard_toast: {
unsupported: {
title: '複製失敗',
@@ -112,14 +113,11 @@ export const zh_TW: LanguageTranslation = {
copied: '已複製!',
side_panel: {
schema: 'Schema:',
filter_by_schema: '依 Schema 篩選',
search_schema: '搜尋 Schema...',
no_schemas_found: '未找到 Schema。',
view_all_options: '顯示所有選項...',
tables_section: {
tables: '表格',
add_table: '新增表格',
add_view: '新增檢視',
filter: '篩選',
collapse: '全部摺疊',
// TODO: Translate
@@ -145,6 +143,7 @@ export const zh_TW: LanguageTranslation = {
field_actions: {
title: '欄位屬性',
unique: '唯一',
auto_increment: '自動遞增',
comments: '註解',
no_comments: '無註解',
delete_field: '刪除欄位',
@@ -160,6 +159,7 @@ export const zh_TW: LanguageTranslation = {
title: '索引屬性',
name: '名稱',
unique: '唯一',
index_type: '索引類型',
delete_index: '刪除索引',
},
table_actions: {
@@ -176,12 +176,15 @@ export const zh_TW: LanguageTranslation = {
description: '請新增表格以開始',
},
},
relationships_section: {
relationships: '關聯',
refs_section: {
refs: 'Refs',
filter: '篩選',
add_relationship: '新增關聯',
collapse: '全部摺疊',
add_relationship: '新增關聯',
relationships: '關聯',
dependencies: '相依性',
relationship: {
relationship: '關聯',
primary: '主表格',
foreign: '參照表格',
cardinality: '基數',
@@ -191,16 +194,8 @@ export const zh_TW: LanguageTranslation = {
delete_relationship: '刪除',
},
},
empty_state: {
title: '尚無關聯',
description: '請新增關聯以連接表格',
},
},
dependencies_section: {
dependencies: '相依性',
filter: '篩選',
collapse: '全部摺疊',
dependency: {
dependency: '相依性',
table: '表格',
dependent_table: '相依檢視',
delete_dependency: '刪除',
@@ -210,8 +205,8 @@ export const zh_TW: LanguageTranslation = {
},
},
empty_state: {
title: '尚無相依性',
description: '請建立檢視以開始',
title: '尚無關聯',
description: '請建立關聯以開始',
},
},
@@ -472,6 +467,7 @@ export const zh_TW: LanguageTranslation = {
canvas_context_menu: {
new_table: '新建表格',
new_view: '新檢視',
new_relationship: '新建關聯',
// TODO: Translate
new_area: 'New Area',
@@ -493,6 +489,9 @@ export const zh_TW: LanguageTranslation = {
language_select: {
change_language: '變更語言',
},
on: '開啟',
off: '關閉',
},
};

View File

@@ -19,3 +19,5 @@ export const randomColor = () => {
export const viewColor = '#b0b0b0';
export const materializedViewColor = '#7d7d7d';
export const defaultTableColor = '#8eb7ff';
export const defaultAreaColor = '#b067e9';

View File

@@ -146,3 +146,22 @@ export const findDataTypeDataById = (
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());
};

View File

@@ -50,5 +50,8 @@ export const sqliteDataTypes: readonly DataTypeData[] = [
{ name: 'smallint', id: 'smallint' },
{ name: 'bigint', id: 'bigint' },
{ name: 'bool', id: 'bool' },
{ name: 'boolean', id: 'boolean' }, // Added for smartquery compatibility
{ name: 'time', id: 'time' },
{ name: 'date', id: 'date' }, // Added for smartquery compatibility
{ name: 'datetime', id: 'datetime' }, // Added for smartquery compatibility
] as const;

View File

@@ -227,7 +227,7 @@ describe('DBML Export - SQL Generation Tests', () => {
expect(sql).not.toContain('DEFAULT DEFAULT has default');
// The fields should still be in the table
expect(sql).toContain('is_active boolean');
expect(sql).toContain('stock_count int NOT NULL'); // integer gets simplified to int
expect(sql).toContain('stock_count integer NOT NULL'); // integer gets simplified to int
});
it('should handle valid default values correctly', () => {

View File

@@ -156,11 +156,11 @@ export function exportMSSQL({
const notNull = field.nullable ? '' : ' NOT NULL';
// Check if identity column
const identity = field.default
?.toLowerCase()
.includes('identity')
? ' IDENTITY(1,1)'
: '';
const identity =
field.increment ||
field.default?.toLowerCase().includes('identity')
? ' IDENTITY(1,1)'
: '';
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
@@ -168,6 +168,7 @@ export function exportMSSQL({
// Handle default value using SQL Server specific parser
const defaultValue =
field.default &&
!field.increment &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(field)}`
: '';

View File

@@ -274,14 +274,15 @@ export function exportMySQL({
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
let autoIncrement = '';
if (
field.primaryKey &&
(field.default
?.toLowerCase()
.includes('identity') ||
field.default
field.increment ||
(field.primaryKey &&
(field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
.includes('identity') ||
field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval')))
) {
autoIncrement = ' AUTO_INCREMENT';
}
@@ -290,9 +291,10 @@ export function exportMySQL({
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value
// Handle default value - skip if auto increment
const defaultValue =
field.default &&
!field.increment &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()

View File

@@ -405,7 +405,7 @@ export function exportPostgreSQL({
.filter(Boolean);
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);

View File

@@ -67,8 +67,9 @@ function parseSQLiteDefault(field: DBField): string {
return `'${defaultValue.replace(/'/g, "''")}'`;
}
// Map problematic types to SQLite compatible types
// Preserve original types for SQLite export (only map when necessary)
function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
const originalType = typeName;
typeName = typeName.toLowerCase();
// Special handling for primary key integer columns (autoincrement requires INTEGER PRIMARY KEY)
@@ -76,59 +77,62 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
return 'INTEGER'; // Must be uppercase for SQLite to recognize it for AUTOINCREMENT
}
// Map common types to SQLite's simplified type system
// Preserve original type names that SQLite accepts
switch (typeName) {
// Keep these types as-is
case 'integer':
case 'text':
case 'real':
case 'blob':
case 'numeric':
case 'decimal':
case 'boolean':
case 'date':
case 'datetime':
case 'timestamp':
case 'float':
case 'double':
case 'varchar':
case 'char':
case 'int':
case 'smallint':
case 'tinyint':
case 'mediumint':
case 'bigint':
return 'INTEGER';
case 'json':
return typeName.toUpperCase();
case 'decimal':
case 'numeric':
case 'float':
case 'double':
case 'real':
return 'REAL';
case 'char':
// Only map types that SQLite truly doesn't recognize
case 'nchar':
case 'varchar':
case 'nvarchar':
case 'text':
case 'ntext':
case 'character varying':
case 'character':
return 'TEXT';
case 'date':
case 'datetime':
case 'timestamp':
case 'datetime2':
return 'TEXT'; // SQLite doesn't have dedicated date types
return 'DATETIME';
case 'blob':
case 'binary':
case 'varbinary':
case 'image':
return 'BLOB';
case 'bit':
case 'boolean':
return 'INTEGER'; // SQLite doesn't have a boolean type, use INTEGER
return 'BOOLEAN';
case 'user-defined':
case 'json':
case 'jsonb':
return 'TEXT'; // Store as JSON text
return 'TEXT';
case 'array':
return 'TEXT'; // Store as serialized array text
return 'TEXT';
case 'geometry':
case 'geography':
return 'BLOB'; // Store spatial data as BLOB in SQLite
return 'BLOB';
case 'mediumint':
return 'INTEGER';
}
// If type has array notation (ends with []), treat as TEXT
@@ -136,8 +140,8 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
return 'TEXT';
}
// For any other types, default to TEXT
return typeName;
// For any other types, preserve the original
return originalType.toUpperCase();
}
export function exportSQLite({
@@ -157,6 +161,11 @@ export function exportSQLite({
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation
let sqlScript = '-- SQLite database export\n';
// Add PRAGMA foreign_keys = ON if there are relationships
if (relationships && relationships.length > 0) {
sqlScript += 'PRAGMA foreign_keys = ON;\n\n';
}
// Begin transaction for faster import
sqlScript += 'BEGIN TRANSACTION;\n';
@@ -205,6 +214,86 @@ export function exportSQLite({
'integer' ||
primaryKeyFields[0].type.name.toLowerCase() === 'int');
// Collect foreign key constraints for this table
const tableForeignKeys: string[] = [];
relationships.forEach((r: DBRelationship) => {
const sourceTable = tables.find(
(t) => t.id === r.sourceTableId
);
const targetTable = tables.find(
(t) => t.id === r.targetTableId
);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView ||
sqliteSystemTables.includes(
sourceTable.name.toLowerCase()
) ||
sqliteSystemTables.includes(
targetTable.name.toLowerCase()
)
) {
return;
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (!sourceField || !targetField) {
return;
}
// Determine which table should have the foreign key based on cardinality
let fkTable, fkField, refTable, refField;
if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'many'
) {
// FK goes on target table
fkTable = targetTable;
fkField = targetField;
refTable = sourceTable;
refField = sourceField;
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
// FK goes on source table
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'one'
) {
// For 1:1, FK can go on either side, but typically goes on the table that references the other
// We'll keep the current behavior for 1:1
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
// Many-to-many relationships need a junction table, skip for now
return;
}
// If this foreign key belongs to the current table, add it
if (fkTable.id === table.id) {
tableForeignKeys.push(
` FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}")`
);
}
});
return `${schemaComment}${
table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
@@ -212,14 +301,40 @@ export function exportSQLite({
const fieldName = `"${field.name}"`;
// Handle type name - map to SQLite compatible types
const typeName = mapSQLiteType(
const baseTypeName = mapSQLiteType(
field.type.name,
field.primaryKey
);
// SQLite ignores length specifiers, so we don't add them
// We'll keep this simple without size info
const typeWithoutSize = typeName;
// Add size/precision/scale parameters if applicable
let typeWithParams = baseTypeName;
// Add character maximum length for VARCHAR, CHAR, etc.
if (
field.characterMaximumLength &&
['VARCHAR', 'CHAR', 'TEXT'].includes(
baseTypeName.toUpperCase()
)
) {
typeWithParams = `${baseTypeName}(${field.characterMaximumLength})`;
}
// Add precision and scale for DECIMAL, NUMERIC, etc.
else if (
field.precision &&
[
'DECIMAL',
'NUMERIC',
'REAL',
'FLOAT',
'DOUBLE',
].includes(baseTypeName.toUpperCase())
) {
if (field.scale) {
typeWithParams = `${baseTypeName}(${field.precision}, ${field.scale})`;
} else {
typeWithParams = `${baseTypeName}(${field.precision})`;
}
}
const notNull = field.nullable ? '' : ' NOT NULL';
@@ -228,9 +343,10 @@ export function exportSQLite({
if (
field.primaryKey &&
singleIntegerPrimaryKey &&
(field.default
?.toLowerCase()
.includes('identity') ||
(field.increment ||
field.default
?.toLowerCase()
.includes('identity') ||
field.default
?.toLowerCase()
.includes('autoincrement') ||
@@ -247,6 +363,7 @@ export function exportSQLite({
let defaultValue = '';
if (
field.default &&
!field.increment &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
@@ -267,7 +384,7 @@ export function exportSQLite({
? ' PRIMARY KEY' + autoIncrement
: '';
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithParams}${primaryKey}${notNull}${unique}${defaultValue}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
@@ -276,6 +393,11 @@ export function exportSQLite({
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}${
// Add foreign key constraints
tableForeignKeys.length > 0
? ',\n' + tableForeignKeys.join(',\n')
: ''
}\n);\n${
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
(() => {
@@ -333,82 +455,8 @@ export function exportSQLite({
.filter(Boolean) // Remove empty strings (views)
.join('\n');
}
// Generate table constraints and triggers for foreign keys
// SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements
// But we'll also provide individual ALTER TABLE statements as comments for reference
if (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n';
sqlScript +=
'-- Note: SQLite requires foreign_keys pragma to be enabled:\n';
sqlScript += '-- PRAGMA foreign_keys = ON;\n';
relationships.forEach((r: DBRelationship) => {
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
const targetTable = tables.find((t) => t.id === r.targetTableId);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView ||
sqliteSystemTables.includes(sourceTable.name.toLowerCase()) ||
sqliteSystemTables.includes(targetTable.name.toLowerCase())
) {
return;
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (!sourceField || !targetField) {
return;
}
// Determine which table should have the foreign key based on cardinality
let fkTable, fkField, refTable, refField;
if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'many'
) {
// FK goes on target table
fkTable = targetTable;
fkField = targetField;
refTable = sourceTable;
refField = sourceField;
} else if (
r.sourceCardinality === 'many' &&
r.targetCardinality === 'one'
) {
// FK goes on source table
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else if (
r.sourceCardinality === 'one' &&
r.targetCardinality === 'one'
) {
// For 1:1, FK can go on either side, but typically goes on the table that references the other
// We'll keep the current behavior for 1:1
fkTable = sourceTable;
fkField = sourceField;
refTable = targetTable;
refField = targetField;
} else {
// Many-to-many relationships need a junction table, skip for now
return;
}
// Create commented out version of what would be ALTER TABLE statement
sqlScript += `-- ALTER TABLE "${fkTable.name}" ADD CONSTRAINT "fk_${fkTable.name}_${fkField.name}" FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}");\n`;
});
}
// Foreign keys are now included inline in CREATE TABLE statements
// No need for separate ALTER TABLE statements in SQLite
// Commit transaction
sqlScript += '\nCOMMIT;\n';

View File

@@ -1,6 +1,9 @@
import type { Diagram } from '../../domain/diagram';
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 { DataType } from '../data-types/data-types';
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
@@ -8,26 +11,11 @@ import { exportMSSQL } from './export-per-type/mssql';
import { exportPostgreSQL } from './export-per-type/postgresql';
import { exportSQLite } from './export-per-type/sqlite';
import { exportMySQL } from './export-per-type/mysql';
import { escapeSQLComment } from './export-per-type/common';
// Function to simplify verbose data type names
const simplifyDataType = (typeName: string): string => {
const typeMap: Record<string, string> = {
'character varying': 'varchar',
'char varying': 'varchar',
integer: 'int',
int4: 'int',
int8: 'bigint',
serial4: 'serial',
serial8: 'bigserial',
float8: 'double precision',
float4: 'real',
bool: 'boolean',
character: 'char',
'timestamp without time zone': 'timestamp',
'timestamp with time zone': 'timestamptz',
'time without time zone': 'time',
'time with time zone': 'timetz',
};
const typeMap: Record<string, string> = {};
return typeMap[typeName.toLowerCase()] || typeName;
};
@@ -286,8 +274,13 @@ export const exportBaseSQL = ({
sqlScript += ` UNIQUE`;
}
// Handle AUTO INCREMENT - add as a comment for AI to process
if (field.increment) {
sqlScript += ` /* AUTO_INCREMENT */`;
}
// Handle DEFAULT value
if (field.default) {
if (field.default && !field.increment) {
// Temp remove default user-define value when it have it
let fieldDefault = field.default;
@@ -339,15 +332,18 @@ export const exportBaseSQL = ({
sqlScript += '\n);\n';
// Add table comment
if (table.comments) {
sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n`;
// Add table comment (only for databases that support COMMENT ON syntax)
const supportsCommentOn =
databaseTypesWithCommentSupport.includes(targetDatabaseType);
if (table.comments && supportsCommentOn) {
sqlScript += `COMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';\n`;
}
table.fields.forEach((field) => {
// Add column comment
if (field.comments) {
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments.replace(/'/g, "''")}';\n`;
// Add column comment (only for databases that support COMMENT ON syntax)
if (field.comments && supportsCommentOn) {
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${escapeSQLComment(field.comments)}';\n`;
}
});
@@ -362,9 +358,10 @@ export const exportBaseSQL = ({
.join(', ');
if (fieldNames) {
const indexName = table.schema
? `${table.schema}_${index.name}`
: index.name;
const indexName =
table.schema && !isDBMLFlow
? `${table.schema}_${index.name}`
: index.name;
sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
}
});

View File

@@ -6,7 +6,7 @@ import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
import type { DataType } from '@/lib/data/data-types/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 type { DBCustomType } from '@/lib/domain/db-custom-type';
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
@@ -727,10 +727,10 @@ export function convertToChartDBDiagram(
indexes,
x: col * tableSpacing,
y: row * tableSpacing,
color: randomColor(),
color: defaultTableColor,
isView: false,
createdAt: Date.now(),
};
} satisfies DBTable;
});
// Process relationships
@@ -786,12 +786,6 @@ export function convertToChartDBDiagram(
);
if (!sourceField || !targetField) {
console.log('Relationship refers to non-existent field:', {
sourceTable: rel.sourceTable,
sourceField: rel.sourceColumn,
targetTable: rel.targetTable,
targetField: rel.targetColumn,
});
return;
}

View File

@@ -203,11 +203,6 @@ export function findTableWithSchemaSupport(
// If still not found with schema, try any match on the table name
if (!table) {
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;
@@ -235,11 +230,7 @@ export function getTableIdWithSchemaSupport(
// If still not found with schema, try without schema
if (!tableId) {
tableId = tableMap[tableName];
if (tableId) {
console.log(
`Found table ID for ${tableName} without schema match, source schema: ${effectiveSchema}`
);
} else {
if (!tableId) {
console.warn(
`No table ID found for ${tableName} with schema ${effectiveSchema}`
);

View File

@@ -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();
}
});
});

View File

@@ -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);
});
});

View File

@@ -162,15 +162,36 @@ function parseAlterTableAddConstraint(statements: string[]): SQLForeignKey[] {
if (match) {
const [
,
sourceSchema = 'dbo',
sourceTable,
sourceSchemaOrTable,
sourceTableIfSchema,
constraintName,
sourceColumn,
targetSchema = 'dbo',
targetTable,
targetSchemaOrTable,
targetTableIfSchema,
targetColumn,
] = 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({
name: constraintName,
sourceTable: sourceTable,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,629 @@
import { defaultSchemas } from '@/lib/data/default-schemas';
import type { Area } from '../../domain/area';
import {
DBCustomTypeKind,
type DBCustomType,
} from '../../domain/db-custom-type';
import type { DBDependency } from '../../domain/db-dependency';
import type { DBField } from '../../domain/db-field';
import type { DBIndex } from '../../domain/db-index';
import type { DBRelationship } from '../../domain/db-relationship';
import type { DBTable } from '../../domain/db-table';
import type { Diagram } from '../../domain/diagram';
type SourceIdToDataMap = Record<
string,
{ schema?: string | null; name: string; color?: string }
>;
type IdMappings = {
tables: Record<string, string>;
fields: Record<string, string>;
};
// Key generation functions remain the same for consistency
const createObjectKey = ({
type,
schema,
otherSchema,
parentName,
otherParentName,
name,
otherName,
}: {
type:
| 'table'
| 'field'
| 'index'
| 'relationship'
| 'customType'
| 'dependency'
| 'area';
schema?: string | null;
otherSchema?: string | null;
parentName?: string | null;
otherParentName?: string | null;
name: string;
otherName?: string | null;
}) =>
`${type}-${schema ? `${schema}.` : ''}${otherSchema ? `${otherSchema}.` : ''}${parentName ? `${parentName}.` : ''}${otherParentName ? `${otherParentName}.` : ''}${name}${otherName ? `.${otherName}` : ''}`;
const createObjectKeyFromTable = (table: DBTable) =>
createObjectKey({
type: 'table',
schema: table.schema,
name: table.name,
});
const createObjectKeyFromField = (table: DBTable, field: DBField) =>
createObjectKey({
type: 'field',
schema: table.schema,
parentName: table.name,
name: field.name,
});
const createObjectKeyFromIndex = (table: DBTable, index: DBIndex) =>
createObjectKey({
type: 'index',
schema: table.schema,
parentName: table.name,
name: index.name,
});
const createObjectKeyFromRelationship = (
relationship: DBRelationship,
sourceIdToNameMap: SourceIdToDataMap
) => {
const sourceTable = sourceIdToNameMap[relationship.sourceTableId];
const targetTable = sourceIdToNameMap[relationship.targetTableId];
const sourceField = sourceIdToNameMap[relationship.sourceFieldId];
const targetField = sourceIdToNameMap[relationship.targetFieldId];
if (!sourceTable || !targetTable || !sourceField || !targetField) {
return null;
}
return createObjectKey({
type: 'relationship',
schema: sourceTable.schema,
otherSchema: targetTable.schema,
parentName: sourceTable.name,
otherParentName: targetTable.name,
name: sourceField.name,
otherName: targetField.name,
});
};
const createObjectKeyFromCustomType = (customType: DBCustomType) =>
createObjectKey({
type: 'customType',
schema: customType.schema,
name: customType.name,
});
const createObjectKeyFromDependency = (
dependency: DBDependency,
sourceIdToNameMap: SourceIdToDataMap
) => {
const dependentTable = sourceIdToNameMap[dependency.dependentTableId];
const table = sourceIdToNameMap[dependency.tableId];
if (!dependentTable || !table) {
return null;
}
return createObjectKey({
type: 'dependency',
schema: dependentTable.schema,
otherSchema: table.schema,
name: dependentTable.name,
otherName: table.name,
});
};
const createObjectKeyFromArea = (area: Area) =>
createObjectKey({
type: 'area',
name: area.name,
});
// Helper function to build source mappings
const buildSourceMappings = (sourceDiagram: Diagram) => {
const objectKeysToIdsMap: Record<string, string> = {};
const sourceIdToDataMap: SourceIdToDataMap = {};
// Map tables and their fields/indexes
sourceDiagram.tables?.forEach((table) => {
const tableKey = createObjectKeyFromTable(table);
objectKeysToIdsMap[tableKey] = table.id;
sourceIdToDataMap[table.id] = {
schema: table.schema,
name: table.name,
color: table.color,
};
table.fields?.forEach((field) => {
const fieldKey = createObjectKeyFromField(table, field);
objectKeysToIdsMap[fieldKey] = field.id;
sourceIdToDataMap[field.id] = {
schema: table.schema,
name: field.name,
};
});
table.indexes?.forEach((index) => {
const indexKey = createObjectKeyFromIndex(table, index);
objectKeysToIdsMap[indexKey] = index.id;
});
});
// Map relationships
sourceDiagram.relationships?.forEach((relationship) => {
const key = createObjectKeyFromRelationship(
relationship,
sourceIdToDataMap
);
if (key) {
objectKeysToIdsMap[key] = relationship.id;
}
});
// Map custom types
sourceDiagram.customTypes?.forEach((customType) => {
const key = createObjectKeyFromCustomType(customType);
objectKeysToIdsMap[key] = customType.id;
});
// Map dependencies
sourceDiagram.dependencies?.forEach((dependency) => {
const key = createObjectKeyFromDependency(
dependency,
sourceIdToDataMap
);
if (key) {
objectKeysToIdsMap[key] = dependency.id;
}
});
// Map areas
sourceDiagram.areas?.forEach((area) => {
const key = createObjectKeyFromArea(area);
objectKeysToIdsMap[key] = area.id;
});
return { objectKeysToIdsMap, sourceIdToDataMap };
};
// Functional helper to update tables and collect ID mappings
const updateTables = ({
targetTables,
sourceTables,
defaultDatabaseSchema,
}: {
targetTables: DBTable[] | undefined;
sourceTables: DBTable[] | undefined;
objectKeysToIdsMap: Record<string, string>;
sourceIdToDataMap: SourceIdToDataMap;
defaultDatabaseSchema?: string;
}): { tables: DBTable[]; idMappings: IdMappings } => {
if (!targetTables)
return { tables: [], idMappings: { tables: {}, fields: {} } };
if (!sourceTables)
return { tables: targetTables, idMappings: { tables: {}, fields: {} } };
const idMappings: IdMappings = { tables: {}, fields: {} };
// Create a map of source tables by schema + name
const sourceTablesByKey = new Map<string, DBTable>();
sourceTables.forEach((table) => {
const key = createObjectKeyFromTable(table);
sourceTablesByKey.set(key, table);
});
const updatedTables = targetTables.map((targetTable) => {
// Try to find matching source table by schema + name
const targetKey = createObjectKeyFromTable(targetTable);
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 (!targetTable.schema) {
// If target table has no schema, try matching with default schema
const defaultKey = createObjectKeyFromTable({
...targetTable,
schema: defaultDatabaseSchema,
});
sourceTable = sourceTablesByKey.get(defaultKey);
} else if (targetTable.schema === defaultDatabaseSchema) {
// Already tried without schema above
}
}
if (!sourceTable) {
// No matching source table found - keep target as-is
return targetTable;
}
const sourceId = sourceTable.id;
idMappings.tables[targetTable.id] = sourceId;
// Update fields by matching on name within the table
const sourceFieldsByName = new Map<string, DBField>();
sourceTable.fields?.forEach((field) => {
sourceFieldsByName.set(field.name, field);
});
const updatedFields = targetTable.fields?.map((targetField) => {
const sourceField = sourceFieldsByName.get(targetField.name);
if (sourceField) {
idMappings.fields[targetField.id] = sourceField.id;
// Use source field properties when there's a match
return {
...targetField,
id: sourceField.id,
createdAt: sourceField.createdAt,
};
}
// For new fields not in source, keep target field as-is
return targetField;
});
// Update indexes by matching on name within the table
const sourceIndexesByName = new Map<string, DBIndex>();
sourceTable.indexes?.forEach((index) => {
sourceIndexesByName.set(index.name, index);
});
const updatedIndexes = targetTable.indexes?.map((targetIndex) => {
const sourceIndex = sourceIndexesByName.get(targetIndex.name);
if (sourceIndex) {
return {
...targetIndex,
id: sourceIndex.id,
createdAt: sourceIndex.createdAt,
};
}
return targetIndex;
});
// Build the result table, preserving source structure
const resultTable: DBTable = {
...sourceTable,
fields: updatedFields,
indexes: updatedIndexes,
comments: targetTable.comments,
};
// Update nullable, unique, primaryKey from target fields
if (targetTable.fields) {
resultTable.fields = resultTable.fields?.map((field) => {
const targetField = targetTable.fields?.find(
(f) => f.name === field.name
);
if (targetField) {
return {
...field,
nullable: targetField.nullable,
unique: targetField.unique,
primaryKey: targetField.primaryKey,
type: targetField.type,
};
}
return field;
});
}
return resultTable;
});
return { tables: updatedTables, idMappings };
};
// Functional helper to update custom types
const updateCustomTypes = (
customTypes: DBCustomType[] | undefined,
objectKeysToIdsMap: Record<string, string>
): DBCustomType[] => {
if (!customTypes) return [];
return customTypes.map((customType) => {
const key = createObjectKeyFromCustomType(customType);
const sourceId = objectKeysToIdsMap[key];
if (sourceId) {
return { ...customType, id: sourceId };
}
return customType;
});
};
// Functional helper to update relationships
const updateRelationships = (
targetRelationships: DBRelationship[] | undefined,
sourceRelationships: DBRelationship[] | undefined,
idMappings: IdMappings
): DBRelationship[] => {
// If target has no relationships, return empty array (relationships were removed)
if (!targetRelationships || targetRelationships.length === 0) return [];
// If source has no relationships, we need to add the target relationships with updated IDs
if (!sourceRelationships || sourceRelationships.length === 0) {
return targetRelationships.map((targetRel) => {
// Find the source IDs by reversing the mapping lookup
let sourceTableId = targetRel.sourceTableId;
let targetTableId = targetRel.targetTableId;
let sourceFieldId = targetRel.sourceFieldId;
let targetFieldId = targetRel.targetFieldId;
// Find source table/field IDs from the mappings
for (const [targetId, srcId] of Object.entries(idMappings.tables)) {
if (targetId === targetRel.sourceTableId) {
sourceTableId = srcId;
}
if (targetId === targetRel.targetTableId) {
targetTableId = srcId;
}
}
for (const [targetId, srcId] of Object.entries(idMappings.fields)) {
if (targetId === targetRel.sourceFieldId) {
sourceFieldId = srcId;
}
if (targetId === targetRel.targetFieldId) {
targetFieldId = srcId;
}
}
return {
...targetRel,
sourceTableId,
targetTableId,
sourceFieldId,
targetFieldId,
};
});
}
// Map source relationships that have matches in target
const resultRelationships: DBRelationship[] = [];
const matchedTargetRelIds = new Set<string>();
sourceRelationships.forEach((sourceRel) => {
// Find matching target relationship by checking if the target has a relationship
// between the same tables and fields (using the ID mappings)
const targetRel = targetRelationships.find((tgtRel) => {
const mappedSourceTableId = idMappings.tables[tgtRel.sourceTableId];
const mappedTargetTableId = idMappings.tables[tgtRel.targetTableId];
const mappedSourceFieldId = idMappings.fields[tgtRel.sourceFieldId];
const mappedTargetFieldId = idMappings.fields[tgtRel.targetFieldId];
// Check both directions since relationships can be defined in either direction
const directMatch =
sourceRel.sourceTableId === mappedSourceTableId &&
sourceRel.targetTableId === mappedTargetTableId &&
sourceRel.sourceFieldId === mappedSourceFieldId &&
sourceRel.targetFieldId === mappedTargetFieldId;
const reverseMatch =
sourceRel.sourceTableId === mappedTargetTableId &&
sourceRel.targetTableId === mappedSourceTableId &&
sourceRel.sourceFieldId === mappedTargetFieldId &&
sourceRel.targetFieldId === mappedSourceFieldId;
return directMatch || reverseMatch;
});
if (targetRel) {
matchedTargetRelIds.add(targetRel.id);
// Preserve source relationship but update cardinalities from target
const result: DBRelationship = {
...sourceRel,
sourceCardinality: targetRel.sourceCardinality,
targetCardinality: targetRel.targetCardinality,
};
// Only include schema fields if they exist in the source relationship
if (!sourceRel.sourceSchema) {
delete result.sourceSchema;
}
if (!sourceRel.targetSchema) {
delete result.targetSchema;
}
resultRelationships.push(result);
}
});
// Add any target relationships that weren't matched (new relationships)
targetRelationships.forEach((targetRel) => {
if (!matchedTargetRelIds.has(targetRel.id)) {
// Find the source IDs by reversing the mapping lookup
let sourceTableId = targetRel.sourceTableId;
let targetTableId = targetRel.targetTableId;
let sourceFieldId = targetRel.sourceFieldId;
let targetFieldId = targetRel.targetFieldId;
// Find source table/field IDs from the mappings
for (const [targetId, srcId] of Object.entries(idMappings.tables)) {
if (targetId === targetRel.sourceTableId) {
sourceTableId = srcId;
}
if (targetId === targetRel.targetTableId) {
targetTableId = srcId;
}
}
for (const [targetId, srcId] of Object.entries(idMappings.fields)) {
if (targetId === targetRel.sourceFieldId) {
sourceFieldId = srcId;
}
if (targetId === targetRel.targetFieldId) {
targetFieldId = srcId;
}
}
resultRelationships.push({
...targetRel,
sourceTableId,
targetTableId,
sourceFieldId,
targetFieldId,
});
}
});
return resultRelationships;
};
// Functional helper to update dependencies
const updateDependencies = (
targetDependencies: DBDependency[] | undefined,
sourceDependencies: DBDependency[] | undefined,
idMappings: IdMappings
): DBDependency[] => {
if (!targetDependencies) return [];
if (!sourceDependencies) return targetDependencies;
return targetDependencies.map((targetDep) => {
// Find matching source dependency
const sourceDep = sourceDependencies.find((srcDep) => {
const srcTableId = idMappings.tables[targetDep.tableId];
const srcDependentTableId =
idMappings.tables[targetDep.dependentTableId];
return (
srcDep.tableId === srcTableId &&
srcDep.dependentTableId === srcDependentTableId
);
});
if (sourceDep) {
return {
...targetDep,
id: sourceDep.id,
tableId:
idMappings.tables[targetDep.tableId] || targetDep.tableId,
dependentTableId:
idMappings.tables[targetDep.dependentTableId] ||
targetDep.dependentTableId,
};
}
// If no match found, just update the table references
return {
...targetDep,
tableId: idMappings.tables[targetDep.tableId] || targetDep.tableId,
dependentTableId:
idMappings.tables[targetDep.dependentTableId] ||
targetDep.dependentTableId,
};
});
};
// Functional helper to update index field references
const updateIndexFieldReferences = (
tables: DBTable[] | undefined,
idMappings: IdMappings
): DBTable[] => {
if (!tables) return [];
return tables.map((table) => ({
...table,
indexes: table.indexes?.map((index) => ({
...index,
fieldIds: index.fieldIds.map(
(fieldId) => idMappings.fields[fieldId] || fieldId
),
})),
}));
};
export const applyDBMLChanges = ({
sourceDiagram,
targetDiagram,
}: {
sourceDiagram: Diagram;
targetDiagram: Diagram;
}): Diagram => {
// Step 1: Build mappings from source diagram
const { objectKeysToIdsMap, sourceIdToDataMap } =
buildSourceMappings(sourceDiagram);
// Step 2: Update tables and collect ID mappings
const { tables: updatedTables, idMappings } = updateTables({
targetTables: targetDiagram.tables,
sourceTables: sourceDiagram.tables,
objectKeysToIdsMap,
sourceIdToDataMap,
defaultDatabaseSchema: defaultSchemas[sourceDiagram.databaseType],
});
// Step 3: Update all other entities functionally
const newCustomTypes = updateCustomTypes(
targetDiagram.customTypes,
objectKeysToIdsMap
);
const updatedCustomTypes = [
...(sourceDiagram.customTypes?.filter(
(ct) => ct.kind === DBCustomTypeKind.composite
) ?? []),
...newCustomTypes,
];
const updatedRelationships = updateRelationships(
targetDiagram.relationships,
sourceDiagram.relationships,
idMappings
);
const updatedDependencies = updateDependencies(
targetDiagram.dependencies,
sourceDiagram.dependencies,
idMappings
);
// Step 4: Update index field references
const finalTables = updateIndexFieldReferences(updatedTables, idMappings);
// Sort relationships to match source order
const sortedRelationships = [...updatedRelationships].sort((a, b) => {
// Find source relationships to get their order
const sourceRelA = sourceDiagram.relationships?.find(
(r) => r.id === a.id
);
const sourceRelB = sourceDiagram.relationships?.find(
(r) => r.id === b.id
);
if (!sourceRelA || !sourceRelB) return 0;
const indexA = sourceDiagram.relationships?.indexOf(sourceRelA) ?? 0;
const indexB = sourceDiagram.relationships?.indexOf(sourceRelB) ?? 0;
return indexA - indexB;
});
// Return a new diagram object with tables sorted by order
const result: Diagram = {
...sourceDiagram,
tables: finalTables.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
areas: targetDiagram.areas,
relationships: sortedRelationships,
dependencies: updatedDependencies,
customTypes: updatedCustomTypes,
};
return result;
};

View File

@@ -937,26 +937,482 @@ describe('DBML Export - Issue Fixes', () => {
// Check that indexes are properly formatted with names
// Note: When a table has a schema, index names are prefixed with the schema
expect(result.standardDbml).toContain(
'email [unique, name: "public_idx_email"]'
'email [unique, name: "idx_email"]'
);
expect(result.standardDbml).toContain(
'created_at [name: "public_idx_created_at"]'
'created_at [name: "idx_created_at"]'
);
expect(result.standardDbml).toContain(
'(email, created_at) [name: "public_idx_email_created"]'
'(email, created_at) [name: "idx_email_created"]'
);
// Verify proper index syntax in the table
const indexSection = result.standardDbml.match(/Indexes \{[\s\S]*?\}/);
expect(indexSection).toBeTruthy();
expect(indexSection![0]).toContain('email [unique, name: "idx_email"]');
expect(indexSection![0]).toContain(
'email [unique, name: "public_idx_email"]'
'created_at [name: "idx_created_at"]'
);
expect(indexSection![0]).toContain(
'created_at [name: "public_idx_created_at"]'
);
expect(indexSection![0]).toContain(
'(email, created_at) [name: "public_idx_email_created"]'
'(email, created_at) [name: "idx_email_created"]'
);
});
it('should export in the right format', () => {
const diagram: Diagram = {
id: 'mqqwkkodrxxd',
name: 'Diagram 9',
createdAt: new Date('2025-07-30T15:44:53.967Z'),
updatedAt: new Date('2025-07-30T16:11:22.554Z'),
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: '8ftpn9qn0o2ddrvhzgdjro3zv',
name: 'table_1',
x: 260,
y: 80,
fields: [
{
id: 'w9wlmimvjaci2krhfb4v9bhy0',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
unique: true,
nullable: false,
primaryKey: true,
createdAt: 1753890297335,
},
],
indexes: [],
color: '#4dee8a',
createdAt: 1753890297335,
isView: false,
order: 0,
parentAreaId: null,
},
{
id: 'wofcygo4u9623oueif9k3v734',
name: 'table_2',
x: -178.62499999999994,
y: -244.375,
fields: [
{
id: '6ca6p6lnss4d2top8pjcfsli7',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
unique: true,
nullable: false,
primaryKey: true,
createdAt: 1753891879081,
},
],
indexes: [],
color: '#4dee8a',
createdAt: 1753891879081,
isView: false,
order: 1,
parentAreaId: null,
},
],
relationships: [
{
id: 'o5ynn1x9nxm5ipuugo690doau',
name: 'table_2_id_fk',
sourceTableId: 'wofcygo4u9623oueif9k3v734',
targetTableId: '8ftpn9qn0o2ddrvhzgdjro3zv',
sourceFieldId: '6ca6p6lnss4d2top8pjcfsli7',
targetFieldId: 'w9wlmimvjaci2krhfb4v9bhy0',
sourceCardinality: 'one',
targetCardinality: 'one',
createdAt: 1753891882554,
},
],
dependencies: [],
areas: [],
customTypes: [],
};
const result = generateDBMLFromDiagram(diagram);
const expectedInlineDBML = `Table "table_1" {
"id" bigint [pk, not null]
}
Table "table_2" {
"id" bigint [pk, not null, ref: < "table_1"."id"]
}
`;
const expectedStandardDBML = `Table "table_1" {
"id" bigint [pk, not null]
}
Table "table_2" {
"id" bigint [pk, not null]
}
Ref "fk_0_table_2_id_fk":"table_1"."id" < "table_2"."id"
`;
expect(result.inlineDbml).toBe(expectedInlineDBML);
expect(result.standardDbml).toBe(expectedStandardDBML);
});
it('should handle tables with multiple relationships correctly', () => {
const diagram: Diagram = {
id: 'test-diagram',
name: 'Test',
databaseType: DatabaseType.POSTGRESQL,
createdAt: new Date(),
updatedAt: new Date(),
tables: [
{
id: 'users',
name: 'users',
x: 0,
y: 0,
fields: [
{
id: 'users_id',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
],
indexes: [],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
{
id: 'posts',
name: 'posts',
x: 0,
y: 0,
fields: [
{
id: 'posts_id',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
{
id: 'posts_user_id',
name: 'user_id',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
],
indexes: [],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
{
id: 'reviews',
name: 'reviews',
x: 0,
y: 0,
fields: [
{
id: 'reviews_id',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
{
id: 'reviews_user_id',
name: 'user_id',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
],
indexes: [],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
{
id: 'user_activities',
name: 'user_activities',
x: 0,
y: 0,
fields: [
{
id: 'activities_id',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
{
id: 'activities_entity_id',
name: 'entity_id',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
{
id: 'activities_type',
name: 'activity_type',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: true,
unique: false,
collation: null,
default: null,
characterMaximumLength: '50',
createdAt: Date.now(),
},
],
indexes: [],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
],
relationships: [
{
id: 'rel1',
name: 'fk_posts_user',
sourceTableId: 'posts',
sourceFieldId: 'posts_user_id',
targetTableId: 'users',
targetFieldId: 'users_id',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'rel2',
name: 'fk_reviews_user',
sourceTableId: 'reviews',
sourceFieldId: 'reviews_user_id',
targetTableId: 'users',
targetFieldId: 'users_id',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'rel3',
name: 'fk_activities_posts',
sourceTableId: 'user_activities',
sourceFieldId: 'activities_entity_id',
targetTableId: 'posts',
targetFieldId: 'posts_id',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'rel4',
name: 'fk_activities_reviews',
sourceTableId: 'user_activities',
sourceFieldId: 'activities_entity_id',
targetTableId: 'reviews',
targetFieldId: 'reviews_id',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
],
};
const result = generateDBMLFromDiagram(diagram);
// Debug output removed
// console.log('Inline DBML:', result.inlineDbml);
// Check standard DBML output
expect(result.standardDbml).toContain('Table "users" {');
expect(result.standardDbml).toContain('Table "posts" {');
expect(result.standardDbml).toContain('Table "reviews" {');
expect(result.standardDbml).toContain('Table "user_activities" {');
// Check that the entity_id field in user_activities has multiple relationships in inline DBML
// The field should have both references in a single bracket
expect(result.inlineDbml).toContain(
'"entity_id" integer [not null, ref: < "posts"."id", ref: < "reviews"."id"]'
);
// Check that standard DBML has separate Ref entries for each relationship
expect(result.standardDbml).toContain(
'Ref "fk_0_fk_posts_user":"users"."id" < "posts"."user_id"'
);
expect(result.standardDbml).toContain(
'Ref "fk_1_fk_reviews_user":"users"."id" < "reviews"."user_id"'
);
expect(result.standardDbml).toContain(
'Ref "fk_2_fk_activities_posts":"posts"."id" < "user_activities"."entity_id"'
);
expect(result.standardDbml).toContain(
'Ref "fk_3_fk_activities_reviews":"reviews"."id" < "user_activities"."entity_id"'
);
// No automatic comment is added for fields with multiple relationships
// Check proper formatting - closing brace should be on a new line
expect(result.inlineDbml).toMatch(
/Table "user_activities" \{\s*\n\s*"id".*\n\s*"entity_id".*\]\s*\n\s*"activity_type".*\n\s*\}/
);
// Ensure no closing brace appears on the same line as a field with inline refs
expect(result.inlineDbml).not.toMatch(/\[.*ref:.*\]\}/);
});
it('should properly format closing brace when table has both indexes and inline refs', () => {
const diagram: Diagram = {
id: 'test-diagram',
name: 'Test',
databaseType: DatabaseType.POSTGRESQL,
createdAt: new Date(),
updatedAt: new Date(),
tables: [
{
id: 'table1',
name: 'table_1',
x: 0,
y: 0,
fields: [
{
id: 'field1',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
],
indexes: [
{
id: 'index1',
name: 'index_1',
unique: false,
fieldIds: ['field1'],
createdAt: Date.now(),
},
],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
{
id: 'table2',
name: 'table_2',
x: 0,
y: 0,
fields: [
{
id: 'field2',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: false,
collation: null,
default: null,
characterMaximumLength: null,
createdAt: Date.now(),
},
],
indexes: [],
color: 'blue',
isView: false,
createdAt: Date.now(),
},
],
relationships: [
{
id: 'rel1',
name: 'table2_id_fkey',
sourceTableId: 'table2',
sourceFieldId: 'field2',
targetTableId: 'table1',
targetFieldId: 'field1',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
],
};
const result = generateDBMLFromDiagram(diagram);
// Check that the inline DBML has proper indentation
expect(result.inlineDbml).toContain(`Table "table_1" {
"id" bigint [pk, not null]
Indexes {
id [name: "index_1"]
}
}`);
expect(result.inlineDbml).toContain(`Table "table_2" {
"id" bigint [pk, not null, ref: < "table_1"."id"]
}`);
// The issue was that it would generate:
// Table "table_1" {
// "id" bigint [pk, not null]
//
// Indexes {
// id [name: "index_1"]
//
// }
// }
// Make sure there's no malformed closing brace
expect(result.inlineDbml).not.toMatch(/\n\s*\n\s*}\s*\n}/);
expect(result.inlineDbml).not.toMatch(/\s+\n}/);
// Ensure there's no extra closing brace
const braceBalance =
(result.inlineDbml.match(/{/g) || []).length -
(result.inlineDbml.match(/}/g) || []).length;
expect(braceBalance).toBe(0);
});
});

View File

@@ -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', () => {
const sql = `ALTER TABLE quest_prerequisites ADD CONSTRAINT fk_quest_prereq FOREIGN KEY (quest_id) REFERENCES quest_prerequisites (quest_id);
it('should preserve valid self-referential foreign keys but filter invalid ones', () => {
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 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);
// 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 spell_components');
expect(sanitized).toContain('-- ALTER TABLE guild_hierarchy');
expect(sanitized).toMatch(
/-- 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', () => {
@@ -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 'NOW'"); // NOW is quoted as a single word
expect(sanitized).toContain('(matrix_pattern)'); // Deduplicated
// Valid self-referencing relationships (different fields) are preserved
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'
);
expect(sanitized).toContain(

View 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');
});
});

View File

@@ -6,7 +6,6 @@ import type { DBTable } from '@/lib/domain/db-table';
import { type DBField } from '@/lib/domain/db-field';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
import { defaultSchemas } from '@/lib/data/default-schemas';
// Use DBCustomType for generating Enum DBML
const generateEnumsDBML = (customTypes: DBCustomType[] | undefined): string => {
@@ -156,14 +155,25 @@ export const sanitizeSQLforDBML = (sql: string): string => {
}
);
// Comment out self-referencing foreign keys to prevent "Two endpoints are the same" error
// Example: ALTER TABLE public.class ADD CONSTRAINT ... FOREIGN KEY (class_id) REFERENCES public.class (class_id);
// Comment out invalid self-referencing foreign keys where the same field references itself
// 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 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 =
/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;
if (selfRefFKPattern.test(line)) {
return `-- ${line}`; // Comment out the line
/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;
const match = selfRefFKPattern.exec(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;
});
@@ -249,34 +259,67 @@ const convertToInlineRefs = (dbml: string): string => {
fullMatch: string;
};
} = {};
// Updated pattern to handle various table name formats including schema.table
const tablePattern =
/Table\s+(?:"([^"]+)"(?:\."([^"]+)")?|(\[?[^\s[]+\]?\.\[?[^\s\]]+\]?)|(\[?[^\s[{]+\]?))\s*{([^}]*)}/g;
let tableMatch;
while ((tableMatch = tablePattern.exec(dbml)) !== null) {
// Extract table name - handle schema.table format
// Use a more sophisticated approach to handle nested braces
let currentPos = 0;
while (currentPos < dbml.length) {
// Find the next table definition
const tableStartPattern =
/Table\s+(?:"([^"]+)"(?:\."([^"]+)")?|(\[?[^\s[]+\]?\.\[?[^\s\]]+\]?)|(\[?[^\s[{]+\]?))\s*{/g;
tableStartPattern.lastIndex = currentPos;
const tableStartMatch = tableStartPattern.exec(dbml);
if (!tableStartMatch) break;
// Extract table name
let tableName;
if (tableMatch[1] && tableMatch[2]) {
// Format: "schema"."table"
tableName = `${tableMatch[1]}.${tableMatch[2]}`;
} else if (tableMatch[1]) {
// Format: "table" (no schema)
tableName = tableMatch[1];
if (tableStartMatch[1] && tableStartMatch[2]) {
tableName = `${tableStartMatch[1]}.${tableStartMatch[2]}`;
} else if (tableStartMatch[1]) {
tableName = tableStartMatch[1];
} else {
// Other formats
tableName = tableMatch[3] || tableMatch[4];
tableName = tableStartMatch[3] || tableStartMatch[4];
}
// Clean up any bracket syntax from table names
const cleanTableName = tableName.replace(/\[([^\]]+)\]/g, '$1');
tables[cleanTableName] = {
start: tableMatch.index,
end: tableMatch.index + tableMatch[0].length,
content: tableMatch[5],
fullMatch: tableMatch[0],
};
// Find the matching closing brace by counting nested braces
const openBracePos =
tableStartMatch.index + tableStartMatch[0].length - 1;
let braceCount = 1;
const contentStart = openBracePos + 1;
let contentEnd = contentStart;
for (let i = contentStart; i < dbml.length && braceCount > 0; i++) {
if (dbml[i] === '{') braceCount++;
else if (dbml[i] === '}') {
braceCount--;
if (braceCount === 0) {
contentEnd = i;
}
}
}
if (braceCount === 0) {
const content = dbml.substring(contentStart, contentEnd);
const fullMatch = dbml.substring(
tableStartMatch.index,
contentEnd + 1
);
tables[cleanTableName] = {
start: tableStartMatch.index,
end: contentEnd + 1,
content: content,
fullMatch: fullMatch,
};
currentPos = contentEnd + 1;
} else {
// Malformed DBML, skip this table
currentPos = tableStartMatch.index + tableStartMatch[0].length;
}
}
if (refs.length === 0 || Object.keys(tables).length === 0) {
@@ -286,9 +329,14 @@ const convertToInlineRefs = (dbml: string): string => {
// Create a map for faster table lookup
const tableMap = new Map(Object.entries(tables));
// 1. Add inline refs to table contents
// 1. First, collect all refs per field
const fieldRefs = new Map<
string,
{ table: string; refs: string[]; relatedTables: string[] }
>();
refs.forEach((ref) => {
let targetTableName, fieldNameToModify, inlineRefSyntax;
let targetTableName, fieldNameToModify, inlineRefSyntax, relatedTable;
if (ref.direction === '<') {
targetTableName = ref.targetSchema
@@ -299,6 +347,7 @@ const convertToInlineRefs = (dbml: string): string => {
? `"${ref.sourceSchema}"."${ref.sourceTable}"."${ref.sourceField}"`
: `"${ref.sourceTable}"."${ref.sourceField}"`;
inlineRefSyntax = `ref: < ${sourceRef}`;
relatedTable = ref.sourceTable;
} else {
targetTableName = ref.sourceSchema
? `${ref.sourceSchema}.${ref.sourceTable}`
@@ -308,13 +357,32 @@ const convertToInlineRefs = (dbml: string): string => {
? `"${ref.targetSchema}"."${ref.targetTable}"."${ref.targetField}"`
: `"${ref.targetTable}"."${ref.targetField}"`;
inlineRefSyntax = `ref: > ${targetRef}`;
relatedTable = ref.targetTable;
}
const tableData = tableMap.get(targetTableName);
const fieldKey = `${targetTableName}.${fieldNameToModify}`;
const existing = fieldRefs.get(fieldKey) || {
table: targetTableName,
refs: [],
relatedTables: [],
};
existing.refs.push(inlineRefSyntax);
existing.relatedTables.push(relatedTable);
fieldRefs.set(fieldKey, existing);
});
// 2. Apply all refs to fields
fieldRefs.forEach((fieldData, fieldKey) => {
// fieldKey might be "schema.table.field" or just "table.field"
const lastDotIndex = fieldKey.lastIndexOf('.');
const tableName = fieldKey.substring(0, lastDotIndex);
const fieldName = fieldKey.substring(lastDotIndex + 1);
const tableData = tableMap.get(tableName);
if (tableData) {
// Updated pattern to capture field definition and all existing attributes in brackets
const fieldPattern = new RegExp(
`^([ \t]*"${fieldNameToModify}"[^\\n]*?)(?:\\s*(\\[[^\\]]*\\]))*\\s*(//.*)?$`,
`^([ \t]*"${fieldName}"[^\\n]*?)(?:\\s*(\\[[^\\]]*\\]))*\\s*(//.*)?$`,
'gm'
);
let newContent = tableData.content;
@@ -322,11 +390,6 @@ const convertToInlineRefs = (dbml: string): string => {
newContent = newContent.replace(
fieldPattern,
(lineMatch, fieldPart, existingBrackets, commentPart) => {
// Avoid adding duplicate refs
if (lineMatch.includes('ref:')) {
return lineMatch;
}
// Collect all attributes from existing brackets
const allAttributes: string[] = [];
if (existingBrackets) {
@@ -344,20 +407,24 @@ const convertToInlineRefs = (dbml: string): string => {
}
}
// Add the new ref
allAttributes.push(inlineRefSyntax);
// Add all refs for this field
allAttributes.push(...fieldData.refs);
// Combine all attributes into a single bracket
const combinedAttributes = allAttributes.join(', ');
return `${fieldPart.trim()} [${combinedAttributes}]${commentPart || ''}`;
// Preserve original spacing from fieldPart
const leadingSpaces = fieldPart.match(/^(\s*)/)?.[1] || '';
const fieldDefWithoutSpaces = fieldPart.trim();
return `${leadingSpaces}${fieldDefWithoutSpaces} [${combinedAttributes}]${commentPart || ''}`;
}
);
// Update the table content if modified
if (newContent !== tableData.content) {
tableData.content = newContent;
tableMap.set(targetTableName, tableData);
tableMap.set(tableName, tableData);
}
}
});
@@ -373,10 +440,48 @@ const convertToInlineRefs = (dbml: string): string => {
reconstructedDbml += dbml.substring(lastIndex, tableData.start);
// Preserve the original table definition format but with updated content
const originalTableDef = tableData.fullMatch;
const updatedTableDef = originalTableDef.replace(
/{[^}]*}/,
`{${tableData.content}}`
let formattedContent = tableData.content;
// Clean up content formatting:
// 1. Split into lines to handle each line individually
const lines = formattedContent.split('\n');
// 2. Process lines to ensure proper formatting
const processedLines = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trimEnd();
// Skip empty lines at the end if followed by a closing brace
if (trimmedLine === '' && i === lines.length - 1) {
continue;
}
// Skip empty lines before a closing brace
if (
trimmedLine === '' &&
i < lines.length - 1 &&
lines[i + 1].trim().startsWith('}')
) {
continue;
}
processedLines.push(line);
}
formattedContent = processedLines.join('\n');
// Ensure content ends with a newline before the table's closing brace
if (!formattedContent.endsWith('\n')) {
formattedContent = formattedContent + '\n';
}
// Since we properly extracted content with nested braces, we need to rebuild the table definition
const tableHeader = originalTableDef.substring(
0,
originalTableDef.indexOf('{') + 1
);
const updatedTableDef = `${tableHeader}${formattedContent}}`;
reconstructedDbml += updatedTableDef;
lastIndex = tableData.end;
}
@@ -388,12 +493,30 @@ const convertToInlineRefs = (dbml: string): string => {
.filter((line) => !line.trim().startsWith('Ref '));
const finalDbml = finalLines.join('\n').trim();
return finalDbml;
// Clean up excessive empty lines - replace multiple consecutive empty lines with just one
// But ensure there's at least one blank line between tables
const cleanedDbml = finalDbml
.replace(/\n\s*\n\s*\n/g, '\n\n')
.replace(/}\n(?=Table)/g, '}\n\n');
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)
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());
};
@@ -483,15 +606,15 @@ const fixTableBracketSyntax = (dbml: string): string => {
};
// Restore schema information that may have been stripped by the DBML importer
const restoreTableSchemas = (dbml: string, diagram: Diagram): string => {
if (!diagram.tables) return dbml;
const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
if (!tables || tables.length === 0) return dbml;
// Group tables by name to handle duplicates
const tablesByName = new Map<
string,
Array<{ table: (typeof diagram.tables)[0]; index: number }>
Array<{ table: DBTable; index: number }>
>();
diagram.tables.forEach((table, index) => {
tables.forEach((table, index) => {
const existing = tablesByName.get(table.name) || [];
existing.push({ table, index });
tablesByName.set(table.name, existing);
@@ -541,30 +664,20 @@ const restoreTableSchemas = (dbml: string, diagram: Diagram): string => {
}
} else {
// Multiple tables with the same name - need to be more careful
const defaultSchema = defaultSchemas[diagram.databaseType];
// Separate tables by whether they have the default schema or not
const defaultSchemaTable = tablesGroup.find(
({ table }) => table.schema === defaultSchema
);
const nonDefaultSchemaTables = tablesGroup.filter(
({ table }) => table.schema && table.schema !== defaultSchema
);
// Find all table definitions for this name
const escapedTableName = tableName.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
// First, handle tables that already have schema in DBML
const schemaTablePattern = new RegExp(
`Table\\s+"[^"]+"\\.\\s*"${escapedTableName}"\\s*{`,
'g'
);
result = result.replace(schemaTablePattern, (match) => {
// This table already has a schema, keep it as is
return match;
// Get tables that need schema restoration (those without schema in DBML)
const tablesNeedingSchema = tablesGroup.filter(({ table }) => {
// Check if this table's schema is already in the DBML
const schemaPattern = new RegExp(
`Table\\s+"${table.schema}"\\.\\s*"${escapedTableName}"\\s*{`,
'g'
);
return !result.match(schemaPattern);
});
// Then handle tables without schema in DBML
@@ -575,21 +688,25 @@ const restoreTableSchemas = (dbml: string, diagram: Diagram): string => {
let noSchemaMatchIndex = 0;
result = result.replace(noSchemaTablePattern, (match) => {
// If we have a table with the default schema and this is the first match without schema,
// it should be the default schema table
if (noSchemaMatchIndex === 0 && defaultSchemaTable) {
noSchemaMatchIndex++;
return `Table "${defaultSchema}"."${tableName}" {`;
// We need to match based on the order in the DBML output
// For PostgreSQL DBML, the @dbml/core sorts tables by:
// 1. Tables with schemas (alphabetically)
// 2. Tables without schemas
// Since both our tables have schemas, they should appear in order
// Only process tables that need schema restoration
if (noSchemaMatchIndex >= tablesNeedingSchema.length) {
return match;
}
// Otherwise, try to match with non-default schema tables
const remainingNonDefault =
nonDefaultSchemaTables[
noSchemaMatchIndex - (defaultSchemaTable ? 1 : 0)
];
if (remainingNonDefault) {
noSchemaMatchIndex++;
return `Table "${remainingNonDefault.table.schema}"."${tableName}" {`;
const correspondingTable =
tablesNeedingSchema[noSchemaMatchIndex];
noSchemaMatchIndex++;
if (correspondingTable && correspondingTable.table.schema) {
return `Table "${correspondingTable.table.schema}"."${tableName}" {`;
}
// If the table doesn't have a schema, keep it as is
return match;
});
}
@@ -664,6 +781,8 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
// --- Final sanitization and renaming pass ---
// Only rename keywords for PostgreSQL/SQLite
// For other databases, we'll wrap problematic names in quotes instead
const shouldRenameKeywords =
diagram.databaseType === DatabaseType.POSTGRESQL ||
diagram.databaseType === DatabaseType.SQLITE;
@@ -683,14 +802,21 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
}
// Rename table if SQL keyword (PostgreSQL only)
if (shouldRenameKeywords && isSQLKeyword(originalName)) {
// Rename table if it's a keyword (PostgreSQL/SQLite only)
if (
shouldRenameKeywords &&
(isDBMLKeyword(originalName) || isSQLKeyword(originalName))
) {
const newName = `${originalName}_table`;
sqlRenamedTables.set(newName, originalName);
safeTableName = /[^\w]/.test(newName)
? `"${newName.replace(/"/g, '\\"')}"`
: newName;
}
// For other databases, just quote DBML keywords
else if (!shouldRenameKeywords && isDBMLKeyword(originalName)) {
safeTableName = `"${originalName.replace(/"/g, '\\"')}"`;
}
const fieldNameCounts = new Map<string, number>();
const processedFields = table.fields.map((field) => {
@@ -717,8 +843,11 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
name: finalSafeName,
};
// Rename field if SQL keyword (PostgreSQL only)
if (shouldRenameKeywords && isSQLKeyword(field.name)) {
// Rename field if it's a keyword (PostgreSQL/SQLite only)
if (
shouldRenameKeywords &&
(isDBMLKeyword(field.name) || isSQLKeyword(field.name))
) {
const newFieldName = `${field.name}_field`;
fieldRenames.push({
table: safeTableName,
@@ -729,6 +858,10 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
? `"${newFieldName.replace(/"/g, '\\"')}"`
: newFieldName;
}
// For other databases, just quote DBML keywords
else if (!shouldRenameKeywords && isDBMLKeyword(field.name)) {
sanitizedField.name = `"${field.name.replace(/"/g, '\\"')}"`;
}
return sanitizedField;
});
@@ -781,8 +914,11 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
baseScript = sanitizeSQLforDBML(baseScript);
// Append comments for renamed tables and fields (PostgreSQL only)
if (shouldRenameKeywords) {
// Append comments for renamed tables and fields (PostgreSQL/SQLite only)
if (
shouldRenameKeywords &&
(sqlRenamedTables.size > 0 || fieldRenames.length > 0)
) {
baseScript = appendRenameComments(
baseScript,
sqlRenamedTables,
@@ -801,12 +937,26 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
);
// Restore schema information that may have been stripped by DBML importer
standard = restoreTableSchemas(standard, diagram);
standard = restoreTableSchemas(standard, uniqueTables);
// Prepend Enum DBML to the standard output
standard = enumsDBML + '\n' + standard;
if (enumsDBML) {
standard = enumsDBML + '\n\n' + standard;
}
inline = normalizeCharTypeFormat(convertToInlineRefs(standard));
// Clean up excessive empty lines in both outputs
standard = standard.replace(/\n\s*\n\s*\n/g, '\n\n');
inline = inline.replace(/\n\s*\n\s*\n/g, '\n\n');
// Ensure proper formatting with newline at end
if (!standard.endsWith('\n')) {
standard += '\n';
}
if (!inline.endsWith('\n')) {
inline += '\n';
}
} catch (error: unknown) {
console.error(
'Error during DBML generation process:',
@@ -822,11 +972,11 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
// If an error occurred, still prepend enums if they exist, or they'll be lost.
// The error message will then follow.
if (standard.startsWith('// Error generating DBML:')) {
standard = enumsDBML + standard;
if (standard.startsWith('// Error generating DBML:') && enumsDBML) {
standard = enumsDBML + '\n\n' + standard;
}
if (inline.startsWith('// Error generating DBML:')) {
inline = enumsDBML + inline;
if (inline.startsWith('// Error generating DBML:') && enumsDBML) {
inline = enumsDBML + '\n\n' + inline;
}
}

View File

@@ -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
});
});

View File

@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { importDBMLToDiagram } from '../dbml-import';
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
describe('DBML Import - Fantasy Examples', () => {
describe('Magical Academy System', () => {
@@ -613,6 +614,229 @@ Note quest_system_note {
});
});
describe('Enum Support', () => {
it('should import enums as customTypes', async () => {
const dbmlWithEnums = `
// Test DBML with various enum definitions
enum job_status {
created [note: 'Waiting to be processed']
running
done
failure
}
// Enum with schema
enum hr.employee_type {
full_time
part_time
contractor
intern
}
// Enum with special characters and spaces
enum grade {
"A+"
"A"
"A-"
"Not Yet Set"
}
Table employees {
id integer [pk]
name varchar(200) [not null]
status job_status
type hr.employee_type
performance_grade grade
created_at timestamp [default: 'now()']
}
Table projects {
id integer [pk]
name varchar(300) [not null]
status job_status [not null]
priority enum // inline enum without values - will be converted to varchar
}`;
const diagram = await importDBMLToDiagram(dbmlWithEnums);
// Verify customTypes are created for enums
expect(diagram.customTypes).toBeDefined();
expect(diagram.customTypes).toHaveLength(3); // job_status, hr.employee_type, grade
// Check job_status enum
const jobStatusEnum = diagram.customTypes?.find(
(ct) => ct.name === 'job_status' && !ct.schema
);
expect(jobStatusEnum).toBeDefined();
expect(jobStatusEnum?.kind).toBe(DBCustomTypeKind.enum);
expect(jobStatusEnum?.values).toEqual([
'created',
'running',
'done',
'failure',
]);
// Check hr.employee_type enum with schema
const employeeTypeEnum = diagram.customTypes?.find(
(ct) => ct.name === 'employee_type' && ct.schema === 'hr'
);
expect(employeeTypeEnum).toBeDefined();
expect(employeeTypeEnum?.kind).toBe(DBCustomTypeKind.enum);
expect(employeeTypeEnum?.values).toEqual([
'full_time',
'part_time',
'contractor',
'intern',
]);
// Check grade enum with quoted values
const gradeEnum = diagram.customTypes?.find(
(ct) => ct.name === 'grade' && !ct.schema
);
expect(gradeEnum).toBeDefined();
expect(gradeEnum?.kind).toBe(DBCustomTypeKind.enum);
expect(gradeEnum?.values).toEqual(['A+', 'A', 'A-', 'Not Yet Set']);
// Verify tables are created
expect(diagram.tables).toHaveLength(2);
// Check that enum fields in tables reference the custom types
const employeesTable = diagram.tables?.find(
(t) => t.name === 'employees'
);
const statusField = employeesTable?.fields.find(
(f) => f.name === 'status'
);
const typeField = employeesTable?.fields.find(
(f) => f.name === 'type'
);
const gradeField = employeesTable?.fields.find(
(f) => f.name === 'performance_grade'
);
// Verify fields have correct types
expect(statusField?.type.id).toBe('job_status');
expect(typeField?.type.id).toBe('employee_type');
expect(gradeField?.type.id).toBe('grade');
// Check inline enum was converted to varchar
const projectsTable = diagram.tables?.find(
(t) => t.name === 'projects'
);
const priorityField = projectsTable?.fields.find(
(f) => f.name === 'priority'
);
expect(priorityField?.type.id).toBe('varchar');
});
it('should handle enum values with notes', async () => {
const dbmlWithEnumNotes = `
enum order_status {
pending [note: 'Order has been placed but not confirmed']
confirmed [note: 'Payment received and order confirmed']
shipped [note: 'Order has been dispatched']
delivered [note: 'Order delivered to customer']
cancelled [note: 'Order cancelled by customer or system']
}
Table orders {
id integer [pk]
status order_status [not null]
}`;
const diagram = await importDBMLToDiagram(dbmlWithEnumNotes);
// Verify enum is created
expect(diagram.customTypes).toHaveLength(1);
const orderStatusEnum = diagram.customTypes?.[0];
expect(orderStatusEnum?.name).toBe('order_status');
expect(orderStatusEnum?.kind).toBe(DBCustomTypeKind.enum);
expect(orderStatusEnum?.values).toEqual([
'pending',
'confirmed',
'shipped',
'delivered',
'cancelled',
]);
});
it('should handle multiple schemas with same enum names', async () => {
const dbmlWithSameEnumNames = `
// Public schema status enum
enum status {
active
inactive
deleted
}
// Admin schema status enum with different values
enum admin.status {
pending_approval
approved
rejected
suspended
}
Table public.users {
id integer [pk]
status status
}
Table admin.users {
id integer [pk]
status admin.status
}`;
const diagram = await importDBMLToDiagram(dbmlWithSameEnumNames);
// Verify both enums are created
expect(diagram.customTypes).toHaveLength(2);
// Check public.status enum
const publicStatusEnum = diagram.customTypes?.find(
(ct) => ct.name === 'status' && !ct.schema
);
expect(publicStatusEnum).toBeDefined();
expect(publicStatusEnum?.values).toEqual([
'active',
'inactive',
'deleted',
]);
// Check admin.status enum
const adminStatusEnum = diagram.customTypes?.find(
(ct) => ct.name === 'status' && ct.schema === 'admin'
);
expect(adminStatusEnum).toBeDefined();
expect(adminStatusEnum?.values).toEqual([
'pending_approval',
'approved',
'rejected',
'suspended',
]);
// Verify fields reference correct enums
// Note: 'public' schema is converted to empty string
const publicUsersTable = diagram.tables?.find(
(t) => t.name === 'users' && t.schema === ''
);
const adminUsersTable = diagram.tables?.find(
(t) => t.name === 'users' && t.schema === 'admin'
);
const publicStatusField = publicUsersTable?.fields.find(
(f) => f.name === 'status'
);
const adminStatusField = adminUsersTable?.fields.find(
(f) => f.name === 'status'
);
expect(publicStatusField?.type.id).toBe('status');
expect(adminStatusField?.type.id).toBe('status');
});
});
describe('Edge Cases and Special Features', () => {
it('should handle tables with all DBML features', async () => {
const edgeCaseDBML = `
@@ -793,11 +1017,11 @@ Table "bb"."users" {
expect(bbUsersTable?.fields).toHaveLength(1);
expect(aaUsersTable?.fields[0].name).toBe('id');
expect(aaUsersTable?.fields[0].type.id).toBe('int');
expect(aaUsersTable?.fields[0].type.id).toBe('integer');
expect(aaUsersTable?.fields[0].primaryKey).toBe(true);
expect(bbUsersTable?.fields[0].name).toBe('id');
expect(bbUsersTable?.fields[0].type.id).toBe('int');
expect(bbUsersTable?.fields[0].type.id).toBe('integer');
expect(bbUsersTable?.fields[0].primaryKey).toBe(true);
});
@@ -852,8 +1076,9 @@ Table "public_3"."comments" {
// Verify tables
expect(diagram.tables).toHaveLength(3);
// Note: 'public' schema is converted to empty string
const usersTable = diagram.tables?.find(
(t) => t.name === 'users' && t.schema === 'public'
(t) => t.name === 'users' && t.schema === ''
);
const postsTable = diagram.tables?.find(
(t) => t.name === 'posts' && t.schema === 'public_2'
@@ -1021,4 +1246,47 @@ Table "public_3"."comments" {
expect(relationshipsHaveSchemas).toBe(true);
});
});
describe('Notes Support', () => {
it('should import table with note', async () => {
const dbmlWithTableNote = `
Table products {
id integer [pk]
name varchar(100)
Note: 'This table stores product information'
}`;
const diagram = await importDBMLToDiagram(dbmlWithTableNote);
expect(diagram.tables).toHaveLength(1);
const productsTable = diagram.tables?.[0];
expect(productsTable?.name).toBe('products');
expect(productsTable?.comments).toBe(
'This table stores product information'
);
});
it('should import field with note', async () => {
const dbmlWithFieldNote = `
Table orders {
id integer [pk]
total numeric(10,2) [note: 'Order total including tax']
}`;
const diagram = await importDBMLToDiagram(dbmlWithFieldNote);
expect(diagram.tables).toHaveLength(1);
const ordersTable = diagram.tables?.[0];
expect(ordersTable?.fields).toHaveLength(2);
const totalField = ordersTable?.fields.find(
(f) => f.name === 'total'
);
// Field notes should be imported
expect(totalField).toBeDefined();
expect(totalField?.name).toBe('total');
expect(totalField?.comments).toBe('Order total including tax');
});
});
});

View 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');
});
});
});

View File

@@ -0,0 +1,40 @@
export interface DBMLError {
message: string;
line: number;
column: number;
}
export function parseDBMLError(error: unknown): DBMLError | null {
try {
if (typeof error === 'string') {
const parsed = JSON.parse(error);
if (parsed.diags?.[0]) {
const diag = parsed.diags[0];
return {
message: diag.message,
line: diag.location.start.line,
column: diag.location.start.column,
};
}
} else if (error && typeof error === 'object' && 'diags' in error) {
const parsed = error as {
diags: Array<{
message: string;
location: { start: { line: number; column: number } };
}>;
};
if (parsed.diags?.[0]) {
return {
message: parsed.diags[0].message,
line: parsed.diags[0].location.start.line,
column: parsed.diags[0].location.start.column,
};
}
}
} catch (e) {
console.error('Error parsing DBML error:', e);
}
return null;
}

View File

@@ -4,10 +4,16 @@ import { generateDiagramId, generateId } from '@/lib/utils';
import type { DBTable } from '@/lib/domain/db-table';
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
import type { DBField } from '@/lib/domain/db-field';
import type { DataType } from '@/lib/data/data-types/data-types';
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
import { randomColor } from '@/lib/colors';
import type { DataTypeData } from '@/lib/data/data-types/data-types';
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
import { defaultTableColor } from '@/lib/colors';
import { DatabaseType } from '@/lib/domain/database-type';
import type Field from '@dbml/core/types/model_structure/field';
import type { DBIndex } from '@/lib/domain';
import {
DBCustomTypeKind,
type DBCustomType,
} from '@/lib/domain/db-custom-type';
// Preprocess DBML to handle unsupported features
export const preprocessDBML = (content: string): string => {
@@ -19,8 +25,8 @@ export const preprocessDBML = (content: string): string => {
// Remove Note blocks
processed = processed.replace(/Note\s+\w+\s*\{[^}]*\}/gs, '');
// Remove enum definitions (blocks)
processed = processed.replace(/enum\s+\w+\s*\{[^}]*\}/gs, '');
// Don't remove enum definitions - we'll parse them
// processed = processed.replace(/enum\s+\w+\s*\{[^}]*\}/gs, '');
// Handle array types by converting them to text
processed = processed.replace(/(\w+)\[\]/g, 'text');
@@ -77,6 +83,10 @@ interface DBMLField {
pk?: boolean;
not_null?: boolean;
increment?: boolean;
characterMaximumLength?: string | null;
precision?: number | null;
scale?: number | null;
note?: string | { value: string } | null;
}
interface DBMLIndexColumn {
@@ -110,39 +120,51 @@ interface DBMLRef {
endpoints: [DBMLEndpoint, DBMLEndpoint];
}
const mapDBMLTypeToGenericType = (dbmlType: string): DataType => {
interface DBMLEnum {
name: string;
schema?: string | { name: string };
values: Array<{ name: string; note?: string }>;
note?: string | { value: string } | null;
}
const mapDBMLTypeToDataType = (
dbmlType: string,
options?: { databaseType?: DatabaseType; enums?: DBMLEnum[] }
): DataTypeData => {
const normalizedType = dbmlType.toLowerCase().replace(/\(.*\)/, '');
const matchedType = genericDataTypes.find((t) => t.id === normalizedType);
if (matchedType) return matchedType;
const typeMap: Record<string, string> = {
int: 'int',
integer: 'int',
varchar: 'varchar',
bool: 'boolean',
boolean: 'boolean',
number: 'numeric',
string: 'varchar',
text: 'text',
timestamp: 'timestamp',
datetime: 'timestamp',
float: 'float',
double: 'double',
decimal: 'decimal',
bigint: 'bigint',
smallint: 'smallint',
char: 'char',
};
const mappedType = typeMap[normalizedType];
if (mappedType) {
const foundType = genericDataTypes.find((t) => t.id === mappedType);
if (foundType) return foundType;
// Check if it's an enum type
if (options?.enums) {
const enumDef = options.enums.find((e) => {
// Check both with and without schema prefix
const enumName = e.name.toLowerCase();
const enumFullName = e.schema
? `${e.schema}.${enumName}`
: enumName;
return (
normalizedType === enumName || normalizedType === enumFullName
);
});
if (enumDef) {
// Return enum as custom type reference
return {
id: enumDef.name,
name: enumDef.name,
} satisfies DataTypeData;
}
}
const type = genericDataTypes.find((t) => t.id === 'varchar')!;
const matchedType = findDataTypeDataById(
normalizedType,
options?.databaseType
);
if (matchedType) return matchedType;
return {
id: type.id,
name: type.name,
};
id: normalizedType.split(' ').join('_').toLowerCase(),
name: normalizedType,
} satisfies DataTypeData;
};
const determineCardinality = (
@@ -163,7 +185,10 @@ const determineCardinality = (
};
export const importDBMLToDiagram = async (
dbmlContent: string
dbmlContent: string,
options?: {
databaseType?: DatabaseType;
}
): Promise<Diagram> => {
try {
// Handle empty content
@@ -171,7 +196,7 @@ export const importDBMLToDiagram = async (
return {
id: generateDiagramId(),
name: 'DBML Import',
databaseType: DatabaseType.GENERIC,
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
tables: [],
relationships: [],
createdAt: new Date(),
@@ -189,7 +214,7 @@ export const importDBMLToDiagram = async (
return {
id: generateDiagramId(),
name: 'DBML Import',
databaseType: DatabaseType.GENERIC,
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
tables: [],
relationships: [],
createdAt: new Date(),
@@ -204,7 +229,7 @@ export const importDBMLToDiagram = async (
return {
id: generateDiagramId(),
name: 'DBML Import',
databaseType: DatabaseType.GENERIC,
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
tables: [],
relationships: [],
createdAt: new Date(),
@@ -215,6 +240,81 @@ export const importDBMLToDiagram = async (
// Process all schemas, not just the first one
const allTables: DBMLTable[] = [];
const allRefs: DBMLRef[] = [];
const allEnums: DBMLEnum[] = [];
const getFieldExtraAttributes = (
field: Field,
enums: DBMLEnum[]
): Partial<DBMLField> => {
// First check if the type name itself contains the length (e.g., "character varying(50)")
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());
}
// 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, {
...options,
enums,
});
// Check if this is a character type that should have a max length
const baseTypeName = typeName
.replace(/\(.*\)/, '')
.toLowerCase()
.replace(/['"]/g, '');
const isCharType =
baseTypeName.includes('char') ||
baseTypeName.includes('varchar') ||
baseTypeName === 'text' ||
baseTypeName === 'string';
if (isCharType && args[0]) {
return {
characterMaximumLength: args[0],
};
} else if (
dataType.fieldAttributes?.precision &&
dataType.fieldAttributes?.scale
) {
const precisionNum = args?.[0] ? parseInt(args[0]) : undefined;
const scaleNum = args?.[1] ? parseInt(args[1]) : undefined;
const precision = precisionNum
? isNaN(precisionNum)
? undefined
: precisionNum
: undefined;
const scale = scaleNum
? isNaN(scaleNum)
? undefined
: scaleNum
: undefined;
return {
precision,
scale,
};
}
return {};
};
parsedData.schemas.forEach((schema) => {
if (schema.tables) {
@@ -230,17 +330,18 @@ export const importDBMLToDiagram = async (
name: table.name,
schema: schemaName,
note: table.note,
fields: table.fields.map(
(field) =>
({
name: field.name,
type: field.type,
unique: field.unique,
pk: field.pk,
not_null: field.not_null,
increment: field.increment,
}) satisfies DBMLField
),
fields: table.fields.map((field): DBMLField => {
return {
name: field.name,
type: field.type,
unique: field.unique,
pk: field.pk,
not_null: field.not_null,
increment: field.increment,
note: field.note,
...getFieldExtraAttributes(field, allEnums),
} satisfies DBMLField;
}),
indexes:
table.indexes?.map((dbmlIndex) => {
let indexColumns: string[];
@@ -314,15 +415,34 @@ export const importDBMLToDiagram = async (
}
});
}
if (schema.enums) {
schema.enums.forEach((enumDef) => {
// Get schema name from enum or use schema's name
const enumSchema =
typeof enumDef.schema === 'string'
? enumDef.schema
: enumDef.schema?.name || schema.name;
allEnums.push({
name: enumDef.name,
schema: enumSchema === 'public' ? '' : enumSchema,
values: enumDef.values || [],
note: enumDef.note,
});
});
}
});
// Extract only the necessary data from the parsed DBML
const extractedData: {
tables: DBMLTable[];
refs: DBMLRef[];
enums: DBMLEnum[];
} = {
tables: allTables,
refs: allRefs,
enums: allEnums,
};
// Convert DBML tables to ChartDB table objects
@@ -332,18 +452,40 @@ export const importDBMLToDiagram = async (
const tableSpacing = 300;
// Create fields first so we have their IDs
const fields = table.fields.map((field) => ({
id: generateId(),
name: field.name.replace(/['"]/g, ''),
type: mapDBMLTypeToGenericType(field.type.type_name),
nullable: !field.not_null,
primaryKey: field.pk || false,
unique: field.unique || false,
createdAt: Date.now(),
}));
const fields: DBField[] = table.fields.map((field) => {
// Extract field note/comment
let fieldComment: string | undefined;
if (field.note) {
if (typeof field.note === 'string') {
fieldComment = field.note;
} else if (
typeof field.note === 'object' &&
'value' in field.note
) {
fieldComment = field.note.value;
}
}
return {
id: generateId(),
name: field.name.replace(/['"]/g, ''),
type: mapDBMLTypeToDataType(field.type.type_name, {
...options,
enums: extractedData.enums,
}),
nullable: !field.not_null,
primaryKey: field.pk || false,
unique: field.unique || false,
createdAt: Date.now(),
characterMaximumLength: field.characterMaximumLength,
precision: field.precision,
scale: field.scale,
...(fieldComment ? { comments: fieldComment } : {}),
};
});
// Convert DBML indexes to ChartDB indexes
const indexes =
const indexes: DBIndex[] =
table.indexes?.map((dbmlIndex) => {
const fieldIds = dbmlIndex.columns.map((columnName) => {
const field = fields.find((f) => f.name === columnName);
@@ -384,18 +526,20 @@ export const importDBMLToDiagram = async (
name: table.name.replace(/['"]/g, ''),
schema:
typeof table.schema === 'string'
? table.schema
? table.schema === 'public'
? ''
: table.schema
: table.schema?.name || '',
order: index,
fields,
indexes,
x: col * tableSpacing,
y: row * tableSpacing,
color: randomColor(),
color: defaultTableColor,
isView: false,
createdAt: Date.now(),
comments: tableComment,
};
} satisfies DBTable;
});
// Create relationships using the refs
@@ -449,12 +593,43 @@ export const importDBMLToDiagram = async (
}
);
// Convert DBML enums to custom types
const customTypes: DBCustomType[] = extractedData.enums.map(
(enumDef) => {
// Extract values from enum
const values = enumDef.values
.map((v) => {
// Handle both string values and objects with name property
if (typeof v === 'string') {
return v;
} else if (v && typeof v === 'object' && 'name' in v) {
return v.name.replace(/["']/g, ''); // Remove quotes from values
}
return '';
})
.filter((v) => v !== '');
return {
id: generateId(),
schema:
typeof enumDef.schema === 'string'
? enumDef.schema
: undefined,
name: enumDef.name,
kind: DBCustomTypeKind.enum,
values,
order: 0,
} satisfies DBCustomType;
}
);
return {
id: generateDiagramId(),
name: 'DBML Import',
databaseType: DatabaseType.GENERIC,
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
tables,
relationships,
customTypes,
createdAt: new Date(),
updatedAt: new Date(),
};

View File

@@ -1,5 +1,4 @@
export interface ChartDBConfig {
defaultDiagramId: string;
exportActions?: Date[];
hiddenTablesByDiagram?: Record<string, string[]>; // Maps diagram ID to array of hidden table IDs
}

View File

@@ -9,3 +9,9 @@ export enum DatabaseType {
COCKROACHDB = 'cockroachdb',
ORACLE = 'oracle',
}
export const databaseTypesWithCommentSupport: DatabaseType[] = [
DatabaseType.POSTGRESQL,
DatabaseType.COCKROACHDB,
DatabaseType.ORACLE,
];

View File

@@ -1,10 +1,7 @@
import { z } from 'zod';
import type { ViewInfo } from '../data/import-metadata/metadata-types/view-info';
import { DatabaseType } from './database-type';
import {
schemaNameToDomainSchemaName,
schemaNameToSchemaId,
} from './db-schema';
import { schemaNameToDomainSchemaName } from './db-schema';
import { decodeViewDefinition, type DBTable } from './db-table';
import { generateId } from '@/lib/utils';
import type { AST } from 'node-sql-parser';
@@ -27,18 +24,6 @@ export const dbDependencySchema: z.ZodType<DBDependency> = z.object({
createdAt: z.number(),
});
export const shouldShowDependencyBySchemaFilter = (
dependency: DBDependency,
filteredSchemas?: string[]
): boolean =>
!filteredSchemas ||
!dependency.schema ||
!dependency.dependentSchema ||
(filteredSchemas.includes(schemaNameToSchemaId(dependency.schema)) &&
filteredSchemas.includes(
schemaNameToSchemaId(dependency.dependentSchema)
));
const astDatabaseTypes: Record<DatabaseType, string> = {
[DatabaseType.POSTGRESQL]: 'postgresql',
[DatabaseType.MYSQL]: 'postgresql',

View File

@@ -2,6 +2,25 @@ import { z } from 'zod';
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
import { generateId } from '../utils';
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 {
id: string;
@@ -9,6 +28,7 @@ export interface DBIndex {
unique: boolean;
fieldIds: string[];
createdAt: number;
type?: IndexType | null;
}
export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
@@ -17,6 +37,7 @@ export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
unique: z.boolean(),
fieldIds: z.array(z.string()),
createdAt: z.number(),
type: z.enum(INDEX_TYPES).optional(),
});
export const createIndexesFromMetadata = ({
@@ -36,5 +57,10 @@ export const createIndexesFromMetadata = ({
.map((c) => fields.find((f) => f.name === c.name)?.id)
.filter((id): id is string => id !== undefined),
createdAt: Date.now(),
type: idx.index_type?.toLowerCase() as IndexType,
})
);
export const databaseIndexTypes: { [key in DatabaseType]?: IndexType[] } = {
[DatabaseType.POSTGRESQL]: ['btree', 'hash'],
};

View File

@@ -1,10 +1,7 @@
import { z } from 'zod';
import type { ForeignKeyInfo } from '../data/import-metadata/metadata-types/foreign-key-info';
import type { DBField } from './db-field';
import {
schemaNameToDomainSchemaName,
schemaNameToSchemaId,
} from './db-schema';
import { schemaNameToDomainSchemaName } from './db-schema';
import type { DBTable } from './db-table';
import { generateId } from '@/lib/utils';
@@ -43,20 +40,6 @@ export type RelationshipType =
| 'many_to_many';
export type Cardinality = 'one' | 'many';
export const shouldShowRelationshipBySchemaFilter = (
relationship: DBRelationship,
filteredSchemas?: string[]
): boolean =>
!filteredSchemas ||
!relationship.sourceSchema ||
!relationship.targetSchema ||
(filteredSchemas.includes(
schemaNameToSchemaId(relationship.sourceSchema)
) &&
filteredSchemas.includes(
schemaNameToSchemaId(relationship.targetSchema)
));
const determineCardinality = (
field: DBField,
isTablePKComplex: boolean

View File

@@ -10,7 +10,11 @@ import {
} from './db-field';
import type { TableInfo } from '../data/import-metadata/metadata-types/table-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 {
decodeBase64ToUtf16LE,
@@ -18,13 +22,11 @@ import {
deepCopy,
generateId,
} from '../utils';
import {
schemaNameToDomainSchemaName,
schemaNameToSchemaId,
} from './db-schema';
import { schemaNameToDomainSchemaName } from './db-schema';
import { DatabaseType } from './database-type';
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
import { z } from 'zod';
import type { Area } from './area';
export const MAX_TABLE_SIZE = 450;
export const MID_TABLE_SIZE = 337;
@@ -77,26 +79,6 @@ export const generateTableKey = ({
tableName: string;
}) => `${schemaNameToDomainSchemaName(schemaName) ?? ''}.${tableName}`;
export const shouldShowTableSchemaBySchemaFilter = ({
filteredSchemas,
tableSchema,
}: {
tableSchema?: string | null;
filteredSchemas?: string[];
}): boolean =>
!filteredSchemas ||
!tableSchema ||
filteredSchemas.includes(schemaNameToSchemaId(tableSchema));
export const shouldShowTablesBySchemaFilter = (
table: DBTable,
filteredSchemas?: string[]
): boolean =>
shouldShowTableSchemaBySchemaFilter({
filteredSchemas,
tableSchema: table?.schema,
});
export const decodeViewDefinition = (
databaseType: DatabaseType,
viewDefinition?: string
@@ -247,7 +229,7 @@ export const createTablesFromMetadata = ({
? materializedViewColor
: isView
? viewColor
: randomColor(),
: defaultTableColor,
isView: isView,
isMaterializedView: isMaterializedView,
createdAt: Date.now(),
@@ -258,89 +240,170 @@ export const createTablesFromMetadata = ({
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 = ({
relationships: inputRelationships,
tables: inputTables,
areas: inputAreas = [],
mode = 'all',
}: {
tables: DBTable[];
relationships: DBRelationship[];
areas?: Area[];
mode?: 'all' | 'perSchema';
}): DBTable[] => {
// For large databases, use simple grid layout for better performance
if (inputTables.length > 200) {
const result = adjustTablePositionsSimple(inputTables, mode);
return result;
}
// For smaller databases, use the existing complex algorithm
// Deep copy inputs for manipulation
const tables = deepCopy(inputTables);
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 defaultTableWidth = 200;
const defaultTableHeight = 300;
@@ -362,8 +425,23 @@ export const adjustTablePositions = ({
tableConnections.get(rel.targetTableId)!.add(rel.sourceTableId);
});
// Sort tables by number of connections
const sortedTables = [...tablesToAdjust].sort(
// Separate tables into connected and isolated
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) =>
(tableConnections.get(b.id)?.size || 0) -
(tableConnections.get(a.id)?.size || 0)
@@ -391,6 +469,7 @@ export const adjustTablePositions = ({
y: number,
currentTableId: string
): boolean => {
// Check overlap with other tables
for (const [tableId, pos] of tablePositions) {
if (tableId === currentTableId) continue;
@@ -402,6 +481,26 @@ export const adjustTablePositions = ({
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;
};
@@ -485,19 +584,80 @@ export const adjustTablePositions = ({
});
};
// Position tables
sortedTables.forEach((table, index) => {
if (!positionedTables.has(table.id)) {
const row = Math.floor(index / 6);
const col = index % 6;
const { width: tableWidth, height: tableHeight } =
getTableWidthAndHeight(table.id);
// Position connected tables first
if (connectedTables.length < 100) {
// Use relationship-based positioning for small sets of connected tables
connectedTables.forEach((table, index) => {
if (!positionedTables.has(table.id)) {
const row = Math.floor(index / 6);
const col = index % 6;
const { width: tableWidth, height: tableHeight } =
getTableWidthAndHeight(table.id);
const x = startX + col * (tableWidth + gapX * 2);
const y = startY + row * (tableHeight + gapY * 2);
positionTable(table, x, y);
const x = startX + col * (tableWidth + gapX * 2);
const y = startY + row * (tableHeight + gapY * 2);
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
tablesToAdjust.forEach((table) => {
@@ -531,7 +691,7 @@ export const adjustTablePositions = ({
}
return tables;
};
}
export const calcTableHeight = (table?: DBTable): number => {
if (!table) {

View File

@@ -0,0 +1,201 @@
// union logic filter
export interface DiagramFilter {
schemaIds?: string[];
tableIds?: string[];
}
export interface FilterTableInfo {
id: string;
schemaId?: string;
schema?: string;
areaId?: string;
}
/**
* Reduces/optimizes a DiagramFilter by removing redundant entries
* - Removes tableIds that belong to schemas already in schemaIds (union logic)
* - Consolidates complete schemas: if all tables from a schema are in tableIds, adds the schema to schemaIds
* - Returns undefined for both fields if everything is displayed
* - Returns empty arrays if nothing should be displayed
*/
export function reduceFilter(
filter: DiagramFilter,
tables: FilterTableInfo[],
options: { databaseWithSchemas: boolean }
): DiagramFilter {
let { schemaIds, tableIds } = filter;
// If no filters are defined, everything is visible
if (!schemaIds && !tableIds) {
return { schemaIds: undefined, tableIds: undefined };
}
if (!schemaIds && tableIds && tableIds.length === 0) {
return { schemaIds: undefined, tableIds: [] };
}
// Get all unique schema IDs from tables
const allSchemaIds = options.databaseWithSchemas
? [...new Set(tables.filter((t) => t.schemaId).map((t) => t.schemaId!))]
: [];
const allTableIds = tables.map((t) => t.id);
// in case its db with no schemas
if (allSchemaIds.length === 0) {
const tableSet = new Set(tableIds);
if (tableSet.size === allTableIds.length) {
return { schemaIds: undefined, tableIds: undefined };
}
return { schemaIds: undefined, tableIds: Array.from(tableSet) };
}
// Build a map of schema to its tables
const schemaToTables = new Map<string, string[]>();
tables.forEach((table) => {
if (table.schemaId) {
if (!schemaToTables.has(table.schemaId)) {
schemaToTables.set(table.schemaId, []);
}
schemaToTables.get(table.schemaId)!.push(table.id);
}
});
// Consolidate complete schemas: if all tables from a schema are in tableIds, add schema to schemaIds
if (tableIds) {
const tableSet = new Set(tableIds);
const consolidatedSchemaIds = new Set(schemaIds || []);
let consolidatedTableIds = [...tableIds];
for (const [schemaId, schemaTables] of schemaToTables.entries()) {
// Check if all tables from this schema are in tableIds
if (schemaTables.every((tableId) => tableSet.has(tableId))) {
// Add schema to schemaIds
consolidatedSchemaIds.add(schemaId);
// Remove these tables from tableIds
consolidatedTableIds = consolidatedTableIds.filter(
(id) => !schemaTables.includes(id)
);
}
}
schemaIds =
consolidatedSchemaIds.size > 0
? Array.from(consolidatedSchemaIds)
: schemaIds;
tableIds =
consolidatedTableIds.length > 0 ? consolidatedTableIds : undefined;
}
// If all schemas are in the filter, everything is visible
if (schemaIds && schemaIds.length === allSchemaIds.length) {
const schemasSet = new Set(schemaIds);
const allSchemasIncluded = allSchemaIds.every((id) =>
schemasSet.has(id)
);
if (allSchemasIncluded) {
return { schemaIds: undefined, tableIds: undefined };
}
}
// If schemaIds is defined, remove tables from tableIds that belong to those schemas
let reducedTableIds = tableIds;
if (schemaIds && tableIds) {
const schemaSet = new Set(schemaIds);
reducedTableIds = tableIds.filter((tableId) => {
const table = tables.find((t) => t.id === tableId);
// Keep table in tableIds only if it doesn't belong to a schema in schemaIds
return !table?.schemaId || !schemaSet.has(table.schemaId);
});
// If no tables remain after reduction, set to undefined
if (reducedTableIds.length === 0) {
reducedTableIds = undefined;
}
}
// Check if all tables are now visible (either through schemas or direct table IDs)
if (schemaIds && reducedTableIds) {
const schemaSet = new Set(schemaIds);
const tableSet = new Set(reducedTableIds);
const visibleTables = tables.filter((table) => {
// Table is visible if it's in tableIds OR its schema is in schemaIds
return (
tableSet.has(table.id) ||
(table.schemaId && schemaSet.has(table.schemaId))
);
});
if (visibleTables.length === tables.length) {
return { schemaIds: undefined, tableIds: undefined };
}
} else if (schemaIds && !reducedTableIds) {
// Only schemaIds is defined, check if all tables are covered by schemas
const schemaSet = new Set(schemaIds);
const visibleTables = tables.filter(
(table) => table.schemaId && schemaSet.has(table.schemaId)
);
if (visibleTables.length === tables.length) {
return { schemaIds: undefined, tableIds: undefined };
}
} else if (!schemaIds && reducedTableIds) {
// Only tableIds is defined, check if all tables are in the filter
if (reducedTableIds.length === allTableIds.length) {
return { schemaIds: undefined, tableIds: undefined };
}
}
return {
schemaIds,
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) };
};

View File

@@ -0,0 +1,134 @@
import { defaultSchemas } from '@/lib/data/default-schemas';
import { schemaNameToSchemaId } from '../db-schema';
import type { Diagram } from '../diagram';
import type { DiagramFilter } from './diagram-filter';
export const filterTable = ({
table,
filter,
options = { defaultSchema: undefined },
}: {
table: { id: string; schema?: string | null };
filter?: DiagramFilter;
options?: {
defaultSchema?: string;
};
}): boolean => {
if (!filter) {
return true;
}
if (!filter.tableIds && !filter.schemaIds) {
return true;
}
if (filter.tableIds && filter.tableIds.includes(table.id)) {
return true;
}
const tableSchema = table.schema ?? options.defaultSchema;
if (
tableSchema &&
filter.schemaIds &&
filter.schemaIds.includes(schemaNameToSchemaId(tableSchema))
) {
return true;
}
return false;
};
export const filterRelationship = ({
tableA: { id: tableAId, schema: tableASchema },
tableB: { id: tableBId, schema: tableBSchema },
filter,
options = { defaultSchema: undefined },
}: {
tableA: { id: string; schema?: string | null };
tableB: { id: string; schema?: string | null };
filter?: DiagramFilter;
options?: {
defaultSchema?: string;
};
}): boolean => {
if (!filter) {
return true;
}
const isTableAVisible = filterTable({
table: { id: tableAId, schema: tableASchema },
filter,
options,
});
const isTableBVisible = filterTable({
table: { id: tableBId, schema: tableBSchema },
filter,
options,
});
return isTableAVisible && isTableBVisible;
};
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,
};
};

View File

@@ -329,6 +329,27 @@ function compareFieldProperties({
changedAttributes.push('comments');
}
if (
(newField.characterMaximumLength || oldField.characterMaximumLength) &&
oldField.characterMaximumLength !== newField.characterMaximumLength
) {
changedAttributes.push('characterMaximumLength');
}
if (
(newField.scale || oldField.scale) &&
oldField.scale !== newField.scale
) {
changedAttributes.push('scale');
}
if (
(newField.precision || oldField.precision) &&
oldField.precision !== newField.precision
) {
changedAttributes.push('precision');
}
if (changedAttributes.length > 0) {
for (const attribute of changedAttributes) {
diffMap.set(

View File

@@ -12,7 +12,10 @@ export type FieldDiffAttribute =
| 'primaryKey'
| 'unique'
| 'nullable'
| 'comments';
| 'comments'
| 'characterMaximumLength'
| 'precision'
| 'scale';
export const fieldDiffAttributeSchema: z.ZodType<FieldDiffAttribute> = z.union([
z.literal('name'),
@@ -61,8 +64,8 @@ export interface FieldDiffChanged {
fieldId: string;
tableId: string;
attribute: FieldDiffAttribute;
oldValue: string | boolean | DataType;
newValue: string | boolean | DataType;
oldValue: string | boolean | DataType | number;
newValue: string | boolean | DataType | number;
}
export const fieldDiffChangedSchema: z.ZodType<FieldDiffChanged> = z.object({

View File

@@ -80,7 +80,7 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo(
<NodeResizer
isVisible={focused}
lineClassName="!border-4 !border-transparent"
handleClassName="!h-[18px] !w-[18px] !rounded-full !bg-pink-600"
handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
minHeight={100}
minWidth={100}
/>

View File

@@ -10,16 +10,19 @@ import { useDialog } from '@/hooks/use-dialog';
import { useReactFlow } from '@xyflow/react';
import React, { useCallback } from 'react';
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 { useLocalConfig } from '@/hooks/use-local-config';
export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { createTable, filteredSchemas, schemas, readonly, createArea } =
useChartDB();
const { createTable, readonly, createArea } = useChartDB();
const { schemasDisplayed } = useDiagramFilter();
const { openCreateRelationshipDialog, openTableSchemaDialog } = useDialog();
const { screenToFlowPosition } = useReactFlow();
const { t } = useTranslation();
const { showDBViews } = useLocalConfig();
const { isMd: isDesktop } = useBreakpoint('md');
@@ -30,7 +33,7 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
y: event.clientY,
});
if ((filteredSchemas?.length ?? 0) > 1) {
if (schemasDisplayed.length > 1) {
openTableSchemaDialog({
onConfirm: ({ schema }) =>
createTable({
@@ -38,14 +41,12 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
y: position.y,
schema: schema.name,
}),
schemas: schemas.filter((schema) =>
filteredSchemas?.includes(schema.id)
),
schemas: schemasDisplayed,
});
} else {
const schema =
filteredSchemas?.length === 1
? schemas.find((s) => s.id === filteredSchemas[0])?.name
schemasDisplayed?.length === 1
? schemasDisplayed[0]?.name
: undefined;
createTable({
x: position.x,
@@ -58,8 +59,46 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
createTable,
screenToFlowPosition,
openTableSchemaDialog,
schemas,
filteredSchemas,
schemasDisplayed,
]
);
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,
]
);
@@ -82,13 +121,15 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
openCreateRelationshipDialog();
}, [openCreateRelationshipDialog]);
if (!isDesktop || readonly) {
if (!isDesktop) {
return <>{children}</>;
}
return (
<ContextMenu>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuTrigger disabled={readonly}>
{children}
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={createTableHandler}
@@ -97,6 +138,15 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
{t('canvas_context_menu.new_table')}
<Table className="size-3.5" />
</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
onClick={createRelationshipHandler}
className="flex justify-between gap-4"

View File

@@ -5,136 +5,98 @@ import React, {
useEffect,
useRef,
} 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 { useTranslation } from 'react-i18next';
import { Button } from '@/components/button/button';
import { Input } from '@/components/input/input';
import { shouldShowTableSchemaBySchemaFilter } from '@/lib/domain/db-table';
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { useReactFlow } from '@xyflow/react';
import { TreeView } from '@/components/tree-view/tree-view';
import type { TreeNode } from '@/components/tree-view/tree';
import { ScrollArea } from '@/components/scroll-area/scroll-area';
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 {
onClose: () => void;
}
type NodeType = 'schema' | 'table';
type SchemaContext = { name: string };
type TableContext = {
tableSchema?: string | null;
hidden: boolean;
};
type NodeContext = {
schema: SchemaContext;
table: TableContext;
};
type RelevantTableData = {
id: string;
name: string;
schema?: string | null;
};
export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
const { t } = useTranslation();
const { tables, databaseType, areas } = useChartDB();
const {
tables,
databaseType,
hiddenTableIds,
addHiddenTableId,
removeHiddenTableId,
filteredSchemas,
filterSchemas,
} = useChartDB();
filter,
toggleSchemaFilter,
toggleTableFilter,
clearTableIdsFilter,
setTableIdsFilterEmpty,
addTablesToFilter,
removeTablesFromFilter,
} = useDiagramFilter();
const { fitView, setNodes } = useReactFlow();
const [searchQuery, setSearchQuery] = useState('');
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [isFilterVisible, setIsFilterVisible] = useState(false);
const [groupingMode, setGroupingMode] = useState<GroupingMode>('schema');
const searchInputRef = useRef<HTMLInputElement>(null);
const { showDBViews } = useLocalConfig();
// Extract only the properties needed for tree data
const relevantTableData = useMemo<RelevantTableData[]>(
() =>
tables.map((table) => ({
id: table.id,
name: table.name,
schema: table.schema,
})),
[tables]
tables
.filter((table) => (showDBViews ? true : !table.isView))
.map((table) => ({
id: table.id,
name: table.name,
schema: table.schema,
parentAreaId: table.parentAreaId,
})),
[tables, showDBViews]
);
const databaseWithSchemas = useMemo(
() => databasesWithSchemas.includes(databaseType),
[databaseType]
);
// Convert tables to tree nodes
const treeData = useMemo(() => {
// Group tables by schema
const tablesBySchema = new Map<string, RelevantTableData[]>();
relevantTableData.forEach((table) => {
const schema =
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) => {
const schemaId = schemaNameToSchemaId(schemaName);
const schemaHidden = filteredSchemas
? !filteredSchemas.includes(schemaId)
: false;
const schemaNode: TreeNode<NodeType, NodeContext> = {
id: `schema-${schemaName}`,
name: `${schemaName} (${schemaTables.length})`,
type: 'schema',
isFolder: true,
icon: Database,
context: { name: schemaName },
className: schemaHidden ? 'opacity-50' : '',
children: schemaTables.map(
(table): TreeNode<NodeType, NodeContext> => {
const tableHidden =
hiddenTableIds?.includes(table.id) ?? false;
const visibleBySchema =
shouldShowTableSchemaBySchemaFilter({
tableSchema: table.schema,
filteredSchemas,
});
const hidden = tableHidden || !visibleBySchema;
return {
id: table.id,
name: table.name,
type: 'table',
isFolder: false,
icon: Table,
context: {
tableSchema: table.schema,
hidden: tableHidden,
},
className: hidden ? 'opacity-50' : '',
};
}
),
};
nodes.push(schemaNode);
});
return nodes;
}, [relevantTableData, databaseType, hiddenTableIds, filteredSchemas]);
if (groupingMode === 'area') {
return generateTreeDataByAreas({
areas,
databaseType,
filter,
relevantTableData,
});
} else {
return generateTreeDataBySchemas({
relevantTableData,
databaseWithSchemas,
databaseType,
filter,
});
}
}, [
relevantTableData,
databaseType,
filter,
databaseWithSchemas,
groupingMode,
areas,
]);
// Initialize expanded state with all schemas expanded
useMemo(() => {
@@ -170,15 +132,29 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
return result;
}, [treeData, searchQuery]);
const toggleTableVisibility = useCallback(
async (tableId: string, hidden: boolean) => {
if (hidden) {
await addHiddenTableId(tableId);
} else {
await removeHiddenTableId(tableId);
}
},
[addHiddenTableId, removeHiddenTableId]
// 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(
@@ -216,141 +192,20 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
[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 schemaHidden = filteredSchemas
? !filteredSchemas.includes(schemaId)
: false;
return (
<Button
variant="ghost"
size="sm"
className="size-7 h-fit p-0"
onClick={(e) => {
e.stopPropagation();
// unhide all tables in this schema
node.children?.forEach((child) => {
if (
child.type === 'table' &&
hiddenTableIds?.includes(child.id)
) {
removeHiddenTableId(child.id);
}
});
if (schemaHidden) {
filterSchemas([
...(filteredSchemas ?? []),
schemaId,
]);
} else {
filterSchemas(
filteredSchemas?.filter(
(s) => s !== schemaId
) ?? []
);
}
}}
>
{schemaHidden ? (
<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 hidden = tableContext.hidden;
const tableSchema = tableContext.tableSchema;
const visibleBySchema = shouldShowTableSchemaBySchemaFilter({
tableSchema,
filteredSchemas,
});
return (
<Button
variant="ghost"
size="sm"
className="size-7 h-fit p-0"
onClick={(e) => {
e.stopPropagation();
if (!visibleBySchema && tableSchema) {
// Unhide schema and hide all other tables
const schemaId =
schemaNameToSchemaId(tableSchema);
filterSchemas([
...(filteredSchemas ?? []),
schemaId,
]);
const schemaNode = treeData.find(
(s) =>
(s.context as SchemaContext).name ===
tableSchema
);
if (schemaNode) {
schemaNode.children?.forEach((child) => {
if (
child.id !== tableId &&
!hiddenTableIds?.includes(child.id)
) {
addHiddenTableId(child.id);
}
});
}
} else {
toggleTableVisibility(tableId, !hidden);
}
}}
>
{hidden || !visibleBySchema ? (
<EyeOff className="size-3.5 text-muted-foreground" />
) : (
<Eye className="size-3.5" />
)}
</Button>
);
}
return null;
},
[
toggleTableVisibility,
filteredSchemas,
filterSchemas,
treeData,
hiddenTableIds,
addHiddenTableId,
removeHiddenTableId,
]
);
// Handle node click
const handleNodeClick = useCallback(
(node: TreeNode<NodeType, NodeContext>) => {
if (node.type === 'table') {
const tableContext = node.context as TableContext;
const tableSchema = tableContext.tableSchema;
const visibleBySchema = shouldShowTableSchemaBySchemaFilter({
tableSchema,
filteredSchemas,
});
const context = node.context as TableContext;
const isTableVisible = context.visible;
// Only focus if neither table is hidden nor filtered by schema
if (!tableContext.hidden && visibleBySchema) {
// Only focus if table is visible
if (isTableVisible) {
focusOnTable(node.id);
}
}
},
[focusOnTable, filteredSchemas]
[focusOnTable]
);
// Animate in on mount and focus search input
@@ -362,6 +217,11 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
}, 300);
}, []);
const openFilterShortcut = useMemo(
() => (getOperatingSystem() === 'mac' ? '⌘' : 'Ctrl+') + 'F',
[]
);
return (
<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 ${
@@ -375,8 +235,11 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
<div className="flex items-center gap-2">
<Funnel className="size-3.5 text-muted-foreground md:size-4" />
<h2 className="text-sm font-medium">
{t('canvas_filter.title', 'Filter Tables')}
{t('canvas_filter.title', 'Filter Tables')}{' '}
</h2>
<span className="text-xs text-muted-foreground">
({openFilterShortcut})
</span>
</div>
<Button
variant="ghost"
@@ -405,13 +268,34 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
</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 */}
<ScrollArea className="flex-1 rounded-b-lg" type="auto">
<TreeView
data={filteredTreeData}
onNodeClick={handleNodeClick}
renderActionsComponent={renderActions}
defaultFolderIcon={Database}
defaultFolderIcon={groupingMode === 'area' ? Box : Database}
defaultIcon={Table}
expanded={expanded}
setExpanded={setExpanded}

View File

@@ -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;
};

View 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;
};

Some files were not shown because too many files have changed in this diff Show More