mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-05 06:23:17 +00:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dfa7cc62e | ||
|
|
23e93bfd01 | ||
|
|
16f9f4671e | ||
|
|
0c300e5e72 | ||
|
|
b9a1e78b53 | ||
|
|
337f7cdab4 | ||
|
|
1b0390f0b7 | ||
|
|
bc52933b58 | ||
|
|
2fdad2344c | ||
|
|
0c7eaa2df2 | ||
|
|
a5f8e56b3c | ||
|
|
8ffde62c1a | ||
|
|
39247b77a2 | ||
|
|
984b2aeee2 | ||
|
|
eed104be5b | ||
|
|
00bd535b3c | ||
|
|
18e914242f | ||
|
|
e68837a34a | ||
|
|
b30162d98b | ||
|
|
dba372d25a | ||
|
|
2eb48e75d3 | ||
|
|
867903cd5f | ||
|
|
8aeb1df0ad | ||
|
|
6bea827293 | ||
|
|
a119854da7 | ||
|
|
bfbfd7b843 | ||
|
|
0ca7008735 | ||
|
|
4bc71c52ff | ||
|
|
8f27f10dec | ||
|
|
a93ec2cab9 | ||
|
|
386e40a0bf | ||
|
|
bda150d4b6 | ||
|
|
87836e53d1 | ||
|
|
7e0483f1a5 | ||
|
|
309ee9cb0f | ||
|
|
79b885502e | ||
|
|
745bdee86d | ||
|
|
08eb9cc55f | ||
|
|
778f85d492 | ||
|
|
fb92be7d3e | ||
|
|
6df588f40e | ||
|
|
b46ed58dff | ||
|
|
0d9f57a9c9 | ||
|
|
b7dbe54c83 | ||
|
|
43d1dfff71 | ||
|
|
9949a46ee3 | ||
|
|
dfbcf05b2f | ||
|
|
f56fab9876 | ||
|
|
c9ea7da092 | ||
|
|
22d46e1e90 | ||
|
|
6af94afc56 | ||
|
|
f7f92903de | ||
|
|
b35e17526b | ||
|
|
bf32c08d37 | ||
|
|
5d337409d6 | ||
|
|
67f5ac303e | ||
|
|
578546a171 | ||
|
|
aa0b629a3e | ||
|
|
69beaa0a83 | ||
|
|
4fcc49d49a | ||
|
|
d15985e399 | ||
|
|
d429128e65 | ||
|
|
2fce8326b6 | ||
|
|
433c68a33d | ||
|
|
58acb65f12 | ||
|
|
7978955819 | ||
|
|
c6118e0cdb | ||
|
|
7d063b905f | ||
|
|
e0ff198c3f | ||
|
|
8b86e1c229 | ||
|
|
24be28a662 | ||
|
|
c6788b4917 | ||
|
|
4a52bf02e6 | ||
|
|
08b627cb8c | ||
|
|
73f542adad | ||
|
|
0d11b0c55a | ||
|
|
5b9d2bd1e3 | ||
|
|
cf1e141837 | ||
|
|
3894a22174 | ||
|
|
cad155e655 | ||
|
|
4477b1ca1f | ||
|
|
cd443466c7 | ||
|
|
18012ddab1 | ||
|
|
beb015194f | ||
|
|
c3904d9fdd | ||
|
|
aee5779983 | ||
|
|
765a1c4354 |
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@@ -25,3 +25,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm run test:ci
|
||||||
109
CHANGELOG.md
109
CHANGELOG.md
@@ -1,5 +1,114 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add floating "Show All" button when tables are out of view ([#787](https://github.com/chartdb/chartdb/issues/787)) ([bda150d](https://github.com/chartdb/chartdb/commit/bda150d4b6d6fb90beb423efba69349d21a037a5))
|
||||||
|
* add table selection for large database imports ([#776](https://github.com/chartdb/chartdb/issues/776)) ([0d9f57a](https://github.com/chartdb/chartdb/commit/0d9f57a9c969a67e350d6bf25e07c3a9ef5bba39))
|
||||||
|
* **canvas:** Add filter tables on canvas ([#774](https://github.com/chartdb/chartdb/issues/774)) ([dfbcf05](https://github.com/chartdb/chartdb/commit/dfbcf05b2f595f5b7b77dd61abf77e6e07acaf8f))
|
||||||
|
* **custom-types:** add highlight fields option for custom types ([#726](https://github.com/chartdb/chartdb/issues/726)) ([7e0483f](https://github.com/chartdb/chartdb/commit/7e0483f1a5512a6a737baf61caf7513e043f2e96))
|
||||||
|
* **datatypes:** Add decimal / numeric attribute support + organize field row ([#715](https://github.com/chartdb/chartdb/issues/715)) ([778f85d](https://github.com/chartdb/chartdb/commit/778f85d49214232a39710e47bb5d4ec41b75d427))
|
||||||
|
* **dbml:** Edit Diagram Directly from DBML ([#819](https://github.com/chartdb/chartdb/issues/819)) ([1b0390f](https://github.com/chartdb/chartdb/commit/1b0390f0b7652fe415540b7942cf53ec87143f08))
|
||||||
|
* **default value:** add default value option to table field settings ([#770](https://github.com/chartdb/chartdb/issues/770)) ([c9ea7da](https://github.com/chartdb/chartdb/commit/c9ea7da0923ff991cb936235674d9a52b8186137))
|
||||||
|
* enhance primary key and unique field handling logic ([#817](https://github.com/chartdb/chartdb/issues/817)) ([39247b7](https://github.com/chartdb/chartdb/commit/39247b77a299caa4f29ea434af3028155c6d37ed))
|
||||||
|
* implement area grouping with parent-child relationships ([#762](https://github.com/chartdb/chartdb/issues/762)) ([b35e175](https://github.com/chartdb/chartdb/commit/b35e17526b3c9b918928ae5f3f89711ea7b2529c))
|
||||||
|
* **schema:** support create new schema ([#801](https://github.com/chartdb/chartdb/issues/801)) ([867903c](https://github.com/chartdb/chartdb/commit/867903cd5f24d96ce1fe718dc9b562e2f2b75276))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add open and create diagram to side menu ([#757](https://github.com/chartdb/chartdb/issues/757)) ([67f5ac3](https://github.com/chartdb/chartdb/commit/67f5ac303ebf5ada97d5c80fb08a2815ca205a91))
|
||||||
|
* add PostgreSQL tests and fix parsing SQL ([#760](https://github.com/chartdb/chartdb/issues/760)) ([5d33740](https://github.com/chartdb/chartdb/commit/5d337409d64d1078b538350016982a98e684c06c))
|
||||||
|
* area resizers size ([#830](https://github.com/chartdb/chartdb/issues/830)) ([23e93bf](https://github.com/chartdb/chartdb/commit/23e93bfd01d741dd3d11aa5c479cef97e1a86fa6))
|
||||||
|
* **area:** redo/undo after dragging an area with tables ([#767](https://github.com/chartdb/chartdb/issues/767)) ([6af94af](https://github.com/chartdb/chartdb/commit/6af94afc56cf8987b8fc9e3f0a9bfa966de35408))
|
||||||
|
* **canvas filter:** improve scroller on canvas filter ([#799](https://github.com/chartdb/chartdb/issues/799)) ([6bea827](https://github.com/chartdb/chartdb/commit/6bea82729362a8c7b73dc089ddd9e52bae176aa2))
|
||||||
|
* **canvas:** fix filter eye button ([#780](https://github.com/chartdb/chartdb/issues/780)) ([b7dbe54](https://github.com/chartdb/chartdb/commit/b7dbe54c83c75cfe3c556f7a162055dcfe2de23d))
|
||||||
|
* clone of custom types ([#804](https://github.com/chartdb/chartdb/issues/804)) ([b30162d](https://github.com/chartdb/chartdb/commit/b30162d98bc659a61aae023cdeaead4ce25c7ae9))
|
||||||
|
* **cockroachdb:** support schema creation for cockroachdb ([#803](https://github.com/chartdb/chartdb/issues/803)) ([dba372d](https://github.com/chartdb/chartdb/commit/dba372d25a8c642baf8600d05aa154882729d446))
|
||||||
|
* **dbml actions:** set dbml tooltips side ([#798](https://github.com/chartdb/chartdb/issues/798)) ([a119854](https://github.com/chartdb/chartdb/commit/a119854da7c935eb595984ea9398e04136ce60c4))
|
||||||
|
* **dbml editor:** move tooltips button to be on the right ([#797](https://github.com/chartdb/chartdb/issues/797)) ([bfbfd7b](https://github.com/chartdb/chartdb/commit/bfbfd7b843f96c894b1966ad95393b866c927466))
|
||||||
|
* **dbml export:** fix handle tables with same name under different schemas ([#807](https://github.com/chartdb/chartdb/issues/807)) ([18e9142](https://github.com/chartdb/chartdb/commit/18e914242faccd6376fe5a7cd5a4478667f065ee))
|
||||||
|
* **dbml export:** handle tables with same name under different schemas ([#806](https://github.com/chartdb/chartdb/issues/806)) ([e68837a](https://github.com/chartdb/chartdb/commit/e68837a34aa635fb6fc02c7f1289495e5c448242))
|
||||||
|
* **dbml field comments:** support export field comments in dbml ([#796](https://github.com/chartdb/chartdb/issues/796)) ([0ca7008](https://github.com/chartdb/chartdb/commit/0ca700873577bbfbf1dd3f8088c258fc89b10c53))
|
||||||
|
* **dbml import:** fix dbml import types + schemas ([#808](https://github.com/chartdb/chartdb/issues/808)) ([00bd535](https://github.com/chartdb/chartdb/commit/00bd535b3c62d26d25a6276d52beb10e26afad76))
|
||||||
|
* **dbml-export:** merge field attributes into single brackets and fix schema syntax ([#790](https://github.com/chartdb/chartdb/issues/790)) ([309ee9c](https://github.com/chartdb/chartdb/commit/309ee9cb0ff1f5a68ed183e3919e1a11a8410909))
|
||||||
|
* **dbml-import:** handle unsupported DBML features and add comprehensive tests ([#766](https://github.com/chartdb/chartdb/issues/766)) ([22d46e1](https://github.com/chartdb/chartdb/commit/22d46e1e90729730cc25dd6961bfe8c3d2ae0c98))
|
||||||
|
* **dbml:** dbml indentation ([#829](https://github.com/chartdb/chartdb/issues/829)) ([16f9f46](https://github.com/chartdb/chartdb/commit/16f9f4671e011eb66ba9594bed47570eda3eed66))
|
||||||
|
* **dbml:** dbml note syntax ([#826](https://github.com/chartdb/chartdb/issues/826)) ([337f7cd](https://github.com/chartdb/chartdb/commit/337f7cdab4759d15cb4d25a8c0e9394e99ba33d4))
|
||||||
|
* **dbml:** fix dbml output format ([#815](https://github.com/chartdb/chartdb/issues/815)) ([eed104b](https://github.com/chartdb/chartdb/commit/eed104be5ba2b7d9940ffac38e7877722ad764fc))
|
||||||
|
* **dbml:** fix schemas with same table names ([#828](https://github.com/chartdb/chartdb/issues/828)) ([0c300e5](https://github.com/chartdb/chartdb/commit/0c300e5e72cc5ff22cac42f8dbaed167061157c6))
|
||||||
|
* **dbml:** import dbml notes (table + fields) ([#827](https://github.com/chartdb/chartdb/issues/827)) ([b9a1e78](https://github.com/chartdb/chartdb/commit/b9a1e78b53c932c0b1a12ee38b62494a5c2f9348))
|
||||||
|
* **dbml:** support multiple relationships on same field in inline DBML ([#822](https://github.com/chartdb/chartdb/issues/822)) ([a5f8e56](https://github.com/chartdb/chartdb/commit/a5f8e56b3ca97b851b6953481644d3a3ff7ce882))
|
||||||
|
* **dbml:** support spaces in names ([#794](https://github.com/chartdb/chartdb/issues/794)) ([8f27f10](https://github.com/chartdb/chartdb/commit/8f27f10dec96af400dc2c12a30b22b3a346803a9))
|
||||||
|
* fix hotkeys on form elements ([#778](https://github.com/chartdb/chartdb/issues/778)) ([43d1dff](https://github.com/chartdb/chartdb/commit/43d1dfff71f2b960358a79b0112b78d11df91fb7))
|
||||||
|
* fix screen freeze after schema select ([#800](https://github.com/chartdb/chartdb/issues/800)) ([8aeb1df](https://github.com/chartdb/chartdb/commit/8aeb1df0ad353c49e91243453f24bfa5921a89ab))
|
||||||
|
* **i18n:** add Croatian (hr) language support ([#802](https://github.com/chartdb/chartdb/issues/802)) ([2eb48e7](https://github.com/chartdb/chartdb/commit/2eb48e75d303d622f51327d22502a6f78e7fb32d))
|
||||||
|
* improve SQL export formatting and add schema-aware FK grouping ([#783](https://github.com/chartdb/chartdb/issues/783)) ([6df588f](https://github.com/chartdb/chartdb/commit/6df588f40e6e7066da6125413b94466429d48767))
|
||||||
|
* lost in canvas button animation ([#793](https://github.com/chartdb/chartdb/issues/793)) ([a93ec2c](https://github.com/chartdb/chartdb/commit/a93ec2cab906d0e4431d8d1668adcf2dbfc3c80f))
|
||||||
|
* **readonly:** fix zoom out on readonly ([#818](https://github.com/chartdb/chartdb/issues/818)) ([8ffde62](https://github.com/chartdb/chartdb/commit/8ffde62c1a00893c4bf6b4dd39068df530375416))
|
||||||
|
* remove error lag after autofix ([#764](https://github.com/chartdb/chartdb/issues/764)) ([bf32c08](https://github.com/chartdb/chartdb/commit/bf32c08d37c02ee6d7946a41633bb97b2271fcb7))
|
||||||
|
* remove unnecessary import ([#791](https://github.com/chartdb/chartdb/issues/791)) ([87836e5](https://github.com/chartdb/chartdb/commit/87836e53d145b825f9c4f80abca72f418df50e6c))
|
||||||
|
* **scroll:** disable scroll x behavior ([#795](https://github.com/chartdb/chartdb/issues/795)) ([4bc71c5](https://github.com/chartdb/chartdb/commit/4bc71c52ff5c462800d8530b72a5aadb7d7f85ed))
|
||||||
|
* set focus on filter search ([#775](https://github.com/chartdb/chartdb/issues/775)) ([9949a46](https://github.com/chartdb/chartdb/commit/9949a46ee3ba7f46a2ea7f2c0d7101cc9336df4f))
|
||||||
|
* solve issue with multiple render of tables ([#823](https://github.com/chartdb/chartdb/issues/823)) ([0c7eaa2](https://github.com/chartdb/chartdb/commit/0c7eaa2df20cfb6994b7e6251c760a2d4581c879))
|
||||||
|
* **sql-export:** escape newlines and quotes in multi-line comments ([#765](https://github.com/chartdb/chartdb/issues/765)) ([f7f9290](https://github.com/chartdb/chartdb/commit/f7f92903def84a94ac0c66f625f96a6681383945))
|
||||||
|
* **sql-server:** improvment for sql-server import via sql script ([#789](https://github.com/chartdb/chartdb/issues/789)) ([79b8855](https://github.com/chartdb/chartdb/commit/79b885502e3385e996a52093a3ccd5f6e469993a))
|
||||||
|
* **table-node:** fix comment icon on field ([#786](https://github.com/chartdb/chartdb/issues/786)) ([745bdee](https://github.com/chartdb/chartdb/commit/745bdee86d07f1e9c3a2d24237c48c25b9a8eeea))
|
||||||
|
* **table-node:** improve field spacing ([#785](https://github.com/chartdb/chartdb/issues/785)) ([08eb9cc](https://github.com/chartdb/chartdb/commit/08eb9cc55f0077f53afea6f9ce720341e1a583c2))
|
||||||
|
* **table-select:** add loading indication for import ([#782](https://github.com/chartdb/chartdb/issues/782)) ([b46ed58](https://github.com/chartdb/chartdb/commit/b46ed58dff1ec74579fb1544dba46b0f77730c52))
|
||||||
|
* **ui:** reduce spacing between primary key icon and short field types ([#816](https://github.com/chartdb/chartdb/issues/816)) ([984b2ae](https://github.com/chartdb/chartdb/commit/984b2aeee22c43cb9bda77df2c22087973079af4))
|
||||||
|
* update MariaDB database import smart query ([#792](https://github.com/chartdb/chartdb/issues/792)) ([386e40a](https://github.com/chartdb/chartdb/commit/386e40a0bf93d9aef1486bb1e729d8f485e675eb))
|
||||||
|
* update multiple schemas toast to require user action ([#771](https://github.com/chartdb/chartdb/issues/771)) ([f56fab9](https://github.com/chartdb/chartdb/commit/f56fab9876fb9fc46c6c708231324a90d8a7851d))
|
||||||
|
* update relationship when table width changes via expand/shrink ([#825](https://github.com/chartdb/chartdb/issues/825)) ([bc52933](https://github.com/chartdb/chartdb/commit/bc52933b58bfe6bc73779d9401128254cbf497d5))
|
||||||
|
|
||||||
|
## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)
|
||||||
|
|
||||||
|
|
||||||
|
### 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)
|
## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ FROM node:22-alpine AS builder
|
|||||||
ARG VITE_OPENAI_API_KEY
|
ARG VITE_OPENAI_API_KEY
|
||||||
ARG VITE_OPENAI_API_ENDPOINT
|
ARG VITE_OPENAI_API_ENDPOINT
|
||||||
ARG VITE_LLM_MODEL_NAME
|
ARG VITE_LLM_MODEL_NAME
|
||||||
ARG VITE_HIDE_BUCKLE_DOT_DEV
|
ARG VITE_HIDE_CHARTDB_CLOUD
|
||||||
|
ARG VITE_DISABLE_ANALYTICS
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ COPY . .
|
|||||||
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
|
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
|
||||||
echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
|
echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
|
||||||
echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .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
|
RUN npm run build
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ docker run \
|
|||||||
-p 8080:80 chartdb
|
-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.
|
> **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`.
|
Open your browser and navigate to `http://localhost:8080`.
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ server {
|
|||||||
OPENAI_API_KEY: \"$OPENAI_API_KEY\",
|
OPENAI_API_KEY: \"$OPENAI_API_KEY\",
|
||||||
OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
|
OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
|
||||||
LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
|
LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
|
||||||
HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\"
|
HIDE_CHARTDB_CLOUD: \"$HIDE_CHARTDB_CLOUD\",
|
||||||
|
DISABLE_ANALYTICS: \"$DISABLE_ANALYTICS\"
|
||||||
};";
|
};";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Replace placeholders in nginx.conf
|
# 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
|
# Start Nginx
|
||||||
nginx -g "daemon off;"
|
nginx -g "daemon off;"
|
||||||
|
|||||||
20
index.html
20
index.html
@@ -13,11 +13,21 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<script src="/config.js"></script>
|
<script src="/config.js"></script>
|
||||||
<script
|
<script>
|
||||||
src="https://cdn.usefathom.com/script.js"
|
// Load analytics only if not disabled
|
||||||
data-site="PRHIVBNN"
|
(function() {
|
||||||
defer
|
const disableAnalytics = (window.env && window.env.DISABLE_ANALYTICS === 'true') ||
|
||||||
></script>
|
(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.defer = true;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
1070
package-lock.json
generated
1070
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chartdb",
|
"name": "chartdb",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.12.0",
|
"version": "1.14.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -9,7 +9,11 @@
|
|||||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "npm run lint -- --fix",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepare": "husky"
|
"prepare": "husky",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ci": "vitest run --reporter=verbose --bail=1",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:coverage": "vitest --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^0.0.51",
|
"@ai-sdk/openai": "^0.0.51",
|
||||||
@@ -32,14 +36,14 @@
|
|||||||
"@radix-ui/react-scroll-area": "1.2.0",
|
"@radix-ui/react-scroll-area": "1.2.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.2",
|
"@radix-ui/react-separator": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"@xyflow/react": "^12.3.1",
|
"@xyflow/react": "^12.8.2",
|
||||||
"ahooks": "^3.8.1",
|
"ahooks": "^3.8.1",
|
||||||
"ai": "^3.3.14",
|
"ai": "^3.3.14",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -50,8 +54,9 @@
|
|||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"i18next": "^23.14.0",
|
"i18next": "^23.14.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"lucide-react": "^0.441.0",
|
"lucide-react": "^0.525.0",
|
||||||
"monaco-editor": "^0.52.0",
|
"monaco-editor": "^0.52.0",
|
||||||
|
"motion": "^12.23.6",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"node-sql-parser": "^5.3.2",
|
"node-sql-parser": "^5.3.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -73,12 +78,16 @@
|
|||||||
"@eslint/compat": "^1.2.4",
|
"@eslint/compat": "^1.2.4",
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.16.0",
|
"@eslint/js": "^9.16.0",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/node": "^22.1.0",
|
"@types/node": "^22.1.0",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||||
"@typescript-eslint/parser": "^8.18.0",
|
"@typescript-eslint/parser": "^8.18.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"@vitest/ui": "^3.2.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
@@ -90,6 +99,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.7",
|
"eslint-plugin-react-refresh": "^0.4.7",
|
||||||
"eslint-plugin-tailwindcss": "^3.17.4",
|
"eslint-plugin-tailwindcss": "^3.17.4",
|
||||||
"globals": "^15.13.0",
|
"globals": "^15.13.0",
|
||||||
|
"happy-dom": "^18.0.1",
|
||||||
"husky": "^9.1.5",
|
"husky": "^9.1.5",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.40",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
@@ -97,6 +107,7 @@
|
|||||||
"tailwindcss": "^3.4.7",
|
"tailwindcss": "^3.4.7",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"unplugin-inject-preload": "^3.0.0",
|
"unplugin-inject-preload": "^3.0.0",
|
||||||
"vite": "^5.3.4"
|
"vite": "^5.3.4",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/assets/oracle_logo.png
Normal file
BIN
src/assets/oracle_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
src/assets/oracle_logo_2.png
Normal file
BIN
src/assets/oracle_logo_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/oracle_logo_dark.png
Normal file
BIN
src/assets/oracle_logo_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -31,6 +31,7 @@ export interface CodeSnippetAction {
|
|||||||
label: string;
|
label: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CodeSnippetProps {
|
export interface CodeSnippetProps {
|
||||||
@@ -43,6 +44,8 @@ export interface CodeSnippetProps {
|
|||||||
isComplete?: boolean;
|
isComplete?: boolean;
|
||||||
editorProps?: React.ComponentProps<EditorType>;
|
editorProps?: React.ComponentProps<EditorType>;
|
||||||
actions?: CodeSnippetAction[];
|
actions?: CodeSnippetAction[];
|
||||||
|
actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left';
|
||||||
|
allowCopy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||||
@@ -56,6 +59,8 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
isComplete = true,
|
isComplete = true,
|
||||||
editorProps,
|
editorProps,
|
||||||
actions,
|
actions,
|
||||||
|
actionsTooltipSide,
|
||||||
|
allowCopy = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const monaco = useMonaco();
|
const monaco = useMonaco();
|
||||||
@@ -129,6 +134,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
<Suspense fallback={<Spinner />}>
|
<Suspense fallback={<Spinner />}>
|
||||||
{isComplete ? (
|
{isComplete ? (
|
||||||
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
|
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
|
||||||
|
{allowCopy ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
onOpenChange={setTooltipOpen}
|
onOpenChange={setTooltipOpen}
|
||||||
open={isCopied || tooltipOpen}
|
open={isCopied || tooltipOpen}
|
||||||
@@ -148,7 +154,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent
|
||||||
|
side={actionsTooltipSide}
|
||||||
|
>
|
||||||
{t(
|
{t(
|
||||||
isCopied
|
isCopied
|
||||||
? 'copied'
|
? 'copied'
|
||||||
@@ -156,6 +164,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
)}
|
)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{actions &&
|
{actions &&
|
||||||
actions.length > 0 &&
|
actions.length > 0 &&
|
||||||
@@ -164,7 +173,10 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
className="h-fit p-1.5"
|
className={cn(
|
||||||
|
'h-fit p-1.5',
|
||||||
|
action.className
|
||||||
|
)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
>
|
>
|
||||||
@@ -174,7 +186,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent
|
||||||
|
side={actionsTooltipSide}
|
||||||
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
51
src/components/code-snippet/dbml/utils.ts
Normal file
51
src/components/code-snippet/dbml/utils.ts
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -37,18 +37,28 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
|
|||||||
const datatypePattern = dataTypesNames.join('|');
|
const datatypePattern = dataTypesNames.join('|');
|
||||||
|
|
||||||
monaco.languages.setMonarchTokensProvider('dbml', {
|
monaco.languages.setMonarchTokensProvider('dbml', {
|
||||||
keywords: ['Table', 'Ref', 'Indexes'],
|
keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
|
||||||
datatypes: dataTypesNames,
|
datatypes: dataTypesNames,
|
||||||
tokenizer: {
|
tokenizer: {
|
||||||
root: [
|
root: [
|
||||||
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
|
[
|
||||||
|
/\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
|
||||||
|
'keyword',
|
||||||
|
],
|
||||||
[/\[.*?\]/, 'annotation'],
|
[/\[.*?\]/, 'annotation'],
|
||||||
|
[/'''/, 'string', '@tripleQuoteString'],
|
||||||
[/".*?"/, 'string'],
|
[/".*?"/, 'string'],
|
||||||
[/'.*?'/, 'string'],
|
[/'.*?'/, 'string'],
|
||||||
|
[/`.*?`/, 'string'],
|
||||||
[/[{}]/, 'delimiter'],
|
[/[{}]/, 'delimiter'],
|
||||||
[/[<>]/, 'operator'],
|
[/[<>]/, 'operator'],
|
||||||
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
|
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
|
||||||
],
|
],
|
||||||
|
tripleQuoteString: [
|
||||||
|
[/[^']+/, 'string'],
|
||||||
|
[/'''/, 'string', '@pop'],
|
||||||
|
[/'/, 'string'],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const DiagramIcon = React.forwardRef<
|
|||||||
<TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild>
|
<TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild>
|
||||||
<img
|
<img
|
||||||
src={databaseEditionToImageMap[databaseEdition]}
|
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"
|
alt="database"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
@@ -42,7 +42,7 @@ export const DiagramIcon = React.forwardRef<
|
|||||||
<TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild>
|
<TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild>
|
||||||
<img
|
<img
|
||||||
src={databaseSecondaryLogoMap[databaseType]}
|
src={databaseSecondaryLogoMap[databaseType]}
|
||||||
className={cn('h-5 max-w-fit', imgClassName)}
|
className={cn('max-h-5 max-w-5', imgClassName)}
|
||||||
alt="database"
|
alt="database"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const EmptyState = forwardRef<
|
|||||||
</Label>
|
</Label>
|
||||||
<Label
|
<Label
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-sm font-normal text-muted-foreground',
|
'text-sm text-center font-normal text-muted-foreground',
|
||||||
descriptionClassName
|
descriptionClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
121
src/components/pagination/pagination.tsx
Normal file
121
src/components/pagination/pagination.tsx
Normal 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,
|
||||||
|
};
|
||||||
@@ -26,6 +26,7 @@ export interface SelectBoxOption {
|
|||||||
description?: string;
|
description?: string;
|
||||||
regex?: string;
|
regex?: string;
|
||||||
extractRegex?: RegExp;
|
extractRegex?: RegExp;
|
||||||
|
group?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectBoxProps {
|
export interface SelectBoxProps {
|
||||||
@@ -51,6 +52,7 @@ export interface SelectBoxProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
popoverClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||||
@@ -75,6 +77,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
disabled,
|
disabled,
|
||||||
open,
|
open,
|
||||||
onOpenChange: setOpen,
|
onOpenChange: setOpen,
|
||||||
|
popoverClassName,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@@ -90,6 +93,8 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
(isOpen: boolean) => {
|
(isOpen: boolean) => {
|
||||||
setOpen?.(isOpen);
|
setOpen?.(isOpen);
|
||||||
setIsOpen(isOpen);
|
setIsOpen(isOpen);
|
||||||
|
|
||||||
|
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
|
||||||
},
|
},
|
||||||
[setOpen]
|
[setOpen]
|
||||||
);
|
);
|
||||||
@@ -175,6 +180,101 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
[isOpen, onOpenChange]
|
[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 (
|
return (
|
||||||
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
|
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
|
||||||
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
|
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
|
||||||
@@ -245,7 +345,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<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"
|
align="center"
|
||||||
>
|
>
|
||||||
<Command
|
<Command
|
||||||
@@ -317,95 +420,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
|
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<div className="max-h-64 w-full">
|
<div className="max-h-64 w-full">
|
||||||
<CommandGroup>
|
|
||||||
<CommandList className="max-h-fit w-full">
|
<CommandList className="max-h-fit w-full">
|
||||||
{options.map((option) => {
|
{hasGroups
|
||||||
const isSelected =
|
? Object.entries(groups).map(
|
||||||
Array.isArray(value) &&
|
([groupName, groupOptions]) => (
|
||||||
value.includes(option.value);
|
<CommandGroup
|
||||||
|
key={groupName}
|
||||||
const isRegexMatch =
|
heading={groupName}
|
||||||
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 && (
|
{groupOptions.map(
|
||||||
<div
|
renderOption
|
||||||
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>
|
</CommandGroup>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: options.map(renderOption)}
|
||||||
|
</CommandList>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Command>
|
</Command>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function Toaster() {
|
|||||||
description,
|
description,
|
||||||
action,
|
action,
|
||||||
layout = 'row',
|
layout = 'row',
|
||||||
|
hideCloseButton = false,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -38,7 +39,7 @@ export function Toaster() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{layout === 'row' ? action : null}
|
{layout === 'row' ? action : null}
|
||||||
<ToastClose />
|
{!hideCloseButton ? <ToastClose /> : null}
|
||||||
</Toast>
|
</Toast>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type ToasterToast = ToastProps & {
|
|||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
action?: ToastActionElement;
|
action?: ToastActionElement;
|
||||||
layout?: 'row' | 'column';
|
layout?: 'row' | 'column';
|
||||||
|
hideCloseButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|||||||
17
src/components/tree-view/tree-item-skeleton.tsx
Normal file
17
src/components/tree-view/tree-item-skeleton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
461
src/components/tree-view/tree-view.tsx
Normal file
461
src/components/tree-view/tree-view.tsx
Normal 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 };
|
||||||
|
}
|
||||||
41
src/components/tree-view/tree.ts
Normal file
41
src/components/tree-view/tree.ts
Normal 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>>;
|
||||||
|
}
|
||||||
153
src/components/tree-view/use-tree.ts
Normal file
153
src/components/tree-view/use-tree.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ export interface CanvasContext {
|
|||||||
}) => void;
|
}) => void;
|
||||||
setOverlapGraph: (graph: Graph<string>) => void;
|
setOverlapGraph: (graph: Graph<string>) => void;
|
||||||
overlapGraph: Graph<string>;
|
overlapGraph: Graph<string>;
|
||||||
|
setShowFilter: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
showFilter: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const canvasContext = createContext<CanvasContext>({
|
export const canvasContext = createContext<CanvasContext>({
|
||||||
@@ -19,4 +21,6 @@ export const canvasContext = createContext<CanvasContext>({
|
|||||||
fitView: emptyFn,
|
fitView: emptyFn,
|
||||||
setOverlapGraph: emptyFn,
|
setOverlapGraph: emptyFn,
|
||||||
overlapGraph: createGraph(),
|
overlapGraph: createGraph(),
|
||||||
|
setShowFilter: emptyFn,
|
||||||
|
showFilter: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
|||||||
const [overlapGraph, setOverlapGraph] =
|
const [overlapGraph, setOverlapGraph] =
|
||||||
useState<Graph<string>>(createGraph());
|
useState<Graph<string>>(createGraph());
|
||||||
|
|
||||||
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
|
|
||||||
const reorderTables = useCallback(
|
const reorderTables = useCallback(
|
||||||
(
|
(
|
||||||
options: { updateHistory?: boolean } = {
|
options: { updateHistory?: boolean } = {
|
||||||
@@ -77,6 +79,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
|||||||
fitView,
|
fitView,
|
||||||
setOverlapGraph,
|
setOverlapGraph,
|
||||||
overlapGraph,
|
overlapGraph,
|
||||||
|
setShowFilter,
|
||||||
|
showFilter,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { DBSchema } from '@/lib/domain/db-schema';
|
|||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
export type ChartDBEventType =
|
export type ChartDBEventType =
|
||||||
| 'add_tables'
|
| 'add_tables'
|
||||||
@@ -72,10 +73,14 @@ export interface ChartDBContext {
|
|||||||
relationships: DBRelationship[];
|
relationships: DBRelationship[];
|
||||||
dependencies: DBDependency[];
|
dependencies: DBDependency[];
|
||||||
areas: Area[];
|
areas: Area[];
|
||||||
|
customTypes: DBCustomType[];
|
||||||
currentDiagram: Diagram;
|
currentDiagram: Diagram;
|
||||||
events: EventEmitter<ChartDBEvent>;
|
events: EventEmitter<ChartDBEvent>;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
|
||||||
|
highlightedCustomType?: DBCustomType;
|
||||||
|
highlightCustomTypeId: (id?: string) => void;
|
||||||
|
|
||||||
filteredSchemas?: string[];
|
filteredSchemas?: string[];
|
||||||
filterSchemas: (schemaIds: string[]) => void;
|
filterSchemas: (schemaIds: string[]) => void;
|
||||||
|
|
||||||
@@ -90,6 +95,10 @@ export interface ChartDBContext {
|
|||||||
updateDiagramUpdatedAt: () => Promise<void>;
|
updateDiagramUpdatedAt: () => Promise<void>;
|
||||||
clearDiagramData: () => Promise<void>;
|
clearDiagramData: () => Promise<void>;
|
||||||
deleteDiagram: () => Promise<void>;
|
deleteDiagram: () => Promise<void>;
|
||||||
|
updateDiagramData: (
|
||||||
|
diagram: Diagram,
|
||||||
|
options?: { forceUpdateStorage?: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
// Database type operations
|
// Database type operations
|
||||||
updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
|
updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
|
||||||
@@ -248,6 +257,38 @@ export interface ChartDBContext {
|
|||||||
area: Partial<Area>,
|
area: Partial<Area>,
|
||||||
options?: { updateHistory: boolean }
|
options?: { updateHistory: boolean }
|
||||||
) => Promise<void>;
|
) => 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>;
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
hiddenTableIds?: string[];
|
||||||
|
addHiddenTableId: (tableId: string) => Promise<void>;
|
||||||
|
removeHiddenTableId: (tableId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chartDBContext = createContext<ChartDBContext>({
|
export const chartDBContext = createContext<ChartDBContext>({
|
||||||
@@ -258,7 +299,9 @@ export const chartDBContext = createContext<ChartDBContext>({
|
|||||||
relationships: [],
|
relationships: [],
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
areas: [],
|
areas: [],
|
||||||
|
customTypes: [],
|
||||||
schemas: [],
|
schemas: [],
|
||||||
|
highlightCustomTypeId: emptyFn,
|
||||||
filteredSchemas: [],
|
filteredSchemas: [],
|
||||||
filterSchemas: emptyFn,
|
filterSchemas: emptyFn,
|
||||||
currentDiagram: {
|
currentDiagram: {
|
||||||
@@ -278,6 +321,7 @@ export const chartDBContext = createContext<ChartDBContext>({
|
|||||||
loadDiagramFromData: emptyFn,
|
loadDiagramFromData: emptyFn,
|
||||||
clearDiagramData: emptyFn,
|
clearDiagramData: emptyFn,
|
||||||
deleteDiagram: emptyFn,
|
deleteDiagram: emptyFn,
|
||||||
|
updateDiagramData: emptyFn,
|
||||||
|
|
||||||
// Database type operations
|
// Database type operations
|
||||||
updateDatabaseType: emptyFn,
|
updateDatabaseType: emptyFn,
|
||||||
@@ -333,4 +377,18 @@ export const chartDBContext = createContext<ChartDBContext>({
|
|||||||
removeArea: emptyFn,
|
removeArea: emptyFn,
|
||||||
removeAreas: emptyFn,
|
removeAreas: emptyFn,
|
||||||
updateArea: emptyFn,
|
updateArea: emptyFn,
|
||||||
|
|
||||||
|
// Custom type operations
|
||||||
|
createCustomType: emptyFn,
|
||||||
|
addCustomType: emptyFn,
|
||||||
|
addCustomTypes: emptyFn,
|
||||||
|
getCustomType: emptyFn,
|
||||||
|
removeCustomType: emptyFn,
|
||||||
|
removeCustomTypes: emptyFn,
|
||||||
|
updateCustomType: emptyFn,
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
hiddenTableIds: [],
|
||||||
|
addHiddenTableId: emptyFn,
|
||||||
|
removeHiddenTableId: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import { deepCopy, generateId } from '@/lib/utils';
|
import { deepCopy, generateId } from '@/lib/utils';
|
||||||
import { randomColor } from '@/lib/colors';
|
import { randomColor } from '@/lib/colors';
|
||||||
@@ -25,6 +25,11 @@ import type { Area } from '@/lib/domain/area';
|
|||||||
import { storageInitialValue } from '../storage-context/storage-context';
|
import { storageInitialValue } from '../storage-context/storage-context';
|
||||||
import { useDiff } from '../diff-context/use-diff';
|
import { useDiff } from '../diff-context/use-diff';
|
||||||
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
||||||
|
import {
|
||||||
|
DBCustomTypeKind,
|
||||||
|
type DBCustomType,
|
||||||
|
} from '@/lib/domain/db-custom-type';
|
||||||
|
import { useConfig } from '@/hooks/use-config';
|
||||||
|
|
||||||
export interface ChartDBProviderProps {
|
export interface ChartDBProviderProps {
|
||||||
diagram?: Diagram;
|
diagram?: Diagram;
|
||||||
@@ -35,11 +40,17 @@ export const ChartDBProvider: React.FC<
|
|||||||
React.PropsWithChildren<ChartDBProviderProps>
|
React.PropsWithChildren<ChartDBProviderProps>
|
||||||
> = ({ children, diagram, readonly: readonlyProp }) => {
|
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||||
const { hasDiff } = useDiff();
|
const { hasDiff } = useDiff();
|
||||||
let db = useStorage();
|
const dbStorage = useStorage();
|
||||||
|
let db = dbStorage;
|
||||||
const events = useEventEmitter<ChartDBEvent>();
|
const events = useEventEmitter<ChartDBEvent>();
|
||||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||||
const { addUndoAction, resetRedoStack, resetUndoStack } =
|
const { addUndoAction, resetRedoStack, resetUndoStack } =
|
||||||
useRedoUndoStack();
|
useRedoUndoStack();
|
||||||
|
const {
|
||||||
|
getHiddenTablesForDiagram,
|
||||||
|
hideTableForDiagram,
|
||||||
|
unhideTableForDiagram,
|
||||||
|
} = useConfig();
|
||||||
const [diagramId, setDiagramId] = useState('');
|
const [diagramId, setDiagramId] = useState('');
|
||||||
const [diagramName, setDiagramName] = useState('');
|
const [diagramName, setDiagramName] = useState('');
|
||||||
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
|
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
|
||||||
@@ -58,8 +69,15 @@ export const ChartDBProvider: React.FC<
|
|||||||
diagram?.dependencies ?? []
|
diagram?.dependencies ?? []
|
||||||
);
|
);
|
||||||
const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
|
const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
|
||||||
|
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
|
||||||
|
diagram?.customTypes ?? []
|
||||||
|
);
|
||||||
|
const [hiddenTableIds, setHiddenTableIds] = useState<string[]>([]);
|
||||||
const { events: diffEvents } = useDiff();
|
const { events: diffEvents } = useDiff();
|
||||||
|
|
||||||
|
const [highlightedCustomTypeId, setHighlightedCustomTypeId] =
|
||||||
|
useState<string>();
|
||||||
|
|
||||||
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
||||||
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
|
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
|
||||||
setTables((tables) =>
|
setTables((tables) =>
|
||||||
@@ -78,6 +96,14 @@ export const ChartDBProvider: React.FC<
|
|||||||
|
|
||||||
diffEvents.useSubscription(diffCalculatedHandler);
|
diffEvents.useSubscription(diffCalculatedHandler);
|
||||||
|
|
||||||
|
// Sync hiddenTableIds with config
|
||||||
|
useEffect(() => {
|
||||||
|
if (diagramId) {
|
||||||
|
const hiddenTables = getHiddenTablesForDiagram(diagramId);
|
||||||
|
setHiddenTableIds(hiddenTables);
|
||||||
|
}
|
||||||
|
}, [diagramId, getHiddenTablesForDiagram]);
|
||||||
|
|
||||||
const defaultSchemaName = defaultSchemas[databaseType];
|
const defaultSchemaName = defaultSchemas[databaseType];
|
||||||
|
|
||||||
const readonly = useMemo(
|
const readonly = useMemo(
|
||||||
@@ -155,6 +181,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
relationships,
|
relationships,
|
||||||
dependencies,
|
dependencies,
|
||||||
areas,
|
areas,
|
||||||
|
customTypes,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
diagramId,
|
diagramId,
|
||||||
@@ -165,6 +192,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
relationships,
|
relationships,
|
||||||
dependencies,
|
dependencies,
|
||||||
areas,
|
areas,
|
||||||
|
customTypes,
|
||||||
diagramCreatedAt,
|
diagramCreatedAt,
|
||||||
diagramUpdatedAt,
|
diagramUpdatedAt,
|
||||||
]
|
]
|
||||||
@@ -177,6 +205,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
setRelationships([]);
|
setRelationships([]);
|
||||||
setDependencies([]);
|
setDependencies([]);
|
||||||
setAreas([]);
|
setAreas([]);
|
||||||
|
setCustomTypes([]);
|
||||||
setDiagramUpdatedAt(updatedAt);
|
setDiagramUpdatedAt(updatedAt);
|
||||||
|
|
||||||
resetRedoStack();
|
resetRedoStack();
|
||||||
@@ -188,6 +217,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
db.deleteDiagramRelationships(diagramId),
|
db.deleteDiagramRelationships(diagramId),
|
||||||
db.deleteDiagramDependencies(diagramId),
|
db.deleteDiagramDependencies(diagramId),
|
||||||
db.deleteDiagramAreas(diagramId),
|
db.deleteDiagramAreas(diagramId),
|
||||||
|
db.deleteDiagramCustomTypes(diagramId),
|
||||||
]);
|
]);
|
||||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||||
|
|
||||||
@@ -201,6 +231,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
setRelationships([]);
|
setRelationships([]);
|
||||||
setDependencies([]);
|
setDependencies([]);
|
||||||
setAreas([]);
|
setAreas([]);
|
||||||
|
setCustomTypes([]);
|
||||||
resetRedoStack();
|
resetRedoStack();
|
||||||
resetUndoStack();
|
resetUndoStack();
|
||||||
|
|
||||||
@@ -210,6 +241,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
db.deleteDiagram(diagramId),
|
db.deleteDiagram(diagramId),
|
||||||
db.deleteDiagramDependencies(diagramId),
|
db.deleteDiagramDependencies(diagramId),
|
||||||
db.deleteDiagramAreas(diagramId),
|
db.deleteDiagramAreas(diagramId),
|
||||||
|
db.deleteDiagramCustomTypes(diagramId),
|
||||||
]);
|
]);
|
||||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||||
|
|
||||||
@@ -291,22 +323,27 @@ export const ChartDBProvider: React.FC<
|
|||||||
);
|
);
|
||||||
|
|
||||||
const addTables: ChartDBContext['addTables'] = useCallback(
|
const addTables: ChartDBContext['addTables'] = useCallback(
|
||||||
async (tables: DBTable[], options = { updateHistory: true }) => {
|
async (tablesToAdd: DBTable[], options = { updateHistory: true }) => {
|
||||||
setTables((currentTables) => [...currentTables, ...tables]);
|
setTables((currentTables) => [...currentTables, ...tablesToAdd]);
|
||||||
const updatedAt = new Date();
|
const updatedAt = new Date();
|
||||||
setDiagramUpdatedAt(updatedAt);
|
setDiagramUpdatedAt(updatedAt);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
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) {
|
if (options.updateHistory) {
|
||||||
addUndoAction({
|
addUndoAction({
|
||||||
action: 'addTables',
|
action: 'addTables',
|
||||||
redoData: { tables },
|
redoData: { tables: tablesToAdd },
|
||||||
undoData: { tableIds: tables.map((t) => t.id) },
|
undoData: { tableIds: tablesToAdd.map((t) => t.id) },
|
||||||
});
|
});
|
||||||
resetRedoStack();
|
resetRedoStack();
|
||||||
}
|
}
|
||||||
@@ -765,13 +802,23 @@ export const ChartDBProvider: React.FC<
|
|||||||
options = { updateHistory: true }
|
options = { updateHistory: true }
|
||||||
) => {
|
) => {
|
||||||
const fields = getTable(tableId)?.fields ?? [];
|
const fields = getTable(tableId)?.fields ?? [];
|
||||||
setTables((tables) =>
|
setTables((tables) => {
|
||||||
tables.map((table) =>
|
return tables.map((table) => {
|
||||||
table.id === tableId
|
if (table.id === tableId) {
|
||||||
? { ...table, fields: [...table.fields, field] }
|
db.updateTable({
|
||||||
: table
|
id: tableId,
|
||||||
)
|
attributes: {
|
||||||
);
|
...table,
|
||||||
|
fields: [...table.fields, field],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...table, fields: [...table.fields, field] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
events.emit({
|
events.emit({
|
||||||
action: 'add_field',
|
action: 'add_field',
|
||||||
@@ -792,13 +839,6 @@ export const ChartDBProvider: React.FC<
|
|||||||
setDiagramUpdatedAt(updatedAt);
|
setDiagramUpdatedAt(updatedAt);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||||
db.updateTable({
|
|
||||||
id: tableId,
|
|
||||||
attributes: {
|
|
||||||
...table,
|
|
||||||
fields: [...table.fields, field],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (options.updateHistory) {
|
if (options.updateHistory) {
|
||||||
@@ -1495,21 +1535,37 @@ export const ChartDBProvider: React.FC<
|
|||||||
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
|
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const highlightCustomTypeId = useCallback(
|
||||||
|
(id?: string) => setHighlightedCustomTypeId(id),
|
||||||
|
[setHighlightedCustomTypeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const highlightedCustomType = useMemo(() => {
|
||||||
|
return highlightedCustomTypeId
|
||||||
|
? customTypes.find((type) => type.id === highlightedCustomTypeId)
|
||||||
|
: undefined;
|
||||||
|
}, [highlightedCustomTypeId, customTypes]);
|
||||||
|
|
||||||
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
|
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
|
||||||
useCallback(
|
useCallback(
|
||||||
async (diagram) => {
|
(diagram) => {
|
||||||
setDiagramId(diagram.id);
|
setDiagramId(diagram.id);
|
||||||
setDiagramName(diagram.name);
|
setDiagramName(diagram.name);
|
||||||
setDatabaseType(diagram.databaseType);
|
setDatabaseType(diagram.databaseType);
|
||||||
setDatabaseEdition(diagram.databaseEdition);
|
setDatabaseEdition(diagram.databaseEdition);
|
||||||
setTables(diagram?.tables ?? []);
|
setTables(diagram.tables ?? []);
|
||||||
setRelationships(diagram?.relationships ?? []);
|
setRelationships(diagram.relationships ?? []);
|
||||||
setDependencies(diagram?.dependencies ?? []);
|
setDependencies(diagram.dependencies ?? []);
|
||||||
setAreas(diagram?.areas ?? []);
|
setAreas(diagram.areas ?? []);
|
||||||
|
setCustomTypes(diagram.customTypes ?? []);
|
||||||
setDiagramCreatedAt(diagram.createdAt);
|
setDiagramCreatedAt(diagram.createdAt);
|
||||||
setDiagramUpdatedAt(diagram.updatedAt);
|
setDiagramUpdatedAt(diagram.updatedAt);
|
||||||
|
setHighlightedCustomTypeId(undefined);
|
||||||
|
|
||||||
events.emit({ action: 'load_diagram', data: { diagram } });
|
events.emit({ action: 'load_diagram', data: { diagram } });
|
||||||
|
|
||||||
|
resetRedoStack();
|
||||||
|
resetUndoStack();
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
setDiagramId,
|
setDiagramId,
|
||||||
@@ -1520,12 +1576,26 @@ export const ChartDBProvider: React.FC<
|
|||||||
setRelationships,
|
setRelationships,
|
||||||
setDependencies,
|
setDependencies,
|
||||||
setAreas,
|
setAreas,
|
||||||
|
setCustomTypes,
|
||||||
setDiagramCreatedAt,
|
setDiagramCreatedAt,
|
||||||
setDiagramUpdatedAt,
|
setDiagramUpdatedAt,
|
||||||
|
setHighlightedCustomTypeId,
|
||||||
events,
|
events,
|
||||||
|
resetRedoStack,
|
||||||
|
resetUndoStack,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
|
||||||
|
async (diagram, options) => {
|
||||||
|
const st = options?.forceUpdateStorage ? dbStorage : db;
|
||||||
|
await st.deleteDiagram(diagram.id);
|
||||||
|
await st.addDiagram({ diagram });
|
||||||
|
loadDiagramFromData(diagram);
|
||||||
|
},
|
||||||
|
[db, dbStorage, loadDiagramFromData]
|
||||||
|
);
|
||||||
|
|
||||||
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
|
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
|
||||||
async (diagramId: string) => {
|
async (diagramId: string) => {
|
||||||
const diagram = await db.getDiagram(diagramId, {
|
const diagram = await db.getDiagram(diagramId, {
|
||||||
@@ -1533,6 +1603,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
includeTables: true,
|
includeTables: true,
|
||||||
includeDependencies: true,
|
includeDependencies: true,
|
||||||
includeAreas: true,
|
includeAreas: true,
|
||||||
|
includeCustomTypes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (diagram) {
|
if (diagram) {
|
||||||
@@ -1544,6 +1615,173 @@ export const ChartDBProvider: React.FC<
|
|||||||
[db, loadDiagramFromData]
|
[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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addHiddenTableId: ChartDBContext['addHiddenTableId'] = useCallback(
|
||||||
|
async (tableId: string) => {
|
||||||
|
if (!hiddenTableIds.includes(tableId)) {
|
||||||
|
setHiddenTableIds((prev) => [...prev, tableId]);
|
||||||
|
await hideTableForDiagram(diagramId, tableId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[hiddenTableIds, diagramId, hideTableForDiagram]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeHiddenTableId: ChartDBContext['removeHiddenTableId'] =
|
||||||
|
useCallback(
|
||||||
|
async (tableId: string) => {
|
||||||
|
if (hiddenTableIds.includes(tableId)) {
|
||||||
|
setHiddenTableIds((prev) =>
|
||||||
|
prev.filter((id) => id !== tableId)
|
||||||
|
);
|
||||||
|
await unhideTableForDiagram(diagramId, tableId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[hiddenTableIds, diagramId, unhideTableForDiagram]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<chartDBContext.Provider
|
<chartDBContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -1560,6 +1798,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
events,
|
events,
|
||||||
readonly,
|
readonly,
|
||||||
filterSchemas,
|
filterSchemas,
|
||||||
|
updateDiagramData,
|
||||||
updateDiagramId,
|
updateDiagramId,
|
||||||
updateDiagramName,
|
updateDiagramName,
|
||||||
loadDiagram,
|
loadDiagram,
|
||||||
@@ -1608,6 +1847,19 @@ export const ChartDBProvider: React.FC<
|
|||||||
removeArea,
|
removeArea,
|
||||||
removeAreas,
|
removeAreas,
|
||||||
updateArea,
|
updateArea,
|
||||||
|
customTypes,
|
||||||
|
createCustomType,
|
||||||
|
addCustomType,
|
||||||
|
addCustomTypes,
|
||||||
|
getCustomType,
|
||||||
|
removeCustomType,
|
||||||
|
removeCustomTypes,
|
||||||
|
updateCustomType,
|
||||||
|
hiddenTableIds,
|
||||||
|
addHiddenTableId,
|
||||||
|
removeHiddenTableId,
|
||||||
|
highlightCustomTypeId,
|
||||||
|
highlightedCustomType,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -8,9 +8,23 @@ export interface ConfigContext {
|
|||||||
config?: Partial<ChartDBConfig>;
|
config?: Partial<ChartDBConfig>;
|
||||||
updateFn?: (config: ChartDBConfig) => ChartDBConfig;
|
updateFn?: (config: ChartDBConfig) => ChartDBConfig;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
|
getHiddenTablesForDiagram: (diagramId: string) => string[];
|
||||||
|
setHiddenTablesForDiagram: (
|
||||||
|
diagramId: string,
|
||||||
|
hiddenTableIds: string[]
|
||||||
|
) => Promise<void>;
|
||||||
|
hideTableForDiagram: (diagramId: string, tableId: string) => Promise<void>;
|
||||||
|
unhideTableForDiagram: (
|
||||||
|
diagramId: string,
|
||||||
|
tableId: string
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfigContext = createContext<ConfigContext>({
|
export const ConfigContext = createContext<ConfigContext>({
|
||||||
config: undefined,
|
config: undefined,
|
||||||
updateConfig: emptyFn,
|
updateConfig: emptyFn,
|
||||||
|
getHiddenTablesForDiagram: () => [],
|
||||||
|
setHiddenTablesForDiagram: emptyFn,
|
||||||
|
hideTableForDiagram: emptyFn,
|
||||||
|
unhideTableForDiagram: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,8 +44,86 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
return promise;
|
return promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getHiddenTablesForDiagram = (diagramId: string): string[] => {
|
||||||
|
return config?.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const setHiddenTablesForDiagram = async (
|
||||||
|
diagramId: string,
|
||||||
|
hiddenTableIds: string[]
|
||||||
|
): Promise<void> => {
|
||||||
|
return updateConfig({
|
||||||
|
updateFn: (currentConfig) => ({
|
||||||
|
...currentConfig,
|
||||||
|
hiddenTablesByDiagram: {
|
||||||
|
...currentConfig.hiddenTablesByDiagram,
|
||||||
|
[diagramId]: hiddenTableIds,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideTableForDiagram = async (
|
||||||
|
diagramId: string,
|
||||||
|
tableId: string
|
||||||
|
): Promise<void> => {
|
||||||
|
return updateConfig({
|
||||||
|
updateFn: (currentConfig) => {
|
||||||
|
const currentHiddenTables =
|
||||||
|
currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||||
|
if (currentHiddenTables.includes(tableId)) {
|
||||||
|
return currentConfig; // Already hidden, no change needed
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentConfig,
|
||||||
|
hiddenTablesByDiagram: {
|
||||||
|
...currentConfig.hiddenTablesByDiagram,
|
||||||
|
[diagramId]: [...currentHiddenTables, tableId],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const unhideTableForDiagram = async (
|
||||||
|
diagramId: string,
|
||||||
|
tableId: string
|
||||||
|
): Promise<void> => {
|
||||||
|
return updateConfig({
|
||||||
|
updateFn: (currentConfig) => {
|
||||||
|
const currentHiddenTables =
|
||||||
|
currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||||
|
const filteredTables = currentHiddenTables.filter(
|
||||||
|
(id) => id !== tableId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredTables.length === currentHiddenTables.length) {
|
||||||
|
return currentConfig; // Not hidden, no change needed
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentConfig,
|
||||||
|
hiddenTablesByDiagram: {
|
||||||
|
...currentConfig.hiddenTablesByDiagram,
|
||||||
|
[diagramId]: filteredTables,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigContext.Provider value={{ config, updateConfig }}>
|
<ConfigContext.Provider
|
||||||
|
value={{
|
||||||
|
config,
|
||||||
|
updateConfig,
|
||||||
|
getHiddenTablesForDiagram,
|
||||||
|
setHiddenTablesForDiagram,
|
||||||
|
hideTableForDiagram,
|
||||||
|
unhideTableForDiagram,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,14 +32,20 @@ export interface DiffContext {
|
|||||||
originalDiagram: Diagram | null;
|
originalDiagram: Diagram | null;
|
||||||
diffMap: DiffMap;
|
diffMap: DiffMap;
|
||||||
hasDiff: boolean;
|
hasDiff: boolean;
|
||||||
|
isSummaryOnly: boolean;
|
||||||
|
|
||||||
calculateDiff: ({
|
calculateDiff: ({
|
||||||
diagram,
|
diagram,
|
||||||
newDiagram,
|
newDiagram,
|
||||||
|
options,
|
||||||
}: {
|
}: {
|
||||||
diagram: Diagram;
|
diagram: Diagram;
|
||||||
newDiagram: Diagram;
|
newDiagram: Diagram;
|
||||||
|
options?: {
|
||||||
|
summaryOnly?: boolean;
|
||||||
|
};
|
||||||
}) => void;
|
}) => void;
|
||||||
|
resetDiff: () => void;
|
||||||
|
|
||||||
// table diff
|
// table diff
|
||||||
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
|
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
|
||||||
@@ -60,6 +66,15 @@ export interface DiffContext {
|
|||||||
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
|
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
|
||||||
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
|
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
|
||||||
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
|
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
|
||||||
|
getFieldNewPrimaryKey: ({ fieldId }: { fieldId: string }) => boolean | null;
|
||||||
|
getFieldNewNullable: ({ fieldId }: { fieldId: string }) => boolean | null;
|
||||||
|
getFieldNewCharacterMaximumLength: ({
|
||||||
|
fieldId,
|
||||||
|
}: {
|
||||||
|
fieldId: string;
|
||||||
|
}) => string | null;
|
||||||
|
getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null;
|
||||||
|
getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null;
|
||||||
|
|
||||||
// relationship diff
|
// relationship diff
|
||||||
checkIfNewRelationship: ({
|
checkIfNewRelationship: ({
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import type {
|
|||||||
} from './diff-context';
|
} from './diff-context';
|
||||||
import { diffContext } 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 type { Diagram } from '@/lib/domain/diagram';
|
||||||
import { useEventEmitter } from 'ahooks';
|
import { useEventEmitter } from 'ahooks';
|
||||||
import type { DBField } from '@/lib/domain/db-field';
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
@@ -29,6 +32,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const [fieldsChanged, setFieldsChanged] = React.useState<
|
const [fieldsChanged, setFieldsChanged] = React.useState<
|
||||||
Map<string, boolean>
|
Map<string, boolean>
|
||||||
>(new Map<string, boolean>());
|
>(new Map<string, boolean>());
|
||||||
|
const [isSummaryOnly, setIsSummaryOnly] = React.useState<boolean>(false);
|
||||||
|
|
||||||
const events = useEventEmitter<DiffEvent>();
|
const events = useEventEmitter<DiffEvent>();
|
||||||
|
|
||||||
@@ -124,7 +128,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
|
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
|
||||||
({ diagram, newDiagram: newDiagramArg }) => {
|
({ diagram, newDiagram: newDiagramArg, options }) => {
|
||||||
const {
|
const {
|
||||||
diffMap: newDiffs,
|
diffMap: newDiffs,
|
||||||
changedTables: newChangedTables,
|
changedTables: newChangedTables,
|
||||||
@@ -136,6 +140,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setFieldsChanged(newChangedFields);
|
setFieldsChanged(newChangedFields);
|
||||||
setNewDiagram(newDiagramArg);
|
setNewDiagram(newDiagramArg);
|
||||||
setOriginalDiagram(diagram);
|
setOriginalDiagram(diagram);
|
||||||
|
setIsSummaryOnly(options?.summaryOnly ?? false);
|
||||||
|
|
||||||
events.emit({
|
events.emit({
|
||||||
action: 'diff_calculated',
|
action: 'diff_calculated',
|
||||||
@@ -302,6 +307,117 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
[diffMap]
|
[diffMap]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getFieldNewPrimaryKey = useCallback<
|
||||||
|
DiffContext['getFieldNewPrimaryKey']
|
||||||
|
>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'primaryKey',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFieldNewNullable = useCallback<DiffContext['getFieldNewNullable']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'nullable',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFieldNewCharacterMaximumLength = useCallback<
|
||||||
|
DiffContext['getFieldNewCharacterMaximumLength']
|
||||||
|
>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'characterMaximumLength',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFieldNewScale = useCallback<DiffContext['getFieldNewScale']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'scale',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFieldNewPrecision = useCallback<
|
||||||
|
DiffContext['getFieldNewPrecision']
|
||||||
|
>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'precision',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
const checkIfNewRelationship = useCallback<
|
const checkIfNewRelationship = useCallback<
|
||||||
DiffContext['checkIfNewRelationship']
|
DiffContext['checkIfNewRelationship']
|
||||||
>(
|
>(
|
||||||
@@ -336,6 +452,15 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
[diffMap]
|
[diffMap]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const resetDiff = useCallback<DiffContext['resetDiff']>(() => {
|
||||||
|
setDiffMap(new Map<string, ChartDBDiff>());
|
||||||
|
setTablesChanged(new Map<string, boolean>());
|
||||||
|
setFieldsChanged(new Map<string, boolean>());
|
||||||
|
setNewDiagram(null);
|
||||||
|
setOriginalDiagram(null);
|
||||||
|
setIsSummaryOnly(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<diffContext.Provider
|
<diffContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -343,8 +468,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
originalDiagram,
|
originalDiagram,
|
||||||
diffMap,
|
diffMap,
|
||||||
hasDiff: diffMap.size > 0,
|
hasDiff: diffMap.size > 0,
|
||||||
|
isSummaryOnly,
|
||||||
|
|
||||||
calculateDiff,
|
calculateDiff,
|
||||||
|
resetDiff,
|
||||||
|
|
||||||
// table diff
|
// table diff
|
||||||
getTableNewName,
|
getTableNewName,
|
||||||
@@ -359,6 +486,11 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
checkIfNewField,
|
checkIfNewField,
|
||||||
getFieldNewName,
|
getFieldNewName,
|
||||||
getFieldNewType,
|
getFieldNewType,
|
||||||
|
getFieldNewPrimaryKey,
|
||||||
|
getFieldNewNullable,
|
||||||
|
getFieldNewCharacterMaximumLength,
|
||||||
|
getFieldNewScale,
|
||||||
|
getFieldNewPrecision,
|
||||||
|
|
||||||
// relationship diff
|
// relationship diff
|
||||||
checkIfNewRelationship,
|
checkIfNewRelationship,
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addAreas,
|
addAreas,
|
||||||
removeAreas,
|
removeAreas,
|
||||||
updateArea,
|
updateArea,
|
||||||
|
addCustomTypes,
|
||||||
|
removeCustomTypes,
|
||||||
|
updateCustomType,
|
||||||
} = useChartDB();
|
} = useChartDB();
|
||||||
|
|
||||||
const redoActionHandlers = useMemo(
|
const redoActionHandlers = useMemo(
|
||||||
@@ -119,6 +122,19 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
updateArea: ({ redoData: { areaId, area } }) => {
|
updateArea: ({ redoData: { areaId, area } }) => {
|
||||||
return updateArea(areaId, area, { updateHistory: false });
|
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,
|
addTables,
|
||||||
@@ -141,6 +157,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addAreas,
|
addAreas,
|
||||||
removeAreas,
|
removeAreas,
|
||||||
updateArea,
|
updateArea,
|
||||||
|
addCustomTypes,
|
||||||
|
removeCustomTypes,
|
||||||
|
updateCustomType,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -239,6 +258,19 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
updateArea: ({ undoData: { areaId, area } }) => {
|
updateArea: ({ undoData: { areaId, area } }) => {
|
||||||
return updateArea(areaId, area, { updateHistory: false });
|
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,
|
addTables,
|
||||||
@@ -261,6 +293,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addAreas,
|
addAreas,
|
||||||
removeAreas,
|
removeAreas,
|
||||||
updateArea,
|
updateArea,
|
||||||
|
addCustomTypes,
|
||||||
|
removeCustomTypes,
|
||||||
|
updateCustomType,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { DBIndex } from '@/lib/domain/db-index';
|
|||||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
type Action = keyof ChartDBContext;
|
type Action = keyof ChartDBContext;
|
||||||
|
|
||||||
@@ -142,6 +143,24 @@ type RedoUndoActionRemoveAreas = RedoUndoActionBase<
|
|||||||
{ areas: Area[] }
|
{ 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 =
|
export type RedoUndoAction =
|
||||||
| RedoUndoActionAddTables
|
| RedoUndoActionAddTables
|
||||||
| RedoUndoActionRemoveTables
|
| RedoUndoActionRemoveTables
|
||||||
@@ -162,7 +181,10 @@ export type RedoUndoAction =
|
|||||||
| RedoUndoActionRemoveDependencies
|
| RedoUndoActionRemoveDependencies
|
||||||
| RedoUndoActionAddAreas
|
| RedoUndoActionAddAreas
|
||||||
| RedoUndoActionUpdateArea
|
| RedoUndoActionUpdateArea
|
||||||
| RedoUndoActionRemoveAreas;
|
| RedoUndoActionRemoveAreas
|
||||||
|
| RedoUndoActionAddCustomTypes
|
||||||
|
| RedoUndoActionUpdateCustomType
|
||||||
|
| RedoUndoActionRemoveCustomTypes;
|
||||||
|
|
||||||
export type RedoActionData<T extends Action> = Extract<
|
export type RedoActionData<T extends Action> = Extract<
|
||||||
RedoUndoAction,
|
RedoUndoAction,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export enum KeyboardShortcutAction {
|
|||||||
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
||||||
SHOW_ALL = 'show_all',
|
SHOW_ALL = 'show_all',
|
||||||
TOGGLE_THEME = 'toggle_theme',
|
TOGGLE_THEME = 'toggle_theme',
|
||||||
|
TOGGLE_FILTER = 'toggle_filter',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeyboardShortcut {
|
export interface KeyboardShortcut {
|
||||||
@@ -71,6 +72,13 @@ export const keyboardShortcuts: Record<
|
|||||||
keyCombinationMac: 'meta+m',
|
keyCombinationMac: 'meta+m',
|
||||||
keyCombinationWin: 'ctrl+m',
|
keyCombinationWin: 'ctrl+m',
|
||||||
},
|
},
|
||||||
|
[KeyboardShortcutAction.TOGGLE_FILTER]: {
|
||||||
|
action: KeyboardShortcutAction.TOGGLE_FILTER,
|
||||||
|
keyCombinationLabelMac: '⌘F',
|
||||||
|
keyCombinationLabelWin: 'Ctrl+F',
|
||||||
|
keyCombinationMac: 'meta+f',
|
||||||
|
keyCombinationWin: 'ctrl+f',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface KeyboardShortcutForOS {
|
export interface KeyboardShortcutForOS {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export type SidebarSection =
|
|||||||
| 'tables'
|
| 'tables'
|
||||||
| 'relationships'
|
| 'relationships'
|
||||||
| 'dependencies'
|
| 'dependencies'
|
||||||
| 'areas';
|
| 'areas'
|
||||||
|
| 'customTypes';
|
||||||
|
|
||||||
export interface LayoutContext {
|
export interface LayoutContext {
|
||||||
openedTableInSidebar: string | undefined;
|
openedTableInSidebar: string | undefined;
|
||||||
@@ -24,6 +25,10 @@ export interface LayoutContext {
|
|||||||
openAreaFromSidebar: (areaId: string) => void;
|
openAreaFromSidebar: (areaId: string) => void;
|
||||||
closeAllAreasInSidebar: () => void;
|
closeAllAreasInSidebar: () => void;
|
||||||
|
|
||||||
|
openedCustomTypeInSidebar: string | undefined;
|
||||||
|
openCustomTypeFromSidebar: (customTypeId: string) => void;
|
||||||
|
closeAllCustomTypesInSidebar: () => void;
|
||||||
|
|
||||||
selectedSidebarSection: SidebarSection;
|
selectedSidebarSection: SidebarSection;
|
||||||
selectSidebarSection: (section: SidebarSection) => void;
|
selectSidebarSection: (section: SidebarSection) => void;
|
||||||
|
|
||||||
@@ -53,6 +58,10 @@ export const layoutContext = createContext<LayoutContext>({
|
|||||||
openAreaFromSidebar: emptyFn,
|
openAreaFromSidebar: emptyFn,
|
||||||
closeAllAreasInSidebar: emptyFn,
|
closeAllAreasInSidebar: emptyFn,
|
||||||
|
|
||||||
|
openedCustomTypeInSidebar: undefined,
|
||||||
|
openCustomTypeFromSidebar: emptyFn,
|
||||||
|
closeAllCustomTypesInSidebar: emptyFn,
|
||||||
|
|
||||||
selectSidebarSection: emptyFn,
|
selectSidebarSection: emptyFn,
|
||||||
openTableFromSidebar: emptyFn,
|
openTableFromSidebar: emptyFn,
|
||||||
closeAllTablesInSidebar: emptyFn,
|
closeAllTablesInSidebar: emptyFn,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
|
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
|
const [openedCustomTypeInSidebar, setOpenedCustomTypeInSidebar] =
|
||||||
|
React.useState<string | undefined>();
|
||||||
const [selectedSidebarSection, setSelectedSidebarSection] =
|
const [selectedSidebarSection, setSelectedSidebarSection] =
|
||||||
React.useState<SidebarSection>('tables');
|
React.useState<SidebarSection>('tables');
|
||||||
const [isSidePanelShowed, setIsSidePanelShowed] =
|
const [isSidePanelShowed, setIsSidePanelShowed] =
|
||||||
@@ -36,6 +38,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
|
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
|
||||||
() => setOpenedAreaInSidebar('');
|
() => setOpenedAreaInSidebar('');
|
||||||
|
|
||||||
|
const closeAllCustomTypesInSidebar: LayoutContext['closeAllCustomTypesInSidebar'] =
|
||||||
|
() => setOpenedCustomTypeInSidebar('');
|
||||||
|
|
||||||
const hideSidePanel: LayoutContext['hideSidePanel'] = () =>
|
const hideSidePanel: LayoutContext['hideSidePanel'] = () =>
|
||||||
setIsSidePanelShowed(false);
|
setIsSidePanelShowed(false);
|
||||||
|
|
||||||
@@ -76,6 +81,13 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setOpenedAreaInSidebar(areaId);
|
setOpenedAreaInSidebar(areaId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openCustomTypeFromSidebar: LayoutContext['openCustomTypeFromSidebar'] =
|
||||||
|
(customTypeId) => {
|
||||||
|
showSidePanel();
|
||||||
|
setSelectedSidebarSection('customTypes');
|
||||||
|
setOpenedTableInSidebar(customTypeId);
|
||||||
|
};
|
||||||
|
|
||||||
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
|
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
|
||||||
setIsSelectSchemaOpen(true);
|
setIsSelectSchemaOpen(true);
|
||||||
|
|
||||||
@@ -105,6 +117,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
openedAreaInSidebar,
|
openedAreaInSidebar,
|
||||||
openAreaFromSidebar,
|
openAreaFromSidebar,
|
||||||
closeAllAreasInSidebar,
|
closeAllAreasInSidebar,
|
||||||
|
openedCustomTypeInSidebar,
|
||||||
|
openCustomTypeFromSidebar,
|
||||||
|
closeAllCustomTypesInSidebar,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export interface LocalConfigContext {
|
|||||||
showCardinality: boolean;
|
showCardinality: boolean;
|
||||||
setShowCardinality: (showCardinality: boolean) => void;
|
setShowCardinality: (showCardinality: boolean) => void;
|
||||||
|
|
||||||
|
showFieldAttributes: boolean;
|
||||||
|
setShowFieldAttributes: (showFieldAttributes: boolean) => void;
|
||||||
|
|
||||||
hideMultiSchemaNotification: boolean;
|
hideMultiSchemaNotification: boolean;
|
||||||
setHideMultiSchemaNotification: (
|
setHideMultiSchemaNotification: (
|
||||||
hideMultiSchemaNotification: boolean
|
hideMultiSchemaNotification: boolean
|
||||||
@@ -50,6 +53,9 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
showCardinality: true,
|
showCardinality: true,
|
||||||
setShowCardinality: emptyFn,
|
setShowCardinality: emptyFn,
|
||||||
|
|
||||||
|
showFieldAttributes: true,
|
||||||
|
setShowFieldAttributes: emptyFn,
|
||||||
|
|
||||||
hideMultiSchemaNotification: false,
|
hideMultiSchemaNotification: false,
|
||||||
setHideMultiSchemaNotification: emptyFn,
|
setHideMultiSchemaNotification: emptyFn,
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const themeKey = 'theme';
|
|||||||
const scrollActionKey = 'scroll_action';
|
const scrollActionKey = 'scroll_action';
|
||||||
const schemasFilterKey = 'schemas_filter';
|
const schemasFilterKey = 'schemas_filter';
|
||||||
const showCardinalityKey = 'show_cardinality';
|
const showCardinalityKey = 'show_cardinality';
|
||||||
|
const showFieldAttributesKey = 'show_field_attributes';
|
||||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
||||||
const githubRepoOpenedKey = 'github_repo_opened';
|
const githubRepoOpenedKey = 'github_repo_opened';
|
||||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||||
@@ -34,6 +35,11 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [showFieldAttributes, setShowFieldAttributes] =
|
||||||
|
React.useState<boolean>(
|
||||||
|
(localStorage.getItem(showFieldAttributesKey) || 'true') === 'true'
|
||||||
|
);
|
||||||
|
|
||||||
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||||
React.useState<boolean>(
|
React.useState<boolean>(
|
||||||
(localStorage.getItem(hideMultiSchemaNotificationKey) ||
|
(localStorage.getItem(hideMultiSchemaNotificationKey) ||
|
||||||
@@ -119,6 +125,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setSchemasFilter,
|
setSchemasFilter,
|
||||||
showCardinality,
|
showCardinality,
|
||||||
setShowCardinality,
|
setShowCardinality,
|
||||||
|
showFieldAttributes,
|
||||||
|
setShowFieldAttributes,
|
||||||
hideMultiSchemaNotification,
|
hideMultiSchemaNotification,
|
||||||
setHideMultiSchemaNotification,
|
setHideMultiSchemaNotification,
|
||||||
setGithubRepoOpened,
|
setGithubRepoOpened,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { DBTable } from '@/lib/domain/db-table';
|
|||||||
import type { ChartDBConfig } from '@/lib/domain/config';
|
import type { ChartDBConfig } from '@/lib/domain/config';
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
export interface StorageContext {
|
export interface StorageContext {
|
||||||
// Config operations
|
// Config operations
|
||||||
@@ -19,6 +20,7 @@ export interface StorageContext {
|
|||||||
includeRelationships?: boolean;
|
includeRelationships?: boolean;
|
||||||
includeDependencies?: boolean;
|
includeDependencies?: boolean;
|
||||||
includeAreas?: boolean;
|
includeAreas?: boolean;
|
||||||
|
includeCustomTypes?: boolean;
|
||||||
}) => Promise<Diagram[]>;
|
}) => Promise<Diagram[]>;
|
||||||
getDiagram: (
|
getDiagram: (
|
||||||
id: string,
|
id: string,
|
||||||
@@ -27,6 +29,7 @@ export interface StorageContext {
|
|||||||
includeRelationships?: boolean;
|
includeRelationships?: boolean;
|
||||||
includeDependencies?: boolean;
|
includeDependencies?: boolean;
|
||||||
includeAreas?: boolean;
|
includeAreas?: boolean;
|
||||||
|
includeCustomTypes?: boolean;
|
||||||
}
|
}
|
||||||
) => Promise<Diagram | undefined>;
|
) => Promise<Diagram | undefined>;
|
||||||
updateDiagram: (params: {
|
updateDiagram: (params: {
|
||||||
@@ -103,6 +106,26 @@ export interface StorageContext {
|
|||||||
deleteArea: (params: { diagramId: string; id: string }) => Promise<void>;
|
deleteArea: (params: { diagramId: string; id: string }) => Promise<void>;
|
||||||
listAreas: (diagramId: string) => Promise<Area[]>;
|
listAreas: (diagramId: string) => Promise<Area[]>;
|
||||||
deleteDiagramAreas: (diagramId: string) => Promise<void>;
|
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 = {
|
export const storageInitialValue: StorageContext = {
|
||||||
@@ -143,6 +166,14 @@ export const storageInitialValue: StorageContext = {
|
|||||||
deleteArea: emptyFn,
|
deleteArea: emptyFn,
|
||||||
listAreas: emptyFn,
|
listAreas: emptyFn,
|
||||||
deleteDiagramAreas: emptyFn,
|
deleteDiagramAreas: emptyFn,
|
||||||
|
|
||||||
|
// Custom type operations
|
||||||
|
addCustomType: emptyFn,
|
||||||
|
getCustomType: emptyFn,
|
||||||
|
updateCustomType: emptyFn,
|
||||||
|
deleteCustomType: emptyFn,
|
||||||
|
listCustomTypes: emptyFn,
|
||||||
|
deleteDiagramCustomTypes: emptyFn,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const storageContext =
|
export const storageContext =
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import type { StorageContext } from './storage-context';
|
import type { StorageContext } from './storage-context';
|
||||||
import { storageContext } from './storage-context';
|
import { storageContext } from './storage-context';
|
||||||
import Dexie, { type EntityTable } from 'dexie';
|
import Dexie, { type EntityTable } from 'dexie';
|
||||||
@@ -9,11 +9,13 @@ import { determineCardinalities } from '@/lib/domain/db-relationship';
|
|||||||
import type { ChartDBConfig } from '@/lib/domain/config';
|
import type { ChartDBConfig } from '@/lib/domain/config';
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const db = new Dexie('ChartDB') as Dexie & {
|
const db = useMemo(() => {
|
||||||
|
const dexieDB = new Dexie('ChartDB') as Dexie & {
|
||||||
diagrams: EntityTable<
|
diagrams: EntityTable<
|
||||||
Diagram,
|
Diagram,
|
||||||
'id' // primary key "id" (for the typings only)
|
'id' // primary key "id" (for the typings only)
|
||||||
@@ -34,6 +36,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
Area & { diagramId: string },
|
Area & { diagramId: string },
|
||||||
'id' // primary key "id" (for the typings only)
|
'id' // primary key "id" (for the typings only)
|
||||||
>;
|
>;
|
||||||
|
db_custom_types: EntityTable<
|
||||||
|
DBCustomType & { diagramId: string },
|
||||||
|
'id' // primary key "id" (for the typings only)
|
||||||
|
>;
|
||||||
config: EntityTable<
|
config: EntityTable<
|
||||||
ChartDBConfig & { id: number },
|
ChartDBConfig & { id: number },
|
||||||
'id' // primary key "id" (for the typings only)
|
'id' // primary key "id" (for the typings only)
|
||||||
@@ -41,7 +47,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Schema declaration:
|
// Schema declaration:
|
||||||
db.version(1).stores({
|
dexieDB.version(1).stores({
|
||||||
diagrams: '++id, name, databaseType, createdAt, updatedAt',
|
diagrams: '++id, name, databaseType, createdAt, updatedAt',
|
||||||
db_tables:
|
db_tables:
|
||||||
'++id, diagramId, name, x, y, fields, indexes, color, createdAt, width',
|
'++id, diagramId, name, x, y, fields, indexes, color, createdAt, width',
|
||||||
@@ -50,7 +56,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
config: '++id, defaultDiagramId',
|
config: '++id, defaultDiagramId',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.version(2).upgrade((tx) =>
|
dexieDB.version(2).upgrade((tx) =>
|
||||||
tx
|
tx
|
||||||
.table<DBTable & { diagramId: string }>('db_tables')
|
.table<DBTable & { diagramId: string }>('db_tables')
|
||||||
.toCollection()
|
.toCollection()
|
||||||
@@ -66,7 +72,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
db.version(3).stores({
|
dexieDB.version(3).stores({
|
||||||
diagrams:
|
diagrams:
|
||||||
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||||
db_tables:
|
db_tables:
|
||||||
@@ -76,7 +82,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
config: '++id, defaultDiagramId',
|
config: '++id, defaultDiagramId',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.version(4).stores({
|
dexieDB.version(4).stores({
|
||||||
diagrams:
|
diagrams:
|
||||||
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||||
db_tables:
|
db_tables:
|
||||||
@@ -86,7 +92,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
config: '++id, defaultDiagramId',
|
config: '++id, defaultDiagramId',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.version(5).stores({
|
dexieDB.version(5).stores({
|
||||||
diagrams:
|
diagrams:
|
||||||
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||||
db_tables:
|
db_tables:
|
||||||
@@ -96,16 +102,18 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
config: '++id, defaultDiagramId',
|
config: '++id, defaultDiagramId',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.version(6).upgrade((tx) =>
|
dexieDB.version(6).upgrade((tx) =>
|
||||||
tx
|
tx
|
||||||
.table<DBRelationship & { diagramId: string }>('db_relationships')
|
.table<DBRelationship & { diagramId: string }>(
|
||||||
|
'db_relationships'
|
||||||
|
)
|
||||||
.toCollection()
|
.toCollection()
|
||||||
.modify((relationship, ref) => {
|
.modify((relationship, ref) => {
|
||||||
const {
|
const { sourceCardinality, targetCardinality } =
|
||||||
sourceCardinality,
|
determineCardinalities(
|
||||||
targetCardinality,
|
// @ts-expect-error string before
|
||||||
} = // @ts-expect-error string before
|
relationship.type ?? 'one_to_one'
|
||||||
determineCardinalities(relationship.type ?? 'one_to_one');
|
);
|
||||||
|
|
||||||
relationship.sourceCardinality = sourceCardinality;
|
relationship.sourceCardinality = sourceCardinality;
|
||||||
relationship.targetCardinality = targetCardinality;
|
relationship.targetCardinality = targetCardinality;
|
||||||
@@ -115,7 +123,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
db.version(7).stores({
|
dexieDB.version(7).stores({
|
||||||
diagrams:
|
diagrams:
|
||||||
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||||
db_tables:
|
db_tables:
|
||||||
@@ -127,7 +135,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
config: '++id, defaultDiagramId',
|
config: '++id, defaultDiagramId',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.version(8).stores({
|
dexieDB.version(8).stores({
|
||||||
diagrams:
|
diagrams:
|
||||||
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||||
db_tables:
|
db_tables:
|
||||||
@@ -139,7 +147,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
config: '++id, defaultDiagramId',
|
config: '++id, defaultDiagramId',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.version(9).upgrade((tx) =>
|
dexieDB.version(9).upgrade((tx) =>
|
||||||
tx
|
tx
|
||||||
.table<DBTable & { diagramId: string }>('db_tables')
|
.table<DBTable & { diagramId: string }>('db_tables')
|
||||||
.toCollection()
|
.toCollection()
|
||||||
@@ -147,13 +155,14 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
for (const field of table.fields) {
|
for (const field of table.fields) {
|
||||||
if (typeof field.nullable === 'string') {
|
if (typeof field.nullable === 'string') {
|
||||||
field.nullable =
|
field.nullable =
|
||||||
(field.nullable as string).toLowerCase() === 'true';
|
(field.nullable as string).toLowerCase() ===
|
||||||
|
'true';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
db.version(10).stores({
|
dexieDB.version(10).stores({
|
||||||
diagrams:
|
diagrams:
|
||||||
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||||
db_tables:
|
db_tables:
|
||||||
@@ -166,36 +175,329 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
config: '++id, defaultDiagramId',
|
config: '++id, defaultDiagramId',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.on('ready', async () => {
|
dexieDB.version(11).stores({
|
||||||
const config = await getConfig();
|
diagrams:
|
||||||
|
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||||
|
db_tables:
|
||||||
|
'++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
|
||||||
|
db_relationships:
|
||||||
|
'++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
|
||||||
|
db_dependencies:
|
||||||
|
'++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
|
||||||
|
areas: '++id, diagramId, name, x, y, width, height, color',
|
||||||
|
db_custom_types:
|
||||||
|
'++id, diagramId, schema, type, kind, values, fields',
|
||||||
|
config: '++id, defaultDiagramId',
|
||||||
|
});
|
||||||
|
|
||||||
|
dexieDB.on('ready', async () => {
|
||||||
|
const config = await dexieDB.config.get(1);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
const diagrams = await db.diagrams.toArray();
|
const diagrams = await dexieDB.diagrams.toArray();
|
||||||
|
|
||||||
await db.config.add({
|
await dexieDB.config.add({
|
||||||
id: 1,
|
id: 1,
|
||||||
defaultDiagramId: diagrams?.[0]?.id ?? '',
|
defaultDiagramId: diagrams?.[0]?.id ?? '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return dexieDB;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const getConfig: StorageContext['getConfig'] = async (): Promise<
|
const getConfig: StorageContext['getConfig'] =
|
||||||
ChartDBConfig | undefined
|
useCallback(async (): Promise<ChartDBConfig | undefined> => {
|
||||||
> => {
|
|
||||||
return await db.config.get(1);
|
return await db.config.get(1);
|
||||||
};
|
}, [db]);
|
||||||
|
|
||||||
const updateConfig: StorageContext['updateConfig'] = async (
|
const updateConfig: StorageContext['updateConfig'] = useCallback(
|
||||||
config: Partial<ChartDBConfig>
|
async (config) => {
|
||||||
) => {
|
|
||||||
await db.config.update(1, config);
|
await db.config.update(1, config);
|
||||||
};
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
const addDiagram: StorageContext['addDiagram'] = async ({
|
const addTable: StorageContext['addTable'] = useCallback(
|
||||||
diagram,
|
async ({ diagramId, table }) => {
|
||||||
}: {
|
await db.db_tables.add({
|
||||||
diagram: Diagram;
|
...table,
|
||||||
}) => {
|
diagramId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getTable: StorageContext['getTable'] = useCallback(
|
||||||
|
async ({ id, diagramId }): Promise<DBTable | undefined> => {
|
||||||
|
return await db.db_tables.get({ id, diagramId });
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteDiagramTables: StorageContext['deleteDiagramTables'] =
|
||||||
|
useCallback(
|
||||||
|
async (diagramId) => {
|
||||||
|
await db.db_tables
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateTable: StorageContext['updateTable'] = useCallback(
|
||||||
|
async ({ id, attributes }) => {
|
||||||
|
await db.db_tables.update(id, attributes);
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const putTable: StorageContext['putTable'] = useCallback(
|
||||||
|
async ({ diagramId, table }) => {
|
||||||
|
await db.db_tables.put({ ...table, diagramId });
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteTable: StorageContext['deleteTable'] = useCallback(
|
||||||
|
async ({ id, diagramId }) => {
|
||||||
|
await db.db_tables.where({ id, diagramId }).delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const listTables: StorageContext['listTables'] = useCallback(
|
||||||
|
async (diagramId): Promise<DBTable[]> => {
|
||||||
|
// Fetch all tables associated with the diagram
|
||||||
|
const tables = await db.db_tables
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return tables;
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addRelationship: StorageContext['addRelationship'] = useCallback(
|
||||||
|
async ({ diagramId, relationship }) => {
|
||||||
|
await db.db_relationships.add({
|
||||||
|
...relationship,
|
||||||
|
diagramId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteDiagramRelationships: StorageContext['deleteDiagramRelationships'] =
|
||||||
|
useCallback(
|
||||||
|
async (diagramId) => {
|
||||||
|
await db.db_relationships
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRelationship: StorageContext['getRelationship'] = useCallback(
|
||||||
|
async ({ id, diagramId }): Promise<DBRelationship | undefined> => {
|
||||||
|
return await db.db_relationships.get({ id, diagramId });
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateRelationship: StorageContext['updateRelationship'] =
|
||||||
|
useCallback(
|
||||||
|
async ({ id, attributes }) => {
|
||||||
|
await db.db_relationships.update(id, attributes);
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteRelationship: StorageContext['deleteRelationship'] =
|
||||||
|
useCallback(
|
||||||
|
async ({ id, diagramId }) => {
|
||||||
|
await db.db_relationships.where({ id, diagramId }).delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const listRelationships: StorageContext['listRelationships'] = useCallback(
|
||||||
|
async (diagramId): Promise<DBRelationship[]> => {
|
||||||
|
// Sort relationships alphabetically
|
||||||
|
return (
|
||||||
|
await db.db_relationships
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.toArray()
|
||||||
|
).sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addDependency: StorageContext['addDependency'] = useCallback(
|
||||||
|
async ({ diagramId, dependency }) => {
|
||||||
|
await db.db_dependencies.add({
|
||||||
|
...dependency,
|
||||||
|
diagramId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDependency: StorageContext['getDependency'] = useCallback(
|
||||||
|
async ({ diagramId, id }) => {
|
||||||
|
return await db.db_dependencies.get({ id, diagramId });
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateDependency: StorageContext['updateDependency'] = useCallback(
|
||||||
|
async ({ id, attributes }) => {
|
||||||
|
await db.db_dependencies.update(id, attributes);
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteDependency: StorageContext['deleteDependency'] = useCallback(
|
||||||
|
async ({ diagramId, id }) => {
|
||||||
|
await db.db_dependencies.where({ id, diagramId }).delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const listDependencies: StorageContext['listDependencies'] = useCallback(
|
||||||
|
async (diagramId) => {
|
||||||
|
return await db.db_dependencies
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.toArray();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteDiagramDependencies: StorageContext['deleteDiagramDependencies'] =
|
||||||
|
useCallback(
|
||||||
|
async (diagramId) => {
|
||||||
|
await db.db_dependencies
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addArea: StorageContext['addArea'] = useCallback(
|
||||||
|
async ({ area, diagramId }) => {
|
||||||
|
await db.areas.add({
|
||||||
|
...area,
|
||||||
|
diagramId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getArea: StorageContext['getArea'] = useCallback(
|
||||||
|
async ({ diagramId, id }) => {
|
||||||
|
return await db.areas.get({ id, diagramId });
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateArea: StorageContext['updateArea'] = useCallback(
|
||||||
|
async ({ id, attributes }) => {
|
||||||
|
await db.areas.update(id, attributes);
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteArea: StorageContext['deleteArea'] = useCallback(
|
||||||
|
async ({ diagramId, id }) => {
|
||||||
|
await db.areas.where({ id, diagramId }).delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const listAreas: StorageContext['listAreas'] = useCallback(
|
||||||
|
async (diagramId) => {
|
||||||
|
return await db.areas
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.toArray();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteDiagramAreas: StorageContext['deleteDiagramAreas'] =
|
||||||
|
useCallback(
|
||||||
|
async (diagramId) => {
|
||||||
|
await db.areas.where('diagramId').equals(diagramId).delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom type operations
|
||||||
|
const addCustomType: StorageContext['addCustomType'] = useCallback(
|
||||||
|
async ({ diagramId, customType }) => {
|
||||||
|
await db.db_custom_types.add({
|
||||||
|
...customType,
|
||||||
|
diagramId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCustomType: StorageContext['getCustomType'] = useCallback(
|
||||||
|
async ({ diagramId, id }): Promise<DBCustomType | undefined> => {
|
||||||
|
return await db.db_custom_types.get({ id, diagramId });
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateCustomType: StorageContext['updateCustomType'] = useCallback(
|
||||||
|
async ({ id, attributes }) => {
|
||||||
|
await db.db_custom_types.update(id, attributes);
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteCustomType: StorageContext['deleteCustomType'] = useCallback(
|
||||||
|
async ({ diagramId, id }) => {
|
||||||
|
await db.db_custom_types.where({ id, diagramId }).delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const listCustomTypes: StorageContext['listCustomTypes'] = useCallback(
|
||||||
|
async (diagramId): Promise<DBCustomType[]> => {
|
||||||
|
return (
|
||||||
|
await db.db_custom_types
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.toArray()
|
||||||
|
).sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteDiagramCustomTypes: StorageContext['deleteDiagramCustomTypes'] =
|
||||||
|
useCallback(
|
||||||
|
async (diagramId) => {
|
||||||
|
await db.db_custom_types
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.delete();
|
||||||
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addDiagram: StorageContext['addDiagram'] = useCallback(
|
||||||
|
async ({ diagram }) => {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
promises.push(
|
promises.push(
|
||||||
db.diagrams.add({
|
db.diagrams.add({
|
||||||
@@ -210,7 +512,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
|
|
||||||
const tables = diagram.tables ?? [];
|
const tables = diagram.tables ?? [];
|
||||||
promises.push(
|
promises.push(
|
||||||
...tables.map((table) => addTable({ diagramId: diagram.id, table }))
|
...tables.map((table) =>
|
||||||
|
addTable({ diagramId: diagram.id, table })
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const relationships = diagram.relationships ?? [];
|
const relationships = diagram.relationships ?? [];
|
||||||
@@ -232,20 +536,26 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
...areas.map((area) => addArea({ diagramId: diagram.id, area }))
|
...areas.map((area) => addArea({ diagramId: diagram.id, area }))
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(promises);
|
const customTypes = diagram.customTypes ?? [];
|
||||||
};
|
promises.push(
|
||||||
|
...customTypes.map((customType) =>
|
||||||
|
addCustomType({ diagramId: diagram.id, customType })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const listDiagrams: StorageContext['listDiagrams'] = async (
|
await Promise.all(promises);
|
||||||
options: {
|
},
|
||||||
includeTables?: boolean;
|
[db, addArea, addCustomType, addDependency, addRelationship, addTable]
|
||||||
includeRelationships?: boolean;
|
);
|
||||||
includeDependencies?: boolean;
|
|
||||||
includeAreas?: boolean;
|
const listDiagrams: StorageContext['listDiagrams'] = useCallback(
|
||||||
} = {
|
async (
|
||||||
|
options = {
|
||||||
includeRelationships: false,
|
includeRelationships: false,
|
||||||
includeTables: false,
|
includeTables: false,
|
||||||
includeDependencies: false,
|
includeDependencies: false,
|
||||||
includeAreas: false,
|
includeAreas: false,
|
||||||
|
includeCustomTypes: false,
|
||||||
}
|
}
|
||||||
): Promise<Diagram[]> => {
|
): Promise<Diagram[]> => {
|
||||||
let diagrams = await db.diagrams.toArray();
|
let diagrams = await db.diagrams.toArray();
|
||||||
@@ -262,7 +572,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
if (options.includeRelationships) {
|
if (options.includeRelationships) {
|
||||||
diagrams = await Promise.all(
|
diagrams = await Promise.all(
|
||||||
diagrams.map(async (diagram) => {
|
diagrams.map(async (diagram) => {
|
||||||
diagram.relationships = await listRelationships(diagram.id);
|
diagram.relationships = await listRelationships(
|
||||||
|
diagram.id
|
||||||
|
);
|
||||||
return diagram;
|
return diagram;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -271,7 +583,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
if (options.includeDependencies) {
|
if (options.includeDependencies) {
|
||||||
diagrams = await Promise.all(
|
diagrams = await Promise.all(
|
||||||
diagrams.map(async (diagram) => {
|
diagrams.map(async (diagram) => {
|
||||||
diagram.dependencies = await listDependencies(diagram.id);
|
diagram.dependencies = await listDependencies(
|
||||||
|
diagram.id
|
||||||
|
);
|
||||||
return diagram;
|
return diagram;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -286,21 +600,36 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return diagrams;
|
if (options.includeCustomTypes) {
|
||||||
};
|
diagrams = await Promise.all(
|
||||||
|
diagrams.map(async (diagram) => {
|
||||||
|
diagram.customTypes = await listCustomTypes(diagram.id);
|
||||||
|
return diagram;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const getDiagram: StorageContext['getDiagram'] = async (
|
return diagrams;
|
||||||
id: string,
|
},
|
||||||
options: {
|
[
|
||||||
includeTables?: boolean;
|
db,
|
||||||
includeRelationships?: boolean;
|
listAreas,
|
||||||
includeDependencies?: boolean;
|
listCustomTypes,
|
||||||
includeAreas?: boolean;
|
listDependencies,
|
||||||
} = {
|
listRelationships,
|
||||||
|
listTables,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDiagram: StorageContext['getDiagram'] = useCallback(
|
||||||
|
async (
|
||||||
|
id,
|
||||||
|
options = {
|
||||||
includeRelationships: false,
|
includeRelationships: false,
|
||||||
includeTables: false,
|
includeTables: false,
|
||||||
includeDependencies: false,
|
includeDependencies: false,
|
||||||
includeAreas: false,
|
includeAreas: false,
|
||||||
|
includeCustomTypes: false,
|
||||||
}
|
}
|
||||||
): Promise<Diagram | undefined> => {
|
): Promise<Diagram | undefined> => {
|
||||||
const diagram = await db.diagrams.get(id);
|
const diagram = await db.diagrams.get(id);
|
||||||
@@ -325,16 +654,24 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
diagram.areas = await listAreas(id);
|
diagram.areas = await listAreas(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return diagram;
|
if (options.includeCustomTypes) {
|
||||||
};
|
diagram.customTypes = await listCustomTypes(id);
|
||||||
|
}
|
||||||
|
|
||||||
const updateDiagram: StorageContext['updateDiagram'] = async ({
|
return diagram;
|
||||||
id,
|
},
|
||||||
attributes,
|
[
|
||||||
}: {
|
db,
|
||||||
id: string;
|
listAreas,
|
||||||
attributes: Partial<Diagram>;
|
listCustomTypes,
|
||||||
}) => {
|
listDependencies,
|
||||||
|
listRelationships,
|
||||||
|
listTables,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateDiagram: StorageContext['updateDiagram'] = useCallback(
|
||||||
|
async ({ id, attributes }) => {
|
||||||
await db.diagrams.update(id, attributes);
|
await db.diagrams.update(id, attributes);
|
||||||
|
|
||||||
if (attributes.id) {
|
if (attributes.id) {
|
||||||
@@ -351,234 +688,32 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
.where('diagramId')
|
.where('diagramId')
|
||||||
.equals(id)
|
.equals(id)
|
||||||
.modify({ diagramId: attributes.id }),
|
.modify({ diagramId: attributes.id }),
|
||||||
|
db.areas.where('diagramId').equals(id).modify({
|
||||||
|
diagramId: attributes.id,
|
||||||
|
}),
|
||||||
|
db.db_custom_types
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(id)
|
||||||
|
.modify({ diagramId: attributes.id }),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[db]
|
||||||
|
);
|
||||||
|
|
||||||
const deleteDiagram: StorageContext['deleteDiagram'] = async (
|
const deleteDiagram: StorageContext['deleteDiagram'] = useCallback(
|
||||||
id: string
|
async (id) => {
|
||||||
) => {
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.diagrams.delete(id),
|
db.diagrams.delete(id),
|
||||||
db.db_tables.where('diagramId').equals(id).delete(),
|
db.db_tables.where('diagramId').equals(id).delete(),
|
||||||
db.db_relationships.where('diagramId').equals(id).delete(),
|
db.db_relationships.where('diagramId').equals(id).delete(),
|
||||||
db.db_dependencies.where('diagramId').equals(id).delete(),
|
db.db_dependencies.where('diagramId').equals(id).delete(),
|
||||||
db.areas.where('diagramId').equals(id).delete(),
|
db.areas.where('diagramId').equals(id).delete(),
|
||||||
|
db.db_custom_types.where('diagramId').equals(id).delete(),
|
||||||
]);
|
]);
|
||||||
};
|
},
|
||||||
|
[db]
|
||||||
const addTable: StorageContext['addTable'] = async ({
|
);
|
||||||
diagramId,
|
|
||||||
table,
|
|
||||||
}: {
|
|
||||||
diagramId: string;
|
|
||||||
table: DBTable;
|
|
||||||
}) => {
|
|
||||||
await db.db_tables.add({
|
|
||||||
...table,
|
|
||||||
diagramId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTable: StorageContext['getTable'] = async ({
|
|
||||||
id,
|
|
||||||
diagramId,
|
|
||||||
}: {
|
|
||||||
diagramId: string;
|
|
||||||
id: string;
|
|
||||||
}): Promise<DBTable | undefined> => {
|
|
||||||
return await db.db_tables.get({ id, diagramId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDiagramTables: StorageContext['deleteDiagramTables'] = async (
|
|
||||||
diagramId: string
|
|
||||||
) => {
|
|
||||||
await db.db_tables.where('diagramId').equals(diagramId).delete();
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTable: StorageContext['updateTable'] = async ({
|
|
||||||
id,
|
|
||||||
attributes,
|
|
||||||
}) => {
|
|
||||||
await db.db_tables.update(id, attributes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const putTable: StorageContext['putTable'] = async ({
|
|
||||||
diagramId,
|
|
||||||
table,
|
|
||||||
}) => {
|
|
||||||
await db.db_tables.put({ ...table, diagramId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteTable: StorageContext['deleteTable'] = async ({
|
|
||||||
id,
|
|
||||||
diagramId,
|
|
||||||
}: {
|
|
||||||
id: string;
|
|
||||||
diagramId: string;
|
|
||||||
}) => {
|
|
||||||
await db.db_tables.where({ id, diagramId }).delete();
|
|
||||||
};
|
|
||||||
|
|
||||||
const listTables: StorageContext['listTables'] = async (
|
|
||||||
diagramId: string
|
|
||||||
): Promise<DBTable[]> => {
|
|
||||||
// Fetch all tables associated with the diagram
|
|
||||||
const tables = await db.db_tables
|
|
||||||
.where('diagramId')
|
|
||||||
.equals(diagramId)
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
return tables;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addRelationship: StorageContext['addRelationship'] = async ({
|
|
||||||
diagramId,
|
|
||||||
relationship,
|
|
||||||
}: {
|
|
||||||
diagramId: string;
|
|
||||||
relationship: DBRelationship;
|
|
||||||
}) => {
|
|
||||||
await db.db_relationships.add({
|
|
||||||
...relationship,
|
|
||||||
diagramId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDiagramRelationships: StorageContext['deleteDiagramRelationships'] =
|
|
||||||
async (diagramId: string) => {
|
|
||||||
await db.db_relationships
|
|
||||||
.where('diagramId')
|
|
||||||
.equals(diagramId)
|
|
||||||
.delete();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRelationship: StorageContext['getRelationship'] = async ({
|
|
||||||
id,
|
|
||||||
diagramId,
|
|
||||||
}: {
|
|
||||||
diagramId: string;
|
|
||||||
id: string;
|
|
||||||
}): Promise<DBRelationship | undefined> => {
|
|
||||||
return await db.db_relationships.get({ id, diagramId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateRelationship: StorageContext['updateRelationship'] = async ({
|
|
||||||
id,
|
|
||||||
attributes,
|
|
||||||
}: {
|
|
||||||
id: string;
|
|
||||||
attributes: Partial<DBRelationship>;
|
|
||||||
}) => {
|
|
||||||
await db.db_relationships.update(id, attributes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteRelationship: StorageContext['deleteRelationship'] = async ({
|
|
||||||
id,
|
|
||||||
diagramId,
|
|
||||||
}: {
|
|
||||||
id: string;
|
|
||||||
diagramId: string;
|
|
||||||
}) => {
|
|
||||||
await db.db_relationships.where({ id, diagramId }).delete();
|
|
||||||
};
|
|
||||||
|
|
||||||
const listRelationships: StorageContext['listRelationships'] = async (
|
|
||||||
diagramId: string
|
|
||||||
): Promise<DBRelationship[]> => {
|
|
||||||
// Sort relationships alphabetically
|
|
||||||
return (
|
|
||||||
await db.db_relationships
|
|
||||||
.where('diagramId')
|
|
||||||
.equals(diagramId)
|
|
||||||
.toArray()
|
|
||||||
).sort((a, b) => {
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addDependency: StorageContext['addDependency'] = async ({
|
|
||||||
diagramId,
|
|
||||||
dependency,
|
|
||||||
}) => {
|
|
||||||
await db.db_dependencies.add({
|
|
||||||
...dependency,
|
|
||||||
diagramId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDependency: StorageContext['getDependency'] = async ({
|
|
||||||
diagramId,
|
|
||||||
id,
|
|
||||||
}) => {
|
|
||||||
return await db.db_dependencies.get({ id, diagramId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateDependency: StorageContext['updateDependency'] = async ({
|
|
||||||
id,
|
|
||||||
attributes,
|
|
||||||
}) => {
|
|
||||||
await db.db_dependencies.update(id, attributes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDependency: StorageContext['deleteDependency'] = async ({
|
|
||||||
diagramId,
|
|
||||||
id,
|
|
||||||
}) => {
|
|
||||||
await db.db_dependencies.where({ id, diagramId }).delete();
|
|
||||||
};
|
|
||||||
|
|
||||||
const listDependencies: StorageContext['listDependencies'] = async (
|
|
||||||
diagramId
|
|
||||||
) => {
|
|
||||||
return await db.db_dependencies
|
|
||||||
.where('diagramId')
|
|
||||||
.equals(diagramId)
|
|
||||||
.toArray();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDiagramDependencies: StorageContext['deleteDiagramDependencies'] =
|
|
||||||
async (diagramId) => {
|
|
||||||
await db.db_dependencies
|
|
||||||
.where('diagramId')
|
|
||||||
.equals(diagramId)
|
|
||||||
.delete();
|
|
||||||
};
|
|
||||||
|
|
||||||
const addArea: StorageContext['addArea'] = async ({ area, diagramId }) => {
|
|
||||||
await db.areas.add({
|
|
||||||
...area,
|
|
||||||
diagramId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getArea: StorageContext['getArea'] = async ({ diagramId, id }) => {
|
|
||||||
return await db.areas.get({ id, diagramId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateArea: StorageContext['updateArea'] = async ({
|
|
||||||
id,
|
|
||||||
attributes,
|
|
||||||
}) => {
|
|
||||||
await db.areas.update(id, attributes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteArea: StorageContext['deleteArea'] = async ({
|
|
||||||
diagramId,
|
|
||||||
id,
|
|
||||||
}) => {
|
|
||||||
await db.areas.where({ id, diagramId }).delete();
|
|
||||||
};
|
|
||||||
|
|
||||||
const listAreas: StorageContext['listAreas'] = async (diagramId) => {
|
|
||||||
return await db.areas.where('diagramId').equals(diagramId).toArray();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDiagramAreas: StorageContext['deleteDiagramAreas'] = async (
|
|
||||||
diagramId
|
|
||||||
) => {
|
|
||||||
await db.areas.where('diagramId').equals(diagramId).delete();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<storageContext.Provider
|
<storageContext.Provider
|
||||||
@@ -615,6 +750,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
deleteArea,
|
deleteArea,
|
||||||
listAreas,
|
listAreas,
|
||||||
deleteDiagramAreas,
|
deleteDiagramAreas,
|
||||||
|
addCustomType,
|
||||||
|
getCustomType,
|
||||||
|
updateCustomType,
|
||||||
|
deleteCustomType,
|
||||||
|
listCustomTypes,
|
||||||
|
deleteDiagramCustomTypes,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import { emptyFn } from '@/lib/utils';
|
import { emptyFn } from '@/lib/utils';
|
||||||
|
import type { Theme, EffectiveTheme } from '@/lib/types';
|
||||||
export type Theme = 'light' | 'dark' | 'system';
|
export type { Theme, EffectiveTheme };
|
||||||
export type EffectiveTheme = Exclude<Theme, 'system'>;
|
|
||||||
|
|
||||||
export interface ThemeContext {
|
export interface ThemeContext {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
handleThemeToggle,
|
handleThemeToggle,
|
||||||
{
|
{
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
|
enableOnFormTags: true,
|
||||||
},
|
},
|
||||||
[handleThemeToggle]
|
[handleThemeToggle]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,22 @@ import type { OnChange } from '@monaco-editor/react';
|
|||||||
import { useDebounce } from '@/hooks/use-debounce-v2';
|
import { useDebounce } from '@/hooks/use-debounce-v2';
|
||||||
import { InstructionsSection } from './instructions-section/instructions-section';
|
import { InstructionsSection } from './instructions-section/instructions-section';
|
||||||
import { parseSQLError } from '@/lib/data/sql-import';
|
import { parseSQLError } from '@/lib/data/sql-import';
|
||||||
import type { editor } from 'monaco-editor';
|
import type { editor, IDisposable } from 'monaco-editor';
|
||||||
|
import { waitFor } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
validateSQL,
|
||||||
|
type ValidationResult,
|
||||||
|
} from '@/lib/data/sql-import/sql-validator';
|
||||||
|
import { SQLValidationStatus } from './sql-validation-status';
|
||||||
|
|
||||||
|
const calculateContentSizeMB = (content: string): number => {
|
||||||
|
return content.length / (1024 * 1024); // Convert to MB
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateIsLargeFile = (content: string): boolean => {
|
||||||
|
const contentSizeMB = calculateContentSizeMB(content);
|
||||||
|
return contentSizeMB > 2; // Consider large if over 2MB
|
||||||
|
};
|
||||||
|
|
||||||
const errorScriptOutputMessage =
|
const errorScriptOutputMessage =
|
||||||
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
|
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
|
||||||
@@ -117,6 +132,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||||
|
const pasteDisposableRef = useRef<IDisposable | null>(null);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isSm: isDesktop } = useBreakpoint('sm');
|
const { isSm: isDesktop } = useBreakpoint('sm');
|
||||||
@@ -124,6 +140,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
|
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
|
||||||
const [isCheckingJson, setIsCheckingJson] = useState(false);
|
const [isCheckingJson, setIsCheckingJson] = useState(false);
|
||||||
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
|
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
|
||||||
|
const [sqlValidation, setSqlValidation] = useState<ValidationResult | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [isAutoFixing, setIsAutoFixing] = useState(false);
|
||||||
|
const [showAutoFixButton, setShowAutoFixButton] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setScriptResult('');
|
setScriptResult('');
|
||||||
@@ -134,11 +155,33 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
// Check if the ddl is valid
|
// Check if the ddl is valid
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (importMethod !== 'ddl') {
|
if (importMethod !== 'ddl') {
|
||||||
|
setSqlValidation(null);
|
||||||
|
setShowAutoFixButton(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scriptResult.trim()) return;
|
if (!scriptResult.trim()) {
|
||||||
|
setSqlValidation(null);
|
||||||
|
setShowAutoFixButton(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First run our validation based on database type
|
||||||
|
const validation = validateSQL(scriptResult, databaseType);
|
||||||
|
setSqlValidation(validation);
|
||||||
|
|
||||||
|
// If we have auto-fixable errors, show the auto-fix button
|
||||||
|
if (validation.fixedSQL && validation.errors.length > 0) {
|
||||||
|
setShowAutoFixButton(true);
|
||||||
|
// Don't try to parse invalid SQL
|
||||||
|
setErrorMessage('SQL contains syntax errors');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide auto-fix button if no fixes available
|
||||||
|
setShowAutoFixButton(false);
|
||||||
|
|
||||||
|
// Validate the SQL (either original or already fixed)
|
||||||
parseSQLError({
|
parseSQLError({
|
||||||
sqlContent: scriptResult,
|
sqlContent: scriptResult,
|
||||||
sourceDatabaseType: databaseType,
|
sourceDatabaseType: databaseType,
|
||||||
@@ -184,8 +227,44 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
}
|
}
|
||||||
}, [errorMessage.length, onImport, scriptResult]);
|
}, [errorMessage.length, onImport, scriptResult]);
|
||||||
|
|
||||||
|
const handleAutoFix = useCallback(() => {
|
||||||
|
if (sqlValidation?.fixedSQL) {
|
||||||
|
setIsAutoFixing(true);
|
||||||
|
setShowAutoFixButton(false);
|
||||||
|
setErrorMessage('');
|
||||||
|
|
||||||
|
// Apply the fix with a delay so user sees the fixing message
|
||||||
|
setTimeout(() => {
|
||||||
|
setScriptResult(sqlValidation.fixedSQL!);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsAutoFixing(false);
|
||||||
|
}, 100);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}, [sqlValidation, setScriptResult]);
|
||||||
|
|
||||||
|
const handleErrorClick = useCallback((line: number) => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
// Set cursor to the error line
|
||||||
|
editorRef.current.setPosition({ lineNumber: line, column: 1 });
|
||||||
|
editorRef.current.revealLineInCenter(line);
|
||||||
|
editorRef.current.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const formatEditor = useCallback(() => {
|
const formatEditor = useCallback(() => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
|
const model = editorRef.current.getModel();
|
||||||
|
if (model) {
|
||||||
|
const content = model.getValue();
|
||||||
|
|
||||||
|
// Skip formatting for large files (> 2MB)
|
||||||
|
if (calculateIsLargeFile(content)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editorRef.current
|
editorRef.current
|
||||||
?.getAction('editor.action.formatDocument')
|
?.getAction('editor.action.formatDocument')
|
||||||
@@ -211,7 +290,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
const handleCheckJson = useCallback(async () => {
|
const handleCheckJson = useCallback(async () => {
|
||||||
setIsCheckingJson(true);
|
setIsCheckingJson(true);
|
||||||
|
|
||||||
const fixedJson = await fixMetadataJson(scriptResult);
|
await waitFor(1000);
|
||||||
|
const fixedJson = fixMetadataJson(scriptResult);
|
||||||
|
|
||||||
if (isStringMetadataJson(fixedJson)) {
|
if (isStringMetadataJson(fixedJson)) {
|
||||||
setScriptResult(fixedJson);
|
setScriptResult(fixedJson);
|
||||||
@@ -227,37 +307,69 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
setIsCheckingJson(false);
|
setIsCheckingJson(false);
|
||||||
}, [scriptResult, setScriptResult, formatEditor]);
|
}, [scriptResult, setScriptResult, formatEditor]);
|
||||||
|
|
||||||
const detectAndSetImportMethod = useCallback(() => {
|
|
||||||
const content = editorRef.current?.getValue();
|
|
||||||
if (content && content.trim()) {
|
|
||||||
const detectedType = detectContentType(content);
|
|
||||||
if (detectedType && detectedType !== importMethod) {
|
|
||||||
setImportMethod(detectedType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [setImportMethod, importMethod]);
|
|
||||||
|
|
||||||
const [editorDidMount, setEditorDidMount] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorRef.current && editorDidMount) {
|
// Cleanup paste handler on unmount
|
||||||
editorRef.current.onDidPaste(() => {
|
return () => {
|
||||||
setTimeout(() => {
|
if (pasteDisposableRef.current) {
|
||||||
editorRef.current
|
pasteDisposableRef.current.dispose();
|
||||||
?.getAction('editor.action.formatDocument')
|
pasteDisposableRef.current = null;
|
||||||
?.run();
|
|
||||||
}, 0);
|
|
||||||
setTimeout(detectAndSetImportMethod, 0);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [detectAndSetImportMethod, editorDidMount]);
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleEditorDidMount = useCallback(
|
const handleEditorDidMount = useCallback(
|
||||||
(editor: editor.IStandaloneCodeEditor) => {
|
(editor: editor.IStandaloneCodeEditor) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
setEditorDidMount(true);
|
|
||||||
|
// Cleanup previous disposable if it exists
|
||||||
|
if (pasteDisposableRef.current) {
|
||||||
|
pasteDisposableRef.current.dispose();
|
||||||
|
pasteDisposableRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add paste handler for all modes
|
||||||
|
const disposable = editor.onDidPaste(() => {
|
||||||
|
const model = editor.getModel();
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
const content = model.getValue();
|
||||||
|
|
||||||
|
// Skip formatting for large files (> 2MB) to prevent browser freezing
|
||||||
|
const isLargeFile = calculateIsLargeFile(content);
|
||||||
|
|
||||||
|
// First, detect content type to determine if we should switch modes
|
||||||
|
const detectedType = detectContentType(content);
|
||||||
|
if (detectedType && detectedType !== importMethod) {
|
||||||
|
// Switch to the detected mode immediately
|
||||||
|
setImportMethod(detectedType);
|
||||||
|
|
||||||
|
// Only format if it's JSON (query mode) AND file is not too large
|
||||||
|
if (detectedType === 'query' && !isLargeFile) {
|
||||||
|
// For JSON mode, format after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
editor
|
||||||
|
.getAction('editor.action.formatDocument')
|
||||||
|
?.run();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
// For DDL mode, do NOT format as it can break the SQL
|
||||||
|
} else {
|
||||||
|
// Content type didn't change, apply formatting based on current mode
|
||||||
|
if (importMethod === 'query' && !isLargeFile) {
|
||||||
|
// Only format JSON content if not too large
|
||||||
|
setTimeout(() => {
|
||||||
|
editor
|
||||||
|
.getAction('editor.action.formatDocument')
|
||||||
|
?.run();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
// For DDL mode or large files, do NOT format
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pasteDisposableRef.current = disposable;
|
||||||
},
|
},
|
||||||
[]
|
[importMethod, setImportMethod]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderHeader = useCallback(() => {
|
const renderHeader = useCallback(() => {
|
||||||
@@ -314,7 +426,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
: 'dbml-light'
|
: 'dbml-light'
|
||||||
}
|
}
|
||||||
options={{
|
options={{
|
||||||
formatOnPaste: true,
|
formatOnPaste: false, // Never format on paste - we handle it manually
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
@@ -343,10 +455,13 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{errorMessage ? (
|
{errorMessage || (importMethod === 'ddl' && sqlValidation) ? (
|
||||||
<div className="mt-2 flex shrink-0 items-center gap-2">
|
<SQLValidationStatus
|
||||||
<p className="text-xs text-red-700">{errorMessage}</p>
|
validation={sqlValidation}
|
||||||
</div>
|
errorMessage={errorMessage}
|
||||||
|
isAutoFixing={isAutoFixing}
|
||||||
|
onErrorClick={handleErrorClick}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -357,6 +472,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
effectiveTheme,
|
effectiveTheme,
|
||||||
debouncedHandleInputChange,
|
debouncedHandleInputChange,
|
||||||
handleEditorDidMount,
|
handleEditorDidMount,
|
||||||
|
sqlValidation,
|
||||||
|
isAutoFixing,
|
||||||
|
handleErrorClick,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -366,7 +484,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
direction={isDesktop ? 'horizontal' : 'vertical'}
|
direction={isDesktop ? 'horizontal' : 'vertical'}
|
||||||
className="min-h-[500px] md:min-h-fit"
|
className="min-h-[500px]"
|
||||||
>
|
>
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
defaultSize={25}
|
defaultSize={25}
|
||||||
@@ -442,13 +560,28 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
) : showAutoFixButton && importMethod === 'ddl' ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleAutoFix}
|
||||||
|
disabled={isAutoFixing}
|
||||||
|
className="bg-sky-600 text-white hover:bg-sky-700"
|
||||||
|
>
|
||||||
|
{isAutoFixing ? (
|
||||||
|
<Spinner size="small" />
|
||||||
|
) : (
|
||||||
|
'Try auto-fix'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
) : keepDialogAfterImport ? (
|
) : keepDialogAfterImport ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="default"
|
variant="default"
|
||||||
disabled={
|
disabled={
|
||||||
scriptResult.trim().length === 0 ||
|
scriptResult.trim().length === 0 ||
|
||||||
errorMessage.length > 0
|
errorMessage.length > 0 ||
|
||||||
|
isAutoFixing
|
||||||
}
|
}
|
||||||
onClick={handleImport}
|
onClick={handleImport}
|
||||||
>
|
>
|
||||||
@@ -461,7 +594,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
variant="default"
|
variant="default"
|
||||||
disabled={
|
disabled={
|
||||||
scriptResult.trim().length === 0 ||
|
scriptResult.trim().length === 0 ||
|
||||||
errorMessage.length > 0
|
errorMessage.length > 0 ||
|
||||||
|
isAutoFixing
|
||||||
}
|
}
|
||||||
onClick={handleImport}
|
onClick={handleImport}
|
||||||
>
|
>
|
||||||
@@ -494,6 +628,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
handleCheckJson,
|
handleCheckJson,
|
||||||
goBack,
|
goBack,
|
||||||
t,
|
t,
|
||||||
|
importMethod,
|
||||||
|
isAutoFixing,
|
||||||
|
showAutoFixButton,
|
||||||
|
handleAutoFix,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { DDLInstructions } from './instructions/ddl-instructions';
|
|||||||
|
|
||||||
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
|
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
|
||||||
DatabaseType.CLICKHOUSE,
|
DatabaseType.CLICKHOUSE,
|
||||||
|
DatabaseType.ORACLE,
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface InstructionsSectionProps {
|
export interface InstructionsSectionProps {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ const DDLInstructionsMap: Record<DatabaseType, DDLInstruction[]> = {
|
|||||||
text: 'Open the exported SQL file, copy its contents, and paste them here.',
|
text: 'Open the exported SQL file, copy its contents, and paste them here.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[DatabaseType.ORACLE]: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DDLInstructionsProps {
|
export interface DDLInstructionsProps {
|
||||||
|
|||||||
179
src/dialogs/common/import-database/sql-validation-status.tsx
Normal file
179
src/dialogs/common/import-database/sql-validation-status.tsx
Normal 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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
src/dialogs/common/select-tables/constants.ts
Normal file
2
src/dialogs/common/select-tables/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const MAX_TABLES_IN_DIAGRAM = 500;
|
||||||
|
export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50;
|
||||||
683
src/dialogs/common/select-tables/select-tables.tsx
Normal file
683
src/dialogs/common/select-tables/select-tables.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum CreateDiagramDialogStep {
|
export enum CreateDiagramDialogStep {
|
||||||
SELECT_DATABASE = 'SELECT_DATABASE',
|
SELECT_DATABASE = 'SELECT_DATABASE',
|
||||||
IMPORT_DATABASE = 'IMPORT_DATABASE',
|
IMPORT_DATABASE = 'IMPORT_DATABASE',
|
||||||
|
SELECT_TABLES = 'SELECT_TABLES',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
|||||||
import { SelectDatabase } from './select-database/select-database';
|
import { SelectDatabase } from './select-database/select-database';
|
||||||
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
|
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
|
||||||
import { ImportDatabase } from '../common/import-database/import-database';
|
import { ImportDatabase } from '../common/import-database/import-database';
|
||||||
|
import { SelectTables } from '../common/select-tables/select-tables';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
import { sqlImportToDiagram } from '@/lib/data/sql-import';
|
import { sqlImportToDiagram } from '@/lib/data/sql-import';
|
||||||
|
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
|
||||||
|
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
|
||||||
|
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
|
||||||
|
|
||||||
export interface CreateDiagramDialogProps extends BaseDialogProps {}
|
export interface CreateDiagramDialogProps extends BaseDialogProps {}
|
||||||
|
|
||||||
@@ -42,6 +46,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
const { listDiagrams, addDiagram } = useStorage();
|
const { listDiagrams, addDiagram } = useStorage();
|
||||||
const [diagramNumber, setDiagramNumber] = useState<number>(1);
|
const [diagramNumber, setDiagramNumber] = useState<number>(1);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
|
||||||
|
const [isParsingMetadata, setIsParsingMetadata] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDatabaseEdition(undefined);
|
setDatabaseEdition(undefined);
|
||||||
@@ -62,11 +68,19 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
setDatabaseEdition(undefined);
|
setDatabaseEdition(undefined);
|
||||||
setScriptResult('');
|
setScriptResult('');
|
||||||
setImportMethod('query');
|
setImportMethod('query');
|
||||||
|
setParsedMetadata(undefined);
|
||||||
}, [dialog.open]);
|
}, [dialog.open]);
|
||||||
|
|
||||||
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
|
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
|
||||||
|
|
||||||
const importNewDiagram = useCallback(async () => {
|
const importNewDiagram = useCallback(
|
||||||
|
async ({
|
||||||
|
selectedTables,
|
||||||
|
databaseMetadata,
|
||||||
|
}: {
|
||||||
|
selectedTables?: SelectedTable[];
|
||||||
|
databaseMetadata?: DatabaseMetadata;
|
||||||
|
} = {}) => {
|
||||||
let diagram: Diagram | undefined;
|
let diagram: Diagram | undefined;
|
||||||
|
|
||||||
if (importMethod === 'ddl') {
|
if (importMethod === 'ddl') {
|
||||||
@@ -76,12 +90,22 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
targetDatabaseType: databaseType,
|
targetDatabaseType: databaseType,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const databaseMetadata: DatabaseMetadata =
|
let metadata: DatabaseMetadata | undefined = databaseMetadata;
|
||||||
loadDatabaseMetadata(scriptResult);
|
|
||||||
|
if (!metadata) {
|
||||||
|
metadata = loadDatabaseMetadata(scriptResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTables && selectedTables.length > 0) {
|
||||||
|
metadata = filterMetadataByTables({
|
||||||
|
metadata,
|
||||||
|
selectedTables,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
diagram = await loadFromDatabaseMetadata({
|
diagram = await loadFromDatabaseMetadata({
|
||||||
databaseType,
|
databaseType,
|
||||||
databaseMetadata,
|
databaseMetadata: metadata,
|
||||||
diagramNumber,
|
diagramNumber,
|
||||||
databaseEdition:
|
databaseEdition:
|
||||||
databaseEdition?.trim().length === 0
|
databaseEdition?.trim().length === 0
|
||||||
@@ -91,10 +115,14 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await addDiagram({ diagram });
|
await addDiagram({ diagram });
|
||||||
await updateConfig({ config: { defaultDiagramId: diagram.id } });
|
await updateConfig({
|
||||||
|
config: { defaultDiagramId: diagram.id },
|
||||||
|
});
|
||||||
|
|
||||||
closeCreateDiagramDialog();
|
closeCreateDiagramDialog();
|
||||||
navigate(`/diagrams/${diagram.id}`);
|
navigate(`/diagrams/${diagram.id}`);
|
||||||
}, [
|
},
|
||||||
|
[
|
||||||
importMethod,
|
importMethod,
|
||||||
databaseType,
|
databaseType,
|
||||||
addDiagram,
|
addDiagram,
|
||||||
@@ -104,7 +132,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
updateConfig,
|
updateConfig,
|
||||||
scriptResult,
|
scriptResult,
|
||||||
diagramNumber,
|
diagramNumber,
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const createEmptyDiagram = useCallback(async () => {
|
const createEmptyDiagram = useCallback(async () => {
|
||||||
const diagram: Diagram = {
|
const diagram: Diagram = {
|
||||||
@@ -138,10 +167,56 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
openImportDBMLDialog,
|
openImportDBMLDialog,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const importNewDiagramOrFilterTables = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsParsingMetadata(true);
|
||||||
|
|
||||||
|
if (importMethod === 'ddl') {
|
||||||
|
await importNewDiagram();
|
||||||
|
} else {
|
||||||
|
// Parse metadata asynchronously to avoid blocking the UI
|
||||||
|
const metadata = await new Promise<DatabaseMetadata>(
|
||||||
|
(resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const result =
|
||||||
|
loadDatabaseMetadata(scriptResult);
|
||||||
|
resolve(result);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalTablesAndViews =
|
||||||
|
metadata.tables.length + (metadata.views?.length || 0);
|
||||||
|
|
||||||
|
setParsedMetadata(metadata);
|
||||||
|
|
||||||
|
// Check if it's a large database that needs table selection
|
||||||
|
if (totalTablesAndViews > MAX_TABLES_WITHOUT_SHOWING_FILTER) {
|
||||||
|
setStep(CreateDiagramDialogStep.SELECT_TABLES);
|
||||||
|
} else {
|
||||||
|
await importNewDiagram({
|
||||||
|
databaseMetadata: metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsParsingMetadata(false);
|
||||||
|
}
|
||||||
|
}, [importMethod, scriptResult, importNewDiagram]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
{...dialog}
|
{...dialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
// Don't allow closing while parsing metadata
|
||||||
|
if (isParsingMetadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasExistingDiagram) {
|
if (!hasExistingDiagram) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -154,6 +229,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
|
className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
|
||||||
showClose={hasExistingDiagram}
|
showClose={hasExistingDiagram}
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
|
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
|
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
|
||||||
<SelectDatabase
|
<SelectDatabase
|
||||||
@@ -165,9 +242,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
|
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
|
||||||
<ImportDatabase
|
<ImportDatabase
|
||||||
onImport={importNewDiagram}
|
onImport={importNewDiagramOrFilterTables}
|
||||||
onCreateEmptyDiagram={createEmptyDiagram}
|
onCreateEmptyDiagram={createEmptyDiagram}
|
||||||
databaseEdition={databaseEdition}
|
databaseEdition={databaseEdition}
|
||||||
databaseType={databaseType}
|
databaseType={databaseType}
|
||||||
@@ -180,8 +257,18 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
title={t('new_diagram_dialog.import_database.title')}
|
title={t('new_diagram_dialog.import_database.title')}
|
||||||
importMethod={importMethod}
|
importMethod={importMethod}
|
||||||
setImportMethod={setImportMethod}
|
setImportMethod={setImportMethod}
|
||||||
|
keepDialogAfterImport={true}
|
||||||
/>
|
/>
|
||||||
)}
|
) : step === CreateDiagramDialogStep.SELECT_TABLES ? (
|
||||||
|
<SelectTables
|
||||||
|
isLoading={isParsingMetadata || !parsedMetadata}
|
||||||
|
databaseMetadata={parsedMetadata}
|
||||||
|
onImport={importNewDiagram}
|
||||||
|
onBack={() =>
|
||||||
|
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const SUPPORTED_DB_TYPES: DatabaseType[] = [
|
|||||||
DatabaseType.MARIADB,
|
DatabaseType.MARIADB,
|
||||||
DatabaseType.SQLITE,
|
DatabaseType.SQLITE,
|
||||||
DatabaseType.SQL_SERVER,
|
DatabaseType.SQL_SERVER,
|
||||||
|
DatabaseType.ORACLE,
|
||||||
DatabaseType.COCKROACHDB,
|
DatabaseType.COCKROACHDB,
|
||||||
DatabaseType.CLICKHOUSE,
|
DatabaseType.CLICKHOUSE,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React, {
|
|||||||
Suspense,
|
Suspense,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import * as monaco from 'monaco-editor';
|
import type * as monaco from 'monaco-editor';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -23,53 +23,24 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Editor } from '@/components/code-snippet/code-snippet';
|
import { Editor } from '@/components/code-snippet/code-snippet';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
import { importDBMLToDiagram, sanitizeDBML } from '@/lib/dbml-import';
|
import {
|
||||||
|
importDBMLToDiagram,
|
||||||
|
sanitizeDBML,
|
||||||
|
preprocessDBML,
|
||||||
|
} from '@/lib/dbml/dbml-import/dbml-import';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { Parser } from '@dbml/core';
|
import { Parser } from '@dbml/core';
|
||||||
import { useCanvas } from '@/hooks/use-canvas';
|
import { useCanvas } from '@/hooks/use-canvas';
|
||||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import { useToast } from '@/components/toast/use-toast';
|
import { useToast } from '@/components/toast/use-toast';
|
||||||
import { Spinner } from '@/components/spinner/spinner';
|
import { Spinner } from '@/components/spinner/spinner';
|
||||||
import { debounce } from '@/lib/utils';
|
import { debounce } from '@/lib/utils';
|
||||||
|
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
|
||||||
interface DBMLError {
|
import {
|
||||||
message: string;
|
clearErrorHighlight,
|
||||||
line: number;
|
highlightErrorLine,
|
||||||
column: number;
|
} from '@/components/code-snippet/dbml/utils';
|
||||||
}
|
|
||||||
|
|
||||||
function parseDBMLError(error: unknown): DBMLError | null {
|
|
||||||
try {
|
|
||||||
if (typeof error === 'string') {
|
|
||||||
const parsed = JSON.parse(error);
|
|
||||||
if (parsed.diags?.[0]) {
|
|
||||||
const diag = parsed.diags[0];
|
|
||||||
return {
|
|
||||||
message: diag.message,
|
|
||||||
line: diag.location.start.line,
|
|
||||||
column: diag.location.start.column,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (error && typeof error === 'object' && 'diags' in error) {
|
|
||||||
const parsed = error as {
|
|
||||||
diags: Array<{
|
|
||||||
message: string;
|
|
||||||
location: { start: { line: number; column: number } };
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
if (parsed.diags?.[0]) {
|
|
||||||
return {
|
|
||||||
message: parsed.diags[0].message,
|
|
||||||
line: parsed.diags[0].location.start.line,
|
|
||||||
column: parsed.diags[0].location.start.column,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing DBML error:', e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImportDBMLDialogProps extends BaseDialogProps {
|
export interface ImportDBMLDialogProps extends BaseDialogProps {
|
||||||
withCreateEmptyDiagram?: boolean;
|
withCreateEmptyDiagram?: boolean;
|
||||||
@@ -145,39 +116,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
|||||||
}
|
}
|
||||||
}, [reorder, reorderTables]);
|
}, [reorder, reorderTables]);
|
||||||
|
|
||||||
const highlightErrorLine = useCallback((error: DBMLError) => {
|
|
||||||
if (!editorRef.current) return;
|
|
||||||
|
|
||||||
const model = editorRef.current.getModel();
|
|
||||||
if (!model) return;
|
|
||||||
|
|
||||||
const decorations = [
|
|
||||||
{
|
|
||||||
range: new monaco.Range(
|
|
||||||
error.line,
|
|
||||||
1,
|
|
||||||
error.line,
|
|
||||||
model.getLineMaxColumn(error.line)
|
|
||||||
),
|
|
||||||
options: {
|
|
||||||
isWholeLine: true,
|
|
||||||
className: 'dbml-error-line',
|
|
||||||
glyphMarginClassName: 'dbml-error-glyph',
|
|
||||||
hoverMessage: { value: error.message },
|
|
||||||
overviewRuler: {
|
|
||||||
color: '#ff0000',
|
|
||||||
position: monaco.editor.OverviewRulerLane.Right,
|
|
||||||
darkColor: '#ff0000',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
decorationsCollection.current?.set(decorations);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearDecorations = useCallback(() => {
|
const clearDecorations = useCallback(() => {
|
||||||
decorationsCollection.current?.clear();
|
clearErrorHighlight(decorationsCollection.current);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const validateDBML = useCallback(
|
const validateDBML = useCallback(
|
||||||
@@ -189,7 +129,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
|||||||
if (!content.trim()) return;
|
if (!content.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sanitizedContent = sanitizeDBML(content);
|
const preprocessedContent = preprocessDBML(content);
|
||||||
|
const sanitizedContent = sanitizeDBML(preprocessedContent);
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
parser.parse(sanitizedContent, 'dbml');
|
parser.parse(sanitizedContent, 'dbml');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -199,7 +140,12 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
|||||||
t('import_dbml_dialog.error.description') +
|
t('import_dbml_dialog.error.description') +
|
||||||
` (1 error found - in line ${parsedError.line})`
|
` (1 error found - in line ${parsedError.line})`
|
||||||
);
|
);
|
||||||
highlightErrorLine(parsedError);
|
highlightErrorLine({
|
||||||
|
error: parsedError,
|
||||||
|
model: editorRef.current?.getModel(),
|
||||||
|
editorDecorationsCollection:
|
||||||
|
decorationsCollection.current,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage(
|
setErrorMessage(
|
||||||
e instanceof Error ? e.message : JSON.stringify(e)
|
e instanceof Error ? e.message : JSON.stringify(e)
|
||||||
@@ -207,7 +153,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[clearDecorations, highlightErrorLine, t]
|
[clearDecorations, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
|
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
|
||||||
@@ -242,13 +188,11 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
|||||||
if (!dbmlContent.trim() || errorMessage) return;
|
if (!dbmlContent.trim() || errorMessage) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Sanitize DBML content before importing
|
const importedDiagram = await importDBMLToDiagram(dbmlContent);
|
||||||
const sanitizedContent = sanitizeDBML(dbmlContent);
|
|
||||||
const importedDiagram = await importDBMLToDiagram(sanitizedContent);
|
|
||||||
const tableIdsToRemove = tables
|
const tableIdsToRemove = tables
|
||||||
.filter((table) =>
|
.filter((table) =>
|
||||||
importedDiagram.tables?.some(
|
importedDiagram.tables?.some(
|
||||||
(t) =>
|
(t: DBTable) =>
|
||||||
t.name === table.name && t.schema === table.schema
|
t.name === table.name && t.schema === table.schema
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -257,19 +201,21 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
|||||||
const relationshipIdsToRemove = relationships
|
const relationshipIdsToRemove = relationships
|
||||||
.filter((relationship) => {
|
.filter((relationship) => {
|
||||||
const sourceTable = tables.find(
|
const sourceTable = tables.find(
|
||||||
(table) => table.id === relationship.sourceTableId
|
(table: DBTable) =>
|
||||||
|
table.id === relationship.sourceTableId
|
||||||
);
|
);
|
||||||
const targetTable = tables.find(
|
const targetTable = tables.find(
|
||||||
(table) => table.id === relationship.targetTableId
|
(table: DBTable) =>
|
||||||
|
table.id === relationship.targetTableId
|
||||||
);
|
);
|
||||||
if (!sourceTable || !targetTable) return true;
|
if (!sourceTable || !targetTable) return true;
|
||||||
const replacementSourceTable = importedDiagram.tables?.find(
|
const replacementSourceTable = importedDiagram.tables?.find(
|
||||||
(table) =>
|
(table: DBTable) =>
|
||||||
table.name === sourceTable.name &&
|
table.name === sourceTable.name &&
|
||||||
table.schema === sourceTable.schema
|
table.schema === sourceTable.schema
|
||||||
);
|
);
|
||||||
const replacementTargetTable = importedDiagram.tables?.find(
|
const replacementTargetTable = importedDiagram.tables?.find(
|
||||||
(table) =>
|
(table: DBTable) =>
|
||||||
table.name === targetTable.name &&
|
table.name === targetTable.name &&
|
||||||
table.schema === targetTable.schema
|
table.schema === targetTable.schema
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -17,11 +17,23 @@ import type { DBSchema } from '@/lib/domain/db-schema';
|
|||||||
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
|
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
import { Separator } from '@/components/separator/separator';
|
||||||
|
import { Group, SquarePlus } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/tooltip/tooltip';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||||
|
import { Label } from '@/components/label/label';
|
||||||
|
|
||||||
export interface TableSchemaDialogProps extends BaseDialogProps {
|
export interface TableSchemaDialogProps extends BaseDialogProps {
|
||||||
table?: DBTable;
|
table?: DBTable;
|
||||||
schemas: DBSchema[];
|
schemas: DBSchema[];
|
||||||
onConfirm: (schema: string) => void;
|
onConfirm: ({ schema }: { schema: DBSchema }) => void;
|
||||||
|
allowSchemaCreation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||||
@@ -29,27 +41,90 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
|||||||
table,
|
table,
|
||||||
schemas,
|
schemas,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
allowSchemaCreation = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedSchema, setSelectedSchema] = React.useState<string>(
|
const { databaseType, filteredSchemas, filterSchemas } = useChartDB();
|
||||||
|
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
|
||||||
table?.schema
|
table?.schema
|
||||||
? schemaNameToSchemaId(table.schema)
|
? schemaNameToSchemaId(table.schema)
|
||||||
: (schemas?.[0]?.id ?? '')
|
: (schemas?.[0]?.id ?? '')
|
||||||
);
|
);
|
||||||
|
const allowSchemaSelection = useMemo(
|
||||||
|
() => schemas && schemas.length > 0,
|
||||||
|
[schemas]
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultSchemaName = useMemo(
|
||||||
|
() => defaultSchemas?.[databaseType],
|
||||||
|
[databaseType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isCreatingNew, setIsCreatingNew] =
|
||||||
|
useState<boolean>(!allowSchemaSelection);
|
||||||
|
const [newSchemaName, setNewSchemaName] = useState<string>(
|
||||||
|
allowSchemaCreation && !allowSchemaSelection
|
||||||
|
? (defaultSchemaName ?? '')
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dialog.open) return;
|
if (!dialog.open) return;
|
||||||
setSelectedSchema(
|
setSelectedSchemaId(
|
||||||
table?.schema
|
table?.schema
|
||||||
? schemaNameToSchemaId(table.schema)
|
? schemaNameToSchemaId(table.schema)
|
||||||
: (schemas?.[0]?.id ?? '')
|
: (schemas?.[0]?.id ?? '')
|
||||||
);
|
);
|
||||||
}, [dialog.open, schemas, table?.schema]);
|
setIsCreatingNew(!allowSchemaSelection);
|
||||||
|
setNewSchemaName(
|
||||||
|
allowSchemaCreation && !allowSchemaSelection
|
||||||
|
? (defaultSchemaName ?? '')
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
defaultSchemaName,
|
||||||
|
dialog.open,
|
||||||
|
schemas,
|
||||||
|
table?.schema,
|
||||||
|
allowSchemaSelection,
|
||||||
|
allowSchemaCreation,
|
||||||
|
]);
|
||||||
|
|
||||||
const { closeTableSchemaDialog } = useDialog();
|
const { closeTableSchemaDialog } = useDialog();
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
onConfirm(selectedSchema);
|
let createdSchemaId: string;
|
||||||
}, [onConfirm, selectedSchema]);
|
if (isCreatingNew && newSchemaName.trim()) {
|
||||||
|
const newSchema: DBSchema = {
|
||||||
|
id: schemaNameToSchemaId(newSchemaName.trim()),
|
||||||
|
name: newSchemaName.trim(),
|
||||||
|
tableCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
createdSchemaId = newSchema.id;
|
||||||
|
|
||||||
|
onConfirm({ schema: newSchema });
|
||||||
|
} else {
|
||||||
|
const schema = schemas.find((s) => s.id === selectedSchemaId);
|
||||||
|
if (!schema) return;
|
||||||
|
|
||||||
|
createdSchemaId = schema.id;
|
||||||
|
onConfirm({ schema });
|
||||||
|
}
|
||||||
|
|
||||||
|
filterSchemas([
|
||||||
|
...(filteredSchemas ?? schemas.map((s) => s.id)),
|
||||||
|
createdSchemaId,
|
||||||
|
]);
|
||||||
|
}, [
|
||||||
|
onConfirm,
|
||||||
|
selectedSchemaId,
|
||||||
|
schemas,
|
||||||
|
isCreatingNew,
|
||||||
|
newSchemaName,
|
||||||
|
filteredSchemas,
|
||||||
|
filterSchemas,
|
||||||
|
]);
|
||||||
|
|
||||||
const schemaOptions: SelectBoxOption[] = useMemo(
|
const schemaOptions: SelectBoxOption[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -60,6 +135,25 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
|||||||
[schemas]
|
[schemas]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderSwitchCreateOrSelectButton = useCallback(
|
||||||
|
() => (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start"
|
||||||
|
onClick={() => setIsCreatingNew(!isCreatingNew)}
|
||||||
|
disabled={!allowSchemaSelection || !allowSchemaCreation}
|
||||||
|
>
|
||||||
|
{!isCreatingNew ? (
|
||||||
|
<SquarePlus className="mr-2 size-4 " />
|
||||||
|
) : (
|
||||||
|
<Group className="mr-2 size-4 " />
|
||||||
|
)}
|
||||||
|
{isCreatingNew ? 'Select existing schema' : 'Create new schema'}
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
[isCreatingNew, allowSchemaSelection, allowSchemaCreation]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
{...dialog}
|
{...dialog}
|
||||||
@@ -67,17 +161,23 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
|||||||
if (!open) {
|
if (!open) {
|
||||||
closeTableSchemaDialog();
|
closeTableSchemaDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="flex flex-col" showClose>
|
<DialogContent className="flex flex-col" showClose>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{table
|
{!allowSchemaSelection && allowSchemaCreation
|
||||||
|
? t('create_table_schema_dialog.title')
|
||||||
|
: table
|
||||||
? t('update_table_schema_dialog.title')
|
? t('update_table_schema_dialog.title')
|
||||||
: t('new_table_schema_dialog.title')}
|
: t('new_table_schema_dialog.title')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{table
|
{!allowSchemaSelection && allowSchemaCreation
|
||||||
|
? t('create_table_schema_dialog.description')
|
||||||
|
: table
|
||||||
? t('update_table_schema_dialog.description', {
|
? t('update_table_schema_dialog.description', {
|
||||||
tableName: table.name,
|
tableName: table.name,
|
||||||
})
|
})
|
||||||
@@ -86,27 +186,79 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-1">
|
<div className="grid gap-4 py-1">
|
||||||
<div className="grid w-full items-center gap-4">
|
<div className="grid w-full items-center gap-4">
|
||||||
|
{!isCreatingNew ? (
|
||||||
<SelectBox
|
<SelectBox
|
||||||
options={schemaOptions}
|
options={schemaOptions}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
value={selectedSchema}
|
value={selectedSchemaId}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setSelectedSchema(value as string)
|
setSelectedSchemaId(value as string)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{allowSchemaCreation &&
|
||||||
|
!allowSchemaSelection ? (
|
||||||
|
<Label htmlFor="new-schema-name">
|
||||||
|
Schema Name
|
||||||
|
</Label>
|
||||||
|
) : null}
|
||||||
|
<Input
|
||||||
|
id="new-schema-name"
|
||||||
|
value={newSchemaName}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewSchemaName(e.target.value)
|
||||||
|
}
|
||||||
|
placeholder={`Enter schema name.${defaultSchemaName ? ` e.g. ${defaultSchemaName}.` : ''}`}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowSchemaCreation && allowSchemaSelection ? (
|
||||||
|
<>
|
||||||
|
<div className="relative">
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-2 text-xs text-muted-foreground">
|
||||||
|
or
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{allowSchemaSelection ? (
|
||||||
|
renderSwitchCreateOrSelectButton()
|
||||||
|
) : (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span>
|
||||||
|
{renderSwitchCreateOrSelectButton()}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>No existing schemas available</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="flex gap-1 md:justify-between">
|
<DialogFooter className="flex gap-1 md:justify-between">
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button variant="secondary">
|
<Button variant="secondary">
|
||||||
{table
|
{isCreatingNew
|
||||||
|
? t('create_table_schema_dialog.cancel')
|
||||||
|
: table
|
||||||
? t('update_table_schema_dialog.cancel')
|
? t('update_table_schema_dialog.cancel')
|
||||||
: t('new_table_schema_dialog.cancel')}
|
: t('new_table_schema_dialog.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button onClick={handleConfirm}>
|
<Button
|
||||||
{table
|
onClick={handleConfirm}
|
||||||
|
disabled={isCreatingNew && !newSchemaName.trim()}
|
||||||
|
>
|
||||||
|
{isCreatingNew
|
||||||
|
? t('create_table_schema_dialog.create')
|
||||||
|
: table
|
||||||
? t('update_table_schema_dialog.confirm')
|
? t('update_table_schema_dialog.confirm')
|
||||||
: t('new_table_schema_dialog.confirm')}
|
: t('new_table_schema_dialog.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-editable {
|
.text-editable {
|
||||||
@@ -154,3 +155,29 @@
|
|||||||
background-size: 650%;
|
background-size: 650%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Edit button emphasis animation */
|
||||||
|
@keyframes dbml_edit-button-emphasis {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
|
||||||
|
background-color: rgba(59, 130, 246, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
|
||||||
|
background-color: rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
||||||
|
background-color: rgba(59, 130, 246, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbml-edit-button-emphasis {
|
||||||
|
animation: dbml_edit-button-emphasis 0.6s ease-in-out;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,23 +23,25 @@ import { bn, bnMetadata } from './locales/bn';
|
|||||||
import { gu, guMetadata } from './locales/gu';
|
import { gu, guMetadata } from './locales/gu';
|
||||||
import { vi, viMetadata } from './locales/vi';
|
import { vi, viMetadata } from './locales/vi';
|
||||||
import { ar, arMetadata } from './locales/ar';
|
import { ar, arMetadata } from './locales/ar';
|
||||||
|
import { hr, hrMetadata } from './locales/hr';
|
||||||
|
|
||||||
export const languages: LanguageMetadata[] = [
|
export const languages: LanguageMetadata[] = [
|
||||||
enMetadata,
|
enMetadata,
|
||||||
esMetadata,
|
|
||||||
frMetadata,
|
frMetadata,
|
||||||
deMetadata,
|
deMetadata,
|
||||||
|
esMetadata,
|
||||||
|
ukMetadata,
|
||||||
|
ruMetadata,
|
||||||
|
trMetadata,
|
||||||
|
hrMetadata,
|
||||||
|
pt_BRMetadata,
|
||||||
hiMetadata,
|
hiMetadata,
|
||||||
jaMetadata,
|
jaMetadata,
|
||||||
ko_KRMetadata,
|
ko_KRMetadata,
|
||||||
pt_BRMetadata,
|
|
||||||
ukMetadata,
|
|
||||||
ruMetadata,
|
|
||||||
zh_CNMetadata,
|
zh_CNMetadata,
|
||||||
zh_TWMetadata,
|
zh_TWMetadata,
|
||||||
neMetadata,
|
neMetadata,
|
||||||
mrMetadata,
|
mrMetadata,
|
||||||
trMetadata,
|
|
||||||
id_IDMetadata,
|
id_IDMetadata,
|
||||||
teMetadata,
|
teMetadata,
|
||||||
bnMetadata,
|
bnMetadata,
|
||||||
@@ -70,6 +72,7 @@ const resources = {
|
|||||||
gu,
|
gu,
|
||||||
vi,
|
vi,
|
||||||
ar,
|
ar,
|
||||||
|
hr,
|
||||||
};
|
};
|
||||||
|
|
||||||
i18n.use(LanguageDetector)
|
i18n.use(LanguageDetector)
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const ar: LanguageTranslation = {
|
|||||||
hide_sidebar: 'إخفاء الشريط الجانبي',
|
hide_sidebar: 'إخفاء الشريط الجانبي',
|
||||||
hide_cardinality: 'إخفاء الكاردينالية',
|
hide_cardinality: 'إخفاء الكاردينالية',
|
||||||
show_cardinality: 'إظهار الكاردينالية',
|
show_cardinality: 'إظهار الكاردينالية',
|
||||||
|
hide_field_attributes: 'إخفاء خصائص الحقل',
|
||||||
|
show_field_attributes: 'إظهار خصائص الحقل',
|
||||||
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
|
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
|
||||||
theme: 'المظهر',
|
theme: 'المظهر',
|
||||||
show_dependencies: 'إظهار الاعتمادات',
|
show_dependencies: 'إظهار الاعتمادات',
|
||||||
@@ -74,8 +76,8 @@ export const ar: LanguageTranslation = {
|
|||||||
title: 'مخططات متعددة',
|
title: 'مخططات متعددة',
|
||||||
description:
|
description:
|
||||||
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
|
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
|
||||||
dont_show_again: 'لا تظهره مجدداً',
|
// TODO: Translate
|
||||||
change_schema: 'تغيير',
|
show_me: 'Show me',
|
||||||
none: 'لا شيء',
|
none: 'لا شيء',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -151,6 +153,10 @@ export const ar: LanguageTranslation = {
|
|||||||
delete_field: 'حذف الحقل',
|
delete_field: 'حذف الحقل',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'الدقة',
|
||||||
|
scale: 'النطاق',
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'خصائص الفهرس',
|
title: 'خصائص الفهرس',
|
||||||
@@ -231,6 +237,36 @@ export const ar: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -242,6 +278,11 @@ export const ar: LanguageTranslation = {
|
|||||||
redo: 'إعادة',
|
redo: 'إعادة',
|
||||||
reorder_diagram: 'إعادة ترتيب الرسم البياني',
|
reorder_diagram: 'إعادة ترتيب الرسم البياني',
|
||||||
highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
|
highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -373,6 +414,13 @@ export const ar: LanguageTranslation = {
|
|||||||
cancel: 'إلغاء',
|
cancel: 'إلغاء',
|
||||||
confirm: 'تغيير',
|
confirm: 'تغيير',
|
||||||
},
|
},
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'إنشاء مخطط جديد',
|
||||||
|
description:
|
||||||
|
'لا توجد مخططات حتى الآن. قم بإنشاء أول مخطط لتنظيم جداولك.',
|
||||||
|
create: 'إنشاء',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: '!ساعدنا على التحسن',
|
title: '!ساعدنا على التحسن',
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const bn: LanguageTranslation = {
|
|||||||
hide_sidebar: 'সাইডবার লুকান',
|
hide_sidebar: 'সাইডবার লুকান',
|
||||||
hide_cardinality: 'কার্ডিনালিটি লুকান',
|
hide_cardinality: 'কার্ডিনালিটি লুকান',
|
||||||
show_cardinality: 'কার্ডিনালিটি দেখান',
|
show_cardinality: 'কার্ডিনালিটি দেখান',
|
||||||
|
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
|
||||||
|
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
|
||||||
zoom_on_scroll: 'স্ক্রলে জুম করুন',
|
zoom_on_scroll: 'স্ক্রলে জুম করুন',
|
||||||
theme: 'থিম',
|
theme: 'থিম',
|
||||||
show_dependencies: 'নির্ভরতাগুলি দেখান',
|
show_dependencies: 'নির্ভরতাগুলি দেখান',
|
||||||
@@ -75,8 +77,8 @@ export const bn: LanguageTranslation = {
|
|||||||
title: 'বহু স্কিমা',
|
title: 'বহু স্কিমা',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
|
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
|
||||||
dont_show_again: 'পুনরায় দেখাবেন না',
|
// TODO: Translate
|
||||||
change_schema: 'পরিবর্তন করুন',
|
show_me: 'Show me',
|
||||||
none: 'কিছুই না',
|
none: 'কিছুই না',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -151,7 +153,12 @@ export const bn: LanguageTranslation = {
|
|||||||
no_comments: 'কোনো মন্তব্য নেই',
|
no_comments: 'কোনো মন্তব্য নেই',
|
||||||
delete_field: 'ফিল্ড মুছুন',
|
delete_field: 'ফিল্ড মুছুন',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'নির্ভুলতা',
|
||||||
|
scale: 'স্কেল',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'ইনডেক্স কর্ম',
|
title: 'ইনডেক্স কর্ম',
|
||||||
@@ -232,6 +239,35 @@ export const bn: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -243,6 +279,12 @@ export const bn: LanguageTranslation = {
|
|||||||
redo: 'পুনরায় করুন',
|
redo: 'পুনরায় করুন',
|
||||||
reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
|
reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
|
||||||
highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
|
highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
|
||||||
|
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -374,6 +416,13 @@ export const bn: LanguageTranslation = {
|
|||||||
cancel: 'বাতিল করুন',
|
cancel: 'বাতিল করুন',
|
||||||
confirm: 'পরিবর্তন করুন',
|
confirm: 'পরিবর্তন করুন',
|
||||||
},
|
},
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'নতুন স্কিমা তৈরি করুন',
|
||||||
|
description:
|
||||||
|
'এখনও কোনো স্কিমা নেই। আপনার টেবিলগুলি সংগঠিত করতে আপনার প্রথম স্কিমা তৈরি করুন।',
|
||||||
|
create: 'তৈরি করুন',
|
||||||
|
cancel: 'বাতিল করুন',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'আমাদের উন্নত করতে সাহায্য করুন!',
|
title: 'আমাদের উন্নত করতে সাহায্য করুন!',
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const de: LanguageTranslation = {
|
|||||||
hide_sidebar: 'Seitenleiste ausblenden',
|
hide_sidebar: 'Seitenleiste ausblenden',
|
||||||
hide_cardinality: 'Kardinalität ausblenden',
|
hide_cardinality: 'Kardinalität ausblenden',
|
||||||
show_cardinality: 'Kardinalität anzeigen',
|
show_cardinality: 'Kardinalität anzeigen',
|
||||||
|
hide_field_attributes: 'Feldattribute ausblenden',
|
||||||
|
show_field_attributes: 'Feldattribute anzeigen',
|
||||||
zoom_on_scroll: 'Zoom beim Scrollen',
|
zoom_on_scroll: 'Zoom beim Scrollen',
|
||||||
theme: 'Stil',
|
theme: 'Stil',
|
||||||
show_dependencies: 'Abhängigkeiten anzeigen',
|
show_dependencies: 'Abhängigkeiten anzeigen',
|
||||||
@@ -75,8 +77,8 @@ export const de: LanguageTranslation = {
|
|||||||
title: 'Mehrere Schemas',
|
title: 'Mehrere Schemas',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
|
'{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'Nicht erneut anzeigen',
|
// TODO: Translate
|
||||||
change_schema: 'Schema ändern',
|
show_me: 'Show me',
|
||||||
none: 'Keine',
|
none: 'Keine',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -152,7 +154,12 @@ export const de: LanguageTranslation = {
|
|||||||
no_comments: 'Keine Kommentare',
|
no_comments: 'Keine Kommentare',
|
||||||
delete_field: 'Feld löschen',
|
delete_field: 'Feld löschen',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'Präzision',
|
||||||
|
scale: 'Skalierung',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Indexattribute',
|
title: 'Indexattribute',
|
||||||
@@ -234,6 +241,35 @@ export const de: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -244,7 +280,14 @@ export const de: LanguageTranslation = {
|
|||||||
undo: 'Rückgängig',
|
undo: 'Rückgängig',
|
||||||
redo: 'Wiederholen',
|
redo: 'Wiederholen',
|
||||||
reorder_diagram: 'Diagramm neu anordnen',
|
reorder_diagram: 'Diagramm neu anordnen',
|
||||||
|
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'Überlappende Tabellen hervorheben',
|
highlight_overlapping_tables: 'Überlappende Tabellen hervorheben',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -377,6 +420,13 @@ export const de: LanguageTranslation = {
|
|||||||
cancel: 'Abbrechen',
|
cancel: 'Abbrechen',
|
||||||
confirm: 'Ändern',
|
confirm: 'Ändern',
|
||||||
},
|
},
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'Neues Schema erstellen',
|
||||||
|
description:
|
||||||
|
'Es existieren noch keine Schemas. Erstellen Sie Ihr erstes Schema, um Ihre Tabellen zu organisieren.',
|
||||||
|
create: 'Erstellen',
|
||||||
|
cancel: 'Abbrechen',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'Hilf uns, uns zu verbessern!',
|
title: 'Hilf uns, uns zu verbessern!',
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const en = {
|
|||||||
hide_sidebar: 'Hide Sidebar',
|
hide_sidebar: 'Hide Sidebar',
|
||||||
hide_cardinality: 'Hide Cardinality',
|
hide_cardinality: 'Hide Cardinality',
|
||||||
show_cardinality: 'Show Cardinality',
|
show_cardinality: 'Show Cardinality',
|
||||||
|
hide_field_attributes: 'Hide Field Attributes',
|
||||||
|
show_field_attributes: 'Show Field Attributes',
|
||||||
zoom_on_scroll: 'Zoom on Scroll',
|
zoom_on_scroll: 'Zoom on Scroll',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
show_dependencies: 'Show Dependencies',
|
show_dependencies: 'Show Dependencies',
|
||||||
@@ -73,8 +75,7 @@ export const en = {
|
|||||||
title: 'Multiple Schemas',
|
title: 'Multiple Schemas',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
|
'{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
|
||||||
dont_show_again: "Don't show again",
|
show_me: 'Show me',
|
||||||
change_schema: 'Change',
|
|
||||||
none: 'none',
|
none: 'none',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -144,8 +145,12 @@ export const en = {
|
|||||||
title: 'Field Attributes',
|
title: 'Field Attributes',
|
||||||
unique: 'Unique',
|
unique: 'Unique',
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'Precision',
|
||||||
|
scale: 'Scale',
|
||||||
comments: 'Comments',
|
comments: 'Comments',
|
||||||
no_comments: 'No comments',
|
no_comments: 'No comments',
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
delete_field: 'Delete Field',
|
delete_field: 'Delete Field',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
@@ -226,6 +231,35 @@ export const en = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -237,6 +271,10 @@ export const en = {
|
|||||||
redo: 'Redo',
|
redo: 'Redo',
|
||||||
reorder_diagram: 'Reorder Diagram',
|
reorder_diagram: 'Reorder Diagram',
|
||||||
highlight_overlapping_tables: 'Highlight Overlapping Tables',
|
highlight_overlapping_tables: 'Highlight Overlapping Tables',
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -368,6 +406,14 @@ export const en = {
|
|||||||
confirm: 'Change',
|
confirm: 'Change',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'Create New Schema',
|
||||||
|
description:
|
||||||
|
'No schemas exist yet. Create your first schema to organize your tables.',
|
||||||
|
create: 'Create',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'Help us improve!',
|
title: 'Help us improve!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export const es: LanguageTranslation = {
|
|||||||
view: 'Ver',
|
view: 'Ver',
|
||||||
hide_cardinality: 'Ocultar Cardinalidad',
|
hide_cardinality: 'Ocultar Cardinalidad',
|
||||||
show_cardinality: 'Mostrar Cardinalidad',
|
show_cardinality: 'Mostrar Cardinalidad',
|
||||||
|
show_field_attributes: 'Mostrar Atributos de Campo',
|
||||||
|
hide_field_attributes: 'Ocultar Atributos de Campo',
|
||||||
show_sidebar: 'Mostrar Barra Lateral',
|
show_sidebar: 'Mostrar Barra Lateral',
|
||||||
hide_sidebar: 'Ocultar Barra Lateral',
|
hide_sidebar: 'Ocultar Barra Lateral',
|
||||||
zoom_on_scroll: 'Zoom al Desplazarse',
|
zoom_on_scroll: 'Zoom al Desplazarse',
|
||||||
@@ -141,7 +143,12 @@ export const es: LanguageTranslation = {
|
|||||||
no_comments: 'Sin comentarios',
|
no_comments: 'Sin comentarios',
|
||||||
delete_field: 'Eliminar Campo',
|
delete_field: 'Eliminar Campo',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'Precisión',
|
||||||
|
scale: 'Escala',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Atributos del Índice',
|
title: 'Atributos del Índice',
|
||||||
@@ -222,6 +229,35 @@ export const es: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -232,7 +268,13 @@ export const es: LanguageTranslation = {
|
|||||||
undo: 'Deshacer',
|
undo: 'Deshacer',
|
||||||
redo: 'Rehacer',
|
redo: 'Rehacer',
|
||||||
reorder_diagram: 'Reordenar Diagrama',
|
reorder_diagram: 'Reordenar Diagrama',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'Resaltar tablas superpuestas',
|
highlight_overlapping_tables: 'Resaltar tablas superpuestas',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -366,6 +408,13 @@ export const es: LanguageTranslation = {
|
|||||||
cancel: 'Cancelar',
|
cancel: 'Cancelar',
|
||||||
confirm: 'Cambiar',
|
confirm: 'Cambiar',
|
||||||
},
|
},
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'Crear Nuevo Esquema',
|
||||||
|
description:
|
||||||
|
'Aún no existen esquemas. Crea tu primer esquema para organizar tus tablas.',
|
||||||
|
create: 'Crear',
|
||||||
|
cancel: 'Cancelar',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: '¡Ayúdanos a mejorar!',
|
title: '¡Ayúdanos a mejorar!',
|
||||||
@@ -379,8 +428,8 @@ export const es: LanguageTranslation = {
|
|||||||
title: 'Múltiples Esquemas',
|
title: 'Múltiples Esquemas',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.',
|
'{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'No mostrar de nuevo',
|
// TODO: Translate
|
||||||
change_schema: 'Cambiar',
|
show_me: 'Show me',
|
||||||
none: 'nada',
|
none: 'nada',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const fr: LanguageTranslation = {
|
|||||||
hide_sidebar: 'Cacher la Barre Latérale',
|
hide_sidebar: 'Cacher la Barre Latérale',
|
||||||
hide_cardinality: 'Cacher la Cardinalité',
|
hide_cardinality: 'Cacher la Cardinalité',
|
||||||
show_cardinality: 'Afficher la Cardinalité',
|
show_cardinality: 'Afficher la Cardinalité',
|
||||||
|
hide_field_attributes: 'Masquer les Attributs de Champ',
|
||||||
|
show_field_attributes: 'Afficher les Attributs de Champ',
|
||||||
zoom_on_scroll: 'Zoom sur le Défilement',
|
zoom_on_scroll: 'Zoom sur le Défilement',
|
||||||
theme: 'Thème',
|
theme: 'Thème',
|
||||||
show_dependencies: 'Afficher les Dépendances',
|
show_dependencies: 'Afficher les Dépendances',
|
||||||
@@ -139,7 +141,12 @@ export const fr: LanguageTranslation = {
|
|||||||
no_comments: 'Pas de commentaires',
|
no_comments: 'Pas de commentaires',
|
||||||
delete_field: 'Supprimer le Champ',
|
delete_field: 'Supprimer le Champ',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'Précision',
|
||||||
|
scale: 'Échelle',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: "Attributs de l'Index",
|
title: "Attributs de l'Index",
|
||||||
@@ -220,6 +227,35 @@ export const fr: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -230,7 +266,13 @@ export const fr: LanguageTranslation = {
|
|||||||
undo: 'Annuler',
|
undo: 'Annuler',
|
||||||
redo: 'Rétablir',
|
redo: 'Rétablir',
|
||||||
reorder_diagram: 'Réorganiser le Diagramme',
|
reorder_diagram: 'Réorganiser le Diagramme',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'Surligner les tables chevauchées',
|
highlight_overlapping_tables: 'Surligner les tables chevauchées',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -319,8 +361,8 @@ export const fr: LanguageTranslation = {
|
|||||||
title: 'Schémas Multiples',
|
title: 'Schémas Multiples',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} schémas dans ce diagramme. Actuellement affiché(s) : {{formattedSchemas}}.',
|
'{{schemasCount}} schémas dans ce diagramme. Actuellement affiché(s) : {{formattedSchemas}}.',
|
||||||
dont_show_again: 'Ne plus afficher',
|
// TODO: Translate
|
||||||
change_schema: 'Changer',
|
show_me: 'Show me',
|
||||||
none: 'Aucun',
|
none: 'Aucun',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -346,6 +388,13 @@ export const fr: LanguageTranslation = {
|
|||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
confirm: 'Modifier',
|
confirm: 'Modifier',
|
||||||
},
|
},
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'Créer un Nouveau Schéma',
|
||||||
|
description:
|
||||||
|
"Aucun schéma n'existe encore. Créez votre premier schéma pour organiser vos tables.",
|
||||||
|
create: 'Créer',
|
||||||
|
cancel: 'Annuler',
|
||||||
|
},
|
||||||
|
|
||||||
create_relationship_dialog: {
|
create_relationship_dialog: {
|
||||||
title: 'Créer une Relation',
|
title: 'Créer une Relation',
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const gu: LanguageTranslation = {
|
|||||||
hide_sidebar: 'સાઇડબાર છુપાવો',
|
hide_sidebar: 'સાઇડબાર છુપાવો',
|
||||||
hide_cardinality: 'કાર્ડિનાલિટી છુપાવો',
|
hide_cardinality: 'કાર્ડિનાલિટી છુપાવો',
|
||||||
show_cardinality: 'કાર્ડિનાલિટી બતાવો',
|
show_cardinality: 'કાર્ડિનાલિટી બતાવો',
|
||||||
|
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
|
||||||
|
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
|
||||||
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
|
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
|
||||||
theme: 'થિમ',
|
theme: 'થિમ',
|
||||||
show_dependencies: 'નિર્ભરતાઓ બતાવો',
|
show_dependencies: 'નિર્ભરતાઓ બતાવો',
|
||||||
@@ -75,8 +77,8 @@ export const gu: LanguageTranslation = {
|
|||||||
title: 'કઈંક વધારે સ્કીમા',
|
title: 'કઈંક વધારે સ્કીમા',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
|
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'ફરીથી ન બતાવો',
|
// TODO: Translate
|
||||||
change_schema: 'બદલો',
|
show_me: 'Show me',
|
||||||
none: 'કઈ નહીં',
|
none: 'કઈ નહીં',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -152,7 +154,12 @@ export const gu: LanguageTranslation = {
|
|||||||
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
||||||
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'ચોકસાઈ',
|
||||||
|
scale: 'માપ',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'ઇન્ડેક્સ લક્ષણો',
|
title: 'ઇન્ડેક્સ લક્ષણો',
|
||||||
@@ -233,6 +240,35 @@ export const gu: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -243,7 +279,13 @@ export const gu: LanguageTranslation = {
|
|||||||
undo: 'અનડુ',
|
undo: 'અનડુ',
|
||||||
redo: 'રીડુ',
|
redo: 'રીડુ',
|
||||||
reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો',
|
reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો',
|
highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -375,6 +417,14 @@ export const gu: LanguageTranslation = {
|
|||||||
confirm: 'બદલો',
|
confirm: 'બદલો',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'નવું સ્કીમા બનાવો',
|
||||||
|
description:
|
||||||
|
'હજી સુધી કોઈ સ્કીમા અસ્તિત્વમાં નથી. તમારા ટેબલ્સ ને વ્યવસ્થિત કરવા માટે તમારું પહેલું સ્કીમા બનાવો.',
|
||||||
|
create: 'બનાવો',
|
||||||
|
cancel: 'રદ કરો',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'અમને સુધારવામાં મદદ કરો!',
|
title: 'અમને સુધારવામાં મદદ કરો!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const hi: LanguageTranslation = {
|
|||||||
hide_sidebar: 'साइडबार छिपाएँ',
|
hide_sidebar: 'साइडबार छिपाएँ',
|
||||||
hide_cardinality: 'कार्डिनैलिटी छिपाएँ',
|
hide_cardinality: 'कार्डिनैलिटी छिपाएँ',
|
||||||
show_cardinality: 'कार्डिनैलिटी दिखाएँ',
|
show_cardinality: 'कार्डिनैलिटी दिखाएँ',
|
||||||
|
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
|
||||||
|
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
|
||||||
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
|
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
|
||||||
theme: 'थीम',
|
theme: 'थीम',
|
||||||
show_dependencies: 'निर्भरता दिखाएँ',
|
show_dependencies: 'निर्भरता दिखाएँ',
|
||||||
@@ -74,8 +76,8 @@ export const hi: LanguageTranslation = {
|
|||||||
title: 'एकाधिक स्कीमा',
|
title: 'एकाधिक स्कीमा',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
|
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
|
||||||
dont_show_again: 'फिर से न दिखाएँ',
|
// TODO: Translate
|
||||||
change_schema: 'बदलें',
|
show_me: 'Show me',
|
||||||
none: 'कोई नहीं',
|
none: 'कोई नहीं',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -151,7 +153,12 @@ export const hi: LanguageTranslation = {
|
|||||||
no_comments: 'कोई टिप्पणी नहीं',
|
no_comments: 'कोई टिप्पणी नहीं',
|
||||||
delete_field: 'फ़ील्ड हटाएँ',
|
delete_field: 'फ़ील्ड हटाएँ',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'Precision',
|
||||||
|
scale: 'Scale',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'सूचकांक विशेषताएँ',
|
title: 'सूचकांक विशेषताएँ',
|
||||||
@@ -233,6 +240,35 @@ export const hi: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -243,7 +279,13 @@ export const hi: LanguageTranslation = {
|
|||||||
undo: 'पूर्ववत करें',
|
undo: 'पूर्ववत करें',
|
||||||
redo: 'पुनः करें',
|
redo: 'पुनः करें',
|
||||||
reorder_diagram: 'आरेख पुनः व्यवस्थित करें',
|
reorder_diagram: 'आरेख पुनः व्यवस्थित करें',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें',
|
highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -378,6 +420,14 @@ export const hi: LanguageTranslation = {
|
|||||||
confirm: 'बदलें',
|
confirm: 'बदलें',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'नया स्कीमा बनाएं',
|
||||||
|
description:
|
||||||
|
'अभी तक कोई स्कीमा मौजूद नहीं है। अपनी तालिकाओं को व्यवस्थित करने के लिए अपना पहला स्कीमा बनाएं।',
|
||||||
|
create: 'बनाएं',
|
||||||
|
cancel: 'रद्द करें',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'हमें सुधारने में मदद करें!',
|
title: 'हमें सुधारने में मदद करें!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
503
src/i18n/locales/hr.ts
Normal file
503
src/i18n/locales/hr.ts
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
import type { LanguageMetadata, LanguageTranslation } from '../types';
|
||||||
|
|
||||||
|
export const hr: LanguageTranslation = {
|
||||||
|
translation: {
|
||||||
|
menu: {
|
||||||
|
file: {
|
||||||
|
file: 'Datoteka',
|
||||||
|
new: 'Nova',
|
||||||
|
open: 'Otvori',
|
||||||
|
save: 'Spremi',
|
||||||
|
import: 'Uvezi',
|
||||||
|
export_sql: 'Izvezi SQL',
|
||||||
|
export_as: 'Izvezi kao',
|
||||||
|
delete_diagram: 'Izbriši dijagram',
|
||||||
|
exit: 'Izađi',
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
|
||||||
|
multiple_schemas_alert: {
|
||||||
|
title: 'Više shema',
|
||||||
|
description:
|
||||||
|
'{{schemasCount}} shema u ovom dijagramu. Trenutno prikazano: {{formattedSchemas}}.',
|
||||||
|
show_me: 'Prikaži mi',
|
||||||
|
none: 'nijedna',
|
||||||
|
},
|
||||||
|
|
||||||
|
copy_to_clipboard_toast: {
|
||||||
|
unsupported: {
|
||||||
|
title: 'Kopiranje neuspješno',
|
||||||
|
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: {
|
||||||
|
schema: 'Shema:',
|
||||||
|
filter_by_schema: 'Filtriraj po shemi',
|
||||||
|
search_schema: 'Pretraži shemu...',
|
||||||
|
no_schemas_found: 'Nema pronađenih shema.',
|
||||||
|
view_all_options: 'Prikaži sve opcije...',
|
||||||
|
tables_section: {
|
||||||
|
tables: 'Tablice',
|
||||||
|
add_table: 'Dodaj tablicu',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relationships_section: {
|
||||||
|
relationships: 'Veze',
|
||||||
|
filter: 'Filtriraj',
|
||||||
|
add_relationship: 'Dodaj vezu',
|
||||||
|
collapse: 'Sažmi sve',
|
||||||
|
relationship: {
|
||||||
|
primary: 'Primarna tablica',
|
||||||
|
foreign: 'Referentna tablica',
|
||||||
|
cardinality: 'Kardinalnost',
|
||||||
|
delete_relationship: 'Izbriši',
|
||||||
|
relationship_actions: {
|
||||||
|
title: 'Radnje',
|
||||||
|
delete_relationship: 'Izbriši',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
empty_state: {
|
||||||
|
title: 'Nema veza',
|
||||||
|
description: 'Stvorite vezu za povezivanje tablica',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dependencies_section: {
|
||||||
|
dependencies: 'Ovisnosti',
|
||||||
|
filter: 'Filtriraj',
|
||||||
|
collapse: 'Sažmi sve',
|
||||||
|
dependency: {
|
||||||
|
table: 'Tablica',
|
||||||
|
dependent_table: 'Ovisni pogled',
|
||||||
|
delete_dependency: 'Izbriši',
|
||||||
|
dependency_actions: {
|
||||||
|
title: 'Radnje',
|
||||||
|
delete_dependency: 'Izbriši',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
empty_state: {
|
||||||
|
title: 'Nema ovisnosti',
|
||||||
|
description: 'Stvorite pogled 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_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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hrMetadata: LanguageMetadata = {
|
||||||
|
name: 'Croatian',
|
||||||
|
nativeName: 'Hrvatski',
|
||||||
|
code: 'hr',
|
||||||
|
};
|
||||||
@@ -26,6 +26,8 @@ export const id_ID: LanguageTranslation = {
|
|||||||
hide_sidebar: 'Sembunyikan Sidebar',
|
hide_sidebar: 'Sembunyikan Sidebar',
|
||||||
hide_cardinality: 'Sembunyikan Kardinalitas',
|
hide_cardinality: 'Sembunyikan Kardinalitas',
|
||||||
show_cardinality: 'Tampilkan Kardinalitas',
|
show_cardinality: 'Tampilkan Kardinalitas',
|
||||||
|
hide_field_attributes: 'Sembunyikan Atribut Kolom',
|
||||||
|
show_field_attributes: 'Tampilkan Atribut Kolom',
|
||||||
zoom_on_scroll: 'Perbesar saat Scroll',
|
zoom_on_scroll: 'Perbesar saat Scroll',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Tampilkan Dependensi',
|
show_dependencies: 'Tampilkan Dependensi',
|
||||||
@@ -74,8 +76,8 @@ export const id_ID: LanguageTranslation = {
|
|||||||
title: 'Schema Lebih dari satu',
|
title: 'Schema Lebih dari satu',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.',
|
'{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'Jangan tampilkan lagi',
|
// TODO: Translate
|
||||||
change_schema: 'Ubah',
|
show_me: 'Show me',
|
||||||
none: 'Tidak ada',
|
none: 'Tidak ada',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -150,7 +152,12 @@ export const id_ID: LanguageTranslation = {
|
|||||||
no_comments: 'Tidak ada komentar',
|
no_comments: 'Tidak ada komentar',
|
||||||
delete_field: 'Hapus Kolom',
|
delete_field: 'Hapus Kolom',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'Presisi',
|
||||||
|
scale: 'Skala',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Atribut Indeks',
|
title: 'Atribut Indeks',
|
||||||
@@ -231,6 +238,35 @@ export const id_ID: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -241,7 +277,13 @@ export const id_ID: LanguageTranslation = {
|
|||||||
undo: 'Undo',
|
undo: 'Undo',
|
||||||
redo: 'Redo',
|
redo: 'Redo',
|
||||||
reorder_diagram: 'Atur Ulang Diagram',
|
reorder_diagram: 'Atur Ulang Diagram',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'Sorot Tabel yang Tumpang Tindih',
|
highlight_overlapping_tables: 'Sorot Tabel yang Tumpang Tindih',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -373,6 +415,14 @@ export const id_ID: LanguageTranslation = {
|
|||||||
confirm: 'Ubah',
|
confirm: 'Ubah',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'Buat Skema Baru',
|
||||||
|
description:
|
||||||
|
'Belum ada skema yang tersedia. Buat skema pertama Anda untuk mengatur tabel-tabel Anda.',
|
||||||
|
create: 'Buat',
|
||||||
|
cancel: 'Batal',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'Bantu kami meningkatkan!',
|
title: 'Bantu kami meningkatkan!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const ja: LanguageTranslation = {
|
|||||||
hide_sidebar: 'サイドバーを非表示',
|
hide_sidebar: 'サイドバーを非表示',
|
||||||
hide_cardinality: 'カーディナリティを非表示',
|
hide_cardinality: 'カーディナリティを非表示',
|
||||||
show_cardinality: 'カーディナリティを表示',
|
show_cardinality: 'カーディナリティを表示',
|
||||||
|
hide_field_attributes: 'フィールド属性を非表示',
|
||||||
|
show_field_attributes: 'フィールド属性を表示',
|
||||||
zoom_on_scroll: 'スクロールでズーム',
|
zoom_on_scroll: 'スクロールでズーム',
|
||||||
theme: 'テーマ',
|
theme: 'テーマ',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
@@ -76,8 +78,8 @@ export const ja: LanguageTranslation = {
|
|||||||
title: '複数のスキーマ',
|
title: '複数のスキーマ',
|
||||||
description:
|
description:
|
||||||
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
|
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
|
||||||
dont_show_again: '再表示しない',
|
// TODO: Translate
|
||||||
change_schema: '変更',
|
show_me: 'Show me',
|
||||||
none: 'なし',
|
none: 'なし',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -154,7 +156,12 @@ export const ja: LanguageTranslation = {
|
|||||||
no_comments: 'コメントがありません',
|
no_comments: 'コメントがありません',
|
||||||
delete_field: 'フィールドを削除',
|
delete_field: 'フィールドを削除',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: '精度',
|
||||||
|
scale: '小数点以下桁数',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'インデックス属性',
|
title: 'インデックス属性',
|
||||||
@@ -237,6 +244,35 @@ export const ja: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -249,6 +285,10 @@ export const ja: LanguageTranslation = {
|
|||||||
reorder_diagram: 'ダイアグラムを並べ替え',
|
reorder_diagram: 'ダイアグラムを並べ替え',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
highlight_overlapping_tables: 'Highlight Overlapping Tables',
|
highlight_overlapping_tables: 'Highlight Overlapping Tables',
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear', // TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -382,6 +422,14 @@ export const ja: LanguageTranslation = {
|
|||||||
confirm: '変更',
|
confirm: '変更',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: '新しいスキーマを作成',
|
||||||
|
description:
|
||||||
|
'スキーマがまだ存在しません。テーブルを整理するために最初のスキーマを作成してください。',
|
||||||
|
create: '作成',
|
||||||
|
cancel: 'キャンセル',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: '改善をサポートしてください!',
|
title: '改善をサポートしてください!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
hide_sidebar: '사이드바 숨기기',
|
hide_sidebar: '사이드바 숨기기',
|
||||||
hide_cardinality: '카디널리티 숨기기',
|
hide_cardinality: '카디널리티 숨기기',
|
||||||
show_cardinality: '카디널리티 보이기',
|
show_cardinality: '카디널리티 보이기',
|
||||||
|
hide_field_attributes: '필드 속성 숨기기',
|
||||||
|
show_field_attributes: '필드 속성 보이기',
|
||||||
zoom_on_scroll: '스크롤 시 확대',
|
zoom_on_scroll: '스크롤 시 확대',
|
||||||
theme: '테마',
|
theme: '테마',
|
||||||
show_dependencies: '종속성 보이기',
|
show_dependencies: '종속성 보이기',
|
||||||
@@ -74,8 +76,8 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
title: '다중 스키마',
|
title: '다중 스키마',
|
||||||
description:
|
description:
|
||||||
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
|
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
|
||||||
dont_show_again: '다시 보여주지 마세요',
|
// TODO: Translate
|
||||||
change_schema: '변경',
|
show_me: 'Show me',
|
||||||
none: '없음',
|
none: '없음',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -150,7 +152,12 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
no_comments: '주석 없음',
|
no_comments: '주석 없음',
|
||||||
delete_field: '필드 삭제',
|
delete_field: '필드 삭제',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: '정밀도',
|
||||||
|
scale: '소수점 자릿수',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: '인덱스 속성',
|
title: '인덱스 속성',
|
||||||
@@ -231,6 +238,35 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -241,7 +277,13 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
undo: '실행 취소',
|
undo: '실행 취소',
|
||||||
redo: '다시 실행',
|
redo: '다시 실행',
|
||||||
reorder_diagram: '다이어그램 재정렬',
|
reorder_diagram: '다이어그램 재정렬',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: '겹치는 테이블 강조 표시',
|
highlight_overlapping_tables: '겹치는 테이블 강조 표시',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -373,6 +415,14 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
confirm: '변경',
|
confirm: '변경',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: '새 스키마 생성',
|
||||||
|
description:
|
||||||
|
'아직 스키마가 없습니다. 테이블을 정리하기 위해 첫 번째 스키마를 생성하세요.',
|
||||||
|
create: '생성',
|
||||||
|
cancel: '취소',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: '개선할 수 있도록 도와주세요!',
|
title: '개선할 수 있도록 도와주세요!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const mr: LanguageTranslation = {
|
|||||||
hide_sidebar: 'साइडबार लपवा',
|
hide_sidebar: 'साइडबार लपवा',
|
||||||
hide_cardinality: 'कार्डिनॅलिटी लपवा',
|
hide_cardinality: 'कार्डिनॅलिटी लपवा',
|
||||||
show_cardinality: 'कार्डिनॅलिटी दाखवा',
|
show_cardinality: 'कार्डिनॅलिटी दाखवा',
|
||||||
|
hide_field_attributes: 'फील्ड गुणधर्म लपवा',
|
||||||
|
show_field_attributes: 'फील्ड गुणधर्म दाखवा',
|
||||||
zoom_on_scroll: 'स्क्रोलवर झूम करा',
|
zoom_on_scroll: 'स्क्रोलवर झूम करा',
|
||||||
theme: 'थीम',
|
theme: 'थीम',
|
||||||
show_dependencies: 'डिपेंडेन्सि दाखवा',
|
show_dependencies: 'डिपेंडेन्सि दाखवा',
|
||||||
@@ -75,8 +77,8 @@ export const mr: LanguageTranslation = {
|
|||||||
title: 'एकाधिक स्कीमा',
|
title: 'एकाधिक स्कीमा',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
|
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'पुन्हा दाखवू नका',
|
// TODO: Translate
|
||||||
change_schema: 'बदला',
|
show_me: 'Show me',
|
||||||
none: 'काहीही नाही',
|
none: 'काहीही नाही',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -153,7 +155,12 @@ export const mr: LanguageTranslation = {
|
|||||||
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
||||||
delete_field: 'फील्ड हटवा',
|
delete_field: 'फील्ड हटवा',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'अचूकता',
|
||||||
|
scale: 'प्रमाण',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'इंडेक्स गुणधर्म',
|
title: 'इंडेक्स गुणधर्म',
|
||||||
@@ -236,6 +243,35 @@ export const mr: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -246,7 +282,13 @@ export const mr: LanguageTranslation = {
|
|||||||
undo: 'पूर्ववत करा',
|
undo: 'पूर्ववत करा',
|
||||||
redo: 'पुन्हा करा',
|
redo: 'पुन्हा करा',
|
||||||
reorder_diagram: 'आरेख पुनःक्रमित करा',
|
reorder_diagram: 'आरेख पुनःक्रमित करा',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा',
|
highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -381,6 +423,14 @@ export const mr: LanguageTranslation = {
|
|||||||
confirm: 'बदला',
|
confirm: 'बदला',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'नवीन स्कीमा तयार करा',
|
||||||
|
description:
|
||||||
|
'अजून कोणतीही स्कीमा अस्तित्वात नाही. आपल्या टेबल्स व्यवस्थित करण्यासाठी आपली पहिली स्कीमा तयार करा.',
|
||||||
|
create: 'तयार करा',
|
||||||
|
cancel: 'रद्द करा',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'आम्हाला सुधारण्यास मदत करा!',
|
title: 'आम्हाला सुधारण्यास मदत करा!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const ne: LanguageTranslation = {
|
|||||||
hide_sidebar: 'साइडबार लुकाउनुहोस्',
|
hide_sidebar: 'साइडबार लुकाउनुहोस्',
|
||||||
hide_cardinality: 'कार्डिन्यालिटी लुकाउनुहोस्',
|
hide_cardinality: 'कार्डिन्यालिटी लुकाउनुहोस्',
|
||||||
show_cardinality: 'कार्डिन्यालिटी देखाउनुहोस्',
|
show_cardinality: 'कार्डिन्यालिटी देखाउनुहोस्',
|
||||||
|
hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्',
|
||||||
|
show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्',
|
||||||
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
|
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
|
||||||
theme: 'थिम',
|
theme: 'थिम',
|
||||||
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
|
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
|
||||||
@@ -75,8 +77,8 @@ export const ne: LanguageTranslation = {
|
|||||||
title: 'विविध स्कीमहरू',
|
title: 'विविध स्कीमहरू',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
|
'{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
|
||||||
dont_show_again: 'फेरि देखाउन नदिनुहोस्',
|
// TODO: Translate
|
||||||
change_schema: 'स्कीम परिवर्तन गर्नुहोस्',
|
show_me: 'Show me',
|
||||||
none: 'कुनै पनि छैन',
|
none: 'कुनै पनि छैन',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -151,7 +153,12 @@ export const ne: LanguageTranslation = {
|
|||||||
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
||||||
delete_field: 'क्षेत्र हटाउनुहोस्',
|
delete_field: 'क्षेत्र हटाउनुहोस्',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'परिशुद्धता',
|
||||||
|
scale: 'स्केल',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'सूचक विशेषताहरू',
|
title: 'सूचक विशेषताहरू',
|
||||||
@@ -233,6 +240,35 @@ export const ne: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -243,8 +279,14 @@ export const ne: LanguageTranslation = {
|
|||||||
undo: 'पूर्ववत',
|
undo: 'पूर्ववत',
|
||||||
redo: 'पुनः गर्नुहोस्',
|
redo: 'पुनः गर्नुहोस्',
|
||||||
reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्',
|
reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables:
|
highlight_overlapping_tables:
|
||||||
'अतिरिक्त तालिकाहरू हाइलाइट गर्नुहोस्',
|
'अतिरिक्त तालिकाहरू हाइलाइट गर्नुहोस्',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -378,6 +420,14 @@ export const ne: LanguageTranslation = {
|
|||||||
confirm: 'परिवर्तन गर्नुहोस्',
|
confirm: 'परिवर्तन गर्नुहोस्',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'नयाँ स्कीम सिर्जना गर्नुहोस्',
|
||||||
|
description:
|
||||||
|
'अहिलेसम्म कुनै स्कीम अस्तित्वमा छैन। आफ्ना तालिकाहरू व्यवस्थित गर्न आफ्नो पहिलो स्कीम सिर्जना गर्नुहोस्।',
|
||||||
|
create: 'सिर्जना गर्नुहोस्',
|
||||||
|
cancel: 'रद्द गर्नुहोस्',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'हामीलाई अझ राम्रो हुन मदत गर्नुहोस!',
|
title: 'हामीलाई अझ राम्रो हुन मदत गर्नुहोस!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
hide_sidebar: 'Ocultar Barra Lateral',
|
hide_sidebar: 'Ocultar Barra Lateral',
|
||||||
hide_cardinality: 'Ocultar Cardinalidade',
|
hide_cardinality: 'Ocultar Cardinalidade',
|
||||||
show_cardinality: 'Mostrar Cardinalidade',
|
show_cardinality: 'Mostrar Cardinalidade',
|
||||||
|
hide_field_attributes: 'Ocultar Atributos de Campo',
|
||||||
|
show_field_attributes: 'Mostrar Atributos de Campo',
|
||||||
zoom_on_scroll: 'Zoom ao Rolar',
|
zoom_on_scroll: 'Zoom ao Rolar',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Mostrar Dependências',
|
show_dependencies: 'Mostrar Dependências',
|
||||||
@@ -75,8 +77,8 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
title: 'Múltiplos Esquemas',
|
title: 'Múltiplos Esquemas',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} esquemas neste diagrama. Atualmente exibindo: {{formattedSchemas}}.',
|
'{{schemasCount}} esquemas neste diagrama. Atualmente exibindo: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'Não mostrar novamente',
|
// TODO: Translate
|
||||||
change_schema: 'Alterar',
|
show_me: 'Show me',
|
||||||
none: 'nenhum',
|
none: 'nenhum',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -151,7 +153,12 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
no_comments: 'Sem comentários',
|
no_comments: 'Sem comentários',
|
||||||
delete_field: 'Excluir Campo',
|
delete_field: 'Excluir Campo',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'Precisão',
|
||||||
|
scale: 'Escala',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Atributos do Índice',
|
title: 'Atributos do Índice',
|
||||||
@@ -232,6 +239,35 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -242,7 +278,13 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
undo: 'Desfazer',
|
undo: 'Desfazer',
|
||||||
redo: 'Refazer',
|
redo: 'Refazer',
|
||||||
reorder_diagram: 'Reordenar Diagrama',
|
reorder_diagram: 'Reordenar Diagrama',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'Destacar Tabelas Sobrepostas',
|
highlight_overlapping_tables: 'Destacar Tabelas Sobrepostas',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -376,6 +418,14 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
confirm: 'Alterar',
|
confirm: 'Alterar',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'Criar Novo Esquema',
|
||||||
|
description:
|
||||||
|
'Ainda não existem esquemas. Crie seu primeiro esquema para organizar suas tabelas.',
|
||||||
|
create: 'Criar',
|
||||||
|
cancel: 'Cancelar',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'Ajude-nos a melhorar!',
|
title: 'Ajude-nos a melhorar!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const ru: LanguageTranslation = {
|
|||||||
hide_sidebar: 'Скрыть боковую панель',
|
hide_sidebar: 'Скрыть боковую панель',
|
||||||
hide_cardinality: 'Скрыть виды связи',
|
hide_cardinality: 'Скрыть виды связи',
|
||||||
show_cardinality: 'Показать виды связи',
|
show_cardinality: 'Показать виды связи',
|
||||||
|
show_field_attributes: 'Показать атрибуты поля',
|
||||||
|
hide_field_attributes: 'Скрыть атрибуты поля',
|
||||||
zoom_on_scroll: 'Увеличение при прокрутке',
|
zoom_on_scroll: 'Увеличение при прокрутке',
|
||||||
theme: 'Тема',
|
theme: 'Тема',
|
||||||
show_dependencies: 'Показать зависимости',
|
show_dependencies: 'Показать зависимости',
|
||||||
@@ -73,8 +75,8 @@ export const ru: LanguageTranslation = {
|
|||||||
title: 'Множественные схемы',
|
title: 'Множественные схемы',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
|
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'Больше не показывать',
|
// TODO: Translate
|
||||||
change_schema: 'Изменить',
|
show_me: 'Show me',
|
||||||
none: 'никто',
|
none: 'никто',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -147,7 +149,12 @@ export const ru: LanguageTranslation = {
|
|||||||
comments: 'Комментарии',
|
comments: 'Комментарии',
|
||||||
no_comments: 'Нет комментария',
|
no_comments: 'Нет комментария',
|
||||||
delete_field: 'Удалить поле',
|
delete_field: 'Удалить поле',
|
||||||
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
character_length: 'Макс. длина',
|
character_length: 'Макс. длина',
|
||||||
|
precision: 'Точность',
|
||||||
|
scale: 'Масштаб',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Атрибуты индекса',
|
title: 'Атрибуты индекса',
|
||||||
@@ -229,6 +236,35 @@ export const ru: LanguageTranslation = {
|
|||||||
description: 'Создайте область, чтобы начать',
|
description: 'Создайте область, чтобы начать',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -239,7 +275,13 @@ export const ru: LanguageTranslation = {
|
|||||||
undo: 'Отменить',
|
undo: 'Отменить',
|
||||||
redo: 'Вернуть',
|
redo: 'Вернуть',
|
||||||
reorder_diagram: 'Переупорядочить диаграмму',
|
reorder_diagram: 'Переупорядочить диаграмму',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'Выделение перекрывающихся таблиц',
|
highlight_overlapping_tables: 'Выделение перекрывающихся таблиц',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -373,6 +415,14 @@ export const ru: LanguageTranslation = {
|
|||||||
confirm: 'Изменить',
|
confirm: 'Изменить',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'Создать новую схему',
|
||||||
|
description:
|
||||||
|
'Схемы еще не существуют. Создайте вашу первую схему, чтобы организовать таблицы.',
|
||||||
|
create: 'Создать',
|
||||||
|
cancel: 'Отменить',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'Помогите нам стать лучше!',
|
title: 'Помогите нам стать лучше!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const te: LanguageTranslation = {
|
|||||||
hide_sidebar: 'సైడ్బార్ దాచండి',
|
hide_sidebar: 'సైడ్బార్ దాచండి',
|
||||||
hide_cardinality: 'కార్డినాలిటీని దాచండి',
|
hide_cardinality: 'కార్డినాలిటీని దాచండి',
|
||||||
show_cardinality: 'కార్డినాలిటీని చూపించండి',
|
show_cardinality: 'కార్డినాలిటీని చూపించండి',
|
||||||
|
show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు',
|
||||||
|
hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి',
|
||||||
zoom_on_scroll: 'స్క్రోల్పై జూమ్',
|
zoom_on_scroll: 'స్క్రోల్పై జూమ్',
|
||||||
theme: 'థీమ్',
|
theme: 'థీమ్',
|
||||||
show_dependencies: 'ఆధారాలు చూపించండి',
|
show_dependencies: 'ఆధారాలు చూపించండి',
|
||||||
@@ -75,8 +77,8 @@ export const te: LanguageTranslation = {
|
|||||||
title: 'బహుళ స్కీమాలు',
|
title: 'బహుళ స్కీమాలు',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.',
|
'{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'మరలా చూపించవద్దు',
|
// TODO: Translate
|
||||||
change_schema: 'మార్చు',
|
show_me: 'Show me',
|
||||||
none: 'ఎదరికాదు',
|
none: 'ఎదరికాదు',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -151,7 +153,12 @@ export const te: LanguageTranslation = {
|
|||||||
no_comments: 'వ్యాఖ్యలు లేవు',
|
no_comments: 'వ్యాఖ్యలు లేవు',
|
||||||
delete_field: 'ఫీల్డ్ తొలగించు',
|
delete_field: 'ఫీల్డ్ తొలగించు',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'సూక్ష్మత',
|
||||||
|
scale: 'స్కేల్',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'ఇండెక్స్ గుణాలు',
|
title: 'ఇండెక్స్ గుణాలు',
|
||||||
@@ -233,6 +240,35 @@ export const te: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -243,7 +279,13 @@ export const te: LanguageTranslation = {
|
|||||||
undo: 'తిరిగి చేయు',
|
undo: 'తిరిగి చేయు',
|
||||||
redo: 'మరలా చేయు',
|
redo: 'మరలా చేయు',
|
||||||
reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి',
|
reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'అవకాశించు పట్టికలను హైలైట్ చేయండి',
|
highlight_overlapping_tables: 'అవకాశించు పట్టికలను హైలైట్ చేయండి',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -377,6 +419,14 @@ export const te: LanguageTranslation = {
|
|||||||
confirm: 'మార్చు',
|
confirm: 'మార్చు',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'కొత్త స్కీమా సృష్టించండి',
|
||||||
|
description:
|
||||||
|
'ఇంకా ఏ స్కీమాలు లేవు. మీ పట్టికలను వ్యవస్థీకరించడానికి మీ మొదటి స్కీమాను సృష్టించండి.',
|
||||||
|
create: 'సృష్టించు',
|
||||||
|
cancel: 'రద్దు',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'మా సహాయంతో మెరుగుపరచండి!',
|
title: 'మా సహాయంతో మెరుగుపరచండి!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const tr: LanguageTranslation = {
|
|||||||
hide_sidebar: 'Kenar Çubuğunu Gizle',
|
hide_sidebar: 'Kenar Çubuğunu Gizle',
|
||||||
hide_cardinality: 'Kardinaliteyi Gizle',
|
hide_cardinality: 'Kardinaliteyi Gizle',
|
||||||
show_cardinality: 'Kardinaliteyi Göster',
|
show_cardinality: 'Kardinaliteyi Göster',
|
||||||
|
show_field_attributes: 'Alan Özelliklerini Göster',
|
||||||
|
hide_field_attributes: 'Alan Özelliklerini Gizle',
|
||||||
zoom_on_scroll: 'Kaydırarak Yakınlaştır',
|
zoom_on_scroll: 'Kaydırarak Yakınlaştır',
|
||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Bağımlılıkları Göster',
|
show_dependencies: 'Bağımlılıkları Göster',
|
||||||
@@ -75,8 +77,8 @@ export const tr: LanguageTranslation = {
|
|||||||
title: 'Birden Fazla Şema',
|
title: 'Birden Fazla Şema',
|
||||||
description:
|
description:
|
||||||
'Bu diyagramda {{schemasCount}} şema var. Şu anda görüntülenen: {{formattedSchemas}}.',
|
'Bu diyagramda {{schemasCount}} şema var. Şu anda görüntülenen: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'Tekrar gösterme',
|
// TODO: Translate
|
||||||
change_schema: 'Değiştir',
|
show_me: 'Show me',
|
||||||
none: 'yok',
|
none: 'yok',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -150,7 +152,12 @@ export const tr: LanguageTranslation = {
|
|||||||
no_comments: 'Yorum yok',
|
no_comments: 'Yorum yok',
|
||||||
delete_field: 'Alanı Sil',
|
delete_field: 'Alanı Sil',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'Hassasiyet',
|
||||||
|
scale: 'Ölçek',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'İndeks Özellikleri',
|
title: 'İndeks Özellikleri',
|
||||||
@@ -232,6 +239,35 @@ export const tr: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
toolbar: {
|
toolbar: {
|
||||||
zoom_in: 'Yakınlaştır',
|
zoom_in: 'Yakınlaştır',
|
||||||
@@ -241,7 +277,13 @@ export const tr: LanguageTranslation = {
|
|||||||
undo: 'Geri Al',
|
undo: 'Geri Al',
|
||||||
redo: 'Yinele',
|
redo: 'Yinele',
|
||||||
reorder_diagram: 'Diyagramı Yeniden Sırala',
|
reorder_diagram: 'Diyagramı Yeniden Sırala',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'Çakışan Tabloları Vurgula',
|
highlight_overlapping_tables: 'Çakışan Tabloları Vurgula',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
database_selection: {
|
database_selection: {
|
||||||
@@ -366,6 +408,14 @@ export const tr: LanguageTranslation = {
|
|||||||
cancel: 'İptal',
|
cancel: 'İptal',
|
||||||
confirm: 'Değiştir',
|
confirm: 'Değiştir',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'Yeni Şema Oluştur',
|
||||||
|
description:
|
||||||
|
'Henüz hiç şema mevcut değil. Tablolarınızı düzenlemek için ilk şemanızı oluşturun.',
|
||||||
|
create: 'Oluştur',
|
||||||
|
cancel: 'İptal',
|
||||||
|
},
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'Bize yardım et!',
|
title: 'Bize yardım et!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const uk: LanguageTranslation = {
|
|||||||
hide_sidebar: 'Приховати бічну панель',
|
hide_sidebar: 'Приховати бічну панель',
|
||||||
hide_cardinality: 'Приховати потужність',
|
hide_cardinality: 'Приховати потужність',
|
||||||
show_cardinality: 'Показати кардинальність',
|
show_cardinality: 'Показати кардинальність',
|
||||||
|
show_field_attributes: 'Показати атрибути полів',
|
||||||
|
hide_field_attributes: 'Приховати атрибути полів',
|
||||||
zoom_on_scroll: 'Масштабувати прокручуванням',
|
zoom_on_scroll: 'Масштабувати прокручуванням',
|
||||||
theme: 'Тема',
|
theme: 'Тема',
|
||||||
show_dependencies: 'Показати залежності',
|
show_dependencies: 'Показати залежності',
|
||||||
@@ -73,8 +75,8 @@ export const uk: LanguageTranslation = {
|
|||||||
title: 'Кілька схем',
|
title: 'Кілька схем',
|
||||||
description:
|
description:
|
||||||
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
|
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'Більше не показувати',
|
// TODO: Translate
|
||||||
change_schema: 'Зміна',
|
show_me: 'Show me',
|
||||||
none: 'немає',
|
none: 'немає',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -149,7 +151,12 @@ export const uk: LanguageTranslation = {
|
|||||||
no_comments: 'Немає коментарів',
|
no_comments: 'Немає коментарів',
|
||||||
delete_field: 'Видалити поле',
|
delete_field: 'Видалити поле',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'Точність',
|
||||||
|
scale: 'Масштаб',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Атрибути індексу',
|
title: 'Атрибути індексу',
|
||||||
@@ -230,6 +237,35 @@ export const uk: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -240,7 +276,13 @@ export const uk: LanguageTranslation = {
|
|||||||
undo: 'Скасувати',
|
undo: 'Скасувати',
|
||||||
redo: 'Повторити',
|
redo: 'Повторити',
|
||||||
reorder_diagram: 'Перевпорядкувати діаграму',
|
reorder_diagram: 'Перевпорядкувати діаграму',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
|
highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -374,6 +416,14 @@ export const uk: LanguageTranslation = {
|
|||||||
confirm: 'Змінити',
|
confirm: 'Змінити',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'Створити нову схему',
|
||||||
|
description:
|
||||||
|
'Поки що не існує жодної схеми. Створіть свою першу схему, щоб організувати ваші таблиці.',
|
||||||
|
create: 'Створити',
|
||||||
|
cancel: 'Скасувати',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'Допоможіть нам покращитися!',
|
title: 'Допоможіть нам покращитися!',
|
||||||
description: 'Поставне на зірку на GitHub? Це лише один клік!',
|
description: 'Поставне на зірку на GitHub? Це лише один клік!',
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const vi: LanguageTranslation = {
|
|||||||
hide_sidebar: 'Ẩn thanh bên',
|
hide_sidebar: 'Ẩn thanh bên',
|
||||||
hide_cardinality: 'Ẩn số lượng',
|
hide_cardinality: 'Ẩn số lượng',
|
||||||
show_cardinality: 'Hiển thị số lượng',
|
show_cardinality: 'Hiển thị số lượng',
|
||||||
|
show_field_attributes: 'Hiển thị thuộc tính trường',
|
||||||
|
hide_field_attributes: 'Ẩn thuộc tính trường',
|
||||||
zoom_on_scroll: 'Thu phóng khi cuộn',
|
zoom_on_scroll: 'Thu phóng khi cuộn',
|
||||||
theme: 'Chủ đề',
|
theme: 'Chủ đề',
|
||||||
show_dependencies: 'Hiển thị các phụ thuộc',
|
show_dependencies: 'Hiển thị các phụ thuộc',
|
||||||
@@ -74,8 +76,8 @@ export const vi: LanguageTranslation = {
|
|||||||
title: 'Có nhiều lược đồ',
|
title: 'Có nhiều lược đồ',
|
||||||
description:
|
description:
|
||||||
'Có {{schemasCount}} lược đồ trong sơ đồ này. Hiện đang hiển thị: {{formattedSchemas}}.',
|
'Có {{schemasCount}} lược đồ trong sơ đồ này. Hiện đang hiển thị: {{formattedSchemas}}.',
|
||||||
dont_show_again: 'Không hiển thị lại',
|
// TODO: Translate
|
||||||
change_schema: 'Thay đổi',
|
show_me: 'Show me',
|
||||||
none: 'không có',
|
none: 'không có',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -150,7 +152,12 @@ export const vi: LanguageTranslation = {
|
|||||||
no_comments: 'Không có bình luận',
|
no_comments: 'Không có bình luận',
|
||||||
delete_field: 'Xóa trường',
|
delete_field: 'Xóa trường',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: 'Độ chính xác',
|
||||||
|
scale: 'Tỷ lệ',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Thuộc tính chỉ mục',
|
title: 'Thuộc tính chỉ mục',
|
||||||
@@ -231,6 +238,35 @@ export const vi: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -241,7 +277,13 @@ export const vi: LanguageTranslation = {
|
|||||||
undo: 'Hoàn tác',
|
undo: 'Hoàn tác',
|
||||||
redo: 'Làm lại',
|
redo: 'Làm lại',
|
||||||
reorder_diagram: 'Sắp xếp lại sơ đồ',
|
reorder_diagram: 'Sắp xếp lại sơ đồ',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: 'Làm nổi bật các bảng chồng chéo',
|
highlight_overlapping_tables: 'Làm nổi bật các bảng chồng chéo',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -373,6 +415,14 @@ export const vi: LanguageTranslation = {
|
|||||||
confirm: 'Xác nhận',
|
confirm: 'Xác nhận',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: 'Tạo lược đồ mới',
|
||||||
|
description:
|
||||||
|
'Chưa có lược đồ nào. Tạo lược đồ đầu tiên của bạn để tổ chức các bảng.',
|
||||||
|
create: 'Tạo',
|
||||||
|
cancel: 'Hủy',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'Hãy giúp chúng tôi cải thiện!',
|
title: 'Hãy giúp chúng tôi cải thiện!',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
hide_sidebar: '隐藏侧边栏',
|
hide_sidebar: '隐藏侧边栏',
|
||||||
hide_cardinality: '隐藏基数',
|
hide_cardinality: '隐藏基数',
|
||||||
show_cardinality: '展示基数',
|
show_cardinality: '展示基数',
|
||||||
|
show_field_attributes: '展示字段属性',
|
||||||
|
hide_field_attributes: '隐藏字段属性',
|
||||||
zoom_on_scroll: '滚动缩放',
|
zoom_on_scroll: '滚动缩放',
|
||||||
theme: '主题',
|
theme: '主题',
|
||||||
show_dependencies: '展示依赖',
|
show_dependencies: '展示依赖',
|
||||||
@@ -71,8 +73,8 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
title: '多个模式',
|
title: '多个模式',
|
||||||
description:
|
description:
|
||||||
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
|
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
|
||||||
dont_show_again: '不再展示',
|
// TODO: Translate
|
||||||
change_schema: '更改',
|
show_me: 'Show me',
|
||||||
none: '无',
|
none: '无',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -147,7 +149,12 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
no_comments: '空',
|
no_comments: '空',
|
||||||
delete_field: '删除字段',
|
delete_field: '删除字段',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: '精度',
|
||||||
|
scale: '小数位',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: '索引属性',
|
title: '索引属性',
|
||||||
@@ -228,6 +235,35 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -238,7 +274,13 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
undo: '撤销',
|
undo: '撤销',
|
||||||
redo: '重做',
|
redo: '重做',
|
||||||
reorder_diagram: '重新排列关系图',
|
reorder_diagram: '重新排列关系图',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: '突出显示重叠的表',
|
highlight_overlapping_tables: '突出显示重叠的表',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -369,6 +411,13 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
confirm: '更改',
|
confirm: '更改',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: '创建新模式',
|
||||||
|
description: '尚未存在任何模式。创建您的第一个模式来组织您的表。',
|
||||||
|
create: '创建',
|
||||||
|
cancel: '取消',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: '帮助我们改进!',
|
title: '帮助我们改进!',
|
||||||
description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!',
|
description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!',
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
hide_sidebar: '隱藏側邊欄',
|
hide_sidebar: '隱藏側邊欄',
|
||||||
hide_cardinality: '隱藏基數',
|
hide_cardinality: '隱藏基數',
|
||||||
show_cardinality: '顯示基數',
|
show_cardinality: '顯示基數',
|
||||||
|
hide_field_attributes: '隱藏欄位屬性',
|
||||||
|
show_field_attributes: '顯示欄位屬性',
|
||||||
zoom_on_scroll: '滾動縮放',
|
zoom_on_scroll: '滾動縮放',
|
||||||
theme: '主題',
|
theme: '主題',
|
||||||
show_dependencies: '顯示相依性',
|
show_dependencies: '顯示相依性',
|
||||||
@@ -71,8 +73,8 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
title: '多重 Schema',
|
title: '多重 Schema',
|
||||||
description:
|
description:
|
||||||
'此圖表中包含 {{schemasCount}} 個 Schema,目前顯示:{{formattedSchemas}}。',
|
'此圖表中包含 {{schemasCount}} 個 Schema,目前顯示:{{formattedSchemas}}。',
|
||||||
dont_show_again: '不再顯示',
|
// TODO: Translate
|
||||||
change_schema: '變更',
|
show_me: 'Show me',
|
||||||
none: '無',
|
none: '無',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -147,7 +149,12 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
no_comments: '無註解',
|
no_comments: '無註解',
|
||||||
delete_field: '刪除欄位',
|
delete_field: '刪除欄位',
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
|
default_value: 'Default Value',
|
||||||
|
no_default: 'No default',
|
||||||
|
// TODO: Translate
|
||||||
character_length: 'Max Length',
|
character_length: 'Max Length',
|
||||||
|
precision: '精度',
|
||||||
|
scale: '小數位',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: '索引屬性',
|
title: '索引屬性',
|
||||||
@@ -228,6 +235,35 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
@@ -238,7 +274,13 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
undo: '復原',
|
undo: '復原',
|
||||||
redo: '重做',
|
redo: '重做',
|
||||||
reorder_diagram: '重新排列圖表',
|
reorder_diagram: '重新排列圖表',
|
||||||
|
// TODO: Translate
|
||||||
|
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||||
|
custom_type_highlight_tooltip:
|
||||||
|
'Highlighting "{{typeName}}" - Click to clear',
|
||||||
highlight_overlapping_tables: '突出顯示重疊表格',
|
highlight_overlapping_tables: '突出顯示重疊表格',
|
||||||
|
// TODO: Translate
|
||||||
|
filter: 'Filter Tables',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
@@ -368,6 +410,14 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
confirm: '變更',
|
confirm: '變更',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
create_table_schema_dialog: {
|
||||||
|
title: '建立新 Schema',
|
||||||
|
description:
|
||||||
|
'尚未存在任何 Schema。建立您的第一個 Schema 來組織您的表格。',
|
||||||
|
create: '建立',
|
||||||
|
cancel: '取消',
|
||||||
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: '協助我們改善!',
|
title: '協助我們改善!',
|
||||||
description: '請在 GitHub 上給我們一顆星,只需點擊一下!',
|
description: '請在 GitHub 上給我們一顆星,只需點擊一下!',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { DBCustomType } from './domain';
|
||||||
import type { Area } from './domain/area';
|
import type { Area } from './domain/area';
|
||||||
import type { DBDependency } from './domain/db-dependency';
|
import type { DBDependency } from './domain/db-dependency';
|
||||||
import type { DBField } from './domain/db-field';
|
import type { DBField } from './domain/db-field';
|
||||||
@@ -48,6 +49,10 @@ const generateIdsMapFromDiagram = (
|
|||||||
idsMap.set(area.id, generateId());
|
idsMap.set(area.id, generateId());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
diagram.customTypes?.forEach((customType) => {
|
||||||
|
idsMap.set(customType.id, generateId());
|
||||||
|
});
|
||||||
|
|
||||||
return idsMap;
|
return idsMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,7 +129,7 @@ export const cloneDiagram = (
|
|||||||
} = {
|
} = {
|
||||||
generateId: defaultGenerateId,
|
generateId: defaultGenerateId,
|
||||||
}
|
}
|
||||||
): Diagram => {
|
): { diagram: Diagram; idsMap: Map<string, string> } => {
|
||||||
const { generateId } = options;
|
const { generateId } = options;
|
||||||
const diagramId = generateId();
|
const diagramId = generateId();
|
||||||
|
|
||||||
@@ -213,14 +218,38 @@ export const cloneDiagram = (
|
|||||||
})
|
})
|
||||||
.filter((area): area is Area => area !== null) ?? [];
|
.filter((area): area is Area => area !== null) ?? [];
|
||||||
|
|
||||||
|
const customTypes: DBCustomType[] =
|
||||||
|
diagram.customTypes
|
||||||
|
?.map((customType) => {
|
||||||
|
const id = getNewId(customType.id);
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
|
...customType,
|
||||||
|
id,
|
||||||
|
} satisfies DBCustomType;
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(customType): customType is DBCustomType => customType !== null
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
diagram: {
|
||||||
...diagram,
|
...diagram,
|
||||||
id: diagramId,
|
id: diagramId,
|
||||||
dependencies,
|
dependencies,
|
||||||
relationships,
|
relationships,
|
||||||
tables,
|
tables,
|
||||||
areas,
|
areas,
|
||||||
createdAt: new Date(),
|
customTypes,
|
||||||
updatedAt: new Date(),
|
createdAt: diagram.createdAt
|
||||||
|
? new Date(diagram.createdAt)
|
||||||
|
: new Date(),
|
||||||
|
updatedAt: diagram.updatedAt
|
||||||
|
? new Date(diagram.updatedAt)
|
||||||
|
: new Date(),
|
||||||
|
},
|
||||||
|
idsMap,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,18 +48,30 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
|
|||||||
{ name: 'mediumblob', id: 'mediumblob' },
|
{ name: 'mediumblob', id: 'mediumblob' },
|
||||||
{ name: 'tinyblob', id: 'tinyblob' },
|
{ name: 'tinyblob', id: 'tinyblob' },
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
{
|
||||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
name: 'varchar',
|
||||||
|
id: 'varchar',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
|
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
|
||||||
{ name: 'char large object', id: 'char_large_object' },
|
{ name: 'char large object', id: 'char_large_object' },
|
||||||
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
|
{
|
||||||
|
name: 'char varying',
|
||||||
|
id: 'char_varying',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
{ name: 'character large object', id: 'character_large_object' },
|
{ name: 'character large object', id: 'character_large_object' },
|
||||||
{
|
{
|
||||||
name: 'character varying',
|
name: 'character varying',
|
||||||
id: 'character_varying',
|
id: 'character_varying',
|
||||||
hasCharMaxLength: true,
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
},
|
},
|
||||||
{ name: 'nchar large object', id: 'nchar_large_object' },
|
{ name: 'nchar large object', id: 'nchar_large_object' },
|
||||||
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
|
{
|
||||||
|
name: 'nchar varying',
|
||||||
|
id: 'nchar_varying',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'national character large object',
|
name: 'national character large object',
|
||||||
id: 'national_character_large_object',
|
id: 'national_character_large_object',
|
||||||
@@ -67,22 +79,34 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
|
|||||||
{
|
{
|
||||||
name: 'national character varying',
|
name: 'national character varying',
|
||||||
id: 'national_character_varying',
|
id: 'national_character_varying',
|
||||||
hasCharMaxLength: true,
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'national char varying',
|
name: 'national char varying',
|
||||||
id: 'national_char_varying',
|
id: 'national_char_varying',
|
||||||
hasCharMaxLength: true,
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'national character',
|
name: 'national character',
|
||||||
id: 'national_character',
|
id: 'national_character',
|
||||||
hasCharMaxLength: true,
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'national char',
|
||||||
|
id: 'national_char',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
},
|
},
|
||||||
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
|
|
||||||
{ name: 'binary large object', id: 'binary_large_object' },
|
{ name: 'binary large object', id: 'binary_large_object' },
|
||||||
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
|
{
|
||||||
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
|
name: 'binary varying',
|
||||||
|
id: 'binary_varying',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fixedstring',
|
||||||
|
id: 'fixedstring',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
{ name: 'string', id: 'string' },
|
{ name: 'string', id: 'string' },
|
||||||
|
|
||||||
// Date Types
|
// Date Types
|
||||||
|
|||||||
@@ -7,15 +7,30 @@ import { mysqlDataTypes } from './mysql-data-types';
|
|||||||
import { postgresDataTypes } from './postgres-data-types';
|
import { postgresDataTypes } from './postgres-data-types';
|
||||||
import { sqlServerDataTypes } from './sql-server-data-types';
|
import { sqlServerDataTypes } from './sql-server-data-types';
|
||||||
import { sqliteDataTypes } from './sqlite-data-types';
|
import { sqliteDataTypes } from './sqlite-data-types';
|
||||||
|
import { oracleDataTypes } from './oracle-data-types';
|
||||||
|
|
||||||
export interface DataType {
|
export interface DataType {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataTypeData extends DataType {
|
export interface FieldAttributeRange {
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
default: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldAttributes {
|
||||||
hasCharMaxLength?: boolean;
|
hasCharMaxLength?: boolean;
|
||||||
|
hasCharMaxLengthOption?: boolean;
|
||||||
|
precision?: FieldAttributeRange;
|
||||||
|
scale?: FieldAttributeRange;
|
||||||
|
maxLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTypeData extends DataType {
|
||||||
usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common
|
usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common
|
||||||
|
fieldAttributes?: FieldAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dataTypeSchema: z.ZodType<DataType> = z.object({
|
export const dataTypeSchema: z.ZodType<DataType> = z.object({
|
||||||
@@ -32,6 +47,7 @@ export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
|
|||||||
[DatabaseType.SQLITE]: sqliteDataTypes,
|
[DatabaseType.SQLITE]: sqliteDataTypes,
|
||||||
[DatabaseType.CLICKHOUSE]: clickhouseDataTypes,
|
[DatabaseType.CLICKHOUSE]: clickhouseDataTypes,
|
||||||
[DatabaseType.COCKROACHDB]: postgresDataTypes,
|
[DatabaseType.COCKROACHDB]: postgresDataTypes,
|
||||||
|
[DatabaseType.ORACLE]: oracleDataTypes,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const sortDataTypes = (dataTypes: DataTypeData[]): DataTypeData[] => {
|
export const sortDataTypes = (dataTypes: DataTypeData[]): DataTypeData[] => {
|
||||||
@@ -71,6 +87,9 @@ export const sortedDataTypeMap: Record<DatabaseType, readonly DataTypeData[]> =
|
|||||||
[DatabaseType.COCKROACHDB]: sortDataTypes([
|
[DatabaseType.COCKROACHDB]: sortDataTypes([
|
||||||
...dataTypeMap[DatabaseType.COCKROACHDB],
|
...dataTypeMap[DatabaseType.COCKROACHDB],
|
||||||
]),
|
]),
|
||||||
|
[DatabaseType.ORACLE]: sortDataTypes([
|
||||||
|
...dataTypeMap[DatabaseType.ORACLE],
|
||||||
|
]),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
|
const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
|
||||||
@@ -88,6 +107,7 @@ const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
|
|||||||
[DatabaseType.SQLITE]: {},
|
[DatabaseType.SQLITE]: {},
|
||||||
[DatabaseType.CLICKHOUSE]: {},
|
[DatabaseType.CLICKHOUSE]: {},
|
||||||
[DatabaseType.COCKROACHDB]: {},
|
[DatabaseType.COCKROACHDB]: {},
|
||||||
|
[DatabaseType.ORACLE]: {},
|
||||||
[DatabaseType.GENERIC]: {},
|
[DatabaseType.GENERIC]: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import type { DataTypeData } from './data-types';
|
|||||||
|
|
||||||
export const genericDataTypes: readonly DataTypeData[] = [
|
export const genericDataTypes: readonly DataTypeData[] = [
|
||||||
// Level 1 - Most commonly used types
|
// Level 1 - Most commonly used types
|
||||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
{
|
||||||
|
name: 'varchar',
|
||||||
|
id: 'varchar',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
usageLevel: 1,
|
||||||
|
},
|
||||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||||
@@ -10,23 +15,62 @@ export const genericDataTypes: readonly DataTypeData[] = [
|
|||||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||||
|
|
||||||
// Level 2 - Second most common types
|
// Level 2 - Second most common types
|
||||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
{
|
||||||
|
name: 'decimal',
|
||||||
|
id: 'decimal',
|
||||||
|
usageLevel: 2,
|
||||||
|
fieldAttributes: {
|
||||||
|
precision: {
|
||||||
|
max: 999,
|
||||||
|
min: 1,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
max: 999,
|
||||||
|
min: 0,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
|
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
|
||||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||||
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
|
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
|
||||||
|
|
||||||
// Less common types
|
// Less common types
|
||||||
{ name: 'bigint', id: 'bigint' },
|
{ name: 'bigint', id: 'bigint' },
|
||||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
{
|
||||||
|
name: 'binary',
|
||||||
|
id: 'binary',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
|
||||||
{ name: 'double', id: 'double' },
|
{ name: 'double', id: 'double' },
|
||||||
{ name: 'enum', id: 'enum' },
|
{ name: 'enum', id: 'enum' },
|
||||||
{ name: 'float', id: 'float' },
|
{ name: 'float', id: 'float' },
|
||||||
{ name: 'numeric', id: 'numeric' },
|
{
|
||||||
|
name: 'numeric',
|
||||||
|
id: 'numeric',
|
||||||
|
fieldAttributes: {
|
||||||
|
precision: {
|
||||||
|
max: 999,
|
||||||
|
min: 1,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
max: 999,
|
||||||
|
min: 0,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'real', id: 'real' },
|
{ name: 'real', id: 'real' },
|
||||||
{ name: 'set', id: 'set' },
|
{ name: 'set', id: 'set' },
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
{ name: 'time', id: 'time' },
|
{ name: 'time', id: 'time' },
|
||||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
{
|
||||||
|
name: 'varbinary',
|
||||||
|
id: 'varbinary',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -4,12 +4,32 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
|
|||||||
// Level 1 - Most commonly used types
|
// Level 1 - Most commonly used types
|
||||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||||
{ name: 'bigint', id: 'bigint', usageLevel: 1 },
|
{ name: 'bigint', id: 'bigint', usageLevel: 1 },
|
||||||
{ name: 'decimal', id: 'decimal', usageLevel: 1 },
|
{
|
||||||
|
name: 'decimal',
|
||||||
|
id: 'decimal',
|
||||||
|
usageLevel: 1,
|
||||||
|
fieldAttributes: {
|
||||||
|
precision: {
|
||||||
|
max: 65,
|
||||||
|
min: 1,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
max: 30,
|
||||||
|
min: 0,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||||
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
|
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
|
||||||
{ name: 'date', id: 'date', usageLevel: 1 },
|
{ name: 'date', id: 'date', usageLevel: 1 },
|
||||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
{
|
||||||
|
name: 'varchar',
|
||||||
|
id: 'varchar',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||||
|
|
||||||
// Level 2 - Second most common types
|
// Level 2 - Second most common types
|
||||||
@@ -20,16 +40,39 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
|
|||||||
{ name: 'tinyint', id: 'tinyint' },
|
{ name: 'tinyint', id: 'tinyint' },
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
{ name: 'mediumint', id: 'mediumint' },
|
{ name: 'mediumint', id: 'mediumint' },
|
||||||
{ name: 'numeric', id: 'numeric' },
|
{
|
||||||
|
name: 'numeric',
|
||||||
|
id: 'numeric',
|
||||||
|
fieldAttributes: {
|
||||||
|
precision: {
|
||||||
|
max: 65,
|
||||||
|
min: 1,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
max: 30,
|
||||||
|
min: 0,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'float', id: 'float' },
|
{ name: 'float', id: 'float' },
|
||||||
{ name: 'double', id: 'double' },
|
{ name: 'double', id: 'double' },
|
||||||
{ name: 'bit', id: 'bit' },
|
{ name: 'bit', id: 'bit' },
|
||||||
{ name: 'bool', id: 'bool' },
|
{ name: 'bool', id: 'bool' },
|
||||||
{ name: 'time', id: 'time' },
|
{ name: 'time', id: 'time' },
|
||||||
{ name: 'year', id: 'year' },
|
{ name: 'year', id: 'year' },
|
||||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
|
||||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
{
|
||||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
name: 'binary',
|
||||||
|
id: 'binary',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'varbinary',
|
||||||
|
id: 'varbinary',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
{ name: 'tinyblob', id: 'tinyblob' },
|
{ name: 'tinyblob', id: 'tinyblob' },
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'mediumblob', id: 'mediumblob' },
|
{ name: 'mediumblob', id: 'mediumblob' },
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import type { DataTypeData } from './data-types';
|
|||||||
export const mysqlDataTypes: readonly DataTypeData[] = [
|
export const mysqlDataTypes: readonly DataTypeData[] = [
|
||||||
// Level 1 - Most commonly used types
|
// Level 1 - Most commonly used types
|
||||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
{
|
||||||
|
name: 'varchar',
|
||||||
|
id: 'varchar',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
usageLevel: 1,
|
||||||
|
},
|
||||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||||
@@ -11,7 +16,23 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
|
|||||||
|
|
||||||
// Level 2 - Second most common types
|
// Level 2 - Second most common types
|
||||||
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
||||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
{
|
||||||
|
name: 'decimal',
|
||||||
|
id: 'decimal',
|
||||||
|
usageLevel: 2,
|
||||||
|
fieldAttributes: {
|
||||||
|
precision: {
|
||||||
|
max: 65,
|
||||||
|
min: 1,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
max: 30,
|
||||||
|
min: 0,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
|
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
|
||||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||||
|
|
||||||
@@ -22,7 +43,7 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
|
|||||||
{ name: 'float', id: 'float' },
|
{ name: 'float', id: 'float' },
|
||||||
{ name: 'double', id: 'double' },
|
{ name: 'double', id: 'double' },
|
||||||
{ name: 'bit', id: 'bit' },
|
{ name: 'bit', id: 'bit' },
|
||||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
|
||||||
{ name: 'tinytext', id: 'tinytext' },
|
{ name: 'tinytext', id: 'tinytext' },
|
||||||
{ name: 'mediumtext', id: 'mediumtext' },
|
{ name: 'mediumtext', id: 'mediumtext' },
|
||||||
{ name: 'longtext', id: 'longtext' },
|
{ name: 'longtext', id: 'longtext' },
|
||||||
|
|||||||
77
src/lib/data/data-types/oracle-data-types.ts
Normal file
77
src/lib/data/data-types/oracle-data-types.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
|
export const oracleDataTypes: readonly DataTypeData[] = [
|
||||||
|
// Character types
|
||||||
|
{
|
||||||
|
name: 'VARCHAR2',
|
||||||
|
id: 'varchar2',
|
||||||
|
usageLevel: 1,
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'NVARCHAR2',
|
||||||
|
id: 'nvarchar2',
|
||||||
|
usageLevel: 1,
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CHAR',
|
||||||
|
id: 'char',
|
||||||
|
usageLevel: 2,
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'NCHAR',
|
||||||
|
id: 'nchar',
|
||||||
|
usageLevel: 2,
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
|
{ name: 'CLOB', id: 'clob', usageLevel: 2 },
|
||||||
|
{ name: 'NCLOB', id: 'nclob', usageLevel: 2 },
|
||||||
|
|
||||||
|
// Numeric types
|
||||||
|
{ name: 'NUMBER', id: 'number', usageLevel: 1 },
|
||||||
|
{ name: 'FLOAT', id: 'float', usageLevel: 2 },
|
||||||
|
{ name: 'BINARY_FLOAT', id: 'binary_float', usageLevel: 2 },
|
||||||
|
{ name: 'BINARY_DOUBLE', id: 'binary_double', usageLevel: 2 },
|
||||||
|
|
||||||
|
// Date/Time types
|
||||||
|
{ name: 'DATE', id: 'date', usageLevel: 1 },
|
||||||
|
{ name: 'TIMESTAMP', id: 'timestamp', usageLevel: 1 },
|
||||||
|
{
|
||||||
|
name: 'TIMESTAMP WITH TIME ZONE',
|
||||||
|
id: 'timestamp_with_time_zone',
|
||||||
|
usageLevel: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TIMESTAMP WITH LOCAL TIME ZONE',
|
||||||
|
id: 'timestamp_with_local_time_zone',
|
||||||
|
usageLevel: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'INTERVAL YEAR TO MONTH',
|
||||||
|
id: 'interval_year_to_month',
|
||||||
|
usageLevel: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'INTERVAL DAY TO SECOND',
|
||||||
|
id: 'interval_day_to_second',
|
||||||
|
usageLevel: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Large Object types
|
||||||
|
{ name: 'BLOB', id: 'blob', usageLevel: 2 },
|
||||||
|
{ name: 'BFILE', id: 'bfile', usageLevel: 2 },
|
||||||
|
|
||||||
|
// Other types
|
||||||
|
{
|
||||||
|
name: 'RAW',
|
||||||
|
id: 'raw',
|
||||||
|
usageLevel: 2,
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
|
{ name: 'LONG RAW', id: 'long_raw', usageLevel: 2 },
|
||||||
|
{ name: 'ROWID', id: 'rowid', usageLevel: 2 },
|
||||||
|
{ name: 'UROWID', id: 'urowid', usageLevel: 2 },
|
||||||
|
{ name: 'XMLType', id: 'xmltype', usageLevel: 2 },
|
||||||
|
] as const;
|
||||||
@@ -3,7 +3,12 @@ import type { DataTypeData } from './data-types';
|
|||||||
export const postgresDataTypes: readonly DataTypeData[] = [
|
export const postgresDataTypes: readonly DataTypeData[] = [
|
||||||
// Level 1 - Most commonly used types
|
// Level 1 - Most commonly used types
|
||||||
{ name: 'integer', id: 'integer', usageLevel: 1 },
|
{ name: 'integer', id: 'integer', usageLevel: 1 },
|
||||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
{
|
||||||
|
name: 'varchar',
|
||||||
|
id: 'varchar',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
usageLevel: 1,
|
||||||
|
},
|
||||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||||
@@ -11,7 +16,23 @@ export const postgresDataTypes: readonly DataTypeData[] = [
|
|||||||
|
|
||||||
// Level 2 - Second most common types
|
// Level 2 - Second most common types
|
||||||
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
||||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
{
|
||||||
|
name: 'decimal',
|
||||||
|
id: 'decimal',
|
||||||
|
usageLevel: 2,
|
||||||
|
fieldAttributes: {
|
||||||
|
precision: {
|
||||||
|
max: 131072,
|
||||||
|
min: 0,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
max: 16383,
|
||||||
|
min: 0,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'serial', id: 'serial', usageLevel: 2 },
|
{ name: 'serial', id: 'serial', usageLevel: 2 },
|
||||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||||
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 },
|
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 },
|
||||||
@@ -23,18 +44,33 @@ export const postgresDataTypes: readonly DataTypeData[] = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Less common types
|
// Less common types
|
||||||
{ name: 'numeric', id: 'numeric' },
|
{
|
||||||
|
name: 'numeric',
|
||||||
|
id: 'numeric',
|
||||||
|
fieldAttributes: {
|
||||||
|
precision: {
|
||||||
|
max: 131072,
|
||||||
|
min: 0,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
max: 16383,
|
||||||
|
min: 0,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'real', id: 'real' },
|
{ name: 'real', id: 'real' },
|
||||||
{ name: 'double precision', id: 'double_precision' },
|
{ name: 'double precision', id: 'double_precision' },
|
||||||
{ name: 'smallserial', id: 'smallserial' },
|
{ name: 'smallserial', id: 'smallserial' },
|
||||||
{ name: 'bigserial', id: 'bigserial' },
|
{ name: 'bigserial', id: 'bigserial' },
|
||||||
{ name: 'money', id: 'money' },
|
{ name: 'money', id: 'money' },
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
|
||||||
{
|
{
|
||||||
name: 'character varying',
|
name: 'character varying',
|
||||||
id: 'character_varying',
|
id: 'character_varying',
|
||||||
hasCharMaxLength: true,
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
},
|
},
|
||||||
{ name: 'time', id: 'time' },
|
{ name: 'time', id: 'time' },
|
||||||
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
|
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
|
||||||
|
|||||||
@@ -4,32 +4,93 @@ export const sqlServerDataTypes: readonly DataTypeData[] = [
|
|||||||
// Level 1 - Most commonly used types
|
// Level 1 - Most commonly used types
|
||||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||||
{ name: 'bit', id: 'bit', usageLevel: 1 },
|
{ name: 'bit', id: 'bit', usageLevel: 1 },
|
||||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
{
|
||||||
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true, usageLevel: 1 },
|
name: 'varchar',
|
||||||
|
id: 'varchar',
|
||||||
|
fieldAttributes: {
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
hasCharMaxLengthOption: true,
|
||||||
|
maxLength: 8000,
|
||||||
|
},
|
||||||
|
usageLevel: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nvarchar',
|
||||||
|
id: 'nvarchar',
|
||||||
|
fieldAttributes: {
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
hasCharMaxLengthOption: true,
|
||||||
|
maxLength: 4000,
|
||||||
|
},
|
||||||
|
usageLevel: 1,
|
||||||
|
},
|
||||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||||
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
|
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
|
||||||
{ name: 'date', id: 'date', usageLevel: 1 },
|
{ name: 'date', id: 'date', usageLevel: 1 },
|
||||||
|
|
||||||
// Level 2 - Second most common types
|
// Level 2 - Second most common types
|
||||||
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
||||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
{
|
||||||
|
name: 'decimal',
|
||||||
|
id: 'decimal',
|
||||||
|
usageLevel: 2,
|
||||||
|
fieldAttributes: {
|
||||||
|
precision: {
|
||||||
|
max: 38,
|
||||||
|
min: 1,
|
||||||
|
default: 18,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
max: 38,
|
||||||
|
min: 0,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'datetime2', id: 'datetime2', usageLevel: 2 },
|
{ name: 'datetime2', id: 'datetime2', usageLevel: 2 },
|
||||||
{ name: 'uniqueidentifier', id: 'uniqueidentifier', usageLevel: 2 },
|
{ name: 'uniqueidentifier', id: 'uniqueidentifier', usageLevel: 2 },
|
||||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||||
|
|
||||||
// Less common types
|
// Less common types
|
||||||
{ name: 'numeric', id: 'numeric' },
|
{
|
||||||
|
name: 'numeric',
|
||||||
|
id: 'numeric',
|
||||||
|
fieldAttributes: {
|
||||||
|
precision: {
|
||||||
|
max: 38,
|
||||||
|
min: 1,
|
||||||
|
default: 18,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
max: 38,
|
||||||
|
min: 0,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
{ name: 'smallmoney', id: 'smallmoney' },
|
{ name: 'smallmoney', id: 'smallmoney' },
|
||||||
{ name: 'tinyint', id: 'tinyint' },
|
{ name: 'tinyint', id: 'tinyint' },
|
||||||
{ name: 'money', id: 'money' },
|
{ name: 'money', id: 'money' },
|
||||||
{ name: 'float', id: 'float' },
|
{ name: 'float', id: 'float' },
|
||||||
{ name: 'real', id: 'real' },
|
{ name: 'real', id: 'real' },
|
||||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
{ name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } },
|
||||||
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
|
{ name: 'nchar', id: 'nchar', fieldAttributes: { hasCharMaxLength: true } },
|
||||||
{ name: 'ntext', id: 'ntext' },
|
{ name: 'ntext', id: 'ntext' },
|
||||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
{
|
||||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
name: 'binary',
|
||||||
|
id: 'binary',
|
||||||
|
fieldAttributes: { hasCharMaxLength: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'varbinary',
|
||||||
|
id: 'varbinary',
|
||||||
|
fieldAttributes: {
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
hasCharMaxLengthOption: true,
|
||||||
|
maxLength: 8000,
|
||||||
|
},
|
||||||
|
},
|
||||||
{ name: 'image', id: 'image' },
|
{ name: 'image', id: 'image' },
|
||||||
{ name: 'datetimeoffset', id: 'datetimeoffset' },
|
{ name: 'datetimeoffset', id: 'datetimeoffset' },
|
||||||
{ name: 'smalldatetime', id: 'smalldatetime' },
|
{ name: 'smalldatetime', id: 'smalldatetime' },
|
||||||
|
|||||||
@@ -10,21 +10,41 @@ export const sqliteDataTypes: readonly DataTypeData[] = [
|
|||||||
|
|
||||||
// SQLite type aliases and common types
|
// SQLite type aliases and common types
|
||||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
{
|
||||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
name: 'varchar',
|
||||||
{ name: 'date', id: 'date', usageLevel: 1 },
|
id: 'varchar',
|
||||||
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
|
fieldAttributes: {
|
||||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
|
usageLevel: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timestamp',
|
||||||
|
id: 'timestamp',
|
||||||
|
usageLevel: 1,
|
||||||
|
},
|
||||||
|
|
||||||
// Level 2 - Second most common types
|
// Level 2 - Second most common types
|
||||||
{ name: 'numeric', id: 'numeric', usageLevel: 2 },
|
{ name: 'numeric', id: 'numeric', usageLevel: 2 },
|
||||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
||||||
{ name: 'float', id: 'float', usageLevel: 2 },
|
{ name: 'float', id: 'float', usageLevel: 2 },
|
||||||
|
{
|
||||||
|
name: 'decimal',
|
||||||
|
id: 'decimal',
|
||||||
|
usageLevel: 2,
|
||||||
|
},
|
||||||
{ name: 'double', id: 'double', usageLevel: 2 },
|
{ name: 'double', id: 'double', usageLevel: 2 },
|
||||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||||
|
|
||||||
// Less common types (all map to SQLite storage classes)
|
// Less common types (all map to SQLite storage classes)
|
||||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
{
|
||||||
|
name: 'char',
|
||||||
|
id: 'char',
|
||||||
|
fieldAttributes: {
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
|
usageLevel: 2,
|
||||||
|
},
|
||||||
{ name: 'binary', id: 'binary' },
|
{ name: 'binary', id: 'binary' },
|
||||||
{ name: 'varbinary', id: 'varbinary' },
|
{ name: 'varbinary', id: 'varbinary' },
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export const defaultSchemas: { [key in DatabaseType]?: string } = {
|
|||||||
[DatabaseType.POSTGRESQL]: 'public',
|
[DatabaseType.POSTGRESQL]: 'public',
|
||||||
[DatabaseType.SQL_SERVER]: 'dbo',
|
[DatabaseType.SQL_SERVER]: 'dbo',
|
||||||
[DatabaseType.CLICKHOUSE]: 'default',
|
[DatabaseType.CLICKHOUSE]: 'default',
|
||||||
|
[DatabaseType.COCKROACHDB]: 'public',
|
||||||
};
|
};
|
||||||
|
|||||||
870
src/lib/data/export-metadata/__tests__/export-sql-dbml.test.ts
Normal file
870
src/lib/data/export-metadata/__tests__/export-sql-dbml.test.ts
Normal file
@@ -0,0 +1,870 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { exportBaseSQL } from '../export-sql-script';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
|
||||||
|
// Mock the dbml/core importer
|
||||||
|
vi.mock('@dbml/core', () => ({
|
||||||
|
importer: {
|
||||||
|
import: vi.fn((sql: string) => {
|
||||||
|
// Return a simplified DBML for testing
|
||||||
|
return sql;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('DBML Export - SQL Generation Tests', () => {
|
||||||
|
// Helper to generate test IDs and timestamps
|
||||||
|
let idCounter = 0;
|
||||||
|
const testId = () => `test-id-${++idCounter}`;
|
||||||
|
const testTime = Date.now();
|
||||||
|
|
||||||
|
// Helper to create a field with all required properties
|
||||||
|
const createField = (overrides: Partial<DBField>): DBField =>
|
||||||
|
({
|
||||||
|
id: testId(),
|
||||||
|
name: 'field',
|
||||||
|
type: { id: 'text', name: 'text' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
createdAt: testTime,
|
||||||
|
...overrides,
|
||||||
|
}) as DBField;
|
||||||
|
|
||||||
|
// Helper to create a table with all required properties
|
||||||
|
const createTable = (overrides: Partial<DBTable>): DBTable =>
|
||||||
|
({
|
||||||
|
id: testId(),
|
||||||
|
name: 'table',
|
||||||
|
fields: [],
|
||||||
|
indexes: [],
|
||||||
|
createdAt: testTime,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 200,
|
||||||
|
...overrides,
|
||||||
|
}) as DBTable;
|
||||||
|
|
||||||
|
// Helper to create a diagram with all required properties
|
||||||
|
const createDiagram = (overrides: Partial<Diagram>): Diagram =>
|
||||||
|
({
|
||||||
|
id: testId(),
|
||||||
|
name: 'diagram',
|
||||||
|
databaseType: DatabaseType.GENERIC,
|
||||||
|
tables: [],
|
||||||
|
relationships: [],
|
||||||
|
createdAt: testTime,
|
||||||
|
updatedAt: testTime,
|
||||||
|
...overrides,
|
||||||
|
}) as Diagram;
|
||||||
|
|
||||||
|
describe('Composite Primary Keys', () => {
|
||||||
|
it('should handle tables with composite primary keys correctly', () => {
|
||||||
|
const tableId = testId();
|
||||||
|
const field1Id = testId();
|
||||||
|
const field2Id = testId();
|
||||||
|
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Enchanted Library',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: tableId,
|
||||||
|
name: 'spell_components',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: field1Id,
|
||||||
|
name: 'spell_id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: field2Id,
|
||||||
|
name: 'component_id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'quantity',
|
||||||
|
type: { id: 'integer', name: 'integer' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
default: '1',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#FFD700',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should contain composite primary key syntax
|
||||||
|
expect(sql).toContain('PRIMARY KEY (spell_id, component_id)');
|
||||||
|
// Should NOT contain individual PRIMARY KEY constraints
|
||||||
|
expect(sql).not.toMatch(/spell_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
|
||||||
|
expect(sql).not.toMatch(
|
||||||
|
/component_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single primary keys inline', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Wizard Academy',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'wizards',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'name',
|
||||||
|
type: { id: 'varchar', name: 'varchar' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#9370DB',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should contain inline PRIMARY KEY
|
||||||
|
expect(sql).toMatch(/id\s+uuid\s+NOT NULL\s+PRIMARY KEY/);
|
||||||
|
// Should NOT contain separate PRIMARY KEY constraint
|
||||||
|
expect(sql).not.toContain('PRIMARY KEY (id)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Default Value Handling', () => {
|
||||||
|
it('should skip invalid default values like "has default"', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Potion Shop',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'potions',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'is_active',
|
||||||
|
type: { id: 'boolean', name: 'boolean' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
default: 'has default',
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'stock_count',
|
||||||
|
type: { id: 'integer', name: 'integer' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
default: 'DEFAULT has default',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#98FB98',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not contain invalid default values
|
||||||
|
expect(sql).not.toContain('DEFAULT has default');
|
||||||
|
expect(sql).not.toContain('DEFAULT DEFAULT has default');
|
||||||
|
// The fields should still be in the table
|
||||||
|
expect(sql).toContain('is_active boolean');
|
||||||
|
expect(sql).toContain('stock_count integer NOT NULL'); // integer gets simplified to int
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle valid default values correctly', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Treasure Vault',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'treasures',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'gold_value',
|
||||||
|
type: { id: 'numeric', name: 'numeric' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
default: '100.50',
|
||||||
|
precision: 10,
|
||||||
|
scale: 2,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'created_at',
|
||||||
|
type: { id: 'timestamp', name: 'timestamp' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
default: 'now()',
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'currency',
|
||||||
|
type: { id: 'char', name: 'char' },
|
||||||
|
characterMaximumLength: '3',
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
default: 'EUR',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#FFD700',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should contain valid defaults
|
||||||
|
expect(sql).toContain('DEFAULT 100.50');
|
||||||
|
expect(sql).toContain('DEFAULT now()');
|
||||||
|
expect(sql).toContain('DEFAULT EUR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle NOW and similar default values', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Quest Log',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'quests',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'created_at',
|
||||||
|
type: { id: 'timestamp', name: 'timestamp' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
default: 'NOW',
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'updated_at',
|
||||||
|
type: { id: 'timestamp', name: 'timestamp' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
default: "('now')",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#4169E1',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should convert NOW to NOW() and ('now') to now()
|
||||||
|
expect(sql).toContain('created_at timestamp DEFAULT NOW');
|
||||||
|
expect(sql).toContain('updated_at timestamp DEFAULT now()');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Character Type Handling', () => {
|
||||||
|
it('should handle char types with and without length correctly', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Dragon Registry',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'dragons',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'element_code',
|
||||||
|
type: { id: 'char', name: 'char' },
|
||||||
|
characterMaximumLength: '2',
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'status',
|
||||||
|
type: { id: 'char', name: 'char' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#FF6347',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should handle char with explicit length
|
||||||
|
expect(sql).toContain('element_code char(2)');
|
||||||
|
// Should add default length for char without length
|
||||||
|
expect(sql).toContain('status char(1)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have spaces between char and parentheses', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Rune Inscriptions',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'runes',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'integer', name: 'integer' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'symbol',
|
||||||
|
type: { id: 'char', name: 'char' },
|
||||||
|
characterMaximumLength: '5',
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#8B4513',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not contain "char (" with space
|
||||||
|
expect(sql).not.toContain('char (');
|
||||||
|
expect(sql).toContain('char(5)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Complex Table Structures', () => {
|
||||||
|
it('should handle tables with no primary key', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Alchemy Log',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'experiment_logs',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'experiment_id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'alchemist_id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'result',
|
||||||
|
type: { id: 'text', name: 'text' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'logged_at',
|
||||||
|
type: { id: 'timestamp', name: 'timestamp' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
default: 'now()',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#32CD32',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should create a valid table without primary key
|
||||||
|
expect(sql).toContain('CREATE TABLE experiment_logs');
|
||||||
|
expect(sql).not.toContain('PRIMARY KEY');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple tables with relationships', () => {
|
||||||
|
const guildTableId = testId();
|
||||||
|
const memberTableId = testId();
|
||||||
|
const guildIdFieldId = testId();
|
||||||
|
const memberGuildIdFieldId = testId();
|
||||||
|
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Adventurer Guild System',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: guildTableId,
|
||||||
|
name: 'guilds',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: guildIdFieldId,
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'name',
|
||||||
|
type: { id: 'varchar', name: 'varchar' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: true,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'founded_year',
|
||||||
|
type: { id: 'integer', name: 'integer' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
color: '#4169E1',
|
||||||
|
}),
|
||||||
|
createTable({
|
||||||
|
id: memberTableId,
|
||||||
|
name: 'guild_members',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: memberGuildIdFieldId,
|
||||||
|
name: 'guild_id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'member_name',
|
||||||
|
type: { id: 'varchar', name: 'varchar' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'rank',
|
||||||
|
type: { id: 'varchar', name: 'varchar' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
default: "'Novice'",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
x: 250,
|
||||||
|
y: 0,
|
||||||
|
color: '#FFD700',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [
|
||||||
|
{
|
||||||
|
id: testId(),
|
||||||
|
name: 'fk_guild_members_guild',
|
||||||
|
sourceTableId: memberTableId,
|
||||||
|
targetTableId: guildTableId,
|
||||||
|
sourceFieldId: memberGuildIdFieldId,
|
||||||
|
targetFieldId: guildIdFieldId,
|
||||||
|
sourceCardinality: 'many',
|
||||||
|
targetCardinality: 'one',
|
||||||
|
createdAt: testTime,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should create both tables
|
||||||
|
expect(sql).toContain('CREATE TABLE guilds');
|
||||||
|
expect(sql).toContain('CREATE TABLE guild_members');
|
||||||
|
// Should create foreign key
|
||||||
|
expect(sql).toContain(
|
||||||
|
'ALTER TABLE guild_members ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY (guild_id) REFERENCES guilds (id)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Schema Support', () => {
|
||||||
|
it('should handle tables with schemas correctly', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Multi-Realm Database',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'portals',
|
||||||
|
schema: 'transportation',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'destination',
|
||||||
|
type: { id: 'varchar', name: 'varchar' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#9370DB',
|
||||||
|
}),
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'spells',
|
||||||
|
schema: 'magic',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'integer', name: 'integer' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'name',
|
||||||
|
type: { id: 'varchar', name: 'varchar' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
x: 250,
|
||||||
|
y: 0,
|
||||||
|
color: '#FF1493',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should create schemas
|
||||||
|
expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS transportation');
|
||||||
|
expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS magic');
|
||||||
|
// Should use schema-qualified table names
|
||||||
|
expect(sql).toContain('CREATE TABLE transportation.portals');
|
||||||
|
expect(sql).toContain('CREATE TABLE magic.spells');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('should handle empty tables array', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Empty Realm',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sql).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle tables with empty fields', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Void Space',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'empty_table',
|
||||||
|
fields: [],
|
||||||
|
indexes: [],
|
||||||
|
color: '#000000',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should still create table structure
|
||||||
|
expect(sql).toContain('CREATE TABLE empty_table');
|
||||||
|
expect(sql).toContain('(\n\n)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle special characters in default values', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Mystic Scrolls',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'scrolls',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'uuid', name: 'uuid' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'inscription',
|
||||||
|
type: { id: 'text', name: 'text' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
default: "'Ancient\\'s Wisdom'",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#8B4513',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should preserve escaped quotes
|
||||||
|
expect(sql).toContain("DEFAULT 'Ancient\\'s Wisdom'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle numeric precision and scale', () => {
|
||||||
|
const diagram: Diagram = createDiagram({
|
||||||
|
id: testId(),
|
||||||
|
name: 'Treasury',
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
tables: [
|
||||||
|
createTable({
|
||||||
|
id: testId(),
|
||||||
|
name: 'gold_reserves',
|
||||||
|
fields: [
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'id',
|
||||||
|
type: { id: 'integer', name: 'integer' },
|
||||||
|
primaryKey: true,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'amount',
|
||||||
|
type: { id: 'numeric', name: 'numeric' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: false,
|
||||||
|
unique: false,
|
||||||
|
precision: 15,
|
||||||
|
scale: 2,
|
||||||
|
}),
|
||||||
|
createField({
|
||||||
|
id: testId(),
|
||||||
|
name: 'interest_rate',
|
||||||
|
type: { id: 'numeric', name: 'numeric' },
|
||||||
|
primaryKey: false,
|
||||||
|
nullable: true,
|
||||||
|
unique: false,
|
||||||
|
precision: 5,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
color: '#FFD700',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
relationships: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sql = exportBaseSQL({
|
||||||
|
diagram,
|
||||||
|
targetDatabaseType: DatabaseType.POSTGRESQL,
|
||||||
|
isDBMLFlow: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should include precision and scale
|
||||||
|
expect(sql).toContain('amount numeric(15, 2)');
|
||||||
|
// Should include precision only when scale is not provided
|
||||||
|
expect(sql).toContain('interest_rate numeric(5)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -48,6 +48,50 @@ export function exportFieldComment(comment: string): string {
|
|||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function escapeSQLComment(comment: string): string {
|
||||||
|
if (!comment) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape single quotes by doubling them
|
||||||
|
let escaped = comment.replace(/'/g, "''");
|
||||||
|
|
||||||
|
// Replace newlines with spaces to prevent breaking SQL syntax
|
||||||
|
// Some databases support multi-line comments with specific syntax,
|
||||||
|
// but for maximum compatibility, we'll replace newlines with spaces
|
||||||
|
escaped = escaped.replace(/[\r\n]+/g, ' ');
|
||||||
|
|
||||||
|
// Trim any excessive whitespace
|
||||||
|
escaped = escaped.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
return escaped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTableComment(comment: string): string {
|
||||||
|
if (!comment) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by newlines and add -- to each line
|
||||||
|
return (
|
||||||
|
comment
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => `-- ${line}`)
|
||||||
|
.join('\n') + '\n'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMSSQLTableComment(comment: string): string {
|
||||||
|
if (!comment) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For MSSQL, we use multi-line comment syntax
|
||||||
|
// Escape */ to prevent breaking the comment block
|
||||||
|
const escaped = comment.replace(/\*\//g, '* /');
|
||||||
|
return `/**\n${escaped}\n*/\n`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getInlineFK(table: DBTable, diagram: Diagram): string {
|
export function getInlineFK(table: DBTable, diagram: Diagram): string {
|
||||||
if (!diagram.relationships) {
|
if (!diagram.relationships) {
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
exportFieldComment,
|
exportFieldComment,
|
||||||
|
formatMSSQLTableComment,
|
||||||
isFunction,
|
isFunction,
|
||||||
isKeyword,
|
isKeyword,
|
||||||
strHasQuotes,
|
strHasQuotes,
|
||||||
@@ -72,7 +73,13 @@ function parseMSSQLDefault(field: DBField): string {
|
|||||||
return `'${defaultValue}'`;
|
return `'${defaultValue}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exportMSSQL(diagram: Diagram): string {
|
export function exportMSSQL({
|
||||||
|
diagram,
|
||||||
|
onlyRelationships = false,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
onlyRelationships?: boolean;
|
||||||
|
}): string {
|
||||||
if (!diagram.tables || !diagram.relationships) {
|
if (!diagram.tables || !diagram.relationships) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -82,6 +89,8 @@ export function exportMSSQL(diagram: Diagram): string {
|
|||||||
|
|
||||||
// Create CREATE SCHEMA statements for all schemas
|
// Create CREATE SCHEMA statements for all schemas
|
||||||
let sqlScript = '';
|
let sqlScript = '';
|
||||||
|
|
||||||
|
if (!onlyRelationships) {
|
||||||
const schemas = new Set<string>();
|
const schemas = new Set<string>();
|
||||||
|
|
||||||
tables.forEach((table) => {
|
tables.forEach((table) => {
|
||||||
@@ -92,7 +101,7 @@ export function exportMSSQL(diagram: Diagram): string {
|
|||||||
|
|
||||||
// Add schema creation statements
|
// Add schema creation statements
|
||||||
schemas.forEach((schema) => {
|
schemas.forEach((schema) => {
|
||||||
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`;
|
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate table creation SQL
|
// Generate table creation SQL
|
||||||
@@ -108,7 +117,9 @@ export function exportMSSQL(diagram: Diagram): string {
|
|||||||
: `[${table.name}]`;
|
: `[${table.name}]`;
|
||||||
|
|
||||||
return `${
|
return `${
|
||||||
table.comments ? `/**\n${table.comments}\n*/\n` : ''
|
table.comments
|
||||||
|
? formatMSSQLTableComment(table.comments)
|
||||||
|
: ''
|
||||||
}CREATE TABLE ${tableName} (\n${table.fields
|
}CREATE TABLE ${tableName} (\n${table.fields
|
||||||
.map((field: DBField) => {
|
.map((field: DBField) => {
|
||||||
const fieldName = `[${field.name}]`;
|
const fieldName = `[${field.name}]`;
|
||||||
@@ -125,7 +136,8 @@ export function exportMSSQL(diagram: Diagram): string {
|
|||||||
) {
|
) {
|
||||||
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||||
}
|
}
|
||||||
} else if (field.precision && field.scale) {
|
}
|
||||||
|
if (field.precision && field.scale) {
|
||||||
if (
|
if (
|
||||||
typeName.toLowerCase() === 'decimal' ||
|
typeName.toLowerCase() === 'decimal' ||
|
||||||
typeName.toLowerCase() === 'numeric'
|
typeName.toLowerCase() === 'numeric'
|
||||||
@@ -170,7 +182,8 @@ export function exportMSSQL(diagram: Diagram): string {
|
|||||||
.map((f) => `[${f.name}]`)
|
.map((f) => `[${f.name}]`)
|
||||||
.join(', ')})`
|
.join(', ')})`
|
||||||
: ''
|
: ''
|
||||||
}\n);\n\n${table.indexes
|
}\n);\n${(() => {
|
||||||
|
const validIndexes = table.indexes
|
||||||
.map((index) => {
|
.map((index) => {
|
||||||
const indexName = table.schema
|
const indexName = table.schema
|
||||||
? `[${table.schema}_${index.name}]`
|
? `[${table.schema}_${index.name}]`
|
||||||
@@ -192,24 +205,38 @@ export function exportMSSQL(diagram: Diagram): string {
|
|||||||
);
|
);
|
||||||
indexFields.length = 32;
|
indexFields.length = 32;
|
||||||
return indexFields.length > 0
|
return indexFields.length > 0
|
||||||
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
|
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return indexFields.length > 0
|
return indexFields.length > 0
|
||||||
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
|
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
|
||||||
: '';
|
: '';
|
||||||
})
|
})
|
||||||
.join('')}`;
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return validIndexes.length > 0
|
||||||
|
? `\n-- Indexes\n${validIndexes.join('\n')}`
|
||||||
|
: '';
|
||||||
|
})()}\n`;
|
||||||
})
|
})
|
||||||
.filter(Boolean) // Remove empty strings (views)
|
.filter(Boolean) // Remove empty strings (views)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
// Generate foreign keys
|
// Generate foreign keys
|
||||||
sqlScript += `\n${relationships
|
if (relationships.length > 0) {
|
||||||
|
sqlScript += '\n-- Foreign key constraints\n';
|
||||||
|
|
||||||
|
// Process all relationships and create FK objects with schema info
|
||||||
|
const foreignKeys = relationships
|
||||||
.map((r: DBRelationship) => {
|
.map((r: DBRelationship) => {
|
||||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
const sourceTable = tables.find(
|
||||||
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
(t) => t.id === r.sourceTableId
|
||||||
|
);
|
||||||
|
const targetTable = tables.find(
|
||||||
|
(t) => t.id === r.targetTableId
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!sourceTable ||
|
!sourceTable ||
|
||||||
@@ -231,17 +258,85 @@ export function exportMSSQL(diagram: Diagram): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceTableName = sourceTable.schema
|
// Determine which table should have the foreign key based on cardinality
|
||||||
? `[${sourceTable.schema}].[${sourceTable.name}]`
|
let fkTable, fkField, refTable, refField;
|
||||||
: `[${sourceTable.name}]`;
|
|
||||||
const targetTableName = targetTable.schema
|
|
||||||
? `[${targetTable.schema}].[${targetTable.name}]`
|
|
||||||
: `[${targetTable.name}]`;
|
|
||||||
|
|
||||||
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`;
|
if (
|
||||||
|
r.sourceCardinality === 'one' &&
|
||||||
|
r.targetCardinality === 'many'
|
||||||
|
) {
|
||||||
|
// FK goes on target table
|
||||||
|
fkTable = targetTable;
|
||||||
|
fkField = targetField;
|
||||||
|
refTable = sourceTable;
|
||||||
|
refField = sourceField;
|
||||||
|
} else if (
|
||||||
|
r.sourceCardinality === 'many' &&
|
||||||
|
r.targetCardinality === 'one'
|
||||||
|
) {
|
||||||
|
// FK goes on source table
|
||||||
|
fkTable = sourceTable;
|
||||||
|
fkField = sourceField;
|
||||||
|
refTable = targetTable;
|
||||||
|
refField = targetField;
|
||||||
|
} else if (
|
||||||
|
r.sourceCardinality === 'one' &&
|
||||||
|
r.targetCardinality === 'one'
|
||||||
|
) {
|
||||||
|
// For 1:1, FK can go on either side, but typically goes on the table that references the other
|
||||||
|
// We'll keep the current behavior for 1:1
|
||||||
|
fkTable = sourceTable;
|
||||||
|
fkField = sourceField;
|
||||||
|
refTable = targetTable;
|
||||||
|
refField = targetField;
|
||||||
|
} else {
|
||||||
|
// Many-to-many relationships need a junction table, skip for now
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fkTableName = fkTable.schema
|
||||||
|
? `[${fkTable.schema}].[${fkTable.name}]`
|
||||||
|
: `[${fkTable.name}]`;
|
||||||
|
const refTableName = refTable.schema
|
||||||
|
? `[${refTable.schema}].[${refTable.name}]`
|
||||||
|
: `[${refTable.name}]`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema: fkTable.schema || 'dbo',
|
||||||
|
sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT [${r.name}] FOREIGN KEY([${fkField.name}]) REFERENCES ${refTableName}([${refField.name}]);`,
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean) // Remove empty strings
|
.filter(Boolean); // Remove empty objects
|
||||||
.join('\n')}`;
|
|
||||||
|
// Group foreign keys by schema
|
||||||
|
const fksBySchema = foreignKeys.reduce(
|
||||||
|
(acc, fk) => {
|
||||||
|
if (!fk) return acc;
|
||||||
|
const schema = fk.schema;
|
||||||
|
if (!acc[schema]) {
|
||||||
|
acc[schema] = [];
|
||||||
|
}
|
||||||
|
acc[schema].push(fk.sql);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort schemas and generate SQL with separators
|
||||||
|
const sortedSchemas = Object.keys(fksBySchema).sort();
|
||||||
|
const fkSql = sortedSchemas
|
||||||
|
.map((schema, index) => {
|
||||||
|
const schemaFks = fksBySchema[schema].join('\n');
|
||||||
|
if (index === 0) {
|
||||||
|
return `-- Schema: ${schema}\n${schemaFks}`;
|
||||||
|
} else {
|
||||||
|
return `\n-- Schema: ${schema}\n${schemaFks}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
sqlScript += fkSql;
|
||||||
|
}
|
||||||
|
|
||||||
return sqlScript;
|
return sqlScript;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
exportFieldComment,
|
exportFieldComment,
|
||||||
|
escapeSQLComment,
|
||||||
|
formatTableComment,
|
||||||
isFunction,
|
isFunction,
|
||||||
isKeyword,
|
isKeyword,
|
||||||
strHasQuotes,
|
strHasQuotes,
|
||||||
@@ -168,7 +170,13 @@ function mapMySQLType(typeName: string): string {
|
|||||||
return typeName;
|
return typeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exportMySQL(diagram: Diagram): string {
|
export function exportMySQL({
|
||||||
|
diagram,
|
||||||
|
onlyRelationships = false,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
onlyRelationships?: boolean;
|
||||||
|
}): string {
|
||||||
if (!diagram.tables || !diagram.relationships) {
|
if (!diagram.tables || !diagram.relationships) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -177,10 +185,11 @@ export function exportMySQL(diagram: Diagram): string {
|
|||||||
const relationships = diagram.relationships;
|
const relationships = diagram.relationships;
|
||||||
|
|
||||||
// Start SQL script
|
// Start SQL script
|
||||||
let sqlScript = '-- MySQL database export\n\n';
|
let sqlScript = '-- MySQL database export\n';
|
||||||
|
|
||||||
|
if (!onlyRelationships) {
|
||||||
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
|
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
|
||||||
sqlScript += 'START TRANSACTION;\n\n';
|
sqlScript += 'START TRANSACTION;\n';
|
||||||
|
|
||||||
// Create databases (schemas) if they don't exist
|
// Create databases (schemas) if they don't exist
|
||||||
const schemas = new Set<string>();
|
const schemas = new Set<string>();
|
||||||
@@ -212,11 +221,13 @@ export function exportMySQL(diagram: Diagram): string {
|
|||||||
: `\`${table.name}\``;
|
: `\`${table.name}\``;
|
||||||
|
|
||||||
// Get primary key fields
|
// Get primary key fields
|
||||||
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
const primaryKeyFields = table.fields.filter(
|
||||||
|
(f) => f.primaryKey
|
||||||
|
);
|
||||||
|
|
||||||
return `${
|
return `${
|
||||||
table.comments ? `-- ${table.comments}\n` : ''
|
table.comments ? formatTableComment(table.comments) : ''
|
||||||
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
}\nCREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
||||||
.map((field: DBField) => {
|
.map((field: DBField) => {
|
||||||
const fieldName = `\`${field.name}\``;
|
const fieldName = `\`${field.name}\``;
|
||||||
|
|
||||||
@@ -233,7 +244,8 @@ export function exportMySQL(diagram: Diagram): string {
|
|||||||
) {
|
) {
|
||||||
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||||
}
|
}
|
||||||
} else if (field.precision && field.scale) {
|
}
|
||||||
|
if (field.precision && field.scale) {
|
||||||
if (
|
if (
|
||||||
typeName.toLowerCase() === 'decimal' ||
|
typeName.toLowerCase() === 'decimal' ||
|
||||||
typeName.toLowerCase() === 'numeric'
|
typeName.toLowerCase() === 'numeric'
|
||||||
@@ -263,7 +275,9 @@ export function exportMySQL(diagram: Diagram): string {
|
|||||||
let autoIncrement = '';
|
let autoIncrement = '';
|
||||||
if (
|
if (
|
||||||
field.primaryKey &&
|
field.primaryKey &&
|
||||||
(field.default?.toLowerCase().includes('identity') ||
|
(field.default
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes('identity') ||
|
||||||
field.default
|
field.default
|
||||||
?.toLowerCase()
|
?.toLowerCase()
|
||||||
.includes('autoincrement') ||
|
.includes('autoincrement') ||
|
||||||
@@ -289,7 +303,7 @@ export function exportMySQL(diagram: Diagram): string {
|
|||||||
|
|
||||||
// MySQL supports inline comments
|
// MySQL supports inline comments
|
||||||
const comment = field.comments
|
const comment = field.comments
|
||||||
? ` COMMENT '${field.comments.replace(/'/g, "''")}'`
|
? ` COMMENT '${escapeSQLComment(field.comments)}'`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
|
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
|
||||||
@@ -304,11 +318,12 @@ export function exportMySQL(diagram: Diagram): string {
|
|||||||
}\n)${
|
}\n)${
|
||||||
// MySQL supports table comments
|
// MySQL supports table comments
|
||||||
table.comments
|
table.comments
|
||||||
? ` COMMENT='${table.comments.replace(/'/g, "''")}'`
|
? ` COMMENT='${escapeSQLComment(table.comments)}'`
|
||||||
: ''
|
: ''
|
||||||
};\n\n${
|
};\n${
|
||||||
// Add indexes - MySQL creates them separately from the table definition
|
// Add indexes - MySQL creates them separately from the table definition
|
||||||
table.indexes
|
(() => {
|
||||||
|
const validIndexes = table.indexes
|
||||||
.map((index) => {
|
.map((index) => {
|
||||||
// Get the list of fields for this index
|
// Get the list of fields for this index
|
||||||
const indexFields = index.fieldIds
|
const indexFields = index.fieldIds
|
||||||
@@ -322,10 +337,12 @@ export function exportMySQL(diagram: Diagram): string {
|
|||||||
|
|
||||||
// Skip if this index exactly matches the primary key fields
|
// Skip if this index exactly matches the primary key fields
|
||||||
if (
|
if (
|
||||||
primaryKeyFields.length === indexFields.length &&
|
primaryKeyFields.length ===
|
||||||
|
indexFields.length &&
|
||||||
primaryKeyFields.every((pk) =>
|
primaryKeyFields.every((pk) =>
|
||||||
indexFields.some(
|
indexFields.some(
|
||||||
(field) => field && field.id === pk.id
|
(field) =>
|
||||||
|
field && field.id === pk.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -336,25 +353,32 @@ export function exportMySQL(diagram: Diagram): string {
|
|||||||
const fieldNamesForIndex = indexFields
|
const fieldNamesForIndex = indexFields
|
||||||
.map((field) => field?.name || '')
|
.map((field) => field?.name || '')
|
||||||
.join('_');
|
.join('_');
|
||||||
const uniqueIndicator = index.unique ? '_unique' : '';
|
const uniqueIndicator = index.unique
|
||||||
|
? '_unique'
|
||||||
|
: '';
|
||||||
const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
|
const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
|
||||||
|
|
||||||
// Get the properly quoted field names
|
// Get the properly quoted field names
|
||||||
const indexFieldNames = indexFields
|
const indexFieldNames = indexFields
|
||||||
.map((field) => (field ? `\`${field.name}\`` : ''))
|
.map((field) =>
|
||||||
|
field ? `\`${field.name}\`` : ''
|
||||||
|
)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
// Check for text/blob fields that need special handling
|
// Check for text/blob fields that need special handling
|
||||||
const hasTextOrBlob = indexFields.some((field) => {
|
const hasTextOrBlob = indexFields.some(
|
||||||
|
(field) => {
|
||||||
const typeName =
|
const typeName =
|
||||||
field?.type.name.toLowerCase() || '';
|
field?.type.name.toLowerCase() ||
|
||||||
|
'';
|
||||||
return (
|
return (
|
||||||
typeName === 'text' ||
|
typeName === 'text' ||
|
||||||
typeName === 'mediumtext' ||
|
typeName === 'mediumtext' ||
|
||||||
typeName === 'longtext' ||
|
typeName === 'longtext' ||
|
||||||
typeName === 'blob'
|
typeName === 'blob'
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// If there are TEXT/BLOB fields, need to add prefix length
|
// If there are TEXT/BLOB fields, need to add prefix length
|
||||||
const indexFieldsWithPrefix = hasTextOrBlob
|
const indexFieldsWithPrefix = hasTextOrBlob
|
||||||
@@ -380,21 +404,26 @@ export function exportMySQL(diagram: Diagram): string {
|
|||||||
: indexFieldNames;
|
: indexFieldNames;
|
||||||
|
|
||||||
return indexFieldNames.length > 0
|
return indexFieldNames.length > 0
|
||||||
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldsWithPrefix.join(', ')});\n`
|
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldsWithPrefix.join(', ')});`
|
||||||
: '';
|
: '';
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean);
|
||||||
.join('\n')
|
|
||||||
}`;
|
return validIndexes.length > 0
|
||||||
|
? `\n-- Indexes\n${validIndexes.join('\n')}`
|
||||||
|
: '';
|
||||||
|
})()
|
||||||
|
}\n`;
|
||||||
})
|
})
|
||||||
.filter(Boolean) // Remove empty strings (views)
|
.filter(Boolean) // Remove empty strings (views)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
// Generate foreign keys
|
// Generate foreign keys
|
||||||
if (relationships.length > 0) {
|
if (relationships.length > 0) {
|
||||||
sqlScript += '\n-- Foreign key constraints\n\n';
|
sqlScript += '\n-- Foreign key constraints\n';
|
||||||
|
|
||||||
sqlScript += relationships
|
const foreignKeys = relationships
|
||||||
.map((r: DBRelationship) => {
|
.map((r: DBRelationship) => {
|
||||||
const sourceTable = tables.find(
|
const sourceTable = tables.find(
|
||||||
(t) => t.id === r.sourceTableId
|
(t) => t.id === r.sourceTableId
|
||||||
@@ -423,25 +452,62 @@ export function exportMySQL(diagram: Diagram): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceTableName = sourceTable.schema
|
// Determine which table should have the foreign key based on cardinality
|
||||||
? `\`${sourceTable.schema}\`.\`${sourceTable.name}\``
|
let fkTable, fkField, refTable, refField;
|
||||||
: `\`${sourceTable.name}\``;
|
|
||||||
const targetTableName = targetTable.schema
|
if (
|
||||||
? `\`${targetTable.schema}\`.\`${targetTable.name}\``
|
r.sourceCardinality === 'one' &&
|
||||||
: `\`${targetTable.name}\``;
|
r.targetCardinality === 'many'
|
||||||
|
) {
|
||||||
|
// FK goes on target table
|
||||||
|
fkTable = targetTable;
|
||||||
|
fkField = targetField;
|
||||||
|
refTable = sourceTable;
|
||||||
|
refField = sourceField;
|
||||||
|
} else if (
|
||||||
|
r.sourceCardinality === 'many' &&
|
||||||
|
r.targetCardinality === 'one'
|
||||||
|
) {
|
||||||
|
// FK goes on source table
|
||||||
|
fkTable = sourceTable;
|
||||||
|
fkField = sourceField;
|
||||||
|
refTable = targetTable;
|
||||||
|
refField = targetField;
|
||||||
|
} else if (
|
||||||
|
r.sourceCardinality === 'one' &&
|
||||||
|
r.targetCardinality === 'one'
|
||||||
|
) {
|
||||||
|
// For 1:1, FK can go on either side, but typically goes on the table that references the other
|
||||||
|
// We'll keep the current behavior for 1:1
|
||||||
|
fkTable = sourceTable;
|
||||||
|
fkField = sourceField;
|
||||||
|
refTable = targetTable;
|
||||||
|
refField = targetField;
|
||||||
|
} else {
|
||||||
|
// Many-to-many relationships need a junction table, skip for now
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fkTableName = fkTable.schema
|
||||||
|
? `\`${fkTable.schema}\`.\`${fkTable.name}\``
|
||||||
|
: `\`${fkTable.name}\``;
|
||||||
|
const refTableName = refTable.schema
|
||||||
|
? `\`${refTable.schema}\`.\`${refTable.name}\``
|
||||||
|
: `\`${refTable.name}\``;
|
||||||
|
|
||||||
// Create a descriptive constraint name
|
// Create a descriptive constraint name
|
||||||
const constraintName = `\`fk_${sourceTable.name}_${sourceField.name}\``;
|
const constraintName = `\`fk_${fkTable.name}_${fkField.name}\``;
|
||||||
|
|
||||||
// MySQL supports ON DELETE and ON UPDATE actions
|
// MySQL supports ON DELETE and ON UPDATE actions
|
||||||
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${sourceField.name}\`) REFERENCES ${targetTableName}(\`${targetField.name}\`)\nON UPDATE CASCADE ON DELETE RESTRICT;\n`;
|
return `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${fkField.name}\`) REFERENCES ${refTableName}(\`${refField.name}\`);`;
|
||||||
})
|
})
|
||||||
.filter(Boolean) // Remove empty strings
|
.filter(Boolean); // Remove empty strings
|
||||||
.join('\n');
|
|
||||||
|
sqlScript += foreignKeys.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit transaction
|
// Commit transaction
|
||||||
sqlScript += '\nCOMMIT;\n';
|
sqlScript += '\n\nCOMMIT;\n';
|
||||||
|
|
||||||
return sqlScript;
|
return sqlScript;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
exportFieldComment,
|
exportFieldComment,
|
||||||
|
escapeSQLComment,
|
||||||
|
formatTableComment,
|
||||||
isFunction,
|
isFunction,
|
||||||
isKeyword,
|
isKeyword,
|
||||||
strHasQuotes,
|
strHasQuotes,
|
||||||
@@ -8,6 +10,8 @@ import type { Diagram } from '@/lib/domain/diagram';
|
|||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import type { DBField } from '@/lib/domain/db-field';
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
function parsePostgresDefault(field: DBField): string {
|
function parsePostgresDefault(field: DBField): string {
|
||||||
if (!field.default || typeof field.default !== 'string') {
|
if (!field.default || typeof field.default !== 'string') {
|
||||||
@@ -90,16 +94,75 @@ function mapPostgresType(typeName: string, fieldName: string): string {
|
|||||||
return typeName;
|
return typeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exportPostgreSQL(diagram: Diagram): string {
|
function exportCustomTypes(customTypes: DBCustomType[]): string {
|
||||||
|
if (!customTypes || customTypes.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let typesSql = '';
|
||||||
|
|
||||||
|
// Sort custom types to ensure enums are created before composite types that might use them
|
||||||
|
const sortedTypes = [...customTypes].sort((a, b) => {
|
||||||
|
if (
|
||||||
|
a.kind === DBCustomTypeKind.enum &&
|
||||||
|
b.kind === DBCustomTypeKind.composite
|
||||||
|
) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
a.kind === DBCustomTypeKind.composite &&
|
||||||
|
b.kind === DBCustomTypeKind.enum
|
||||||
|
) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedTypes.forEach((customType) => {
|
||||||
|
const typeName = customType.schema
|
||||||
|
? `"${customType.schema}"."${customType.name}"`
|
||||||
|
: `"${customType.name}"`;
|
||||||
|
|
||||||
|
if (customType.kind === DBCustomTypeKind.enum) {
|
||||||
|
// Export enum type
|
||||||
|
if (customType.values && customType.values.length > 0) {
|
||||||
|
const enumValues = customType.values
|
||||||
|
.map((value) => `'${value.replace(/'/g, "''")}'`)
|
||||||
|
.join(', ');
|
||||||
|
typesSql += `CREATE TYPE ${typeName} AS ENUM (${enumValues});\n`;
|
||||||
|
}
|
||||||
|
} else if (customType.kind === DBCustomTypeKind.composite) {
|
||||||
|
// Export composite type
|
||||||
|
if (customType.fields && customType.fields.length > 0) {
|
||||||
|
const compositeFields = customType.fields
|
||||||
|
.map((field) => `"${field.field}" ${field.type}`)
|
||||||
|
.join(', ');
|
||||||
|
typesSql += `CREATE TYPE ${typeName} AS (${compositeFields});\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return typesSql ? typesSql + '\n' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportPostgreSQL({
|
||||||
|
diagram,
|
||||||
|
onlyRelationships = false,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
onlyRelationships?: boolean;
|
||||||
|
}): string {
|
||||||
if (!diagram.tables || !diagram.relationships) {
|
if (!diagram.tables || !diagram.relationships) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const tables = diagram.tables;
|
const tables = diagram.tables;
|
||||||
const relationships = diagram.relationships;
|
const relationships = diagram.relationships;
|
||||||
|
const customTypes = diagram.customTypes || [];
|
||||||
|
|
||||||
// Create CREATE SCHEMA statements for all schemas
|
// Create CREATE SCHEMA statements for all schemas
|
||||||
let sqlScript = '';
|
let sqlScript = '';
|
||||||
|
if (!onlyRelationships) {
|
||||||
const schemas = new Set<string>();
|
const schemas = new Set<string>();
|
||||||
|
|
||||||
tables.forEach((table) => {
|
tables.forEach((table) => {
|
||||||
@@ -108,11 +171,23 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also collect schemas from custom types
|
||||||
|
customTypes.forEach((customType) => {
|
||||||
|
if (customType.schema) {
|
||||||
|
schemas.add(customType.schema);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Add schema creation statements
|
// Add schema creation statements
|
||||||
schemas.forEach((schema) => {
|
schemas.forEach((schema) => {
|
||||||
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
|
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
|
||||||
});
|
});
|
||||||
|
if (schemas.size > 0) {
|
||||||
sqlScript += '\n';
|
sqlScript += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom types (enums and composite types)
|
||||||
|
sqlScript += exportCustomTypes(customTypes);
|
||||||
|
|
||||||
// Add sequence creation statements
|
// Add sequence creation statements
|
||||||
const sequences = new Set<string>();
|
const sequences = new Set<string>();
|
||||||
@@ -134,7 +209,9 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
sequences.forEach((sequence) => {
|
sequences.forEach((sequence) => {
|
||||||
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
|
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
|
||||||
});
|
});
|
||||||
|
if (sequences.size > 0) {
|
||||||
sqlScript += '\n';
|
sqlScript += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
// Generate table creation SQL
|
// Generate table creation SQL
|
||||||
sqlScript += tables
|
sqlScript += tables
|
||||||
@@ -149,10 +226,12 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
: `"${table.name}"`;
|
: `"${table.name}"`;
|
||||||
|
|
||||||
// Get primary key fields
|
// Get primary key fields
|
||||||
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
const primaryKeyFields = table.fields.filter(
|
||||||
|
(f) => f.primaryKey
|
||||||
|
);
|
||||||
|
|
||||||
return `${
|
return `${
|
||||||
table.comments ? `-- ${table.comments}\n` : ''
|
table.comments ? formatTableComment(table.comments) : ''
|
||||||
}CREATE TABLE ${tableName} (\n${table.fields
|
}CREATE TABLE ${tableName} (\n${table.fields
|
||||||
.map((field: DBField) => {
|
.map((field: DBField) => {
|
||||||
const fieldName = `"${field.name}"`;
|
const fieldName = `"${field.name}"`;
|
||||||
@@ -183,13 +262,15 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
if (field.characterMaximumLength) {
|
if (field.characterMaximumLength) {
|
||||||
if (
|
if (
|
||||||
typeName.toLowerCase() === 'varchar' ||
|
typeName.toLowerCase() === 'varchar' ||
|
||||||
typeName.toLowerCase() === 'character varying' ||
|
typeName.toLowerCase() ===
|
||||||
|
'character varying' ||
|
||||||
typeName.toLowerCase() === 'char' ||
|
typeName.toLowerCase() === 'char' ||
|
||||||
typeName.toLowerCase() === 'character'
|
typeName.toLowerCase() === 'character'
|
||||||
) {
|
) {
|
||||||
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||||
}
|
}
|
||||||
} else if (field.precision && field.scale) {
|
}
|
||||||
|
if (field.precision && field.scale) {
|
||||||
if (
|
if (
|
||||||
typeName.toLowerCase() === 'decimal' ||
|
typeName.toLowerCase() === 'decimal' ||
|
||||||
typeName.toLowerCase() === 'numeric'
|
typeName.toLowerCase() === 'numeric'
|
||||||
@@ -207,14 +288,18 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
|
|
||||||
// Handle array types (check if the type name ends with '[]')
|
// Handle array types (check if the type name ends with '[]')
|
||||||
if (typeName.endsWith('[]')) {
|
if (typeName.endsWith('[]')) {
|
||||||
typeWithSize = typeWithSize.replace('[]', '') + '[]';
|
typeWithSize =
|
||||||
|
typeWithSize.replace('[]', '') + '[]';
|
||||||
}
|
}
|
||||||
|
|
||||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||||
|
|
||||||
// Handle identity generation
|
// Handle identity generation
|
||||||
let identity = '';
|
let identity = '';
|
||||||
if (field.default && field.default.includes('nextval')) {
|
if (
|
||||||
|
field.default &&
|
||||||
|
field.default.includes('nextval')
|
||||||
|
) {
|
||||||
// PostgreSQL already handles this with DEFAULT nextval()
|
// PostgreSQL already handles this with DEFAULT nextval()
|
||||||
} else if (
|
} else if (
|
||||||
field.default &&
|
field.default &&
|
||||||
@@ -244,10 +329,10 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
.map((f) => `"${f.name}"`)
|
.map((f) => `"${f.name}"`)
|
||||||
.join(', ')})`
|
.join(', ')})`
|
||||||
: ''
|
: ''
|
||||||
}\n);\n\n${
|
}\n);${
|
||||||
// Add table comments
|
// Add table comments
|
||||||
table.comments
|
table.comments
|
||||||
? `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n\n`
|
? `\nCOMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';`
|
||||||
: ''
|
: ''
|
||||||
}${
|
}${
|
||||||
// Add column comments
|
// Add column comments
|
||||||
@@ -255,13 +340,14 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
.filter((f) => f.comments)
|
.filter((f) => f.comments)
|
||||||
.map(
|
.map(
|
||||||
(f) =>
|
(f) =>
|
||||||
`COMMENT ON COLUMN ${tableName}."${f.name}" IS '${f.comments?.replace(/'/g, "''")}';\n`
|
`\nCOMMENT ON COLUMN ${tableName}."${f.name}" IS '${escapeSQLComment(f.comments || '')}';`
|
||||||
)
|
)
|
||||||
.join('')
|
.join('')
|
||||||
}\n${
|
}${
|
||||||
// Add indexes only for non-primary key fields or composite indexes
|
// Add indexes only for non-primary key fields or composite indexes
|
||||||
// This avoids duplicate indexes on primary key columns
|
// This avoids duplicate indexes on primary key columns
|
||||||
table.indexes
|
(() => {
|
||||||
|
const validIndexes = table.indexes
|
||||||
.map((index) => {
|
.map((index) => {
|
||||||
// Get the list of fields for this index
|
// Get the list of fields for this index
|
||||||
const indexFields = index.fieldIds
|
const indexFields = index.fieldIds
|
||||||
@@ -276,10 +362,12 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
// Skip if this index exactly matches the primary key fields
|
// Skip if this index exactly matches the primary key fields
|
||||||
// This prevents creating redundant indexes
|
// This prevents creating redundant indexes
|
||||||
if (
|
if (
|
||||||
primaryKeyFields.length === indexFields.length &&
|
primaryKeyFields.length ===
|
||||||
|
indexFields.length &&
|
||||||
primaryKeyFields.every((pk) =>
|
primaryKeyFields.every((pk) =>
|
||||||
indexFields.some(
|
indexFields.some(
|
||||||
(field) => field && field.id === pk.id
|
(field) =>
|
||||||
|
field && field.id === pk.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -311,25 +399,40 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
|
|
||||||
// Get the properly quoted field names
|
// Get the properly quoted field names
|
||||||
const indexFieldNames = indexFields
|
const indexFieldNames = indexFields
|
||||||
.map((field) => (field ? `"${field.name}"` : ''))
|
.map((field) =>
|
||||||
|
field ? `"${field.name}"` : ''
|
||||||
|
)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
return indexFieldNames.length > 0
|
return indexFieldNames.length > 0
|
||||||
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldNames.join(', ')});\n\n`
|
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});`
|
||||||
: '';
|
: '';
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean);
|
||||||
.join('')
|
|
||||||
}`;
|
return validIndexes.length > 0
|
||||||
|
? `\n-- Indexes\n${validIndexes.join('\n')}`
|
||||||
|
: '';
|
||||||
|
})()
|
||||||
|
}\n`;
|
||||||
})
|
})
|
||||||
.filter(Boolean) // Remove empty strings (views)
|
.filter(Boolean) // Remove empty strings (views)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
// Generate foreign keys
|
// Generate foreign keys
|
||||||
sqlScript += `\n${relationships
|
if (relationships.length > 0) {
|
||||||
|
sqlScript += '\n-- Foreign key constraints\n';
|
||||||
|
|
||||||
|
// Process all relationships and create FK objects with schema info
|
||||||
|
const foreignKeys = relationships
|
||||||
.map((r: DBRelationship) => {
|
.map((r: DBRelationship) => {
|
||||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
const sourceTable = tables.find(
|
||||||
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
(t) => t.id === r.sourceTableId
|
||||||
|
);
|
||||||
|
const targetTable = tables.find(
|
||||||
|
(t) => t.id === r.targetTableId
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!sourceTable ||
|
!sourceTable ||
|
||||||
@@ -351,29 +454,99 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceTableName = sourceTable.schema
|
// Determine which table should have the foreign key based on cardinality
|
||||||
? `"${sourceTable.schema}"."${sourceTable.name}"`
|
let fkTable, fkField, refTable, refField;
|
||||||
: `"${sourceTable.name}"`;
|
|
||||||
const targetTableName = targetTable.schema
|
if (
|
||||||
? `"${targetTable.schema}"."${targetTable.name}"`
|
r.sourceCardinality === 'one' &&
|
||||||
: `"${targetTable.name}"`;
|
r.targetCardinality === 'many'
|
||||||
|
) {
|
||||||
|
// FK goes on target table
|
||||||
|
fkTable = targetTable;
|
||||||
|
fkField = targetField;
|
||||||
|
refTable = sourceTable;
|
||||||
|
refField = sourceField;
|
||||||
|
} else if (
|
||||||
|
r.sourceCardinality === 'many' &&
|
||||||
|
r.targetCardinality === 'one'
|
||||||
|
) {
|
||||||
|
// FK goes on source table
|
||||||
|
fkTable = sourceTable;
|
||||||
|
fkField = sourceField;
|
||||||
|
refTable = targetTable;
|
||||||
|
refField = targetField;
|
||||||
|
} else if (
|
||||||
|
r.sourceCardinality === 'one' &&
|
||||||
|
r.targetCardinality === 'one'
|
||||||
|
) {
|
||||||
|
// For 1:1, FK can go on either side, but typically goes on the table that references the other
|
||||||
|
// We'll keep the current behavior for 1:1
|
||||||
|
fkTable = sourceTable;
|
||||||
|
fkField = sourceField;
|
||||||
|
refTable = targetTable;
|
||||||
|
refField = targetField;
|
||||||
|
} else {
|
||||||
|
// Many-to-many relationships need a junction table, skip for now
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fkTableName = fkTable.schema
|
||||||
|
? `"${fkTable.schema}"."${fkTable.name}"`
|
||||||
|
: `"${fkTable.name}"`;
|
||||||
|
const refTableName = refTable.schema
|
||||||
|
? `"${refTable.schema}"."${refTable.name}"`
|
||||||
|
: `"${refTable.name}"`;
|
||||||
|
|
||||||
// Create a unique constraint name by combining table and field names
|
// Create a unique constraint name by combining table and field names
|
||||||
// Ensure it stays within PostgreSQL's 63-character limit for identifiers
|
// Ensure it stays within PostgreSQL's 63-character limit for identifiers
|
||||||
// and doesn't get truncated in a way that breaks SQL syntax
|
// and doesn't get truncated in a way that breaks SQL syntax
|
||||||
const baseName = `fk_${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`;
|
const baseName = `fk_${fkTable.name}_${fkField.name}_${refTable.name}_${refField.name}`;
|
||||||
// Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits
|
// Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits
|
||||||
const safeConstraintName =
|
const safeConstraintName =
|
||||||
baseName.length > 60
|
baseName.length > 60
|
||||||
? baseName.substring(0, 60).replace(/[^a-zA-Z0-9_]/g, '_')
|
? baseName
|
||||||
|
.substring(0, 60)
|
||||||
|
.replace(/[^a-zA-Z0-9_]/g, '_')
|
||||||
: baseName.replace(/[^a-zA-Z0-9_]/g, '_');
|
: baseName.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||||
|
|
||||||
const constraintName = `"${safeConstraintName}"`;
|
const constraintName = `"${safeConstraintName}"`;
|
||||||
|
|
||||||
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}");\n`;
|
return {
|
||||||
|
schema: fkTable.schema || 'public',
|
||||||
|
sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${constraintName} FOREIGN KEY("${fkField.name}") REFERENCES ${refTableName}("${refField.name}");`,
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean) // Remove empty strings
|
.filter(Boolean); // Remove empty objects
|
||||||
.join('\n')}`;
|
|
||||||
|
// Group foreign keys by schema
|
||||||
|
const fksBySchema = foreignKeys.reduce(
|
||||||
|
(acc, fk) => {
|
||||||
|
if (!fk) return acc;
|
||||||
|
const schema = fk.schema;
|
||||||
|
if (!acc[schema]) {
|
||||||
|
acc[schema] = [];
|
||||||
|
}
|
||||||
|
acc[schema].push(fk.sql);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort schemas and generate SQL with separators
|
||||||
|
const sortedSchemas = Object.keys(fksBySchema).sort();
|
||||||
|
const fkSql = sortedSchemas
|
||||||
|
.map((schema, index) => {
|
||||||
|
const schemaFks = fksBySchema[schema].join('\n');
|
||||||
|
if (index === 0) {
|
||||||
|
return `-- Schema: ${schema}\n${schemaFks}`;
|
||||||
|
} else {
|
||||||
|
return `\n-- Schema: ${schema}\n${schemaFks}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
sqlScript += fkSql;
|
||||||
|
}
|
||||||
|
|
||||||
return sqlScript;
|
return sqlScript;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
exportFieldComment,
|
exportFieldComment,
|
||||||
|
formatTableComment,
|
||||||
isFunction,
|
isFunction,
|
||||||
isKeyword,
|
isKeyword,
|
||||||
strHasQuotes,
|
strHasQuotes,
|
||||||
@@ -139,7 +140,13 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
|
|||||||
return typeName;
|
return typeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exportSQLite(diagram: Diagram): string {
|
export function exportSQLite({
|
||||||
|
diagram,
|
||||||
|
onlyRelationships = false,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
onlyRelationships?: boolean;
|
||||||
|
}): string {
|
||||||
if (!diagram.tables || !diagram.relationships) {
|
if (!diagram.tables || !diagram.relationships) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -148,10 +155,10 @@ export function exportSQLite(diagram: Diagram): string {
|
|||||||
const relationships = diagram.relationships;
|
const relationships = diagram.relationships;
|
||||||
|
|
||||||
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation
|
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation
|
||||||
let sqlScript = '-- SQLite database export\n\n';
|
let sqlScript = '-- SQLite database export\n';
|
||||||
|
|
||||||
// Begin transaction for faster import
|
// Begin transaction for faster import
|
||||||
sqlScript += 'BEGIN TRANSACTION;\n\n';
|
sqlScript += 'BEGIN TRANSACTION;\n';
|
||||||
|
|
||||||
// SQLite doesn't have sequences, so we skip sequence creation
|
// SQLite doesn't have sequences, so we skip sequence creation
|
||||||
|
|
||||||
@@ -165,6 +172,7 @@ export function exportSQLite(diagram: Diagram): string {
|
|||||||
'sqlite_master',
|
'sqlite_master',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!onlyRelationships) {
|
||||||
// Generate table creation SQL
|
// Generate table creation SQL
|
||||||
sqlScript += tables
|
sqlScript += tables
|
||||||
.map((table: DBTable) => {
|
.map((table: DBTable) => {
|
||||||
@@ -186,16 +194,19 @@ export function exportSQLite(diagram: Diagram): string {
|
|||||||
const tableName = `"${table.name}"`;
|
const tableName = `"${table.name}"`;
|
||||||
|
|
||||||
// Get primary key fields
|
// Get primary key fields
|
||||||
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
const primaryKeyFields = table.fields.filter(
|
||||||
|
(f) => f.primaryKey
|
||||||
|
);
|
||||||
|
|
||||||
// Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
|
// Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
|
||||||
const singleIntegerPrimaryKey =
|
const singleIntegerPrimaryKey =
|
||||||
primaryKeyFields.length === 1 &&
|
primaryKeyFields.length === 1 &&
|
||||||
(primaryKeyFields[0].type.name.toLowerCase() === 'integer' ||
|
(primaryKeyFields[0].type.name.toLowerCase() ===
|
||||||
|
'integer' ||
|
||||||
primaryKeyFields[0].type.name.toLowerCase() === 'int');
|
primaryKeyFields[0].type.name.toLowerCase() === 'int');
|
||||||
|
|
||||||
return `${schemaComment}${
|
return `${schemaComment}${
|
||||||
table.comments ? `-- ${table.comments}\n` : ''
|
table.comments ? formatTableComment(table.comments) : ''
|
||||||
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
||||||
.map((field: DBField) => {
|
.map((field: DBField) => {
|
||||||
const fieldName = `"${field.name}"`;
|
const fieldName = `"${field.name}"`;
|
||||||
@@ -217,7 +228,9 @@ export function exportSQLite(diagram: Diagram): string {
|
|||||||
if (
|
if (
|
||||||
field.primaryKey &&
|
field.primaryKey &&
|
||||||
singleIntegerPrimaryKey &&
|
singleIntegerPrimaryKey &&
|
||||||
(field.default?.toLowerCase().includes('identity') ||
|
(field.default
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes('identity') ||
|
||||||
field.default
|
field.default
|
||||||
?.toLowerCase()
|
?.toLowerCase()
|
||||||
.includes('autoincrement') ||
|
.includes('autoincrement') ||
|
||||||
@@ -263,9 +276,10 @@ export function exportSQLite(diagram: Diagram): string {
|
|||||||
.map((f) => `"${f.name}"`)
|
.map((f) => `"${f.name}"`)
|
||||||
.join(', ')})`
|
.join(', ')})`
|
||||||
: ''
|
: ''
|
||||||
}\n);\n\n${
|
}\n);\n${
|
||||||
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
|
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
|
||||||
table.indexes
|
(() => {
|
||||||
|
const validIndexes = table.indexes
|
||||||
.map((index) => {
|
.map((index) => {
|
||||||
// Skip indexes that exactly match the primary key
|
// Skip indexes that exactly match the primary key
|
||||||
const indexFields = index.fieldIds
|
const indexFields = index.fieldIds
|
||||||
@@ -279,15 +293,19 @@ export function exportSQLite(diagram: Diagram): string {
|
|||||||
|
|
||||||
// Get the properly quoted field names
|
// Get the properly quoted field names
|
||||||
const indexFieldNames = indexFields
|
const indexFieldNames = indexFields
|
||||||
.map((field) => (field ? `"${field.name}"` : ''))
|
.map((field) =>
|
||||||
|
field ? `"${field.name}"` : ''
|
||||||
|
)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
// Skip if this index exactly matches the primary key fields
|
// Skip if this index exactly matches the primary key fields
|
||||||
if (
|
if (
|
||||||
primaryKeyFields.length === indexFields.length &&
|
primaryKeyFields.length ===
|
||||||
|
indexFields.length &&
|
||||||
primaryKeyFields.every((pk) =>
|
primaryKeyFields.every((pk) =>
|
||||||
indexFields.some(
|
indexFields.some(
|
||||||
(field) => field && field.id === pk.id
|
(field) =>
|
||||||
|
field && field.id === pk.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -295,21 +313,26 @@ export function exportSQLite(diagram: Diagram): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create safe index name
|
// Create safe index name
|
||||||
const safeIndexName = `${table.name}_${index.name}`
|
const safeIndexName =
|
||||||
|
`${table.name}_${index.name}`
|
||||||
.replace(/[^a-zA-Z0-9_]/g, '_')
|
.replace(/[^a-zA-Z0-9_]/g, '_')
|
||||||
.substring(0, 60);
|
.substring(0, 60);
|
||||||
|
|
||||||
return indexFieldNames.length > 0
|
return indexFieldNames.length > 0
|
||||||
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});\n`
|
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});`
|
||||||
: '';
|
: '';
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean);
|
||||||
.join('\n')
|
|
||||||
}`;
|
return validIndexes.length > 0
|
||||||
|
? `\n-- Indexes\n${validIndexes.join('\n')}`
|
||||||
|
: '';
|
||||||
|
})()
|
||||||
|
}\n`;
|
||||||
})
|
})
|
||||||
.filter(Boolean) // Remove empty strings (views)
|
.filter(Boolean) // Remove empty strings (views)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
}
|
||||||
// Generate table constraints and triggers for foreign keys
|
// Generate table constraints and triggers for foreign keys
|
||||||
// SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements
|
// SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements
|
||||||
// But we'll also provide individual ALTER TABLE statements as comments for reference
|
// But we'll also provide individual ALTER TABLE statements as comments for reference
|
||||||
@@ -318,7 +341,7 @@ export function exportSQLite(diagram: Diagram): string {
|
|||||||
sqlScript += '\n-- Foreign key constraints\n';
|
sqlScript += '\n-- Foreign key constraints\n';
|
||||||
sqlScript +=
|
sqlScript +=
|
||||||
'-- Note: SQLite requires foreign_keys pragma to be enabled:\n';
|
'-- Note: SQLite requires foreign_keys pragma to be enabled:\n';
|
||||||
sqlScript += '-- PRAGMA foreign_keys = ON;\n\n';
|
sqlScript += '-- PRAGMA foreign_keys = ON;\n';
|
||||||
|
|
||||||
relationships.forEach((r: DBRelationship) => {
|
relationships.forEach((r: DBRelationship) => {
|
||||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||||
@@ -346,8 +369,44 @@ export function exportSQLite(diagram: Diagram): string {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine which table should have the foreign key based on cardinality
|
||||||
|
let fkTable, fkField, refTable, refField;
|
||||||
|
|
||||||
|
if (
|
||||||
|
r.sourceCardinality === 'one' &&
|
||||||
|
r.targetCardinality === 'many'
|
||||||
|
) {
|
||||||
|
// FK goes on target table
|
||||||
|
fkTable = targetTable;
|
||||||
|
fkField = targetField;
|
||||||
|
refTable = sourceTable;
|
||||||
|
refField = sourceField;
|
||||||
|
} else if (
|
||||||
|
r.sourceCardinality === 'many' &&
|
||||||
|
r.targetCardinality === 'one'
|
||||||
|
) {
|
||||||
|
// FK goes on source table
|
||||||
|
fkTable = sourceTable;
|
||||||
|
fkField = sourceField;
|
||||||
|
refTable = targetTable;
|
||||||
|
refField = targetField;
|
||||||
|
} else if (
|
||||||
|
r.sourceCardinality === 'one' &&
|
||||||
|
r.targetCardinality === 'one'
|
||||||
|
) {
|
||||||
|
// For 1:1, FK can go on either side, but typically goes on the table that references the other
|
||||||
|
// We'll keep the current behavior for 1:1
|
||||||
|
fkTable = sourceTable;
|
||||||
|
fkField = sourceField;
|
||||||
|
refTable = targetTable;
|
||||||
|
refField = targetField;
|
||||||
|
} else {
|
||||||
|
// Many-to-many relationships need a junction table, skip for now
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create commented out version of what would be ALTER TABLE statement
|
// Create commented out version of what would be ALTER TABLE statement
|
||||||
sqlScript += `-- ALTER TABLE "${sourceTable.name}" ADD CONSTRAINT "fk_${sourceTable.name}_${sourceField.name}" FOREIGN KEY("${sourceField.name}") REFERENCES "${targetTable.name}"("${targetField.name}");\n`;
|
sqlScript += `-- ALTER TABLE "${fkTable.name}" ADD CONSTRAINT "fk_${fkTable.name}_${fkField.name}" FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}");\n`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,23 +11,7 @@ import { exportMySQL } from './export-per-type/mysql';
|
|||||||
|
|
||||||
// Function to simplify verbose data type names
|
// Function to simplify verbose data type names
|
||||||
const simplifyDataType = (typeName: string): string => {
|
const simplifyDataType = (typeName: string): string => {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {};
|
||||||
'character varying': 'varchar',
|
|
||||||
'char varying': 'varchar',
|
|
||||||
integer: 'int',
|
|
||||||
int4: 'int',
|
|
||||||
int8: 'bigint',
|
|
||||||
serial4: 'serial',
|
|
||||||
serial8: 'bigserial',
|
|
||||||
float8: 'double precision',
|
|
||||||
float4: 'real',
|
|
||||||
bool: 'boolean',
|
|
||||||
character: 'char',
|
|
||||||
'timestamp without time zone': 'timestamp',
|
|
||||||
'timestamp with time zone': 'timestamptz',
|
|
||||||
'time without time zone': 'time',
|
|
||||||
'time with time zone': 'timetz',
|
|
||||||
};
|
|
||||||
|
|
||||||
return typeMap[typeName.toLowerCase()] || typeName;
|
return typeMap[typeName.toLowerCase()] || typeName;
|
||||||
};
|
};
|
||||||
@@ -36,10 +20,12 @@ export const exportBaseSQL = ({
|
|||||||
diagram,
|
diagram,
|
||||||
targetDatabaseType,
|
targetDatabaseType,
|
||||||
isDBMLFlow = false,
|
isDBMLFlow = false,
|
||||||
|
onlyRelationships = false,
|
||||||
}: {
|
}: {
|
||||||
diagram: Diagram;
|
diagram: Diagram;
|
||||||
targetDatabaseType: DatabaseType;
|
targetDatabaseType: DatabaseType;
|
||||||
isDBMLFlow?: boolean;
|
isDBMLFlow?: boolean;
|
||||||
|
onlyRelationships?: boolean;
|
||||||
}): string => {
|
}): string => {
|
||||||
const { tables, relationships } = diagram;
|
const { tables, relationships } = diagram;
|
||||||
|
|
||||||
@@ -50,16 +36,16 @@ export const exportBaseSQL = ({
|
|||||||
if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
|
if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
|
||||||
switch (diagram.databaseType) {
|
switch (diagram.databaseType) {
|
||||||
case DatabaseType.SQL_SERVER:
|
case DatabaseType.SQL_SERVER:
|
||||||
return exportMSSQL(diagram);
|
return exportMSSQL({ diagram, onlyRelationships });
|
||||||
case DatabaseType.POSTGRESQL:
|
case DatabaseType.POSTGRESQL:
|
||||||
return exportPostgreSQL(diagram);
|
return exportPostgreSQL({ diagram, onlyRelationships });
|
||||||
case DatabaseType.SQLITE:
|
case DatabaseType.SQLITE:
|
||||||
return exportSQLite(diagram);
|
return exportSQLite({ diagram, onlyRelationships });
|
||||||
case DatabaseType.MYSQL:
|
case DatabaseType.MYSQL:
|
||||||
case DatabaseType.MARIADB:
|
case DatabaseType.MARIADB:
|
||||||
return exportMySQL(diagram);
|
return exportMySQL({ diagram, onlyRelationships });
|
||||||
default:
|
default:
|
||||||
return exportPostgreSQL(diagram);
|
return exportPostgreSQL({ diagram, onlyRelationships });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +70,71 @@ export const exportBaseSQL = ({
|
|||||||
schemas.forEach((schema) => {
|
schemas.forEach((schema) => {
|
||||||
sqlScript += `CREATE SCHEMA IF NOT EXISTS ${schema};\n`;
|
sqlScript += `CREATE SCHEMA IF NOT EXISTS ${schema};\n`;
|
||||||
});
|
});
|
||||||
|
if (schemas.size > 0) sqlScript += '\n'; // Add newline only if schemas were added
|
||||||
|
|
||||||
|
// Add CREATE TYPE statements for ENUMs and COMPOSITE types from diagram.customTypes
|
||||||
|
if (diagram.customTypes && diagram.customTypes.length > 0) {
|
||||||
|
diagram.customTypes.forEach((customType) => {
|
||||||
|
const typeNameWithSchema = customType.schema
|
||||||
|
? `${customType.schema}.${customType.name}`
|
||||||
|
: customType.name;
|
||||||
|
|
||||||
|
if (
|
||||||
|
customType.kind === 'enum' &&
|
||||||
|
customType.values &&
|
||||||
|
customType.values.length > 0
|
||||||
|
) {
|
||||||
|
// For PostgreSQL, generate CREATE TYPE ... AS ENUM
|
||||||
|
// For other DBs, this might need adjustment or be omitted if not supported directly
|
||||||
|
// or if we rely on the DBML generator to create Enums separately (as currently done)
|
||||||
|
// For now, let's assume PostgreSQL-style for demonstration if isDBMLFlow is false.
|
||||||
|
// If isDBMLFlow is true, we let TableDBML.tsx handle Enum syntax directly.
|
||||||
|
if (
|
||||||
|
targetDatabaseType === DatabaseType.POSTGRESQL &&
|
||||||
|
!isDBMLFlow
|
||||||
|
) {
|
||||||
|
const enumValues = customType.values
|
||||||
|
.map((v) => `'${v.replace(/'/g, "''")}'`)
|
||||||
|
.join(', ');
|
||||||
|
sqlScript += `CREATE TYPE ${typeNameWithSchema} AS ENUM (${enumValues});\n`;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
customType.kind === 'composite' &&
|
||||||
|
customType.fields &&
|
||||||
|
customType.fields.length > 0
|
||||||
|
) {
|
||||||
|
// For PostgreSQL, generate CREATE TYPE ... AS (...)
|
||||||
|
// This is crucial for composite types to be recognized by the DBML importer
|
||||||
|
if (
|
||||||
|
targetDatabaseType === DatabaseType.POSTGRESQL ||
|
||||||
|
isDBMLFlow
|
||||||
|
) {
|
||||||
|
// Assume other DBs might not support this or DBML flow needs it
|
||||||
|
const compositeFields = customType.fields
|
||||||
|
.map((f) => `${f.field} ${simplifyDataType(f.type)}`)
|
||||||
|
.join(',\n ');
|
||||||
|
sqlScript += `CREATE TYPE ${typeNameWithSchema} AS (\n ${compositeFields}\n);\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
diagram.customTypes.some(
|
||||||
|
(ct) =>
|
||||||
|
(ct.kind === 'enum' &&
|
||||||
|
ct.values &&
|
||||||
|
ct.values.length > 0 &&
|
||||||
|
targetDatabaseType === DatabaseType.POSTGRESQL &&
|
||||||
|
!isDBMLFlow) ||
|
||||||
|
(ct.kind === 'composite' &&
|
||||||
|
ct.fields &&
|
||||||
|
ct.fields.length > 0 &&
|
||||||
|
(targetDatabaseType === DatabaseType.POSTGRESQL ||
|
||||||
|
isDBMLFlow))
|
||||||
|
)
|
||||||
|
) {
|
||||||
sqlScript += '\n';
|
sqlScript += '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add CREATE SEQUENCE statements
|
// Add CREATE SEQUENCE statements
|
||||||
const sequences = new Set<string>();
|
const sequences = new Set<string>();
|
||||||
@@ -106,7 +156,9 @@ export const exportBaseSQL = ({
|
|||||||
sequences.forEach((sequence) => {
|
sequences.forEach((sequence) => {
|
||||||
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
|
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
|
||||||
});
|
});
|
||||||
|
if (sequences.size > 0) {
|
||||||
sqlScript += '\n';
|
sqlScript += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
// Loop through each non-view table to generate the SQL statements
|
// Loop through each non-view table to generate the SQL statements
|
||||||
nonViewTables.forEach((table) => {
|
nonViewTables.forEach((table) => {
|
||||||
@@ -115,12 +167,55 @@ export const exportBaseSQL = ({
|
|||||||
: table.name;
|
: table.name;
|
||||||
sqlScript += `CREATE TABLE ${tableName} (\n`;
|
sqlScript += `CREATE TABLE ${tableName} (\n`;
|
||||||
|
|
||||||
|
// Check for composite primary keys
|
||||||
|
const primaryKeyFields = table.fields.filter(
|
||||||
|
(field) => field.primaryKey
|
||||||
|
);
|
||||||
|
const hasCompositePrimaryKey = primaryKeyFields.length > 1;
|
||||||
|
|
||||||
table.fields.forEach((field, index) => {
|
table.fields.forEach((field, index) => {
|
||||||
let typeName = simplifyDataType(field.type.name);
|
let typeName = simplifyDataType(field.type.name);
|
||||||
|
|
||||||
// Handle ENUM type
|
// Handle ENUM type
|
||||||
if (typeName.toLowerCase() === 'enum') {
|
// If we are generating SQL for DBML flow, and we ALREADY generated CREATE TYPE for enums (e.g., for PG),
|
||||||
// Map enum to TEXT for broader compatibility, especially with DBML importer
|
// then we should use the enum type name. Otherwise, map to text.
|
||||||
|
// However, the current TableDBML.tsx generates its own Enum blocks, so for DBML flow,
|
||||||
|
// converting to TEXT here might still be the safest bet to avoid conflicts if SQL enums aren't perfectly parsed.
|
||||||
|
// Let's adjust: if it's a known custom enum type, use its name for PG, otherwise TEXT.
|
||||||
|
const customEnumType = diagram.customTypes?.find(
|
||||||
|
(ct) =>
|
||||||
|
ct.name === field.type.name &&
|
||||||
|
ct.kind === 'enum' &&
|
||||||
|
(ct.schema ? ct.schema === table.schema : true)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
customEnumType &&
|
||||||
|
targetDatabaseType === DatabaseType.POSTGRESQL &&
|
||||||
|
!isDBMLFlow
|
||||||
|
) {
|
||||||
|
typeName = customEnumType.schema
|
||||||
|
? `${customEnumType.schema}.${customEnumType.name}`
|
||||||
|
: customEnumType.name;
|
||||||
|
} else if (typeName.toLowerCase() === 'enum') {
|
||||||
|
// Fallback for non-PG or if custom type not found, or for DBML flow if not handled by CREATE TYPE above
|
||||||
|
typeName = 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the field type is a known composite custom type
|
||||||
|
const customCompositeType = diagram.customTypes?.find(
|
||||||
|
(ct) =>
|
||||||
|
ct.name === field.type.name &&
|
||||||
|
ct.kind === 'composite' &&
|
||||||
|
(ct.schema ? ct.schema === table.schema : true)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (customCompositeType) {
|
||||||
|
typeName = customCompositeType.schema
|
||||||
|
? `${customCompositeType.schema}.${customCompositeType.name}`
|
||||||
|
: customCompositeType.name;
|
||||||
|
} else if (typeName.toLowerCase() === 'user-defined') {
|
||||||
|
// If it's 'user-defined' but not a known composite, fallback to TEXT
|
||||||
typeName = 'text';
|
typeName = 'text';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,19 +224,33 @@ export const exportBaseSQL = ({
|
|||||||
typeName = 'text[]';
|
typeName = 'text[]';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temp fix for 'user-defined' to be text
|
// Handle special types
|
||||||
if (typeName.toLowerCase() === 'user-defined') {
|
if (
|
||||||
typeName = 'text';
|
typeName.toLowerCase() === 'char' &&
|
||||||
|
!field.characterMaximumLength
|
||||||
|
) {
|
||||||
|
// Default char without length to char(1)
|
||||||
|
typeName = 'char';
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlScript += ` ${field.name} ${typeName}`;
|
sqlScript += ` ${field.name} ${typeName}`;
|
||||||
|
|
||||||
// Add size for character types
|
// Add size for character types
|
||||||
if (field.characterMaximumLength) {
|
if (
|
||||||
|
field.characterMaximumLength &&
|
||||||
|
parseInt(field.characterMaximumLength) > 0 &&
|
||||||
|
field.type.name.toLowerCase() !== 'decimal'
|
||||||
|
) {
|
||||||
sqlScript += `(${field.characterMaximumLength})`;
|
sqlScript += `(${field.characterMaximumLength})`;
|
||||||
} else if (field.type.name.toLowerCase().includes('varchar')) {
|
} else if (field.type.name.toLowerCase().includes('varchar')) {
|
||||||
// Keep varchar sizing, but don't apply to TEXT (previously enum)
|
// Keep varchar sizing, but don't apply to TEXT (previously enum)
|
||||||
sqlScript += `(500)`;
|
sqlScript += `(500)`;
|
||||||
|
} else if (
|
||||||
|
typeName.toLowerCase() === 'char' &&
|
||||||
|
!field.characterMaximumLength
|
||||||
|
) {
|
||||||
|
// Default char without explicit length to char(1) for compatibility
|
||||||
|
sqlScript += `(1)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add precision and scale for numeric types
|
// Add precision and scale for numeric types
|
||||||
@@ -166,6 +275,13 @@ export const exportBaseSQL = ({
|
|||||||
// Temp remove default user-define value when it have it
|
// Temp remove default user-define value when it have it
|
||||||
let fieldDefault = field.default;
|
let fieldDefault = field.default;
|
||||||
|
|
||||||
|
// Skip invalid default values for DBML export
|
||||||
|
if (
|
||||||
|
fieldDefault === 'has default' ||
|
||||||
|
fieldDefault === 'DEFAULT has default'
|
||||||
|
) {
|
||||||
|
// Skip this default value as it's invalid SQL
|
||||||
|
} else {
|
||||||
// Remove the type cast part after :: if it exists
|
// Remove the type cast part after :: if it exists
|
||||||
if (fieldDefault.includes('::')) {
|
if (fieldDefault.includes('::')) {
|
||||||
const endedWithParentheses = fieldDefault.endsWith(')');
|
const endedWithParentheses = fieldDefault.endsWith(')');
|
||||||
@@ -186,29 +302,36 @@ export const exportBaseSQL = ({
|
|||||||
|
|
||||||
sqlScript += ` DEFAULT ${fieldDefault}`;
|
sqlScript += ` DEFAULT ${fieldDefault}`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle PRIMARY KEY constraint
|
// Handle PRIMARY KEY constraint - only add inline if not composite
|
||||||
if (field.primaryKey) {
|
if (field.primaryKey && !hasCompositePrimaryKey) {
|
||||||
sqlScript += ' PRIMARY KEY';
|
sqlScript += ' PRIMARY KEY';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a comma after each field except the last one
|
// Add a comma after each field except the last one (or before composite primary key)
|
||||||
if (index < table.fields.length - 1) {
|
if (index < table.fields.length - 1 || hasCompositePrimaryKey) {
|
||||||
sqlScript += ',\n';
|
sqlScript += ',\n';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
sqlScript += '\n);\n\n';
|
// Add composite primary key constraint if needed
|
||||||
|
if (hasCompositePrimaryKey) {
|
||||||
|
const pkFieldNames = primaryKeyFields.map((f) => f.name).join(', ');
|
||||||
|
sqlScript += `\n PRIMARY KEY (${pkFieldNames})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlScript += '\n);\n';
|
||||||
|
|
||||||
// Add table comment
|
// Add table comment
|
||||||
if (table.comments) {
|
if (table.comments) {
|
||||||
sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments}';\n`;
|
sqlScript += `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.fields.forEach((field) => {
|
table.fields.forEach((field) => {
|
||||||
// Add column comment
|
// Add column comment
|
||||||
if (field.comments) {
|
if (field.comments) {
|
||||||
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments}';\n`;
|
sqlScript += `COMMENT ON COLUMN ${tableName}.${field.name} IS '${field.comments.replace(/'/g, "''")}';\n`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,16 +346,19 @@ export const exportBaseSQL = ({
|
|||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
if (fieldNames) {
|
if (fieldNames) {
|
||||||
const indexName = table.schema
|
const indexName =
|
||||||
|
table.schema && !isDBMLFlow
|
||||||
? `${table.schema}_${index.name}`
|
? `${table.schema}_${index.name}`
|
||||||
: index.name;
|
: index.name;
|
||||||
sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
|
sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
sqlScript += '\n';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (nonViewTables.length > 0 && (relationships?.length ?? 0) > 0) {
|
||||||
|
sqlScript += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
// Handle relationships (foreign keys)
|
// Handle relationships (foreign keys)
|
||||||
relationships?.forEach((relationship) => {
|
relationships?.forEach((relationship) => {
|
||||||
const sourceTable = nonViewTables.find(
|
const sourceTable = nonViewTables.find(
|
||||||
@@ -255,13 +381,52 @@ export const exportBaseSQL = ({
|
|||||||
sourceTableField &&
|
sourceTableField &&
|
||||||
targetTableField
|
targetTableField
|
||||||
) {
|
) {
|
||||||
const sourceTableName = sourceTable.schema
|
// Determine which table should have the foreign key based on cardinality
|
||||||
? `${sourceTable.schema}.${sourceTable.name}`
|
// In a 1:many relationship, the foreign key goes on the "many" side
|
||||||
: sourceTable.name;
|
// If source is "one" and target is "many", FK goes on target table
|
||||||
const targetTableName = targetTable.schema
|
// If source is "many" and target is "one", FK goes on source table
|
||||||
? `${targetTable.schema}.${targetTable.name}`
|
let fkTable, fkField, refTable, refField;
|
||||||
: targetTable.name;
|
|
||||||
sqlScript += `ALTER TABLE ${sourceTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${sourceTableField.name}) REFERENCES ${targetTableName} (${targetTableField.name});\n`;
|
if (
|
||||||
|
relationship.sourceCardinality === 'one' &&
|
||||||
|
relationship.targetCardinality === 'many'
|
||||||
|
) {
|
||||||
|
// FK goes on target table
|
||||||
|
fkTable = targetTable;
|
||||||
|
fkField = targetTableField;
|
||||||
|
refTable = sourceTable;
|
||||||
|
refField = sourceTableField;
|
||||||
|
} else if (
|
||||||
|
relationship.sourceCardinality === 'many' &&
|
||||||
|
relationship.targetCardinality === 'one'
|
||||||
|
) {
|
||||||
|
// FK goes on source table
|
||||||
|
fkTable = sourceTable;
|
||||||
|
fkField = sourceTableField;
|
||||||
|
refTable = targetTable;
|
||||||
|
refField = targetTableField;
|
||||||
|
} else if (
|
||||||
|
relationship.sourceCardinality === 'one' &&
|
||||||
|
relationship.targetCardinality === 'one'
|
||||||
|
) {
|
||||||
|
// For 1:1, FK can go on either side, but typically goes on the table that references the other
|
||||||
|
// We'll keep the current behavior for 1:1
|
||||||
|
fkTable = sourceTable;
|
||||||
|
fkField = sourceTableField;
|
||||||
|
refTable = targetTable;
|
||||||
|
refField = targetTableField;
|
||||||
|
} else {
|
||||||
|
// Many-to-many relationships need a junction table, skip for now
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fkTableName = fkTable.schema
|
||||||
|
? `${fkTable.schema}.${fkTable.name}`
|
||||||
|
: fkTable.name;
|
||||||
|
const refTableName = refTable.schema
|
||||||
|
? `${refTable.schema}.${refTable.name}`
|
||||||
|
: refTable.name;
|
||||||
|
sqlScript += `ALTER TABLE ${fkTableName} ADD CONSTRAINT ${relationship.name} FOREIGN KEY (${fkField.name}) REFERENCES ${refTableName} (${refField.name});\n`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -522,6 +687,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
|
|||||||
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
||||||
- **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
- **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
||||||
`,
|
`,
|
||||||
|
oracle: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const dialectInstruction = dialectInstructionMap[databaseType] ?? '';
|
const dialectInstruction = dialectInstructionMap[databaseType] ?? '';
|
||||||
|
|||||||
126
src/lib/data/import-metadata/filter-metadata.ts
Normal file
126
src/lib/data/import-metadata/filter-metadata.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import type { DatabaseMetadata } from './metadata-types/database-metadata';
|
||||||
|
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
|
||||||
|
|
||||||
|
export interface SelectedTable {
|
||||||
|
schema?: string | null;
|
||||||
|
table: string;
|
||||||
|
type: 'table' | 'view';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterMetadataByTables({
|
||||||
|
metadata,
|
||||||
|
selectedTables: inputSelectedTables,
|
||||||
|
}: {
|
||||||
|
metadata: DatabaseMetadata;
|
||||||
|
selectedTables: SelectedTable[];
|
||||||
|
}): DatabaseMetadata {
|
||||||
|
const selectedTables = inputSelectedTables.map((st) => {
|
||||||
|
// Normalize schema names to ensure consistent filtering
|
||||||
|
const schema = schemaNameToDomainSchemaName(st.schema) ?? '';
|
||||||
|
return {
|
||||||
|
...st,
|
||||||
|
schema,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create sets for faster lookup
|
||||||
|
const selectedTableSet = new Set(
|
||||||
|
selectedTables
|
||||||
|
.filter((st) => st.type === 'table')
|
||||||
|
.map((st) => `${st.schema}.${st.table}`)
|
||||||
|
);
|
||||||
|
const selectedViewSet = new Set(
|
||||||
|
selectedTables
|
||||||
|
.filter((st) => st.type === 'view')
|
||||||
|
.map((st) => `${st.schema}.${st.table}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter tables
|
||||||
|
const filteredTables = metadata.tables.filter((table) => {
|
||||||
|
const schema = schemaNameToDomainSchemaName(table.schema) ?? '';
|
||||||
|
const tableId = `${schema}.${table.table}`;
|
||||||
|
return selectedTableSet.has(tableId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter views - include views that were explicitly selected
|
||||||
|
const filteredViews =
|
||||||
|
metadata.views?.filter((view) => {
|
||||||
|
const schema = schemaNameToDomainSchemaName(view.schema) ?? '';
|
||||||
|
const viewName = view.view_name ?? '';
|
||||||
|
const viewId = `${schema}.${viewName}`;
|
||||||
|
return selectedViewSet.has(viewId);
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
// Filter columns - include columns from both tables and views
|
||||||
|
const filteredColumns = metadata.columns.filter((col) => {
|
||||||
|
const fromTable = filteredTables.some(
|
||||||
|
(tb) => tb.schema === col.schema && tb.table === col.table
|
||||||
|
);
|
||||||
|
// For views, the column.table field might contain the view name
|
||||||
|
const fromView = filteredViews.some(
|
||||||
|
(view) => view.schema === col.schema && view.view_name === col.table
|
||||||
|
);
|
||||||
|
return fromTable || fromView;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter primary keys
|
||||||
|
const filteredPrimaryKeys = metadata.pk_info.filter((pk) =>
|
||||||
|
filteredTables.some(
|
||||||
|
(tb) => tb.schema === pk.schema && tb.table === pk.table
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter indexes
|
||||||
|
const filteredIndexes = metadata.indexes.filter((idx) =>
|
||||||
|
filteredTables.some(
|
||||||
|
(tb) => tb.schema === idx.schema && tb.table === idx.table
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter foreign keys - include if either source or target table is selected
|
||||||
|
// This ensures all relationships related to selected tables are preserved
|
||||||
|
const filteredForeignKeys = metadata.fk_info.filter((fk) => {
|
||||||
|
// Handle reference_schema and reference_table fields from the JSON
|
||||||
|
const targetSchema = fk.reference_schema;
|
||||||
|
const targetTable = (fk.reference_table || '').replace(/^"+|"+$/g, ''); // Remove extra quotes
|
||||||
|
|
||||||
|
const sourceIncluded = filteredTables.some(
|
||||||
|
(tb) => tb.schema === fk.schema && tb.table === fk.table
|
||||||
|
);
|
||||||
|
const targetIncluded = filteredTables.some(
|
||||||
|
(tb) => tb.schema === targetSchema && tb.table === targetTable
|
||||||
|
);
|
||||||
|
return sourceIncluded || targetIncluded;
|
||||||
|
});
|
||||||
|
|
||||||
|
const schemasWithTables = new Set(filteredTables.map((tb) => tb.schema));
|
||||||
|
const schemasWithViews = new Set(filteredViews.map((view) => view.schema));
|
||||||
|
|
||||||
|
// Filter custom types if they exist
|
||||||
|
const filteredCustomTypes =
|
||||||
|
metadata.custom_types?.filter((customType) => {
|
||||||
|
// Also check if the type is used by any of the selected tables' columns
|
||||||
|
const typeUsedInColumns = filteredColumns.some(
|
||||||
|
(col) =>
|
||||||
|
col.type === customType.type ||
|
||||||
|
col.type.includes(customType.type) // Handle array types like "custom_type[]"
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
schemasWithTables.has(customType.schema) ||
|
||||||
|
schemasWithViews.has(customType.schema) ||
|
||||||
|
typeUsedInColumns
|
||||||
|
);
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...metadata,
|
||||||
|
tables: filteredTables,
|
||||||
|
columns: filteredColumns,
|
||||||
|
pk_info: filteredPrimaryKeys,
|
||||||
|
indexes: filteredIndexes,
|
||||||
|
fk_info: filteredForeignKeys,
|
||||||
|
views: filteredViews,
|
||||||
|
custom_types: filteredCustomTypes,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export interface DBCustomTypeFieldInfo {
|
||||||
|
field: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DBCustomTypeFieldInfoSchema = z.object({
|
||||||
|
field: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface DBCustomTypeInfo {
|
||||||
|
schema: string;
|
||||||
|
type: string;
|
||||||
|
kind: 'enum' | 'composite';
|
||||||
|
values?: string[];
|
||||||
|
fields?: DBCustomTypeFieldInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DBCustomTypeInfoSchema: z.ZodType<DBCustomTypeInfo> = z.object({
|
||||||
|
schema: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
kind: z.enum(['enum', 'composite']),
|
||||||
|
values: z.array(z.string()).optional(),
|
||||||
|
fields: z.array(DBCustomTypeFieldInfoSchema).optional(),
|
||||||
|
});
|
||||||
@@ -5,6 +5,10 @@ import { ColumnInfoSchema, type ColumnInfo } from './column-info';
|
|||||||
import { IndexInfoSchema, type IndexInfo } from './index-info';
|
import { IndexInfoSchema, type IndexInfo } from './index-info';
|
||||||
import { TableInfoSchema, type TableInfo } from './table-info';
|
import { TableInfoSchema, type TableInfo } from './table-info';
|
||||||
import { ViewInfoSchema, type ViewInfo } from './view-info';
|
import { ViewInfoSchema, type ViewInfo } from './view-info';
|
||||||
|
import {
|
||||||
|
DBCustomTypeInfoSchema,
|
||||||
|
type DBCustomTypeInfo,
|
||||||
|
} from './custom-type-info';
|
||||||
|
|
||||||
export interface DatabaseMetadata {
|
export interface DatabaseMetadata {
|
||||||
fk_info: ForeignKeyInfo[];
|
fk_info: ForeignKeyInfo[];
|
||||||
@@ -13,6 +17,7 @@ export interface DatabaseMetadata {
|
|||||||
indexes: IndexInfo[];
|
indexes: IndexInfo[];
|
||||||
tables: TableInfo[];
|
tables: TableInfo[];
|
||||||
views: ViewInfo[];
|
views: ViewInfo[];
|
||||||
|
custom_types?: DBCustomTypeInfo[];
|
||||||
database_name: string;
|
database_name: string;
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
@@ -24,6 +29,7 @@ export const DatabaseMetadataSchema: z.ZodType<DatabaseMetadata> = z.object({
|
|||||||
indexes: z.array(IndexInfoSchema),
|
indexes: z.array(IndexInfoSchema),
|
||||||
tables: z.array(TableInfoSchema),
|
tables: z.array(TableInfoSchema),
|
||||||
views: z.array(ViewInfoSchema),
|
views: z.array(ViewInfoSchema),
|
||||||
|
custom_types: z.array(DBCustomTypeInfoSchema).optional(),
|
||||||
database_name: z.string(),
|
database_name: z.string(),
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user