Compare commits

...

91 Commits

Author SHA1 Message Date
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
Guy Ben-Aharon
8ffde62c1a fix(readonly): fix zoom out on readonly (#818) 2025-07-31 15:27:38 +03:00
Guy Ben-Aharon
39247b77a2 feat: enhance primary key and unique field handling logic (#817) 2025-07-31 11:38:33 +03:00
Guy Ben-Aharon
984b2aeee2 fix(ui): reduce spacing between primary key icon and short field types (#816) 2025-07-31 11:04:48 +03:00
Guy Ben-Aharon
eed104be5b fix(dbml): fix dbml output format (#815) 2025-07-30 14:31:56 +03:00
Guy Ben-Aharon
00bd535b3c fix(dbml import): fix dbml import types + schemas (#808)
* fix(dbml import): fix dbml import types + schemas

* fix(dbml import): fix dbml import types + schemas

* fix(dbml import): fix dbml import types + schemas

* fix
2025-07-29 17:55:29 +03:00
Guy Ben-Aharon
18e914242f fix(dbml export): fix handle tables with same name under different schemas (#807) 2025-07-29 16:22:09 +03:00
Guy Ben-Aharon
e68837a34a fix(dbml export): handle tables with same name under different schemas (#806) 2025-07-29 14:59:08 +03:00
Guy Ben-Aharon
b30162d98b fix: clone of custom types (#804) 2025-07-29 12:49:28 +03:00
Guy Ben-Aharon
dba372d25a fix(cockroachdb): support schema creation for cockroachdb (#803) 2025-07-28 18:55:05 +03:00
Jonathan Fishner
2eb48e75d3 fix(i18n): add Croatian (hr) language support (#802)
* feat: add Croatian (hr) language support

* fix translation

* fix: change langs order

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-28 18:12:17 +03:00
Guy Ben-Aharon
867903cd5f feat(schema): support create new schema (#801)
* feat(schema): support create new schema

* fix

* fix
2025-07-28 17:32:28 +03:00
Guy Ben-Aharon
8aeb1df0ad fix: fix screen freeze after schema select (#800)
* fix: fix screen freeze after schema select

* fix: fix screen freeze after schema select
2025-07-28 12:00:34 +03:00
Guy Ben-Aharon
6bea827293 fix(canvas filter): improve scroller on canvas filter (#799) 2025-07-28 11:48:45 +03:00
Guy Ben-Aharon
a119854da7 fix(dbml actions): set dbml tooltips side (#798) 2025-07-28 10:22:35 +03:00
Jonathan Fishner
bfbfd7b843 fix(dbml editor): move tooltips button to be on the right (#797) 2025-07-27 23:11:34 +03:00
Guy Ben-Aharon
0ca7008735 fix(dbml field comments): support export field comments in dbml (#796)
* fix(dbml field comments): support export field comments in dbml

* add tests
2025-07-27 20:53:55 +03:00
Guy Ben-Aharon
4bc71c52ff fix(scroll): disable scroll x behavior (#795) 2025-07-27 20:15:04 +03:00
Guy Ben-Aharon
8f27f10dec fix(dbml): support spaces in names (#794) 2025-07-27 19:44:43 +03:00
Guy Ben-Aharon
a93ec2cab9 fix: lost in canvas button animation (#793) 2025-07-27 17:34:48 +03:00
Jonathan Fishner
386e40a0bf fix: update MariaDB database import smart query (#792) 2025-07-27 16:29:16 +03:00
Jonathan Fishner
bda150d4b6 feat: add floating "Show All" button when tables are out of view (#787)
* feat: add floating "Show All" button when tables are out of view

* fix view of show all for mobile

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-27 16:25:14 +03:00
Guy Ben-Aharon
87836e53d1 fix: remove unnecessary import (#791) 2025-07-27 12:29:19 +03:00
Jonathan Fishner
7e0483f1a5 feat(custom-types): add highlight fields option for custom types (#726)
* feat(custom-types): add highlight feilds option for custom types

* fix(custom-types): show indicator when custom type is in used

* feat(canvas): add enum highlight indicator with pulse animation and double-click to clear

* some fixes

* some fixes

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-27 12:17:57 +03:00
Jonathan Fishner
309ee9cb0f fix(dbml-export): merge field attributes into single brackets and fix schema syntax (#790)
* fix(dbml-export): merge field attributes into single brackets and fix schema syntax

* fix build
2025-07-26 22:03:02 +03:00
Jonathan Fishner
79b885502e fix(sql-server): improvment for sql-server import via sql script (#789)
* feat: improvment for sql-server import via sql script

* fix for test

* some fixes

* some fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-25 19:16:35 +03:00
Guy Ben-Aharon
745bdee86d fix(table-node): fix comment icon on field (#786) 2025-07-24 17:27:23 +03:00
Guy Ben-Aharon
08eb9cc55f fix(table-node): improve field spacing (#785) 2025-07-24 16:41:45 +03:00
Jonathan Fishner
778f85d492 feat(datatypes): Add decimal / numeric attribute support + organize field row (#715)
* added decimal scale and precision support

* update i18n

* added button to reset - made values always enabled in pairs

* made button use ml

* added fix for when manually defined scales are set to 0

* fix

* some fixes

* some fixes

* some fixes

* some fixes

* some fixes

---------

Co-authored-by: Alexander Harris <mcalapurge@techie.com>
Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-24 15:18:33 +03:00
Guy Ben-Aharon
fb92be7d3e alignment sql export scripts (#784) 2025-07-23 21:00:52 +03:00
Jonathan Fishner
6df588f40e fix: improve SQL export formatting and add schema-aware FK grouping (#783)
* fix: correct foreign key direction based on relationship cardinality in SQL exports

* fix: improve SQL export formatting and add schema-aware FK grouping

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-23 18:05:59 +03:00
Guy Ben-Aharon
b46ed58dff fix(table-select): add loading indication for import (#782) 2025-07-23 15:53:26 +03:00
Jonathan Fishner
0d9f57a9c9 feat: add table selection for large database imports (#776)
* feat: add table selection UI for large database imports (>50 tables)

* some changes

* some changes

* some changes

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-23 11:35:27 +03:00
Guy Ben-Aharon
b7dbe54c83 fix(canvas): fix filter eye button (#780) 2025-07-21 19:01:52 +03:00
Guy Ben-Aharon
43d1dfff71 fix: fix hotkeys on form elements (#778) 2025-07-21 17:31:42 +03:00
Guy Ben-Aharon
9949a46ee3 fix: set focus on filter search (#775) 2025-07-21 16:18:18 +03:00
Guy Ben-Aharon
dfbcf05b2f feat(canvas): Add filter tables on canvas (#774)
* feat(canvas): filter tables on canvas

* fix build

* fix

* fix
2025-07-21 15:54:27 +03:00
Jonathan Fishner
f56fab9876 fix: update multiple schemas toast to require user action (#771)
* fix: update multiple schemas toast to require user action

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-21 12:43:57 +03:00
Jonathan Fishner
c9ea7da092 feat(default value): add default value option to table field settings (#770)
* feat: add default value option to table field settings

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-21 12:29:53 +03:00
Jonathan Fishner
22d46e1e90 fix(dbml-import): handle unsupported DBML features and add comprehensive tests (#766)
* fix(dbml-import): handle unsupported DBML features and add comprehensive tests

* fix build

* fix(dbml-export): handle composite primary keys, invalid defaults, and char type formatting

* fix build

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-21 11:33:34 +03:00
Guy Ben-Aharon
6af94afc56 fix(area): redo/undo after dragging an area with tables (#767) 2025-07-17 16:49:05 +03:00
Jonathan Fishner
f7f92903de fix(sql-export): escape newlines and quotes in multi-line comments (#765) 2025-07-16 15:18:52 +03:00
Jonathan Fishner
b35e17526b feat: implement area grouping with parent-child relationships (#762)
* feat: implement area grouping with parent-child relationships

* fix: improve area node visual appearance and text visibility

* update area header color

* fix build

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-16 15:11:16 +03:00
Guy Ben-Aharon
bf32c08d37 fix: remove error lag after autofix (#764) 2025-07-14 21:27:05 +03:00
Jonathan Fishner
5d337409d6 fix: add PostgreSQL tests and fix parsing SQL (#760)
* feat: add PostgreSQL tests and fix parsing SQL

* fix: disable format on paste for SQL DDL import

* some ui fixes + add tests to the ci

* fix

* fix validator and importer

* fix for maria-db

* fix

* remove improved

* fix

* fix

* fix

* fix for test

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-14 19:25:44 +03:00
Guy Ben-Aharon
67f5ac303e fix: add open and create diagram to side menu (#757) 2025-07-08 13:28:44 +03:00
238 changed files with 34977 additions and 6017 deletions

View File

@@ -25,3 +25,6 @@ jobs:
- name: Build - name: Build
run: npm run build run: npm run build
- name: Run tests
run: npm run test:ci

View File

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

View File

@@ -1,5 +1,109 @@
# Changelog # Changelog
## [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) ## [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" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="max-image-preview:large" /> <meta name="robots" content="noindex, max-image-preview:large" />
<title>ChartDB - Create & Visualize Database Schema Diagrams</title> <title>ChartDB - Create & Visualize Database Schema Diagrams</title>
<link rel="canonical" href="https://chartdb.io" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link
@@ -15,14 +16,19 @@
<script src="/config.js"></script> <script src="/config.js"></script>
<script> <script>
// Load analytics only if not disabled // Load analytics only if not disabled
(function() { (function () {
const disableAnalytics = (window.env && window.env.DISABLE_ANALYTICS === 'true') || const disableAnalytics =
(typeof process !== 'undefined' && process.env && process.env.VITE_DISABLE_ANALYTICS === 'true'); (window.env && window.env.DISABLE_ANALYTICS === 'true') ||
(typeof process !== 'undefined' &&
process.env &&
process.env.VITE_DISABLE_ANALYTICS === 'true');
if (!disableAnalytics) { if (!disableAnalytics) {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = 'https://cdn.usefathom.com/script.js'; script.src = 'https://cdn.usefathom.com/script.js';
script.setAttribute('data-site', 'PRHIVBNN'); script.setAttribute('data-site', 'PRHIVBNN');
script.setAttribute('data-canonical', 'false');
script.setAttribute('data-spa', 'auto');
script.defer = true; script.defer = true;
document.head.appendChild(script); document.head.appendChild(script);
} }

1602
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "chartdb", "name": "chartdb",
"private": true, "private": true,
"version": "1.13.2", "version": "1.15.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -9,7 +9,11 @@
"lint": "eslint . --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"lint:fix": "npm run lint -- --fix", "lint:fix": "npm run lint -- --fix",
"preview": "vite preview", "preview": "vite preview",
"prepare": "husky" "prepare": "husky",
"test": "vitest",
"test:ci": "vitest run --reporter=verbose --bail=1",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/openai": "^0.0.51", "@ai-sdk/openai": "^0.0.51",
@@ -22,24 +26,24 @@
"@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0", "@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1", "@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1", "@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1", "@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "1.2.0", "@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.2.7",
"@uidotdev/usehooks": "^2.4.1", "@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.3.1", "@xyflow/react": "^12.8.2",
"ahooks": "^3.8.1", "ahooks": "^3.8.1",
"ai": "^3.3.14", "ai": "^3.3.14",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -50,8 +54,9 @@
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"i18next": "^23.14.0", "i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.441.0", "lucide-react": "^0.525.0",
"monaco-editor": "^0.52.0", "monaco-editor": "^0.52.0",
"motion": "^12.23.6",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"node-sql-parser": "^5.3.2", "node-sql-parser": "^5.3.2",
"react": "^18.3.1", "react": "^18.3.1",
@@ -73,12 +78,16 @@
"@eslint/compat": "^1.2.4", "@eslint/compat": "^1.2.4",
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.16.0", "@eslint/js": "^9.16.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.1.0", "@types/node": "^22.1.0",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.18.0", "@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0", "@typescript-eslint/parser": "^8.18.0",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.16.0", "eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@@ -90,6 +99,7 @@
"eslint-plugin-react-refresh": "^0.4.7", "eslint-plugin-react-refresh": "^0.4.7",
"eslint-plugin-tailwindcss": "^3.17.4", "eslint-plugin-tailwindcss": "^3.17.4",
"globals": "^15.13.0", "globals": "^15.13.0",
"happy-dom": "^18.0.1",
"husky": "^9.1.5", "husky": "^9.1.5",
"postcss": "^8.4.40", "postcss": "^8.4.40",
"prettier": "^3.3.3", "prettier": "^3.3.3",
@@ -97,6 +107,7 @@
"tailwindcss": "^3.4.7", "tailwindcss": "^3.4.7",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"unplugin-inject-preload": "^3.0.0", "unplugin-inject-preload": "^3.0.0",
"vite": "^5.3.4" "vite": "^5.3.4",
"vitest": "^3.2.4"
} }
} }

View File

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

View File

@@ -1,7 +1,7 @@
import { cva } from 'class-variance-authority'; import { cva } from 'class-variance-authority';
export const buttonVariants = cva( export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{ {
variants: { variants: {
variant: { variant: {

View File

@@ -0,0 +1,112 @@
import React from 'react';
import { ChevronDownIcon } from '@radix-ui/react-icons';
import { Slot } from '@radix-ui/react-slot';
import { type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { buttonVariants } from './button-variants';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
export interface ButtonWithAlternativesProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
alternatives: Array<{
label: string;
onClick: () => void;
disabled?: boolean;
icon?: React.ReactNode;
className?: string;
}>;
dropdownTriggerClassName?: string;
chevronDownIconClassName?: string;
}
const ButtonWithAlternatives = React.forwardRef<
HTMLButtonElement,
ButtonWithAlternativesProps
>(
(
{
className,
variant,
size,
asChild = false,
alternatives,
children,
onClick,
dropdownTriggerClassName,
chevronDownIconClassName,
...props
},
ref
) => {
const Comp = asChild ? Slot : 'button';
const hasAlternatives = (alternatives?.length ?? 0) > 0;
return (
<div className="inline-flex items-stretch">
<Comp
className={cn(
buttonVariants({ variant, size }),
{ 'rounded-r-none': hasAlternatives },
className
)}
ref={ref}
onClick={onClick}
{...props}
>
{children}
</Comp>
{hasAlternatives ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className={cn(
buttonVariants({ variant, size }),
'rounded-l-none border-l border-l-primary/5 px-2 min-w-0',
className?.includes('h-') &&
className.match(/h-\d+/)?.[0],
className?.includes('text-') &&
className.match(/text-\w+/)?.[0],
dropdownTriggerClassName
)}
type="button"
>
<ChevronDownIcon
className={cn(
'size-4 shrink-0',
chevronDownIconClassName
)}
/>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{alternatives.map((alternative, index) => (
<DropdownMenuItem
key={index}
onClick={alternative.onClick}
disabled={alternative.disabled}
className={cn(alternative.className)}
>
<span className="flex w-full items-center justify-between gap-2">
{alternative.label}
{alternative.icon}
</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : null}
</div>
);
}
);
ButtonWithAlternatives.displayName = 'ButtonWithAlternatives';
export { ButtonWithAlternatives };

View File

@@ -31,6 +31,7 @@ export interface CodeSnippetAction {
label: string; label: string;
icon: LucideIcon; icon: LucideIcon;
onClick: () => void; onClick: () => void;
className?: string;
} }
export interface CodeSnippetProps { export interface CodeSnippetProps {
@@ -43,6 +44,8 @@ export interface CodeSnippetProps {
isComplete?: boolean; isComplete?: boolean;
editorProps?: React.ComponentProps<EditorType>; editorProps?: React.ComponentProps<EditorType>;
actions?: CodeSnippetAction[]; actions?: CodeSnippetAction[];
actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left';
allowCopy?: boolean;
} }
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
@@ -56,6 +59,8 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
isComplete = true, isComplete = true,
editorProps, editorProps,
actions, actions,
actionsTooltipSide,
allowCopy = true,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const monaco = useMonaco(); const monaco = useMonaco();
@@ -129,6 +134,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
<Suspense fallback={<Spinner />}> <Suspense fallback={<Spinner />}>
{isComplete ? ( {isComplete ? (
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1"> <div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
{allowCopy ? (
<Tooltip <Tooltip
onOpenChange={setTooltipOpen} onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen} open={isCopied || tooltipOpen}
@@ -148,7 +154,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
</Button> </Button>
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent
side={actionsTooltipSide}
>
{t( {t(
isCopied isCopied
? 'copied' ? 'copied'
@@ -156,6 +164,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
)} )}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
) : null}
{actions && {actions &&
actions.length > 0 && actions.length > 0 &&
@@ -164,7 +173,10 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span> <span>
<Button <Button
className="h-fit p-1.5" className={cn(
'h-fit p-1.5',
action.className
)}
variant="outline" variant="outline"
onClick={action.onClick} onClick={action.onClick}
> >
@@ -174,7 +186,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
</Button> </Button>
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent
side={actionsTooltipSide}
>
{action.label} {action.label}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

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,18 +37,28 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
const datatypePattern = dataTypesNames.join('|'); const datatypePattern = dataTypesNames.join('|');
monaco.languages.setMonarchTokensProvider('dbml', { monaco.languages.setMonarchTokensProvider('dbml', {
keywords: ['Table', 'Ref', 'Indexes'], keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
datatypes: dataTypesNames, datatypes: dataTypesNames,
tokenizer: { tokenizer: {
root: [ 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'], [/\[.*?\]/, 'annotation'],
[/'''/, 'string', '@tripleQuoteString'],
[/".*?"/, 'string'], [/".*?"/, 'string'],
[/'.*?'/, 'string'], [/'.*?'/, 'string'],
[/`.*?`/, 'string'],
[/[{}]/, 'delimiter'], [/[{}]/, 'delimiter'],
[/[<>]/, 'operator'], [/[<>]/, 'operator'],
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
], ],
tripleQuoteString: [
[/[^']+/, 'string'],
[/'''/, 'string', '@pop'],
[/'/, 'string'],
],
}, },
}); });
}; };

View File

@@ -4,6 +4,7 @@ import { Cross2Icon } from '@radix-ui/react-icons';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { ScrollArea } from '../scroll-area/scroll-area'; import { ScrollArea } from '../scroll-area/scroll-area';
import { ChevronLeft } from 'lucide-react';
const Dialog = DialogPrimitive.Root; const Dialog = DialogPrimitive.Root;
@@ -32,10 +33,45 @@ const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showClose?: boolean; showClose?: boolean;
showBack?: boolean;
backButtonClassName?: string;
blurBackground?: boolean;
forceOverlay?: boolean;
onBackClick?: () => void;
} }
>(({ className, children, showClose, ...props }, ref) => ( >(
(
{
className,
children,
showClose,
showBack,
onBackClick,
backButtonClassName,
blurBackground,
forceOverlay,
...props
},
ref
) => (
<DialogPortal> <DialogPortal>
<DialogOverlay /> {forceOverlay ? (
<div
className={cn(
'fixed inset-0 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
{
'bg-black/80': !blurBackground,
'bg-black/30 backdrop-blur-sm': blurBackground,
}
)}
data-state="open"
/>
) : null}
<DialogOverlay
className={cn({
'bg-black/30 backdrop-blur-sm': blurBackground,
})}
/>
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
@@ -45,6 +81,17 @@ const DialogContent = React.forwardRef<
{...props} {...props}
> >
{children} {children}
{showBack && (
<button
onClick={() => onBackClick?.()}
className={cn(
'absolute left-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground',
backButtonClassName
)}
>
<ChevronLeft className="size-4" />
</button>
)}
{showClose && ( {showClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="size-4" /> <Cross2Icon className="size-4" />
@@ -53,7 +100,8 @@ const DialogContent = React.forwardRef<
)} )}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)); )
);
DialogContent.displayName = DialogPrimitive.Content.displayName; DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ const DialogHeader = ({

View File

@@ -52,7 +52,7 @@ export const EmptyState = forwardRef<
</Label> </Label>
<Label <Label
className={cn( className={cn(
'text-sm font-normal text-muted-foreground', 'text-sm text-center font-normal text-muted-foreground',
descriptionClassName descriptionClassName
)} )}
> >

View File

@@ -2,16 +2,13 @@ import React from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
export interface InputProps const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
return ( return (
<input <input
type={type} type={type}
className={cn( className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className className
)} )}
ref={ref} ref={ref}

View File

@@ -0,0 +1,121 @@
import React from 'react';
import { cn } from '@/lib/utils';
import type { ButtonProps } from '../button/button';
import { buttonVariants } from '../button/button-variants';
import {
ChevronLeftIcon,
ChevronRightIcon,
DotsHorizontalIcon,
} from '@radix-ui/react-icons';
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
role="navigation"
aria-label="pagination"
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn('flex flex-row items-center gap-1', className)}
{...props}
/>
));
PaginationContent.displayName = 'PaginationContent';
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn('', className)} {...props} />
));
PaginationItem.displayName = 'PaginationItem';
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, 'size'> &
React.ComponentProps<'a'>;
const PaginationLink = ({
className,
isActive,
size = 'icon',
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? 'page' : undefined}
className={cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size,
}),
className
)}
{...props}
/>
);
PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeftIcon className="size-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn('gap-1 pr-2.5', className)}
{...props}
>
<span>Next</span>
<ChevronRightIcon className="size-4" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) => (
<span
aria-hidden
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<DotsHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = 'PaginationEllipsis';
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};

View File

@@ -93,6 +93,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
(isOpen: boolean) => { (isOpen: boolean) => {
setOpen?.(isOpen); setOpen?.(isOpen);
setIsOpen(isOpen); setIsOpen(isOpen);
if (isOpen) {
setSearchTerm('');
}
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
}, },
[setOpen] [setOpen]
); );
@@ -227,7 +233,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
onSelect={() => onSelect={() =>
handleSelect( handleSelect(
option.value, option.value,
matches?.map((match) => match.toString()) matches?.map((match) => match?.toString())
) )
} }
> >

View File

@@ -29,6 +29,7 @@ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem'; const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem'; const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem'; const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_WIDTH_ICON_EXTENDED = '4rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
type SidebarContext = { type SidebarContext = {
@@ -142,6 +143,8 @@ const SidebarProvider = React.forwardRef<
{ {
'--sidebar-width': SIDEBAR_WIDTH, '--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON, '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
'--sidebar-width-icon-extended':
SIDEBAR_WIDTH_ICON_EXTENDED,
...style, ...style,
} as React.CSSProperties } as React.CSSProperties
} }
@@ -166,7 +169,7 @@ const Sidebar = React.forwardRef<
React.ComponentProps<'div'> & { React.ComponentProps<'div'> & {
side?: 'left' | 'right'; side?: 'left' | 'right';
variant?: 'sidebar' | 'floating' | 'inset'; variant?: 'sidebar' | 'floating' | 'inset';
collapsible?: 'offcanvas' | 'icon' | 'none'; collapsible?: 'offcanvas' | 'icon' | 'icon-extended' | 'none';
} }
>( >(
( (
@@ -245,8 +248,8 @@ const Sidebar = React.forwardRef<
'group-data-[collapsible=offcanvas]:w-0', 'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180', 'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset' variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]' ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]' : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended]'
)} )}
/> />
<div <div
@@ -257,8 +260,8 @@ const Sidebar = React.forwardRef<
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants. // Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset' variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]' ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l', : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended] group-data-[side=left]:border-r group-data-[side=right]:border-l',
className className
)} )}
{...props} {...props}
@@ -421,7 +424,7 @@ const SidebarContent = React.forwardRef<
ref={ref} ref={ref}
data-sidebar="content" data-sidebar="content"
className={cn( className={cn(
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden group-data-[collapsible=icon-extended]:overflow-hidden',
className className
)} )}
{...props} {...props}
@@ -461,6 +464,7 @@ const SidebarGroupLabel = React.forwardRef<
className={cn( className={cn(
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', 'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
'group-data-[collapsible=icon-extended]:-mt-8 group-data-[collapsible=icon-extended]:opacity-0',
className className
)} )}
{...props} {...props}
@@ -483,7 +487,7 @@ const SidebarGroupAction = React.forwardRef<
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', 'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile. // Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden', 'after:absolute after:-inset-2 after:md:hidden',
'group-data-[collapsible=icon]:hidden', 'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
className className
)} )}
{...props} {...props}
@@ -532,7 +536,7 @@ const SidebarMenuItem = React.forwardRef<
SidebarMenuItem.displayName = 'SidebarMenuItem'; SidebarMenuItem.displayName = 'SidebarMenuItem';
const sidebarMenuButtonVariants = cva( const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon-extended]:h-auto group-data-[collapsible=icon-extended]:flex-col group-data-[collapsible=icon-extended]:gap-1 group-data-[collapsible=icon-extended]:p-2 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate group-data-[collapsible=icon-extended]:[&>span]:w-full group-data-[collapsible=icon-extended]:[&>span]:text-center group-data-[collapsible=icon-extended]:[&>span]:text-[10px] group-data-[collapsible=icon-extended]:[&>span]:leading-tight [&>svg]:size-4 [&>svg]:shrink-0',
{ {
variants: { variants: {
variant: { variant: {
@@ -636,7 +640,7 @@ const SidebarMenuAction = React.forwardRef<
'peer-data-[size=sm]/menu-button:top-1', 'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5', 'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5', 'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden', 'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
showOnHover && showOnHover &&
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0', 'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
className className
@@ -753,7 +757,7 @@ const SidebarMenuSubButton = React.forwardRef<
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground', 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs', size === 'sm' && 'text-xs',
size === 'md' && 'text-sm', size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden', 'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
className className
)} )}
{...props} {...props}

View File

@@ -20,6 +20,7 @@ export function Toaster() {
description, description,
action, action,
layout = 'row', layout = 'row',
hideCloseButton = false,
...props ...props
}) { }) {
return ( return (
@@ -38,7 +39,7 @@ export function Toaster() {
) : null} ) : null}
</div> </div>
{layout === 'row' ? action : null} {layout === 'row' ? action : null}
<ToastClose /> {!hideCloseButton ? <ToastClose /> : null}
</Toast> </Toast>
); );
})} })}

View File

@@ -12,6 +12,7 @@ type ToasterToast = ToastProps & {
description?: React.ReactNode; description?: React.ReactNode;
action?: ToastActionElement; action?: ToastActionElement;
layout?: 'row' | 'column'; layout?: 'row' | 'column';
hideCloseButton?: boolean;
}; };
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@@ -13,15 +13,17 @@ const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
// <TooltipPrimitive.Portal>
<TooltipPrimitive.Content <TooltipPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', 'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]',
className className
)} )}
{...props} {...props}
/> />
// </TooltipPrimitive.Portal>
)); ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName; TooltipContent.displayName = TooltipPrimitive.Content.displayName;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Skeleton } from '../skeleton/skeleton';
import { cn } from '@/lib/utils';
export interface TreeItemSkeletonProps
extends React.HTMLAttributes<HTMLDivElement> {}
export const TreeItemSkeleton: React.FC<TreeItemSkeletonProps> = ({
className,
style,
}) => {
return (
<div className={cn('px-2 py-1', className)} style={style}>
<Skeleton className="h-3.5 w-full rounded-sm" />
</div>
);
};

View File

@@ -0,0 +1,461 @@
import {
ChevronRight,
File,
Folder,
Loader2,
type LucideIcon,
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '@/lib/utils';
import { Button } from '@/components/button/button';
import type {
TreeNode,
FetchChildrenFunction,
SelectableTreeProps,
} from './tree';
import type { ExpandedState } from './use-tree';
import { useTree } from './use-tree';
import type { Dispatch, ReactNode, SetStateAction } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { TreeItemSkeleton } from './tree-item-skeleton';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
interface TreeViewProps<
Type extends string,
Context extends Record<Type, unknown>,
> {
data: TreeNode<Type, Context>[];
fetchChildren?: FetchChildrenFunction<Type, Context>;
onNodeClick?: (node: TreeNode<Type, Context>) => void;
className?: string;
defaultIcon?: LucideIcon;
defaultFolderIcon?: LucideIcon;
defaultIconProps?: React.ComponentProps<LucideIcon>;
defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
selectable?: SelectableTreeProps<Type, Context>;
expanded?: ExpandedState;
setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
loadingNodeIds?: string[];
}
export function TreeView<
Type extends string,
Context extends Record<Type, unknown>,
>({
data,
fetchChildren,
onNodeClick,
className,
defaultIcon = File,
defaultFolderIcon = Folder,
defaultIconProps,
defaultFolderIconProps,
selectable,
expanded: expandedProp,
setExpanded: setExpandedProp,
renderHoverComponent,
renderActionsComponent,
loadingNodeIds,
}: TreeViewProps<Type, Context>) {
const { expanded, loading, loadedChildren, hasMoreChildren, toggleNode } =
useTree({
fetchChildren,
expanded: expandedProp,
setExpanded: setExpandedProp,
});
const [selectedIdInternal, setSelectedIdInternal] = React.useState<
string | undefined
>(selectable?.defaultSelectedId);
const selectedId = useMemo(() => {
return selectable?.selectedId ?? selectedIdInternal;
}, [selectable?.selectedId, selectedIdInternal]);
const setSelectedId = useCallback(
(value: SetStateAction<string | undefined>) => {
if (selectable?.setSelectedId) {
selectable.setSelectedId(value);
} else {
setSelectedIdInternal(value);
}
},
[selectable, setSelectedIdInternal]
);
useEffect(() => {
if (selectable?.enabled && selectable.defaultSelectedId) {
if (selectable.defaultSelectedId === selectedId) return;
setSelectedId(selectable.defaultSelectedId);
const { node, path } = findNodeById(
data,
selectable.defaultSelectedId
);
if (node) {
selectable.onSelectedChange?.(node);
// Expand all parent nodes
for (const parent of path) {
if (expanded[parent.id]) continue;
toggleNode(
parent.id,
parent.type,
parent.context,
parent.children
);
}
}
}
}, [selectable, toggleNode, selectedId, data, expanded, setSelectedId]);
const handleNodeSelect = (node: TreeNode<Type, Context>) => {
if (selectable?.enabled) {
setSelectedId(node.id);
selectable.onSelectedChange?.(node);
}
};
return (
<div className={cn('w-full', className)}>
{data.map((node, index) => (
<TreeNode
key={node.id}
node={node}
level={0}
expanded={expanded}
loading={loading}
loadedChildren={loadedChildren}
hasMoreChildren={hasMoreChildren}
onToggle={toggleNode}
onNodeClick={onNodeClick}
defaultIcon={defaultIcon}
defaultFolderIcon={defaultFolderIcon}
defaultIconProps={defaultIconProps}
defaultFolderIconProps={defaultFolderIconProps}
selectable={selectable?.enabled}
selectedId={selectedId}
onSelect={handleNodeSelect}
className={index > 0 ? 'mt-0.5' : ''}
renderHoverComponent={renderHoverComponent}
renderActionsComponent={renderActionsComponent}
loadingNodeIds={loadingNodeIds}
/>
))}
</div>
);
}
interface TreeNodeProps<
Type extends string,
Context extends Record<Type, unknown>,
> {
node: TreeNode<Type, Context>;
level: number;
expanded: Record<string, boolean>;
loading: Record<string, boolean>;
loadedChildren: Record<string, TreeNode<Type, Context>[]>;
hasMoreChildren: Record<string, boolean>;
onToggle: (
nodeId: string,
nodeType: Type,
nodeContext: Context[Type],
staticChildren?: TreeNode<Type, Context>[]
) => void;
onNodeClick?: (node: TreeNode<Type, Context>) => void;
defaultIcon: LucideIcon;
defaultFolderIcon: LucideIcon;
defaultIconProps?: React.ComponentProps<LucideIcon>;
defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
selectable?: boolean;
selectedId?: string;
onSelect: (node: TreeNode<Type, Context>) => void;
className?: string;
renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
loadingNodeIds?: string[];
}
function TreeNode<Type extends string, Context extends Record<Type, unknown>>({
node,
level,
expanded,
loading,
loadedChildren,
hasMoreChildren,
onToggle,
onNodeClick,
defaultIcon: DefaultIcon,
defaultFolderIcon: DefaultFolderIcon,
defaultIconProps,
defaultFolderIconProps,
selectable,
selectedId,
onSelect,
className,
renderHoverComponent,
renderActionsComponent,
loadingNodeIds,
}: TreeNodeProps<Type, Context>) {
const [isHovered, setIsHovered] = useState(false);
const isExpanded = expanded[node.id];
const isLoading = loading[node.id];
const children = loadedChildren[node.id] || node.children;
const isSelected = selectedId === node.id;
const IconComponent =
node.icon || (node.isFolder ? DefaultFolderIcon : DefaultIcon);
const iconProps: React.ComponentProps<LucideIcon> = {
strokeWidth: isSelected ? 2.5 : 2,
...(node.isFolder ? defaultFolderIconProps : defaultIconProps),
...node.iconProps,
className: cn(
'h-3.5 w-3.5 text-muted-foreground flex-none',
isSelected && 'text-primary text-white',
node.iconProps?.className
),
};
return (
<div className={cn(className)}>
<div
className={cn(
'flex items-center gap-1.5 px-2 py-1 rounded-lg cursor-pointer group h-6',
'transition-colors duration-200',
isSelected
? 'bg-sky-500 border border-sky-600 border dark:bg-sky-600 dark:border-sky-700'
: 'hover:bg-gray-200/50 border border-transparent dark:hover:bg-gray-700/50',
node.className
)}
{...(isSelected ? { 'data-selected': true } : {})}
style={{ paddingLeft: `${level * 16 + 8}px` }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={(e) => {
e.stopPropagation();
if (selectable && !node.unselectable) {
onSelect(node);
}
// if (node.isFolder) {
// onToggle(node.id, node.children);
// }
// called only once in case of double click
if (e.detail !== 2) {
onNodeClick?.(node);
}
}}
onDoubleClick={(e) => {
e.stopPropagation();
if (node.isFolder) {
onToggle(
node.id,
node.type,
node.context,
node.children
);
}
}}
>
<div className="flex flex-none items-center gap-1.5">
<Button
variant="ghost"
size="icon"
className={cn(
'h-3.5 w-3.5 p-0 hover:bg-transparent flex-none',
isExpanded && 'rotate-90',
'transition-transform duration-200'
)}
onClick={(e) => {
e.stopPropagation();
if (node.isFolder) {
onToggle(
node.id,
node.type,
node.context,
node.children
);
}
}}
>
{node.isFolder &&
(isLoading ? (
<Loader2
className={cn('size-3.5 animate-spin', {
'text-white': isSelected,
})}
/>
) : (
<ChevronRight
className={cn('size-3.5', {
'text-white': isSelected,
})}
strokeWidth={2}
/>
))}
</Button>
{node.tooltip ? (
<Tooltip>
<TooltipTrigger asChild>
{loadingNodeIds?.includes(node.id) ? (
<Loader2
className={cn('size-3.5 animate-spin', {
'text-white': isSelected,
})}
/>
) : (
<IconComponent
{...(isSelected
? { 'data-selected': true }
: {})}
{...iconProps}
/>
)}
</TooltipTrigger>
<TooltipContent
align="center"
className="max-w-[400px]"
>
{node.tooltip}
</TooltipContent>
</Tooltip>
) : node.empty ? null : loadingNodeIds?.includes(
node.id
) ? (
<Loader2
className={cn('size-3.5 animate-spin', {
// 'text-white': isSelected,
})}
/>
) : (
<IconComponent
{...(isSelected ? { 'data-selected': true } : {})}
{...iconProps}
/>
)}
</div>
<span
{...node.labelProps}
className={cn(
'text-xs truncate min-w-0 flex-1 w-0',
isSelected && 'font-medium text-primary text-white',
node.labelProps?.className
)}
{...(isSelected ? { 'data-selected': true } : {})}
>
{node.empty ? '' : node.name}
</span>
{renderActionsComponent && renderActionsComponent(node)}
{isHovered && renderHoverComponent
? renderHoverComponent(node)
: null}
</div>
<AnimatePresence initial={false}>
{isExpanded && children && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{
height: 'auto',
opacity: 1,
transition: {
height: {
duration: Math.min(
0.3 + children.length * 0.018,
0.7
),
ease: 'easeInOut',
},
opacity: {
duration: Math.min(
0.2 + children.length * 0.012,
0.4
),
ease: 'easeInOut',
},
},
}}
exit={{
height: 0,
opacity: 0,
transition: {
height: {
duration: Math.min(
0.2 + children.length * 0.01,
0.45
),
ease: 'easeInOut',
},
opacity: {
duration: 0.1,
ease: 'easeOut',
},
},
}}
style={{ overflow: 'hidden' }}
>
{children.map((child) => (
<TreeNode
key={child.id}
node={child}
level={level + 1}
expanded={expanded}
loading={loading}
loadedChildren={loadedChildren}
hasMoreChildren={hasMoreChildren}
onToggle={onToggle}
onNodeClick={onNodeClick}
defaultIcon={DefaultIcon}
defaultFolderIcon={DefaultFolderIcon}
defaultIconProps={defaultIconProps}
defaultFolderIconProps={defaultFolderIconProps}
selectable={selectable}
selectedId={selectedId}
onSelect={onSelect}
className="mt-0.5"
renderHoverComponent={renderHoverComponent}
renderActionsComponent={renderActionsComponent}
loadingNodeIds={loadingNodeIds}
/>
))}
{isLoading ? (
<TreeItemSkeleton
style={{
paddingLeft: `${level + 2 * 16 + 8}px`,
}}
/>
) : null}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
function findNodeById<
Type extends string,
Context extends Record<Type, unknown>,
>(
nodes: TreeNode<Type, Context>[],
id: string,
initialPath: TreeNode<Type, Context>[] = []
): { node: TreeNode<Type, Context> | null; path: TreeNode<Type, Context>[] } {
const path: TreeNode<Type, Context>[] = [...initialPath];
for (const node of nodes) {
if (node.id === id) return { node, path };
if (node.children) {
const found = findNodeById(node.children, id, [...path, node]);
if (found.node) {
return found;
}
}
}
return { node: null, path };
}

View File

@@ -0,0 +1,41 @@
import type { LucideIcon } from 'lucide-react';
import type React from 'react';
export interface TreeNode<
Type extends string,
Context extends Record<Type, unknown>,
> {
id: string;
name: string;
isFolder?: boolean;
children?: TreeNode<Type, Context>[];
icon?: LucideIcon;
iconProps?: React.ComponentProps<LucideIcon>;
labelProps?: React.ComponentProps<'span'>;
type: Type;
unselectable?: boolean;
tooltip?: string;
context: Context[Type];
empty?: boolean;
className?: string;
}
export type FetchChildrenFunction<
Type extends string,
Context extends Record<Type, unknown>,
> = (
nodeId: string,
nodeType: Type,
nodeContext: Context[Type]
) => Promise<TreeNode<Type, Context>[]>;
export interface SelectableTreeProps<
Type extends string,
Context extends Record<Type, unknown>,
> {
enabled: boolean;
defaultSelectedId?: string;
onSelectedChange?: (node: TreeNode<Type, Context>) => void;
selectedId?: string;
setSelectedId?: React.Dispatch<React.SetStateAction<string | undefined>>;
}

View File

@@ -0,0 +1,153 @@
import type { Dispatch, SetStateAction } from 'react';
import { useState, useCallback, useMemo } from 'react';
import type { TreeNode, FetchChildrenFunction } from './tree';
export interface ExpandedState {
[key: string]: boolean;
}
interface LoadingState {
[key: string]: boolean;
}
interface LoadedChildren<
Type extends string,
Context extends Record<Type, unknown>,
> {
[key: string]: TreeNode<Type, Context>[];
}
interface HasMoreChildrenState {
[key: string]: boolean;
}
export function useTree<
Type extends string,
Context extends Record<Type, unknown>,
>({
fetchChildren,
expanded: expandedProp,
setExpanded: setExpandedProp,
}: {
fetchChildren?: FetchChildrenFunction<Type, Context>;
expanded?: ExpandedState;
setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
}) {
const [expandedInternal, setExpandedInternal] = useState<ExpandedState>({});
const expanded = useMemo(
() => expandedProp ?? expandedInternal,
[expandedProp, expandedInternal]
);
const setExpanded = useCallback(
(value: SetStateAction<ExpandedState>) => {
if (setExpandedProp) {
setExpandedProp(value);
} else {
setExpandedInternal(value);
}
},
[setExpandedProp, setExpandedInternal]
);
const [loading, setLoading] = useState<LoadingState>({});
const [loadedChildren, setLoadedChildren] = useState<
LoadedChildren<Type, Context>
>({});
const [hasMoreChildren, setHasMoreChildren] =
useState<HasMoreChildrenState>({});
const mergeChildren = useCallback(
(
staticChildren: TreeNode<Type, Context>[] = [],
fetchedChildren: TreeNode<Type, Context>[] = []
) => {
const fetchedChildrenIds = new Set(
fetchedChildren.map((child) => child.id)
);
const uniqueStaticChildren = staticChildren.filter(
(child) => !fetchedChildrenIds.has(child.id)
);
return [...uniqueStaticChildren, ...fetchedChildren];
},
[]
);
const toggleNode = useCallback(
async (
nodeId: string,
nodeType: Type,
nodeContext: Context[Type],
staticChildren?: TreeNode<Type, Context>[]
) => {
if (expanded[nodeId]) {
// If we're collapsing, just update expanded state
setExpanded((prev) => ({ ...prev, [nodeId]: false }));
return;
}
// Get any previously fetched children
const previouslyFetchedChildren = loadedChildren[nodeId] || [];
// If we have static children, merge them with any previously fetched children
if (staticChildren?.length) {
const mergedChildren = mergeChildren(
staticChildren,
previouslyFetchedChildren
);
setLoadedChildren((prev) => ({
...prev,
[nodeId]: mergedChildren,
}));
// Only show "more loading" if we haven't fetched children before
setHasMoreChildren((prev) => ({
...prev,
[nodeId]: !previouslyFetchedChildren.length,
}));
}
// Set expanded state immediately to show static/previously fetched children
setExpanded((prev) => ({ ...prev, [nodeId]: true }));
// If we haven't loaded dynamic children yet
if (!previouslyFetchedChildren.length) {
setLoading((prev) => ({ ...prev, [nodeId]: true }));
try {
const fetchedChildren = await fetchChildren?.(
nodeId,
nodeType,
nodeContext
);
// Merge static and newly fetched children
const allChildren = mergeChildren(
staticChildren || [],
fetchedChildren
);
setLoadedChildren((prev) => ({
...prev,
[nodeId]: allChildren,
}));
setHasMoreChildren((prev) => ({
...prev,
[nodeId]: false,
}));
} catch (error) {
console.error('Error loading children:', error);
} finally {
setLoading((prev) => ({ ...prev, [nodeId]: false }));
}
}
},
[expanded, loadedChildren, fetchChildren, mergeChildren, setExpanded]
);
return {
expanded,
loading,
loadedChildren,
hasMoreChildren,
toggleNode,
};
}

View File

@@ -12,6 +12,8 @@ export interface CanvasContext {
}) => void; }) => void;
setOverlapGraph: (graph: Graph<string>) => void; setOverlapGraph: (graph: Graph<string>) => void;
overlapGraph: Graph<string>; overlapGraph: Graph<string>;
setShowFilter: React.Dispatch<React.SetStateAction<boolean>>;
showFilter: boolean;
} }
export const canvasContext = createContext<CanvasContext>({ export const canvasContext = createContext<CanvasContext>({
@@ -19,4 +21,6 @@ export const canvasContext = createContext<CanvasContext>({
fitView: emptyFn, fitView: emptyFn,
setOverlapGraph: emptyFn, setOverlapGraph: emptyFn,
overlapGraph: createGraph(), overlapGraph: createGraph(),
setShowFilter: emptyFn,
showFilter: false,
}); });

View File

@@ -1,26 +1,56 @@
import React, { type ReactNode, useCallback, useState } from 'react'; import React, {
type ReactNode,
useCallback,
useState,
useEffect,
useRef,
} from 'react';
import { canvasContext } from './canvas-context'; import { canvasContext } from './canvas-context';
import { useChartDB } from '@/hooks/use-chartdb'; import { useChartDB } from '@/hooks/use-chartdb';
import { import { adjustTablePositions } from '@/lib/domain/db-table';
adjustTablePositions,
shouldShowTablesBySchemaFilter,
} from '@/lib/domain/db-table';
import { useReactFlow } from '@xyflow/react'; import { useReactFlow } from '@xyflow/react';
import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils'; import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils';
import type { Graph } from '@/lib/graph'; import type { Graph } from '@/lib/graph';
import { createGraph } 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 { interface CanvasProviderProps {
children: ReactNode; children: ReactNode;
} }
export const CanvasProvider = ({ children }: CanvasProviderProps) => { export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const { tables, relationships, updateTablesState, filteredSchemas } = const {
useChartDB(); tables,
relationships,
updateTablesState,
databaseType,
areas,
diagramId,
} = useChartDB();
const { filter, loading: filterLoading } = useDiagramFilter();
const { fitView } = useReactFlow(); const { fitView } = useReactFlow();
const [overlapGraph, setOverlapGraph] = const [overlapGraph, setOverlapGraph] =
useState<Graph<string>>(createGraph()); useState<Graph<string>>(createGraph());
const [showFilter, setShowFilter] = useState(false);
const diagramIdActiveFilterRef = useRef<string>();
useEffect(() => {
if (filterLoading) {
return;
}
if (diagramIdActiveFilterRef.current === diagramId) {
return;
}
diagramIdActiveFilterRef.current = diagramId;
setShowFilter(true);
}, [filterLoading, diagramId]);
const reorderTables = useCallback( const reorderTables = useCallback(
( (
options: { updateHistory?: boolean } = { options: { updateHistory?: boolean } = {
@@ -30,9 +60,19 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const newTables = adjustTablePositions({ const newTables = adjustTablePositions({
relationships, relationships,
tables: tables.filter((table) => 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({ const updatedOverlapGraph = findOverlappingTables({
@@ -67,7 +107,15 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
}); });
}, 500); }, 500);
}, },
[filteredSchemas, relationships, tables, updateTablesState, fitView] [
filter,
relationships,
tables,
updateTablesState,
fitView,
databaseType,
areas,
]
); );
return ( return (
@@ -77,6 +125,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
fitView, fitView,
setOverlapGraph, setOverlapGraph,
overlapGraph, overlapGraph,
setShowFilter,
showFilter,
}} }}
> >
{children} {children}

View File

@@ -78,8 +78,8 @@ export interface ChartDBContext {
events: EventEmitter<ChartDBEvent>; events: EventEmitter<ChartDBEvent>;
readonly?: boolean; readonly?: boolean;
filteredSchemas?: string[]; highlightedCustomType?: DBCustomType;
filterSchemas: (schemaIds: string[]) => void; highlightCustomTypeId: (id?: string) => void;
// General operations // General operations
updateDiagramId: (id: string) => Promise<void>; updateDiagramId: (id: string) => Promise<void>;
@@ -92,6 +92,10 @@ export interface ChartDBContext {
updateDiagramUpdatedAt: () => Promise<void>; updateDiagramUpdatedAt: () => Promise<void>;
clearDiagramData: () => Promise<void>; clearDiagramData: () => Promise<void>;
deleteDiagram: () => Promise<void>; deleteDiagram: () => Promise<void>;
updateDiagramData: (
diagram: Diagram,
options?: { forceUpdateStorage?: boolean }
) => Promise<void>;
// Database type operations // Database type operations
updateDatabaseType: (databaseType: DatabaseType) => Promise<void>; updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
@@ -289,8 +293,7 @@ export const chartDBContext = createContext<ChartDBContext>({
areas: [], areas: [],
customTypes: [], customTypes: [],
schemas: [], schemas: [],
filteredSchemas: [], highlightCustomTypeId: emptyFn,
filterSchemas: emptyFn,
currentDiagram: { currentDiagram: {
id: '', id: '',
name: '', name: '',
@@ -308,6 +311,7 @@ export const chartDBContext = createContext<ChartDBContext>({
loadDiagramFromData: emptyFn, loadDiagramFromData: emptyFn,
clearDiagramData: emptyFn, clearDiagramData: emptyFn,
deleteDiagram: emptyFn, deleteDiagram: emptyFn,
updateDiagramData: emptyFn,
// Database type operations // Database type operations
updateDatabaseType: emptyFn, updateDatabaseType: emptyFn,

View File

@@ -1,12 +1,15 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import type { DBTable } from '@/lib/domain/db-table'; import type { DBTable } from '@/lib/domain/db-table';
import { deepCopy, generateId } from '@/lib/utils'; import { deepCopy, generateId } from '@/lib/utils';
import { randomColor } from '@/lib/colors'; import { defaultTableColor, defaultAreaColor, viewColor } from '@/lib/colors';
import type { ChartDBContext, ChartDBEvent } from './chartdb-context'; import type { ChartDBContext, ChartDBEvent } from './chartdb-context';
import { chartDBContext } from './chartdb-context'; import { chartDBContext } from './chartdb-context';
import { DatabaseType } from '@/lib/domain/database-type'; import { DatabaseType } from '@/lib/domain/database-type';
import type { DBField } from '@/lib/domain/db-field'; 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 type { DBRelationship } from '@/lib/domain/db-relationship';
import { useStorage } from '@/hooks/use-storage'; import { useStorage } from '@/hooks/use-storage';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
@@ -17,7 +20,6 @@ import {
databasesWithSchemas, databasesWithSchemas,
schemaNameToSchemaId, schemaNameToSchemaId,
} from '@/lib/domain/db-schema'; } from '@/lib/domain/db-schema';
import { useLocalConfig } from '@/hooks/use-local-config';
import { defaultSchemas } from '@/lib/data/default-schemas'; import { defaultSchemas } from '@/lib/data/default-schemas';
import { useEventEmitter } from 'ahooks'; import { useEventEmitter } from 'ahooks';
import type { DBDependency } from '@/lib/domain/db-dependency'; import type { DBDependency } from '@/lib/domain/db-dependency';
@@ -39,11 +41,12 @@ export const ChartDBProvider: React.FC<
React.PropsWithChildren<ChartDBProviderProps> React.PropsWithChildren<ChartDBProviderProps>
> = ({ children, diagram, readonly: readonlyProp }) => { > = ({ children, diagram, readonly: readonlyProp }) => {
const { hasDiff } = useDiff(); const { hasDiff } = useDiff();
let db = useStorage(); const dbStorage = useStorage();
let db = dbStorage;
const events = useEventEmitter<ChartDBEvent>(); const events = useEventEmitter<ChartDBEvent>();
const { setSchemasFilter, schemasFilter } = useLocalConfig();
const { addUndoAction, resetRedoStack, resetUndoStack } = const { addUndoAction, resetRedoStack, resetUndoStack } =
useRedoUndoStack(); useRedoUndoStack();
const [diagramId, setDiagramId] = useState(''); const [diagramId, setDiagramId] = useState('');
const [diagramName, setDiagramName] = useState(''); const [diagramName, setDiagramName] = useState('');
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date()); const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
@@ -65,8 +68,12 @@ export const ChartDBProvider: React.FC<
const [customTypes, setCustomTypes] = useState<DBCustomType[]>( const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
diagram?.customTypes ?? [] diagram?.customTypes ?? []
); );
const { events: diffEvents } = useDiff(); const { events: diffEvents } = useDiff();
const [highlightedCustomTypeId, setHighlightedCustomTypeId] =
useState<string>();
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => { const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data; const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
setTables((tables) => setTables((tables) =>
@@ -85,7 +92,10 @@ export const ChartDBProvider: React.FC<
diffEvents.useSubscription(diffCalculatedHandler); diffEvents.useSubscription(diffCalculatedHandler);
const defaultSchemaName = defaultSchemas[databaseType]; const defaultSchemaName = useMemo(
() => defaultSchemas[databaseType],
[databaseType]
);
const readonly = useMemo( const readonly = useMemo(
() => readonlyProp ?? hasDiff ?? false, () => readonlyProp ?? hasDiff ?? false,
@@ -106,9 +116,11 @@ export const ChartDBProvider: React.FC<
.filter((schema) => !!schema) as string[] .filter((schema) => !!schema) as string[]
), ),
] ]
.sort((a, b) => .sort((a, b) => {
a === defaultSchemaName ? -1 : a.localeCompare(b) if (a === defaultSchemaName) return -1;
) if (b === defaultSchemaName) return 1;
return a.localeCompare(b);
})
.map( .map(
(schema): DBSchema => ({ (schema): DBSchema => ({
id: schemaNameToSchemaId(schema), id: schemaNameToSchemaId(schema),
@@ -122,34 +134,6 @@ export const ChartDBProvider: React.FC<
[tables, defaultSchemaName, databaseType] [tables, defaultSchemaName, databaseType]
); );
const filterSchemas: ChartDBContext['filterSchemas'] = useCallback(
(schemaIds) => {
setSchemasFilter((prev) => ({
...prev,
[diagramId]: schemaIds,
}));
},
[diagramId, setSchemasFilter]
);
const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(() => {
if (schemas.length === 0) {
return undefined;
}
const schemasFilterFromCache =
(schemasFilter[diagramId] ?? []).length === 0
? undefined // in case of empty filter, skip cache
: schemasFilter[diagramId];
return (
schemasFilterFromCache ?? [
schemas.find((s) => s.name === defaultSchemaName)?.id ??
schemas[0]?.id,
]
);
}, [schemasFilter, diagramId, schemas, defaultSchemaName]);
const currentDiagram: Diagram = useMemo( const currentDiagram: Diagram = useMemo(
() => ({ () => ({
id: diagramId, id: diagramId,
@@ -361,12 +345,17 @@ export const ChartDBProvider: React.FC<
}, },
], ],
indexes: [], indexes: [],
color: randomColor(), color: attributes?.isView ? viewColor : defaultTableColor,
createdAt: Date.now(), createdAt: Date.now(),
isView: false, isView: false,
order: tables.length, order: tables.length,
...attributes, ...attributes,
}; };
table.indexes = getTableIndexesWithPrimaryKey({
table,
});
await addTable(table); await addTable(table);
return table; return table;
@@ -658,17 +647,30 @@ export const ChartDBProvider: React.FC<
options = { updateHistory: true } options = { updateHistory: true }
) => { ) => {
const prevField = getField(tableId, fieldId); const prevField = getField(tableId, fieldId);
setTables((tables) =>
tables.map((table) => const updateTableFn = (table: DBTable) => {
table.id === tableId const updatedTable: DBTable = {
? {
...table, ...table,
fields: table.fields.map((f) => fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f f.id === fieldId ? { ...f, ...field } : f
), ),
} satisfies DBTable;
updatedTable.indexes = getTableIndexesWithPrimaryKey({
table: updatedTable,
});
return updatedTable;
};
setTables((tables) =>
tables.map((table) => {
if (table.id === tableId) {
return updateTableFn(table);
} }
: table
) return table;
})
); );
const table = await db.getTable({ diagramId, id: tableId }); const table = await db.getTable({ diagramId, id: tableId });
@@ -683,10 +685,7 @@ export const ChartDBProvider: React.FC<
db.updateTable({ db.updateTable({
id: tableId, id: tableId,
attributes: { attributes: {
...table, ...updateTableFn(table),
fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f
),
}, },
}), }),
]); ]);
@@ -713,19 +712,29 @@ export const ChartDBProvider: React.FC<
fieldId: string, fieldId: string,
options = { updateHistory: true } 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 fields = getTable(tableId)?.fields ?? [];
const prevField = getField(tableId, fieldId); const prevField = getField(tableId, fieldId);
setTables((tables) => setTables((tables) =>
tables.map((table) => tables.map((table) => {
table.id === tableId if (table.id === tableId) {
? { return updateTableFn(table);
...table,
fields: table.fields.filter(
(f) => f.id !== fieldId
),
} }
: table
) return table;
})
); );
events.emit({ events.emit({
@@ -749,8 +758,7 @@ export const ChartDBProvider: React.FC<
db.updateTable({ db.updateTable({
id: tableId, id: tableId,
attributes: { attributes: {
...table, ...updateTableFn(table),
fields: table.fields.filter((f) => f.id !== fieldId),
}, },
}), }),
]); ]);
@@ -1106,12 +1114,15 @@ export const ChartDBProvider: React.FC<
const sourceFieldName = sourceField?.name ?? ''; const sourceFieldName = sourceField?.name ?? '';
const targetTable = getTable(targetTableId);
const targetTableSchema = targetTable?.schema;
const relationship: DBRelationship = { const relationship: DBRelationship = {
id: generateId(), id: generateId(),
name: `${sourceTableName}_${sourceFieldName}_fk`, name: `${sourceTableName}_${sourceFieldName}_fk`,
sourceSchema: sourceTable?.schema, sourceSchema: sourceTable?.schema,
sourceTableId, sourceTableId,
targetSchema: sourceTable?.schema, targetSchema: targetTableSchema,
targetTableId, targetTableId,
sourceFieldId, sourceFieldId,
targetFieldId, targetFieldId,
@@ -1433,7 +1444,7 @@ export const ChartDBProvider: React.FC<
y: 0, y: 0,
width: 300, width: 300,
height: 200, height: 200,
color: randomColor(), color: defaultAreaColor,
...attributes, ...attributes,
}; };
@@ -1516,22 +1527,37 @@ export const ChartDBProvider: React.FC<
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack] [db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
); );
const highlightCustomTypeId = useCallback(
(id?: string) => setHighlightedCustomTypeId(id),
[setHighlightedCustomTypeId]
);
const highlightedCustomType = useMemo(() => {
return highlightedCustomTypeId
? customTypes.find((type) => type.id === highlightedCustomTypeId)
: undefined;
}, [highlightedCustomTypeId, customTypes]);
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] = const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
useCallback( useCallback(
async (diagram) => { (diagram) => {
setDiagramId(diagram.id); setDiagramId(diagram.id);
setDiagramName(diagram.name); setDiagramName(diagram.name);
setDatabaseType(diagram.databaseType); setDatabaseType(diagram.databaseType);
setDatabaseEdition(diagram.databaseEdition); setDatabaseEdition(diagram.databaseEdition);
setTables(diagram?.tables ?? []); setTables(diagram.tables ?? []);
setRelationships(diagram?.relationships ?? []); setRelationships(diagram.relationships ?? []);
setDependencies(diagram?.dependencies ?? []); setDependencies(diagram.dependencies ?? []);
setAreas(diagram?.areas ?? []); setAreas(diagram.areas ?? []);
setCustomTypes(diagram?.customTypes ?? []); setCustomTypes(diagram.customTypes ?? []);
setDiagramCreatedAt(diagram.createdAt); setDiagramCreatedAt(diagram.createdAt);
setDiagramUpdatedAt(diagram.updatedAt); setDiagramUpdatedAt(diagram.updatedAt);
setHighlightedCustomTypeId(undefined);
events.emit({ action: 'load_diagram', data: { diagram } }); events.emit({ action: 'load_diagram', data: { diagram } });
resetRedoStack();
resetUndoStack();
}, },
[ [
setDiagramId, setDiagramId,
@@ -1545,10 +1571,23 @@ export const ChartDBProvider: React.FC<
setCustomTypes, setCustomTypes,
setDiagramCreatedAt, setDiagramCreatedAt,
setDiagramUpdatedAt, setDiagramUpdatedAt,
setHighlightedCustomTypeId,
events, events,
resetRedoStack,
resetUndoStack,
] ]
); );
const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
async (diagram, options) => {
const st = options?.forceUpdateStorage ? dbStorage : db;
await st.deleteDiagram(diagram.id);
await st.addDiagram({ diagram });
loadDiagramFromData(diagram);
},
[db, dbStorage, loadDiagramFromData]
);
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback( const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
async (diagramId: string) => { async (diagramId: string) => {
const diagram = await db.getDiagram(diagramId, { const diagram = await db.getDiagram(diagramId, {
@@ -1724,10 +1763,9 @@ export const ChartDBProvider: React.FC<
areas, areas,
currentDiagram, currentDiagram,
schemas, schemas,
filteredSchemas,
events, events,
readonly, readonly,
filterSchemas, updateDiagramData,
updateDiagramId, updateDiagramId,
updateDiagramName, updateDiagramName,
loadDiagram, loadDiagram,
@@ -1784,6 +1822,8 @@ export const ChartDBProvider: React.FC<
removeCustomType, removeCustomType,
removeCustomTypes, removeCustomTypes,
updateCustomType, updateCustomType,
highlightCustomTypeId,
highlightedCustomType,
}} }}
> >
{children} {children}

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { ConfigContext } from './config-context'; import { ConfigContext } from './config-context';
import { useStorage } from '@/hooks/use-storage'; import { useStorage } from '@/hooks/use-storage';
@@ -8,7 +8,7 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
children, children,
}) => { }) => {
const { getConfig, updateConfig: updateDataConfig } = useStorage(); const { getConfig, updateConfig: updateDataConfig } = useStorage();
const [config, setConfig] = React.useState<ChartDBConfig | undefined>(); const [config, setConfig] = useState<ChartDBConfig | undefined>();
useEffect(() => { useEffect(() => {
const loadConfig = async () => { const loadConfig = async () => {
@@ -45,7 +45,12 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
}; };
return ( return (
<ConfigContext.Provider value={{ config, updateConfig }}> <ConfigContext.Provider
value={{
config,
updateConfig,
}}
>
{children} {children}
</ConfigContext.Provider> </ConfigContext.Provider>
); );

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; originalDiagram: Diagram | null;
diffMap: DiffMap; diffMap: DiffMap;
hasDiff: boolean; hasDiff: boolean;
isSummaryOnly: boolean;
calculateDiff: ({ calculateDiff: ({
diagram, diagram,
newDiagram, newDiagram,
options,
}: { }: {
diagram: Diagram; diagram: Diagram;
newDiagram: Diagram; newDiagram: Diagram;
options?: {
summaryOnly?: boolean;
};
}) => void; }) => void;
resetDiff: () => void;
// table diff // table diff
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean; checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
@@ -60,6 +66,15 @@ export interface DiffContext {
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean; checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null; getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | 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 // relationship diff
checkIfNewRelationship: ({ checkIfNewRelationship: ({

View File

@@ -32,6 +32,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
const [fieldsChanged, setFieldsChanged] = React.useState< const [fieldsChanged, setFieldsChanged] = React.useState<
Map<string, boolean> Map<string, boolean>
>(new Map<string, boolean>()); >(new Map<string, boolean>());
const [isSummaryOnly, setIsSummaryOnly] = React.useState<boolean>(false);
const events = useEventEmitter<DiffEvent>(); const events = useEventEmitter<DiffEvent>();
@@ -127,7 +128,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
); );
const calculateDiff: DiffContext['calculateDiff'] = useCallback( const calculateDiff: DiffContext['calculateDiff'] = useCallback(
({ diagram, newDiagram: newDiagramArg }) => { ({ diagram, newDiagram: newDiagramArg, options }) => {
const { const {
diffMap: newDiffs, diffMap: newDiffs,
changedTables: newChangedTables, changedTables: newChangedTables,
@@ -139,6 +140,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
setFieldsChanged(newChangedFields); setFieldsChanged(newChangedFields);
setNewDiagram(newDiagramArg); setNewDiagram(newDiagramArg);
setOriginalDiagram(diagram); setOriginalDiagram(diagram);
setIsSummaryOnly(options?.summaryOnly ?? false);
events.emit({ events.emit({
action: 'diff_calculated', action: 'diff_calculated',
@@ -305,6 +307,117 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
[diffMap] [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< const checkIfNewRelationship = useCallback<
DiffContext['checkIfNewRelationship'] DiffContext['checkIfNewRelationship']
>( >(
@@ -339,6 +452,15 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
[diffMap] [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 ( return (
<diffContext.Provider <diffContext.Provider
value={{ value={{
@@ -346,8 +468,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
originalDiagram, originalDiagram,
diffMap, diffMap,
hasDiff: diffMap.size > 0, hasDiff: diffMap.size > 0,
isSummaryOnly,
calculateDiff, calculateDiff,
resetDiff,
// table diff // table diff
getTableNewName, getTableNewName,
@@ -362,6 +486,11 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
checkIfNewField, checkIfNewField,
getFieldNewName, getFieldNewName,
getFieldNewType, getFieldNewType,
getFieldNewPrimaryKey,
getFieldNewNullable,
getFieldNewCharacterMaximumLength,
getFieldNewScale,
getFieldNewPrecision,
// relationship diff // relationship diff
checkIfNewRelationship, checkIfNewRelationship,

View File

@@ -8,6 +8,7 @@ export enum KeyboardShortcutAction {
TOGGLE_SIDE_PANEL = 'toggle_side_panel', TOGGLE_SIDE_PANEL = 'toggle_side_panel',
SHOW_ALL = 'show_all', SHOW_ALL = 'show_all',
TOGGLE_THEME = 'toggle_theme', TOGGLE_THEME = 'toggle_theme',
TOGGLE_FILTER = 'toggle_filter',
} }
export interface KeyboardShortcut { export interface KeyboardShortcut {
@@ -71,6 +72,13 @@ export const keyboardShortcuts: Record<
keyCombinationMac: 'meta+m', keyCombinationMac: 'meta+m',
keyCombinationWin: 'ctrl+m', keyCombinationWin: 'ctrl+m',
}, },
[KeyboardShortcutAction.TOGGLE_FILTER]: {
action: KeyboardShortcutAction.TOGGLE_FILTER,
keyCombinationLabelMac: '⌘F',
keyCombinationLabelWin: 'Ctrl+F',
keyCombinationMac: 'meta+f',
keyCombinationWin: 'ctrl+f',
},
}; };
export interface KeyboardShortcutForOS { export interface KeyboardShortcutForOS {

View File

@@ -2,9 +2,9 @@ import { emptyFn } from '@/lib/utils';
import { createContext } from 'react'; import { createContext } from 'react';
export type SidebarSection = export type SidebarSection =
| 'dbml'
| 'tables' | 'tables'
| 'relationships' | 'refs'
| 'dependencies'
| 'areas' | 'areas'
| 'customTypes'; | 'customTypes';
@@ -13,14 +13,16 @@ export interface LayoutContext {
openTableFromSidebar: (tableId: string) => void; openTableFromSidebar: (tableId: string) => void;
closeAllTablesInSidebar: () => void; closeAllTablesInSidebar: () => void;
openedRelationshipInSidebar: string | undefined;
openRelationshipFromSidebar: (relationshipId: string) => void; openRelationshipFromSidebar: (relationshipId: string) => void;
closeAllRelationshipsInSidebar: () => void; closeAllRelationshipsInSidebar: () => void;
openedDependencyInSidebar: string | undefined;
openDependencyFromSidebar: (dependencyId: string) => void; openDependencyFromSidebar: (dependencyId: string) => void;
closeAllDependenciesInSidebar: () => void; closeAllDependenciesInSidebar: () => void;
openedRefInSidebar: string | undefined;
openRefFromSidebar: (refId: string) => void;
closeAllRefsInSidebar: () => void;
openedAreaInSidebar: string | undefined; openedAreaInSidebar: string | undefined;
openAreaFromSidebar: (areaId: string) => void; openAreaFromSidebar: (areaId: string) => void;
closeAllAreasInSidebar: () => void; closeAllAreasInSidebar: () => void;
@@ -36,24 +38,22 @@ export interface LayoutContext {
hideSidePanel: () => void; hideSidePanel: () => void;
showSidePanel: () => void; showSidePanel: () => void;
toggleSidePanel: () => void; toggleSidePanel: () => void;
isSelectSchemaOpen: boolean;
openSelectSchema: () => void;
closeSelectSchema: () => void;
} }
export const layoutContext = createContext<LayoutContext>({ export const layoutContext = createContext<LayoutContext>({
openedTableInSidebar: undefined, openedTableInSidebar: undefined,
selectedSidebarSection: 'tables', selectedSidebarSection: 'tables',
openedRelationshipInSidebar: undefined,
openRelationshipFromSidebar: emptyFn, openRelationshipFromSidebar: emptyFn,
closeAllRelationshipsInSidebar: emptyFn, closeAllRelationshipsInSidebar: emptyFn,
openedDependencyInSidebar: undefined,
openDependencyFromSidebar: emptyFn, openDependencyFromSidebar: emptyFn,
closeAllDependenciesInSidebar: emptyFn, closeAllDependenciesInSidebar: emptyFn,
openedRefInSidebar: undefined,
openRefFromSidebar: emptyFn,
closeAllRefsInSidebar: emptyFn,
openedAreaInSidebar: undefined, openedAreaInSidebar: undefined,
openAreaFromSidebar: emptyFn, openAreaFromSidebar: emptyFn,
closeAllAreasInSidebar: emptyFn, closeAllAreasInSidebar: emptyFn,
@@ -70,8 +70,4 @@ export const layoutContext = createContext<LayoutContext>({
hideSidePanel: emptyFn, hideSidePanel: emptyFn,
showSidePanel: emptyFn, showSidePanel: emptyFn,
toggleSidePanel: 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< const [openedTableInSidebar, setOpenedTableInSidebar] = React.useState<
string | undefined string | undefined
>(); >();
const [openedRelationshipInSidebar, setOpenedRelationshipInSidebar] = const [openedRefInSidebar, setOpenedRefInSidebar] = React.useState<
React.useState<string | undefined>(); string | undefined
const [openedDependencyInSidebar, setOpenedDependencyInSidebar] = >();
React.useState<string | undefined>();
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState< const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
string | undefined string | undefined
>(); >();
@@ -23,17 +22,18 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
React.useState<SidebarSection>('tables'); React.useState<SidebarSection>('tables');
const [isSidePanelShowed, setIsSidePanelShowed] = const [isSidePanelShowed, setIsSidePanelShowed] =
React.useState<boolean>(isDesktop); React.useState<boolean>(isDesktop);
const [isSelectSchemaOpen, setIsSelectSchemaOpen] =
React.useState<boolean>(false);
const closeAllTablesInSidebar: LayoutContext['closeAllTablesInSidebar'] = const closeAllTablesInSidebar: LayoutContext['closeAllTablesInSidebar'] =
() => setOpenedTableInSidebar(''); () => setOpenedTableInSidebar('');
const closeAllRelationshipsInSidebar: LayoutContext['closeAllRelationshipsInSidebar'] = const closeAllRelationshipsInSidebar: LayoutContext['closeAllRelationshipsInSidebar'] =
() => setOpenedRelationshipInSidebar(''); () => setOpenedRefInSidebar('');
const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] = const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] =
() => setOpenedDependencyInSidebar(''); () => setOpenedRefInSidebar('');
const closeAllRefsInSidebar: LayoutContext['closeAllRefsInSidebar'] = () =>
setOpenedRefInSidebar('');
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] = const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
() => setOpenedAreaInSidebar(''); () => setOpenedAreaInSidebar('');
@@ -62,15 +62,21 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
const openRelationshipFromSidebar: LayoutContext['openRelationshipFromSidebar'] = const openRelationshipFromSidebar: LayoutContext['openRelationshipFromSidebar'] =
(relationshipId) => { (relationshipId) => {
showSidePanel(); showSidePanel();
setSelectedSidebarSection('relationships'); setSelectedSidebarSection('refs');
setOpenedRelationshipInSidebar(relationshipId); setOpenedRefInSidebar(relationshipId);
}; };
const openDependencyFromSidebar: LayoutContext['openDependencyFromSidebar'] = const openDependencyFromSidebar: LayoutContext['openDependencyFromSidebar'] =
(dependencyId) => { (dependencyId) => {
showSidePanel(); showSidePanel();
setSelectedSidebarSection('dependencies'); setSelectedSidebarSection('refs');
setOpenedDependencyInSidebar(dependencyId); setOpenedRefInSidebar(dependencyId);
};
const openRefFromSidebar: LayoutContext['openRefFromSidebar'] = (refId) => {
showSidePanel();
setSelectedSidebarSection('refs');
setOpenedRefInSidebar(refId);
}; };
const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = ( const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = (
@@ -88,11 +94,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
setOpenedTableInSidebar(customTypeId); setOpenedTableInSidebar(customTypeId);
}; };
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
setIsSelectSchemaOpen(true);
const closeSelectSchema: LayoutContext['closeSelectSchema'] = () =>
setIsSelectSchemaOpen(false);
return ( return (
<layoutContext.Provider <layoutContext.Provider
value={{ value={{
@@ -100,7 +101,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
selectedSidebarSection, selectedSidebarSection,
openTableFromSidebar, openTableFromSidebar,
selectSidebarSection: setSelectedSidebarSection, selectSidebarSection: setSelectedSidebarSection,
openedRelationshipInSidebar,
openRelationshipFromSidebar, openRelationshipFromSidebar,
closeAllTablesInSidebar, closeAllTablesInSidebar,
closeAllRelationshipsInSidebar, closeAllRelationshipsInSidebar,
@@ -108,12 +108,11 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
hideSidePanel, hideSidePanel,
showSidePanel, showSidePanel,
toggleSidePanel, toggleSidePanel,
isSelectSchemaOpen,
openSelectSchema,
closeSelectSchema,
openedDependencyInSidebar,
openDependencyFromSidebar, openDependencyFromSidebar,
closeAllDependenciesInSidebar, closeAllDependenciesInSidebar,
openedRefInSidebar,
openRefFromSidebar,
closeAllRefsInSidebar,
openedAreaInSidebar, openedAreaInSidebar,
openAreaFromSidebar, openAreaFromSidebar,
closeAllAreasInSidebar, closeAllAreasInSidebar,

View File

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

View File

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

View File

@@ -7,12 +7,21 @@ import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency'; import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area'; import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type'; import type { DBCustomType } from '@/lib/domain/db-custom-type';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
export interface StorageContext { export interface StorageContext {
// Config operations // Config operations
getConfig: () => Promise<ChartDBConfig | undefined>; getConfig: () => Promise<ChartDBConfig | undefined>;
updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>; 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 // Diagram operations
addDiagram: (params: { diagram: Diagram }) => Promise<void>; addDiagram: (params: { diagram: Diagram }) => Promise<void>;
listDiagrams: (options?: { listDiagrams: (options?: {
@@ -132,6 +141,10 @@ export const storageInitialValue: StorageContext = {
getConfig: emptyFn, getConfig: emptyFn,
updateConfig: emptyFn, updateConfig: emptyFn,
getDiagramFilter: emptyFn,
updateDiagramFilter: emptyFn,
deleteDiagramFilter: emptyFn,
addDiagram: emptyFn, addDiagram: emptyFn,
listDiagrams: emptyFn, listDiagrams: emptyFn,
getDiagram: 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 { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area'; import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type'; 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> = ({ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
children, children,
@@ -44,6 +45,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
ChartDBConfig & { id: number }, ChartDBConfig & { id: number },
'id' // primary key "id" (for the typings only) 'id' // primary key "id" (for the typings only)
>; >;
diagram_filters: EntityTable<
DiagramFilter & { diagramId: string },
'diagramId' // primary key "id" (for the typings only)
>;
}; };
// Schema declaration: // Schema declaration:
@@ -190,6 +195,27 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
config: '++id, defaultDiagramId', 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 () => { dexieDB.on('ready', async () => {
const config = await dexieDB.config.get(1); const config = await dexieDB.config.get(1);
@@ -217,6 +243,34 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
[db] [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( const addTable: StorageContext['addTable'] = useCallback(
async ({ diagramId, table }) => { async ({ diagramId, table }) => {
await db.db_tables.add({ await db.db_tables.add({
@@ -756,6 +810,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
deleteCustomType, deleteCustomType,
listCustomTypes, listCustomTypes,
deleteDiagramCustomTypes, deleteDiagramCustomTypes,
getDiagramFilter,
updateDiagramFilter,
deleteDiagramFilter,
}} }}
> >
{children} {children}

View File

@@ -48,6 +48,7 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
handleThemeToggle, handleThemeToggle,
{ {
preventDefault: true, preventDefault: true,
enableOnFormTags: true,
}, },
[handleThemeToggle] [handleThemeToggle]
); );

View File

@@ -35,8 +35,22 @@ import type { OnChange } from '@monaco-editor/react';
import { useDebounce } from '@/hooks/use-debounce-v2'; import { useDebounce } from '@/hooks/use-debounce-v2';
import { InstructionsSection } from './instructions-section/instructions-section'; import { InstructionsSection } from './instructions-section/instructions-section';
import { parseSQLError } from '@/lib/data/sql-import'; import { parseSQLError } from '@/lib/data/sql-import';
import type { editor } from 'monaco-editor'; import type { editor, IDisposable } from 'monaco-editor';
import { waitFor } from '@/lib/utils'; import { waitFor } from '@/lib/utils';
import {
validateSQL,
type ValidationResult,
} from '@/lib/data/sql-import/sql-validator';
import { SQLValidationStatus } from './sql-validation-status';
const calculateContentSizeMB = (content: string): number => {
return content.length / (1024 * 1024); // Convert to MB
};
const calculateIsLargeFile = (content: string): boolean => {
const contentSizeMB = calculateContentSizeMB(content);
return contentSizeMB > 2; // Consider large if over 2MB
};
const errorScriptOutputMessage = const errorScriptOutputMessage =
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.'; 'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
@@ -118,6 +132,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
const { effectiveTheme } = useTheme(); const { effectiveTheme } = useTheme();
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null); const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const pasteDisposableRef = useRef<IDisposable | null>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const { isSm: isDesktop } = useBreakpoint('sm'); const { isSm: isDesktop } = useBreakpoint('sm');
@@ -125,6 +140,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false); const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
const [isCheckingJson, setIsCheckingJson] = useState(false); const [isCheckingJson, setIsCheckingJson] = useState(false);
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false); const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
const [sqlValidation, setSqlValidation] = useState<ValidationResult | null>(
null
);
const [isAutoFixing, setIsAutoFixing] = useState(false);
const [showAutoFixButton, setShowAutoFixButton] = useState(false);
useEffect(() => { useEffect(() => {
setScriptResult(''); setScriptResult('');
@@ -135,11 +155,33 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
// Check if the ddl is valid // Check if the ddl is valid
useEffect(() => { useEffect(() => {
if (importMethod !== 'ddl') { if (importMethod !== 'ddl') {
setSqlValidation(null);
setShowAutoFixButton(false);
return; return;
} }
if (!scriptResult.trim()) return; if (!scriptResult.trim()) {
setSqlValidation(null);
setShowAutoFixButton(false);
return;
}
// First run our validation based on database type
const validation = validateSQL(scriptResult, databaseType);
setSqlValidation(validation);
// If we have auto-fixable errors, show the auto-fix button
if (validation.fixedSQL && validation.errors.length > 0) {
setShowAutoFixButton(true);
// Don't try to parse invalid SQL
setErrorMessage('SQL contains syntax errors');
return;
}
// Hide auto-fix button if no fixes available
setShowAutoFixButton(false);
// Validate the SQL (either original or already fixed)
parseSQLError({ parseSQLError({
sqlContent: scriptResult, sqlContent: scriptResult,
sourceDatabaseType: databaseType, sourceDatabaseType: databaseType,
@@ -185,8 +227,44 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
} }
}, [errorMessage.length, onImport, scriptResult]); }, [errorMessage.length, onImport, scriptResult]);
const handleAutoFix = useCallback(() => {
if (sqlValidation?.fixedSQL) {
setIsAutoFixing(true);
setShowAutoFixButton(false);
setErrorMessage('');
// Apply the fix with a delay so user sees the fixing message
setTimeout(() => {
setScriptResult(sqlValidation.fixedSQL!);
setTimeout(() => {
setIsAutoFixing(false);
}, 100);
}, 1000);
}
}, [sqlValidation, setScriptResult]);
const handleErrorClick = useCallback((line: number) => {
if (editorRef.current) {
// Set cursor to the error line
editorRef.current.setPosition({ lineNumber: line, column: 1 });
editorRef.current.revealLineInCenter(line);
editorRef.current.focus();
}
}, []);
const formatEditor = useCallback(() => { const formatEditor = useCallback(() => {
if (editorRef.current) { if (editorRef.current) {
const model = editorRef.current.getModel();
if (model) {
const content = model.getValue();
// Skip formatting for large files (> 2MB)
if (calculateIsLargeFile(content)) {
return;
}
}
setTimeout(() => { setTimeout(() => {
editorRef.current editorRef.current
?.getAction('editor.action.formatDocument') ?.getAction('editor.action.formatDocument')
@@ -229,37 +307,69 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
setIsCheckingJson(false); setIsCheckingJson(false);
}, [scriptResult, setScriptResult, formatEditor]); }, [scriptResult, setScriptResult, formatEditor]);
const detectAndSetImportMethod = useCallback(() => {
const content = editorRef.current?.getValue();
if (content && content.trim()) {
const detectedType = detectContentType(content);
if (detectedType && detectedType !== importMethod) {
setImportMethod(detectedType);
}
}
}, [setImportMethod, importMethod]);
const [editorDidMount, setEditorDidMount] = useState(false);
useEffect(() => { useEffect(() => {
if (editorRef.current && editorDidMount) { // Cleanup paste handler on unmount
editorRef.current.onDidPaste(() => { return () => {
setTimeout(() => { if (pasteDisposableRef.current) {
editorRef.current pasteDisposableRef.current.dispose();
?.getAction('editor.action.formatDocument') pasteDisposableRef.current = null;
?.run();
}, 0);
setTimeout(detectAndSetImportMethod, 0);
});
} }
}, [detectAndSetImportMethod, editorDidMount]); };
}, []);
const handleEditorDidMount = useCallback( const handleEditorDidMount = useCallback(
(editor: editor.IStandaloneCodeEditor) => { (editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor; editorRef.current = editor;
setEditorDidMount(true);
// Cleanup previous disposable if it exists
if (pasteDisposableRef.current) {
pasteDisposableRef.current.dispose();
pasteDisposableRef.current = null;
}
// Add paste handler for all modes
const disposable = editor.onDidPaste(() => {
const model = editor.getModel();
if (!model) return;
const content = model.getValue();
// Skip formatting for large files (> 2MB) to prevent browser freezing
const isLargeFile = calculateIsLargeFile(content);
// First, detect content type to determine if we should switch modes
const detectedType = detectContentType(content);
if (detectedType && detectedType !== importMethod) {
// Switch to the detected mode immediately
setImportMethod(detectedType);
// Only format if it's JSON (query mode) AND file is not too large
if (detectedType === 'query' && !isLargeFile) {
// For JSON mode, format after a short delay
setTimeout(() => {
editor
.getAction('editor.action.formatDocument')
?.run();
}, 100);
}
// For DDL mode, do NOT format as it can break the SQL
} else {
// Content type didn't change, apply formatting based on current mode
if (importMethod === 'query' && !isLargeFile) {
// Only format JSON content if not too large
setTimeout(() => {
editor
.getAction('editor.action.formatDocument')
?.run();
}, 100);
}
// For DDL mode or large files, do NOT format
}
});
pasteDisposableRef.current = disposable;
}, },
[] [importMethod, setImportMethod]
); );
const renderHeader = useCallback(() => { const renderHeader = useCallback(() => {
@@ -316,7 +426,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
: 'dbml-light' : 'dbml-light'
} }
options={{ options={{
formatOnPaste: true, formatOnPaste: false, // Never format on paste - we handle it manually
minimap: { enabled: false }, minimap: { enabled: false },
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
automaticLayout: true, automaticLayout: true,
@@ -345,10 +455,13 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
</Suspense> </Suspense>
</div> </div>
{errorMessage ? ( {errorMessage || (importMethod === 'ddl' && sqlValidation) ? (
<div className="mt-2 flex shrink-0 items-center gap-2"> <SQLValidationStatus
<p className="text-xs text-red-700">{errorMessage}</p> validation={sqlValidation}
</div> errorMessage={errorMessage}
isAutoFixing={isAutoFixing}
onErrorClick={handleErrorClick}
/>
) : null} ) : null}
</div> </div>
), ),
@@ -359,6 +472,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
effectiveTheme, effectiveTheme,
debouncedHandleInputChange, debouncedHandleInputChange,
handleEditorDidMount, handleEditorDidMount,
sqlValidation,
isAutoFixing,
handleErrorClick,
] ]
); );
@@ -444,13 +560,28 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
) )
)} )}
</Button> </Button>
) : showAutoFixButton && importMethod === 'ddl' ? (
<Button
type="button"
variant="secondary"
onClick={handleAutoFix}
disabled={isAutoFixing}
className="bg-sky-600 text-white hover:bg-sky-700"
>
{isAutoFixing ? (
<Spinner size="small" />
) : (
'Try auto-fix'
)}
</Button>
) : keepDialogAfterImport ? ( ) : keepDialogAfterImport ? (
<Button <Button
type="button" type="button"
variant="default" variant="default"
disabled={ disabled={
scriptResult.trim().length === 0 || scriptResult.trim().length === 0 ||
errorMessage.length > 0 errorMessage.length > 0 ||
isAutoFixing
} }
onClick={handleImport} onClick={handleImport}
> >
@@ -463,7 +594,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
variant="default" variant="default"
disabled={ disabled={
scriptResult.trim().length === 0 || scriptResult.trim().length === 0 ||
errorMessage.length > 0 errorMessage.length > 0 ||
isAutoFixing
} }
onClick={handleImport} onClick={handleImport}
> >
@@ -496,6 +628,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
handleCheckJson, handleCheckJson,
goBack, goBack,
t, t,
importMethod,
isAutoFixing,
showAutoFixButton,
handleAutoFix,
]); ]);
return ( return (

View File

@@ -0,0 +1,179 @@
import React, { useMemo } from 'react';
import { CheckCircle, AlertTriangle, MessageCircleWarning } from 'lucide-react';
import { Alert, AlertDescription } from '@/components/alert/alert';
import type { ValidationResult } from '@/lib/data/sql-import/sql-validator';
import { Separator } from '@/components/separator/separator';
import { ScrollArea } from '@/components/scroll-area/scroll-area';
import { Spinner } from '@/components/spinner/spinner';
interface SQLValidationStatusProps {
validation?: ValidationResult | null;
errorMessage: string;
isAutoFixing?: boolean;
onErrorClick?: (line: number) => void;
}
export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
validation,
errorMessage,
isAutoFixing = false,
onErrorClick,
}) => {
const hasErrors = useMemo(
() => validation?.errors.length && validation.errors.length > 0,
[validation?.errors]
);
const hasWarnings = useMemo(
() => validation?.warnings && validation.warnings.length > 0,
[validation?.warnings]
);
const wasAutoFixed = useMemo(
() =>
validation?.warnings?.some((w) =>
w.message.includes('Auto-fixed')
) || false,
[validation?.warnings]
);
if (!validation && !errorMessage && !isAutoFixing) return null;
if (isAutoFixing) {
return (
<>
<Separator className="mb-1 mt-2" />
<div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
<div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
<div className="flex items-start gap-2">
<Spinner className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
<div className="flex-1 text-sm text-sky-700 dark:text-sky-300">
Auto-fixing SQL syntax errors...
</div>
</div>
</div>
</div>
</>
);
}
// If we have parser errors (errorMessage) after validation
if (errorMessage && !hasErrors) {
return (
<>
<Separator className="mb-1 mt-2" />
<div className="mb-1 flex shrink-0 items-center gap-2">
<p className="text-xs text-red-700">{errorMessage}</p>
</div>
</>
);
}
return (
<>
<Separator className="mb-1 mt-2" />
{hasErrors ? (
<div className="rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
<ScrollArea className="h-24">
<div className="space-y-3 p-3 pt-2 text-red-700 dark:text-red-300">
{validation?.errors
.slice(0, 3)
.map((error, idx) => (
<div
key={idx}
className="flex items-start gap-2"
>
<MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" />
<div className="flex-1 text-sm text-red-700 dark:text-red-300">
<button
onClick={() =>
onErrorClick?.(error.line)
}
className="rounded font-medium underline hover:text-red-600 focus:outline-none focus:ring-1 focus:ring-red-500 dark:hover:text-red-200"
type="button"
>
Line {error.line}
</button>
<span className="mx-1">:</span>
<span className="text-xs">
{error.message}
</span>
{error.suggestion && (
<div className="mt-1 flex items-start gap-2">
<span className="text-xs font-medium ">
{error.suggestion}
</span>
</div>
)}
</div>
</div>
))}
{validation?.errors &&
validation?.errors.length > 3 ? (
<div className="flex items-center gap-2">
<MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" />
<span className="text-xs font-medium">
{validation.errors.length - 3} more
error
{validation.errors.length - 3 > 1
? 's'
: ''}
</span>
</div>
) : null}
</div>
</ScrollArea>
</div>
) : null}
{wasAutoFixed && !hasErrors ? (
<Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">
<CheckCircle className="size-4 text-green-600 dark:text-green-400" />
<AlertDescription className="text-sm text-green-700 dark:text-green-300">
SQL syntax errors were automatically fixed. Your SQL is
now ready to import.
</AlertDescription>
</Alert>
) : null}
{hasWarnings && !hasErrors ? (
<div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
<ScrollArea className="h-24">
<div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
<div className="flex-1 text-sm text-sky-700 dark:text-sky-300">
<div className="mb-1 font-medium">
Import Info:
</div>
{validation?.warnings.map(
(warning, idx) => (
<div
key={idx}
className="ml-2 text-xs"
>
{warning.message}
</div>
)
)}
</div>
</div>
</div>
</ScrollArea>
</div>
) : null}
{!hasErrors && !hasWarnings && !errorMessage && validation ? (
<div className="rounded-md border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">
<div className="space-y-3 p-3 pt-2 text-green-700 dark:text-green-300">
<div className="flex items-start gap-2">
<CheckCircle className="mt-0.5 size-4 shrink-0 text-green-700 dark:text-green-300" />
<div className="flex-1 text-sm text-green-700 dark:text-green-300">
SQL syntax validated successfully
</div>
</div>
</div>
</div>
) : null}
</>
);
};

View File

@@ -0,0 +1,2 @@
export const MAX_TABLES_IN_DIAGRAM = 500;
export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50;

View File

@@ -0,0 +1,683 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button } from '@/components/button/button';
import { Input } from '@/components/input/input';
import { Search, AlertCircle, Check, X, View, Table } from 'lucide-react';
import { Checkbox } from '@/components/checkbox/checkbox';
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
import { cn } from '@/lib/utils';
import {
DialogDescription,
DialogFooter,
DialogHeader,
DialogInternalContent,
DialogTitle,
} from '@/components/dialog/dialog';
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
import { generateTableKey } from '@/lib/domain';
import { Spinner } from '@/components/spinner/spinner';
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationPrevious,
PaginationNext,
} from '@/components/pagination/pagination';
import { MAX_TABLES_IN_DIAGRAM } from './constants';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useTranslation } from 'react-i18next';
export interface SelectTablesProps {
databaseMetadata?: DatabaseMetadata;
onImport: ({
selectedTables,
databaseMetadata,
}: {
selectedTables?: SelectedTable[];
databaseMetadata?: DatabaseMetadata;
}) => Promise<void>;
onBack: () => void;
isLoading?: boolean;
}
const TABLES_PER_PAGE = 10;
interface TableInfo {
key: string;
schema?: string;
tableName: string;
fullName: string;
type: 'table' | 'view';
}
export const SelectTables: React.FC<SelectTablesProps> = ({
databaseMetadata,
onImport,
onBack,
isLoading = false,
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [showTables, setShowTables] = useState(true);
const [showViews, setShowViews] = useState(false);
const { t } = useTranslation();
const [isImporting, setIsImporting] = useState(false);
// Prepare all tables and views with their metadata
const allTables = useMemo(() => {
const tables: TableInfo[] = [];
// Add regular tables
databaseMetadata?.tables.forEach((table) => {
const schema = schemaNameToDomainSchemaName(table.schema);
const tableName = table.table;
const key = `table:${generateTableKey({ tableName, schemaName: schema })}`;
tables.push({
key,
schema,
tableName,
fullName: schema ? `${schema}.${tableName}` : tableName,
type: 'table',
});
});
// Add views
databaseMetadata?.views?.forEach((view) => {
const schema = schemaNameToDomainSchemaName(view.schema);
const viewName = view.view_name;
if (!viewName) {
return;
}
const key = `view:${generateTableKey({
tableName: viewName,
schemaName: schema,
})}`;
tables.push({
key,
schema,
tableName: viewName,
fullName:
schema === 'default' ? viewName : `${schema}.${viewName}`,
type: 'view',
});
});
return tables.sort((a, b) => a.fullName.localeCompare(b.fullName));
}, [databaseMetadata?.tables, databaseMetadata?.views]);
// Count tables and views separately
const tableCount = useMemo(
() => allTables.filter((t) => t.type === 'table').length,
[allTables]
);
const viewCount = useMemo(
() => allTables.filter((t) => t.type === 'view').length,
[allTables]
);
// Initialize selectedTables with all tables (not views) if less than 100 tables
const [selectedTables, setSelectedTables] = useState<Set<string>>(() => {
const tables = allTables.filter((t) => t.type === 'table');
if (tables.length < MAX_TABLES_IN_DIAGRAM) {
return new Set(tables.map((t) => t.key));
}
return new Set();
});
// Filter tables based on search term and type filters
const filteredTables = useMemo(() => {
let filtered = allTables;
// Filter by type
filtered = filtered.filter((table) => {
if (table.type === 'table' && !showTables) return false;
if (table.type === 'view' && !showViews) return false;
return true;
});
// Filter by search term
if (searchTerm.trim()) {
const searchLower = searchTerm.toLowerCase();
filtered = filtered.filter(
(table) =>
table.tableName.toLowerCase().includes(searchLower) ||
table.schema?.toLowerCase().includes(searchLower) ||
table.fullName.toLowerCase().includes(searchLower)
);
}
return filtered;
}, [allTables, searchTerm, showTables, showViews]);
// Calculate pagination
const totalPages = useMemo(
() => Math.max(1, Math.ceil(filteredTables.length / TABLES_PER_PAGE)),
[filteredTables.length]
);
const paginatedTables = useMemo(() => {
const startIndex = (currentPage - 1) * TABLES_PER_PAGE;
const endIndex = startIndex + TABLES_PER_PAGE;
return filteredTables.slice(startIndex, endIndex);
}, [filteredTables, currentPage]);
// Get currently visible selected tables
const visibleSelectedTables = useMemo(() => {
return paginatedTables.filter((table) => selectedTables.has(table.key));
}, [paginatedTables, selectedTables]);
const canAddMore = useMemo(
() => selectedTables.size < MAX_TABLES_IN_DIAGRAM,
[selectedTables.size]
);
const hasSearchResults = useMemo(
() => filteredTables.length > 0,
[filteredTables.length]
);
const allVisibleSelected = useMemo(
() =>
visibleSelectedTables.length === paginatedTables.length &&
paginatedTables.length > 0,
[visibleSelectedTables.length, paginatedTables.length]
);
const canSelectAllFiltered = useMemo(
() =>
filteredTables.length > 0 &&
filteredTables.some((table) => !selectedTables.has(table.key)) &&
canAddMore,
[filteredTables, selectedTables, canAddMore]
);
// Reset to first page when search changes
useEffect(() => {
setCurrentPage(1);
}, [searchTerm]);
const handleTableToggle = useCallback(
(tableKey: string) => {
const newSelected = new Set(selectedTables);
if (newSelected.has(tableKey)) {
newSelected.delete(tableKey);
} else if (selectedTables.size < MAX_TABLES_IN_DIAGRAM) {
newSelected.add(tableKey);
}
setSelectedTables(newSelected);
},
[selectedTables]
);
const handleTogglePageSelection = useCallback(() => {
const newSelected = new Set(selectedTables);
if (allVisibleSelected) {
// Deselect all on current page
for (const table of paginatedTables) {
newSelected.delete(table.key);
}
} else {
// Select all on current page
for (const table of paginatedTables) {
if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
newSelected.add(table.key);
}
}
setSelectedTables(newSelected);
}, [allVisibleSelected, paginatedTables, selectedTables]);
const handleSelectAllFiltered = useCallback(() => {
const newSelected = new Set(selectedTables);
for (const table of filteredTables) {
if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
newSelected.add(table.key);
}
setSelectedTables(newSelected);
}, [filteredTables, selectedTables]);
const handleNextPage = useCallback(() => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
}, [currentPage, totalPages]);
const handlePrevPage = useCallback(() => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
}, [currentPage]);
const handleClearSelection = useCallback(() => {
setSelectedTables(new Set());
}, []);
const handleConfirm = useCallback(async () => {
if (isImporting) {
return;
}
setIsImporting(true);
try {
const selectedTableObjects: SelectedTable[] = Array.from(
selectedTables
)
.map((key): SelectedTable | null => {
const table = allTables.find((t) => t.key === key);
if (!table) return null;
return {
schema: table.schema,
table: table.tableName,
type: table.type,
} satisfies SelectedTable;
})
.filter((t): t is SelectedTable => t !== null);
await onImport({
selectedTables: selectedTableObjects,
databaseMetadata,
});
} finally {
setIsImporting(false);
}
}, [selectedTables, allTables, onImport, databaseMetadata, isImporting]);
const { isMd: isDesktop } = useBreakpoint('md');
const renderPagination = useCallback(
() => (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={handlePrevPage}
className={cn(
'cursor-pointer',
currentPage === 1 &&
'pointer-events-none opacity-50'
)}
/>
</PaginationItem>
<PaginationItem>
<span className="px-3 text-sm text-muted-foreground">
Page {currentPage} of {totalPages}
</span>
</PaginationItem>
<PaginationItem>
<PaginationNext
onClick={handleNextPage}
className={cn(
'cursor-pointer',
(currentPage >= totalPages ||
filteredTables.length === 0) &&
'pointer-events-none opacity-50'
)}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
),
[
currentPage,
totalPages,
handlePrevPage,
handleNextPage,
filteredTables.length,
]
);
if (isLoading) {
return (
<div className="flex h-[400px] items-center justify-center">
<div className="text-center">
<Spinner className="mb-4" />
<p className="text-sm text-muted-foreground">
Parsing database metadata...
</p>
</div>
</div>
);
}
return (
<>
<DialogHeader>
<DialogTitle>Select Tables to Import</DialogTitle>
<DialogDescription>
{tableCount} {tableCount === 1 ? 'table' : 'tables'}
{viewCount > 0 && (
<>
{' and '}
{viewCount} {viewCount === 1 ? 'view' : 'views'}
</>
)}
{' found. '}
{allTables.length > MAX_TABLES_IN_DIAGRAM
? `Select up to ${MAX_TABLES_IN_DIAGRAM} to import.`
: 'Choose which ones to import.'}
</DialogDescription>
</DialogHeader>
<DialogInternalContent>
<div className="flex h-full flex-col space-y-4">
{/* Warning/Info Banner */}
{allTables.length > MAX_TABLES_IN_DIAGRAM ? (
<div
className={cn(
'flex items-center gap-2 rounded-lg p-3 text-sm',
'bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200'
)}
>
<AlertCircle className="size-4 shrink-0" />
<span>
Due to performance limitations, you can import a
maximum of {MAX_TABLES_IN_DIAGRAM} tables.
</span>
</div>
) : null}
{/* Search Input */}
<div className="relative">
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search tables..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="px-9"
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="size-4" />
</button>
)}
</div>
{/* Selection Status and Actions - Responsive layout */}
<div className="flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
{/* Left side: selection count -> checkboxes -> results found */}
<div className="flex flex-col items-center gap-3 text-sm sm:flex-row sm:items-center sm:gap-4">
<div className="flex flex-col items-center gap-1 sm:flex-row sm:items-center sm:gap-4">
<span className="text-center font-medium">
{selectedTables.size} /{' '}
{Math.min(
MAX_TABLES_IN_DIAGRAM,
allTables.length
)}{' '}
items selected
</span>
</div>
<div className="flex items-center gap-3 sm:border-x sm:px-4">
<div className="flex items-center gap-2">
<Checkbox
checked={showTables}
onCheckedChange={(checked) => {
// Prevent unchecking if it's the only one checked
if (!checked && !showViews) return;
setShowTables(!!checked);
}}
/>
<Table
className="size-4"
strokeWidth={1.5}
/>
<span>tables</span>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={showViews}
onCheckedChange={(checked) => {
// Prevent unchecking if it's the only one checked
if (!checked && !showTables) return;
setShowViews(!!checked);
}}
/>
<View
className="size-4"
strokeWidth={1.5}
/>
<span>views</span>
</div>
</div>
<span className="hidden text-muted-foreground sm:inline">
{filteredTables.length}{' '}
{filteredTables.length === 1
? 'result'
: 'results'}{' '}
found
</span>
</div>
{/* Right side: action buttons */}
<div className="flex flex-wrap items-center justify-center gap-2">
{hasSearchResults && (
<>
{/* Show page selection button when not searching and no selection */}
{!searchTerm &&
selectedTables.size === 0 && (
<Button
variant="outline"
size="sm"
onClick={
handleTogglePageSelection
}
disabled={
paginatedTables.length === 0
}
>
{allVisibleSelected
? 'Deselect'
: 'Select'}{' '}
page
</Button>
)}
{/* Show Select all button when there are unselected tables */}
{canSelectAllFiltered &&
selectedTables.size === 0 && (
<Button
variant="outline"
size="sm"
onClick={
handleSelectAllFiltered
}
disabled={!canSelectAllFiltered}
title={(() => {
const unselectedCount =
filteredTables.filter(
(table) =>
!selectedTables.has(
table.key
)
).length;
const remainingCapacity =
MAX_TABLES_IN_DIAGRAM -
selectedTables.size;
if (
unselectedCount >
remainingCapacity
) {
return `Can only select ${remainingCapacity} more tables (${MAX_TABLES_IN_DIAGRAM} max limit)`;
}
return undefined;
})()}
>
{(() => {
const unselectedCount =
filteredTables.filter(
(table) =>
!selectedTables.has(
table.key
)
).length;
const remainingCapacity =
MAX_TABLES_IN_DIAGRAM -
selectedTables.size;
if (
unselectedCount >
remainingCapacity
) {
return `Select ${remainingCapacity} of ${unselectedCount}`;
}
return `Select all ${unselectedCount}`;
})()}
</Button>
)}
</>
)}
{selectedTables.size > 0 && (
<>
{/* Show page selection/deselection button when user has selections */}
{paginatedTables.length > 0 && (
<Button
variant="outline"
size="sm"
onClick={handleTogglePageSelection}
>
{allVisibleSelected
? 'Deselect'
: 'Select'}{' '}
page
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={handleClearSelection}
>
Clear selection
</Button>
</>
)}
</div>
</div>
</div>
{/* Table List */}
<div className="flex min-h-[428px] flex-1 flex-col">
{hasSearchResults ? (
<>
<div className="flex-1 py-4">
<div className="space-y-1">
{paginatedTables.map((table) => {
const isSelected = selectedTables.has(
table.key
);
const isDisabled =
!isSelected &&
selectedTables.size >=
MAX_TABLES_IN_DIAGRAM;
return (
<div
key={table.key}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
{
'cursor-not-allowed':
isDisabled,
'bg-muted hover:bg-muted/80':
isSelected,
'hover:bg-accent':
!isSelected &&
!isDisabled,
}
)}
>
<Checkbox
checked={isSelected}
disabled={isDisabled}
onCheckedChange={() =>
handleTableToggle(
table.key
)
}
/>
{table.type === 'view' ? (
<View
className="size-4"
strokeWidth={1.5}
/>
) : (
<Table
className="size-4"
strokeWidth={1.5}
/>
)}
<span className="flex-1">
{table.schema ? (
<span className="text-muted-foreground">
{table.schema}.
</span>
) : null}
<span className="font-medium">
{table.tableName}
</span>
{table.type === 'view' && (
<span className="ml-2 text-xs text-muted-foreground">
(view)
</span>
)}
</span>
{isSelected && (
<Check className="size-4 text-pink-600" />
)}
</div>
);
})}
</div>
</div>
</>
) : (
<div className="flex h-full items-center justify-center py-4">
<p className="text-sm text-muted-foreground">
{searchTerm
? 'No tables found matching your search.'
: 'Start typing to search for tables...'}
</p>
</div>
)}
</div>
{isDesktop ? renderPagination() : null}
</DialogInternalContent>
<DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2 md:justify-between md:gap-0">
<Button
type="button"
variant="secondary"
onClick={onBack}
disabled={isImporting}
>
{t('new_diagram_dialog.back')}
</Button>
<Button
onClick={handleConfirm}
disabled={selectedTables.size === 0 || isImporting}
className="bg-pink-500 text-white hover:bg-pink-600"
>
{isImporting ? (
<>
<Spinner className="mr-2 size-4 text-white" />
Importing...
</>
) : (
`Import ${selectedTables.size} Tables`
)}
</Button>
{!isDesktop ? renderPagination() : null}
</DialogFooter>
</>
);
};

View File

@@ -1,4 +1,5 @@
export enum CreateDiagramDialogStep { export enum CreateDiagramDialogStep {
SELECT_DATABASE = 'SELECT_DATABASE', SELECT_DATABASE = 'SELECT_DATABASE',
IMPORT_DATABASE = 'IMPORT_DATABASE', IMPORT_DATABASE = 'IMPORT_DATABASE',
SELECT_TABLES = 'SELECT_TABLES',
} }

View File

@@ -15,9 +15,13 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
import { SelectDatabase } from './select-database/select-database'; import { SelectDatabase } from './select-database/select-database';
import { CreateDiagramDialogStep } from './create-diagram-dialog-step'; import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
import { ImportDatabase } from '../common/import-database/import-database'; import { ImportDatabase } from '../common/import-database/import-database';
import { SelectTables } from '../common/select-tables/select-tables';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props'; import type { BaseDialogProps } from '../common/base-dialog-props';
import { sqlImportToDiagram } from '@/lib/data/sql-import'; import { sqlImportToDiagram } from '@/lib/data/sql-import';
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
export interface CreateDiagramDialogProps extends BaseDialogProps {} export interface CreateDiagramDialogProps extends BaseDialogProps {}
@@ -42,6 +46,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
const { listDiagrams, addDiagram } = useStorage(); const { listDiagrams, addDiagram } = useStorage();
const [diagramNumber, setDiagramNumber] = useState<number>(1); const [diagramNumber, setDiagramNumber] = useState<number>(1);
const navigate = useNavigate(); const navigate = useNavigate();
const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
const [isParsingMetadata, setIsParsingMetadata] = useState(false);
useEffect(() => { useEffect(() => {
setDatabaseEdition(undefined); setDatabaseEdition(undefined);
@@ -62,11 +68,19 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
setDatabaseEdition(undefined); setDatabaseEdition(undefined);
setScriptResult(''); setScriptResult('');
setImportMethod('query'); setImportMethod('query');
setParsedMetadata(undefined);
}, [dialog.open]); }, [dialog.open]);
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0; const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
const importNewDiagram = useCallback(async () => { const importNewDiagram = useCallback(
async ({
selectedTables,
databaseMetadata,
}: {
selectedTables?: SelectedTable[];
databaseMetadata?: DatabaseMetadata;
} = {}) => {
let diagram: Diagram | undefined; let diagram: Diagram | undefined;
if (importMethod === 'ddl') { if (importMethod === 'ddl') {
@@ -76,12 +90,22 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
targetDatabaseType: databaseType, targetDatabaseType: databaseType,
}); });
} else { } else {
const databaseMetadata: DatabaseMetadata = let metadata: DatabaseMetadata | undefined = databaseMetadata;
loadDatabaseMetadata(scriptResult);
if (!metadata) {
metadata = loadDatabaseMetadata(scriptResult);
}
if (selectedTables && selectedTables.length > 0) {
metadata = filterMetadataByTables({
metadata,
selectedTables,
});
}
diagram = await loadFromDatabaseMetadata({ diagram = await loadFromDatabaseMetadata({
databaseType, databaseType,
databaseMetadata, databaseMetadata: metadata,
diagramNumber, diagramNumber,
databaseEdition: databaseEdition:
databaseEdition?.trim().length === 0 databaseEdition?.trim().length === 0
@@ -91,10 +115,14 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
} }
await addDiagram({ diagram }); await addDiagram({ diagram });
await updateConfig({ config: { defaultDiagramId: diagram.id } }); await updateConfig({
config: { defaultDiagramId: diagram.id },
});
closeCreateDiagramDialog(); closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`); navigate(`/diagrams/${diagram.id}`);
}, [ },
[
importMethod, importMethod,
databaseType, databaseType,
addDiagram, addDiagram,
@@ -104,7 +132,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
updateConfig, updateConfig,
scriptResult, scriptResult,
diagramNumber, diagramNumber,
]); ]
);
const createEmptyDiagram = useCallback(async () => { const createEmptyDiagram = useCallback(async () => {
const diagram: Diagram = { const diagram: Diagram = {
@@ -138,10 +167,56 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
openImportDBMLDialog, openImportDBMLDialog,
]); ]);
const importNewDiagramOrFilterTables = useCallback(async () => {
try {
setIsParsingMetadata(true);
if (importMethod === 'ddl') {
await importNewDiagram();
} else {
// Parse metadata asynchronously to avoid blocking the UI
const metadata = await new Promise<DatabaseMetadata>(
(resolve, reject) => {
setTimeout(() => {
try {
const result =
loadDatabaseMetadata(scriptResult);
resolve(result);
} catch (err) {
reject(err);
}
}, 0);
}
);
const totalTablesAndViews =
metadata.tables.length + (metadata.views?.length || 0);
setParsedMetadata(metadata);
// Check if it's a large database that needs table selection
if (totalTablesAndViews > MAX_TABLES_WITHOUT_SHOWING_FILTER) {
setStep(CreateDiagramDialogStep.SELECT_TABLES);
} else {
await importNewDiagram({
databaseMetadata: metadata,
});
}
}
} finally {
setIsParsingMetadata(false);
}
}, [importMethod, scriptResult, importNewDiagram]);
return ( return (
<Dialog <Dialog
{...dialog} {...dialog}
onOpenChange={(open) => { onOpenChange={(open) => {
// Don't allow closing while parsing metadata
if (isParsingMetadata) {
return;
}
if (!hasExistingDiagram) { if (!hasExistingDiagram) {
return; return;
} }
@@ -154,6 +229,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
<DialogContent <DialogContent
className="flex max-h-dvh w-full flex-col md:max-w-[900px]" className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
showClose={hasExistingDiagram} showClose={hasExistingDiagram}
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
> >
{step === CreateDiagramDialogStep.SELECT_DATABASE ? ( {step === CreateDiagramDialogStep.SELECT_DATABASE ? (
<SelectDatabase <SelectDatabase
@@ -165,9 +242,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
setStep(CreateDiagramDialogStep.IMPORT_DATABASE) setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
} }
/> />
) : ( ) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
<ImportDatabase <ImportDatabase
onImport={importNewDiagram} onImport={importNewDiagramOrFilterTables}
onCreateEmptyDiagram={createEmptyDiagram} onCreateEmptyDiagram={createEmptyDiagram}
databaseEdition={databaseEdition} databaseEdition={databaseEdition}
databaseType={databaseType} databaseType={databaseType}
@@ -180,8 +257,18 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
title={t('new_diagram_dialog.import_database.title')} title={t('new_diagram_dialog.import_database.title')}
importMethod={importMethod} importMethod={importMethod}
setImportMethod={setImportMethod} setImportMethod={setImportMethod}
keepDialogAfterImport={true}
/> />
)} ) : step === CreateDiagramDialogStep.SELECT_TABLES ? (
<SelectTables
isLoading={isParsingMetadata || !parsedMetadata}
databaseMetadata={parsedMetadata}
onImport={importNewDiagram}
onBack={() =>
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
}
/>
) : null}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import React, {
Suspense, Suspense,
useRef, useRef,
} from 'react'; } from 'react';
import * as monaco from 'monaco-editor'; import type * as monaco from 'monaco-editor';
import { useDialog } from '@/hooks/use-dialog'; import { useDialog } from '@/hooks/use-dialog';
import { import {
Dialog, Dialog,
@@ -23,53 +23,24 @@ import { useTranslation } from 'react-i18next';
import { Editor } from '@/components/code-snippet/code-snippet'; import { Editor } from '@/components/code-snippet/code-snippet';
import { useTheme } from '@/hooks/use-theme'; import { useTheme } from '@/hooks/use-theme';
import { AlertCircle } from 'lucide-react'; import { AlertCircle } from 'lucide-react';
import { importDBMLToDiagram, sanitizeDBML } from '@/lib/dbml-import'; import {
importDBMLToDiagram,
sanitizeDBML,
preprocessDBML,
} from '@/lib/dbml/dbml-import/dbml-import';
import { useChartDB } from '@/hooks/use-chartdb'; import { useChartDB } from '@/hooks/use-chartdb';
import { Parser } from '@dbml/core'; import { Parser } from '@dbml/core';
import { useCanvas } from '@/hooks/use-canvas'; import { useCanvas } from '@/hooks/use-canvas';
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language'; import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
import type { DBTable } from '@/lib/domain/db-table';
import { useToast } from '@/components/toast/use-toast'; import { useToast } from '@/components/toast/use-toast';
import { Spinner } from '@/components/spinner/spinner'; import { Spinner } from '@/components/spinner/spinner';
import { debounce } from '@/lib/utils'; import { debounce } from '@/lib/utils';
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
interface DBMLError { import {
message: string; clearErrorHighlight,
line: number; highlightErrorLine,
column: number; } from '@/components/code-snippet/dbml/utils';
}
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;
}
export interface ImportDBMLDialogProps extends BaseDialogProps { export interface ImportDBMLDialogProps extends BaseDialogProps {
withCreateEmptyDiagram?: boolean; withCreateEmptyDiagram?: boolean;
@@ -145,39 +116,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
} }
}, [reorder, reorderTables]); }, [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(() => { const clearDecorations = useCallback(() => {
decorationsCollection.current?.clear(); clearErrorHighlight(decorationsCollection.current);
}, []); }, []);
const validateDBML = useCallback( const validateDBML = useCallback(
@@ -189,7 +129,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
if (!content.trim()) return; if (!content.trim()) return;
try { try {
const sanitizedContent = sanitizeDBML(content); const preprocessedContent = preprocessDBML(content);
const sanitizedContent = sanitizeDBML(preprocessedContent);
const parser = new Parser(); const parser = new Parser();
parser.parse(sanitizedContent, 'dbml'); parser.parse(sanitizedContent, 'dbml');
} catch (e) { } catch (e) {
@@ -199,7 +140,12 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
t('import_dbml_dialog.error.description') + t('import_dbml_dialog.error.description') +
` (1 error found - in line ${parsedError.line})` ` (1 error found - in line ${parsedError.line})`
); );
highlightErrorLine(parsedError); highlightErrorLine({
error: parsedError,
model: editorRef.current?.getModel(),
editorDecorationsCollection:
decorationsCollection.current,
});
} else { } else {
setErrorMessage( setErrorMessage(
e instanceof Error ? e.message : JSON.stringify(e) e instanceof Error ? e.message : JSON.stringify(e)
@@ -207,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); const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
@@ -242,13 +188,11 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
if (!dbmlContent.trim() || errorMessage) return; if (!dbmlContent.trim() || errorMessage) return;
try { try {
// Sanitize DBML content before importing const importedDiagram = await importDBMLToDiagram(dbmlContent);
const sanitizedContent = sanitizeDBML(dbmlContent);
const importedDiagram = await importDBMLToDiagram(sanitizedContent);
const tableIdsToRemove = tables const tableIdsToRemove = tables
.filter((table) => .filter((table) =>
importedDiagram.tables?.some( importedDiagram.tables?.some(
(t) => (t: DBTable) =>
t.name === table.name && t.schema === table.schema t.name === table.name && t.schema === table.schema
) )
) )
@@ -257,19 +201,21 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
const relationshipIdsToRemove = relationships const relationshipIdsToRemove = relationships
.filter((relationship) => { .filter((relationship) => {
const sourceTable = tables.find( const sourceTable = tables.find(
(table) => table.id === relationship.sourceTableId (table: DBTable) =>
table.id === relationship.sourceTableId
); );
const targetTable = tables.find( const targetTable = tables.find(
(table) => table.id === relationship.targetTableId (table: DBTable) =>
table.id === relationship.targetTableId
); );
if (!sourceTable || !targetTable) return true; if (!sourceTable || !targetTable) return true;
const replacementSourceTable = importedDiagram.tables?.find( const replacementSourceTable = importedDiagram.tables?.find(
(table) => (table: DBTable) =>
table.name === sourceTable.name && table.name === sourceTable.name &&
table.schema === sourceTable.schema table.schema === sourceTable.schema
); );
const replacementTargetTable = importedDiagram.tables?.find( const replacementTargetTable = importedDiagram.tables?.find(
(table) => (table: DBTable) =>
table.name === targetTable.name && table.name === targetTable.name &&
table.schema === targetTable.schema table.schema === targetTable.schema
); );

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDialog } from '@/hooks/use-dialog'; import { useDialog } from '@/hooks/use-dialog';
import { import {
Dialog, Dialog,
@@ -17,11 +17,23 @@ import type { DBSchema } from '@/lib/domain/db-schema';
import { schemaNameToSchemaId } from '@/lib/domain/db-schema'; import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
import type { BaseDialogProps } from '../common/base-dialog-props'; import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Input } from '@/components/input/input';
import { Separator } from '@/components/separator/separator';
import { Group, SquarePlus } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useChartDB } from '@/hooks/use-chartdb';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { Label } from '@/components/label/label';
export interface TableSchemaDialogProps extends BaseDialogProps { export interface TableSchemaDialogProps extends BaseDialogProps {
table?: DBTable; table?: DBTable;
schemas: DBSchema[]; schemas: DBSchema[];
onConfirm: (schema: string) => void; onConfirm: ({ schema }: { schema: DBSchema }) => void;
allowSchemaCreation?: boolean;
} }
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
@@ -29,27 +41,73 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
table, table,
schemas, schemas,
onConfirm, onConfirm,
allowSchemaCreation = false,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedSchema, setSelectedSchema] = React.useState<string>( const { databaseType } = useChartDB();
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
table?.schema table?.schema
? schemaNameToSchemaId(table.schema) ? schemaNameToSchemaId(table.schema)
: (schemas?.[0]?.id ?? '') : (schemas?.[0]?.id ?? '')
); );
const allowSchemaSelection = useMemo(
() => schemas && schemas.length > 0,
[schemas]
);
const defaultSchemaName = useMemo(
() => defaultSchemas?.[databaseType],
[databaseType]
);
const [isCreatingNew, setIsCreatingNew] =
useState<boolean>(!allowSchemaSelection);
const [newSchemaName, setNewSchemaName] = useState<string>(
allowSchemaCreation && !allowSchemaSelection
? (defaultSchemaName ?? '')
: ''
);
useEffect(() => { useEffect(() => {
if (!dialog.open) return; if (!dialog.open) return;
setSelectedSchema( setSelectedSchemaId(
table?.schema table?.schema
? schemaNameToSchemaId(table.schema) ? schemaNameToSchemaId(table.schema)
: (schemas?.[0]?.id ?? '') : (schemas?.[0]?.id ?? '')
); );
}, [dialog.open, schemas, table?.schema]); setIsCreatingNew(!allowSchemaSelection);
setNewSchemaName(
allowSchemaCreation && !allowSchemaSelection
? (defaultSchemaName ?? '')
: ''
);
}, [
defaultSchemaName,
dialog.open,
schemas,
table?.schema,
allowSchemaSelection,
allowSchemaCreation,
]);
const { closeTableSchemaDialog } = useDialog(); const { closeTableSchemaDialog } = useDialog();
const handleConfirm = useCallback(() => { const handleConfirm = useCallback(() => {
onConfirm(selectedSchema); if (isCreatingNew && newSchemaName.trim()) {
}, [onConfirm, selectedSchema]); const newSchema: DBSchema = {
id: schemaNameToSchemaId(newSchemaName.trim()),
name: newSchemaName.trim(),
tableCount: 0,
};
onConfirm({ schema: newSchema });
} else {
const schema = schemas.find((s) => s.id === selectedSchemaId);
if (!schema) return;
onConfirm({ schema });
}
}, [onConfirm, selectedSchemaId, schemas, isCreatingNew, newSchemaName]);
const schemaOptions: SelectBoxOption[] = useMemo( const schemaOptions: SelectBoxOption[] = useMemo(
() => () =>
@@ -60,6 +118,25 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
[schemas] [schemas]
); );
const renderSwitchCreateOrSelectButton = useCallback(
() => (
<Button
variant="outline"
className="w-full justify-start"
onClick={() => setIsCreatingNew(!isCreatingNew)}
disabled={!allowSchemaSelection || !allowSchemaCreation}
>
{!isCreatingNew ? (
<SquarePlus className="mr-2 size-4 " />
) : (
<Group className="mr-2 size-4 " />
)}
{isCreatingNew ? 'Select existing schema' : 'Create new schema'}
</Button>
),
[isCreatingNew, allowSchemaSelection, allowSchemaCreation]
);
return ( return (
<Dialog <Dialog
{...dialog} {...dialog}
@@ -67,17 +144,23 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
if (!open) { if (!open) {
closeTableSchemaDialog(); closeTableSchemaDialog();
} }
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
}} }}
> >
<DialogContent className="flex flex-col" showClose> <DialogContent className="flex flex-col" showClose>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{table {!allowSchemaSelection && allowSchemaCreation
? t('create_table_schema_dialog.title')
: table
? t('update_table_schema_dialog.title') ? t('update_table_schema_dialog.title')
: t('new_table_schema_dialog.title')} : t('new_table_schema_dialog.title')}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{table {!allowSchemaSelection && allowSchemaCreation
? t('create_table_schema_dialog.description')
: table
? t('update_table_schema_dialog.description', { ? t('update_table_schema_dialog.description', {
tableName: table.name, tableName: table.name,
}) })
@@ -86,27 +169,79 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-1"> <div className="grid gap-4 py-1">
<div className="grid w-full items-center gap-4"> <div className="grid w-full items-center gap-4">
{!isCreatingNew ? (
<SelectBox <SelectBox
options={schemaOptions} options={schemaOptions}
multiple={false} multiple={false}
value={selectedSchema} value={selectedSchemaId}
onChange={(value) => onChange={(value) =>
setSelectedSchema(value as string) setSelectedSchemaId(value as string)
} }
/> />
) : (
<div className="flex flex-col gap-2">
{allowSchemaCreation &&
!allowSchemaSelection ? (
<Label htmlFor="new-schema-name">
Schema Name
</Label>
) : null}
<Input
id="new-schema-name"
value={newSchemaName}
onChange={(e) =>
setNewSchemaName(e.target.value)
}
placeholder={`Enter schema name.${defaultSchemaName ? ` e.g. ${defaultSchemaName}.` : ''}`}
autoFocus
/>
</div>
)}
{allowSchemaCreation && allowSchemaSelection ? (
<>
<div className="relative">
<Separator className="my-2" />
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-2 text-xs text-muted-foreground">
or
</span>
</div>
{allowSchemaSelection ? (
renderSwitchCreateOrSelectButton()
) : (
<Tooltip>
<TooltipTrigger asChild>
<span>
{renderSwitchCreateOrSelectButton()}
</span>
</TooltipTrigger>
<TooltipContent>
<p>No existing schemas available</p>
</TooltipContent>
</Tooltip>
)}
</>
) : null}
</div> </div>
</div> </div>
<DialogFooter className="flex gap-1 md:justify-between"> <DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild> <DialogClose asChild>
<Button variant="secondary"> <Button variant="secondary">
{table {isCreatingNew
? t('create_table_schema_dialog.cancel')
: table
? t('update_table_schema_dialog.cancel') ? t('update_table_schema_dialog.cancel')
: t('new_table_schema_dialog.cancel')} : t('new_table_schema_dialog.cancel')}
</Button> </Button>
</DialogClose> </DialogClose>
<DialogClose asChild> <DialogClose asChild>
<Button onClick={handleConfirm}> <Button
{table onClick={handleConfirm}
disabled={isCreatingNew && !newSchemaName.trim()}
>
{isCreatingNew
? t('create_table_schema_dialog.create')
: table
? t('update_table_schema_dialog.confirm') ? t('update_table_schema_dialog.confirm')
: t('new_table_schema_dialog.confirm')} : t('new_table_schema_dialog.confirm')}
</Button> </Button>

View File

@@ -83,6 +83,7 @@
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
overscroll-behavior-x: none;
} }
.text-editable { .text-editable {
@@ -154,3 +155,29 @@
background-size: 650%; 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

@@ -23,23 +23,25 @@ import { bn, bnMetadata } from './locales/bn';
import { gu, guMetadata } from './locales/gu'; import { gu, guMetadata } from './locales/gu';
import { vi, viMetadata } from './locales/vi'; import { vi, viMetadata } from './locales/vi';
import { ar, arMetadata } from './locales/ar'; import { ar, arMetadata } from './locales/ar';
import { hr, hrMetadata } from './locales/hr';
export const languages: LanguageMetadata[] = [ export const languages: LanguageMetadata[] = [
enMetadata, enMetadata,
esMetadata,
frMetadata, frMetadata,
deMetadata, deMetadata,
esMetadata,
ukMetadata,
ruMetadata,
trMetadata,
hrMetadata,
pt_BRMetadata,
hiMetadata, hiMetadata,
jaMetadata, jaMetadata,
ko_KRMetadata, ko_KRMetadata,
pt_BRMetadata,
ukMetadata,
ruMetadata,
zh_CNMetadata, zh_CNMetadata,
zh_TWMetadata, zh_TWMetadata,
neMetadata, neMetadata,
mrMetadata, mrMetadata,
trMetadata,
id_IDMetadata, id_IDMetadata,
teMetadata, teMetadata,
bnMetadata, bnMetadata,
@@ -70,6 +72,7 @@ const resources = {
gu, gu,
vi, vi,
ar, ar,
hr,
}; };
i18n.use(LanguageDetector) i18n.use(LanguageDetector)

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ar: LanguageTranslation = { export const ar: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'جديد',
browse: 'تصفح',
tables: 'الجداول',
refs: 'المراجع',
areas: 'المناطق',
dependencies: 'التبعيات',
custom_types: 'الأنواع المخصصة',
},
menu: { menu: {
file: { databases: {
file: 'ملف', databases: 'قواعد البيانات',
new: 'جديد', new: 'مخطط جديد',
open: 'فتح', browse: 'تصفح...',
save: 'حفظ', save: 'حفظ',
import: 'استيراد قاعدة بيانات', import: 'استيراد قاعدة بيانات',
export_sql: 'SQL تصدير', export_sql: 'SQL تصدير',
export_as: 'تصدير كـ', export_as: 'تصدير كـ',
delete_diagram: 'حذف الرسم البياني', delete_diagram: 'حذف الرسم البياني',
exit: 'خروج',
}, },
edit: { edit: {
edit: 'تحرير', edit: 'تحرير',
@@ -26,7 +34,10 @@ export const ar: LanguageTranslation = {
hide_sidebar: 'إخفاء الشريط الجانبي', hide_sidebar: 'إخفاء الشريط الجانبي',
hide_cardinality: 'إخفاء الكاردينالية', hide_cardinality: 'إخفاء الكاردينالية',
show_cardinality: 'إظهار الكاردينالية', show_cardinality: 'إظهار الكاردينالية',
hide_field_attributes: 'إخفاء خصائص الحقل',
show_field_attributes: 'إظهار خصائص الحقل',
zoom_on_scroll: 'تكبير/تصغير عند التمرير', zoom_on_scroll: 'تكبير/تصغير عند التمرير',
show_views: 'عروض قاعدة البيانات',
theme: 'المظهر', theme: 'المظهر',
show_dependencies: 'إظهار الاعتمادات', show_dependencies: 'إظهار الاعتمادات',
hide_dependencies: 'إخفاء الاعتمادات', hide_dependencies: 'إخفاء الاعتمادات',
@@ -70,15 +81,6 @@ export const ar: LanguageTranslation = {
cancel: 'إلغاء', cancel: 'إلغاء',
}, },
multiple_schemas_alert: {
title: 'مخططات متعددة',
description:
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
dont_show_again: 'لا تظهره مجدداً',
change_schema: 'تغيير',
none: 'لا شيء',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'فشل النسخ', title: 'فشل النسخ',
@@ -113,14 +115,11 @@ export const ar: LanguageTranslation = {
copied: '!تم النسخ', copied: '!تم النسخ',
side_panel: { side_panel: {
schema: ':المخطط',
filter_by_schema: 'تصفية حسب المخطط',
search_schema: '...بحث في المخطط',
no_schemas_found: '.لم يتم العثور على مخططات',
view_all_options: '...عرض جميع الخيارات', view_all_options: '...عرض جميع الخيارات',
tables_section: { tables_section: {
tables: 'الجداول', tables: 'الجداول',
add_table: 'إضافة جدول', add_table: 'إضافة جدول',
add_view: 'إضافة عرض',
filter: 'تصفية', filter: 'تصفية',
collapse: 'طي الكل', collapse: 'طي الكل',
// TODO: Translate // TODO: Translate
@@ -146,16 +145,22 @@ export const ar: LanguageTranslation = {
field_actions: { field_actions: {
title: 'خصائص الحقل', title: 'خصائص الحقل',
unique: 'فريد', unique: 'فريد',
auto_increment: 'زيادة تلقائية',
comments: 'تعليقات', comments: 'تعليقات',
no_comments: 'لا يوجد تعليقات', no_comments: 'لا يوجد تعليقات',
delete_field: 'حذف الحقل', delete_field: 'حذف الحقل',
// TODO: Translate // TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'الدقة',
scale: 'النطاق',
default_value: 'Default Value',
no_default: 'No default',
}, },
index_actions: { index_actions: {
title: 'خصائص الفهرس', title: 'خصائص الفهرس',
name: 'الإسم', name: 'الإسم',
unique: 'فريد', unique: 'فريد',
index_type: 'نوع الفهرس',
delete_index: 'حذف الفهرس', delete_index: 'حذف الفهرس',
}, },
table_actions: { table_actions: {
@@ -172,12 +177,15 @@ export const ar: LanguageTranslation = {
description: 'أنشئ جدولاً للبدء', description: 'أنشئ جدولاً للبدء',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'العلاقات', refs: 'المراجع',
filter: 'تصفية', filter: 'تصفية',
add_relationship: 'إضافة علاقة',
collapse: 'طي الكل', collapse: 'طي الكل',
add_relationship: 'إضافة علاقة',
relationships: 'العلاقات',
dependencies: 'الاعتمادات',
relationship: { relationship: {
relationship: 'العلاقة',
primary: 'الجدول الأساسي', primary: 'الجدول الأساسي',
foreign: 'الجدول المرتبط', foreign: 'الجدول المرتبط',
cardinality: 'الكاردينالية', cardinality: 'الكاردينالية',
@@ -187,16 +195,8 @@ export const ar: LanguageTranslation = {
delete_relationship: 'حذف', delete_relationship: 'حذف',
}, },
}, },
empty_state: {
title: 'لا توجد علاقات',
description: 'إنشئ علاقة لربط الجداول',
},
},
dependencies_section: {
dependencies: 'الاعتمادات',
filter: 'تصفية',
collapse: 'طي الكل',
dependency: { dependency: {
dependency: 'الاعتماد',
table: 'الجدول', table: 'الجدول',
dependent_table: 'عرض الاعتمادات', dependent_table: 'عرض الاعتمادات',
delete_dependency: 'حذف', delete_dependency: 'حذف',
@@ -206,8 +206,8 @@ export const ar: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'لا توجد اعتمادات', title: 'لا توجد علاقات',
description: 'إنشاء اعتماد للبدء', description: 'إنشاء علاقة للبدء',
}, },
}, },
@@ -251,9 +251,12 @@ export const ar: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -269,6 +272,11 @@ export const ar: LanguageTranslation = {
redo: 'إعادة', redo: 'إعادة',
reorder_diagram: 'إعادة ترتيب الرسم البياني', reorder_diagram: 'إعادة ترتيب الرسم البياني',
highlight_overlapping_tables: 'تمييز الجداول المتداخلة', highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
// TODO: Translate
filter: 'Filter Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -400,6 +408,13 @@ export const ar: LanguageTranslation = {
cancel: 'إلغاء', cancel: 'إلغاء',
confirm: 'تغيير', confirm: 'تغيير',
}, },
create_table_schema_dialog: {
title: 'إنشاء مخطط جديد',
description:
'لا توجد مخططات حتى الآن. قم بإنشاء أول مخطط لتنظيم جداولك.',
create: 'إنشاء',
cancel: 'إلغاء',
},
star_us_dialog: { star_us_dialog: {
title: '!ساعدنا على التحسن', title: '!ساعدنا على التحسن',
@@ -453,6 +468,7 @@ export const ar: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'جدول جديد', new_table: 'جدول جديد',
new_view: 'عرض جديد',
new_relationship: 'علاقة جديدة', new_relationship: 'علاقة جديدة',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -474,6 +490,8 @@ export const ar: LanguageTranslation = {
language_select: { language_select: {
change_language: 'اللغة', change_language: 'اللغة',
}, },
on: 'تشغيل',
off: 'إيقاف',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const bn: LanguageTranslation = { export const bn: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'নতুন',
browse: 'ব্রাউজ',
tables: 'টেবিল',
refs: 'রেফস',
areas: 'এলাকা',
dependencies: 'নির্ভরতা',
custom_types: 'কাস্টম টাইপ',
},
menu: { menu: {
file: { databases: {
file: 'ফাইল', databases: 'ডাটাবেস',
new: 'নতুন', new: 'নতুন ডায়াগ্রাম',
open: 'খুলুন', browse: 'ব্রাউজ করুন...',
save: 'সংরক্ষণ করুন', save: 'সংরক্ষণ করুন',
import: 'ডাটাবেস আমদানি করুন', import: 'ডাটাবেস আমদানি করুন',
export_sql: 'SQL রপ্তানি করুন', export_sql: 'SQL রপ্তানি করুন',
export_as: 'রূপে রপ্তানি করুন', export_as: 'রূপে রপ্তানি করুন',
delete_diagram: 'ডায়াগ্রাম মুছুন', delete_diagram: 'ডায়াগ্রাম মুছুন',
exit: 'প্রস্থান করুন',
}, },
edit: { edit: {
edit: 'সম্পাদনা', edit: 'সম্পাদনা',
@@ -26,7 +34,10 @@ export const bn: LanguageTranslation = {
hide_sidebar: 'সাইডবার লুকান', hide_sidebar: 'সাইডবার লুকান',
hide_cardinality: 'কার্ডিনালিটি লুকান', hide_cardinality: 'কার্ডিনালিটি লুকান',
show_cardinality: 'কার্ডিনালিটি দেখান', show_cardinality: 'কার্ডিনালিটি দেখান',
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
zoom_on_scroll: 'স্ক্রলে জুম করুন', zoom_on_scroll: 'স্ক্রলে জুম করুন',
show_views: 'ডাটাবেস ভিউ',
theme: 'থিম', theme: 'থিম',
show_dependencies: 'নির্ভরতাগুলি দেখান', show_dependencies: 'নির্ভরতাগুলি দেখান',
hide_dependencies: 'নির্ভরতাগুলি লুকান', hide_dependencies: 'নির্ভরতাগুলি লুকান',
@@ -71,15 +82,6 @@ export const bn: LanguageTranslation = {
cancel: 'বাতিল করুন', cancel: 'বাতিল করুন',
}, },
multiple_schemas_alert: {
title: 'বহু স্কিমা',
description:
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
dont_show_again: 'পুনরায় দেখাবেন না',
change_schema: 'পরিবর্তন করুন',
none: 'কিছুই না',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'কপি ব্যর্থ হয়েছে', title: 'কপি ব্যর্থ হয়েছে',
@@ -114,14 +116,11 @@ export const bn: LanguageTranslation = {
copied: 'অনুলিপি সম্পন্ন!', copied: 'অনুলিপি সম্পন্ন!',
side_panel: { side_panel: {
schema: 'স্কিমা:',
filter_by_schema: 'স্কিমা দ্বারা ফিল্টার করুন',
search_schema: 'স্কিমা খুঁজুন...',
no_schemas_found: 'কোনো স্কিমা পাওয়া যায়নি।',
view_all_options: 'সমস্ত বিকল্প দেখুন...', view_all_options: 'সমস্ত বিকল্প দেখুন...',
tables_section: { tables_section: {
tables: 'টেবিল', tables: 'টেবিল',
add_table: 'টেবিল যোগ করুন', add_table: 'টেবিল যোগ করুন',
add_view: 'ভিউ যোগ করুন',
filter: 'ফিল্টার', filter: 'ফিল্টার',
collapse: 'সব ভাঁজ করুন', collapse: 'সব ভাঁজ করুন',
// TODO: Translate // TODO: Translate
@@ -147,16 +146,23 @@ export const bn: LanguageTranslation = {
field_actions: { field_actions: {
title: 'ফিল্ড কর্ম', title: 'ফিল্ড কর্ম',
unique: 'অদ্বিতীয়', unique: 'অদ্বিতীয়',
auto_increment: 'স্বয়ংক্রিয় বৃদ্ধি',
comments: 'মন্তব্য', comments: 'মন্তব্য',
no_comments: 'কোনো মন্তব্য নেই', no_comments: 'কোনো মন্তব্য নেই',
delete_field: 'ফিল্ড মুছুন', delete_field: 'ফিল্ড মুছুন',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'নির্ভুলতা',
scale: 'স্কেল',
}, },
index_actions: { index_actions: {
title: 'ইনডেক্স কর্ম', title: 'ইনডেক্স কর্ম',
name: 'নাম', name: 'নাম',
unique: 'অদ্বিতীয়', unique: 'অদ্বিতীয়',
index_type: 'ইনডেক্স ধরন',
delete_index: 'ইনডেক্স মুছুন', delete_index: 'ইনডেক্স মুছুন',
}, },
table_actions: { table_actions: {
@@ -173,14 +179,17 @@ export const bn: LanguageTranslation = {
description: 'শুরু করতে একটি টেবিল তৈরি করুন', description: 'শুরু করতে একটি টেবিল তৈরি করুন',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'সম্পর্ক', refs: 'রেফস',
filter: 'ফিল্টার', filter: 'ফিল্টার',
add_relationship: 'সম্পর্ক যোগ করুন',
collapse: 'সব ভাঁজ করুন', collapse: 'সব ভাঁজ করুন',
add_relationship: 'সম্পর্ক যোগ করুন',
relationships: 'সম্পর্ক',
dependencies: 'নির্ভরতাগুলি',
relationship: { relationship: {
relationship: 'সম্পর্ক',
primary: 'প্রাথমিক টেবিল', primary: 'প্রাথমিক টেবিল',
foreign: 'বিদেশি টেবিল', foreign: 'রেফারেন্স করা টেবিল',
cardinality: 'কার্ডিনালিটি', cardinality: 'কার্ডিনালিটি',
delete_relationship: 'মুছুন', delete_relationship: 'মুছুন',
relationship_actions: { relationship_actions: {
@@ -188,27 +197,19 @@ export const bn: LanguageTranslation = {
delete_relationship: 'মুছুন', delete_relationship: 'মুছুন',
}, },
}, },
empty_state: {
title: 'কোনো সম্পর্ক নেই',
description: 'টেবিল সংযোগ করতে একটি সম্পর্ক তৈরি করুন',
},
},
dependencies_section: {
dependencies: 'নির্ভরতাগুলি',
filter: 'ফিল্টার',
collapse: 'ভাঁজ করুন',
dependency: { dependency: {
dependency: 'নির্ভরতা',
table: 'টেবিল', table: 'টেবিল',
dependent_table: 'নির্ভরশীল টেবিল', dependent_table: 'নির্ভরশীল ভিউ',
delete_dependency: 'নির্ভরতা মুছুন', delete_dependency: 'মুছুন',
dependency_actions: { dependency_actions: {
title: 'কর্ম', title: 'কর্ম',
delete_dependency: 'নির্ভরতা মুছুন', delete_dependency: 'মুছুন',
}, },
}, },
empty_state: { empty_state: {
title: 'কোনো নির্ভরতাগুলি নেই', title: 'কোনো সম্পর্ক নেই',
description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।', description: 'শুরু করতে একটি সম্পর্ক তৈরি করুন',
}, },
}, },
@@ -251,9 +252,12 @@ export const bn: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -269,6 +273,12 @@ export const bn: LanguageTranslation = {
redo: 'পুনরায় করুন', redo: 'পুনরায় করুন',
reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন', reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন', highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -400,6 +410,13 @@ export const bn: LanguageTranslation = {
cancel: 'বাতিল করুন', cancel: 'বাতিল করুন',
confirm: 'পরিবর্তন করুন', confirm: 'পরিবর্তন করুন',
}, },
create_table_schema_dialog: {
title: 'নতুন স্কিমা তৈরি করুন',
description:
'এখনও কোনো স্কিমা নেই। আপনার টেবিলগুলি সংগঠিত করতে আপনার প্রথম স্কিমা তৈরি করুন।',
create: 'তৈরি করুন',
cancel: 'বাতিল করুন',
},
star_us_dialog: { star_us_dialog: {
title: 'আমাদের উন্নত করতে সাহায্য করুন!', title: 'আমাদের উন্নত করতে সাহায্য করুন!',
@@ -456,6 +473,7 @@ export const bn: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'নতুন টেবিল', new_table: 'নতুন টেবিল',
new_view: 'নতুন ভিউ',
new_relationship: 'নতুন সম্পর্ক', new_relationship: 'নতুন সম্পর্ক',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -477,6 +495,9 @@ export const bn: LanguageTranslation = {
language_select: { language_select: {
change_language: 'ভাষা পরিবর্তন করুন', change_language: 'ভাষা পরিবর্তন করুন',
}, },
on: 'চালু',
off: 'বন্ধ',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const de: LanguageTranslation = { export const de: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'Neu',
browse: 'Durchsuchen',
tables: 'Tabellen',
refs: 'Refs',
areas: 'Bereiche',
dependencies: 'Abhängigkeiten',
custom_types: 'Benutzerdefinierte Typen',
},
menu: { menu: {
file: { databases: {
file: 'Datei', databases: 'Datenbanken',
new: 'Neu', new: 'Neues Diagramm',
open: 'Öffnen', browse: 'Durchsuchen...',
save: 'Speichern', save: 'Speichern',
import: 'Datenbank importieren', import: 'Datenbank importieren',
export_sql: 'SQL exportieren', export_sql: 'SQL exportieren',
export_as: 'Exportieren als', export_as: 'Exportieren als',
delete_diagram: 'Diagramm löschen', delete_diagram: 'Diagramm löschen',
exit: 'Beenden',
}, },
edit: { edit: {
edit: 'Bearbeiten', edit: 'Bearbeiten',
@@ -26,7 +34,10 @@ export const de: LanguageTranslation = {
hide_sidebar: 'Seitenleiste ausblenden', hide_sidebar: 'Seitenleiste ausblenden',
hide_cardinality: 'Kardinalität ausblenden', hide_cardinality: 'Kardinalität ausblenden',
show_cardinality: 'Kardinalität anzeigen', show_cardinality: 'Kardinalität anzeigen',
hide_field_attributes: 'Feldattribute ausblenden',
show_field_attributes: 'Feldattribute anzeigen',
zoom_on_scroll: 'Zoom beim Scrollen', zoom_on_scroll: 'Zoom beim Scrollen',
show_views: 'Datenbankansichten',
theme: 'Stil', theme: 'Stil',
show_dependencies: 'Abhängigkeiten anzeigen', show_dependencies: 'Abhängigkeiten anzeigen',
hide_dependencies: 'Abhängigkeiten ausblenden', hide_dependencies: 'Abhängigkeiten ausblenden',
@@ -71,15 +82,6 @@ export const de: LanguageTranslation = {
cancel: 'Abbrechen', cancel: 'Abbrechen',
}, },
multiple_schemas_alert: {
title: 'Mehrere Schemas',
description:
'{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
dont_show_again: 'Nicht erneut anzeigen',
change_schema: 'Schema ändern',
none: 'Keine',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'Kopieren fehlgeschlagen', title: 'Kopieren fehlgeschlagen',
@@ -115,14 +117,11 @@ export const de: LanguageTranslation = {
copied: 'Kopiert!', copied: 'Kopiert!',
side_panel: { 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...', view_all_options: 'Alle Optionen anzeigen...',
tables_section: { tables_section: {
tables: 'Tabellen', tables: 'Tabellen',
add_table: 'Tabelle hinzufügen', add_table: 'Tabelle hinzufügen',
add_view: 'Ansicht hinzufügen',
filter: 'Filter', filter: 'Filter',
collapse: 'Alle einklappen', collapse: 'Alle einklappen',
// TODO: Translate // TODO: Translate
@@ -148,16 +147,23 @@ export const de: LanguageTranslation = {
field_actions: { field_actions: {
title: 'Feldattribute', title: 'Feldattribute',
unique: 'Eindeutig', unique: 'Eindeutig',
auto_increment: 'Automatisch hochzählen',
comments: 'Kommentare', comments: 'Kommentare',
no_comments: 'Keine Kommentare', no_comments: 'Keine Kommentare',
delete_field: 'Feld löschen', delete_field: 'Feld löschen',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Präzision',
scale: 'Skalierung',
}, },
index_actions: { index_actions: {
title: 'Indexattribute', title: 'Indexattribute',
name: 'Name', name: 'Name',
unique: 'Eindeutig', unique: 'Eindeutig',
index_type: 'Indextyp',
delete_index: 'Index löschen', delete_index: 'Index löschen',
}, },
table_actions: { table_actions: {
@@ -174,32 +180,26 @@ export const de: LanguageTranslation = {
description: 'Erstellen Sie eine Tabelle, um zu beginnen', description: 'Erstellen Sie eine Tabelle, um zu beginnen',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'Beziehungen', refs: 'Refs',
filter: 'Filter', filter: 'Filter',
add_relationship: 'Beziehung hinzufügen',
collapse: 'Alle einklappen', collapse: 'Alle einklappen',
add_relationship: 'Beziehung hinzufügen',
relationships: 'Beziehungen',
dependencies: 'Abhängigkeiten',
relationship: { relationship: {
relationship: 'Beziehung',
primary: 'Primäre Tabelle', primary: 'Primäre Tabelle',
foreign: 'Referenzierte Tabelle', foreign: 'Referenzierte Tabelle',
cardinality: 'Kardinalität', cardinality: 'Kardinalität',
delete_relationship: 'Beziehung löschen', delete_relationship: 'Löschen',
relationship_actions: { relationship_actions: {
title: 'Aktionen', title: 'Aktionen',
delete_relationship: 'Beziehung löschen', delete_relationship: 'Löschen',
}, },
}, },
empty_state: {
title: 'Keine Beziehungen',
description:
'Erstellen Sie eine Beziehung, um Tabellen zu verbinden',
},
},
dependencies_section: {
dependencies: 'Abhängigkeiten',
filter: 'Filter',
collapse: 'Alle einklappen',
dependency: { dependency: {
dependency: 'Abhängigkeit',
table: 'Tabelle', table: 'Tabelle',
dependent_table: 'Abhängige Ansicht', dependent_table: 'Abhängige Ansicht',
delete_dependency: 'Löschen', delete_dependency: 'Löschen',
@@ -209,8 +209,8 @@ export const de: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'Keine Abhängigkeiten', title: 'Keine Beziehungen',
description: 'Erstellen Sie eine Ansicht, um zu beginnen', description: 'Erstellen Sie eine Beziehung, um zu beginnen',
}, },
}, },
@@ -253,9 +253,12 @@ export const de: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -270,7 +273,14 @@ export const de: LanguageTranslation = {
undo: 'Rückgängig', undo: 'Rückgängig',
redo: 'Wiederholen', redo: 'Wiederholen',
reorder_diagram: 'Diagramm neu anordnen', reorder_diagram: 'Diagramm neu anordnen',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Überlappende Tabellen hervorheben', highlight_overlapping_tables: 'Überlappende Tabellen hervorheben',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -403,6 +413,13 @@ export const de: LanguageTranslation = {
cancel: 'Abbrechen', cancel: 'Abbrechen',
confirm: 'Ändern', confirm: 'Ändern',
}, },
create_table_schema_dialog: {
title: 'Neues Schema erstellen',
description:
'Es existieren noch keine Schemas. Erstellen Sie Ihr erstes Schema, um Ihre Tabellen zu organisieren.',
create: 'Erstellen',
cancel: 'Abbrechen',
},
star_us_dialog: { star_us_dialog: {
title: 'Hilf uns, uns zu verbessern!', title: 'Hilf uns, uns zu verbessern!',
@@ -459,6 +476,7 @@ export const de: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'Neue Tabelle', new_table: 'Neue Tabelle',
new_view: 'Neue Ansicht',
new_relationship: 'Neue Beziehung', new_relationship: 'Neue Beziehung',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -481,6 +499,9 @@ export const de: LanguageTranslation = {
language_select: { language_select: {
change_language: 'Sprache', change_language: 'Sprache',
}, },
on: 'Ein',
off: 'Aus',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata } from '../types';
export const en = { export const en = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'New',
browse: 'Browse',
tables: 'Tables',
refs: 'Refs',
areas: 'Areas',
dependencies: 'Dependencies',
custom_types: 'Custom Types',
},
menu: { menu: {
file: { databases: {
file: 'File', databases: 'Databases',
new: 'New', new: 'New Diagram',
open: 'Open', browse: 'Browse...',
save: 'Save', save: 'Save',
import: 'Import', import: 'Import',
export_sql: 'Export SQL', export_sql: 'Export SQL',
export_as: 'Export as', export_as: 'Export as',
delete_diagram: 'Delete Diagram', delete_diagram: 'Delete Diagram',
exit: 'Exit',
}, },
edit: { edit: {
edit: 'Edit', edit: 'Edit',
@@ -26,7 +34,10 @@ export const en = {
hide_sidebar: 'Hide Sidebar', hide_sidebar: 'Hide Sidebar',
hide_cardinality: 'Hide Cardinality', hide_cardinality: 'Hide Cardinality',
show_cardinality: 'Show Cardinality', show_cardinality: 'Show Cardinality',
hide_field_attributes: 'Hide Field Attributes',
show_field_attributes: 'Show Field Attributes',
zoom_on_scroll: 'Zoom on Scroll', zoom_on_scroll: 'Zoom on Scroll',
show_views: 'Database Views',
theme: 'Theme', theme: 'Theme',
show_dependencies: 'Show Dependencies', show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies', hide_dependencies: 'Hide Dependencies',
@@ -69,15 +80,6 @@ export const en = {
cancel: 'Cancel', cancel: 'Cancel',
}, },
multiple_schemas_alert: {
title: 'Multiple Schemas',
description:
'{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
dont_show_again: "Don't show again",
change_schema: 'Change',
none: 'none',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'Copy failed', title: 'Copy failed',
@@ -112,14 +114,11 @@ export const en = {
copied: 'Copied!', copied: 'Copied!',
side_panel: { 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...', view_all_options: 'View all Options...',
tables_section: { tables_section: {
tables: 'Tables', tables: 'Tables',
add_table: 'Add Table', add_table: 'Add Table',
add_view: 'Add View',
filter: 'Filter', filter: 'Filter',
collapse: 'Collapse All', collapse: 'Collapse All',
clear: 'Clear Filter', clear: 'Clear Filter',
@@ -143,15 +142,21 @@ export const en = {
field_actions: { field_actions: {
title: 'Field Attributes', title: 'Field Attributes',
unique: 'Unique', unique: 'Unique',
auto_increment: 'Auto Increment',
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
comments: 'Comments', comments: 'Comments',
no_comments: 'No comments', no_comments: 'No comments',
default_value: 'Default Value',
no_default: 'No default',
delete_field: 'Delete Field', delete_field: 'Delete Field',
}, },
index_actions: { index_actions: {
title: 'Index Attributes', title: 'Index Attributes',
name: 'Name', name: 'Name',
unique: 'Unique', unique: 'Unique',
index_type: 'Index Type',
delete_index: 'Delete Index', delete_index: 'Delete Index',
}, },
table_actions: { table_actions: {
@@ -168,12 +173,15 @@ export const en = {
description: 'Create a table to get started', description: 'Create a table to get started',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'Relationships', refs: 'Refs',
filter: 'Filter', filter: 'Filter',
add_relationship: 'Add Relationship',
collapse: 'Collapse All', collapse: 'Collapse All',
add_relationship: 'Add Relationship',
relationships: 'Relationships',
dependencies: 'Dependencies',
relationship: { relationship: {
relationship: 'Relationship',
primary: 'Primary Table', primary: 'Primary Table',
foreign: 'Referenced Table', foreign: 'Referenced Table',
cardinality: 'Cardinality', cardinality: 'Cardinality',
@@ -183,16 +191,8 @@ export const en = {
delete_relationship: 'Delete', delete_relationship: 'Delete',
}, },
}, },
empty_state: {
title: 'No relationships',
description: 'Create a relationship to connect tables',
},
},
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: { dependency: {
dependency: 'Dependency',
table: 'Table', table: 'Table',
dependent_table: 'Dependent View', dependent_table: 'Dependent View',
delete_dependency: 'Delete', delete_dependency: 'Delete',
@@ -202,8 +202,8 @@ export const en = {
}, },
}, },
empty_state: { empty_state: {
title: 'No dependencies', title: 'No relationships',
description: 'Create a view to get started', description: 'Create a relationship to get started',
}, },
}, },
@@ -245,8 +245,11 @@ export const en = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
clear_field_highlight: 'Clear Highlight',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
@@ -263,6 +266,10 @@ export const en = {
redo: 'Redo', redo: 'Redo',
reorder_diagram: 'Reorder Diagram', reorder_diagram: 'Reorder Diagram',
highlight_overlapping_tables: 'Highlight Overlapping Tables', highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -394,6 +401,14 @@ export const en = {
confirm: 'Change', confirm: 'Change',
}, },
create_table_schema_dialog: {
title: 'Create New Schema',
description:
'No schemas exist yet. Create your first schema to organize your tables.',
create: 'Create',
cancel: 'Cancel',
},
star_us_dialog: { star_us_dialog: {
title: 'Help us improve!', title: 'Help us improve!',
description: description:
@@ -448,6 +463,7 @@ export const en = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'New Table', new_table: 'New Table',
new_view: 'New View',
new_relationship: 'New Relationship', new_relationship: 'New Relationship',
new_area: 'New Area', new_area: 'New Area',
}, },
@@ -468,6 +484,9 @@ export const en = {
language_select: { language_select: {
change_language: 'Language', change_language: 'Language',
}, },
on: 'On',
off: 'Off',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const es: LanguageTranslation = { export const es: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'Nuevo',
browse: 'Examinar',
tables: 'Tablas',
refs: 'Refs',
areas: 'Áreas',
dependencies: 'Dependencias',
custom_types: 'Tipos Personalizados',
},
menu: { menu: {
file: { databases: {
file: 'Archivo', databases: 'Bases de Datos',
new: 'Nuevo', new: 'Nuevo Diagrama',
open: 'Abrir', browse: 'Examinar...',
save: 'Guardar', save: 'Guardar',
import: 'Importar Base de Datos', import: 'Importar Base de Datos',
export_sql: 'Exportar SQL', export_sql: 'Exportar SQL',
export_as: 'Exportar como', export_as: 'Exportar como',
delete_diagram: 'Eliminar Diagrama', delete_diagram: 'Eliminar Diagrama',
exit: 'Salir',
}, },
edit: { edit: {
edit: 'Editar', edit: 'Editar',
@@ -24,9 +32,12 @@ export const es: LanguageTranslation = {
view: 'Ver', view: 'Ver',
hide_cardinality: 'Ocultar Cardinalidad', hide_cardinality: 'Ocultar Cardinalidad',
show_cardinality: 'Mostrar Cardinalidad', show_cardinality: 'Mostrar Cardinalidad',
show_field_attributes: 'Mostrar Atributos de Campo',
hide_field_attributes: 'Ocultar Atributos de Campo',
show_sidebar: 'Mostrar Barra Lateral', show_sidebar: 'Mostrar Barra Lateral',
hide_sidebar: 'Ocultar Barra Lateral', hide_sidebar: 'Ocultar Barra Lateral',
zoom_on_scroll: 'Zoom al Desplazarse', zoom_on_scroll: 'Zoom al Desplazarse',
show_views: 'Vistas de Base de Datos',
theme: 'Tema', theme: 'Tema',
show_dependencies: 'Mostrar dependencias', show_dependencies: 'Mostrar dependencias',
hide_dependencies: 'Ocultar dependencias', hide_dependencies: 'Ocultar dependencias',
@@ -104,14 +115,11 @@ export const es: LanguageTranslation = {
copied: 'Copied!', copied: 'Copied!',
side_panel: { 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...', view_all_options: 'Ver todas las opciones...',
tables_section: { tables_section: {
tables: 'Tablas', tables: 'Tablas',
add_table: 'Agregar Tabla', add_table: 'Agregar Tabla',
add_view: 'Agregar Vista',
filter: 'Filtrar', filter: 'Filtrar',
collapse: 'Colapsar Todo', collapse: 'Colapsar Todo',
// TODO: Translate // TODO: Translate
@@ -137,16 +145,23 @@ export const es: LanguageTranslation = {
field_actions: { field_actions: {
title: 'Atributos del Campo', title: 'Atributos del Campo',
unique: 'Único', unique: 'Único',
auto_increment: 'Autoincremento',
comments: 'Comentarios', comments: 'Comentarios',
no_comments: 'Sin comentarios', no_comments: 'Sin comentarios',
delete_field: 'Eliminar Campo', delete_field: 'Eliminar Campo',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Precisión',
scale: 'Escala',
}, },
index_actions: { index_actions: {
title: 'Atributos del Índice', title: 'Atributos del Índice',
name: 'Nombre', name: 'Nombre',
unique: 'Único', unique: 'Único',
index_type: 'Tipo de Índice',
delete_index: 'Eliminar Índice', delete_index: 'Eliminar Índice',
}, },
table_actions: { table_actions: {
@@ -163,14 +178,17 @@ export const es: LanguageTranslation = {
description: 'Crea una tabla para comenzar', description: 'Crea una tabla para comenzar',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'Relaciones', refs: 'Refs',
add_relationship: 'Agregar Relación',
filter: 'Filtrar', filter: 'Filtrar',
collapse: 'Colapsar Todo', collapse: 'Colapsar Todo',
add_relationship: 'Agregar Relación',
relationships: 'Relaciones',
dependencies: 'Dependencias',
relationship: { relationship: {
primary: 'Primaria', relationship: 'Relación',
foreign: 'Foránea', primary: 'Tabla Primaria',
foreign: 'Tabla Referenciada',
cardinality: 'Cardinalidad', cardinality: 'Cardinalidad',
delete_relationship: 'Eliminar', delete_relationship: 'Eliminar',
relationship_actions: { relationship_actions: {
@@ -178,18 +196,10 @@ export const es: LanguageTranslation = {
delete_relationship: 'Eliminar', delete_relationship: 'Eliminar',
}, },
}, },
empty_state: {
title: 'No hay relaciones',
description: 'Crea una relación para conectar tablas',
},
},
dependencies_section: {
dependencies: 'Dependencias',
filter: 'Filtro',
collapse: 'Colapsar todo',
dependency: { dependency: {
dependency: 'Dependencia',
table: 'Tabla', table: 'Tabla',
dependent_table: 'Vista dependiente', dependent_table: 'Vista Dependiente',
delete_dependency: 'Eliminar', delete_dependency: 'Eliminar',
dependency_actions: { dependency_actions: {
title: 'Acciones', title: 'Acciones',
@@ -197,8 +207,8 @@ export const es: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'Sin dependencias', title: 'Sin relaciones',
description: 'Crea una vista para comenzar', description: 'Crea una relación para comenzar',
}, },
}, },
@@ -241,9 +251,12 @@ export const es: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -258,7 +271,13 @@ export const es: LanguageTranslation = {
undo: 'Deshacer', undo: 'Deshacer',
redo: 'Rehacer', redo: 'Rehacer',
reorder_diagram: 'Reordenar Diagrama', reorder_diagram: 'Reordenar Diagrama',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Resaltar tablas superpuestas', highlight_overlapping_tables: 'Resaltar tablas superpuestas',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -392,6 +411,13 @@ export const es: LanguageTranslation = {
cancel: 'Cancelar', cancel: 'Cancelar',
confirm: 'Cambiar', confirm: 'Cambiar',
}, },
create_table_schema_dialog: {
title: 'Crear Nuevo Esquema',
description:
'Aún no existen esquemas. Crea tu primer esquema para organizar tus tablas.',
create: 'Crear',
cancel: 'Cancelar',
},
star_us_dialog: { star_us_dialog: {
title: '¡Ayúdanos a mejorar!', title: '¡Ayúdanos a mejorar!',
@@ -401,14 +427,6 @@ export const es: LanguageTranslation = {
confirm: '¡Claro!', confirm: '¡Claro!',
}, },
multiple_schemas_alert: {
title: 'Múltiples Esquemas',
description:
'{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.',
dont_show_again: 'No mostrar de nuevo',
change_schema: 'Cambiar',
none: 'nada',
},
// TODO: Translate // TODO: Translate
export_diagram_dialog: { export_diagram_dialog: {
title: 'Export Diagram', title: 'Export Diagram',
@@ -457,6 +475,7 @@ export const es: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'Nueva Tabla', new_table: 'Nueva Tabla',
new_view: 'Nueva Vista',
new_relationship: 'Nueva Relación', new_relationship: 'Nueva Relación',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -479,6 +498,9 @@ export const es: LanguageTranslation = {
language_select: { language_select: {
change_language: 'Idioma', change_language: 'Idioma',
}, },
on: 'Encendido',
off: 'Apagado',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const fr: LanguageTranslation = { export const fr: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'Nouveau',
browse: 'Parcourir',
tables: 'Tables',
refs: 'Refs',
areas: 'Zones',
dependencies: 'Dépendances',
custom_types: 'Types Personnalisés',
},
menu: { menu: {
file: { databases: {
file: 'Fichier', databases: 'Bases de Données',
new: 'Nouveau', new: 'Nouveau Diagramme',
open: 'Ouvrir', browse: 'Parcourir...',
save: 'Enregistrer', save: 'Enregistrer',
import: 'Importer Base de Données', import: 'Importer Base de Données',
export_sql: 'Exporter SQL', export_sql: 'Exporter SQL',
export_as: 'Exporter en tant que', export_as: 'Exporter en tant que',
delete_diagram: 'Supprimer le Diagramme', delete_diagram: 'Supprimer le Diagramme',
exit: 'Quitter',
}, },
edit: { edit: {
edit: 'Édition', edit: 'Édition',
@@ -26,7 +34,10 @@ export const fr: LanguageTranslation = {
hide_sidebar: 'Cacher la Barre Latérale', hide_sidebar: 'Cacher la Barre Latérale',
hide_cardinality: 'Cacher la Cardinalité', hide_cardinality: 'Cacher la Cardinalité',
show_cardinality: 'Afficher la Cardinalité', show_cardinality: 'Afficher la Cardinalité',
hide_field_attributes: 'Masquer les Attributs de Champ',
show_field_attributes: 'Afficher les Attributs de Champ',
zoom_on_scroll: 'Zoom sur le Défilement', zoom_on_scroll: 'Zoom sur le Défilement',
show_views: 'Vues de Base de Données',
theme: 'Thème', theme: 'Thème',
show_dependencies: 'Afficher les Dépendances', show_dependencies: 'Afficher les Dépendances',
hide_dependencies: 'Masquer les Dépendances', hide_dependencies: 'Masquer les Dépendances',
@@ -103,14 +114,11 @@ export const fr: LanguageTranslation = {
copied: 'Copié !', copied: 'Copié !',
side_panel: { 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...', view_all_options: 'Voir toutes les Options...',
tables_section: { tables_section: {
tables: 'Tables', tables: 'Tables',
add_table: 'Ajouter une Table', add_table: 'Ajouter une Table',
add_view: 'Ajouter une Vue',
filter: 'Filtrer', filter: 'Filtrer',
collapse: 'Réduire Tout', collapse: 'Réduire Tout',
clear: 'Effacer le Filtre', clear: 'Effacer le Filtre',
@@ -135,16 +143,23 @@ export const fr: LanguageTranslation = {
field_actions: { field_actions: {
title: 'Attributs du Champ', title: 'Attributs du Champ',
unique: 'Unique', unique: 'Unique',
auto_increment: 'Auto-incrément',
comments: 'Commentaires', comments: 'Commentaires',
no_comments: 'Pas de commentaires', no_comments: 'Pas de commentaires',
delete_field: 'Supprimer le Champ', delete_field: 'Supprimer le Champ',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Précision',
scale: 'Échelle',
}, },
index_actions: { index_actions: {
title: "Attributs de l'Index", title: "Attributs de l'Index",
name: 'Nom', name: 'Nom',
unique: 'Unique', unique: 'Unique',
index_type: "Type d'index",
delete_index: "Supprimer l'Index", delete_index: "Supprimer l'Index",
}, },
table_actions: { table_actions: {
@@ -161,12 +176,15 @@ export const fr: LanguageTranslation = {
description: 'Créez une table pour commencer', description: 'Créez une table pour commencer',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'Relations', refs: 'Refs',
filter: 'Filtrer', filter: 'Filtrer',
add_relationship: 'Ajouter une Relation',
collapse: 'Réduire Tout', collapse: 'Réduire Tout',
add_relationship: 'Ajouter une Relation',
relationships: 'Relations',
dependencies: 'Dépendances',
relationship: { relationship: {
relationship: 'Relation',
primary: 'Table Principale', primary: 'Table Principale',
foreign: 'Table Référencée', foreign: 'Table Référencée',
cardinality: 'Cardinalité', cardinality: 'Cardinalité',
@@ -176,16 +194,8 @@ export const fr: LanguageTranslation = {
delete_relationship: 'Supprimer', delete_relationship: 'Supprimer',
}, },
}, },
empty_state: {
title: 'Aucune relation',
description: 'Créez une relation pour connecter les tables',
},
},
dependencies_section: {
dependencies: 'Dépendances',
filter: 'Filtrer',
collapse: 'Réduire Tout',
dependency: { dependency: {
dependency: 'Dépendance',
table: 'Table', table: 'Table',
dependent_table: 'Vue Dépendante', dependent_table: 'Vue Dépendante',
delete_dependency: 'Supprimer', delete_dependency: 'Supprimer',
@@ -195,8 +205,8 @@ export const fr: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'Aucune dépendance', title: 'Aucune relation',
description: 'Créez une vue pour commencer', description: 'Créez une relation pour commencer',
}, },
}, },
@@ -239,9 +249,12 @@ export const fr: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -256,7 +269,13 @@ export const fr: LanguageTranslation = {
undo: 'Annuler', undo: 'Annuler',
redo: 'Rétablir', redo: 'Rétablir',
reorder_diagram: 'Réorganiser le Diagramme', reorder_diagram: 'Réorganiser le Diagramme',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Surligner les tables chevauchées', highlight_overlapping_tables: 'Surligner les tables chevauchées',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -341,15 +360,6 @@ export const fr: LanguageTranslation = {
transparent_description: 'Remove background color from image.', 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}}.',
dont_show_again: 'Ne plus afficher',
change_schema: 'Changer',
none: 'Aucun',
},
new_table_schema_dialog: { new_table_schema_dialog: {
title: 'Sélectionner un Schéma', title: 'Sélectionner un Schéma',
description: description:
@@ -372,6 +382,13 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler', cancel: 'Annuler',
confirm: 'Modifier', confirm: 'Modifier',
}, },
create_table_schema_dialog: {
title: 'Créer un Nouveau Schéma',
description:
"Aucun schéma n'existe encore. Créez votre premier schéma pour organiser vos tables.",
create: 'Créer',
cancel: 'Annuler',
},
create_relationship_dialog: { create_relationship_dialog: {
title: 'Créer une Relation', title: 'Créer une Relation',
@@ -454,6 +471,7 @@ export const fr: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'Nouvelle Table', new_table: 'Nouvelle Table',
new_view: 'Nouvelle Vue',
new_relationship: 'Nouvelle Relation', new_relationship: 'Nouvelle Relation',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -476,6 +494,9 @@ export const fr: LanguageTranslation = {
language_select: { language_select: {
change_language: 'Langue', change_language: 'Langue',
}, },
on: 'Activé',
off: 'Désactivé',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const gu: LanguageTranslation = { export const gu: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'નવું',
browse: 'બ્રાઉજ',
tables: 'ટેબલો',
refs: 'રેફ્સ',
areas: 'ક્ષેત્રો',
dependencies: 'નિર્ભરતાઓ',
custom_types: 'કસ્ટમ ટાઇપ',
},
menu: { menu: {
file: { databases: {
file: 'ફાઇલ', databases: 'ડેટાબેસેસ',
new: 'નવું', new: 'નવું ડાયાગ્રામ',
open: 'ખોલો', browse: 'બ્રાઉજ કરો...',
save: 'સાચવો', save: 'સાચવો',
import: 'ડેટાબેસ આયાત કરો', import: 'ડેટાબેસ આયાત કરો',
export_sql: 'SQL નિકાસ કરો', export_sql: 'SQL નિકાસ કરો',
export_as: 'રૂપે નિકાસ કરો', export_as: 'રૂપે નિકાસ કરો',
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો', delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
exit: 'બહાર જાઓ',
}, },
edit: { edit: {
edit: 'ફેરફાર', edit: 'ફેરફાર',
@@ -26,7 +34,10 @@ export const gu: LanguageTranslation = {
hide_sidebar: 'સાઇડબાર છુપાવો', hide_sidebar: 'સાઇડબાર છુપાવો',
hide_cardinality: 'કાર્ડિનાલિટી છુપાવો', hide_cardinality: 'કાર્ડિનાલિટી છુપાવો',
show_cardinality: 'કાર્ડિનાલિટી બતાવો', show_cardinality: 'કાર્ડિનાલિટી બતાવો',
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો', zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
show_views: 'ડેટાબેઝ વ્યૂઝ',
theme: 'થિમ', theme: 'થિમ',
show_dependencies: 'નિર્ભરતાઓ બતાવો', show_dependencies: 'નિર્ભરતાઓ બતાવો',
hide_dependencies: 'નિર્ભરતાઓ છુપાવો', hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
@@ -71,15 +82,6 @@ export const gu: LanguageTranslation = {
cancel: 'રદ કરો', cancel: 'રદ કરો',
}, },
multiple_schemas_alert: {
title: 'કઈંક વધારે સ્કીમા',
description:
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
dont_show_again: 'ફરીથી ન બતાવો',
change_schema: 'બદલો',
none: 'કઈ નહીં',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'નકલ નિષ્ફળ', title: 'નકલ નિષ્ફળ',
@@ -114,14 +116,11 @@ export const gu: LanguageTranslation = {
copied: 'નકલ થયું!', copied: 'નકલ થયું!',
side_panel: { side_panel: {
schema: 'સ્કીમા:',
filter_by_schema: 'સ્કીમા દ્વારા ફિલ્ટર કરો',
search_schema: 'સ્કીમા શોધો...',
no_schemas_found: 'કોઈ સ્કીમા મળ્યા નથી.',
view_all_options: 'બધા વિકલ્પો જુઓ...', view_all_options: 'બધા વિકલ્પો જુઓ...',
tables_section: { tables_section: {
tables: 'ટેબલ્સ', tables: 'ટેબલ્સ',
add_table: 'ટેબલ ઉમેરો', add_table: 'ટેબલ ઉમેરો',
add_view: 'વ્યૂ ઉમેરો',
filter: 'ફિલ્ટર', filter: 'ફિલ્ટર',
collapse: 'બધાને સકુચિત કરો', collapse: 'બધાને સકુચિત કરો',
// TODO: Translate // TODO: Translate
@@ -148,16 +147,23 @@ export const gu: LanguageTranslation = {
field_actions: { field_actions: {
title: 'ફીલ્ડ લક્ષણો', title: 'ફીલ્ડ લક્ષણો',
unique: 'અદ્વિતીય', unique: 'અદ્વિતીય',
auto_increment: 'ઑટો ઇન્ક્રિમેન્ટ',
comments: 'ટિપ્પણીઓ', comments: 'ટિપ્પણીઓ',
no_comments: 'કોઈ ટિપ્પણીઓ નથી', no_comments: 'કોઈ ટિપ્પણીઓ નથી',
delete_field: 'ફીલ્ડ કાઢી નાખો', delete_field: 'ફીલ્ડ કાઢી નાખો',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'ચોકસાઈ',
scale: 'માપ',
}, },
index_actions: { index_actions: {
title: 'ઇન્ડેક્સ લક્ષણો', title: 'ઇન્ડેક્સ લક્ષણો',
name: 'નામ', name: 'નામ',
unique: 'અદ્વિતીય', unique: 'અદ્વિતીય',
index_type: 'ઇન્ડેક્સ પ્રકાર',
delete_index: 'ઇન્ડેક્સ કાઢી નાખો', delete_index: 'ઇન્ડેક્સ કાઢી નાખો',
}, },
table_actions: { table_actions: {
@@ -174,14 +180,17 @@ export const gu: LanguageTranslation = {
description: 'શરૂ કરવા માટે એક ટેબલ બનાવો', description: 'શરૂ કરવા માટે એક ટેબલ બનાવો',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'સંબંધો', refs: 'રેફ્સ',
filter: 'ફિલ્ટર', filter: 'ફિલ્ટર',
add_relationship: 'સંબંધ ઉમેરો',
collapse: 'બધાને સકુચિત કરો', collapse: 'બધાને સકુચિત કરો',
add_relationship: 'સંબંધ ઉમેરો',
relationships: 'સંબંધો',
dependencies: 'નિર્ભરતાઓ',
relationship: { relationship: {
relationship: 'સંબંધ',
primary: 'પ્રાથમિક ટેબલ', primary: 'પ્રાથમિક ટેબલ',
foreign: 'સંદર્ભ ટેબલ', foreign: 'સંદર્ભિત ટેબલ',
cardinality: 'કાર્ડિનાલિટી', cardinality: 'કાર્ડિનાલિટી',
delete_relationship: 'કાઢી નાખો', delete_relationship: 'કાઢી નાખો',
relationship_actions: { relationship_actions: {
@@ -189,27 +198,19 @@ export const gu: LanguageTranslation = {
delete_relationship: 'કાઢી નાખો', delete_relationship: 'કાઢી નાખો',
}, },
}, },
empty_state: {
title: 'કોઈ સંબંધો નથી',
description: 'ટેબલ્સ કનેક્ટ કરવા માટે એક સંબંધ બનાવો',
},
},
dependencies_section: {
dependencies: 'નિર્ભરતાઓ',
filter: 'ફિલ્ટર',
collapse: 'સિકોડો',
dependency: { dependency: {
dependency: 'નિર્ભરતા',
table: 'ટેબલ', table: 'ટેબલ',
dependent_table: 'આધાર રાખેલું ટેબલ', dependent_table: 'નિર્ભરશીલ વ્યૂ',
delete_dependency: 'નિર્ભરતા કાઢી નાખો', delete_dependency: 'કાઢી નાખો',
dependency_actions: { dependency_actions: {
title: 'ક્રિયાઓ', title: 'ક્રિયાઓ',
delete_dependency: 'નિર્ભરતા કાઢી નાખો', delete_dependency: 'કાઢી નાખો',
}, },
}, },
empty_state: { empty_state: {
title: 'કોઈ નિર્ભરતાઓ નથી', title: 'કોઈ સંબંધો નથી',
description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.', description: 'શરૂ કરવા માટે એક સંબંધ બનાવો',
}, },
}, },
@@ -252,9 +253,12 @@ export const gu: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -269,7 +273,13 @@ export const gu: LanguageTranslation = {
undo: 'અનડુ', undo: 'અનડુ',
redo: 'રીડુ', redo: 'રીડુ',
reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો', reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો', highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -401,6 +411,14 @@ export const gu: LanguageTranslation = {
confirm: 'બદલો', confirm: 'બદલો',
}, },
create_table_schema_dialog: {
title: 'નવું સ્કીમા બનાવો',
description:
'હજી સુધી કોઈ સ્કીમા અસ્તિત્વમાં નથી. તમારા ટેબલ્સ ને વ્યવસ્થિત કરવા માટે તમારું પહેલું સ્કીમા બનાવો.',
create: 'બનાવો',
cancel: 'રદ કરો',
},
star_us_dialog: { star_us_dialog: {
title: 'અમને સુધારવામાં મદદ કરો!', title: 'અમને સુધારવામાં મદદ કરો!',
description: description:
@@ -456,6 +474,7 @@ export const gu: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'નવું ટેબલ', new_table: 'નવું ટેબલ',
new_view: 'નવું વ્યૂ',
new_relationship: 'નવો સંબંધ', new_relationship: 'નવો સંબંધ',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -477,6 +496,9 @@ export const gu: LanguageTranslation = {
language_select: { language_select: {
change_language: 'ભાષા બદલો', change_language: 'ભાષા બદલો',
}, },
on: 'ચાલુ',
off: 'બંધ',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const hi: LanguageTranslation = { export const hi: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'नया',
browse: 'ब्राउज़',
tables: 'टेबल',
refs: 'रेफ्स',
areas: 'क्षेत्र',
dependencies: 'निर्भरताएं',
custom_types: 'कस्टम टाइप',
},
menu: { menu: {
file: { databases: {
file: 'फ़ाइल', databases: 'डेटाबेस',
new: 'नया', new: 'नया आरेख',
open: 'खोलें', browse: 'ब्राउज़ करें...',
save: 'सहेजें', save: 'सहेजें',
import: 'डेटाबेस आयात करें', import: 'डेटाबेस आयात करें',
export_sql: 'SQL निर्यात करें', export_sql: 'SQL निर्यात करें',
export_as: 'के रूप में निर्यात करें', export_as: 'के रूप में निर्यात करें',
delete_diagram: 'आरेख हटाएँ', delete_diagram: 'आरेख हटाएँ',
exit: 'बाहर जाएँ',
}, },
edit: { edit: {
edit: 'संपादित करें', edit: 'संपादित करें',
@@ -26,7 +34,10 @@ export const hi: LanguageTranslation = {
hide_sidebar: 'साइडबार छिपाएँ', hide_sidebar: 'साइडबार छिपाएँ',
hide_cardinality: 'कार्डिनैलिटी छिपाएँ', hide_cardinality: 'कार्डिनैलिटी छिपाएँ',
show_cardinality: 'कार्डिनैलिटी दिखाएँ', show_cardinality: 'कार्डिनैलिटी दिखाएँ',
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
zoom_on_scroll: 'स्क्रॉल पर ज़ूम', zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
show_views: 'डेटाबेस व्यू',
theme: 'थीम', theme: 'थीम',
show_dependencies: 'निर्भरता दिखाएँ', show_dependencies: 'निर्भरता दिखाएँ',
hide_dependencies: 'निर्भरता छिपाएँ', hide_dependencies: 'निर्भरता छिपाएँ',
@@ -70,15 +81,6 @@ export const hi: LanguageTranslation = {
cancel: 'रद्द करें', cancel: 'रद्द करें',
}, },
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
dont_show_again: 'फिर से न दिखाएँ',
change_schema: 'बदलें',
none: 'कोई नहीं',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'कॉपी असफल', title: 'कॉपी असफल',
@@ -114,14 +116,11 @@ export const hi: LanguageTranslation = {
copied: 'Copied!', copied: 'Copied!',
side_panel: { side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारा फ़िल्टर करें',
search_schema: 'स्कीमा खोजें...',
no_schemas_found: 'कोई स्कीमा नहीं मिला।',
view_all_options: 'सभी विकल्प देखें...', view_all_options: 'सभी विकल्प देखें...',
tables_section: { tables_section: {
tables: 'तालिकाएँ', tables: 'तालिकाएँ',
add_table: 'तालिका जोड़ें', add_table: 'तालिका जोड़ें',
add_view: 'व्यू जोड़ें',
filter: 'फ़िल्टर', filter: 'फ़िल्टर',
collapse: 'सभी को संक्षिप्त करें', collapse: 'सभी को संक्षिप्त करें',
// TODO: Translate // TODO: Translate
@@ -147,16 +146,23 @@ export const hi: LanguageTranslation = {
field_actions: { field_actions: {
title: 'फ़ील्ड विशेषताएँ', title: 'फ़ील्ड विशेषताएँ',
unique: 'अद्वितीय', unique: 'अद्वितीय',
auto_increment: 'ऑटो इंक्रीमेंट',
comments: 'टिप्पणियाँ', comments: 'टिप्पणियाँ',
no_comments: 'कोई टिप्पणी नहीं', no_comments: 'कोई टिप्पणी नहीं',
delete_field: 'फ़ील्ड हटाएँ', delete_field: 'फ़ील्ड हटाएँ',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
}, },
index_actions: { index_actions: {
title: 'सूचकांक विशेषताएँ', title: 'सूचकांक विशेषताएँ',
name: 'नाम', name: 'नाम',
unique: 'अद्वितीय', unique: 'अद्वितीय',
index_type: 'इंडेक्स प्रकार',
delete_index: 'सूचकांक हटाएँ', delete_index: 'सूचकांक हटाएँ',
}, },
table_actions: { table_actions: {
@@ -173,12 +179,15 @@ export const hi: LanguageTranslation = {
description: 'शुरू करने के लिए एक तालिका बनाएँ', description: 'शुरू करने के लिए एक तालिका बनाएँ',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'संबंध', refs: 'रेफ्स',
filter: 'फ़िल्टर', filter: 'फ़िल्टर',
add_relationship: 'संबंध जोड़ें',
collapse: 'सभी को संक्षिप्त करें', collapse: 'सभी को संक्षिप्त करें',
add_relationship: 'संबंध जोड़ें',
relationships: 'संबंध',
dependencies: 'निर्भरताएँ',
relationship: { relationship: {
relationship: 'संबंध',
primary: 'प्राथमिक तालिका', primary: 'प्राथमिक तालिका',
foreign: 'संदर्भित तालिका', foreign: 'संदर्भित तालिका',
cardinality: 'कार्डिनैलिटी', cardinality: 'कार्डिनैलिटी',
@@ -188,28 +197,19 @@ export const hi: LanguageTranslation = {
delete_relationship: 'हटाएँ', delete_relationship: 'हटाएँ',
}, },
}, },
dependency: {
dependency: 'निर्भरता',
table: 'तालिका',
dependent_table: 'आश्रित दृश्य',
delete_dependency: 'हटाएँ',
dependency_actions: {
title: 'क्रियाएँ',
delete_dependency: 'हटाएँ',
},
},
empty_state: { empty_state: {
title: 'कोई संबंध नहीं', title: 'कोई संबंध नहीं',
description: description: 'शुरू करने के लिए एक संबंध बनाएँ',
'तालिकाओं को कनेक्ट करने के लिए एक संबंध बनाएँ',
},
},
dependencies_section: {
dependencies: 'निर्भरताएँ',
filter: 'फ़िल्टर',
collapse: 'सिकोड़ें',
dependency: {
table: 'तालिका',
dependent_table: 'आश्रित तालिका',
delete_dependency: 'निर्भरता हटाएँ',
dependency_actions: {
title: 'कार्रवाइयाँ',
delete_dependency: 'निर्भरता हटाएँ',
},
},
empty_state: {
title: 'कोई निर्भरता नहीं',
description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।',
}, },
}, },
@@ -252,9 +252,12 @@ export const hi: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -269,7 +272,13 @@ export const hi: LanguageTranslation = {
undo: 'पूर्ववत करें', undo: 'पूर्ववत करें',
redo: 'पुनः करें', redo: 'पुनः करें',
reorder_diagram: 'आरेख पुनः व्यवस्थित करें', reorder_diagram: 'आरेख पुनः व्यवस्थित करें',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें', highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -404,6 +413,14 @@ export const hi: LanguageTranslation = {
confirm: 'बदलें', confirm: 'बदलें',
}, },
create_table_schema_dialog: {
title: 'नया स्कीमा बनाएं',
description:
'अभी तक कोई स्कीमा मौजूद नहीं है। अपनी तालिकाओं को व्यवस्थित करने के लिए अपना पहला स्कीमा बनाएं।',
create: 'बनाएं',
cancel: 'रद्द करें',
},
star_us_dialog: { star_us_dialog: {
title: 'हमें सुधारने में मदद करें!', title: 'हमें सुधारने में मदद करें!',
description: description:
@@ -459,6 +476,7 @@ export const hi: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'नई तालिका', new_table: 'नई तालिका',
new_view: 'नया व्यू',
new_relationship: 'नया संबंध', new_relationship: 'नया संबंध',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -481,6 +499,9 @@ export const hi: LanguageTranslation = {
language_select: { language_select: {
change_language: 'भाषा बदलें', change_language: 'भाषा बदलें',
}, },
on: 'चालू',
off: 'बंद',
}, },
}; };

502
src/i18n/locales/hr.ts Normal file
View File

@@ -0,0 +1,502 @@
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: {
databases: {
databases: 'Baze Podataka',
new: 'Novi Dijagram',
browse: 'Pregledaj...',
save: 'Spremi',
import: 'Uvezi',
export_sql: 'Izvezi SQL',
export_as: 'Izvezi kao',
delete_diagram: 'Izbriši dijagram',
},
edit: {
edit: 'Uredi',
undo: 'Poništi',
redo: 'Ponovi',
clear: 'Očisti',
},
view: {
view: 'Prikaz',
show_sidebar: 'Prikaži bočnu traku',
hide_sidebar: 'Sakrij bočnu traku',
hide_cardinality: 'Sakrij kardinalnost',
show_cardinality: 'Prikaži kardinalnost',
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',
show_minimap: 'Prikaži mini kartu',
hide_minimap: 'Sakrij mini kartu',
},
backup: {
backup: 'Sigurnosna kopija',
export_diagram: 'Izvezi dijagram',
restore_diagram: 'Vrati dijagram',
},
help: {
help: 'Pomoć',
docs_website: 'Dokumentacija',
join_discord: 'Pridružite nam se na Discordu',
},
},
delete_diagram_alert: {
title: 'Izbriši dijagram',
description:
'Ova radnja se ne može poništiti. Ovo će trajno izbrisati dijagram.',
cancel: 'Odustani',
delete: 'Izbriši',
},
clear_diagram_alert: {
title: 'Očisti dijagram',
description:
'Ova radnja se ne može poništiti. Ovo će trajno izbrisati sve podatke u dijagramu.',
cancel: 'Odustani',
clear: 'Očisti',
},
reorder_diagram_alert: {
title: 'Preuredi dijagram',
description:
'Ova radnja će preurediti sve tablice u dijagramu. Želite li nastaviti?',
reorder: 'Preuredi',
cancel: 'Odustani',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Kopiranje neuspješno',
description: 'Međuspremnik nije podržan.',
},
failed: {
title: 'Kopiranje neuspješno',
description: 'Nešto je pošlo po zlu. Molimo pokušajte ponovno.',
},
},
theme: {
system: 'Sustav',
light: 'Svijetla',
dark: 'Tamna',
},
zoom: {
on: 'Uključeno',
off: 'Isključeno',
},
last_saved: 'Zadnje spremljeno',
saved: 'Spremljeno',
loading_diagram: 'Učitavanje dijagrama...',
deselect_all: 'Odznači sve',
select_all: 'Označi sve',
clear: 'Očisti',
show_more: 'Prikaži više',
show_less: 'Prikaži manje',
copy_to_clipboard: 'Kopiraj u međuspremnik',
copied: 'Kopirano!',
side_panel: {
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',
no_results:
'Nema pronađenih tablica koje odgovaraju vašem filteru.',
show_list: 'Prikaži popis tablica',
show_dbml: 'Prikaži DBML uređivač',
table: {
fields: 'Polja',
nullable: 'Može biti null?',
primary_key: 'Primarni ključ',
indexes: 'Indeksi',
comments: 'Komentari',
no_comments: 'Nema komentara',
add_field: 'Dodaj polje',
add_index: 'Dodaj indeks',
index_select_fields: 'Odaberi polja',
no_types_found: 'Nema pronađenih tipova',
field_name: 'Naziv',
field_type: 'Tip',
field_actions: {
title: 'Atributi polja',
unique: 'Jedinstven',
auto_increment: 'Automatsko povećavanje',
character_length: 'Maksimalna dužina',
precision: 'Preciznost',
scale: 'Skala',
comments: 'Komentari',
no_comments: 'Nema komentara',
default_value: 'Zadana vrijednost',
no_default: 'Nema zadane vrijednosti',
delete_field: 'Izbriši polje',
},
index_actions: {
title: 'Atributi indeksa',
name: 'Naziv',
unique: 'Jedinstven',
index_type: 'Vrsta indeksa',
delete_index: 'Izbriši indeks',
},
table_actions: {
title: 'Radnje nad tablicom',
change_schema: 'Promijeni shemu',
add_field: 'Dodaj polje',
add_index: 'Dodaj indeks',
duplicate_table: 'Dupliciraj tablicu',
delete_table: 'Izbriši tablicu',
},
},
empty_state: {
title: 'Nema tablica',
description: 'Stvorite tablicu za početak',
},
},
refs_section: {
refs: 'Refs',
filter: 'Filtriraj',
collapse: 'Sažmi sve',
add_relationship: 'Dodaj vezu',
relationships: 'Veze',
dependencies: 'Ovisnosti',
relationship: {
relationship: 'Veza',
primary: 'Primarna tablica',
foreign: 'Referentna tablica',
cardinality: 'Kardinalnost',
delete_relationship: 'Izbriši',
relationship_actions: {
title: 'Radnje',
delete_relationship: 'Izbriši',
},
},
dependency: {
dependency: 'Ovisnost',
table: 'Tablica',
dependent_table: 'Ovisni pogled',
delete_dependency: 'Izbriši',
dependency_actions: {
title: 'Radnje',
delete_dependency: 'Izbriši',
},
},
empty_state: {
title: 'Nema veze',
description: 'Stvorite vezu za početak',
},
},
areas_section: {
areas: 'Područja',
add_area: 'Dodaj područje',
filter: 'Filtriraj',
clear: 'Očisti filter',
no_results:
'Nema pronađenih područja koja odgovaraju vašem filteru.',
area: {
area_actions: {
title: 'Radnje nad područjem',
edit_name: 'Uredi naziv',
delete_area: 'Izbriši područje',
},
},
empty_state: {
title: 'Nema područja',
description: 'Stvorite područje za početak',
},
},
custom_types_section: {
custom_types: 'Prilagođeni tipovi',
filter: 'Filtriraj',
clear: 'Očisti filter',
no_results:
'Nema pronađenih prilagođenih tipova koji odgovaraju vašem filteru.',
empty_state: {
title: 'Nema prilagođenih tipova',
description:
'Prilagođeni tipovi će se pojaviti ovdje kada budu dostupni u vašoj bazi podataka',
},
custom_type: {
kind: 'Vrsta',
enum_values: 'Enum vrijednosti',
composite_fields: 'Polja',
no_fields: 'Nema definiranih polja',
field_name_placeholder: 'Naziv polja',
field_type_placeholder: 'Odaberi tip',
add_field: 'Dodaj polje',
no_fields_tooltip:
'Nema definiranih polja za ovaj prilagođeni tip',
custom_type_actions: {
title: 'Radnje',
highlight_fields: 'Istakni polja',
clear_field_highlight: 'Ukloni isticanje',
delete_custom_type: 'Izbriši',
},
delete_custom_type: 'Izbriši tip',
},
},
},
toolbar: {
zoom_in: 'Uvećaj',
zoom_out: 'Smanji',
save: 'Spremi',
show_all: 'Prikaži sve',
undo: 'Poništi',
redo: 'Ponovi',
reorder_diagram: 'Preuredi dijagram',
highlight_overlapping_tables: 'Istakni preklapajuće tablice',
clear_custom_type_highlight: 'Ukloni isticanje za "{{typeName}}"',
custom_type_highlight_tooltip:
'Isticanje "{{typeName}}" - Kliknite za uklanjanje',
filter: 'Filtriraj tablice',
},
new_diagram_dialog: {
database_selection: {
title: 'Koja je vaša baza podataka?',
description:
'Svaka baza podataka ima svoje jedinstvene značajke i mogućnosti.',
check_examples_long: 'Pogledaj primjere',
check_examples_short: 'Primjeri',
},
import_database: {
title: 'Uvezite svoju bazu podataka',
database_edition: 'Verzija baze podataka:',
step_1: 'Pokrenite ovu skriptu u svojoj bazi podataka:',
step_2: 'Zalijepite rezultat skripte u ovaj dio →',
script_results_placeholder: 'Rezultati skripte ovdje...',
ssms_instructions: {
button_text: 'SSMS upute',
title: 'Upute',
step_1: 'Idite na Tools > Options > Query Results > SQL Server.',
step_2: 'Ako koristite "Results to Grid," promijenite Maximum Characters Retrieved za Non-XML podatke (postavite na 9999999).',
},
instructions_link: 'Trebate pomoć? Pogledajte kako',
check_script_result: 'Provjeri rezultat skripte',
},
cancel: 'Odustani',
import_from_file: 'Uvezi iz datoteke',
back: 'Natrag',
empty_diagram: 'Prazan dijagram',
continue: 'Nastavi',
import: 'Uvezi',
},
open_diagram_dialog: {
title: 'Otvori dijagram',
description: 'Odaberite dijagram za otvaranje iz popisa ispod.',
table_columns: {
name: 'Naziv',
created_at: 'Stvoreno',
last_modified: 'Zadnje izmijenjeno',
tables_count: 'Tablice',
},
cancel: 'Odustani',
open: 'Otvori',
},
export_sql_dialog: {
title: 'Izvezi SQL',
description:
'Izvezite shemu vašeg dijagrama u {{databaseType}} skriptu',
close: 'Zatvori',
loading: {
text: 'AI generira SQL za {{databaseType}}...',
description: 'Ovo bi trebalo potrajati do 30 sekundi.',
},
error: {
message:
'Greška pri generiranju SQL skripte. Molimo pokušajte ponovno kasnije ili <0>kontaktirajte nas</0>.',
description:
'Slobodno koristite svoj OPENAI_TOKEN, pogledajte priručnik <0>ovdje</0>.',
},
},
create_relationship_dialog: {
title: 'Kreiraj vezu',
primary_table: 'Primarna tablica',
primary_field: 'Primarno polje',
referenced_table: 'Referentna tablica',
referenced_field: 'Referentno polje',
primary_table_placeholder: 'Odaberi tablicu',
primary_field_placeholder: 'Odaberi polje',
referenced_table_placeholder: 'Odaberi tablicu',
referenced_field_placeholder: 'Odaberi polje',
no_tables_found: 'Nema pronađenih tablica',
no_fields_found: 'Nema pronađenih polja',
create: 'Kreiraj',
cancel: 'Odustani',
},
import_database_dialog: {
title: 'Uvezi u trenutni dijagram',
override_alert: {
title: 'Uvezi bazu podataka',
content: {
alert: 'Uvoz ovog dijagrama će utjecati na postojeće tablice i veze.',
new_tables:
'<bold>{{newTablesNumber}}</bold> novih tablica će biti dodano.',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> novih veza će biti stvoreno.',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> tablica će biti prepisano.',
proceed: 'Želite li nastaviti?',
},
import: 'Uvezi',
cancel: 'Odustani',
},
},
export_image_dialog: {
title: 'Izvezi sliku',
description: 'Odaberite faktor veličine za izvoz:',
scale_1x: '1x Obično',
scale_2x: '2x (Preporučeno)',
scale_3x: '3x',
scale_4x: '4x',
cancel: 'Odustani',
export: 'Izvezi',
advanced_options: 'Napredne opcije',
pattern: 'Uključi pozadinski uzorak',
pattern_description: 'Dodaj suptilni mrežni uzorak u pozadinu.',
transparent: 'Prozirna pozadina',
transparent_description: 'Ukloni boju pozadine iz slike.',
},
new_table_schema_dialog: {
title: 'Odaberi shemu',
description:
'Trenutno je prikazano više shema. Odaberite jednu za novu tablicu.',
cancel: 'Odustani',
confirm: 'Potvrdi',
},
update_table_schema_dialog: {
title: 'Promijeni shemu',
description: 'Ažuriraj shemu tablice "{{tableName}}"',
cancel: 'Odustani',
confirm: 'Promijeni',
},
create_table_schema_dialog: {
title: 'Stvori novu shemu',
description:
'Još ne postoje sheme. Stvorite svoju prvu shemu za organiziranje tablica.',
create: 'Stvori',
cancel: 'Odustani',
},
star_us_dialog: {
title: 'Pomozite nam da se poboljšamo!',
description:
'Želite li nam dati zvjezdicu na GitHubu? Samo je jedan klik!',
close: 'Ne sada',
confirm: 'Naravno!',
},
export_diagram_dialog: {
title: 'Izvezi dijagram',
description: 'Odaberite format za izvoz:',
format_json: 'JSON',
cancel: 'Odustani',
export: 'Izvezi',
error: {
title: 'Greška pri izvozu dijagrama',
description:
'Nešto je pošlo po zlu. Trebate pomoć? support@chartdb.io',
},
},
import_diagram_dialog: {
title: 'Uvezi dijagram',
description: 'Uvezite dijagram iz JSON datoteke.',
cancel: 'Odustani',
import: 'Uvezi',
error: {
title: 'Greška pri uvozu dijagrama',
description:
'JSON dijagrama je nevažeći. Molimo provjerite JSON i pokušajte ponovno. Trebate pomoć? support@chartdb.io',
},
},
import_dbml_dialog: {
example_title: 'Uvezi primjer DBML-a',
title: 'Uvezi DBML',
description: 'Uvezite shemu baze podataka iz DBML formata.',
import: 'Uvezi',
cancel: 'Odustani',
skip_and_empty: 'Preskoči i isprazni',
show_example: 'Prikaži primjer',
error: {
title: 'Greška pri uvozu DBML-a',
description:
'Neuspješno parsiranje DBML-a. Molimo provjerite sintaksu.',
},
},
relationship_type: {
one_to_one: 'Jedan na jedan',
one_to_many: 'Jedan na više',
many_to_one: 'Više na jedan',
many_to_many: 'Više na više',
},
canvas_context_menu: {
new_table: 'Nova tablica',
new_view: 'Novi Pogled',
new_relationship: 'Nova veza',
new_area: 'Novo područje',
},
table_node_context_menu: {
edit_table: 'Uredi tablicu',
duplicate_table: 'Dupliciraj tablicu',
delete_table: 'Izbriši tablicu',
add_relationship: 'Dodaj vezu',
},
snap_to_grid_tooltip: 'Priljepljivanje na mrežu (Drži {{key}})',
tool_tips: {
double_click_to_edit: 'Dvostruki klik za uređivanje',
},
language_select: {
change_language: 'Jezik',
},
on: 'Uključeno',
off: 'Isključeno',
},
};
export const hrMetadata: LanguageMetadata = {
name: 'Croatian',
nativeName: 'Hrvatski',
code: 'hr',
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const id_ID: LanguageTranslation = { export const id_ID: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'Baru',
browse: 'Jelajahi',
tables: 'Tabel',
refs: 'Refs',
areas: 'Area',
dependencies: 'Ketergantungan',
custom_types: 'Tipe Kustom',
},
menu: { menu: {
file: { databases: {
file: 'Berkas', databases: 'Basis Data',
new: 'Buat Baru', new: 'Diagram Baru',
open: 'Buka', browse: 'Jelajahi...',
save: 'Simpan', save: 'Simpan',
import: 'Impor Database', import: 'Impor Database',
export_sql: 'Ekspor SQL', export_sql: 'Ekspor SQL',
export_as: 'Ekspor Sebagai', export_as: 'Ekspor Sebagai',
delete_diagram: 'Hapus Diagram', delete_diagram: 'Hapus Diagram',
exit: 'Keluar',
}, },
edit: { edit: {
edit: 'Ubah', edit: 'Ubah',
@@ -26,7 +34,10 @@ export const id_ID: LanguageTranslation = {
hide_sidebar: 'Sembunyikan Sidebar', hide_sidebar: 'Sembunyikan Sidebar',
hide_cardinality: 'Sembunyikan Kardinalitas', hide_cardinality: 'Sembunyikan Kardinalitas',
show_cardinality: 'Tampilkan Kardinalitas', show_cardinality: 'Tampilkan Kardinalitas',
hide_field_attributes: 'Sembunyikan Atribut Kolom',
show_field_attributes: 'Tampilkan Atribut Kolom',
zoom_on_scroll: 'Perbesar saat Scroll', zoom_on_scroll: 'Perbesar saat Scroll',
show_views: 'Tampilan Database',
theme: 'Tema', theme: 'Tema',
show_dependencies: 'Tampilkan Dependensi', show_dependencies: 'Tampilkan Dependensi',
hide_dependencies: 'Sembunyikan Dependensi', hide_dependencies: 'Sembunyikan Dependensi',
@@ -70,15 +81,6 @@ export const id_ID: LanguageTranslation = {
cancel: 'Batal', cancel: 'Batal',
}, },
multiple_schemas_alert: {
title: 'Schema Lebih dari satu',
description:
'{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.',
dont_show_again: 'Jangan tampilkan lagi',
change_schema: 'Ubah',
none: 'Tidak ada',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'Gagal menyalin', title: 'Gagal menyalin',
@@ -113,14 +115,11 @@ export const id_ID: LanguageTranslation = {
copied: 'Tersalin!', copied: 'Tersalin!',
side_panel: { 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...', view_all_options: 'Tampilkan Semua Pilihan...',
tables_section: { tables_section: {
tables: 'Tabel', tables: 'Tabel',
add_table: 'Tambah Tabel', add_table: 'Tambah Tabel',
add_view: 'Tambah Tampilan',
filter: 'Saring', filter: 'Saring',
collapse: 'Lipat Semua', collapse: 'Lipat Semua',
// TODO: Translate // TODO: Translate
@@ -146,16 +145,23 @@ export const id_ID: LanguageTranslation = {
field_actions: { field_actions: {
title: 'Atribut Kolom', title: 'Atribut Kolom',
unique: 'Unik', unique: 'Unik',
auto_increment: 'Kenaikan Otomatis',
comments: 'Komentar', comments: 'Komentar',
no_comments: 'Tidak ada komentar', no_comments: 'Tidak ada komentar',
delete_field: 'Hapus Kolom', delete_field: 'Hapus Kolom',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Presisi',
scale: 'Skala',
}, },
index_actions: { index_actions: {
title: 'Atribut Indeks', title: 'Atribut Indeks',
name: 'Nama', name: 'Nama',
unique: 'Unik', unique: 'Unik',
index_type: 'Tipe Indeks',
delete_index: 'Hapus Indeks', delete_index: 'Hapus Indeks',
}, },
table_actions: { table_actions: {
@@ -172,12 +178,15 @@ export const id_ID: LanguageTranslation = {
description: 'Buat tabel untuk memulai', description: 'Buat tabel untuk memulai',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'Hubungan', refs: 'Refs',
filter: 'Saring', filter: 'Saring',
add_relationship: 'Tambah Hubungan',
collapse: 'Lipat Semua', collapse: 'Lipat Semua',
add_relationship: 'Tambah Hubungan',
relationships: 'Hubungan',
dependencies: 'Dependensi',
relationship: { relationship: {
relationship: 'Hubungan',
primary: 'Tabel Primer', primary: 'Tabel Primer',
foreign: 'Tabel Referensi', foreign: 'Tabel Referensi',
cardinality: 'Kardinalitas', cardinality: 'Kardinalitas',
@@ -187,16 +196,8 @@ export const id_ID: LanguageTranslation = {
delete_relationship: 'Hapus', delete_relationship: 'Hapus',
}, },
}, },
empty_state: {
title: 'Tidak ada hubungan',
description: 'Buat hubungan untuk menghubungkan tabel',
},
},
dependencies_section: {
dependencies: 'Dependensi',
filter: 'Saring',
collapse: 'Lipat Semua',
dependency: { dependency: {
dependency: 'Dependensi',
table: 'Tabel', table: 'Tabel',
dependent_table: 'Tampilan Dependen', dependent_table: 'Tampilan Dependen',
delete_dependency: 'Hapus', delete_dependency: 'Hapus',
@@ -206,8 +207,8 @@ export const id_ID: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'Tidak ada dependensi', title: 'Tidak ada hubungan',
description: 'Buat tampilan untuk memulai', description: 'Buat hubungan untuk memulai',
}, },
}, },
@@ -250,9 +251,12 @@ export const id_ID: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -267,7 +271,13 @@ export const id_ID: LanguageTranslation = {
undo: 'Undo', undo: 'Undo',
redo: 'Redo', redo: 'Redo',
reorder_diagram: 'Atur Ulang Diagram', reorder_diagram: 'Atur Ulang Diagram',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Sorot Tabel yang Tumpang Tindih', highlight_overlapping_tables: 'Sorot Tabel yang Tumpang Tindih',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -399,6 +409,14 @@ export const id_ID: LanguageTranslation = {
confirm: 'Ubah', confirm: 'Ubah',
}, },
create_table_schema_dialog: {
title: 'Buat Skema Baru',
description:
'Belum ada skema yang tersedia. Buat skema pertama Anda untuk mengatur tabel-tabel Anda.',
create: 'Buat',
cancel: 'Batal',
},
star_us_dialog: { star_us_dialog: {
title: 'Bantu kami meningkatkan!', title: 'Bantu kami meningkatkan!',
description: description:
@@ -455,6 +473,7 @@ export const id_ID: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'Tabel Baru', new_table: 'Tabel Baru',
new_view: 'Tampilan Baru',
new_relationship: 'Hubungan Baru', new_relationship: 'Hubungan Baru',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -476,6 +495,9 @@ export const id_ID: LanguageTranslation = {
language_select: { language_select: {
change_language: 'Bahasa', change_language: 'Bahasa',
}, },
on: 'Aktif',
off: 'Nonaktif',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ja: LanguageTranslation = { export const ja: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: '新規',
browse: '参照',
tables: 'テーブル',
refs: '参照',
areas: 'エリア',
dependencies: '依存関係',
custom_types: 'カスタムタイプ',
},
menu: { menu: {
file: { databases: {
file: 'ファイル', databases: 'データベース',
new: '新', new: '新しいダイアグラム',
open: '開く', browse: '参照...',
save: '保存', save: '保存',
import: 'データベースをインポート', import: 'データベースをインポート',
export_sql: 'SQLをエクスポート', export_sql: 'SQLをエクスポート',
export_as: '形式を指定してエクスポート', export_as: '形式を指定してエクスポート',
delete_diagram: 'ダイアグラムを削除', delete_diagram: 'ダイアグラムを削除',
exit: '終了',
}, },
edit: { edit: {
edit: '編集', edit: '編集',
@@ -26,7 +34,10 @@ export const ja: LanguageTranslation = {
hide_sidebar: 'サイドバーを非表示', hide_sidebar: 'サイドバーを非表示',
hide_cardinality: 'カーディナリティを非表示', hide_cardinality: 'カーディナリティを非表示',
show_cardinality: 'カーディナリティを表示', show_cardinality: 'カーディナリティを表示',
hide_field_attributes: 'フィールド属性を非表示',
show_field_attributes: 'フィールド属性を表示',
zoom_on_scroll: 'スクロールでズーム', zoom_on_scroll: 'スクロールでズーム',
show_views: 'データベースビュー',
theme: 'テーマ', theme: 'テーマ',
// TODO: Translate // TODO: Translate
show_dependencies: 'Show Dependencies', show_dependencies: 'Show Dependencies',
@@ -72,15 +83,6 @@ export const ja: LanguageTranslation = {
cancel: 'キャンセル', cancel: 'キャンセル',
}, },
multiple_schemas_alert: {
title: '複数のスキーマ',
description:
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
dont_show_again: '再表示しない',
change_schema: '変更',
none: 'なし',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'コピー失敗', title: 'コピー失敗',
@@ -117,14 +119,11 @@ export const ja: LanguageTranslation = {
copied: 'Copied!', copied: 'Copied!',
side_panel: { side_panel: {
schema: 'スキーマ:',
filter_by_schema: 'スキーマでフィルタ',
search_schema: 'スキーマを検索...',
no_schemas_found: 'スキーマが見つかりません。',
view_all_options: 'すべてのオプションを表示...', view_all_options: 'すべてのオプションを表示...',
tables_section: { tables_section: {
tables: 'テーブル', tables: 'テーブル',
add_table: 'テーブルを追加', add_table: 'テーブルを追加',
add_view: 'ビューを追加',
filter: 'フィルタ', filter: 'フィルタ',
collapse: 'すべて折りたたむ', collapse: 'すべて折りたたむ',
// TODO: Translate // TODO: Translate
@@ -150,16 +149,23 @@ export const ja: LanguageTranslation = {
field_actions: { field_actions: {
title: 'フィールド属性', title: 'フィールド属性',
unique: 'ユニーク', unique: 'ユニーク',
auto_increment: 'オートインクリメント',
comments: 'コメント', comments: 'コメント',
no_comments: 'コメントがありません', no_comments: 'コメントがありません',
delete_field: 'フィールドを削除', delete_field: 'フィールドを削除',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: '精度',
scale: '小数点以下桁数',
}, },
index_actions: { index_actions: {
title: 'インデックス属性', title: 'インデックス属性',
name: '名前', name: '名前',
unique: 'ユニーク', unique: 'ユニーク',
index_type: 'インデックスタイプ',
delete_index: 'インデックスを削除', delete_index: 'インデックスを削除',
}, },
table_actions: { table_actions: {
@@ -176,12 +182,15 @@ export const ja: LanguageTranslation = {
description: 'テーブルを作成して開始してください', description: 'テーブルを作成して開始してください',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'リレーションシップ', refs: '参照',
filter: 'フィルタ', filter: 'フィルタ',
add_relationship: 'リレーションシップを追加',
collapse: 'すべて折りたたむ', collapse: 'すべて折りたたむ',
add_relationship: 'リレーションシップを追加',
relationships: 'リレーションシップ',
dependencies: '依存関係',
relationship: { relationship: {
relationship: 'リレーションシップ',
primary: '主テーブル', primary: '主テーブル',
foreign: '参照テーブル', foreign: '参照テーブル',
cardinality: 'カーディナリティ', cardinality: 'カーディナリティ',
@@ -191,29 +200,20 @@ export const ja: LanguageTranslation = {
delete_relationship: '削除', delete_relationship: '削除',
}, },
}, },
dependency: {
dependency: '依存関係',
table: 'テーブル',
dependent_table: '依存ビュー',
delete_dependency: '削除',
dependency_actions: {
title: '操作',
delete_dependency: '削除',
},
},
empty_state: { empty_state: {
title: 'リレーションシップがありません', title: 'リレーションシップがありません',
description: description:
'テーブルを接続するためにリレーションシップを作成してください', '開始するためにリレーションシップを作成してください',
},
},
// TODO: Translate
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
}, },
}, },
@@ -256,9 +256,12 @@ export const ja: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -275,6 +278,10 @@ export const ja: LanguageTranslation = {
reorder_diagram: 'ダイアグラムを並べ替え', reorder_diagram: 'ダイアグラムを並べ替え',
// TODO: Translate // TODO: Translate
highlight_overlapping_tables: 'Highlight Overlapping Tables', highlight_overlapping_tables: 'Highlight Overlapping Tables',
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear', // TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -408,6 +415,14 @@ export const ja: LanguageTranslation = {
confirm: '変更', confirm: '変更',
}, },
create_table_schema_dialog: {
title: '新しいスキーマを作成',
description:
'スキーマがまだ存在しません。テーブルを整理するために最初のスキーマを作成してください。',
create: '作成',
cancel: 'キャンセル',
},
star_us_dialog: { star_us_dialog: {
title: '改善をサポートしてください!', title: '改善をサポートしてください!',
description: description:
@@ -463,6 +478,7 @@ export const ja: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: '新しいテーブル', new_table: '新しいテーブル',
new_view: '新しいビュー',
new_relationship: '新しいリレーションシップ', new_relationship: '新しいリレーションシップ',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -485,6 +501,9 @@ export const ja: LanguageTranslation = {
language_select: { language_select: {
change_language: '言語', change_language: '言語',
}, },
on: 'オン',
off: 'オフ',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ko_KR: LanguageTranslation = { export const ko_KR: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: '새로 만들기',
browse: '찾아보기',
tables: '테이블',
refs: 'Refs',
areas: '영역',
dependencies: '종속성',
custom_types: '사용자 지정 타입',
},
menu: { menu: {
file: { databases: {
file: '파일', databases: '데이터베이스',
new: '새 다이어그램', new: '새 다이어그램',
open: '열기', browse: '찾아보기...',
save: '저장', save: '저장',
import: '데이터베이스 가져오기', import: '데이터베이스 가져오기',
export_sql: 'SQL로 저장', export_sql: 'SQL로 저장',
export_as: '다른 형식으로 저장', export_as: '다른 형식으로 저장',
delete_diagram: '다이어그램 삭제', delete_diagram: '다이어그램 삭제',
exit: '종료',
}, },
edit: { edit: {
edit: '편집', edit: '편집',
@@ -26,7 +34,10 @@ export const ko_KR: LanguageTranslation = {
hide_sidebar: '사이드바 숨기기', hide_sidebar: '사이드바 숨기기',
hide_cardinality: '카디널리티 숨기기', hide_cardinality: '카디널리티 숨기기',
show_cardinality: '카디널리티 보이기', show_cardinality: '카디널리티 보이기',
hide_field_attributes: '필드 속성 숨기기',
show_field_attributes: '필드 속성 보이기',
zoom_on_scroll: '스크롤 시 확대', zoom_on_scroll: '스크롤 시 확대',
show_views: '데이터베이스 뷰',
theme: '테마', theme: '테마',
show_dependencies: '종속성 보이기', show_dependencies: '종속성 보이기',
hide_dependencies: '종속성 숨기기', hide_dependencies: '종속성 숨기기',
@@ -70,15 +81,6 @@ export const ko_KR: LanguageTranslation = {
cancel: '취소', cancel: '취소',
}, },
multiple_schemas_alert: {
title: '다중 스키마',
description:
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
dont_show_again: '다시 보여주지 마세요',
change_schema: '변경',
none: '없음',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: '복사 실패', title: '복사 실패',
@@ -113,14 +115,11 @@ export const ko_KR: LanguageTranslation = {
copied: '복사됨!', copied: '복사됨!',
side_panel: { side_panel: {
schema: '스키마:',
filter_by_schema: '스키마로 필터링',
search_schema: '스키마 검색...',
no_schemas_found: '스키마를 찾을 수 없습니다.',
view_all_options: '전체 옵션 보기...', view_all_options: '전체 옵션 보기...',
tables_section: { tables_section: {
tables: '테이블', tables: '테이블',
add_table: '테이블 추가', add_table: '테이블 추가',
add_view: '뷰 추가',
filter: '필터', filter: '필터',
collapse: '모두 접기', collapse: '모두 접기',
// TODO: Translate // TODO: Translate
@@ -146,16 +145,23 @@ export const ko_KR: LanguageTranslation = {
field_actions: { field_actions: {
title: '필드 속성', title: '필드 속성',
unique: '유니크 여부', unique: '유니크 여부',
auto_increment: '자동 증가',
comments: '주석', comments: '주석',
no_comments: '주석 없음', no_comments: '주석 없음',
delete_field: '필드 삭제', delete_field: '필드 삭제',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: '정밀도',
scale: '소수점 자릿수',
}, },
index_actions: { index_actions: {
title: '인덱스 속성', title: '인덱스 속성',
name: '인덱스 명', name: '인덱스 명',
unique: '유니크 여부', unique: '유니크 여부',
index_type: '인덱스 타입',
delete_index: '인덱스 삭제', delete_index: '인덱스 삭제',
}, },
table_actions: { table_actions: {
@@ -172,12 +178,15 @@ export const ko_KR: LanguageTranslation = {
description: '테이블을 만들어 시작하세요.', description: '테이블을 만들어 시작하세요.',
}, },
}, },
relationships_section: { refs_section: {
relationships: '연관 관계', refs: 'Refs',
filter: '필터', filter: '필터',
add_relationship: '연관 관계 추가',
collapse: '모두 접기', collapse: '모두 접기',
add_relationship: '연관 관계 추가',
relationships: '연관 관계',
dependencies: '종속성',
relationship: { relationship: {
relationship: '연관 관계',
primary: '주 테이블', primary: '주 테이블',
foreign: '참조 테이블', foreign: '참조 테이블',
cardinality: '카디널리티', cardinality: '카디널리티',
@@ -187,16 +196,8 @@ export const ko_KR: LanguageTranslation = {
delete_relationship: '연관 관계 삭제', delete_relationship: '연관 관계 삭제',
}, },
}, },
empty_state: {
title: '연관 관계',
description: '테이블 연결을 위해 연관 관계를 생성하세요',
},
},
dependencies_section: {
dependencies: '종속성',
filter: '필터',
collapse: '모두 접기',
dependency: { dependency: {
dependency: '종속성',
table: '테이블', table: '테이블',
dependent_table: '뷰 테이블', dependent_table: '뷰 테이블',
delete_dependency: '삭제', delete_dependency: '삭제',
@@ -206,8 +207,8 @@ export const ko_KR: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: '뷰 테이블 없음', title: '연관 관계 없음',
description: '뷰 테이블을 만들어 시작하세요.', description: '연관 관계를 만들어 시작하세요.',
}, },
}, },
@@ -250,9 +251,12 @@ export const ko_KR: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -267,7 +271,13 @@ export const ko_KR: LanguageTranslation = {
undo: '실행 취소', undo: '실행 취소',
redo: '다시 실행', redo: '다시 실행',
reorder_diagram: '다이어그램 재정렬', reorder_diagram: '다이어그램 재정렬',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '겹치는 테이블 강조 표시', highlight_overlapping_tables: '겹치는 테이블 강조 표시',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -399,6 +409,14 @@ export const ko_KR: LanguageTranslation = {
confirm: '변경', confirm: '변경',
}, },
create_table_schema_dialog: {
title: '새 스키마 생성',
description:
'아직 스키마가 없습니다. 테이블을 정리하기 위해 첫 번째 스키마를 생성하세요.',
create: '생성',
cancel: '취소',
},
star_us_dialog: { star_us_dialog: {
title: '개선할 수 있도록 도와주세요!', title: '개선할 수 있도록 도와주세요!',
description: description:
@@ -452,6 +470,7 @@ export const ko_KR: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: '새 테이블', new_table: '새 테이블',
new_view: '새 뷰',
new_relationship: '새 연관관계', new_relationship: '새 연관관계',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -473,6 +492,9 @@ export const ko_KR: LanguageTranslation = {
language_select: { language_select: {
change_language: '언어', change_language: '언어',
}, },
on: '켜기',
off: '끄기',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const mr: LanguageTranslation = { export const mr: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'नवीन',
browse: 'ब्राउज',
tables: 'टेबल',
refs: 'Refs',
areas: 'क्षेत्रे',
dependencies: 'अवलंबने',
custom_types: 'कस्टम प्रकार',
},
menu: { menu: {
file: { databases: {
file: 'फाइल', databases: 'डेटाबेस',
new: 'नवीन', new: 'नवीन आरेख',
open: 'उघडा', browse: 'ब्राउज करा...',
save: 'जतन करा', save: 'जतन करा',
import: 'डेटाबेस इम्पोर्ट करा', import: 'डेटाबेस इम्पोर्ट करा',
export_sql: 'SQL एक्स्पोर्ट करा', export_sql: 'SQL एक्स्पोर्ट करा',
export_as: 'म्हणून एक्स्पोर्ट करा', export_as: 'म्हणून एक्स्पोर्ट करा',
delete_diagram: 'आरेख हटवा', delete_diagram: 'आरेख हटवा',
exit: 'बाहेर पडा',
}, },
edit: { edit: {
edit: 'संपादन करा', edit: 'संपादन करा',
@@ -26,7 +34,10 @@ export const mr: LanguageTranslation = {
hide_sidebar: 'साइडबार लपवा', hide_sidebar: 'साइडबार लपवा',
hide_cardinality: 'कार्डिनॅलिटी लपवा', hide_cardinality: 'कार्डिनॅलिटी लपवा',
show_cardinality: 'कार्डिनॅलिटी दाखवा', show_cardinality: 'कार्डिनॅलिटी दाखवा',
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
zoom_on_scroll: 'स्क्रोलवर झूम करा', zoom_on_scroll: 'स्क्रोलवर झूम करा',
show_views: 'डेटाबेस व्ह्यूज',
theme: 'थीम', theme: 'थीम',
show_dependencies: 'डिपेंडेन्सि दाखवा', show_dependencies: 'डिपेंडेन्सि दाखवा',
hide_dependencies: 'डिपेंडेन्सि लपवा', hide_dependencies: 'डिपेंडेन्सि लपवा',
@@ -71,15 +82,6 @@ export const mr: LanguageTranslation = {
cancel: 'रद्द करा', cancel: 'रद्द करा',
}, },
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
dont_show_again: 'पुन्हा दाखवू नका',
change_schema: 'बदला',
none: 'काहीही नाही',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'कॉपी अयशस्वी', title: 'कॉपी अयशस्वी',
@@ -116,14 +118,11 @@ export const mr: LanguageTranslation = {
copied: 'Copied!', copied: 'Copied!',
side_panel: { side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारे फिल्टर करा',
search_schema: 'स्कीमा शोधा...',
no_schemas_found: 'कोणतेही स्कीमा सापडले नाहीत.',
view_all_options: 'सर्व पर्याय पहा...', view_all_options: 'सर्व पर्याय पहा...',
tables_section: { tables_section: {
tables: 'टेबल्स', tables: 'टेबल्स',
add_table: 'टेबल जोडा', add_table: 'टेबल जोडा',
add_view: 'व्ह्यू जोडा',
filter: 'फिल्टर', filter: 'फिल्टर',
collapse: 'सर्व संकुचित करा', collapse: 'सर्व संकुचित करा',
// TODO: Translate // TODO: Translate
@@ -149,16 +148,23 @@ export const mr: LanguageTranslation = {
field_actions: { field_actions: {
title: 'फील्ड गुणधर्म', title: 'फील्ड गुणधर्म',
unique: 'युनिक', unique: 'युनिक',
auto_increment: 'ऑटो इंक्रिमेंट',
comments: 'टिप्पण्या', comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत', no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा', delete_field: 'फील्ड हटवा',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'अचूकता',
scale: 'प्रमाण',
}, },
index_actions: { index_actions: {
title: 'इंडेक्स गुणधर्म', title: 'इंडेक्स गुणधर्म',
name: 'नाव', name: 'नाव',
unique: 'युनिक', unique: 'युनिक',
index_type: 'इंडेक्स प्रकार',
delete_index: 'इंडेक्स हटवा', delete_index: 'इंडेक्स हटवा',
}, },
table_actions: { table_actions: {
@@ -176,12 +182,15 @@ export const mr: LanguageTranslation = {
description: 'सुरू करण्यासाठी एक टेबल तयार करा', description: 'सुरू करण्यासाठी एक टेबल तयार करा',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'रिलेशनशिप', refs: 'Refs',
filter: 'फिल्टर', filter: 'फिल्टर',
add_relationship: 'रिलेशनशिप जोडा',
collapse: 'सर्व संकुचित करा', collapse: 'सर्व संकुचित करा',
add_relationship: 'रिलेशनशिप जोडा',
relationships: 'रिलेशनशिप',
dependencies: 'डिपेंडेन्सि',
relationship: { relationship: {
relationship: 'रिलेशनशिप',
primary: 'प्राथमिक टेबल', primary: 'प्राथमिक टेबल',
foreign: 'रेफरंस टेबल', foreign: 'रेफरंस टेबल',
cardinality: 'कार्डिनॅलिटी', cardinality: 'कार्डिनॅलिटी',
@@ -191,17 +200,8 @@ export const mr: LanguageTranslation = {
delete_relationship: 'हटवा', delete_relationship: 'हटवा',
}, },
}, },
empty_state: {
title: 'कोणतेही रिलेशनशिप नाहीत',
description:
'टेबल्स कनेक्ट करण्यासाठी एक रिलेशनशिप तयार करा',
},
},
dependencies_section: {
dependencies: 'डिपेंडेन्सि',
filter: 'फिल्टर',
collapse: 'सर्व संकुचित करा',
dependency: { dependency: {
dependency: 'डिपेंडेन्सि',
table: 'टेबल', table: 'टेबल',
dependent_table: 'डिपेंडेन्सि दृश्य', dependent_table: 'डिपेंडेन्सि दृश्य',
delete_dependency: 'हटवा', delete_dependency: 'हटवा',
@@ -211,8 +211,8 @@ export const mr: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'कोणत्याही डिपेंडेन्सि नाहीत', title: 'कोणतेही रिलेशनशिप नाहीत',
description: 'सुरू करण्यासाठी एक दृश्य तयार करा', description: 'सुरू करण्यासाठी एक रिलेशनशिप तयार करा',
}, },
}, },
@@ -255,9 +255,12 @@ export const mr: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -272,7 +275,13 @@ export const mr: LanguageTranslation = {
undo: 'पूर्ववत करा', undo: 'पूर्ववत करा',
redo: 'पुन्हा करा', redo: 'पुन्हा करा',
reorder_diagram: 'आरेख पुनःक्रमित करा', reorder_diagram: 'आरेख पुनःक्रमित करा',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा', highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -407,6 +416,14 @@ export const mr: LanguageTranslation = {
confirm: 'बदला', confirm: 'बदला',
}, },
create_table_schema_dialog: {
title: 'नवीन स्कीमा तयार करा',
description:
'अजून कोणतीही स्कीमा अस्तित्वात नाही. आपल्या टेबल्स व्यवस्थित करण्यासाठी आपली पहिली स्कीमा तयार करा.',
create: 'तयार करा',
cancel: 'रद्द करा',
},
star_us_dialog: { star_us_dialog: {
title: 'आम्हाला सुधारण्यास मदत करा!', title: 'आम्हाला सुधारण्यास मदत करा!',
description: description:
@@ -465,6 +482,7 @@ export const mr: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'नवीन टेबल', new_table: 'नवीन टेबल',
new_view: 'नवीन व्ह्यू',
new_relationship: 'नवीन रिलेशनशिप', new_relationship: 'नवीन रिलेशनशिप',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -488,6 +506,9 @@ export const mr: LanguageTranslation = {
language_select: { language_select: {
change_language: 'भाषा बदला', change_language: 'भाषा बदला',
}, },
on: 'चालू',
off: 'बंद',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ne: LanguageTranslation = { export const ne: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'नयाँ',
browse: 'ब्राउज',
tables: 'टेबलहरू',
refs: 'Refs',
areas: 'क्षेत्रहरू',
dependencies: 'निर्भरताहरू',
custom_types: 'कस्टम प्रकारहरू',
},
menu: { menu: {
file: { databases: {
file: 'फाइल', databases: 'डाटाबेसहरू',
new: 'नयाँ', new: 'नयाँ डायाग्राम',
open: 'खोल्नुहोस्', browse: 'ब्राउज गर्नुहोस्...',
save: 'सुरक्षित गर्नुहोस्', save: 'सुरक्षित गर्नुहोस्',
import: 'डाटाबेस आयात गर्नुहोस्', import: 'डाटाबेस आयात गर्नुहोस्',
export_sql: 'SQL निर्यात गर्नुहोस्', export_sql: 'SQL निर्यात गर्नुहोस्',
export_as: 'निर्यात गर्नुहोस्', export_as: 'निर्यात गर्नुहोस्',
delete_diagram: 'डायाग्राम हटाउनुहोस्', delete_diagram: 'डायाग्राम हटाउनुहोस्',
exit: 'बाहिर निस्कनुहोस्',
}, },
edit: { edit: {
edit: 'सम्पादन', edit: 'सम्पादन',
@@ -26,7 +34,10 @@ export const ne: LanguageTranslation = {
hide_sidebar: 'साइडबार लुकाउनुहोस्', hide_sidebar: 'साइडबार लुकाउनुहोस्',
hide_cardinality: 'कार्डिन्यालिटी लुकाउनुहोस्', hide_cardinality: 'कार्डिन्यालिटी लुकाउनुहोस्',
show_cardinality: 'कार्डिन्यालिटी देखाउनुहोस्', show_cardinality: 'कार्डिन्यालिटी देखाउनुहोस्',
hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्', zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
show_views: 'डाटाबेस भ्यूहरू',
theme: 'थिम', theme: 'थिम',
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्', show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्', hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
@@ -71,15 +82,6 @@ export const ne: LanguageTranslation = {
cancel: 'रद्द गर्नुहोस्', cancel: 'रद्द गर्नुहोस्',
}, },
multiple_schemas_alert: {
title: 'विविध स्कीमहरू',
description:
'{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
dont_show_again: 'फेरि देखाउन नदिनुहोस्',
change_schema: 'स्कीम परिवर्तन गर्नुहोस्',
none: 'कुनै पनि छैन',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'प्रतिलिपि असफल', title: 'प्रतिलिपि असफल',
@@ -114,14 +116,11 @@ export const ne: LanguageTranslation = {
copied: 'प्रतिलिपि गरियो!', copied: 'प्रतिलिपि गरियो!',
side_panel: { side_panel: {
schema: 'स्कीम:',
filter_by_schema: 'स्कीम अनुसार फिल्टर गर्नुहोस्',
search_schema: 'स्कीम खोज्नुहोस्...',
no_schemas_found: 'कुनै स्कीमहरू फेला परेनन्',
view_all_options: 'सबै विकल्पहरू हेर्नुहोस्', view_all_options: 'सबै विकल्पहरू हेर्नुहोस्',
tables_section: { tables_section: {
tables: 'तालिकाहरू', tables: 'तालिकाहरू',
add_table: 'तालिका थप्नुहोस्', add_table: 'तालिका थप्नुहोस्',
add_view: 'भ्यू थप्नुहोस्',
filter: 'फिल्टर', filter: 'फिल्टर',
collapse: 'सबै लुकाउनुहोस्', collapse: 'सबै लुकाउनुहोस्',
// TODO: Translate // TODO: Translate
@@ -147,16 +146,23 @@ export const ne: LanguageTranslation = {
field_actions: { field_actions: {
title: 'क्षेत्र विशेषताहरू', title: 'क्षेत्र विशेषताहरू',
unique: 'अनन्य', unique: 'अनन्य',
auto_increment: 'स्वचालित वृद्धि',
comments: 'टिप्पणीहरू', comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्', no_comments: 'कुनै टिप्पणीहरू छैनन्',
delete_field: 'क्षेत्र हटाउनुहोस्', delete_field: 'क्षेत्र हटाउनुहोस्',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'परिशुद्धता',
scale: 'स्केल',
}, },
index_actions: { index_actions: {
title: 'सूचक विशेषताहरू', title: 'सूचक विशेषताहरू',
name: 'नाम', name: 'नाम',
unique: 'अनन्य', unique: 'अनन्य',
index_type: 'इन्डेक्स प्रकार',
delete_index: 'सूचक हटाउनुहोस्', delete_index: 'सूचक हटाउनुहोस्',
}, },
table_actions: { table_actions: {
@@ -173,12 +179,15 @@ export const ne: LanguageTranslation = {
description: 'सुरु गर्नका लागि एक तालिका बनाउनुहोस्', description: 'सुरु गर्नका लागि एक तालिका बनाउनुहोस्',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'सम्बन्धहरू', refs: 'Refs',
filter: 'फिल्टर', filter: 'फिल्टर',
add_relationship: 'सम्बन्ध थप्नुहोस्',
collapse: 'सबै लुकाउनुहोस्', collapse: 'सबै लुकाउनुहोस्',
add_relationship: 'सम्बन्ध थप्नुहोस्',
relationships: 'सम्बन्धहरू',
dependencies: 'डिपेन्डेन्सीहरू',
relationship: { relationship: {
relationship: 'सम्बन्ध',
primary: 'मुख्य तालिका', primary: 'मुख्य तालिका',
foreign: 'परिचित तालिका', foreign: 'परिचित तालिका',
cardinality: 'कार्डिन्यालिटी', cardinality: 'कार्डिन्यालिटी',
@@ -188,16 +197,8 @@ export const ne: LanguageTranslation = {
delete_relationship: 'हटाउनुहोस्', delete_relationship: 'हटाउनुहोस्',
}, },
}, },
empty_state: {
title: 'कुनै सम्बन्धहरू छैनन्',
description: 'तालिकाहरू जोड्नका लागि एक सम्बन्ध बनाउनुहोस्',
},
},
dependencies_section: {
dependencies: 'डिपेन्डेन्सीहरू',
filter: 'फिल्टर',
collapse: 'सबै लुकाउनुहोस्',
dependency: { dependency: {
dependency: 'डिपेन्डेन्सी',
table: 'तालिका', table: 'तालिका',
dependent_table: 'विचलित तालिका', dependent_table: 'विचलित तालिका',
delete_dependency: 'हटाउनुहोस्', delete_dependency: 'हटाउनुहोस्',
@@ -207,9 +208,8 @@ export const ne: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'कुनै डिपेन्डेन्सीहरू छैनन्', title: 'कुनै सम्बन्धहरू छैनन्',
description: description: 'सुरु गर्नका लागि एक सम्बन्ध बनाउनुहोस्',
'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्',
}, },
}, },
@@ -252,9 +252,12 @@ export const ne: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -269,8 +272,14 @@ export const ne: LanguageTranslation = {
undo: 'पूर्ववत', undo: 'पूर्ववत',
redo: 'पुनः गर्नुहोस्', redo: 'पुनः गर्नुहोस्',
reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्', reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: highlight_overlapping_tables:
'अतिरिक्त तालिकाहरू हाइलाइट गर्नुहोस्', 'अतिरिक्त तालिकाहरू हाइलाइट गर्नुहोस्',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -404,6 +413,14 @@ export const ne: LanguageTranslation = {
confirm: 'परिवर्तन गर्नुहोस्', confirm: 'परिवर्तन गर्नुहोस्',
}, },
create_table_schema_dialog: {
title: 'नयाँ स्कीम सिर्जना गर्नुहोस्',
description:
'अहिलेसम्म कुनै स्कीम अस्तित्वमा छैन। आफ्ना तालिकाहरू व्यवस्थित गर्न आफ्नो पहिलो स्कीम सिर्जना गर्नुहोस्।',
create: 'सिर्जना गर्नुहोस्',
cancel: 'रद्द गर्नुहोस्',
},
star_us_dialog: { star_us_dialog: {
title: 'हामीलाई अझ राम्रो हुन मदत गर्नुहोस!', title: 'हामीलाई अझ राम्रो हुन मदत गर्नुहोस!',
description: description:
@@ -459,6 +476,7 @@ export const ne: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'नयाँ तालिका', new_table: 'नयाँ तालिका',
new_view: 'नयाँ भ्यू',
new_relationship: 'नयाँ सम्बन्ध', new_relationship: 'नयाँ सम्बन्ध',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -480,6 +498,9 @@ export const ne: LanguageTranslation = {
language_select: { language_select: {
change_language: 'भाषा परिवर्तन गर्नुहोस्', change_language: 'भाषा परिवर्तन गर्नुहोस्',
}, },
on: 'सक्रिय',
off: 'निष्क्रिय',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const pt_BR: LanguageTranslation = { export const pt_BR: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'Novo',
browse: 'Navegar',
tables: 'Tabelas',
refs: 'Refs',
areas: 'Áreas',
dependencies: 'Dependências',
custom_types: 'Tipos Personalizados',
},
menu: { menu: {
file: { databases: {
file: 'Arquivo', databases: 'Bancos de Dados',
new: 'Novo', new: 'Novo Diagrama',
open: 'Abrir', browse: 'Navegar...',
save: 'Salvar', save: 'Salvar',
import: 'Importar Banco de Dados', import: 'Importar Banco de Dados',
export_sql: 'Exportar SQL', export_sql: 'Exportar SQL',
export_as: 'Exportar como', export_as: 'Exportar como',
delete_diagram: 'Excluir Diagrama', delete_diagram: 'Excluir Diagrama',
exit: 'Sair',
}, },
edit: { edit: {
edit: 'Editar', edit: 'Editar',
@@ -26,7 +34,10 @@ export const pt_BR: LanguageTranslation = {
hide_sidebar: 'Ocultar Barra Lateral', hide_sidebar: 'Ocultar Barra Lateral',
hide_cardinality: 'Ocultar Cardinalidade', hide_cardinality: 'Ocultar Cardinalidade',
show_cardinality: 'Mostrar Cardinalidade', show_cardinality: 'Mostrar Cardinalidade',
hide_field_attributes: 'Ocultar Atributos de Campo',
show_field_attributes: 'Mostrar Atributos de Campo',
zoom_on_scroll: 'Zoom ao Rolar', zoom_on_scroll: 'Zoom ao Rolar',
show_views: 'Visualizações do Banco de Dados',
theme: 'Tema', theme: 'Tema',
show_dependencies: 'Mostrar Dependências', show_dependencies: 'Mostrar Dependências',
hide_dependencies: 'Ocultar Dependências', hide_dependencies: 'Ocultar Dependências',
@@ -71,15 +82,6 @@ export const pt_BR: LanguageTranslation = {
cancel: 'Cancelar', cancel: 'Cancelar',
}, },
multiple_schemas_alert: {
title: 'Múltiplos Esquemas',
description:
'{{schemasCount}} esquemas neste diagrama. Atualmente exibindo: {{formattedSchemas}}.',
dont_show_again: 'Não mostrar novamente',
change_schema: 'Alterar',
none: 'nenhum',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'Falha na cópia', title: 'Falha na cópia',
@@ -114,14 +116,11 @@ export const pt_BR: LanguageTranslation = {
copied: 'Copiado!', copied: 'Copiado!',
side_panel: { 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...', view_all_options: 'Ver todas as Opções...',
tables_section: { tables_section: {
tables: 'Tabelas', tables: 'Tabelas',
add_table: 'Adicionar Tabela', add_table: 'Adicionar Tabela',
add_view: 'Adicionar Visualização',
filter: 'Filtrar', filter: 'Filtrar',
collapse: 'Colapsar Todas', collapse: 'Colapsar Todas',
// TODO: Translate // TODO: Translate
@@ -147,16 +146,23 @@ export const pt_BR: LanguageTranslation = {
field_actions: { field_actions: {
title: 'Atributos do Campo', title: 'Atributos do Campo',
unique: 'Único', unique: 'Único',
auto_increment: 'Incremento Automático',
comments: 'Comentários', comments: 'Comentários',
no_comments: 'Sem comentários', no_comments: 'Sem comentários',
delete_field: 'Excluir Campo', delete_field: 'Excluir Campo',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Precisão',
scale: 'Escala',
}, },
index_actions: { index_actions: {
title: 'Atributos do Índice', title: 'Atributos do Índice',
name: 'Nome', name: 'Nome',
unique: 'Único', unique: 'Único',
index_type: 'Tipo de Índice',
delete_index: 'Excluir Índice', delete_index: 'Excluir Índice',
}, },
table_actions: { table_actions: {
@@ -173,12 +179,15 @@ export const pt_BR: LanguageTranslation = {
description: 'Crie uma tabela para começar', description: 'Crie uma tabela para começar',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'Relacionamentos', refs: 'Refs',
filter: 'Filtrar', filter: 'Filtrar',
add_relationship: 'Adicionar Relacionamento',
collapse: 'Colapsar Todas', collapse: 'Colapsar Todas',
add_relationship: 'Adicionar Relacionamento',
relationships: 'Relacionamentos',
dependencies: 'Dependências',
relationship: { relationship: {
relationship: 'Relacionamento',
primary: 'Tabela Primária', primary: 'Tabela Primária',
foreign: 'Tabela Referenciada', foreign: 'Tabela Referenciada',
cardinality: 'Cardinalidade', cardinality: 'Cardinalidade',
@@ -188,16 +197,8 @@ export const pt_BR: LanguageTranslation = {
delete_relationship: 'Excluir', delete_relationship: 'Excluir',
}, },
}, },
empty_state: {
title: 'Sem relacionamentos',
description: 'Crie um relacionamento para conectar tabelas',
},
},
dependencies_section: {
dependencies: 'Dependências',
filter: 'Filtrar',
collapse: 'Colapsar Todas',
dependency: { dependency: {
dependency: 'Dependência',
table: 'Tabela', table: 'Tabela',
dependent_table: 'Visualização Dependente', dependent_table: 'Visualização Dependente',
delete_dependency: 'Excluir', delete_dependency: 'Excluir',
@@ -207,8 +208,8 @@ export const pt_BR: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'Sem dependências', title: 'Sem relacionamentos',
description: 'Crie uma visualização para começar', description: 'Crie um relacionamento para começar',
}, },
}, },
@@ -251,9 +252,12 @@ export const pt_BR: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -268,7 +272,13 @@ export const pt_BR: LanguageTranslation = {
undo: 'Desfazer', undo: 'Desfazer',
redo: 'Refazer', redo: 'Refazer',
reorder_diagram: 'Reordenar Diagrama', reorder_diagram: 'Reordenar Diagrama',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Destacar Tabelas Sobrepostas', highlight_overlapping_tables: 'Destacar Tabelas Sobrepostas',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -402,6 +412,14 @@ export const pt_BR: LanguageTranslation = {
confirm: 'Alterar', confirm: 'Alterar',
}, },
create_table_schema_dialog: {
title: 'Criar Novo Esquema',
description:
'Ainda não existem esquemas. Crie seu primeiro esquema para organizar suas tabelas.',
create: 'Criar',
cancel: 'Cancelar',
},
star_us_dialog: { star_us_dialog: {
title: 'Ajude-nos a melhorar!', title: 'Ajude-nos a melhorar!',
description: description:
@@ -457,6 +475,7 @@ export const pt_BR: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'Nova Tabela', new_table: 'Nova Tabela',
new_view: 'Nova Visualização',
new_relationship: 'Novo Relacionamento', new_relationship: 'Novo Relacionamento',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -479,6 +498,9 @@ export const pt_BR: LanguageTranslation = {
language_select: { language_select: {
change_language: 'Idioma', change_language: 'Idioma',
}, },
on: 'Ligado',
off: 'Desligado',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ru: LanguageTranslation = { export const ru: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'Новая',
browse: 'Обзор',
tables: 'Таблицы',
refs: 'Ссылки',
areas: 'Области',
dependencies: 'Зависимости',
custom_types: 'Пользовательские типы',
},
menu: { menu: {
file: { databases: {
file: 'Файл', databases: 'Базы данных',
new: 'Создать', new: 'Новая диаграмма',
open: 'Открыть', browse: 'Обзор...',
save: 'Сохранить', save: 'Сохранить',
import: 'Импортировать базу данных', import: 'Импортировать базу данных',
export_sql: 'Экспорт SQL', export_sql: 'Экспорт SQL',
export_as: 'Экспортировать как', export_as: 'Экспортировать как',
delete_diagram: 'Удалить диаграмму', delete_diagram: 'Удалить диаграмму',
exit: 'Выход',
}, },
edit: { edit: {
edit: 'Изменение', edit: 'Изменение',
@@ -26,7 +34,10 @@ export const ru: LanguageTranslation = {
hide_sidebar: 'Скрыть боковую панель', hide_sidebar: 'Скрыть боковую панель',
hide_cardinality: 'Скрыть виды связи', hide_cardinality: 'Скрыть виды связи',
show_cardinality: 'Показать виды связи', show_cardinality: 'Показать виды связи',
show_field_attributes: 'Показать атрибуты поля',
hide_field_attributes: 'Скрыть атрибуты поля',
zoom_on_scroll: 'Увеличение при прокрутке', zoom_on_scroll: 'Увеличение при прокрутке',
show_views: 'Представления базы данных',
theme: 'Тема', theme: 'Тема',
show_dependencies: 'Показать зависимости', show_dependencies: 'Показать зависимости',
hide_dependencies: 'Скрыть зависимости', hide_dependencies: 'Скрыть зависимости',
@@ -69,15 +80,6 @@ export const ru: LanguageTranslation = {
cancel: 'Отменить', cancel: 'Отменить',
}, },
multiple_schemas_alert: {
title: 'Множественные схемы',
description:
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
dont_show_again: 'Больше не показывать',
change_schema: 'Изменить',
none: 'никто',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'Ошибка копирования', title: 'Ошибка копирования',
@@ -111,14 +113,11 @@ export const ru: LanguageTranslation = {
show_less: 'Показать меньше', show_less: 'Показать меньше',
side_panel: { side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фильтр по схеме',
search_schema: 'Схема поиска...',
no_schemas_found: 'Схемы не найдены.',
view_all_options: 'Просмотреть все варианты...', view_all_options: 'Просмотреть все варианты...',
tables_section: { tables_section: {
tables: 'Таблицы', tables: 'Таблицы',
add_table: 'Добавить таблицу', add_table: 'Добавить таблицу',
add_view: 'Добавить представление',
filter: 'Фильтр', filter: 'Фильтр',
collapse: 'Свернуть все', collapse: 'Свернуть все',
clear: 'Очистить фильтр', clear: 'Очистить фильтр',
@@ -144,15 +143,22 @@ export const ru: LanguageTranslation = {
field_actions: { field_actions: {
title: 'Атрибуты поля', title: 'Атрибуты поля',
unique: 'Уникальный', unique: 'Уникальный',
auto_increment: 'Автоинкремент',
comments: 'Комментарии', comments: 'Комментарии',
no_comments: 'Нет комментария', no_comments: 'Нет комментария',
delete_field: 'Удалить поле', delete_field: 'Удалить поле',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
character_length: 'Макс. длина', character_length: 'Макс. длина',
precision: 'Точность',
scale: 'Масштаб',
}, },
index_actions: { index_actions: {
title: 'Атрибуты индекса', title: 'Атрибуты индекса',
name: 'Имя', name: 'Имя',
unique: 'Уникальный', unique: 'Уникальный',
index_type: 'Тип индекса',
delete_index: 'Удалить индекс', delete_index: 'Удалить индекс',
}, },
table_actions: { table_actions: {
@@ -169,12 +175,15 @@ export const ru: LanguageTranslation = {
description: 'Создайте таблицу, чтобы начать', description: 'Создайте таблицу, чтобы начать',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'Отношения', refs: 'Ссылки',
filter: 'Фильтр', filter: 'Фильтр',
add_relationship: 'Добавить отношение',
collapse: 'Свернуть все', collapse: 'Свернуть все',
add_relationship: 'Добавить отношение',
relationships: 'Отношения',
dependencies: 'Зависимости',
relationship: { relationship: {
relationship: 'Отношение',
primary: 'Основная таблица', primary: 'Основная таблица',
foreign: 'Справочная таблица', foreign: 'Справочная таблица',
cardinality: 'Тип множественной связи', cardinality: 'Тип множественной связи',
@@ -184,18 +193,10 @@ export const ru: LanguageTranslation = {
delete_relationship: 'Удалить', delete_relationship: 'Удалить',
}, },
}, },
empty_state: {
title: 'Нет отношений',
description: 'Создайте связь для соединения таблиц',
},
},
dependencies_section: {
dependencies: 'Зависимости',
filter: 'Фильтр',
collapse: 'Свернуть все',
dependency: { dependency: {
table: 'Стол', dependency: 'Зависимость',
dependent_table: 'Зависимый вид', table: 'Таблица',
dependent_table: 'Зависимое представление',
delete_dependency: 'Удалить', delete_dependency: 'Удалить',
dependency_actions: { dependency_actions: {
title: 'Действия', title: 'Действия',
@@ -203,8 +204,8 @@ export const ru: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'Нет зависимостей', title: 'Нет отношений',
description: 'Создайте представление, чтобы начать', description: 'Создайте отношение, чтобы начать',
}, },
}, },
@@ -248,9 +249,12 @@ export const ru: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -265,7 +269,13 @@ export const ru: LanguageTranslation = {
undo: 'Отменить', undo: 'Отменить',
redo: 'Вернуть', redo: 'Вернуть',
reorder_diagram: 'Переупорядочить диаграмму', reorder_diagram: 'Переупорядочить диаграмму',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Выделение перекрывающихся таблиц', highlight_overlapping_tables: 'Выделение перекрывающихся таблиц',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -399,6 +409,14 @@ export const ru: LanguageTranslation = {
confirm: 'Изменить', confirm: 'Изменить',
}, },
create_table_schema_dialog: {
title: 'Создать новую схему',
description:
'Схемы еще не существуют. Создайте вашу первую схему, чтобы организовать таблицы.',
create: 'Создать',
cancel: 'Отменить',
},
star_us_dialog: { star_us_dialog: {
title: 'Помогите нам стать лучше!', title: 'Помогите нам стать лучше!',
description: description:
@@ -453,6 +471,7 @@ export const ru: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'Создать таблицу', new_table: 'Создать таблицу',
new_view: 'Новое представление',
new_relationship: 'Создать отношение', new_relationship: 'Создать отношение',
new_area: 'Новая область', new_area: 'Новая область',
}, },
@@ -474,6 +493,9 @@ export const ru: LanguageTranslation = {
language_select: { language_select: {
change_language: 'Сменить язык', change_language: 'Сменить язык',
}, },
on: 'Вкл',
off: 'Выкл',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const te: LanguageTranslation = { export const te: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'కొత్తది',
browse: 'బ్రాఉజ్',
tables: 'టేబల్లు',
refs: 'సంబంధాలు',
areas: 'ప్రదేశాలు',
dependencies: 'ఆధారతలు',
custom_types: 'కస్టమ్ టైప్స్',
},
menu: { menu: {
file: { databases: {
file: 'ఫైల్', databases: 'డేటాబేస్లు',
new: 'కొత్తది', new: 'కొత్త డైగ్రాం',
open: 'తెరవు', browse: 'బ్రాఉజ్ చేయండి...',
save: 'సేవ్', save: 'సేవ్',
import: 'డేటాబేస్‌ను దిగుమతి చేసుకోండి', import: 'డేటాబేస్‌ను దిగుమతి చేసుకోండి',
export_sql: 'SQL ఎగుమతి', export_sql: 'SQL ఎగుమతి',
export_as: 'వగా ఎగుమతి చేయండి', export_as: 'వగా ఎగుమతి చేయండి',
delete_diagram: 'చిత్రాన్ని తొలగించండి', delete_diagram: 'చిత్రాన్ని తొలగించండి',
exit: 'నిష్క్రమించు',
}, },
edit: { edit: {
edit: 'సవరించు', edit: 'సవరించు',
@@ -26,7 +34,10 @@ export const te: LanguageTranslation = {
hide_sidebar: 'సైడ్‌బార్ దాచండి', hide_sidebar: 'సైడ్‌బార్ దాచండి',
hide_cardinality: 'కార్డినాలిటీని దాచండి', hide_cardinality: 'కార్డినాలిటీని దాచండి',
show_cardinality: 'కార్డినాలిటీని చూపించండి', show_cardinality: 'కార్డినాలిటీని చూపించండి',
show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
zoom_on_scroll: 'స్క్రోల్‌పై జూమ్', zoom_on_scroll: 'స్క్రోల్‌పై జూమ్',
show_views: 'డేటాబేస్ వ్యూలు',
theme: 'థీమ్', theme: 'థీమ్',
show_dependencies: 'ఆధారాలు చూపించండి', show_dependencies: 'ఆధారాలు చూపించండి',
hide_dependencies: 'ఆధారాలను దాచండి', hide_dependencies: 'ఆధారాలను దాచండి',
@@ -71,15 +82,6 @@ export const te: LanguageTranslation = {
cancel: 'రద్దు', cancel: 'రద్దు',
}, },
multiple_schemas_alert: {
title: 'బహుళ స్కీమాలు',
description:
'{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.',
dont_show_again: 'మరలా చూపించవద్దు',
change_schema: 'మార్చు',
none: 'ఎదరికాదు',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'కాపీ విఫలమైంది', title: 'కాపీ విఫలమైంది',
@@ -114,14 +116,11 @@ export const te: LanguageTranslation = {
copied: 'కాపీ చేయబడింది!', copied: 'కాపీ చేయబడింది!',
side_panel: { side_panel: {
schema: 'స్కీమా:',
filter_by_schema: 'స్కీమా ద్వారా ఫిల్టర్ చేయండి',
search_schema: 'స్కీమా కోసం శోధించండి...',
no_schemas_found: 'ఏ స్కీమాలు కూడా కనుగొనబడలేదు.',
view_all_options: 'అన్ని ఎంపికలను చూడండి...', view_all_options: 'అన్ని ఎంపికలను చూడండి...',
tables_section: { tables_section: {
tables: 'పట్టికలు', tables: 'పట్టికలు',
add_table: 'పట్టికను జోడించు', add_table: 'పట్టికను జోడించు',
add_view: 'వ్యూ జోడించండి',
filter: 'ఫిల్టర్', filter: 'ఫిల్టర్',
collapse: 'అన్ని కూల్ చేయి', collapse: 'అన్ని కూల్ చేయి',
// TODO: Translate // TODO: Translate
@@ -147,16 +146,23 @@ export const te: LanguageTranslation = {
field_actions: { field_actions: {
title: 'ఫీల్డ్ గుణాలు', title: 'ఫీల్డ్ గుణాలు',
unique: 'అద్వితీయ', unique: 'అద్వితీయ',
auto_increment: 'ఆటో ఇంక్రిమెంట్',
comments: 'వ్యాఖ్యలు', comments: 'వ్యాఖ్యలు',
no_comments: 'వ్యాఖ్యలు లేవు', no_comments: 'వ్యాఖ్యలు లేవు',
delete_field: 'ఫీల్డ్ తొలగించు', delete_field: 'ఫీల్డ్ తొలగించు',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'సూక్ష్మత',
scale: 'స్కేల్',
}, },
index_actions: { index_actions: {
title: 'ఇండెక్స్ గుణాలు', title: 'ఇండెక్స్ గుణాలు',
name: 'పేరు', name: 'పేరు',
unique: 'అద్వితీయ', unique: 'అద్వితీయ',
index_type: 'ఇండెక్స్ రకం',
delete_index: 'ఇండెక్స్ తొలగించు', delete_index: 'ఇండెక్స్ తొలగించు',
}, },
table_actions: { table_actions: {
@@ -174,12 +180,15 @@ export const te: LanguageTranslation = {
description: 'ప్రారంభించడానికి ఒక పట్టిక సృష్టించండి', description: 'ప్రారంభించడానికి ఒక పట్టిక సృష్టించండి',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'సంబంధాలు', refs: 'Refs',
filter: 'ఫిల్టర్', filter: 'ఫిల్టర్',
add_relationship: 'సంబంధం జోడించు',
collapse: 'అన్ని కూల్ చేయి', collapse: 'అన్ని కూల్ చేయి',
add_relationship: 'సంబంధం జోడించు',
relationships: 'సంబంధాలు',
dependencies: 'ఆధారాలు',
relationship: { relationship: {
relationship: 'సంబంధం',
primary: 'ప్రాథమిక పట్టిక', primary: 'ప్రాథమిక పట్టిక',
foreign: 'సూచించబడిన పట్టిక', foreign: 'సూచించబడిన పట్టిక',
cardinality: 'కార్డినాలిటీ', cardinality: 'కార్డినాలిటీ',
@@ -189,16 +198,8 @@ export const te: LanguageTranslation = {
delete_relationship: 'సంబంధం తొలగించు', delete_relationship: 'సంబంధం తొలగించు',
}, },
}, },
empty_state: {
title: 'సంబంధాలు లేవు',
description: 'పట్టికలను అనుసంధించడానికి సంబంధం సృష్టించండి',
},
},
dependencies_section: {
dependencies: 'ఆధారాలు',
filter: 'ఫిల్టర్',
collapse: 'అన్ని కూల్ చేయి',
dependency: { dependency: {
dependency: 'ఆధారం',
table: 'పట్టిక', table: 'పట్టిక',
dependent_table: 'ఆధారిత వీక్షణ', dependent_table: 'ఆధారిత వీక్షణ',
delete_dependency: 'ఆధారాన్ని తొలగించు', delete_dependency: 'ఆధారాన్ని తొలగించు',
@@ -208,8 +209,8 @@ export const te: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'ఆధారాలు లేవు', title: 'సంబంధాలు లేవు',
description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి', description: 'ప్రారంభించడానికి ఒక సంబంధం సృష్టించండి',
}, },
}, },
@@ -252,9 +253,12 @@ export const te: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -269,7 +273,13 @@ export const te: LanguageTranslation = {
undo: 'తిరిగి చేయు', undo: 'తిరిగి చేయు',
redo: 'మరలా చేయు', redo: 'మరలా చేయు',
reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి', reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'అవకాశించు పట్టికలను హైలైట్ చేయండి', highlight_overlapping_tables: 'అవకాశించు పట్టికలను హైలైట్ చేయండి',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -403,6 +413,14 @@ export const te: LanguageTranslation = {
confirm: 'మార్చు', confirm: 'మార్చు',
}, },
create_table_schema_dialog: {
title: 'కొత్త స్కీమా సృష్టించండి',
description:
'ఇంకా ఏ స్కీమాలు లేవు. మీ పట్టికలను వ్యవస్థీకరించడానికి మీ మొదటి స్కీమాను సృష్టించండి.',
create: 'సృష్టించు',
cancel: 'రద్దు',
},
star_us_dialog: { star_us_dialog: {
title: 'మా సహాయంతో మెరుగుపరచండి!', title: 'మా సహాయంతో మెరుగుపరచండి!',
description: description:
@@ -461,6 +479,7 @@ export const te: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'కొత్త పట్టిక', new_table: 'కొత్త పట్టిక',
new_view: 'కొత్త వ్యూ',
new_relationship: 'కొత్త సంబంధం', new_relationship: 'కొత్త సంబంధం',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -484,6 +503,9 @@ export const te: LanguageTranslation = {
language_select: { language_select: {
change_language: 'భాష మార్చు', change_language: 'భాష మార్చు',
}, },
on: 'ఆన్',
off: 'ఆఫ్',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const tr: LanguageTranslation = { export const tr: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'Yeni',
browse: 'Gözat',
tables: 'Tablolar',
refs: 'Refs',
areas: 'Alanlar',
dependencies: 'Bağımlılıklar',
custom_types: 'Özel Tipler',
},
menu: { menu: {
file: { databases: {
file: 'Dosya', databases: 'Veritabanları',
new: 'Yeni', new: 'Yeni Diyagram',
open: '', browse: 'Gözat...',
save: 'Kaydet', save: 'Kaydet',
import: 'Veritabanı İçe Aktar', import: 'Veritabanı İçe Aktar',
export_sql: 'SQL Olarak Dışa Aktar', export_sql: 'SQL Olarak Dışa Aktar',
export_as: 'Olarak Dışa Aktar', export_as: 'Olarak Dışa Aktar',
delete_diagram: 'Diyagramı Sil', delete_diagram: 'Diyagramı Sil',
exit: ıkış',
}, },
edit: { edit: {
edit: 'Düzenle', edit: 'Düzenle',
@@ -26,7 +34,10 @@ export const tr: LanguageTranslation = {
hide_sidebar: 'Kenar Çubuğunu Gizle', hide_sidebar: 'Kenar Çubuğunu Gizle',
hide_cardinality: 'Kardinaliteyi Gizle', hide_cardinality: 'Kardinaliteyi Gizle',
show_cardinality: 'Kardinaliteyi Göster', show_cardinality: 'Kardinaliteyi Göster',
show_field_attributes: 'Alan Özelliklerini Göster',
hide_field_attributes: 'Alan Özelliklerini Gizle',
zoom_on_scroll: 'Kaydırarak Yakınlaştır', zoom_on_scroll: 'Kaydırarak Yakınlaştır',
show_views: 'Veritabanı Görünümleri',
theme: 'Tema', theme: 'Tema',
show_dependencies: 'Bağımlılıkları Göster', show_dependencies: 'Bağımlılıkları Göster',
hide_dependencies: 'Bağımlılıkları Gizle', hide_dependencies: 'Bağımlılıkları Gizle',
@@ -71,15 +82,6 @@ export const tr: LanguageTranslation = {
cancel: 'İptal', cancel: 'İptal',
}, },
multiple_schemas_alert: {
title: 'Birden Fazla Şema',
description:
'Bu diyagramda {{schemasCount}} şema var. Şu anda görüntülenen: {{formattedSchemas}}.',
dont_show_again: 'Tekrar gösterme',
change_schema: 'Değiştir',
none: 'yok',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'Kopyalama başarısız', title: 'Kopyalama başarısız',
@@ -113,14 +115,11 @@ export const tr: LanguageTranslation = {
copy_to_clipboard: 'Panoya Kopyala', copy_to_clipboard: 'Panoya Kopyala',
copied: 'Kopyalandı!', copied: 'Kopyalandı!',
side_panel: { 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...', view_all_options: 'Tüm Seçenekleri Gör...',
tables_section: { tables_section: {
tables: 'Tablolar', tables: 'Tablolar',
add_table: 'Tablo Ekle', add_table: 'Tablo Ekle',
add_view: 'Görünüm Ekle',
filter: 'Filtrele', filter: 'Filtrele',
collapse: 'Hepsini Daralt', collapse: 'Hepsini Daralt',
// TODO: Translate // TODO: Translate
@@ -146,16 +145,23 @@ export const tr: LanguageTranslation = {
field_actions: { field_actions: {
title: 'Alan Özellikleri', title: 'Alan Özellikleri',
unique: 'Tekil', unique: 'Tekil',
auto_increment: 'Otomatik Artış',
comments: 'Yorumlar', comments: 'Yorumlar',
no_comments: 'Yorum yok', no_comments: 'Yorum yok',
delete_field: 'Alanı Sil', delete_field: 'Alanı Sil',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Hassasiyet',
scale: 'Ölçek',
}, },
index_actions: { index_actions: {
title: 'İndeks Özellikleri', title: 'İndeks Özellikleri',
name: 'Ad', name: 'Ad',
unique: 'Tekil', unique: 'Tekil',
index_type: 'İndeks Türü',
delete_index: 'İndeksi Sil', delete_index: 'İndeksi Sil',
}, },
table_actions: { table_actions: {
@@ -173,12 +179,15 @@ export const tr: LanguageTranslation = {
description: 'Başlamak için bir tablo oluşturun', description: 'Başlamak için bir tablo oluşturun',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'İlişkiler', refs: 'Refs',
filter: 'Filtrele', filter: 'Filtrele',
add_relationship: 'İlişki Ekle',
collapse: 'Hepsini Daralt', collapse: 'Hepsini Daralt',
add_relationship: 'İlişki Ekle',
relationships: 'İlişkiler',
dependencies: 'Bağımlılıklar',
relationship: { relationship: {
relationship: 'İlişki',
primary: 'Birincil Tablo', primary: 'Birincil Tablo',
foreign: 'Referans Tablo', foreign: 'Referans Tablo',
cardinality: 'Kardinalite', cardinality: 'Kardinalite',
@@ -188,16 +197,8 @@ export const tr: LanguageTranslation = {
delete_relationship: 'Sil', delete_relationship: 'Sil',
}, },
}, },
empty_state: {
title: 'İlişki yok',
description: 'Tabloları bağlamak için bir ilişki oluşturun',
},
},
dependencies_section: {
dependencies: 'Bağımlılıklar',
filter: 'Filtrele',
collapse: 'Hepsini Daralt',
dependency: { dependency: {
dependency: 'Bağımlılık',
table: 'Tablo', table: 'Tablo',
dependent_table: 'Bağımlı Görünüm', dependent_table: 'Bağımlı Görünüm',
delete_dependency: 'Sil', delete_dependency: 'Sil',
@@ -207,8 +208,8 @@ export const tr: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'Bağımlılık yok', title: 'İlişki yok',
description: 'Başlamak için bir görünüm oluşturun', description: 'Başlamak için bir ilişki oluşturun',
}, },
}, },
@@ -251,9 +252,12 @@ export const tr: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -267,7 +271,13 @@ export const tr: LanguageTranslation = {
undo: 'Geri Al', undo: 'Geri Al',
redo: 'Yinele', redo: 'Yinele',
reorder_diagram: 'Diyagramı Yeniden Sırala', reorder_diagram: 'Diyagramı Yeniden Sırala',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Çakışan Tabloları Vurgula', highlight_overlapping_tables: 'Çakışan Tabloları Vurgula',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
database_selection: { database_selection: {
@@ -392,6 +402,14 @@ export const tr: LanguageTranslation = {
cancel: 'İptal', cancel: 'İptal',
confirm: 'Değiştir', confirm: 'Değiştir',
}, },
create_table_schema_dialog: {
title: 'Yeni Şema Oluştur',
description:
'Henüz hiç şema mevcut değil. Tablolarınızı düzenlemek için ilk şemanızı oluşturun.',
create: 'Oluştur',
cancel: 'İptal',
},
star_us_dialog: { star_us_dialog: {
title: 'Bize yardım et!', title: 'Bize yardım et!',
description: description:
@@ -446,6 +464,7 @@ export const tr: LanguageTranslation = {
}, },
canvas_context_menu: { canvas_context_menu: {
new_table: 'Yeni Tablo', new_table: 'Yeni Tablo',
new_view: 'Yeni Görünüm',
new_relationship: 'Yeni İlişki', new_relationship: 'Yeni İlişki',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -468,6 +487,9 @@ export const tr: LanguageTranslation = {
language_select: { language_select: {
change_language: 'Dil', change_language: 'Dil',
}, },
on: 'Açık',
off: 'Kapalı',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const uk: LanguageTranslation = { export const uk: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'Нова',
browse: 'Огляд',
tables: 'Таблиці',
refs: 'Зв’язки',
areas: 'Області',
dependencies: 'Залежності',
custom_types: 'Користувацькі типи',
},
menu: { menu: {
file: { databases: {
file: 'Файл', databases: 'Бази даних',
new: 'Новий', new: 'Нова діаграма',
open: 'Відкрити', browse: 'Огляд...',
save: 'Зберегти', save: 'Зберегти',
import: 'Імпорт бази даних', import: 'Імпорт бази даних',
export_sql: 'Експорт SQL', export_sql: 'Експорт SQL',
export_as: 'Експортувати як', export_as: 'Експортувати як',
delete_diagram: 'Видалити діаграму', delete_diagram: 'Видалити діаграму',
exit: 'Вийти',
}, },
edit: { edit: {
edit: 'Редагувати', edit: 'Редагувати',
@@ -26,7 +34,10 @@ export const uk: LanguageTranslation = {
hide_sidebar: 'Приховати бічну панель', hide_sidebar: 'Приховати бічну панель',
hide_cardinality: 'Приховати потужність', hide_cardinality: 'Приховати потужність',
show_cardinality: 'Показати кардинальність', show_cardinality: 'Показати кардинальність',
show_field_attributes: 'Показати атрибути полів',
hide_field_attributes: 'Приховати атрибути полів',
zoom_on_scroll: 'Масштабувати прокручуванням', zoom_on_scroll: 'Масштабувати прокручуванням',
show_views: 'Представлення бази даних',
theme: 'Тема', theme: 'Тема',
show_dependencies: 'Показати залежності', show_dependencies: 'Показати залежності',
hide_dependencies: 'Приховати залежності', hide_dependencies: 'Приховати залежності',
@@ -69,15 +80,6 @@ export const uk: LanguageTranslation = {
cancel: 'Скасувати', cancel: 'Скасувати',
}, },
multiple_schemas_alert: {
title: 'Кілька схем',
description:
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
dont_show_again: 'Більше не показувати',
change_schema: 'Зміна',
none: 'немає',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'Помилка копіювання', title: 'Помилка копіювання',
@@ -112,14 +114,11 @@ export const uk: LanguageTranslation = {
copied: 'Скопійовано!', copied: 'Скопійовано!',
side_panel: { side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фільтрувати за схемою',
search_schema: 'Пошук схеми…',
no_schemas_found: 'Схеми не знайдено.',
view_all_options: 'Переглянути всі параметри…', view_all_options: 'Переглянути всі параметри…',
tables_section: { tables_section: {
tables: 'Таблиці', tables: 'Таблиці',
add_table: 'Додати таблицю', add_table: 'Додати таблицю',
add_view: 'Додати представлення',
filter: 'Фільтр', filter: 'Фільтр',
collapse: 'Згорнути все', collapse: 'Згорнути все',
// TODO: Translate // TODO: Translate
@@ -145,16 +144,23 @@ export const uk: LanguageTranslation = {
field_actions: { field_actions: {
title: 'Атрибути полів', title: 'Атрибути полів',
unique: 'Унікальне', unique: 'Унікальне',
auto_increment: 'Автоінкремент',
comments: 'Коментарі', comments: 'Коментарі',
no_comments: 'Немає коментарів', no_comments: 'Немає коментарів',
delete_field: 'Видалити поле', delete_field: 'Видалити поле',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Точність',
scale: 'Масштаб',
}, },
index_actions: { index_actions: {
title: 'Атрибути індексу', title: 'Атрибути індексу',
name: 'Назва індекса', name: 'Назва індекса',
unique: 'Унікальний', unique: 'Унікальний',
index_type: 'Тип індексу',
delete_index: 'Видалити індекс', delete_index: 'Видалити індекс',
}, },
table_actions: { table_actions: {
@@ -171,12 +177,15 @@ export const uk: LanguageTranslation = {
description: 'Щоб почати, створіть таблицю', description: 'Щоб почати, створіть таблицю',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'Звʼязки', refs: 'Refs',
filter: 'Фільтр', filter: 'Фільтр',
add_relationship: 'Додати звʼязок',
collapse: 'Згорнути все', collapse: 'Згорнути все',
add_relationship: 'Додати звʼязок',
relationships: 'Звʼязки',
dependencies: 'Залежності',
relationship: { relationship: {
relationship: 'Звʼязок',
primary: 'Первинна таблиця', primary: 'Первинна таблиця',
foreign: 'Посилання на таблицю', foreign: 'Посилання на таблицю',
cardinality: 'Звʼязок', cardinality: 'Звʼязок',
@@ -186,16 +195,8 @@ export const uk: LanguageTranslation = {
delete_relationship: 'Видалити', delete_relationship: 'Видалити',
}, },
}, },
empty_state: {
title: 'Звʼязків немає',
description: 'Створіть звʼязок для зʼєднання таблиць',
},
},
dependencies_section: {
dependencies: 'Залежності',
filter: 'Фільтр',
collapse: 'Згорнути все',
dependency: { dependency: {
dependency: 'Залежність',
table: 'Таблиця', table: 'Таблиця',
dependent_table: 'Залежне подання', dependent_table: 'Залежне подання',
delete_dependency: 'Видалити', delete_dependency: 'Видалити',
@@ -205,8 +206,8 @@ export const uk: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'Жодних залежностей', title: 'Жодних зв’язків',
description: 'Створіть подання, щоб почати', description: 'Створіть зв’язок, щоб почати',
}, },
}, },
@@ -249,9 +250,12 @@ export const uk: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -266,7 +270,13 @@ export const uk: LanguageTranslation = {
undo: 'Скасувати', undo: 'Скасувати',
redo: 'Повторити', redo: 'Повторити',
reorder_diagram: 'Перевпорядкувати діаграму', reorder_diagram: 'Перевпорядкувати діаграму',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Показати таблиці, що перекриваються', highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -400,6 +410,14 @@ export const uk: LanguageTranslation = {
confirm: 'Змінити', confirm: 'Змінити',
}, },
create_table_schema_dialog: {
title: 'Створити нову схему',
description:
'Поки що не існує жодної схеми. Створіть свою першу схему, щоб організувати ваші таблиці.',
create: 'Створити',
cancel: 'Скасувати',
},
star_us_dialog: { star_us_dialog: {
title: 'Допоможіть нам покращитися!', title: 'Допоможіть нам покращитися!',
description: 'Поставне на зірку на GitHub? Це лише один клік!', description: 'Поставне на зірку на GitHub? Це лише один клік!',
@@ -452,6 +470,7 @@ export const uk: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'Нова таблиця', new_table: 'Нова таблиця',
new_view: 'Нове представлення',
new_relationship: 'Новий звʼязок', new_relationship: 'Новий звʼязок',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -473,6 +492,9 @@ export const uk: LanguageTranslation = {
language_select: { language_select: {
change_language: 'Мова', change_language: 'Мова',
}, },
on: 'Увімк',
off: 'Вимк',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const vi: LanguageTranslation = { export const vi: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: 'Mới',
browse: 'Duyệt',
tables: 'Bảng',
refs: 'Refs',
areas: 'Khu vực',
dependencies: 'Phụ thuộc',
custom_types: 'Kiểu tùy chỉnh',
},
menu: { menu: {
file: { databases: {
file: 'Tệp', databases: 'Cơ sở dữ liệu',
new: 'Tạo mới', new: 'Sơ đồ mới',
open: 'Mở', browse: 'Duyệt...',
save: 'Lưu', save: 'Lưu',
import: 'Nhập cơ sở dữ liệu', import: 'Nhập cơ sở dữ liệu',
export_sql: 'Xuất SQL', export_sql: 'Xuất SQL',
export_as: 'Xuất thành', export_as: 'Xuất thành',
delete_diagram: 'Xóa sơ đồ', delete_diagram: 'Xóa sơ đồ',
exit: 'Thoát',
}, },
edit: { edit: {
edit: 'Sửa', edit: 'Sửa',
@@ -26,7 +34,10 @@ export const vi: LanguageTranslation = {
hide_sidebar: 'Ẩn thanh bên', hide_sidebar: 'Ẩn thanh bên',
hide_cardinality: 'Ẩn số lượng', hide_cardinality: 'Ẩn số lượng',
show_cardinality: 'Hiển thị số lượng', show_cardinality: 'Hiển thị số lượng',
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', zoom_on_scroll: 'Thu phóng khi cuộn',
show_views: 'Chế độ xem Cơ sở dữ liệu',
theme: 'Chủ đề', theme: 'Chủ đề',
show_dependencies: 'Hiển thị các phụ thuộc', show_dependencies: 'Hiển thị các phụ thuộc',
hide_dependencies: 'Ẩn các phụ thuộc', hide_dependencies: 'Ẩn các phụ thuộc',
@@ -70,15 +81,6 @@ export const vi: LanguageTranslation = {
cancel: 'Hủy', 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}}.',
dont_show_again: 'Không hiển thị lại',
change_schema: 'Thay đổi',
none: 'không có',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: 'Sao chép thất bại', title: 'Sao chép thất bại',
@@ -113,14 +115,11 @@ export const vi: LanguageTranslation = {
copied: 'Đã sao chép!', copied: 'Đã sao chép!',
side_panel: { 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...', view_all_options: 'Xem tất cả tùy chọn...',
tables_section: { tables_section: {
tables: 'Bảng', tables: 'Bảng',
add_table: 'Thêm bảng', add_table: 'Thêm bảng',
add_view: 'Thêm Chế độ xem',
filter: 'Lọc', filter: 'Lọc',
collapse: 'Thu gọn tất cả', collapse: 'Thu gọn tất cả',
// TODO: Translate // TODO: Translate
@@ -146,16 +145,23 @@ export const vi: LanguageTranslation = {
field_actions: { field_actions: {
title: 'Thuộc tính trường', title: 'Thuộc tính trường',
unique: 'Giá trị duy nhất', unique: 'Giá trị duy nhất',
auto_increment: 'Tự động tăng',
comments: 'Bình luận', comments: 'Bình luận',
no_comments: 'Không có bình luận', no_comments: 'Không có bình luận',
delete_field: 'Xóa trường', delete_field: 'Xóa trường',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: 'Độ chính xác',
scale: 'Tỷ lệ',
}, },
index_actions: { index_actions: {
title: 'Thuộc tính chỉ mục', title: 'Thuộc tính chỉ mục',
name: 'Tên', name: 'Tên',
unique: 'Giá trị duy nhất', unique: 'Giá trị duy nhất',
index_type: 'Loại chỉ mục',
delete_index: 'Xóa chỉ mục', delete_index: 'Xóa chỉ mục',
}, },
table_actions: { table_actions: {
@@ -172,12 +178,15 @@ export const vi: LanguageTranslation = {
description: 'Tạo một bảng để bắt đầu', description: 'Tạo một bảng để bắt đầu',
}, },
}, },
relationships_section: { refs_section: {
relationships: 'Quan hệ', refs: 'Refs',
filter: 'Lọc', filter: 'Lọc',
add_relationship: 'Thêm quan hệ',
collapse: 'Thu gọn tất cả', collapse: 'Thu gọn tất cả',
add_relationship: 'Thêm quan hệ',
relationships: 'Quan hệ',
dependencies: 'Phụ thuộc',
relationship: { relationship: {
relationship: 'Quan hệ',
primary: 'Bảng khóa chính', primary: 'Bảng khóa chính',
foreign: 'Bảng khóa ngoại', foreign: 'Bảng khóa ngoại',
cardinality: 'Quan hệ', cardinality: 'Quan hệ',
@@ -187,16 +196,8 @@ export const vi: LanguageTranslation = {
delete_relationship: 'Xóa', delete_relationship: 'Xóa',
}, },
}, },
empty_state: {
title: 'Không có quan hệ',
description: 'Tạo quan hệ để kết nối các bảng',
},
},
dependencies_section: {
dependencies: 'Phụ thuộc',
filter: 'Lọc',
collapse: 'Thu gọn tất cả',
dependency: { dependency: {
dependency: 'Phụ thuộc',
table: 'Bảng', table: 'Bảng',
dependent_table: 'Bảng xem phụ thuộc', dependent_table: 'Bảng xem phụ thuộc',
delete_dependency: 'Xóa', delete_dependency: 'Xóa',
@@ -206,8 +207,8 @@ export const vi: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: 'Không có phụ thuộc', title: 'Không có quan hệ',
description: 'Tạo bảng xem phụ thuộc để bắt đầu', description: 'Tạo một quan hệ để bắt đầu',
}, },
}, },
@@ -250,9 +251,12 @@ export const vi: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -267,7 +271,13 @@ export const vi: LanguageTranslation = {
undo: 'Hoàn tác', undo: 'Hoàn tác',
redo: 'Làm lại', redo: 'Làm lại',
reorder_diagram: 'Sắp xếp lại sơ đồ', reorder_diagram: 'Sắp xếp lại sơ đồ',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'Làm nổi bật các bảng chồng chéo', highlight_overlapping_tables: 'Làm nổi bật các bảng chồng chéo',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -399,6 +409,14 @@ export const vi: LanguageTranslation = {
confirm: 'Xác nhận', confirm: 'Xác nhận',
}, },
create_table_schema_dialog: {
title: 'Tạo lược đồ mới',
description:
'Chưa có lược đồ nào. Tạo lược đồ đầu tiên của bạn để tổ chức các bảng.',
create: 'Tạo',
cancel: 'Hủy',
},
star_us_dialog: { star_us_dialog: {
title: 'Hãy giúp chúng tôi cải thiện!', title: 'Hãy giúp chúng tôi cải thiện!',
description: description:
@@ -453,6 +471,7 @@ export const vi: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: 'Tạo bảng mới', new_table: 'Tạo bảng mới',
new_view: 'Chế độ xem Mới',
new_relationship: 'Tạo quan hệ mới', new_relationship: 'Tạo quan hệ mới',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -474,6 +493,9 @@ export const vi: LanguageTranslation = {
language_select: { language_select: {
change_language: 'Ngôn ngữ', change_language: 'Ngôn ngữ',
}, },
on: 'Bật',
off: 'Tắt',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_CN: LanguageTranslation = { export const zh_CN: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: '新建',
browse: '浏览',
tables: '表',
refs: '引用',
areas: '区域',
dependencies: '依赖关系',
custom_types: '自定义类型',
},
menu: { menu: {
file: { databases: {
file: '文件', databases: '数据库',
new: '新建', new: '新建关系图',
open: '打开', browse: '浏览...',
save: '保存', save: '保存',
import: '导入数据库', import: '导入数据库',
export_sql: '导出 SQL 语句', export_sql: '导出 SQL 语句',
export_as: '导出为', export_as: '导出为',
delete_diagram: '删除关系图', delete_diagram: '删除关系图',
exit: '退出',
}, },
edit: { edit: {
edit: '编辑', edit: '编辑',
@@ -26,7 +34,10 @@ export const zh_CN: LanguageTranslation = {
hide_sidebar: '隐藏侧边栏', hide_sidebar: '隐藏侧边栏',
hide_cardinality: '隐藏基数', hide_cardinality: '隐藏基数',
show_cardinality: '展示基数', show_cardinality: '展示基数',
show_field_attributes: '展示字段属性',
hide_field_attributes: '隐藏字段属性',
zoom_on_scroll: '滚动缩放', zoom_on_scroll: '滚动缩放',
show_views: '数据库视图',
theme: '主题', theme: '主题',
show_dependencies: '展示依赖', show_dependencies: '展示依赖',
hide_dependencies: '隐藏依赖', hide_dependencies: '隐藏依赖',
@@ -67,15 +78,6 @@ export const zh_CN: LanguageTranslation = {
cancel: '取消', cancel: '取消',
}, },
multiple_schemas_alert: {
title: '多个模式',
description:
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
dont_show_again: '不再展示',
change_schema: '更改',
none: '无',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: '复制失败', title: '复制失败',
@@ -110,14 +112,11 @@ export const zh_CN: LanguageTranslation = {
copied: '复制了!', copied: '复制了!',
side_panel: { side_panel: {
schema: '模式:',
filter_by_schema: '按模式筛选',
search_schema: '搜索模式...',
no_schemas_found: '未找到模式。',
view_all_options: '查看所有选项...', view_all_options: '查看所有选项...',
tables_section: { tables_section: {
tables: '表', tables: '表',
add_table: '添加表', add_table: '添加表',
add_view: '添加视图',
filter: '筛选', filter: '筛选',
collapse: '全部折叠', collapse: '全部折叠',
// TODO: Translate // TODO: Translate
@@ -143,16 +142,23 @@ export const zh_CN: LanguageTranslation = {
field_actions: { field_actions: {
title: '字段属性', title: '字段属性',
unique: '唯一', unique: '唯一',
auto_increment: '自动递增',
comments: '注释', comments: '注释',
no_comments: '空', no_comments: '空',
delete_field: '删除字段', delete_field: '删除字段',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: '精度',
scale: '小数位',
}, },
index_actions: { index_actions: {
title: '索引属性', title: '索引属性',
name: '名称', name: '名称',
unique: '唯一', unique: '唯一',
index_type: '索引类型',
delete_index: '删除索引', delete_index: '删除索引',
}, },
table_actions: { table_actions: {
@@ -169,12 +175,15 @@ export const zh_CN: LanguageTranslation = {
description: '新建表以开始', description: '新建表以开始',
}, },
}, },
relationships_section: { refs_section: {
relationships: '关系', refs: '引用',
filter: '筛选', filter: '筛选',
add_relationship: '添加关系',
collapse: '全部折叠', collapse: '全部折叠',
add_relationship: '添加关系',
relationships: '关系',
dependencies: '依赖关系',
relationship: { relationship: {
relationship: '关系',
primary: '主表', primary: '主表',
foreign: '被引用表', foreign: '被引用表',
cardinality: '基数', cardinality: '基数',
@@ -184,16 +193,8 @@ export const zh_CN: LanguageTranslation = {
delete_relationship: '删除', delete_relationship: '删除',
}, },
}, },
empty_state: {
title: '无关系',
description: '创建关系以连接表',
},
},
dependencies_section: {
dependencies: '依赖关系',
filter: '筛选',
collapse: '全部折叠',
dependency: { dependency: {
dependency: '依赖',
table: '表', table: '表',
dependent_table: '依赖视图', dependent_table: '依赖视图',
delete_dependency: '删除', delete_dependency: '删除',
@@ -203,8 +204,8 @@ export const zh_CN: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: '无依赖', title: '无关系',
description: '创建视图以开始', description: '创建关系以开始',
}, },
}, },
@@ -247,9 +248,12 @@ export const zh_CN: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -264,7 +268,13 @@ export const zh_CN: LanguageTranslation = {
undo: '撤销', undo: '撤销',
redo: '重做', redo: '重做',
reorder_diagram: '重新排列关系图', reorder_diagram: '重新排列关系图',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '突出显示重叠的表', highlight_overlapping_tables: '突出显示重叠的表',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -395,6 +405,13 @@ export const zh_CN: LanguageTranslation = {
confirm: '更改', confirm: '更改',
}, },
create_table_schema_dialog: {
title: '创建新模式',
description: '尚未存在任何模式。创建您的第一个模式来组织您的表。',
create: '创建',
cancel: '取消',
},
star_us_dialog: { star_us_dialog: {
title: '帮助我们改进!', title: '帮助我们改进!',
description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!', description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!',
@@ -449,6 +466,7 @@ export const zh_CN: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: '新建表', new_table: '新建表',
new_view: '新建视图',
new_relationship: '新建关系', new_relationship: '新建关系',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -470,6 +488,9 @@ export const zh_CN: LanguageTranslation = {
language_select: { language_select: {
change_language: '语言', change_language: '语言',
}, },
on: '开启',
off: '关闭',
}, },
}; };

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_TW: LanguageTranslation = { export const zh_TW: LanguageTranslation = {
translation: { translation: {
editor_sidebar: {
new_diagram: '新建',
browse: '瀏覽',
tables: '表格',
refs: 'Refs',
areas: '區域',
dependencies: '相依性',
custom_types: '自定義類型',
},
menu: { menu: {
file: { databases: {
file: '檔案', databases: '資料庫',
new: '新增', new: '新增圖表',
open: '開啟', browse: '瀏覽...',
save: '儲存', save: '儲存',
import: '匯入資料庫', import: '匯入資料庫',
export_sql: '匯出 SQL', export_sql: '匯出 SQL',
export_as: '匯出為特定格式', export_as: '匯出為特定格式',
delete_diagram: '刪除圖表', delete_diagram: '刪除圖表',
exit: '退出',
}, },
edit: { edit: {
edit: '編輯', edit: '編輯',
@@ -26,7 +34,10 @@ export const zh_TW: LanguageTranslation = {
hide_sidebar: '隱藏側邊欄', hide_sidebar: '隱藏側邊欄',
hide_cardinality: '隱藏基數', hide_cardinality: '隱藏基數',
show_cardinality: '顯示基數', show_cardinality: '顯示基數',
hide_field_attributes: '隱藏欄位屬性',
show_field_attributes: '顯示欄位屬性',
zoom_on_scroll: '滾動縮放', zoom_on_scroll: '滾動縮放',
show_views: '資料庫檢視',
theme: '主題', theme: '主題',
show_dependencies: '顯示相依性', show_dependencies: '顯示相依性',
hide_dependencies: '隱藏相依性', hide_dependencies: '隱藏相依性',
@@ -67,15 +78,6 @@ export const zh_TW: LanguageTranslation = {
cancel: '取消', cancel: '取消',
}, },
multiple_schemas_alert: {
title: '多重 Schema',
description:
'此圖表中包含 {{schemasCount}} 個 Schema目前顯示{{formattedSchemas}}。',
dont_show_again: '不再顯示',
change_schema: '變更',
none: '無',
},
copy_to_clipboard_toast: { copy_to_clipboard_toast: {
unsupported: { unsupported: {
title: '複製失敗', title: '複製失敗',
@@ -110,14 +112,11 @@ export const zh_TW: LanguageTranslation = {
copied: '已複製!', copied: '已複製!',
side_panel: { side_panel: {
schema: 'Schema:',
filter_by_schema: '依 Schema 篩選',
search_schema: '搜尋 Schema...',
no_schemas_found: '未找到 Schema。',
view_all_options: '顯示所有選項...', view_all_options: '顯示所有選項...',
tables_section: { tables_section: {
tables: '表格', tables: '表格',
add_table: '新增表格', add_table: '新增表格',
add_view: '新增檢視',
filter: '篩選', filter: '篩選',
collapse: '全部摺疊', collapse: '全部摺疊',
// TODO: Translate // TODO: Translate
@@ -143,16 +142,23 @@ export const zh_TW: LanguageTranslation = {
field_actions: { field_actions: {
title: '欄位屬性', title: '欄位屬性',
unique: '唯一', unique: '唯一',
auto_increment: '自動遞增',
comments: '註解', comments: '註解',
no_comments: '無註解', no_comments: '無註解',
delete_field: '刪除欄位', delete_field: '刪除欄位',
// TODO: Translate // TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length', character_length: 'Max Length',
precision: '精度',
scale: '小數位',
}, },
index_actions: { index_actions: {
title: '索引屬性', title: '索引屬性',
name: '名稱', name: '名稱',
unique: '唯一', unique: '唯一',
index_type: '索引類型',
delete_index: '刪除索引', delete_index: '刪除索引',
}, },
table_actions: { table_actions: {
@@ -169,12 +175,15 @@ export const zh_TW: LanguageTranslation = {
description: '請新增表格以開始', description: '請新增表格以開始',
}, },
}, },
relationships_section: { refs_section: {
relationships: '關聯', refs: 'Refs',
filter: '篩選', filter: '篩選',
add_relationship: '新增關聯',
collapse: '全部摺疊', collapse: '全部摺疊',
add_relationship: '新增關聯',
relationships: '關聯',
dependencies: '相依性',
relationship: { relationship: {
relationship: '關聯',
primary: '主表格', primary: '主表格',
foreign: '參照表格', foreign: '參照表格',
cardinality: '基數', cardinality: '基數',
@@ -184,16 +193,8 @@ export const zh_TW: LanguageTranslation = {
delete_relationship: '刪除', delete_relationship: '刪除',
}, },
}, },
empty_state: {
title: '尚無關聯',
description: '請新增關聯以連接表格',
},
},
dependencies_section: {
dependencies: '相依性',
filter: '篩選',
collapse: '全部摺疊',
dependency: { dependency: {
dependency: '相依性',
table: '表格', table: '表格',
dependent_table: '相依檢視', dependent_table: '相依檢視',
delete_dependency: '刪除', delete_dependency: '刪除',
@@ -203,8 +204,8 @@ export const zh_TW: LanguageTranslation = {
}, },
}, },
empty_state: { empty_state: {
title: '尚無相依性', title: '尚無關聯',
description: '請建立檢視以開始', description: '請建立關聯以開始',
}, },
}, },
@@ -247,9 +248,12 @@ export const zh_TW: LanguageTranslation = {
field_name_placeholder: 'Field name', field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type', field_type_placeholder: 'Select type',
add_field: 'Add Field', add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: { custom_type_actions: {
title: 'Actions', title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete', delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
}, },
delete_custom_type: 'Delete Type', delete_custom_type: 'Delete Type',
}, },
@@ -264,7 +268,13 @@ export const zh_TW: LanguageTranslation = {
undo: '復原', undo: '復原',
redo: '重做', redo: '重做',
reorder_diagram: '重新排列圖表', reorder_diagram: '重新排列圖表',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '突出顯示重疊表格', highlight_overlapping_tables: '突出顯示重疊表格',
// TODO: Translate
filter: 'Filter Tables',
}, },
new_diagram_dialog: { new_diagram_dialog: {
@@ -394,6 +404,14 @@ export const zh_TW: LanguageTranslation = {
confirm: '變更', confirm: '變更',
}, },
create_table_schema_dialog: {
title: '建立新 Schema',
description:
'尚未存在任何 Schema。建立您的第一個 Schema 來組織您的表格。',
create: '建立',
cancel: '取消',
},
star_us_dialog: { star_us_dialog: {
title: '協助我們改善!', title: '協助我們改善!',
description: '請在 GitHub 上給我們一顆星,只需點擊一下!', description: '請在 GitHub 上給我們一顆星,只需點擊一下!',
@@ -448,6 +466,7 @@ export const zh_TW: LanguageTranslation = {
canvas_context_menu: { canvas_context_menu: {
new_table: '新建表格', new_table: '新建表格',
new_view: '新檢視',
new_relationship: '新建關聯', new_relationship: '新建關聯',
// TODO: Translate // TODO: Translate
new_area: 'New Area', new_area: 'New Area',
@@ -469,6 +488,9 @@ export const zh_TW: LanguageTranslation = {
language_select: { language_select: {
change_language: '變更語言', change_language: '變更語言',
}, },
on: '開啟',
off: '關閉',
}, },
}; };

View File

@@ -1,3 +1,4 @@
import type { DBCustomType } from './domain';
import type { Area } from './domain/area'; import type { Area } from './domain/area';
import type { DBDependency } from './domain/db-dependency'; import type { DBDependency } from './domain/db-dependency';
import type { DBField } from './domain/db-field'; import type { DBField } from './domain/db-field';
@@ -48,6 +49,10 @@ const generateIdsMapFromDiagram = (
idsMap.set(area.id, generateId()); idsMap.set(area.id, generateId());
}); });
diagram.customTypes?.forEach((customType) => {
idsMap.set(customType.id, generateId());
});
return idsMap; return idsMap;
}; };
@@ -213,6 +218,22 @@ export const cloneDiagram = (
}) })
.filter((area): area is Area => area !== null) ?? []; .filter((area): area is Area => area !== null) ?? [];
const customTypes: DBCustomType[] =
diagram.customTypes
?.map((customType) => {
const id = getNewId(customType.id);
if (!id) {
return null;
}
return {
...customType,
id,
} satisfies DBCustomType;
})
.filter(
(customType): customType is DBCustomType => customType !== null
) ?? [];
return { return {
diagram: { diagram: {
...diagram, ...diagram,
@@ -221,6 +242,7 @@ export const cloneDiagram = (
relationships, relationships,
tables, tables,
areas, areas,
customTypes,
createdAt: diagram.createdAt createdAt: diagram.createdAt
? new Date(diagram.createdAt) ? new Date(diagram.createdAt)
: new Date(), : new Date(),

View File

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

View File

@@ -48,18 +48,30 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
{ name: 'mediumblob', id: 'mediumblob' }, { name: 'mediumblob', id: 'mediumblob' },
{ name: 'tinyblob', id: 'tinyblob' }, { name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' }, { name: 'blob', id: 'blob' },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true }, {
{ name: 'char', id: 'char', hasCharMaxLength: true }, name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'char large object', id: 'char_large_object' }, { name: 'char large object', id: 'char_large_object' },
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true }, {
name: 'char varying',
id: 'char_varying',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'character large object', id: 'character_large_object' }, { name: 'character large object', id: 'character_large_object' },
{ {
name: 'character varying', name: 'character varying',
id: 'character_varying', id: 'character_varying',
hasCharMaxLength: true, fieldAttributes: { hasCharMaxLength: true },
}, },
{ name: 'nchar large object', id: 'nchar_large_object' }, { name: 'nchar large object', id: 'nchar_large_object' },
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true }, {
name: 'nchar varying',
id: 'nchar_varying',
fieldAttributes: { hasCharMaxLength: true },
},
{ {
name: 'national character large object', name: 'national character large object',
id: 'national_character_large_object', id: 'national_character_large_object',
@@ -67,22 +79,34 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
{ {
name: 'national character varying', name: 'national character varying',
id: 'national_character_varying', id: 'national_character_varying',
hasCharMaxLength: true, fieldAttributes: { hasCharMaxLength: true },
}, },
{ {
name: 'national char varying', name: 'national char varying',
id: 'national_char_varying', id: 'national_char_varying',
hasCharMaxLength: true, fieldAttributes: { hasCharMaxLength: true },
}, },
{ {
name: 'national character', name: 'national character',
id: 'national_character', id: 'national_character',
hasCharMaxLength: true, fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'national char',
id: 'national_char',
fieldAttributes: { hasCharMaxLength: true },
}, },
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
{ name: 'binary large object', id: 'binary_large_object' }, { name: 'binary large object', id: 'binary_large_object' },
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true }, {
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true }, name: 'binary varying',
id: 'binary_varying',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'fixedstring',
id: 'fixedstring',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'string', id: 'string' }, { name: 'string', id: 'string' },
// Date Types // Date Types

View File

@@ -14,9 +14,23 @@ export interface DataType {
name: string; name: string;
} }
export interface DataTypeData extends DataType { export interface FieldAttributeRange {
max: number;
min: number;
default: number;
}
interface FieldAttributes {
hasCharMaxLength?: boolean; hasCharMaxLength?: boolean;
hasCharMaxLengthOption?: boolean;
precision?: FieldAttributeRange;
scale?: FieldAttributeRange;
maxLength?: number;
}
export interface DataTypeData extends DataType {
usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common
fieldAttributes?: FieldAttributes;
} }
export const dataTypeSchema: z.ZodType<DataType> = z.object({ export const dataTypeSchema: z.ZodType<DataType> = z.object({
@@ -132,3 +146,22 @@ export const findDataTypeDataById = (
return dataTypesOptions.find((dataType) => dataType.id === id); return dataTypesOptions.find((dataType) => dataType.id === id);
}; };
export const supportsAutoIncrementDataType = (
dataTypeName: string
): boolean => {
return [
'integer',
'int',
'bigint',
'smallint',
'tinyint',
'mediumint',
'serial',
'bigserial',
'smallserial',
'number',
'numeric',
'decimal',
].includes(dataTypeName.toLocaleLowerCase());
};

View File

@@ -2,7 +2,12 @@ import type { DataTypeData } from './data-types';
export const genericDataTypes: readonly DataTypeData[] = [ export const genericDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types // Level 1 - Most commonly used types
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, {
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
usageLevel: 1,
},
{ name: 'int', id: 'int', usageLevel: 1 }, { name: 'int', id: 'int', usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 }, { name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 }, { name: 'boolean', id: 'boolean', usageLevel: 1 },
@@ -10,23 +15,62 @@ export const genericDataTypes: readonly DataTypeData[] = [
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 }, { name: 'timestamp', id: 'timestamp', usageLevel: 1 },
// Level 2 - Second most common types // Level 2 - Second most common types
{ name: 'decimal', id: 'decimal', usageLevel: 2 }, {
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 999,
min: 1,
default: 10,
},
scale: {
max: 999,
min: 0,
default: 2,
},
},
},
{ name: 'datetime', id: 'datetime', usageLevel: 2 }, { name: 'datetime', id: 'datetime', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 }, { name: 'json', id: 'json', usageLevel: 2 },
{ name: 'uuid', id: 'uuid', usageLevel: 2 }, { name: 'uuid', id: 'uuid', usageLevel: 2 },
// Less common types // Less common types
{ name: 'bigint', id: 'bigint' }, { name: 'bigint', id: 'bigint' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true }, {
name: 'binary',
id: 'binary',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'blob', id: 'blob' }, { name: 'blob', id: 'blob' },
{ name: 'char', id: 'char', hasCharMaxLength: true }, { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'double', id: 'double' }, { name: 'double', id: 'double' },
{ name: 'enum', id: 'enum' }, { name: 'enum', id: 'enum' },
{ name: 'float', id: 'float' }, { name: 'float', id: 'float' },
{ name: 'numeric', id: 'numeric' }, {
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 999,
min: 1,
default: 10,
},
scale: {
max: 999,
min: 0,
default: 2,
},
},
},
{ name: 'real', id: 'real' }, { name: 'real', id: 'real' },
{ name: 'set', id: 'set' }, { name: 'set', id: 'set' },
{ name: 'smallint', id: 'smallint' }, { name: 'smallint', id: 'smallint' },
{ name: 'time', id: 'time' }, { name: 'time', id: 'time' },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true }, {
name: 'varbinary',
id: 'varbinary',
fieldAttributes: { hasCharMaxLength: true },
},
] as const; ] as const;

View File

@@ -4,12 +4,32 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types // Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 }, { name: 'int', id: 'int', usageLevel: 1 },
{ name: 'bigint', id: 'bigint', usageLevel: 1 }, { name: 'bigint', id: 'bigint', usageLevel: 1 },
{ name: 'decimal', id: 'decimal', usageLevel: 1 }, {
name: 'decimal',
id: 'decimal',
usageLevel: 1,
fieldAttributes: {
precision: {
max: 65,
min: 1,
default: 10,
},
scale: {
max: 30,
min: 0,
default: 0,
},
},
},
{ name: 'boolean', id: 'boolean', usageLevel: 1 }, { name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 }, { name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 }, { name: 'date', id: 'date', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 }, { name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, {
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'text', id: 'text', usageLevel: 1 }, { name: 'text', id: 'text', usageLevel: 1 },
// Level 2 - Second most common types // Level 2 - Second most common types
@@ -20,16 +40,39 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
{ name: 'tinyint', id: 'tinyint' }, { name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' }, { name: 'smallint', id: 'smallint' },
{ name: 'mediumint', id: 'mediumint' }, { name: 'mediumint', id: 'mediumint' },
{ name: 'numeric', id: 'numeric' }, {
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 65,
min: 1,
default: 10,
},
scale: {
max: 30,
min: 0,
default: 0,
},
},
},
{ name: 'float', id: 'float' }, { name: 'float', id: 'float' },
{ name: 'double', id: 'double' }, { name: 'double', id: 'double' },
{ name: 'bit', id: 'bit' }, { name: 'bit', id: 'bit' },
{ name: 'bool', id: 'bool' }, { name: 'bool', id: 'bool' },
{ name: 'time', id: 'time' }, { name: 'time', id: 'time' },
{ name: 'year', id: 'year' }, { name: 'year', id: 'year' },
{ name: 'char', id: 'char', hasCharMaxLength: true }, { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'binary', id: 'binary', hasCharMaxLength: true }, {
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true }, name: 'binary',
id: 'binary',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'varbinary',
id: 'varbinary',
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'tinyblob', id: 'tinyblob' }, { name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' }, { name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' }, { name: 'mediumblob', id: 'mediumblob' },

View File

@@ -3,7 +3,12 @@ import type { DataTypeData } from './data-types';
export const mysqlDataTypes: readonly DataTypeData[] = [ export const mysqlDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types // Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 }, { name: 'int', id: 'int', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, {
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
usageLevel: 1,
},
{ name: 'text', id: 'text', usageLevel: 1 }, { name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 }, { name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 }, { name: 'timestamp', id: 'timestamp', usageLevel: 1 },
@@ -11,7 +16,23 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
// Level 2 - Second most common types // Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 }, { name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 }, {
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 65,
min: 1,
default: 10,
},
scale: {
max: 30,
min: 0,
default: 0,
},
},
},
{ name: 'datetime', id: 'datetime', usageLevel: 2 }, { name: 'datetime', id: 'datetime', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 }, { name: 'json', id: 'json', usageLevel: 2 },
@@ -22,7 +43,7 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
{ name: 'float', id: 'float' }, { name: 'float', id: 'float' },
{ name: 'double', id: 'double' }, { name: 'double', id: 'double' },
{ name: 'bit', id: 'bit' }, { name: 'bit', id: 'bit' },
{ name: 'char', id: 'char', hasCharMaxLength: true }, { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'tinytext', id: 'tinytext' }, { name: 'tinytext', id: 'tinytext' },
{ name: 'mediumtext', id: 'mediumtext' }, { name: 'mediumtext', id: 'mediumtext' },
{ name: 'longtext', id: 'longtext' }, { name: 'longtext', id: 'longtext' },

View File

@@ -2,15 +2,30 @@ import type { DataTypeData } from './data-types';
export const oracleDataTypes: readonly DataTypeData[] = [ export const oracleDataTypes: readonly DataTypeData[] = [
// Character types // Character types
{ name: 'VARCHAR2', id: 'varchar2', usageLevel: 1, hasCharMaxLength: true }, {
name: 'VARCHAR2',
id: 'varchar2',
usageLevel: 1,
fieldAttributes: { hasCharMaxLength: true },
},
{ {
name: 'NVARCHAR2', name: 'NVARCHAR2',
id: 'nvarchar2', id: 'nvarchar2',
usageLevel: 1, usageLevel: 1,
hasCharMaxLength: true, fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'CHAR',
id: 'char',
usageLevel: 2,
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'NCHAR',
id: 'nchar',
usageLevel: 2,
fieldAttributes: { hasCharMaxLength: true },
}, },
{ name: 'CHAR', id: 'char', usageLevel: 2, hasCharMaxLength: true },
{ name: 'NCHAR', id: 'nchar', usageLevel: 2, hasCharMaxLength: true },
{ name: 'CLOB', id: 'clob', usageLevel: 2 }, { name: 'CLOB', id: 'clob', usageLevel: 2 },
{ name: 'NCLOB', id: 'nclob', usageLevel: 2 }, { name: 'NCLOB', id: 'nclob', usageLevel: 2 },
@@ -49,7 +64,12 @@ export const oracleDataTypes: readonly DataTypeData[] = [
{ name: 'BFILE', id: 'bfile', usageLevel: 2 }, { name: 'BFILE', id: 'bfile', usageLevel: 2 },
// Other types // Other types
{ name: 'RAW', id: 'raw', usageLevel: 2, hasCharMaxLength: true }, {
name: 'RAW',
id: 'raw',
usageLevel: 2,
fieldAttributes: { hasCharMaxLength: true },
},
{ name: 'LONG RAW', id: 'long_raw', usageLevel: 2 }, { name: 'LONG RAW', id: 'long_raw', usageLevel: 2 },
{ name: 'ROWID', id: 'rowid', usageLevel: 2 }, { name: 'ROWID', id: 'rowid', usageLevel: 2 },
{ name: 'UROWID', id: 'urowid', usageLevel: 2 }, { name: 'UROWID', id: 'urowid', usageLevel: 2 },

View File

@@ -3,7 +3,12 @@ import type { DataTypeData } from './data-types';
export const postgresDataTypes: readonly DataTypeData[] = [ export const postgresDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types // Level 1 - Most commonly used types
{ name: 'integer', id: 'integer', usageLevel: 1 }, { name: 'integer', id: 'integer', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, {
name: 'varchar',
id: 'varchar',
fieldAttributes: { hasCharMaxLength: true },
usageLevel: 1,
},
{ name: 'text', id: 'text', usageLevel: 1 }, { name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 }, { name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 }, { name: 'timestamp', id: 'timestamp', usageLevel: 1 },
@@ -11,7 +16,23 @@ export const postgresDataTypes: readonly DataTypeData[] = [
// Level 2 - Second most common types // Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 }, { name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 }, {
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 131072,
min: 0,
default: 10,
},
scale: {
max: 16383,
min: 0,
default: 2,
},
},
},
{ name: 'serial', id: 'serial', usageLevel: 2 }, { name: 'serial', id: 'serial', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 }, { name: 'json', id: 'json', usageLevel: 2 },
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 }, { name: 'jsonb', id: 'jsonb', usageLevel: 2 },
@@ -23,18 +44,33 @@ export const postgresDataTypes: readonly DataTypeData[] = [
}, },
// Less common types // Less common types
{ name: 'numeric', id: 'numeric' }, {
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 131072,
min: 0,
default: 10,
},
scale: {
max: 16383,
min: 0,
default: 2,
},
},
},
{ name: 'real', id: 'real' }, { name: 'real', id: 'real' },
{ name: 'double precision', id: 'double_precision' }, { name: 'double precision', id: 'double_precision' },
{ name: 'smallserial', id: 'smallserial' }, { name: 'smallserial', id: 'smallserial' },
{ name: 'bigserial', id: 'bigserial' }, { name: 'bigserial', id: 'bigserial' },
{ name: 'money', id: 'money' }, { name: 'money', id: 'money' },
{ name: 'smallint', id: 'smallint' }, { name: 'smallint', id: 'smallint' },
{ name: 'char', id: 'char', hasCharMaxLength: true }, { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ {
name: 'character varying', name: 'character varying',
id: 'character_varying', id: 'character_varying',
hasCharMaxLength: true, fieldAttributes: { hasCharMaxLength: true },
}, },
{ name: 'time', id: 'time' }, { name: 'time', id: 'time' },
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' }, { name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },

View File

@@ -4,32 +4,93 @@ export const sqlServerDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types // Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 }, { name: 'int', id: 'int', usageLevel: 1 },
{ name: 'bit', id: 'bit', usageLevel: 1 }, { name: 'bit', id: 'bit', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, {
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true, usageLevel: 1 }, name: 'varchar',
id: 'varchar',
fieldAttributes: {
hasCharMaxLength: true,
hasCharMaxLengthOption: true,
maxLength: 8000,
},
usageLevel: 1,
},
{
name: 'nvarchar',
id: 'nvarchar',
fieldAttributes: {
hasCharMaxLength: true,
hasCharMaxLengthOption: true,
maxLength: 4000,
},
usageLevel: 1,
},
{ name: 'text', id: 'text', usageLevel: 1 }, { name: 'text', id: 'text', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 }, { name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 }, { name: 'date', id: 'date', usageLevel: 1 },
// Level 2 - Second most common types // Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 }, { name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 }, {
name: 'decimal',
id: 'decimal',
usageLevel: 2,
fieldAttributes: {
precision: {
max: 38,
min: 1,
default: 18,
},
scale: {
max: 38,
min: 0,
default: 0,
},
},
},
{ name: 'datetime2', id: 'datetime2', usageLevel: 2 }, { name: 'datetime2', id: 'datetime2', usageLevel: 2 },
{ name: 'uniqueidentifier', id: 'uniqueidentifier', usageLevel: 2 }, { name: 'uniqueidentifier', id: 'uniqueidentifier', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 }, { name: 'json', id: 'json', usageLevel: 2 },
// Less common types // Less common types
{ name: 'numeric', id: 'numeric' }, {
name: 'numeric',
id: 'numeric',
fieldAttributes: {
precision: {
max: 38,
min: 1,
default: 18,
},
scale: {
max: 38,
min: 0,
default: 0,
},
},
},
{ name: 'smallint', id: 'smallint' }, { name: 'smallint', id: 'smallint' },
{ name: 'smallmoney', id: 'smallmoney' }, { name: 'smallmoney', id: 'smallmoney' },
{ name: 'tinyint', id: 'tinyint' }, { name: 'tinyint', id: 'tinyint' },
{ name: 'money', id: 'money' }, { name: 'money', id: 'money' },
{ name: 'float', id: 'float' }, { name: 'float', id: 'float' },
{ name: 'real', id: 'real' }, { name: 'real', id: 'real' },
{ name: 'char', id: 'char', hasCharMaxLength: true }, { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true }, { name: 'nchar', id: 'nchar', fieldAttributes: { hasCharMaxLength: true } },
{ name: 'ntext', id: 'ntext' }, { name: 'ntext', id: 'ntext' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true }, {
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true }, name: 'binary',
id: 'binary',
fieldAttributes: { hasCharMaxLength: true },
},
{
name: 'varbinary',
id: 'varbinary',
fieldAttributes: {
hasCharMaxLength: true,
hasCharMaxLengthOption: true,
maxLength: 8000,
},
},
{ name: 'image', id: 'image' }, { name: 'image', id: 'image' },
{ name: 'datetimeoffset', id: 'datetimeoffset' }, { name: 'datetimeoffset', id: 'datetimeoffset' },
{ name: 'smalldatetime', id: 'smalldatetime' }, { name: 'smalldatetime', id: 'smalldatetime' },

View File

@@ -10,25 +10,48 @@ export const sqliteDataTypes: readonly DataTypeData[] = [
// SQLite type aliases and common types // SQLite type aliases and common types
{ name: 'int', id: 'int', usageLevel: 1 }, { name: 'int', id: 'int', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, {
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 }, name: 'varchar',
{ name: 'date', id: 'date', usageLevel: 1 }, id: 'varchar',
{ name: 'datetime', id: 'datetime', usageLevel: 1 }, fieldAttributes: {
{ name: 'boolean', id: 'boolean', usageLevel: 1 }, hasCharMaxLength: true,
},
usageLevel: 1,
},
{
name: 'timestamp',
id: 'timestamp',
usageLevel: 1,
},
// Level 2 - Second most common types // Level 2 - Second most common types
{ name: 'numeric', id: 'numeric', usageLevel: 2 }, { name: 'numeric', id: 'numeric', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 }, { name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'float', id: 'float', usageLevel: 2 }, { name: 'float', id: 'float', usageLevel: 2 },
{
name: 'decimal',
id: 'decimal',
usageLevel: 2,
},
{ name: 'double', id: 'double', usageLevel: 2 }, { name: 'double', id: 'double', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 }, { name: 'json', id: 'json', usageLevel: 2 },
// Less common types (all map to SQLite storage classes) // Less common types (all map to SQLite storage classes)
{ name: 'char', id: 'char', hasCharMaxLength: true }, {
name: 'char',
id: 'char',
fieldAttributes: {
hasCharMaxLength: true,
},
usageLevel: 2,
},
{ name: 'binary', id: 'binary' }, { name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' }, { name: 'varbinary', id: 'varbinary' },
{ name: 'smallint', id: 'smallint' }, { name: 'smallint', id: 'smallint' },
{ name: 'bigint', id: 'bigint' }, { name: 'bigint', id: 'bigint' },
{ name: 'bool', id: 'bool' }, { name: 'bool', id: 'bool' },
{ name: 'boolean', id: 'boolean' }, // Added for smartquery compatibility
{ name: 'time', id: 'time' }, { name: 'time', id: 'time' },
{ name: 'date', id: 'date' }, // Added for smartquery compatibility
{ name: 'datetime', id: 'datetime' }, // Added for smartquery compatibility
] as const; ] as const;

View File

@@ -4,4 +4,5 @@ export const defaultSchemas: { [key in DatabaseType]?: string } = {
[DatabaseType.POSTGRESQL]: 'public', [DatabaseType.POSTGRESQL]: 'public',
[DatabaseType.SQL_SERVER]: 'dbo', [DatabaseType.SQL_SERVER]: 'dbo',
[DatabaseType.CLICKHOUSE]: 'default', [DatabaseType.CLICKHOUSE]: 'default',
[DatabaseType.COCKROACHDB]: 'public',
}; };

View File

@@ -0,0 +1,960 @@
import { describe, it, expect, vi } 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;
const testId = () => `test-id-${++idCounter}`;
const testTime = Date.now();
// Helper to create a field with all required properties
const createField = (overrides: Partial<DBField>): DBField =>
({
id: testId(),
name: 'field',
type: { id: 'text', name: 'text' },
primaryKey: false,
nullable: true,
unique: false,
createdAt: testTime,
...overrides,
}) as DBField;
// Helper to create a table with all required properties
const createTable = (overrides: Partial<DBTable>): DBTable =>
({
id: testId(),
name: 'table',
fields: [],
indexes: [],
createdAt: testTime,
x: 0,
y: 0,
width: 200,
...overrides,
}) as DBTable;
// Helper to create a diagram with all required properties
const createDiagram = (overrides: Partial<Diagram>): Diagram =>
({
id: testId(),
name: 'diagram',
databaseType: DatabaseType.GENERIC,
tables: [],
relationships: [],
createdAt: testTime,
updatedAt: testTime,
...overrides,
}) as Diagram;
describe('Composite Primary Keys', () => {
it('should handle tables with composite primary keys correctly', () => {
const tableId = testId();
const field1Id = testId();
const field2Id = testId();
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Enchanted Library',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: tableId,
name: 'spell_components',
fields: [
createField({
id: field1Id,
name: 'spell_id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: field2Id,
name: 'component_id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'quantity',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
default: '1',
}),
],
indexes: [],
color: '#FFD700',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should contain composite primary key syntax
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(
/component_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/
);
});
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(),
name: 'Wizard Academy',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'wizards',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: false,
}),
],
indexes: [],
color: '#9370DB',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should contain inline 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)');
});
});
describe('Default Value Handling', () => {
it('should skip invalid default values like "has default"', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Potion Shop',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'potions',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'is_active',
type: { id: 'boolean', name: 'boolean' },
primaryKey: false,
nullable: true,
unique: false,
default: 'has default',
}),
createField({
id: testId(),
name: 'stock_count',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
default: 'DEFAULT has default',
}),
],
indexes: [],
color: '#98FB98',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should not contain invalid default values
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 integer NOT NULL'); // integer gets simplified to int
});
it('should handle valid default values correctly', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Treasure Vault',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'treasures',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'gold_value',
type: { id: 'numeric', name: 'numeric' },
primaryKey: false,
nullable: false,
unique: false,
default: '100.50',
precision: 10,
scale: 2,
}),
createField({
id: testId(),
name: 'created_at',
type: { id: 'timestamp', name: 'timestamp' },
primaryKey: false,
nullable: true,
unique: false,
default: 'now()',
}),
createField({
id: testId(),
name: 'currency',
type: { id: 'char', name: 'char' },
characterMaximumLength: '3',
primaryKey: false,
nullable: false,
unique: false,
default: 'EUR',
}),
],
indexes: [],
color: '#FFD700',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should contain valid defaults
expect(sql).toContain('DEFAULT 100.50');
expect(sql).toContain('DEFAULT now()');
expect(sql).toContain('DEFAULT EUR');
});
it('should handle NOW and similar default values', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Quest Log',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'quests',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'created_at',
type: { id: 'timestamp', name: 'timestamp' },
primaryKey: false,
nullable: true,
unique: false,
default: 'NOW',
}),
createField({
id: testId(),
name: 'updated_at',
type: { id: 'timestamp', name: 'timestamp' },
primaryKey: false,
nullable: true,
unique: false,
default: "('now')",
}),
],
indexes: [],
color: '#4169E1',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// 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()');
});
});
describe('Character Type Handling', () => {
it('should handle char types with and without length correctly', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Dragon Registry',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'dragons',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'element_code',
type: { id: 'char', name: 'char' },
characterMaximumLength: '2',
primaryKey: false,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'status',
type: { id: 'char', name: 'char' },
primaryKey: false,
nullable: false,
unique: false,
}),
],
indexes: [],
color: '#FF6347',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should handle char with explicit length
expect(sql).toContain('element_code char(2)');
// Should add default length for char without length
expect(sql).toContain('status char(1)');
});
it('should not have spaces between char and parentheses', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Rune Inscriptions',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'runes',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'symbol',
type: { id: 'char', name: 'char' },
characterMaximumLength: '5',
primaryKey: false,
nullable: false,
unique: true,
}),
],
indexes: [],
color: '#8B4513',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should not contain "char (" with space
expect(sql).not.toContain('char (');
expect(sql).toContain('char(5)');
});
});
describe('Complex Table Structures', () => {
it('should handle tables with no primary key', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Alchemy Log',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'experiment_logs',
fields: [
createField({
id: testId(),
name: 'experiment_id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: false,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'alchemist_id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: false,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'result',
type: { id: 'text', name: 'text' },
primaryKey: false,
nullable: true,
unique: false,
}),
createField({
id: testId(),
name: 'logged_at',
type: { id: 'timestamp', name: 'timestamp' },
primaryKey: false,
nullable: false,
unique: false,
default: 'now()',
}),
],
indexes: [],
color: '#32CD32',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should create a valid table without primary key
expect(sql).toContain('CREATE TABLE experiment_logs');
expect(sql).not.toContain('PRIMARY KEY');
});
it('should handle multiple tables with relationships', () => {
const guildTableId = testId();
const memberTableId = testId();
const guildIdFieldId = testId();
const memberGuildIdFieldId = testId();
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Adventurer Guild System',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: guildTableId,
name: 'guilds',
fields: [
createField({
id: guildIdFieldId,
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: true,
}),
createField({
id: testId(),
name: 'founded_year',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: true,
unique: false,
}),
],
indexes: [],
x: 0,
y: 0,
color: '#4169E1',
}),
createTable({
id: memberTableId,
name: 'guild_members',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: memberGuildIdFieldId,
name: 'guild_id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: false,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'member_name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'rank',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: true,
unique: false,
default: "'Novice'",
}),
],
indexes: [],
x: 250,
y: 0,
color: '#FFD700',
}),
],
relationships: [
{
id: testId(),
name: 'fk_guild_members_guild',
sourceTableId: memberTableId,
targetTableId: guildTableId,
sourceFieldId: memberGuildIdFieldId,
targetFieldId: guildIdFieldId,
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: testTime,
},
],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should create both tables
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)'
);
});
});
describe('Schema Support', () => {
it('should handle tables with schemas correctly', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Multi-Realm Database',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'portals',
schema: 'transportation',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'destination',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: false,
}),
],
indexes: [],
color: '#9370DB',
}),
createTable({
id: testId(),
name: 'spells',
schema: 'magic',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: true,
}),
],
indexes: [],
x: 250,
y: 0,
color: '#FF1493',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
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');
});
});
describe('Edge Cases', () => {
it('should handle empty tables array', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Empty Realm',
databaseType: DatabaseType.POSTGRESQL,
tables: [],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
expect(sql).toBe('');
});
it('should handle tables with empty fields', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Void Space',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'empty_table',
fields: [],
indexes: [],
color: '#000000',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should still create table structure
expect(sql).toContain('CREATE TABLE empty_table');
expect(sql).toContain('(\n\n)');
});
it('should handle special characters in default values', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Mystic Scrolls',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'scrolls',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'inscription',
type: { id: 'text', name: 'text' },
primaryKey: false,
nullable: true,
unique: false,
default: "'Ancient\\'s Wisdom'",
}),
],
indexes: [],
color: '#8B4513',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should preserve escaped quotes
expect(sql).toContain("DEFAULT 'Ancient\\'s Wisdom'");
});
it('should handle numeric precision and scale', () => {
const diagram: Diagram = createDiagram({
id: testId(),
name: 'Treasury',
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: testId(),
name: 'gold_reserves',
fields: [
createField({
id: testId(),
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: false,
}),
createField({
id: testId(),
name: 'amount',
type: { id: 'numeric', name: 'numeric' },
primaryKey: false,
nullable: false,
unique: false,
precision: 15,
scale: 2,
}),
createField({
id: testId(),
name: 'interest_rate',
type: { id: 'numeric', name: 'numeric' },
primaryKey: false,
nullable: true,
unique: false,
precision: 5,
}),
],
indexes: [],
color: '#FFD700',
}),
],
relationships: [],
});
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
// Should include precision and scale
expect(sql).toContain('amount numeric(15, 2)');
// Should include precision only when scale is not provided
expect(sql).toContain('interest_rate numeric(5)');
});
});
});

View File

@@ -48,6 +48,50 @@ export function exportFieldComment(comment: string): string {
.join(''); .join('');
} }
export function escapeSQLComment(comment: string): string {
if (!comment) {
return '';
}
// Escape single quotes by doubling them
let escaped = comment.replace(/'/g, "''");
// Replace newlines with spaces to prevent breaking SQL syntax
// Some databases support multi-line comments with specific syntax,
// but for maximum compatibility, we'll replace newlines with spaces
escaped = escaped.replace(/[\r\n]+/g, ' ');
// Trim any excessive whitespace
escaped = escaped.replace(/\s+/g, ' ').trim();
return escaped;
}
export function formatTableComment(comment: string): string {
if (!comment) {
return '';
}
// Split by newlines and add -- to each line
return (
comment
.split('\n')
.map((line) => `-- ${line}`)
.join('\n') + '\n'
);
}
export function formatMSSQLTableComment(comment: string): string {
if (!comment) {
return '';
}
// For MSSQL, we use multi-line comment syntax
// Escape */ to prevent breaking the comment block
const escaped = comment.replace(/\*\//g, '* /');
return `/**\n${escaped}\n*/\n`;
}
export function getInlineFK(table: DBTable, diagram: Diagram): string { export function getInlineFK(table: DBTable, diagram: Diagram): string {
if (!diagram.relationships) { if (!diagram.relationships) {
return ''; return '';

View File

@@ -1,5 +1,6 @@
import { import {
exportFieldComment, exportFieldComment,
formatMSSQLTableComment,
isFunction, isFunction,
isKeyword, isKeyword,
strHasQuotes, strHasQuotes,
@@ -72,7 +73,13 @@ function parseMSSQLDefault(field: DBField): string {
return `'${defaultValue}'`; return `'${defaultValue}'`;
} }
export function exportMSSQL(diagram: Diagram): string { export function exportMSSQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) { if (!diagram.tables || !diagram.relationships) {
return ''; return '';
} }
@@ -82,6 +89,8 @@ export function exportMSSQL(diagram: Diagram): string {
// Create CREATE SCHEMA statements for all schemas // Create CREATE SCHEMA statements for all schemas
let sqlScript = ''; let sqlScript = '';
if (!onlyRelationships) {
const schemas = new Set<string>(); const schemas = new Set<string>();
tables.forEach((table) => { tables.forEach((table) => {
@@ -92,7 +101,7 @@ export function exportMSSQL(diagram: Diagram): string {
// Add schema creation statements // Add schema creation statements
schemas.forEach((schema) => { schemas.forEach((schema) => {
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`; sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n`;
}); });
// Generate table creation SQL // Generate table creation SQL
@@ -108,7 +117,9 @@ export function exportMSSQL(diagram: Diagram): string {
: `[${table.name}]`; : `[${table.name}]`;
return `${ return `${
table.comments ? `/**\n${table.comments}\n*/\n` : '' table.comments
? formatMSSQLTableComment(table.comments)
: ''
}CREATE TABLE ${tableName} (\n${table.fields }CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => { .map((field: DBField) => {
const fieldName = `[${field.name}]`; const fieldName = `[${field.name}]`;
@@ -125,7 +136,8 @@ export function exportMSSQL(diagram: Diagram): string {
) { ) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`; typeWithSize = `${typeName}(${field.characterMaximumLength})`;
} }
} else if (field.precision && field.scale) { }
if (field.precision && field.scale) {
if ( if (
typeName.toLowerCase() === 'decimal' || typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric' typeName.toLowerCase() === 'numeric'
@@ -144,9 +156,9 @@ export function exportMSSQL(diagram: Diagram): string {
const notNull = field.nullable ? '' : ' NOT NULL'; const notNull = field.nullable ? '' : ' NOT NULL';
// Check if identity column // Check if identity column
const identity = field.default const identity =
?.toLowerCase() field.increment ||
.includes('identity') field.default?.toLowerCase().includes('identity')
? ' IDENTITY(1,1)' ? ' IDENTITY(1,1)'
: ''; : '';
@@ -156,6 +168,7 @@ export function exportMSSQL(diagram: Diagram): string {
// Handle default value using SQL Server specific parser // Handle default value using SQL Server specific parser
const defaultValue = const defaultValue =
field.default && field.default &&
!field.increment &&
!field.default.toLowerCase().includes('identity') !field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(field)}` ? ` DEFAULT ${parseMSSQLDefault(field)}`
: ''; : '';
@@ -165,12 +178,21 @@ export function exportMSSQL(diagram: Diagram): string {
}) })
.join(',\n')}${ .join(',\n')}${
table.fields.filter((f) => f.primaryKey).length > 0 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) .filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`) .map((f) => `[${f.name}]`)
.join(', ')})` .join(', ')})`
: '' : ''
}\n);\n\n${table.indexes }\n);\n${(() => {
const validIndexes = table.indexes
.map((index) => { .map((index) => {
const indexName = table.schema const indexName = table.schema
? `[${table.schema}_${index.name}]` ? `[${table.schema}_${index.name}]`
@@ -192,24 +214,38 @@ export function exportMSSQL(diagram: Diagram): string {
); );
indexFields.length = 32; indexFields.length = 32;
return indexFields.length > 0 return indexFields.length > 0
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n` ? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
: ''; : '';
} }
return indexFields.length > 0 return indexFields.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n` ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
: ''; : '';
}) })
.join('')}`; .filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()}\n`;
}) })
.filter(Boolean) // Remove empty strings (views) .filter(Boolean) // Remove empty strings (views)
.join('\n'); .join('\n');
}
// Generate foreign keys // Generate foreign keys
sqlScript += `\n${relationships if (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n';
// Process all relationships and create FK objects with schema info
const foreignKeys = relationships
.map((r: DBRelationship) => { .map((r: DBRelationship) => {
const sourceTable = tables.find((t) => t.id === r.sourceTableId); const sourceTable = tables.find(
const targetTable = tables.find((t) => t.id === r.targetTableId); (t) => t.id === r.sourceTableId
);
const targetTable = tables.find(
(t) => t.id === r.targetTableId
);
if ( if (
!sourceTable || !sourceTable ||
@@ -231,17 +267,85 @@ export function exportMSSQL(diagram: Diagram): string {
return ''; return '';
} }
const sourceTableName = sourceTable.schema // Determine which table should have the foreign key based on cardinality
? `[${sourceTable.schema}].[${sourceTable.name}]` let fkTable, fkField, refTable, refField;
: `[${sourceTable.name}]`;
const targetTableName = targetTable.schema
? `[${targetTable.schema}].[${targetTable.name}]`
: `[${targetTable.name}]`;
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`; 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 '';
}
const fkTableName = fkTable.schema
? `[${fkTable.schema}].[${fkTable.name}]`
: `[${fkTable.name}]`;
const refTableName = refTable.schema
? `[${refTable.schema}].[${refTable.name}]`
: `[${refTable.name}]`;
return {
schema: fkTable.schema || 'dbo',
sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT [${r.name}] FOREIGN KEY([${fkField.name}]) REFERENCES ${refTableName}([${refField.name}]);`,
};
}) })
.filter(Boolean) // Remove empty strings .filter(Boolean); // Remove empty objects
.join('\n')}`;
// Group foreign keys by schema
const fksBySchema = foreignKeys.reduce(
(acc, fk) => {
if (!fk) return acc;
const schema = fk.schema;
if (!acc[schema]) {
acc[schema] = [];
}
acc[schema].push(fk.sql);
return acc;
},
{} as Record<string, string[]>
);
// Sort schemas and generate SQL with separators
const sortedSchemas = Object.keys(fksBySchema).sort();
const fkSql = sortedSchemas
.map((schema, index) => {
const schemaFks = fksBySchema[schema].join('\n');
if (index === 0) {
return `-- Schema: ${schema}\n${schemaFks}`;
} else {
return `\n-- Schema: ${schema}\n${schemaFks}`;
}
})
.join('\n');
sqlScript += fkSql;
}
return sqlScript; return sqlScript;
} }

View File

@@ -1,5 +1,7 @@
import { import {
exportFieldComment, exportFieldComment,
escapeSQLComment,
formatTableComment,
isFunction, isFunction,
isKeyword, isKeyword,
strHasQuotes, strHasQuotes,
@@ -168,7 +170,13 @@ function mapMySQLType(typeName: string): string {
return typeName; return typeName;
} }
export function exportMySQL(diagram: Diagram): string { export function exportMySQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) { if (!diagram.tables || !diagram.relationships) {
return ''; return '';
} }
@@ -177,10 +185,11 @@ export function exportMySQL(diagram: Diagram): string {
const relationships = diagram.relationships; const relationships = diagram.relationships;
// Start SQL script // Start SQL script
let sqlScript = '-- MySQL database export\n\n'; let sqlScript = '-- MySQL database export\n';
if (!onlyRelationships) {
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency // MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
sqlScript += 'START TRANSACTION;\n\n'; sqlScript += 'START TRANSACTION;\n';
// Create databases (schemas) if they don't exist // Create databases (schemas) if they don't exist
const schemas = new Set<string>(); const schemas = new Set<string>();
@@ -212,11 +221,13 @@ export function exportMySQL(diagram: Diagram): string {
: `\`${table.name}\``; : `\`${table.name}\``;
// Get primary key fields // Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey); const primaryKeyFields = table.fields.filter(
(f) => f.primaryKey
);
return `${ return `${
table.comments ? `-- ${table.comments}\n` : '' table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields }\nCREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => { .map((field: DBField) => {
const fieldName = `\`${field.name}\``; const fieldName = `\`${field.name}\``;
@@ -233,7 +244,8 @@ export function exportMySQL(diagram: Diagram): string {
) { ) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`; typeWithSize = `${typeName}(${field.characterMaximumLength})`;
} }
} else if (field.precision && field.scale) { }
if (field.precision && field.scale) {
if ( if (
typeName.toLowerCase() === 'decimal' || typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric' typeName.toLowerCase() === 'numeric'
@@ -262,12 +274,15 @@ export function exportMySQL(diagram: Diagram): string {
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword // Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
let autoIncrement = ''; let autoIncrement = '';
if ( if (
field.primaryKey && field.increment ||
(field.default?.toLowerCase().includes('identity') || (field.primaryKey &&
(field.default
?.toLowerCase()
.includes('identity') ||
field.default field.default
?.toLowerCase() ?.toLowerCase()
.includes('autoincrement') || .includes('autoincrement') ||
field.default?.includes('nextval')) field.default?.includes('nextval')))
) { ) {
autoIncrement = ' AUTO_INCREMENT'; autoIncrement = ' AUTO_INCREMENT';
} }
@@ -276,9 +291,10 @@ export function exportMySQL(diagram: Diagram): string {
const unique = const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : ''; !field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value // Handle default value - skip if auto increment
const defaultValue = const defaultValue =
field.default && field.default &&
!field.increment &&
!field.default.toLowerCase().includes('identity') && !field.default.toLowerCase().includes('identity') &&
!field.default !field.default
.toLowerCase() .toLowerCase()
@@ -289,7 +305,7 @@ export function exportMySQL(diagram: Diagram): string {
// MySQL supports inline comments // MySQL supports inline comments
const comment = field.comments const comment = field.comments
? ` COMMENT '${field.comments.replace(/'/g, "''")}'` ? ` COMMENT '${escapeSQLComment(field.comments)}'`
: ''; : '';
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`; return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
@@ -297,18 +313,27 @@ export function exportMySQL(diagram: Diagram): string {
.join(',\n')}${ .join(',\n')}${
// Add PRIMARY KEY as table constraint // Add PRIMARY KEY as table constraint
primaryKeyFields.length > 0 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}\``) .map((f) => `\`${f.name}\``)
.join(', ')})` .join(', ')})`
: '' : ''
}\n)${ }\n)${
// MySQL supports table comments // MySQL supports table comments
table.comments table.comments
? ` COMMENT='${table.comments.replace(/'/g, "''")}'` ? ` COMMENT='${escapeSQLComment(table.comments)}'`
: '' : ''
};\n\n${ };\n${
// Add indexes - MySQL creates them separately from the table definition // Add indexes - MySQL creates them separately from the table definition
table.indexes (() => {
const validIndexes = table.indexes
.map((index) => { .map((index) => {
// Get the list of fields for this index // Get the list of fields for this index
const indexFields = index.fieldIds const indexFields = index.fieldIds
@@ -322,10 +347,12 @@ export function exportMySQL(diagram: Diagram): string {
// Skip if this index exactly matches the primary key fields // Skip if this index exactly matches the primary key fields
if ( if (
primaryKeyFields.length === indexFields.length && primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) => primaryKeyFields.every((pk) =>
indexFields.some( indexFields.some(
(field) => field && field.id === pk.id (field) =>
field && field.id === pk.id
) )
) )
) { ) {
@@ -336,25 +363,32 @@ export function exportMySQL(diagram: Diagram): string {
const fieldNamesForIndex = indexFields const fieldNamesForIndex = indexFields
.map((field) => field?.name || '') .map((field) => field?.name || '')
.join('_'); .join('_');
const uniqueIndicator = index.unique ? '_unique' : ''; const uniqueIndicator = index.unique
? '_unique'
: '';
const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``; const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
// Get the properly quoted field names // Get the properly quoted field names
const indexFieldNames = indexFields const indexFieldNames = indexFields
.map((field) => (field ? `\`${field.name}\`` : '')) .map((field) =>
field ? `\`${field.name}\`` : ''
)
.filter(Boolean); .filter(Boolean);
// Check for text/blob fields that need special handling // Check for text/blob fields that need special handling
const hasTextOrBlob = indexFields.some((field) => { const hasTextOrBlob = indexFields.some(
(field) => {
const typeName = const typeName =
field?.type.name.toLowerCase() || ''; field?.type.name.toLowerCase() ||
'';
return ( return (
typeName === 'text' || typeName === 'text' ||
typeName === 'mediumtext' || typeName === 'mediumtext' ||
typeName === 'longtext' || typeName === 'longtext' ||
typeName === 'blob' typeName === 'blob'
); );
}); }
);
// If there are TEXT/BLOB fields, need to add prefix length // If there are TEXT/BLOB fields, need to add prefix length
const indexFieldsWithPrefix = hasTextOrBlob const indexFieldsWithPrefix = hasTextOrBlob
@@ -380,21 +414,26 @@ export function exportMySQL(diagram: Diagram): string {
: indexFieldNames; : indexFieldNames;
return indexFieldNames.length > 0 return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldsWithPrefix.join(', ')});\n` ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldsWithPrefix.join(', ')});`
: ''; : '';
}) })
.filter(Boolean) .filter(Boolean);
.join('\n')
}`; return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()
}\n`;
}) })
.filter(Boolean) // Remove empty strings (views) .filter(Boolean) // Remove empty strings (views)
.join('\n'); .join('\n');
}
// Generate foreign keys // Generate foreign keys
if (relationships.length > 0) { if (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n\n'; sqlScript += '\n-- Foreign key constraints\n';
sqlScript += relationships const foreignKeys = relationships
.map((r: DBRelationship) => { .map((r: DBRelationship) => {
const sourceTable = tables.find( const sourceTable = tables.find(
(t) => t.id === r.sourceTableId (t) => t.id === r.sourceTableId
@@ -423,25 +462,62 @@ export function exportMySQL(diagram: Diagram): string {
return ''; return '';
} }
const sourceTableName = sourceTable.schema // Determine which table should have the foreign key based on cardinality
? `\`${sourceTable.schema}\`.\`${sourceTable.name}\`` let fkTable, fkField, refTable, refField;
: `\`${sourceTable.name}\``;
const targetTableName = targetTable.schema if (
? `\`${targetTable.schema}\`.\`${targetTable.name}\`` r.sourceCardinality === 'one' &&
: `\`${targetTable.name}\``; 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 '';
}
const fkTableName = fkTable.schema
? `\`${fkTable.schema}\`.\`${fkTable.name}\``
: `\`${fkTable.name}\``;
const refTableName = refTable.schema
? `\`${refTable.schema}\`.\`${refTable.name}\``
: `\`${refTable.name}\``;
// Create a descriptive constraint name // Create a descriptive constraint name
const constraintName = `\`fk_${sourceTable.name}_${sourceField.name}\``; const constraintName = `\`fk_${fkTable.name}_${fkField.name}\``;
// MySQL supports ON DELETE and ON UPDATE actions // MySQL supports ON DELETE and ON UPDATE actions
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${sourceField.name}\`) REFERENCES ${targetTableName}(\`${targetField.name}\`)\nON UPDATE CASCADE ON DELETE RESTRICT;\n`; return `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${fkField.name}\`) REFERENCES ${refTableName}(\`${refField.name}\`);`;
}) })
.filter(Boolean) // Remove empty strings .filter(Boolean); // Remove empty strings
.join('\n');
sqlScript += foreignKeys.join('\n');
} }
// Commit transaction // Commit transaction
sqlScript += '\nCOMMIT;\n'; sqlScript += '\n\nCOMMIT;\n';
return sqlScript; return sqlScript;
} }

View File

@@ -1,5 +1,7 @@
import { import {
exportFieldComment, exportFieldComment,
escapeSQLComment,
formatTableComment,
isFunction, isFunction,
isKeyword, isKeyword,
strHasQuotes, strHasQuotes,
@@ -140,10 +142,16 @@ function exportCustomTypes(customTypes: DBCustomType[]): string {
} }
}); });
return typesSql + '\n'; return typesSql ? typesSql + '\n' : '';
} }
export function exportPostgreSQL(diagram: Diagram): string { export function exportPostgreSQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) { if (!diagram.tables || !diagram.relationships) {
return ''; return '';
} }
@@ -154,6 +162,7 @@ export function exportPostgreSQL(diagram: Diagram): string {
// Create CREATE SCHEMA statements for all schemas // Create CREATE SCHEMA statements for all schemas
let sqlScript = ''; let sqlScript = '';
if (!onlyRelationships) {
const schemas = new Set<string>(); const schemas = new Set<string>();
tables.forEach((table) => { tables.forEach((table) => {
@@ -173,7 +182,9 @@ export function exportPostgreSQL(diagram: Diagram): string {
schemas.forEach((schema) => { schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`; sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
}); });
if (schemas.size > 0) {
sqlScript += '\n'; sqlScript += '\n';
}
// Add custom types (enums and composite types) // Add custom types (enums and composite types)
sqlScript += exportCustomTypes(customTypes); sqlScript += exportCustomTypes(customTypes);
@@ -198,7 +209,9 @@ export function exportPostgreSQL(diagram: Diagram): string {
sequences.forEach((sequence) => { sequences.forEach((sequence) => {
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`; sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
}); });
if (sequences.size > 0) {
sqlScript += '\n'; sqlScript += '\n';
}
// Generate table creation SQL // Generate table creation SQL
sqlScript += tables sqlScript += tables
@@ -213,10 +226,12 @@ export function exportPostgreSQL(diagram: Diagram): string {
: `"${table.name}"`; : `"${table.name}"`;
// Get primary key fields // Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey); const primaryKeyFields = table.fields.filter(
(f) => f.primaryKey
);
return `${ return `${
table.comments ? `-- ${table.comments}\n` : '' table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE ${tableName} (\n${table.fields }CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => { .map((field: DBField) => {
const fieldName = `"${field.name}"`; const fieldName = `"${field.name}"`;
@@ -247,13 +262,15 @@ export function exportPostgreSQL(diagram: Diagram): string {
if (field.characterMaximumLength) { if (field.characterMaximumLength) {
if ( if (
typeName.toLowerCase() === 'varchar' || typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'character varying' || typeName.toLowerCase() ===
'character varying' ||
typeName.toLowerCase() === 'char' || typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'character' typeName.toLowerCase() === 'character'
) { ) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`; typeWithSize = `${typeName}(${field.characterMaximumLength})`;
} }
} else if (field.precision && field.scale) { }
if (field.precision && field.scale) {
if ( if (
typeName.toLowerCase() === 'decimal' || typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric' typeName.toLowerCase() === 'numeric'
@@ -271,14 +288,18 @@ export function exportPostgreSQL(diagram: Diagram): string {
// Handle array types (check if the type name ends with '[]') // Handle array types (check if the type name ends with '[]')
if (typeName.endsWith('[]')) { if (typeName.endsWith('[]')) {
typeWithSize = typeWithSize.replace('[]', '') + '[]'; typeWithSize =
typeWithSize.replace('[]', '') + '[]';
} }
const notNull = field.nullable ? '' : ' NOT NULL'; const notNull = field.nullable ? '' : ' NOT NULL';
// Handle identity generation // Handle identity generation
let identity = ''; let identity = '';
if (field.default && field.default.includes('nextval')) { if (
field.default &&
field.default.includes('nextval')
) {
// PostgreSQL already handles this with DEFAULT nextval() // PostgreSQL already handles this with DEFAULT nextval()
} else if ( } else if (
field.default && field.default &&
@@ -304,14 +325,22 @@ export function exportPostgreSQL(diagram: Diagram): string {
}) })
.join(',\n')}${ .join(',\n')}${
primaryKeyFields.length > 0 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}"`) .map((f) => `"${f.name}"`)
.join(', ')})` .join(', ')})`
: '' : ''
}\n);\n\n${ }\n);${
// Add table comments // Add table comments
table.comments table.comments
? `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n\n` ? `\nCOMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';`
: '' : ''
}${ }${
// Add column comments // Add column comments
@@ -319,13 +348,14 @@ export function exportPostgreSQL(diagram: Diagram): string {
.filter((f) => f.comments) .filter((f) => f.comments)
.map( .map(
(f) => (f) =>
`COMMENT ON COLUMN ${tableName}."${f.name}" IS '${f.comments?.replace(/'/g, "''")}';\n` `\nCOMMENT ON COLUMN ${tableName}."${f.name}" IS '${escapeSQLComment(f.comments || '')}';`
) )
.join('') .join('')
}\n${ }${
// Add indexes only for non-primary key fields or composite indexes // Add indexes only for non-primary key fields or composite indexes
// This avoids duplicate indexes on primary key columns // This avoids duplicate indexes on primary key columns
table.indexes (() => {
const validIndexes = table.indexes
.map((index) => { .map((index) => {
// Get the list of fields for this index // Get the list of fields for this index
const indexFields = index.fieldIds const indexFields = index.fieldIds
@@ -340,10 +370,12 @@ export function exportPostgreSQL(diagram: Diagram): string {
// Skip if this index exactly matches the primary key fields // Skip if this index exactly matches the primary key fields
// This prevents creating redundant indexes // This prevents creating redundant indexes
if ( if (
primaryKeyFields.length === indexFields.length && primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) => primaryKeyFields.every((pk) =>
indexFields.some( indexFields.some(
(field) => field && field.id === pk.id (field) =>
field && field.id === pk.id
) )
) )
) { ) {
@@ -375,25 +407,40 @@ export function exportPostgreSQL(diagram: Diagram): string {
// Get the properly quoted field names // Get the properly quoted field names
const indexFieldNames = indexFields const indexFieldNames = indexFields
.map((field) => (field ? `"${field.name}"` : '')) .map((field) =>
field ? `"${field.name}"` : ''
)
.filter(Boolean); .filter(Boolean);
return indexFieldNames.length > 0 return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldNames.join(', ')});\n\n` ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName}${index.type && index.type !== 'btree' ? ` USING ${index.type.toUpperCase()}` : ''} (${indexFieldNames.join(', ')});`
: ''; : '';
}) })
.filter(Boolean) .filter(Boolean);
.join('')
}`; return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()
}\n`;
}) })
.filter(Boolean) // Remove empty strings (views) .filter(Boolean) // Remove empty strings (views)
.join('\n'); .join('\n');
}
// Generate foreign keys // Generate foreign keys
sqlScript += `\n${relationships if (relationships.length > 0) {
sqlScript += '\n-- Foreign key constraints\n';
// Process all relationships and create FK objects with schema info
const foreignKeys = relationships
.map((r: DBRelationship) => { .map((r: DBRelationship) => {
const sourceTable = tables.find((t) => t.id === r.sourceTableId); const sourceTable = tables.find(
const targetTable = tables.find((t) => t.id === r.targetTableId); (t) => t.id === r.sourceTableId
);
const targetTable = tables.find(
(t) => t.id === r.targetTableId
);
if ( if (
!sourceTable || !sourceTable ||
@@ -415,29 +462,99 @@ export function exportPostgreSQL(diagram: Diagram): string {
return ''; return '';
} }
const sourceTableName = sourceTable.schema // Determine which table should have the foreign key based on cardinality
? `"${sourceTable.schema}"."${sourceTable.name}"` let fkTable, fkField, refTable, refField;
: `"${sourceTable.name}"`;
const targetTableName = targetTable.schema if (
? `"${targetTable.schema}"."${targetTable.name}"` r.sourceCardinality === 'one' &&
: `"${targetTable.name}"`; 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 '';
}
const fkTableName = fkTable.schema
? `"${fkTable.schema}"."${fkTable.name}"`
: `"${fkTable.name}"`;
const refTableName = refTable.schema
? `"${refTable.schema}"."${refTable.name}"`
: `"${refTable.name}"`;
// Create a unique constraint name by combining table and field names // Create a unique constraint name by combining table and field names
// Ensure it stays within PostgreSQL's 63-character limit for identifiers // Ensure it stays within PostgreSQL's 63-character limit for identifiers
// and doesn't get truncated in a way that breaks SQL syntax // and doesn't get truncated in a way that breaks SQL syntax
const baseName = `fk_${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`; const baseName = `fk_${fkTable.name}_${fkField.name}_${refTable.name}_${refField.name}`;
// Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits // Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits
const safeConstraintName = const safeConstraintName =
baseName.length > 60 baseName.length > 60
? baseName.substring(0, 60).replace(/[^a-zA-Z0-9_]/g, '_') ? baseName
.substring(0, 60)
.replace(/[^a-zA-Z0-9_]/g, '_')
: baseName.replace(/[^a-zA-Z0-9_]/g, '_'); : baseName.replace(/[^a-zA-Z0-9_]/g, '_');
const constraintName = `"${safeConstraintName}"`; const constraintName = `"${safeConstraintName}"`;
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}");\n`; return {
schema: fkTable.schema || 'public',
sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY("${fkField.name}") REFERENCES ${refTableName}("${refField.name}");`,
};
}) })
.filter(Boolean) // Remove empty strings .filter(Boolean); // Remove empty objects
.join('\n')}`;
// Group foreign keys by schema
const fksBySchema = foreignKeys.reduce(
(acc, fk) => {
if (!fk) return acc;
const schema = fk.schema;
if (!acc[schema]) {
acc[schema] = [];
}
acc[schema].push(fk.sql);
return acc;
},
{} as Record<string, string[]>
);
// Sort schemas and generate SQL with separators
const sortedSchemas = Object.keys(fksBySchema).sort();
const fkSql = sortedSchemas
.map((schema, index) => {
const schemaFks = fksBySchema[schema].join('\n');
if (index === 0) {
return `-- Schema: ${schema}\n${schemaFks}`;
} else {
return `\n-- Schema: ${schema}\n${schemaFks}`;
}
})
.join('\n');
sqlScript += fkSql;
}
return sqlScript; return sqlScript;
} }

View File

@@ -1,5 +1,6 @@
import { import {
exportFieldComment, exportFieldComment,
formatTableComment,
isFunction, isFunction,
isKeyword, isKeyword,
strHasQuotes, strHasQuotes,
@@ -66,8 +67,9 @@ function parseSQLiteDefault(field: DBField): string {
return `'${defaultValue.replace(/'/g, "''")}'`; 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 { function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
const originalType = typeName;
typeName = typeName.toLowerCase(); typeName = typeName.toLowerCase();
// Special handling for primary key integer columns (autoincrement requires INTEGER PRIMARY KEY) // Special handling for primary key integer columns (autoincrement requires INTEGER PRIMARY KEY)
@@ -75,59 +77,62 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
return 'INTEGER'; // Must be uppercase for SQLite to recognize it for AUTOINCREMENT 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) { 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 'int':
case 'smallint': case 'smallint':
case 'tinyint': case 'tinyint':
case 'mediumint':
case 'bigint': case 'bigint':
return 'INTEGER'; case 'json':
return typeName.toUpperCase();
case 'decimal': // Only map types that SQLite truly doesn't recognize
case 'numeric':
case 'float':
case 'double':
case 'real':
return 'REAL';
case 'char':
case 'nchar': case 'nchar':
case 'varchar':
case 'nvarchar': case 'nvarchar':
case 'text':
case 'ntext': case 'ntext':
case 'character varying': case 'character varying':
case 'character': case 'character':
return 'TEXT'; return 'TEXT';
case 'date':
case 'datetime':
case 'timestamp':
case 'datetime2': case 'datetime2':
return 'TEXT'; // SQLite doesn't have dedicated date types return 'DATETIME';
case 'blob':
case 'binary': case 'binary':
case 'varbinary': case 'varbinary':
case 'image': case 'image':
return 'BLOB'; return 'BLOB';
case 'bit': case 'bit':
case 'boolean': return 'BOOLEAN';
return 'INTEGER'; // SQLite doesn't have a boolean type, use INTEGER
case 'user-defined': case 'user-defined':
case 'json':
case 'jsonb': case 'jsonb':
return 'TEXT'; // Store as JSON text return 'TEXT';
case 'array': case 'array':
return 'TEXT'; // Store as serialized array text return 'TEXT';
case 'geometry': case 'geometry':
case 'geography': 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 // If type has array notation (ends with []), treat as TEXT
@@ -135,11 +140,17 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
return 'TEXT'; return 'TEXT';
} }
// For any other types, default to TEXT // For any other types, preserve the original
return typeName; return originalType.toUpperCase();
} }
export function exportSQLite(diagram: Diagram): string { export function exportSQLite({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) { if (!diagram.tables || !diagram.relationships) {
return ''; return '';
} }
@@ -148,10 +159,15 @@ export function exportSQLite(diagram: Diagram): string {
const relationships = diagram.relationships; const relationships = diagram.relationships;
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation // Start SQL script - SQLite doesn't use schemas, so we skip schema creation
let sqlScript = '-- SQLite database export\n\n'; 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 // Begin transaction for faster import
sqlScript += 'BEGIN TRANSACTION;\n\n'; sqlScript += 'BEGIN TRANSACTION;\n';
// SQLite doesn't have sequences, so we skip sequence creation // SQLite doesn't have sequences, so we skip sequence creation
@@ -165,6 +181,7 @@ export function exportSQLite(diagram: Diagram): string {
'sqlite_master', 'sqlite_master',
]; ];
if (!onlyRelationships) {
// Generate table creation SQL // Generate table creation SQL
sqlScript += tables sqlScript += tables
.map((table: DBTable) => { .map((table: DBTable) => {
@@ -186,29 +203,138 @@ export function exportSQLite(diagram: Diagram): string {
const tableName = `"${table.name}"`; const tableName = `"${table.name}"`;
// Get primary key fields // Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey); const primaryKeyFields = table.fields.filter(
(f) => f.primaryKey
);
// Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT) // Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
const singleIntegerPrimaryKey = const singleIntegerPrimaryKey =
primaryKeyFields.length === 1 && primaryKeyFields.length === 1 &&
(primaryKeyFields[0].type.name.toLowerCase() === 'integer' || (primaryKeyFields[0].type.name.toLowerCase() ===
'integer' ||
primaryKeyFields[0].type.name.toLowerCase() === 'int'); 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}${ return `${schemaComment}${
table.comments ? `-- ${table.comments}\n` : '' table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields }CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => { .map((field: DBField) => {
const fieldName = `"${field.name}"`; const fieldName = `"${field.name}"`;
// Handle type name - map to SQLite compatible types // Handle type name - map to SQLite compatible types
const typeName = mapSQLiteType( const baseTypeName = mapSQLiteType(
field.type.name, field.type.name,
field.primaryKey field.primaryKey
); );
// SQLite ignores length specifiers, so we don't add them // Add size/precision/scale parameters if applicable
// We'll keep this simple without size info let typeWithParams = baseTypeName;
const typeWithoutSize = typeName;
// 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'; const notNull = field.nullable ? '' : ' NOT NULL';
@@ -217,7 +343,10 @@ export function exportSQLite(diagram: Diagram): string {
if ( if (
field.primaryKey && field.primaryKey &&
singleIntegerPrimaryKey && singleIntegerPrimaryKey &&
(field.default?.toLowerCase().includes('identity') || (field.increment ||
field.default
?.toLowerCase()
.includes('identity') ||
field.default field.default
?.toLowerCase() ?.toLowerCase()
.includes('autoincrement') || .includes('autoincrement') ||
@@ -234,6 +363,7 @@ export function exportSQLite(diagram: Diagram): string {
let defaultValue = ''; let defaultValue = '';
if ( if (
field.default && field.default &&
!field.increment &&
!field.default.toLowerCase().includes('identity') && !field.default.toLowerCase().includes('identity') &&
!field.default !field.default
.toLowerCase() .toLowerCase()
@@ -254,7 +384,7 @@ export function exportSQLite(diagram: Diagram): string {
? ' PRIMARY KEY' + autoIncrement ? ' 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')}${ .join(',\n')}${
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys // Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
@@ -263,9 +393,15 @@ export function exportSQLite(diagram: Diagram): string {
.map((f) => `"${f.name}"`) .map((f) => `"${f.name}"`)
.join(', ')})` .join(', ')})`
: '' : ''
}\n);\n\n${ }${
// Add foreign key constraints
tableForeignKeys.length > 0
? ',\n' + tableForeignKeys.join(',\n')
: ''
}\n);\n${
// Add indexes - SQLite doesn't support indexes in CREATE TABLE // Add indexes - SQLite doesn't support indexes in CREATE TABLE
table.indexes (() => {
const validIndexes = table.indexes
.map((index) => { .map((index) => {
// Skip indexes that exactly match the primary key // Skip indexes that exactly match the primary key
const indexFields = index.fieldIds const indexFields = index.fieldIds
@@ -279,15 +415,19 @@ export function exportSQLite(diagram: Diagram): string {
// Get the properly quoted field names // Get the properly quoted field names
const indexFieldNames = indexFields const indexFieldNames = indexFields
.map((field) => (field ? `"${field.name}"` : '')) .map((field) =>
field ? `"${field.name}"` : ''
)
.filter(Boolean); .filter(Boolean);
// Skip if this index exactly matches the primary key fields // Skip if this index exactly matches the primary key fields
if ( if (
primaryKeyFields.length === indexFields.length && primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) => primaryKeyFields.every((pk) =>
indexFields.some( indexFields.some(
(field) => field && field.id === pk.id (field) =>
field && field.id === pk.id
) )
) )
) { ) {
@@ -295,61 +435,28 @@ export function exportSQLite(diagram: Diagram): string {
} }
// Create safe index name // Create safe index name
const safeIndexName = `${table.name}_${index.name}` const safeIndexName =
`${table.name}_${index.name}`
.replace(/[^a-zA-Z0-9_]/g, '_') .replace(/[^a-zA-Z0-9_]/g, '_')
.substring(0, 60); .substring(0, 60);
return indexFieldNames.length > 0 return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});\n` ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});`
: ''; : '';
}) })
.filter(Boolean) .filter(Boolean);
.join('\n')
}`; return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()
}\n`;
}) })
.filter(Boolean) // Remove empty strings (views) .filter(Boolean) // Remove empty strings (views)
.join('\n'); .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\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;
}
// Create commented out version of what would be ALTER TABLE statement
sqlScript += `-- ALTER TABLE "${sourceTable.name}" ADD CONSTRAINT "fk_${sourceTable.name}_${sourceField.name}" FOREIGN KEY("${sourceField.name}") REFERENCES "${targetTable.name}"("${targetField.name}");\n`;
});
} }
// Foreign keys are now included inline in CREATE TABLE statements
// No need for separate ALTER TABLE statements in SQLite
// Commit transaction // Commit transaction
sqlScript += '\nCOMMIT;\n'; sqlScript += '\nCOMMIT;\n';

View File

@@ -1,6 +1,9 @@
import type { Diagram } from '../../domain/diagram'; import type { Diagram } from '../../domain/diagram';
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env'; import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
import { DatabaseType } from '@/lib/domain/database-type'; import {
DatabaseType,
databaseTypesWithCommentSupport,
} from '@/lib/domain/database-type';
import type { DBTable } from '@/lib/domain/db-table'; import type { DBTable } from '@/lib/domain/db-table';
import type { DataType } from '../data-types/data-types'; import type { DataType } from '../data-types/data-types';
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache'; import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
@@ -8,26 +11,11 @@ import { exportMSSQL } from './export-per-type/mssql';
import { exportPostgreSQL } from './export-per-type/postgresql'; import { exportPostgreSQL } from './export-per-type/postgresql';
import { exportSQLite } from './export-per-type/sqlite'; import { exportSQLite } from './export-per-type/sqlite';
import { exportMySQL } from './export-per-type/mysql'; import { exportMySQL } from './export-per-type/mysql';
import { escapeSQLComment } from './export-per-type/common';
// Function to simplify verbose data type names // Function to simplify verbose data type names
const simplifyDataType = (typeName: string): string => { const simplifyDataType = (typeName: string): string => {
const typeMap: Record<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',
};
return typeMap[typeName.toLowerCase()] || typeName; return typeMap[typeName.toLowerCase()] || typeName;
}; };
@@ -36,10 +24,12 @@ export const exportBaseSQL = ({
diagram, diagram,
targetDatabaseType, targetDatabaseType,
isDBMLFlow = false, isDBMLFlow = false,
onlyRelationships = false,
}: { }: {
diagram: Diagram; diagram: Diagram;
targetDatabaseType: DatabaseType; targetDatabaseType: DatabaseType;
isDBMLFlow?: boolean; isDBMLFlow?: boolean;
onlyRelationships?: boolean;
}): string => { }): string => {
const { tables, relationships } = diagram; const { tables, relationships } = diagram;
@@ -50,16 +40,16 @@ export const exportBaseSQL = ({
if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) { if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
switch (diagram.databaseType) { switch (diagram.databaseType) {
case DatabaseType.SQL_SERVER: case DatabaseType.SQL_SERVER:
return exportMSSQL(diagram); return exportMSSQL({ diagram, onlyRelationships });
case DatabaseType.POSTGRESQL: case DatabaseType.POSTGRESQL:
return exportPostgreSQL(diagram); return exportPostgreSQL({ diagram, onlyRelationships });
case DatabaseType.SQLITE: case DatabaseType.SQLITE:
return exportSQLite(diagram); return exportSQLite({ diagram, onlyRelationships });
case DatabaseType.MYSQL: case DatabaseType.MYSQL:
case DatabaseType.MARIADB: case DatabaseType.MARIADB:
return exportMySQL(diagram); return exportMySQL({ diagram, onlyRelationships });
default: default:
return exportPostgreSQL(diagram); return exportPostgreSQL({ diagram, onlyRelationships });
} }
} }
@@ -131,7 +121,23 @@ export const exportBaseSQL = ({
} }
} }
}); });
sqlScript += '\n'; // Add a newline if custom types were processed if (
diagram.customTypes.some(
(ct) =>
(ct.kind === 'enum' &&
ct.values &&
ct.values.length > 0 &&
targetDatabaseType === DatabaseType.POSTGRESQL &&
!isDBMLFlow) ||
(ct.kind === 'composite' &&
ct.fields &&
ct.fields.length > 0 &&
(targetDatabaseType === DatabaseType.POSTGRESQL ||
isDBMLFlow))
)
) {
sqlScript += '\n';
}
} }
// Add CREATE SEQUENCE statements // Add CREATE SEQUENCE statements
@@ -154,7 +160,9 @@ export const exportBaseSQL = ({
sequences.forEach((sequence) => { sequences.forEach((sequence) => {
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`; sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
}); });
if (sequences.size > 0) {
sqlScript += '\n'; sqlScript += '\n';
}
// Loop through each non-view table to generate the SQL statements // Loop through each non-view table to generate the SQL statements
nonViewTables.forEach((table) => { nonViewTables.forEach((table) => {
@@ -163,6 +171,12 @@ export const exportBaseSQL = ({
: table.name; : table.name;
sqlScript += `CREATE TABLE ${tableName} (\n`; sqlScript += `CREATE TABLE ${tableName} (\n`;
// Check for composite primary keys
const primaryKeyFields = table.fields.filter(
(field) => field.primaryKey
);
const hasCompositePrimaryKey = primaryKeyFields.length > 1;
table.fields.forEach((field, index) => { table.fields.forEach((field, index) => {
let typeName = simplifyDataType(field.type.name); let typeName = simplifyDataType(field.type.name);
@@ -214,17 +228,33 @@ export const exportBaseSQL = ({
typeName = 'text[]'; typeName = 'text[]';
} }
// Handle special types
if (
typeName.toLowerCase() === 'char' &&
!field.characterMaximumLength
) {
// Default char without length to char(1)
typeName = 'char';
}
sqlScript += ` ${field.name} ${typeName}`; sqlScript += ` ${field.name} ${typeName}`;
// Add size for character types // Add size for character types
if ( if (
field.characterMaximumLength && field.characterMaximumLength &&
parseInt(field.characterMaximumLength) > 0 parseInt(field.characterMaximumLength) > 0 &&
field.type.name.toLowerCase() !== 'decimal'
) { ) {
sqlScript += `(${field.characterMaximumLength})`; sqlScript += `(${field.characterMaximumLength})`;
} else if (field.type.name.toLowerCase().includes('varchar')) { } else if (field.type.name.toLowerCase().includes('varchar')) {
// Keep varchar sizing, but don't apply to TEXT (previously enum) // Keep varchar sizing, but don't apply to TEXT (previously enum)
sqlScript += `(500)`; sqlScript += `(500)`;
} else if (
typeName.toLowerCase() === 'char' &&
!field.characterMaximumLength
) {
// Default char without explicit length to char(1) for compatibility
sqlScript += `(1)`;
} }
// Add precision and scale for numeric types // Add precision and scale for numeric types
@@ -244,11 +274,23 @@ export const exportBaseSQL = ({
sqlScript += ` UNIQUE`; sqlScript += ` UNIQUE`;
} }
// Handle AUTO INCREMENT - add as a comment for AI to process
if (field.increment) {
sqlScript += ` /* AUTO_INCREMENT */`;
}
// Handle DEFAULT value // Handle DEFAULT value
if (field.default) { if (field.default && !field.increment) {
// Temp remove default user-define value when it have it // Temp remove default user-define value when it have it
let fieldDefault = field.default; let fieldDefault = field.default;
// Skip invalid default values for DBML export
if (
fieldDefault === 'has default' ||
fieldDefault === 'DEFAULT has default'
) {
// Skip this default value as it's invalid SQL
} else {
// Remove the type cast part after :: if it exists // Remove the type cast part after :: if it exists
if (fieldDefault.includes('::')) { if (fieldDefault.includes('::')) {
const endedWithParentheses = fieldDefault.endsWith(')'); const endedWithParentheses = fieldDefault.endsWith(')');
@@ -269,53 +311,99 @@ export const exportBaseSQL = ({
sqlScript += ` DEFAULT ${fieldDefault}`; sqlScript += ` DEFAULT ${fieldDefault}`;
} }
}
// Handle PRIMARY KEY constraint // Handle PRIMARY KEY constraint - only add inline if no PK index with custom name
if (field.primaryKey) { const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
if (field.primaryKey && !hasCompositePrimaryKey && !pkIndex?.name) {
sqlScript += ' PRIMARY KEY'; sqlScript += ' PRIMARY KEY';
} }
// Add a comma after each field except the last one // Add a comma after each field except the last one (or before PK constraint)
if (index < table.fields.length - 1) { const needsPKConstraint =
hasCompositePrimaryKey ||
(primaryKeyFields.length === 1 && pkIndex?.name);
if (index < table.fields.length - 1 || needsPKConstraint) {
sqlScript += ',\n'; sqlScript += ',\n';
} }
}); });
sqlScript += '\n);\n\n'; // 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) => f.name).join(', ');
if (pkIndex?.name) {
sqlScript += `\n CONSTRAINT ${pkIndex.name} PRIMARY KEY (${pkFieldNames})`;
} else {
sqlScript += `\n PRIMARY KEY (${pkFieldNames})`;
}
}
// Add table comment sqlScript += '\n);\n';
if (table.comments) {
sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments}';\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) => { table.fields.forEach((field) => {
// Add column comment // Add column comment (only for databases that support COMMENT ON syntax)
if (field.comments) { if (field.comments && supportsCommentOn) {
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments}';\n`; sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${escapeSQLComment(field.comments)}';\n`;
} }
}); });
// Generate SQL for indexes // Generate SQL for indexes
table.indexes.forEach((index) => { table.indexes.forEach((index) => {
const fieldNames = index.fieldIds // Skip the primary key index (it's already handled as a constraint)
.map( if (index.isPrimaryKey) {
(fieldId) => return;
table.fields.find((field) => field.id === fieldId)?.name }
// 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) => field.name)
.join(', '); .join(', ');
if (fieldNames) { if (fieldNames) {
const indexName = table.schema const indexName =
table.schema && !isDBMLFlow
? `${table.schema}_${index.name}` ? `${table.schema}_${index.name}`
: index.name; : index.name;
sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`; sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
} }
}); });
sqlScript += '\n';
}); });
if (nonViewTables.length > 0 && (relationships?.length ?? 0) > 0) {
sqlScript += '\n';
}
// Handle relationships (foreign keys) // Handle relationships (foreign keys)
relationships?.forEach((relationship) => { relationships?.forEach((relationship) => {
const sourceTable = nonViewTables.find( const sourceTable = nonViewTables.find(
@@ -338,13 +426,52 @@ export const exportBaseSQL = ({
sourceTableField && sourceTableField &&
targetTableField targetTableField
) { ) {
const sourceTableName = sourceTable.schema // Determine which table should have the foreign key based on cardinality
? `${sourceTable.schema}.${sourceTable.name}` // In a 1:many relationship, the foreign key goes on the "many" side
: sourceTable.name; // If source is "one" and target is "many", FK goes on target table
const targetTableName = targetTable.schema // If source is "many" and target is "one", FK goes on source table
? `${targetTable.schema}.${targetTable.name}` let fkTable, fkField, refTable, refField;
: targetTable.name;
sqlScript += `ALTER TABLE ${sourceTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${sourceTableField.name}) REFERENCES ${targetTableName} (${targetTableField.name});\n`; if (
relationship.sourceCardinality === 'one' &&
relationship.targetCardinality === 'many'
) {
// FK goes on target table
fkTable = targetTable;
fkField = targetTableField;
refTable = sourceTable;
refField = sourceTableField;
} else if (
relationship.sourceCardinality === 'many' &&
relationship.targetCardinality === 'one'
) {
// FK goes on source table
fkTable = sourceTable;
fkField = sourceTableField;
refTable = targetTable;
refField = targetTableField;
} else if (
relationship.sourceCardinality === 'one' &&
relationship.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 = sourceTableField;
refTable = targetTable;
refField = targetTableField;
} else {
// Many-to-many relationships need a junction table, skip for now
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`;
} }
}); });

View File

@@ -0,0 +1,126 @@
import type { DatabaseMetadata } from './metadata-types/database-metadata';
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
export interface SelectedTable {
schema?: string | null;
table: string;
type: 'table' | 'view';
}
export function filterMetadataByTables({
metadata,
selectedTables: inputSelectedTables,
}: {
metadata: DatabaseMetadata;
selectedTables: SelectedTable[];
}): DatabaseMetadata {
const selectedTables = inputSelectedTables.map((st) => {
// Normalize schema names to ensure consistent filtering
const schema = schemaNameToDomainSchemaName(st.schema) ?? '';
return {
...st,
schema,
};
});
// Create sets for faster lookup
const selectedTableSet = new Set(
selectedTables
.filter((st) => st.type === 'table')
.map((st) => `${st.schema}.${st.table}`)
);
const selectedViewSet = new Set(
selectedTables
.filter((st) => st.type === 'view')
.map((st) => `${st.schema}.${st.table}`)
);
// Filter tables
const filteredTables = metadata.tables.filter((table) => {
const schema = schemaNameToDomainSchemaName(table.schema) ?? '';
const tableId = `${schema}.${table.table}`;
return selectedTableSet.has(tableId);
});
// Filter views - include views that were explicitly selected
const filteredViews =
metadata.views?.filter((view) => {
const schema = schemaNameToDomainSchemaName(view.schema) ?? '';
const viewName = view.view_name ?? '';
const viewId = `${schema}.${viewName}`;
return selectedViewSet.has(viewId);
}) || [];
// Filter columns - include columns from both tables and views
const filteredColumns = metadata.columns.filter((col) => {
const fromTable = filteredTables.some(
(tb) => tb.schema === col.schema && tb.table === col.table
);
// For views, the column.table field might contain the view name
const fromView = filteredViews.some(
(view) => view.schema === col.schema && view.view_name === col.table
);
return fromTable || fromView;
});
// Filter primary keys
const filteredPrimaryKeys = metadata.pk_info.filter((pk) =>
filteredTables.some(
(tb) => tb.schema === pk.schema && tb.table === pk.table
)
);
// Filter indexes
const filteredIndexes = metadata.indexes.filter((idx) =>
filteredTables.some(
(tb) => tb.schema === idx.schema && tb.table === idx.table
)
);
// Filter foreign keys - include if either source or target table is selected
// This ensures all relationships related to selected tables are preserved
const filteredForeignKeys = metadata.fk_info.filter((fk) => {
// Handle reference_schema and reference_table fields from the JSON
const targetSchema = fk.reference_schema;
const targetTable = (fk.reference_table || '').replace(/^"+|"+$/g, ''); // Remove extra quotes
const sourceIncluded = filteredTables.some(
(tb) => tb.schema === fk.schema && tb.table === fk.table
);
const targetIncluded = filteredTables.some(
(tb) => tb.schema === targetSchema && tb.table === targetTable
);
return sourceIncluded || targetIncluded;
});
const schemasWithTables = new Set(filteredTables.map((tb) => tb.schema));
const schemasWithViews = new Set(filteredViews.map((view) => view.schema));
// Filter custom types if they exist
const filteredCustomTypes =
metadata.custom_types?.filter((customType) => {
// Also check if the type is used by any of the selected tables' columns
const typeUsedInColumns = filteredColumns.some(
(col) =>
col.type === customType.type ||
col.type.includes(customType.type) // Handle array types like "custom_type[]"
);
return (
schemasWithTables.has(customType.schema) ||
schemasWithViews.has(customType.schema) ||
typeUsedInColumns
);
}) || [];
return {
...metadata,
tables: filteredTables,
columns: filteredColumns,
pk_info: filteredPrimaryKeys,
indexes: filteredIndexes,
fk_info: filteredForeignKeys,
views: filteredViews,
custom_types: filteredCustomTypes,
};
}

View File

@@ -1,4 +1,3 @@
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
import type { TableInfo } from './table-info'; import type { TableInfo } from './table-info';
import { z } from 'zod'; import { z } from 'zod';
@@ -33,20 +32,12 @@ export type AggregatedIndexInfo = Omit<IndexInfo, 'column'> & {
}; };
export const createAggregatedIndexes = ({ export const createAggregatedIndexes = ({
tableInfo, tableIndexes,
tableSchema,
indexes,
}: { }: {
tableInfo: TableInfo; tableInfo: TableInfo;
indexes: IndexInfo[]; tableIndexes: IndexInfo[];
tableSchema?: string; tableSchema?: string;
}): AggregatedIndexInfo[] => { }): AggregatedIndexInfo[] => {
const tableIndexes = indexes.filter((idx) => {
const indexSchema = schemaNameToDomainSchemaName(idx.schema);
return idx.table === tableInfo.table && indexSchema === tableSchema;
});
return Object.values( return Object.values(
tableIndexes.reduce( tableIndexes.reduce(
(acc, idx) => { (acc, idx) => {

View File

@@ -2,7 +2,8 @@ const withExtras = false;
const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`; const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
const withoutDefault = `""`; const withoutDefault = `""`;
export const mariaDBQuery = `SELECT CAST(CONCAT( export const mariaDBQuery = `SET SESSION group_concat_max_len = 10000000;
SELECT CAST(CONCAT(
'{"fk_info": [', '{"fk_info": [',
IFNULL((SELECT GROUP_CONCAT( IFNULL((SELECT GROUP_CONCAT(
CONCAT('{"schema":"', cast(fk.table_schema as CHAR), CONCAT('{"schema":"', cast(fk.table_schema as CHAR),

View File

@@ -0,0 +1,132 @@
import { describe, it, expect } from 'vitest';
import { validateSQL } from '../sql-validator';
import { DatabaseType } from '@/lib/domain';
describe('SQL Validator Auto-fix', () => {
it('should provide auto-fix for cast operator errors', () => {
const sql = `
CREATE TABLE dragons (
id UUID PRIMARY KEY,
lair_location GEOGRAPHY(POINT, 4326)
);
-- Problematic queries with cast operator errors
SELECT id: :text FROM dragons;
SELECT ST_X(lair_location: :geometry) AS longitude FROM dragons;
`;
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
// Should detect errors
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
// Should provide fixed SQL
expect(result.fixedSQL).toBeDefined();
// Fixed SQL should have correct cast operators
expect(result.fixedSQL).toContain('::text');
expect(result.fixedSQL).toContain('::geometry');
expect(result.fixedSQL).not.toContain(': :');
// The CREATE TABLE should remain intact
expect(result.fixedSQL).toContain('GEOGRAPHY(POINT, 4326)');
});
it('should handle multi-line cast operator errors', () => {
const sql = `
SELECT AVG(power_level): :DECIMAL(3,
2) FROM enchantments;
`;
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(false);
expect(result.fixedSQL).toBeDefined();
expect(result.fixedSQL).toContain('::DECIMAL(3,');
expect(result.fixedSQL).not.toContain(': :');
});
it('should auto-fix split DECIMAL declarations', () => {
const sql = `
CREATE TABLE potions (
id INTEGER PRIMARY KEY,
strength DECIMAL(10,
2) NOT NULL,
effectiveness NUMERIC(5,
3) DEFAULT 0.000
);`;
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
// Should provide fixed SQL
expect(result.fixedSQL).toBeDefined();
// Fixed SQL should have DECIMAL on one line
expect(result.fixedSQL).toContain('DECIMAL(10,2)');
expect(result.fixedSQL).toContain('NUMERIC(5,3)');
expect(result.fixedSQL).not.toMatch(
/DECIMAL\s*\(\s*\d+\s*,\s*\n\s*\d+\s*\)/
);
// Should have warning about auto-fix
expect(
result.warnings.some((w) =>
w.message.includes('Auto-fixed split DECIMAL/NUMERIC')
)
).toBe(true);
});
it('should handle multiple auto-fixes together', () => {
const sql = `
CREATE TABLE enchantments (
id INTEGER PRIMARY KEY,
power_level DECIMAL(10,
2) NOT NULL,
magic_type VARCHAR(50)
);
SELECT AVG(power_level): :DECIMAL(3,
2) FROM enchantments;
`;
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(false);
expect(result.fixedSQL).toBeDefined();
// Should fix both issues
expect(result.fixedSQL).toContain('DECIMAL(10,2)');
expect(result.fixedSQL).toContain('::DECIMAL(3,');
expect(result.fixedSQL).not.toContain(': :');
// Should have warnings for both fixes
expect(
result.warnings.some((w) =>
w.message.includes('Auto-fixed cast operator')
)
).toBe(true);
expect(
result.warnings.some((w) =>
w.message.includes('Auto-fixed split DECIMAL/NUMERIC')
)
).toBe(true);
});
it('should preserve original SQL when no errors', () => {
const sql = `
CREATE TABLE wizards (
id UUID PRIMARY KEY,
name VARCHAR(100)
);`;
const result = validateSQL(sql, DatabaseType.POSTGRESQL);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
expect(result.fixedSQL).toBeUndefined();
});
});

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