Compare commits

...

157 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
Guy Ben-Aharon
578546a171 chore(main): release 1.13.2 (#756) 2025-07-06 14:02:21 +03:00
Jonathan Fishner
aa0b629a3e fix: add DISABLE_ANALYTICS flag to opt-out of Fathom analytics (#750)
* feat: add DISABLE_ANALYTICS flag to opt-out of Fathom analytics

* fix: HIDE_BUCKLE_DOT_DEV -> HIDE_CHARTDB_CLOUD

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-07-06 13:52:58 +03:00
Guy Ben-Aharon
69beaa0a83 chore(main): release 1.13.1 (#735) 2025-07-05 18:14:47 +03:00
Guy Ben-Aharon
4fcc49d49a fix: general performance improvements on canvas (#751) 2025-07-04 12:23:29 +03:00
Guy Ben-Aharon
d15985e399 fix: resolve unresponsive cursor and input glitches when editing field comments (#749) 2025-07-02 20:33:52 +03:00
Jonathan Fishner
d429128e65 fix(dbml): Filter duplicate tables at diagram level before export dbml (#746) 2025-06-24 12:46:56 +03:00
Jonathan Fishner
2fce8326b6 fix(import-database): for custom types query to import supabase & timescale (#745) 2025-06-11 21:50:42 +03:00
Guy Ben-Aharon
433c68a33d lib refactor (#744) 2025-06-10 11:04:51 +03:00
Guy Ben-Aharon
58acb65f12 fix duplicate (#743)
* fix duplicate

* fix duplicate
2025-06-08 15:27:50 +03:00
Guy Ben-Aharon
7978955819 lib fix (#742) 2025-06-08 14:44:45 +03:00
Jonathan Fishner
c6118e0cdb fix(export-sql): conditionally show generic option and reorder by diagram type (#708)
* fix(export-sql): conditionally show generic option and reorder by diagram type

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-06-08 12:32:05 +03:00
Jonathan Fishner
7d063b905f fix(import-db): fix mariadb import (#740) 2025-06-06 19:45:13 +03:00
Jonathan Fishner
e0ff198c3f fix(dbml-editor): for some cases that the dbml had issues (#739) 2025-06-06 19:41:51 +03:00
Vlad Kovaliov
8b86e1c229 fix(table name): updates table name value when its updated from canvas/sidebar (#716) 2025-06-05 15:36:59 +03:00
Guy Ben-Aharon
24be28a662 fix(custom_types): fix display custom types in select box (#737) 2025-06-05 15:21:55 +03:00
Guy Ben-Aharon
c6788b4917 fix(performance): improve storage provider performance (#734)
* fix(performance): improve storage provider performance

* fix
2025-06-04 11:22:33 +03:00
Guy Ben-Aharon
4a52bf02e6 chore(main): release 1.13.0 (#712) 2025-06-03 20:34:49 +03:00
Guy Ben-Aharon
08b627cb8c fix(relationship): fix creating of relationships (#732) 2025-05-28 14:48:53 +03:00
Jonathan Fishner
73f542adad fix(dbml-editor): export comments with schema if existsed (#728) 2025-05-28 10:14:35 +03:00
Jonathan Fishner
0d11b0c55a fix(import-database): remove the default fetch from import database (#718)
* fix(import-database): remove the default fetch from import database

* fix(import-default): keep the option to fetch extras like default and comments
2025-05-28 10:10:33 +03:00
Guy Ben-Aharon
5b9d2bd1e3 clean unused param (#730) 2025-05-28 09:19:20 +03:00
Jonathan Fishner
cf1e141837 fix(custom-types): fetch directly via the smart-query the custom types (#729) 2025-05-27 19:26:07 +03:00
Jonathan Fishner
3894a22174 fix(dbml-editor): fix export dbml - to show enums (#724) 2025-05-26 17:55:18 +03:00
Jonathan Fishner
cad155e655 feat(export-sql): add custom types to export sql script (#720) 2025-05-26 17:51:38 +03:00
Guy Ben-Aharon
4477b1ca1f fix(canvas): prevent canvas blink and lag on primary field edit (#725) 2025-05-26 17:39:05 +03:00
Guy Ben-Aharon
cd443466c7 fix(canvas): prevent canvas blink and lag on field edit (#723) 2025-05-26 16:57:25 +03:00
Guy Ben-Aharon
18012ddab1 fix(custom_types): fix custom types on storage provider (#722) 2025-05-26 15:51:39 +03:00
Guy Ben-Aharon
beb015194f fix(custom_types): fix custom types on storage provider (#721) 2025-05-26 15:42:22 +03:00
Jonathan Fishner
c3904d9fdd feat(custom-types): add enums and composite types for Postgres (#714)
* feat(postgres): add custom datatypes to import script

* import only enums & composite types

* init commit to support custom types in postgres

* add support in matching custom fields on import + be able to choose it as type for feild

* update select box + type names

* all but ui

* update ui

* fix build

* fix build

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-26 15:18:49 +03:00
Guy Ben-Aharon
aee5779983 fix(menu): add oracle to import menu (#713) 2025-05-21 12:56:27 +03:00
Jonathan Fishner
765a1c4354 feat(oracle): support oracle in ChartDB (#709)
* feat(import-oracle): support oracle in ChartDB

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-21 12:12:48 +03:00
Guy Ben-Aharon
86840a8822 chore(main): release 1.12.0 (#662) 2025-05-21 12:03:11 +03:00
Jonathan Fishner
487fb2d5c1 fix(sql-script): change ddl to be sql-script (#710) 2025-05-20 17:52:08 +03:00
Jonathan Fishner
54d5e96a6d fix(expanded-table): persist expanded state across renders (#707)
* fix(expanded-table): persist expanded state across renders

* fix(reorder-tables): account for table sizes when reordering

* some fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-20 12:04:11 +03:00
Jonathan Fishner
481ad3c844 fix(import-database): remove view_definition when importing via query (#702)
* fix(import-database): remove view_definition when importing via query

* fix(view_definition): remove view_definition in cockroachdb & maria

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-14 18:59:09 +03:00
Guy Ben-Aharon
0ce85cf76b fix(export image): Fix usage of advanced options accordion (#703) 2025-05-14 18:41:05 +03:00
Jonathan Fishner
5849e4586c fix(ddl): inline fks ddl script (#701)
* fix(import-ddl): when importing postgres with inline fks

* fix(import-ddl): for postgres and mysql

* remove logs

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-13 11:47:22 +03:00
Jonathan Fishner
34c0a7163f fix(import): dbml and query - senetize before import (#699) 2025-05-12 19:10:40 +03:00
Jonathan Fishner
89e3ceab00 fix(postgres): fix import of postgres fks (#700)
* Add regex-based foreign key detection for PostgreSQL to handle complex formatting

* fix(import-ddl): for postgres know to deal with SERIAL type

* fix(export-sql): for postgres know to deal with SERIAL type

* fix(import-ddl): when importing default values
2025-05-12 18:22:30 +03:00
Jonathan Fishner
5a5e64abef fix(import-database): auto detect when user try to import ddl script (#698)
* fix(import-database): auto detect when user try to import ddl script

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-12 09:44:35 +03:00
Jonathan Fishner
2368e0d263 fix(import-json): for broken json imports (#697)
* fix(impoort-json): for broken json imports

* fix(import-db): after checking the JSON keep it formated
2025-05-11 18:17:52 +03:00
Jonathan Fishner
547149da44 fix(dependencies): hide icon when diagram has no dependencies (#684) 2025-05-08 16:28:31 +03:00
Jonathan Fishner
a1144bbf76 fix(ddl-import): fix datatypes when importing via ddl (#696)
* fix(ddl-import): fix datatypes when importing via ddl

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-08 14:48:08 +03:00
STPN
6b8d637b75 feat(image-export): add transparent and pattern export image toggles (#671)
* feat(image-export): add watermark, transparent and pattern export image toggles

* fix(export-image-provider): use pattern background enabling

* add translations + small ui fixes

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-08 14:35:17 +03:00
Guy Ben-Aharon
fd47eb7f4b update email address (#695) 2025-05-07 19:34:14 +03:00
Guy Ben-Aharon
7db86dcf8c fix(navbar): open diagram directly from diagram icon (#694) 2025-05-07 18:24:22 +03:00
Guy Ben-Aharon
e75323c16e update config fix (#693) 2025-05-07 18:20:15 +03:00
Тоха Лис
97d01d7201 fix(translations): Add some translations for ru-RU language (#690)
* chore: install unmet deps

* fix(i18n): Update Russian translation

* fix(dialog): adjust width of import DBML dialog for better responsiveness

* revert package.json

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-07 11:21:26 +03:00
Guy Ben-Aharon
90b42a4bb7 update created at on examples (#691)
* update created at on examples + templates

* update created at on examples + templates
2025-05-04 13:47:39 +03:00
Jonathan Fishner
fbf2fe919c fix(dbml-editor): add inline refs mode + fix issues with DBML syntax (#687)
* fix(dbml-editor): support & fix cases when mismatching with special chars

* fix(dbml-editor): add inline refs mode

* fix(dbml-editor): more fixes for dbml parser

* fix(dbml-editor): show colors for datatypes like char, varchar etc

* fix(sql-export): normalize verbose SQL types to simplified names

* some fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-03 17:53:36 +03:00
Guy Ben-Aharon
d3ddf7c51e fix(performance): update field only when changed (#685) 2025-04-30 17:16:57 +03:00
Jonathan Fishner
5759241573 fix(dbml-editor): remove invalid fields before showing DBML + warning (#683) 2025-04-30 12:08:35 +03:00
Guy Ben-Aharon
3747abbc3b refactor(dialog): pass params to create diagram dialog (#682) 2025-04-29 17:39:48 +03:00
Jonathan Fishner
226e6cf1ce fix(import-json): simplify import script for fixing invalid JSON (#681) 2025-04-29 17:36:28 +03:00
Guy Ben-Aharon
1778abb683 fix(examples): fix clone examples (#679) 2025-04-27 15:25:49 +03:00
Guy Ben-Aharon
90a20dd1b0 fix(examples): add loader (#678) 2025-04-27 15:04:42 +03:00
Jonathan Fishner
21c9129e14 feat(examples): update examples to have areas (#677) 2025-04-27 14:32:03 +03:00
Guy Ben-Aharon
19d2d0bddd fix(table): enhance field focus behavior to include table hover state (#676) 2025-04-27 12:44:07 +03:00
Guy Ben-Aharon
83c43332d4 fix(performance): Only render visible (#672) 2025-04-23 12:11:27 +03:00
Jonathan Fishner
3a1b8d1db1 fix: add sorting based on how common the datatype on side-panel (#651)
* fix: add sorting based on how common the datatype on side-panel

* fix type to new version

* remove colors

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-04-23 11:48:31 +03:00
Guy Ben-Aharon
46426e27b4 add cla workflow (#667) 2025-04-22 17:27:29 +03:00
Guy Ben-Aharon
9402822fa3 fix(canvas): disable edit area name on read only (#666) 2025-04-22 16:39:21 +03:00
Guy Ben-Aharon
651fe361fc fix(canvas): read only mode (#665) 2025-04-22 16:29:39 +03:00
Guy Ben-Aharon
aee1713aec fix(clone): add areas to clone diagram (#664) 2025-04-22 15:44:38 +03:00
Guy Ben-Aharon
ecfa14829b fix(schema): add areas to diagram schema (#663) 2025-04-22 15:24:21 +03:00
Guy Ben-Aharon
92e3ec785c feat(areas): implement area to enable logical diagram arrangement (#661)
* initial

* translations + update from canvas

* create area from canvas

* fix build

* fix build

* fix build
2025-04-22 15:00:52 +03:00
364 changed files with 55346 additions and 18845 deletions

View File

@@ -24,4 +24,7 @@ jobs:
run: npm run lint
- name: Build
run: npm run build
run: npm run build
- name: Run tests
run: npm run test:ci

33
.github/workflows/cla.yaml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened,closed,synchronize]
permissions:
actions: write
contents: read
pull-requests: write
statuses: write
jobs:
CLAAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: contributor-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.CHARTDB_CLA_SIGNATURES_PAT }}
with:
remote-organization-name: 'chartdb'
remote-repository-name: 'cla-signatures'
path-to-signatures: 'signatures/version1/cla.json'
path-to-document: 'https://github.com/chartdb/chartdb/blob/main/CLA.md'
# branch should not be protected
branch: 'main'
allowlist:

View File

@@ -1,5 +1,194 @@
# 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)
### Bug Fixes
* add DISABLE_ANALYTICS flag to opt-out of Fathom analytics ([#750](https://github.com/chartdb/chartdb/issues/750)) ([aa0b629](https://github.com/chartdb/chartdb/commit/aa0b629a3eaf8e8b60473ea3f28f769270c7714c))
## [1.13.1](https://github.com/chartdb/chartdb/compare/v1.13.0...v1.13.1) (2025-07-04)
### Bug Fixes
* **custom_types:** fix display custom types in select box ([#737](https://github.com/chartdb/chartdb/issues/737)) ([24be28a](https://github.com/chartdb/chartdb/commit/24be28a662c48fc5bc62e76446b9669d83d7d3e0))
* **dbml-editor:** for some cases that the dbml had issues ([#739](https://github.com/chartdb/chartdb/issues/739)) ([e0ff198](https://github.com/chartdb/chartdb/commit/e0ff198c3fd416498dac5680bb323ec88c54b65c))
* **dbml:** Filter duplicate tables at diagram level before export dbml ([#746](https://github.com/chartdb/chartdb/issues/746)) ([d429128](https://github.com/chartdb/chartdb/commit/d429128e65aa28c500eac2487356e4869506e948))
* **export-sql:** conditionally show generic option and reorder by diagram type ([#708](https://github.com/chartdb/chartdb/issues/708)) ([c6118e0](https://github.com/chartdb/chartdb/commit/c6118e0cdb0e5caaf73447d33db2fde1a98efe60))
* general performance improvements on canvas ([#751](https://github.com/chartdb/chartdb/issues/751)) ([4fcc49d](https://github.com/chartdb/chartdb/commit/4fcc49d49a76a4b886ffd6cf0b40cf2fc49952ec))
* **import-database:** for custom types query to import supabase & timescale ([#745](https://github.com/chartdb/chartdb/issues/745)) ([2fce832](https://github.com/chartdb/chartdb/commit/2fce8326b67b751d38dd34f409fea574449d0298))
* **import-db:** fix mariadb import ([#740](https://github.com/chartdb/chartdb/issues/740)) ([7d063b9](https://github.com/chartdb/chartdb/commit/7d063b905f19f51501468bd0bd794a25cf65e1be))
* **performance:** improve storage provider performance ([#734](https://github.com/chartdb/chartdb/issues/734)) ([c6788b4](https://github.com/chartdb/chartdb/commit/c6788b49173d9cce23571daeb460285cb7cffb11))
* resolve unresponsive cursor and input glitches when editing field comments ([#749](https://github.com/chartdb/chartdb/issues/749)) ([d15985e](https://github.com/chartdb/chartdb/commit/d15985e3999a0cd54213b2fb08c55d48a1b8b3b2))
* **table name:** updates table name value when its updated from canvas/sidebar ([#716](https://github.com/chartdb/chartdb/issues/716)) ([8b86e1c](https://github.com/chartdb/chartdb/commit/8b86e1c22992aaadcce7ad5fc1d267c5a57a99f0))
## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28)
### Features
* **custom-types:** add enums and composite types for Postgres ([#714](https://github.com/chartdb/chartdb/issues/714)) ([c3904d9](https://github.com/chartdb/chartdb/commit/c3904d9fdd63ef5b76a44e73582d592f2c418687))
* **export-sql:** add custom types to export sql script ([#720](https://github.com/chartdb/chartdb/issues/720)) ([cad155e](https://github.com/chartdb/chartdb/commit/cad155e6550f171b8faecbfdff27032798ecea43))
* **oracle:** support oracle in ChartDB ([#709](https://github.com/chartdb/chartdb/issues/709)) ([765a1c4](https://github.com/chartdb/chartdb/commit/765a1c43547a29bd3428c942c7afb56f63aaf046))
### Bug Fixes
* **canvas:** prevent canvas blink and lag on field edit ([#723](https://github.com/chartdb/chartdb/issues/723)) ([cd44346](https://github.com/chartdb/chartdb/commit/cd443466c7952f1cdc3739645c12130b9231e3a1))
* **canvas:** prevent canvas blink and lag on primary field edit ([#725](https://github.com/chartdb/chartdb/issues/725)) ([4477b1c](https://github.com/chartdb/chartdb/commit/4477b1ca1fe6b282b604739a23e31181acd4d7bc))
* **custom_types:** fix custom types on storage provider ([#721](https://github.com/chartdb/chartdb/issues/721)) ([beb0151](https://github.com/chartdb/chartdb/commit/beb015194f917c0ba644458410162d2b7599918c))
* **custom_types:** fix custom types on storage provider ([#722](https://github.com/chartdb/chartdb/issues/722)) ([18012dd](https://github.com/chartdb/chartdb/commit/18012ddab1718bcce3432aea626adf6fc9be25d9))
* **custom-types:** fetch directly via the smart-query the custom types ([#729](https://github.com/chartdb/chartdb/issues/729)) ([cf1e141](https://github.com/chartdb/chartdb/commit/cf1e141837eda77d717ad87489ce9946b688e226))
* **dbml-editor:** export comments with schema if existsed ([#728](https://github.com/chartdb/chartdb/issues/728)) ([73f542a](https://github.com/chartdb/chartdb/commit/73f542adad2d66a1e84fc656a0c34d9b1f39f33c))
* **dbml-editor:** fix export dbml - to show enums ([#724](https://github.com/chartdb/chartdb/issues/724)) ([3894a22](https://github.com/chartdb/chartdb/commit/3894a221745d32c13160bedcb1bcf53d89897698))
* **import-database:** remove the default fetch from import database ([#718](https://github.com/chartdb/chartdb/issues/718)) ([0d11b0c](https://github.com/chartdb/chartdb/commit/0d11b0c55a94a12a764785cfdcf2ba10437241d6))
* **menu:** add oracle to import menu ([#713](https://github.com/chartdb/chartdb/issues/713)) ([aee5779](https://github.com/chartdb/chartdb/commit/aee577998342eb4a2b05b3e03181992a435712d8))
* **relationship:** fix creating of relationships ([#732](https://github.com/chartdb/chartdb/issues/732)) ([08b627c](https://github.com/chartdb/chartdb/commit/08b627cb8ca8fdf08d8ed2ff7e89104887deffb7))
## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
### Features
* **areas:** implement area to enable logical diagram arrangement ([#661](https://github.com/chartdb/chartdb/issues/661)) ([92e3ec7](https://github.com/chartdb/chartdb/commit/92e3ec785c91f7f19881c6d9d0692257af4651bc))
* **examples:** update examples to have areas ([#677](https://github.com/chartdb/chartdb/issues/677)) ([21c9129](https://github.com/chartdb/chartdb/commit/21c9129e14670c744950cd43a5cbdd4b7d47c639))
* **image-export:** add transparent and pattern export image toggles ([#671](https://github.com/chartdb/chartdb/issues/671)) ([6b8d637](https://github.com/chartdb/chartdb/commit/6b8d637b757b94630ecd7521b4a2c99634afae69))
### Bug Fixes
* add sorting based on how common the datatype on side-panel ([#651](https://github.com/chartdb/chartdb/issues/651)) ([3a1b8d1](https://github.com/chartdb/chartdb/commit/3a1b8d1db13d8dd7cb6cbe5ef8c5a60faccfeae5))
* **canvas:** disable edit area name on read only ([#666](https://github.com/chartdb/chartdb/issues/666)) ([9402822](https://github.com/chartdb/chartdb/commit/9402822fa31f8cd94fe7971277839ee5425e29bf))
* **canvas:** read only mode ([#665](https://github.com/chartdb/chartdb/issues/665)) ([651fe36](https://github.com/chartdb/chartdb/commit/651fe361fce61fe0577d2593f268131e9ca359d0))
* **clone:** add areas to clone diagram ([#664](https://github.com/chartdb/chartdb/issues/664)) ([aee1713](https://github.com/chartdb/chartdb/commit/aee1713aecdd5e54228a16cbc3c4fc184661c56b))
* **dbml-editor:** add inline refs mode + fix issues with DBML syntax ([#687](https://github.com/chartdb/chartdb/issues/687)) ([fbf2fe9](https://github.com/chartdb/chartdb/commit/fbf2fe919c2168c715f8231c0246753b19635f14))
* **dbml-editor:** remove invalid fields before showing DBML + warning ([#683](https://github.com/chartdb/chartdb/issues/683)) ([5759241](https://github.com/chartdb/chartdb/commit/5759241573db204183c92599588d59f4aadaeafb))
* **ddl-import:** fix datatypes when importing via ddl ([#696](https://github.com/chartdb/chartdb/issues/696)) ([a1144bb](https://github.com/chartdb/chartdb/commit/a1144bbf761a0daedd546b5d9b92300be59e0157))
* **ddl:** inline fks ddl script ([#701](https://github.com/chartdb/chartdb/issues/701)) ([5849e45](https://github.com/chartdb/chartdb/commit/5849e4586c7c2a7cd86bd064df8916b130fc6234))
* **dependencies:** hide icon when diagram has no dependencies ([#684](https://github.com/chartdb/chartdb/issues/684)) ([547149d](https://github.com/chartdb/chartdb/commit/547149da44db6d3d1e36d619d475fe52ff83a472))
* **examples:** add loader ([#678](https://github.com/chartdb/chartdb/issues/678)) ([90a20dd](https://github.com/chartdb/chartdb/commit/90a20dd1b0277c4aee848fae5ed7a8347c5ba77d))
* **examples:** fix clone examples ([#679](https://github.com/chartdb/chartdb/issues/679)) ([1778abb](https://github.com/chartdb/chartdb/commit/1778abb683d575af244edcd9a11f8d03f903f719))
* **expanded-table:** persist expanded state across renders ([#707](https://github.com/chartdb/chartdb/issues/707)) ([54d5e96](https://github.com/chartdb/chartdb/commit/54d5e96a6db1e3abd52229a89ac503ff31885386))
* **export image:** Fix usage of advanced options accordion ([#703](https://github.com/chartdb/chartdb/issues/703)) ([0ce85cf](https://github.com/chartdb/chartdb/commit/0ce85cf76b733f441f661608278c0db3122c5074))
* **import-database:** auto detect when user try to import ddl script ([#698](https://github.com/chartdb/chartdb/issues/698)) ([5a5e64a](https://github.com/chartdb/chartdb/commit/5a5e64abef510cff28b3d8972520d0b9df29b024))
* **import-database:** remove view_definition when importing via query ([#702](https://github.com/chartdb/chartdb/issues/702)) ([481ad3c](https://github.com/chartdb/chartdb/commit/481ad3c8449f469bf2b4418e4cdcc5b5608dfd36))
* **import-json:** for broken json imports ([#697](https://github.com/chartdb/chartdb/issues/697)) ([2368e0d](https://github.com/chartdb/chartdb/commit/2368e0d2639021c4a11a8e5131d6af44fb6a47db))
* **import-json:** simplify import script for fixing invalid JSON ([#681](https://github.com/chartdb/chartdb/issues/681)) ([226e6cf](https://github.com/chartdb/chartdb/commit/226e6cf1ce4d2edcfbee6a4de7ab0bc0cfeb17fe))
* **import:** dbml and query - senetize before import ([#699](https://github.com/chartdb/chartdb/issues/699)) ([34c0a71](https://github.com/chartdb/chartdb/commit/34c0a7163f47bde7ddfaa8f044341e3c971b7e03))
* **navbar:** open diagram directly from diagram icon ([#694](https://github.com/chartdb/chartdb/issues/694)) ([7db86dc](https://github.com/chartdb/chartdb/commit/7db86dcf8c97d34b056e4b5b85a0dda0438322ea))
* **performance:** Only render visible ([#672](https://github.com/chartdb/chartdb/issues/672)) ([83c4333](https://github.com/chartdb/chartdb/commit/83c43332d497e9fc148a18b9cb4d9ecc85e44183))
* **performance:** update field only when changed ([#685](https://github.com/chartdb/chartdb/issues/685)) ([d3ddf7c](https://github.com/chartdb/chartdb/commit/d3ddf7c51eaa4b9cddb961defd52d423f39f281d))
* **postgres:** fix import of postgres fks ([#700](https://github.com/chartdb/chartdb/issues/700)) ([89e3cea](https://github.com/chartdb/chartdb/commit/89e3ceab00defaabc079e165fc90e92ca00722cf))
* **schema:** add areas to diagram schema ([#663](https://github.com/chartdb/chartdb/issues/663)) ([ecfa148](https://github.com/chartdb/chartdb/commit/ecfa14829bcb1b813c7b154b4bd59f24e3032d8f))
* **sql-script:** change ddl to be sql-script ([#710](https://github.com/chartdb/chartdb/issues/710)) ([487fb2d](https://github.com/chartdb/chartdb/commit/487fb2d5c17b70ac54aa17af9a2ac9aded6b40ba))
* **table:** enhance field focus behavior to include table hover state ([#676](https://github.com/chartdb/chartdb/issues/676)) ([19d2d0b](https://github.com/chartdb/chartdb/commit/19d2d0bddd3a464995b79e97e6caf6e652836081))
* **translations:** Add some translations for ru-RU language ([#690](https://github.com/chartdb/chartdb/issues/690)) ([97d01d7](https://github.com/chartdb/chartdb/commit/97d01d72014e473c42348c9ebcbe7a0b973d31aa))
## [1.11.0](https://github.com/chartdb/chartdb/compare/v1.10.0...v1.11.0) (2025-04-17)

45
CLA.md Normal file
View File

@@ -0,0 +1,45 @@
# ChartDB Contributors License Agreement
This Contributors License Agreement ("CLA") is entered into between the Contributor, and ChartDB, Inc. ("ChartDB"), collectively referred to as the "Parties."
## Background:
ChartDB is an open-source project aimed at providing an open-source database diagramming and visualization tool for all parties.This CLA governs the rights and contributions made by the Contributor to the ChartDB project.
## Agreement:
**Contributor Grant of License:**
By submitting code, documentation, or any other materials (collectively, "Contributions") to the ChartDB project, the Contributor grants ChartDB a perpetual, worldwide, non-exclusive, royalty-free, sublicensable license to use, modify, distribute, and otherwise exploit the Contributions, including any intellectual property rights therein, for the purposes of the ChartDB project.
**Representation of Ownership and Right to Contribute:**
The Contributor represents that they have the legal right to grant the license stated in Section 1, and that the Contributions do not infringe upon the intellectual property rights of any third party. The Contributor also represents that they have the authority to submit the Contributions on their own behalf or, if applicable, on behalf of their employer or any other entity.
**Patent Grant:**
If the Contributions include any method, process, or apparatus that is covered by a patent, the Contributor agrees to grant ChartDB a non-exclusive, worldwide, royalty-free license under any patent claims necessary to use, modify, distribute, and otherwise exploit the Contributions for the purposes of the ChartDB project.
**No Implied Warranties or Support:**
The Contributor acknowledges that the Contributions are provided "as is," without any warranties or support of any kind. ChartDB shall have no obligation to provide maintenance, updates, bug fixes, or support for the Contributions.
**Retention of Contributor Rights:**
The Contributor retains all right, title, and interest in and to their Contributions. This CLA does not restrict the Contributor from using their own Contributions for any other purpose.
**Governing Law:**
This CLA shall be governed by and construed in accordance with the laws of Delaware (DE), without regard to its conflict of laws principles.
**Entire Agreement:**
This CLA constitutes the entire agreement between the Parties with respect to the subject matter hereof and supersedes all prior and contemporaneous understandings, agreements, representations, and warranties.
**Acceptance:**
By submitting Contributions to the ChartDB project, the Contributor acknowledges and agrees to the terms and conditions of this CLA. If the Contributor is agreeing to this CLA on behalf of an entity, they represent that they have the necessary authority to bind that entity to these terms.
**Effective Date:**
This CLA is effective as of the date of the first Contribution made by the Contributor to the ChartDB project.

View File

@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
chartdb.io@gmail.com.
support@chartdb.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the

View File

@@ -18,7 +18,7 @@ To submit a pull request:
If you find a bug, check [GitHub issues](https://github.com/chartdb/chartdb/issues) to see if its already reported. If not, feel free to [report it](https://github.com/chartdb/chartdb/issues/new?labels=bug).
For questions about using ChartDB, reach out to us via Email (chartdb.io@gmail.com) or [Discord](https://discord.gg/QeFwyWSKwC). For feature requests, create a [new feature](https://github.com/chartdb/chartdb/issues/new?labels=enhancement).
For questions about using ChartDB, reach out to us via Email (support@chartdb.io) or [Discord](https://discord.gg/QeFwyWSKwC). For feature requests, create a [new feature](https://github.com/chartdb/chartdb/issues/new?labels=enhancement).
### Creating a Branch
@@ -35,7 +35,7 @@ By contributing, you agree that your work will be licensed under ChartDB's [lice
## Questions?
Feel free to ask in `#contributing` on [Discord](https://discord.gg/QeFwyWSKwC) if you have questions about our process, how to proceed, etc.
or [Email](chartdb.io@gmail.com)
or [Email](support@chartdb.io)
---

View File

@@ -3,7 +3,8 @@ FROM node:22-alpine AS builder
ARG VITE_OPENAI_API_KEY
ARG VITE_OPENAI_API_ENDPOINT
ARG VITE_LLM_MODEL_NAME
ARG VITE_HIDE_BUCKLE_DOT_DEV
ARG VITE_HIDE_CHARTDB_CLOUD
ARG VITE_DISABLE_ANALYTICS
WORKDIR /usr/src/app
@@ -16,7 +17,8 @@ COPY . .
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \
echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env
echo "VITE_HIDE_CHARTDB_CLOUD=${VITE_HIDE_CHARTDB_CLOUD}" >> .env && \
echo "VITE_DISABLE_ANALYTICS=${VITE_DISABLE_ANALYTICS}" >> .env
RUN npm run build

View File

@@ -125,6 +125,8 @@ docker run \
-p 8080:80 chartdb
```
> **Privacy Note:** ChartDB includes privacy-focused analytics via Fathom Analytics. You can disable this by adding `-e DISABLE_ANALYTICS=true` to the run command or `--build-arg VITE_DISABLE_ANALYTICS=true` when building.
> **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options.
Open your browser and navigate to `http://localhost:8080`.

View File

@@ -10,11 +10,12 @@ server {
location /config.js {
default_type application/javascript;
return 200 "window.env = {
return 200 "window.env = {
OPENAI_API_KEY: \"$OPENAI_API_KEY\",
OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\"
HIDE_CHARTDB_CLOUD: \"$HIDE_CHARTDB_CLOUD\",
DISABLE_ANALYTICS: \"$DISABLE_ANALYTICS\"
};";
}

View File

@@ -1,7 +1,7 @@
#!/bin/sh
# Replace placeholders in nginx.conf
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_BUCKLE_DOT_DEV}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_CHARTDB_CLOUD} ${DISABLE_ANALYTICS}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
# Start Nginx
nginx -g "daemon off;"

View File

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

1610
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.11.0",
"version": "1.15.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -9,7 +9,11 @@
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"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": {
"@ai-sdk/openai": "^0.0.51",
@@ -22,24 +26,24 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-tooltip": "^1.2.7",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.3.1",
"@xyflow/react": "^12.8.2",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.1",
@@ -50,8 +54,9 @@
"html-to-image": "^1.11.11",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.441.0",
"lucide-react": "^0.525.0",
"monaco-editor": "^0.52.0",
"motion": "^12.23.6",
"nanoid": "^5.0.7",
"node-sql-parser": "^5.3.2",
"react": "^18.3.1",
@@ -73,12 +78,16 @@
"@eslint/compat": "^1.2.4",
"@eslint/eslintrc": "^3.2.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/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"@vitejs/plugin-react": "^4.3.1",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0",
@@ -90,6 +99,7 @@
"eslint-plugin-react-refresh": "^0.4.7",
"eslint-plugin-tailwindcss": "^3.17.4",
"globals": "^15.13.0",
"happy-dom": "^18.0.1",
"husky": "^9.1.5",
"postcss": "^8.4.40",
"prettier": "^3.3.3",
@@ -97,6 +107,7 @@
"tailwindcss": "^3.4.7",
"typescript": "^5.2.2",
"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: *
Allow: /
Disallow: /
Sitemap: https://app.chartdb.io/sitemap.xml

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 KiB

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 359 KiB

BIN
src/assets/oracle_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import { useTheme } from '@/hooks/use-theme';
import { useMonaco } from '@monaco-editor/react';
import { useToast } from '@/components/toast/use-toast';
import { Button } from '../button/button';
import type { LucideIcon } from 'lucide-react';
import { Copy, CopyCheck } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
import { useTranslation } from 'react-i18next';
@@ -26,6 +27,13 @@ export const DiffEditor = lazy(() =>
type EditorType = typeof Editor;
export interface CodeSnippetAction {
label: string;
icon: LucideIcon;
onClick: () => void;
className?: string;
}
export interface CodeSnippetProps {
className?: string;
code: string;
@@ -35,6 +43,9 @@ export interface CodeSnippetProps {
autoScroll?: boolean;
isComplete?: boolean;
editorProps?: React.ComponentProps<EditorType>;
actions?: CodeSnippetAction[];
actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left';
allowCopy?: boolean;
}
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
@@ -47,6 +58,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
autoScroll = false,
isComplete = true,
editorProps,
actions,
actionsTooltipSide,
allowCopy = true,
}) => {
const { t } = useTranslation();
const monaco = useMonaco();
@@ -119,36 +133,67 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
) : (
<Suspense fallback={<Spinner />}>
{isComplete ? (
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger
asChild
className="absolute right-1 top-1 z-10"
>
<span>
<Button
className=" h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
{allowCopy ? (
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent
side={actionsTooltipSide}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
</TooltipContent>
</Tooltip>
) : null}
{actions &&
actions.length > 0 &&
actions.map((action, index) => (
<Tooltip key={index}>
<TooltipTrigger asChild>
<span>
<Button
className={cn(
'h-fit p-1.5',
action.className
)}
variant="outline"
onClick={action.onClick}
>
<action.icon
size={16}
/>
</Button>
</span>
</TooltipTrigger>
<TooltipContent
side={actionsTooltipSide}
>
{action.label}
</TooltipContent>
</Tooltip>
))}
</div>
) : null}
<Editor

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('|');
monaco.languages.setMonarchTokensProvider('dbml', {
keywords: ['Table', 'Ref', 'Indexes'],
keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
datatypes: dataTypesNames,
tokenizer: {
root: [
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
[
/\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
'keyword',
],
[/\[.*?\]/, 'annotation'],
[/'''/, 'string', '@tripleQuoteString'],
[/".*?"/, 'string'],
[/'.*?'/, 'string'],
[/`.*?`/, 'string'],
[/[{}]/, 'delimiter'],
[/[<>]/, 'operator'],
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
],
tripleQuoteString: [
[/[^']+/, 'string'],
[/'''/, 'string', '@pop'],
[/'/, 'string'],
],
},
});
};

View File

@@ -22,14 +22,15 @@ export interface DiagramIconProps
export const DiagramIcon = React.forwardRef<
React.ElementRef<typeof TooltipTrigger>,
DiagramIconProps
>(({ databaseType, databaseEdition, className, imgClassName }, ref) =>
>(({ databaseType, databaseEdition, className, imgClassName, onClick }, ref) =>
databaseEdition ? (
<Tooltip>
<TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild>
<img
src={databaseEditionToImageMap[databaseEdition]}
className={cn('h-5 max-w-fit rounded-full', imgClassName)}
className={cn('max-h-5 max-w-5 rounded-full', imgClassName)}
alt="database"
onClick={onClick}
/>
</TooltipTrigger>
<TooltipContent>
@@ -41,8 +42,9 @@ export const DiagramIcon = React.forwardRef<
<TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild>
<img
src={databaseSecondaryLogoMap[databaseType]}
className={cn('h-5 max-w-fit', imgClassName)}
className={cn('max-h-5 max-w-5', imgClassName)}
alt="database"
onClick={onClick}
/>
</TooltipTrigger>
<TooltipContent>

View File

@@ -4,6 +4,7 @@ import { Cross2Icon } from '@radix-ui/react-icons';
import { cn } from '@/lib/utils';
import { ScrollArea } from '../scroll-area/scroll-area';
import { ChevronLeft } from 'lucide-react';
const Dialog = DialogPrimitive.Root;
@@ -32,28 +33,75 @@ const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showClose?: boolean;
showBack?: boolean;
backButtonClassName?: string;
blurBackground?: boolean;
forceOverlay?: boolean;
onBackClick?: () => void;
}
>(({ className, children, showClose, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
{showClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));
>(
(
{
className,
children,
showClose,
showBack,
onBackClick,
backButtonClassName,
blurBackground,
forceOverlay,
...props
},
ref
) => (
<DialogPortal>
{forceOverlay ? (
<div
className={cn(
'fixed inset-0 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
{
'bg-black/80': !blurBackground,
'bg-black/30 backdrop-blur-sm': blurBackground,
}
)}
data-state="open"
/>
) : null}
<DialogOverlay
className={cn({
'bg-black/30 backdrop-blur-sm': blurBackground,
})}
/>
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
{showBack && (
<button
onClick={() => onBackClick?.()}
className={cn(
'absolute left-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground',
backButtonClassName
)}
>
<ChevronLeft className="size-4" />
</button>
)}
{showClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
);
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({

View File

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

View File

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

View File

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

@@ -26,6 +26,7 @@ export interface SelectBoxOption {
description?: string;
regex?: string;
extractRegex?: RegExp;
group?: string;
}
export interface SelectBoxProps {
@@ -51,6 +52,7 @@ export interface SelectBoxProps {
disabled?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
popoverClassName?: string;
}
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
@@ -75,6 +77,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
disabled,
open,
onOpenChange: setOpen,
popoverClassName,
},
ref
) => {
@@ -90,6 +93,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
(isOpen: boolean) => {
setOpen?.(isOpen);
setIsOpen(isOpen);
if (isOpen) {
setSearchTerm('');
}
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
},
[setOpen]
);
@@ -175,6 +184,101 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
[isOpen, onOpenChange]
);
const groups = React.useMemo(
() =>
options.reduce(
(acc, option) => {
if (option.group) {
if (!acc[option.group]) {
acc[option.group] = [];
}
acc[option.group].push(option);
} else {
if (!acc['default']) {
acc['default'] = [];
}
acc['default'].push(option);
}
return acc;
},
{} as Record<string, SelectBoxOption[]>
),
[options]
);
const hasGroups = React.useMemo(
() =>
Object.keys(groups).filter((group) => group !== 'default')
.length > 0,
[groups]
);
const renderOption = React.useCallback(
(option: SelectBoxOption) => {
const isSelected =
Array.isArray(value) && value.includes(option.value);
const isRegexMatch =
option.regex && new RegExp(option.regex)?.test(searchTerm);
const matches = option.extractRegex
? searchTerm.match(option.extractRegex)
: undefined;
return (
<CommandItem
className="flex items-center"
key={option.value}
keywords={option.regex ? [option.regex] : undefined}
onSelect={() =>
handleSelect(
option.value,
matches?.map((match) => match?.toString())
)
}
>
{multiple && (
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
)}
>
<CheckIcon />
</div>
)}
<div className="flex flex-1 items-center truncate">
<span>
{isRegexMatch ? searchTerm : option.label}
{!isRegexMatch && optionSuffix
? optionSuffix(option)
: ''}
</span>
{option.description && (
<span className="ml-1 w-0 flex-1 truncate text-xs text-muted-foreground">
{option.description}
</span>
)}
</div>
{((!multiple && option.value === value) ||
isRegexMatch) && (
<CheckIcon
className={cn(
'ml-auto',
option.value === value
? 'opacity-100'
: 'opacity-0'
)}
/>
)}
</CommandItem>
);
},
[value, multiple, searchTerm, handleSelect, optionSuffix]
);
return (
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
@@ -245,7 +349,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
</div>
</PopoverTrigger>
<PopoverContent
className="w-fit min-w-[var(--radix-popover-trigger-width)] p-0"
className={cn(
'w-fit min-w-[var(--radix-popover-trigger-width)] p-0',
popoverClassName
)}
align="center"
>
<Command
@@ -317,96 +424,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
<ScrollArea>
<div className="max-h-64 w-full">
<CommandGroup>
<CommandList className="max-h-fit w-full">
{options.map((option) => {
const isSelected =
Array.isArray(value) &&
value.includes(option.value);
const isRegexMatch =
option.regex &&
new RegExp(option.regex)?.test(
searchTerm
);
const matches = option.extractRegex
? searchTerm.match(
option.extractRegex
)
: undefined;
return (
<CommandItem
className="flex items-center"
key={option.value}
keywords={
option.regex
? [option.regex]
: undefined
}
// value={option.value}
onSelect={() =>
handleSelect(
option.value,
matches?.map(
(match) =>
match.toString()
)
)
}
>
{multiple && (
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
)}
>
<CheckIcon />
</div>
)}
<div className="flex items-center truncate">
<span>
{isRegexMatch
? searchTerm
: option.label}
{!isRegexMatch &&
optionSuffix
? optionSuffix(
option
)
: ''}
</span>
{option.description && (
<span className="ml-1 text-xs text-muted-foreground">
{
option.description
}
</span>
)}
</div>
{((!multiple &&
option.value ===
value) ||
isRegexMatch) && (
<CheckIcon
className={cn(
'ml-auto',
option.value ===
value
? 'opacity-100'
: 'opacity-0'
)}
/>
)}
</CommandItem>
);
})}
</CommandList>
</CommandGroup>
<CommandList className="max-h-fit w-full">
{hasGroups
? Object.entries(groups).map(
([groupName, groupOptions]) => (
<CommandGroup
key={groupName}
heading={groupName}
>
{groupOptions.map(
renderOption
)}
</CommandGroup>
)
)
: options.map(renderOption)}
</CommandList>
</div>
</ScrollArea>
</Command>

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ type ToasterToast = ToastProps & {
description?: React.ReactNode;
action?: ToastActionElement;
layout?: 'row' | 'column';
hideCloseButton?: boolean;
};
// 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.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
// <TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]',
className
)}
{...props}
/>
// </TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;

View File

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

View File

@@ -10,6 +10,8 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
import type { DBSchema } from '@/lib/domain/db-schema';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
export type ChartDBEventType =
| 'add_tables'
@@ -70,12 +72,14 @@ export interface ChartDBContext {
schemas: DBSchema[];
relationships: DBRelationship[];
dependencies: DBDependency[];
areas: Area[];
customTypes: DBCustomType[];
currentDiagram: Diagram;
events: EventEmitter<ChartDBEvent>;
readonly?: boolean;
filteredSchemas?: string[];
filterSchemas: (schemaIds: string[]) => void;
highlightedCustomType?: DBCustomType;
highlightCustomTypeId: (id?: string) => void;
// General operations
updateDiagramId: (id: string) => Promise<void>;
@@ -88,6 +92,10 @@ export interface ChartDBContext {
updateDiagramUpdatedAt: () => Promise<void>;
clearDiagramData: () => Promise<void>;
deleteDiagram: () => Promise<void>;
updateDiagramData: (
diagram: Diagram,
options?: { forceUpdateStorage?: boolean }
) => Promise<void>;
// Database type operations
updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
@@ -221,6 +229,58 @@ export interface ChartDBContext {
dependency: Partial<DBDependency>,
options?: { updateHistory: boolean }
) => Promise<void>;
// Area operations
createArea: (attributes?: Partial<Omit<Area, 'id'>>) => Promise<Area>;
addArea: (
area: Area,
options?: { updateHistory: boolean }
) => Promise<void>;
addAreas: (
areas: Area[],
options?: { updateHistory: boolean }
) => Promise<void>;
getArea: (id: string) => Area | null;
removeArea: (
id: string,
options?: { updateHistory: boolean }
) => Promise<void>;
removeAreas: (
ids: string[],
options?: { updateHistory: boolean }
) => Promise<void>;
updateArea: (
id: string,
area: Partial<Area>,
options?: { updateHistory: boolean }
) => Promise<void>;
// Custom type operations
createCustomType: (
attributes?: Partial<Omit<DBCustomType, 'id'>>
) => Promise<DBCustomType>;
addCustomType: (
customType: DBCustomType,
options?: { updateHistory: boolean }
) => Promise<void>;
addCustomTypes: (
customTypes: DBCustomType[],
options?: { updateHistory: boolean }
) => Promise<void>;
getCustomType: (id: string) => DBCustomType | null;
removeCustomType: (
id: string,
options?: { updateHistory: boolean }
) => Promise<void>;
removeCustomTypes: (
ids: string[],
options?: { updateHistory: boolean }
) => Promise<void>;
updateCustomType: (
id: string,
customType: Partial<DBCustomType>,
options?: { updateHistory: boolean }
) => Promise<void>;
}
export const chartDBContext = createContext<ChartDBContext>({
@@ -230,9 +290,10 @@ export const chartDBContext = createContext<ChartDBContext>({
tables: [],
relationships: [],
dependencies: [],
areas: [],
customTypes: [],
schemas: [],
filteredSchemas: [],
filterSchemas: emptyFn,
highlightCustomTypeId: emptyFn,
currentDiagram: {
id: '',
name: '',
@@ -250,6 +311,7 @@ export const chartDBContext = createContext<ChartDBContext>({
loadDiagramFromData: emptyFn,
clearDiagramData: emptyFn,
deleteDiagram: emptyFn,
updateDiagramData: emptyFn,
// Database type operations
updateDatabaseType: emptyFn,
@@ -296,4 +358,22 @@ export const chartDBContext = createContext<ChartDBContext>({
removeDependencies: emptyFn,
addDependencies: emptyFn,
updateDependency: emptyFn,
// Area operations
createArea: emptyFn,
addArea: emptyFn,
addAreas: emptyFn,
getArea: emptyFn,
removeArea: emptyFn,
removeAreas: emptyFn,
updateArea: emptyFn,
// Custom type operations
createCustomType: emptyFn,
addCustomType: emptyFn,
addCustomTypes: emptyFn,
getCustomType: emptyFn,
removeCustomType: emptyFn,
removeCustomTypes: emptyFn,
updateCustomType: emptyFn,
});

View File

@@ -1,12 +1,15 @@
import React, { useCallback, useMemo, useState } from 'react';
import type { DBTable } from '@/lib/domain/db-table';
import { deepCopy, generateId } from '@/lib/utils';
import { randomColor } from '@/lib/colors';
import { defaultTableColor, defaultAreaColor, viewColor } from '@/lib/colors';
import type { ChartDBContext, ChartDBEvent } from './chartdb-context';
import { chartDBContext } from './chartdb-context';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
import {
getTableIndexesWithPrimaryKey,
type DBIndex,
} from '@/lib/domain/db-index';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import { useStorage } from '@/hooks/use-storage';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
@@ -17,13 +20,17 @@ import {
databasesWithSchemas,
schemaNameToSchemaId,
} from '@/lib/domain/db-schema';
import { useLocalConfig } from '@/hooks/use-local-config';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { useEventEmitter } from 'ahooks';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import { storageInitialValue } from '../storage-context/storage-context';
import { useDiff } from '../diff-context/use-diff';
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
import {
DBCustomTypeKind,
type DBCustomType,
} from '@/lib/domain/db-custom-type';
export interface ChartDBProviderProps {
diagram?: Diagram;
@@ -34,11 +41,12 @@ export const ChartDBProvider: React.FC<
React.PropsWithChildren<ChartDBProviderProps>
> = ({ children, diagram, readonly: readonlyProp }) => {
const { hasDiff } = useDiff();
let db = useStorage();
const dbStorage = useStorage();
let db = dbStorage;
const events = useEventEmitter<ChartDBEvent>();
const { setSchemasFilter, schemasFilter } = useLocalConfig();
const { addUndoAction, resetRedoStack, resetUndoStack } =
useRedoUndoStack();
const [diagramId, setDiagramId] = useState('');
const [diagramName, setDiagramName] = useState('');
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
@@ -56,8 +64,16 @@ export const ChartDBProvider: React.FC<
const [dependencies, setDependencies] = useState<DBDependency[]>(
diagram?.dependencies ?? []
);
const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
diagram?.customTypes ?? []
);
const { events: diffEvents } = useDiff();
const [highlightedCustomTypeId, setHighlightedCustomTypeId] =
useState<string>();
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
setTables((tables) =>
@@ -76,7 +92,10 @@ export const ChartDBProvider: React.FC<
diffEvents.useSubscription(diffCalculatedHandler);
const defaultSchemaName = defaultSchemas[databaseType];
const defaultSchemaName = useMemo(
() => defaultSchemas[databaseType],
[databaseType]
);
const readonly = useMemo(
() => readonlyProp ?? hasDiff ?? false,
@@ -97,9 +116,11 @@ export const ChartDBProvider: React.FC<
.filter((schema) => !!schema) as string[]
),
]
.sort((a, b) =>
a === defaultSchemaName ? -1 : a.localeCompare(b)
)
.sort((a, b) => {
if (a === defaultSchemaName) return -1;
if (b === defaultSchemaName) return 1;
return a.localeCompare(b);
})
.map(
(schema): DBSchema => ({
id: schemaNameToSchemaId(schema),
@@ -113,34 +134,6 @@ export const ChartDBProvider: React.FC<
[tables, defaultSchemaName, databaseType]
);
const filterSchemas: ChartDBContext['filterSchemas'] = useCallback(
(schemaIds) => {
setSchemasFilter((prev) => ({
...prev,
[diagramId]: schemaIds,
}));
},
[diagramId, setSchemasFilter]
);
const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(() => {
if (schemas.length === 0) {
return undefined;
}
const schemasFilterFromCache =
(schemasFilter[diagramId] ?? []).length === 0
? undefined // in case of empty filter, skip cache
: schemasFilter[diagramId];
return (
schemasFilterFromCache ?? [
schemas.find((s) => s.name === defaultSchemaName)?.id ??
schemas[0]?.id,
]
);
}, [schemasFilter, diagramId, schemas, defaultSchemaName]);
const currentDiagram: Diagram = useMemo(
() => ({
id: diagramId,
@@ -152,6 +145,8 @@ export const ChartDBProvider: React.FC<
tables,
relationships,
dependencies,
areas,
customTypes,
}),
[
diagramId,
@@ -161,6 +156,8 @@ export const ChartDBProvider: React.FC<
tables,
relationships,
dependencies,
areas,
customTypes,
diagramCreatedAt,
diagramUpdatedAt,
]
@@ -172,6 +169,8 @@ export const ChartDBProvider: React.FC<
setTables([]);
setRelationships([]);
setDependencies([]);
setAreas([]);
setCustomTypes([]);
setDiagramUpdatedAt(updatedAt);
resetRedoStack();
@@ -182,6 +181,8 @@ export const ChartDBProvider: React.FC<
db.deleteDiagramTables(diagramId),
db.deleteDiagramRelationships(diagramId),
db.deleteDiagramDependencies(diagramId),
db.deleteDiagramAreas(diagramId),
db.deleteDiagramCustomTypes(diagramId),
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
@@ -194,6 +195,8 @@ export const ChartDBProvider: React.FC<
setTables([]);
setRelationships([]);
setDependencies([]);
setAreas([]);
setCustomTypes([]);
resetRedoStack();
resetUndoStack();
@@ -202,6 +205,8 @@ export const ChartDBProvider: React.FC<
db.deleteDiagramRelationships(diagramId),
db.deleteDiagram(diagramId),
db.deleteDiagramDependencies(diagramId),
db.deleteDiagramAreas(diagramId),
db.deleteDiagramCustomTypes(diagramId),
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
@@ -283,22 +288,27 @@ export const ChartDBProvider: React.FC<
);
const addTables: ChartDBContext['addTables'] = useCallback(
async (tables: DBTable[], options = { updateHistory: true }) => {
setTables((currentTables) => [...currentTables, ...tables]);
async (tablesToAdd: DBTable[], options = { updateHistory: true }) => {
setTables((currentTables) => [...currentTables, ...tablesToAdd]);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
...tables.map((table) => db.addTable({ diagramId, table })),
...tablesToAdd.map((table) =>
db.addTable({ diagramId, table })
),
]);
events.emit({ action: 'add_tables', data: { tables } });
events.emit({
action: 'add_tables',
data: { tables: tablesToAdd },
});
if (options.updateHistory) {
addUndoAction({
action: 'addTables',
redoData: { tables },
undoData: { tableIds: tables.map((t) => t.id) },
redoData: { tables: tablesToAdd },
undoData: { tableIds: tablesToAdd.map((t) => t.id) },
});
resetRedoStack();
}
@@ -335,12 +345,17 @@ export const ChartDBProvider: React.FC<
},
],
indexes: [],
color: randomColor(),
color: attributes?.isView ? viewColor : defaultTableColor,
createdAt: Date.now(),
isView: false,
order: tables.length,
...attributes,
};
table.indexes = getTableIndexesWithPrimaryKey({
table,
});
await addTable(table);
return table;
@@ -632,17 +647,30 @@ export const ChartDBProvider: React.FC<
options = { updateHistory: true }
) => {
const prevField = getField(tableId, fieldId);
const updateTableFn = (table: DBTable) => {
const updatedTable: DBTable = {
...table,
fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f
),
} satisfies DBTable;
updatedTable.indexes = getTableIndexesWithPrimaryKey({
table: updatedTable,
});
return updatedTable;
};
setTables((tables) =>
tables.map((table) =>
table.id === tableId
? {
...table,
fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f
),
}
: table
)
tables.map((table) => {
if (table.id === tableId) {
return updateTableFn(table);
}
return table;
})
);
const table = await db.getTable({ diagramId, id: tableId });
@@ -657,10 +685,7 @@ export const ChartDBProvider: React.FC<
db.updateTable({
id: tableId,
attributes: {
...table,
fields: table.fields.map((f) =>
f.id === fieldId ? { ...f, ...field } : f
),
...updateTableFn(table),
},
}),
]);
@@ -687,19 +712,29 @@ export const ChartDBProvider: React.FC<
fieldId: string,
options = { updateHistory: true }
) => {
const updateTableFn = (table: DBTable) => {
const updatedTable: DBTable = {
...table,
fields: table.fields.filter((f) => f.id !== fieldId),
} satisfies DBTable;
updatedTable.indexes = getTableIndexesWithPrimaryKey({
table: updatedTable,
});
return updatedTable;
};
const fields = getTable(tableId)?.fields ?? [];
const prevField = getField(tableId, fieldId);
setTables((tables) =>
tables.map((table) =>
table.id === tableId
? {
...table,
fields: table.fields.filter(
(f) => f.id !== fieldId
),
}
: table
)
tables.map((table) => {
if (table.id === tableId) {
return updateTableFn(table);
}
return table;
})
);
events.emit({
@@ -723,8 +758,7 @@ export const ChartDBProvider: React.FC<
db.updateTable({
id: tableId,
attributes: {
...table,
fields: table.fields.filter((f) => f.id !== fieldId),
...updateTableFn(table),
},
}),
]);
@@ -757,13 +791,23 @@ export const ChartDBProvider: React.FC<
options = { updateHistory: true }
) => {
const fields = getTable(tableId)?.fields ?? [];
setTables((tables) =>
tables.map((table) =>
table.id === tableId
? { ...table, fields: [...table.fields, field] }
: table
)
);
setTables((tables) => {
return tables.map((table) => {
if (table.id === tableId) {
db.updateTable({
id: tableId,
attributes: {
...table,
fields: [...table.fields, field],
},
});
return { ...table, fields: [...table.fields, field] };
}
return table;
});
});
events.emit({
action: 'add_field',
@@ -784,13 +828,6 @@ export const ChartDBProvider: React.FC<
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.updateTable({
id: tableId,
attributes: {
...table,
fields: [...table.fields, field],
},
}),
]);
if (options.updateHistory) {
@@ -1077,12 +1114,15 @@ export const ChartDBProvider: React.FC<
const sourceFieldName = sourceField?.name ?? '';
const targetTable = getTable(targetTableId);
const targetTableSchema = targetTable?.schema;
const relationship: DBRelationship = {
id: generateId(),
name: `${sourceTableName}_${sourceFieldName}_fk`,
sourceSchema: sourceTable?.schema,
sourceTableId,
targetSchema: sourceTable?.schema,
targetSchema: targetTableSchema,
targetTableId,
sourceFieldId,
targetFieldId,
@@ -1363,20 +1403,161 @@ export const ChartDBProvider: React.FC<
]
);
// Area operations
const addAreas: ChartDBContext['addAreas'] = useCallback(
async (areas: Area[], options = { updateHistory: true }) => {
setAreas((currentAreas) => [...currentAreas, ...areas]);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
...areas.map((area) => db.addArea({ diagramId, area })),
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
]);
if (options.updateHistory) {
addUndoAction({
action: 'addAreas',
redoData: { areas },
undoData: { areaIds: areas.map((a) => a.id) },
});
resetRedoStack();
}
},
[db, diagramId, setAreas, addUndoAction, resetRedoStack]
);
const addArea: ChartDBContext['addArea'] = useCallback(
async (area: Area, options = { updateHistory: true }) => {
return addAreas([area], options);
},
[addAreas]
);
const createArea: ChartDBContext['createArea'] = useCallback(
async (attributes) => {
const area: Area = {
id: generateId(),
name: `Area ${areas.length + 1}`,
x: 0,
y: 0,
width: 300,
height: 200,
color: defaultAreaColor,
...attributes,
};
await addArea(area);
return area;
},
[areas, addArea]
);
const getArea: ChartDBContext['getArea'] = useCallback(
(id: string) => areas.find((area) => area.id === id) ?? null,
[areas]
);
const removeAreas: ChartDBContext['removeAreas'] = useCallback(
async (ids: string[], options = { updateHistory: true }) => {
const prevAreas = [
...areas.filter((area) => ids.includes(area.id)),
];
setAreas((areas) => areas.filter((area) => !ids.includes(area.id)));
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
...ids.map((id) => db.deleteArea({ diagramId, id })),
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
]);
if (prevAreas.length > 0 && options.updateHistory) {
addUndoAction({
action: 'removeAreas',
redoData: { areaIds: ids },
undoData: { areas: prevAreas },
});
resetRedoStack();
}
},
[db, diagramId, setAreas, areas, addUndoAction, resetRedoStack]
);
const removeArea: ChartDBContext['removeArea'] = useCallback(
async (id: string, options = { updateHistory: true }) => {
return removeAreas([id], options);
},
[removeAreas]
);
const updateArea: ChartDBContext['updateArea'] = useCallback(
async (
id: string,
area: Partial<Area>,
options = { updateHistory: true }
) => {
const prevArea = getArea(id);
setAreas((areas) =>
areas.map((a) => (a.id === id ? { ...a, ...area } : a))
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.updateArea({ id, attributes: area }),
]);
if (!!prevArea && options.updateHistory) {
addUndoAction({
action: 'updateArea',
redoData: { areaId: id, area },
undoData: { areaId: id, area: prevArea },
});
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'] =
useCallback(
async (diagram) => {
(diagram) => {
setDiagramId(diagram.id);
setDiagramName(diagram.name);
setDatabaseType(diagram.databaseType);
setDatabaseEdition(diagram.databaseEdition);
setTables(diagram?.tables ?? []);
setRelationships(diagram?.relationships ?? []);
setDependencies(diagram?.dependencies ?? []);
setTables(diagram.tables ?? []);
setRelationships(diagram.relationships ?? []);
setDependencies(diagram.dependencies ?? []);
setAreas(diagram.areas ?? []);
setCustomTypes(diagram.customTypes ?? []);
setDiagramCreatedAt(diagram.createdAt);
setDiagramUpdatedAt(diagram.updatedAt);
setHighlightedCustomTypeId(undefined);
events.emit({ action: 'load_diagram', data: { diagram } });
resetRedoStack();
resetUndoStack();
},
[
setDiagramId,
@@ -1386,18 +1567,35 @@ export const ChartDBProvider: React.FC<
setTables,
setRelationships,
setDependencies,
setAreas,
setCustomTypes,
setDiagramCreatedAt,
setDiagramUpdatedAt,
setHighlightedCustomTypeId,
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(
async (diagramId: string) => {
const diagram = await db.getDiagram(diagramId, {
includeRelationships: true,
includeTables: true,
includeDependencies: true,
includeAreas: true,
includeCustomTypes: true,
});
if (diagram) {
@@ -1409,6 +1607,150 @@ export const ChartDBProvider: React.FC<
[db, loadDiagramFromData]
);
// Custom type operations
const getCustomType: ChartDBContext['getCustomType'] = useCallback(
(id: string) => customTypes.find((type) => type.id === id) ?? null,
[customTypes]
);
const addCustomTypes: ChartDBContext['addCustomTypes'] = useCallback(
async (
customTypes: DBCustomType[],
options = { updateHistory: true }
) => {
setCustomTypes((currentTypes) => [...currentTypes, ...customTypes]);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
...customTypes.map((customType) =>
db.addCustomType({ diagramId, customType })
),
]);
if (options.updateHistory) {
addUndoAction({
action: 'addCustomTypes',
redoData: { customTypes },
undoData: { customTypeIds: customTypes.map((t) => t.id) },
});
resetRedoStack();
}
},
[db, diagramId, setCustomTypes, addUndoAction, resetRedoStack]
);
const addCustomType: ChartDBContext['addCustomType'] = useCallback(
async (customType: DBCustomType, options = { updateHistory: true }) => {
return addCustomTypes([customType], options);
},
[addCustomTypes]
);
const createCustomType: ChartDBContext['createCustomType'] = useCallback(
async (attributes) => {
const customType: DBCustomType = {
id: generateId(),
name: `type_${customTypes.length + 1}`,
kind: DBCustomTypeKind.enum,
values: [],
fields: [],
...attributes,
};
await addCustomType(customType);
return customType;
},
[addCustomType, customTypes]
);
const removeCustomTypes: ChartDBContext['removeCustomTypes'] = useCallback(
async (ids, options = { updateHistory: true }) => {
const typesToRemove = ids
.map((id) => getCustomType(id))
.filter(Boolean) as DBCustomType[];
setCustomTypes((types) =>
types.filter((type) => !ids.includes(type.id))
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
...ids.map((id) => db.deleteCustomType({ diagramId, id })),
]);
if (typesToRemove.length > 0 && options.updateHistory) {
addUndoAction({
action: 'removeCustomTypes',
redoData: {
customTypeIds: ids,
},
undoData: {
customTypes: typesToRemove,
},
});
resetRedoStack();
}
},
[
db,
diagramId,
setCustomTypes,
addUndoAction,
resetRedoStack,
getCustomType,
]
);
const removeCustomType: ChartDBContext['removeCustomType'] = useCallback(
async (id: string, options = { updateHistory: true }) => {
return removeCustomTypes([id], options);
},
[removeCustomTypes]
);
const updateCustomType: ChartDBContext['updateCustomType'] = useCallback(
async (
id: string,
customType: Partial<DBCustomType>,
options = { updateHistory: true }
) => {
const prevCustomType = getCustomType(id);
setCustomTypes((types) =>
types.map((t) => (t.id === id ? { ...t, ...customType } : t))
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.updateCustomType({ id, attributes: customType }),
]);
if (!!prevCustomType && options.updateHistory) {
addUndoAction({
action: 'updateCustomType',
redoData: { customTypeId: id, customType },
undoData: { customTypeId: id, customType: prevCustomType },
});
resetRedoStack();
}
},
[
db,
setCustomTypes,
addUndoAction,
resetRedoStack,
getCustomType,
diagramId,
]
);
return (
<chartDBContext.Provider
value={{
@@ -1418,12 +1760,12 @@ export const ChartDBProvider: React.FC<
tables,
relationships,
dependencies,
areas,
currentDiagram,
schemas,
filteredSchemas,
events,
readonly,
filterSchemas,
updateDiagramData,
updateDiagramId,
updateDiagramName,
loadDiagram,
@@ -1465,6 +1807,23 @@ export const ChartDBProvider: React.FC<
removeDependency,
removeDependencies,
updateDependency,
createArea,
addArea,
addAreas,
getArea,
removeArea,
removeAreas,
updateArea,
customTypes,
createCustomType,
addCustomType,
addCustomTypes,
getCustomType,
removeCustomType,
removeCustomTypes,
updateCustomType,
highlightCustomTypeId,
highlightedCustomType,
}}
>
{children}

View File

@@ -4,7 +4,10 @@ import type { ChartDBConfig } from '@/lib/domain/config';
export interface ConfigContext {
config?: ChartDBConfig;
updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>;
updateConfig: (params: {
config?: Partial<ChartDBConfig>;
updateFn?: (config: ChartDBConfig) => ChartDBConfig;
}) => Promise<void>;
}
export const ConfigContext = createContext<ConfigContext>({

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { ConfigContext } from './config-context';
import { useStorage } from '@/hooks/use-storage';
@@ -8,7 +8,7 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { getConfig, updateConfig: updateDataConfig } = useStorage();
const [config, setConfig] = React.useState<ChartDBConfig | undefined>();
const [config, setConfig] = useState<ChartDBConfig | undefined>();
useEffect(() => {
const loadConfig = async () => {
@@ -19,19 +19,38 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
loadConfig();
}, [getConfig]);
const updateConfig: ConfigContext['updateConfig'] = async (
config: Partial<ChartDBConfig>
) => {
await updateDataConfig(config);
setConfig((prevConfig) =>
prevConfig
? { ...prevConfig, ...config }
: { ...{ defaultDiagramId: '' }, ...config }
);
const updateConfig: ConfigContext['updateConfig'] = async ({
config,
updateFn,
}) => {
const promise = new Promise<void>((resolve) => {
setConfig((prevConfig) => {
let baseConfig: ChartDBConfig = { defaultDiagramId: '' };
if (prevConfig) {
baseConfig = prevConfig;
}
const updatedConfig = updateFn
? updateFn(baseConfig)
: { ...baseConfig, ...config };
updateDataConfig(updatedConfig).then(() => {
resolve();
});
return updatedConfig;
});
});
return promise;
};
return (
<ConfigContext.Provider value={{ config, updateConfig }}>
<ConfigContext.Provider
value={{
config,
updateConfig,
}}
>
{children}
</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

@@ -9,10 +9,13 @@ import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/i
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
export interface DialogContext {
// Create diagram dialog
openCreateDiagramDialog: () => void;
openCreateDiagramDialog: (
params?: Omit<CreateDiagramDialogProps, 'dialog'>
) => void;
closeCreateDiagramDialog: () => void;
// Open diagram dialog

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react';
import type { DialogContext } from './dialog-context';
import { dialogContext } from './dialog-context';
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
@@ -26,6 +27,17 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
const [newDiagramDialogParams, setNewDiagramDialogParams] =
useState<Omit<CreateDiagramDialogProps, 'dialog'>>();
const openNewDiagramDialogHandler: DialogContext['openCreateDiagramDialog'] =
useCallback(
(props) => {
setNewDiagramDialogParams(props);
setOpenNewDiagramDialog(true);
},
[setOpenNewDiagramDialog]
);
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
const [openDiagramDialogParams, setOpenDiagramDialogParams] =
useState<Omit<OpenDiagramDialogProps, 'dialog'>>();
@@ -128,7 +140,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
return (
<dialogContext.Provider
value={{
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
openCreateDiagramDialog: openNewDiagramDialogHandler,
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
openOpenDiagramDialog: openOpenDiagramDialogHandler,
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
@@ -161,7 +173,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
}}
>
{children}
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
<CreateDiagramDialog
dialog={{ open: openNewDiagramDialog }}
{...newDiagramDialogParams}
/>
<OpenDiagramDialog
dialog={{ open: openOpenDiagramDialog }}
{...openDiagramDialogParams}

View File

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

View File

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

View File

@@ -3,7 +3,14 @@ import { emptyFn } from '@/lib/utils';
export type ImageType = 'png' | 'jpeg' | 'svg';
export interface ExportImageContext {
exportImage: (type: ImageType, scale: number) => Promise<void>;
exportImage: (
type: ImageType,
options: {
includePatternBG: boolean;
transparent: boolean;
scale: number;
}
) => Promise<void>;
}
export const exportImageContext = createContext<ExportImageContext>({

View File

@@ -8,6 +8,7 @@ import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
import { useTheme } from '@/hooks/use-theme';
import logoDark from '@/assets/logo-dark.png';
import logoLight from '@/assets/logo-light.png';
import type { EffectiveTheme } from '../theme-context/theme-context';
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -57,8 +58,16 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
[]
);
const getBackgroundColor = useCallback(
(theme: EffectiveTheme, transparent: boolean): string => {
if (transparent) return 'transparent';
return theme === 'light' ? '#ffffff' : '#141414';
},
[]
);
const exportImage: ExportImageContext['exportImage'] = useCallback(
async (type, scale = 1) => {
async (type, { includePatternBG, transparent, scale }) => {
showLoader({
animated: false,
});
@@ -114,34 +123,37 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
defs.innerHTML = markerDefs.innerHTML;
}
const pattern = document.createElementNS(
'http://www.w3.org/2000/svg',
'pattern'
);
pattern.setAttribute('id', 'background-pattern');
pattern.setAttribute('width', String(16 * viewport.zoom));
pattern.setAttribute('height', String(16 * viewport.zoom));
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
pattern.setAttribute(
'patternTransform',
`translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
);
if (includePatternBG) {
const pattern = document.createElementNS(
'http://www.w3.org/2000/svg',
'pattern'
);
pattern.setAttribute('id', 'background-pattern');
pattern.setAttribute('width', String(16 * viewport.zoom));
pattern.setAttribute('height', String(16 * viewport.zoom));
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
pattern.setAttribute(
'patternTransform',
`translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
);
const dot = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
const dot = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
const dotSize = viewport.zoom * 0.5;
dot.setAttribute('cx', String(viewport.zoom));
dot.setAttribute('cy', String(viewport.zoom));
dot.setAttribute('r', String(dotSize));
const dotColor =
effectiveTheme === 'light' ? '#92939C' : '#777777';
dot.setAttribute('fill', dotColor);
const dotSize = viewport.zoom * 0.5;
dot.setAttribute('cx', String(viewport.zoom));
dot.setAttribute('cy', String(viewport.zoom));
dot.setAttribute('r', String(dotSize));
const dotColor =
effectiveTheme === 'light' ? '#92939C' : '#777777';
dot.setAttribute('fill', dotColor);
pattern.appendChild(dot);
defs.appendChild(pattern);
}
pattern.appendChild(dot);
defs.appendChild(pattern);
tempSvg.appendChild(defs);
const backgroundRect = document.createElementNS(
@@ -196,10 +208,10 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
const initialDataUrl = await imageCreateFn(
viewportElement,
{
backgroundColor:
effectiveTheme === 'light'
? '#ffffff'
: '#141414',
backgroundColor: getBackgroundColor(
effectiveTheme,
transparent
),
width: reactFlowBounds.width,
height: reactFlowBounds.height,
style: {
@@ -285,6 +297,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
}, 0);
},
[
getBackgroundColor,
downloadImage,
getViewport,
hideLoader,

View File

@@ -33,6 +33,12 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
removeIndex,
updateIndex,
removeRelationships,
addAreas,
removeAreas,
updateArea,
addCustomTypes,
removeCustomTypes,
updateCustomType,
} = useChartDB();
const redoActionHandlers = useMemo(
@@ -107,6 +113,28 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateHistory: false,
});
},
addAreas: ({ redoData: { areas } }) => {
return addAreas(areas, { updateHistory: false });
},
removeAreas: ({ redoData: { areaIds } }) => {
return removeAreas(areaIds, { updateHistory: false });
},
updateArea: ({ redoData: { areaId, area } }) => {
return updateArea(areaId, area, { updateHistory: false });
},
addCustomTypes: ({ redoData: { customTypes } }) => {
return addCustomTypes(customTypes, { updateHistory: false });
},
removeCustomTypes: ({ redoData: { customTypeIds } }) => {
return removeCustomTypes(customTypeIds, {
updateHistory: false,
});
},
updateCustomType: ({ redoData: { customTypeId, customType } }) => {
return updateCustomType(customTypeId, customType, {
updateHistory: false,
});
},
}),
[
addTables,
@@ -126,6 +154,12 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
addDependencies,
removeDependencies,
updateDependency,
addAreas,
removeAreas,
updateArea,
addCustomTypes,
removeCustomTypes,
updateCustomType,
]
);
@@ -215,6 +249,28 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateHistory: false,
});
},
addAreas: ({ undoData: { areaIds } }) => {
return removeAreas(areaIds, { updateHistory: false });
},
removeAreas: ({ undoData: { areas } }) => {
return addAreas(areas, { updateHistory: false });
},
updateArea: ({ undoData: { areaId, area } }) => {
return updateArea(areaId, area, { updateHistory: false });
},
addCustomTypes: ({ undoData: { customTypeIds } }) => {
return removeCustomTypes(customTypeIds, {
updateHistory: false,
});
},
removeCustomTypes: ({ undoData: { customTypes } }) => {
return addCustomTypes(customTypes, { updateHistory: false });
},
updateCustomType: ({ undoData: { customTypeId, customType } }) => {
return updateCustomType(customTypeId, customType, {
updateHistory: false,
});
},
}),
[
addTables,
@@ -234,6 +290,12 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
addDependencies,
removeDependencies,
updateDependency,
addAreas,
removeAreas,
updateArea,
addCustomTypes,
removeCustomTypes,
updateCustomType,
]
);

View File

@@ -4,6 +4,8 @@ import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
type Action = keyof ChartDBContext;
@@ -123,6 +125,42 @@ type RedoUndoActionRemoveDependencies = RedoUndoActionBase<
{ dependencies: DBDependency[] }
>;
type RedoUndoActionAddAreas = RedoUndoActionBase<
'addAreas',
{ areas: Area[] },
{ areaIds: string[] }
>;
type RedoUndoActionUpdateArea = RedoUndoActionBase<
'updateArea',
{ areaId: string; area: Partial<Area> },
{ areaId: string; area: Partial<Area> }
>;
type RedoUndoActionRemoveAreas = RedoUndoActionBase<
'removeAreas',
{ areaIds: string[] },
{ areas: Area[] }
>;
type RedoUndoActionAddCustomTypes = RedoUndoActionBase<
'addCustomTypes',
{ customTypes: DBCustomType[] },
{ customTypeIds: string[] }
>;
type RedoUndoActionUpdateCustomType = RedoUndoActionBase<
'updateCustomType',
{ customTypeId: string; customType: Partial<DBCustomType> },
{ customTypeId: string; customType: Partial<DBCustomType> }
>;
type RedoUndoActionRemoveCustomTypes = RedoUndoActionBase<
'removeCustomTypes',
{ customTypeIds: string[] },
{ customTypes: DBCustomType[] }
>;
export type RedoUndoAction =
| RedoUndoActionAddTables
| RedoUndoActionRemoveTables
@@ -140,7 +178,13 @@ export type RedoUndoAction =
| RedoUndoActionRemoveRelationships
| RedoUndoActionAddDependencies
| RedoUndoActionUpdateDependency
| RedoUndoActionRemoveDependencies;
| RedoUndoActionRemoveDependencies
| RedoUndoActionAddAreas
| RedoUndoActionUpdateArea
| RedoUndoActionRemoveAreas
| RedoUndoActionAddCustomTypes
| RedoUndoActionUpdateCustomType
| RedoUndoActionRemoveCustomTypes;
export type RedoActionData<T extends Action> = Extract<
RedoUndoAction,

View File

@@ -8,6 +8,7 @@ export enum KeyboardShortcutAction {
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
SHOW_ALL = 'show_all',
TOGGLE_THEME = 'toggle_theme',
TOGGLE_FILTER = 'toggle_filter',
}
export interface KeyboardShortcut {
@@ -71,6 +72,13 @@ export const keyboardShortcuts: Record<
keyCombinationMac: 'meta+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 {

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,18 +5,31 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { DBTable } from '@/lib/domain/db-table';
import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
export interface StorageContext {
// Config operations
getConfig: () => Promise<ChartDBConfig | undefined>;
updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>;
// Diagram filter operations
getDiagramFilter: (diagramId: string) => Promise<DiagramFilter | undefined>;
updateDiagramFilter: (
diagramId: string,
filter: DiagramFilter
) => Promise<void>;
deleteDiagramFilter: (diagramId: string) => Promise<void>;
// Diagram operations
addDiagram: (params: { diagram: Diagram }) => Promise<void>;
listDiagrams: (options?: {
includeTables?: boolean;
includeRelationships?: boolean;
includeDependencies?: boolean;
includeAreas?: boolean;
includeCustomTypes?: boolean;
}) => Promise<Diagram[]>;
getDiagram: (
id: string,
@@ -24,6 +37,8 @@ export interface StorageContext {
includeTables?: boolean;
includeRelationships?: boolean;
includeDependencies?: boolean;
includeAreas?: boolean;
includeCustomTypes?: boolean;
}
) => Promise<Diagram | undefined>;
updateDiagram: (params: {
@@ -86,12 +101,50 @@ export interface StorageContext {
}) => Promise<void>;
listDependencies: (diagramId: string) => Promise<DBDependency[]>;
deleteDiagramDependencies: (diagramId: string) => Promise<void>;
// Area operations
addArea: (params: { diagramId: string; area: Area }) => Promise<void>;
getArea: (params: {
diagramId: string;
id: string;
}) => Promise<Area | undefined>;
updateArea: (params: {
id: string;
attributes: Partial<Area>;
}) => Promise<void>;
deleteArea: (params: { diagramId: string; id: string }) => Promise<void>;
listAreas: (diagramId: string) => Promise<Area[]>;
deleteDiagramAreas: (diagramId: string) => Promise<void>;
// Custom type operations
addCustomType: (params: {
diagramId: string;
customType: DBCustomType;
}) => Promise<void>;
getCustomType: (params: {
diagramId: string;
id: string;
}) => Promise<DBCustomType | undefined>;
updateCustomType: (params: {
id: string;
attributes: Partial<DBCustomType>;
}) => Promise<void>;
deleteCustomType: (params: {
diagramId: string;
id: string;
}) => Promise<void>;
listCustomTypes: (diagramId: string) => Promise<DBCustomType[]>;
deleteDiagramCustomTypes: (diagramId: string) => Promise<void>;
}
export const storageInitialValue: StorageContext = {
getConfig: emptyFn,
updateConfig: emptyFn,
getDiagramFilter: emptyFn,
updateDiagramFilter: emptyFn,
deleteDiagramFilter: emptyFn,
addDiagram: emptyFn,
listDiagrams: emptyFn,
getDiagram: emptyFn,
@@ -119,6 +172,21 @@ export const storageInitialValue: StorageContext = {
deleteDependency: emptyFn,
listDependencies: emptyFn,
deleteDiagramDependencies: emptyFn,
addArea: emptyFn,
getArea: emptyFn,
updateArea: emptyFn,
deleteArea: emptyFn,
listAreas: emptyFn,
deleteDiagramAreas: emptyFn,
// Custom type operations
addCustomType: emptyFn,
getCustomType: emptyFn,
updateCustomType: emptyFn,
deleteCustomType: emptyFn,
listCustomTypes: emptyFn,
deleteDiagramCustomTypes: emptyFn,
};
export const storageContext =

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
import { createContext } from 'react';
import { emptyFn } from '@/lib/utils';
export type Theme = 'light' | 'dark' | 'system';
export type EffectiveTheme = Exclude<Theme, 'system'>;
import type { Theme, EffectiveTheme } from '@/lib/types';
export type { Theme, EffectiveTheme };
export interface ThemeContext {
theme: Theme;

View File

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

View File

@@ -1,4 +1,10 @@
import React, { Suspense, useCallback, useEffect, useState } from 'react';
import React, {
Suspense,
useCallback,
useEffect,
useState,
useRef,
} from 'react';
import { Button } from '@/components/button/button';
import {
DialogClose,
@@ -29,10 +35,68 @@ import type { OnChange } from '@monaco-editor/react';
import { useDebounce } from '@/hooks/use-debounce-v2';
import { InstructionsSection } from './instructions-section/instructions-section';
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 {
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 =
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
// Helper to detect if content is likely SQL DDL or JSON
const detectContentType = (content: string): 'query' | 'ddl' | null => {
if (!content || content.trim().length === 0) return null;
// Common SQL DDL keywords
const ddlKeywords = [
'CREATE TABLE',
'ALTER TABLE',
'DROP TABLE',
'CREATE INDEX',
'CREATE VIEW',
'CREATE PROCEDURE',
'CREATE FUNCTION',
'CREATE SCHEMA',
'CREATE DATABASE',
];
const upperContent = content.toUpperCase();
// Check for SQL DDL patterns
const hasDDLKeywords = ddlKeywords.some((keyword) =>
upperContent.includes(keyword)
);
if (hasDDLKeywords) return 'ddl';
// Check if it looks like JSON
try {
// Just check structure, don't need full parse for detection
if (
(content.trim().startsWith('{') && content.trim().endsWith('}')) ||
(content.trim().startsWith('[') && content.trim().endsWith(']'))
) {
return 'query';
}
} catch (error) {
// Not valid JSON, might be partial
console.error('Error detecting content type:', error);
}
// If we can't confidently detect, return null
return null;
};
export interface ImportDatabaseProps {
goBack?: () => void;
@@ -67,6 +131,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
}) => {
const { effectiveTheme } = useTheme();
const [errorMessage, setErrorMessage] = useState('');
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const pasteDisposableRef = useRef<IDisposable | null>(null);
const { t } = useTranslation();
const { isSm: isDesktop } = useBreakpoint('sm');
@@ -74,6 +140,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
const [isCheckingJson, setIsCheckingJson] = 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(() => {
setScriptResult('');
@@ -84,11 +155,33 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
// Check if the ddl is valid
useEffect(() => {
if (importMethod !== 'ddl') {
setSqlValidation(null);
setShowAutoFixButton(false);
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({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
@@ -134,6 +227,52 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
}
}, [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(() => {
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(() => {
editorRef.current
?.getAction('editor.action.formatDocument')
?.run();
}, 50);
}
}, []);
const handleInputChange: OnChange = useCallback(
(inputValue) => {
setScriptResult(inputValue ?? '');
@@ -151,29 +290,86 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
const handleCheckJson = useCallback(async () => {
setIsCheckingJson(true);
const fixedJson = await fixMetadataJson(scriptResult);
await waitFor(1000);
const fixedJson = fixMetadataJson(scriptResult);
if (isStringMetadataJson(fixedJson)) {
setScriptResult(fixedJson);
setErrorMessage('');
formatEditor();
} else {
setScriptResult(fixedJson);
setErrorMessage(errorScriptOutputMessage);
formatEditor();
}
setShowCheckJsonButton(false);
setIsCheckingJson(false);
}, [scriptResult, setScriptResult]);
}, [scriptResult, setScriptResult, formatEditor]);
useEffect(() => {
// Cleanup paste handler on unmount
return () => {
if (pasteDisposableRef.current) {
pasteDisposableRef.current.dispose();
pasteDisposableRef.current = null;
}
};
}, []);
const handleEditorDidMount = useCallback(
(editor: editor.IStandaloneCodeEditor) => {
editor.onDidPaste(() => {
setTimeout(() => {
editor.getAction('editor.action.formatDocument')?.run();
}, 0);
editorRef.current = editor;
// 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(() => {
@@ -214,7 +410,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
<div className="w-full text-center text-xs text-muted-foreground">
{importMethod === 'query'
? 'Smart Query Output'
: 'SQL DDL'}
: 'SQL Script'}
</div>
<div className="flex-1 overflow-hidden">
<Suspense fallback={<Spinner />}>
@@ -230,7 +426,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
: 'dbml-light'
}
options={{
formatOnPaste: true,
formatOnPaste: false, // Never format on paste - we handle it manually
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
@@ -259,31 +455,13 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
</Suspense>
</div>
{showCheckJsonButton || errorMessage ? (
<div className="mt-2 flex shrink-0 items-center gap-2">
{showCheckJsonButton ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleCheckJson}
disabled={isCheckingJson}
className="h-7"
>
{isCheckingJson ? (
<Spinner size="small" />
) : (
t(
'new_diagram_dialog.import_database.check_script_result'
)
)}
</Button>
) : (
<p className="text-xs text-red-700">
{errorMessage}
</p>
)}
</div>
{errorMessage || (importMethod === 'ddl' && sqlValidation) ? (
<SQLValidationStatus
validation={sqlValidation}
errorMessage={errorMessage}
isAutoFixing={isAutoFixing}
onErrorClick={handleErrorClick}
/>
) : null}
</div>
),
@@ -294,10 +472,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
effectiveTheme,
debouncedHandleInputChange,
handleEditorDidMount,
showCheckJsonButton,
isCheckingJson,
handleCheckJson,
t,
sqlValidation,
isAutoFixing,
handleErrorClick,
]
);
@@ -307,7 +484,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
{isDesktop ? (
<ResizablePanelGroup
direction={isDesktop ? 'horizontal' : 'vertical'}
className="min-h-[500px] md:min-h-fit"
className="min-h-[500px]"
>
<ResizablePanel
defaultSize={25}
@@ -368,13 +545,43 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
</DialogClose>
)}
{keepDialogAfterImport ? (
{showCheckJsonButton ? (
<Button
type="button"
variant="default"
onClick={handleCheckJson}
disabled={isCheckingJson}
>
{isCheckingJson ? (
<Spinner size="small" />
) : (
t(
'new_diagram_dialog.import_database.check_script_result'
)
)}
</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 ? (
<Button
type="button"
variant="default"
disabled={
scriptResult.trim().length === 0 ||
errorMessage.length > 0
errorMessage.length > 0 ||
isAutoFixing
}
onClick={handleImport}
>
@@ -386,9 +593,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
type="button"
variant="default"
disabled={
showCheckJsonButton ||
scriptResult.trim().length === 0 ||
errorMessage.length > 0
errorMessage.length > 0 ||
isAutoFixing
}
onClick={handleImport}
>
@@ -417,8 +624,14 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
errorMessage.length,
scriptResult,
showCheckJsonButton,
isCheckingJson,
handleCheckJson,
goBack,
t,
importMethod,
isAutoFixing,
showAutoFixButton,
handleAutoFix,
]);
return (

View File

@@ -21,6 +21,7 @@ import { DDLInstructions } from './instructions/ddl-instructions';
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
DatabaseType.CLICKHOUSE,
DatabaseType.ORACLE,
];
export interface InstructionsSectionProps {
@@ -151,7 +152,7 @@ export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
<Avatar className="size-4 rounded-none">
<Code size={16} />
</Avatar>
DDL
SQL Script
</ToggleGroupItem>
</ToggleGroup>
</div>

View File

@@ -91,6 +91,7 @@ const DDLInstructionsMap: Record<DatabaseType, DDLInstruction[]> = {
text: 'Open the exported SQL file, copy its contents, and paste them here.',
},
],
[DatabaseType.ORACLE]: [],
};
export interface DDLInstructionsProps {

View File

@@ -6,7 +6,7 @@ import { SSMSInfo } from './ssms-info/ssms-info';
import { useTranslation } from 'react-i18next';
import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
import type { DatabaseClient } from '@/lib/domain/database-clients';
import { minimizeQuery } from '@/lib/data/import-metadata/scripts/minimize-script';
import { minimizeQuery } from '@/lib/data/import-metadata/utils';
import {
databaseClientToLabelMap,
databaseTypeToClientsMap,

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 {
SELECT_DATABASE = 'SELECT_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 { CreateDiagramDialogStep } from './create-diagram-dialog-step';
import { ImportDatabase } from '../common/import-database/import-database';
import { SelectTables } from '../common/select-tables/select-tables';
import { useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
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 {}
@@ -42,6 +46,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
const { listDiagrams, addDiagram } = useStorage();
const [diagramNumber, setDiagramNumber] = useState<number>(1);
const navigate = useNavigate();
const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
const [isParsingMetadata, setIsParsingMetadata] = useState(false);
useEffect(() => {
setDatabaseEdition(undefined);
@@ -62,49 +68,72 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
setDatabaseEdition(undefined);
setScriptResult('');
setImportMethod('query');
setParsedMetadata(undefined);
}, [dialog.open]);
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
const importNewDiagram = useCallback(async () => {
let diagram: Diagram | undefined;
const importNewDiagram = useCallback(
async ({
selectedTables,
databaseMetadata,
}: {
selectedTables?: SelectedTable[];
databaseMetadata?: DatabaseMetadata;
} = {}) => {
let diagram: Diagram | undefined;
if (importMethod === 'ddl') {
diagram = await sqlImportToDiagram({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
if (importMethod === 'ddl') {
diagram = await sqlImportToDiagram({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
});
} else {
let metadata: DatabaseMetadata | undefined = databaseMetadata;
if (!metadata) {
metadata = loadDatabaseMetadata(scriptResult);
}
if (selectedTables && selectedTables.length > 0) {
metadata = filterMetadataByTables({
metadata,
selectedTables,
});
}
diagram = await loadFromDatabaseMetadata({
databaseType,
databaseMetadata: metadata,
diagramNumber,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
});
}
await addDiagram({ diagram });
await updateConfig({
config: { defaultDiagramId: diagram.id },
});
} else {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
diagram = await loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
diagramNumber,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
});
}
await addDiagram({ diagram });
await updateConfig({ defaultDiagramId: diagram.id });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
}, [
importMethod,
databaseType,
addDiagram,
databaseEdition,
closeCreateDiagramDialog,
navigate,
updateConfig,
scriptResult,
diagramNumber,
]);
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
},
[
importMethod,
databaseType,
addDiagram,
databaseEdition,
closeCreateDiagramDialog,
navigate,
updateConfig,
scriptResult,
diagramNumber,
]
);
const createEmptyDiagram = useCallback(async () => {
const diagram: Diagram = {
@@ -120,7 +149,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
};
await addDiagram({ diagram });
await updateConfig({ defaultDiagramId: diagram.id });
await updateConfig({ config: { defaultDiagramId: diagram.id } });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
setTimeout(
@@ -138,10 +167,56 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
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 (
<Dialog
{...dialog}
onOpenChange={(open) => {
// Don't allow closing while parsing metadata
if (isParsingMetadata) {
return;
}
if (!hasExistingDiagram) {
return;
}
@@ -154,6 +229,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
<DialogContent
className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
showClose={hasExistingDiagram}
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
<SelectDatabase
@@ -165,9 +242,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
}
/>
) : (
) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
<ImportDatabase
onImport={importNewDiagram}
onImport={importNewDiagramOrFilterTables}
onCreateEmptyDiagram={createEmptyDiagram}
databaseEdition={databaseEdition}
databaseType={databaseType}
@@ -180,8 +257,18 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
title={t('new_diagram_dialog.import_database.title')}
importMethod={importMethod}
setImportMethod={setImportMethod}
keepDialogAfterImport={true}
/>
)}
) : step === CreateDiagramDialogStep.SELECT_TABLES ? (
<SelectTables
isLoading={isParsingMetadata || !parsedMetadata}
databaseMetadata={parsedMetadata}
onImport={importNewDiagram}
onBack={() =>
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
}
/>
) : null}
</DialogContent>
</Dialog>
);

View File

@@ -20,6 +20,7 @@ const SUPPORTED_DB_TYPES: DatabaseType[] = [
DatabaseType.MARIADB,
DatabaseType.SQLITE,
DatabaseType.SQL_SERVER,
DatabaseType.ORACLE,
DatabaseType.COCKROACHDB,
DatabaseType.CLICKHOUSE,
];

View File

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

View File

@@ -16,11 +16,20 @@ import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next';
import type { ImageType } from '@/context/export-image-context/export-image-context';
import { useExportImage } from '@/hooks/use-export-image';
import { Checkbox } from '@/components/checkbox/checkbox';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/accordion/accordion';
export interface ExportImageDialogProps extends BaseDialogProps {
format: ImageType;
}
const DEFAULT_INCLUDE_PATTERN_BG = true;
const DEFAULT_TRANSPARENT = false;
const DEFAULT_SCALE = '2';
export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({
dialog,
@@ -28,17 +37,28 @@ export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({
}) => {
const { t } = useTranslation();
const [scale, setScale] = useState<string>(DEFAULT_SCALE);
const [includePatternBG, setIncludePatternBG] = useState<boolean>(
DEFAULT_INCLUDE_PATTERN_BG
);
const [transparent, setTransparent] =
useState<boolean>(DEFAULT_TRANSPARENT);
const { exportImage } = useExportImage();
useEffect(() => {
if (!dialog.open) return;
setScale(DEFAULT_SCALE);
setIncludePatternBG(DEFAULT_INCLUDE_PATTERN_BG);
setTransparent(DEFAULT_TRANSPARENT);
}, [dialog.open]);
const { closeExportImageDialog } = useDialog();
const handleExport = useCallback(() => {
exportImage(format, Number(scale));
}, [exportImage, format, scale]);
exportImage(format, {
transparent,
includePatternBG,
scale: Number(scale),
});
}, [exportImage, format, includePatternBG, transparent, scale]);
const scaleOptions: SelectBoxOption[] = useMemo(
() =>
@@ -65,15 +85,79 @@ export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({
{t('export_image_dialog.description')}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-1">
<div className="grid w-full items-center gap-4">
<SelectBox
options={scaleOptions}
multiple={false}
value={scale}
onChange={(value) => setScale(value as string)}
/>
</div>
<div className="flex flex-col gap-4 py-1">
<SelectBox
options={scaleOptions}
multiple={false}
value={scale}
onChange={(value) => setScale(value as string)}
/>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="settings" className="border-0">
<AccordionTrigger
className="py-1.5"
iconPosition="right"
>
{t('export_image_dialog.advanced_options')}
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col gap-3 py-2">
<div className="flex items-start gap-3">
<Checkbox
id="pattern-checkbox"
className="mt-1 data-[state=checked]:border-pink-600 data-[state=checked]:bg-pink-600 data-[state=checked]:text-white"
checked={includePatternBG}
onCheckedChange={(value) =>
setIncludePatternBG(
value as boolean
)
}
/>
<div className="flex flex-col">
<label
htmlFor="pattern-checkbox"
className="cursor-pointer font-medium"
>
{t(
'export_image_dialog.pattern'
)}
</label>
<span className="text-sm text-muted-foreground">
{t(
'export_image_dialog.pattern_description'
)}
</span>
</div>
</div>
<div className="flex items-start gap-3">
<Checkbox
id="transparent-checkbox"
className="mt-1 data-[state=checked]:border-pink-600 data-[state=checked]:bg-pink-600 data-[state=checked]:text-white"
checked={transparent}
onCheckedChange={(value) =>
setTransparent(value as boolean)
}
/>
<div className="flex flex-col">
<label
htmlFor="transparent-checkbox"
className="cursor-pointer font-medium"
>
{t(
'export_image_dialog.transparent'
)}
</label>
<span className="text-sm text-muted-foreground">
{t(
'export_image_dialog.transparent_description'
)}
</span>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild>

View File

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

View File

@@ -5,7 +5,7 @@ import React, {
Suspense,
useRef,
} from 'react';
import * as monaco from 'monaco-editor';
import type * as monaco from 'monaco-editor';
import { useDialog } from '@/hooks/use-dialog';
import {
Dialog,
@@ -23,53 +23,24 @@ import { useTranslation } from 'react-i18next';
import { Editor } from '@/components/code-snippet/code-snippet';
import { useTheme } from '@/hooks/use-theme';
import { AlertCircle } from 'lucide-react';
import { importDBMLToDiagram } from '@/lib/dbml-import';
import {
importDBMLToDiagram,
sanitizeDBML,
preprocessDBML,
} from '@/lib/dbml/dbml-import/dbml-import';
import { useChartDB } from '@/hooks/use-chartdb';
import { Parser } from '@dbml/core';
import { useCanvas } from '@/hooks/use-canvas';
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 { Spinner } from '@/components/spinner/spinner';
import { debounce } from '@/lib/utils';
interface DBMLError {
message: string;
line: number;
column: number;
}
function parseDBMLError(error: unknown): DBMLError | null {
try {
if (typeof error === 'string') {
const parsed = JSON.parse(error);
if (parsed.diags?.[0]) {
const diag = parsed.diags[0];
return {
message: diag.message,
line: diag.location.start.line,
column: diag.location.start.column,
};
}
} else if (error && typeof error === 'object' && 'diags' in error) {
const parsed = error as {
diags: Array<{
message: string;
location: { start: { line: number; column: number } };
}>;
};
if (parsed.diags?.[0]) {
return {
message: parsed.diags[0].message,
line: parsed.diags[0].location.start.line,
column: parsed.diags[0].location.start.column,
};
}
}
} catch (e) {
console.error('Error parsing DBML error:', e);
}
return null;
}
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
import {
clearErrorHighlight,
highlightErrorLine,
} from '@/components/code-snippet/dbml/utils';
export interface ImportDBMLDialogProps extends BaseDialogProps {
withCreateEmptyDiagram?: boolean;
@@ -145,39 +116,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
}
}, [reorder, reorderTables]);
const highlightErrorLine = useCallback((error: DBMLError) => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;
const decorations = [
{
range: new monaco.Range(
error.line,
1,
error.line,
model.getLineMaxColumn(error.line)
),
options: {
isWholeLine: true,
className: 'dbml-error-line',
glyphMarginClassName: 'dbml-error-glyph',
hoverMessage: { value: error.message },
overviewRuler: {
color: '#ff0000',
position: monaco.editor.OverviewRulerLane.Right,
darkColor: '#ff0000',
},
},
},
];
decorationsCollection.current?.set(decorations);
}, []);
const clearDecorations = useCallback(() => {
decorationsCollection.current?.clear();
clearErrorHighlight(decorationsCollection.current);
}, []);
const validateDBML = useCallback(
@@ -189,8 +129,10 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
if (!content.trim()) return;
try {
const preprocessedContent = preprocessDBML(content);
const sanitizedContent = sanitizeDBML(preprocessedContent);
const parser = new Parser();
parser.parse(content, 'dbml');
parser.parse(sanitizedContent, 'dbml');
} catch (e) {
const parsedError = parseDBMLError(e);
if (parsedError) {
@@ -198,7 +140,12 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
t('import_dbml_dialog.error.description') +
` (1 error found - in line ${parsedError.line})`
);
highlightErrorLine(parsedError);
highlightErrorLine({
error: parsedError,
model: editorRef.current?.getModel(),
editorDecorationsCollection:
decorationsCollection.current,
});
} else {
setErrorMessage(
e instanceof Error ? e.message : JSON.stringify(e)
@@ -206,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);
@@ -245,7 +192,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
const tableIdsToRemove = tables
.filter((table) =>
importedDiagram.tables?.some(
(t) =>
(t: DBTable) =>
t.name === table.name && t.schema === table.schema
)
)
@@ -254,19 +201,21 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
const relationshipIdsToRemove = relationships
.filter((relationship) => {
const sourceTable = tables.find(
(table) => table.id === relationship.sourceTableId
(table: DBTable) =>
table.id === relationship.sourceTableId
);
const targetTable = tables.find(
(table) => table.id === relationship.targetTableId
(table: DBTable) =>
table.id === relationship.targetTableId
);
if (!sourceTable || !targetTable) return true;
const replacementSourceTable = importedDiagram.tables?.find(
(table) =>
(table: DBTable) =>
table.name === sourceTable.name &&
table.schema === sourceTable.schema
);
const replacementTargetTable = importedDiagram.tables?.find(
(table) =>
(table: DBTable) =>
table.name === targetTable.name &&
table.schema === targetTable.schema
);
@@ -330,7 +279,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
}}
>
<DialogContent
className="flex h-[80vh] max-h-screen flex-col"
className="flex h-[80vh] max-h-screen w-full flex-col md:max-w-[900px]"
showClose
>
<DialogHeader>

View File

@@ -65,7 +65,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
const openDiagram = useCallback(
(diagramId: string) => {
if (diagramId) {
updateConfig({ defaultDiagramId: diagramId });
updateConfig({ config: { defaultDiagramId: diagramId } });
navigate(`/diagrams/${diagramId}`);
}
},

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 {
Dialog,
@@ -17,11 +17,23 @@ import type { DBSchema } from '@/lib/domain/db-schema';
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
import type { BaseDialogProps } from '../common/base-dialog-props';
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 {
table?: DBTable;
schemas: DBSchema[];
onConfirm: (schema: string) => void;
onConfirm: ({ schema }: { schema: DBSchema }) => void;
allowSchemaCreation?: boolean;
}
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
@@ -29,27 +41,73 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
table,
schemas,
onConfirm,
allowSchemaCreation = false,
}) => {
const { t } = useTranslation();
const [selectedSchema, setSelectedSchema] = React.useState<string>(
const { databaseType } = useChartDB();
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
table?.schema
? schemaNameToSchemaId(table.schema)
: (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(() => {
if (!dialog.open) return;
setSelectedSchema(
setSelectedSchemaId(
table?.schema
? schemaNameToSchemaId(table.schema)
: (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 handleConfirm = useCallback(() => {
onConfirm(selectedSchema);
}, [onConfirm, selectedSchema]);
if (isCreatingNew && newSchemaName.trim()) {
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(
() =>
@@ -60,6 +118,25 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
[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 (
<Dialog
{...dialog}
@@ -67,48 +144,106 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
if (!open) {
closeTableSchemaDialog();
}
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
}}
>
<DialogContent className="flex flex-col" showClose>
<DialogHeader>
<DialogTitle>
{table
? t('update_table_schema_dialog.title')
: t('new_table_schema_dialog.title')}
{!allowSchemaSelection && allowSchemaCreation
? t('create_table_schema_dialog.title')
: table
? t('update_table_schema_dialog.title')
: t('new_table_schema_dialog.title')}
</DialogTitle>
<DialogDescription>
{table
? t('update_table_schema_dialog.description', {
tableName: table.name,
})
: t('new_table_schema_dialog.description')}
{!allowSchemaSelection && allowSchemaCreation
? t('create_table_schema_dialog.description')
: table
? t('update_table_schema_dialog.description', {
tableName: table.name,
})
: t('new_table_schema_dialog.description')}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-1">
<div className="grid w-full items-center gap-4">
<SelectBox
options={schemaOptions}
multiple={false}
value={selectedSchema}
onChange={(value) =>
setSelectedSchema(value as string)
}
/>
{!isCreatingNew ? (
<SelectBox
options={schemaOptions}
multiple={false}
value={selectedSchemaId}
onChange={(value) =>
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>
<DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild>
<Button variant="secondary">
{table
? t('update_table_schema_dialog.cancel')
: t('new_table_schema_dialog.cancel')}
{isCreatingNew
? t('create_table_schema_dialog.cancel')
: table
? t('update_table_schema_dialog.cancel')
: t('new_table_schema_dialog.cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={handleConfirm}>
{table
? t('update_table_schema_dialog.confirm')
: t('new_table_schema_dialog.confirm')}
<Button
onClick={handleConfirm}
disabled={isCreatingNew && !newSchemaName.trim()}
>
{isCreatingNew
? t('create_table_schema_dialog.create')
: table
? t('update_table_schema_dialog.confirm')
: t('new_table_schema_dialog.confirm')}
</Button>
</DialogClose>
</DialogFooter>

View File

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

View File

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

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ar: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'جديد',
browse: 'تصفح',
tables: 'الجداول',
refs: 'المراجع',
areas: 'المناطق',
dependencies: 'التبعيات',
custom_types: 'الأنواع المخصصة',
},
menu: {
file: {
file: 'ملف',
new: 'جديد',
open: 'فتح',
databases: {
databases: 'قواعد البيانات',
new: 'مخطط جديد',
browse: 'تصفح...',
save: 'حفظ',
import: 'استيراد قاعدة بيانات',
export_sql: 'SQL تصدير',
export_as: 'تصدير كـ',
delete_diagram: 'حذف الرسم البياني',
exit: 'خروج',
},
edit: {
edit: 'تحرير',
@@ -26,7 +34,10 @@ export const ar: LanguageTranslation = {
hide_sidebar: 'إخفاء الشريط الجانبي',
hide_cardinality: 'إخفاء الكاردينالية',
show_cardinality: 'إظهار الكاردينالية',
hide_field_attributes: 'إخفاء خصائص الحقل',
show_field_attributes: 'إظهار خصائص الحقل',
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
show_views: 'عروض قاعدة البيانات',
theme: 'المظهر',
show_dependencies: 'إظهار الاعتمادات',
hide_dependencies: 'إخفاء الاعتمادات',
@@ -70,15 +81,6 @@ export const ar: LanguageTranslation = {
cancel: 'إلغاء',
},
multiple_schemas_alert: {
title: 'مخططات متعددة',
description:
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
dont_show_again: 'لا تظهره مجدداً',
change_schema: 'تغيير',
none: 'لا شيء',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'فشل النسخ',
@@ -113,14 +115,11 @@ export const ar: LanguageTranslation = {
copied: '!تم النسخ',
side_panel: {
schema: ':المخطط',
filter_by_schema: 'تصفية حسب المخطط',
search_schema: '...بحث في المخطط',
no_schemas_found: '.لم يتم العثور على مخططات',
view_all_options: '...عرض جميع الخيارات',
tables_section: {
tables: 'الجداول',
add_table: 'إضافة جدول',
add_view: 'إضافة عرض',
filter: 'تصفية',
collapse: 'طي الكل',
// TODO: Translate
@@ -146,16 +145,22 @@ export const ar: LanguageTranslation = {
field_actions: {
title: 'خصائص الحقل',
unique: 'فريد',
auto_increment: 'زيادة تلقائية',
comments: 'تعليقات',
no_comments: 'لا يوجد تعليقات',
delete_field: 'حذف الحقل',
// TODO: Translate
character_length: 'Max Length',
precision: 'الدقة',
scale: 'النطاق',
default_value: 'Default Value',
no_default: 'No default',
},
index_actions: {
title: 'خصائص الفهرس',
name: 'الإسم',
unique: 'فريد',
index_type: 'نوع الفهرس',
delete_index: 'حذف الفهرس',
},
table_actions: {
@@ -172,12 +177,15 @@ export const ar: LanguageTranslation = {
description: 'أنشئ جدولاً للبدء',
},
},
relationships_section: {
relationships: 'العلاقات',
refs_section: {
refs: 'المراجع',
filter: 'تصفية',
add_relationship: 'إضافة علاقة',
collapse: 'طي الكل',
add_relationship: 'إضافة علاقة',
relationships: 'العلاقات',
dependencies: 'الاعتمادات',
relationship: {
relationship: 'العلاقة',
primary: 'الجدول الأساسي',
foreign: 'الجدول المرتبط',
cardinality: 'الكاردينالية',
@@ -187,16 +195,8 @@ export const ar: LanguageTranslation = {
delete_relationship: 'حذف',
},
},
empty_state: {
title: 'لا توجد علاقات',
description: 'إنشئ علاقة لربط الجداول',
},
},
dependencies_section: {
dependencies: 'الاعتمادات',
filter: 'تصفية',
collapse: 'طي الكل',
dependency: {
dependency: 'الاعتماد',
table: 'الجدول',
dependent_table: 'عرض الاعتمادات',
delete_dependency: 'حذف',
@@ -206,8 +206,59 @@ export const ar: LanguageTranslation = {
},
},
empty_state: {
title: 'لا توجد اعتمادات',
description: 'إنشاء اعتماد للبدء',
title: 'لا توجد علاقات',
description: 'إنشاء علاقة للبدء',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -221,6 +272,11 @@ export const ar: LanguageTranslation = {
redo: 'إعادة',
reorder_diagram: 'إعادة ترتيب الرسم البياني',
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: {
@@ -236,7 +292,7 @@ export const ar: LanguageTranslation = {
title: 'إسترد قاعدة بياناتك',
database_edition: ':إصدار قاعدة البيانات',
step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك',
step_2: ':إلصق نتيجة البرنامج النصي هنا',
step_2: ':إلصق نتيجة البرنامج النصي هنا',
script_results_placeholder: '...نتيجة البرنامج النصي هنا',
ssms_instructions: {
button_text: 'SSMS تعليمات',
@@ -330,6 +386,12 @@ export const ar: LanguageTranslation = {
scale_4x: '4x',
cancel: 'إلغاء',
export: 'تصدير',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -346,6 +408,13 @@ export const ar: LanguageTranslation = {
cancel: 'إلغاء',
confirm: 'تغيير',
},
create_table_schema_dialog: {
title: 'إنشاء مخطط جديد',
description:
'لا توجد مخططات حتى الآن. قم بإنشاء أول مخطط لتنظيم جداولك.',
create: 'إنشاء',
cancel: 'إلغاء',
},
star_us_dialog: {
title: '!ساعدنا على التحسن',
@@ -362,7 +431,7 @@ export const ar: LanguageTranslation = {
error: {
title: 'حدث خطأ أثناء التصدير',
description:
'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
'support@chartdb.io حدث خطأ ما. هل تحتاج إلى مساعدة؟',
},
},
import_diagram_dialog: {
@@ -373,7 +442,7 @@ export const ar: LanguageTranslation = {
error: {
title: 'حدث خطأ أثناء الاستيراد',
description:
'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
'support@chartdb.io و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
},
},
import_dbml_dialog: {
@@ -399,7 +468,10 @@ export const ar: LanguageTranslation = {
canvas_context_menu: {
new_table: 'جدول جديد',
new_view: 'عرض جديد',
new_relationship: 'علاقة جديدة',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -418,6 +490,8 @@ export const ar: LanguageTranslation = {
language_select: {
change_language: 'اللغة',
},
on: 'تشغيل',
off: 'إيقاف',
},
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const bn: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'নতুন',
browse: 'ব্রাউজ',
tables: 'টেবিল',
refs: 'রেফস',
areas: 'এলাকা',
dependencies: 'নির্ভরতা',
custom_types: 'কাস্টম টাইপ',
},
menu: {
file: {
file: 'ফাইল',
new: 'নতুন',
open: 'খুলুন',
databases: {
databases: 'ডাটাবেস',
new: 'নতুন ডায়াগ্রাম',
browse: 'ব্রাউজ করুন...',
save: 'সংরক্ষণ করুন',
import: 'ডাটাবেস আমদানি করুন',
export_sql: 'SQL রপ্তানি করুন',
export_as: 'রূপে রপ্তানি করুন',
delete_diagram: 'ডায়াগ্রাম মুছুন',
exit: 'প্রস্থান করুন',
},
edit: {
edit: 'সম্পাদনা',
@@ -26,7 +34,10 @@ export const bn: LanguageTranslation = {
hide_sidebar: 'সাইডবার লুকান',
hide_cardinality: 'কার্ডিনালিটি লুকান',
show_cardinality: 'কার্ডিনালিটি দেখান',
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
zoom_on_scroll: 'স্ক্রলে জুম করুন',
show_views: 'ডাটাবেস ভিউ',
theme: 'থিম',
show_dependencies: 'নির্ভরতাগুলি দেখান',
hide_dependencies: 'নির্ভরতাগুলি লুকান',
@@ -71,15 +82,6 @@ export const bn: LanguageTranslation = {
cancel: 'বাতিল করুন',
},
multiple_schemas_alert: {
title: 'বহু স্কিমা',
description:
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
dont_show_again: 'পুনরায় দেখাবেন না',
change_schema: 'পরিবর্তন করুন',
none: 'কিছুই না',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'কপি ব্যর্থ হয়েছে',
@@ -114,14 +116,11 @@ export const bn: LanguageTranslation = {
copied: 'অনুলিপি সম্পন্ন!',
side_panel: {
schema: 'স্কিমা:',
filter_by_schema: 'স্কিমা দ্বারা ফিল্টার করুন',
search_schema: 'স্কিমা খুঁজুন...',
no_schemas_found: 'কোনো স্কিমা পাওয়া যায়নি।',
view_all_options: 'সমস্ত বিকল্প দেখুন...',
tables_section: {
tables: 'টেবিল',
add_table: 'টেবিল যোগ করুন',
add_view: 'ভিউ যোগ করুন',
filter: 'ফিল্টার',
collapse: 'সব ভাঁজ করুন',
// TODO: Translate
@@ -147,16 +146,23 @@ export const bn: LanguageTranslation = {
field_actions: {
title: 'ফিল্ড কর্ম',
unique: 'অদ্বিতীয়',
auto_increment: 'স্বয়ংক্রিয় বৃদ্ধি',
comments: 'মন্তব্য',
no_comments: 'কোনো মন্তব্য নেই',
delete_field: 'ফিল্ড মুছুন',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'নির্ভুলতা',
scale: 'স্কেল',
},
index_actions: {
title: 'ইনডেক্স কর্ম',
name: 'নাম',
unique: 'অদ্বিতীয়',
index_type: 'ইনডেক্স ধরন',
delete_index: 'ইনডেক্স মুছুন',
},
table_actions: {
@@ -173,14 +179,17 @@ export const bn: LanguageTranslation = {
description: 'শুরু করতে একটি টেবিল তৈরি করুন',
},
},
relationships_section: {
relationships: 'সম্পর্ক',
refs_section: {
refs: 'রেফস',
filter: 'ফিল্টার',
add_relationship: 'সম্পর্ক যোগ করুন',
collapse: 'সব ভাঁজ করুন',
add_relationship: 'সম্পর্ক যোগ করুন',
relationships: 'সম্পর্ক',
dependencies: 'নির্ভরতাগুলি',
relationship: {
relationship: 'সম্পর্ক',
primary: 'প্রাথমিক টেবিল',
foreign: 'বিদেশি টেবিল',
foreign: 'রেফারেন্স করা টেবিল',
cardinality: 'কার্ডিনালিটি',
delete_relationship: 'মুছুন',
relationship_actions: {
@@ -188,27 +197,69 @@ export const bn: LanguageTranslation = {
delete_relationship: 'মুছুন',
},
},
empty_state: {
title: 'কোনো সম্পর্ক নেই',
description: 'টেবিল সংযোগ করতে একটি সম্পর্ক তৈরি করুন',
},
},
dependencies_section: {
dependencies: 'নির্ভরতাগুলি',
filter: 'ফিল্টার',
collapse: 'ভাঁজ করুন',
dependency: {
dependency: 'নির্ভরতা',
table: 'টেবিল',
dependent_table: 'নির্ভরশীল টেবিল',
delete_dependency: 'নির্ভরতা মুছুন',
dependent_table: 'নির্ভরশীল ভিউ',
delete_dependency: 'মুছুন',
dependency_actions: {
title: 'কর্ম',
delete_dependency: 'নির্ভরতা মুছুন',
delete_dependency: 'মুছুন',
},
},
empty_state: {
title: 'কোনো নির্ভরতাগুলি নেই',
description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।',
title: 'কোনো সম্পর্ক নেই',
description: 'শুরু করতে একটি সম্পর্ক তৈরি করুন',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -222,6 +273,12 @@ export const bn: LanguageTranslation = {
redo: 'পুনরায় করুন',
reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
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: {
@@ -237,7 +294,7 @@ export const bn: LanguageTranslation = {
title: 'আপনার ডাটাবেস আমদানি করুন',
database_edition: 'ডাটাবেস সংস্করণ:',
step_1: 'আপনার ডাটাবেসে এই স্ক্রিপ্ট চালান:',
step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন:',
step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন',
script_results_placeholder: 'স্ক্রিপ্টের ফলাফল এখানে...',
ssms_instructions: {
button_text: 'SSMS নির্দেশনা',
@@ -331,6 +388,12 @@ export const bn: LanguageTranslation = {
scale_4x: '4x',
cancel: 'বাতিল করুন',
export: 'রপ্তানি করুন',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -347,6 +410,13 @@ export const bn: LanguageTranslation = {
cancel: 'বাতিল করুন',
confirm: 'পরিবর্তন করুন',
},
create_table_schema_dialog: {
title: 'নতুন স্কিমা তৈরি করুন',
description:
'এখনও কোনো স্কিমা নেই। আপনার টেবিলগুলি সংগঠিত করতে আপনার প্রথম স্কিমা তৈরি করুন।',
create: 'তৈরি করুন',
cancel: 'বাতিল করুন',
},
star_us_dialog: {
title: 'আমাদের উন্নত করতে সাহায্য করুন!',
@@ -365,7 +435,7 @@ export const bn: LanguageTranslation = {
error: {
title: 'চিত্র রপ্তানিতে ত্রুটি',
description:
'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
},
},
@@ -377,7 +447,7 @@ export const bn: LanguageTranslation = {
error: {
title: 'চিত্র আমদানিতে ত্রুটি',
description:
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
},
},
// TODO: Translate
@@ -403,7 +473,10 @@ export const bn: LanguageTranslation = {
canvas_context_menu: {
new_table: 'নতুন টেবিল',
new_view: 'নতুন ভিউ',
new_relationship: 'নতুন সম্পর্ক',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -422,6 +495,9 @@ export const bn: LanguageTranslation = {
language_select: {
change_language: 'ভাষা পরিবর্তন করুন',
},
on: 'চালু',
off: 'বন্ধ',
},
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const de: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Neu',
browse: 'Durchsuchen',
tables: 'Tabellen',
refs: 'Refs',
areas: 'Bereiche',
dependencies: 'Abhängigkeiten',
custom_types: 'Benutzerdefinierte Typen',
},
menu: {
file: {
file: 'Datei',
new: 'Neu',
open: 'Öffnen',
databases: {
databases: 'Datenbanken',
new: 'Neues Diagramm',
browse: 'Durchsuchen...',
save: 'Speichern',
import: 'Datenbank importieren',
export_sql: 'SQL exportieren',
export_as: 'Exportieren als',
delete_diagram: 'Diagramm löschen',
exit: 'Beenden',
},
edit: {
edit: 'Bearbeiten',
@@ -26,7 +34,10 @@ export const de: LanguageTranslation = {
hide_sidebar: 'Seitenleiste ausblenden',
hide_cardinality: 'Kardinalität ausblenden',
show_cardinality: 'Kardinalität anzeigen',
hide_field_attributes: 'Feldattribute ausblenden',
show_field_attributes: 'Feldattribute anzeigen',
zoom_on_scroll: 'Zoom beim Scrollen',
show_views: 'Datenbankansichten',
theme: 'Stil',
show_dependencies: 'Abhängigkeiten anzeigen',
hide_dependencies: 'Abhängigkeiten ausblenden',
@@ -71,15 +82,6 @@ export const de: LanguageTranslation = {
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: {
unsupported: {
title: 'Kopieren fehlgeschlagen',
@@ -115,14 +117,11 @@ export const de: LanguageTranslation = {
copied: 'Kopiert!',
side_panel: {
schema: 'Schema:',
filter_by_schema: 'Nach Schema filtern',
search_schema: 'Schema suchen...',
no_schemas_found: 'Keine Schemas gefunden.',
view_all_options: 'Alle Optionen anzeigen...',
tables_section: {
tables: 'Tabellen',
add_table: 'Tabelle hinzufügen',
add_view: 'Ansicht hinzufügen',
filter: 'Filter',
collapse: 'Alle einklappen',
// TODO: Translate
@@ -148,16 +147,23 @@ export const de: LanguageTranslation = {
field_actions: {
title: 'Feldattribute',
unique: 'Eindeutig',
auto_increment: 'Automatisch hochzählen',
comments: 'Kommentare',
no_comments: 'Keine Kommentare',
delete_field: 'Feld löschen',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Präzision',
scale: 'Skalierung',
},
index_actions: {
title: 'Indexattribute',
name: 'Name',
unique: 'Eindeutig',
index_type: 'Indextyp',
delete_index: 'Index löschen',
},
table_actions: {
@@ -174,32 +180,26 @@ export const de: LanguageTranslation = {
description: 'Erstellen Sie eine Tabelle, um zu beginnen',
},
},
relationships_section: {
relationships: 'Beziehungen',
refs_section: {
refs: 'Refs',
filter: 'Filter',
add_relationship: 'Beziehung hinzufügen',
collapse: 'Alle einklappen',
add_relationship: 'Beziehung hinzufügen',
relationships: 'Beziehungen',
dependencies: 'Abhängigkeiten',
relationship: {
relationship: 'Beziehung',
primary: 'Primäre Tabelle',
foreign: 'Referenzierte Tabelle',
cardinality: 'Kardinalität',
delete_relationship: 'Beziehung löschen',
delete_relationship: 'Löschen',
relationship_actions: {
title: 'Aktionen',
delete_relationship: 'Beziehung löschen',
delete_relationship: 'Löschen',
},
},
empty_state: {
title: 'Keine Beziehungen',
description:
'Erstellen Sie eine Beziehung, um Tabellen zu verbinden',
},
},
dependencies_section: {
dependencies: 'Abhängigkeiten',
filter: 'Filter',
collapse: 'Alle einklappen',
dependency: {
dependency: 'Abhängigkeit',
table: 'Tabelle',
dependent_table: 'Abhängige Ansicht',
delete_dependency: 'Löschen',
@@ -209,8 +209,58 @@ export const de: LanguageTranslation = {
},
},
empty_state: {
title: 'Keine Abhängigkeiten',
description: 'Erstellen Sie eine Ansicht, um zu beginnen',
title: 'Keine Beziehungen',
description: 'Erstellen Sie eine Beziehung, um zu beginnen',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -223,7 +273,14 @@ export const de: LanguageTranslation = {
undo: 'Rückgängig',
redo: 'Wiederholen',
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',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -239,7 +296,7 @@ export const de: LanguageTranslation = {
title: 'Datenbank importieren',
database_edition: 'Datenbank Edition:',
step_1: 'Führen Sie dieses Skript in Ihrer Datenbank aus:',
step_2: 'Fügen Sie das Skriptergebnis hier ein:',
step_2: 'Fügen Sie das Skriptergebnis hier ein',
script_results_placeholder: 'Skriptergebnisse hier...',
ssms_instructions: {
button_text: 'SSMS Anweisungen',
@@ -334,6 +391,12 @@ export const de: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Abbrechen',
export: 'Exportieren',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -350,6 +413,13 @@ export const de: LanguageTranslation = {
cancel: 'Abbrechen',
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: {
title: 'Hilf uns, uns zu verbessern!',
@@ -368,7 +438,7 @@ export const de: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -380,7 +450,7 @@ export const de: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -406,7 +476,10 @@ export const de: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Neue Tabelle',
new_view: 'Neue Ansicht',
new_relationship: 'Neue Beziehung',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -426,6 +499,9 @@ export const de: LanguageTranslation = {
language_select: {
change_language: 'Sprache',
},
on: 'Ein',
off: 'Aus',
},
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata } from '../types';
export const en = {
translation: {
editor_sidebar: {
new_diagram: 'New',
browse: 'Browse',
tables: 'Tables',
refs: 'Refs',
areas: 'Areas',
dependencies: 'Dependencies',
custom_types: 'Custom Types',
},
menu: {
file: {
file: 'File',
new: 'New',
open: 'Open',
databases: {
databases: 'Databases',
new: 'New Diagram',
browse: 'Browse...',
save: 'Save',
import: 'Import',
export_sql: 'Export SQL',
export_as: 'Export as',
delete_diagram: 'Delete Diagram',
exit: 'Exit',
},
edit: {
edit: 'Edit',
@@ -26,7 +34,10 @@ export const en = {
hide_sidebar: 'Hide Sidebar',
hide_cardinality: 'Hide Cardinality',
show_cardinality: 'Show Cardinality',
hide_field_attributes: 'Hide Field Attributes',
show_field_attributes: 'Show Field Attributes',
zoom_on_scroll: 'Zoom on Scroll',
show_views: 'Database Views',
theme: 'Theme',
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
@@ -69,15 +80,6 @@ export const en = {
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: {
unsupported: {
title: 'Copy failed',
@@ -112,14 +114,11 @@ export const en = {
copied: 'Copied!',
side_panel: {
schema: 'Schema:',
filter_by_schema: 'Filter by schema',
search_schema: 'Search schema...',
no_schemas_found: 'No schemas found.',
view_all_options: 'View all Options...',
tables_section: {
tables: 'Tables',
add_table: 'Add Table',
add_view: 'Add View',
filter: 'Filter',
collapse: 'Collapse All',
clear: 'Clear Filter',
@@ -143,15 +142,21 @@ export const en = {
field_actions: {
title: 'Field Attributes',
unique: 'Unique',
auto_increment: 'Auto Increment',
character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
comments: 'Comments',
no_comments: 'No comments',
default_value: 'Default Value',
no_default: 'No default',
delete_field: 'Delete Field',
},
index_actions: {
title: 'Index Attributes',
name: 'Name',
unique: 'Unique',
index_type: 'Index Type',
delete_index: 'Delete Index',
},
table_actions: {
@@ -168,12 +173,15 @@ export const en = {
description: 'Create a table to get started',
},
},
relationships_section: {
relationships: 'Relationships',
refs_section: {
refs: 'Refs',
filter: 'Filter',
add_relationship: 'Add Relationship',
collapse: 'Collapse All',
add_relationship: 'Add Relationship',
relationships: 'Relationships',
dependencies: 'Dependencies',
relationship: {
relationship: 'Relationship',
primary: 'Primary Table',
foreign: 'Referenced Table',
cardinality: 'Cardinality',
@@ -183,16 +191,8 @@ export const en = {
delete_relationship: 'Delete',
},
},
empty_state: {
title: 'No relationships',
description: 'Create a relationship to connect tables',
},
},
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
dependency: 'Dependency',
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
@@ -202,8 +202,57 @@ export const en = {
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
title: 'No relationships',
description: 'Create a relationship to get started',
},
},
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
clear_field_highlight: 'Clear Highlight',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -217,6 +266,10 @@ export const en = {
redo: 'Redo',
reorder_diagram: 'Reorder Diagram',
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: {
@@ -232,7 +285,7 @@ export const en = {
title: 'Import your Database',
database_edition: 'Database Edition:',
step_1: 'Run this script in your database:',
step_2: 'Paste the script result into this modal.',
step_2: 'Paste the script result into this modal',
script_results_placeholder: 'Script results here...',
ssms_instructions: {
button_text: 'SSMS Instructions',
@@ -326,6 +379,11 @@ export const en = {
scale_4x: '4x',
cancel: 'Cancel',
export: 'Export',
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -343,6 +401,14 @@ export const en = {
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: {
title: 'Help us improve!',
description:
@@ -359,7 +425,7 @@ export const en = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -371,7 +437,7 @@ export const en = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
@@ -397,7 +463,9 @@ export const en = {
canvas_context_menu: {
new_table: 'New Table',
new_view: 'New View',
new_relationship: 'New Relationship',
new_area: 'New Area',
},
table_node_context_menu: {
@@ -416,6 +484,9 @@ export const en = {
language_select: {
change_language: 'Language',
},
on: 'On',
off: 'Off',
},
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const es: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Nuevo',
browse: 'Examinar',
tables: 'Tablas',
refs: 'Refs',
areas: 'Áreas',
dependencies: 'Dependencias',
custom_types: 'Tipos Personalizados',
},
menu: {
file: {
file: 'Archivo',
new: 'Nuevo',
open: 'Abrir',
databases: {
databases: 'Bases de Datos',
new: 'Nuevo Diagrama',
browse: 'Examinar...',
save: 'Guardar',
import: 'Importar Base de Datos',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Eliminar Diagrama',
exit: 'Salir',
},
edit: {
edit: 'Editar',
@@ -24,9 +32,12 @@ export const es: LanguageTranslation = {
view: 'Ver',
hide_cardinality: 'Ocultar Cardinalidad',
show_cardinality: 'Mostrar Cardinalidad',
show_field_attributes: 'Mostrar Atributos de Campo',
hide_field_attributes: 'Ocultar Atributos de Campo',
show_sidebar: 'Mostrar Barra Lateral',
hide_sidebar: 'Ocultar Barra Lateral',
zoom_on_scroll: 'Zoom al Desplazarse',
show_views: 'Vistas de Base de Datos',
theme: 'Tema',
show_dependencies: 'Mostrar dependencias',
hide_dependencies: 'Ocultar dependencias',
@@ -104,14 +115,11 @@ export const es: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'Esquema:',
filter_by_schema: 'Filtrar por esquema',
search_schema: 'Buscar esquema...',
no_schemas_found: 'No se encontraron esquemas.',
view_all_options: 'Ver todas las opciones...',
tables_section: {
tables: 'Tablas',
add_table: 'Agregar Tabla',
add_view: 'Agregar Vista',
filter: 'Filtrar',
collapse: 'Colapsar Todo',
// TODO: Translate
@@ -137,16 +145,23 @@ export const es: LanguageTranslation = {
field_actions: {
title: 'Atributos del Campo',
unique: 'Único',
auto_increment: 'Autoincremento',
comments: 'Comentarios',
no_comments: 'Sin comentarios',
delete_field: 'Eliminar Campo',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Precisión',
scale: 'Escala',
},
index_actions: {
title: 'Atributos del Índice',
name: 'Nombre',
unique: 'Único',
index_type: 'Tipo de Índice',
delete_index: 'Eliminar Índice',
},
table_actions: {
@@ -163,14 +178,17 @@ export const es: LanguageTranslation = {
description: 'Crea una tabla para comenzar',
},
},
relationships_section: {
relationships: 'Relaciones',
add_relationship: 'Agregar Relación',
refs_section: {
refs: 'Refs',
filter: 'Filtrar',
collapse: 'Colapsar Todo',
add_relationship: 'Agregar Relación',
relationships: 'Relaciones',
dependencies: 'Dependencias',
relationship: {
primary: 'Primaria',
foreign: 'Foránea',
relationship: 'Relación',
primary: 'Tabla Primaria',
foreign: 'Tabla Referenciada',
cardinality: 'Cardinalidad',
delete_relationship: 'Eliminar',
relationship_actions: {
@@ -178,18 +196,10 @@ export const es: LanguageTranslation = {
delete_relationship: 'Eliminar',
},
},
empty_state: {
title: 'No hay relaciones',
description: 'Crea una relación para conectar tablas',
},
},
dependencies_section: {
dependencies: 'Dependencias',
filter: 'Filtro',
collapse: 'Colapsar todo',
dependency: {
dependency: 'Dependencia',
table: 'Tabla',
dependent_table: 'Vista dependiente',
dependent_table: 'Vista Dependiente',
delete_dependency: 'Eliminar',
dependency_actions: {
title: 'Acciones',
@@ -197,8 +207,58 @@ export const es: LanguageTranslation = {
},
},
empty_state: {
title: 'Sin dependencias',
description: 'Crea una vista para comenzar',
title: 'Sin relaciones',
description: 'Crea una relación para comenzar',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -211,7 +271,13 @@ export const es: LanguageTranslation = {
undo: 'Deshacer',
redo: 'Rehacer',
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',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -227,7 +293,7 @@ export const es: LanguageTranslation = {
title: 'Importa tu Base de Datos',
database_edition: 'Edición de Base de Datos:',
step_1: 'Ejecuta este script en tu base de datos:',
step_2: 'Pega el resultado del script aquí:',
step_2: 'Pega el resultado del script aquí',
script_results_placeholder: 'Resultados del script aquí...',
ssms_instructions: {
button_text: 'Instrucciones SSMS',
@@ -323,6 +389,12 @@ export const es: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Cancelar',
export: 'Exportar',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -339,6 +411,13 @@ export const es: LanguageTranslation = {
cancel: 'Cancelar',
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: {
title: '¡Ayúdanos a mejorar!',
@@ -348,14 +427,6 @@ export const es: LanguageTranslation = {
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
export_diagram_dialog: {
title: 'Export Diagram',
@@ -366,7 +437,7 @@ export const es: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -378,7 +449,7 @@ export const es: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -404,7 +475,10 @@ export const es: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nueva Tabla',
new_view: 'Nueva Vista',
new_relationship: 'Nueva Relación',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -424,6 +498,9 @@ export const es: LanguageTranslation = {
language_select: {
change_language: 'Idioma',
},
on: 'Encendido',
off: 'Apagado',
},
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const fr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Nouveau',
browse: 'Parcourir',
tables: 'Tables',
refs: 'Refs',
areas: 'Zones',
dependencies: 'Dépendances',
custom_types: 'Types Personnalisés',
},
menu: {
file: {
file: 'Fichier',
new: 'Nouveau',
open: 'Ouvrir',
databases: {
databases: 'Bases de Données',
new: 'Nouveau Diagramme',
browse: 'Parcourir...',
save: 'Enregistrer',
import: 'Importer Base de Données',
export_sql: 'Exporter SQL',
export_as: 'Exporter en tant que',
delete_diagram: 'Supprimer le Diagramme',
exit: 'Quitter',
},
edit: {
edit: 'Édition',
@@ -26,7 +34,10 @@ export const fr: LanguageTranslation = {
hide_sidebar: 'Cacher la Barre Latérale',
hide_cardinality: 'Cacher 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',
show_views: 'Vues de Base de Données',
theme: 'Thème',
show_dependencies: 'Afficher les Dépendances',
hide_dependencies: 'Masquer les Dépendances',
@@ -103,14 +114,11 @@ export const fr: LanguageTranslation = {
copied: 'Copié !',
side_panel: {
schema: 'Schéma:',
filter_by_schema: 'Filtrer par schéma',
search_schema: 'Rechercher un schéma...',
no_schemas_found: 'Aucun schéma trouvé.',
view_all_options: 'Voir toutes les Options...',
tables_section: {
tables: 'Tables',
add_table: 'Ajouter une Table',
add_view: 'Ajouter une Vue',
filter: 'Filtrer',
collapse: 'Réduire Tout',
clear: 'Effacer le Filtre',
@@ -135,16 +143,23 @@ export const fr: LanguageTranslation = {
field_actions: {
title: 'Attributs du Champ',
unique: 'Unique',
auto_increment: 'Auto-incrément',
comments: 'Commentaires',
no_comments: 'Pas de commentaires',
delete_field: 'Supprimer le Champ',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Précision',
scale: 'Échelle',
},
index_actions: {
title: "Attributs de l'Index",
name: 'Nom',
unique: 'Unique',
index_type: "Type d'index",
delete_index: "Supprimer l'Index",
},
table_actions: {
@@ -161,12 +176,15 @@ export const fr: LanguageTranslation = {
description: 'Créez une table pour commencer',
},
},
relationships_section: {
relationships: 'Relations',
refs_section: {
refs: 'Refs',
filter: 'Filtrer',
add_relationship: 'Ajouter une Relation',
collapse: 'Réduire Tout',
add_relationship: 'Ajouter une Relation',
relationships: 'Relations',
dependencies: 'Dépendances',
relationship: {
relationship: 'Relation',
primary: 'Table Principale',
foreign: 'Table Référencée',
cardinality: 'Cardinalité',
@@ -176,16 +194,8 @@ export const fr: LanguageTranslation = {
delete_relationship: 'Supprimer',
},
},
empty_state: {
title: 'Aucune relation',
description: 'Créez une relation pour connecter les tables',
},
},
dependencies_section: {
dependencies: 'Dépendances',
filter: 'Filtrer',
collapse: 'Réduire Tout',
dependency: {
dependency: 'Dépendance',
table: 'Table',
dependent_table: 'Vue Dépendante',
delete_dependency: 'Supprimer',
@@ -195,8 +205,58 @@ export const fr: LanguageTranslation = {
},
},
empty_state: {
title: 'Aucune dépendance',
description: 'Créez une vue pour commencer',
title: 'Aucune relation',
description: 'Créez une relation pour commencer',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -209,7 +269,13 @@ export const fr: LanguageTranslation = {
undo: 'Annuler',
redo: 'Rétablir',
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',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -225,7 +291,7 @@ export const fr: LanguageTranslation = {
title: 'Importer votre Base de Données',
database_edition: 'Édition de la Base de Données :',
step_1: 'Exécutez ce script dans votre base de données :',
step_2: 'Collez le résultat du script ici :',
step_2: 'Collez le résultat du script ici ',
script_results_placeholder: 'Résultats du script ici...',
ssms_instructions: {
button_text: 'Instructions SSMS',
@@ -286,15 +352,12 @@ export const fr: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Annuler',
export: 'Exporter',
},
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',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -319,6 +382,13 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler',
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: {
title: 'Créer une Relation',
@@ -363,7 +433,7 @@ export const fr: LanguageTranslation = {
error: {
title: "Erreur lors de l'exportation du diagramme",
description:
"Une erreur s'est produite. Besoin d'aide ? chartdb.io@gmail.com",
"Une erreur s'est produite. Besoin d'aide ? support@chartdb.io",
},
},
import_diagram_dialog: {
@@ -374,7 +444,7 @@ export const fr: LanguageTranslation = {
error: {
title: "Erreur lors de l'exportation du diagramme",
description:
"Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? chartdb.io@gmail.com",
"Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? support@chartdb.io",
},
},
import_dbml_dialog: {
@@ -401,7 +471,10 @@ export const fr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nouvelle Table',
new_view: 'Nouvelle Vue',
new_relationship: 'Nouvelle Relation',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -421,6 +494,9 @@ export const fr: LanguageTranslation = {
language_select: {
change_language: 'Langue',
},
on: 'Activé',
off: 'Désactivé',
},
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const gu: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'નવું',
browse: 'બ્રાઉજ',
tables: 'ટેબલો',
refs: 'રેફ્સ',
areas: 'ક્ષેત્રો',
dependencies: 'નિર્ભરતાઓ',
custom_types: 'કસ્ટમ ટાઇપ',
},
menu: {
file: {
file: 'ફાઇલ',
new: 'નવું',
open: 'ખોલો',
databases: {
databases: 'ડેટાબેસેસ',
new: 'નવું ડાયાગ્રામ',
browse: 'બ્રાઉજ કરો...',
save: 'સાચવો',
import: 'ડેટાબેસ આયાત કરો',
export_sql: 'SQL નિકાસ કરો',
export_as: 'રૂપે નિકાસ કરો',
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
exit: 'બહાર જાઓ',
},
edit: {
edit: 'ફેરફાર',
@@ -26,7 +34,10 @@ export const gu: LanguageTranslation = {
hide_sidebar: 'સાઇડબાર છુપાવો',
hide_cardinality: 'કાર્ડિનાલિટી છુપાવો',
show_cardinality: 'કાર્ડિનાલિટી બતાવો',
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
show_views: 'ડેટાબેઝ વ્યૂઝ',
theme: 'થિમ',
show_dependencies: 'નિર્ભરતાઓ બતાવો',
hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
@@ -71,15 +82,6 @@ export const gu: LanguageTranslation = {
cancel: 'રદ કરો',
},
multiple_schemas_alert: {
title: 'કઈંક વધારે સ્કીમા',
description:
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
dont_show_again: 'ફરીથી ન બતાવો',
change_schema: 'બદલો',
none: 'કઈ નહીં',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'નકલ નિષ્ફળ',
@@ -114,14 +116,11 @@ export const gu: LanguageTranslation = {
copied: 'નકલ થયું!',
side_panel: {
schema: 'સ્કીમા:',
filter_by_schema: 'સ્કીમા દ્વારા ફિલ્ટર કરો',
search_schema: 'સ્કીમા શોધો...',
no_schemas_found: 'કોઈ સ્કીમા મળ્યા નથી.',
view_all_options: 'બધા વિકલ્પો જુઓ...',
tables_section: {
tables: 'ટેબલ્સ',
add_table: 'ટેબલ ઉમેરો',
add_view: 'વ્યૂ ઉમેરો',
filter: 'ફિલ્ટર',
collapse: 'બધાને સકુચિત કરો',
// TODO: Translate
@@ -148,16 +147,23 @@ export const gu: LanguageTranslation = {
field_actions: {
title: 'ફીલ્ડ લક્ષણો',
unique: 'અદ્વિતીય',
auto_increment: 'ઑટો ઇન્ક્રિમેન્ટ',
comments: 'ટિપ્પણીઓ',
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
delete_field: 'ફીલ્ડ કાઢી નાખો',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'ચોકસાઈ',
scale: 'માપ',
},
index_actions: {
title: 'ઇન્ડેક્સ લક્ષણો',
name: 'નામ',
unique: 'અદ્વિતીય',
index_type: 'ઇન્ડેક્સ પ્રકાર',
delete_index: 'ઇન્ડેક્સ કાઢી નાખો',
},
table_actions: {
@@ -174,14 +180,17 @@ export const gu: LanguageTranslation = {
description: 'શરૂ કરવા માટે એક ટેબલ બનાવો',
},
},
relationships_section: {
relationships: 'સંબંધો',
refs_section: {
refs: 'રેફ્સ',
filter: 'ફિલ્ટર',
add_relationship: 'સંબંધ ઉમેરો',
collapse: 'બધાને સકુચિત કરો',
add_relationship: 'સંબંધ ઉમેરો',
relationships: 'સંબંધો',
dependencies: 'નિર્ભરતાઓ',
relationship: {
relationship: 'સંબંધ',
primary: 'પ્રાથમિક ટેબલ',
foreign: 'સંદર્ભ ટેબલ',
foreign: 'સંદર્ભિત ટેબલ',
cardinality: 'કાર્ડિનાલિટી',
delete_relationship: 'કાઢી નાખો',
relationship_actions: {
@@ -189,27 +198,69 @@ export const gu: LanguageTranslation = {
delete_relationship: 'કાઢી નાખો',
},
},
empty_state: {
title: 'કોઈ સંબંધો નથી',
description: 'ટેબલ્સ કનેક્ટ કરવા માટે એક સંબંધ બનાવો',
},
},
dependencies_section: {
dependencies: 'નિર્ભરતાઓ',
filter: 'ફિલ્ટર',
collapse: 'સિકોડો',
dependency: {
dependency: 'નિર્ભરતા',
table: 'ટેબલ',
dependent_table: 'આધાર રાખેલું ટેબલ',
delete_dependency: 'નિર્ભરતા કાઢી નાખો',
dependent_table: 'નિર્ભરશીલ વ્યૂ',
delete_dependency: 'કાઢી નાખો',
dependency_actions: {
title: 'ક્રિયાઓ',
delete_dependency: 'નિર્ભરતા કાઢી નાખો',
delete_dependency: 'કાઢી નાખો',
},
},
empty_state: {
title: 'કોઈ નિર્ભરતાઓ નથી',
description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.',
title: 'કોઈ સંબંધો નથી',
description: 'શરૂ કરવા માટે એક સંબંધ બનાવો',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -222,7 +273,13 @@ export const gu: LanguageTranslation = {
undo: 'અનડુ',
redo: 'રીડુ',
reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -237,7 +294,7 @@ export const gu: LanguageTranslation = {
title: 'તમારું ડેટાબેસ આયાત કરો',
database_edition: 'ડેટાબેસ આવૃત્તિ:',
step_1: 'તમારા ડેટાબેસમાં આ સ્ક્રિપ્ટ ચલાવો:',
step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો:',
step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો',
script_results_placeholder: 'સ્ક્રિપ્ટના પરિણામ અહીં...',
ssms_instructions: {
button_text: 'SSMS સૂચનાઓ',
@@ -331,6 +388,12 @@ export const gu: LanguageTranslation = {
scale_4x: '4x',
cancel: 'રદ કરો',
export: 'નિકાસ કરો',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -348,6 +411,14 @@ export const gu: LanguageTranslation = {
confirm: 'બદલો',
},
create_table_schema_dialog: {
title: 'નવું સ્કીમા બનાવો',
description:
'હજી સુધી કોઈ સ્કીમા અસ્તિત્વમાં નથી. તમારા ટેબલ્સ ને વ્યવસ્થિત કરવા માટે તમારું પહેલું સ્કીમા બનાવો.',
create: 'બનાવો',
cancel: 'રદ કરો',
},
star_us_dialog: {
title: 'અમને સુધારવામાં મદદ કરો!',
description:
@@ -365,7 +436,7 @@ export const gu: LanguageTranslation = {
error: {
title: 'ડાયાગ્રામ નિકાસમાં ભૂલ',
description:
'કશુક તો ખોટું થયું. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
'કશુક તો ખોટું થયું. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.',
},
},
@@ -377,7 +448,7 @@ export const gu: LanguageTranslation = {
error: {
title: 'ડાયાગ્રામ આયાતમાં ભૂલ',
description:
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.',
},
},
// TODO: Translate
@@ -403,7 +474,10 @@ export const gu: LanguageTranslation = {
canvas_context_menu: {
new_table: 'નવું ટેબલ',
new_view: 'નવું વ્યૂ',
new_relationship: 'નવો સંબંધ',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -422,6 +496,9 @@ export const gu: LanguageTranslation = {
language_select: {
change_language: 'ભાષા બદલો',
},
on: 'ચાલુ',
off: 'બંધ',
},
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const hi: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नया',
browse: 'ब्राउज़',
tables: 'टेबल',
refs: 'रेफ्स',
areas: 'क्षेत्र',
dependencies: 'निर्भरताएं',
custom_types: 'कस्टम टाइप',
},
menu: {
file: {
file: 'फ़ाइल',
new: 'नया',
open: 'खोलें',
databases: {
databases: 'डेटाबेस',
new: 'नया आरेख',
browse: 'ब्राउज़ करें...',
save: 'सहेजें',
import: 'डेटाबेस आयात करें',
export_sql: 'SQL निर्यात करें',
export_as: 'के रूप में निर्यात करें',
delete_diagram: 'आरेख हटाएँ',
exit: 'बाहर जाएँ',
},
edit: {
edit: 'संपादित करें',
@@ -26,7 +34,10 @@ export const hi: LanguageTranslation = {
hide_sidebar: 'साइडबार छिपाएँ',
hide_cardinality: 'कार्डिनैलिटी छिपाएँ',
show_cardinality: 'कार्डिनैलिटी दिखाएँ',
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
show_views: 'डेटाबेस व्यू',
theme: 'थीम',
show_dependencies: 'निर्भरता दिखाएँ',
hide_dependencies: 'निर्भरता छिपाएँ',
@@ -70,15 +81,6 @@ export const hi: LanguageTranslation = {
cancel: 'रद्द करें',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
dont_show_again: 'फिर से न दिखाएँ',
change_schema: 'बदलें',
none: 'कोई नहीं',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'कॉपी असफल',
@@ -114,14 +116,11 @@ export const hi: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारा फ़िल्टर करें',
search_schema: 'स्कीमा खोजें...',
no_schemas_found: 'कोई स्कीमा नहीं मिला।',
view_all_options: 'सभी विकल्प देखें...',
tables_section: {
tables: 'तालिकाएँ',
add_table: 'तालिका जोड़ें',
add_view: 'व्यू जोड़ें',
filter: 'फ़िल्टर',
collapse: 'सभी को संक्षिप्त करें',
// TODO: Translate
@@ -147,16 +146,23 @@ export const hi: LanguageTranslation = {
field_actions: {
title: 'फ़ील्ड विशेषताएँ',
unique: 'अद्वितीय',
auto_increment: 'ऑटो इंक्रीमेंट',
comments: 'टिप्पणियाँ',
no_comments: 'कोई टिप्पणी नहीं',
delete_field: 'फ़ील्ड हटाएँ',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Precision',
scale: 'Scale',
},
index_actions: {
title: 'सूचकांक विशेषताएँ',
name: 'नाम',
unique: 'अद्वितीय',
index_type: 'इंडेक्स प्रकार',
delete_index: 'सूचकांक हटाएँ',
},
table_actions: {
@@ -173,12 +179,15 @@ export const hi: LanguageTranslation = {
description: 'शुरू करने के लिए एक तालिका बनाएँ',
},
},
relationships_section: {
relationships: 'संबंध',
refs_section: {
refs: 'रेफ्स',
filter: 'फ़िल्टर',
add_relationship: 'संबंध जोड़ें',
collapse: 'सभी को संक्षिप्त करें',
add_relationship: 'संबंध जोड़ें',
relationships: 'संबंध',
dependencies: 'निर्भरताएँ',
relationship: {
relationship: 'संबंध',
primary: 'प्राथमिक तालिका',
foreign: 'संदर्भित तालिका',
cardinality: 'कार्डिनैलिटी',
@@ -188,28 +197,69 @@ export const hi: LanguageTranslation = {
delete_relationship: 'हटाएँ',
},
},
empty_state: {
title: 'कोई संबंध नहीं',
description:
'तालिकाओं को कनेक्ट करने के लिए एक संबंध बनाएँ',
},
},
dependencies_section: {
dependencies: 'निर्भरताएँ',
filter: 'फ़िल्टर',
collapse: 'सिकोड़ें',
dependency: {
dependency: 'निर्भरता',
table: 'तालिका',
dependent_table: 'आश्रित तालिका',
delete_dependency: 'निर्भरता हटाएँ',
dependent_table: 'आश्रित दृश्य',
delete_dependency: 'हटाएँ',
dependency_actions: {
title: 'कार्रवाइयाँ',
delete_dependency: 'निर्भरता हटाएँ',
title: 'क्रियाँ',
delete_dependency: 'हटाएँ',
},
},
empty_state: {
title: 'कोई निर्भरता नहीं',
description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।',
title: 'कोई संबंध नहीं',
description: 'शुरू करने के लिए एक संबंध बनाएँ',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -222,7 +272,13 @@ export const hi: LanguageTranslation = {
undo: 'पूर्ववत करें',
redo: 'पुनः करें',
reorder_diagram: 'आरेख पुनः व्यवस्थित करें',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -238,7 +294,7 @@ export const hi: LanguageTranslation = {
title: 'अपना डेटाबेस आयात करें',
database_edition: 'डेटाबेस संस्करण:',
step_1: 'अपने डेटाबेस में यह स्क्रिप्ट चलाएँ:',
step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें:',
step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें',
script_results_placeholder: 'स्क्रिप्ट के परिणाम यहाँ...',
ssms_instructions: {
button_text: 'SSMS निर्देश',
@@ -334,6 +390,12 @@ export const hi: LanguageTranslation = {
scale_4x: '4x',
cancel: 'रद्द करें',
export: 'निर्यात करें',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -351,6 +413,14 @@ export const hi: LanguageTranslation = {
confirm: 'बदलें',
},
create_table_schema_dialog: {
title: 'नया स्कीमा बनाएं',
description:
'अभी तक कोई स्कीमा मौजूद नहीं है। अपनी तालिकाओं को व्यवस्थित करने के लिए अपना पहला स्कीमा बनाएं।',
create: 'बनाएं',
cancel: 'रद्द करें',
},
star_us_dialog: {
title: 'हमें सुधारने में मदद करें!',
description:
@@ -368,7 +438,7 @@ export const hi: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -380,7 +450,7 @@ export const hi: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -406,7 +476,10 @@ export const hi: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नई तालिका',
new_view: 'नया व्यू',
new_relationship: 'नया संबंध',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -426,6 +499,9 @@ export const hi: LanguageTranslation = {
language_select: {
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 = {
translation: {
editor_sidebar: {
new_diagram: 'Baru',
browse: 'Jelajahi',
tables: 'Tabel',
refs: 'Refs',
areas: 'Area',
dependencies: 'Ketergantungan',
custom_types: 'Tipe Kustom',
},
menu: {
file: {
file: 'Berkas',
new: 'Buat Baru',
open: 'Buka',
databases: {
databases: 'Basis Data',
new: 'Diagram Baru',
browse: 'Jelajahi...',
save: 'Simpan',
import: 'Impor Database',
export_sql: 'Ekspor SQL',
export_as: 'Ekspor Sebagai',
delete_diagram: 'Hapus Diagram',
exit: 'Keluar',
},
edit: {
edit: 'Ubah',
@@ -26,7 +34,10 @@ export const id_ID: LanguageTranslation = {
hide_sidebar: 'Sembunyikan Sidebar',
hide_cardinality: 'Sembunyikan Kardinalitas',
show_cardinality: 'Tampilkan Kardinalitas',
hide_field_attributes: 'Sembunyikan Atribut Kolom',
show_field_attributes: 'Tampilkan Atribut Kolom',
zoom_on_scroll: 'Perbesar saat Scroll',
show_views: 'Tampilan Database',
theme: 'Tema',
show_dependencies: 'Tampilkan Dependensi',
hide_dependencies: 'Sembunyikan Dependensi',
@@ -70,15 +81,6 @@ export const id_ID: LanguageTranslation = {
cancel: 'Batal',
},
multiple_schemas_alert: {
title: 'Schema Lebih dari satu',
description:
'{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.',
dont_show_again: 'Jangan tampilkan lagi',
change_schema: 'Ubah',
none: 'Tidak ada',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'Gagal menyalin',
@@ -113,14 +115,11 @@ export const id_ID: LanguageTranslation = {
copied: 'Tersalin!',
side_panel: {
schema: 'Skema:',
filter_by_schema: 'Saring berdasarkan skema',
search_schema: 'Cari skema...',
no_schemas_found: 'Tidak ada skema yang ditemukan.',
view_all_options: 'Tampilkan Semua Pilihan...',
tables_section: {
tables: 'Tabel',
add_table: 'Tambah Tabel',
add_view: 'Tambah Tampilan',
filter: 'Saring',
collapse: 'Lipat Semua',
// TODO: Translate
@@ -146,16 +145,23 @@ export const id_ID: LanguageTranslation = {
field_actions: {
title: 'Atribut Kolom',
unique: 'Unik',
auto_increment: 'Kenaikan Otomatis',
comments: 'Komentar',
no_comments: 'Tidak ada komentar',
delete_field: 'Hapus Kolom',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'Presisi',
scale: 'Skala',
},
index_actions: {
title: 'Atribut Indeks',
name: 'Nama',
unique: 'Unik',
index_type: 'Tipe Indeks',
delete_index: 'Hapus Indeks',
},
table_actions: {
@@ -172,12 +178,15 @@ export const id_ID: LanguageTranslation = {
description: 'Buat tabel untuk memulai',
},
},
relationships_section: {
relationships: 'Hubungan',
refs_section: {
refs: 'Refs',
filter: 'Saring',
add_relationship: 'Tambah Hubungan',
collapse: 'Lipat Semua',
add_relationship: 'Tambah Hubungan',
relationships: 'Hubungan',
dependencies: 'Dependensi',
relationship: {
relationship: 'Hubungan',
primary: 'Tabel Primer',
foreign: 'Tabel Referensi',
cardinality: 'Kardinalitas',
@@ -187,16 +196,8 @@ export const id_ID: LanguageTranslation = {
delete_relationship: 'Hapus',
},
},
empty_state: {
title: 'Tidak ada hubungan',
description: 'Buat hubungan untuk menghubungkan tabel',
},
},
dependencies_section: {
dependencies: 'Dependensi',
filter: 'Saring',
collapse: 'Lipat Semua',
dependency: {
dependency: 'Dependensi',
table: 'Tabel',
dependent_table: 'Tampilan Dependen',
delete_dependency: 'Hapus',
@@ -206,8 +207,58 @@ export const id_ID: LanguageTranslation = {
},
},
empty_state: {
title: 'Tidak ada dependensi',
description: 'Buat tampilan untuk memulai',
title: 'Tidak ada hubungan',
description: 'Buat hubungan untuk memulai',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -220,7 +271,13 @@ export const id_ID: LanguageTranslation = {
undo: 'Undo',
redo: 'Redo',
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',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -236,7 +293,7 @@ export const id_ID: LanguageTranslation = {
title: 'Impor Database Anda',
database_edition: 'Edisi Database:',
step_1: 'Jalankan skrip ini di database Anda:',
step_2: 'Tempel hasil skrip di sini:',
step_2: 'Tempel hasil skrip di sini',
script_results_placeholder: 'Hasil skrip di sini...',
ssms_instructions: {
button_text: 'Instruksi SSMS',
@@ -329,6 +386,12 @@ export const id_ID: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Batal',
export: 'Ekspor',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -346,6 +409,14 @@ export const id_ID: LanguageTranslation = {
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: {
title: 'Bantu kami meningkatkan!',
description:
@@ -363,7 +434,7 @@ export const id_ID: LanguageTranslation = {
error: {
title: 'Error ekspor diagram',
description:
'Sesuatu yang salah. Butuh bantuan? chartdb.io@gmail.com',
'Sesuatu yang salah. Butuh bantuan? support@chartdb.io',
},
},
@@ -375,7 +446,7 @@ export const id_ID: LanguageTranslation = {
error: {
title: 'Error impor diagram',
description:
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com',
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? support@chartdb.io',
},
},
// TODO: Translate
@@ -402,7 +473,10 @@ export const id_ID: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Tabel Baru',
new_view: 'Tampilan Baru',
new_relationship: 'Hubungan Baru',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -421,6 +495,9 @@ export const id_ID: LanguageTranslation = {
language_select: {
change_language: 'Bahasa',
},
on: 'Aktif',
off: 'Nonaktif',
},
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ja: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '新規',
browse: '参照',
tables: 'テーブル',
refs: '参照',
areas: 'エリア',
dependencies: '依存関係',
custom_types: 'カスタムタイプ',
},
menu: {
file: {
file: 'ファイル',
new: '新',
open: '開く',
databases: {
databases: 'データベース',
new: '新しいダイアグラム',
browse: '参照...',
save: '保存',
import: 'データベースをインポート',
export_sql: 'SQLをエクスポート',
export_as: '形式を指定してエクスポート',
delete_diagram: 'ダイアグラムを削除',
exit: '終了',
},
edit: {
edit: '編集',
@@ -26,7 +34,10 @@ export const ja: LanguageTranslation = {
hide_sidebar: 'サイドバーを非表示',
hide_cardinality: 'カーディナリティを非表示',
show_cardinality: 'カーディナリティを表示',
hide_field_attributes: 'フィールド属性を非表示',
show_field_attributes: 'フィールド属性を表示',
zoom_on_scroll: 'スクロールでズーム',
show_views: 'データベースビュー',
theme: 'テーマ',
// TODO: Translate
show_dependencies: 'Show Dependencies',
@@ -72,15 +83,6 @@ export const ja: LanguageTranslation = {
cancel: 'キャンセル',
},
multiple_schemas_alert: {
title: '複数のスキーマ',
description:
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
dont_show_again: '再表示しない',
change_schema: '変更',
none: 'なし',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'コピー失敗',
@@ -117,14 +119,11 @@ export const ja: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'スキーマ:',
filter_by_schema: 'スキーマでフィルタ',
search_schema: 'スキーマを検索...',
no_schemas_found: 'スキーマが見つかりません。',
view_all_options: 'すべてのオプションを表示...',
tables_section: {
tables: 'テーブル',
add_table: 'テーブルを追加',
add_view: 'ビューを追加',
filter: 'フィルタ',
collapse: 'すべて折りたたむ',
// TODO: Translate
@@ -150,16 +149,23 @@ export const ja: LanguageTranslation = {
field_actions: {
title: 'フィールド属性',
unique: 'ユニーク',
auto_increment: 'オートインクリメント',
comments: 'コメント',
no_comments: 'コメントがありません',
delete_field: 'フィールドを削除',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: '精度',
scale: '小数点以下桁数',
},
index_actions: {
title: 'インデックス属性',
name: '名前',
unique: 'ユニーク',
index_type: 'インデックスタイプ',
delete_index: 'インデックスを削除',
},
table_actions: {
@@ -176,12 +182,15 @@ export const ja: LanguageTranslation = {
description: 'テーブルを作成して開始してください',
},
},
relationships_section: {
relationships: 'リレーションシップ',
refs_section: {
refs: '参照',
filter: 'フィルタ',
add_relationship: 'リレーションシップを追加',
collapse: 'すべて折りたたむ',
add_relationship: 'リレーションシップを追加',
relationships: 'リレーションシップ',
dependencies: '依存関係',
relationship: {
relationship: 'リレーションシップ',
primary: '主テーブル',
foreign: '参照テーブル',
cardinality: 'カーディナリティ',
@@ -191,29 +200,70 @@ export const ja: LanguageTranslation = {
delete_relationship: '削除',
},
},
empty_state: {
title: 'リレーションシップがありません',
description:
'テーブルを接続するためにリレーションシップを作成してください',
},
},
// TODO: Translate
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
dependency: '依存関係',
table: 'テーブル',
dependent_table: '依存ビュー',
delete_dependency: '削除',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
title: '操作',
delete_dependency: '削除',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
title: 'リレーションシップがありません',
description:
'開始するためにリレーションシップを作成してください',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -228,6 +278,10 @@ export const ja: LanguageTranslation = {
reorder_diagram: 'ダイアグラムを並べ替え',
// TODO: Translate
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: {
@@ -242,7 +296,7 @@ export const ja: LanguageTranslation = {
title: 'データベースをインポート',
database_edition: 'データベースエディション:',
step_1: 'このスクリプトをデータベースで実行してください:',
step_2: 'ここにスクリプトの結果を貼り付けてください:',
step_2: 'ここにスクリプトの結果を貼り付けてください',
script_results_placeholder: 'ここにスクリプトの結果...',
ssms_instructions: {
button_text: 'SSMSの手順',
@@ -338,6 +392,12 @@ export const ja: LanguageTranslation = {
scale_4x: '4x',
cancel: 'キャンセル',
export: 'エクスポート',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -355,6 +415,14 @@ export const ja: LanguageTranslation = {
confirm: '変更',
},
create_table_schema_dialog: {
title: '新しいスキーマを作成',
description:
'スキーマがまだ存在しません。テーブルを整理するために最初のスキーマを作成してください。',
create: '作成',
cancel: 'キャンセル',
},
star_us_dialog: {
title: '改善をサポートしてください!',
description:
@@ -372,7 +440,7 @@ export const ja: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -384,7 +452,7 @@ export const ja: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -410,7 +478,10 @@ export const ja: LanguageTranslation = {
canvas_context_menu: {
new_table: '新しいテーブル',
new_view: '新しいビュー',
new_relationship: '新しいリレーションシップ',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -430,6 +501,9 @@ export const ja: LanguageTranslation = {
language_select: {
change_language: '言語',
},
on: 'オン',
off: 'オフ',
},
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ko_KR: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '새로 만들기',
browse: '찾아보기',
tables: '테이블',
refs: 'Refs',
areas: '영역',
dependencies: '종속성',
custom_types: '사용자 지정 타입',
},
menu: {
file: {
file: '파일',
databases: {
databases: '데이터베이스',
new: '새 다이어그램',
open: '열기',
browse: '찾아보기...',
save: '저장',
import: '데이터베이스 가져오기',
export_sql: 'SQL로 저장',
export_as: '다른 형식으로 저장',
delete_diagram: '다이어그램 삭제',
exit: '종료',
},
edit: {
edit: '편집',
@@ -26,7 +34,10 @@ export const ko_KR: LanguageTranslation = {
hide_sidebar: '사이드바 숨기기',
hide_cardinality: '카디널리티 숨기기',
show_cardinality: '카디널리티 보이기',
hide_field_attributes: '필드 속성 숨기기',
show_field_attributes: '필드 속성 보이기',
zoom_on_scroll: '스크롤 시 확대',
show_views: '데이터베이스 뷰',
theme: '테마',
show_dependencies: '종속성 보이기',
hide_dependencies: '종속성 숨기기',
@@ -70,15 +81,6 @@ export const ko_KR: LanguageTranslation = {
cancel: '취소',
},
multiple_schemas_alert: {
title: '다중 스키마',
description:
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
dont_show_again: '다시 보여주지 마세요',
change_schema: '변경',
none: '없음',
},
copy_to_clipboard_toast: {
unsupported: {
title: '복사 실패',
@@ -113,14 +115,11 @@ export const ko_KR: LanguageTranslation = {
copied: '복사됨!',
side_panel: {
schema: '스키마:',
filter_by_schema: '스키마로 필터링',
search_schema: '스키마 검색...',
no_schemas_found: '스키마를 찾을 수 없습니다.',
view_all_options: '전체 옵션 보기...',
tables_section: {
tables: '테이블',
add_table: '테이블 추가',
add_view: '뷰 추가',
filter: '필터',
collapse: '모두 접기',
// TODO: Translate
@@ -146,16 +145,23 @@ export const ko_KR: LanguageTranslation = {
field_actions: {
title: '필드 속성',
unique: '유니크 여부',
auto_increment: '자동 증가',
comments: '주석',
no_comments: '주석 없음',
delete_field: '필드 삭제',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: '정밀도',
scale: '소수점 자릿수',
},
index_actions: {
title: '인덱스 속성',
name: '인덱스 명',
unique: '유니크 여부',
index_type: '인덱스 타입',
delete_index: '인덱스 삭제',
},
table_actions: {
@@ -172,12 +178,15 @@ export const ko_KR: LanguageTranslation = {
description: '테이블을 만들어 시작하세요.',
},
},
relationships_section: {
relationships: '연관 관계',
refs_section: {
refs: 'Refs',
filter: '필터',
add_relationship: '연관 관계 추가',
collapse: '모두 접기',
add_relationship: '연관 관계 추가',
relationships: '연관 관계',
dependencies: '종속성',
relationship: {
relationship: '연관 관계',
primary: '주 테이블',
foreign: '참조 테이블',
cardinality: '카디널리티',
@@ -187,16 +196,8 @@ export const ko_KR: LanguageTranslation = {
delete_relationship: '연관 관계 삭제',
},
},
empty_state: {
title: '연관 관계',
description: '테이블 연결을 위해 연관 관계를 생성하세요',
},
},
dependencies_section: {
dependencies: '종속성',
filter: '필터',
collapse: '모두 접기',
dependency: {
dependency: '종속성',
table: '테이블',
dependent_table: '뷰 테이블',
delete_dependency: '삭제',
@@ -206,8 +207,58 @@ export const ko_KR: LanguageTranslation = {
},
},
empty_state: {
title: '뷰 테이블 없음',
description: '뷰 테이블을 만들어 시작하세요.',
title: '연관 관계 없음',
description: '연관 관계를 만들어 시작하세요.',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -220,7 +271,13 @@ export const ko_KR: LanguageTranslation = {
undo: '실행 취소',
redo: '다시 실행',
reorder_diagram: '다이어그램 재정렬',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: '겹치는 테이블 강조 표시',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -236,7 +293,7 @@ export const ko_KR: LanguageTranslation = {
title: '당신의 데이터베이스를 가져오세요',
database_edition: '데이터베이스 세부 종류:',
step_1: '데이터베이스에서 아래의 SQL을 실행해주세요:',
step_2: '이곳에 결과를 붙여넣어주세요:',
step_2: '이곳에 결과를 붙여넣어주세요',
script_results_placeholder: '이곳에 스크립트 결과를 입력...',
ssms_instructions: {
button_text: 'SSMS을 사용하시는 경우',
@@ -329,6 +386,12 @@ export const ko_KR: LanguageTranslation = {
scale_4x: '4x',
cancel: '취소',
export: '내보내기',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -346,6 +409,14 @@ export const ko_KR: LanguageTranslation = {
confirm: '변경',
},
create_table_schema_dialog: {
title: '새 스키마 생성',
description:
'아직 스키마가 없습니다. 테이블을 정리하기 위해 첫 번째 스키마를 생성하세요.',
create: '생성',
cancel: '취소',
},
star_us_dialog: {
title: '개선할 수 있도록 도와주세요!',
description:
@@ -362,7 +433,7 @@ export const ko_KR: LanguageTranslation = {
error: {
title: '다이어그램 내보내기 오류',
description:
'무언가 문제가 발생하였습니다. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
'무언가 문제가 발생하였습니다. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.',
},
},
import_diagram_dialog: {
@@ -373,7 +444,7 @@ export const ko_KR: LanguageTranslation = {
error: {
title: '다이어그램 가져오기 오류',
description:
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.',
},
},
// TODO: Translate
@@ -399,7 +470,10 @@ export const ko_KR: LanguageTranslation = {
canvas_context_menu: {
new_table: '새 테이블',
new_view: '새 뷰',
new_relationship: '새 연관관계',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -418,6 +492,9 @@ export const ko_KR: LanguageTranslation = {
language_select: {
change_language: '언어',
},
on: '켜기',
off: '끄기',
},
};

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const mr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नवीन',
browse: 'ब्राउज',
tables: 'टेबल',
refs: 'Refs',
areas: 'क्षेत्रे',
dependencies: 'अवलंबने',
custom_types: 'कस्टम प्रकार',
},
menu: {
file: {
file: 'फाइल',
new: 'नवीन',
open: 'उघडा',
databases: {
databases: 'डेटाबेस',
new: 'नवीन आरेख',
browse: 'ब्राउज करा...',
save: 'जतन करा',
import: 'डेटाबेस इम्पोर्ट करा',
export_sql: 'SQL एक्स्पोर्ट करा',
export_as: 'म्हणून एक्स्पोर्ट करा',
delete_diagram: 'आरेख हटवा',
exit: 'बाहेर पडा',
},
edit: {
edit: 'संपादन करा',
@@ -26,7 +34,10 @@ export const mr: LanguageTranslation = {
hide_sidebar: 'साइडबार लपवा',
hide_cardinality: 'कार्डिनॅलिटी लपवा',
show_cardinality: 'कार्डिनॅलिटी दाखवा',
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
zoom_on_scroll: 'स्क्रोलवर झूम करा',
show_views: 'डेटाबेस व्ह्यूज',
theme: 'थीम',
show_dependencies: 'डिपेंडेन्सि दाखवा',
hide_dependencies: 'डिपेंडेन्सि लपवा',
@@ -71,15 +82,6 @@ export const mr: LanguageTranslation = {
cancel: 'रद्द करा',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
dont_show_again: 'पुन्हा दाखवू नका',
change_schema: 'बदला',
none: 'काहीही नाही',
},
copy_to_clipboard_toast: {
unsupported: {
title: 'कॉपी अयशस्वी',
@@ -116,14 +118,11 @@ export const mr: LanguageTranslation = {
copied: 'Copied!',
side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारे फिल्टर करा',
search_schema: 'स्कीमा शोधा...',
no_schemas_found: 'कोणतेही स्कीमा सापडले नाहीत.',
view_all_options: 'सर्व पर्याय पहा...',
tables_section: {
tables: 'टेबल्स',
add_table: 'टेबल जोडा',
add_view: 'व्ह्यू जोडा',
filter: 'फिल्टर',
collapse: 'सर्व संकुचित करा',
// TODO: Translate
@@ -149,16 +148,23 @@ export const mr: LanguageTranslation = {
field_actions: {
title: 'फील्ड गुणधर्म',
unique: 'युनिक',
auto_increment: 'ऑटो इंक्रिमेंट',
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा',
// TODO: Translate
default_value: 'Default Value',
no_default: 'No default',
// TODO: Translate
character_length: 'Max Length',
precision: 'अचूकता',
scale: 'प्रमाण',
},
index_actions: {
title: 'इंडेक्स गुणधर्म',
name: 'नाव',
unique: 'युनिक',
index_type: 'इंडेक्स प्रकार',
delete_index: 'इंडेक्स हटवा',
},
table_actions: {
@@ -176,12 +182,15 @@ export const mr: LanguageTranslation = {
description: 'सुरू करण्यासाठी एक टेबल तयार करा',
},
},
relationships_section: {
relationships: 'रिलेशनशिप',
refs_section: {
refs: 'Refs',
filter: 'फिल्टर',
add_relationship: 'रिलेशनशिप जोडा',
collapse: 'सर्व संकुचित करा',
add_relationship: 'रिलेशनशिप जोडा',
relationships: 'रिलेशनशिप',
dependencies: 'डिपेंडेन्सि',
relationship: {
relationship: 'रिलेशनशिप',
primary: 'प्राथमिक टेबल',
foreign: 'रेफरंस टेबल',
cardinality: 'कार्डिनॅलिटी',
@@ -191,17 +200,8 @@ export const mr: LanguageTranslation = {
delete_relationship: 'हटवा',
},
},
empty_state: {
title: 'कोणतेही रिलेशनशिप नाहीत',
description:
'टेबल्स कनेक्ट करण्यासाठी एक रिलेशनशिप तयार करा',
},
},
dependencies_section: {
dependencies: 'डिपेंडेन्सि',
filter: 'फिल्टर',
collapse: 'सर्व संकुचित करा',
dependency: {
dependency: 'डिपेंडेन्सि',
table: 'टेबल',
dependent_table: 'डिपेंडेन्सि दृश्य',
delete_dependency: 'हटवा',
@@ -211,8 +211,58 @@ export const mr: LanguageTranslation = {
},
},
empty_state: {
title: 'कोणत्याही डिपेंडेन्सि नाहीत',
description: 'सुरू करण्यासाठी एक दृश्य तयार करा',
title: 'कोणतेही रिलेशनशिप नाहीत',
description: 'सुरू करण्यासाठी एक रिलेशनशिप तयार करा',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
no_fields_tooltip: 'No fields defined for this custom type',
custom_type_actions: {
title: 'Actions',
highlight_fields: 'Highlight Fields',
delete_custom_type: 'Delete',
clear_field_highlight: 'Clear Highlight',
},
delete_custom_type: 'Delete Type',
},
},
},
@@ -225,7 +275,13 @@ export const mr: LanguageTranslation = {
undo: 'पूर्ववत करा',
redo: 'पुन्हा करा',
reorder_diagram: 'आरेख पुनःक्रमित करा',
// TODO: Translate
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
custom_type_highlight_tooltip:
'Highlighting "{{typeName}}" - Click to clear',
highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा',
// TODO: Translate
filter: 'Filter Tables',
},
new_diagram_dialog: {
@@ -241,7 +297,7 @@ export const mr: LanguageTranslation = {
title: 'तुमचा डेटाबेस आयात करा',
database_edition: 'डेटाबेस संस्करण:',
step_1: 'तुमच्या डेटाबेसमध्ये हा स्क्रिप्ट चालवा:',
step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा:',
step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा',
script_results_placeholder: 'स्क्रिप्ट परिणाम येथे...',
ssms_instructions: {
button_text: 'SSMS सूचना',
@@ -337,6 +393,12 @@ export const mr: LanguageTranslation = {
scale_4x: '4x',
cancel: 'रद्द करा',
export: 'निर्यात करा',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -354,6 +416,14 @@ export const mr: LanguageTranslation = {
confirm: 'बदला',
},
create_table_schema_dialog: {
title: 'नवीन स्कीमा तयार करा',
description:
'अजून कोणतीही स्कीमा अस्तित्वात नाही. आपल्या टेबल्स व्यवस्थित करण्यासाठी आपली पहिली स्कीमा तयार करा.',
create: 'तयार करा',
cancel: 'रद्द करा',
},
star_us_dialog: {
title: 'आम्हाला सुधारण्यास मदत करा!',
description:
@@ -372,7 +442,7 @@ export const mr: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -385,7 +455,7 @@ export const mr: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -412,7 +482,10 @@ export const mr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नवीन टेबल',
new_view: 'नवीन व्ह्यू',
new_relationship: 'नवीन रिलेशनशिप',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
@@ -433,6 +506,9 @@ export const mr: LanguageTranslation = {
language_select: {
change_language: 'भाषा बदला',
},
on: 'चालू',
off: 'बंद',
},
};

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