Compare commits

...

55 Commits

Author SHA1 Message Date
Guy Ben-Aharon
44eac7daff chore(main): release 1.9.0 (#607) 2025-03-13 12:20:07 +02:00
Guy Ben-Aharon
502472b083 fix: remove Buckle dialog (#617) 2025-03-13 11:32:54 +02:00
Guy Ben-Aharon
52d2ea596c add diff mode (#614)
* initial diff viewer

* test

* test

* name change

* new table show

* diff viewer
2025-03-13 11:12:11 +02:00
Guy Ben-Aharon
bd67ccfbcf feat(chart max length): enable edit length from data type select box (#616)
* feat(chart max length): enable edit length from data type select box

* fix
2025-03-11 18:49:10 +02:00
Jonathan Fishner
62beb68fa1 feat(canvas): highlight the Show-All button when No-Tables are visible in the canvas (#612)
* feat(canvas): highlight the Show-All button when No-Tables are visible in the canvas

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-03-10 15:51:33 +02:00
Guy Ben-Aharon
09b1275475 feat(chart max length): add support for edit char max length (#613)
* feat(chart max length): add support for edit char max length

* fix

* update datatypes for max chars

---------

Co-authored-by: johnnyfish <jonathanfishner11@gmail.com>
2025-03-10 13:25:55 +02:00
Guy Ben-Aharon
5dd7fe75d1 fix(performance): Optimize performance of field comments editing (#610) 2025-03-05 18:20:04 +02:00
Jonathan Fishner
2939320a15 fix(cardinality): set true as default (#583) 2025-03-04 11:07:49 +02:00
Jonathan Fishner
a643852837 fix(shorcuts): add shortcut to toggle the theme (#602) 2025-03-04 10:28:30 +02:00
Guy Ben-Aharon
467ff697c9 chore(main): release 1.8.1 (#581) 2025-03-02 13:15:36 +02:00
Sibi Krishnamoorthy
d6919f3033 fix(docker config): Environment Variable Handling and Configuration Logic (#605)
* fixed custom openai endpoint not working

* minor fix
2025-03-02 13:12:39 +02:00
Guy Ben-Aharon
56382a9fdc fix(sql_server_export): use sql server export (#600) 2025-02-26 22:05:40 +02:00
Jonathan Fishner
e06eb2a48e fix(import-mssql): fix import/export scripts to handle data correctly (#598)
* fix(import-mssql): fix import/export scripts to handle data correctly

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-26 21:38:34 +02:00
Guy Ben-Aharon
543b716c77 refactor: move export diagram to hook (#599) 2025-02-26 20:03:26 +02:00
Jonathan Fishner
b55d631146 fix(add-docs): add link to ChartDB documentation (#597) 2025-02-24 21:35:10 +02:00
Guy Ben-Aharon
ef118929ad fix: open create new diagram when there is no diagram (#594) 2025-02-23 21:19:40 +02:00
Guy Ben-Aharon
68f48190c9 fix(open diagram): in case there is no diagram, opens the dialog (#593)
* fix(open diagram): in case there is no diagram, opens the dialog

* fix

* fix
2025-02-23 19:57:01 +02:00
Guy Ben-Aharon
bba265ad43 refactor(editor): import default diagram (#592)
* refactor(editor): import default diagram

* refactor(editor): import default diagram
2025-02-23 15:13:57 +02:00
Guy Ben-Aharon
cbc4e85a14 fix: components config (#591)
* fix: fix components config

* fix: fix components config
2025-02-23 09:16:55 +02:00
Jonathan Fishner
26a0a5b550 fix(menu-backup): update export to be backup (#590)
* fix(menu-backup): update copy change export to be backup

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-23 09:14:27 +02:00
Jonathan Fishner
b935b7f251 fix(img-export): add ChartDB watermark to exported image (#588)
* fix(img-export): add ChartDB watermark to exported image

* change watermark size to be smaller

* fix back the export for SVG
2025-02-23 08:42:37 +02:00
Jonathan Fishner
a1c0cf102a fix(side-panel): simplify how to add field and index (#573)
* fix(side-panel): simplify how to add feild and index

* fix(side-panel): auto-open the index Attributes when adding index

* fix(side-pannel): add focus after adding feild/index

* add some fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-19 14:46:41 +02:00
Anthony Mini
ab89bad6d5 fix(i18n): add [FR] translation (#579)
Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2025-02-16 20:08:18 +02:00
Jonathan Fishner
deb218423f fix(sqlite-import): import nuallable columns correctly + add json type (#571) 2025-02-16 11:36:02 +02:00
Jonathan Fishner
48342471ac fix(empty-state): show diff buttons on import-dbml when triggered by empty (#574)
* feat(empty-state): trigger import-dbml when clicked empty diagram

* fix(empty-state): show diff buttons on import-dbml when triggered by empty

* fix(empty-state): add missing translations for empty state flow

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-13 17:53:21 +02:00
Guy Ben-Aharon
47bb87a88f chore(main): release 1.8.0 (#558) 2025-02-13 15:49:53 +02:00
Guy Ben-Aharon
a96c2e1078 fix(docker): add option to hide popups (#580) 2025-02-13 15:47:02 +02:00
Guy Ben-Aharon
26d95eed25 fix(table actions): fix size of table actions (#578) 2025-02-11 16:22:54 +02:00
Jonathan Fishner
be65328f24 fix(mssql-import): improve script readability by adding edition comment (#572)
* fix(mssql-import): improve script readability by adding edition comment

* fix(mssql-import): fixing issues with import query
2025-02-10 11:00:51 +02:00
Jonathan Fishner
85fd14fa02 fix(export-sql): show create script for only filtered schemas (#570)
* fix(export-sql): show create script for only filtered schemas

* add dependencies filter

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-10 10:54:22 +02:00
Jonathan Fishner
9c485b3b01 fix(sqlserver-import): open ssms guide when max chars (#565)
* fix(sqlserver-import): open ssms guide when max chars (truncated chars) for input

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-10 10:37:32 +02:00
Jonathan Fishner
e993f1549c fix(canvas): add right-click option to create relationships (#568)
* feat(create-relationship): add right-click option to easy create relationships

* add missing translations

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-10 10:06:31 +02:00
Jonathan Fishner
0db67ea42a fix(import dbml): add import for indexes (#566) 2025-02-10 09:12:11 +02:00
Jonathan Fishner
b9e621bd68 fix(realtionships section): add the schema to source/target tables (#561) 2025-02-09 23:23:11 +02:00
Jonathan Fishner
93d59f8887 fix(import-query): improve the cleanup for messy json input (#562) 2025-02-07 14:03:38 +02:00
Jonathan Fishner
190e4f4ffa feat(dbml-import): add error highlighting for dbml imports (#556)
* feat(dbml-import): add error highlighting for dbml imports

* fix deprecated api

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-06 21:42:14 +02:00
Guy Ben-Aharon
dc404c9d7e fix(canvas): locate table from canvas (#560) 2025-02-06 21:39:55 +02:00
Guy Ben-Aharon
dd4324d64f fix(index unique): extract unique toggle for faster editing (#559) 2025-02-06 21:37:11 +02:00
Calum Siemer
1878083056 feat(docker image): add support for custom inference servers (#543)
- Add OPENAI_API_ENDPOINT configuration
- Add LLM_MODEL_NAME configuration
- Update documentation for custom server setup
- Add error handling for endpoint configuration
2025-02-06 20:16:15 +02:00
Andrii Holovin
7b6271962a fix(i18n): fix Ukrainian (#554) 2025-02-06 18:59:06 +02:00
Guy Ben-Aharon
2edc8dfde8 chore(main): release 1.7.0 (#539) 2025-02-05 12:49:38 +02:00
Guy Ben-Aharon
004d530880 fix(performance): reduce bundle size (#553) 2025-02-03 12:08:02 +02:00
Guy Ben-Aharon
fd2cc9fcfc fix(performance): resolve error on startup (#552) 2025-02-02 17:19:53 +02:00
Guy Ben-Aharon
4c93326bb6 fix(performance): fix bundle size (#551) 2025-02-02 16:45:16 +02:00
Guy Ben-Aharon
ef3d7a8b67 fix(scroll): fix scroll area (#550) 2025-02-02 16:34:18 +02:00
Jonathan Fishner
3b3be086b1 fix(dbml-editor): add shortcuts to dbml and filter: #534 (#535)
* fix(dbml-editor): add shortcuts to dbml and filter

* some fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-02 15:36:53 +02:00
Jonathan Fishner
b424518212 feat(import-dbml): add import dbml functionality (#549)
* feat(import-dbml): add import dbml functionality

* fix: fit to view do not run after every tbl move

* fix: add missing translation keys

* some fixes

* some fixes

* fix: update for longer timeout before fitToView

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-02 15:20:01 +02:00
Jonathan Fishner
99a8201398 fix(empty-state): fix dark-mode for empty-state (#547) 2025-02-02 11:48:09 +02:00
Jonathan Fishner
eb9b41e4f6 fix(psql-import): remove typo for import command (psql) (#546) 2025-01-30 14:53:01 +02:00
Guy Ben-Aharon
fef6d3f499 fix(dbml): add error handling (#545) 2025-01-28 13:18:54 +02:00
Jonathan Fishner
14f11c27a7 fix(open-diagram): add arrow keys navigation in open diagram dialog (#537)
* fix(open-diagram): add arrow keys navigation in open diagram dialog

* some refining

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-01-28 13:16:14 +02:00
Guy Ben-Aharon
2118bce0f0 fix(examples): fix employee example dbml (#544) 2025-01-28 09:49:21 +02:00
Jonathan Fishner
88be6c1fd4 feat(dbml-editor): add dbml editor in side pannel (#534)
* add dbml mode

* add dark theme to dbml editor

* add the same sort logic to the dbml

* add comments to dbml

* fix colors + filtering

* fix(dbml-editor): add read-only toast for dbml editor

* update toast location

* fix(translations): add dbml translations

* dbml ui improvements

* dbml ui improvements

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-01-27 20:32:39 +02:00
Jonathan Fishner
0dcc9b9568 fix(canvas edit): add option to edit names in canvas (#536)
* fix(canvas edit): add option to edit names in canvas

* fix(canvas edit): add option to edit field names in canvas

* some few fixes + style

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-01-27 14:26:04 +02:00
Andrii Holovin
ff3269ec05 fix(i18n): translation/Ukrainian (#529)
* Update uk.ts

* Apply suggestions from code review

Fix linter issues

---------

Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2025-01-27 12:47:52 +02:00
106 changed files with 7624 additions and 4042 deletions

View File

@@ -1,5 +1,89 @@
# Changelog
## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13)
### Features
* **canvas:** highlight the Show-All button when No-Tables are visible in the canvas ([#612](https://github.com/chartdb/chartdb/issues/612)) ([62beb68](https://github.com/chartdb/chartdb/commit/62beb68fa1ec22ccd4fe5e59a8ceb9d3e8f6d374))
* **chart max length:** add support for edit char max length ([#613](https://github.com/chartdb/chartdb/issues/613)) ([09b1275](https://github.com/chartdb/chartdb/commit/09b12754757b9625ca287d91a92cf0d83c9e2b89))
* **chart max length:** enable edit length from data type select box ([#616](https://github.com/chartdb/chartdb/issues/616)) ([bd67ccf](https://github.com/chartdb/chartdb/commit/bd67ccfbcf66b919453ca6c0bfd71e16772b3d8e))
### Bug Fixes
* **cardinality:** set true as default ([#583](https://github.com/chartdb/chartdb/issues/583)) ([2939320](https://github.com/chartdb/chartdb/commit/2939320a15a9ccd9eccfe46c26e04ca1edca2420))
* **performance:** Optimize performance of field comments editing ([#610](https://github.com/chartdb/chartdb/issues/610)) ([5dd7fe7](https://github.com/chartdb/chartdb/commit/5dd7fe75d1b0378ba406c75183c5e2356730c3b4))
* remove Buckle dialog ([#617](https://github.com/chartdb/chartdb/issues/617)) ([502472b](https://github.com/chartdb/chartdb/commit/502472b08342be425e66e2b6c94e5fe37ba14aa9))
* **shorcuts:** add shortcut to toggle the theme ([#602](https://github.com/chartdb/chartdb/issues/602)) ([a643852](https://github.com/chartdb/chartdb/commit/a6438528375ab54d3ec7d80ac6b6ddd65ea8cf1e))
## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02)
### Bug Fixes
* **add-docs:** add link to ChartDB documentation ([#597](https://github.com/chartdb/chartdb/issues/597)) ([b55d631](https://github.com/chartdb/chartdb/commit/b55d631146ff3a1f7d63c800d44b5d3d3a223c76))
* components config ([#591](https://github.com/chartdb/chartdb/issues/591)) ([cbc4e85](https://github.com/chartdb/chartdb/commit/cbc4e85a14e24a43f9ff470518f8fe2845046bdb))
* **docker config:** Environment Variable Handling and Configuration Logic ([#605](https://github.com/chartdb/chartdb/issues/605)) ([d6919f3](https://github.com/chartdb/chartdb/commit/d6919f30336cc846fe6e6505b5a5278aa14dcce6))
* **empty-state:** show diff buttons on import-dbml when triggered by empty ([#574](https://github.com/chartdb/chartdb/issues/574)) ([4834247](https://github.com/chartdb/chartdb/commit/48342471ac231922f2ca4455b74a9879127a54f1))
* **i18n:** add [FR] translation ([#579](https://github.com/chartdb/chartdb/issues/579)) ([ab89bad](https://github.com/chartdb/chartdb/commit/ab89bad6d544ba4c339a3360eeec7d29e5579511))
* **img-export:** add ChartDB watermark to exported image ([#588](https://github.com/chartdb/chartdb/issues/588)) ([b935b7f](https://github.com/chartdb/chartdb/commit/b935b7f25111d5f72b7f8d7c552a4ea5974f791e))
* **import-mssql:** fix import/export scripts to handle data correctly ([#598](https://github.com/chartdb/chartdb/issues/598)) ([e06eb2a](https://github.com/chartdb/chartdb/commit/e06eb2a48e6bd3bcf352f4bcf128214c7da4c1b1))
* **menu-backup:** update export to be backup ([#590](https://github.com/chartdb/chartdb/issues/590)) ([26a0a5b](https://github.com/chartdb/chartdb/commit/26a0a5b550ef5e47e89b00d0232dc98936f63f23))
* open create new diagram when there is no diagram ([#594](https://github.com/chartdb/chartdb/issues/594)) ([ef11892](https://github.com/chartdb/chartdb/commit/ef118929ad5d5cbfae0290061bd8ea30bd262496))
* **open diagram:** in case there is no diagram, opens the dialog ([#593](https://github.com/chartdb/chartdb/issues/593)) ([68f4819](https://github.com/chartdb/chartdb/commit/68f48190c93f155398cca15dd7af2a025de2d45f))
* **side-panel:** simplify how to add field and index ([#573](https://github.com/chartdb/chartdb/issues/573)) ([a1c0cf1](https://github.com/chartdb/chartdb/commit/a1c0cf102add4fb235e913e75078139b3961341b))
* **sql_server_export:** use sql server export ([#600](https://github.com/chartdb/chartdb/issues/600)) ([56382a9](https://github.com/chartdb/chartdb/commit/56382a9fdc5e3044f8811873dd8a79f590771896))
* **sqlite-import:** import nuallable columns correctly + add json type ([#571](https://github.com/chartdb/chartdb/issues/571)) ([deb2184](https://github.com/chartdb/chartdb/commit/deb218423f77f0c0945a93005696456f62b00ce3))
## [1.8.0](https://github.com/chartdb/chartdb/compare/v1.7.0...v1.8.0) (2025-02-13)
### Features
* **dbml-import:** add error highlighting for dbml imports ([#556](https://github.com/chartdb/chartdb/issues/556)) ([190e4f4](https://github.com/chartdb/chartdb/commit/190e4f4ffa834fa621f264dc608ca3f3b393a331))
* **docker image:** add support for custom inference servers ([#543](https://github.com/chartdb/chartdb/issues/543)) ([1878083](https://github.com/chartdb/chartdb/commit/1878083056ea4db7a05cdeeb38a4f7b9f5f95bd1))
### Bug Fixes
* **canvas:** add right-click option to create relationships ([#568](https://github.com/chartdb/chartdb/issues/568)) ([e993f15](https://github.com/chartdb/chartdb/commit/e993f1549c4c86bb9e7e36062db803ba6613b3b3))
* **canvas:** locate table from canvas ([#560](https://github.com/chartdb/chartdb/issues/560)) ([dc404c9](https://github.com/chartdb/chartdb/commit/dc404c9d7ee272c93aac69646bac859829a5234e))
* **docker:** add option to hide popups ([#580](https://github.com/chartdb/chartdb/issues/580)) ([a96c2e1](https://github.com/chartdb/chartdb/commit/a96c2e107838d2dc13b586923fd9dbe06598cdd8))
* **export-sql:** show create script for only filtered schemas ([#570](https://github.com/chartdb/chartdb/issues/570)) ([85fd14f](https://github.com/chartdb/chartdb/commit/85fd14fa02bb2879c36bba53369dbf2e7fa578d4))
* **i18n:** fix Ukrainian ([#554](https://github.com/chartdb/chartdb/issues/554)) ([7b62719](https://github.com/chartdb/chartdb/commit/7b6271962a99bfe5ffbd0176e714c76368ef5c41))
* **import dbml:** add import for indexes ([#566](https://github.com/chartdb/chartdb/issues/566)) ([0db67ea](https://github.com/chartdb/chartdb/commit/0db67ea42a5f9585ca1d246db7a7ff0239bec0ba))
* **import-query:** improve the cleanup for messy json input ([#562](https://github.com/chartdb/chartdb/issues/562)) ([93d59f8](https://github.com/chartdb/chartdb/commit/93d59f8887765098d040a3184aaee32112f67267))
* **index unique:** extract unique toggle for faster editing ([#559](https://github.com/chartdb/chartdb/issues/559)) ([dd4324d](https://github.com/chartdb/chartdb/commit/dd4324d64f7638ada5c022a2ab38bd8e6986af25))
* **mssql-import:** improve script readability by adding edition comment ([#572](https://github.com/chartdb/chartdb/issues/572)) ([be65328](https://github.com/chartdb/chartdb/commit/be65328f24b0361638b9e2edb39eaa9906e77f67))
* **realtionships section:** add the schema to source/target tables ([#561](https://github.com/chartdb/chartdb/issues/561)) ([b9e621b](https://github.com/chartdb/chartdb/commit/b9e621bd680730a0ffbf1054d735bfa418711cae))
* **sqlserver-import:** open ssms guide when max chars ([#565](https://github.com/chartdb/chartdb/issues/565)) ([9c485b3](https://github.com/chartdb/chartdb/commit/9c485b3b01a131bf551c7e95916b0c416f6aa0b5))
* **table actions:** fix size of table actions ([#578](https://github.com/chartdb/chartdb/issues/578)) ([26d95ee](https://github.com/chartdb/chartdb/commit/26d95eed25d86452d9168a9d93a301ba50d934e3))
## [1.7.0](https://github.com/chartdb/chartdb/compare/v1.6.1...v1.7.0) (2025-02-03)
### Features
* **dbml-editor:** add dbml editor in side pannel ([#534](https://github.com/chartdb/chartdb/issues/534)) ([88be6c1](https://github.com/chartdb/chartdb/commit/88be6c1fd4a7e1f20937e8204c14d8fc1c2665b4))
* **import-dbml:** add import dbml functionality ([#549](https://github.com/chartdb/chartdb/issues/549)) ([b424518](https://github.com/chartdb/chartdb/commit/b424518212290a870fdb7c420a303f65f5901429))
### Bug Fixes
* **canvas edit:** add option to edit names in canvas ([#536](https://github.com/chartdb/chartdb/issues/536)) ([0dcc9b9](https://github.com/chartdb/chartdb/commit/0dcc9b9568cfe749d44d2e93cb365ba3d3a1e71c))
* **dbml-editor:** add shortcuts to dbml and filter: [#534](https://github.com/chartdb/chartdb/issues/534) ([#535](https://github.com/chartdb/chartdb/issues/535)) ([3b3be08](https://github.com/chartdb/chartdb/commit/3b3be086b1e8d5acf999f8504580d9e2f956f7da))
* **dbml:** add error handling ([#545](https://github.com/chartdb/chartdb/issues/545)) ([fef6d3f](https://github.com/chartdb/chartdb/commit/fef6d3f4996130a3769d1f25b4b1f2090293a1bf))
* **empty-state:** fix dark-mode for empty-state ([#547](https://github.com/chartdb/chartdb/issues/547)) ([99a8201](https://github.com/chartdb/chartdb/commit/99a820139861546a012d7b562ddbb9b77698151a))
* **examples:** fix employee example dbml ([#544](https://github.com/chartdb/chartdb/issues/544)) ([2118bce](https://github.com/chartdb/chartdb/commit/2118bce0f00d55eb19d22b9fa2d4964ba2533a09))
* **i18n:** translation/Ukrainian ([#529](https://github.com/chartdb/chartdb/issues/529)) ([ff3269e](https://github.com/chartdb/chartdb/commit/ff3269ec0510bbae4bc114e65a1ea86a656e8785))
* **open-diagram:** add arrow keys navigation in open diagram dialog ([#537](https://github.com/chartdb/chartdb/issues/537)) ([14f11c2](https://github.com/chartdb/chartdb/commit/14f11c27a7ad5b990131c8495148cabf12835082))
* **performance:** fix bundle size ([#551](https://github.com/chartdb/chartdb/issues/551)) ([4c93326](https://github.com/chartdb/chartdb/commit/4c93326bb6e3eaa143373c500a0c641e95a53fb9))
* **performance:** reduce bundle size ([#553](https://github.com/chartdb/chartdb/issues/553)) ([004d530](https://github.com/chartdb/chartdb/commit/004d530880a50dea6e9786eb9ae63cf592a4d852))
* **performance:** resolve error on startup ([#552](https://github.com/chartdb/chartdb/issues/552)) ([fd2cc9f](https://github.com/chartdb/chartdb/commit/fd2cc9fcfc8f4a9f0bc79def47d89114159392fb))
* **psql-import:** remove typo for import command (psql) ([#546](https://github.com/chartdb/chartdb/issues/546)) ([eb9b41e](https://github.com/chartdb/chartdb/commit/eb9b41e4f656bec1451c45763f4ea5b547aeec5c))
* **scroll:** fix scroll area ([#550](https://github.com/chartdb/chartdb/issues/550)) ([ef3d7a8](https://github.com/chartdb/chartdb/commit/ef3d7a8b67431e923b75bf8287b86bbc8abe723b))
## [1.6.1](https://github.com/chartdb/chartdb/compare/v1.6.0...v1.6.1) (2025-01-26)

View File

@@ -1,6 +1,9 @@
FROM node:22-alpine AS builder
ARG VITE_OPENAI_API_KEY
ARG VITE_OPENAI_API_ENDPOINT
ARG VITE_LLM_MODEL_NAME
ARG VITE_HIDE_BUCKLE_DOT_DEV
WORKDIR /usr/src/app
@@ -10,9 +13,13 @@ RUN npm ci
COPY . .
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \
echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env
RUN npm run build
# Use a lightweight web server to serve the production build
FROM nginx:stable-alpine AS production
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
@@ -20,7 +27,6 @@ COPY ./default.conf.template /etc/nginx/conf.d/default.conf.template
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Expose the default port for the Nginx web server
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -107,8 +107,33 @@ docker build -t chartdb .
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb
```
#### Using Custom Inference Server
```bash
# Build
docker build \
--build-arg VITE_OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \
--build-arg VITE_LLM_MODEL_NAME=<YOUR_MODEL_NAME> \
-t chartdb .
# Run
docker run \
-e OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \
-e LLM_MODEL_NAME=<YOUR_MODEL_NAME> \
-p 8080:80 chartdb
```
> **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`.
Example configuration for a local vLLM server:
```bash
VITE_OPENAI_API_ENDPOINT=http://localhost:8000/v1
VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ
```
## Try it on our website
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)

View File

@@ -1,17 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "src/components",
"utils": "@/lib/utils"
}
}
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "src/components",
"utils": "src/lib/utils",
"ui": "src/components/ui",
"lib": "src/lib",
"hooks": "src/hooks"
}
}

View File

@@ -10,7 +10,12 @@ server {
location /config.js {
default_type application/javascript;
return 200 "window.env = { OPENAI_API_KEY: \"$OPENAI_API_KEY\" };";
return 200 "window.env = {
OPENAI_API_KEY: \"$OPENAI_API_KEY\",
OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\"
};";
}
error_page 500 502 503 504 /50x.html;

View File

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

4784
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.6.1",
"version": "1.9.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -13,6 +13,7 @@
},
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",
"@dnd-kit/sortable": "^8.0.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-accordion": "^1.2.0",
@@ -28,7 +29,7 @@
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -12,6 +12,14 @@ import { DarkTheme } from './themes/dark';
import { LightTheme } from './themes/light';
import './config.ts';
export const Editor = lazy(() =>
import('./code-editor').then((module) => ({
default: module.Editor,
}))
);
type EditorType = typeof Editor;
export interface CodeSnippetProps {
className?: string;
code: string;
@@ -19,14 +27,9 @@ export interface CodeSnippetProps {
loading?: boolean;
autoScroll?: boolean;
isComplete?: boolean;
editorProps?: React.ComponentProps<EditorType>;
}
export const Editor = lazy(() =>
import('./code-editor').then((module) => ({
default: module.Editor,
}))
);
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
({
className,
@@ -35,6 +38,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
language = 'sql',
autoScroll = false,
isComplete = true,
editorProps,
}) => {
const { t } = useTranslation();
const monaco = useMonaco();
@@ -144,27 +148,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
language={language}
loading={<Spinner />}
theme={effectiveTheme}
{...editorProps}
options={{
minimap: {
enabled: false,
},
readOnly: true,
automaticLayout: true,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
alwaysConsumeMouseWheel: false,
},
scrollBeyondLastLine: false,
renderValidationDecorations: 'off',
lineDecorationsWidth: 0,
overviewRulerBorder: false,
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
contextmenu: false,
...editorProps?.options,
guides: {
indentation: false,
...editorProps?.options?.guides,
},
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
alwaysConsumeMouseWheel: false,
...editorProps?.options?.scrollbar,
},
minimap: {
enabled: false,
...editorProps?.options?.minimap,
},
contextmenu: false,
}}
/>
{!isComplete ? (

View File

@@ -0,0 +1,54 @@
import type { Monaco } from '@monaco-editor/react';
import { dataTypes } from '@/lib/data/data-types/data-types';
export const setupDBMLLanguage = (monaco: Monaco) => {
monaco.languages.register({ id: 'dbml' });
// Define themes for DBML
monaco.editor.defineTheme('dbml-dark', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'keyword', foreground: '569CD6' }, // Table, Ref keywords
{ token: 'string', foreground: 'CE9178' }, // Strings
{ token: 'annotation', foreground: '9CDCFE' }, // [annotations]
{ token: 'delimiter', foreground: 'D4D4D4' }, // Braces {}
{ token: 'operator', foreground: 'D4D4D4' }, // Operators
{ token: 'datatype', foreground: '4EC9B0' }, // Data types
],
colors: {},
});
monaco.editor.defineTheme('dbml-light', {
base: 'vs',
inherit: true,
rules: [
{ token: 'keyword', foreground: '0000FF' }, // Table, Ref keywords
{ token: 'string', foreground: 'A31515' }, // Strings
{ token: 'annotation', foreground: '001080' }, // [annotations]
{ token: 'delimiter', foreground: '000000' }, // Braces {}
{ token: 'operator', foreground: '000000' }, // Operators
{ token: 'type', foreground: '267F99' }, // Data types
],
colors: {},
});
const dataTypesNames = dataTypes.map((dt) => dt.name);
const datatypePattern = dataTypesNames.join('|');
monaco.languages.setMonarchTokensProvider('dbml', {
keywords: ['Table', 'Ref', 'Indexes'],
datatypes: dataTypesNames,
tokenizer: {
root: [
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
[/\[.*?\]/, 'annotation'],
[/".*?"/, 'string'],
[/'.*?'/, 'string'],
[/[{}]/, 'delimiter'],
[/[<>]/, 'operator'],
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
],
},
});
};

View File

@@ -1,7 +1,9 @@
import React, { forwardRef } from 'react';
import EmptyStateImage from '@/assets/empty_state.png';
import EmptyStateImageDark from '@/assets/empty_state_dark.png';
import { Label } from '@/components/label/label';
import { cn } from '@/lib/utils';
import { useTheme } from '@/hooks/use-theme';
export interface EmptyStateProps {
title: string;
@@ -25,30 +27,40 @@ export const EmptyState = forwardRef<
imageClassName,
},
ref
) => (
<div
ref={ref}
className={cn(
'flex flex-1 flex-col items-center justify-center space-y-1',
className
)}
>
<img
src={EmptyStateImage}
alt="Empty state"
className={cn('mb-2 w-20', imageClassName)}
/>
<Label className={cn('text-base', titleClassName)}>{title}</Label>
<Label
) => {
const { effectiveTheme } = useTheme();
return (
<div
ref={ref}
className={cn(
'text-sm font-normal text-muted-foreground',
descriptionClassName
'flex flex-1 flex-col items-center justify-center space-y-1',
className
)}
>
{description}
</Label>
</div>
)
<img
src={
effectiveTheme === 'dark'
? EmptyStateImageDark
: EmptyStateImage
}
alt="Empty state"
className={cn('mb-2 w-20', imageClassName)}
/>
<Label className={cn('text-base', titleClassName)}>
{title}
</Label>
<Label
className={cn(
'text-sm font-normal text-muted-foreground',
descriptionClassName
)}
>
{description}
</Label>
</div>
);
}
);
EmptyState.displayName = 'EmptyState';

View File

@@ -24,12 +24,19 @@ export interface SelectBoxOption {
value: string;
label: string;
description?: string;
regex?: string;
extractRegex?: RegExp;
}
export interface SelectBoxProps {
options: SelectBoxOption[];
value?: string[] | string;
onChange?: (values: string[] | string) => void;
valueSuffix?: string;
optionSuffix?: (option: SelectBoxOption) => string;
onChange?: (
values: string[] | string,
regexMatches?: string[] | string
) => void;
placeholder?: string;
inputPlaceholder?: string;
emptyPlaceholder?: string;
@@ -55,10 +62,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
className,
options,
value,
valueSuffix,
onChange,
multiple,
oneLine,
selectAll,
optionSuffix,
deselectAll,
clearText,
showClear,
@@ -86,7 +95,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
);
const handleSelect = React.useCallback(
(selectedValue: string) => {
(selectedValue: string, regexMatches?: string[]) => {
if (multiple) {
const newValue =
value?.includes(selectedValue) && Array.isArray(value)
@@ -94,7 +103,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
: [...(value ?? []), selectedValue];
onChange?.(newValue);
} else {
onChange?.(selectedValue);
onChange?.(selectedValue, regexMatches);
setIsOpen(false);
}
},
@@ -199,6 +208,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
(opt) => opt.value === value
)?.label
}
{valueSuffix ? valueSuffix : ''}
</div>
)
) : (
@@ -239,11 +249,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
align="center"
>
<Command
filter={(value, search) =>
value.toLowerCase().includes(search.toLowerCase())
filter={(value, search, keywords) => {
if (
keywords?.length &&
keywords.some((keyword) =>
new RegExp(keyword).test(search)
)
) {
return 1;
}
return value
.toLowerCase()
.includes(search.toLowerCase())
? 1
: 0
}
: 0;
}}
>
<div className="relative">
<CommandInput
@@ -302,14 +323,36 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
const isSelected =
Array.isArray(value) &&
value.includes(option.value);
const isRegexMatch =
option.regex &&
new RegExp(option.regex)?.test(
searchTerm
);
const matches = option.extractRegex
? searchTerm.match(
option.extractRegex
)
: undefined;
return (
<CommandItem
className="flex items-center"
key={option.value}
keywords={
option.regex
? [option.regex]
: undefined
}
// value={option.value}
onSelect={() =>
handleSelect(
option.value
option.value,
matches?.map(
(match) =>
match.toString()
)
)
}
>
@@ -327,7 +370,15 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
)}
<div className="flex items-center truncate">
<span>
{option.label}
{isRegexMatch
? searchTerm
: option.label}
{!isRegexMatch &&
optionSuffix
? optionSuffix(
option
)
: ''}
</span>
{option.description && (
<span className="ml-1 text-xs text-muted-foreground">
@@ -337,19 +388,20 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
</span>
)}
</div>
{!multiple &&
{((!multiple &&
option.value ===
value && (
<CheckIcon
className={cn(
'ml-auto',
option.value ===
value
? 'opacity-100'
: 'opacity-0'
)}
/>
)}
value) ||
isRegexMatch) && (
<CheckIcon
className={cn(
'ml-auto',
option.value ===
value
? 'opacity-100'
: 'opacity-0'
)}
/>
)}
</CommandItem>
);
})}

View File

@@ -0,0 +1,22 @@
import { createContext } from 'react';
import { emptyFn } from '@/lib/utils';
import type { Graph } from '@/lib/graph';
import { createGraph } from '@/lib/graph';
export interface CanvasContext {
reorderTables: (options?: { updateHistory?: boolean }) => void;
fitView: (options?: {
duration?: number;
padding?: number;
maxZoom?: number;
}) => void;
setOverlapGraph: (graph: Graph<string>) => void;
overlapGraph: Graph<string>;
}
export const canvasContext = createContext<CanvasContext>({
reorderTables: emptyFn,
fitView: emptyFn,
setOverlapGraph: emptyFn,
overlapGraph: createGraph(),
});

View File

@@ -0,0 +1,85 @@
import React, { type ReactNode, useCallback, useState } from 'react';
import { canvasContext } from './canvas-context';
import { useChartDB } from '@/hooks/use-chartdb';
import {
adjustTablePositions,
shouldShowTablesBySchemaFilter,
} from '@/lib/domain/db-table';
import { useReactFlow } from '@xyflow/react';
import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils';
import type { Graph } from '@/lib/graph';
import { createGraph } from '@/lib/graph';
interface CanvasProviderProps {
children: ReactNode;
}
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const { tables, relationships, updateTablesState, filteredSchemas } =
useChartDB();
const { fitView } = useReactFlow();
const [overlapGraph, setOverlapGraph] =
useState<Graph<string>>(createGraph());
const reorderTables = useCallback(
(
options: { updateHistory?: boolean } = {
updateHistory: true,
}
) => {
const newTables = adjustTablePositions({
relationships,
tables: tables.filter((table) =>
shouldShowTablesBySchemaFilter(table, filteredSchemas)
),
mode: 'all', // Use 'all' mode for manual reordering
});
const updatedOverlapGraph = findOverlappingTables({
tables: newTables,
});
updateTablesState(
(currentTables) =>
currentTables.map((table) => {
const newTable = newTables.find(
(t) => t.id === table.id
);
return {
id: table.id,
x: newTable?.x ?? table.x,
y: newTable?.y ?? table.y,
};
}),
{
updateHistory: options.updateHistory ?? true,
forceOverride: false,
}
);
setOverlapGraph(updatedOverlapGraph);
setTimeout(() => {
fitView({
duration: 500,
padding: 0.2,
maxZoom: 0.8,
});
}, 500);
},
[filteredSchemas, relationships, tables, updateTablesState, fitView]
);
return (
<canvasContext.Provider
value={{
reorderTables,
fitView,
setOverlapGraph,
overlapGraph,
}}
>
{children}
</canvasContext.Provider>
);
};

View File

@@ -22,6 +22,8 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
import { useEventEmitter } from 'ahooks';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { storageInitialValue } from '../storage-context/storage-context';
import { useDiff } from '../diff-context/use-diff';
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
export interface ChartDBProviderProps {
diagram?: Diagram;
@@ -30,7 +32,8 @@ export interface ChartDBProviderProps {
export const ChartDBProvider: React.FC<
React.PropsWithChildren<ChartDBProviderProps>
> = ({ children, diagram, readonly }) => {
> = ({ children, diagram, readonly: readonlyProp }) => {
const { hasDiff } = useDiff();
let db = useStorage();
const events = useEventEmitter<ChartDBEvent>();
const { setSchemasFilter, schemasFilter } = useLocalConfig();
@@ -53,9 +56,33 @@ export const ChartDBProvider: React.FC<
const [dependencies, setDependencies] = useState<DBDependency[]>(
diagram?.dependencies ?? []
);
const { events: diffEvents } = useDiff();
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
setTables((tables) =>
[...tables, ...(tablesAdded ?? [])].map((table) => {
const fields = fieldsAdded.get(table.id);
return fields
? { ...table, fields: [...table.fields, ...fields] }
: table;
})
);
setRelationships((relationships) => [
...relationships,
...(relationshipsAdded ?? []),
]);
}, []);
diffEvents.useSubscription(diffCalculatedHandler);
const defaultSchemaName = defaultSchemas[databaseType];
const readonly = useMemo(
() => readonlyProp ?? hasDiff ?? false,
[readonlyProp, hasDiff]
);
if (readonly) {
db = storageInitialValue;
}

View File

@@ -6,6 +6,9 @@ import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sq
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
export interface DialogContext {
// Create diagram dialog
@@ -13,7 +16,9 @@ export interface DialogContext {
closeCreateDiagramDialog: () => void;
// Open diagram dialog
openOpenDiagramDialog: () => void;
openOpenDiagramDialog: (
params?: Omit<OpenDiagramDialogProps, 'dialog'>
) => void;
closeOpenDiagramDialog: () => void;
// Export SQL dialog
@@ -21,7 +26,9 @@ export interface DialogContext {
closeExportSQLDialog: () => void;
// Create relationship dialog
openCreateRelationshipDialog: () => void;
openCreateRelationshipDialog: (
params?: Omit<CreateRelationshipDialogProps, 'dialog'>
) => void;
closeCreateRelationshipDialog: () => void;
// Import database dialog
@@ -40,10 +47,6 @@ export interface DialogContext {
openStarUsDialog: () => void;
closeStarUsDialog: () => void;
// Buckle dialog
openBuckleDialog: () => void;
closeBuckleDialog: () => void;
// Export image dialog
openExportImageDialog: (
params: Omit<ExportImageDialogProps, 'dialog'>
@@ -61,6 +64,12 @@ export interface DialogContext {
params: Omit<ImportDiagramDialogProps, 'dialog'>
) => void;
closeImportDiagramDialog: () => void;
// Import DBML dialog
openImportDBMLDialog: (
params?: Omit<ImportDBMLDialogProps, 'dialog'>
) => void;
closeImportDBMLDialog: () => void;
}
export const dialogContext = createContext<DialogContext>({
@@ -84,6 +93,6 @@ export const dialogContext = createContext<DialogContext>({
closeExportDiagramDialog: emptyFn,
openImportDiagramDialog: emptyFn,
closeImportDiagramDialog: emptyFn,
openBuckleDialog: emptyFn,
closeBuckleDialog: emptyFn,
openImportDBMLDialog: emptyFn,
closeImportDBMLDialog: emptyFn,
});

View File

@@ -2,10 +2,12 @@ import React, { useCallback, useState } from 'react';
import type { DialogContext } from './dialog-context';
import { dialogContext } from './dialog-context';
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
import { DatabaseType } from '@/lib/domain/database-type';
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
@@ -17,18 +19,40 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog';
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
const [openDiagramDialogParams, setOpenDiagramDialogParams] =
useState<Omit<OpenDiagramDialogProps, 'dialog'>>();
const openOpenDiagramDialogHandler: DialogContext['openOpenDiagramDialog'] =
useCallback(
(props) => {
setOpenDiagramDialogParams(props);
setOpenOpenDiagramDialog(true);
},
[setOpenOpenDiagramDialog]
);
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
useState(false);
const [createRelationshipDialogParams, setCreateRelationshipDialogParams] =
useState<Omit<CreateRelationshipDialogProps, 'dialog'>>();
const openCreateRelationshipDialogHandler: DialogContext['openCreateRelationshipDialog'] =
useCallback(
(params) => {
setCreateRelationshipDialogParams(params);
setOpenCreateRelationshipDialog(true);
},
[setOpenCreateRelationshipDialog]
);
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
const [openBuckleDialog, setOpenBuckleDialog] = useState(false);
// Export image dialog
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
@@ -88,7 +112,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
[setOpenTableSchemaDialog]
);
// Export image dialog
// Export diagram dialog
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
useState(false);
@@ -96,17 +120,22 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
useState(false);
// Import DBML dialog
const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
const [importDBMLDialogParams, setImportDBMLDialogParams] =
useState<Omit<ImportDBMLDialogProps, 'dialog'>>();
return (
<dialogContext.Provider
value={{
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true),
openOpenDiagramDialog: openOpenDiagramDialogHandler,
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
openExportSQLDialog: openExportSQLDialogHandler,
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
openCreateRelationshipDialog: () =>
setOpenCreateRelationshipDialog(true),
openCreateRelationshipDialog:
openCreateRelationshipDialogHandler,
closeCreateRelationshipDialog: () =>
setOpenCreateRelationshipDialog(false),
openImportDatabaseDialog: openImportDatabaseDialogHandler,
@@ -116,8 +145,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
openStarUsDialog: () => setOpenStarUsDialog(true),
closeStarUsDialog: () => setOpenStarUsDialog(false),
closeBuckleDialog: () => setOpenBuckleDialog(false),
openBuckleDialog: () => setOpenBuckleDialog(true),
closeExportImageDialog: () => setOpenExportImageDialog(false),
openExportImageDialog: openExportImageDialogHandler,
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
@@ -126,17 +153,26 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
closeImportDiagramDialog: () =>
setOpenImportDiagramDialog(false),
openImportDBMLDialog: (params) => {
setImportDBMLDialogParams(params);
setOpenImportDBMLDialog(true);
},
closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
}}
>
{children}
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
<OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} />
<OpenDiagramDialog
dialog={{ open: openOpenDiagramDialog }}
{...openDiagramDialogParams}
/>
<ExportSQLDialog
dialog={{ open: openExportSQLDialog }}
{...exportSQLDialogParams}
/>
<CreateRelationshipDialog
dialog={{ open: openCreateRelationshipDialog }}
{...createRelationshipDialogParams}
/>
<ImportDatabaseDialog
dialog={{ open: openImportDatabaseDialog }}
@@ -153,7 +189,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
/>
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
<BuckleDialog dialog={{ open: openBuckleDialog }} />
<ImportDBMLDialog
dialog={{ open: openImportDBMLDialog }}
{...importDBMLDialogParams}
/>
</dialogContext.Provider>
);
};

View File

@@ -0,0 +1,433 @@
import type { Diagram } from '@/lib/domain/diagram';
import type {
ChartDBDiff,
DiffMap,
DiffObject,
FieldDiffAttribute,
} from '../types';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
export function getDiffMapKey({
diffObject,
objectId,
attribute,
}: {
diffObject: DiffObject;
objectId: string;
attribute?: string;
}): string {
return attribute
? `${diffObject}-${attribute}-${objectId}`
: `${diffObject}-${objectId}`;
}
export function generateDiff({
diagram,
newDiagram,
}: {
diagram: Diagram;
newDiagram: Diagram;
}): {
diffMap: DiffMap;
changedTables: Map<string, boolean>;
changedFields: Map<string, boolean>;
} {
const newDiffs = new Map<string, ChartDBDiff>();
const changedTables = new Map<string, boolean>();
const changedFields = new Map<string, boolean>();
// Compare tables
compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables });
// Compare fields and indexes for matching tables
compareTableContents({
diagram,
newDiagram,
diffMap: newDiffs,
changedTables,
changedFields,
});
// Compare relationships
compareRelationships({ diagram, newDiagram, diffMap: newDiffs });
return { diffMap: newDiffs, changedTables, changedFields };
}
// Compare tables between diagrams
function compareTables({
diagram,
newDiagram,
diffMap,
changedTables,
}: {
diagram: Diagram;
newDiagram: Diagram;
diffMap: DiffMap;
changedTables: Map<string, boolean>;
}) {
const oldTables = diagram.tables || [];
const newTables = newDiagram.tables || [];
// Check for added tables
for (const newTable of newTables) {
if (!oldTables.find((t) => t.id === newTable.id)) {
diffMap.set(
getDiffMapKey({ diffObject: 'table', objectId: newTable.id }),
{
object: 'table',
type: 'added',
tableId: newTable.id,
}
);
changedTables.set(newTable.id, true);
}
}
// Check for removed tables
for (const oldTable of oldTables) {
if (!newTables.find((t) => t.id === oldTable.id)) {
diffMap.set(
getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }),
{
object: 'table',
type: 'removed',
tableId: oldTable.id,
}
);
changedTables.set(oldTable.id, true);
}
}
// Check for table name and comments changes
for (const oldTable of oldTables) {
const newTable = newTables.find((t) => t.id === oldTable.id);
if (!newTable) continue;
if (oldTable.name !== newTable.name) {
diffMap.set(
getDiffMapKey({
diffObject: 'table',
objectId: oldTable.id,
attribute: 'name',
}),
{
object: 'table',
type: 'changed',
tableId: oldTable.id,
attributes: 'name',
newValue: newTable.name,
oldValue: oldTable.name,
}
);
changedTables.set(oldTable.id, true);
}
if (oldTable.comments !== newTable.comments) {
diffMap.set(
getDiffMapKey({
diffObject: 'table',
objectId: oldTable.id,
attribute: 'comments',
}),
{
object: 'table',
type: 'changed',
tableId: oldTable.id,
attributes: 'comments',
newValue: newTable.comments,
oldValue: oldTable.comments,
}
);
changedTables.set(oldTable.id, true);
}
}
}
// Compare fields and indexes for matching tables
function compareTableContents({
diagram,
newDiagram,
diffMap,
changedTables,
changedFields,
}: {
diagram: Diagram;
newDiagram: Diagram;
diffMap: DiffMap;
changedTables: Map<string, boolean>;
changedFields: Map<string, boolean>;
}) {
const oldTables = diagram.tables || [];
const newTables = newDiagram.tables || [];
// For each table that exists in both diagrams
for (const oldTable of oldTables) {
const newTable = newTables.find((t) => t.id === oldTable.id);
if (!newTable) continue;
// Compare fields
compareFields({
tableId: oldTable.id,
oldFields: oldTable.fields,
newFields: newTable.fields,
diffMap,
changedTables,
changedFields,
});
// Compare indexes
compareIndexes({
tableId: oldTable.id,
oldIndexes: oldTable.indexes,
newIndexes: newTable.indexes,
diffMap,
changedTables,
});
}
}
// Compare fields between tables
function compareFields({
tableId,
oldFields,
newFields,
diffMap,
changedTables,
changedFields,
}: {
tableId: string;
oldFields: DBField[];
newFields: DBField[];
diffMap: DiffMap;
changedTables: Map<string, boolean>;
changedFields: Map<string, boolean>;
}) {
// Check for added fields
for (const newField of newFields) {
if (!oldFields.find((f) => f.id === newField.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'field',
objectId: newField.id,
}),
{
object: 'field',
type: 'added',
fieldId: newField.id,
tableId,
}
);
changedTables.set(tableId, true);
changedFields.set(newField.id, true);
}
}
// Check for removed fields
for (const oldField of oldFields) {
if (!newFields.find((f) => f.id === oldField.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'field',
objectId: oldField.id,
}),
{
object: 'field',
type: 'removed',
fieldId: oldField.id,
tableId,
}
);
changedTables.set(tableId, true);
changedFields.set(oldField.id, true);
}
}
// Check for field changes
for (const oldField of oldFields) {
const newField = newFields.find((f) => f.id === oldField.id);
if (!newField) continue;
// Compare basic field properties
compareFieldProperties({
tableId,
oldField,
newField,
diffMap,
changedTables,
changedFields,
});
}
}
// Compare field properties
function compareFieldProperties({
tableId,
oldField,
newField,
diffMap,
changedTables,
changedFields,
}: {
tableId: string;
oldField: DBField;
newField: DBField;
diffMap: DiffMap;
changedTables: Map<string, boolean>;
changedFields: Map<string, boolean>;
}) {
const changedAttributes: FieldDiffAttribute[] = [];
if (oldField.name !== newField.name) {
changedAttributes.push('name');
}
if (oldField.type.id !== newField.type.id) {
changedAttributes.push('type');
}
if (oldField.primaryKey !== newField.primaryKey) {
changedAttributes.push('primaryKey');
}
if (oldField.unique !== newField.unique) {
changedAttributes.push('unique');
}
if (oldField.nullable !== newField.nullable) {
changedAttributes.push('nullable');
}
if (oldField.comments !== newField.comments) {
changedAttributes.push('comments');
}
if (changedAttributes.length > 0) {
for (const attribute of changedAttributes) {
diffMap.set(
getDiffMapKey({
diffObject: 'field',
objectId: oldField.id,
attribute: attribute,
}),
{
object: 'field',
type: 'changed',
fieldId: oldField.id,
tableId,
attributes: attribute,
oldValue: oldField[attribute],
newValue: newField[attribute],
}
);
}
changedTables.set(tableId, true);
changedFields.set(oldField.id, true);
}
}
// Compare indexes between tables
function compareIndexes({
tableId,
oldIndexes,
newIndexes,
diffMap,
changedTables,
}: {
tableId: string;
oldIndexes: DBIndex[];
newIndexes: DBIndex[];
diffMap: DiffMap;
changedTables: Map<string, boolean>;
}) {
// Check for added indexes
for (const newIndex of newIndexes) {
if (!oldIndexes.find((i) => i.id === newIndex.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'index',
objectId: newIndex.id,
}),
{
object: 'index',
type: 'added',
indexId: newIndex.id,
tableId,
}
);
changedTables.set(tableId, true);
}
}
// Check for removed indexes
for (const oldIndex of oldIndexes) {
if (!newIndexes.find((i) => i.id === oldIndex.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'index',
objectId: oldIndex.id,
}),
{
object: 'index',
type: 'removed',
indexId: oldIndex.id,
tableId,
}
);
changedTables.set(tableId, true);
}
}
}
// Compare relationships between diagrams
function compareRelationships({
diagram,
newDiagram,
diffMap,
}: {
diagram: Diagram;
newDiagram: Diagram;
diffMap: DiffMap;
}) {
const oldRelationships = diagram.relationships || [];
const newRelationships = newDiagram.relationships || [];
// Check for added relationships
for (const newRelationship of newRelationships) {
if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'relationship',
objectId: newRelationship.id,
}),
{
object: 'relationship',
type: 'added',
relationshipId: newRelationship.id,
}
);
}
}
// Check for removed relationships
for (const oldRelationship of oldRelationships) {
if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'relationship',
objectId: oldRelationship.id,
}),
{
object: 'relationship',
type: 'removed',
relationshipId: oldRelationship.id,
}
);
}
}
}

View File

@@ -0,0 +1,75 @@
import { createContext } from 'react';
import type { DiffMap } from './types';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
import type { DBField } from '@/lib/domain/db-field';
import type { DataType } from '@/lib/data/data-types/data-types';
import type { DBRelationship } from '@/lib/domain/db-relationship';
export type DiffEventType = 'diff_calculated';
export type DiffEventBase<T extends DiffEventType, D> = {
action: T;
data: D;
};
export type DiffCalculatedEvent = DiffEventBase<
'diff_calculated',
{
tablesAdded: DBTable[];
fieldsAdded: Map<string, DBField[]>;
relationshipsAdded: DBRelationship[];
}
>;
export type DiffEvent = DiffCalculatedEvent;
export interface DiffContext {
newDiagram: Diagram | null;
diffMap: DiffMap;
hasDiff: boolean;
calculateDiff: ({
diagram,
newDiagram,
}: {
diagram: Diagram;
newDiagram: Diagram;
}) => void;
// table diff
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
checkIfNewTable: ({ tableId }: { tableId: string }) => boolean;
checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean;
getTableNewName: ({ tableId }: { tableId: string }) => string | null;
// field diff
checkIfFieldHasChange: ({
tableId,
fieldId,
}: {
tableId: string;
fieldId: string;
}) => boolean;
checkIfFieldRemoved: ({ fieldId }: { fieldId: string }) => boolean;
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
// relationship diff
checkIfNewRelationship: ({
relationshipId,
}: {
relationshipId: string;
}) => boolean;
checkIfRelationshipRemoved: ({
relationshipId,
}: {
relationshipId: string;
}) => boolean;
events: EventEmitter<DiffEvent>;
}
export const diffContext = createContext<DiffContext | undefined>(undefined);

View File

@@ -0,0 +1,327 @@
import React, { useCallback } from 'react';
import type { DiffContext, DiffEvent } from './diff-context';
import { diffContext } from './diff-context';
import type { ChartDBDiff, DiffMap } from './types';
import { generateDiff, getDiffMapKey } from './diff-check/diff-check';
import type { Diagram } from '@/lib/domain/diagram';
import { useEventEmitter } from 'ahooks';
import type { DBField } from '@/lib/domain/db-field';
import type { DataType } from '@/lib/data/data-types/data-types';
import type { DBRelationship } from '@/lib/domain/db-relationship';
export const DiffProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
const [diffMap, setDiffMap] = React.useState<DiffMap>(
new Map<string, ChartDBDiff>()
);
const [tablesChanged, setTablesChanged] = React.useState<
Map<string, boolean>
>(new Map<string, boolean>());
const [fieldsChanged, setFieldsChanged] = React.useState<
Map<string, boolean>
>(new Map<string, boolean>());
const events = useEventEmitter<DiffEvent>();
const generateNewFieldsMap = useCallback(
({
diffMap,
newDiagram,
}: {
diffMap: DiffMap;
newDiagram: Diagram;
}) => {
const newFieldsMap = new Map<string, DBField[]>();
diffMap.forEach((diff) => {
if (diff.object === 'field' && diff.type === 'added') {
const field = newDiagram?.tables
?.find((table) => table.id === diff.tableId)
?.fields.find((f) => f.id === diff.fieldId);
if (field) {
newFieldsMap.set(diff.tableId, [
...(newFieldsMap.get(diff.tableId) ?? []),
field,
]);
}
}
});
return newFieldsMap;
},
[]
);
const findNewRelationships = useCallback(
({
diffMap,
newDiagram,
}: {
diffMap: DiffMap;
newDiagram: Diagram;
}) => {
const relationships: DBRelationship[] = [];
diffMap.forEach((diff) => {
if (diff.object === 'relationship' && diff.type === 'added') {
const relationship = newDiagram?.relationships?.find(
(rel) => rel.id === diff.relationshipId
);
if (relationship) {
relationships.push(relationship);
}
}
});
return relationships;
},
[]
);
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
({ diagram, newDiagram: newDiagramArg }) => {
const {
diffMap: newDiffs,
changedTables: newChangedTables,
changedFields: newChangedFields,
} = generateDiff({ diagram, newDiagram: newDiagramArg });
setDiffMap(newDiffs);
setTablesChanged(newChangedTables);
setFieldsChanged(newChangedFields);
setNewDiagram(newDiagramArg);
events.emit({
action: 'diff_calculated',
data: {
tablesAdded:
newDiagramArg?.tables?.filter((table) => {
const tableKey = getDiffMapKey({
diffObject: 'table',
objectId: table.id,
});
return (
newDiffs.has(tableKey) &&
newDiffs.get(tableKey)?.type === 'added'
);
}) ?? [],
fieldsAdded: generateNewFieldsMap({
diffMap: newDiffs,
newDiagram: newDiagramArg,
}),
relationshipsAdded: findNewRelationships({
diffMap: newDiffs,
newDiagram: newDiagramArg,
}),
},
});
},
[setDiffMap, events, generateNewFieldsMap, findNewRelationships]
);
const getTableNewName = useCallback<DiffContext['getTableNewName']>(
({ tableId }) => {
const tableNameKey = getDiffMapKey({
diffObject: 'table',
objectId: tableId,
attribute: 'name',
});
if (diffMap.has(tableNameKey)) {
const diff = diffMap.get(tableNameKey);
if (diff?.type === 'changed') {
return diff.newValue as string;
}
}
return null;
},
[diffMap]
);
const checkIfTableHasChange = useCallback<
DiffContext['checkIfTableHasChange']
>(({ tableId }) => tablesChanged.get(tableId) ?? false, [tablesChanged]);
const checkIfNewTable = useCallback<DiffContext['checkIfNewTable']>(
({ tableId }) => {
const tableKey = getDiffMapKey({
diffObject: 'table',
objectId: tableId,
});
return (
diffMap.has(tableKey) && diffMap.get(tableKey)?.type === 'added'
);
},
[diffMap]
);
const checkIfTableRemoved = useCallback<DiffContext['checkIfTableRemoved']>(
({ tableId }) => {
const tableKey = getDiffMapKey({
diffObject: 'table',
objectId: tableId,
});
return (
diffMap.has(tableKey) &&
diffMap.get(tableKey)?.type === 'removed'
);
},
[diffMap]
);
const checkIfFieldHasChange = useCallback<
DiffContext['checkIfFieldHasChange']
>(
({ fieldId }) => {
return fieldsChanged.get(fieldId) ?? false;
},
[fieldsChanged]
);
const checkIfFieldRemoved = useCallback<DiffContext['checkIfFieldRemoved']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
});
return (
diffMap.has(fieldKey) &&
diffMap.get(fieldKey)?.type === 'removed'
);
},
[diffMap]
);
const checkIfNewField = useCallback<DiffContext['checkIfNewField']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
});
return (
diffMap.has(fieldKey) && diffMap.get(fieldKey)?.type === 'added'
);
},
[diffMap]
);
const getFieldNewName = useCallback<DiffContext['getFieldNewName']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'name',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as string;
}
}
return null;
},
[diffMap]
);
const getFieldNewType = useCallback<DiffContext['getFieldNewType']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'type',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return diff.newValue as DataType;
}
}
return null;
},
[diffMap]
);
const checkIfNewRelationship = useCallback<
DiffContext['checkIfNewRelationship']
>(
({ relationshipId }) => {
const relationshipKey = getDiffMapKey({
diffObject: 'relationship',
objectId: relationshipId,
});
return (
diffMap.has(relationshipKey) &&
diffMap.get(relationshipKey)?.type === 'added'
);
},
[diffMap]
);
const checkIfRelationshipRemoved = useCallback<
DiffContext['checkIfRelationshipRemoved']
>(
({ relationshipId }) => {
const relationshipKey = getDiffMapKey({
diffObject: 'relationship',
objectId: relationshipId,
});
return (
diffMap.has(relationshipKey) &&
diffMap.get(relationshipKey)?.type === 'removed'
);
},
[diffMap]
);
return (
<diffContext.Provider
value={{
newDiagram,
diffMap,
hasDiff: diffMap.size > 0,
calculateDiff,
// table diff
getTableNewName,
checkIfNewTable,
checkIfTableRemoved,
checkIfTableHasChange,
// field diff
checkIfFieldHasChange,
checkIfFieldRemoved,
checkIfNewField,
getFieldNewName,
getFieldNewType,
// relationship diff
checkIfNewRelationship,
checkIfRelationshipRemoved,
events,
}}
>
{children}
</diffContext.Provider>
);
};

View File

@@ -0,0 +1,53 @@
import type { DataType } from '@/lib/data/data-types/data-types';
export type TableDiffAttribute = 'name' | 'comments';
export interface TableDiff {
object: 'table';
type: 'added' | 'removed' | 'changed';
tableId: string;
attributes?: TableDiffAttribute;
oldValue?: string;
newValue?: string;
}
export interface RelationshipDiff {
object: 'relationship';
type: 'added' | 'removed';
relationshipId: string;
}
export type FieldDiffAttribute =
| 'name'
| 'type'
| 'primaryKey'
| 'unique'
| 'nullable'
| 'comments';
export interface FieldDiff {
object: 'field';
type: 'added' | 'removed' | 'changed';
fieldId: string;
tableId: string;
attributes?: FieldDiffAttribute;
oldValue?: string | boolean | DataType;
newValue?: string | boolean | DataType;
}
export interface IndexDiff {
object: 'index';
type: 'added' | 'removed';
indexId: string;
tableId: string;
}
export type ChartDBDiff = TableDiff | FieldDiff | IndexDiff | RelationshipDiff;
export type DiffMap = Map<string, ChartDBDiff>;
export type DiffObject =
| TableDiff['object']
| FieldDiff['object']
| IndexDiff['object']
| RelationshipDiff['object'];

View File

@@ -0,0 +1,10 @@
import { useContext } from 'react';
import { diffContext } from './diff-context';
export const useDiff = () => {
const context = useContext(diffContext);
if (context === undefined) {
throw new Error('useDiff must be used within an DiffProvider');
}
return context;
};

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useEffect, useState } from 'react';
import type { ExportImageContext, ImageType } from './export-image-context';
import { exportImageContext } from './export-image-context';
import { toJpeg, toPng, toSvg } from 'html-to-image';
@@ -6,6 +6,8 @@ import { useReactFlow } from '@xyflow/react';
import { useChartDB } from '@/hooks/use-chartdb';
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
import { useTheme } from '@/hooks/use-theme';
import logoDark from '@/assets/logo-dark.png';
import logoLight from '@/assets/logo-light.png';
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -14,6 +16,24 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
const { setNodes, getViewport } = useReactFlow();
const { effectiveTheme } = useTheme();
const { diagramName } = useChartDB();
const [logoBase64, setLogoBase64] = useState<string>('');
useEffect(() => {
// Convert logo to base64 on component mount
const img = new Image();
img.src = effectiveTheme === 'light' ? logoLight : logoDark;
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(img, 0, 0);
const base64 = canvas.toDataURL('image/png');
setLogoBase64(base64);
}
};
}, [effectiveTheme]);
const downloadImage = useCallback(
(dataUrl: string, type: ImageType) => {
@@ -128,16 +148,22 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
'http://www.w3.org/2000/svg',
'rect'
);
const padding = 2000;
backgroundRect.setAttribute('x', String(-viewport.x - padding));
backgroundRect.setAttribute('y', String(-viewport.y - padding));
const bgPadding = 2000;
backgroundRect.setAttribute(
'x',
String(-viewport.x - bgPadding)
);
backgroundRect.setAttribute(
'y',
String(-viewport.y - bgPadding)
);
backgroundRect.setAttribute(
'width',
String(reactFlowBounds.width + 2 * padding)
String(reactFlowBounds.width + 2 * bgPadding)
);
backgroundRect.setAttribute(
'height',
String(reactFlowBounds.height + 2 * padding)
String(reactFlowBounds.height + 2 * bgPadding)
);
backgroundRect.setAttribute('fill', 'url(#background-pattern)');
tempSvg.appendChild(backgroundRect);
@@ -148,28 +174,110 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
);
try {
const dataUrl = await imageCreateFn(viewportElement, {
...(type === 'jpeg' || type === 'png'
? {
backgroundColor:
effectiveTheme === 'light'
? '#ffffff'
: '#141414',
}
: {}),
width: reactFlowBounds.width,
height: reactFlowBounds.height,
style: {
width: `${reactFlowBounds.width}px`,
height: `${reactFlowBounds.height}px`,
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
},
quality: 1,
pixelRatio: scale,
skipFonts: true,
});
// Handle SVG export differently
if (type === 'svg') {
const dataUrl = await imageCreateFn(viewportElement, {
width: reactFlowBounds.width,
height: reactFlowBounds.height,
style: {
width: `${reactFlowBounds.width}px`,
height: `${reactFlowBounds.height}px`,
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
},
quality: 1,
pixelRatio: scale,
skipFonts: true,
});
downloadImage(dataUrl, type);
return;
}
downloadImage(dataUrl, type);
// For PNG and JPEG, continue with the watermark process
const initialDataUrl = await imageCreateFn(
viewportElement,
{
backgroundColor:
effectiveTheme === 'light'
? '#ffffff'
: '#141414',
width: reactFlowBounds.width,
height: reactFlowBounds.height,
style: {
width: `${reactFlowBounds.width}px`,
height: `${reactFlowBounds.height}px`,
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
},
quality: 1,
pixelRatio: scale,
skipFonts: true,
}
);
// Create a canvas to combine the diagram and watermark
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
downloadImage(initialDataUrl, type);
return;
}
// Set canvas size to match the export size
canvas.width = reactFlowBounds.width * scale;
canvas.height = reactFlowBounds.height * scale;
// Load the exported diagram
const diagramImage = new Image();
diagramImage.src = initialDataUrl;
await new Promise((resolve) => {
diagramImage.onload = async () => {
// Draw the diagram
ctx.drawImage(diagramImage, 0, 0);
// Calculate logo size
const logoHeight = Math.max(
24,
Math.floor(canvas.width * 0.024)
);
const padding = Math.max(
12,
Math.floor(logoHeight * 0.5)
);
// Load and draw the logo
const logoImage = new Image();
logoImage.src = logoBase64;
await new Promise((resolve) => {
logoImage.onload = () => {
// Calculate logo width while maintaining aspect ratio
const logoWidth =
(logoImage.width / logoImage.height) *
logoHeight;
// Draw logo in bottom-left corner
ctx.globalAlpha = 0.9;
ctx.drawImage(
logoImage,
padding,
canvas.height - logoHeight - padding,
logoWidth,
logoHeight
);
ctx.globalAlpha = 1;
resolve(null);
};
});
// Convert canvas to data URL
const finalDataUrl = canvas.toDataURL(
type === 'png' ? 'image/png' : 'image/jpeg'
);
downloadImage(finalDataUrl, type);
resolve(null);
};
});
} finally {
viewportElement.removeChild(tempSvg);
hideLoader();
@@ -184,6 +292,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
setNodes,
showLoader,
effectiveTheme,
logoBase64,
]
);

View File

@@ -39,7 +39,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
useHotkeys(
keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM]
.keyCombination,
openOpenDiagramDialog,
() => openOpenDiagramDialog(),
{
preventDefault: true,
},

View File

@@ -7,6 +7,7 @@ export enum KeyboardShortcutAction {
SAVE_DIAGRAM = 'save_diagram',
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
SHOW_ALL = 'show_all',
TOGGLE_THEME = 'toggle_theme',
}
export interface KeyboardShortcut {
@@ -63,6 +64,13 @@ export const keyboardShortcuts: Record<
keyCombinationMac: 'meta+0',
keyCombinationWin: 'ctrl+0',
},
[KeyboardShortcutAction.TOGGLE_THEME]: {
action: KeyboardShortcutAction.TOGGLE_THEME,
keyCombinationLabelMac: '⌘M',
keyCombinationLabelWin: 'Ctrl+M',
keyCombinationMac: 'meta+m',
keyCombinationWin: 'ctrl+m',
},
};
export interface KeyboardShortcutForOS {

View File

@@ -30,12 +30,6 @@ export interface LocalConfigContext {
starUsDialogLastOpen: number;
setStarUsDialogLastOpen: (lastOpen: number) => void;
buckleWaitlistOpened: boolean;
setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void;
buckleDialogLastOpen: number;
setBuckleDialogLastOpen: (lastOpen: number) => void;
showDependenciesOnCanvas: boolean;
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
@@ -53,7 +47,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
schemasFilter: {},
setSchemasFilter: emptyFn,
showCardinality: false,
showCardinality: true,
setShowCardinality: emptyFn,
hideMultiSchemaNotification: false,
@@ -65,12 +59,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
starUsDialogLastOpen: 0,
setStarUsDialogLastOpen: emptyFn,
buckleWaitlistOpened: false,
setBuckleWaitlistOpened: emptyFn,
buckleDialogLastOpen: 0,
setBuckleDialogLastOpen: emptyFn,
showDependenciesOnCanvas: false,
setShowDependenciesOnCanvas: emptyFn,

View File

@@ -10,8 +10,6 @@ const showCardinalityKey = 'show_cardinality';
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
const githubRepoOpenedKey = 'github_repo_opened';
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
const buckleWaitlistOpenedKey = 'buckle_waitlist_opened';
const buckleDialogLastOpenKey = 'buckle_dialog_last_open';
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
@@ -33,7 +31,7 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
);
const [showCardinality, setShowCardinality] = React.useState<boolean>(
(localStorage.getItem(showCardinalityKey) || 'false') === 'true'
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
);
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
@@ -51,17 +49,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
);
const [buckleWaitlistOpened, setBuckleWaitlistOpened] =
React.useState<boolean>(
(localStorage.getItem(buckleWaitlistOpenedKey) || 'false') ===
'true'
);
const [buckleDialogLastOpen, setBuckleDialogLastOpen] =
React.useState<number>(
parseInt(localStorage.getItem(buckleDialogLastOpenKey) || '0')
);
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
React.useState<boolean>(
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
@@ -84,20 +71,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
}, [githubRepoOpened]);
useEffect(() => {
localStorage.setItem(
buckleDialogLastOpenKey,
buckleDialogLastOpen.toString()
);
}, [buckleDialogLastOpen]);
useEffect(() => {
localStorage.setItem(
buckleWaitlistOpenedKey,
buckleWaitlistOpened.toString()
);
}, [buckleWaitlistOpened]);
useEffect(() => {
localStorage.setItem(
hideMultiSchemaNotificationKey,
@@ -154,10 +127,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
setStarUsDialogLastOpen,
showDependenciesOnCanvas,
setShowDependenciesOnCanvas,
setBuckleDialogLastOpen,
buckleDialogLastOpen,
buckleWaitlistOpened,
setBuckleWaitlistOpened,
showMiniMapOnCanvas,
setShowMiniMapOnCanvas,
}}

View File

@@ -1,8 +1,13 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import type { EffectiveTheme } from './theme-context';
import { ThemeContext } from './theme-context';
import { useMediaQuery } from 'react-responsive';
import { useLocalConfig } from '@/hooks/use-local-config';
import { useHotkeys } from 'react-hotkeys-hook';
import {
KeyboardShortcutAction,
keyboardShortcutsForOS,
} from '../keyboard-shortcuts-context/keyboard-shortcuts';
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -29,6 +34,24 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
}
}, [effectiveTheme]);
const handleThemeToggle = useCallback(() => {
if (theme === 'system') {
setTheme(effectiveTheme === 'dark' ? 'light' : 'dark');
} else {
setTheme(theme === 'dark' ? 'light' : 'dark');
}
}, [theme, effectiveTheme, setTheme]);
useHotkeys(
keyboardShortcutsForOS[KeyboardShortcutAction.TOGGLE_THEME]
.keyCombination,
handleThemeToggle,
{
preventDefault: true,
},
[handleThemeToggle]
);
return (
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
{children}

View File

@@ -1,80 +0,0 @@
import React, { useCallback, useEffect } from 'react';
import { useDialog } from '@/hooks/use-dialog';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/dialog/dialog';
import { Button } from '@/components/button/button';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useLocalConfig } from '@/hooks/use-local-config';
import { useTheme } from '@/hooks/use-theme';
export interface BuckleDialogProps extends BaseDialogProps {}
export const BuckleDialog: React.FC<BuckleDialogProps> = ({ dialog }) => {
const { setBuckleWaitlistOpened } = useLocalConfig();
const { effectiveTheme } = useTheme();
useEffect(() => {
if (!dialog.open) return;
}, [dialog.open]);
const { closeBuckleDialog } = useDialog();
const handleConfirm = useCallback(() => {
setBuckleWaitlistOpened(true);
window.open('https://waitlist.buckle.dev', '_blank');
}, [setBuckleWaitlistOpened]);
return (
<Dialog
{...dialog}
onOpenChange={(open) => {
if (!open) {
closeBuckleDialog();
}
}}
>
<DialogContent
className="flex flex-col"
showClose={false}
onInteractOutside={(e) => {
e.preventDefault();
}}
>
<DialogHeader>
<DialogTitle className="hidden" />
<DialogDescription className="hidden" />
</DialogHeader>
<div className="flex w-full flex-col items-center">
<img
src={
effectiveTheme === 'light'
? '/buckle-animated.gif'
: '/buckle.png'
}
className="h-16"
/>
<div className="mt-6 text-center text-base">
We've been working on something big -{' '}
<span className="font-semibold">Ready to explore?</span>
</div>
</div>
<DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild>
<Button variant="secondary">Not now</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={handleConfirm}>
Try ChartDB v2.0!
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -85,6 +85,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
const [isCheckingJson, setIsCheckingJson] = useState(false);
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
useEffect(() => {
const loadScripts = async () => {
const { importMetadataScripts } = await import(
@@ -127,6 +129,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const inputValue = e.target.value;
setScriptResult(inputValue);
// Automatically open SSMS info when input length is exactly 65535
if (inputValue.length === 65535) {
setShowSSMSInfoDialog(true);
}
},
[setScriptResult]
);
@@ -245,7 +252,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
{t('new_diagram_dialog.import_database.step_1')}
</div>
{databaseType === DatabaseType.SQL_SERVER && (
<SSMSInfo />
<SSMSInfo
open={showSSMSInfoDialog}
setOpen={setShowSSMSInfoDialog}
/>
)}
</div>
{databaseTypeToClientsMap[databaseType].length > 0 ? (
@@ -369,6 +379,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
showCheckJsonButton,
isCheckingJson,
handleCheckJson,
showSSMSInfoDialog,
setShowSSMSInfoDialog,
]);
const renderFooter = useCallback(() => {

View File

@@ -4,32 +4,55 @@ import {
HoverCardTrigger,
} from '@/components/hover-card/hover-card';
import { Label } from '@/components/label/label';
import { Info } from 'lucide-react';
import React from 'react';
import { Info, X } from 'lucide-react';
import React, { useCallback, useEffect, useMemo } from 'react';
import SSMSInstructions from '@/assets/ssms-instructions.png';
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
import { useTranslation } from 'react-i18next';
export interface SSMSInfoProps {}
export interface SSMSInfoProps {
open?: boolean;
setOpen?: (open: boolean) => void;
}
export const SSMSInfo = React.forwardRef<
React.ElementRef<typeof HoverCardTrigger>,
SSMSInfoProps
>((props, ref) => {
>(({ open: controlledOpen, setOpen: setControlledOpen }, ref) => {
const [open, setOpen] = React.useState(false);
const { t } = useTranslation();
useEffect(() => {
if (controlledOpen) {
setOpen(true);
}
}, [controlledOpen]);
const closeHandler = useCallback(() => {
setOpen(false);
setControlledOpen?.(false);
}, [setControlledOpen]);
const isOpen = useMemo(
() => open || controlledOpen,
[open, controlledOpen]
);
return (
<HoverCard
open={open}
open={isOpen}
onOpenChange={(isOpen) => {
if (controlledOpen) {
return;
}
setOpen(isOpen);
}}
>
<HoverCardTrigger ref={ref} {...props} asChild>
<HoverCardTrigger ref={ref} asChild>
<div
className="flex flex-row items-center gap-1 text-pink-600"
onClick={() => {
setOpen(!open);
setOpen?.(!open);
}}
>
<Info size={14} />
@@ -41,13 +64,21 @@ export const SSMSInfo = React.forwardRef<
</div>
</HoverCardTrigger>
<HoverCardContent className="w-80">
<div className="flex">
<div className="space-y-1">
<div className="flex flex-col">
<div className="flex items-start justify-between">
<h4 className="text-sm font-semibold">
{t(
'new_diagram_dialog.import_database.ssms_instructions.title'
)}
</h4>
<button
onClick={closeHandler}
className="text-muted-foreground hover:text-foreground"
>
<X size={16} />
</button>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground">
<span className="font-semibold">1. </span>
{t(

View File

@@ -28,7 +28,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
const [databaseType, setDatabaseType] = useState<DatabaseType>(
DatabaseType.GENERIC
);
const { closeCreateDiagramDialog } = useDialog();
const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog();
const { updateConfig } = useConfig();
const [scriptResult, setScriptResult] = useState('');
const [databaseEdition, setDatabaseEdition] = useState<
@@ -104,6 +104,10 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
await updateConfig({ defaultDiagramId: diagram.id });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
setTimeout(
() => openImportDBMLDialog({ withCreateEmptyDiagram: true }),
700
);
}, [
databaseType,
addDiagram,
@@ -112,6 +116,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
navigate,
updateConfig,
diagramNumber,
openImportDBMLDialog,
]);
return (

View File

@@ -22,13 +22,17 @@ import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types';
const ErrorMessageRelationshipFieldsNotSameType =
'Relationships can only be created between fields of the same type';
export interface CreateRelationshipDialogProps extends BaseDialogProps {}
export interface CreateRelationshipDialogProps extends BaseDialogProps {
sourceTableId?: string;
}
export const CreateRelationshipDialog: React.FC<
CreateRelationshipDialogProps
> = ({ dialog }) => {
> = ({ dialog, sourceTableId: preSelectedSourceTableId }) => {
const { closeCreateRelationshipDialog } = useDialog();
const [primaryTableId, setPrimaryTableId] = useState<string | undefined>();
const [primaryTableId, setPrimaryTableId] = useState<string | undefined>(
preSelectedSourceTableId
);
const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>();
const [referencedTableId, setReferencedTableId] = useState<
string | undefined
@@ -43,6 +47,9 @@ export const CreateRelationshipDialog: React.FC<
const [canCreateRelationship, setCanCreateRelationship] = useState(false);
const { fitView, setEdges } = useReactFlow();
const { databaseType } = useChartDB();
const [primaryFieldSelectOpen, setPrimaryFieldSelectOpen] = useState(false);
const [referencedTableSelectOpen, setReferencedTableSelectOpen] =
useState(false);
const tableOptions = useMemo(() => {
return tables.map(
@@ -89,8 +96,23 @@ export const CreateRelationshipDialog: React.FC<
setReferencedTableId(undefined);
setReferencedFieldId(undefined);
setErrorMessage('');
setPrimaryFieldSelectOpen(false);
setReferencedTableSelectOpen(false);
}, [dialog.open]);
useEffect(() => {
if (preSelectedSourceTableId) {
const table = getTable(preSelectedSourceTableId);
if (table) {
setPrimaryTableId(preSelectedSourceTableId);
}
setTimeout(() => {
setPrimaryFieldSelectOpen(true);
}, 100);
}
}, [preSelectedSourceTableId, getTable]);
useEffect(() => {
setCanCreateRelationship(false);
setErrorMessage('');
@@ -223,8 +245,14 @@ export const CreateRelationshipDialog: React.FC<
)}
value={primaryTableId}
onChange={(value) => {
setPrimaryTableId(value as string);
setPrimaryFieldId(undefined);
const newTableId = value as string;
setPrimaryTableId(newTableId);
if (
newTableId !==
preSelectedSourceTableId
) {
setPrimaryFieldId(undefined);
}
}}
emptyPlaceholder={t(
'create_relationship_dialog.no_tables_found'
@@ -253,6 +281,8 @@ export const CreateRelationshipDialog: React.FC<
'create_relationship_dialog.primary_field_placeholder'
)}
value={primaryFieldId}
open={primaryFieldSelectOpen}
onOpenChange={setPrimaryFieldSelectOpen}
onChange={(value) =>
setPrimaryFieldId(value as string)
}
@@ -283,6 +313,8 @@ export const CreateRelationshipDialog: React.FC<
'create_relationship_dialog.referenced_table_placeholder'
)}
value={referencedTableId}
open={referencedTableSelectOpen}
onOpenChange={setReferencedTableSelectOpen}
onChange={(value) => {
setReferencedTableId(value as string);
setReferencedFieldId(undefined);

View File

@@ -15,11 +15,10 @@ import { SelectBox } from '@/components/select-box/select-box';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next';
import { useChartDB } from '@/hooks/use-chartdb';
import { diagramToJSONOutput } from '@/lib/export-import-utils';
import { Spinner } from '@/components/spinner/spinner';
import { waitFor } from '@/lib/utils';
import { AlertCircle } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
import { useExportDiagram } from '@/hooks/use-export-diagram';
export interface ExportDiagramDialogProps extends BaseDialogProps {}
@@ -27,44 +26,27 @@ export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
dialog,
}) => {
const { t } = useTranslation();
const { diagramName, currentDiagram } = useChartDB();
const [isLoading, setIsLoading] = useState(false);
const { currentDiagram } = useChartDB();
const { closeExportDiagramDialog } = useDialog();
const [error, setError] = useState(false);
useEffect(() => {
if (!dialog.open) return;
setIsLoading(false);
setError(false);
}, [dialog.open]);
const downloadOutput = useCallback(
(dataUrl: string) => {
const a = document.createElement('a');
a.setAttribute('download', `ChartDB(${diagramName}).json`);
a.setAttribute('href', dataUrl);
a.click();
},
[diagramName]
);
const { exportDiagram, isExporting: isLoading } = useExportDiagram();
const handleExport = useCallback(async () => {
setIsLoading(true);
await waitFor(1000);
try {
const json = diagramToJSONOutput(currentDiagram);
const blob = new Blob([json], { type: 'application/json' });
const dataUrl = URL.createObjectURL(blob);
downloadOutput(dataUrl);
setIsLoading(false);
await exportDiagram({ diagram: currentDiagram });
closeExportDiagramDialog();
} catch (e) {
setError(true);
setIsLoading(false);
throw e;
}
}, [downloadOutput, currentDiagram, closeExportDiagramDialog]);
}, [exportDiagram, currentDiagram, closeExportDiagramDialog]);
const outputTypeOptions: SelectBoxOption[] = useMemo(
() =>

View File

@@ -20,10 +20,12 @@ import {
} from '@/lib/data/export-metadata/export-sql-script';
import { databaseTypeToLabelMap } from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type';
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
import { Annoyed, Sparkles } from 'lucide-react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
import type { Diagram } from '@/lib/domain/diagram';
export interface ExportSQLDialogProps extends BaseDialogProps {
targetDatabaseType: DatabaseType;
@@ -34,7 +36,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
targetDatabaseType,
}) => {
const { closeExportSQLDialog } = useDialog();
const { currentDiagram } = useChartDB();
const { currentDiagram, filteredSchemas } = useChartDB();
const { t } = useTranslation();
const [script, setScript] = React.useState<string>();
const [error, setError] = React.useState<boolean>(false);
@@ -43,17 +45,58 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
const abortControllerRef = useRef<AbortController | null>(null);
const exportSQLScript = useCallback(async () => {
const filteredDiagram: Diagram = {
...currentDiagram,
tables: currentDiagram.tables?.filter((table) =>
shouldShowTablesBySchemaFilter(table, filteredSchemas)
),
relationships: currentDiagram.relationships?.filter((rel) => {
const sourceTable = currentDiagram.tables?.find(
(t) => t.id === rel.sourceTableId
);
const targetTable = currentDiagram.tables?.find(
(t) => t.id === rel.targetTableId
);
return (
sourceTable &&
targetTable &&
shouldShowTablesBySchemaFilter(
sourceTable,
filteredSchemas
) &&
shouldShowTablesBySchemaFilter(targetTable, filteredSchemas)
);
}),
dependencies: currentDiagram.dependencies?.filter((dep) => {
const table = currentDiagram.tables?.find(
(t) => t.id === dep.tableId
);
const dependentTable = currentDiagram.tables?.find(
(t) => t.id === dep.dependentTableId
);
return (
table &&
dependentTable &&
shouldShowTablesBySchemaFilter(table, filteredSchemas) &&
shouldShowTablesBySchemaFilter(
dependentTable,
filteredSchemas
)
);
}),
};
if (targetDatabaseType === DatabaseType.GENERIC) {
return Promise.resolve(exportBaseSQL(currentDiagram));
return Promise.resolve(exportBaseSQL(filteredDiagram));
} else {
return exportSQL(currentDiagram, targetDatabaseType, {
return exportSQL(filteredDiagram, targetDatabaseType, {
stream: true,
onResultStream: (text) =>
setScript((prev) => (prev ? prev + text : text)),
signal: abortControllerRef.current?.signal,
});
}
}, [targetDatabaseType, currentDiagram]);
}, [targetDatabaseType, currentDiagram, filteredSchemas]);
useEffect(() => {
if (!dialog.open) {

View File

@@ -0,0 +1,410 @@
import React, {
useCallback,
useEffect,
useState,
Suspense,
useRef,
} from 'react';
import * as monaco from 'monaco-editor';
import { useDialog } from '@/hooks/use-dialog';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogInternalContent,
DialogTitle,
} from '@/components/dialog/dialog';
import { Button } from '@/components/button/button';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next';
import { Editor } from '@/components/code-snippet/code-snippet';
import { useTheme } from '@/hooks/use-theme';
import { AlertCircle } from 'lucide-react';
import { importDBMLToDiagram } from '@/lib/dbml-import';
import { useChartDB } from '@/hooks/use-chartdb';
import { Parser } from '@dbml/core';
import { useCanvas } from '@/hooks/use-canvas';
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
import { useToast } from '@/components/toast/use-toast';
import { Spinner } from '@/components/spinner/spinner';
import { debounce } from '@/lib/utils';
interface DBMLError {
message: string;
line: number;
column: number;
}
function parseDBMLError(error: unknown): DBMLError | null {
try {
if (typeof error === 'string') {
const parsed = JSON.parse(error);
if (parsed.diags?.[0]) {
const diag = parsed.diags[0];
return {
message: diag.message,
line: diag.location.start.line,
column: diag.location.start.column,
};
}
} else if (error && typeof error === 'object' && 'diags' in error) {
const parsed = error as {
diags: Array<{
message: string;
location: { start: { line: number; column: number } };
}>;
};
if (parsed.diags?.[0]) {
return {
message: parsed.diags[0].message,
line: parsed.diags[0].location.start.line,
column: parsed.diags[0].location.start.column,
};
}
}
} catch (e) {
console.error('Error parsing DBML error:', e);
}
return null;
}
export interface ImportDBMLDialogProps extends BaseDialogProps {
withCreateEmptyDiagram?: boolean;
}
export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({
dialog,
withCreateEmptyDiagram,
}) => {
const { t } = useTranslation();
const initialDBML = `// Use DBML to define your database structure
// Simple Blog System with Comments Example
Table users {
id integer [primary key]
name varchar
email varchar
}
Table posts {
id integer [primary key]
title varchar
content text
user_id integer
created_at timestamp
}
Table comments {
id integer [primary key]
content text
post_id integer
user_id integer
created_at timestamp
}
// Relationships
Ref: posts.user_id > users.id // Each post belongs to one user
Ref: comments.post_id > posts.id // Each comment belongs to one post
Ref: comments.user_id > users.id // Each comment is written by one user`;
const [dbmlContent, setDBMLContent] = useState<string>(initialDBML);
const { closeImportDBMLDialog } = useDialog();
const [errorMessage, setErrorMessage] = useState<string | undefined>();
const { effectiveTheme } = useTheme();
const { toast } = useToast();
const {
addTables,
addRelationships,
tables,
relationships,
removeTables,
removeRelationships,
} = useChartDB();
const { reorderTables } = useCanvas();
const [reorder, setReorder] = useState(false);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const decorationsCollection =
useRef<monaco.editor.IEditorDecorationsCollection>();
const handleEditorDidMount = (
editor: monaco.editor.IStandaloneCodeEditor
) => {
editorRef.current = editor;
decorationsCollection.current = editor.createDecorationsCollection();
};
useEffect(() => {
if (reorder) {
reorderTables({
updateHistory: false,
});
setReorder(false);
}
}, [reorder, reorderTables]);
const highlightErrorLine = useCallback((error: DBMLError) => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;
const decorations = [
{
range: new monaco.Range(
error.line,
1,
error.line,
model.getLineMaxColumn(error.line)
),
options: {
isWholeLine: true,
className: 'dbml-error-line',
glyphMarginClassName: 'dbml-error-glyph',
hoverMessage: { value: error.message },
overviewRuler: {
color: '#ff0000',
position: monaco.editor.OverviewRulerLane.Right,
darkColor: '#ff0000',
},
},
},
];
decorationsCollection.current?.set(decorations);
}, []);
const clearDecorations = useCallback(() => {
decorationsCollection.current?.clear();
}, []);
const validateDBML = useCallback(
async (content: string) => {
// Clear previous errors
setErrorMessage(undefined);
clearDecorations();
if (!content.trim()) return;
try {
const parser = new Parser();
parser.parse(content, 'dbml');
} catch (e) {
const parsedError = parseDBMLError(e);
if (parsedError) {
setErrorMessage(
t('import_dbml_dialog.error.description') +
` (1 error found - in line ${parsedError.line})`
);
highlightErrorLine(parsedError);
} else {
setErrorMessage(
e instanceof Error ? e.message : JSON.stringify(e)
);
}
}
},
[clearDecorations, highlightErrorLine, t]
);
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
// Set up debounced validation
useEffect(() => {
debouncedValidateRef.current = debounce((value: string) => {
validateDBML(value);
}, 500);
return () => {
debouncedValidateRef.current = null;
};
}, [validateDBML]);
// Trigger validation when content changes
useEffect(() => {
if (debouncedValidateRef.current) {
debouncedValidateRef.current(dbmlContent);
}
}, [dbmlContent]);
useEffect(() => {
if (!dialog.open) {
setErrorMessage(undefined);
clearDecorations();
setDBMLContent(initialDBML);
}
}, [dialog.open, initialDBML, clearDecorations]);
const handleImport = useCallback(async () => {
if (!dbmlContent.trim() || errorMessage) return;
try {
const importedDiagram = await importDBMLToDiagram(dbmlContent);
const tableIdsToRemove = tables
.filter((table) =>
importedDiagram.tables?.some(
(t) =>
t.name === table.name && t.schema === table.schema
)
)
.map((table) => table.id);
// Find relationships that need to be removed
const relationshipIdsToRemove = relationships
.filter((relationship) => {
const sourceTable = tables.find(
(table) => table.id === relationship.sourceTableId
);
const targetTable = tables.find(
(table) => table.id === relationship.targetTableId
);
if (!sourceTable || !targetTable) return true;
const replacementSourceTable = importedDiagram.tables?.find(
(table) =>
table.name === sourceTable.name &&
table.schema === sourceTable.schema
);
const replacementTargetTable = importedDiagram.tables?.find(
(table) =>
table.name === targetTable.name &&
table.schema === targetTable.schema
);
return replacementSourceTable || replacementTargetTable;
})
.map((relationship) => relationship.id);
// Remove existing items
await Promise.all([
removeTables(tableIdsToRemove, { updateHistory: false }),
removeRelationships(relationshipIdsToRemove, {
updateHistory: false,
}),
]);
// Add new items
await Promise.all([
addTables(importedDiagram.tables ?? [], {
updateHistory: false,
}),
addRelationships(importedDiagram.relationships ?? [], {
updateHistory: false,
}),
]);
setReorder(true);
closeImportDBMLDialog();
} catch (e) {
toast({
title: t('import_dbml_dialog.error.title'),
variant: 'destructive',
description: (
<>
<div>{t('import_dbml_dialog.error.description')}</div>
{e instanceof Error ? e.message : JSON.stringify(e)}
</>
),
});
}
}, [
dbmlContent,
closeImportDBMLDialog,
tables,
relationships,
removeTables,
removeRelationships,
addTables,
addRelationships,
errorMessage,
toast,
setReorder,
t,
]);
return (
<Dialog
{...dialog}
onOpenChange={(open) => {
if (!open) {
closeImportDBMLDialog();
}
}}
>
<DialogContent
className="flex h-[80vh] max-h-screen flex-col"
showClose
>
<DialogHeader>
<DialogTitle>
{withCreateEmptyDiagram
? t('import_dbml_dialog.example_title')
: t('import_dbml_dialog.title')}
</DialogTitle>
<DialogDescription>
{t('import_dbml_dialog.description')}
</DialogDescription>
</DialogHeader>
<DialogInternalContent>
<Suspense fallback={<Spinner />}>
<Editor
value={dbmlContent}
onChange={(value) => setDBMLContent(value || '')}
language="dbml"
onMount={handleEditorDidMount}
theme={
effectiveTheme === 'dark'
? 'dbml-dark'
: 'dbml-light'
}
beforeMount={setupDBMLLanguage}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
glyphMargin: true,
lineNumbers: 'on',
scrollbar: {
vertical: 'visible',
horizontal: 'visible',
},
}}
className="size-full"
/>
</Suspense>
</DialogInternalContent>
<DialogFooter>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-4">
<DialogClose asChild>
<Button variant="secondary">
{withCreateEmptyDiagram
? t('import_dbml_dialog.skip_and_empty')
: t('import_dbml_dialog.cancel')}
</Button>
</DialogClose>
{errorMessage ? (
<div className="flex items-center gap-1">
<AlertCircle className="size-4 text-destructive" />
<span className="text-xs text-destructive">
{errorMessage ||
t(
'import_dbml_dialog.error.description'
)}
</span>
</div>
) : null}
</div>
<Button
onClick={handleImport}
disabled={!dbmlContent.trim() || !!errorMessage}
>
{withCreateEmptyDiagram
? t('import_dbml_dialog.show_example')
: t('import_dbml_dialog.import')}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -22,15 +22,19 @@ import { useConfig } from '@/hooks/use-config';
import { useDialog } from '@/hooks/use-dialog';
import { useStorage } from '@/hooks/use-storage';
import type { Diagram } from '@/lib/domain/diagram';
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useDebounce } from '@/hooks/use-debounce';
export interface OpenDiagramDialogProps extends BaseDialogProps {}
export interface OpenDiagramDialogProps extends BaseDialogProps {
canClose?: boolean;
}
export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
dialog,
canClose = true,
}) => {
const { closeOpenDiagramDialog } = useDialog();
const { t } = useTranslation();
@@ -58,24 +62,77 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
fetchDiagrams();
}, [listDiagrams, setDiagrams, dialog.open]);
const openDiagram = (diagramId: string) => {
if (diagramId) {
updateConfig({ defaultDiagramId: diagramId });
navigate(`/diagrams/${diagramId}`);
}
};
const openDiagram = useCallback(
(diagramId: string) => {
if (diagramId) {
updateConfig({ defaultDiagramId: diagramId });
navigate(`/diagrams/${diagramId}`);
}
},
[updateConfig, navigate]
);
const handleRowKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTableRowElement>) => {
const element = e.target as HTMLElement;
const diagramId = element.getAttribute('data-diagram-id');
const selectionIndexAttr = element.getAttribute(
'data-selection-index'
);
if (!diagramId || !selectionIndexAttr) return;
const selectionIndex = parseInt(selectionIndexAttr, 10);
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
openDiagram(diagramId);
closeOpenDiagramDialog();
break;
case 'ArrowDown': {
e.preventDefault();
(
document.querySelector(
`[data-selection-index="${selectionIndex + 1}"]`
) as HTMLElement
)?.focus();
break;
}
case 'ArrowUp': {
e.preventDefault();
(
document.querySelector(
`[data-selection-index="${selectionIndex - 1}"]`
) as HTMLElement
)?.focus();
break;
}
}
},
[openDiagram, closeOpenDiagramDialog]
);
const onFocusHandler = useDebounce(
(diagramId: string) => setSelectedDiagramId(diagramId),
50
);
return (
<Dialog
{...dialog}
onOpenChange={(open) => {
if (!open) {
if (!open && canClose) {
closeOpenDiagramDialog();
}
}}
>
<DialogContent
className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]"
showClose
showClose={canClose}
>
<DialogHeader>
<DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle>
@@ -112,10 +169,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
</TableRow>
</TableHeader>
<TableBody>
{diagrams.map((diagram) => (
{diagrams.map((diagram, index) => (
<TableRow
key={diagram.id}
data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`}
data-diagram-id={diagram.id}
data-selection-index={index}
tabIndex={0}
onFocus={() =>
onFocusHandler(diagram.id)
}
className="focus:bg-accent focus:outline-none"
onClick={(e) => {
switch (e.detail) {
case 1:
@@ -133,6 +197,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
);
}
}}
onKeyDown={handleRowKeyDown}
>
<TableCell className="table-cell">
<div className="flex justify-center">
@@ -164,11 +229,15 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
</DialogInternalContent>
<DialogFooter className="flex !justify-between gap-2">
<DialogClose asChild>
<Button type="button" variant="secondary">
{t('open_diagram_dialog.cancel')}
</Button>
</DialogClose>
{canClose ? (
<DialogClose asChild>
<Button type="button" variant="secondary">
{t('open_diagram_dialog.cancel')}
</Button>
</DialogClose>
) : (
<div />
)}
<DialogClose asChild>
<Button
type="submit"

View File

@@ -109,6 +109,10 @@
animation: rainbow-text-simple-animation 0.5s ease-in forwards;
}
.dbml-error-line {
background-color: rgba(255, 0, 0, 0.2) !important;
}
@keyframes rainbow-text-simple-animation-rev {
0% {
background-size: 650%;

4
src/hooks/use-canvas.ts Normal file
View File

@@ -0,0 +1,4 @@
import { useContext } from 'react';
import { canvasContext } from '@/context/canvas-context/canvas-context';
export const useCanvas = () => useContext(canvasContext);

View File

@@ -0,0 +1,47 @@
import { useEffect, useRef, useCallback } from 'react';
import { debounce as utilsDebounce } from '@/lib/utils';
interface DebouncedFunction {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(...args: any[]): void;
cancel?: () => void;
}
/**
* A hook that returns a debounced version of the provided function.
* The debounced function will only be called after the specified delay
* has passed without the function being called again.
*
* @param callback The function to debounce
* @param delay The delay in milliseconds
* @returns A debounced version of the callback
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useDebounce<T extends (...args: any[]) => any>(
callback: T,
delay: number
): (...args: Parameters<T>) => void {
// Use a ref to store the debounced function
const debouncedFnRef = useRef<DebouncedFunction>();
// Update the debounced function when dependencies change
useEffect(() => {
// Create the debounced function
debouncedFnRef.current = utilsDebounce(callback, delay);
// Clean up when component unmounts or dependencies change
return () => {
if (debouncedFnRef.current?.cancel) {
debouncedFnRef.current.cancel();
}
};
}, [callback, delay]);
// Create a stable callback that uses the ref
const debouncedCallback = useCallback((...args: Parameters<T>) => {
debouncedFnRef.current?.(...args);
}, []);
return debouncedCallback;
}

21
src/hooks/use-debounce.ts Normal file
View File

@@ -0,0 +1,21 @@
import { useCallback, useRef } from 'react';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyFunction = (...args: any[]) => any;
export const useDebounce = <T extends AnyFunction>(
func: T,
delay: number
): ((...args: Parameters<T>) => void) => {
const inDebounce = useRef<NodeJS.Timeout>();
const debounce = useCallback(
(...args: Parameters<T>) => {
clearTimeout(inDebounce.current);
inDebounce.current = setTimeout(() => func(...args), delay);
},
[func, delay]
);
return debounce;
};

View File

@@ -0,0 +1,40 @@
import { useCallback, useState } from 'react';
import { useDialog } from '@/hooks/use-dialog';
import { diagramToJSONOutput } from '@/lib/export-import-utils';
import { waitFor } from '@/lib/utils';
import type { Diagram } from '@/lib/domain/diagram';
export const useExportDiagram = () => {
const [isLoading, setIsLoading] = useState(false);
const { closeExportDiagramDialog } = useDialog();
const downloadOutput = useCallback((name: string, dataUrl: string) => {
const a = document.createElement('a');
a.setAttribute('download', `ChartDB(${name}).json`);
a.setAttribute('href', dataUrl);
a.click();
}, []);
const handleExport = useCallback(
async ({ diagram }: { diagram: Diagram }) => {
setIsLoading(true);
await waitFor(1000);
try {
const json = diagramToJSONOutput(diagram);
const blob = new Blob([json], { type: 'application/json' });
const dataUrl = URL.createObjectURL(blob);
downloadOutput(diagram.name, dataUrl);
setIsLoading(false);
closeExportDiagramDialog();
} finally {
setIsLoading(false);
}
},
[downloadOutput, closeExportDiagramDialog]
);
return {
exportDiagram: handleExport,
isExporting: isLoading,
};
};

View File

@@ -8,7 +8,7 @@ export const ar: LanguageTranslation = {
new: 'جديد',
open: 'فتح',
save: 'حفظ',
import_database: 'استيراد قاعدة بيانات',
import: 'استيراد قاعدة بيانات',
export_sql: 'SQL تصدير',
export_as: 'تصدير كـ',
delete_diagram: 'حذف الرسم البياني',
@@ -34,13 +34,14 @@ export const ar: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
share: {
share: 'مشاركة',
backup: {
backup: 'النسخ الاحتياطي',
export_diagram: 'تصدير المخطط',
import_diagram: 'استيراد المخطط',
restore_diagram: 'استعادة المخطط',
},
help: {
help: 'مساعدة',
docs_website: 'الوثائق',
visit_website: 'ChartDB قم بزيارة',
join_discord: 'Discord انضم إلينا على',
schedule_a_call: '!تحدث معنا',
@@ -127,6 +128,9 @@ export const ar: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'الحقول',
@@ -147,6 +151,8 @@ export const ar: LanguageTranslation = {
comments: 'تعليقات',
no_comments: 'لا يوجد تعليقات',
delete_field: 'حذف الحقل',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'خصائص الفهرس',
@@ -361,7 +367,6 @@ export const ar: LanguageTranslation = {
'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
},
},
import_diagram_dialog: {
title: 'استيراد الرسم البياني',
description: ':للرسم البياني ادناه JSON قم بلصق',
@@ -373,6 +378,20 @@ export const ar: LanguageTranslation = {
'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
},
},
import_dbml_dialog: {
// TODO: Translate
title: 'Import DBML',
example_title: 'Import Example DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'واحد إلى واحد',
one_to_many: 'واحد إلى متعدد',
@@ -389,6 +408,7 @@ export const ar: LanguageTranslation = {
edit_table: 'تعديل الجدول',
duplicate_table: 'نسخ الجدول',
delete_table: 'حذف الجدول',
add_relationship: 'Add Relationship', // TODO: Translate
},
snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على',

View File

@@ -8,7 +8,7 @@ export const bn: LanguageTranslation = {
new: 'নতুন',
open: 'খুলুন',
save: 'সংরক্ষণ করুন',
import_database: 'ডাটাবেস আমদানি করুন',
import: 'ডাটাবেস আমদানি করুন',
export_sql: 'SQL রপ্তানি করুন',
export_as: 'রূপে রপ্তানি করুন',
delete_diagram: 'ডায়াগ্রাম মুছুন',
@@ -35,13 +35,14 @@ export const bn: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
share: {
share: 'শেয়ার করুন',
backup: {
backup: 'ব্যাকআপ',
export_diagram: 'ডায়াগ্রাম রপ্তানি করুন',
import_diagram: 'ডায়াগ্রাম আমদানি করুন',
restore_diagram: 'ডায়াগ্রাম পুনরুদ্ধার করুন',
},
help: {
help: 'সাহায্য',
docs_website: 'ডকুমেন্টেশন',
visit_website: 'ChartDB ওয়েবসাইটে যান',
join_discord: 'আমাদের Discord-এ যোগ দিন',
schedule_a_call: 'আমাদের সাথে কথা বলুন!',
@@ -128,6 +129,9 @@ export const bn: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'ফিল্ড',
@@ -148,6 +152,8 @@ export const bn: LanguageTranslation = {
comments: 'মন্তব্য',
no_comments: 'কোনো মন্তব্য নেই',
delete_field: 'ফিল্ড মুছুন',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ইনডেক্স কর্ম',
@@ -376,6 +382,20 @@ export const bn: LanguageTranslation = {
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'এক থেকে এক',
one_to_many: 'এক থেকে অনেক',
@@ -392,6 +412,7 @@ export const bn: LanguageTranslation = {
edit_table: 'টেবিল সম্পাদনা করুন',
duplicate_table: 'টেবিল নকল করুন',
delete_table: 'টেবিল মুছে ফেলুন',
add_relationship: 'Add Relationship', // TODO: Translate
},
snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})',

View File

@@ -8,7 +8,7 @@ export const de: LanguageTranslation = {
new: 'Neu',
open: 'Öffnen',
save: 'Speichern',
import_database: 'Datenbank importieren',
import: 'Datenbank importieren',
export_sql: 'SQL exportieren',
export_as: 'Exportieren als',
delete_diagram: 'Diagramm löschen',
@@ -35,13 +35,14 @@ export const de: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
share: {
share: 'Share',
backup: {
backup: 'Backup',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
restore_diagram: 'Restore Diagram',
},
help: {
help: 'Hilfe',
docs_website: 'Dokumentation',
visit_website: 'ChartDB Webseite',
join_discord: 'Auf Discord beitreten',
schedule_a_call: 'Gespräch vereinbaren',
@@ -129,6 +130,9 @@ export const de: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'Felder',
@@ -149,6 +153,8 @@ export const de: LanguageTranslation = {
comments: 'Kommentare',
no_comments: 'Keine Kommentare',
delete_field: 'Feld löschen',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Indexattribute',
@@ -379,6 +385,20 @@ export const de: LanguageTranslation = {
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'Ein zu Eins (1:1)',
one_to_many: 'Ein zu Viele (1:n)',
@@ -395,6 +415,7 @@ export const de: LanguageTranslation = {
edit_table: 'Tabelle bearbeiten',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Tabelle löschen',
add_relationship: 'Add Relationship', // TODO: Translate
},
// TODO: Add translations

View File

@@ -8,7 +8,7 @@ export const en = {
new: 'New',
open: 'Open',
save: 'Save',
import_database: 'Import Database',
import: 'Import',
export_sql: 'Export SQL',
export_as: 'Export as',
delete_diagram: 'Delete Diagram',
@@ -33,13 +33,14 @@ export const en = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
share: {
share: 'Share',
backup: {
backup: 'Backup',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
restore_diagram: 'Restore Diagram',
},
help: {
help: 'Help',
docs_website: 'Docs',
visit_website: 'Visit ChartDB',
join_discord: 'Join us on Discord',
schedule_a_call: 'Talk with us!',
@@ -125,6 +126,8 @@ export const en = {
collapse: 'Collapse All',
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'Fields',
@@ -142,6 +145,7 @@ export const en = {
field_actions: {
title: 'Field Attributes',
unique: 'Unique',
character_length: 'Max Length',
comments: 'Comments',
no_comments: 'No comments',
delete_field: 'Delete Field',
@@ -363,7 +367,7 @@ export const en = {
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
description: 'Import a diagram from a JSON file.',
cancel: 'Cancel',
import: 'Import',
error: {
@@ -372,6 +376,20 @@ export const en = {
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error importing DBML',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'One to One',
one_to_many: 'One to Many',
@@ -388,6 +406,7 @@ export const en = {
edit_table: 'Edit Table',
duplicate_table: 'Duplicate Table',
delete_table: 'Delete Table',
add_relationship: 'Add Relationship',
},
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',

View File

@@ -8,7 +8,7 @@ export const es: LanguageTranslation = {
new: 'Nuevo',
open: 'Abrir',
save: 'Guardar',
import_database: 'Importar Base de Datos',
import: 'Importar Base de Datos',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Eliminar Diagrama',
@@ -34,14 +34,14 @@ export const es: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
backup: {
backup: 'Respaldo',
export_diagram: 'Exportar Diagrama',
restore_diagram: 'Restaurar Diagrama',
},
help: {
help: 'Ayuda',
docs_website: 'Documentación',
visit_website: 'Visitar ChartDB',
join_discord: 'Únete a nosotros en Discord',
schedule_a_call: '¡Habla con nosotros!',
@@ -119,6 +119,9 @@ export const es: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'Campos',
@@ -139,6 +142,8 @@ export const es: LanguageTranslation = {
comments: 'Comentarios',
no_comments: 'Sin comentarios',
delete_field: 'Eliminar Campo',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Atributos del Índice',
@@ -378,6 +383,20 @@ export const es: LanguageTranslation = {
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'Uno a Uno',
one_to_many: 'Uno a Muchos',
@@ -394,6 +413,7 @@ export const es: LanguageTranslation = {
edit_table: 'Editar Tabla',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Eliminar Tabla',
add_relationship: 'Add Relationship', // TODO: Translate
},
// TODO: Add translations

View File

@@ -8,7 +8,7 @@ export const fr: LanguageTranslation = {
new: 'Nouveau',
open: 'Ouvrir',
save: 'Enregistrer',
import_database: 'Importer Base de Données',
import: 'Importer Base de Données',
export_sql: 'Exporter SQL',
export_as: 'Exporter en tant que',
delete_diagram: 'Supprimer le Diagramme',
@@ -30,17 +30,17 @@ export const fr: LanguageTranslation = {
theme: 'Thème',
show_dependencies: 'Afficher les Dépendances',
hide_dependencies: 'Masquer les Dépendances',
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
show_minimap: 'Afficher la Mini Carte',
hide_minimap: 'Masquer la Mini Carte',
},
share: {
share: 'Partage',
backup: {
backup: 'Sauvegarde',
export_diagram: 'Exporter le diagramme',
import_diagram: 'Importer un diagramme',
restore_diagram: 'Restaurer le diagramme',
},
help: {
help: 'Aide',
docs_website: 'Documentation',
visit_website: 'Visitez ChartDB',
join_discord: 'Rejoignez-nous sur Discord',
schedule_a_call: 'Parlez avec nous !',
@@ -101,9 +101,8 @@ export const fr: LanguageTranslation = {
clear: 'Effacer',
show_more: 'Afficher Plus',
show_less: 'Afficher Moins',
// TODO: Translate
copy_to_clipboard: 'Copy to Clipboard',
copied: 'Copied!',
copy_to_clipboard: 'Copier dans le presse-papiers',
copied: 'Copié !',
side_panel: {
schema: 'Schéma:',
@@ -116,9 +115,11 @@ export const fr: LanguageTranslation = {
add_table: 'Ajouter une Table',
filter: 'Filtrer',
collapse: 'Réduire Tout',
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
clear: 'Effacer le Filtre',
no_results:
'Aucune table trouvée correspondant à votre filtre.',
show_list: 'Afficher la Liste des Tableaux',
show_dbml: "Afficher l'éditeur DBML",
table: {
fields: 'Champs',
@@ -139,6 +140,8 @@ export const fr: LanguageTranslation = {
comments: 'Commentaires',
no_comments: 'Pas de commentaires',
delete_field: 'Supprimer le Champ',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: "Attributs de l'Index",
@@ -150,7 +153,7 @@ export const fr: LanguageTranslation = {
title: 'Actions de la Table',
add_field: 'Ajouter un Champ',
add_index: 'Ajouter un Index',
duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Tableau dupliqué',
delete_table: 'Supprimer la Table',
change_schema: 'Changer le Schéma',
},
@@ -233,14 +236,12 @@ export const fr: LanguageTranslation = {
step_2: 'Si vous utilisez "Résultats en Grille", changez le nombre maximum de caractères récupérés pour les données non-XML (définir à 9999999).',
},
instructions_link: "Besoin d'aide ? Regardez comment",
// TODO: Translate
check_script_result: 'Check Script Result',
check_script_result: 'Vérifier le résultat du Script',
},
cancel: 'Annuler',
back: 'Retour',
// TODO: Translate
import_from_file: 'Import from File',
import_from_file: "Importer à partir d'un fichier",
empty_diagram: 'Diagramme vide',
continue: 'Continuer',
import: 'Importer',
@@ -355,29 +356,42 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler',
},
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
title: 'Exporter le Diagramme',
description: "Sélectionner le format d'exportation :",
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
cancel: 'Annuler',
export: 'Exporter',
error: {
title: 'Error exporting diagram',
title: "Erreur lors de l'exportation du diagramme",
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
"Une erreur s'est produite. Besoin d'aide ? chartdb.io@gmail.com",
},
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
title: 'Importer un diagramme',
description: 'Coller le diagramme au format JSON ci-dessous :',
cancel: 'Annuler',
import: 'Exporter',
error: {
title: 'Error importing diagram',
title: "Erreur lors de l'exportation du diagramme",
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
"Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? chartdb.io@gmail.com",
},
},
import_dbml_dialog: {
example_title: "Exemple d'importation DBML",
title: 'Import DBML',
description:
'Importer un schéma de base de données à partir du format DBML.',
import: 'Importer',
cancel: 'Annuler',
skip_and_empty: 'Passer et vider',
show_example: 'Afficher un exemple',
error: {
title: 'Erreur',
description:
"Erreur d'analyse du DBML. Veuillez vérifier la syntaxe.",
},
},
relationship_type: {
@@ -394,12 +408,13 @@ export const fr: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'Éditer la Table',
duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Tableau Dupliqué',
delete_table: 'Supprimer la Table',
add_relationship: 'Ajouter une Relation',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
snap_to_grid_tooltip:
'Aligner sur la grille (maintenir la touche {{key}})',
tool_tips: {
double_click_to_edit: 'Double-cliquez pour modifier',

View File

@@ -8,7 +8,7 @@ export const gu: LanguageTranslation = {
new: 'નવું',
open: 'ખોલો',
save: 'સાચવો',
import_database: 'ડેટાબેસ આયાત કરો',
import: 'ડેટાબેસ આયાત કરો',
export_sql: 'SQL નિકાસ કરો',
export_as: 'રૂપે નિકાસ કરો',
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
@@ -35,13 +35,14 @@ export const gu: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
share: {
share: 'શેર કરો',
backup: {
backup: 'બેકઅપ',
export_diagram: 'ડાયાગ્રામ નિકાસ કરો',
import_diagram: 'ડાયાગ્રામ આયાત કરો',
restore_diagram: 'ડાયાગ્રામ પુનઃસ્થાપિત કરો',
},
help: {
help: 'મદદ',
docs_website: 'દસ્તાવેજીકરણ',
visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
join_discord: 'અમારા Discordમાં જોડાઓ',
schedule_a_call: 'અમારી સાથે વાત કરો!',
@@ -128,6 +129,9 @@ export const gu: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'ફીલ્ડ્સ',
@@ -149,6 +153,8 @@ export const gu: LanguageTranslation = {
comments: 'ટિપ્પણીઓ',
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
delete_field: 'ફીલ્ડ કાઢી નાખો',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ઇન્ડેક્સ લક્ષણો',
@@ -376,6 +382,20 @@ export const gu: LanguageTranslation = {
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'એકથી એક',
one_to_many: 'એકથી ઘણા',
@@ -392,6 +412,7 @@ export const gu: LanguageTranslation = {
edit_table: 'ટેબલ સંપાદિત કરો',
duplicate_table: 'ટેબલ નકલ કરો',
delete_table: 'ટેબલ કાઢી નાખો',
add_relationship: 'Add Relationship', // TODO: Translate
},
snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})',

View File

@@ -8,7 +8,7 @@ export const hi: LanguageTranslation = {
new: 'नया',
open: 'खोलें',
save: 'सहेजें',
import_database: 'डेटाबेस आयात करें',
import: 'डेटाबेस आयात करें',
export_sql: 'SQL निर्यात करें',
export_as: 'के रूप में निर्यात करें',
delete_diagram: 'आरेख हटाएँ',
@@ -34,14 +34,14 @@ export const hi: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
backup: {
backup: 'बैकअप',
export_diagram: 'आरेख निर्यात करें',
restore_diagram: 'आरेख पुनर्स्थापित करें',
},
help: {
help: 'मदद',
docs_website: 'દસ્તાવેજીકરણ',
visit_website: 'ChartDB वेबसाइट पर जाएँ',
join_discord: 'हमसे Discord पर जुड़ें',
schedule_a_call: 'हमसे बात करें!',
@@ -129,6 +129,9 @@ export const hi: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'फ़ील्ड्स',
@@ -149,6 +152,8 @@ export const hi: LanguageTranslation = {
comments: 'टिप्पणियाँ',
no_comments: 'कोई टिप्पणी नहीं',
delete_field: 'फ़ील्ड हटाएँ',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'सूचकांक विशेषताएँ',
@@ -380,6 +385,20 @@ export const hi: LanguageTranslation = {
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'एक से एक',
one_to_many: 'एक से कई',
@@ -396,6 +415,7 @@ export const hi: LanguageTranslation = {
edit_table: 'तालिका संपादित करें',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'तालिका हटाएँ',
add_relationship: 'Add Relationship', // TODO: Translate
},
// TODO: Add translations

View File

@@ -8,7 +8,7 @@ export const id_ID: LanguageTranslation = {
new: 'Buat Baru',
open: 'Buka',
save: 'Simpan',
import_database: 'Impor Database',
import: 'Impor Database',
export_sql: 'Ekspor SQL',
export_as: 'Ekspor Sebagai',
delete_diagram: 'Hapus Diagram',
@@ -34,13 +34,14 @@ export const id_ID: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
share: {
share: 'Bagikan',
backup: {
backup: 'Cadangan',
export_diagram: 'Ekspor Diagram',
import_diagram: 'Impor Diagram',
restore_diagram: 'Pulihkan Diagram',
},
help: {
help: 'Bantuan',
docs_website: 'દસ્તાવેજીકરણ',
visit_website: 'Kunjungi ChartDB',
join_discord: 'Bergabunglah di Discord kami',
schedule_a_call: 'Berbicara dengan kami!',
@@ -127,6 +128,9 @@ export const id_ID: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'Kolom',
@@ -147,6 +151,8 @@ export const id_ID: LanguageTranslation = {
comments: 'Komentar',
no_comments: 'Tidak ada komentar',
delete_field: 'Hapus Kolom',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Atribut Indeks',
@@ -374,6 +380,20 @@ export const id_ID: LanguageTranslation = {
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'Satu ke Satu',
@@ -391,6 +411,7 @@ export const id_ID: LanguageTranslation = {
edit_table: 'Ubah Tabel',
delete_table: 'Hapus Tabel',
duplicate_table: 'Duplikat Tabel',
add_relationship: 'Add Relationship', // TODO: Translate
},
snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})',

View File

@@ -8,7 +8,7 @@ export const ja: LanguageTranslation = {
new: '新規',
open: '開く',
save: '保存',
import_database: 'データベースをインポート',
import: 'データベースをインポート',
export_sql: 'SQLをエクスポート',
export_as: '形式を指定してエクスポート',
delete_diagram: 'ダイアグラムを削除',
@@ -36,13 +36,14 @@ export const ja: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
share: {
share: 'Share',
backup: {
backup: 'Backup',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
restore_diagram: 'Restore Diagram',
},
help: {
help: 'ヘルプ',
docs_website: 'ドキュメント',
visit_website: 'ChartDBにアクセス',
join_discord: 'Discordに参加',
schedule_a_call: '話しかけてください!',
@@ -131,6 +132,9 @@ export const ja: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'フィールド',
@@ -151,6 +155,8 @@ export const ja: LanguageTranslation = {
comments: 'コメント',
no_comments: 'コメントがありません',
delete_field: 'フィールドを削除',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'インデックス属性',
@@ -383,6 +389,20 @@ export const ja: LanguageTranslation = {
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: '1対1',
one_to_many: '1対多',
@@ -399,6 +419,7 @@ export const ja: LanguageTranslation = {
edit_table: 'テーブルを編集',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'テーブルを削除',
add_relationship: 'Add Relationship', // TODO: Translate
},
// TODO: Add translations

View File

@@ -8,7 +8,7 @@ export const ko_KR: LanguageTranslation = {
new: '새 다이어그램',
open: '열기',
save: '저장',
import_database: '데이터베이스 가져오기',
import: '데이터베이스 가져오기',
export_sql: 'SQL로 저장',
export_as: '다른 형식으로 저장',
delete_diagram: '다이어그램 삭제',
@@ -34,13 +34,14 @@ export const ko_KR: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
share: {
share: '공유',
backup: {
backup: '백업',
export_diagram: '다이어그램 내보내기',
import_diagram: '다이어그램 가져오기',
restore_diagram: '다이어그램 복구',
},
help: {
help: '도움말',
docs_website: '선적 서류 비치',
visit_website: 'ChartDB 사이트 방문',
join_discord: 'Discord 가입',
schedule_a_call: 'Talk with us!',
@@ -127,6 +128,9 @@ export const ko_KR: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: '필드',
@@ -147,6 +151,8 @@ export const ko_KR: LanguageTranslation = {
comments: '주석',
no_comments: '주석 없음',
delete_field: '필드 삭제',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '인덱스 속성',
@@ -372,6 +378,20 @@ export const ko_KR: LanguageTranslation = {
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: '일대일 (1:1)',
one_to_many: '일대다 (1:N)',
@@ -388,6 +408,7 @@ export const ko_KR: LanguageTranslation = {
edit_table: '테이블 수정',
duplicate_table: '테이블 복제',
delete_table: '테이블 삭제',
add_relationship: 'Add Relationship', // TODO: Translate
},
snap_to_grid_tooltip: '그리드에 맞추기 ({{key}}를 누른채 유지)',

View File

@@ -8,7 +8,7 @@ export const mr: LanguageTranslation = {
new: 'नवीन',
open: 'उघडा',
save: 'जतन करा',
import_database: 'डेटाबेस इम्पोर्ट करा',
import: 'डेटाबेस इम्पोर्ट करा',
export_sql: 'SQL एक्स्पोर्ट करा',
export_as: 'म्हणून एक्स्पोर्ट करा',
delete_diagram: 'आरेख हटवा',
@@ -34,14 +34,15 @@ export const mr: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
share: {
backup: {
// TODO: Add translations
share: 'Share',
backup: 'Backup',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
restore_diagram: 'Restore Diagram',
},
help: {
help: 'मदत',
docs_website: 'दस्तऐवजीकरण',
visit_website: 'ChartDB ला भेट द्या',
join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
schedule_a_call: 'आमच्याशी बोला!',
@@ -130,6 +131,9 @@ export const mr: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'फील्ड्स',
@@ -150,6 +154,8 @@ export const mr: LanguageTranslation = {
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'इंडेक्स गुणधर्म',
@@ -384,6 +390,20 @@ export const mr: LanguageTranslation = {
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'एक ते एक',
@@ -400,8 +420,8 @@ export const mr: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'टेबल संपादित करा',
delete_table: 'टेबल हटवा',
// TODO: Add translations
duplicate_table: 'Duplicate Table',
duplicate_table: 'Duplicate Table', // TODO: Translate
add_relationship: 'Add Relationship', // TODO: Translate
},
// TODO: Add translations

View File

@@ -8,7 +8,7 @@ export const ne: LanguageTranslation = {
new: 'नयाँ',
open: 'खोल्नुहोस्',
save: 'सुरक्षित गर्नुहोस्',
import_database: 'डाटाबेस आयात गर्नुहोस्',
import: 'डाटाबेस आयात गर्नुहोस्',
export_sql: 'SQL निर्यात गर्नुहोस्',
export_as: 'निर्यात गर्नुहोस्',
delete_diagram: 'डायाग्राम हटाउनुहोस्',
@@ -34,13 +34,15 @@ export const ne: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
share: {
share: 'शेयर गर्नुहोस्',
export_diagram: 'डायाग्राम निर्यात गर्नुहोस्',
import_diagram: 'डायाग्राम आयात गर्नुहोस्',
// TODO: Translate
backup: {
backup: 'Backup',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
},
help: {
help: 'मद्दत',
docs_website: 'कागजात',
visit_website: 'वेबसाइटमा जानुहोस्',
join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
schedule_a_call: 'कल अनुसूची गर्नुहोस्',
@@ -127,6 +129,9 @@ export const ne: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'क्षेत्रहरू',
@@ -147,6 +152,8 @@ export const ne: LanguageTranslation = {
comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्',
delete_field: 'क्षेत्र हटाउनुहोस्',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'सूचक विशेषताहरू',
@@ -377,6 +384,20 @@ export const ne: LanguageTranslation = {
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'एक देखि एक',
@@ -394,6 +415,7 @@ export const ne: LanguageTranslation = {
edit_table: 'तालिका सम्पादन गर्नुहोस्',
duplicate_table: 'तालिका नक्कली गर्नुहोस्',
delete_table: 'तालिका हटाउनुहोस्',
add_relationship: 'Add Relationship', // TODO: Translate
},
snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)',

View File

@@ -8,7 +8,7 @@ export const pt_BR: LanguageTranslation = {
new: 'Novo',
open: 'Abrir',
save: 'Salvar',
import_database: 'Importar Banco de Dados',
import: 'Importar Banco de Dados',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Excluir Diagrama',
@@ -35,13 +35,14 @@ export const pt_BR: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
backup: {
backup: 'Backup',
export_diagram: 'Exportar Diagrama',
restore_diagram: 'Restaurar Diagrama',
},
help: {
help: 'Ajuda',
docs_website: 'Documentação',
visit_website: 'Visitar ChartDB',
join_discord: 'Junte-se a nós no Discord',
schedule_a_call: 'Fale Conosco!',
@@ -128,6 +129,9 @@ export const pt_BR: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'Campos',
@@ -148,6 +152,8 @@ export const pt_BR: LanguageTranslation = {
comments: 'Comentários',
no_comments: 'Sem comentários',
delete_field: 'Excluir Campo',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Atributos do Índice',
@@ -377,6 +383,20 @@ export const pt_BR: LanguageTranslation = {
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'Um para Um',
one_to_many: 'Um para Muitos',
@@ -393,6 +413,7 @@ export const pt_BR: LanguageTranslation = {
edit_table: 'Editar Tabela',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Excluir Tabela',
add_relationship: 'Add Relationship', // TODO: Translate
},
// TODO: Add translations

View File

@@ -8,7 +8,7 @@ export const ru: LanguageTranslation = {
new: 'Создать',
open: 'Открыть',
save: 'Сохранить',
import_database: 'Импортировать базу данных',
import: 'Импортировать базу данных',
export_sql: 'Экспорт SQL',
export_as: 'Экспортировать как',
delete_diagram: 'Удалить диаграмму',
@@ -34,13 +34,15 @@ export const ru: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
share: {
share: 'Поделиться',
export_diagram: 'Экспорт кода диаграммы',
import_diagram: 'Импорт кода диаграммы',
// TODO: Translate
backup: {
backup: 'Backup',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
},
help: {
help: 'Помощь',
docs_website: 'Документация',
visit_website: 'Перейти на сайт ChartDB',
join_discord: 'Присоединиться к сообществу в Discord',
schedule_a_call: 'Поговорите с нами!',
@@ -126,6 +128,9 @@ export const ru: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'Поля',
@@ -146,6 +151,8 @@ export const ru: LanguageTranslation = {
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Атрибуты индекса',
@@ -373,6 +380,20 @@ export const ru: LanguageTranslation = {
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'Один к одному',
one_to_many: 'Один ко многим',
@@ -389,6 +410,7 @@ export const ru: LanguageTranslation = {
edit_table: 'Изменить таблицу',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Удалить таблицу',
add_relationship: 'Add Relationship', // TODO: Translate
},
copy_to_clipboard: 'Скопировать в буфер обмена',

View File

@@ -8,7 +8,7 @@ export const te: LanguageTranslation = {
new: 'కొత్తది',
open: 'తెరవు',
save: 'సేవ్',
import_database: 'డేటాబేస్‌ను దిగుమతి చేసుకోండి',
import: 'డేటాబేస్‌ను దిగుమతి చేసుకోండి',
export_sql: 'SQL ఎగుమతి',
export_as: 'వగా ఎగుమతి చేయండి',
delete_diagram: 'చిత్రాన్ని తొలగించండి',
@@ -35,13 +35,14 @@ export const te: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
share: {
share: 'Share',
backup: {
backup: 'Backup',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
restore_diagram: 'Restore Diagram',
},
help: {
help: 'సహాయం',
docs_website: 'డాక్యుమెంటేషన్',
visit_website: 'ChartDB సందర్శించండి',
join_discord: 'డిస్కార్డ్‌లో మా నుంచి చేరండి',
schedule_a_call: 'మాతో మాట్లాడండి!',
@@ -128,6 +129,9 @@ export const te: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'ఫీల్డులు',
@@ -148,6 +152,8 @@ export const te: LanguageTranslation = {
comments: 'వ్యాఖ్యలు',
no_comments: 'వ్యాఖ్యలు లేవు',
delete_field: 'ఫీల్డ్ తొలగించు',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ఇండెక్స్ గుణాలు',
@@ -380,6 +386,20 @@ export const te: LanguageTranslation = {
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'ఒకటి_కీ_ఒకటి',
@@ -395,9 +415,9 @@ export const te: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'పట్టికను సవరించు',
// TODO: Translate
duplicate_table: 'Duplicate Table',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'పట్టికను తొలగించు',
add_relationship: 'Add Relationship', // TODO: Translate
},
// TODO: Translate

View File

@@ -8,7 +8,7 @@ export const tr: LanguageTranslation = {
new: 'Yeni',
open: 'Aç',
save: 'Kaydet',
import_database: 'Veritabanı İçe Aktar',
import: 'Veritabanı İçe Aktar',
export_sql: 'SQL Olarak Dışa Aktar',
export_as: 'Olarak Dışa Aktar',
delete_diagram: 'Diyagramı Sil',
@@ -35,13 +35,14 @@ export const tr: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
share: {
share: 'Share',
backup: {
backup: 'Backup',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
restore_diagram: 'Restore Diagram',
},
help: {
help: 'Yardım',
docs_website: 'Belgeleme',
visit_website: "ChartDB'yi Ziyaret Et",
join_discord: "Discord'a Katıl",
schedule_a_call: 'Bize Ulaş!',
@@ -127,6 +128,9 @@ export const tr: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'Alanlar',
@@ -147,6 +151,8 @@ export const tr: LanguageTranslation = {
comments: 'Yorumlar',
no_comments: 'Yorum yok',
delete_field: 'Alanı Sil',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'İndeks Özellikleri',
@@ -367,6 +373,20 @@ export const tr: LanguageTranslation = {
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'Bir Bir',
one_to_many: 'Bir Çok',
@@ -380,8 +400,8 @@ export const tr: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'Tabloyu Düzenle',
delete_table: 'Tabloyu Sil',
// TODO: Translate
duplicate_table: 'Duplicate Table',
duplicate_table: 'Duplicate Table', // TODO: Translate
add_relationship: 'Add Relationship', // TODO: Translate
},
// TODO: Translate

View File

@@ -4,47 +4,46 @@ export const uk: LanguageTranslation = {
translation: {
menu: {
file: {
file: 'файл',
new: 'новий',
open: 'відкрити',
save: 'зберегти',
import_database: 'Імпорт бази даних',
file: 'Файл',
new: 'Новий',
open: 'Відкрити',
save: 'Зберегти',
import: 'Імпорт бази даних',
export_sql: 'Експорт SQL',
export_as: 'Експортувати як',
delete_diagram: 'Видалити діаграму',
exit: 'вийти',
exit: 'Вийти',
},
edit: {
edit: 'редагувати',
edit: 'Редагувати',
undo: 'Скасувати',
redo: 'Повторити',
clear: 'очистити',
clear: 'Очистити',
},
view: {
view: 'переглянути',
view: 'Перегляд',
show_sidebar: 'Показати бічну панель',
hide_sidebar: 'Приховати бічну панель',
hide_cardinality: 'Приховати потужність',
show_cardinality: 'Показати кардинальність',
zoom_on_scroll: 'Збільшити прокручування',
zoom_on_scroll: 'Масштабувати прокручуванням',
theme: 'Тема',
show_dependencies: 'Показати залежності',
hide_dependencies: 'Приховати залежності',
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
show_minimap: 'Показати мінімапу',
hide_minimap: 'Приховати мінімапу',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
backup: {
backup: 'Резервне копіювання',
export_diagram: 'Експорт діаграми',
restore_diagram: 'Відновити діаграму',
},
help: {
help: 'Допомога',
visit_website: 'Відвідайте ChartDB',
help: 'Довідка',
docs_website: 'Документація',
visit_website: 'Сайт ChartDB',
join_discord: 'Приєднуйтесь до нас в Діскорд',
schedule_a_call: 'Поговоріть з нами!',
schedule_a_call: 'Забронювати зустріч!',
},
},
@@ -57,18 +56,18 @@ export const uk: LanguageTranslation = {
},
clear_diagram_alert: {
title: 'Чітка діаграма',
title: 'Очистити діаграму',
description:
'Цю дію не можна скасувати. Це назавжди видалить усі дані на діаграмі.',
cancel: 'Скасувати',
clear: 'очистити',
clear: 'Очистити',
},
reorder_diagram_alert: {
title: 'Діаграма зміни порядку',
title: 'Перевпорядкувати діаграму',
description:
'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
reorder: 'Змінити порядок',
reorder: 'Перевпорядкувати',
cancel: 'Скасувати',
},
@@ -93,23 +92,23 @@ export const uk: LanguageTranslation = {
},
theme: {
system: 'система',
light: 'світлий',
dark: 'Темний',
system: 'Системна',
light: 'Світла',
dark: 'Темна',
},
zoom: {
on: 'увімкнути',
off: 'вимкнути',
on: 'Увімкнути',
off: 'Вимкнути',
},
last_saved: 'Востаннє збережено',
saved: 'Збережено',
loading_diagram: 'Діаграма завантаження...',
deselect_all: 'Зняти вибір із усіх',
loading_diagram: 'Завантаження діаграми…',
deselect_all: 'Зняти виділення з усіх',
select_all: 'Вибрати усі',
clear: 'Очистити',
show_more: 'показати більше',
show_more: 'Показати більше',
show_less: 'Показати менше',
copy_to_clipboard: 'Копіювати в буфер обміну',
copied: 'Скопійовано!',
@@ -117,50 +116,55 @@ export const uk: LanguageTranslation = {
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фільтрувати за схемою',
search_schema: 'Схема пошуку...',
search_schema: 'Пошук схеми…',
no_schemas_found: 'Схеми не знайдено.',
view_all_options: 'Переглянути всі параметри...',
view_all_options: 'Переглянути всі параметри',
tables_section: {
tables: 'Таблиці',
add_table: 'Додати таблицю',
filter: 'фільтр',
filter: 'Фільтр',
collapse: 'Згорнути все',
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'поля',
nullable: 'Зведений нанівець?',
fields: 'Поля',
nullable: 'Може бути Null?',
primary_key: 'Первинний ключ',
indexes: 'Індекси',
comments: 'Коментарі',
no_comments: 'Без коментарів',
no_comments: 'Немає коментарів',
add_field: 'Додати поле',
add_index: 'Додати індекс',
index_select_fields: 'Виберіть поля',
no_types_found: 'Типи не знайдено',
field_name: "Ім'я",
field_name: 'Назва поля',
field_type: 'Тип',
field_actions: {
title: 'Атрибути полів',
unique: 'Унікальний',
unique: 'Унікальне',
comments: 'Коментарі',
no_comments: 'Без коментарів',
no_comments: 'Немає коментарів',
delete_field: 'Видалити поле',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Атрибути індексу',
name: "Ім'я",
name: 'Назва індекса',
unique: 'Унікальний',
delete_index: 'Видалити індекс',
},
table_actions: {
title: 'Дії таблиці',
title: 'Дії з таблицею',
change_schema: 'Змінити схему',
add_field: 'Додати поле',
add_index: 'Додати індекс',
duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Дублювати таблицю',
delete_table: 'Видалити таблицю',
},
},
@@ -170,14 +174,14 @@ export const uk: LanguageTranslation = {
},
},
relationships_section: {
relationships: 'стосунки',
filter: 'фільтр',
add_relationship: "Додати зв'язок",
relationships: 'Звʼязки',
filter: 'Фільтр',
add_relationship: 'Додати звʼязок',
collapse: 'Згорнути все',
relationship: {
primary: 'Первинна таблиця',
foreign: 'Посилання на таблицю',
cardinality: 'Кардинальність',
cardinality: 'Звʼязок',
delete_relationship: 'Видалити',
relationship_actions: {
title: 'Дії',
@@ -185,17 +189,17 @@ export const uk: LanguageTranslation = {
},
},
empty_state: {
title: 'Жодних стосунків',
description: 'Створіть звязок для зєднання таблиць',
title: 'Звʼязків немає',
description: 'Створіть звʼязок для зʼєднання таблиць',
},
},
dependencies_section: {
dependencies: 'Залежності',
filter: 'фільтр',
filter: 'Фільтр',
collapse: 'Згорнути все',
dependency: {
table: 'Таблиця',
dependent_table: 'Залежний вид',
dependent_table: 'Залежне подання',
delete_dependency: 'Видалити',
dependency_actions: {
title: 'Дії',
@@ -212,34 +216,34 @@ export const uk: LanguageTranslation = {
toolbar: {
zoom_in: 'Збільшити',
zoom_out: 'Зменшити',
save: 'зберегти',
save: 'Зберегти',
show_all: 'Показати все',
undo: 'Скасувати',
redo: 'Повторити',
reorder_diagram: 'Діаграма зміни порядку',
highlight_overlapping_tables: 'Виділіть таблиці, що перекриваються',
reorder_diagram: 'Перевпорядкувати діаграму',
highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
},
new_diagram_dialog: {
database_selection: {
title: 'Що таке ваша база даних?',
title: 'Яка у вас база даних?',
description:
'Кожна база даних має свої унікальні особливості та можливості.',
check_examples_long: еревірте приклади',
check_examples_long: одивіться приклади',
check_examples_short: 'Приклади',
},
import_database: {
title: 'Імпортуйте вашу базу даних',
database_edition: 'Редакція бази даних:',
database_edition: 'Варіант бази даних:',
step_1: 'Запустіть цей сценарій у своїй базі даних:',
step_2: 'Вставте сюди результат сценарію:',
script_results_placeholder: 'Результати сценарію тут...',
script_results_placeholder: 'Результати сценарію має бути тут…',
ssms_instructions: {
button_text: 'SSMS Інструкції',
title: 'Інструкції',
step_1: 'Перейдіть до Інструменти > Опції > Результати запиту > SQL Сервер.',
step_2: 'Якщо ви використовуєте «Результати в сітку», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
step_2: 'Якщо ви використовуєте «Results to Grid», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
},
instructions_link: 'Потрібна допомога? Подивіться як',
check_script_result: 'Перевірте результат сценарію',
@@ -247,20 +251,19 @@ export const uk: LanguageTranslation = {
cancel: 'Скасувати',
back: 'Назад',
// TODO: Translate
import_from_file: 'Import from File',
import_from_file: 'Імпортувати з файлу',
empty_diagram: 'Порожня діаграма',
continue: 'Продовжити',
import: 'Імпорт',
},
open_diagram_dialog: {
title: 'Відкрита діаграма',
title: 'Відкрити діаграму',
description:
'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
table_columns: {
name: "Ім'я",
created_at: 'Створено в',
name: 'Назва',
created_at: 'Створено0',
last_modified: 'Востаннє змінено',
tables_count: 'Таблиці',
},
@@ -274,23 +277,23 @@ export const uk: LanguageTranslation = {
'Експортуйте свою схему діаграми в {{databaseType}} сценарій',
close: 'Закрити',
loading: {
text: 'ШІ створює SQL для {{databaseType}}...',
text: 'ШІ створює SQL для {{databaseType}}',
description: 'Це має зайняти до 30 секунд.',
},
error: {
message:
"Помилка створення сценарію SQL. Спробуйте пізніше або <0>зв'яжіться з нами</0>.",
'Помилка створення сценарію SQL. Спробуйте пізніше або <0>звʼяжіться з нами</0>.',
description:
'Не соромтеся використовувати свій OPENAI_TOKEN, дивіться посібник <0>тут</0>.',
},
},
create_relationship_dialog: {
title: 'Створити відносини',
title: 'Створити звʼязок',
primary_table: 'Первинна таблиця',
primary_field: 'Первинне поле',
referenced_table: 'Посилання на таблицю',
referenced_field: 'Поле посилання',
referenced_table: 'Звʼязана таблиця',
referenced_field: 'Повʼязане поле',
primary_table_placeholder: 'Виберіть таблицю',
primary_field_placeholder: 'Виберіть поле',
referenced_table_placeholder: 'Виберіть таблицю',
@@ -310,12 +313,12 @@ export const uk: LanguageTranslation = {
new_tables:
'<bold>{{newTablesNumber}}</bold> будуть додані нові таблиці.',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> будуть створені нові відносини.',
'<bold>{{newRelationshipsNumber}}</bold> будуть створені нові звʼязки.',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> таблиці будуть перезаписані.',
proceed: 'Ви хочете продовжити?',
},
import: 'Імпорт',
import: 'Імпортувати',
cancel: 'Скасувати',
},
},
@@ -323,83 +326,95 @@ export const uk: LanguageTranslation = {
export_image_dialog: {
title: 'Експорт зображення',
description: 'Виберіть коефіцієнт масштабування для експорту:',
scale_1x: '1x Регулярний',
scale_1x: '1x Звичайний',
scale_2x: '2x (Рекомендовано)',
scale_3x: '3x',
scale_4x: '4x',
cancel: 'Скасувати',
export: 'Експорт',
export: 'Експортувати',
},
new_table_schema_dialog: {
title: 'Виберіть Схему',
description:
'Наразі відображається кілька схем. Виберіть один для нової таблиці.',
'Наразі показується кілька схем. Виберіть одну для нової таблиці.',
cancel: 'Скасувати',
confirm: 'Підтвердити',
},
update_table_schema_dialog: {
title: 'Змінити схему',
description: 'Оновити таблицю "{{tableName}}" схему',
description: 'Оновити схему таблиці "{{tableName}}"',
cancel: 'Скасувати',
confirm: 'Змінити',
},
star_us_dialog: {
title: 'Допоможіть нам покращитися!',
description: 'Хочете позначити нас на Ґітхаб? Це лише один клік!',
description: 'Поставне на зірку на GitHub? Це лише один клік!',
close: 'Не зараз',
confirm: 'звичайно!',
confirm: 'Звісно!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
title: 'Експорт Діаграми',
description: 'Оберіть формат експорту:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
cancel: 'Скасувати',
export: 'Експортувати',
error: {
title: 'Error exporting diagram',
title: 'Помилка експорут діаграми',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Щось пішло не так. Потрібна допомога? chartdb.io@gmail.com',
},
},
import_diagram_dialog: {
title: 'Імпорт Діаграми',
description: 'Вставте JSON діаграми нижче:',
cancel: 'Скасувати',
import: 'Імпортувати',
error: {
title: 'Помилка імпорту діаграми',
description:
'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'Один до одного',
one_to_many: 'Один до багатьох',
many_to_one: 'Багато до одного',
many_to_many: 'Багато до багатьох',
one_to_one: 'Один до Одного',
one_to_many: 'Один до Багатьох',
many_to_one: 'Багато до Одного',
many_to_many: 'Багато до Багатьох',
},
canvas_context_menu: {
new_table: 'Нова таблиця',
new_relationship: 'Нові стосунки',
new_relationship: 'Новий звʼязок',
},
table_node_context_menu: {
edit_table: 'Редагувати таблицю',
duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Дублювати таблицю',
delete_table: 'Видалити таблицю',
add_relationship: 'Add Relationship', // TODO: Translate
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
snap_to_grid_tooltip: 'Вирівнювати за сіткою (Отримуйте {{key}})',
tool_tips: {
double_click_to_edit: 'Двойной клик для редактирования',
double_click_to_edit: 'Подвійне клацання для редагування',
},
language_select: {

View File

@@ -8,7 +8,7 @@ export const vi: LanguageTranslation = {
new: 'Tạo mới',
open: 'Mở',
save: 'Lưu',
import_database: 'Nhập cơ sở dữ liệu',
import: 'Nhập cơ sở dữ liệu',
export_sql: 'Xuất SQL',
export_as: 'Xuất thành',
delete_diagram: 'Xóa sơ đồ',
@@ -34,13 +34,14 @@ export const vi: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
share: {
share: 'Chia sẻ',
backup: {
backup: 'Hỗ trợ',
export_diagram: 'Xuất sơ đồ',
import_diagram: 'Nhập sơ đồ',
restore_diagram: 'Khôi phục sơ đồ',
},
help: {
help: 'Trợ giúp',
docs_website: 'Tài liệu',
visit_website: 'Truy cập ChartDB',
join_discord: 'Tham gia Discord',
schedule_a_call: 'Trò chuyện cùng chúng tôi!',
@@ -127,6 +128,9 @@ export const vi: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: 'Trường',
@@ -147,6 +151,8 @@ export const vi: LanguageTranslation = {
comments: 'Bình luận',
no_comments: 'Không có bình luận',
delete_field: 'Xóa trường',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Thuộc tính chỉ mục',
@@ -373,6 +379,20 @@ export const vi: LanguageTranslation = {
'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: 'Quan hệ một-một',
one_to_many: 'Quan hệ một-nhiều',
@@ -389,6 +409,7 @@ export const vi: LanguageTranslation = {
edit_table: 'Sửa bảng',
duplicate_table: 'Nhân đôi bảng',
delete_table: 'Xóa bảng',
add_relationship: 'Add Relationship', // TODO: Translate
},
snap_to_grid_tooltip: 'Căn lưới (Giữ phím {{key}})',

View File

@@ -8,7 +8,7 @@ export const zh_CN: LanguageTranslation = {
new: '新建',
open: '打开',
save: '保存',
import_database: '导入数据库',
import: '导入数据库',
export_sql: '导出 SQL 语句',
export_as: '导出为',
delete_diagram: '删除关系图',
@@ -34,13 +34,14 @@ export const zh_CN: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
share: {
share: '分享',
backup: {
backup: '备份',
export_diagram: '导出关系图',
import_diagram: '导入关系图',
restore_diagram: '还原图表',
},
help: {
help: '帮助',
docs_website: '文档',
visit_website: '访问 ChartDB',
join_discord: '在 Discord 上加入我们',
schedule_a_call: '和我们交流!',
@@ -124,6 +125,9 @@ export const zh_CN: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: '字段',
@@ -144,6 +148,8 @@ export const zh_CN: LanguageTranslation = {
comments: '注释',
no_comments: '空',
delete_field: '删除字段',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '索引属性',
@@ -369,6 +375,20 @@ export const zh_CN: LanguageTranslation = {
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: '一对一',
one_to_many: '一对多',
@@ -385,6 +405,7 @@ export const zh_CN: LanguageTranslation = {
edit_table: '编辑表',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: '删除表',
add_relationship: 'Add Relationship', // TODO: Translate
},
snap_to_grid_tooltip: '对齐到网格(按住 {{key}}',

View File

@@ -8,7 +8,7 @@ export const zh_TW: LanguageTranslation = {
new: '新增',
open: '開啟',
save: '儲存',
import_database: '匯入資料庫',
import: '匯入資料庫',
export_sql: '匯出 SQL',
export_as: '匯出為特定格式',
delete_diagram: '刪除圖表',
@@ -34,13 +34,14 @@ export const zh_TW: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
share: {
share: '分享',
backup: {
backup: '備份',
export_diagram: '匯出圖表',
import_diagram: '匯入圖表',
restore_diagram: '恢復圖表',
},
help: {
help: '幫助',
docs_website: '文件',
visit_website: '訪問 ChartDB 網站',
join_discord: '加入 Discord',
schedule_a_call: '與我們聯絡!',
@@ -124,6 +125,9 @@ export const zh_TW: LanguageTranslation = {
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
table: {
fields: '欄位',
@@ -144,6 +148,8 @@ export const zh_TW: LanguageTranslation = {
comments: '註解',
no_comments: '無註解',
delete_field: '刪除欄位',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '索引屬性',
@@ -368,6 +374,20 @@ export const zh_TW: LanguageTranslation = {
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
},
},
relationship_type: {
one_to_one: '一對一',
one_to_many: '一對多',
@@ -384,6 +404,7 @@ export const zh_TW: LanguageTranslation = {
edit_table: '編輯表格',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: '刪除表格',
add_relationship: 'Add Relationship', // TODO: Translate
},
snap_to_grid_tooltip: '對齊網格(按住 {{key}}',

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const clickhouseDataTypes: readonly DataType[] = [
export const clickhouseDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'uint8', id: 'uint8' },
{ name: 'uint16', id: 'uint16' },
@@ -48,25 +48,41 @@ export const clickhouseDataTypes: readonly DataType[] = [
{ name: 'mediumblob', id: 'mediumblob' },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'varchar', id: 'varchar' },
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'char large object', id: 'char_large_object' },
{ name: 'char varying', id: 'char_varying' },
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
{ name: 'character large object', id: 'character_large_object' },
{ name: 'character varying', id: 'character_varying' },
{
name: 'character varying',
id: 'character_varying',
hasCharMaxLength: true,
},
{ name: 'nchar large object', id: 'nchar_large_object' },
{ name: 'nchar varying', id: 'nchar_varying' },
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
{
name: 'national character large object',
id: 'national_character_large_object',
},
{ name: 'national character varying', id: 'national_character_varying' },
{ name: 'national char varying', id: 'national_char_varying' },
{ name: 'national character', id: 'national_character' },
{ name: 'national char', id: 'national_char' },
{
name: 'national character varying',
id: 'national_character_varying',
hasCharMaxLength: true,
},
{
name: 'national char varying',
id: 'national_char_varying',
hasCharMaxLength: true,
},
{
name: 'national character',
id: 'national_character',
hasCharMaxLength: true,
},
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
{ name: 'binary large object', id: 'binary_large_object' },
{ name: 'binary varying', id: 'binary_varying' },
{ name: 'fixedstring', id: 'fixedstring' },
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
{ name: 'string', id: 'string' },
// Date Types

View File

@@ -13,12 +13,16 @@ export interface DataType {
name: string;
}
export interface DataTypeData extends DataType {
hasCharMaxLength?: boolean;
}
export const dataTypeSchema: z.ZodType<DataType> = z.object({
id: z.string(),
name: z.string(),
});
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
[DatabaseType.GENERIC]: genericDataTypes,
[DatabaseType.POSTGRESQL]: postgresDataTypes,
[DatabaseType.MYSQL]: mysqlDataTypes,
@@ -62,3 +66,23 @@ export function areFieldTypesCompatible(
dbCompatibleTypes[type2.id]?.includes(type1.id)
);
}
export const dataTypes = Object.values(dataTypeMap).flat();
export const dataTypeDataToDataType = (
dataTypeData: DataTypeData
): DataType => ({
id: dataTypeData.id,
name: dataTypeData.name,
});
export const findDataTypeDataById = (
id: string,
databaseType?: DatabaseType
): DataTypeData | undefined => {
const dataTypesOptions = databaseType
? dataTypeMap[databaseType]
: dataTypes;
return dataTypesOptions.find((dataType) => dataType.id === id);
};

View File

@@ -1,11 +1,11 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const genericDataTypes: readonly DataType[] = [
export const genericDataTypes: readonly DataTypeData[] = [
{ name: 'bigint', id: 'bigint' },
{ name: 'binary', id: 'binary' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'blob', id: 'blob' },
{ name: 'boolean', id: 'boolean' },
{ name: 'char', id: 'char' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
{ name: 'decimal', id: 'decimal' },
@@ -22,6 +22,6 @@ export const genericDataTypes: readonly DataType[] = [
{ name: 'time', id: 'time' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'uuid', id: 'uuid' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'varchar', id: 'varchar' },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
] as const;

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const mariadbDataTypes: readonly DataType[] = [
export const mariadbDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
@@ -23,10 +23,10 @@ export const mariadbDataTypes: readonly DataType[] = [
{ name: 'year', id: 'year' },
// String Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const mysqlDataTypes: readonly DataType[] = [
export const mysqlDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
@@ -23,10 +23,10 @@ export const mysqlDataTypes: readonly DataType[] = [
{ name: 'year', id: 'year' },
// String Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const postgresDataTypes: readonly DataType[] = [
export const postgresDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'smallint', id: 'smallint' },
{ name: 'integer', id: 'integer' },
@@ -15,9 +15,13 @@ export const postgresDataTypes: readonly DataType[] = [
{ name: 'money', id: 'money' },
// Character Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'character varying', id: 'character_varying' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{
name: 'character varying',
id: 'character_varying',
hasCharMaxLength: true,
},
{ name: 'text', id: 'text' },
// Binary Data Types

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const sqlServerDataTypes: readonly DataType[] = [
export const sqlServerDataTypes: readonly DataTypeData[] = [
// Exact Numerics
{ name: 'bigint', id: 'bigint' },
{ name: 'bit', id: 'bit' },
@@ -25,18 +25,18 @@ export const sqlServerDataTypes: readonly DataType[] = [
{ name: 'time', id: 'time' },
// Character Strings
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'text', id: 'text' },
// Unicode Character Strings
{ name: 'nchar', id: 'nchar' },
{ name: 'nvarchar', id: 'nvarchar' },
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true },
{ name: 'ntext', id: 'ntext' },
// Binary Strings
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'image', id: 'image' },
// Other Data Types

View File

@@ -1,6 +1,6 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const sqliteDataTypes: readonly DataType[] = [
export const sqliteDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'integer', id: 'integer' },
{ name: 'real', id: 'real' },
@@ -12,6 +12,9 @@ export const sqliteDataTypes: readonly DataType[] = [
// Blob Type
{ name: 'blob', id: 'blob' },
// Blob Type
{ name: 'json', id: 'json' },
// Date/Time Types (SQLite uses TEXT, REAL, or INTEGER types for dates and times)
{ name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
@@ -19,6 +22,6 @@ export const sqliteDataTypes: readonly DataType[] = [
{ name: 'int', id: 'int' },
{ name: 'float', id: 'float' },
{ name: 'boolean', id: 'boolean' },
{ name: 'varchar', id: 'varchar' },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'decimal', id: 'decimal' },
] as const;

View File

@@ -0,0 +1,82 @@
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
export function isFunction(value: string): boolean {
// Common SQL functions
const functionPatterns = [
/^CURRENT_TIMESTAMP$/i,
/^NOW\(\)$/i,
/^GETDATE\(\)$/i,
/^CURRENT_DATE$/i,
/^CURRENT_TIME$/i,
/^UUID\(\)$/i,
/^NEWID\(\)$/i,
/^NEXT VALUE FOR/i,
/^IDENTITY\s*\(\d+,\s*\d+\)$/i,
];
return functionPatterns.some((pattern) => pattern.test(value.trim()));
}
export function isKeyword(value: string): boolean {
// Common SQL keywords that can be used as default values
const keywords = [
'NULL',
'TRUE',
'FALSE',
'CURRENT_TIMESTAMP',
'CURRENT_DATE',
'CURRENT_TIME',
'CURRENT_USER',
'SESSION_USER',
'SYSTEM_USER',
];
return keywords.includes(value.trim().toUpperCase());
}
export function strHasQuotes(value: string): boolean {
return /^['"].*['"]$/.test(value.trim());
}
export function exportFieldComment(comment: string): string {
if (!comment) {
return '';
}
return comment
.split('\n')
.map((commentLine) => ` -- ${commentLine}\n`)
.join('');
}
export function getInlineFK(table: DBTable, diagram: Diagram): string {
if (!diagram.relationships) {
return '';
}
const fks = diagram.relationships
.filter((r) => r.sourceTableId === table.id)
.map((r) => {
const targetTable = diagram.tables?.find(
(t) => t.id === r.targetTableId
);
const sourceField = table.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable?.fields.find(
(f) => f.id === r.targetFieldId
);
if (!targetTable || !sourceField || !targetField) {
return '';
}
const targetTableName = targetTable.schema
? `"${targetTable.schema}"."${targetTable.name}"`
: `"${targetTable.name}"`;
return ` FOREIGN KEY ("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}")`;
})
.filter(Boolean);
return fks.join(',\n');
}

View File

@@ -0,0 +1,247 @@
import {
exportFieldComment,
isFunction,
isKeyword,
strHasQuotes,
} from './common';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
import type { DBRelationship } from '@/lib/domain/db-relationship';
function parseMSSQLDefault(field: DBField): string {
if (!field.default) {
return '';
}
let defaultValue = field.default.trim();
// Remove type casting for SQL Server
defaultValue = defaultValue.split('::')[0];
// Handle nextval sequences for SQL Server
if (defaultValue.includes('nextval')) {
return 'IDENTITY(1,1)';
}
// Special handling for SQL Server DEFAULT values
if (defaultValue.match(/^\(\(.*\)\)$/)) {
// Handle ((0)), ((0.00)) style defaults
return defaultValue.replace(/^\(\(|\)\)$/g, '');
} else if (defaultValue.match(/^\(N'.*'\)$/)) {
// Handle (N'value') style defaults
const innerValue = defaultValue.replace(/^\(N'|'\)$/g, '');
return `N'${innerValue}'`;
} else if (defaultValue.match(/^\(NULL\)$/i)) {
// Handle (NULL) defaults
return 'NULL';
} else if (defaultValue.match(/^\(getdate\(\)\)$/i)) {
// Handle (getdate()) defaults
return 'getdate()';
} else if (defaultValue.match(/^\('?\*'?\)$/i) || defaultValue === '*') {
// Handle ('*') or (*) or * defaults - common for "all" values
return "N'*'";
} else if (defaultValue.match(/^\((['"])(.*)\1\)$/)) {
// Handle ('value') or ("value") style defaults
const matches = defaultValue.match(/^\((['"])(.*)\1\)$/);
return matches ? `N'${matches[2]}'` : defaultValue;
}
// Handle special characters that could be interpreted as operators
const sqlServerSpecialChars = /[*+\-/%&|^!=<>~]/;
if (sqlServerSpecialChars.test(defaultValue)) {
// If the value contains special characters and isn't already properly quoted
if (
!strHasQuotes(defaultValue) &&
!isFunction(defaultValue) &&
!isKeyword(defaultValue)
) {
return `N'${defaultValue.replace(/'/g, "''")}'`;
}
}
if (
strHasQuotes(defaultValue) ||
isFunction(defaultValue) ||
isKeyword(defaultValue) ||
/^-?\d+(\.\d+)?$/.test(defaultValue)
) {
return defaultValue;
}
return `'${defaultValue}'`;
}
export function exportMSSQL(diagram: Diagram): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
const tables = diagram.tables;
const relationships = diagram.relationships;
// Create CREATE SCHEMA statements for all schemas
let sqlScript = '';
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`;
});
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
const tableName = table.schema
? `[${table.schema}].[${table.name}]`
: `[${table.name}]`;
return `${
table.comments ? `/**\n${table.comments}\n*/\n` : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `[${field.name}]`;
const typeName = field.type.name;
// Handle SQL Server specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'nvarchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'nchar'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
const notNull = field.nullable ? '' : ' NOT NULL';
// Check if identity column
const identity = field.default
?.toLowerCase()
.includes('identity')
? ' IDENTITY(1,1)'
: '';
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value using SQL Server specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(field)}`
: '';
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
table.fields.filter((f) => f.primaryKey).length > 0
? `,\n PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`
: ''
}\n);\n\n${table.indexes
.map((index) => {
const indexName = table.schema
? `[${table.schema}_${index.name}]`
: `[${index.name}]`;
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? `[${field.name}]` : '';
})
.filter(Boolean);
// SQL Server has a limit of 32 columns in an index
if (indexFields.length > 32) {
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
console.warn(
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
);
indexFields.length = 32;
return indexFields.length > 0
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
: '';
}
return indexFields.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
: '';
})
.join('')}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// Generate foreign keys
sqlScript += `\n${relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
const targetTable = tables.find((t) => t.id === r.targetTableId);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (!sourceField || !targetField) {
return '';
}
const sourceTableName = sourceTable.schema
? `[${sourceTable.schema}].[${sourceTable.name}]`
: `[${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`;
})
.filter(Boolean) // Remove empty strings
.join('\n')}`;
return sqlScript;
}

View File

@@ -1,9 +1,10 @@
import type { Diagram } from '../../domain/diagram';
import { OPENAI_API_KEY } from '@/lib/env';
import type { DatabaseType } from '@/lib/domain/database-type';
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DBTable } from '@/lib/domain/db-table';
import type { DataType } from '../data-types/data-types';
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
import { exportMSSQL } from './export-per-type/mssql';
export const exportBaseSQL = (diagram: Diagram): string => {
const { tables, relationships } = diagram;
@@ -12,6 +13,10 @@ export const exportBaseSQL = (diagram: Diagram): string => {
return '';
}
if (diagram.databaseType === DatabaseType.SQL_SERVER) {
return exportMSSQL(diagram);
}
// Filter out the tables that are views
const nonViewTables = tables.filter((table) => !table.isView);
@@ -110,8 +115,22 @@ export const exportBaseSQL = (diagram: Diagram): string => {
// Remove the type cast part after :: if it exists
if (fieldDefault.includes('::')) {
const endedWithParentheses = fieldDefault.endsWith(')');
fieldDefault = fieldDefault.split('::')[0];
if (
(fieldDefault.startsWith('(') &&
!fieldDefault.endsWith(')')) ||
endedWithParentheses
) {
fieldDefault += ')';
}
}
if (fieldDefault === `('now')`) {
fieldDefault = `now()`;
}
sqlScript += ` DEFAULT ${fieldDefault}`;
}
@@ -196,6 +215,26 @@ export const exportBaseSQL = (diagram: Diagram): string => {
return sqlScript;
};
const validateConfiguration = () => {
const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY;
const baseUrl = window?.env?.OPENAI_API_ENDPOINT ?? OPENAI_API_ENDPOINT;
const modelName = window?.env?.LLM_MODEL_NAME ?? LLM_MODEL_NAME;
// If using custom endpoint and model, don't require OpenAI API key
if (baseUrl && modelName) {
return { useCustomEndpoint: true };
}
// If using OpenAI's service, require API key
if (apiKey) {
return { useCustomEndpoint: false };
}
throw new Error(
'Configuration Error: Either provide an OpenAI API key or both a custom endpoint and model name'
);
};
export const exportSQL = async (
diagram: Diagram,
databaseType: DatabaseType,
@@ -206,6 +245,10 @@ export const exportSQL = async (
}
): Promise<string> => {
const sqlScript = exportBaseSQL(diagram);
if (databaseType === DatabaseType.SQL_SERVER) {
return sqlScript;
}
const cacheKey = await generateCacheKey(databaseType, sqlScript);
const cachedResult = getFromCache(cacheKey);
@@ -213,43 +256,76 @@ export const exportSQL = async (
return cachedResult;
}
// Validate configuration before proceeding
const { useCustomEndpoint } = validateConfiguration();
const [{ streamText, generateText }, { createOpenAI }] = await Promise.all([
import('ai'),
import('@ai-sdk/openai'),
]);
const openai = createOpenAI({
apiKey: window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY,
});
const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY;
const baseUrl = window?.env?.OPENAI_API_ENDPOINT ?? OPENAI_API_ENDPOINT;
const modelName =
window?.env?.LLM_MODEL_NAME ??
LLM_MODEL_NAME ??
'gpt-4o-mini-2024-07-18';
let config: { apiKey: string; baseUrl?: string };
if (useCustomEndpoint) {
config = {
apiKey: apiKey,
baseUrl: baseUrl,
};
} else {
config = {
apiKey: apiKey,
};
}
const openai = createOpenAI(config);
const prompt = generateSQLPrompt(databaseType, sqlScript);
if (options?.stream) {
const { textStream, text: textPromise } = await streamText({
model: openai('gpt-4o-mini-2024-07-18'),
try {
if (options?.stream) {
const { textStream, text: textPromise } = await streamText({
model: openai(modelName),
prompt: prompt,
});
for await (const textPart of textStream) {
if (options.signal?.aborted) {
return '';
}
options.onResultStream(textPart);
}
const text = await textPromise;
setInCache(cacheKey, text);
return text;
}
const { text } = await generateText({
model: openai(modelName),
prompt: prompt,
});
for await (const textPart of textStream) {
if (options.signal?.aborted) {
return '';
}
options.onResultStream(textPart);
}
const text = await textPromise;
setInCache(cacheKey, text);
return text;
} catch (error: unknown) {
console.error('Error generating SQL:', error);
if (error instanceof Error && error.message.includes('API key')) {
throw new Error(
'Error: Please check your API configuration. If using a custom endpoint, make sure the endpoint URL is correct.'
);
}
throw new Error(
'Error generating SQL script. Please check your configuration and try again.'
);
}
const { text } = await generateText({
model: openai('gpt-4o-mini-2024-07-18'),
prompt: prompt,
});
setInCache(cacheKey, text);
return text;
};
function getMySQLDataTypeSize(type: DataType) {

View File

@@ -275,7 +275,7 @@ FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, inde
if (options.databaseClient === DatabaseClient.POSTGRESQL_PSQL) {
return `${psqlPreCommand}psql -h HOST_NAME -p PORT -U USER_NAME -d DATABASE_NAME -c "
${query.replace(/"/g, '\\"').replace(/\\\\/g, '\\\\\\').replace(/\\x/g, '\\\\x')}
" -t -A > output.json";`;
" -t -A > output.json;`;
}
return query;

View File

@@ -85,7 +85,7 @@ export const sqliteQuery = `WITH fk_info AS (
ELSE LOWER(p.type)
END,
'ordinal_position', p.cid,
'nullable', (CASE WHEN p."notnull" = 0 THEN 'true' ELSE 'false' END),
'nullable', (CASE WHEN p."notnull" = 0 THEN true ELSE false END),
'collation', '',
'character_maximum_length',
CASE

View File

@@ -1,23 +1,26 @@
import { DatabaseEdition } from '@/lib/domain/database-edition';
const sqlServerQuery = `WITH fk_info AS (
const sqlServerQuery = `${`/* SQL Server 2017 and above edition (14.0, 15.0, 16.0, 17.0)*/`}
WITH fk_info AS (
SELECT
JSON_QUERY(
'[' + STRING_AGG(
N'[' + STRING_AGG(
CONVERT(nvarchar(max),
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(tp_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "table": "' + COALESCE(REPLACE(tp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "column": "' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "foreign_key_name": "' + COALESCE(REPLACE(fk.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "reference_schema": "' + COALESCE(REPLACE(tr_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "reference_table": "' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "reference_column": "' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "fk_def": "FOREIGN KEY (' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
') REFERENCES ' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'(' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
') ON DELETE ' + fk.delete_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS +
' ON UPDATE ' + fk.update_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '"}')
), ','
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tp_schema.name, '"', ''), ''), 'json') +
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(tp.name, '"', ''), ''), 'json') +
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
'", "foreign_key_name": "' + STRING_ESCAPE(COALESCE(REPLACE(fk.name, '"', ''), ''), 'json') +
'", "reference_schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tr_schema.name, '"', ''), ''), 'json') +
'", "reference_table": "' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
'", "reference_column": "' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
'", "fk_def": "FOREIGN KEY (' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
') REFERENCES ' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
'(' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
') ON DELETE ' + STRING_ESCAPE(fk.delete_referential_action_desc, 'json') +
' ON UPDATE ' + STRING_ESCAPE(fk.update_referential_action_desc, 'json') +
'"}') COLLATE DATABASE_DEFAULT
), N','
) + N']'
) AS all_fks_json
FROM sys.foreign_keys AS fk
@@ -31,299 +34,271 @@ const sqlServerQuery = `WITH fk_info AS (
), pk_info AS (
SELECT
JSON_QUERY(
'[' + STRING_AGG(
CONVERT(nvarchar(max),
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "table": "' + COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "column": "' + COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "pk_def": "PRIMARY KEY (' + pk.COLUMN_NAME COLLATE SQL_Latin1_General_CP1_CI_AS + ')"}')
), ','
) + N']'
N'[' +
STRING_AGG(
CONVERT(nvarchar(max),
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), ''), 'json') +
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), ''), 'json') +
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), ''), 'json') +
'", "pk_def": "PRIMARY KEY (' + STRING_ESCAPE(pk.COLUMN_NAME, 'json') + N')"}') COLLATE DATABASE_DEFAULT
), N','
) + N']'
) AS all_pks_json
FROM
(
SELECT
kcu.TABLE_SCHEMA,
kcu.TABLE_NAME,
kcu.COLUMN_NAME
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
JOIN
INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
WHERE
tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
) pk
FROM (
SELECT
kcu.TABLE_SCHEMA,
kcu.TABLE_NAME,
kcu.COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
) pk
),
cols AS (
SELECT
JSON_QUERY(
'[' + STRING_AGG(
JSON_QUERY(N'[' +
STRING_AGG(
CONVERT(nvarchar(max),
JSON_QUERY('{"schema": "' + COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), '') +
'", "table": "' + COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), '') +
'", "name": "' + COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), '') +
'", "ordinal_position": "' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
'", "type": "' + LOWER(cols.DATA_TYPE) +
'", "character_maximum_length": "' +
COALESCE(CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX)), 'null') +
'", "precision": ' +
CASE
WHEN cols.DATA_TYPE IN ('numeric', 'decimal') THEN
CONCAT('{"precision":', COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null'),
',"scale":', COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null'), '}')
ELSE
'null'
END +
', "nullable": ' +
CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
', "default": "' +
COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), '') +
'", "collation": "' +
COALESCE(cols.COLLATION_NAME, '') +
'"}')
), ','
) + ']'
) AS all_columns_json
FROM
INFORMATION_SCHEMA.COLUMNS cols
WHERE
cols.TABLE_CATALOG = DB_NAME()
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), ''), 'json') +
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), ''), 'json') +
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), ''), 'json') +
'", "ordinal_position": ' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
', "type": "' + STRING_ESCAPE(LOWER(cols.DATA_TYPE), 'json') +
'", "character_maximum_length": ' +
CASE
WHEN cols.CHARACTER_MAXIMUM_LENGTH IS NULL THEN 'null'
ELSE CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX))
END +
', "precision": ' +
CASE
WHEN cols.DATA_TYPE IN ('numeric', 'decimal')
THEN '{"precision":' + COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null') +
',"scale":' + COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null') + '}'
ELSE 'null'
END +
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
', "default": ' +
'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
', "collation": ' + CASE
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
END +
N'}') COLLATE DATABASE_DEFAULT
), N','
) +
N']') AS all_columns_json
FROM INFORMATION_SCHEMA.COLUMNS cols
WHERE cols.TABLE_CATALOG = DB_NAME()
),
indexes AS (
SELECT
'[' + STRING_AGG(
CONVERT(nvarchar(max),
JSON_QUERY(
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "table": "' + COALESCE(REPLACE(t.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "name": "' + COALESCE(REPLACE(i.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "column": "' + COALESCE(REPLACE(c.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "index_type": "' + LOWER(i.type_desc) COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
)
), ','
) + N']' AS all_indexes_json
FROM
sys.indexes i
JOIN
sys.tables t ON i.object_id = t.object_id
JOIN
sys.schemas s ON t.schema_id = s.schema_id
JOIN
sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
JOIN
sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE
s.name LIKE '%'
AND i.name IS NOT NULL
N'[' +
STRING_AGG(
CONVERT(nvarchar(max),
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(i.name, '"', ''), ''), 'json') +
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(c.name, '"', ''), ''), 'json') +
'", "index_type": "' + STRING_ESCAPE(LOWER(i.type_desc), 'json') +
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END +
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
) COLLATE DATABASE_DEFAULT
), N','
) +
N']' AS all_indexes_json
FROM sys.indexes i
JOIN sys.tables t ON i.object_id = t.object_id
JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE s.name LIKE '%' AND i.name IS NOT NULL AND ic.is_included_column = 0
),
tbls AS (
SELECT
'[' + STRING_AGG(
CONVERT(nvarchar(max),
JSON_QUERY(
N'{"schema": "' + COALESCE(REPLACE(aggregated.schema_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "table": "' + COALESCE(REPLACE(aggregated.table_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "row_count": "' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
'", "table_type": "' + aggregated.table_type COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + '"}'
)
), ','
) + N']' AS all_tables_json
FROM
(
-- Select from tables
SELECT
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
SUM(p.rows) AS row_count,
t.type_desc AS table_type,
t.create_date AS creation_date
FROM
sys.tables t
JOIN
sys.schemas s ON t.schema_id = s.schema_id
JOIN
sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
WHERE
s.name LIKE '%'
GROUP BY
s.name, t.name, t.type_desc, t.create_date
N'[' + STRING_AGG(
CONVERT(nvarchar(max),
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.schema_name, '"', ''), ''), 'json') +
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.table_name, '"', ''), ''), 'json') +
'", "row_count": ' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
', "table_type": "' + STRING_ESCAPE(aggregated.table_type, 'json') +
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + N'"}'
) COLLATE DATABASE_DEFAULT
), N','
) +
N']' AS all_tables_json
FROM (
SELECT
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
SUM(p.rows) AS row_count,
t.type_desc AS table_type,
t.create_date AS creation_date
FROM sys.tables t
JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
WHERE s.name LIKE '%'
GROUP BY s.name, t.name, t.type_desc, t.create_date
UNION ALL
UNION ALL
-- Select from views
SELECT
COALESCE(REPLACE(s.name, '"', ''), '') AS table_name,
COALESCE(REPLACE(v.name, '"', ''), '') AS object_name,
0 AS row_count, -- Views don't have row counts
'VIEW' AS table_type,
v.create_date AS creation_date
FROM
sys.views v
JOIN
sys.schemas s ON v.schema_id = s.schema_id
WHERE
s.name LIKE '%'
) AS aggregated
SELECT
COALESCE(REPLACE(s.name, '"', ''), '') AS table_name,
COALESCE(REPLACE(v.name, '"', ''), '') AS object_name,
0 AS row_count,
'VIEW' AS table_type,
v.create_date AS creation_date
FROM sys.views v
JOIN sys.schemas s ON v.schema_id = s.schema_id
WHERE s.name LIKE '%'
) AS aggregated
),
views AS (
SELECT
'[' + STRING_AGG(
CONVERT(nvarchar(max),
JSON_QUERY(
N'{"schema": "' + STRING_ESCAPE(COALESCE(s.name, ''), 'json') +
'", "view_name": "' + STRING_ESCAPE(COALESCE(v.name, ''), 'json') +
'", "view_definition": "' +
STRING_ESCAPE(
CAST(
'' AS XML
).value(
'xs:base64Binary(sql:column("DefinitionBinary"))',
'VARCHAR(MAX)'
), 'json') +
'"}'
)
), ','
CONVERT(nvarchar(max),
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
'", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
'", "view_definition": "' +
STRING_ESCAPE(
CAST(
'' AS XML
).value(
'xs:base64Binary(sql:column("DefinitionBinary"))',
'VARCHAR(MAX)'
), 'json') +
N'"}') COLLATE DATABASE_DEFAULT
), N','
) + N']' AS all_views_json
FROM
sys.views v
JOIN
sys.schemas s ON v.schema_id = s.schema_id
JOIN
sys.sql_modules m ON v.object_id = m.object_id
FROM sys.views v
JOIN sys.schemas s ON v.schema_id = s.schema_id
JOIN sys.sql_modules m ON v.object_id = m.object_id
CROSS APPLY
(SELECT CONVERT(VARBINARY(MAX), m.definition) AS DefinitionBinary) AS bin
WHERE
s.name LIKE '%'
WHERE s.name LIKE '%'
)
SELECT JSON_QUERY(
N'{"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
N'{
"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
', "tables": ' + ISNULL((SELECT cast(all_tables_json as nvarchar(max)) FROM tbls), N'[]') +
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
', "database_name": "' + DB_NAME() + '"' +
', "version": ""}'
', "database_name": "' + STRING_ESCAPE(DB_NAME(), 'json') +
'", "version": ""
}'
) AS metadata_json_to_import;
`;
const sqlServer2016AndBelowQuery = `WITH fk_info AS (
SELECT
JSON_QUERY(
'[' + ISNULL(
STUFF((
SELECT ',' +
CONVERT(nvarchar(max),
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(tp_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "table": "' + COALESCE(REPLACE(tp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "column": "' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "foreign_key_name": "' + COALESCE(REPLACE(fk.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "reference_schema": "' + COALESCE(REPLACE(tr_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "reference_table": "' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "reference_column": "' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "fk_def": "FOREIGN KEY (' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
') REFERENCES ' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'(' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
') ON DELETE ' + fk.delete_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS +
' ON UPDATE ' + fk.update_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '"}')
)
FROM
sys.foreign_keys AS fk
JOIN
sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id
JOIN
sys.tables AS tp ON fkc.parent_object_id = tp.object_id
JOIN
sys.schemas AS tp_schema ON tp.schema_id = tp_schema.schema_id
JOIN
sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
JOIN
sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
JOIN
sys.schemas AS tr_schema ON tr.schema_id = tr_schema.schema_id
JOIN
sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id
FOR XML PATH('')
), 1, 1, ''), '')
+ N']'
) AS all_fks_json
const sqlServer2016AndBelowQuery = `${`/* SQL Server 2016 and below edition (13.0, 12.0, 11.0..) */`}
WITH fk_info AS (
SELECT JSON_QUERY('[' +
ISNULL(
STUFF((
SELECT ',' +
CONVERT(nvarchar(max),
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tp_schema.name, '"', ''), ''), 'json') +
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(tp.name, '"', ''), ''), 'json') +
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
'", "foreign_key_name": "' + STRING_ESCAPE(COALESCE(REPLACE(fk.name, '"', ''), ''), 'json') +
'", "reference_schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tr_schema.name, '"', ''), ''), 'json') +
'", "reference_table": "' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
'", "reference_column": "' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
'", "fk_def": "FOREIGN KEY (' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
') REFERENCES ' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
'(' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
') ON DELETE ' + STRING_ESCAPE(fk.delete_referential_action_desc, 'json') +
' ON UPDATE ' + STRING_ESCAPE(fk.update_referential_action_desc, 'json') +
'"}') COLLATE DATABASE_DEFAULT
)
FROM sys.foreign_keys AS fk
JOIN sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id
JOIN sys.tables AS tp ON fkc.parent_object_id = tp.object_id
JOIN sys.schemas AS tp_schema ON tp.schema_id = tp_schema.schema_id
JOIN sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
JOIN sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
JOIN sys.schemas AS tr_schema ON tr.schema_id = tr_schema.schema_id
JOIN sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id
FOR XML PATH('')
), 1, 1, ''), '')
+ N']') AS all_fks_json
),
pk_info AS (
SELECT
JSON_QUERY(
'[' + ISNULL(
STUFF((
SELECT JSON_QUERY('[' +
ISNULL(STUFF((
SELECT ',' +
CONVERT(nvarchar(max),
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "table": "' + COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "column": "' + COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "pk_def": "PRIMARY KEY (' + pk.COLUMN_NAME COLLATE SQL_Latin1_General_CP1_CI_AS + ')"}')
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), ''), 'json') +
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), ''), 'json') +
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), ''), 'json') +
'", "pk_def": "PRIMARY KEY (' + STRING_ESCAPE(pk.COLUMN_NAME, 'json') + N')"}') COLLATE DATABASE_DEFAULT
)
FROM
(
SELECT
kcu.TABLE_SCHEMA,
kcu.TABLE_NAME,
kcu.COLUMN_NAME
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
JOIN
INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
SELECT kcu.TABLE_SCHEMA,
kcu.TABLE_NAME,
kcu.COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
WHERE
tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
) pk
FOR XML PATH('')
), 1, 1, ''), '')
+ N']'
) AS all_pks_json
+ N']') AS all_pks_json
),
cols AS (
SELECT
JSON_QUERY(
'[' + ISNULL(
STUFF((
SELECT ',' +
CONVERT(nvarchar(max),
JSON_QUERY('{"schema": "' + COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), '') +
'", "table": "' + COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), '') +
'", "name": "' + COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), '') +
'", "ordinal_position": "' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
'", "type": "' + LOWER(cols.DATA_TYPE) +
'", "character_maximum_length": "' +
COALESCE(CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX)), 'null') +
'", "precision": ' +
CASE
WHEN cols.DATA_TYPE IN ('numeric', 'decimal') THEN
CONCAT('{"precision":', COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null'),
',"scale":', COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null'), '}')
ELSE
'null'
END +
', "nullable": ' +
CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
', "default": "' +
COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '"'), '') +
'", "collation": "' +
COALESCE(cols.COLLATION_NAME, '') +
'"}')
)
FROM
INFORMATION_SCHEMA.COLUMNS cols
WHERE
cols.TABLE_CATALOG = DB_NAME()
FOR XML PATH('')
), 1, 1, ''), '')
+ ']'
) AS all_columns_json
SELECT JSON_QUERY('[' +
ISNULL(
STUFF((
SELECT ',' +
CONVERT(nvarchar(max),
JSON_QUERY('{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), ''), 'json') +
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), ''), 'json') +
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), ''), 'json') +
'", "ordinal_position": ' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
', "type": "' + STRING_ESCAPE(LOWER(cols.DATA_TYPE), 'json') +
'", "character_maximum_length": ' +
CASE
WHEN cols.CHARACTER_MAXIMUM_LENGTH IS NULL THEN 'null'
ELSE CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX))
END +
', "precision": ' +
CASE
WHEN cols.DATA_TYPE IN ('numeric', 'decimal')
THEN '{"precision":' + COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null') +
',"scale":' + COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null') + '}'
ELSE 'null'
END +
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
', "default": ' +
'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
', "collation": ' +
CASE
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
END +
N'}')
)
FROM
INFORMATION_SCHEMA.COLUMNS cols
WHERE
cols.TABLE_CATALOG = DB_NAME()
FOR XML PATH('')
), 1, 1, ''), '')
+ ']') AS all_columns_json
),
indexes AS (
SELECT
@@ -331,30 +306,25 @@ indexes AS (
STUFF((
SELECT ',' +
CONVERT(nvarchar(max),
JSON_QUERY(
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "table": "' + COALESCE(REPLACE(t.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "name": "' + COALESCE(REPLACE(i.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "column": "' + COALESCE(REPLACE(c.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "index_type": "' + LOWER(i.type_desc) COLLATE SQL_Latin1_General_CP1_CI_AS +
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(i.name, '"', ''), ''), 'json') +
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(c.name, '"', ''), ''), 'json') +
'", "index_type": "' + STRING_ESCAPE(LOWER(i.type_desc), 'json') +
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END COLLATE SQL_Latin1_General_CP1_CI_AS +
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END +
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
)
) COLLATE DATABASE_DEFAULT
)
FROM
sys.indexes i
JOIN
sys.tables t ON i.object_id = t.object_id
JOIN
sys.schemas s ON t.schema_id = s.schema_id
JOIN
sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
JOIN
sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE
s.name LIKE '%'
AND i.name IS NOT NULL
FROM sys.indexes i
JOIN sys.tables t ON i.object_id = t.object_id
JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE s.name LIKE '%'
AND i.name IS NOT NULL
AND ic.is_included_column = 0
FOR XML PATH('')
), 1, 1, ''), '')
+ N']' AS all_indexes_json
@@ -365,12 +335,12 @@ tbls AS (
STUFF((
SELECT ',' +
CONVERT(nvarchar(max),
JSON_QUERY(
N'{"schema": "' + COALESCE(REPLACE(aggregated.schema_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "table": "' + COALESCE(REPLACE(aggregated.object_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "row_count": "' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
'", "object_type": "' + aggregated.object_type COLLATE SQL_Latin1_General_CP1_CI_AS +
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + '"}'
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.schema_name, '"', ''), ''), 'json') +
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.table_name, '"', ''), ''), 'json') +
'", "row_count": ' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
', "table_type": "' + STRING_ESCAPE(aggregated.table_type, 'json') +
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + N'"}'
)
)
FROM
@@ -378,20 +348,15 @@ tbls AS (
-- Select from tables
SELECT
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
COALESCE(REPLACE(t.name, '"', ''), '') AS object_name,
COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
SUM(p.rows) AS row_count,
t.type_desc AS object_type,
t.type_desc AS table_type,
t.create_date AS creation_date
FROM
sys.tables t
JOIN
sys.schemas s ON t.schema_id = s.schema_id
JOIN
sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
WHERE
s.name LIKE '%'
GROUP BY
s.name, t.name, t.type_desc, t.create_date
FROM sys.tables t
JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
WHERE s.name LIKE '%'
GROUP BY s.name, t.name, t.type_desc, t.create_date
UNION ALL
@@ -402,12 +367,9 @@ tbls AS (
0 AS row_count, -- Views don't have row counts
'VIEW' AS object_type,
v.create_date AS creation_date
FROM
sys.views v
JOIN
sys.schemas s ON v.schema_id = s.schema_id
WHERE
s.name LIKE '%'
FROM sys.views v
JOIN sys.schemas s ON v.schema_id = s.schema_id
WHERE s.name LIKE '%'
) AS aggregated
FOR XML PATH('')
), 1, 1, ''), '')
@@ -417,38 +379,40 @@ views AS (
SELECT
'[' +
(
SELECT
STUFF((
SELECT ',' + CONVERT(nvarchar(max),
JSON_QUERY(
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') +
'", "view_name": "' + COALESCE(REPLACE(v.name, '"', ''), '') +
'", "view_definition": "' +
CAST(
(
SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('')
) AS NVARCHAR(MAX)
) + '"}'
SELECT STUFF((
SELECT ',' + CONVERT(nvarchar(max),
JSON_QUERY(
N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
'", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
'", "view_definition": "' +
CAST(
(
SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('')
) AS NVARCHAR(MAX)
) + N'"}'
)
)
)
FROM
sys.views v
JOIN
sys.schemas s ON v.schema_id = s.schema_id
WHERE
s.name LIKE '%'
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
FROM
sys.views v
JOIN
sys.schemas s ON v.schema_id = s.schema_id
WHERE
s.name LIKE '%'
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
) + ']' AS all_views_json
)
SELECT JSON_QUERY(
N'{"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
N'{
"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
', "tables": ' + ISNULL((SELECT cast(all_objects_json as nvarchar(max)) FROM tbls), N'[]') +
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
', "database_name": "' + DB_NAME() + '"' +
', "version": ""}'
', "version": ""
}'
) AS metadata_json_to_import;`;
export const getSqlServerQuery = (

View File

@@ -10,14 +10,20 @@ export const fixMetadataJson = async (
return (
metadataJson
.trim()
// First unescape the JSON string
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\')
.replace(/^[^{]*/, '') // Remove everything before the first '{'
.replace(/}[^}]*$/, '}') // Remove everything after the last '}'
.replace(/:""([^"]+)""/g, ':"$1"') // Convert :""value"" to :"value"
.replace(/""(\w+)""/g, '"$1"') // Convert ""key"" to "key"
.replace(/^\s+|\s+$/g, '')
.replace(/^"|"$/g, '')
.replace(/^'|'$/g, '')
.replace(/""""/g, '""') // Remove Quadruple quotes from keys
.replace(/"""([^",}]+)"""/g, '"$1"') // Remove tripple quotes from keys
.replace(/""([^",}]+)""/g, '"$1"') // Remove double quotes from keys
/* eslint-disable-next-line no-useless-escape */
.replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings
.replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings

304
src/lib/dbml-import.ts Normal file
View File

@@ -0,0 +1,304 @@
import { Parser } from '@dbml/core';
import type { Diagram } from '@/lib/domain/diagram';
import { generateDiagramId, generateId } from '@/lib/utils';
import type { DBTable } from '@/lib/domain/db-table';
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
import type { DBField } from '@/lib/domain/db-field';
import type { DataType } from '@/lib/data/data-types/data-types';
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
import { randomColor } from '@/lib/colors';
import { DatabaseType } from '@/lib/domain/database-type';
interface DBMLTypeArgs {
length?: number;
precision?: number;
scale?: number;
values?: string[]; // For enum types
}
interface DBMLField {
name: string;
type: {
type_name: string;
args?: DBMLTypeArgs;
};
unique?: boolean;
pk?: boolean;
not_null?: boolean;
increment?: boolean;
}
interface DBMLIndexColumn {
value: string;
type?: string;
length?: number;
order?: 'asc' | 'desc';
}
interface DBMLIndex {
columns: string | (string | DBMLIndexColumn)[];
unique?: boolean;
name?: string;
}
interface DBMLTable {
name: string;
schema?: string | { name: string };
fields: DBMLField[];
indexes?: DBMLIndex[];
}
interface DBMLEndpoint {
tableName: string;
fieldNames: string[];
relation: string;
}
interface DBMLRef {
endpoints: [DBMLEndpoint, DBMLEndpoint];
}
const mapDBMLTypeToGenericType = (dbmlType: string): DataType => {
const normalizedType = dbmlType.toLowerCase().replace(/\(.*\)/, '');
const matchedType = genericDataTypes.find((t) => t.id === normalizedType);
if (matchedType) return matchedType;
const typeMap: Record<string, string> = {
int: 'integer',
varchar: 'varchar',
bool: 'boolean',
number: 'numeric',
string: 'varchar',
text: 'text',
timestamp: 'timestamp',
datetime: 'timestamp',
float: 'float',
double: 'double',
decimal: 'decimal',
bigint: 'bigint',
smallint: 'smallint',
char: 'char',
};
const mappedType = typeMap[normalizedType];
if (mappedType) {
const foundType = genericDataTypes.find((t) => t.id === mappedType);
if (foundType) return foundType;
}
return genericDataTypes.find((t) => t.id === 'varchar')!;
};
const determineCardinality = (
field: DBField,
referencedField: DBField
): { sourceCardinality: string; targetCardinality: string } => {
const isSourceUnique = field.unique || field.primaryKey;
const isTargetUnique = referencedField.unique || referencedField.primaryKey;
if (isSourceUnique && isTargetUnique) {
return { sourceCardinality: 'one', targetCardinality: 'one' };
} else if (isSourceUnique) {
return { sourceCardinality: 'one', targetCardinality: 'many' };
} else if (isTargetUnique) {
return { sourceCardinality: 'many', targetCardinality: 'one' };
} else {
return { sourceCardinality: 'many', targetCardinality: 'many' };
}
};
export const importDBMLToDiagram = async (
dbmlContent: string
): Promise<Diagram> => {
try {
const parser = new Parser();
const parsedData = parser.parse(dbmlContent, 'dbml');
const dbmlData = parsedData.schemas[0];
// Extract only the necessary data from the parsed DBML
const extractedData = {
tables: (dbmlData.tables as unknown as DBMLTable[]).map(
(table) => ({
name: table.name,
schema: table.schema,
fields: table.fields.map((field: DBMLField) => ({
name: field.name,
type: field.type,
unique: field.unique,
pk: field.pk,
not_null: field.not_null,
increment: field.increment,
})),
indexes:
table.indexes?.map((dbmlIndex) => {
let indexColumns: string[];
// Handle composite index case "(col1, col2)"
if (typeof dbmlIndex.columns === 'string') {
if (dbmlIndex.columns.includes('(')) {
// Composite index
const columnsStr =
dbmlIndex.columns.replace(/[()]/g, '');
indexColumns = columnsStr
.split(',')
.map((c) => c.trim());
} else {
// Single column
indexColumns = [dbmlIndex.columns.trim()];
}
} else {
// Handle array of columns
indexColumns = Array.isArray(dbmlIndex.columns)
? dbmlIndex.columns.map((col) =>
typeof col === 'object' &&
'value' in col
? (col.value as string).trim()
: (col as string).trim()
)
: [String(dbmlIndex.columns).trim()];
}
// Generate a consistent index name
const indexName =
dbmlIndex.name ||
`idx_${table.name}_${indexColumns.join('_')}`;
return {
columns: indexColumns,
unique: dbmlIndex.unique || false,
name: indexName,
};
}) || [],
})
),
refs: (dbmlData.refs as unknown as DBMLRef[]).map((ref) => ({
endpoints: (ref.endpoints as [DBMLEndpoint, DBMLEndpoint]).map(
(endpoint) => ({
tableName: endpoint.tableName,
fieldNames: endpoint.fieldNames,
relation: endpoint.relation,
})
),
})),
};
// Convert DBML tables to ChartDB table objects
const tables: DBTable[] = extractedData.tables.map((table, index) => {
const row = Math.floor(index / 4);
const col = index % 4;
const tableSpacing = 300;
// Create fields first so we have their IDs
const fields = table.fields.map((field) => ({
id: generateId(),
name: field.name.replace(/['"]/g, ''),
type: mapDBMLTypeToGenericType(field.type.type_name),
nullable: !field.not_null,
primaryKey: field.pk || false,
unique: field.unique || false,
createdAt: Date.now(),
}));
// Convert DBML indexes to ChartDB indexes
const indexes =
table.indexes?.map((dbmlIndex) => {
const fieldIds = dbmlIndex.columns.map((columnName) => {
const field = fields.find((f) => f.name === columnName);
if (!field) {
throw new Error(
`Index references non-existent column: ${columnName}`
);
}
return field.id;
});
return {
id: generateId(),
name:
dbmlIndex.name ||
`idx_${table.name}_${dbmlIndex.columns.join('_')}`,
fieldIds,
unique: dbmlIndex.unique || false,
createdAt: Date.now(),
};
}) || [];
return {
id: generateId(),
name: table.name.replace(/['"]/g, ''),
schema:
typeof table.schema === 'string'
? table.schema
: table.schema?.name || '',
order: index,
fields,
indexes,
x: col * tableSpacing,
y: row * tableSpacing,
color: randomColor(),
isView: false,
createdAt: Date.now(),
};
});
// Create relationships using the refs
const relationships: DBRelationship[] = extractedData.refs.map(
(ref) => {
const [source, target] = ref.endpoints;
const sourceTable = tables.find(
(t) =>
t.name === source.tableName.replace(/['"]/g, '') &&
(!source.tableName.includes('.') ||
t.schema === source.tableName.split('.')[0])
);
const targetTable = tables.find(
(t) =>
t.name === target.tableName.replace(/['"]/g, '') &&
(!target.tableName.includes('.') ||
t.schema === target.tableName.split('.')[0])
);
if (!sourceTable || !targetTable) {
throw new Error('Invalid relationship: tables not found');
}
const sourceField = sourceTable.fields.find(
(f) => f.name === source.fieldNames[0].replace(/['"]/g, '')
);
const targetField = targetTable.fields.find(
(f) => f.name === target.fieldNames[0].replace(/['"]/g, '')
);
if (!sourceField || !targetField) {
throw new Error('Invalid relationship: fields not found');
}
const { sourceCardinality, targetCardinality } =
determineCardinality(sourceField, targetField);
return {
id: generateId(),
name: `${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`,
sourceSchema: sourceTable.schema,
targetSchema: targetTable.schema,
sourceTableId: sourceTable.id,
targetTableId: targetTable.id,
sourceFieldId: sourceField.id,
targetFieldId: targetField.id,
sourceCardinality: sourceCardinality as Cardinality,
targetCardinality: targetCardinality as Cardinality,
createdAt: Date.now(),
};
}
);
return {
id: generateDiagramId(),
name: 'DBML Import',
databaseType: DatabaseType.GENERIC,
tables,
relationships,
createdAt: new Date(),
updatedAt: new Date(),
};
} catch (error) {
console.error('DBML parsing error:', error);
throw error;
}
};

View File

@@ -95,7 +95,7 @@ export const createFieldsFromMetadata = ({
nullable: col.nullable,
...(col.character_maximum_length &&
col.character_maximum_length !== 'null'
? { character_maximum_length: col.character_maximum_length }
? { characterMaximumLength: col.character_maximum_length }
: {}),
...(col.precision?.precision
? { precision: col.precision.precision }

View File

@@ -1,5 +1,11 @@
export const OPENAI_API_KEY: string = import.meta.env.VITE_OPENAI_API_KEY;
export const OPENAI_API_ENDPOINT: string = import.meta.env
.VITE_OPENAI_API_ENDPOINT;
export const LLM_MODEL_NAME: string = import.meta.env.VITE_LLM_MODEL_NAME;
export const IS_CHARTDB_IO: boolean =
import.meta.env.VITE_IS_CHARTDB_IO === 'true';
export const APP_URL: string = import.meta.env.VITE_APP_URL;
export const HOST_URL: string = import.meta.env.VITE_HOST_URL ?? '';
export const HIDE_BUCKLE_DOT_DEV: boolean =
(window?.env?.HIDE_BUCKLE_DOT_DEV ??
import.meta.env.VITE_HIDE_BUCKLE_DOT_DEV) === 'true';

View File

@@ -45,10 +45,7 @@ import { Badge } from '@/components/badge/badge';
import { useTheme } from '@/hooks/use-theme';
import { useTranslation } from 'react-i18next';
import type { DBTable } from '@/lib/domain/db-table';
import {
adjustTablePositions,
shouldShowTablesBySchemaFilter,
} from '@/lib/domain/db-table';
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
import { useLocalConfig } from '@/hooks/use-local-config';
import {
Tooltip,
@@ -64,7 +61,7 @@ import {
findTableOverlapping,
} from './canvas-utils';
import type { Graph } from '@/lib/graph';
import { createGraph, removeVertex } from '@/lib/graph';
import { removeVertex } from '@/lib/graph';
import type { ChartDBEvent } from '@/context/chartdb-context/chartdb-context';
import { cn, debounce, getOperatingSystem } from '@/lib/utils';
import type { DependencyEdgeType } from './dependency-edge';
@@ -76,6 +73,7 @@ import {
} from './table-node/table-node-dependency-indicator';
import { DatabaseType } from '@/lib/domain/database-type';
import { useAlert } from '@/context/alert-context/alert-context';
import { useCanvas } from '@/hooks/use-canvas';
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
@@ -105,12 +103,10 @@ const tableToTableNode = (
export interface CanvasProps {
initialTables: DBTable[];
readonly?: boolean;
}
export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
const { getEdge, getInternalNode, fitView, getEdges, getNode } =
useReactFlow();
export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
string[]
@@ -130,6 +126,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
filteredSchemas,
events,
dependencies,
readonly,
} = useChartDB();
const { showSidePanel } = useLayout();
const { effectiveTheme } = useTheme();
@@ -140,10 +137,10 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
const nodeTypes = useMemo(() => ({ table: TableNode }), []);
const [highlightOverlappingTables, setHighlightOverlappingTables] =
useState(false);
const { reorderTables, fitView, setOverlapGraph, overlapGraph } =
useCanvas();
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
const [overlapGraph, setOverlapGraph] =
useState<Graph<string>>(createGraph());
const [nodes, setNodes, onNodesChange] = useNodesState<TableNodeType>(
initialTables.map((table) => tableToTableNode(table, filteredSchemas))
@@ -345,7 +342,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
}, 500)();
prevFilteredSchemas.current = filteredSchemas;
}
}, [filteredSchemas, fitView, tables]);
}, [filteredSchemas, fitView, tables, setOverlapGraph]);
const onConnectHandler = useCallback(
async (params: AddEdgeParams) => {
@@ -657,33 +654,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
const isLoadingDOM =
tables.length > 0 ? !getInternalNode(tables[0].id) : false;
const reorderTables = useCallback(() => {
const newTables = adjustTablePositions({
relationships,
tables: tables.filter((table) =>
shouldShowTablesBySchemaFilter(table, filteredSchemas)
),
mode: 'all', // Use 'all' mode for manual reordering
});
const updatedOverlapGraph = findOverlappingTables({
tables: newTables,
});
updateTablesState((currentTables) =>
currentTables.map((table) => {
const newTable = newTables.find((t) => t.id === table.id);
return {
id: table.id,
x: newTable?.x ?? table.x,
y: newTable?.y ?? table.y,
};
})
);
setOverlapGraph(updatedOverlapGraph);
}, [filteredSchemas, relationships, tables, updateTablesState]);
const showReorderConfirmation = useCallback(() => {
showAlert({
title: t('reorder_diagram_alert.title'),
@@ -712,7 +682,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
return (
<CanvasContextMenu>
<div className="relative flex h-full">
<div className="relative flex h-full" id="canvas">
<ReactFlow
colorMode={effectiveTheme}
className="canvas-cursor-default nodes-animated"

View File

@@ -0,0 +1,86 @@
import { useCallback, useState } from 'react';
import { getTableDimensions } from '../canvas-utils';
import type { TableNodeType } from '../table-node/table-node';
import { useOnViewportChange, useReactFlow } from '@xyflow/react';
import { useDebounce } from '@/hooks/use-debounce-v2';
export const useIsLostInCanvas = () => {
const { getNodes, getViewport } = useReactFlow();
const [noTablesVisible, setNoTablesVisible] = useState<boolean>(false);
// Check if any tables are visible in the current viewport
const checkVisibleTables = useCallback(() => {
const nodes = getNodes();
const viewport = getViewport();
// If there are no nodes at all, don't highlight the button
if (nodes.length === 0) {
setNoTablesVisible(false);
return;
}
// Count visible (not hidden) nodes
const visibleNodes = nodes.filter((node) => !node.hidden);
// If there are no visible nodes at all, don't highlight the button
if (visibleNodes.length === 0) {
setNoTablesVisible(false);
return;
}
// Calculate viewport boundaries
const viewportLeft = -viewport.x / viewport.zoom;
const viewportTop = -viewport.y / viewport.zoom;
const width =
document.getElementById('canvas')?.clientWidth || window.innerWidth;
const height =
document.getElementById('canvas')?.clientHeight ||
window.innerHeight;
const viewportRight = viewportLeft + width / viewport.zoom;
const viewportBottom = viewportTop + height / viewport.zoom;
// Check if any node is visible in the viewport
const anyNodeVisible = visibleNodes.some((node) => {
let nodeWidth = node.width || 0;
let nodeHeight = node.height || 0;
if (node.type === 'table' && node.data?.table) {
const tableNodeType = node as TableNodeType;
const dimensions = getTableDimensions(tableNodeType.data.table);
nodeWidth = dimensions.width;
nodeHeight = dimensions.height;
}
// Node boundaries
const nodeLeft = node.position.x;
const nodeTop = node.position.y;
const nodeRight = nodeLeft + nodeWidth;
const nodeBottom = nodeTop + nodeHeight;
return (
nodeRight >= viewportLeft &&
nodeLeft <= viewportRight &&
nodeBottom >= viewportTop &&
nodeTop <= viewportBottom
);
});
// Only set to true if there are tables but none are visible
setNoTablesVisible(!anyNodeVisible);
}, [getNodes, getViewport]);
// Create a debounced version of checkVisibleTables
const debouncedCheckVisibleTables = useDebounce(checkVisibleTables, 1000);
useOnViewportChange({
onEnd: () => {
debouncedCheckVisibleTables();
},
});
return {
isLostInCanvas: noTablesVisible,
};
};

View File

@@ -7,6 +7,7 @@ import { useChartDB } from '@/hooks/use-chartdb';
import { useLayout } from '@/hooks/use-layout';
import { cn } from '@/lib/utils';
import { getCardinalityMarkerId } from './canvas-utils';
import { useDiff } from '@/context/diff-context/use-diff';
export type RelationshipEdgeType = Edge<
{
@@ -29,6 +30,7 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
}) => {
const { getInternalNode, getEdge } = useReactFlow();
const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
const { relationships } = useChartDB();
@@ -149,6 +151,25 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
}),
[relationship?.targetCardinality, selected, targetSide]
);
const isDiffNewRelationship = useMemo(
() =>
relationship?.id
? checkIfNewRelationship({ relationshipId: relationship.id })
: false,
[checkIfNewRelationship, relationship?.id]
);
const isDiffRelationshipRemoved = useMemo(
() =>
relationship?.id
? checkIfRelationshipRemoved({
relationshipId: relationship.id,
})
: false,
[checkIfRelationshipRemoved, relationship?.id]
);
return (
<>
<path
@@ -160,6 +181,10 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
className={cn([
'react-flow__edge-path',
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
{
'!stroke-green-500': isDiffNewRelationship,
'!stroke-red-500': isDiffRelationshipRemoved,
},
])}
onClick={(e) => {
if (e.detail === 2) {

View File

@@ -9,9 +9,10 @@ import { useChartDB } from '@/hooks/use-chartdb';
import { useLayout } from '@/hooks/use-layout';
import { cloneTable } from '@/lib/clone';
import type { DBTable } from '@/lib/domain/db-table';
import { Copy, Pencil, Trash2 } from 'lucide-react';
import { Copy, Pencil, Trash2, Workflow } from 'lucide-react';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDialog } from '@/hooks/use-dialog';
export interface TableNodeContextMenuProps {
table: DBTable;
@@ -24,6 +25,7 @@ export const TableNodeContextMenu: React.FC<
const { openTableFromSidebar } = useLayout();
const { t } = useTranslation();
const { isMd: isDesktop } = useBreakpoint('md');
const { openCreateRelationshipDialog } = useDialog();
const duplicateTableHandler = useCallback(() => {
const clonedTable = cloneTable(table);
@@ -43,6 +45,12 @@ export const TableNodeContextMenu: React.FC<
removeTable(table.id);
}, [removeTable, table.id]);
const addRelationshipHandler = useCallback(() => {
openCreateRelationshipDialog({
sourceTableId: table.id,
});
}, [openCreateRelationshipDialog, table.id]);
if (!isDesktop || readonly) {
return <>{children}</>;
}
@@ -64,6 +72,13 @@ export const TableNodeContextMenu: React.FC<
<span>{t('table_node_context_menu.duplicate_table')}</span>
<Copy className="size-3.5" />
</ContextMenuItem>
<ContextMenuItem
onClick={addRelationshipHandler}
className="flex justify-between gap-3"
>
<span>{t('table_node_context_menu.add_relationship')}</span>
<Workflow className="size-3.5" />
</ContextMenuItem>
<ContextMenuItem
onClick={removeTableHandler}
className="flex justify-between gap-3"

View File

@@ -1,4 +1,10 @@
import React, { useEffect, useMemo, useRef } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Handle,
Position,
@@ -6,7 +12,15 @@ import {
useUpdateNodeInternals,
} from '@xyflow/react';
import { Button } from '@/components/button/button';
import { KeyRound, MessageCircleMore, Trash2 } from 'lucide-react';
import {
Check,
KeyRound,
MessageCircleMore,
SquareDot,
SquareMinus,
SquarePlus,
Trash2,
} from 'lucide-react';
import type { DBField } from '@/lib/domain/db-field';
import { useChartDB } from '@/hooks/use-chartdb';
import { cn } from '@/lib/utils';
@@ -15,6 +29,9 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useClickAway, useKeyPressEvent } from 'react-use';
import { Input } from '@/components/input/input';
import { useDiff } from '@/context/diff-context/use-diff';
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
@@ -31,7 +48,12 @@ export interface TableNodeFieldProps {
export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
({ field, focused, tableNodeId, highlighted, visible, isConnectable }) => {
const { removeField, relationships, readonly } = useChartDB();
const { removeField, relationships, readonly, updateField } =
useChartDB();
const [editMode, setEditMode] = useState(false);
const [fieldName, setFieldName] = useState(field.name);
const inputRef = React.useRef<HTMLInputElement>(null);
const updateNodeInternals = useUpdateNodeInternals();
const connection = useConnection();
const isTarget = useMemo(
@@ -65,15 +87,84 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
}
}, [tableNodeId, updateNodeInternals, numberOfEdgesToField]);
const editFieldName = useCallback(() => {
if (!editMode) return;
if (fieldName.trim()) {
updateField(tableNodeId, field.id, { name: fieldName.trim() });
}
setEditMode(false);
}, [fieldName, field.id, updateField, editMode, tableNodeId]);
const abortEdit = useCallback(() => {
setEditMode(false);
setFieldName(field.name);
}, [field.name]);
useClickAway(inputRef, editFieldName);
useKeyPressEvent('Enter', editFieldName);
useKeyPressEvent('Escape', abortEdit);
const {
checkIfFieldRemoved,
checkIfNewField,
getFieldNewName,
getFieldNewType,
checkIfFieldHasChange,
} = useDiff();
const isDiffFieldRemoved = useMemo(
() => checkIfFieldRemoved({ fieldId: field.id }),
[checkIfFieldRemoved, field.id]
);
const isDiffNewField = useMemo(
() => checkIfNewField({ fieldId: field.id }),
[checkIfNewField, field.id]
);
const fieldDiffChangedName = useMemo(
() => getFieldNewName({ fieldId: field.id }),
[getFieldNewName, field.id]
);
const fieldDiffChangedType = useMemo(
() => getFieldNewType({ fieldId: field.id }),
[getFieldNewType, field.id]
);
const isDiffFieldChanged = useMemo(
() =>
checkIfFieldHasChange({
fieldId: field.id,
tableId: tableNodeId,
}),
[checkIfFieldHasChange, field.id, tableNodeId]
);
const enterEditMode = (e: React.MouseEvent) => {
e.stopPropagation();
setEditMode(true);
};
return (
<div
className={`group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 ${
highlighted ? 'bg-pink-100 dark:bg-pink-900' : ''
} transition-all duration-200 ease-in-out ${
visible
? 'max-h-8 opacity-100'
: 'z-0 max-h-0 overflow-hidden opacity-0'
}`}
className={cn(
'group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800',
'transition-all duration-200 ease-in-out',
{
'bg-pink-100 dark:bg-pink-900': highlighted,
'max-h-8 opacity-100': visible,
'z-0 max-h-0 overflow-hidden opacity-0': !visible,
'bg-sky-200 dark:bg-sky-800 hover:bg-sky-100 dark:hover:bg-sky-900 border-sky-300 dark:border-sky-700':
isDiffFieldChanged &&
!isDiffFieldRemoved &&
!isDiffNewField,
'bg-red-200 dark:bg-red-800 hover:bg-red-100 dark:hover:bg-red-900 border-red-300 dark:border-red-700':
isDiffFieldRemoved,
'bg-green-200 dark:bg-green-800 hover:bg-green-100 dark:hover:bg-green-900 border-green-300 dark:border-green-700':
isDiffNewField,
}
)}
>
{isConnectable ? (
<>
@@ -122,11 +213,71 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
'flex items-center gap-1 truncate text-left',
{
'font-semibold': field.primaryKey || field.unique,
'w-full': editMode,
}
)}
>
<span className="truncate">{field.name}</span>
{field.comments ? (
{isDiffFieldRemoved ? (
<SquareMinus className="size-3.5 text-red-800 dark:text-red-200" />
) : isDiffNewField ? (
<SquarePlus className="size-3.5 text-green-800 dark:text-green-200" />
) : isDiffFieldChanged ? (
<SquareDot className="size-3.5 shrink-0 text-sky-800 dark:text-sky-200" />
) : null}
{editMode && !readonly ? (
<>
<Input
ref={inputRef}
onBlur={editFieldName}
placeholder={field.name}
autoFocus
type="text"
value={fieldName}
onClick={(e) => e.stopPropagation()}
onChange={(e) => setFieldName(e.target.value)}
className="h-5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900"
/>
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={editFieldName}
>
<Check className="size-4" />
</Button>
</>
) : (
// <span
// className="truncate"
// onClick={readonly ? undefined : enterEditMode}
// >
// {field.name}
// </span>
<span
className={cn('truncate', {
'text-red-800 font-normal dark:text-red-200':
isDiffFieldRemoved,
'text-green-800 font-normal dark:text-green-200':
isDiffNewField,
'text-sky-800 font-normal dark:text-sky-200':
isDiffFieldChanged &&
!isDiffFieldRemoved &&
!isDiffNewField,
})}
onDoubleClick={enterEditMode}
>
{fieldDiffChangedName ? (
<>
{field.name}{' '}
<span className="font-medium"></span>{' '}
{fieldDiffChangedName}
</>
) : (
field.name
)}
</span>
)}
{/* <span className="truncate">{field.name}</span> */}
{field.comments && !editMode ? (
<Tooltip>
<TooltipTrigger asChild>
<div className="shrink-0 cursor-pointer text-muted-foreground">
@@ -137,42 +288,75 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
</Tooltip>
) : null}
</div>
<div className="flex max-w-[35%] justify-end gap-1.5 truncate hover:shrink-0">
{field.primaryKey ? (
{editMode ? null : (
<div className="flex max-w-[35%] justify-end gap-1.5 truncate hover:shrink-0">
{field.primaryKey ? (
<div
className={cn(
'text-muted-foreground',
!readonly ? 'group-hover:hidden' : '',
isDiffFieldRemoved
? 'text-red-800 dark:text-red-200'
: '',
isDiffNewField
? 'text-green-800 dark:text-green-200'
: '',
isDiffFieldChanged &&
!isDiffFieldRemoved &&
!isDiffNewField
? 'text-sky-800 dark:text-sky-200'
: ''
)}
>
<KeyRound size={14} />
</div>
) : null}
<div
className={cn(
'text-muted-foreground',
!readonly ? 'group-hover:hidden' : ''
'content-center truncate text-right text-xs text-muted-foreground',
!readonly ? 'group-hover:hidden' : '',
isDiffFieldRemoved
? 'text-red-800 dark:text-red-200'
: '',
isDiffNewField
? 'text-green-800 dark:text-green-200'
: '',
isDiffFieldChanged &&
!isDiffFieldRemoved &&
!isDiffNewField
? 'text-sky-800 dark:text-sky-200'
: ''
)}
>
<KeyRound size={14} />
{fieldDiffChangedType ? (
<>
<span className="line-through">
{field.type.name.split(' ')[0]}
</span>{' '}
{fieldDiffChangedType.name.split(' ')[0]}
</>
) : (
field.type.name.split(' ')[0]
)}
{field.nullable ? '?' : ''}
</div>
) : null}
<div
className={cn(
'content-center truncate text-right text-xs text-muted-foreground shrink-0',
!readonly ? 'group-hover:hidden' : ''
{readonly ? null : (
<div className="hidden flex-row group-hover:flex">
<Button
variant="ghost"
className="size-6 p-0 hover:bg-primary-foreground"
onClick={(e) => {
e.stopPropagation();
removeField(tableNodeId, field.id);
}}
>
<Trash2 className="size-3.5 text-red-700" />
</Button>
</div>
)}
>
{field.type.name}
{field.nullable ? '?' : ''}
</div>
{readonly ? null : (
<div className="hidden flex-row group-hover:flex">
<Button
variant="ghost"
className="size-6 p-0 hover:bg-primary-foreground"
onClick={(e) => {
e.stopPropagation();
removeField(tableNodeId, field.id);
}}
>
<Trash2 className="size-3.5 text-red-700" />
</Button>
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { cn } from '@/lib/utils';
import React from 'react';
export interface TableNodeStatusProps {
status: 'new' | 'changed' | 'removed' | 'none';
}
export const TableNodeStatus: React.FC<TableNodeStatusProps> = ({ status }) => {
if (status === 'none') {
return null;
}
return (
<div className="absolute left-1/2 top-0 z-10 -translate-x-1/2 -translate-y-1/2">
<span
className={cn(
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium text-white',
{
'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100':
status === 'new',
'bg-sky-100 text-sky-800 dark:bg-sky-800 dark:text-sky-100':
status === 'changed',
'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100':
status === 'removed',
}
)}
>
{status === 'new'
? 'New'
: status === 'changed'
? 'Modified'
: 'Deleted'}
</span>
</div>
);
};

View File

@@ -5,10 +5,14 @@ import { Button } from '@/components/button/button';
import {
ChevronsLeftRight,
ChevronsRightLeft,
Pencil,
Table2,
ChevronDown,
ChevronUp,
Check,
CircleDotDashed,
SquareDot,
SquarePlus,
SquareMinus,
} from 'lucide-react';
import { Label } from '@/components/label/label';
import type { DBTable } from '@/lib/domain/db-table';
@@ -22,6 +26,15 @@ import { TableNodeContextMenu } from './table-node-context-menu';
import { cn } from '@/lib/utils';
import { TableNodeDependencyIndicator } from './table-node-dependency-indicator';
import type { EdgeType } from '../canvas';
import { Input } from '@/components/input/input';
import { useClickAway, useKeyPressEvent } from 'react-use';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useDiff } from '@/context/diff-context/use-diff';
import { TableNodeStatus } from './table-node-status/table-node-status';
export type TableNodeType = Node<
{
@@ -49,6 +62,38 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
const { openTableFromSidebar, selectSidebarSection } = useLayout();
const [expanded, setExpanded] = useState(false);
const { t } = useTranslation();
const [editMode, setEditMode] = useState(false);
const [tableName, setTableName] = useState(table.name);
const inputRef = React.useRef<HTMLInputElement>(null);
const {
getTableNewName,
checkIfTableHasChange,
checkIfNewTable,
checkIfTableRemoved,
} = useDiff();
const fields = useMemo(() => table.fields, [table.fields]);
const tableChangedName = useMemo(
() => getTableNewName({ tableId: table.id }),
[getTableNewName, table.id]
);
const isDiffTableChanged = useMemo(
() => checkIfTableHasChange({ tableId: table.id }),
[checkIfTableHasChange, table.id]
);
const isDiffNewTable = useMemo(
() => checkIfNewTable({ tableId: table.id }),
[checkIfNewTable, table.id]
);
const isDiffTableRemoved = useMemo(
() => checkIfTableRemoved({ tableId: table.id }),
[checkIfTableRemoved, table.id]
);
const selectedRelEdges = edges.filter(
(edge) =>
@@ -98,13 +143,13 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
const visibleFields = useMemo(() => {
if (expanded) {
return table.fields;
return fields;
}
const mustDisplayedFields = table.fields.filter((field: DBField) =>
const mustDisplayedFields = fields.filter((field: DBField) =>
isMustDisplayedField(field)
);
const nonMustDisplayedFields = table.fields.filter(
const nonMustDisplayedFields = fields.filter(
(field: DBField) => !isMustDisplayedField(field)
);
@@ -122,8 +167,30 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
return [
...visibleMustDisplayedFields,
...visibleNonMustDisplayedFields,
].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
}, [expanded, table.fields, isMustDisplayedField]);
].sort((a, b) => fields.indexOf(a) - fields.indexOf(b));
}, [expanded, fields, isMustDisplayedField]);
const editTableName = useCallback(() => {
if (!editMode) return;
if (tableName.trim()) {
updateTable(table.id, { name: tableName.trim() });
}
setEditMode(false);
}, [tableName, table.id, updateTable, editMode]);
const abortEdit = useCallback(() => {
setEditMode(false);
setTableName(table.name);
}, [table.name]);
useClickAway(inputRef, editTableName);
useKeyPressEvent('Enter', editTableName);
useKeyPressEvent('Escape', abortEdit);
const enterEditMode = (e: React.MouseEvent) => {
e.stopPropagation();
setEditMode(true);
};
return (
<TableNodeContextMenu table={table}>
@@ -141,6 +208,17 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
: '',
highlightOverlappingTables && isOverlapping
? 'animate-scale-2'
: '',
isDiffTableChanged &&
!isDiffNewTable &&
!isDiffTableRemoved
? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]'
: '',
isDiffNewTable
? 'outline outline-[3px] outline-green-500 dark:outline-green-900 outline-offset-[5px]'
: '',
isDiffTableRemoved
? 'outline outline-[3px] outline-red-500 dark:outline-red-900 outline-offset-[5px]'
: ''
)}
onClick={(e) => {
@@ -161,53 +239,163 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
table={table}
focused={focused}
/>
{/* Badge added here */}
<TableNodeStatus
status={
isDiffNewTable
? 'new'
: isDiffTableRemoved
? 'removed'
: isDiffTableChanged
? 'changed'
: 'none'
}
/>
<div
className="h-2 rounded-t-[6px]"
style={{ backgroundColor: table.color }}
></div>
<div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
<div className="flex min-w-0 flex-1 items-center gap-2">
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
<Label className="truncate text-sm font-bold">
{table.name}
</Label>
{isDiffNewTable ? (
<Tooltip>
<TooltipTrigger asChild>
<SquarePlus
className="size-3.5 shrink-0 text-green-600"
strokeWidth={2.5}
/>
</TooltipTrigger>
<TooltipContent>New Table</TooltipContent>
</Tooltip>
) : isDiffTableRemoved ? (
<Tooltip>
<TooltipTrigger asChild>
<SquareMinus
className="size-3.5 shrink-0 text-red-600"
strokeWidth={2.5}
/>
</TooltipTrigger>
<TooltipContent>
Table Removed
</TooltipContent>
</Tooltip>
) : isDiffTableChanged ? (
<Tooltip>
<TooltipTrigger asChild>
<SquareDot
className="size-3.5 shrink-0 text-sky-600"
strokeWidth={2.5}
/>
</TooltipTrigger>
<TooltipContent>
Table Changed
</TooltipContent>
</Tooltip>
) : (
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
)}
{tableChangedName ? (
<Label className="flex h-5 items-center justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
<span className="truncate">
{table.name}
</span>
<span className="mx-1 font-semibold">
</span>
<span className="truncate">
{tableChangedName}
</span>
</Label>
) : isDiffNewTable ? (
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-green-200 px-2 py-0.5 text-sm font-normal text-green-900 dark:bg-green-800 dark:text-green-200">
{table.name}
</Label>
) : isDiffTableRemoved ? (
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-red-200 px-2 py-0.5 text-sm font-normal text-red-900 dark:bg-red-800 dark:text-red-200">
{table.name}
</Label>
) : isDiffTableChanged ? (
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
{table.name}
</Label>
) : editMode && !readonly ? (
<>
<Input
ref={inputRef}
onBlur={editTableName}
placeholder={table.name}
autoFocus
type="text"
value={tableName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setTableName(e.target.value)
}
className="h-6 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900"
/>
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={editTableName}
>
<Check className="size-4" />
</Button>
</>
) : (
<Tooltip>
<TooltipTrigger asChild>
<Label
className="text-editable truncate px-2 py-0.5 text-sm font-bold"
onDoubleClick={enterEditMode}
>
{table.name}
</Label>
</TooltipTrigger>
<TooltipContent>
{t('tool_tips.double_click_to_edit')}
</TooltipContent>
</Tooltip>
)}
</div>
<div className="hidden shrink-0 flex-row group-hover:flex">
{readonly ? null : (
{readonly || editMode ? null : (
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={openTableInEditor}
>
<Pencil className="size-4" />
<CircleDotDashed className="size-4" />
</Button>
)}
{editMode ? null : (
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={
table.width !== MAX_TABLE_SIZE
? expandTable
: shrinkTable
}
>
{table.width !== MAX_TABLE_SIZE ? (
<ChevronsLeftRight className="size-4" />
) : (
<ChevronsRightLeft className="size-4" />
)}
</Button>
)}
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={
table.width !== MAX_TABLE_SIZE
? expandTable
: shrinkTable
}
>
{table.width !== MAX_TABLE_SIZE ? (
<ChevronsLeftRight className="size-4" />
) : (
<ChevronsRightLeft className="size-4" />
)}
</Button>
</div>
</div>
<div
className="transition-[max-height] duration-200 ease-in-out"
style={{
maxHeight: expanded
? `${table.fields.length * 2}rem` // h-8 per field
? `${fields.length * 2}rem` // h-8 per field
: `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field
}}
>
{table.fields.map((field: DBField) => (
{fields.map((field: DBField) => (
<TableNodeField
key={field.id}
focused={focused}
@@ -225,7 +413,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
/>
))}
</div>
{table.fields.length > TABLE_MINIMIZED_FIELDS && (
{fields.length > TABLE_MINIMIZED_FIELDS && (
<div
className="z-10 flex h-8 cursor-pointer items-center justify-center rounded-b-md border-t text-xs text-muted-foreground transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800"
onClick={toggleExpand}

View File

@@ -1,17 +1,22 @@
import React from 'react';
import type { ButtonProps } from '@/components/button/button';
import { Button } from '@/components/button/button';
import { cn } from '@/lib/utils';
export const ToolbarButton = React.forwardRef<
React.ElementRef<typeof Button>,
ButtonProps
>((props, ref) => {
const { className, ...rest } = props;
return (
<Button
ref={ref}
variant="ghost"
className={'w-[36px] p-2 hover:bg-primary-foreground'}
{...props}
className={cn(
'w-[36px] p-2 hover:bg-primary-foreground',
className
)}
{...rest}
/>
);
});

View File

@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next';
import { Button } from '@/components/button/button';
import { keyboardShortcutsForOS } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
import { KeyboardShortcutAction } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
import { useIsLostInCanvas } from '../hooks/use-is-lost-in-canvas';
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
@@ -28,6 +29,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
const { redo, undo, hasRedo, hasUndo } = useHistory();
const { getZoom, zoomIn, zoomOut, fitView } = useReactFlow();
const [zoom, setZoom] = useState<string>(convertToPercentage(getZoom()));
const { isLostInCanvas } = useIsLostInCanvas();
useOnViewportChange({
onChange: ({ zoom }) => {
setZoom(convertToPercentage(zoom));
@@ -93,7 +96,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
<Tooltip>
<TooltipTrigger asChild>
<span>
<ToolbarButton onClick={showAll}>
<ToolbarButton
onClick={showAll}
className={
isLostInCanvas
? 'bg-pink-500 text-white hover:bg-pink-600 hover:text-white'
: ''
}
>
<Scan />
</ToolbarButton>
</span>

View File

@@ -1,22 +1,12 @@
import React, {
Suspense,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import React, { Suspense, useCallback, useEffect, useRef } from 'react';
import { TopNavbar } from './top-navbar/top-navbar';
import { useNavigate, useParams } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
import { useParams } from 'react-router-dom';
import { useChartDB } from '@/hooks/use-chartdb';
import { useDialog } from '@/hooks/use-dialog';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import { Toaster } from '@/components/toast/toaster';
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useLayout } from '@/hooks/use-layout';
import { useToast } from '@/components/toast/use-toast';
import type { Diagram } from '@/lib/domain/diagram';
import { ToastAction } from '@/components/toast/toast';
import { useLocalConfig } from '@/hooks/use-local-config';
import { useTranslation } from 'react-i18next';
@@ -35,16 +25,15 @@ import { DialogProvider } from '@/context/dialog-context/dialog-provider';
import { KeyboardShortcutsProvider } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts-provider';
import { Spinner } from '@/components/spinner/spinner';
import { Helmet } from 'react-helmet-async';
import { useStorage } from '@/hooks/use-storage';
import { AlertProvider } from '@/context/alert-context/alert-provider';
import { CanvasProvider } from '@/context/canvas-context/canvas-provider';
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
import { useDiagramLoader } from './use-diagram-loader';
import { DiffProvider } from '@/context/diff-context/diff-provider';
const OPEN_STAR_US_AFTER_SECONDS = 30;
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
const OPEN_BUCKLE_AFTER_SECONDS = 60;
const SHOW_BUCKLE_AGAIN_AFTER_DAYS = 1;
const SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS = 7;
export const EditorDesktopLayoutLazy = React.lazy(
() => import('./editor-desktop-layout')
);
@@ -54,104 +43,28 @@ export const EditorMobileLayoutLazy = React.lazy(
);
const EditorPageComponent: React.FC = () => {
const {
loadDiagram,
diagramName,
currentDiagram,
schemas,
filteredSchemas,
} = useChartDB();
const { diagramName, currentDiagram, schemas, filteredSchemas } =
useChartDB();
const { openSelectSchema, showSidePanel } = useLayout();
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
const { showLoader, hideLoader } = useFullScreenLoader();
const { openCreateDiagramDialog, openStarUsDialog, openBuckleDialog } =
useDialog();
const { openStarUsDialog } = useDialog();
const { diagramId } = useParams<{ diagramId: string }>();
const { config, updateConfig } = useConfig();
const navigate = useNavigate();
const { isMd: isDesktop } = useBreakpoint('md');
const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>();
const {
hideMultiSchemaNotification,
setHideMultiSchemaNotification,
starUsDialogLastOpen,
setStarUsDialogLastOpen,
githubRepoOpened,
setBuckleDialogLastOpen,
buckleDialogLastOpen,
buckleWaitlistOpened,
} = useLocalConfig();
const { toast } = useToast();
const { t } = useTranslation();
const { listDiagrams } = useStorage();
const { initialDiagram } = useDiagramLoader();
useEffect(() => {
if (!config) {
if (HIDE_BUCKLE_DOT_DEV) {
return;
}
if (currentDiagram?.id === diagramId) {
return;
}
const loadDefaultDiagram = async () => {
if (diagramId) {
setInitialDiagram(undefined);
showLoader();
resetRedoStack();
resetUndoStack();
const diagram = await loadDiagram(diagramId);
if (!diagram) {
if (currentDiagram?.id) {
await updateConfig({
defaultDiagramId: currentDiagram.id,
});
navigate(`/diagrams/${currentDiagram.id}`);
} else {
navigate('/');
}
}
setInitialDiagram(diagram);
hideLoader();
} else if (!diagramId && config.defaultDiagramId) {
const diagram = await loadDiagram(config.defaultDiagramId);
if (!diagram) {
await updateConfig({
defaultDiagramId: '',
});
navigate('/');
} else {
navigate(`/diagrams/${config.defaultDiagramId}`);
}
} else {
const diagrams = await listDiagrams();
if (diagrams.length > 0) {
const defaultDiagramId = diagrams[0].id;
await updateConfig({ defaultDiagramId });
navigate(`/diagrams/${defaultDiagramId}`);
} else {
openCreateDiagramDialog();
}
}
};
loadDefaultDiagram();
}, [
diagramId,
openCreateDiagramDialog,
config,
navigate,
listDiagrams,
loadDiagram,
resetRedoStack,
resetUndoStack,
hideLoader,
showLoader,
currentDiagram?.id,
updateConfig,
]);
useEffect(() => {
if (!currentDiagram?.id || githubRepoOpened) {
return;
}
@@ -172,33 +85,6 @@ const EditorPageComponent: React.FC = () => {
starUsDialogLastOpen,
]);
useEffect(() => {
if (!currentDiagram?.id) {
return;
}
if (
new Date().getTime() - buckleDialogLastOpen >
1000 *
60 *
60 *
24 *
(buckleWaitlistOpened
? SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS
: SHOW_BUCKLE_AGAIN_AFTER_DAYS)
) {
const lastOpen = new Date().getTime();
setBuckleDialogLastOpen(lastOpen);
setTimeout(openBuckleDialog, OPEN_BUCKLE_AFTER_SECONDS * 1000);
}
}, [
currentDiagram?.id,
buckleWaitlistOpened,
openBuckleDialog,
setBuckleDialogLastOpen,
buckleDialogLastOpen,
]);
const lastDiagramId = useRef<string>('');
const handleChangeSchema = useCallback(async () => {
@@ -312,21 +198,25 @@ export const EditorPage: React.FC = () => (
<StorageProvider>
<ConfigProvider>
<RedoUndoStackProvider>
<ChartDBProvider>
<HistoryProvider>
<ReactFlowProvider>
<ExportImageProvider>
<AlertProvider>
<DialogProvider>
<KeyboardShortcutsProvider>
<EditorPageComponent />
</KeyboardShortcutsProvider>
</DialogProvider>
</AlertProvider>
</ExportImageProvider>
</ReactFlowProvider>
</HistoryProvider>
</ChartDBProvider>
<DiffProvider>
<ChartDBProvider>
<HistoryProvider>
<ReactFlowProvider>
<CanvasProvider>
<ExportImageProvider>
<AlertProvider>
<DialogProvider>
<KeyboardShortcutsProvider>
<EditorPageComponent />
</KeyboardShortcutsProvider>
</DialogProvider>
</AlertProvider>
</ExportImageProvider>
</CanvasProvider>
</ReactFlowProvider>
</HistoryProvider>
</ChartDBProvider>
</DiffProvider>
</RedoUndoStackProvider>
</ConfigProvider>
</StorageProvider>

View File

@@ -98,10 +98,16 @@ export const RelationshipListItemContent: React.FC<
<Tooltip>
<TooltipTrigger>
<div className="truncate text-left text-sm">
{sourceTable?.schema
? `${sourceTable.schema}.`
: ''}
{sourceTable?.name}({sourceField?.name})
</div>
</TooltipTrigger>
<TooltipContent>
{sourceTable?.schema
? `${sourceTable.schema}.`
: ''}
{sourceTable?.name}({sourceField?.name})
</TooltipContent>
</Tooltip>
@@ -117,11 +123,17 @@ export const RelationshipListItemContent: React.FC<
</div>
<Tooltip>
<TooltipTrigger>
<div className="truncate text-left text-sm ">
<div className="truncate text-left text-sm">
{targetTable?.schema
? `${targetTable.schema}.`
: ''}
{targetTable?.name}({targetField?.name})
</div>
</TooltipTrigger>
<TooltipContent>
{targetTable?.schema
? `${targetTable.schema}.`
: ''}
{targetTable?.name}({targetField?.name})
</TooltipContent>
</Tooltip>

View File

@@ -0,0 +1,96 @@
import React, { useMemo } from 'react';
import type { DBTable } from '@/lib/domain/db-table';
import { useChartDB } from '@/hooks/use-chartdb';
import { useTheme } from '@/hooks/use-theme';
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
import type { EffectiveTheme } from '@/context/theme-context/theme-context';
import { importer } from '@dbml/core';
import { exportBaseSQL } from '@/lib/data/export-metadata/export-sql-script';
import type { Diagram } from '@/lib/domain/diagram';
import { useToast } from '@/components/toast/use-toast';
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
export interface TableDBMLProps {
filteredTables: DBTable[];
}
const getEditorTheme = (theme: EffectiveTheme) => {
return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
};
export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
const { currentDiagram } = useChartDB();
const { effectiveTheme } = useTheme();
const { toast } = useToast();
const generateDBML = useMemo(() => {
const filteredDiagram: Diagram = {
...currentDiagram,
tables: filteredTables,
relationships:
currentDiagram.relationships?.filter((rel) => {
const sourceTable = filteredTables.find(
(t) => t.id === rel.sourceTableId
);
const targetTable = filteredTables.find(
(t) => t.id === rel.targetTableId
);
return sourceTable && targetTable;
}) ?? [],
} satisfies Diagram;
const filteredDiagramWithoutSpaces: Diagram = {
...filteredDiagram,
tables:
filteredDiagram.tables?.map((table) => ({
...table,
name: table.name.replace(/\s/g, '_'),
fields: table.fields.map((field) => ({
...field,
name: field.name.replace(/\s/g, '_'),
})),
indexes: table.indexes?.map((index) => ({
...index,
name: index.name.replace(/\s/g, '_'),
})),
})) ?? [],
} satisfies Diagram;
const baseScript = exportBaseSQL(filteredDiagramWithoutSpaces);
try {
return importer.import(baseScript, 'postgres');
} catch (e) {
console.error(e);
toast({
title: 'Error',
description:
'Failed to generate DBML. We would appreciate if you could report this issue!',
variant: 'destructive',
});
return '';
}
}, [currentDiagram, filteredTables, toast]);
return (
<CodeSnippet
code={generateDBML}
className="my-0.5"
editorProps={{
height: '100%',
defaultLanguage: 'dbml',
beforeMount: setupDBMLLanguage,
loading: false,
theme: getEditorTheme(effectiveTheme),
options: {
wordWrap: 'off',
mouseWheelZoom: false,
domReadOnly: true,
},
}}
/>
);
};

View File

@@ -0,0 +1,162 @@
import React, { useEffect, useRef } from 'react';
import { Ellipsis, Trash2 } from 'lucide-react';
import { Input } from '@/components/input/input';
import { Button } from '@/components/button/button';
import { Separator } from '@/components/separator/separator';
import type { DBField } from '@/lib/domain/db-field';
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/popover/popover';
import { Label } from '@/components/label/label';
import { Checkbox } from '@/components/checkbox/checkbox';
import { useTranslation } from 'react-i18next';
import { Textarea } from '@/components/textarea/textarea';
import { debounce } from '@/lib/utils';
export interface TableFieldPopoverProps {
field: DBField;
updateField: (attrs: Partial<DBField>) => void;
removeField: () => void;
}
export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
field,
updateField,
removeField,
}) => {
const { t } = useTranslation();
const [localField, setLocalField] = React.useState<DBField>(field);
const debouncedUpdateFieldRef = useRef<((value?: DBField) => void) | null>(
null
);
useEffect(() => {
debouncedUpdateFieldRef.current = debounce((value?: DBField) => {
updateField({
comments: value?.comments,
characterMaximumLength: value?.characterMaximumLength,
unique: value?.unique,
});
}, 200);
return () => {
debouncedUpdateFieldRef.current = null;
};
}, [updateField]);
useEffect(() => {
if (debouncedUpdateFieldRef.current) {
debouncedUpdateFieldRef.current(localField);
}
}, [localField]);
return (
<Popover
onOpenChange={(isOpen) => {
if (isOpen) {
setLocalField(field);
}
}}
>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="h-8 w-[32px] p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
>
<Ellipsis className="size-3.5" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-52">
<div className="flex flex-col gap-2">
<div className="text-sm font-semibold">
{t(
'side_panel.tables_section.table.field_actions.title'
)}
</div>
<Separator orientation="horizontal" />
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<Label htmlFor="width" className="text-subtitle">
{t(
'side_panel.tables_section.table.field_actions.unique'
)}
</Label>
<Checkbox
checked={localField.unique}
disabled={field.primaryKey}
onCheckedChange={(value) =>
setLocalField((current) => ({
...current,
unique: !!value,
}))
}
/>
</div>
{findDataTypeDataById(field.type.id)
?.hasCharMaxLength ? (
<div className="flex flex-col gap-2">
<Label
htmlFor="width"
className="text-subtitle"
>
{t(
'side_panel.tables_section.table.field_actions.character_length'
)}
</Label>
<Input
value={
localField.characterMaximumLength ?? ''
}
type="number"
onChange={(e) =>
setLocalField((current) => ({
...current,
characterMaximumLength:
e.target.value,
}))
}
className="w-full rounded-md bg-muted text-sm"
/>
</div>
) : null}
<div className="flex flex-col gap-2">
<Label htmlFor="width" className="text-subtitle">
{t(
'side_panel.tables_section.table.field_actions.comments'
)}
</Label>
<Textarea
value={localField.comments}
onChange={(e) =>
setLocalField((current) => ({
...current,
comments: e.target.value,
}))
}
placeholder={t(
'side_panel.tables_section.table.field_actions.no_comments'
)}
className="w-full rounded-md bg-muted text-sm"
/>
</div>
</div>
<Separator orientation="horizontal" />
<Button
variant="outline"
className="flex gap-2 !text-red-700"
onClick={removeField}
>
<Trash2 className="size-3.5 text-red-700" />
{t(
'side_panel.tables_section.table.field_actions.delete_field'
)}
</Button>
</div>
</PopoverContent>
</Popover>
);
};

View File

@@ -1,30 +1,27 @@
import React from 'react';
import { Ellipsis, GripVertical, Trash2, KeyRound } from 'lucide-react';
import React, { useCallback } from 'react';
import { GripVertical, KeyRound } from 'lucide-react';
import { Input } from '@/components/input/input';
import { Button } from '@/components/button/button';
import { Separator } from '@/components/separator/separator';
import type { DBField } from '@/lib/domain/db-field';
import { useChartDB } from '@/hooks/use-chartdb';
import { dataTypeMap } from '@/lib/data/data-types/data-types';
import {
dataTypeDataToDataType,
dataTypeMap,
} from '@/lib/data/data-types/data-types';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/popover/popover';
import { Label } from '@/components/label/label';
import { Checkbox } from '@/components/checkbox/checkbox';
import { useTranslation } from 'react-i18next';
import { Textarea } from '@/components/textarea/textarea';
import { TableFieldToggle } from './table-field-toggle';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import type {
SelectBoxOption,
SelectBoxProps,
} from '@/components/select-box/select-box';
import { SelectBox } from '@/components/select-box/select-box';
import { TableFieldPopover } from './table-field-modal/table-field-modal';
export interface TableFieldProps {
field: DBField;
@@ -39,13 +36,55 @@ export const TableField: React.FC<TableFieldProps> = ({
}) => {
const { databaseType } = useChartDB();
const { t } = useTranslation();
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: field.id });
const dataFieldOptions = dataTypeMap[databaseType].map((type) => ({
label: type.name,
value: type.id,
}));
const dataFieldOptions: SelectBoxOption[] = dataTypeMap[databaseType].map(
(type) => ({
label: type.name,
value: type.id,
regex: type.hasCharMaxLength
? `^${type.name}\\(\\d+\\)$`
: undefined,
extractRegex: type.hasCharMaxLength ? /\((\d+)\)/ : undefined,
})
);
const onChangeDataType = useCallback<
NonNullable<SelectBoxProps['onChange']>
>(
(value, regexMatches) => {
const dataType = dataTypeMap[databaseType].find(
(v) => v.id === value
) ?? {
id: value as string,
name: value as string,
};
let characterMaximumLength: string | undefined = undefined;
if (regexMatches?.length && dataType?.hasCharMaxLength) {
characterMaximumLength = regexMatches[1];
} else if (
field.characterMaximumLength &&
dataType?.hasCharMaxLength
) {
characterMaximumLength = field.characterMaximumLength;
}
updateField({
characterMaximumLength,
type: dataTypeDataToDataType(
dataType ?? {
id: value as string,
name: value as string,
}
),
});
},
[updateField, databaseType, field.characterMaximumLength]
);
const style = {
transform: CSS.Translate.toString(transform),
@@ -96,20 +135,39 @@ export const TableField: React.FC<TableFieldProps> = ({
'side_panel.tables_section.table.field_type'
)}
value={field.type.id}
onChange={(value) =>
updateField({
type: dataTypeMap[databaseType].find(
(v) => v.id === value
),
})
valueSuffix={
field.characterMaximumLength
? `(${field.characterMaximumLength})`
: ''
}
optionSuffix={(option) => {
const type = dataTypeMap[databaseType].find(
(v) => v.id === option.value
);
if (!type) {
return '';
}
if (type.hasCharMaxLength) {
return `(${!field.characterMaximumLength ? 'n' : field.characterMaximumLength})`;
}
return '';
}}
onChange={onChangeDataType}
emptyPlaceholder={t(
'side_panel.tables_section.table.no_types_found'
)}
/>
</span>
</TooltipTrigger>
<TooltipContent>{field.type.name}</TooltipContent>
<TooltipContent>
{field.type.name}
{field.characterMaximumLength
? `(${field.characterMaximumLength})`
: ''}
</TooltipContent>
</Tooltip>
</div>
<div className="flex w-4/12 justify-end gap-1 overflow-hidden">
@@ -152,80 +210,11 @@ export const TableField: React.FC<TableFieldProps> = ({
{t('side_panel.tables_section.table.primary_key')}
</TooltipContent>
</Tooltip>
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="h-8 w-[32px] p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
>
<Ellipsis className="size-3.5" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-52">
<div className="flex flex-col gap-2">
<div className="text-sm font-semibold">
{t(
'side_panel.tables_section.table.field_actions.title'
)}
</div>
<Separator orientation="horizontal" />
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<Label
htmlFor="width"
className="text-subtitle"
>
{t(
'side_panel.tables_section.table.field_actions.unique'
)}
</Label>
<Checkbox
checked={field.unique}
disabled={field.primaryKey}
onCheckedChange={(value) =>
updateField({
unique: !!value,
})
}
/>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="width"
className="text-subtitle"
>
{t(
'side_panel.tables_section.table.field_actions.comments'
)}
</Label>
<Textarea
value={field.comments}
onChange={(e) =>
updateField({
comments: e.target.value,
})
}
placeholder={t(
'side_panel.tables_section.table.field_actions.no_comments'
)}
className="w-full rounded-md bg-muted text-sm"
/>
</div>
</div>
<Separator orientation="horizontal" />
<Button
variant="outline"
className="flex gap-2 !text-red-700"
onClick={removeField}
>
<Trash2 className="size-3.5 text-red-700" />
{t(
'side_panel.tables_section.table.field_actions.delete_field'
)}
</Button>
</div>
</PopoverContent>
</Popover>
<TableFieldPopover
field={field}
updateField={updateField}
removeField={removeField}
/>
</div>
</div>
);

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Toggle } from '@/components/toggle/toggle';
export const TableIndexToggle = React.forwardRef<
React.ElementRef<typeof Toggle>,
React.ComponentPropsWithoutRef<typeof Toggle>
>((props, ref) => {
return (
<Toggle
{...props}
ref={ref}
variant="default"
className="h-8 w-[32px] p-2 text-xs text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
/>
);
});
TableIndexToggle.displayName = Toggle.displayName;

View File

@@ -14,6 +14,12 @@ import { Label } from '@/components/label/label';
import { Input } from '@/components/input/input';
import { useTranslation } from 'react-i18next';
import { SelectBox } from '@/components/select-box/select-box';
import { TableIndexToggle } from './table-index-toggle';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
export interface TableIndexProps {
index: DBIndex;
@@ -54,7 +60,28 @@ export const TableIndex: React.FC<TableIndexProps> = ({
)}
keepOrder
/>
<div className="flex shrink-0">
<div className="flex shrink-0 gap-1">
<Tooltip>
<TooltipTrigger asChild>
<span>
<TableIndexToggle
pressed={index.unique}
onPressedChange={(value) =>
updateIndex({
unique: !!value,
})
}
>
U
</TableIndexToggle>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
'side_panel.tables_section.table.index_actions.unique'
)}
</TooltipContent>
</Tooltip>
<Popover>
<PopoverTrigger asChild>
<Button

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Plus, FileType2, FileKey2, MessageCircleMore } from 'lucide-react';
import { Button } from '@/components/button/button';
import {
@@ -70,17 +70,29 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
}
};
const createIndexHandler = () => {
setSelectedItems((prev) => {
if (prev.includes('indexes')) {
return prev;
}
const createIndexHandler = useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
setSelectedItems((prev) => {
if (prev.includes('indexes')) {
return prev;
}
return [...prev, 'indexes'];
});
return [...prev, 'indexes'];
});
createIndex(table.id);
};
createIndex(table.id);
},
[createIndex, table.id, setSelectedItems]
);
const createFieldHandler = useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
createField(table.id);
},
[createField, table.id]
);
return (
<div
@@ -113,10 +125,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
<Button
variant="ghost"
className="size-4 p-0 text-xs hover:bg-primary-foreground"
onClick={(e) => {
e.stopPropagation();
createField(table.id);
}}
onClick={createFieldHandler}
>
<Plus className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</Button>
@@ -153,6 +162,18 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
/>
))}
</SortableContext>
<div className="flex justify-start p-1">
<Button
variant="ghost"
className="flex h-7 items-center gap-1 px-2 text-xs"
onClick={createFieldHandler}
>
<Plus className="size-4 text-muted-foreground" />
{t(
'side_panel.tables_section.table.add_field'
)}
</Button>
</div>
</DndContext>
</AccordionContent>
</AccordionItem>
@@ -173,10 +194,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
<Button
variant="ghost"
className="size-4 p-0 text-xs hover:bg-primary-foreground"
onClick={(e) => {
e.stopPropagation();
createIndexHandler();
}}
onClick={createIndexHandler}
>
<Plus className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</Button>
@@ -198,6 +216,16 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
fields={table.fields}
/>
))}
<div className="flex justify-start p-1">
<Button
variant="ghost"
className="flex h-7 items-center gap-1 px-2 text-xs"
onClick={createIndexHandler}
>
<Plus className="size-4 text-muted-foreground" />
{t('side_panel.tables_section.table.add_index')}
</Button>
</div>
</AccordionContent>
</AccordionItem>
@@ -248,7 +276,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
<Button
variant="outline"
className="h-8 p-2 text-xs"
onClick={() => createField(table.id)}
onClick={createFieldHandler}
>
<FileType2 className="h-4" />
{t('side_panel.tables_section.table.add_field')}

View File

@@ -73,8 +73,14 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
setEditMode(false);
}, [tableName, table.id, updateTable, editMode]);
const abortEdit = useCallback(() => {
setEditMode(false);
setTableName(table.name);
}, [table.name]);
useClickAway(inputRef, editTableName);
useKeyPressEvent('Enter', editTableName);
useKeyPressEvent('Escape', abortEdit);
const enterEditMode = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -156,7 +162,7 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
<EllipsisVertical />
</ListItemHeaderButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-fit">
<DropdownMenuContent className="w-fit min-w-40">
<DropdownMenuLabel>
{t(
'side_panel.tables_section.table.table_actions.title'

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