Compare commits

...

68 Commits

Author SHA1 Message Date
johnnyfish
80b23c3f4b feat: add right-click context menu with edit/delete options for areas on canvas 2025-09-15 18:58:49 +03:00
Guy Ben-Aharon
8954d893bb feat: add quick table mode on canvas (#915)
* fix: add quick table mode on canvas

* fix

* fix

* fix
2025-09-14 20:42:01 +03:00
Guy Ben-Aharon
1a6688e85e alignment (#912) 2025-09-11 12:32:58 +03:00
Guy Ben-Aharon
5e81c1848a fix(dbml): export array fields without quotes (#911) 2025-09-10 22:24:05 +03:00
Guy Ben-Aharon
2bd9ca25b2 fix: update deps vulns (#909) 2025-09-10 16:37:33 +03:00
Guy Ben-Aharon
b016a70691 fix: move auto arrange to toolbar (#904) 2025-09-07 12:02:33 +03:00
Jonathan Fishner
a0fb1ed08b feat: add zoom navigation buttons to canvas filter for tables and areas (#903)
* feat: add zoom navigation buttons to canvas filter for tables and areas

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-09-04 16:18:50 +03:00
Guy Ben-Aharon
ffddcdcc98 fix: export sql + import metadata lib (#902) 2025-09-04 12:10:56 +03:00
Jonathan Fishner
fe9ef275b8 fix: improve SQL default value parsing for PostgreSQL, MySQL, and SQL Server with proper type handling and casting support (#900) 2025-09-04 11:18:02 +03:00
Guy Ben-Aharon
df89f0b6b9 fix: remove general db creation (#901) 2025-09-03 20:57:12 +03:00
Guy Ben-Aharon
534d2858af readonly editor (#899) 2025-09-03 15:59:21 +03:00
Jonathan Fishner
2a64deebb8 fix(sql-import): handle SQL Server DDL with multiple tables, inline foreign keys, and case-insensitive field matching (#897) 2025-09-02 15:15:15 +03:00
Guy Ben-Aharon
e5e1d59327 fix: reset increment and default when change field (#896) 2025-09-01 18:48:00 +03:00
Guy Ben-Aharon
aa290615ca fix(sql-import): support ALTER TABLE ALTER COLUMN TYPE in PostgreSQL importer (#895) 2025-09-01 17:13:42 +03:00
Guy Ben-Aharon
ec6e46fe81 fix: add support for ALTER TABLE ADD COLUMN in PostgreSQL importer (#892) 2025-09-01 11:45:14 +03:00
Guy Ben-Aharon
ac128d67de align filter (#890) 2025-08-31 19:18:43 +03:00
Guy Ben-Aharon
07937a2f51 fix: export dbml issues after upgrade version (#883)
* fix: dbml export

* fix

* fix

* fix

* fix

* fix
2025-08-27 20:44:18 +03:00
Guy Ben-Aharon
d8e0bc7db8 fix: upgrade dbml lib (#880) 2025-08-27 14:42:02 +03:00
Guy Ben-Aharon
1ce265781b chore(main): release 1.15.1 (#878) 2025-08-27 12:53:00 +03:00
Aaron Dewes
60c5675cbf fix(custom-types): Make schema optional (#866)
* fix(custom-types): Make schema optional

The schema is optional in practice for custom types (as seen in the TS types above), and not always included in exports.

* add nullable

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-27 12:48:14 +03:00
Jonathan Fishner
66b086378c fix: handle quoted identifiers with special characters in SQL import/export and DBML generation (#877)
* fix: handle quoted identifiers with special characters in SQL import/export and DBML generation

* add tests and fix build

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-27 12:21:41 +03:00
Guy Ben-Aharon
abd2a6ccbe fix: add actions menu to diagram list + add duplicate diagram (#876) 2025-08-26 17:10:25 +03:00
Guy Ben-Aharon
459c5f1ce3 chore(main): release 1.15.0 (#835) 2025-08-26 15:15:41 +03:00
Guy Ben-Aharon
44be48ff3a fix: improve creating view to table dependency (#874) 2025-08-26 15:10:11 +03:00
Aaron Dewes
ad8e34483f fix(cla): Harden action (#867)
The CLA action does not need contents: write permission. Limit it to read for security.
2025-08-26 13:41:29 +03:00
Jonathan Fishner
215d57979d fix: preserve composite primary key constraint names across import/export workflows (#869)
* fix: composite primary key constraint names across import/export workflows

* fix: enhance PK index management with automatic lifecycle and improved UI

* fix build

* fix

* fix

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-26 12:48:24 +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
199 changed files with 101226 additions and 5486 deletions

View File

@@ -7,7 +7,7 @@ on:
permissions:
actions: write
contents: write # this can be 'read' if the signatures are in remote repository
contents: read
pull-requests: write
statuses: write

2
.nvmrc
View File

@@ -1 +1 @@
v22.5.1
v22.18.0

View File

@@ -1,5 +1,118 @@
# Changelog
## [1.15.1](https://github.com/chartdb/chartdb/compare/v1.15.0...v1.15.1) (2025-08-27)
### Bug Fixes
* add actions menu to diagram list + add duplicate diagram ([#876](https://github.com/chartdb/chartdb/issues/876)) ([abd2a6c](https://github.com/chartdb/chartdb/commit/abd2a6ccbe1aa63db44ec28b3eff525cc5d3f8b0))
* **custom-types:** Make schema optional ([#866](https://github.com/chartdb/chartdb/issues/866)) ([60c5675](https://github.com/chartdb/chartdb/commit/60c5675cbfe205859d2d0c9848d8345a0a854671))
* handle quoted identifiers with special characters in SQL import/export and DBML generation ([#877](https://github.com/chartdb/chartdb/issues/877)) ([66b0863](https://github.com/chartdb/chartdb/commit/66b086378cd63347acab5fc7f13db7db4feaa872))
## [1.15.0](https://github.com/chartdb/chartdb/compare/v1.14.0...v1.15.0) (2025-08-26)
### Features
* add auto increment support for fields with database-specific export ([#851](https://github.com/chartdb/chartdb/issues/851)) ([c77c983](https://github.com/chartdb/chartdb/commit/c77c983989ae38a6b1139dd9015f4f3178d4e103))
* **filter:** filter tables by areas ([#836](https://github.com/chartdb/chartdb/issues/836)) ([e9c5442](https://github.com/chartdb/chartdb/commit/e9c5442d9df2beadad78187da3363bb6406636c4))
* include foreign keys inline in SQLite CREATE TABLE statements ([#833](https://github.com/chartdb/chartdb/issues/833)) ([43fc1d7](https://github.com/chartdb/chartdb/commit/43fc1d7fc26876b22c61405f6c3df89fc66b7992))
* **postgres:** add support hash index types ([#812](https://github.com/chartdb/chartdb/issues/812)) ([0d623a8](https://github.com/chartdb/chartdb/commit/0d623a86b1cb7cbd223e10ad23d09fc0e106c006))
* support create views ([#868](https://github.com/chartdb/chartdb/issues/868)) ([0a5874a](https://github.com/chartdb/chartdb/commit/0a5874a69b6323145430c1fb4e3482ac7da4916c))
### Bug Fixes
* area filter logic ([#861](https://github.com/chartdb/chartdb/issues/861)) ([73daf0d](https://github.com/chartdb/chartdb/commit/73daf0df2142a29c2eeebe60b43198bcca869026))
* **area filter:** fix dragging tables over filtered areas ([#842](https://github.com/chartdb/chartdb/issues/842)) ([19fd94c](https://github.com/chartdb/chartdb/commit/19fd94c6bde3a9ec749cd1ccacbedb6abc96d037))
* **canvas:** delete table + area together bug ([#859](https://github.com/chartdb/chartdb/issues/859)) ([b697e26](https://github.com/chartdb/chartdb/commit/b697e26170da95dcb427ff6907b6f663c98ba59f))
* **cla:** Harden action ([#867](https://github.com/chartdb/chartdb/issues/867)) ([ad8e344](https://github.com/chartdb/chartdb/commit/ad8e34483fdf4226de76c9e7768bc2ba9bf154de))
* DBML export error with multi-line table comments for SQL Server ([#852](https://github.com/chartdb/chartdb/issues/852)) ([0545b41](https://github.com/chartdb/chartdb/commit/0545b411407b2449220d10981a04c3e368a90ca3))
* filter to default schema on load new diagram ([#849](https://github.com/chartdb/chartdb/issues/849)) ([712bdf5](https://github.com/chartdb/chartdb/commit/712bdf5b958919d940c4f2a1c3b7c7e969990f02))
* **filter:** filter toggle issues with no schemas dbs ([#856](https://github.com/chartdb/chartdb/issues/856)) ([d0dee84](https://github.com/chartdb/chartdb/commit/d0dee849702161d979b4f589a7e6579fbaade22d))
* **filters:** refactor diagram filters - remove schema filter ([#832](https://github.com/chartdb/chartdb/issues/832)) ([4f1d329](https://github.com/chartdb/chartdb/commit/4f1d3295c09782ab46d82ce21b662032aa094f22))
* for sqlite import - add more types & include type parameters ([#834](https://github.com/chartdb/chartdb/issues/834)) ([5936500](https://github.com/chartdb/chartdb/commit/5936500ca00a57b3f161616264c26152a13c36d2))
* improve creating view to table dependency ([#874](https://github.com/chartdb/chartdb/issues/874)) ([44be48f](https://github.com/chartdb/chartdb/commit/44be48ff3ad1361279331c17364090b13af471a1))
* initially show filter when filter active ([#853](https://github.com/chartdb/chartdb/issues/853)) ([ab4845c](https://github.com/chartdb/chartdb/commit/ab4845c7728e6e0b2d852f8005921fd90630eef9))
* **menu:** clear file menu ([#843](https://github.com/chartdb/chartdb/issues/843)) ([eaebe34](https://github.com/chartdb/chartdb/commit/eaebe3476824af779214a354b3e991923a22f195))
* merge relationship & dependency sections to ref section ([#870](https://github.com/chartdb/chartdb/issues/870)) ([ec3719e](https://github.com/chartdb/chartdb/commit/ec3719ebce4664b2aa6e3322fb3337e72bc21015))
* move dbml into sections menu ([#862](https://github.com/chartdb/chartdb/issues/862)) ([2531a70](https://github.com/chartdb/chartdb/commit/2531a7023f36ef29e67c0da6bca4fd0346b18a51))
* open filter by default ([#863](https://github.com/chartdb/chartdb/issues/863)) ([7e0fdd1](https://github.com/chartdb/chartdb/commit/7e0fdd1595bffe29e769d29602d04f42edfe417e))
* preserve composite primary key constraint names across import/export workflows ([#869](https://github.com/chartdb/chartdb/issues/869)) ([215d579](https://github.com/chartdb/chartdb/commit/215d57979df2e91fa61988acff590daad2f4e771))
* prevent false change detection in DBML editor by stripping public schema on import ([#858](https://github.com/chartdb/chartdb/issues/858)) ([0aaa451](https://github.com/chartdb/chartdb/commit/0aaa451479911d047e4cc83f063afa68a122ba9b))
* remove unnecessary space ([#845](https://github.com/chartdb/chartdb/issues/845)) ([f1a4298](https://github.com/chartdb/chartdb/commit/f1a429836221aacdda73b91665bf33ffb011164c))
* reorder with areas ([#846](https://github.com/chartdb/chartdb/issues/846)) ([d7c9536](https://github.com/chartdb/chartdb/commit/d7c9536272cf1d42104b7064ea448d128d091a20))
* **select-box:** fix select box issue in dialog ([#840](https://github.com/chartdb/chartdb/issues/840)) ([cb2ba66](https://github.com/chartdb/chartdb/commit/cb2ba66233c8c04e2d963cf2d210499d8512a268))
* set default filter only if has more than 1 schemas ([#855](https://github.com/chartdb/chartdb/issues/855)) ([b4ccfcd](https://github.com/chartdb/chartdb/commit/b4ccfcdcde2f3565b0d3bbc46fa1715feb6cd925))
* show default schema first ([#854](https://github.com/chartdb/chartdb/issues/854)) ([1759b0b](https://github.com/chartdb/chartdb/commit/1759b0b9f271ed25f7c71f26c344e3f1d97bc5fb))
* **sidebar:** add titles to sidebar ([#844](https://github.com/chartdb/chartdb/issues/844)) ([b8f2141](https://github.com/chartdb/chartdb/commit/b8f2141bd2e67272030896fb4009a7925f9f09e4))
* **sql-import:** fix SQL Server foreign key parsing for tables without schema prefix ([#857](https://github.com/chartdb/chartdb/issues/857)) ([04d91c6](https://github.com/chartdb/chartdb/commit/04d91c67b1075e94948f75186878e633df7abbca))
* **table colors:** switch to default table color ([#841](https://github.com/chartdb/chartdb/issues/841)) ([0da3cae](https://github.com/chartdb/chartdb/commit/0da3caeeac37926dd22f38d98423611f39c0412a))
* update filter on adding table ([#838](https://github.com/chartdb/chartdb/issues/838)) ([41ba251](https://github.com/chartdb/chartdb/commit/41ba25137789dda25266178cd7c96ecbb37e62a4))
## [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);
}

887
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.13.2",
"version": "1.15.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -17,7 +17,7 @@
},
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",
"@dbml/core": "^3.13.9",
"@dnd-kit/sortable": "^8.0.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-accordion": "^1.2.0",
@@ -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,137 @@
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';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
export interface ButtonAlternative {
label: string;
onClick: () => void;
disabled?: boolean;
icon?: React.ReactNode;
className?: string;
tooltip?: string;
}
export interface ButtonWithAlternativesProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
alternatives: Array<ButtonAlternative>;
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) => {
const menuItem = (
<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>
);
if (alternative.tooltip) {
return (
<Tooltip key={index}>
<TooltipTrigger asChild>
{menuItem}
</TooltipTrigger>
<TooltipContent side="left">
{alternative.tooltip}
</TooltipContent>
</Tooltip>
);
}
return menuItem;
})}
</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 {
@@ -172,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

@@ -5,27 +5,45 @@ import {
PopoverTrigger,
} from '@/components/popover/popover';
import { colorOptions } from '@/lib/colors';
import { cn } from '@/lib/utils';
export interface ColorPickerProps {
color: string;
onChange: (color: string) => void;
disabled?: boolean;
popoverOnMouseDown?: (e: React.MouseEvent) => void;
popoverOnClick?: (e: React.MouseEvent) => void;
}
export const ColorPicker = React.forwardRef<
React.ElementRef<typeof PopoverTrigger>,
ColorPickerProps
>(({ color, onChange }, ref) => {
>(({ color, onChange, disabled, popoverOnMouseDown, popoverOnClick }, ref) => {
return (
<Popover>
<PopoverTrigger asChild ref={ref}>
<PopoverTrigger
asChild
ref={ref}
disabled={disabled}
{...(disabled ? { onClick: (e) => e.preventDefault() } : {})}
>
<div
className="h-6 w-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md"
className={cn(
'h-6 w-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md',
{
'hover:shadow-none cursor-default': disabled,
}
)}
style={{
backgroundColor: color,
}}
/>
</PopoverTrigger>
<PopoverContent className="w-fit">
<PopoverContent
className="w-fit"
onMouseDown={popoverOnMouseDown}
onClick={popoverOnClick}
>
<div className="grid grid-cols-4 gap-2">
{colorOptions.map((option) => (
<div

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

@@ -27,6 +27,7 @@ export interface SelectBoxOption {
regex?: string;
extractRegex?: RegExp;
group?: string;
icon?: React.ReactNode;
}
export interface SelectBoxProps {
@@ -53,6 +54,10 @@ export interface SelectBoxProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
popoverClassName?: string;
readonly?: boolean;
footerButtons?: React.ReactNode;
commandOnMouseDown?: (e: React.MouseEvent) => void;
commandOnClick?: (e: React.MouseEvent) => void;
}
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
@@ -78,6 +83,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
open,
onOpenChange: setOpen,
popoverClassName,
readonly,
footerButtons,
commandOnMouseDown,
commandOnClick,
},
ref
) => {
@@ -94,6 +103,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
setOpen?.(isOpen);
setIsOpen(isOpen);
if (isOpen) {
setSearchTerm('');
}
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
},
[setOpen]
@@ -148,18 +161,20 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
className={`inline-flex min-w-0 shrink-0 items-center gap-1 rounded-md border py-0.5 pl-2 pr-1 text-xs font-medium text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${oneLine ? 'mx-0.5' : ''}`}
>
<span>{option.label}</span>
<span
onClick={(e) => {
e.preventDefault();
handleSelect(option.value);
}}
className="flex items-center rounded-sm px-px text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground"
>
<Cross2Icon />
</span>
{!readonly ? (
<span
onClick={(e) => {
e.preventDefault();
handleSelect(option.value);
}}
className="flex items-center rounded-sm px-px text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground"
>
<Cross2Icon />
</span>
) : null}
</span>
)),
[options, value, handleSelect, oneLine, keepOrder]
[options, value, handleSelect, oneLine, keepOrder, readonly]
);
const isAllSelected = React.useMemo(
@@ -232,6 +247,8 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
matches?.map((match) => match?.toString())
)
}
onMouseDown={commandOnMouseDown}
onClick={commandOnClick}
>
{multiple && (
<div
@@ -246,6 +263,11 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
</div>
)}
<div className="flex flex-1 items-center truncate">
{option.icon ? (
<span className="mr-2 shrink-0">
{option.icon}
</span>
) : null}
<span>
{isRegexMatch ? searchTerm : option.label}
{!isRegexMatch && optionSuffix
@@ -272,7 +294,15 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
</CommandItem>
);
},
[value, multiple, searchTerm, handleSelect, optionSuffix]
[
value,
multiple,
searchTerm,
handleSelect,
optionSuffix,
commandOnClick,
commandOnMouseDown,
]
);
return (
@@ -280,7 +310,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
<div
className={cn(
`flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''}`,
`flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''} ${readonly ? 'pointer-events-none' : ''}`,
className
)}
>
@@ -350,6 +380,8 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
popoverClassName
)}
align="center"
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<Command
filter={(value, search, keywords) => {
@@ -439,6 +471,9 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
</div>
</ScrollArea>
</Command>
{footerButtons ? (
<div className="border-t">{footerButtons}</div>
) : null}
</PopoverContent>
</Popover>
);

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

@@ -14,6 +14,16 @@ export interface CanvasContext {
overlapGraph: Graph<string>;
setShowFilter: React.Dispatch<React.SetStateAction<boolean>>;
showFilter: boolean;
editTableModeTable: {
tableId: string;
fieldId?: string;
} | null;
setEditTableModeTable: React.Dispatch<
React.SetStateAction<{
tableId: string;
fieldId?: string;
} | null>
>;
}
export const canvasContext = createContext<CanvasContext>({
@@ -23,4 +33,6 @@ export const canvasContext = createContext<CanvasContext>({
overlapGraph: createGraph(),
setShowFilter: emptyFn,
showFilter: false,
editTableModeTable: null,
setEditTableModeTable: emptyFn,
});

View File

@@ -1,27 +1,59 @@
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 [editTableModeTable, setEditTableModeTable] = useState<{
tableId: string;
fieldId?: string;
} | null>(null);
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 +64,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 +111,15 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
});
}, 500);
},
[filteredSchemas, relationships, tables, updateTablesState, fitView]
[
filter,
relationships,
tables,
updateTablesState,
fitView,
databaseType,
areas,
]
);
return (
@@ -81,6 +131,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
overlapGraph,
setShowFilter,
showFilter,
editTableModeTable,
setEditTableModeTable,
}}
>
{children}

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,12 +1,15 @@
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';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
import {
getTableIndexesWithPrimaryKey,
type DBIndex,
} from '@/lib/domain/db-index';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import { useStorage } from '@/hooks/use-storage';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
@@ -17,7 +20,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 +31,6 @@ import {
DBCustomTypeKind,
type DBCustomType,
} from '@/lib/domain/db-custom-type';
import { useConfig } from '@/hooks/use-config';
export interface ChartDBProviderProps {
diagram?: Diagram;
@@ -40,16 +41,11 @@ export const ChartDBProvider: React.FC<
React.PropsWithChildren<ChartDBProviderProps>
> = ({ children, diagram, readonly: readonlyProp }) => {
const { hasDiff } = useDiff();
let db = useStorage();
const storageDB = useStorage();
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 +67,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,25 +91,16 @@ 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,
[readonlyProp, hasDiff]
);
if (readonly) {
db = storageInitialValue;
}
const schemas = useMemo(
() =>
databasesWithSchemas.includes(databaseType)
@@ -124,9 +111,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 +129,11 @@ export const ChartDBProvider: React.FC<
[tables, defaultSchemaName, databaseType]
);
const filterSchemas: ChartDBContext['filterSchemas'] = useCallback(
(schemaIds) => {
setSchemasFilter((prev) => ({
...prev,
[diagramId]: schemaIds,
}));
},
[diagramId, setSchemasFilter]
const db = useMemo(
() => (readonly ? storageInitialValue : storageDB),
[storageDB, readonly]
);
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,12 +345,17 @@ export const ChartDBProvider: React.FC<
},
],
indexes: [],
color: randomColor(),
color: attributes?.isView ? viewColor : defaultTableColor,
createdAt: Date.now(),
isView: false,
order: tables.length,
...attributes,
};
table.indexes = getTableIndexesWithPrimaryKey({
table,
});
await addTable(table);
return table;
@@ -676,17 +647,30 @@ export const ChartDBProvider: React.FC<
options = { updateHistory: true }
) => {
const prevField = getField(tableId, fieldId);
const updateTableFn = (table: DBTable) => {
const updatedTable: DBTable = {
...table,
fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f
),
} satisfies DBTable;
updatedTable.indexes = getTableIndexesWithPrimaryKey({
table: updatedTable,
});
return updatedTable;
};
setTables((tables) =>
tables.map((table) =>
table.id === tableId
? {
...table,
fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f
),
}
: table
)
tables.map((table) => {
if (table.id === tableId) {
return updateTableFn(table);
}
return table;
})
);
const table = await db.getTable({ diagramId, id: tableId });
@@ -701,10 +685,7 @@ export const ChartDBProvider: React.FC<
db.updateTable({
id: tableId,
attributes: {
...table,
fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f
),
...updateTableFn(table),
},
}),
]);
@@ -731,19 +712,29 @@ export const ChartDBProvider: React.FC<
fieldId: string,
options = { updateHistory: true }
) => {
const updateTableFn = (table: DBTable) => {
const updatedTable: DBTable = {
...table,
fields: table.fields.filter((f) => f.id !== fieldId),
} satisfies DBTable;
updatedTable.indexes = getTableIndexesWithPrimaryKey({
table: updatedTable,
});
return updatedTable;
};
const fields = getTable(tableId)?.fields ?? [];
const prevField = getField(tableId, fieldId);
setTables((tables) =>
tables.map((table) =>
table.id === tableId
? {
...table,
fields: table.fields.filter(
(f) => f.id !== fieldId
),
}
: table
)
tables.map((table) => {
if (table.id === tableId) {
return updateTableFn(table);
}
return table;
})
);
events.emit({
@@ -767,8 +758,7 @@ export const ChartDBProvider: React.FC<
db.updateTable({
id: tableId,
attributes: {
...table,
fields: table.fields.filter((f) => f.id !== fieldId),
...updateTableFn(table),
},
}),
]);
@@ -1124,12 +1114,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 +1444,7 @@ export const ChartDBProvider: React.FC<
y: 0,
width: 300,
height: 200,
color: randomColor(),
color: defaultAreaColor,
...attributes,
};
@@ -1585,9 +1578,19 @@ export const ChartDBProvider: React.FC<
]
);
const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
async (diagram, options) => {
const st = options?.forceUpdateStorage ? storageDB : db;
await st.deleteDiagram(diagram.id);
await st.addDiagram({ diagram });
loadDiagramFromData(diagram);
},
[db, storageDB, loadDiagramFromData]
);
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
async (diagramId: string) => {
const diagram = await db.getDiagram(diagramId, {
const diagram = await storageDB.getDiagram(diagramId, {
includeRelationships: true,
includeTables: true,
includeDependencies: true,
@@ -1601,7 +1604,7 @@ export const ChartDBProvider: React.FC<
return diagram;
},
[db, loadDiagramFromData]
[storageDB, loadDiagramFromData]
);
// Custom type operations
@@ -1748,29 +1751,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 +1763,9 @@ export const ChartDBProvider: React.FC<
areas,
currentDiagram,
schemas,
filteredSchemas,
events,
readonly,
filterSchemas,
updateDiagramData,
updateDiagramId,
updateDiagramName,
loadDiagram,
@@ -1843,9 +1822,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({
@@ -756,6 +810,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
deleteCustomType,
listCustomTypes,
deleteDiagramCustomTypes,
getDiagramFilter,
updateDiagramFilter,
deleteDiagramFilter,
}}
>
{children}

View File

@@ -3,7 +3,7 @@ import { Dialog, DialogContent } from '@/components/dialog/dialog';
import { DatabaseType } from '@/lib/domain/database-type';
import { useStorage } from '@/hooks/use-storage';
import type { Diagram } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/data/import-metadata/import';
import { useNavigate } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';

View File

@@ -69,6 +69,7 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
type="button"
variant="outline"
onClick={createNewDiagram}
disabled={databaseType === DatabaseType.GENERIC}
>
{t('new_diagram_dialog.empty_diagram')}
</Button>

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

@@ -17,15 +17,21 @@ import { useDialog } from '@/hooks/use-dialog';
import {
exportBaseSQL,
exportSQL,
} from '@/lib/data/export-metadata/export-sql-script';
} from '@/lib/data/sql-export/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

@@ -7,7 +7,7 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
import { loadDatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
import type { Diagram } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/data/import-metadata/import';
import { useChartDB } from '@/hooks/use-chartdb';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import { Trans, useTranslation } from 'react-i18next';

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(
@@ -197,7 +132,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
const preprocessedContent = preprocessDBML(content);
const sanitizedContent = sanitizeDBML(preprocessedContent);
const parser = new Parser();
parser.parse(sanitizedContent, 'dbml');
parser.parse(sanitizedContent, 'dbmlv2');
} catch (e) {
const parsedError = parseDBMLError(e);
if (parsedError) {
@@ -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,98 @@
import React, { useCallback } from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
import { Button } from '@/components/button/button';
import { Ellipsis, Layers2, SquareArrowOutUpRight, Trash2 } from 'lucide-react';
import { useChartDB } from '@/hooks/use-chartdb';
import type { Diagram } from '@/lib/domain';
import { useStorage } from '@/hooks/use-storage';
import { cloneDiagram } from '@/lib/clone';
import { useTranslation } from 'react-i18next';
interface DiagramRowActionsMenuProps {
diagram: Diagram;
onOpen: () => void;
refetch: () => void;
numberOfDiagrams: number;
}
export const DiagramRowActionsMenu: React.FC<DiagramRowActionsMenuProps> = ({
diagram,
onOpen,
refetch,
numberOfDiagrams,
}) => {
const { diagramId } = useChartDB();
const { deleteDiagram, addDiagram } = useStorage();
const { t } = useTranslation();
const onDelete = useCallback(async () => {
deleteDiagram(diagram.id);
refetch();
if (diagram.id === diagramId || numberOfDiagrams <= 1) {
window.location.href = '/';
}
}, [deleteDiagram, diagram.id, diagramId, refetch, numberOfDiagrams]);
const onDuplicate = useCallback(async () => {
const duplicatedDiagram = cloneDiagram(diagram);
const diagramToAdd = duplicatedDiagram.diagram;
if (!diagramToAdd) {
return;
}
diagramToAdd.name = `${diagram.name} (Copy)`;
addDiagram({ diagram: diagramToAdd });
refetch();
}, [addDiagram, refetch, diagram]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8 p-0"
onClick={(e) => e.stopPropagation()}
>
<Ellipsis className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={onOpen}
className="flex justify-between gap-4"
>
{t('open_diagram_dialog.diagram_actions.open')}
<SquareArrowOutUpRight className="size-3.5" />
</DropdownMenuItem>
<DropdownMenuItem
onClick={onDuplicate}
className="flex justify-between gap-4"
>
{t('open_diagram_dialog.diagram_actions.duplicate')}
<Layers2 className="size-3.5" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onDelete}
className="flex justify-between gap-4 text-red-700"
>
{t('open_diagram_dialog.diagram_actions.delete')}
<Trash2 className="size-3.5 text-red-700" />
</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;
@@ -46,21 +47,22 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
string | undefined
>();
useEffect(() => {
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()
)
);
};
if (!dialog.open) {
return;
}
setSelectedDiagramId(undefined);
fetchDiagrams();
}, [listDiagrams, setDiagrams, dialog.open]);
}, [dialog.open, fetchDiagrams]);
const openDiagram = useCallback(
(diagramId: string) => {
@@ -166,6 +168,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
'open_diagram_dialog.table_columns.tables_count'
)}
</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
@@ -221,6 +224,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();
}}
numberOfDiagrams={
diagrams.length
}
refetch={fetchDiagrams}
/>
</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

@@ -0,0 +1,94 @@
import type { RefObject } from 'react';
import { useEffect } from 'react';
/**
* Hook that handles clicks outside of the passed ref elements
* @param refs - Array of refs to consider as "inside" (e.g., inputRef, buttonRef)
* @param handler - Function to call when clicking outside
* @param enabled - Whether the hook is active (default: true)
* @param delay - Delay before adding listeners to avoid immediate trigger (default: 100ms)
*/
export function useClickOutside(
refs: RefObject<HTMLElement>[],
handler: (() => void) | null,
enabled = true,
delay = 100
) {
useEffect(() => {
if (!enabled || !handler) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is on any of the refs
const isInsideAnyRef = refs.some((ref) => {
if (!ref.current) return false;
return ref.current.contains(target) || ref.current === target;
});
if (!isInsideAnyRef) {
handler();
}
};
// Small delay to avoid immediate trigger when entering edit mode
const timer = setTimeout(() => {
// Use capture phase to catch events before they might be stopped
document.addEventListener('mousedown', handleClickOutside, true);
document.addEventListener('click', handleClickOutside, true);
}, delay);
return () => {
clearTimeout(timer);
document.removeEventListener('mousedown', handleClickOutside, true);
document.removeEventListener('click', handleClickOutside, true);
};
}, [refs, handler, enabled, delay]);
}
/**
* Simplified version for edit mode scenarios
* Automatically saves and exits edit mode when clicking outside
*/
export function useEditClickOutside(
inputRef: RefObject<HTMLElement>,
editMode: boolean,
onSave: () => void,
delay = 100
) {
useEffect(() => {
if (!editMode) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Check if click is on the input
const isInput =
inputRef.current &&
(inputRef.current.contains(target) ||
inputRef.current === target);
// Check if click is on a button inside the same parent
const isRelatedButton =
target.closest('button') &&
inputRef.current?.parentElement?.contains(target);
if (!isInput && !isRelatedButton) {
onSave();
}
};
// Small delay to avoid immediate trigger when entering edit mode
const timer = setTimeout(() => {
// Use capture phase to catch events before they might be stopped
document.addEventListener('mousedown', handleClickOutside, true);
document.addEventListener('click', handleClickOutside, true);
}, delay);
return () => {
clearTimeout(timer);
document.removeEventListener('mousedown', handleClickOutside, true);
document.removeEventListener('click', handleClickOutside, true);
};
}, [editMode, inputRef, onSave, delay]);
}

142
src/hooks/use-focus-on.ts Normal file
View File

@@ -0,0 +1,142 @@
import { useCallback } from 'react';
import { useReactFlow } from '@xyflow/react';
import { useLayout } from '@/hooks/use-layout';
import { useBreakpoint } from '@/hooks/use-breakpoint';
interface FocusOptions {
select?: boolean;
}
export const useFocusOn = () => {
const { fitView, setNodes, setEdges } = useReactFlow();
const { hideSidePanel } = useLayout();
const { isMd: isDesktop } = useBreakpoint('md');
const focusOnArea = useCallback(
(areaId: string, options: FocusOptions = {}) => {
const { select = true } = options;
if (select) {
setNodes((nodes) =>
nodes.map((node) =>
node.id === areaId
? {
...node,
selected: true,
}
: {
...node,
selected: false,
}
)
);
}
fitView({
duration: 500,
maxZoom: 1,
minZoom: 1,
nodes: [
{
id: areaId,
},
],
});
if (!isDesktop) {
hideSidePanel();
}
},
[fitView, setNodes, hideSidePanel, isDesktop]
);
const focusOnTable = useCallback(
(tableId: string, options: FocusOptions = {}) => {
const { select = true } = options;
if (select) {
setNodes((nodes) =>
nodes.map((node) =>
node.id === tableId
? {
...node,
selected: true,
}
: {
...node,
selected: false,
}
)
);
}
fitView({
duration: 500,
maxZoom: 1,
minZoom: 1,
nodes: [
{
id: tableId,
},
],
});
if (!isDesktop) {
hideSidePanel();
}
},
[fitView, setNodes, hideSidePanel, isDesktop]
);
const focusOnRelationship = useCallback(
(
relationshipId: string,
sourceTableId: string,
targetTableId: string,
options: FocusOptions = {}
) => {
const { select = true } = options;
if (select) {
setEdges((edges) =>
edges.map((edge) =>
edge.id === relationshipId
? {
...edge,
selected: true,
}
: {
...edge,
selected: false,
}
)
);
}
fitView({
duration: 500,
maxZoom: 1,
minZoom: 1,
nodes: [
{
id: sourceTableId,
},
{
id: targetTableId,
},
],
});
if (!isDesktop) {
hideSidePanel();
}
},
[fitView, setEdges, hideSidePanel, isDesktop]
);
return {
focusOnArea,
focusOnTable,
focusOnRelationship,
};
};

View File

@@ -0,0 +1,320 @@
import { useCallback, useMemo, useState, useEffect } from 'react';
import { useChartDB } from './use-chartdb';
import { useDebounce } from './use-debounce-v2';
import type { DBField, DBTable } from '@/lib/domain';
import type {
SelectBoxOption,
SelectBoxProps,
} from '@/components/select-box/select-box';
import {
dataTypeDataToDataType,
sortedDataTypeMap,
} from '@/lib/data/data-types/data-types';
import { generateDBFieldSuffix } from '@/lib/domain/db-field';
import type { DataTypeData } from '@/lib/data/data-types/data-types';
const generateFieldRegexPatterns = (
dataType: DataTypeData
): {
regex?: string;
extractRegex?: RegExp;
} => {
if (!dataType.fieldAttributes) {
return { regex: undefined, extractRegex: undefined };
}
const typeName = dataType.name;
const fieldAttributes = dataType.fieldAttributes;
if (fieldAttributes.hasCharMaxLength) {
if (fieldAttributes.hasCharMaxLengthOption) {
return {
regex: `^${typeName}\\((\\d+|[mM][aA][xX])\\)$`,
extractRegex: /\((\d+|max)\)/i,
};
}
return {
regex: `^${typeName}\\(\\d+\\)$`,
extractRegex: /\((\d+)\)/,
};
}
if (fieldAttributes.precision && fieldAttributes.scale) {
return {
regex: `^${typeName}\\s*\\(\\s*\\d+\\s*(?:,\\s*\\d+\\s*)?\\)$`,
extractRegex: new RegExp(
`${typeName}\\s*\\(\\s*(\\d+)\\s*(?:,\\s*(\\d+)\\s*)?\\)`
),
};
}
if (fieldAttributes.precision) {
return {
regex: `^${typeName}\\s*\\(\\s*\\d+\\s*\\)$`,
extractRegex: /\((\d+)\)/,
};
}
return { regex: undefined, extractRegex: undefined };
};
export const useUpdateTableField = (
table: DBTable,
field: DBField,
customUpdateField?: (attrs: Partial<DBField>) => void
) => {
const {
databaseType,
customTypes,
updateField: chartDBUpdateField,
removeField: chartDBRemoveField,
} = useChartDB();
// Local state for responsive UI
const [localFieldName, setLocalFieldName] = useState(field.name);
const [localNullable, setLocalNullable] = useState(field.nullable);
const [localPrimaryKey, setLocalPrimaryKey] = useState(field.primaryKey);
// Update local state when field properties change externally
useEffect(() => {
setLocalFieldName(field.name);
setLocalNullable(field.nullable);
setLocalPrimaryKey(field.primaryKey);
}, [field.name, field.nullable, field.primaryKey]);
// Use custom updateField if provided, otherwise use the chartDB one
const updateField = useMemo(
() =>
customUpdateField
? (
_tableId: string,
_fieldId: string,
attrs: Partial<DBField>
) => customUpdateField(attrs)
: chartDBUpdateField,
[customUpdateField, chartDBUpdateField]
);
// Calculate primary key fields for validation
const primaryKeyFields = useMemo(() => {
return table.fields.filter((f) => f.primaryKey);
}, [table.fields]);
const primaryKeyCount = useMemo(
() => primaryKeyFields.length,
[primaryKeyFields.length]
);
// Generate data type options for select box
const dataFieldOptions = useMemo(() => {
const standardTypes: SelectBoxOption[] = sortedDataTypeMap[
databaseType
].map((type) => {
const regexPatterns = generateFieldRegexPatterns(type);
return {
label: type.name,
value: type.id,
regex: regexPatterns.regex,
extractRegex: regexPatterns.extractRegex,
group: customTypes?.length ? 'Standard Types' : undefined,
};
});
if (!customTypes?.length) {
return standardTypes;
}
// Add custom types as options
const customTypeOptions: SelectBoxOption[] = customTypes.map(
(type) => ({
label: type.name,
value: type.name,
description:
type.kind === 'enum' ? `${type.values?.join(' | ')}` : '',
group: 'Custom Types',
})
);
return [...standardTypes, ...customTypeOptions];
}, [databaseType, customTypes]);
// Handle data type change
const handleDataTypeChange = useCallback<
NonNullable<SelectBoxProps['onChange']>
>(
(value, regexMatches) => {
const dataType = sortedDataTypeMap[databaseType].find(
(v) => v.id === value
) ?? {
id: value as string,
name: value as string,
};
let characterMaximumLength: string | undefined = undefined;
let precision: number | undefined = undefined;
let scale: number | undefined = undefined;
if (regexMatches?.length) {
if (dataType?.fieldAttributes?.hasCharMaxLength) {
characterMaximumLength = regexMatches[1]?.toLowerCase();
} else if (
dataType?.fieldAttributes?.precision &&
dataType?.fieldAttributes?.scale
) {
precision = parseInt(regexMatches[1]);
scale = regexMatches[2]
? parseInt(regexMatches[2])
: undefined;
} else if (dataType?.fieldAttributes?.precision) {
precision = parseInt(regexMatches[1]);
}
} else {
if (
dataType?.fieldAttributes?.hasCharMaxLength &&
field.characterMaximumLength
) {
characterMaximumLength = field.characterMaximumLength;
}
if (dataType?.fieldAttributes?.precision && field.precision) {
precision = field.precision;
}
if (dataType?.fieldAttributes?.scale && field.scale) {
scale = field.scale;
}
}
updateField(table.id, field.id, {
characterMaximumLength,
precision,
scale,
increment: undefined,
default: undefined,
type: dataTypeDataToDataType(
dataType ?? {
id: value as string,
name: value as string,
}
),
});
},
[
updateField,
databaseType,
field.characterMaximumLength,
field.precision,
field.scale,
field.id,
table.id,
]
);
// Debounced update for field name
const debouncedNameUpdate = useDebounce(
useCallback(
(value: string) => {
if (value.trim() !== field.name) {
updateField(table.id, field.id, { name: value });
}
},
[updateField, table.id, field.id, field.name]
),
300 // 300ms debounce for text input
);
// Debounced update for nullable toggle
const debouncedNullableUpdate = useDebounce(
useCallback(
(value: boolean) => {
updateField(table.id, field.id, { nullable: value });
},
[updateField, table.id, field.id]
),
100 // 100ms debounce for toggle
);
// Debounced update for primary key toggle
const debouncedPrimaryKeyUpdate = useDebounce(
useCallback(
(value: boolean, primaryKeyCount: number) => {
if (value) {
// When setting as primary key
const updates: Partial<DBField> = {
primaryKey: true,
};
// Only auto-set unique if this will be the only primary key
if (primaryKeyCount === 0) {
updates.unique = true;
}
updateField(table.id, field.id, updates);
} else {
// When removing primary key
updateField(table.id, field.id, {
primaryKey: false,
});
}
},
[updateField, table.id, field.id]
),
100 // 100ms debounce for toggle
);
// Handle primary key toggle with optimistic update
const handlePrimaryKeyToggle = useCallback(
(value: boolean) => {
setLocalPrimaryKey(value);
debouncedPrimaryKeyUpdate(value, primaryKeyCount);
},
[primaryKeyCount, debouncedPrimaryKeyUpdate]
);
// Handle nullable toggle with optimistic update
const handleNullableToggle = useCallback(
(value: boolean) => {
setLocalNullable(value);
debouncedNullableUpdate(value);
},
[debouncedNullableUpdate]
);
// Handle name change with optimistic update
const handleNameChange = useCallback(
(value: string) => {
setLocalFieldName(value);
debouncedNameUpdate(value);
},
[debouncedNameUpdate]
);
// Utility function to generate field suffix for display
const generateFieldSuffix = useCallback(
(typeId?: string) => {
return generateDBFieldSuffix(field, {
databaseType,
forceExtended: true,
typeId,
});
},
[field, databaseType]
);
const removeField = useCallback(() => {
chartDBRemoveField(table.id, field.id);
}, [chartDBRemoveField, table.id, field.id]);
return {
dataFieldOptions,
handleDataTypeChange,
handlePrimaryKeyToggle,
handleNullableToggle,
handleNameChange,
generateFieldSuffix,
primaryKeyCount,
fieldName: localFieldName,
nullable: localNullable,
primaryKey: localPrimaryKey,
removeField,
};
};

View File

@@ -0,0 +1,42 @@
import { useCallback, useState, useEffect } from 'react';
import { useChartDB } from './use-chartdb';
import { useDebounce } from './use-debounce-v2';
import type { DBTable } from '@/lib/domain';
// Hook for updating table properties with debouncing for performance
export const useUpdateTable = (table: DBTable) => {
const { updateTable: chartDBUpdateTable } = useChartDB();
const [localTableName, setLocalTableName] = useState(table.name);
// Debounced update function
const debouncedUpdate = useDebounce(
useCallback(
(value: string) => {
if (value.trim() && value.trim() !== table.name) {
chartDBUpdateTable(table.id, { name: value.trim() });
}
},
[chartDBUpdateTable, table.id, table.name]
),
1000 // 1000ms debounce
);
// Update local state immediately for responsive UI
const handleTableNameChange = useCallback(
(value: string) => {
setLocalTableName(value);
debouncedUpdate(value);
},
[debouncedUpdate]
);
// Update local state when table name changes externally
useEffect(() => {
setLocalTableName(table.name);
}, [table.name]);
return {
tableName: localTableName,
handleTableNameChange,
};
};

View File

@@ -2,17 +2,25 @@ 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: 'فتح',
actions: {
actions: 'الإجراءات',
new: 'جديد...',
browse: 'تصفح...',
save: 'حفظ',
import: 'استيراد قاعدة بيانات',
export_sql: 'SQL تصدير',
export_as: 'تصدير كـ',
delete_diagram: 'حذف الرسم البياني',
exit: 'خروج',
delete_diagram: 'حذف',
},
edit: {
edit: 'تحرير',
@@ -29,6 +37,7 @@ export const ar: LanguageTranslation = {
hide_field_attributes: 'إخفاء خصائص الحقل',
show_field_attributes: 'إظهار خصائص الحقل',
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
show_views: 'عروض قاعدة البيانات',
theme: 'المظهر',
show_dependencies: 'إظهار الاعتمادات',
hide_dependencies: 'إخفاء الاعتمادات',
@@ -65,22 +74,13 @@ export const ar: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'إعادة ترتيب الرسم البياني',
title: 'ترتيب تلقائي للرسم البياني',
description:
'هذا الإجراء سيقوم بإعادة ترتيب الجداول في المخطط بشكل تلقائي. هل تريد المتابعة؟',
reorder: 'إعادة ترتيب',
reorder: 'ترتيب تلقائي',
cancel: 'إلغاء',
},
multiple_schemas_alert: {
title: 'مخططات متعددة',
description:
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
// TODO: Translate
show_me: 'Show me',
none: 'لا شيء',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'فشل النسخ',
@@ -115,14 +115,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 +145,7 @@ export const ar: LanguageTranslation = {
field_actions: {
title: 'خصائص الحقل',
unique: 'فريد',
auto_increment: 'زيادة تلقائية',
comments: 'تعليقات',
no_comments: 'لا يوجد تعليقات',
delete_field: 'حذف الحقل',
@@ -162,6 +160,7 @@ export const ar: LanguageTranslation = {
title: 'خصائص الفهرس',
name: 'الإسم',
unique: 'فريد',
index_type: 'نوع الفهرس',
delete_index: 'حذف الفهرس',
},
table_actions: {
@@ -178,12 +177,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 +195,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 +206,8 @@ export const ar: LanguageTranslation = {
},
},
empty_state: {
title: 'لا توجد اعتمادات',
description: 'إنشاء اعتماد للبدء',
title: 'لا توجد علاقات',
description: 'إنشاء علاقة للبدء',
},
},
@@ -254,6 +248,7 @@ export const ar: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'لم يتم تحديد قيم التعداد',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -276,7 +271,7 @@ export const ar: LanguageTranslation = {
show_all: 'عرض الكل',
undo: 'تراجع',
redo: 'إعادة',
reorder_diagram: 'إعادة ترتيب الرسم البياني',
reorder_diagram: 'ترتيب تلقائي للرسم البياني',
highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
// TODO: Translate
filter: 'Filter Tables',
@@ -319,7 +314,7 @@ export const ar: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'فتح مخطط',
title: 'فتح قاعدة بيانات',
description: 'اختر مخططًا لفتحه من القائمة ادناه',
table_columns: {
name: 'الإسم',
@@ -329,6 +324,12 @@ export const ar: LanguageTranslation = {
},
cancel: 'إلغاء',
open: 'فتح',
diagram_actions: {
open: 'فتح',
duplicate: 'تكرار',
delete: 'حذف',
},
},
export_sql_dialog: {
@@ -474,6 +475,7 @@ export const ar: LanguageTranslation = {
canvas_context_menu: {
new_table: 'جدول جديد',
new_view: 'عرض جديد',
new_relationship: 'علاقة جديدة',
// TODO: Translate
new_area: 'New Area',
@@ -495,6 +497,8 @@ export const ar: LanguageTranslation = {
language_select: {
change_language: 'اللغة',
},
on: 'تشغيل',
off: 'إيقاف',
},
};

View File

@@ -2,17 +2,25 @@ 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: 'খুলুন',
actions: {
actions: 'কার্য',
new: 'নতুন...',
browse: 'ব্রাউজ করুন...',
save: 'সংরক্ষণ করুন',
import: 'ডাটাবেস আমদানি করুন',
export_sql: 'SQL রপ্তানি করুন',
export_as: 'রূপে রপ্তানি করুন',
delete_diagram: 'ডায়াগ্রাম মুছুন',
exit: 'প্রস্থান করুন',
delete_diagram: 'মুছুন',
},
edit: {
edit: 'সম্পাদনা',
@@ -29,6 +37,7 @@ export const bn: LanguageTranslation = {
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
zoom_on_scroll: 'স্ক্রলে জুম করুন',
show_views: 'ডাটাবেস ভিউ',
theme: 'থিম',
show_dependencies: 'নির্ভরতাগুলি দেখান',
hide_dependencies: 'নির্ভরতাগুলি লুকান',
@@ -66,22 +75,13 @@ export const bn: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
title: 'স্বয়ংক্রিয় ডায়াগ্রাম সাজান',
description:
'এই কাজটি ডায়াগ্রামের সমস্ত টেবিল পুনর্বিন্যাস করবে। আপনি কি চালিয়ে যেতে চান?',
reorder: 'পুনর্বিন্যাস করুন',
reorder: 'স্বয়ংক্রিয় সাজান',
cancel: 'বাতিল করুন',
},
multiple_schemas_alert: {
title: 'বহু স্কিমা',
description:
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
// TODO: Translate
show_me: 'Show me',
none: 'কিছুই না',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'কপি ব্যর্থ হয়েছে',
@@ -116,14 +116,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 +146,7 @@ export const bn: LanguageTranslation = {
field_actions: {
title: 'ফিল্ড কর্ম',
unique: 'অদ্বিতীয়',
auto_increment: 'স্বয়ংক্রিয় বৃদ্ধি',
comments: 'মন্তব্য',
no_comments: 'কোনো মন্তব্য নেই',
delete_field: 'ফিল্ড মুছুন',
@@ -164,6 +162,7 @@ export const bn: LanguageTranslation = {
title: 'ইনডেক্স কর্ম',
name: 'নাম',
unique: 'অদ্বিতীয়',
index_type: 'ইনডেক্স ধরন',
delete_index: 'ইনডেক্স মুছুন',
},
table_actions: {
@@ -180,14 +179,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 +197,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: 'শুরু করতে একটি সম্পর্ক তৈরি করুন',
},
},
@@ -255,6 +249,7 @@ export const bn: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'কোন enum মান সংজ্ঞায়িত নেই',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -277,7 +272,7 @@ export const bn: LanguageTranslation = {
show_all: 'সব দেখান',
undo: 'পূর্বাবস্থায় ফিরুন',
redo: 'পুনরায় করুন',
reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
reorder_diagram: 'স্বয়ংক্রিয় ডায়াগ্রাম সাজান',
highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
// TODO: Translate
@@ -321,7 +316,7 @@ export const bn: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'চিত্র খুলুন',
title: 'ডেটাবেস খুলুন',
description: 'নিচের তালিকা থেকে একটি চিত্র নির্বাচন করুন।',
table_columns: {
name: 'নাম',
@@ -331,6 +326,12 @@ export const bn: LanguageTranslation = {
},
cancel: 'বাতিল করুন',
open: 'খুলুন',
diagram_actions: {
open: 'খুলুন',
duplicate: 'ডুপ্লিকেট',
delete: 'মুছুন',
},
},
export_sql_dialog: {
@@ -479,6 +480,7 @@ export const bn: LanguageTranslation = {
canvas_context_menu: {
new_table: 'নতুন টেবিল',
new_view: 'নতুন ভিউ',
new_relationship: 'নতুন সম্পর্ক',
// TODO: Translate
new_area: 'New Area',
@@ -500,6 +502,9 @@ export const bn: LanguageTranslation = {
language_select: {
change_language: 'ভাষা পরিবর্তন করুন',
},
on: 'চালু',
off: 'বন্ধ',
},
};

View File

@@ -2,17 +2,25 @@ 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',
actions: {
actions: 'Aktionen',
new: 'Neu...',
browse: 'Durchsuchen...',
save: 'Speichern',
import: 'Datenbank importieren',
export_sql: 'SQL exportieren',
export_as: 'Exportieren als',
delete_diagram: 'Diagramm löschen',
exit: 'Beenden',
delete_diagram: 'Löschen',
},
edit: {
edit: 'Bearbeiten',
@@ -29,6 +37,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',
@@ -66,22 +75,13 @@ export const de: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Diagramm neu anordnen',
title: 'Diagramm automatisch anordnen',
description:
'Diese Aktion wird alle Tabellen im Diagramm neu anordnen. Möchten Sie fortfahren?',
reorder: 'Neu anordnen',
reorder: 'Automatisch anordnen',
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 +117,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 +147,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 +163,7 @@ export const de: LanguageTranslation = {
title: 'Indexattribute',
name: 'Name',
unique: 'Eindeutig',
index_type: 'Indextyp',
delete_index: 'Index löschen',
},
table_actions: {
@@ -181,32 +180,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 +209,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',
},
},
@@ -257,6 +250,7 @@ export const de: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Keine Enum-Werte definiert',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -279,7 +273,7 @@ export const de: LanguageTranslation = {
show_all: 'Alle anzeigen',
undo: 'Rückgängig',
redo: 'Wiederholen',
reorder_diagram: 'Diagramm neu anordnen',
reorder_diagram: 'Diagramm automatisch anordnen',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
@@ -325,7 +319,7 @@ export const de: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Diagramm öffnen',
title: 'Datenbank öffnen',
description: 'Wählen Sie ein Diagramm aus der Liste unten aus.',
table_columns: {
name: 'Name',
@@ -335,6 +329,12 @@ export const de: LanguageTranslation = {
},
cancel: 'Abbrechen',
open: 'Öffnen',
diagram_actions: {
open: 'Öffnen',
duplicate: 'Duplizieren',
delete: 'Löschen',
},
},
export_sql_dialog: {
@@ -483,6 +483,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 +506,9 @@ export const de: LanguageTranslation = {
language_select: {
change_language: 'Sprache',
},
on: 'Ein',
off: 'Aus',
},
};

View File

@@ -2,17 +2,25 @@ 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',
actions: {
actions: 'Actions',
new: 'New...',
browse: 'Browse...',
save: 'Save',
import: 'Import',
export_sql: 'Export SQL',
export_as: 'Export as',
delete_diagram: 'Delete Diagram',
exit: 'Exit',
delete_diagram: 'Delete',
},
edit: {
edit: 'Edit',
@@ -29,6 +37,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',
@@ -64,21 +73,13 @@ export const en = {
},
reorder_diagram_alert: {
title: 'Reorder Diagram',
title: 'Auto Arrange Diagram',
description:
'This action will rearrange all tables in the diagram. Do you want to continue?',
reorder: 'Reorder',
reorder: 'Auto Arrange',
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 +114,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 +142,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 +156,7 @@ export const en = {
title: 'Index Attributes',
name: 'Name',
unique: 'Unique',
index_type: 'Index Type',
delete_index: 'Delete Index',
},
table_actions: {
@@ -173,12 +173,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 +191,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 +202,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',
},
},
@@ -247,6 +242,7 @@ export const en = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'No enum values defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -269,7 +265,7 @@ export const en = {
show_all: 'Show All',
undo: 'Undo',
redo: 'Redo',
reorder_diagram: 'Reorder Diagram',
reorder_diagram: 'Auto Arrange Diagram',
highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -311,7 +307,7 @@ export const en = {
},
open_diagram_dialog: {
title: 'Open Diagram',
title: 'Open Database',
description: 'Select a diagram to open from the list below.',
table_columns: {
name: 'Name',
@@ -321,6 +317,12 @@ export const en = {
},
cancel: 'Cancel',
open: 'Open',
diagram_actions: {
open: 'Open',
duplicate: 'Duplicate',
delete: 'Delete',
},
},
export_sql_dialog: {
@@ -468,6 +470,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 +491,9 @@ export const en = {
language_select: {
change_language: 'Language',
},
on: 'On',
off: 'Off',
},
};

View File

@@ -2,17 +2,25 @@ 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',
actions: {
actions: 'Acciones',
new: 'Nuevo...',
browse: 'Examinar...',
save: 'Guardar',
import: 'Importar Base de Datos',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Eliminar Diagrama',
exit: 'Salir',
delete_diagram: 'Eliminar',
},
edit: {
edit: 'Editar',
@@ -29,6 +37,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',
@@ -65,10 +74,10 @@ export const es: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Reordenar Diagrama',
title: 'Organizar Diagrama Automáticamente',
description:
'Esta acción reorganizará todas las tablas en el diagrama. ¿Deseas continuar?',
reorder: 'Reordenar',
reorder: 'Organizar Automáticamente',
cancel: 'Cancelar',
},
@@ -106,14 +115,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 +145,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 +161,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 +178,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 +196,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 +207,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',
},
},
@@ -245,6 +248,7 @@ export const es: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'No hay valores de enum definidos',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -267,7 +271,7 @@ export const es: LanguageTranslation = {
show_all: 'Mostrar Todo',
undo: 'Deshacer',
redo: 'Rehacer',
reorder_diagram: 'Reordenar Diagrama',
reorder_diagram: 'Organizar Diagrama Automáticamente',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -312,7 +316,7 @@ export const es: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Abrir Diagrama',
title: 'Abrir Base de Datos',
description:
'Selecciona un diagrama para abrir de la lista a continuación.',
table_columns: {
@@ -323,6 +327,12 @@ export const es: LanguageTranslation = {
},
cancel: 'Cancelar',
open: 'Abrir',
diagram_actions: {
open: 'Abrir',
duplicate: 'Duplicar',
delete: 'Eliminar',
},
},
export_sql_dialog: {
@@ -424,14 +434,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 +482,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 +505,9 @@ export const es: LanguageTranslation = {
language_select: {
change_language: 'Idioma',
},
on: 'Encendido',
off: 'Apagado',
},
};

View File

@@ -2,17 +2,25 @@ 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',
actions: {
actions: 'Actions',
new: 'Nouveau...',
browse: 'Parcourir...',
save: 'Enregistrer',
import: 'Importer Base de Données',
export_sql: 'Exporter SQL',
export_as: 'Exporter en tant que',
delete_diagram: 'Supprimer le Diagramme',
exit: 'Quitter',
delete_diagram: 'Supprimer',
},
edit: {
edit: 'Édition',
@@ -29,6 +37,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',
@@ -64,10 +73,10 @@ export const fr: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Réorganiser le Diagramme',
title: 'Organiser Automatiquement le Diagramme',
description:
'Cette action réorganisera toutes les tables dans le diagramme. Voulez-vous continuer ?',
reorder: 'Réorganiser',
reorder: 'Organiser Automatiquement',
cancel: 'Annuler',
},
@@ -105,14 +114,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 +143,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 +159,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 +176,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 +194,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 +205,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',
},
},
@@ -243,6 +246,7 @@ export const fr: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: "Aucune valeur d'énumération définie",
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -265,7 +269,7 @@ export const fr: LanguageTranslation = {
show_all: 'Afficher Tout',
undo: 'Annuler',
redo: 'Rétablir',
reorder_diagram: 'Réorganiser le Diagramme',
reorder_diagram: 'Organiser Automatiquement le Diagramme',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -309,7 +313,7 @@ export const fr: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Ouvrir Diagramme',
title: 'Ouvrir Base de Données',
description:
'Sélectionnez un diagramme à ouvrir dans la liste ci-dessous.',
table_columns: {
@@ -320,6 +324,12 @@ export const fr: LanguageTranslation = {
},
cancel: 'Annuler',
open: 'Ouvrir',
diagram_actions: {
open: 'Ouvrir',
duplicate: 'Dupliquer',
delete: 'Supprimer',
},
},
export_sql_dialog: {
@@ -357,15 +367,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 +478,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 +501,9 @@ export const fr: LanguageTranslation = {
language_select: {
change_language: 'Langue',
},
on: 'Activé',
off: 'Désactivé',
},
};

View File

@@ -2,17 +2,25 @@ 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: 'ખોલો',
actions: {
actions: 'ક્રિયાઓ',
new: 'નવું...',
browse: 'બ્રાઉજ કરો...',
save: 'સાચવો',
import: 'ડેટાબેસ આયાત કરો',
export_sql: 'SQL નિકાસ કરો',
export_as: 'રૂપે નિકાસ કરો',
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
exit: 'બહાર જાઓ',
delete_diagram: 'કાઢી નાખો',
},
edit: {
edit: 'ફેરફાર',
@@ -29,6 +37,7 @@ export const gu: LanguageTranslation = {
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
show_views: 'ડેટાબેઝ વ્યૂઝ',
theme: 'થિમ',
show_dependencies: 'નિર્ભરતાઓ બતાવો',
hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
@@ -66,22 +75,13 @@ export const gu: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'ડાયાગ્રામ ફરી વ્યવસ્થિત કરો',
title: 'ડાયાગ્રામ ઑટોમેટિક ગોઠવો',
description:
'આ ક્રિયા ડાયાગ્રામમાં બધી ટેબલ્સને ફરીથી વ્યવસ્થિત કરશે. શું તમે ચાલુ રાખવા માંગો છો?',
reorder: 'ફરી વ્યવસ્થિત કરો',
reorder: 'ઑટોમેટિક ગોઠવો',
cancel: 'રદ કરો',
},
multiple_schemas_alert: {
title: 'કઈંક વધારે સ્કીમા',
description:
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'કઈ નહીં',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'નકલ નિષ્ફળ',
@@ -116,14 +116,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 +147,7 @@ export const gu: LanguageTranslation = {
field_actions: {
title: 'ફીલ્ડ લક્ષણો',
unique: 'અદ્વિતીય',
auto_increment: 'ઑટો ઇન્ક્રિમેન્ટ',
comments: 'ટિપ્પણીઓ',
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
delete_field: 'ફીલ્ડ કાઢી નાખો',
@@ -165,6 +163,7 @@ export const gu: LanguageTranslation = {
title: 'ઇન્ડેક્સ લક્ષણો',
name: 'નામ',
unique: 'અદ્વિતીય',
index_type: 'ઇન્ડેક્સ પ્રકાર',
delete_index: 'ઇન્ડેક્સ કાઢી નાખો',
},
table_actions: {
@@ -181,14 +180,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 +198,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: 'શરૂ કરવા માટે એક સંબંધ બનાવો',
},
},
@@ -256,6 +250,7 @@ export const gu: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'કોઈ enum મૂલ્યો વ્યાખ્યાયિત નથી',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -278,7 +273,7 @@ export const gu: LanguageTranslation = {
show_all: 'બધું બતાવો',
undo: 'અનડુ',
redo: 'રીડુ',
reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો',
reorder_diagram: 'ડાયાગ્રામ ઑટોમેટિક ગોઠવો',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -321,7 +316,7 @@ export const gu: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'ડાયાગ્રામ ખોલો',
title: 'ડેટાબેસ ખોલો',
description: 'નીચેની યાદીમાંથી એક ડાયાગ્રામ પસંદ કરો.',
table_columns: {
name: 'નામ',
@@ -331,6 +326,12 @@ export const gu: LanguageTranslation = {
},
cancel: 'રદ કરો',
open: 'ખોલો',
diagram_actions: {
open: 'ખોલો',
duplicate: 'ડુપ્લિકેટ',
delete: 'કાઢી નાખો',
},
},
export_sql_dialog: {
@@ -480,6 +481,7 @@ export const gu: LanguageTranslation = {
canvas_context_menu: {
new_table: 'નવું ટેબલ',
new_view: 'નવું વ્યૂ',
new_relationship: 'નવો સંબંધ',
// TODO: Translate
new_area: 'New Area',
@@ -501,6 +503,9 @@ export const gu: LanguageTranslation = {
language_select: {
change_language: 'ભાષા બદલો',
},
on: 'ચાલુ',
off: 'બંધ',
},
};

View File

@@ -2,17 +2,25 @@ 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: 'खोलें',
actions: {
actions: 'कार्य',
new: 'नया...',
browse: 'ब्राउज़ करें...',
save: 'सहेजें',
import: 'डेटाबेस आयात करें',
export_sql: 'SQL निर्यात करें',
export_as: 'के रूप में निर्यात करें',
delete_diagram: 'आरेख हटाएँ',
exit: 'बाहर जाएँ',
delete_diagram: 'हटाएँ',
},
edit: {
edit: 'संपादित करें',
@@ -29,6 +37,7 @@ export const hi: LanguageTranslation = {
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
show_views: 'डेटाबेस व्यू',
theme: 'थीम',
show_dependencies: 'निर्भरता दिखाएँ',
hide_dependencies: 'निर्भरता छिपाएँ',
@@ -65,22 +74,13 @@ export const hi: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'आरेख पुनः व्यवस्थित करें',
title: 'आरेख स्वचालित व्यवस्थित करें',
description:
'यह क्रिया आरेख में सभी तालिकाओं को पुनः व्यवस्थित कर देगी। क्या आप जारी रखना चाहते हैं?',
reorder: 'पुनः व्यवस्थित करें',
reorder: 'स्वचालित व्यवस्थित करें',
cancel: 'रद्द करें',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
// TODO: Translate
show_me: 'Show me',
none: 'कोई नहीं',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'कॉपी असफल',
@@ -116,14 +116,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 +146,7 @@ export const hi: LanguageTranslation = {
field_actions: {
title: 'फ़ील्ड विशेषताएँ',
unique: 'अद्वितीय',
auto_increment: 'ऑटो इंक्रीमेंट',
comments: 'टिप्पणियाँ',
no_comments: 'कोई टिप्पणी नहीं',
delete_field: 'फ़ील्ड हटाएँ',
@@ -164,6 +162,7 @@ export const hi: LanguageTranslation = {
title: 'सूचकांक विशेषताएँ',
name: 'नाम',
unique: 'अद्वितीय',
index_type: 'इंडेक्स प्रकार',
delete_index: 'सूचकांक हटाएँ',
},
table_actions: {
@@ -180,12 +179,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 +197,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: 'शुरू करने के लिए एक संबंध बनाएँ',
},
},
@@ -256,6 +249,7 @@ export const hi: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'कोई enum मान परिभाषित नहीं',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -278,7 +272,7 @@ export const hi: LanguageTranslation = {
show_all: 'सभी दिखाएँ',
undo: 'पूर्ववत करें',
redo: 'पुनः करें',
reorder_diagram: 'आरेख पुनः व्यवस्थित करें',
reorder_diagram: 'आरेख स्वचालित व्यवस्थित करें',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -324,7 +318,7 @@ export const hi: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'आरेख खोलें',
title: 'डेटाबेस खोलें',
description: 'नीचे दी गई सूची से एक आरेख चुनें।',
table_columns: {
name: 'नाम',
@@ -334,6 +328,12 @@ export const hi: LanguageTranslation = {
},
cancel: 'रद्द करें',
open: 'खोलें',
diagram_actions: {
open: 'खोलें',
duplicate: 'डुप्लिकेट',
delete: 'हटाएं',
},
},
export_sql_dialog: {
@@ -483,6 +483,7 @@ export const hi: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नई तालिका',
new_view: 'नया व्यू',
new_relationship: 'नया संबंध',
// TODO: Translate
new_area: 'New Area',
@@ -505,6 +506,9 @@ export const hi: LanguageTranslation = {
language_select: {
change_language: 'भाषा बदलें',
},
on: 'चालू',
off: 'बंद',
},
};

View File

@@ -2,17 +2,25 @@ 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',
actions: {
actions: 'Akcije',
new: 'Novi...',
browse: 'Pregledaj...',
save: 'Spremi',
import: 'Uvezi',
export_sql: 'Izvezi SQL',
export_as: 'Izvezi kao',
delete_diagram: 'Izbriši dijagram',
exit: 'Izađi',
delete_diagram: 'Izbriši',
},
edit: {
edit: 'Uredi',
@@ -29,6 +37,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',
@@ -64,21 +73,13 @@ export const hr: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Preuredi dijagram',
title: 'Automatski preuredi dijagram',
description:
'Ova radnja će preurediti sve tablice u dijagramu. Želite li nastaviti?',
reorder: 'Preuredi',
reorder: 'Automatski preuredi',
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 +114,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 +143,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 +157,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 +174,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 +192,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 +203,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',
},
},
@@ -250,6 +245,7 @@ export const hr: LanguageTranslation = {
enum_values: 'Enum vrijednosti',
composite_fields: 'Polja',
no_fields: 'Nema definiranih polja',
no_values: 'Nema definiranih enum vrijednosti',
field_name_placeholder: 'Naziv polja',
field_type_placeholder: 'Odaberi tip',
add_field: 'Dodaj polje',
@@ -273,7 +269,7 @@ export const hr: LanguageTranslation = {
show_all: 'Prikaži sve',
undo: 'Poništi',
redo: 'Ponovi',
reorder_diagram: 'Preuredi dijagram',
reorder_diagram: 'Automatski preuredi dijagram',
highlight_overlapping_tables: 'Istakni preklapajuće tablice',
clear_custom_type_highlight: 'Ukloni isticanje za "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -315,7 +311,7 @@ export const hr: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Otvori dijagram',
title: 'Otvori bazu podataka',
description: 'Odaberite dijagram za otvaranje iz popisa ispod.',
table_columns: {
name: 'Naziv',
@@ -325,6 +321,12 @@ export const hr: LanguageTranslation = {
},
cancel: 'Odustani',
open: 'Otvori',
diagram_actions: {
open: 'Otvori',
duplicate: 'Dupliciraj',
delete: 'Obriši',
},
},
export_sql_dialog: {
@@ -473,6 +475,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 +496,9 @@ export const hr: LanguageTranslation = {
language_select: {
change_language: 'Jezik',
},
on: 'Uključeno',
off: 'Isključeno',
},
};

View File

@@ -2,17 +2,25 @@ 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',
actions: {
actions: 'Aksi',
new: 'Baru...',
browse: 'Jelajahi...',
save: 'Simpan',
import: 'Impor Database',
export_sql: 'Ekspor SQL',
export_as: 'Ekspor Sebagai',
delete_diagram: 'Hapus Diagram',
exit: 'Keluar',
delete_diagram: 'Hapus',
},
edit: {
edit: 'Ubah',
@@ -29,6 +37,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',
@@ -65,22 +74,13 @@ export const id_ID: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Atur Ulang Diagram',
title: 'Atur Otomatis Diagram',
description:
'Tindakan ini akan mengatur ulang semua tabel di diagram. Apakah Anda ingin melanjutkan?',
reorder: 'Atur Ulang',
reorder: 'Atur Otomatis',
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 +115,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 +145,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 +161,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 +178,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 +196,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 +207,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',
},
},
@@ -254,6 +248,7 @@ export const id_ID: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Tidak ada nilai enum yang ditentukan',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -276,7 +271,7 @@ export const id_ID: LanguageTranslation = {
show_all: 'Tampilkan Semua',
undo: 'Undo',
redo: 'Redo',
reorder_diagram: 'Atur Ulang Diagram',
reorder_diagram: 'Atur Otomatis Diagram',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -320,7 +315,7 @@ export const id_ID: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Buka Diagram',
title: 'Buka Database',
description: 'Pilih diagram untuk dibuka dari daftar di bawah.',
table_columns: {
name: 'Name',
@@ -330,6 +325,12 @@ export const id_ID: LanguageTranslation = {
},
cancel: 'Batal',
open: 'Buka',
diagram_actions: {
open: 'Buka',
duplicate: 'Duplikat',
delete: 'Hapus',
},
},
export_sql_dialog: {
@@ -479,6 +480,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 +502,9 @@ export const id_ID: LanguageTranslation = {
language_select: {
change_language: 'Bahasa',
},
on: 'Aktif',
off: 'Nonaktif',
},
};

View File

@@ -2,17 +2,25 @@ 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: '開く',
actions: {
actions: 'アクション',
new: '新規...',
browse: '参照...',
save: '保存',
import: 'データベースをインポート',
export_sql: 'SQLをエクスポート',
export_as: '形式を指定してエクスポート',
delete_diagram: 'ダイアグラムを削除',
exit: '終了',
delete_diagram: '削除',
},
edit: {
edit: '編集',
@@ -29,6 +37,7 @@ export const ja: LanguageTranslation = {
hide_field_attributes: 'フィールド属性を非表示',
show_field_attributes: 'フィールド属性を表示',
zoom_on_scroll: 'スクロールでズーム',
show_views: 'データベースビュー',
theme: 'テーマ',
// TODO: Translate
show_dependencies: 'Show Dependencies',
@@ -67,22 +76,13 @@ export const ja: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'ダイアグラムを並べ替え',
title: 'ダイアグラムを自動配置',
description:
'この操作によりダイアグラム内のすべてのテーブルが再配置されます。続行しますか?',
reorder: '並べ替え',
reorder: '自動配置',
cancel: 'キャンセル',
},
multiple_schemas_alert: {
title: '複数のスキーマ',
description:
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
// TODO: Translate
show_me: 'Show me',
none: 'なし',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'コピー失敗',
@@ -119,14 +119,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 +149,7 @@ export const ja: LanguageTranslation = {
field_actions: {
title: 'フィールド属性',
unique: 'ユニーク',
auto_increment: 'オートインクリメント',
comments: 'コメント',
no_comments: 'コメントがありません',
delete_field: 'フィールドを削除',
@@ -167,6 +165,7 @@ export const ja: LanguageTranslation = {
title: 'インデックス属性',
name: '名前',
unique: 'ユニーク',
index_type: 'インデックスタイプ',
delete_index: 'インデックスを削除',
},
table_actions: {
@@ -183,12 +182,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 +200,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:
'開始するためにリレーションシップを作成してください',
},
},
@@ -260,6 +253,7 @@ export const ja: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: '列挙値が定義されていません',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -282,7 +276,7 @@ export const ja: LanguageTranslation = {
show_all: 'すべて表示',
undo: '元に戻す',
redo: 'やり直し',
reorder_diagram: 'ダイアグラムを並べ替え',
reorder_diagram: 'ダイアグラムを自動配置',
// TODO: Translate
highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
@@ -326,7 +320,7 @@ export const ja: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'ダイアグラムを開く',
title: 'データベースを開く',
description: '以下のリストからダイアグラムを選択してください。',
table_columns: {
name: '名前',
@@ -336,6 +330,12 @@ export const ja: LanguageTranslation = {
},
cancel: 'キャンセル',
open: '開く',
diagram_actions: {
open: '開く',
duplicate: '複製',
delete: '削除',
},
},
export_sql_dialog: {
@@ -485,6 +485,7 @@ export const ja: LanguageTranslation = {
canvas_context_menu: {
new_table: '新しいテーブル',
new_view: '新しいビュー',
new_relationship: '新しいリレーションシップ',
// TODO: Translate
new_area: 'New Area',
@@ -507,6 +508,9 @@ export const ja: LanguageTranslation = {
language_select: {
change_language: '言語',
},
on: 'オン',
off: 'オフ',
},
};

View File

@@ -2,17 +2,25 @@ 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: '파일',
new: '새 다이어그램',
open: '열기',
actions: {
actions: '작업',
new: '새로 만들기...',
browse: '찾아보기...',
save: '저장',
import: '데이터베이스 가져오기',
export_sql: 'SQL로 저장',
export_as: '다른 형식으로 저장',
delete_diagram: '다이어그램 삭제',
exit: '종료',
delete_diagram: '삭제',
},
edit: {
edit: '편집',
@@ -29,6 +37,7 @@ export const ko_KR: LanguageTranslation = {
hide_field_attributes: '필드 속성 숨기기',
show_field_attributes: '필드 속성 보이기',
zoom_on_scroll: '스크롤 시 확대',
show_views: '데이터베이스 뷰',
theme: '테마',
show_dependencies: '종속성 보이기',
hide_dependencies: '종속성 숨기기',
@@ -65,22 +74,13 @@ export const ko_KR: LanguageTranslation = {
},
reorder_diagram_alert: {
title: '다이어그램 정렬',
title: '다이어그램 자동 정렬',
description:
'이 작업은 모든 다이어그램이 재정렬됩니다. 계속하시겠습니까?',
reorder: '정렬',
reorder: '자동 정렬',
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 +115,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 +145,7 @@ export const ko_KR: LanguageTranslation = {
field_actions: {
title: '필드 속성',
unique: '유니크 여부',
auto_increment: '자동 증가',
comments: '주석',
no_comments: '주석 없음',
delete_field: '필드 삭제',
@@ -163,6 +161,7 @@ export const ko_KR: LanguageTranslation = {
title: '인덱스 속성',
name: '인덱스 명',
unique: '유니크 여부',
index_type: '인덱스 타입',
delete_index: '인덱스 삭제',
},
table_actions: {
@@ -179,12 +178,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 +196,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 +207,8 @@ export const ko_KR: LanguageTranslation = {
},
},
empty_state: {
title: '뷰 테이블 없음',
description: '뷰 테이블을 만들어 시작하세요.',
title: '연관 관계 없음',
description: '연관 관계를 만들어 시작하세요.',
},
},
@@ -254,6 +248,7 @@ export const ko_KR: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: '정의된 열거형 값이 없습니다',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -276,7 +271,7 @@ export const ko_KR: LanguageTranslation = {
show_all: '전체 저장',
undo: '실행 취소',
redo: '다시 실행',
reorder_diagram: '다이어그램 정렬',
reorder_diagram: '다이어그램 자동 정렬',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -320,7 +315,7 @@ export const ko_KR: LanguageTranslation = {
},
open_diagram_dialog: {
title: '다이어그램 열기',
title: '데이터베이스 열기',
description: '아래의 목록에서 다이어그램을 선택하세요.',
table_columns: {
name: '이름',
@@ -330,6 +325,12 @@ export const ko_KR: LanguageTranslation = {
},
cancel: '취소',
open: '열기',
diagram_actions: {
open: '열기',
duplicate: '복제',
delete: '삭제',
},
},
export_sql_dialog: {
@@ -476,6 +477,7 @@ export const ko_KR: LanguageTranslation = {
canvas_context_menu: {
new_table: '새 테이블',
new_view: '새 뷰',
new_relationship: '새 연관관계',
// TODO: Translate
new_area: 'New Area',
@@ -497,6 +499,9 @@ export const ko_KR: LanguageTranslation = {
language_select: {
change_language: '언어',
},
on: '켜기',
off: '끄기',
},
};

View File

@@ -2,17 +2,25 @@ 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: 'उघडा',
actions: {
actions: 'क्रिया',
new: 'नवीन...',
browse: 'ब्राउज करा...',
save: 'जतन करा',
import: 'डेटाबेस इम्पोर्ट करा',
export_sql: 'SQL एक्स्पोर्ट करा',
export_as: 'म्हणून एक्स्पोर्ट करा',
delete_diagram: 'आरेख हटवा',
exit: 'बाहेर पडा',
delete_diagram: 'हटवा',
},
edit: {
edit: 'संपादन करा',
@@ -29,6 +37,7 @@ export const mr: LanguageTranslation = {
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
zoom_on_scroll: 'स्क्रोलवर झूम करा',
show_views: 'डेटाबेस व्ह्यूज',
theme: 'थीम',
show_dependencies: 'डिपेंडेन्सि दाखवा',
hide_dependencies: 'डिपेंडेन्सि लपवा',
@@ -66,22 +75,13 @@ export const mr: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'आरेख पुनःक्रमित करा',
title: 'आरेख स्वयंचलित व्यवस्थित करा',
description:
'ही क्रिया आरेखातील सर्व टेबल्सची पुनर्रचना करेल. तुम्हाला पुढे जायचे आहे का?',
reorder: 'पुनःक्रमित करा',
reorder: 'स्वयंचलित व्यवस्थित करा',
cancel: 'रद्द करा',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'काहीही नाही',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'कॉपी अयशस्वी',
@@ -118,14 +118,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 +148,7 @@ export const mr: LanguageTranslation = {
field_actions: {
title: 'फील्ड गुणधर्म',
unique: 'युनिक',
auto_increment: 'ऑटो इंक्रिमेंट',
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा',
@@ -166,6 +164,7 @@ export const mr: LanguageTranslation = {
title: 'इंडेक्स गुणधर्म',
name: 'नाव',
unique: 'युनिक',
index_type: 'इंडेक्स प्रकार',
delete_index: 'इंडेक्स हटवा',
},
table_actions: {
@@ -183,12 +182,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 +200,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 +211,8 @@ export const mr: LanguageTranslation = {
},
},
empty_state: {
title: 'कोणत्याही डिपेंडेन्सि नाहीत',
description: 'सुरू करण्यासाठी एक दृश्य तयार करा',
title: 'कोणतेही रिलेशनशिप नाहीत',
description: 'सुरू करण्यासाठी एक रिलेशनशिप तयार करा',
},
},
@@ -259,6 +252,7 @@ export const mr: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'कोणतीही enum मूल्ये परिभाषित नाहीत',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -281,7 +275,7 @@ export const mr: LanguageTranslation = {
show_all: 'सर्व दाखवा',
undo: 'पूर्ववत करा',
redo: 'पुन्हा करा',
reorder_diagram: 'आरेख पुनःक्रमित करा',
reorder_diagram: 'आरेख स्वयंचलित व्यवस्थित करा',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -327,7 +321,7 @@ export const mr: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'आरेख उघडा',
title: 'डेटाबेस उघडा',
description: 'खालील यादीतून उघडण्यासाठी एक आरेख निवडा.',
table_columns: {
name: 'नाव',
@@ -337,6 +331,12 @@ export const mr: LanguageTranslation = {
},
cancel: 'रद्द करा',
open: 'उघडा',
diagram_actions: {
open: 'उघडा',
duplicate: 'डुप्लिकेट',
delete: 'हटवा',
},
},
export_sql_dialog: {
@@ -489,6 +489,7 @@ export const mr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नवीन टेबल',
new_view: 'नवीन व्ह्यू',
new_relationship: 'नवीन रिलेशनशिप',
// TODO: Translate
new_area: 'New Area',
@@ -512,6 +513,9 @@ export const mr: LanguageTranslation = {
language_select: {
change_language: 'भाषा बदला',
},
on: 'चालू',
off: 'बंद',
},
};

View File

@@ -2,17 +2,25 @@ 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: 'खोल्नुहोस्',
actions: {
actions: 'कार्यहरू',
new: 'नयाँ...',
browse: 'ब्राउज गर्नुहोस्...',
save: 'सुरक्षित गर्नुहोस्',
import: 'डाटाबेस आयात गर्नुहोस्',
export_sql: 'SQL निर्यात गर्नुहोस्',
export_as: 'निर्यात गर्नुहोस्',
delete_diagram: 'डायाग्राम हटाउनुहोस्',
exit: 'बाहिर निस्कनुहोस्',
delete_diagram: 'हटाउनुहोस्',
},
edit: {
edit: 'सम्पादन',
@@ -29,6 +37,7 @@ export const ne: LanguageTranslation = {
hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
show_views: 'डाटाबेस भ्यूहरू',
theme: 'थिम',
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
@@ -66,22 +75,13 @@ export const ne: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'डायाग्राम पुनः क्रमबद्ध गर्नुहोस्',
title: 'डायाग्राम स्वचालित मिलाउनुहोस्',
description:
'यो कार्य पूर्ववत गर्न सकिँदैन। यो डायाग्राम स्थायी रूपमा हटाउनेछ।',
reorder: 'पुनः क्रमबद्ध गर्नुहोस्',
reorder: 'स्वचालित मिलाउनुहोस्',
cancel: 'रद्द गर्नुहोस्',
},
multiple_schemas_alert: {
title: 'विविध स्कीमहरू',
description:
'{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
// TODO: Translate
show_me: 'Show me',
none: 'कुनै पनि छैन',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'प्रतिलिपि असफल',
@@ -116,14 +116,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 +146,7 @@ export const ne: LanguageTranslation = {
field_actions: {
title: 'क्षेत्र विशेषताहरू',
unique: 'अनन्य',
auto_increment: 'स्वचालित वृद्धि',
comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्',
delete_field: 'क्षेत्र हटाउनुहोस्',
@@ -164,6 +162,7 @@ export const ne: LanguageTranslation = {
title: 'सूचक विशेषताहरू',
name: 'नाम',
unique: 'अनन्य',
index_type: 'इन्डेक्स प्रकार',
delete_index: 'सूचक हटाउनुहोस्',
},
table_actions: {
@@ -180,12 +179,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 +197,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 +208,8 @@ export const ne: LanguageTranslation = {
},
},
empty_state: {
title: 'कुनै डिपेन्डेन्सीहरू छैनन्',
description:
'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्',
title: 'कुनै सम्बन्धहरू छैनन्',
description: 'सुरु गर्नका लागि एक सम्बन्ध बनाउनुहोस्',
},
},
@@ -256,6 +249,7 @@ export const ne: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'कुनै enum मानहरू परिभाषित छैनन्',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -278,7 +272,7 @@ export const ne: LanguageTranslation = {
show_all: 'सबै देखाउनुहोस्',
undo: 'पूर्ववत',
redo: 'पुनः गर्नुहोस्',
reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्',
reorder_diagram: 'डायाग्राम स्वचालित मिलाउनुहोस्',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -323,7 +317,7 @@ export const ne: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'डायाग्राम खोल्नुहोस्',
title: 'डाटाबेस खोल्नुहोस्',
description:
'तलको सूचीबाट खोल्नका लागि एक डायाग्राम चयन गर्नुहोस्।',
table_columns: {
@@ -334,6 +328,12 @@ export const ne: LanguageTranslation = {
},
cancel: 'रद्द गर्नुहोस्',
open: 'खोल्नुहोस्',
diagram_actions: {
open: 'खोल्नुहोस्',
duplicate: 'डुप्लिकेट',
delete: 'मेटाउनुहोस्',
},
},
export_sql_dialog: {
@@ -483,6 +483,7 @@ export const ne: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नयाँ तालिका',
new_view: 'नयाँ भ्यू',
new_relationship: 'नयाँ सम्बन्ध',
// TODO: Translate
new_area: 'New Area',
@@ -504,6 +505,9 @@ export const ne: LanguageTranslation = {
language_select: {
change_language: 'भाषा परिवर्तन गर्नुहोस्',
},
on: 'सक्रिय',
off: 'निष्क्रिय',
},
};

View File

@@ -2,17 +2,25 @@ 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',
actions: {
actions: 'Ações',
new: 'Novo...',
browse: 'Navegar...',
save: 'Salvar',
import: 'Importar Banco de Dados',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Excluir Diagrama',
exit: 'Sair',
delete_diagram: 'Excluir',
},
edit: {
edit: 'Editar',
@@ -29,6 +37,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',
@@ -66,22 +75,13 @@ export const pt_BR: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Reordenar Diagrama',
title: 'Organizar Diagrama Automaticamente',
description:
'Esta ação reorganizará todas as tabelas no diagrama. Deseja continuar?',
reorder: 'Reordenar',
reorder: 'Organizar Automaticamente',
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 +116,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 +146,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 +162,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 +179,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 +197,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 +208,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',
},
},
@@ -255,6 +249,7 @@ export const pt_BR: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Nenhum valor de enum definido',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -277,7 +272,7 @@ export const pt_BR: LanguageTranslation = {
show_all: 'Mostrar Tudo',
undo: 'Desfazer',
redo: 'Refazer',
reorder_diagram: 'Reordenar Diagrama',
reorder_diagram: 'Organizar Diagrama Automaticamente',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -322,7 +317,7 @@ export const pt_BR: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Abrir Diagrama',
title: 'Abrir Banco de Dados',
description: 'Selecione um diagrama para abrir da lista abaixo.',
table_columns: {
name: 'Nome',
@@ -332,6 +327,12 @@ export const pt_BR: LanguageTranslation = {
},
cancel: 'Cancelar',
open: 'Abrir',
diagram_actions: {
open: 'Abrir',
duplicate: 'Duplicar',
delete: 'Excluir',
},
},
export_sql_dialog: {
@@ -481,6 +482,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 +505,9 @@ export const pt_BR: LanguageTranslation = {
language_select: {
change_language: 'Idioma',
},
on: 'Ligado',
off: 'Desligado',
},
};

View File

@@ -2,17 +2,25 @@ 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: 'Открыть',
actions: {
actions: 'Действия',
new: 'Новая...',
browse: 'Обзор...',
save: 'Сохранить',
import: 'Импортировать базу данных',
export_sql: 'Экспорт SQL',
export_as: 'Экспортировать как',
delete_diagram: 'Удалить диаграмму',
exit: 'Выход',
delete_diagram: 'Удалить',
},
edit: {
edit: 'Изменение',
@@ -29,6 +37,7 @@ export const ru: LanguageTranslation = {
show_field_attributes: 'Показать атрибуты поля',
hide_field_attributes: 'Скрыть атрибуты поля',
zoom_on_scroll: 'Увеличение при прокрутке',
show_views: 'Представления базы данных',
theme: 'Тема',
show_dependencies: 'Показать зависимости',
hide_dependencies: 'Скрыть зависимости',
@@ -64,22 +73,13 @@ export const ru: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Переупорядочить диаграмму',
title: 'Автоматическая расстановка диаграммы',
description:
'Это действие переставит все таблицы на диаграмме. Хотите продолжить?',
reorder: 'Изменить порядок',
reorder: 'Автоматическая расстановка',
cancel: 'Отменить',
},
multiple_schemas_alert: {
title: 'Множественные схемы',
description:
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'никто',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Ошибка копирования',
@@ -113,14 +113,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 +143,7 @@ export const ru: LanguageTranslation = {
field_actions: {
title: 'Атрибуты поля',
unique: 'Уникальный',
auto_increment: 'Автоинкремент',
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
@@ -160,6 +158,7 @@ export const ru: LanguageTranslation = {
title: 'Атрибуты индекса',
name: 'Имя',
unique: 'Уникальный',
index_type: 'Тип индекса',
delete_index: 'Удалить индекс',
},
table_actions: {
@@ -176,12 +175,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 +193,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 +204,8 @@ export const ru: LanguageTranslation = {
},
},
empty_state: {
title: 'Нет зависимостей',
description: 'Создайте представление, чтобы начать',
title: 'Нет отношений',
description: 'Создайте отношение, чтобы начать',
},
},
@@ -252,6 +246,7 @@ export const ru: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Значения перечисления не определены',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -274,7 +269,7 @@ export const ru: LanguageTranslation = {
show_all: 'Показать все',
undo: 'Отменить',
redo: 'Вернуть',
reorder_diagram: 'Переупорядочить диаграмму',
reorder_diagram: 'Автоматическая расстановка диаграммы',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -318,7 +313,7 @@ export const ru: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Открыть диаграмму',
title: 'Открыть базу данных',
description:
'Выберите диаграмму, которую нужно открыть, из списка ниже.',
table_columns: {
@@ -329,6 +324,12 @@ export const ru: LanguageTranslation = {
},
cancel: 'Отмена',
open: 'Открыть',
diagram_actions: {
open: 'Открыть',
duplicate: 'Дублировать',
delete: 'Удалить',
},
},
export_sql_dialog: {
@@ -477,6 +478,7 @@ export const ru: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Создать таблицу',
new_view: 'Новое представление',
new_relationship: 'Создать отношение',
new_area: 'Новая область',
},
@@ -498,6 +500,9 @@ export const ru: LanguageTranslation = {
language_select: {
change_language: 'Сменить язык',
},
on: 'Вкл',
off: 'Выкл',
},
};

View File

@@ -2,17 +2,25 @@ 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: 'తెరవు',
actions: {
actions: 'చర్యలు',
new: 'కొత్తది...',
browse: 'బ్రాఉజ్ చేయండి...',
save: 'సేవ్',
import: 'డేటాబేస్‌ను దిగుమతి చేసుకోండి',
export_sql: 'SQL ఎగుమతి',
export_as: 'వగా ఎగుమతి చేయండి',
delete_diagram: 'చిత్రాన్ని తొలగించండి',
exit: 'నిష్క్రమించు',
delete_diagram: 'తొలగించండి',
},
edit: {
edit: 'సవరించు',
@@ -29,6 +37,7 @@ export const te: LanguageTranslation = {
show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
zoom_on_scroll: 'స్క్రోల్‌పై జూమ్',
show_views: 'డేటాబేస్ వ్యూలు',
theme: 'థీమ్',
show_dependencies: 'ఆధారాలు చూపించండి',
hide_dependencies: 'ఆధారాలను దాచండి',
@@ -66,22 +75,13 @@ export const te: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'చిత్రాన్ని పునఃసరిచేయండి',
title: 'చిత్రాన్ని స్వయంచాలకంగా అమర్చండి',
description:
'ఈ చర్య చిత్రంలోని అన్ని పట్టికలను పునఃస్థాపిస్తుంది. మీరు కొనసాగించాలనుకుంటున్నారా?',
reorder: 'పునఃసరిచేయండి',
reorder: 'స్వయంచాలకంగా అమర్చండి',
cancel: 'రద్దు',
},
multiple_schemas_alert: {
title: 'బహుళ స్కీమాలు',
description:
'{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'ఎదరికాదు',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'కాపీ విఫలమైంది',
@@ -116,14 +116,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 +146,7 @@ export const te: LanguageTranslation = {
field_actions: {
title: 'ఫీల్డ్ గుణాలు',
unique: 'అద్వితీయ',
auto_increment: 'ఆటో ఇంక్రిమెంట్',
comments: 'వ్యాఖ్యలు',
no_comments: 'వ్యాఖ్యలు లేవు',
delete_field: 'ఫీల్డ్ తొలగించు',
@@ -164,6 +162,7 @@ export const te: LanguageTranslation = {
title: 'ఇండెక్స్ గుణాలు',
name: 'పేరు',
unique: 'అద్వితీయ',
index_type: 'ఇండెక్స్ రకం',
delete_index: 'ఇండెక్స్ తొలగించు',
},
table_actions: {
@@ -181,12 +180,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 +198,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 +209,8 @@ export const te: LanguageTranslation = {
},
},
empty_state: {
title: 'ఆధారాలు లేవు',
description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి',
title: 'సంబంధాలు లేవు',
description: 'ప్రారంభించడానికి ఒక సంబంధం సృష్టించండి',
},
},
@@ -256,6 +250,7 @@ export const te: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'ఏ enum విలువలు నిర్వచించబడలేదు',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -278,7 +273,7 @@ export const te: LanguageTranslation = {
show_all: 'అన్ని చూపించు',
undo: 'తిరిగి చేయు',
redo: 'మరలా చేయు',
reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి',
reorder_diagram: 'చిత్రాన్ని స్వయంచాలకంగా అమర్చండి',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -323,7 +318,7 @@ export const te: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'చిత్రం తెరవండి',
title: 'డేటాబేస్ తెరవండి',
description: 'కింద ఉన్న జాబితా నుండి చిత్రాన్ని ఎంచుకోండి.',
table_columns: {
name: 'పేరు',
@@ -333,6 +328,12 @@ export const te: LanguageTranslation = {
},
cancel: 'రద్దు',
open: 'తెరవు',
diagram_actions: {
open: 'తెరవు',
duplicate: 'నకలు',
delete: 'తొలగించు',
},
},
export_sql_dialog: {
@@ -485,6 +486,7 @@ export const te: LanguageTranslation = {
canvas_context_menu: {
new_table: 'కొత్త పట్టిక',
new_view: 'కొత్త వ్యూ',
new_relationship: 'కొత్త సంబంధం',
// TODO: Translate
new_area: 'New Area',
@@ -508,6 +510,9 @@ export const te: LanguageTranslation = {
language_select: {
change_language: 'భాష మార్చు',
},
on: 'ఆన్',
off: 'ఆఫ్',
},
};

View File

@@ -2,17 +2,25 @@ 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: '',
actions: {
actions: 'Eylemler',
new: 'Yeni...',
browse: 'Gözat...',
save: 'Kaydet',
import: 'Veritabanı İçe Aktar',
export_sql: 'SQL Olarak Dışa Aktar',
export_as: 'Olarak Dışa Aktar',
delete_diagram: 'Diyagramı Sil',
exit: ıkış',
delete_diagram: 'Sil',
},
edit: {
edit: 'Düzenle',
@@ -29,6 +37,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',
@@ -66,22 +75,13 @@ export const tr: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Diyagramı Yeniden Sırala',
title: 'Diyagramı Otomatik Düzenle',
description:
'Bu işlem tüm tabloları yeniden düzenleyecektir. Devam etmek istiyor musunuz?',
reorder: 'Yeniden Sırala',
reorder: 'Otomatik Düzenle',
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 +115,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 +145,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 +161,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 +179,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 +197,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 +208,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',
},
},
@@ -255,6 +249,7 @@ export const tr: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Tanımlanmış enum değeri yok',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -276,7 +271,7 @@ export const tr: LanguageTranslation = {
show_all: 'Hepsini Gör',
undo: 'Geri Al',
redo: 'Yinele',
reorder_diagram: 'Diyagramı Yeniden Sırala',
reorder_diagram: 'Diyagramı Otomatik Düzenle',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -318,7 +313,7 @@ export const tr: LanguageTranslation = {
import: 'İçe Aktar',
},
open_diagram_dialog: {
title: 'Diyagramı Aç',
title: 'Veritabanı Aç',
description: 'Aşağıdaki listeden açmak için bir diyagram seçin.',
table_columns: {
name: 'Ad',
@@ -328,6 +323,12 @@ export const tr: LanguageTranslation = {
},
cancel: 'İptal',
open: 'Aç',
diagram_actions: {
open: 'Aç',
duplicate: 'Kopyala',
delete: 'Sil',
},
},
export_sql_dialog: {
@@ -470,6 +471,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 +494,9 @@ export const tr: LanguageTranslation = {
language_select: {
change_language: 'Dil',
},
on: 'Açık',
off: 'Kapalı',
},
};

View File

@@ -2,17 +2,25 @@ 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: 'Відкрити',
actions: {
actions: 'Дії',
new: 'Нова...',
browse: 'Огляд...',
save: 'Зберегти',
import: 'Імпорт бази даних',
export_sql: 'Експорт SQL',
export_as: 'Експортувати як',
delete_diagram: 'Видалити діаграму',
exit: 'Вийти',
delete_diagram: 'Видалити',
},
edit: {
edit: 'Редагувати',
@@ -29,6 +37,7 @@ export const uk: LanguageTranslation = {
show_field_attributes: 'Показати атрибути полів',
hide_field_attributes: 'Приховати атрибути полів',
zoom_on_scroll: 'Масштабувати прокручуванням',
show_views: 'Представлення бази даних',
theme: 'Тема',
show_dependencies: 'Показати залежності',
hide_dependencies: 'Приховати залежності',
@@ -64,22 +73,13 @@ export const uk: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Перевпорядкувати діаграму',
title: 'Автоматичне розміщення діаграми',
description:
'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
reorder: 'Перевпорядкувати',
reorder: 'Автоматичне розміщення',
cancel: 'Скасувати',
},
multiple_schemas_alert: {
title: 'Кілька схем',
description:
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
// TODO: Translate
show_me: 'Show me',
none: 'немає',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Помилка копіювання',
@@ -114,14 +114,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 +144,7 @@ export const uk: LanguageTranslation = {
field_actions: {
title: 'Атрибути полів',
unique: 'Унікальне',
auto_increment: 'Автоінкремент',
comments: 'Коментарі',
no_comments: 'Немає коментарів',
delete_field: 'Видалити поле',
@@ -162,6 +160,7 @@ export const uk: LanguageTranslation = {
title: 'Атрибути індексу',
name: 'Назва індекса',
unique: 'Унікальний',
index_type: 'Тип індексу',
delete_index: 'Видалити індекс',
},
table_actions: {
@@ -178,12 +177,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 +195,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 +206,8 @@ export const uk: LanguageTranslation = {
},
},
empty_state: {
title: 'Жодних залежностей',
description: 'Створіть подання, щоб почати',
title: 'Жодних зв’язків',
description: 'Створіть зв’язок, щоб почати',
},
},
@@ -253,6 +247,7 @@ export const uk: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Значення переліку не визначені',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -275,7 +270,7 @@ export const uk: LanguageTranslation = {
show_all: 'Показати все',
undo: 'Скасувати',
redo: 'Повторити',
reorder_diagram: 'Перевпорядкувати діаграму',
reorder_diagram: 'Автоматичне розміщення діаграми',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -319,7 +314,7 @@ export const uk: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Відкрити діаграму',
title: 'Відкрити базу даних',
description:
'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
table_columns: {
@@ -330,6 +325,12 @@ export const uk: LanguageTranslation = {
},
cancel: 'Скасувати',
open: 'Відкрити',
diagram_actions: {
open: 'Відкрити',
duplicate: 'Дублювати',
delete: 'Видалити',
},
},
export_sql_dialog: {
@@ -476,6 +477,7 @@ export const uk: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Нова таблиця',
new_view: 'Нове представлення',
new_relationship: 'Новий звʼязок',
// TODO: Translate
new_area: 'New Area',
@@ -497,6 +499,9 @@ export const uk: LanguageTranslation = {
language_select: {
change_language: 'Мова',
},
on: 'Увімк',
off: 'Вимк',
},
};

View File

@@ -2,17 +2,25 @@ 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ở',
actions: {
actions: 'Hành động',
new: 'Mới...',
browse: 'Duyệt...',
save: 'Lưu',
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',
delete_diagram: 'Xóa',
},
edit: {
edit: 'Sửa',
@@ -29,6 +37,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',
@@ -65,22 +74,13 @@ export const vi: LanguageTranslation = {
},
reorder_diagram_alert: {
title: 'Sắp xếp lại sơ đồ',
title: 'Tự động sắp xếp sơ đồ',
description:
'Hành động này sẽ sắp xếp lại tất cả các bảng trong sơ đồ. Bạn có muốn tiếp tục không?',
reorder: 'Sắp xếp',
reorder: 'Tự động sắp xếp',
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 +115,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 +145,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 +161,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 +178,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 +196,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 +207,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',
},
},
@@ -254,6 +248,7 @@ export const vi: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: 'Không có giá trị enum được định nghĩa',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -276,7 +271,7 @@ export const vi: LanguageTranslation = {
show_all: 'Hiển thị tất cả',
undo: 'Hoàn tác',
redo: 'Làm lại',
reorder_diagram: 'Sắp xếp lại sơ đồ',
reorder_diagram: 'Tự động sắp xếp sơ đồ',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -320,7 +315,7 @@ export const vi: LanguageTranslation = {
},
open_diagram_dialog: {
title: 'Mở sơ đồ',
title: 'Mở cơ sở dữ liệu',
description: 'Chọn sơ đồ để mở từ danh sách bên dưới.',
table_columns: {
name: 'Tên',
@@ -330,6 +325,12 @@ export const vi: LanguageTranslation = {
},
cancel: 'Hủy',
open: 'Mở',
diagram_actions: {
open: 'Mở',
duplicate: 'Nhân bản',
delete: 'Xóa',
},
},
export_sql_dialog: {
@@ -477,6 +478,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 +500,9 @@ export const vi: LanguageTranslation = {
language_select: {
change_language: 'Ngôn ngữ',
},
on: 'Bật',
off: 'Tắt',
},
};

View File

@@ -2,17 +2,25 @@ 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: '打开',
actions: {
actions: '操作',
new: '新建...',
browse: '浏览...',
save: '保存',
import: '导入数据库',
export_sql: '导出 SQL 语句',
export_as: '导出为',
delete_diagram: '删除关系图',
exit: '退出',
delete_diagram: '删除',
},
edit: {
edit: '编辑',
@@ -29,6 +37,7 @@ export const zh_CN: LanguageTranslation = {
show_field_attributes: '展示字段属性',
hide_field_attributes: '隐藏字段属性',
zoom_on_scroll: '滚动缩放',
show_views: '数据库视图',
theme: '主题',
show_dependencies: '展示依赖',
hide_dependencies: '隐藏依赖',
@@ -63,21 +72,12 @@ export const zh_CN: LanguageTranslation = {
},
reorder_diagram_alert: {
title: '重新排列关系图',
title: '自动排列关系图',
description: '此操作将重新排列关系图中的所有表。是否要继续?',
reorder: '重新排列',
reorder: '自动排列',
cancel: '取消',
},
multiple_schemas_alert: {
title: '多个模式',
description:
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
// TODO: Translate
show_me: 'Show me',
none: '无',
},
copy_to_clipboard_toast: {
unsupported: {
title: '复制失败',
@@ -112,14 +112,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 +142,7 @@ export const zh_CN: LanguageTranslation = {
field_actions: {
title: '字段属性',
unique: '唯一',
auto_increment: '自动递增',
comments: '注释',
no_comments: '空',
delete_field: '删除字段',
@@ -160,6 +158,7 @@ export const zh_CN: LanguageTranslation = {
title: '索引属性',
name: '名称',
unique: '唯一',
index_type: '索引类型',
delete_index: '删除索引',
},
table_actions: {
@@ -176,12 +175,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 +193,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 +204,8 @@ export const zh_CN: LanguageTranslation = {
},
},
empty_state: {
title: '无依赖',
description: '创建视图以开始',
title: '无关系',
description: '创建关系以开始',
},
},
@@ -251,6 +245,7 @@ export const zh_CN: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: '没有定义枚举值',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -273,7 +268,7 @@ export const zh_CN: LanguageTranslation = {
show_all: '展示全部',
undo: '撤销',
redo: '重做',
reorder_diagram: '重新排列关系图',
reorder_diagram: '自动排列关系图',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -317,7 +312,7 @@ export const zh_CN: LanguageTranslation = {
},
open_diagram_dialog: {
title: '打开关系图',
title: '打开数据库',
description: '从下面的列表中选择一个图表打开。',
table_columns: {
name: '名称',
@@ -327,6 +322,12 @@ export const zh_CN: LanguageTranslation = {
},
cancel: '取消',
open: '打开',
diagram_actions: {
open: '打开',
duplicate: '复制',
delete: '删除',
},
},
export_sql_dialog: {
@@ -472,6 +473,7 @@ export const zh_CN: LanguageTranslation = {
canvas_context_menu: {
new_table: '新建表',
new_view: '新建视图',
new_relationship: '新建关系',
// TODO: Translate
new_area: 'New Area',
@@ -493,6 +495,9 @@ export const zh_CN: LanguageTranslation = {
language_select: {
change_language: '语言',
},
on: '开启',
off: '关闭',
},
};

View File

@@ -2,17 +2,25 @@ 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: '開啟',
actions: {
actions: '操作',
new: '新增...',
browse: '瀏覽...',
save: '儲存',
import: '匯入資料庫',
export_sql: '匯出 SQL',
export_as: '匯出為特定格式',
delete_diagram: '刪除圖表',
exit: '退出',
delete_diagram: '刪除',
},
edit: {
edit: '編輯',
@@ -29,6 +37,7 @@ export const zh_TW: LanguageTranslation = {
hide_field_attributes: '隱藏欄位屬性',
show_field_attributes: '顯示欄位屬性',
zoom_on_scroll: '滾動縮放',
show_views: '資料庫檢視',
theme: '主題',
show_dependencies: '顯示相依性',
hide_dependencies: '隱藏相依性',
@@ -63,21 +72,12 @@ export const zh_TW: LanguageTranslation = {
},
reorder_diagram_alert: {
title: '重新排列圖表',
title: '自動排列圖表',
description: '此操作將重新排列圖表中的所有表格。是否繼續?',
reorder: '重新排列',
reorder: '自動排列',
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 +112,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 +142,7 @@ export const zh_TW: LanguageTranslation = {
field_actions: {
title: '欄位屬性',
unique: '唯一',
auto_increment: '自動遞增',
comments: '註解',
no_comments: '無註解',
delete_field: '刪除欄位',
@@ -160,6 +158,7 @@ export const zh_TW: LanguageTranslation = {
title: '索引屬性',
name: '名稱',
unique: '唯一',
index_type: '索引類型',
delete_index: '刪除索引',
},
table_actions: {
@@ -176,12 +175,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 +193,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 +204,8 @@ export const zh_TW: LanguageTranslation = {
},
},
empty_state: {
title: '尚無相依性',
description: '請建立檢視以開始',
title: '尚無關聯',
description: '請建立關聯以開始',
},
},
@@ -251,6 +245,7 @@ export const zh_TW: LanguageTranslation = {
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
no_values: '沒有定義列舉值',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
@@ -273,7 +268,7 @@ export const zh_TW: LanguageTranslation = {
show_all: '顯示全部',
undo: '復原',
redo: '重做',
reorder_diagram: '重新排列圖表',
reorder_diagram: '自動排列圖表',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
@@ -316,7 +311,7 @@ export const zh_TW: LanguageTranslation = {
},
open_diagram_dialog: {
title: '開啟圖表',
title: '開啟資料庫',
description: '請從以下列表中選擇一個圖表。',
table_columns: {
name: '名稱',
@@ -326,6 +321,12 @@ export const zh_TW: LanguageTranslation = {
},
cancel: '取消',
open: '開啟',
diagram_actions: {
open: '開啟',
duplicate: '複製',
delete: '刪除',
},
},
export_sql_dialog: {
@@ -472,6 +473,7 @@ export const zh_TW: LanguageTranslation = {
canvas_context_menu: {
new_table: '新建表格',
new_view: '新檢視',
new_relationship: '新建關聯',
// TODO: Translate
new_area: 'New Area',
@@ -493,6 +495,9 @@ export const zh_TW: LanguageTranslation = {
language_select: {
change_language: '變更語言',
},
on: '開啟',
off: '關閉',
},
};

View File

@@ -18,4 +18,7 @@
.marker-definitions {
}
.nodrag {
}
}

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

@@ -0,0 +1,21 @@
import type { DBCustomType, DBCustomTypeKind } from '@/lib/domain';
import { schemaNameToDomainSchemaName } from '@/lib/domain';
import type { DBCustomTypeInfo } from '../metadata-types/custom-type-info';
import { generateId } from '@/lib/utils';
export const createCustomTypesFromMetadata = ({
customTypes,
}: {
customTypes: DBCustomTypeInfo[];
}): DBCustomType[] => {
return customTypes.map((customType) => {
return {
id: generateId(),
schema: schemaNameToDomainSchemaName(customType.schema),
name: customType.type,
kind: customType.kind as DBCustomTypeKind,
values: customType.values,
fields: customType.fields,
};
});
};

View File

@@ -0,0 +1,351 @@
import { generateId } from '@/lib/utils';
import type { AST } from 'node-sql-parser';
import type { DBDependency, DBTable } from '@/lib/domain';
import { DatabaseType, schemaNameToDomainSchemaName } from '@/lib/domain';
import type { ViewInfo } from '../metadata-types/view-info';
import { decodeViewDefinition } from './tables';
const astDatabaseTypes: Record<DatabaseType, string> = {
[DatabaseType.POSTGRESQL]: 'postgresql',
[DatabaseType.MYSQL]: 'postgresql',
[DatabaseType.MARIADB]: 'postgresql',
[DatabaseType.GENERIC]: 'postgresql',
[DatabaseType.SQLITE]: 'postgresql',
[DatabaseType.SQL_SERVER]: 'postgresql',
[DatabaseType.CLICKHOUSE]: 'postgresql',
[DatabaseType.COCKROACHDB]: 'postgresql',
[DatabaseType.ORACLE]: 'postgresql',
};
export const createDependenciesFromMetadata = async ({
views,
tables,
databaseType,
}: {
views: ViewInfo[];
tables: DBTable[];
databaseType: DatabaseType;
}): Promise<DBDependency[]> => {
if (!views || views.length === 0) {
return [];
}
const { Parser } = await import('node-sql-parser');
const parser = new Parser();
const dependencies = views
.flatMap((view) => {
const viewSchema = schemaNameToDomainSchemaName(view.schema);
const viewTable = tables.find(
(table) =>
table.name === view.view_name && viewSchema === table.schema
);
if (!viewTable) {
console.warn(
`Source table for view ${view.view_name} not found (schema: ${viewSchema})`
);
return []; // Skip this view and proceed to the next
}
if (view.view_definition) {
try {
const decodedViewDefinition = decodeViewDefinition(
databaseType,
view.view_definition
);
let modifiedViewDefinition = '';
if (
databaseType === DatabaseType.MYSQL ||
databaseType === DatabaseType.MARIADB
) {
modifiedViewDefinition = preprocessViewDefinitionMySQL(
decodedViewDefinition
);
} else if (databaseType === DatabaseType.SQL_SERVER) {
modifiedViewDefinition =
preprocessViewDefinitionSQLServer(
decodedViewDefinition
);
} else {
modifiedViewDefinition = preprocessViewDefinition(
decodedViewDefinition
);
}
// Parse using the appropriate dialect
const ast = parser.astify(modifiedViewDefinition, {
database: astDatabaseTypes[databaseType],
type: 'select', // Parsing a SELECT statement
});
let relatedTables = extractTablesFromAST(ast);
// Filter out duplicate tables without schema
relatedTables = filterDuplicateTables(relatedTables);
return relatedTables.map((relTable) => {
const relSchema = relTable.schema || view.schema; // Use view's schema if relSchema is undefined
const relTableName = relTable.tableName;
const table = tables.find(
(table) =>
table.name === relTableName &&
(table.schema || '') === relSchema
);
if (table) {
const dependency: DBDependency = {
id: generateId(),
schema: view.schema,
tableId: table.id, // related table
dependentSchema: table.schema,
dependentTableId: viewTable.id, // dependent view
createdAt: Date.now(),
};
return dependency;
} else {
console.warn(
`Dependent table ${relSchema}.${relTableName} not found for view ${view.schema}.${view.view_name}`
);
return null;
}
});
} catch (error) {
console.error(
`Error parsing view ${view.schema}.${view.view_name}:`,
error
);
return [];
}
} else {
console.warn(
`View definition missing for ${view.schema}.${view.view_name}`
);
return [];
}
})
.filter((dependency) => dependency !== null);
return dependencies;
};
// Add this new function to filter out duplicate tables
function filterDuplicateTables(
tables: { schema?: string; tableName: string }[]
): { schema?: string; tableName: string }[] {
const tableMap = new Map<string, { schema?: string; tableName: string }>();
for (const table of tables) {
const key = table.tableName;
const existingTable = tableMap.get(key);
if (!existingTable || (table.schema && !existingTable.schema)) {
tableMap.set(key, table);
}
}
return Array.from(tableMap.values());
}
// Preprocess the view_definition to remove schema from CREATE VIEW
function preprocessViewDefinition(viewDefinition: string): string {
if (!viewDefinition) {
return '';
}
// Remove leading and trailing whitespace
viewDefinition = viewDefinition.replace(/\s+/g, ' ').trim();
// Replace escaped double quotes with regular ones
viewDefinition = viewDefinition.replace(/\\"/g, '"');
// Replace 'CREATE MATERIALIZED VIEW' with 'CREATE VIEW'
viewDefinition = viewDefinition.replace(
/CREATE\s+MATERIALIZED\s+VIEW/i,
'CREATE VIEW'
);
// Regular expression to match 'CREATE VIEW [schema.]view_name [ (column definitions) ] AS'
// This regex captures the view name and skips any content between the view name and 'AS'
const regex =
/CREATE\s+VIEW\s+(?:(?:`[^`]+`|"[^"]+"|\w+)\.)?(?:`([^`]+)`|"([^"]+)"|(\w+))[\s\S]*?\bAS\b\s+/i;
const match = viewDefinition.match(regex);
let modifiedDefinition: string;
if (match) {
const viewName = match[1] || match[2] || match[3];
// Extract the SQL after the 'AS' keyword
const restOfDefinition = viewDefinition.substring(
match.index! + match[0].length
);
// Replace double-quoted identifiers with unquoted ones
let modifiedSQL = restOfDefinition.replace(/"(\w+)"/g, '$1');
// Replace '::' type casts with 'CAST' expressions
modifiedSQL = modifiedSQL.replace(
/\(([^()]+)\)::(\w+)/g,
'CAST($1 AS $2)'
);
// Remove ClickHouse-specific syntax that may still be present
// For example, remove SETTINGS clauses inside the SELECT statement
modifiedSQL = modifiedSQL.replace(/\bSETTINGS\b[\s\S]*$/i, '');
modifiedDefinition = `CREATE VIEW ${viewName} AS ${modifiedSQL}`;
} else {
console.warn('Could not preprocess view definition:', viewDefinition);
modifiedDefinition = viewDefinition;
}
return modifiedDefinition;
}
// Preprocess the view_definition for SQL Server
function preprocessViewDefinitionSQLServer(viewDefinition: string): string {
if (!viewDefinition) {
return '';
}
// Remove BOM if present
viewDefinition = viewDefinition.replace(/^\uFEFF/, '');
// Normalize whitespace
viewDefinition = viewDefinition.replace(/\s+/g, ' ').trim();
// Remove square brackets and replace with double quotes
viewDefinition = viewDefinition.replace(/\[([^\]]+)\]/g, '"$1"');
// Remove database names from fully qualified identifiers
viewDefinition = viewDefinition.replace(
/"([a-zA-Z0-9_]+)"\."([a-zA-Z0-9_]+)"\."([a-zA-Z0-9_]+)"/g,
'"$2"."$3"'
);
// Replace SQL Server functions with PostgreSQL equivalents
viewDefinition = viewDefinition.replace(/\bGETDATE\(\)/gi, 'NOW()');
viewDefinition = viewDefinition.replace(/\bISNULL\(/gi, 'COALESCE(');
// Replace 'TOP N' with 'LIMIT N' at the end of the query
const topMatch = viewDefinition.match(/SELECT\s+TOP\s+(\d+)/i);
if (topMatch) {
const topN = topMatch[1];
viewDefinition = viewDefinition.replace(
/SELECT\s+TOP\s+\d+/i,
'SELECT'
);
viewDefinition = viewDefinition.replace(/;+\s*$/, ''); // Remove semicolons at the end
viewDefinition += ` LIMIT ${topN}`;
}
viewDefinition = viewDefinition.replace(/\n/g, ''); // Remove newlines
// Adjust CREATE VIEW syntax
const regex =
/CREATE\s+VIEW\s+(?:"?([^".\s]+)"?\.)?"?([^".\s]+)"?\s+AS\s+/i;
const match = viewDefinition.match(regex);
let modifiedDefinition: string;
if (match) {
const viewName = match[2];
const modifiedSQL = viewDefinition.substring(
match.index! + match[0].length
);
// Remove semicolons at the end
const finalSQL = modifiedSQL.replace(/;+\s*$/, '');
modifiedDefinition = `CREATE VIEW "${viewName}" AS ${finalSQL}`;
} else {
console.warn('Could not preprocess view definition:', viewDefinition);
modifiedDefinition = viewDefinition;
}
return modifiedDefinition;
}
// Preprocess the view_definition to remove schema from CREATE VIEW
function preprocessViewDefinitionMySQL(viewDefinition: string): string {
if (!viewDefinition) {
return '';
}
// Remove any trailing semicolons
viewDefinition = viewDefinition.replace(/;\s*$/, '');
// Remove backticks from identifiers
viewDefinition = viewDefinition.replace(/`/g, '');
// Remove unnecessary parentheses around joins and ON clauses
viewDefinition = removeRedundantParentheses(viewDefinition);
return viewDefinition;
}
function removeRedundantParentheses(sql: string): string {
// Regular expressions to match unnecessary parentheses
const patterns = [
/\(\s*(JOIN\s+[^()]+?)\s*\)/gi,
/\(\s*(ON\s+[^()]+?)\s*\)/gi,
// Additional patterns if necessary
];
let prevSql;
do {
prevSql = sql;
patterns.forEach((pattern) => {
sql = sql.replace(pattern, '$1');
});
} while (sql !== prevSql);
return sql;
}
function extractTablesFromAST(
ast: AST | AST[]
): { schema?: string; tableName: string }[] {
const tablesMap = new Map<string, { schema: string; tableName: string }>();
const visitedNodes = new Set();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function traverse(node: any) {
if (!node || visitedNodes.has(node)) return;
visitedNodes.add(node);
if (Array.isArray(node)) {
node.forEach(traverse);
} else if (typeof node === 'object') {
// Check if node represents a table
if (
Object.hasOwnProperty.call(node, 'table') &&
typeof node.table === 'string'
) {
let schema = node.db || node.schema;
const tableName = node.table;
if (tableName) {
// Assign default schema if undefined
schema = schemaNameToDomainSchemaName(schema) || '';
const key = `${schema}.${tableName}`;
if (!tablesMap.has(key)) {
tablesMap.set(key, { schema, tableName });
}
}
}
// Recursively traverse all properties
for (const key in node) {
if (Object.hasOwnProperty.call(node, key)) {
traverse(node[key]);
}
}
}
}
traverse(ast);
return Array.from(tablesMap.values());
}

View File

@@ -0,0 +1,64 @@
import type { DBField } from '@/lib/domain';
import type { ColumnInfo } from '../metadata-types/column-info';
import type { AggregatedIndexInfo } from '../metadata-types/index-info';
import type { PrimaryKeyInfo } from '../metadata-types/primary-key-info';
import type { TableInfo } from '../metadata-types/table-info';
import { generateId } from '@/lib/utils';
export const createFieldsFromMetadata = ({
tableColumns,
tablePrimaryKeys,
aggregatedIndexes,
}: {
tableColumns: ColumnInfo[];
tableSchema?: string;
tableInfo: TableInfo;
tablePrimaryKeys: PrimaryKeyInfo[];
aggregatedIndexes: AggregatedIndexInfo[];
}) => {
const uniqueColumns = tableColumns.reduce((acc, col) => {
if (!acc.has(col.name)) {
acc.set(col.name, col);
}
return acc;
}, new Map<string, ColumnInfo>());
const sortedColumns = Array.from(uniqueColumns.values()).sort(
(a, b) => a.ordinal_position - b.ordinal_position
);
const tablePrimaryKeysColumns = tablePrimaryKeys.map((pk) =>
pk.column.trim()
);
return sortedColumns.map(
(col: ColumnInfo): DBField => ({
id: generateId(),
name: col.name,
type: {
id: col.type.split(' ').join('_').toLowerCase(),
name: col.type.toLowerCase(),
},
primaryKey: tablePrimaryKeysColumns.includes(col.name),
unique: Object.values(aggregatedIndexes).some(
(idx) =>
idx.unique &&
idx.columns.length === 1 &&
idx.columns[0].name === col.name
),
nullable: Boolean(col.nullable),
...(col.character_maximum_length &&
col.character_maximum_length !== 'null'
? { characterMaximumLength: col.character_maximum_length }
: {}),
...(col.precision?.precision
? { precision: col.precision.precision }
: {}),
...(col.precision?.scale ? { scale: col.precision.scale } : {}),
...(col.default ? { default: col.default } : {}),
...(col.collation ? { collation: col.collation } : {}),
createdAt: Date.now(),
comments: col.comment ? col.comment : undefined,
})
);
};

View File

@@ -0,0 +1,82 @@
import type { DatabaseEdition, Diagram } from '@/lib/domain';
import { adjustTablePositions, DatabaseType } from '@/lib/domain';
import { generateDiagramId } from '@/lib/utils';
import type { DatabaseMetadata } from '../metadata-types/database-metadata';
import { createCustomTypesFromMetadata } from './custom-types';
import { createRelationshipsFromMetadata } from './relationships';
import { createTablesFromMetadata } from './tables';
import { createDependenciesFromMetadata } from './dependencies';
export const loadFromDatabaseMetadata = async ({
databaseType,
databaseMetadata,
diagramNumber,
databaseEdition,
}: {
databaseType: DatabaseType;
databaseMetadata: DatabaseMetadata;
diagramNumber?: number;
databaseEdition?: DatabaseEdition;
}): Promise<Diagram> => {
const {
fk_info: foreignKeys,
views: views,
custom_types: customTypes,
} = databaseMetadata;
const tables = createTablesFromMetadata({
databaseMetadata,
databaseType,
});
const relationships = createRelationshipsFromMetadata({
foreignKeys,
tables,
});
const dependencies = await createDependenciesFromMetadata({
views,
tables,
databaseType,
});
const dbCustomTypes = customTypes
? createCustomTypesFromMetadata({
customTypes,
})
: [];
const adjustedTables = adjustTablePositions({
tables,
relationships,
mode: 'perSchema',
});
const sortedTables = adjustedTables.sort((a, b) => {
if (a.isView === b.isView) {
// Both are either tables or views, so sort alphabetically by name
return a.name.localeCompare(b.name);
}
// If one is a view and the other is not, put tables first
return a.isView ? 1 : -1;
});
const diagram: Diagram = {
id: generateDiagramId(),
name: databaseMetadata.database_name
? `${databaseMetadata.database_name}-db`
: diagramNumber
? `Diagram ${diagramNumber}`
: 'New Diagram',
databaseType: databaseType ?? DatabaseType.GENERIC,
databaseEdition,
tables: sortedTables,
relationships,
dependencies,
customTypes: dbCustomTypes,
createdAt: new Date(),
updatedAt: new Date(),
};
return diagram;
};

View File

@@ -0,0 +1,24 @@
import type { DBField, DBIndex, IndexType } from '@/lib/domain';
import type { AggregatedIndexInfo } from '../metadata-types/index-info';
import { generateId } from '@/lib/utils';
export const createIndexesFromMetadata = ({
aggregatedIndexes,
fields,
}: {
aggregatedIndexes: AggregatedIndexInfo[];
fields: DBField[];
}): DBIndex[] =>
aggregatedIndexes.map(
(idx): DBIndex => ({
id: generateId(),
name: idx.name,
unique: Boolean(idx.unique),
fieldIds: idx.columns
.sort((a, b) => a.position - b.position)
.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,
})
);

View File

@@ -0,0 +1,85 @@
import type {
Cardinality,
DBField,
DBRelationship,
DBTable,
} from '@/lib/domain';
import { schemaNameToDomainSchemaName } from '@/lib/domain';
import type { ForeignKeyInfo } from '../metadata-types/foreign-key-info';
import { generateId } from '@/lib/utils';
const determineCardinality = (
field: DBField,
isTablePKComplex: boolean
): Cardinality => {
return field.unique || (field.primaryKey && !isTablePKComplex)
? 'one'
: 'many';
};
export const createRelationshipsFromMetadata = ({
foreignKeys,
tables,
}: {
foreignKeys: ForeignKeyInfo[];
tables: DBTable[];
}): DBRelationship[] => {
return foreignKeys
.map((fk: ForeignKeyInfo): DBRelationship | null => {
const schema = schemaNameToDomainSchemaName(fk.schema);
const sourceTable = tables.find(
(table) => table.name === fk.table && table.schema === schema
);
const targetSchema = schemaNameToDomainSchemaName(
fk.reference_schema
);
const targetTable = tables.find(
(table) =>
table.name === fk.reference_table &&
table.schema === targetSchema
);
const sourceField = sourceTable?.fields.find(
(field) => field.name === fk.column
);
const targetField = targetTable?.fields.find(
(field) => field.name === fk.reference_column
);
const isSourceTablePKComplex =
(sourceTable?.fields.filter((field) => field.primaryKey) ?? [])
.length > 1;
const isTargetTablePKComplex =
(targetTable?.fields.filter((field) => field.primaryKey) ?? [])
.length > 1;
if (sourceTable && targetTable && sourceField && targetField) {
const sourceCardinality = determineCardinality(
sourceField,
isSourceTablePKComplex
);
const targetCardinality = determineCardinality(
targetField,
isTargetTablePKComplex
);
return {
id: generateId(),
name: fk.foreign_key_name,
sourceSchema: schema,
targetSchema: targetSchema,
sourceTableId: sourceTable.id,
targetTableId: targetTable.id,
sourceFieldId: sourceField.id,
targetFieldId: targetField.id,
sourceCardinality,
targetCardinality,
createdAt: Date.now(),
};
}
return null;
})
.filter((rel) => rel !== null) as DBRelationship[];
};

View File

@@ -0,0 +1,228 @@
import type { DBIndex, DBTable } from '@/lib/domain';
import {
DatabaseType,
generateTableKey,
schemaNameToDomainSchemaName,
} from '@/lib/domain';
import type { DatabaseMetadata } from '../metadata-types/database-metadata';
import type { TableInfo } from '../metadata-types/table-info';
import { createAggregatedIndexes } from '../metadata-types/index-info';
import {
decodeBase64ToUtf16LE,
decodeBase64ToUtf8,
generateId,
} from '@/lib/utils';
import {
defaultTableColor,
materializedViewColor,
viewColor,
} from '@/lib/colors';
import { createFieldsFromMetadata } from './fields';
import { createIndexesFromMetadata } from './indexes';
export const decodeViewDefinition = (
databaseType: DatabaseType,
viewDefinition?: string
): string => {
if (!viewDefinition) {
return '';
}
let decodedViewDefinition: string;
if (databaseType === DatabaseType.SQL_SERVER) {
decodedViewDefinition = decodeBase64ToUtf16LE(viewDefinition);
} else {
decodedViewDefinition = decodeBase64ToUtf8(viewDefinition);
}
return decodedViewDefinition;
};
export const createTablesFromMetadata = ({
databaseMetadata,
databaseType,
}: {
databaseMetadata: DatabaseMetadata;
databaseType: DatabaseType;
}): DBTable[] => {
const {
tables: tableInfos,
pk_info: primaryKeys,
columns,
indexes,
views: views,
} = databaseMetadata;
// Pre-compute view names for faster lookup if there are views
const viewNamesSet = new Set<string>();
const materializedViewNamesSet = new Set<string>();
if (views && views.length > 0) {
views.forEach((view) => {
const key = generateTableKey({
schemaName: view.schema,
tableName: view.view_name,
});
viewNamesSet.add(key);
if (
view.view_definition &&
decodeViewDefinition(databaseType, view.view_definition)
.toLowerCase()
.includes('materialized')
) {
materializedViewNamesSet.add(key);
}
});
}
// Pre-compute lookup maps for better performance
const columnsByTable = new Map<string, (typeof columns)[0][]>();
const indexesByTable = new Map<string, (typeof indexes)[0][]>();
const primaryKeysByTable = new Map<string, (typeof primaryKeys)[0][]>();
// Group columns by table
columns.forEach((col) => {
const key = generateTableKey({
schemaName: col.schema,
tableName: col.table,
});
if (!columnsByTable.has(key)) {
columnsByTable.set(key, []);
}
columnsByTable.get(key)!.push(col);
});
// Group indexes by table
indexes.forEach((idx) => {
const key = generateTableKey({
schemaName: idx.schema,
tableName: idx.table,
});
if (!indexesByTable.has(key)) {
indexesByTable.set(key, []);
}
indexesByTable.get(key)!.push(idx);
});
// Group primary keys by table
primaryKeys.forEach((pk) => {
const key = generateTableKey({
schemaName: pk.schema,
tableName: pk.table,
});
if (!primaryKeysByTable.has(key)) {
primaryKeysByTable.set(key, []);
}
primaryKeysByTable.get(key)!.push(pk);
});
const result = tableInfos.map((tableInfo: TableInfo) => {
const tableSchema = schemaNameToDomainSchemaName(tableInfo.schema);
const tableKey = generateTableKey({
schemaName: tableInfo.schema,
tableName: tableInfo.table,
});
// Use pre-computed lookups instead of filtering entire arrays
const tableIndexes = indexesByTable.get(tableKey) || [];
const tablePrimaryKeys = primaryKeysByTable.get(tableKey) || [];
const tableColumns = columnsByTable.get(tableKey) || [];
// Aggregate indexes with multiple columns
const aggregatedIndexes = createAggregatedIndexes({
tableInfo,
tableSchema,
tableIndexes,
});
const fields = createFieldsFromMetadata({
aggregatedIndexes,
tableColumns,
tablePrimaryKeys,
tableInfo,
tableSchema,
});
// Check for composite primary key and find matching index name
const primaryKeyFields = fields.filter((f) => f.primaryKey);
let pkMatchingIndexName: string | undefined;
let pkIndex: DBIndex | undefined;
if (primaryKeyFields.length >= 1) {
// We have a composite primary key, look for an index that matches all PK columns
const pkFieldNames = primaryKeyFields.map((f) => f.name).sort();
// Find an index that matches the primary key columns exactly
const matchingIndex = aggregatedIndexes.find((index) => {
const indexColumnNames = index.columns
.map((c) => c.name)
.sort();
return (
indexColumnNames.length === pkFieldNames.length &&
indexColumnNames.every((col, i) => col === pkFieldNames[i])
);
});
if (matchingIndex) {
pkMatchingIndexName = matchingIndex.name;
// Create a special PK index
pkIndex = {
id: generateId(),
name: matchingIndex.name,
unique: true,
fieldIds: primaryKeyFields.map((f) => f.id),
createdAt: Date.now(),
isPrimaryKey: true,
};
}
}
// Filter out the index that matches the composite PK (to avoid duplication)
const filteredAggregatedIndexes = pkMatchingIndexName
? aggregatedIndexes.filter(
(idx) => idx.name !== pkMatchingIndexName
)
: aggregatedIndexes;
const dbIndexes = createIndexesFromMetadata({
aggregatedIndexes: filteredAggregatedIndexes,
fields,
});
// Add the PK index if it exists
if (pkIndex) {
dbIndexes.push(pkIndex);
}
// Determine if the current table is a view by checking against pre-computed sets
const viewKey = generateTableKey({
schemaName: tableSchema,
tableName: tableInfo.table,
});
const isView = viewNamesSet.has(viewKey);
const isMaterializedView = materializedViewNamesSet.has(viewKey);
// Initial random positions; these will be adjusted later
return {
id: generateId(),
name: tableInfo.table,
schema: tableSchema,
x: Math.random() * 1000, // Placeholder X
y: Math.random() * 800, // Placeholder Y
fields,
indexes: dbIndexes,
color: isMaterializedView
? materializedViewColor
: isView
? viewColor
: defaultTableColor,
isView: isView,
isMaterializedView: isMaterializedView,
createdAt: Date.now(),
comments: tableInfo.comment ? tableInfo.comment : undefined,
};
});
return result;
};

View File

@@ -1,20 +1,10 @@
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import { exportBaseSQL } from '../export-sql-script';
import { DatabaseType } from '@/lib/domain/database-type';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
// Mock the dbml/core importer
vi.mock('@dbml/core', () => ({
importer: {
import: vi.fn((sql: string) => {
// Return a simplified DBML for testing
return sql;
}),
},
}));
describe('DBML Export - SQL Generation Tests', () => {
// Helper to generate test IDs and timestamps
let idCounter = 0;
@@ -116,7 +106,7 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should contain composite primary key syntax
expect(sql).toContain('PRIMARY KEY (spell_id, component_id)');
expect(sql).toContain('PRIMARY KEY ("spell_id", "component_id")');
// Should NOT contain individual PRIMARY KEY constraints
expect(sql).not.toMatch(/spell_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
expect(sql).not.toMatch(
@@ -124,6 +114,96 @@ describe('DBML Export - SQL Generation Tests', () => {
);
});
it('should not create duplicate index for composite primary key', () => {
const tableId = testId();
const field1Id = testId();
const field2Id = testId();
const field3Id = testId();
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Landlord System',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: tableId,
name: 'users_master_table',
schema: 'landlord',
fields: [
createField({
id: field1Id,
name: 'master_user_id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: field2Id,
name: 'tenant_id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: field3Id,
name: 'tenant_user_id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'enabled',
type: { id: 'boolean', name: 'boolean' },
primaryKey: false,
nullable: true,
unique: false,
}),
],
indexes: [
{
id: testId(),
name: 'idx_users_master_table_master_user_id_tenant_id_tenant_user_id',
unique: false,
fieldIds: [field1Id, field2Id, field3Id],
createdAt: testTime,
},
{
id: testId(),
name: 'index_1',
unique: true,
fieldIds: [field2Id, field3Id],
createdAt: testTime,
},
],
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should contain composite primary key constraint
expect(sql).toContain(
'PRIMARY KEY ("master_user_id", "tenant_id", "tenant_user_id")'
);
// Should NOT contain the duplicate index for the primary key fields
expect(sql).not.toContain(
'CREATE INDEX idx_users_master_table_master_user_id_tenant_id_tenant_user_id'
);
// Should still contain the unique index on subset of fields
expect(sql).toContain('CREATE UNIQUE INDEX index_1');
});
it('should handle single primary keys inline', () => {
const diagram: Diagram = createDiagram({
id: testId(),
@@ -165,7 +245,7 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should contain inline PRIMARY KEY
expect(sql).toMatch(/id\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
expect(sql).toMatch(/"id"\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
// Should NOT contain separate PRIMARY KEY constraint
expect(sql).not.toContain('PRIMARY KEY (id)');
});
@@ -226,8 +306,8 @@ describe('DBML Export - SQL Generation Tests', () => {
expect(sql).not.toContain('DEFAULT has default');
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('"is_active" boolean');
expect(sql).toContain('"stock_count" integer NOT NULL'); // integer gets simplified to int
});
it('should handle valid default values correctly', () => {
@@ -349,8 +429,8 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should convert NOW to NOW() and ('now') to now()
expect(sql).toContain('created_at timestamp DEFAULT NOW');
expect(sql).toContain('updated_at timestamp DEFAULT now()');
expect(sql).toContain('"created_at" timestamp DEFAULT NOW');
expect(sql).toContain('"updated_at" timestamp DEFAULT now()');
});
});
@@ -405,9 +485,9 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should handle char with explicit length
expect(sql).toContain('element_code char(2)');
expect(sql).toContain('"element_code" char(2)');
// Should add default length for char without length
expect(sql).toContain('status char(1)');
expect(sql).toContain('"status" char(1)');
});
it('should not have spaces between char and parentheses', () => {
@@ -516,7 +596,7 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should create a valid table without primary key
expect(sql).toContain('CREATE TABLE experiment_logs');
expect(sql).toContain('CREATE TABLE "experiment_logs"');
expect(sql).not.toContain('PRIMARY KEY');
});
@@ -631,11 +711,11 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should create both tables
expect(sql).toContain('CREATE TABLE guilds');
expect(sql).toContain('CREATE TABLE guild_members');
expect(sql).toContain('CREATE TABLE "guilds"');
expect(sql).toContain('CREATE TABLE "guild_members"');
// Should create foreign key
expect(sql).toContain(
'ALTER TABLE guild_members ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY (guild_id) REFERENCES guilds (id)'
'ALTER TABLE "guild_members" ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY ("guild_id") REFERENCES "guilds" ("id");'
);
});
});
@@ -709,12 +789,9 @@ describe('DBML Export - SQL Generation Tests', () => {
isDBMLFlow: true,
});
// Should create schemas
expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS transportation');
expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS magic');
// Should use schema-qualified table names
expect(sql).toContain('CREATE TABLE transportation.portals');
expect(sql).toContain('CREATE TABLE magic.spells');
expect(sql).toContain('CREATE TABLE "transportation"."portals"');
expect(sql).toContain('CREATE TABLE "magic"."spells"');
});
});
@@ -761,7 +838,7 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should still create table structure
expect(sql).toContain('CREATE TABLE empty_table');
expect(sql).toContain('CREATE TABLE "empty_table"');
expect(sql).toContain('(\n\n)');
});
@@ -862,9 +939,9 @@ describe('DBML Export - SQL Generation Tests', () => {
});
// Should include precision and scale
expect(sql).toContain('amount numeric(15, 2)');
expect(sql).toContain('"amount" numeric(15, 2)');
// Should include precision only when scale is not provided
expect(sql).toContain('interest_rate numeric(5)');
expect(sql).toContain('"interest_rate" numeric(5)');
});
});
});

File diff suppressed because it is too large Load Diff

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)}`
: '';
@@ -177,7 +178,15 @@ export function exportMSSQL({
})
.join(',\n')}${
table.fields.filter((f) => f.primaryKey).length > 0
? `,\n PRIMARY KEY (${table.fields
? `,\n ${(() => {
// Find PK index to get the constraint name
const pkIndex = table.indexes.find(
(idx) => idx.isPrimaryKey
);
return pkIndex?.name
? `CONSTRAINT [${pkIndex.name}] `
: '';
})()}PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`

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()
@@ -311,7 +313,15 @@ export function exportMySQL({
.join(',\n')}${
// Add PRIMARY KEY as table constraint
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
? `,\n ${(() => {
// Find PK index to get the constraint name
const pkIndex = table.indexes.find(
(idx) => idx.isPrimaryKey
);
return pkIndex?.name
? `CONSTRAINT \`${pkIndex.name}\` `
: '';
})()}PRIMARY KEY (${primaryKeyFields
.map((f) => `\`${f.name}\``)
.join(', ')})`
: ''

View File

@@ -325,7 +325,15 @@ export function exportPostgreSQL({
})
.join(',\n')}${
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
? `,\n ${(() => {
// Find PK index to get the constraint name
const pkIndex = table.indexes.find(
(idx) => idx.isPrimaryKey
);
return pkIndex?.name
? `CONSTRAINT "${pkIndex.name}" `
: '';
})()}PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
@@ -405,7 +413,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,30 +11,70 @@ 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;
};
// Helper function to properly quote table/schema names with special characters
const getQuotedTableName = (
table: DBTable,
isDBMLFlow: boolean = false
): string => {
// Check if a name is already quoted
const isAlreadyQuoted = (name: string) => {
return (
(name.startsWith('"') && name.endsWith('"')) ||
(name.startsWith('`') && name.endsWith('`')) ||
(name.startsWith('[') && name.endsWith(']'))
);
};
// Only add quotes if needed and not already quoted
const quoteIfNeeded = (name: string) => {
if (isAlreadyQuoted(name)) {
return name;
}
const needsQuoting = /[^a-zA-Z0-9_]/.test(name) || isDBMLFlow;
return needsQuoting ? `"${name}"` : name;
};
if (table.schema) {
const quotedSchema = quoteIfNeeded(table.schema);
const quotedTable = quoteIfNeeded(table.name);
return `${quotedSchema}.${quotedTable}`;
} else {
return quoteIfNeeded(table.name);
}
};
const getQuotedFieldName = (
fieldName: string,
isDBMLFlow: boolean = false
): string => {
// Check if a name is already quoted
const isAlreadyQuoted = (name: string) => {
return (
(name.startsWith('"') && name.endsWith('"')) ||
(name.startsWith('`') && name.endsWith('`')) ||
(name.startsWith('[') && name.endsWith(']'))
);
};
if (isAlreadyQuoted(fieldName)) {
return fieldName;
}
// For DBML flow, always quote field names
// Otherwise, only quote if it contains special characters
const needsQuoting = /[^a-zA-Z0-9_]/.test(fieldName) || isDBMLFlow;
return needsQuoting ? `"${fieldName}"` : fieldName;
};
export const exportBaseSQL = ({
diagram,
targetDatabaseType,
@@ -75,18 +118,21 @@ export const exportBaseSQL = ({
let sqlScript = '';
// First create the CREATE SCHEMA statements for all the found schemas based on tables
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Skip schema creation for DBML flow as DBML doesn't support CREATE SCHEMA syntax
if (!isDBMLFlow) {
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Add CREATE SCHEMA statements if any schemas exist
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS ${schema};\n`;
});
if (schemas.size > 0) sqlScript += '\n'; // Add newline only if schemas were added
// Add CREATE SCHEMA statements if any schemas exist
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
});
if (schemas.size > 0) sqlScript += '\n'; // Add newline only if schemas were added
}
// Add CREATE TYPE statements for ENUMs and COMPOSITE types from diagram.customTypes
if (diagram.customTypes && diagram.customTypes.length > 0) {
@@ -178,9 +224,7 @@ export const exportBaseSQL = ({
// Loop through each non-view table to generate the SQL statements
nonViewTables.forEach((table) => {
const tableName = table.schema
? `${table.schema}.${table.name}`
: table.name;
const tableName = getQuotedTableName(table, isDBMLFlow);
sqlScript += `CREATE TABLE ${tableName} (\n`;
// Check for composite primary keys
@@ -249,7 +293,8 @@ export const exportBaseSQL = ({
typeName = 'char';
}
sqlScript += ` ${field.name} ${typeName}`;
const quotedFieldName = getQuotedFieldName(field.name, isDBMLFlow);
sqlScript += ` ${quotedFieldName} ${typeName}`;
// Add size for character types
if (
@@ -286,8 +331,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;
@@ -320,45 +370,87 @@ export const exportBaseSQL = ({
}
}
// Handle PRIMARY KEY constraint - only add inline if not composite
if (field.primaryKey && !hasCompositePrimaryKey) {
// Handle PRIMARY KEY constraint - only add inline if no PK index with custom name
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
if (field.primaryKey && !hasCompositePrimaryKey && !pkIndex?.name) {
sqlScript += ' PRIMARY KEY';
}
// Add a comma after each field except the last one (or before composite primary key)
if (index < table.fields.length - 1 || hasCompositePrimaryKey) {
// Add a comma after each field except the last one (or before PK constraint)
const needsPKConstraint =
hasCompositePrimaryKey ||
(primaryKeyFields.length === 1 && pkIndex?.name);
if (index < table.fields.length - 1 || needsPKConstraint) {
sqlScript += ',\n';
}
});
// Add composite primary key constraint if needed
if (hasCompositePrimaryKey) {
const pkFieldNames = primaryKeyFields.map((f) => f.name).join(', ');
sqlScript += `\n PRIMARY KEY (${pkFieldNames})`;
// Add primary key constraint if needed (for composite PKs or single PK with custom name)
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
if (
hasCompositePrimaryKey ||
(primaryKeyFields.length === 1 && pkIndex?.name)
) {
const pkFieldNames = primaryKeyFields
.map((f) => getQuotedFieldName(f.name, isDBMLFlow))
.join(', ');
if (pkIndex?.name) {
sqlScript += `\n CONSTRAINT ${pkIndex.name} PRIMARY KEY (${pkFieldNames})`;
} else {
sqlScript += `\n PRIMARY KEY (${pkFieldNames})`;
}
}
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) {
const quotedFieldName = getQuotedFieldName(
field.name,
isDBMLFlow
);
sqlScript += `COMMENT ON COLUMN ${tableName}.${quotedFieldName} IS '${escapeSQLComment(field.comments)}';\n`;
}
});
// Generate SQL for indexes
table.indexes.forEach((index) => {
const fieldNames = index.fieldIds
.map(
(fieldId) =>
table.fields.find((field) => field.id === fieldId)?.name
// Skip the primary key index (it's already handled as a constraint)
if (index.isPrimaryKey) {
return;
}
// Get the fields for this index
const indexFields = index.fieldIds
.map((fieldId) => table.fields.find((f) => f.id === fieldId))
.filter(
(field): field is NonNullable<typeof field> =>
field !== undefined
);
// Skip if this index exactly matches the primary key fields
// This prevents creating redundant indexes for composite primary keys
if (
primaryKeyFields.length > 0 &&
primaryKeyFields.length === indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some((field) => field.id === pk.id)
)
.filter(Boolean)
) {
return; // Skip this index as it's redundant with the primary key
}
const fieldNames = indexFields
.map((field) => getQuotedFieldName(field.name, isDBMLFlow))
.join(', ');
if (fieldNames) {
@@ -436,13 +528,18 @@ export const exportBaseSQL = ({
return;
}
const fkTableName = fkTable.schema
? `${fkTable.schema}.${fkTable.name}`
: fkTable.name;
const refTableName = refTable.schema
? `${refTable.schema}.${refTable.name}`
: refTable.name;
sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${fkField.name}) REFERENCES ${refTableName} (${refField.name});\n`;
const fkTableName = getQuotedTableName(fkTable, isDBMLFlow);
const refTableName = getQuotedTableName(refTable, isDBMLFlow);
const quotedFkFieldName = getQuotedFieldName(
fkField.name,
isDBMLFlow
);
const quotedRefFieldName = getQuotedFieldName(
refField.name,
isDBMLFlow
);
sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${quotedFkFieldName}) REFERENCES ${refTableName} (${quotedRefFieldName});\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';
@@ -86,7 +86,7 @@ export interface SQLBinaryExpr extends SQLASTNode {
export interface SQLFunctionNode extends SQLASTNode {
type: 'function';
name: string;
name: string | { name: Array<{ value: string }> };
args?: {
value: SQLASTArg[];
};
@@ -108,6 +108,31 @@ export interface SQLStringLiteral extends SQLASTNode {
value: string;
}
export interface SQLDefaultNode extends SQLASTNode {
type: 'default';
value: SQLASTNode;
}
export interface SQLCastNode extends SQLASTNode {
type: 'cast';
expr: SQLASTNode;
target: Array<{ dataType: string }>;
}
export interface SQLBooleanNode extends SQLASTNode {
type: 'bool';
value: boolean;
}
export interface SQLNullNode extends SQLASTNode {
type: 'null';
}
export interface SQLNumberNode extends SQLASTNode {
type: 'number';
value: number;
}
export type SQLASTArg =
| SQLColumnRef
| SQLStringLiteral
@@ -146,6 +171,22 @@ export function buildSQLFromAST(
): string {
if (!ast) return '';
// Handle default value wrapper
if (ast.type === 'default' && 'value' in ast) {
const defaultNode = ast as SQLDefaultNode;
return buildSQLFromAST(defaultNode.value, dbType);
}
// Handle PostgreSQL cast expressions (e.g., 'value'::type)
if (ast.type === 'cast' && 'expr' in ast && 'target' in ast) {
const castNode = ast as SQLCastNode;
const expr = buildSQLFromAST(castNode.expr, dbType);
if (castNode.target.length > 0 && castNode.target[0].dataType) {
return `${expr}::${castNode.target[0].dataType.toLowerCase()}`;
}
return expr;
}
if (ast.type === 'binary_expr') {
const expr = ast as SQLBinaryExpr;
const leftSQL = buildSQLFromAST(expr.left, dbType);
@@ -155,7 +196,59 @@ export function buildSQLFromAST(
if (ast.type === 'function') {
const func = ast as SQLFunctionNode;
let expr = func.name;
let funcName = '';
// Handle nested function name structure
if (typeof func.name === 'object' && func.name && 'name' in func.name) {
const nameObj = func.name as { name: Array<{ value: string }> };
if (nameObj.name.length > 0) {
funcName = nameObj.name[0].value || '';
}
} else if (typeof func.name === 'string') {
funcName = func.name;
}
if (!funcName) return '';
// Normalize PostgreSQL function names to uppercase for consistency
if (dbType === DatabaseType.POSTGRESQL) {
const pgFunctions = [
'now',
'current_timestamp',
'current_date',
'current_time',
'gen_random_uuid',
'random',
'nextval',
'currval',
];
if (pgFunctions.includes(funcName.toLowerCase())) {
funcName = funcName.toUpperCase();
}
}
// Some PostgreSQL functions don't have parentheses (like CURRENT_TIMESTAMP)
if (funcName === 'CURRENT_TIMESTAMP' && !func.args) {
return funcName;
}
// Handle SQL Server function defaults that were preprocessed as strings
// The preprocessor converts NEWID() to 'newid', GETDATE() to 'getdate', etc.
if (dbType === DatabaseType.SQL_SERVER) {
const sqlServerFunctions: Record<string, string> = {
newid: 'NEWID()',
newsequentialid: 'NEWSEQUENTIALID()',
getdate: 'GETDATE()',
sysdatetime: 'SYSDATETIME()',
};
const lowerFuncName = funcName.toLowerCase();
if (sqlServerFunctions[lowerFuncName]) {
return sqlServerFunctions[lowerFuncName];
}
}
let expr = funcName;
if (func.args) {
expr +=
'(' +
@@ -175,12 +268,31 @@ export function buildSQLFromAST(
})
.join(', ') +
')';
} else {
expr += '()';
}
return expr;
} else if (ast.type === 'column_ref') {
return quoteIdentifier((ast as SQLColumnRef).column, dbType);
} else if (ast.type === 'expr_list') {
return (ast as SQLExprList).value.map((v) => v.value).join(' AND ');
} else if (ast.type === 'single_quote_string') {
// String literal with single quotes
const strNode = ast as SQLStringLiteral;
return `'${strNode.value}'`;
} else if (ast.type === 'double_quote_string') {
// String literal with double quotes
const strNode = ast as SQLStringLiteral;
return `"${strNode.value}"`;
} else if (ast.type === 'bool') {
// Boolean value
const boolNode = ast as SQLBooleanNode;
return boolNode.value ? 'TRUE' : 'FALSE';
} else if (ast.type === 'null') {
return 'NULL';
} else if (ast.type === 'number') {
const numNode = ast as SQLNumberNode;
return String(numNode.value);
} else {
const valueNode = ast as { type: string; value: string | number };
return typeof valueNode.value === 'string'
@@ -727,10 +839,10 @@ export function convertToChartDBDiagram(
indexes,
x: col * tableSpacing,
y: row * tableSpacing,
color: randomColor(),
color: defaultTableColor,
isView: false,
createdAt: Date.now(),
};
} satisfies DBTable;
});
// Process relationships
@@ -779,19 +891,13 @@ export function convertToChartDBDiagram(
}
const sourceField = sourceTable.fields.find(
(f) => f.name === rel.sourceColumn
(f) => f.name.toLowerCase() === rel.sourceColumn.toLowerCase()
);
const targetField = targetTable.fields.find(
(f) => f.name === rel.targetColumn
(f) => f.name.toLowerCase() === rel.targetColumn.toLowerCase()
);
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

@@ -0,0 +1,228 @@
import { describe, it, expect } from 'vitest';
import { fromMySQL } from '../mysql';
describe('MySQL Default Value Import', () => {
describe('String Default Values', () => {
it('should parse simple string defaults with single quotes', async () => {
const sql = `
CREATE TABLE tavern_patrons (
patron_id INT NOT NULL,
membership_status VARCHAR(50) DEFAULT 'regular',
PRIMARY KEY (patron_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const statusColumn = result.tables[0].columns.find(
(c) => c.name === 'membership_status'
);
expect(statusColumn?.default).toBe("'regular'");
});
it('should parse string defaults with escaped quotes', async () => {
const sql = `
CREATE TABLE wizard_spellbooks (
spellbook_id INT NOT NULL,
incantation VARCHAR(255) DEFAULT 'Dragon\\'s flame',
spell_metadata TEXT DEFAULT '{"type": "fire"}',
PRIMARY KEY (spellbook_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const incantationColumn = result.tables[0].columns.find(
(c) => c.name === 'incantation'
);
expect(incantationColumn?.default).toBeTruthy();
const metadataColumn = result.tables[0].columns.find(
(c) => c.name === 'spell_metadata'
);
expect(metadataColumn?.default).toBeTruthy();
});
});
describe('Numeric Default Values', () => {
it('should parse integer defaults', async () => {
const sql = `
CREATE TABLE dungeon_levels (
level_id INT NOT NULL,
monster_count INT DEFAULT 0,
max_treasure INT DEFAULT 1000,
PRIMARY KEY (level_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const monsterColumn = result.tables[0].columns.find(
(c) => c.name === 'monster_count'
);
expect(monsterColumn?.default).toBe('0');
const treasureColumn = result.tables[0].columns.find(
(c) => c.name === 'max_treasure'
);
expect(treasureColumn?.default).toBe('1000');
});
it('should parse decimal defaults', async () => {
const sql = `
CREATE TABLE merchant_inventory (
item_id INT NOT NULL,
base_price DECIMAL(10, 2) DEFAULT 99.99,
loyalty_discount FLOAT DEFAULT 0.15,
PRIMARY KEY (item_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const priceColumn = result.tables[0].columns.find(
(c) => c.name === 'base_price'
);
expect(priceColumn?.default).toBe('99.99');
const discountColumn = result.tables[0].columns.find(
(c) => c.name === 'loyalty_discount'
);
expect(discountColumn?.default).toBe('0.15');
});
});
describe('Boolean Default Values', () => {
it('should parse boolean defaults in MySQL (using TINYINT)', async () => {
const sql = `
CREATE TABLE character_status (
character_id INT NOT NULL,
is_alive TINYINT(1) DEFAULT 1,
is_cursed TINYINT(1) DEFAULT 0,
has_magic BOOLEAN DEFAULT TRUE,
PRIMARY KEY (character_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const aliveColumn = result.tables[0].columns.find(
(c) => c.name === 'is_alive'
);
expect(aliveColumn?.default).toBe('1');
const cursedColumn = result.tables[0].columns.find(
(c) => c.name === 'is_cursed'
);
expect(cursedColumn?.default).toBe('0');
});
});
describe('NULL Default Values', () => {
it('should parse NULL defaults', async () => {
const sql = `
CREATE TABLE companion_animals (
companion_id INT NOT NULL,
special_trait VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (companion_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const traitColumn = result.tables[0].columns.find(
(c) => c.name === 'special_trait'
);
expect(traitColumn?.default).toBe('NULL');
});
});
describe('Function Default Values', () => {
it('should parse function defaults', async () => {
const sql = `
CREATE TABLE quest_entries (
entry_id INT NOT NULL AUTO_INCREMENT,
quest_accepted TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
quest_uuid VARCHAR(36) DEFAULT (UUID()),
PRIMARY KEY (entry_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const acceptedColumn = result.tables[0].columns.find(
(c) => c.name === 'quest_accepted'
);
expect(acceptedColumn?.default).toBe('CURRENT_TIMESTAMP');
const updatedColumn = result.tables[0].columns.find(
(c) => c.name === 'last_updated'
);
expect(updatedColumn?.default).toBe('CURRENT_TIMESTAMP');
});
});
describe('AUTO_INCREMENT', () => {
it('should handle AUTO_INCREMENT columns correctly', async () => {
const sql = `
CREATE TABLE hero_registry (
hero_id INT NOT NULL AUTO_INCREMENT,
hero_name VARCHAR(100),
PRIMARY KEY (hero_id)
);
`;
const result = await fromMySQL(sql);
expect(result.tables).toHaveLength(1);
const idColumn = result.tables[0].columns.find(
(c) => c.name === 'hero_id'
);
expect(idColumn?.increment).toBe(true);
// AUTO_INCREMENT columns typically don't have a default value
expect(idColumn?.default).toBeUndefined();
});
});
describe('Complex Real-World Example', () => {
it('should handle complex table with multiple default types', async () => {
const sql = `
CREATE TABLE adventurer_profiles (
adventurer_id BIGINT NOT NULL AUTO_INCREMENT,
character_name VARCHAR(50) NOT NULL,
guild_email VARCHAR(255) NOT NULL,
rank VARCHAR(20) DEFAULT 'novice',
is_guild_verified TINYINT(1) DEFAULT 0,
gold_coins INT DEFAULT 100,
account_balance DECIMAL(10, 2) DEFAULT 0.00,
joined_realm TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_quest TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
inventory_data JSON DEFAULT NULL,
PRIMARY KEY (adventurer_id),
UNIQUE KEY uk_guild_email (guild_email),
INDEX idx_rank (rank)
);
`;
const result = await fromMySQL(sql);
const table = result.tables[0];
expect(table).toBeDefined();
// Check various default values
const rankColumn = table.columns.find((c) => c.name === 'rank');
expect(rankColumn?.default).toBe("'novice'");
const verifiedColumn = table.columns.find(
(c) => c.name === 'is_guild_verified'
);
expect(verifiedColumn?.default).toBe('0');
const goldColumn = table.columns.find(
(c) => c.name === 'gold_coins'
);
expect(goldColumn?.default).toBe('100');
const balanceColumn = table.columns.find(
(c) => c.name === 'account_balance'
);
expect(balanceColumn?.default).toBe('0.00');
const joinedColumn = table.columns.find(
(c) => c.name === 'joined_realm'
);
expect(joinedColumn?.default).toBe('CURRENT_TIMESTAMP');
const inventoryColumn = table.columns.find(
(c) => c.name === 'inventory_data'
);
expect(inventoryColumn?.default).toBe('NULL');
});
});
});

View File

@@ -101,12 +101,28 @@ function extractColumnsFromCreateTable(statement: string): SQLColumn[] {
const typeMatch = definition.match(/^([^\s(]+)(?:\(([^)]+)\))?/);
const dataType = typeMatch ? typeMatch[1] : '';
// Extract default value
let defaultValue: string | undefined;
const defaultMatch = definition.match(
/DEFAULT\s+('[^']*'|"[^"]*"|NULL|CURRENT_TIMESTAMP|\S+)/i
);
if (defaultMatch) {
defaultValue = defaultMatch[1];
}
// Check for AUTO_INCREMENT
const increment = definition
.toUpperCase()
.includes('AUTO_INCREMENT');
columns.push({
name: columnName,
type: dataType,
nullable,
primaryKey,
unique: definition.toUpperCase().includes('UNIQUE'),
default: defaultValue,
increment,
});
}
}
@@ -721,7 +737,28 @@ export async function fromMySQL(sqlContent: string): Promise<SQLParserResult> {
parseError
);
// Error handling without logging
// Try fallback parser when main parser fails
const tableMatch = trimmedStmt.match(
/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([^`\s(]+)`?\s*\(/i
);
if (tableMatch) {
const tableName = tableMatch[1];
const tableId = generateId();
tableMap[tableName] = tableId;
const extractedColumns =
extractColumnsFromCreateTable(trimmedStmt);
if (extractedColumns.length > 0) {
tables.push({
id: tableId,
name: tableName,
schema: undefined,
columns: extractedColumns,
indexes: [],
order: tables.length,
});
}
}
}
}
}

View File

@@ -0,0 +1,215 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => {
it('should handle ALTER TABLE ADD COLUMN statements', async () => {
const sql = `
CREATE SCHEMA IF NOT EXISTS "public";
CREATE TABLE "public"."location" (
"id" bigint NOT NULL,
CONSTRAINT "pk_table_7_id" PRIMARY KEY ("id")
);
-- Add new fields to existing location table
ALTER TABLE location ADD COLUMN country_id INT;
ALTER TABLE location ADD COLUMN state_id INT;
ALTER TABLE location ADD COLUMN location_type_id INT;
ALTER TABLE location ADD COLUMN city_id INT;
ALTER TABLE location ADD COLUMN street TEXT;
ALTER TABLE location ADD COLUMN block TEXT;
ALTER TABLE location ADD COLUMN building TEXT;
ALTER TABLE location ADD COLUMN floor TEXT;
ALTER TABLE location ADD COLUMN apartment TEXT;
ALTER TABLE location ADD COLUMN lat INT;
ALTER TABLE location ADD COLUMN long INT;
ALTER TABLE location ADD COLUMN elevation INT;
ALTER TABLE location ADD COLUMN erp_site_id INT;
ALTER TABLE location ADD COLUMN is_active TEXT;
ALTER TABLE location ADD COLUMN remarks TEXT;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const locationTable = result.tables[0];
expect(locationTable.name).toBe('location');
expect(locationTable.schema).toBe('public');
// Should have the original id column plus all the added columns
expect(locationTable.columns).toHaveLength(16);
// Check that the id column is present
const idColumn = locationTable.columns.find((col) => col.name === 'id');
expect(idColumn).toBeDefined();
expect(idColumn?.type).toBe('BIGINT');
expect(idColumn?.primaryKey).toBe(true);
// Check some of the added columns
const countryIdColumn = locationTable.columns.find(
(col) => col.name === 'country_id'
);
expect(countryIdColumn).toBeDefined();
expect(countryIdColumn?.type).toBe('INTEGER');
const streetColumn = locationTable.columns.find(
(col) => col.name === 'street'
);
expect(streetColumn).toBeDefined();
expect(streetColumn?.type).toBe('TEXT');
const remarksColumn = locationTable.columns.find(
(col) => col.name === 'remarks'
);
expect(remarksColumn).toBeDefined();
expect(remarksColumn?.type).toBe('TEXT');
});
it('should handle ALTER TABLE ADD COLUMN with schema qualification', async () => {
const sql = `
CREATE TABLE public.users (
id INTEGER PRIMARY KEY
);
ALTER TABLE public.users ADD COLUMN email VARCHAR(255);
ALTER TABLE public.users ADD COLUMN created_at TIMESTAMP;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const usersTable = result.tables[0];
expect(usersTable.columns).toHaveLength(3);
const emailColumn = usersTable.columns.find(
(col) => col.name === 'email'
);
expect(emailColumn).toBeDefined();
expect(emailColumn?.type).toBe('VARCHAR(255)');
const createdAtColumn = usersTable.columns.find(
(col) => col.name === 'created_at'
);
expect(createdAtColumn).toBeDefined();
expect(createdAtColumn?.type).toBe('TIMESTAMP');
});
it('should handle ALTER TABLE ADD COLUMN with constraints', async () => {
const sql = `
CREATE TABLE products (
id SERIAL PRIMARY KEY
);
ALTER TABLE products ADD COLUMN name VARCHAR(100) NOT NULL;
ALTER TABLE products ADD COLUMN sku VARCHAR(50) UNIQUE;
ALTER TABLE products ADD COLUMN price DECIMAL(10,2) DEFAULT 0.00;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const productsTable = result.tables[0];
expect(productsTable.columns).toHaveLength(4);
const nameColumn = productsTable.columns.find(
(col) => col.name === 'name'
);
expect(nameColumn).toBeDefined();
expect(nameColumn?.nullable).toBe(false);
const skuColumn = productsTable.columns.find(
(col) => col.name === 'sku'
);
expect(skuColumn).toBeDefined();
expect(skuColumn?.unique).toBe(true);
const priceColumn = productsTable.columns.find(
(col) => col.name === 'price'
);
expect(priceColumn).toBeDefined();
expect(priceColumn?.default).toBe('0');
});
it('should not add duplicate columns', async () => {
const sql = `
CREATE TABLE items (
id INTEGER PRIMARY KEY,
name VARCHAR(100)
);
ALTER TABLE items ADD COLUMN description TEXT;
ALTER TABLE items ADD COLUMN name VARCHAR(200); -- Should not be added as duplicate
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const itemsTable = result.tables[0];
// Should only have 3 columns: id, name (original), and description
expect(itemsTable.columns).toHaveLength(3);
const nameColumns = itemsTable.columns.filter(
(col) => col.name === 'name'
);
expect(nameColumns).toHaveLength(1);
expect(nameColumns[0].type).toBe('VARCHAR(100)'); // Should keep original type
});
it('should use default schema when not specified', async () => {
const sql = `
CREATE TABLE test_table (
id INTEGER PRIMARY KEY
);
ALTER TABLE test_table ADD COLUMN value TEXT;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const testTable = result.tables[0];
expect(testTable.schema).toBe('public');
expect(testTable.columns).toHaveLength(2);
const valueColumn = testTable.columns.find(
(col) => col.name === 'value'
);
expect(valueColumn).toBeDefined();
});
it('should handle quoted identifiers in ALTER TABLE ADD COLUMN', async () => {
const sql = `
CREATE TABLE "my-table" (
"id" INTEGER PRIMARY KEY
);
ALTER TABLE "my-table" ADD COLUMN "my-column" VARCHAR(50);
ALTER TABLE "my-table" ADD COLUMN "another-column" INTEGER;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const myTable = result.tables[0];
expect(myTable.name).toBe('my-table');
expect(myTable.columns).toHaveLength(3);
const myColumn = myTable.columns.find(
(col) => col.name === 'my-column'
);
expect(myColumn).toBeDefined();
expect(myColumn?.type).toBe('VARCHAR(50)');
const anotherColumn = myTable.columns.find(
(col) => col.name === 'another-column'
);
expect(anotherColumn).toBeDefined();
expect(anotherColumn?.type).toBe('INTEGER');
});
});

View File

@@ -0,0 +1,118 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL ALTER TABLE ALTER COLUMN TYPE', () => {
it('should handle ALTER TABLE ALTER COLUMN TYPE statements', async () => {
const sql = `
CREATE SCHEMA IF NOT EXISTS "public";
CREATE TABLE "public"."table_12" (
"id" SERIAL,
"field1" varchar(200),
"field2" varchar(200),
"field3" varchar(200),
PRIMARY KEY ("id")
);
ALTER TABLE table_12 ALTER COLUMN field1 TYPE VARCHAR(254);
ALTER TABLE table_12 ALTER COLUMN field2 TYPE VARCHAR(254);
ALTER TABLE table_12 ALTER COLUMN field3 TYPE VARCHAR(254);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.name).toBe('table_12');
expect(table.columns).toHaveLength(4); // id, field1, field2, field3
// Check that the columns have the updated type
const field1 = table.columns.find((col) => col.name === 'field1');
expect(field1).toBeDefined();
expect(field1?.type).toBe('VARCHAR(254)'); // Should be updated from 200 to 254
const field2 = table.columns.find((col) => col.name === 'field2');
expect(field2).toBeDefined();
expect(field2?.type).toBe('VARCHAR(254)');
const field3 = table.columns.find((col) => col.name === 'field3');
expect(field3).toBeDefined();
expect(field3?.type).toBe('VARCHAR(254)');
});
it('should handle various ALTER COLUMN TYPE scenarios', async () => {
const sql = `
CREATE TABLE test_table (
id INTEGER PRIMARY KEY,
name VARCHAR(50),
age SMALLINT,
score NUMERIC(5,2)
);
-- Change varchar length
ALTER TABLE test_table ALTER COLUMN name TYPE VARCHAR(100);
-- Change numeric type
ALTER TABLE test_table ALTER COLUMN age TYPE INTEGER;
-- Change precision
ALTER TABLE test_table ALTER COLUMN score TYPE NUMERIC(10,4);
`;
const result = await fromPostgres(sql);
const table = result.tables[0];
const nameCol = table.columns.find((col) => col.name === 'name');
expect(nameCol?.type).toBe('VARCHAR(100)');
const ageCol = table.columns.find((col) => col.name === 'age');
expect(ageCol?.type).toBe('INTEGER');
const scoreCol = table.columns.find((col) => col.name === 'score');
expect(scoreCol?.type).toBe('NUMERIC(10,4)');
});
it('should handle multiple type changes on the same column', async () => {
const sql = `
CREATE SCHEMA IF NOT EXISTS "public";
CREATE TABLE "public"."table_12" (
"id" SERIAL,
"field1" varchar(200),
"field2" varchar(200),
"field3" varchar(200),
PRIMARY KEY ("id")
);
ALTER TABLE table_12 ALTER COLUMN field1 TYPE VARCHAR(254);
ALTER TABLE table_12 ALTER COLUMN field2 TYPE VARCHAR(254);
ALTER TABLE table_12 ALTER COLUMN field3 TYPE VARCHAR(254);
ALTER TABLE table_12 ALTER COLUMN field1 TYPE BIGINT;
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.name).toBe('table_12');
expect(table.schema).toBe('public');
expect(table.columns).toHaveLength(4);
// Check that field1 has the final type (BIGINT), not the intermediate VARCHAR(254)
const field1 = table.columns.find((col) => col.name === 'field1');
expect(field1).toBeDefined();
expect(field1?.type).toBe('BIGINT'); // Should be BIGINT, not VARCHAR(254)
// Check that field2 and field3 still have VARCHAR(254)
const field2 = table.columns.find((col) => col.name === 'field2');
expect(field2).toBeDefined();
expect(field2?.type).toBe('VARCHAR(254)');
const field3 = table.columns.find((col) => col.name === 'field3');
expect(field3).toBeDefined();
expect(field3?.type).toBe('VARCHAR(254)');
});
});

View File

@@ -0,0 +1,117 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL ALTER TABLE with Foreign Keys', () => {
it('should handle ALTER TABLE ADD COLUMN followed by ALTER TABLE ADD FOREIGN KEY', async () => {
const sql = `
CREATE SCHEMA IF NOT EXISTS "public";
CREATE TABLE "public"."location" (
"id" bigint NOT NULL,
CONSTRAINT "pk_table_7_id" PRIMARY KEY ("id")
);
-- Add new fields to existing location table
ALTER TABLE location ADD COLUMN country_id INT;
ALTER TABLE location ADD COLUMN state_id INT;
ALTER TABLE location ADD COLUMN location_type_id INT;
ALTER TABLE location ADD COLUMN city_id INT;
ALTER TABLE location ADD COLUMN street TEXT;
ALTER TABLE location ADD COLUMN block TEXT;
ALTER TABLE location ADD COLUMN building TEXT;
ALTER TABLE location ADD COLUMN floor TEXT;
ALTER TABLE location ADD COLUMN apartment TEXT;
ALTER TABLE location ADD COLUMN lat INT;
ALTER TABLE location ADD COLUMN long INT;
ALTER TABLE location ADD COLUMN elevation INT;
ALTER TABLE location ADD COLUMN erp_site_id INT;
ALTER TABLE location ADD COLUMN is_active TEXT;
ALTER TABLE location ADD COLUMN remarks TEXT;
-- Create lookup tables
CREATE TABLE country (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
code VARCHAR(3) UNIQUE
);
CREATE TABLE state (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
country_id INT NOT NULL,
FOREIGN KEY (country_id) REFERENCES country(id)
);
CREATE TABLE location_type (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE city (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
state_id INT NOT NULL,
FOREIGN KEY (state_id) REFERENCES state(id)
);
-- Add foreign key constraints from location to lookup tables
ALTER TABLE location ADD CONSTRAINT fk_location_country
FOREIGN KEY (country_id) REFERENCES country(id);
ALTER TABLE location ADD CONSTRAINT fk_location_state
FOREIGN KEY (state_id) REFERENCES state(id);
ALTER TABLE location ADD CONSTRAINT fk_location_location_type
FOREIGN KEY (location_type_id) REFERENCES location_type(id);
ALTER TABLE location ADD CONSTRAINT fk_location_city
FOREIGN KEY (city_id) REFERENCES city(id);
`;
const result = await fromPostgres(sql);
const locationTable = result.tables.find((t) => t.name === 'location');
// Check tables
expect(result.tables).toHaveLength(5); // location, country, state, location_type, city
// Check location table has all columns
expect(locationTable).toBeDefined();
expect(locationTable?.columns).toHaveLength(16); // id + 15 added columns
// Check foreign key relationships
const locationRelationships = result.relationships.filter(
(r) => r.sourceTable === 'location'
);
// Should have 4 FKs from location to lookup tables + 2 from state/city
expect(result.relationships.length).toBeGreaterThanOrEqual(6);
// Check specific foreign keys from location
expect(
locationRelationships.some(
(r) =>
r.sourceColumn === 'country_id' &&
r.targetTable === 'country'
)
).toBe(true);
expect(
locationRelationships.some(
(r) =>
r.sourceColumn === 'state_id' && r.targetTable === 'state'
)
).toBe(true);
expect(
locationRelationships.some(
(r) =>
r.sourceColumn === 'location_type_id' &&
r.targetTable === 'location_type'
)
).toBe(true);
expect(
locationRelationships.some(
(r) => r.sourceColumn === 'city_id' && r.targetTable === 'city'
)
).toBe(true);
});
});

View File

@@ -0,0 +1,395 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL Default Value Import', () => {
describe('String Default Values', () => {
it('should parse simple string defaults with single quotes', async () => {
const sql = `
CREATE TABLE heroes (
hero_id INTEGER NOT NULL,
hero_status CHARACTER VARYING DEFAULT 'questing',
PRIMARY KEY (hero_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const statusColumn = result.tables[0].columns.find(
(c) => c.name === 'hero_status'
);
expect(statusColumn?.default).toBe("'questing'");
});
it('should parse string defaults with special characters that need escaping', async () => {
const sql = `
CREATE TABLE spell_scrolls (
scroll_id INTEGER NOT NULL,
incantation CHARACTER VARYING DEFAULT 'Dragon''s breath',
rune_inscription TEXT DEFAULT 'Ancient rune
Sacred symbol',
PRIMARY KEY (scroll_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const incantationColumn = result.tables[0].columns.find(
(c) => c.name === 'incantation'
);
expect(incantationColumn?.default).toBe("'Dragon''s breath'");
});
it('should parse elvish text default values', async () => {
const sql = `
CREATE TABLE elven_greetings (
greeting_id INTEGER NOT NULL,
elvish_welcome CHARACTER VARYING DEFAULT 'Mae govannen',
PRIMARY KEY (greeting_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const greetingColumn = result.tables[0].columns.find(
(c) => c.name === 'elvish_welcome'
);
expect(greetingColumn?.default).toBe("'Mae govannen'");
});
});
describe('Numeric Default Values', () => {
it('should parse integer defaults', async () => {
const sql = `
CREATE TABLE dragon_hoards (
hoard_id INTEGER NOT NULL,
gold_pieces INTEGER DEFAULT 0,
max_treasure_value INTEGER DEFAULT 10000,
PRIMARY KEY (hoard_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const goldColumn = result.tables[0].columns.find(
(c) => c.name === 'gold_pieces'
);
expect(goldColumn?.default).toBe('0');
const treasureColumn = result.tables[0].columns.find(
(c) => c.name === 'max_treasure_value'
);
expect(treasureColumn?.default).toBe('10000');
});
it('should parse decimal defaults', async () => {
const sql = `
CREATE TABLE enchanted_items (
item_id INTEGER NOT NULL,
market_price DECIMAL(10, 2) DEFAULT 99.99,
magic_power_rating NUMERIC DEFAULT 0.85,
PRIMARY KEY (item_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const priceColumn = result.tables[0].columns.find(
(c) => c.name === 'market_price'
);
expect(priceColumn?.default).toBe('99.99');
const powerColumn = result.tables[0].columns.find(
(c) => c.name === 'magic_power_rating'
);
expect(powerColumn?.default).toBe('0.85');
});
});
describe('Boolean Default Values', () => {
it('should parse boolean defaults', async () => {
const sql = `
CREATE TABLE magical_artifacts (
artifact_id INTEGER NOT NULL,
is_cursed BOOLEAN DEFAULT TRUE,
is_destroyed BOOLEAN DEFAULT FALSE,
is_legendary BOOLEAN DEFAULT '1',
is_identified BOOLEAN DEFAULT '0',
PRIMARY KEY (artifact_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const cursedColumn = result.tables[0].columns.find(
(c) => c.name === 'is_cursed'
);
expect(cursedColumn?.default).toBe('TRUE');
const destroyedColumn = result.tables[0].columns.find(
(c) => c.name === 'is_destroyed'
);
expect(destroyedColumn?.default).toBe('FALSE');
const legendaryColumn = result.tables[0].columns.find(
(c) => c.name === 'is_legendary'
);
expect(legendaryColumn?.default).toBe("'1'");
const identifiedColumn = result.tables[0].columns.find(
(c) => c.name === 'is_identified'
);
expect(identifiedColumn?.default).toBe("'0'");
});
});
describe('NULL Default Values', () => {
it('should parse NULL defaults', async () => {
const sql = `
CREATE TABLE wizard_familiars (
familiar_id INTEGER NOT NULL,
special_ability CHARACTER VARYING DEFAULT NULL,
PRIMARY KEY (familiar_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const abilityColumn = result.tables[0].columns.find(
(c) => c.name === 'special_ability'
);
expect(abilityColumn?.default).toBe('NULL');
});
});
describe('Function Default Values', () => {
it('should parse function defaults', async () => {
const sql = `
CREATE TABLE quest_logs (
quest_id UUID DEFAULT gen_random_uuid(),
quest_started TIMESTAMP DEFAULT NOW(),
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
difficulty_roll INTEGER DEFAULT random()
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const questIdColumn = result.tables[0].columns.find(
(c) => c.name === 'quest_id'
);
expect(questIdColumn?.default).toBe('GEN_RANDOM_UUID()');
const startedColumn = result.tables[0].columns.find(
(c) => c.name === 'quest_started'
);
expect(startedColumn?.default).toBe('NOW()');
const updatedColumn = result.tables[0].columns.find(
(c) => c.name === 'last_updated'
);
expect(updatedColumn?.default).toBe('CURRENT_TIMESTAMP');
const difficultyColumn = result.tables[0].columns.find(
(c) => c.name === 'difficulty_roll'
);
expect(difficultyColumn?.default).toBe('RANDOM()');
});
});
describe('Complex Real-World Example', () => {
it('should handle a complex guild management table correctly', async () => {
const sql = `
CREATE TABLE "realm"(
"realm_id" integer NOT NULL
);
CREATE TABLE "guild"(
"guild_id" CHARACTER VARYING NOT NULL UNIQUE,
PRIMARY KEY ("guild_id")
);
CREATE TABLE "guild_schedule"(
"schedule_id" CHARACTER VARYING NOT NULL UNIQUE,
PRIMARY KEY ("schedule_id")
);
CREATE TABLE "guild_quests"(
"is_active" CHARACTER VARYING NOT NULL DEFAULT 'active',
"quest_description" CHARACTER VARYING,
"quest_type" CHARACTER VARYING,
"quest_status" CHARACTER VARYING DEFAULT 'pending',
"quest_id" CHARACTER VARYING NOT NULL UNIQUE,
"reward_gold" CHARACTER VARYING,
"quest_giver" CHARACTER VARYING,
"party_size" CHARACTER VARYING,
"difficulty_level" CHARACTER VARYING,
"monster_type" CHARACTER VARYING,
"dungeon_location" CHARACTER VARYING,
"main_guild_ref" CHARACTER VARYING NOT NULL,
"schedule_ref" CHARACTER VARYING,
"last_attempt" CHARACTER VARYING,
"max_attempts" INTEGER,
"failed_attempts" INTEGER,
"party_members" INTEGER,
"loot_distributor" CHARACTER VARYING,
"quest_validator" CHARACTER VARYING,
"scout_report" CHARACTER VARYING,
"completion_xp" INTEGER,
"bonus_xp" INTEGER,
"map_coordinates" CHARACTER VARYING,
"quest_correlation" CHARACTER VARYING,
"is_completed" BOOLEAN NOT NULL DEFAULT '0',
"reward_items" CHARACTER VARYING,
"quest_priority" INTEGER,
"started_at" CHARACTER VARYING,
"status" CHARACTER VARYING,
"completed_at" CHARACTER VARYING,
"party_level" INTEGER,
"quest_master" CHARACTER VARYING,
PRIMARY KEY ("quest_id"),
FOREIGN KEY ("main_guild_ref") REFERENCES "guild"("guild_id"),
FOREIGN KEY ("schedule_ref") REFERENCES "guild_schedule"("schedule_id")
);
`;
const result = await fromPostgres(sql);
// Find the guild_quests table
const questTable = result.tables.find(
(t) => t.name === 'guild_quests'
);
expect(questTable).toBeDefined();
// Check specific default values
const activeColumn = questTable?.columns.find(
(c) => c.name === 'is_active'
);
expect(activeColumn?.default).toBe("'active'");
const statusColumn = questTable?.columns.find(
(c) => c.name === 'quest_status'
);
expect(statusColumn?.default).toBe("'pending'");
const completedColumn = questTable?.columns.find(
(c) => c.name === 'is_completed'
);
expect(completedColumn?.default).toBe("'0'");
});
});
describe('ALTER TABLE ADD COLUMN with defaults', () => {
it('should handle ALTER TABLE ADD COLUMN with default values', async () => {
const sql = `
CREATE TABLE adventurers (
adventurer_id INTEGER NOT NULL,
PRIMARY KEY (adventurer_id)
);
ALTER TABLE adventurers ADD COLUMN class_type VARCHAR(50) DEFAULT 'warrior';
ALTER TABLE adventurers ADD COLUMN experience_points INTEGER DEFAULT 0;
ALTER TABLE adventurers ADD COLUMN is_guild_member BOOLEAN DEFAULT TRUE;
ALTER TABLE adventurers ADD COLUMN joined_at TIMESTAMP DEFAULT NOW();
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const classColumn = result.tables[0].columns.find(
(c) => c.name === 'class_type'
);
expect(classColumn?.default).toBe("'warrior'");
const xpColumn = result.tables[0].columns.find(
(c) => c.name === 'experience_points'
);
expect(xpColumn?.default).toBe('0');
const guildColumn = result.tables[0].columns.find(
(c) => c.name === 'is_guild_member'
);
expect(guildColumn?.default).toBe('TRUE');
const joinedColumn = result.tables[0].columns.find(
(c) => c.name === 'joined_at'
);
expect(joinedColumn?.default).toBe('NOW()');
});
});
describe('Edge Cases and Special Characters', () => {
it('should handle defaults with parentheses in strings', async () => {
const sql = `
CREATE TABLE spell_formulas (
formula_id INTEGER NOT NULL,
damage_calculation VARCHAR DEFAULT '(strength + magic) * 2',
mana_cost TEXT DEFAULT 'cast(level * 10 - wisdom)',
PRIMARY KEY (formula_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const damageColumn = result.tables[0].columns.find(
(c) => c.name === 'damage_calculation'
);
expect(damageColumn?.default).toBe("'(strength + magic) * 2'");
const manaColumn = result.tables[0].columns.find(
(c) => c.name === 'mana_cost'
);
expect(manaColumn?.default).toBe("'cast(level * 10 - wisdom)'");
});
it('should handle defaults with JSON strings', async () => {
const sql = `
CREATE TABLE item_enchantments (
enchantment_id INTEGER NOT NULL,
properties JSON DEFAULT '{"element": "fire"}',
modifiers JSONB DEFAULT '[]',
PRIMARY KEY (enchantment_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const propertiesColumn = result.tables[0].columns.find(
(c) => c.name === 'properties'
);
expect(propertiesColumn?.default).toBe(`'{"element": "fire"}'`);
const modifiersColumn = result.tables[0].columns.find(
(c) => c.name === 'modifiers'
);
expect(modifiersColumn?.default).toBe("'[]'");
});
it('should handle casting in defaults', async () => {
const sql = `
CREATE TABLE ancient_runes (
rune_id INTEGER NOT NULL,
rune_type VARCHAR DEFAULT 'healing'::text,
PRIMARY KEY (rune_id)
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const runeColumn = result.tables[0].columns.find(
(c) => c.name === 'rune_type'
);
expect(runeColumn?.default).toBe("'healing'::text");
});
});
describe('Serial Types', () => {
it('should not set default for SERIAL types as they auto-increment', async () => {
const sql = `
CREATE TABLE monster_spawns (
spawn_id SERIAL PRIMARY KEY,
minion_id SMALLSERIAL,
boss_id BIGSERIAL
);
`;
const result = await fromPostgres(sql);
expect(result.tables).toHaveLength(1);
const spawnColumn = result.tables[0].columns.find(
(c) => c.name === 'spawn_id'
);
expect(spawnColumn?.default).toBeUndefined();
expect(spawnColumn?.increment).toBe(true);
const minionColumn = result.tables[0].columns.find(
(c) => c.name === 'minion_id'
);
expect(minionColumn?.default).toBeUndefined();
expect(minionColumn?.increment).toBe(true);
const bossColumn = result.tables[0].columns.find(
(c) => c.name === 'boss_id'
);
expect(bossColumn?.default).toBeUndefined();
expect(bossColumn?.increment).toBe(true);
});
});
});

View File

@@ -0,0 +1,350 @@
import { describe, it, expect } from 'vitest';
import { fromPostgres } from '../postgresql';
describe('PostgreSQL Import - Quoted Identifiers with Special Characters', () => {
describe('CREATE TABLE with quoted identifiers', () => {
it('should handle tables with quoted schema and table names', async () => {
const sql = `
CREATE TABLE "my-schema"."user-profiles" (
id serial PRIMARY KEY,
name text NOT NULL
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('my-schema');
expect(table.name).toBe('user-profiles');
});
it('should handle tables with spaces in schema and table names', async () => {
const sql = `
CREATE TABLE "user schema"."profile table" (
"user id" integer PRIMARY KEY,
"full name" varchar(255)
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('user schema');
expect(table.name).toBe('profile table');
expect(table.columns).toBeDefined();
expect(table.columns.length).toBeGreaterThan(0);
// Note: Column names with spaces might be parsed differently
});
it('should handle mixed quoted and unquoted identifiers', async () => {
const sql = `
CREATE TABLE "special-schema".users (
id serial PRIMARY KEY
);
CREATE TABLE public."special-table" (
id serial PRIMARY KEY
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.tables[0].schema).toBe('special-schema');
expect(result.tables[0].name).toBe('users');
expect(result.tables[1].schema).toBe('public');
expect(result.tables[1].name).toBe('special-table');
});
it('should handle tables with dots in names', async () => {
const sql = `
CREATE TABLE "schema.with.dots"."table.with.dots" (
id serial PRIMARY KEY,
data text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('schema.with.dots');
expect(table.name).toBe('table.with.dots');
});
});
describe('FOREIGN KEY with quoted identifiers', () => {
it('should handle inline REFERENCES with quoted identifiers', async () => {
const sql = `
CREATE TABLE "auth-schema"."users" (
"user-id" serial PRIMARY KEY,
email text UNIQUE
);
CREATE TABLE "app-schema"."user-profiles" (
id serial PRIMARY KEY,
"user-id" integer REFERENCES "auth-schema"."users"("user-id"),
bio text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.sourceTable).toBe('user-profiles');
expect(relationship.targetTable).toBe('users');
expect(relationship.sourceColumn).toBe('user-id');
expect(relationship.targetColumn).toBe('user-id');
});
it('should handle FOREIGN KEY constraints with quoted identifiers', async () => {
const sql = `
CREATE TABLE "schema one"."table one" (
"id field" serial PRIMARY KEY,
"data field" text
);
CREATE TABLE "schema two"."table two" (
id serial PRIMARY KEY,
"ref id" integer,
FOREIGN KEY ("ref id") REFERENCES "schema one"."table one"("id field")
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.sourceTable).toBe('table two');
expect(relationship.targetTable).toBe('table one');
expect(relationship.sourceColumn).toBe('ref id');
expect(relationship.targetColumn).toBe('id field');
});
it('should handle named constraints with quoted identifiers', async () => {
const sql = `
CREATE TABLE "auth"."users" (
id serial PRIMARY KEY
);
CREATE TABLE "app"."profiles" (
id serial PRIMARY KEY,
user_id integer,
CONSTRAINT "fk-user-profile" FOREIGN KEY (user_id) REFERENCES "auth"."users"(id)
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
// Note: Constraint names with special characters might be normalized
expect(relationship.name).toBeDefined();
});
it('should handle ALTER TABLE ADD CONSTRAINT with quoted identifiers', async () => {
const sql = `
CREATE TABLE "user-schema"."user-accounts" (
"account-id" serial PRIMARY KEY,
username text
);
CREATE TABLE "order-schema"."user-orders" (
"order-id" serial PRIMARY KEY,
"account-id" integer
);
ALTER TABLE "order-schema"."user-orders"
ADD CONSTRAINT "fk_orders_accounts"
FOREIGN KEY ("account-id")
REFERENCES "user-schema"."user-accounts"("account-id");
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(2);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.name).toBe('fk_orders_accounts');
expect(relationship.sourceTable).toBe('user-orders');
expect(relationship.targetTable).toBe('user-accounts');
expect(relationship.sourceColumn).toBe('account-id');
expect(relationship.targetColumn).toBe('account-id');
});
it('should handle complex mixed quoting scenarios', async () => {
const sql = `
CREATE TABLE auth.users (
id serial PRIMARY KEY
);
CREATE TABLE "app-data"."user_profiles" (
profile_id serial PRIMARY KEY,
"user-id" integer REFERENCES auth.users(id)
);
CREATE TABLE "app-data".posts (
id serial PRIMARY KEY,
profile_id integer
);
ALTER TABLE "app-data".posts
ADD CONSTRAINT fk_posts_profiles
FOREIGN KEY (profile_id)
REFERENCES "app-data"."user_profiles"(profile_id);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(3);
expect(result.relationships).toHaveLength(2);
// Verify the relationships were correctly identified
const profilesTable = result.tables.find(
(t) => t.name === 'user_profiles'
);
expect(profilesTable?.schema).toBe('app-data');
const postsTable = result.tables.find((t) => t.name === 'posts');
expect(postsTable?.schema).toBe('app-data');
});
});
describe('Edge cases and special scenarios', () => {
it('should handle Unicode characters in quoted identifiers', async () => {
const sql = `
CREATE TABLE "схема"."таблица" (
"идентификатор" serial PRIMARY KEY,
"данные" text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('схема');
expect(table.name).toBe('таблица');
expect(table.columns).toBeDefined();
expect(table.columns.length).toBeGreaterThan(0);
});
it('should handle parentheses in quoted identifiers', async () => {
const sql = `
CREATE TABLE "schema(prod)"."users(archived)" (
id serial PRIMARY KEY,
data text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('schema(prod)');
expect(table.name).toBe('users(archived)');
});
it('should handle forward slashes in quoted identifiers', async () => {
const sql = `
CREATE TABLE "api/v1"."users/profiles" (
id serial PRIMARY KEY
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('api/v1');
expect(table.name).toBe('users/profiles');
});
it('should handle IF NOT EXISTS with quoted identifiers', async () => {
const sql = `
CREATE TABLE IF NOT EXISTS "test-schema"."test-table" (
id serial PRIMARY KEY
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('test-schema');
expect(table.name).toBe('test-table');
});
it('should handle ONLY keyword with quoted identifiers', async () => {
const sql = `
CREATE TABLE ONLY "parent-schema"."parent-table" (
id serial PRIMARY KEY
);
ALTER TABLE ONLY "parent-schema"."parent-table"
ADD CONSTRAINT "unique-constraint" UNIQUE (id);
`;
const result = await fromPostgres(sql);
// ONLY keyword might trigger warnings
expect(result.warnings).toBeDefined();
expect(result.tables).toHaveLength(1);
const table = result.tables[0];
expect(table.schema).toBe('parent-schema');
expect(table.name).toBe('parent-table');
});
it('should handle self-referencing foreign keys with quoted identifiers', async () => {
const sql = `
CREATE TABLE "org-schema"."departments" (
"dept-id" serial PRIMARY KEY,
"parent-dept-id" integer REFERENCES "org-schema"."departments"("dept-id"),
name text
);
`;
const result = await fromPostgres(sql);
expect(result.warnings || []).toHaveLength(0);
expect(result.tables).toHaveLength(1);
expect(result.relationships).toHaveLength(1);
const relationship = result.relationships[0];
expect(relationship.sourceTable).toBe('departments');
expect(relationship.targetTable).toBe('departments'); // Self-reference
expect(relationship.sourceColumn).toBe('parent-dept-id');
expect(relationship.targetColumn).toBe('dept-id');
});
});
});

View File

@@ -91,7 +91,38 @@ export interface AlterTableExprItem {
action: string;
resource?: string;
type?: string;
keyword?: string;
constraint?: { constraint_type?: string };
// Properties for ADD COLUMN
column?:
| {
column?:
| {
expr?: {
value?: string;
};
}
| string;
}
| string
| ColumnReference;
definition?: {
dataType?: string;
length?: number;
precision?: number;
scale?: number;
suffix?: unknown[];
nullable?: { type: string };
unique?: string;
primary_key?: string;
constraint?: string;
default_val?: unknown;
auto_increment?: string;
};
nullable?: { type: string; value?: string };
unique?: string;
default_val?: unknown;
// Properties for constraints
create_definitions?:
| AlterTableConstraintDefinition
| {
@@ -203,11 +234,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 +261,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}`
);

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