Compare commits

...

111 Commits

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

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

* fix

* fix

---------

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

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

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

* import only enums & composite types

* init commit to support custom types in postgres

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

* update select box + type names

* all but ui

* update ui

* fix build

* fix build

* fix

---------

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

* fix build

---------

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

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

* some fixes

---------

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

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

* fix

---------

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

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

* remove logs

* fix build

---------

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

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

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

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

* fix

* fix

---------

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

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

* fix build

---------

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

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

* add translations + small ui fixes

* fix build

---------

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

* fix(i18n): Update Russian translation

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

* revert package.json

---------

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

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

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

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

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

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

* some fix

* fix

---------

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

* fix type to new version

* remove colors

---------

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

* translations + update from canvas

* create area from canvas

* fix build

* fix build

* fix build
2025-04-22 15:00:52 +03:00
Guy Ben-Aharon
8102f19f79 chore(main): release 1.11.0 (#637) 2025-04-17 20:35:00 +03:00
Jonathan Fishner
840a00ebcd update x.com link (#659) 2025-04-17 19:08:30 +03:00
Torgny Bjers
181f96d250 ci: support multi-arch build and publish (#656) 2025-04-15 16:04:28 +03:00
Guy Ben-Aharon
ce2389f135 fix(sidebar): turn sidebar to responsive for mobile (#658) 2025-04-13 16:23:49 +03:00
Guy Ben-Aharon
f15dc77f33 update twitter (#654) 2025-04-09 12:05:56 +03:00
Jonathan Fishner
caa81c24a6 fix(import): display query result formatted (#644)
* fix: formatOnPaste: true for import database + remove spaces for smart query

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-04-08 14:56:51 +03:00
Guy Ben-Aharon
e3cb62788c fix(performance): Import deps dynamically (#652) 2025-04-07 17:08:02 +03:00
Jonathan Fishner
fc46cbb893 feat: add sidebar footer help buttons (#650)
* feat: add sidebar footer help buttons

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-04-07 12:47:56 +03:00
Guy Ben-Aharon
d94a71e9e1 add colors to diff (#647)
* add colors to diff

* fix schema

* fix vizual differ
2025-04-04 11:20:25 +03:00
Jonathan Fishner
cf81253535 fix(mysql-ddl): update the script to import - for create fks (#642) 2025-04-04 09:15:28 +03:00
Guy Ben-Aharon
25c4b42538 fix(mobile): fix create diagram modal on mobile (#646) 2025-04-04 09:09:04 +03:00
Jonathan Fishner
f7a6e0cb5e feat(import-sql): import postgresql via SQL (DDL script) (#639)
* working version of import pg

* change modal

* update create & import diagram dialog

* add mysql DDL parser

* add sqlserver DDL parser

* add sqlite DDL parser

* instructions

* logs

* fix lint + logs

* remove

* order tables

* order tables

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-04-02 13:55:59 +03:00
Jonathan Fishner
85275e5dd6 fix: remove unused links from help menu (#623) 2025-03-30 20:24:36 +03:00
Guy Ben-Aharon
4e5b467ce5 remove hidden from table type (#638) 2025-03-30 15:47:56 +03:00
Guy Ben-Aharon
874aa5ab75 update diff types (#636)
* update diff types

* remove comments
2025-03-30 15:40:51 +03:00
Guy Ben-Aharon
0940d72d5d fix(import): strict parse of database metadata (#635)
* fix: strict parst of database metadata

* fix: strict parst of database metadata

* fix: strict parst of database metadata

* fix: strict parst of database metadata

* fix: strict parst of database metadata

* remove partial

* fix: strict parst of database metadata

* fix: strict parst of database metadata

* fix slqite

* udapte mysql + maria

* fix: strict parst of database metadata

* fix mssql

* commit mysql

---------

Co-authored-by: johnnyfish <jonathanfishner11@gmail.com>
2025-03-27 17:32:35 +02:00
Guy Ben-Aharon
0d1739d70f alignments (#633) 2025-03-26 17:10:05 +02:00
Guy Ben-Aharon
60fe0843ac chore(main): release 1.10.0 (#619) 2025-03-25 11:18:58 +02:00
Jonathan Fishner
794f226209 feat(cloudflare-d1): add support to cloudflare-d1 + wrangler cli (#632)
* feat(cloudflare-d1): add support to Sqlite cloudflare-d1 database + wrangler cli

* revert export-per-type :: sqlite, no need special export for now

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-03-25 11:15:23 +02:00
Jonathan Fishner
2fbf3476b8 fix(export-sql): move from AI sql-export for MySQL&MariaDB to deterministic script (#628) 2025-03-20 11:26:45 +02:00
Jonathan Fishner
897ac60a82 fix(export-sql): move from AI sql-export for sqlite to deterministic script (#627) 2025-03-20 11:17:30 +02:00
Jonathan Fishner
18f228ca1d fix(export-sql): move from AI sql-export for postgres to deterministic script (#626)
* fix: move from AI export sql for postgres to export script

* update export for postgres to be without AI

* fix build

* make isDBMLFlow optional

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-03-19 11:02:01 +02:00
Jonathan Fishner
14de30b7aa fix(dbml-editor): dealing with dbml editor for non-generic db-type (#624)
* fix(dbml-editor): dealing with dbml editor for non-generic db-type

* small change

* Handle ENUM type when exporting as varchar

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-03-17 19:40:12 +02:00
Guy Ben-Aharon
3faa39e787 fix(sidebar): opens sidepanel in case its closed and click on sidebar (#620) 2025-03-13 15:41:39 +02:00
Guy Ben-Aharon
63b5ba0bb9 fix(sidebar): add sidebar for diagram objects (#618)
* fix(sidebar): add sidebar for diagram objects

* fix
2025-03-13 15:07:01 +02:00
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
260 changed files with 34481 additions and 14971 deletions

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

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

View File

@@ -32,7 +32,7 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: npm ci
@@ -42,6 +42,12 @@ jobs:
- name: Build project
run: npm run build
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
@@ -50,10 +56,11 @@ jobs:
tags: |
type=semver,pattern={{version}}
- name: Build and push Docker image
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,5 +1,155 @@
# Changelog
## [1.13.1](https://github.com/chartdb/chartdb/compare/v1.13.0...v1.13.1) (2025-07-04)
### Bug Fixes
* **custom_types:** fix display custom types in select box ([#737](https://github.com/chartdb/chartdb/issues/737)) ([24be28a](https://github.com/chartdb/chartdb/commit/24be28a662c48fc5bc62e76446b9669d83d7d3e0))
* **dbml-editor:** for some cases that the dbml had issues ([#739](https://github.com/chartdb/chartdb/issues/739)) ([e0ff198](https://github.com/chartdb/chartdb/commit/e0ff198c3fd416498dac5680bb323ec88c54b65c))
* **dbml:** Filter duplicate tables at diagram level before export dbml ([#746](https://github.com/chartdb/chartdb/issues/746)) ([d429128](https://github.com/chartdb/chartdb/commit/d429128e65aa28c500eac2487356e4869506e948))
* **export-sql:** conditionally show generic option and reorder by diagram type ([#708](https://github.com/chartdb/chartdb/issues/708)) ([c6118e0](https://github.com/chartdb/chartdb/commit/c6118e0cdb0e5caaf73447d33db2fde1a98efe60))
* general performance improvements on canvas ([#751](https://github.com/chartdb/chartdb/issues/751)) ([4fcc49d](https://github.com/chartdb/chartdb/commit/4fcc49d49a76a4b886ffd6cf0b40cf2fc49952ec))
* **import-database:** for custom types query to import supabase & timescale ([#745](https://github.com/chartdb/chartdb/issues/745)) ([2fce832](https://github.com/chartdb/chartdb/commit/2fce8326b67b751d38dd34f409fea574449d0298))
* **import-db:** fix mariadb import ([#740](https://github.com/chartdb/chartdb/issues/740)) ([7d063b9](https://github.com/chartdb/chartdb/commit/7d063b905f19f51501468bd0bd794a25cf65e1be))
* **performance:** improve storage provider performance ([#734](https://github.com/chartdb/chartdb/issues/734)) ([c6788b4](https://github.com/chartdb/chartdb/commit/c6788b49173d9cce23571daeb460285cb7cffb11))
* resolve unresponsive cursor and input glitches when editing field comments ([#749](https://github.com/chartdb/chartdb/issues/749)) ([d15985e](https://github.com/chartdb/chartdb/commit/d15985e3999a0cd54213b2fb08c55d48a1b8b3b2))
* **table name:** updates table name value when its updated from canvas/sidebar ([#716](https://github.com/chartdb/chartdb/issues/716)) ([8b86e1c](https://github.com/chartdb/chartdb/commit/8b86e1c22992aaadcce7ad5fc1d267c5a57a99f0))
## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28)
### Features
* **custom-types:** add enums and composite types for Postgres ([#714](https://github.com/chartdb/chartdb/issues/714)) ([c3904d9](https://github.com/chartdb/chartdb/commit/c3904d9fdd63ef5b76a44e73582d592f2c418687))
* **export-sql:** add custom types to export sql script ([#720](https://github.com/chartdb/chartdb/issues/720)) ([cad155e](https://github.com/chartdb/chartdb/commit/cad155e6550f171b8faecbfdff27032798ecea43))
* **oracle:** support oracle in ChartDB ([#709](https://github.com/chartdb/chartdb/issues/709)) ([765a1c4](https://github.com/chartdb/chartdb/commit/765a1c43547a29bd3428c942c7afb56f63aaf046))
### Bug Fixes
* **canvas:** prevent canvas blink and lag on field edit ([#723](https://github.com/chartdb/chartdb/issues/723)) ([cd44346](https://github.com/chartdb/chartdb/commit/cd443466c7952f1cdc3739645c12130b9231e3a1))
* **canvas:** prevent canvas blink and lag on primary field edit ([#725](https://github.com/chartdb/chartdb/issues/725)) ([4477b1c](https://github.com/chartdb/chartdb/commit/4477b1ca1fe6b282b604739a23e31181acd4d7bc))
* **custom_types:** fix custom types on storage provider ([#721](https://github.com/chartdb/chartdb/issues/721)) ([beb0151](https://github.com/chartdb/chartdb/commit/beb015194f917c0ba644458410162d2b7599918c))
* **custom_types:** fix custom types on storage provider ([#722](https://github.com/chartdb/chartdb/issues/722)) ([18012dd](https://github.com/chartdb/chartdb/commit/18012ddab1718bcce3432aea626adf6fc9be25d9))
* **custom-types:** fetch directly via the smart-query the custom types ([#729](https://github.com/chartdb/chartdb/issues/729)) ([cf1e141](https://github.com/chartdb/chartdb/commit/cf1e141837eda77d717ad87489ce9946b688e226))
* **dbml-editor:** export comments with schema if existsed ([#728](https://github.com/chartdb/chartdb/issues/728)) ([73f542a](https://github.com/chartdb/chartdb/commit/73f542adad2d66a1e84fc656a0c34d9b1f39f33c))
* **dbml-editor:** fix export dbml - to show enums ([#724](https://github.com/chartdb/chartdb/issues/724)) ([3894a22](https://github.com/chartdb/chartdb/commit/3894a221745d32c13160bedcb1bcf53d89897698))
* **import-database:** remove the default fetch from import database ([#718](https://github.com/chartdb/chartdb/issues/718)) ([0d11b0c](https://github.com/chartdb/chartdb/commit/0d11b0c55a94a12a764785cfdcf2ba10437241d6))
* **menu:** add oracle to import menu ([#713](https://github.com/chartdb/chartdb/issues/713)) ([aee5779](https://github.com/chartdb/chartdb/commit/aee577998342eb4a2b05b3e03181992a435712d8))
* **relationship:** fix creating of relationships ([#732](https://github.com/chartdb/chartdb/issues/732)) ([08b627c](https://github.com/chartdb/chartdb/commit/08b627cb8ca8fdf08d8ed2ff7e89104887deffb7))
## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
### Features
* **areas:** implement area to enable logical diagram arrangement ([#661](https://github.com/chartdb/chartdb/issues/661)) ([92e3ec7](https://github.com/chartdb/chartdb/commit/92e3ec785c91f7f19881c6d9d0692257af4651bc))
* **examples:** update examples to have areas ([#677](https://github.com/chartdb/chartdb/issues/677)) ([21c9129](https://github.com/chartdb/chartdb/commit/21c9129e14670c744950cd43a5cbdd4b7d47c639))
* **image-export:** add transparent and pattern export image toggles ([#671](https://github.com/chartdb/chartdb/issues/671)) ([6b8d637](https://github.com/chartdb/chartdb/commit/6b8d637b757b94630ecd7521b4a2c99634afae69))
### Bug Fixes
* add sorting based on how common the datatype on side-panel ([#651](https://github.com/chartdb/chartdb/issues/651)) ([3a1b8d1](https://github.com/chartdb/chartdb/commit/3a1b8d1db13d8dd7cb6cbe5ef8c5a60faccfeae5))
* **canvas:** disable edit area name on read only ([#666](https://github.com/chartdb/chartdb/issues/666)) ([9402822](https://github.com/chartdb/chartdb/commit/9402822fa31f8cd94fe7971277839ee5425e29bf))
* **canvas:** read only mode ([#665](https://github.com/chartdb/chartdb/issues/665)) ([651fe36](https://github.com/chartdb/chartdb/commit/651fe361fce61fe0577d2593f268131e9ca359d0))
* **clone:** add areas to clone diagram ([#664](https://github.com/chartdb/chartdb/issues/664)) ([aee1713](https://github.com/chartdb/chartdb/commit/aee1713aecdd5e54228a16cbc3c4fc184661c56b))
* **dbml-editor:** add inline refs mode + fix issues with DBML syntax ([#687](https://github.com/chartdb/chartdb/issues/687)) ([fbf2fe9](https://github.com/chartdb/chartdb/commit/fbf2fe919c2168c715f8231c0246753b19635f14))
* **dbml-editor:** remove invalid fields before showing DBML + warning ([#683](https://github.com/chartdb/chartdb/issues/683)) ([5759241](https://github.com/chartdb/chartdb/commit/5759241573db204183c92599588d59f4aadaeafb))
* **ddl-import:** fix datatypes when importing via ddl ([#696](https://github.com/chartdb/chartdb/issues/696)) ([a1144bb](https://github.com/chartdb/chartdb/commit/a1144bbf761a0daedd546b5d9b92300be59e0157))
* **ddl:** inline fks ddl script ([#701](https://github.com/chartdb/chartdb/issues/701)) ([5849e45](https://github.com/chartdb/chartdb/commit/5849e4586c7c2a7cd86bd064df8916b130fc6234))
* **dependencies:** hide icon when diagram has no dependencies ([#684](https://github.com/chartdb/chartdb/issues/684)) ([547149d](https://github.com/chartdb/chartdb/commit/547149da44db6d3d1e36d619d475fe52ff83a472))
* **examples:** add loader ([#678](https://github.com/chartdb/chartdb/issues/678)) ([90a20dd](https://github.com/chartdb/chartdb/commit/90a20dd1b0277c4aee848fae5ed7a8347c5ba77d))
* **examples:** fix clone examples ([#679](https://github.com/chartdb/chartdb/issues/679)) ([1778abb](https://github.com/chartdb/chartdb/commit/1778abb683d575af244edcd9a11f8d03f903f719))
* **expanded-table:** persist expanded state across renders ([#707](https://github.com/chartdb/chartdb/issues/707)) ([54d5e96](https://github.com/chartdb/chartdb/commit/54d5e96a6db1e3abd52229a89ac503ff31885386))
* **export image:** Fix usage of advanced options accordion ([#703](https://github.com/chartdb/chartdb/issues/703)) ([0ce85cf](https://github.com/chartdb/chartdb/commit/0ce85cf76b733f441f661608278c0db3122c5074))
* **import-database:** auto detect when user try to import ddl script ([#698](https://github.com/chartdb/chartdb/issues/698)) ([5a5e64a](https://github.com/chartdb/chartdb/commit/5a5e64abef510cff28b3d8972520d0b9df29b024))
* **import-database:** remove view_definition when importing via query ([#702](https://github.com/chartdb/chartdb/issues/702)) ([481ad3c](https://github.com/chartdb/chartdb/commit/481ad3c8449f469bf2b4418e4cdcc5b5608dfd36))
* **import-json:** for broken json imports ([#697](https://github.com/chartdb/chartdb/issues/697)) ([2368e0d](https://github.com/chartdb/chartdb/commit/2368e0d2639021c4a11a8e5131d6af44fb6a47db))
* **import-json:** simplify import script for fixing invalid JSON ([#681](https://github.com/chartdb/chartdb/issues/681)) ([226e6cf](https://github.com/chartdb/chartdb/commit/226e6cf1ce4d2edcfbee6a4de7ab0bc0cfeb17fe))
* **import:** dbml and query - senetize before import ([#699](https://github.com/chartdb/chartdb/issues/699)) ([34c0a71](https://github.com/chartdb/chartdb/commit/34c0a7163f47bde7ddfaa8f044341e3c971b7e03))
* **navbar:** open diagram directly from diagram icon ([#694](https://github.com/chartdb/chartdb/issues/694)) ([7db86dc](https://github.com/chartdb/chartdb/commit/7db86dcf8c97d34b056e4b5b85a0dda0438322ea))
* **performance:** Only render visible ([#672](https://github.com/chartdb/chartdb/issues/672)) ([83c4333](https://github.com/chartdb/chartdb/commit/83c43332d497e9fc148a18b9cb4d9ecc85e44183))
* **performance:** update field only when changed ([#685](https://github.com/chartdb/chartdb/issues/685)) ([d3ddf7c](https://github.com/chartdb/chartdb/commit/d3ddf7c51eaa4b9cddb961defd52d423f39f281d))
* **postgres:** fix import of postgres fks ([#700](https://github.com/chartdb/chartdb/issues/700)) ([89e3cea](https://github.com/chartdb/chartdb/commit/89e3ceab00defaabc079e165fc90e92ca00722cf))
* **schema:** add areas to diagram schema ([#663](https://github.com/chartdb/chartdb/issues/663)) ([ecfa148](https://github.com/chartdb/chartdb/commit/ecfa14829bcb1b813c7b154b4bd59f24e3032d8f))
* **sql-script:** change ddl to be sql-script ([#710](https://github.com/chartdb/chartdb/issues/710)) ([487fb2d](https://github.com/chartdb/chartdb/commit/487fb2d5c17b70ac54aa17af9a2ac9aded6b40ba))
* **table:** enhance field focus behavior to include table hover state ([#676](https://github.com/chartdb/chartdb/issues/676)) ([19d2d0b](https://github.com/chartdb/chartdb/commit/19d2d0bddd3a464995b79e97e6caf6e652836081))
* **translations:** Add some translations for ru-RU language ([#690](https://github.com/chartdb/chartdb/issues/690)) ([97d01d7](https://github.com/chartdb/chartdb/commit/97d01d72014e473c42348c9ebcbe7a0b973d31aa))
## [1.11.0](https://github.com/chartdb/chartdb/compare/v1.10.0...v1.11.0) (2025-04-17)
### Features
* add sidebar footer help buttons ([#650](https://github.com/chartdb/chartdb/issues/650)) ([fc46cbb](https://github.com/chartdb/chartdb/commit/fc46cbb8933761c7bac3604664f7de812f6f5b6b))
* **import-sql:** import postgresql via SQL (DDL script) ([#639](https://github.com/chartdb/chartdb/issues/639)) ([f7a6e0c](https://github.com/chartdb/chartdb/commit/f7a6e0cb5e4921dd9540739f9da269858e7ca7be))
### Bug Fixes
* **import:** display query result formatted ([#644](https://github.com/chartdb/chartdb/issues/644)) ([caa81c2](https://github.com/chartdb/chartdb/commit/caa81c24a6535bc87129c38622aac5a62a6d479d))
* **import:** strict parse of database metadata ([#635](https://github.com/chartdb/chartdb/issues/635)) ([0940d72](https://github.com/chartdb/chartdb/commit/0940d72d5d3726650213257639f24ba47e729854))
* **mobile:** fix create diagram modal on mobile ([#646](https://github.com/chartdb/chartdb/issues/646)) ([25c4b42](https://github.com/chartdb/chartdb/commit/25c4b4253849575d7a781ed197281e2a35e7184a))
* **mysql-ddl:** update the script to import - for create fks ([#642](https://github.com/chartdb/chartdb/issues/642)) ([cf81253](https://github.com/chartdb/chartdb/commit/cf81253535ca5a3b8a65add78287c1bdb283a1c7))
* **performance:** Import deps dynamically ([#652](https://github.com/chartdb/chartdb/issues/652)) ([e3cb627](https://github.com/chartdb/chartdb/commit/e3cb62788c13f149e35e1a5020191bd43d14b52f))
* remove unused links from help menu ([#623](https://github.com/chartdb/chartdb/issues/623)) ([85275e5](https://github.com/chartdb/chartdb/commit/85275e5dd6e7845f06f682eeceda7932fc87e875))
* **sidebar:** turn sidebar to responsive for mobile ([#658](https://github.com/chartdb/chartdb/issues/658)) ([ce2389f](https://github.com/chartdb/chartdb/commit/ce2389f135d399d82c9848335d31174bac8a3791))
## [1.10.0](https://github.com/chartdb/chartdb/compare/v1.9.0...v1.10.0) (2025-03-25)
### Features
* **cloudflare-d1:** add support to cloudflare-d1 + wrangler cli ([#632](https://github.com/chartdb/chartdb/issues/632)) ([794f226](https://github.com/chartdb/chartdb/commit/794f2262092fbe36e27e92220221ed98cb51ae37))
### Bug Fixes
* **dbml-editor:** dealing with dbml editor for non-generic db-type ([#624](https://github.com/chartdb/chartdb/issues/624)) ([14de30b](https://github.com/chartdb/chartdb/commit/14de30b7aaa0ccaca8372f0213b692266d53f0de))
* **export-sql:** move from AI sql-export for MySQL&MariaDB to deterministic script ([#628](https://github.com/chartdb/chartdb/issues/628)) ([2fbf347](https://github.com/chartdb/chartdb/commit/2fbf3476b87f1177af17de8242a74d195dae5f35))
* **export-sql:** move from AI sql-export for postgres to deterministic script ([#626](https://github.com/chartdb/chartdb/issues/626)) ([18f228c](https://github.com/chartdb/chartdb/commit/18f228ca1d5a6c6056cb7c3bfc24d04ec470edf1))
* **export-sql:** move from AI sql-export for sqlite to deterministic script ([#627](https://github.com/chartdb/chartdb/issues/627)) ([897ac60](https://github.com/chartdb/chartdb/commit/897ac60a829a00e9453d670cceeb2282e9e93f1c))
* **sidebar:** add sidebar for diagram objects ([#618](https://github.com/chartdb/chartdb/issues/618)) ([63b5ba0](https://github.com/chartdb/chartdb/commit/63b5ba0bb9934c4e5c5d0d1b6f995afbbd3acf36))
* **sidebar:** opens sidepanel in case its closed and click on sidebar ([#620](https://github.com/chartdb/chartdb/issues/620)) ([3faa39e](https://github.com/chartdb/chartdb/commit/3faa39e7875d836dfe526d94a10f8aed070ac1c1))
## [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)

45
CLA.md Normal file
View File

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

View File

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

View File

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

View File

@@ -30,8 +30,8 @@
<a href="https://discord.gg/QeFwyWSKwC">
<img src="https://img.shields.io/discord/1277047413705670678?color=5865F2&label=Discord&logo=discord&logoColor=white" alt="Discord community channel" />
</a>
<a href="https://x.com/chartdb_io">
<img src="https://img.shields.io/twitter/follow/ChartDB?style=social"/>
<a href="https://x.com/intent/follow?screen_name=jonathanfishner">
<img src="https://img.shields.io/twitter/follow/jonathanfishner?style=social"/>
</a>
</h4>
@@ -49,13 +49,13 @@ Instantly visualize your database schema with a single **"Smart Query."** Custom
**What it does**:
- **Instant Schema Import**
Run a single query to instantly retrieve your database schema as JSON. This makes it incredibly fast to visualize your database schema, whether for documentation, team discussions, or simply understanding your data better.
- **Instant Schema Import**
Run a single query to instantly retrieve your database schema as JSON. This makes it incredibly fast to visualize your database schema, whether for documentation, team discussions, or simply understanding your data better.
- **AI-Powered Export for Easy Migration**
Our AI-driven export feature allows you to generate the DDL script in the dialect of your choice. Whether youre migrating from MySQL to PostgreSQL or from SQLite to MariaDB, ChartDB simplifies the process by providing the necessary scripts tailored to your target database.
- **Interactive Editing**
Fine-tune your database schema using our intuitive editor. Easily make adjustments or annotations to better visualize complex structures.
- **AI-Powered Export for Easy Migration**
Our AI-driven export feature allows you to generate the DDL script in the dialect of your choice. Whether you're migrating from MySQL to PostgreSQL or from SQLite to MariaDB, ChartDB simplifies the process by providing the necessary scripts tailored to your target database.
- **Interactive Editing**
Fine-tune your database schema using our intuitive editor. Easily make adjustments or annotations to better visualize complex structures.
### Status
@@ -63,13 +63,13 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
### Supported Databases
- ✅ PostgreSQL (<img src="./src/assets/postgresql_logo_2.png" width="15"/> + <img src="./src/assets/supabase.png" alt="Supabase" width="15"/> + <img src="./src/assets/timescale.png" alt="Timescale" width="15"/> )
- ✅ MySQL
- ✅ SQL Server
- ✅ MariaDB
- ✅ SQLite
- ✅ CockroachDB
- ✅ ClickHouse
- ✅ PostgreSQL (<img src="./src/assets/postgresql_logo_2.png" width="15"/> + <img src="./src/assets/supabase.png" alt="Supabase" width="15"/> + <img src="./src/assets/timescale.png" alt="Timescale" width="15"/> )
- ✅ MySQL
- ✅ SQL Server
- ✅ MariaDB
- ✅ SQLite (<img src="./src/assets/sqlite_logo_2.png" width="15"/> + <img src="./src/assets/cloudflare_d1.png" alt="Cloudflare D1" width="15"/> Cloudflare D1)
- ✅ CockroachDB
- ✅ ClickHouse
## Getting Started
@@ -91,17 +91,19 @@ npm run build
Or like this if you want to have AI capabilities:
```
```bash
npm install
VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build
```
### Run the Docker Container
```bash
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest
```
#### Build and Run locally
```bash
docker build -t chartdb .
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb
@@ -145,9 +147,9 @@ VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ
## 💚 Community & Support
- [Discord](https://discord.gg/QeFwyWSKwC) (For live discussion with the community and the ChartDB team)
- [GitHub Issues](https://github.com/chartdb/chartdb/issues) (For any bugs and errors you encounter using ChartDB)
- [Twitter](https://x.com/chartdb_io) (Get news fast)
- [Discord](https://discord.gg/QeFwyWSKwC) (For live discussion with the community and the ChartDB team)
- [GitHub Issues](https://github.com/chartdb/chartdb/issues) (For any bugs and errors you encounter using ChartDB)
- [Twitter](https://x.com/intent/follow?screen_name=jonathanfishner) (Get news fast)
## Contributing

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"
}
}

488
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.8.0",
"version": "1.13.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.8.0",
"version": "1.13.1",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",
@@ -18,7 +18,7 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
@@ -27,18 +27,18 @@
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.3.1",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dexie": "^4.0.8",
@@ -1834,6 +1834,60 @@
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.5.tgz",
"integrity": "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.4",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
@@ -1969,6 +2023,24 @@
}
}
},
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
@@ -2028,25 +2100,124 @@
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.5.tgz",
"integrity": "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==",
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.4",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.2"
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2295,6 +2466,24 @@
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menubar": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.5.tgz",
@@ -2364,6 +2553,24 @@
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
@@ -2467,6 +2674,24 @@
}
}
},
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
@@ -2658,13 +2883,54 @@
}
}
},
"node_modules/@radix-ui/react-separator": {
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz",
"integrity": "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1"
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
"integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -2682,9 +2948,9 @@
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
@@ -2818,23 +3084,175 @@
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.7.tgz",
"integrity": "sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==",
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
"integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.4",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.1",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-popper": "1.2.2",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.1"
"@radix-ui/react-visually-hidden": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
"integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
"integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-rect": "1.1.0",
"@radix-ui/react-use-size": "1.1.0",
"@radix-ui/rect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
"integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",
@@ -4470,9 +4888,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001696",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz",
"integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==",
"version": "1.0.30001726",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
"integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
"dev": true,
"funding": [
{

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.8.0",
"version": "1.13.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -22,7 +22,7 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
@@ -31,18 +31,18 @@
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.3.1",
"ahooks": "^3.8.1",
"ai": "^3.3.14",
"class-variance-authority": "^0.7.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dexie": "^4.0.8",

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 KiB

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 359 KiB

BIN
src/assets/oracle_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,2 +1,3 @@
import './config.ts';
export { Editor } from '@monaco-editor/react';
export { DiffEditor } from '@monaco-editor/react';

View File

@@ -5,6 +5,7 @@ import { useTheme } from '@/hooks/use-theme';
import { useMonaco } from '@monaco-editor/react';
import { useToast } from '@/components/toast/use-toast';
import { Button } from '../button/button';
import type { LucideIcon } from 'lucide-react';
import { Copy, CopyCheck } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
import { useTranslation } from 'react-i18next';
@@ -18,27 +19,43 @@ export const Editor = lazy(() =>
}))
);
export const DiffEditor = lazy(() =>
import('./code-editor').then((module) => ({
default: module.DiffEditor,
}))
);
type EditorType = typeof Editor;
export interface CodeSnippetAction {
label: string;
icon: LucideIcon;
onClick: () => void;
}
export interface CodeSnippetProps {
className?: string;
code: string;
codeToCopy?: string;
language?: 'sql' | 'shell';
loading?: boolean;
autoScroll?: boolean;
isComplete?: boolean;
editorProps?: React.ComponentProps<EditorType>;
actions?: CodeSnippetAction[];
}
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
({
className,
code,
codeToCopy,
loading,
language = 'sql',
autoScroll = false,
isComplete = true,
editorProps,
actions,
}) => {
const { t } = useTranslation();
const monaco = useMonaco();
@@ -85,7 +102,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
}
try {
await navigator.clipboard.writeText(code);
await navigator.clipboard.writeText(codeToCopy ?? code);
setIsCopied(true);
} catch {
setIsCopied(false);
@@ -97,7 +114,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
),
});
}
}, [code, t, toast]);
}, [code, codeToCopy, t, toast]);
return (
<div
@@ -111,36 +128,58 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
) : (
<Suspense fallback={<Spinner />}>
{isComplete ? (
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger
asChild
className="absolute right-1 top-1 z-10"
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<span>
<Button
className=" h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
{actions &&
actions.length > 0 &&
actions.map((action, index) => (
<Tooltip key={index}>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={action.onClick}
>
<action.icon
size={16}
/>
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{action.label}
</TooltipContent>
</Tooltip>
))}
</div>
) : null}
<Editor

View File

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

View File

@@ -24,12 +24,20 @@ export interface SelectBoxOption {
value: string;
label: string;
description?: string;
regex?: string;
extractRegex?: RegExp;
group?: string;
}
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;
@@ -44,6 +52,7 @@ export interface SelectBoxProps {
disabled?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
popoverClassName?: string;
}
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
@@ -55,10 +64,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
className,
options,
value,
valueSuffix,
onChange,
multiple,
oneLine,
selectAll,
optionSuffix,
deselectAll,
clearText,
showClear,
@@ -66,6 +77,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
disabled,
open,
onOpenChange: setOpen,
popoverClassName,
},
ref
) => {
@@ -86,7 +98,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 +106,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
: [...(value ?? []), selectedValue];
onChange?.(newValue);
} else {
onChange?.(selectedValue);
onChange?.(selectedValue, regexMatches);
setIsOpen(false);
}
},
@@ -166,6 +178,101 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
[isOpen, onOpenChange]
);
const groups = React.useMemo(
() =>
options.reduce(
(acc, option) => {
if (option.group) {
if (!acc[option.group]) {
acc[option.group] = [];
}
acc[option.group].push(option);
} else {
if (!acc['default']) {
acc['default'] = [];
}
acc['default'].push(option);
}
return acc;
},
{} as Record<string, SelectBoxOption[]>
),
[options]
);
const hasGroups = React.useMemo(
() =>
Object.keys(groups).filter((group) => group !== 'default')
.length > 0,
[groups]
);
const renderOption = React.useCallback(
(option: SelectBoxOption) => {
const isSelected =
Array.isArray(value) && value.includes(option.value);
const isRegexMatch =
option.regex && new RegExp(option.regex)?.test(searchTerm);
const matches = option.extractRegex
? searchTerm.match(option.extractRegex)
: undefined;
return (
<CommandItem
className="flex items-center"
key={option.value}
keywords={option.regex ? [option.regex] : undefined}
onSelect={() =>
handleSelect(
option.value,
matches?.map((match) => match.toString())
)
}
>
{multiple && (
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
)}
>
<CheckIcon />
</div>
)}
<div className="flex flex-1 items-center truncate">
<span>
{isRegexMatch ? searchTerm : option.label}
{!isRegexMatch && optionSuffix
? optionSuffix(option)
: ''}
</span>
{option.description && (
<span className="ml-1 w-0 flex-1 truncate text-xs text-muted-foreground">
{option.description}
</span>
)}
</div>
{((!multiple && option.value === value) ||
isRegexMatch) && (
<CheckIcon
className={cn(
'ml-auto',
option.value === value
? 'opacity-100'
: 'opacity-0'
)}
/>
)}
</CommandItem>
);
},
[value, multiple, searchTerm, handleSelect, optionSuffix]
);
return (
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
@@ -199,6 +306,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
(opt) => opt.value === value
)?.label
}
{valueSuffix ? valueSuffix : ''}
</div>
)
) : (
@@ -235,15 +343,29 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
</div>
</PopoverTrigger>
<PopoverContent
className="w-fit min-w-[var(--radix-popover-trigger-width)] p-0"
className={cn(
'w-fit min-w-[var(--radix-popover-trigger-width)] p-0',
popoverClassName
)}
align="center"
>
<Command
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
@@ -296,65 +418,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
<ScrollArea>
<div className="max-h-64 w-full">
<CommandGroup>
<CommandList className="max-h-fit w-full">
{options.map((option) => {
const isSelected =
Array.isArray(value) &&
value.includes(option.value);
return (
<CommandItem
className="flex items-center"
key={option.value}
// value={option.value}
onSelect={() =>
handleSelect(
option.value
)
}
>
{multiple && (
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
)}
>
<CheckIcon />
</div>
)}
<div className="flex items-center truncate">
<span>
{option.label}
</span>
{option.description && (
<span className="ml-1 text-xs text-muted-foreground">
{
option.description
}
</span>
)}
</div>
{!multiple &&
option.value ===
value && (
<CheckIcon
className={cn(
'ml-auto',
option.value ===
value
? 'opacity-100'
: 'opacity-0'
)}
/>
)}
</CommandItem>
);
})}
</CommandList>
</CommandGroup>
<CommandList className="max-h-fit w-full">
{hasGroups
? Object.entries(groups).map(
([groupName, groupOptions]) => (
<CommandGroup
key={groupName}
heading={groupName}
>
{groupOptions.map(
renderOption
)}
</CommandGroup>
)
)
: options.map(renderOption)}
</CommandList>
</div>
</ScrollArea>
</Command>

View File

@@ -0,0 +1,135 @@
import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { Cross2Icon } from '@radix-ui/react-icons';
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom: 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right: 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: 'right',
},
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<Cross2Icon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
);
SheetHeader.displayName = 'SheetHeader';
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
SheetFooter.displayName = 'SheetFooter';
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@@ -0,0 +1,790 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { useIsMobile } from '@/hooks/use-mobile';
import { cn } from '@/lib/utils';
import { Button } from '@/components/button/button';
import { Input } from '@/components/input/input';
import { Separator } from '@/components/separator/separator';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/sheet/sheet';
import { Skeleton } from '@/components/skeleton/skeleton';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { ViewVerticalIcon } from '@radix-ui/react-icons';
import { useSidebar } from './use-sidebar';
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
type SidebarContext = {
state: 'expanded' | 'collapsed';
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContext | null>(null);
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState =
typeof value === 'function' ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? 'expanded' : 'collapsed';
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
]
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
'group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar',
className
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
);
SidebarProvider.displayName = 'SidebarProvider';
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
side?: 'left' | 'right';
variant?: 'sidebar' | 'floating' | 'inset';
collapsible?: 'offcanvas' | 'icon' | 'none';
}
>(
(
{
side = 'left',
variant = 'sidebar',
collapsible = 'offcanvas',
className,
children,
...props
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === 'none') {
return (
<div
className={cn(
'flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground',
className
)}
ref={ref}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet
open={openMobile}
onOpenChange={setOpenMobile}
{...props}
>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>
Displays the mobile sidebar.
</SheetDescription>
</SheetHeader>
<div className="flex size-full flex-col">
{children}
</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === 'collapsed' ? collapsible : ''}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
'relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
)}
/>
<div
className={cn(
'fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
className
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
);
}
);
Sidebar.displayName = 'Sidebar';
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn('h-7 w-7', className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<ViewVerticalIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
});
SidebarTrigger.displayName = 'SidebarTrigger';
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'>
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className
)}
{...props}
/>
);
});
SidebarRail.displayName = 'SidebarRail';
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'main'>
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
'relative flex w-full flex-1 flex-col bg-background',
'md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
className
)}
{...props}
/>
);
});
SidebarInset.displayName = 'SidebarInset';
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring',
className
)}
{...props}
/>
);
});
SidebarInput.displayName = 'SidebarInput';
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
});
SidebarHeader.displayName = 'SidebarHeader';
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
});
SidebarFooter.displayName = 'SidebarFooter';
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn('mx-2 w-auto bg-sidebar-border', className)}
{...props}
/>
);
});
SidebarSeparator.displayName = 'SidebarSeparator';
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
className
)}
{...props}
/>
);
});
SidebarContent.displayName = 'SidebarContent';
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn(
'relative flex w-full min-w-0 flex-col p-2',
className
)}
{...props}
/>
);
});
SidebarGroup.displayName = 'SidebarGroup';
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
className
)}
{...props}
/>
);
});
SidebarGroupLabel.displayName = 'SidebarGroupLabel';
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'group-data-[collapsible=icon]:hidden',
className
)}
{...props}
/>
);
});
SidebarGroupAction.displayName = 'SidebarGroupAction';
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn('w-full text-sm', className)}
{...props}
/>
));
SidebarGroupContent.displayName = 'SidebarGroupContent';
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
{...props}
/>
));
SidebarMenu.displayName = 'SidebarMenu';
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn('group/menu-item relative', className)}
{...props}
/>
));
SidebarMenuItem.displayName = 'SidebarMenuItem';
const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
{
variants: {
variant: {
default:
'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
outline:
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
},
size: {
default: 'h-8 text-sm',
sm: 'h-7 text-xs',
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = 'default',
size = 'default',
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : 'button';
const { isMobile, state } = useSidebar();
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(
sidebarMenuButtonVariants({ variant, size }),
className
)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === 'string') {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== 'collapsed' || isMobile}
{...tooltip}
/>
</Tooltip>
);
}
);
SidebarMenuButton.displayName = 'SidebarMenuButton';
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & {
asChild?: boolean;
showOnHover?: boolean;
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
showOnHover &&
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
className
)}
{...props}
/>
);
});
SidebarMenuAction.displayName = 'SidebarMenuAction';
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
'pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
className
)}
{...props}
/>
));
SidebarMenuBadge.displayName = 'SidebarMenuBadge';
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
showIcon?: boolean;
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn(
'flex h-8 items-center gap-2 rounded-md px-2',
className
)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
'--skeleton-width': width,
} as React.CSSProperties
}
/>
</div>
);
});
SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5',
'group-data-[collapsible=icon]:hidden',
className
)}
{...props}
/>
));
SidebarMenuSub.displayName = 'SidebarMenuSub';
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ ...props }, ref) => <li ref={ref} {...props} />);
SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<'a'> & {
asChild?: boolean;
size?: 'sm' | 'md';
isActive?: boolean;
}
>(({ asChild = false, size = 'md', isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'a';
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
className
)}
{...props}
/>
);
});
SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
SidebarContext,
};

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { SidebarContext } from './sidebar';
export const useSidebar = () => {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider.');
}
return context;
};

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { cn } from '@/lib/utils';
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('animate-pulse rounded-md bg-primary/10', className)}
{...props}
/>
);
}
export { Skeleton };

View File

@@ -10,6 +10,8 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
import type { DBSchema } from '@/lib/domain/db-schema';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
export type ChartDBEventType =
| 'add_tables'
@@ -70,6 +72,8 @@ export interface ChartDBContext {
schemas: DBSchema[];
relationships: DBRelationship[];
dependencies: DBDependency[];
areas: Area[];
customTypes: DBCustomType[];
currentDiagram: Diagram;
events: EventEmitter<ChartDBEvent>;
readonly?: boolean;
@@ -221,6 +225,58 @@ export interface ChartDBContext {
dependency: Partial<DBDependency>,
options?: { updateHistory: boolean }
) => Promise<void>;
// Area operations
createArea: (attributes?: Partial<Omit<Area, 'id'>>) => Promise<Area>;
addArea: (
area: Area,
options?: { updateHistory: boolean }
) => Promise<void>;
addAreas: (
areas: Area[],
options?: { updateHistory: boolean }
) => Promise<void>;
getArea: (id: string) => Area | null;
removeArea: (
id: string,
options?: { updateHistory: boolean }
) => Promise<void>;
removeAreas: (
ids: string[],
options?: { updateHistory: boolean }
) => Promise<void>;
updateArea: (
id: string,
area: Partial<Area>,
options?: { updateHistory: boolean }
) => Promise<void>;
// Custom type operations
createCustomType: (
attributes?: Partial<Omit<DBCustomType, 'id'>>
) => Promise<DBCustomType>;
addCustomType: (
customType: DBCustomType,
options?: { updateHistory: boolean }
) => Promise<void>;
addCustomTypes: (
customTypes: DBCustomType[],
options?: { updateHistory: boolean }
) => Promise<void>;
getCustomType: (id: string) => DBCustomType | null;
removeCustomType: (
id: string,
options?: { updateHistory: boolean }
) => Promise<void>;
removeCustomTypes: (
ids: string[],
options?: { updateHistory: boolean }
) => Promise<void>;
updateCustomType: (
id: string,
customType: Partial<DBCustomType>,
options?: { updateHistory: boolean }
) => Promise<void>;
}
export const chartDBContext = createContext<ChartDBContext>({
@@ -230,6 +286,8 @@ export const chartDBContext = createContext<ChartDBContext>({
tables: [],
relationships: [],
dependencies: [],
areas: [],
customTypes: [],
schemas: [],
filteredSchemas: [],
filterSchemas: emptyFn,
@@ -296,4 +354,22 @@ export const chartDBContext = createContext<ChartDBContext>({
removeDependencies: emptyFn,
addDependencies: emptyFn,
updateDependency: emptyFn,
// Area operations
createArea: emptyFn,
addArea: emptyFn,
addAreas: emptyFn,
getArea: emptyFn,
removeArea: emptyFn,
removeAreas: emptyFn,
updateArea: emptyFn,
// Custom type operations
createCustomType: emptyFn,
addCustomType: emptyFn,
addCustomTypes: emptyFn,
getCustomType: emptyFn,
removeCustomType: emptyFn,
removeCustomTypes: emptyFn,
updateCustomType: emptyFn,
});

View File

@@ -21,7 +21,14 @@ import { useLocalConfig } from '@/hooks/use-local-config';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { useEventEmitter } from 'ahooks';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import { storageInitialValue } from '../storage-context/storage-context';
import { useDiff } from '../diff-context/use-diff';
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
import {
DBCustomTypeKind,
type DBCustomType,
} from '@/lib/domain/db-custom-type';
export interface ChartDBProviderProps {
diagram?: Diagram;
@@ -30,7 +37,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 +61,37 @@ export const ChartDBProvider: React.FC<
const [dependencies, setDependencies] = useState<DBDependency[]>(
diagram?.dependencies ?? []
);
const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
diagram?.customTypes ?? []
);
const { events: diffEvents } = useDiff();
const 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;
}
@@ -125,6 +161,8 @@ export const ChartDBProvider: React.FC<
tables,
relationships,
dependencies,
areas,
customTypes,
}),
[
diagramId,
@@ -134,6 +172,8 @@ export const ChartDBProvider: React.FC<
tables,
relationships,
dependencies,
areas,
customTypes,
diagramCreatedAt,
diagramUpdatedAt,
]
@@ -145,6 +185,8 @@ export const ChartDBProvider: React.FC<
setTables([]);
setRelationships([]);
setDependencies([]);
setAreas([]);
setCustomTypes([]);
setDiagramUpdatedAt(updatedAt);
resetRedoStack();
@@ -155,6 +197,8 @@ export const ChartDBProvider: React.FC<
db.deleteDiagramTables(diagramId),
db.deleteDiagramRelationships(diagramId),
db.deleteDiagramDependencies(diagramId),
db.deleteDiagramAreas(diagramId),
db.deleteDiagramCustomTypes(diagramId),
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
@@ -167,6 +211,8 @@ export const ChartDBProvider: React.FC<
setTables([]);
setRelationships([]);
setDependencies([]);
setAreas([]);
setCustomTypes([]);
resetRedoStack();
resetUndoStack();
@@ -175,6 +221,8 @@ export const ChartDBProvider: React.FC<
db.deleteDiagramRelationships(diagramId),
db.deleteDiagram(diagramId),
db.deleteDiagramDependencies(diagramId),
db.deleteDiagramAreas(diagramId),
db.deleteDiagramCustomTypes(diagramId),
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
@@ -256,22 +304,27 @@ export const ChartDBProvider: React.FC<
);
const addTables: ChartDBContext['addTables'] = useCallback(
async (tables: DBTable[], options = { updateHistory: true }) => {
setTables((currentTables) => [...currentTables, ...tables]);
async (tablesToAdd: DBTable[], options = { updateHistory: true }) => {
setTables((currentTables) => [...currentTables, ...tablesToAdd]);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
...tables.map((table) => db.addTable({ diagramId, table })),
...tablesToAdd.map((table) =>
db.addTable({ diagramId, table })
),
]);
events.emit({ action: 'add_tables', data: { tables } });
events.emit({
action: 'add_tables',
data: { tables: tablesToAdd },
});
if (options.updateHistory) {
addUndoAction({
action: 'addTables',
redoData: { tables },
undoData: { tableIds: tables.map((t) => t.id) },
redoData: { tables: tablesToAdd },
undoData: { tableIds: tablesToAdd.map((t) => t.id) },
});
resetRedoStack();
}
@@ -730,13 +783,23 @@ export const ChartDBProvider: React.FC<
options = { updateHistory: true }
) => {
const fields = getTable(tableId)?.fields ?? [];
setTables((tables) =>
tables.map((table) =>
table.id === tableId
? { ...table, fields: [...table.fields, field] }
: table
)
);
setTables((tables) => {
return tables.map((table) => {
if (table.id === tableId) {
db.updateTable({
id: tableId,
attributes: {
...table,
fields: [...table.fields, field],
},
});
return { ...table, fields: [...table.fields, field] };
}
return table;
});
});
events.emit({
action: 'add_field',
@@ -757,13 +820,6 @@ export const ChartDBProvider: React.FC<
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.updateTable({
id: tableId,
attributes: {
...table,
fields: [...table.fields, field],
},
}),
]);
if (options.updateHistory) {
@@ -1336,6 +1392,130 @@ export const ChartDBProvider: React.FC<
]
);
// Area operations
const addAreas: ChartDBContext['addAreas'] = useCallback(
async (areas: Area[], options = { updateHistory: true }) => {
setAreas((currentAreas) => [...currentAreas, ...areas]);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
...areas.map((area) => db.addArea({ diagramId, area })),
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
]);
if (options.updateHistory) {
addUndoAction({
action: 'addAreas',
redoData: { areas },
undoData: { areaIds: areas.map((a) => a.id) },
});
resetRedoStack();
}
},
[db, diagramId, setAreas, addUndoAction, resetRedoStack]
);
const addArea: ChartDBContext['addArea'] = useCallback(
async (area: Area, options = { updateHistory: true }) => {
return addAreas([area], options);
},
[addAreas]
);
const createArea: ChartDBContext['createArea'] = useCallback(
async (attributes) => {
const area: Area = {
id: generateId(),
name: `Area ${areas.length + 1}`,
x: 0,
y: 0,
width: 300,
height: 200,
color: randomColor(),
...attributes,
};
await addArea(area);
return area;
},
[areas, addArea]
);
const getArea: ChartDBContext['getArea'] = useCallback(
(id: string) => areas.find((area) => area.id === id) ?? null,
[areas]
);
const removeAreas: ChartDBContext['removeAreas'] = useCallback(
async (ids: string[], options = { updateHistory: true }) => {
const prevAreas = [
...areas.filter((area) => ids.includes(area.id)),
];
setAreas((areas) => areas.filter((area) => !ids.includes(area.id)));
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
...ids.map((id) => db.deleteArea({ diagramId, id })),
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
]);
if (prevAreas.length > 0 && options.updateHistory) {
addUndoAction({
action: 'removeAreas',
redoData: { areaIds: ids },
undoData: { areas: prevAreas },
});
resetRedoStack();
}
},
[db, diagramId, setAreas, areas, addUndoAction, resetRedoStack]
);
const removeArea: ChartDBContext['removeArea'] = useCallback(
async (id: string, options = { updateHistory: true }) => {
return removeAreas([id], options);
},
[removeAreas]
);
const updateArea: ChartDBContext['updateArea'] = useCallback(
async (
id: string,
area: Partial<Area>,
options = { updateHistory: true }
) => {
const prevArea = getArea(id);
setAreas((areas) =>
areas.map((a) => (a.id === id ? { ...a, ...area } : a))
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.updateArea({ id, attributes: area }),
]);
if (!!prevArea && options.updateHistory) {
addUndoAction({
action: 'updateArea',
redoData: { areaId: id, area },
undoData: { areaId: id, area: prevArea },
});
resetRedoStack();
}
},
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
);
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
useCallback(
async (diagram) => {
@@ -1346,6 +1526,8 @@ export const ChartDBProvider: React.FC<
setTables(diagram?.tables ?? []);
setRelationships(diagram?.relationships ?? []);
setDependencies(diagram?.dependencies ?? []);
setAreas(diagram?.areas ?? []);
setCustomTypes(diagram?.customTypes ?? []);
setDiagramCreatedAt(diagram.createdAt);
setDiagramUpdatedAt(diagram.updatedAt);
@@ -1359,6 +1541,8 @@ export const ChartDBProvider: React.FC<
setTables,
setRelationships,
setDependencies,
setAreas,
setCustomTypes,
setDiagramCreatedAt,
setDiagramUpdatedAt,
events,
@@ -1371,6 +1555,8 @@ export const ChartDBProvider: React.FC<
includeRelationships: true,
includeTables: true,
includeDependencies: true,
includeAreas: true,
includeCustomTypes: true,
});
if (diagram) {
@@ -1382,6 +1568,150 @@ export const ChartDBProvider: React.FC<
[db, loadDiagramFromData]
);
// Custom type operations
const getCustomType: ChartDBContext['getCustomType'] = useCallback(
(id: string) => customTypes.find((type) => type.id === id) ?? null,
[customTypes]
);
const addCustomTypes: ChartDBContext['addCustomTypes'] = useCallback(
async (
customTypes: DBCustomType[],
options = { updateHistory: true }
) => {
setCustomTypes((currentTypes) => [...currentTypes, ...customTypes]);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
...customTypes.map((customType) =>
db.addCustomType({ diagramId, customType })
),
]);
if (options.updateHistory) {
addUndoAction({
action: 'addCustomTypes',
redoData: { customTypes },
undoData: { customTypeIds: customTypes.map((t) => t.id) },
});
resetRedoStack();
}
},
[db, diagramId, setCustomTypes, addUndoAction, resetRedoStack]
);
const addCustomType: ChartDBContext['addCustomType'] = useCallback(
async (customType: DBCustomType, options = { updateHistory: true }) => {
return addCustomTypes([customType], options);
},
[addCustomTypes]
);
const createCustomType: ChartDBContext['createCustomType'] = useCallback(
async (attributes) => {
const customType: DBCustomType = {
id: generateId(),
name: `type_${customTypes.length + 1}`,
kind: DBCustomTypeKind.enum,
values: [],
fields: [],
...attributes,
};
await addCustomType(customType);
return customType;
},
[addCustomType, customTypes]
);
const removeCustomTypes: ChartDBContext['removeCustomTypes'] = useCallback(
async (ids, options = { updateHistory: true }) => {
const typesToRemove = ids
.map((id) => getCustomType(id))
.filter(Boolean) as DBCustomType[];
setCustomTypes((types) =>
types.filter((type) => !ids.includes(type.id))
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
...ids.map((id) => db.deleteCustomType({ diagramId, id })),
]);
if (typesToRemove.length > 0 && options.updateHistory) {
addUndoAction({
action: 'removeCustomTypes',
redoData: {
customTypeIds: ids,
},
undoData: {
customTypes: typesToRemove,
},
});
resetRedoStack();
}
},
[
db,
diagramId,
setCustomTypes,
addUndoAction,
resetRedoStack,
getCustomType,
]
);
const removeCustomType: ChartDBContext['removeCustomType'] = useCallback(
async (id: string, options = { updateHistory: true }) => {
return removeCustomTypes([id], options);
},
[removeCustomTypes]
);
const updateCustomType: ChartDBContext['updateCustomType'] = useCallback(
async (
id: string,
customType: Partial<DBCustomType>,
options = { updateHistory: true }
) => {
const prevCustomType = getCustomType(id);
setCustomTypes((types) =>
types.map((t) => (t.id === id ? { ...t, ...customType } : t))
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.updateCustomType({ id, attributes: customType }),
]);
if (!!prevCustomType && options.updateHistory) {
addUndoAction({
action: 'updateCustomType',
redoData: { customTypeId: id, customType },
undoData: { customTypeId: id, customType: prevCustomType },
});
resetRedoStack();
}
},
[
db,
setCustomTypes,
addUndoAction,
resetRedoStack,
getCustomType,
diagramId,
]
);
return (
<chartDBContext.Provider
value={{
@@ -1391,6 +1721,7 @@ export const ChartDBProvider: React.FC<
tables,
relationships,
dependencies,
areas,
currentDiagram,
schemas,
filteredSchemas,
@@ -1438,6 +1769,21 @@ export const ChartDBProvider: React.FC<
removeDependency,
removeDependencies,
updateDependency,
createArea,
addArea,
addAreas,
getArea,
removeArea,
removeAreas,
updateArea,
customTypes,
createCustomType,
addCustomType,
addCustomTypes,
getCustomType,
removeCustomType,
removeCustomTypes,
updateCustomType,
}}
>
{children}

View File

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

View File

@@ -19,15 +19,29 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
loadConfig();
}, [getConfig]);
const updateConfig: ConfigContext['updateConfig'] = async (
config: Partial<ChartDBConfig>
) => {
await updateDataConfig(config);
setConfig((prevConfig) =>
prevConfig
? { ...prevConfig, ...config }
: { ...{ defaultDiagramId: '' }, ...config }
);
const updateConfig: ConfigContext['updateConfig'] = async ({
config,
updateFn,
}) => {
const promise = new Promise<void>((resolve) => {
setConfig((prevConfig) => {
let baseConfig: ChartDBConfig = { defaultDiagramId: '' };
if (prevConfig) {
baseConfig = prevConfig;
}
const updatedConfig = updateFn
? updateFn(baseConfig)
: { ...baseConfig, ...config };
updateDataConfig(updatedConfig).then(() => {
resolve();
});
return updatedConfig;
});
});
return promise;
};
return (

View File

@@ -8,14 +8,20 @@ import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/e
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';
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
export interface DialogContext {
// Create diagram dialog
openCreateDiagramDialog: () => void;
openCreateDiagramDialog: (
params?: Omit<CreateDiagramDialogProps, 'dialog'>
) => void;
closeCreateDiagramDialog: () => void;
// Open diagram dialog
openOpenDiagramDialog: () => void;
openOpenDiagramDialog: (
params?: Omit<OpenDiagramDialogProps, 'dialog'>
) => void;
closeOpenDiagramDialog: () => void;
// Export SQL dialog
@@ -44,10 +50,6 @@ export interface DialogContext {
openStarUsDialog: () => void;
closeStarUsDialog: () => void;
// Buckle dialog
openBuckleDialog: () => void;
closeBuckleDialog: () => void;
// Export image dialog
openExportImageDialog: (
params: Omit<ExportImageDialogProps, 'dialog'>
@@ -94,8 +96,6 @@ export const dialogContext = createContext<DialogContext>({
closeExportDiagramDialog: emptyFn,
openImportDiagramDialog: emptyFn,
closeImportDiagramDialog: emptyFn,
openBuckleDialog: emptyFn,
closeBuckleDialog: emptyFn,
openImportDBMLDialog: emptyFn,
closeImportDBMLDialog: emptyFn,
});

View File

@@ -1,7 +1,9 @@
import React, { useCallback, useState } from 'react';
import type { DialogContext } from './dialog-context';
import { dialogContext } from './dialog-context';
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
@@ -18,7 +20,6 @@ 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';
@@ -26,7 +27,29 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
const [newDiagramDialogParams, setNewDiagramDialogParams] =
useState<Omit<CreateDiagramDialogProps, 'dialog'>>();
const openNewDiagramDialogHandler: DialogContext['openCreateDiagramDialog'] =
useCallback(
(props) => {
setNewDiagramDialogParams(props);
setOpenNewDiagramDialog(true);
},
[setOpenNewDiagramDialog]
);
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
const [openDiagramDialogParams, setOpenDiagramDialogParams] =
useState<Omit<OpenDiagramDialogProps, 'dialog'>>();
const openOpenDiagramDialogHandler: DialogContext['openOpenDiagramDialog'] =
useCallback(
(props) => {
setOpenDiagramDialogParams(props);
setOpenOpenDiagramDialog(true);
},
[setOpenOpenDiagramDialog]
);
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
useState(false);
@@ -42,7 +65,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
);
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
const [openBuckleDialog, setOpenBuckleDialog] = useState(false);
// Export image dialog
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
@@ -118,9 +140,9 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
return (
<dialogContext.Provider
value={{
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
openCreateDiagramDialog: openNewDiagramDialogHandler,
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true),
openOpenDiagramDialog: openOpenDiagramDialogHandler,
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
openExportSQLDialog: openExportSQLDialogHandler,
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
@@ -135,8 +157,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),
@@ -153,8 +173,14 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
}}
>
{children}
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
<OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} />
<CreateDiagramDialog
dialog={{ open: openNewDiagramDialog }}
{...newDiagramDialogParams}
/>
<OpenDiagramDialog
dialog={{ open: openOpenDiagramDialog }}
{...openDiagramDialogParams}
/>
<ExportSQLDialog
dialog={{ open: openExportSQLDialog }}
{...exportSQLDialogParams}
@@ -178,7 +204,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
/>
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
<BuckleDialog dialog={{ open: openBuckleDialog }} />
<ImportDBMLDialog
dialog={{ open: openImportDBMLDialog }}
{...importDBMLDialogParams}

View File

@@ -0,0 +1,79 @@
import { createContext } from 'react';
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';
import type { DiffMap } from '@/lib/domain/diff/diff';
export type DiffEventType = 'diff_calculated';
export type DiffEventBase<T extends DiffEventType, D> = {
action: T;
data: D;
};
export type DiffCalculatedData = {
tablesAdded: DBTable[];
fieldsAdded: Map<string, DBField[]>;
relationshipsAdded: DBRelationship[];
};
export type DiffCalculatedEvent = DiffEventBase<
'diff_calculated',
DiffCalculatedData
>;
export type DiffEvent = DiffCalculatedEvent;
export interface DiffContext {
newDiagram: Diagram | null;
originalDiagram: 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;
getTableNewColor: ({ 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,376 @@
import React, { useCallback } from 'react';
import type {
DiffCalculatedData,
DiffContext,
DiffEvent,
} from './diff-context';
import { diffContext } from './diff-context';
import {
generateDiff,
getDiffMapKey,
} from '@/lib/domain/diff/diff-check/diff-check';
import type { Diagram } from '@/lib/domain/diagram';
import { useEventEmitter } from 'ahooks';
import type { DBField } from '@/lib/domain/db-field';
import type { DataType } from '@/lib/data/data-types/data-types';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { ChartDBDiff, DiffMap } from '@/lib/domain/diff/diff';
export const DiffProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
const [originalDiagram, setOriginalDiagram] =
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.newField.id);
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.newRelationship.id
);
if (relationship) {
relationships.push(relationship);
}
}
});
return relationships;
},
[]
);
const generateDiffCalculatedData = useCallback(
({
newDiagram,
diffMap,
}: {
newDiagram: Diagram;
diffMap: DiffMap;
}): DiffCalculatedData => {
return {
tablesAdded:
newDiagram?.tables?.filter((table) => {
const tableKey = getDiffMapKey({
diffObject: 'table',
objectId: table.id,
});
return (
diffMap.has(tableKey) &&
diffMap.get(tableKey)?.type === 'added'
);
}) ?? [],
fieldsAdded: generateNewFieldsMap({
diffMap: diffMap,
newDiagram: newDiagram,
}),
relationshipsAdded: findNewRelationships({
diffMap: diffMap,
newDiagram: newDiagram,
}),
};
},
[findNewRelationships, generateNewFieldsMap]
);
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);
setOriginalDiagram(diagram);
events.emit({
action: 'diff_calculated',
data: generateDiffCalculatedData({
diffMap: newDiffs,
newDiagram: newDiagramArg,
}),
});
},
[setDiffMap, events, generateDiffCalculatedData]
);
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 getTableNewColor = useCallback<DiffContext['getTableNewColor']>(
({ tableId }) => {
const tableColorKey = getDiffMapKey({
diffObject: 'table',
objectId: tableId,
attribute: 'color',
});
if (diffMap.has(tableColorKey)) {
const diff = diffMap.get(tableColorKey);
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,
originalDiagram,
diffMap,
hasDiff: diffMap.size > 0,
calculateDiff,
// table diff
getTableNewName,
checkIfNewTable,
checkIfTableRemoved,
checkIfTableHasChange,
getTableNewColor,
// field diff
checkIfFieldHasChange,
checkIfFieldRemoved,
checkIfNewField,
getFieldNewName,
getFieldNewType,
// relationship diff
checkIfNewRelationship,
checkIfRelationshipRemoved,
events,
}}
>
{children}
</diffContext.Provider>
);
};

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

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

View File

@@ -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,9 @@ 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';
import type { EffectiveTheme } from '../theme-context/theme-context';
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -14,6 +17,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) => {
@@ -37,8 +58,16 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
[]
);
const getBackgroundColor = useCallback(
(theme: EffectiveTheme, transparent: boolean): string => {
if (transparent) return 'transparent';
return theme === 'light' ? '#ffffff' : '#141414';
},
[]
);
const exportImage: ExportImageContext['exportImage'] = useCallback(
async (type, scale = 1) => {
async (type, { includePatternBG, transparent, scale }) => {
showLoader({
animated: false,
});
@@ -94,50 +123,59 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
defs.innerHTML = markerDefs.innerHTML;
}
const pattern = document.createElementNS(
'http://www.w3.org/2000/svg',
'pattern'
);
pattern.setAttribute('id', 'background-pattern');
pattern.setAttribute('width', String(16 * viewport.zoom));
pattern.setAttribute('height', String(16 * viewport.zoom));
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
pattern.setAttribute(
'patternTransform',
`translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
);
if (includePatternBG) {
const pattern = document.createElementNS(
'http://www.w3.org/2000/svg',
'pattern'
);
pattern.setAttribute('id', 'background-pattern');
pattern.setAttribute('width', String(16 * viewport.zoom));
pattern.setAttribute('height', String(16 * viewport.zoom));
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
pattern.setAttribute(
'patternTransform',
`translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
);
const dot = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
const dot = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
const dotSize = viewport.zoom * 0.5;
dot.setAttribute('cx', String(viewport.zoom));
dot.setAttribute('cy', String(viewport.zoom));
dot.setAttribute('r', String(dotSize));
const dotColor =
effectiveTheme === 'light' ? '#92939C' : '#777777';
dot.setAttribute('fill', dotColor);
const dotSize = viewport.zoom * 0.5;
dot.setAttribute('cx', String(viewport.zoom));
dot.setAttribute('cy', String(viewport.zoom));
dot.setAttribute('r', String(dotSize));
const dotColor =
effectiveTheme === 'light' ? '#92939C' : '#777777';
dot.setAttribute('fill', dotColor);
pattern.appendChild(dot);
defs.appendChild(pattern);
}
pattern.appendChild(dot);
defs.appendChild(pattern);
tempSvg.appendChild(defs);
const backgroundRect = document.createElementNS(
'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 +186,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: getBackgroundColor(
effectiveTheme,
transparent
),
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();
@@ -177,6 +297,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
}, 0);
},
[
getBackgroundColor,
downloadImage,
getViewport,
hideLoader,
@@ -184,6 +305,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
setNodes,
showLoader,
effectiveTheme,
logoBase64,
]
);

View File

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

View File

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

View File

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

@@ -1,7 +1,12 @@
import { emptyFn } from '@/lib/utils';
import { createContext } from 'react';
export type SidebarSection = 'tables' | 'relationships' | 'dependencies';
export type SidebarSection =
| 'tables'
| 'relationships'
| 'dependencies'
| 'areas'
| 'customTypes';
export interface LayoutContext {
openedTableInSidebar: string | undefined;
@@ -16,6 +21,14 @@ export interface LayoutContext {
openDependencyFromSidebar: (dependencyId: string) => void;
closeAllDependenciesInSidebar: () => void;
openedAreaInSidebar: string | undefined;
openAreaFromSidebar: (areaId: string) => void;
closeAllAreasInSidebar: () => void;
openedCustomTypeInSidebar: string | undefined;
openCustomTypeFromSidebar: (customTypeId: string) => void;
closeAllCustomTypesInSidebar: () => void;
selectedSidebarSection: SidebarSection;
selectSidebarSection: (section: SidebarSection) => void;
@@ -41,6 +54,14 @@ export const layoutContext = createContext<LayoutContext>({
openDependencyFromSidebar: emptyFn,
closeAllDependenciesInSidebar: emptyFn,
openedAreaInSidebar: undefined,
openAreaFromSidebar: emptyFn,
closeAllAreasInSidebar: emptyFn,
openedCustomTypeInSidebar: undefined,
openCustomTypeFromSidebar: emptyFn,
closeAllCustomTypesInSidebar: emptyFn,
selectSidebarSection: emptyFn,
openTableFromSidebar: emptyFn,
closeAllTablesInSidebar: emptyFn,

View File

@@ -14,6 +14,11 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
React.useState<string | undefined>();
const [openedDependencyInSidebar, setOpenedDependencyInSidebar] =
React.useState<string | undefined>();
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
string | undefined
>();
const [openedCustomTypeInSidebar, setOpenedCustomTypeInSidebar] =
React.useState<string | undefined>();
const [selectedSidebarSection, setSelectedSidebarSection] =
React.useState<SidebarSection>('tables');
const [isSidePanelShowed, setIsSidePanelShowed] =
@@ -30,6 +35,12 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] =
() => setOpenedDependencyInSidebar('');
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
() => setOpenedAreaInSidebar('');
const closeAllCustomTypesInSidebar: LayoutContext['closeAllCustomTypesInSidebar'] =
() => setOpenedCustomTypeInSidebar('');
const hideSidePanel: LayoutContext['hideSidePanel'] = () =>
setIsSidePanelShowed(false);
@@ -62,6 +73,21 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
setOpenedDependencyInSidebar(dependencyId);
};
const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = (
areaId
) => {
showSidePanel();
setSelectedSidebarSection('areas');
setOpenedAreaInSidebar(areaId);
};
const openCustomTypeFromSidebar: LayoutContext['openCustomTypeFromSidebar'] =
(customTypeId) => {
showSidePanel();
setSelectedSidebarSection('customTypes');
setOpenedTableInSidebar(customTypeId);
};
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
setIsSelectSchemaOpen(true);
@@ -88,6 +114,12 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
openedDependencyInSidebar,
openDependencyFromSidebar,
closeAllDependenciesInSidebar,
openedAreaInSidebar,
openAreaFromSidebar,
closeAllAreasInSidebar,
openedCustomTypeInSidebar,
openCustomTypeFromSidebar,
closeAllCustomTypesInSidebar,
}}
>
{children}

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

@@ -5,6 +5,8 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { DBTable } from '@/lib/domain/db-table';
import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
export interface StorageContext {
// Config operations
@@ -17,6 +19,8 @@ export interface StorageContext {
includeTables?: boolean;
includeRelationships?: boolean;
includeDependencies?: boolean;
includeAreas?: boolean;
includeCustomTypes?: boolean;
}) => Promise<Diagram[]>;
getDiagram: (
id: string,
@@ -24,6 +28,8 @@ export interface StorageContext {
includeTables?: boolean;
includeRelationships?: boolean;
includeDependencies?: boolean;
includeAreas?: boolean;
includeCustomTypes?: boolean;
}
) => Promise<Diagram | undefined>;
updateDiagram: (params: {
@@ -86,6 +92,40 @@ export interface StorageContext {
}) => Promise<void>;
listDependencies: (diagramId: string) => Promise<DBDependency[]>;
deleteDiagramDependencies: (diagramId: string) => Promise<void>;
// Area operations
addArea: (params: { diagramId: string; area: Area }) => Promise<void>;
getArea: (params: {
diagramId: string;
id: string;
}) => Promise<Area | undefined>;
updateArea: (params: {
id: string;
attributes: Partial<Area>;
}) => Promise<void>;
deleteArea: (params: { diagramId: string; id: string }) => Promise<void>;
listAreas: (diagramId: string) => Promise<Area[]>;
deleteDiagramAreas: (diagramId: string) => Promise<void>;
// Custom type operations
addCustomType: (params: {
diagramId: string;
customType: DBCustomType;
}) => Promise<void>;
getCustomType: (params: {
diagramId: string;
id: string;
}) => Promise<DBCustomType | undefined>;
updateCustomType: (params: {
id: string;
attributes: Partial<DBCustomType>;
}) => Promise<void>;
deleteCustomType: (params: {
diagramId: string;
id: string;
}) => Promise<void>;
listCustomTypes: (diagramId: string) => Promise<DBCustomType[]>;
deleteDiagramCustomTypes: (diagramId: string) => Promise<void>;
}
export const storageInitialValue: StorageContext = {
@@ -119,6 +159,21 @@ export const storageInitialValue: StorageContext = {
deleteDependency: emptyFn,
listDependencies: emptyFn,
deleteDiagramDependencies: emptyFn,
addArea: emptyFn,
getArea: emptyFn,
updateArea: emptyFn,
deleteArea: emptyFn,
listAreas: emptyFn,
deleteDiagramAreas: emptyFn,
// Custom type operations
addCustomType: emptyFn,
getCustomType: emptyFn,
updateCustomType: emptyFn,
deleteCustomType: emptyFn,
listCustomTypes: emptyFn,
deleteDiagramCustomTypes: emptyFn,
};
export const storageContext =

File diff suppressed because it is too large Load Diff

View File

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

View File

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

@@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, {
Suspense,
useCallback,
useEffect,
useState,
useRef,
} from 'react';
import { Button } from '@/components/button/button';
import {
DialogClose,
@@ -8,31 +14,10 @@ import {
DialogInternalContent,
DialogTitle,
} from '@/components/dialog/dialog';
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
import { DatabaseType } from '@/lib/domain/database-type';
import { databaseSecondaryLogoMap } from '@/lib/databases';
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
import { Textarea } from '@/components/textarea/textarea';
import type { DatabaseType } from '@/lib/domain/database-type';
import { Editor } from '@/components/code-snippet/code-snippet';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
import {
databaseEditionToImageMap,
databaseEditionToLabelMap,
databaseTypeToEditionMap,
} from '@/lib/domain/database-edition';
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@/components/avatar/avatar';
import { SSMSInfo } from './ssms-info/ssms-info';
import { useTranslation } from 'react-i18next';
import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
import type { DatabaseClient } from '@/lib/domain/database-clients';
import {
databaseClientToLabelMap,
databaseTypeToClientsMap,
} from '@/lib/domain/database-clients';
import type { ImportMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts';
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { Spinner } from '@/components/spinner/spinner';
@@ -40,9 +25,64 @@ import {
fixMetadataJson,
isStringMetadataJson,
} from '@/lib/data/import-metadata/utils';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/resizable/resizable';
import { useTheme } from '@/hooks/use-theme';
import type { OnChange } from '@monaco-editor/react';
import { useDebounce } from '@/hooks/use-debounce-v2';
import { InstructionsSection } from './instructions-section/instructions-section';
import { parseSQLError } from '@/lib/data/sql-import';
import type { editor } from 'monaco-editor';
import { waitFor } from '@/lib/utils';
const errorScriptOutputMessage =
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
// Helper to detect if content is likely SQL DDL or JSON
const detectContentType = (content: string): 'query' | 'ddl' | null => {
if (!content || content.trim().length === 0) return null;
// Common SQL DDL keywords
const ddlKeywords = [
'CREATE TABLE',
'ALTER TABLE',
'DROP TABLE',
'CREATE INDEX',
'CREATE VIEW',
'CREATE PROCEDURE',
'CREATE FUNCTION',
'CREATE SCHEMA',
'CREATE DATABASE',
];
const upperContent = content.toUpperCase();
// Check for SQL DDL patterns
const hasDDLKeywords = ddlKeywords.some((keyword) =>
upperContent.includes(keyword)
);
if (hasDDLKeywords) return 'ddl';
// Check if it looks like JSON
try {
// Just check structure, don't need full parse for detection
if (
(content.trim().startsWith('{') && content.trim().endsWith('}')) ||
(content.trim().startsWith('[') && content.trim().endsWith(']'))
) {
return 'query';
}
} catch (error) {
// Not valid JSON, might be partial
console.error('Error detecting content type:', error);
}
// If we can't confidently detect, return null
return null;
};
export interface ImportDatabaseProps {
goBack?: () => void;
@@ -57,6 +97,8 @@ export interface ImportDatabaseProps {
>;
keepDialogAfterImport?: boolean;
title: string;
importMethod: 'query' | 'ddl';
setImportMethod: (method: 'query' | 'ddl') => void;
}
export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
@@ -70,34 +112,52 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
setDatabaseEdition,
keepDialogAfterImport,
title,
importMethod,
setImportMethod,
}) => {
const databaseClients = databaseTypeToClientsMap[databaseType];
const { effectiveTheme } = useTheme();
const [errorMessage, setErrorMessage] = useState('');
const [databaseClient, setDatabaseClient] = useState<
DatabaseClient | undefined
>();
const { t } = useTranslation();
const [importMetadataScripts, setImportMetadataScripts] =
useState<ImportMetadataScripts | null>(null);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const { t } = useTranslation();
const { isSm: isDesktop } = useBreakpoint('sm');
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
const [isCheckingJson, setIsCheckingJson] = useState(false);
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
useEffect(() => {
const loadScripts = async () => {
const { importMetadataScripts } = await import(
'@/lib/data/import-metadata/scripts/scripts'
);
setImportMetadataScripts(importMetadataScripts);
};
loadScripts();
}, []);
setScriptResult('');
setErrorMessage('');
setShowCheckJsonButton(false);
}, [importMethod, setScriptResult]);
// Check if the ddl is valid
useEffect(() => {
if (importMethod !== 'ddl') {
return;
}
if (!scriptResult.trim()) return;
parseSQLError({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
}).then((result) => {
if (result.success) {
setErrorMessage('');
} else if (!result.success && result.error) {
setErrorMessage(result.error);
}
});
}, [importMethod, scriptResult, databaseType]);
// Check if the script result is a valid JSON
useEffect(() => {
if (importMethod !== 'query') {
return;
}
if (scriptResult.trim().length === 0) {
setErrorMessage('');
setShowCheckJsonButton(false);
@@ -117,7 +177,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
setErrorMessage(errorScriptOutputMessage);
setShowCheckJsonButton(false);
}
}, [scriptResult]);
}, [scriptResult, importMethod]);
const handleImport = useCallback(() => {
if (errorMessage.length === 0 && scriptResult.trim().length !== 0) {
@@ -125,35 +185,82 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
}
}, [errorMessage.length, onImport, scriptResult]);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const inputValue = e.target.value;
setScriptResult(inputValue);
const formatEditor = useCallback(() => {
if (editorRef.current) {
setTimeout(() => {
editorRef.current
?.getAction('editor.action.formatDocument')
?.run();
}, 50);
}
}, []);
const handleInputChange: OnChange = useCallback(
(inputValue) => {
setScriptResult(inputValue ?? '');
// Automatically open SSMS info when input length is exactly 65535
if (inputValue.length === 65535) {
if ((inputValue ?? '').length === 65535) {
setShowSSMSInfoDialog(true);
}
},
[setScriptResult]
);
const debouncedHandleInputChange = useDebounce(handleInputChange, 500);
const handleCheckJson = useCallback(async () => {
setIsCheckingJson(true);
const fixedJson = await fixMetadataJson(scriptResult);
await waitFor(1000);
const fixedJson = fixMetadataJson(scriptResult);
if (isStringMetadataJson(fixedJson)) {
setScriptResult(fixedJson);
setErrorMessage('');
formatEditor();
} else {
setScriptResult(fixedJson);
setErrorMessage(errorScriptOutputMessage);
formatEditor();
}
setShowCheckJsonButton(false);
setIsCheckingJson(false);
}, [scriptResult, setScriptResult]);
}, [scriptResult, setScriptResult, formatEditor]);
const detectAndSetImportMethod = useCallback(() => {
const content = editorRef.current?.getValue();
if (content && content.trim()) {
const detectedType = detectContentType(content);
if (detectedType && detectedType !== importMethod) {
setImportMethod(detectedType);
}
}
}, [setImportMethod, importMethod]);
const [editorDidMount, setEditorDidMount] = useState(false);
useEffect(() => {
if (editorRef.current && editorDidMount) {
editorRef.current.onDidPaste(() => {
setTimeout(() => {
editorRef.current
?.getAction('editor.action.formatDocument')
?.run();
}, 0);
setTimeout(detectAndSetImportMethod, 0);
});
}
}, [detectAndSetImportMethod, editorDidMount]);
const handleEditorDidMount = useCallback(
(editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor;
setEditorDidMount(true);
},
[]
);
const renderHeader = useCallback(() => {
return (
@@ -164,228 +271,131 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
);
}, [title]);
const renderInstructions = useCallback(
() => (
<InstructionsSection
databaseType={databaseType}
importMethod={importMethod}
setDatabaseEdition={setDatabaseEdition}
setImportMethod={setImportMethod}
databaseEdition={databaseEdition}
setShowSSMSInfoDialog={setShowSSMSInfoDialog}
showSSMSInfoDialog={showSSMSInfoDialog}
/>
),
[
databaseType,
importMethod,
setDatabaseEdition,
setImportMethod,
databaseEdition,
setShowSSMSInfoDialog,
showSSMSInfoDialog,
]
);
const renderOutputTextArea = useCallback(
() => (
<div className="flex size-full flex-col gap-1 overflow-hidden rounded-md border p-1">
<div className="w-full text-center text-xs text-muted-foreground">
{importMethod === 'query'
? 'Smart Query Output'
: 'SQL Script'}
</div>
<div className="flex-1 overflow-hidden">
<Suspense fallback={<Spinner />}>
<Editor
value={scriptResult}
onChange={debouncedHandleInputChange}
language={importMethod === 'query' ? 'json' : 'sql'}
loading={<Spinner />}
onMount={handleEditorDidMount}
theme={
effectiveTheme === 'dark'
? 'dbml-dark'
: 'dbml-light'
}
options={{
formatOnPaste: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
glyphMargin: false,
lineNumbers: 'on',
guides: {
indentation: false,
},
folding: true,
lineNumbersMinChars: 3,
renderValidationDecorations: 'off',
lineDecorationsWidth: 0,
overviewRulerBorder: false,
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
contextmenu: false,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
alwaysConsumeMouseWheel: false,
},
}}
className="size-full min-h-40"
/>
</Suspense>
</div>
{errorMessage ? (
<div className="mt-2 flex shrink-0 items-center gap-2">
<p className="text-xs text-red-700">{errorMessage}</p>
</div>
) : null}
</div>
),
[
errorMessage,
scriptResult,
importMethod,
effectiveTheme,
debouncedHandleInputChange,
handleEditorDidMount,
]
);
const renderContent = useCallback(() => {
return (
<DialogInternalContent>
<div className="flex w-full flex-1 flex-col gap-6">
{databaseTypeToEditionMap[databaseType].length > 0 ? (
<div className="flex flex-col gap-1 md:flex-row">
<p className="text-sm leading-6 text-muted-foreground">
{t(
'new_diagram_dialog.import_database.database_edition'
)}
</p>
<ToggleGroup
type="single"
className="ml-1 flex-wrap gap-2"
value={
!databaseEdition
? 'regular'
: databaseEdition
}
onValueChange={(value) => {
setDatabaseEdition(
value === 'regular'
? undefined
: (value as DatabaseEdition)
);
}}
>
<ToggleGroupItem
value="regular"
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none"
>
<Avatar className="size-4 rounded-none">
<AvatarImage
src={
databaseSecondaryLogoMap[
databaseType
]
}
alt="Regular"
/>
<AvatarFallback>Regular</AvatarFallback>
</Avatar>
Regular
</ToggleGroupItem>
{databaseTypeToEditionMap[databaseType].map(
(edition) => (
<ToggleGroupItem
value={edition}
key={edition}
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none"
>
<Avatar className="size-4">
<AvatarImage
src={
databaseEditionToImageMap[
edition
]
}
alt={
databaseEditionToLabelMap[
edition
]
}
/>
<AvatarFallback>
{
databaseEditionToLabelMap[
edition
]
}
</AvatarFallback>
</Avatar>
{databaseEditionToLabelMap[edition]}
</ToggleGroupItem>
)
)}
</ToggleGroup>
</div>
) : null}
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1 text-sm text-muted-foreground md:flex-row md:justify-between">
<div>
1.{' '}
{t('new_diagram_dialog.import_database.step_1')}
</div>
{databaseType === DatabaseType.SQL_SERVER && (
<SSMSInfo
open={showSSMSInfoDialog}
setOpen={setShowSSMSInfoDialog}
/>
)}
</div>
{databaseTypeToClientsMap[databaseType].length > 0 ? (
<Tabs
value={
!databaseClient
? 'dbclient'
: databaseClient
}
onValueChange={(value) => {
setDatabaseClient(
value === 'dbclient'
? undefined
: (value as DatabaseClient)
);
}}
>
<div className="flex flex-1">
<TabsList className="h-8 justify-start rounded-none rounded-t-sm ">
<TabsTrigger
value="dbclient"
className="h-6 w-20"
>
DB Client
</TabsTrigger>
{databaseClients?.map((client) => (
<TabsTrigger
key={client}
value={client}
className="h-6 !w-20"
>
{
databaseClientToLabelMap[
client
]
}
</TabsTrigger>
)) ?? []}
</TabsList>
</div>
<CodeSnippet
className="h-40 w-full"
loading={!importMetadataScripts}
code={
importMetadataScripts?.[databaseType]?.(
{
databaseEdition,
databaseClient,
}
) ?? ''
}
language={databaseClient ? 'shell' : 'sql'}
/>
</Tabs>
) : (
<CodeSnippet
className="h-40 w-full flex-auto"
loading={!importMetadataScripts}
code={
importMetadataScripts?.[databaseType]?.({
databaseEdition,
}) ?? ''
}
language="sql"
/>
)}
{isDesktop ? (
<ResizablePanelGroup
direction={isDesktop ? 'horizontal' : 'vertical'}
className="min-h-[500px]"
>
<ResizablePanel
defaultSize={25}
minSize={25}
maxSize={99}
className="min-h-fit rounded-md bg-gradient-to-b from-slate-50 to-slate-100 p-2 dark:from-slate-900 dark:to-slate-800 md:min-h-fit md:min-w-[350px] md:rounded-l-md md:p-2"
>
{renderInstructions()}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel className="min-h-40 py-2 md:px-2 md:py-0">
{renderOutputTextArea()}
</ResizablePanel>
</ResizablePanelGroup>
) : (
<div className="flex flex-col gap-2">
{renderInstructions()}
{renderOutputTextArea()}
</div>
<div className="flex h-48 flex-col gap-1">
<p className="text-sm text-muted-foreground">
2. {t('new_diagram_dialog.import_database.step_2')}
</p>
<Textarea
className="w-full flex-1 rounded-md bg-muted p-2 text-sm"
placeholder={t(
'new_diagram_dialog.import_database.script_results_placeholder'
)}
value={scriptResult}
onChange={handleInputChange}
/>
{showCheckJsonButton || errorMessage ? (
<div className="mt-2 flex items-center gap-2">
{showCheckJsonButton ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleCheckJson}
disabled={isCheckingJson}
>
{isCheckingJson ? (
<Spinner size="small" />
) : (
t(
'new_diagram_dialog.import_database.check_script_result'
)
)}
</Button>
) : (
<p className="text-sm text-red-700">
{errorMessage}
</p>
)}
</div>
) : null}
</div>
</div>
)}
</DialogInternalContent>
);
}, [
databaseEdition,
databaseType,
errorMessage,
handleInputChange,
scriptResult,
setDatabaseEdition,
databaseClients,
databaseClient,
importMetadataScripts,
t,
showCheckJsonButton,
isCheckingJson,
handleCheckJson,
showSSMSInfoDialog,
setShowSSMSInfoDialog,
]);
}, [renderOutputTextArea, renderInstructions, isDesktop]);
const renderFooter = useCallback(() => {
return (
<DialogFooter className="mt-4 flex !justify-between gap-2">
<DialogFooter className="flex !justify-between gap-2">
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
{goBack && (
<Button
@@ -419,7 +429,22 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
</DialogClose>
)}
{keepDialogAfterImport ? (
{showCheckJsonButton ? (
<Button
type="button"
variant="default"
onClick={handleCheckJson}
disabled={isCheckingJson}
>
{isCheckingJson ? (
<Spinner size="small" />
) : (
t(
'new_diagram_dialog.import_database.check_script_result'
)
)}
</Button>
) : keepDialogAfterImport ? (
<Button
type="button"
variant="default"
@@ -437,7 +462,6 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
type="button"
variant="default"
disabled={
showCheckJsonButton ||
scriptResult.trim().length === 0 ||
errorMessage.length > 0
}
@@ -468,6 +492,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
errorMessage.length,
scriptResult,
showCheckJsonButton,
isCheckingJson,
handleCheckJson,
goBack,
t,
]);

View File

@@ -0,0 +1,179 @@
import React from 'react';
import logo from '@/assets/logo-2.png';
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
import { DatabaseType } from '@/lib/domain/database-type';
import { databaseSecondaryLogoMap } from '@/lib/databases';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
import {
databaseEditionToImageMap,
databaseEditionToLabelMap,
databaseTypeToEditionMap,
} from '@/lib/domain/database-edition';
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@/components/avatar/avatar';
import { useTranslation } from 'react-i18next';
import { Code } from 'lucide-react';
import { SmartQueryInstructions } from './instructions/smart-query-instructions';
import { DDLInstructions } from './instructions/ddl-instructions';
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
DatabaseType.CLICKHOUSE,
DatabaseType.ORACLE,
];
export interface InstructionsSectionProps {
databaseType: DatabaseType;
databaseEdition?: DatabaseEdition;
setDatabaseEdition: React.Dispatch<
React.SetStateAction<DatabaseEdition | undefined>
>;
importMethod: 'query' | 'ddl';
setImportMethod: (method: 'query' | 'ddl') => void;
showSSMSInfoDialog: boolean;
setShowSSMSInfoDialog: (show: boolean) => void;
}
export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
databaseType,
databaseEdition,
setDatabaseEdition,
importMethod,
setImportMethod,
setShowSSMSInfoDialog,
showSSMSInfoDialog,
}) => {
const { t } = useTranslation();
return (
<div className="flex w-full flex-1 flex-col gap-4">
{databaseTypeToEditionMap[databaseType].length > 0 ? (
<div className="flex flex-col gap-1">
<p className="text-sm leading-6 text-primary">
{t(
'new_diagram_dialog.import_database.database_edition'
)}
</p>
<ToggleGroup
type="single"
className="ml-1 flex-wrap justify-start gap-2"
value={!databaseEdition ? 'regular' : databaseEdition}
onValueChange={(value) => {
setDatabaseEdition(
value === 'regular'
? undefined
: (value as DatabaseEdition)
);
}}
>
<ToggleGroupItem
value="regular"
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
>
<Avatar className="size-4 rounded-none">
<AvatarImage
src={databaseSecondaryLogoMap[databaseType]}
alt="Regular"
/>
<AvatarFallback>Regular</AvatarFallback>
</Avatar>
Regular
</ToggleGroupItem>
{databaseTypeToEditionMap[databaseType].map(
(edition) => (
<ToggleGroupItem
value={edition}
key={edition}
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
>
<Avatar className="size-4">
<AvatarImage
src={
databaseEditionToImageMap[
edition
]
}
alt={
databaseEditionToLabelMap[
edition
]
}
/>
<AvatarFallback>
{databaseEditionToLabelMap[edition]}
</AvatarFallback>
</Avatar>
{databaseEditionToLabelMap[edition]}
</ToggleGroupItem>
)
)}
</ToggleGroup>
</div>
) : null}
{DatabasesWithoutDDLInstructions.includes(databaseType) ? null : (
<div className="flex flex-col gap-1">
<p className="text-sm leading-6 text-primary">
How would you like to import?
</p>
<ToggleGroup
type="single"
className="ml-1 flex-wrap justify-start gap-2"
value={importMethod}
onValueChange={(value) => {
let selectedImportMethod: 'query' | 'ddl' = 'query';
if (value) {
selectedImportMethod = value as 'query' | 'ddl';
}
setImportMethod(selectedImportMethod);
}}
>
<ToggleGroupItem
value="query"
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
>
<Avatar className="h-3 w-4 rounded-none">
<AvatarImage src={logo} alt="query" />
<AvatarFallback>Query</AvatarFallback>
</Avatar>
Smart Query
</ToggleGroupItem>
<ToggleGroupItem
value="ddl"
variant="outline"
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
>
<Avatar className="size-4 rounded-none">
<Code size={16} />
</Avatar>
SQL Script
</ToggleGroupItem>
</ToggleGroup>
</div>
)}
<div className="flex flex-col gap-2">
<div className="text-sm font-semibold">Instructions:</div>
{importMethod === 'query' ? (
<SmartQueryInstructions
databaseType={databaseType}
databaseEdition={databaseEdition}
showSSMSInfoDialog={showSSMSInfoDialog}
setShowSSMSInfoDialog={setShowSSMSInfoDialog}
/>
) : (
<DDLInstructions
databaseType={databaseType}
databaseEdition={databaseEdition}
/>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
export interface DDLInstructionStepProps {
index: number;
text: string;
code?: string;
example?: string;
}
export const DDLInstructionStep: React.FC<DDLInstructionStepProps> = ({
index,
text,
code,
example,
}) => {
return (
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1 text-sm text-primary">
<div>
<span className="font-medium">{index}.</span> {text}
</div>
{code ? (
<div className="h-[60px]">
<CodeSnippet
className="h-full"
code={code}
language={'shell'}
/>
</div>
) : null}
{example ? (
<>
<div className="my-2">Example:</div>
<div className="h-[60px]">
<CodeSnippet
className="h-full"
code={example}
language={'shell'}
/>
</div>
</>
) : null}
</div>
</div>
);
};

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
import { DDLInstructionStep } from './ddl-instruction-step';
interface DDLInstruction {
text: string;
code?: string;
example?: string;
}
const DDLInstructionsMap: Record<DatabaseType, DDLInstruction[]> = {
[DatabaseType.GENERIC]: [],
[DatabaseType.MYSQL]: [
{
text: 'Install mysqldump.',
},
{
text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):',
code: `mysqldump -h <host> -u <username>\n-P <port> -p --no-data\n<database_name> > <output_path>`,
example: `mysqldump -h localhost -u root -P\n3306 -p --no-data my_db >\nschema_export.sql`,
},
{
text: 'Open the exported SQL file, copy its contents, and paste them here.',
},
],
[DatabaseType.POSTGRESQL]: [
{
text: 'Install pg_dump.',
},
{
text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):',
code: `pg_dump -h <host> -p <port> -d <database_name> \n -U <username> -s -F p -E UTF-8 \n -f <output_file_path>`,
example: `pg_dump -h localhost -p 5432 -d my_db \n -U postgres -s -F p -E UTF-8 \n -f schema_export.sql`,
},
{
text: 'Open the exported SQL file, copy its contents, and paste them here.',
},
],
[DatabaseType.SQLITE]: [
{
text: 'Install sqlite3.',
},
{
text: 'Execute the following command in your terminal:',
code: `sqlite3 <database_file_path>\n.dump > <output_file_path>`,
example: `sqlite3 my_db.db\n.dump > schema_export.sql`,
},
{
text: 'Open the exported SQL file, copy its contents, and paste them here.',
},
],
[DatabaseType.SQL_SERVER]: [
{
text: 'Download and install SQL Server Management Studio (SSMS).',
},
{
text: 'Connect to your SQL Server instance using SSMS.',
},
{
text: 'Right-click on the database you want to export and select Script Database as > CREATE To > New Query Editor Window.',
},
{
text: 'Copy the generated script and paste it here.',
},
],
[DatabaseType.CLICKHOUSE]: [],
[DatabaseType.COCKROACHDB]: [
{
text: 'Install pg_dump.',
},
{
text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):',
code: `pg_dump -h <host> -p <port> -d <database_name> \n -U <username> -s -F p -E UTF-8 \n -f <output_file_path>`,
example: `pg_dump -h localhost -p 5432 -d my_db \n -U postgres -s -F p -E UTF-8 \n -f schema_export.sql`,
},
{
text: 'Open the exported SQL file, copy its contents, and paste them here.',
},
],
[DatabaseType.MARIADB]: [
{
text: 'Install mysqldump.',
},
{
text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):',
code: `mysqldump -h <host> -u <username>\n-P <port> -p --no-data\n<database_name> > <output_path>`,
example: `mysqldump -h localhost -u root -P\n3306 -p --no-data my_db >\nschema_export.sql`,
},
{
text: 'Open the exported SQL file, copy its contents, and paste them here.',
},
],
[DatabaseType.ORACLE]: [],
};
export interface DDLInstructionsProps {
databaseType: DatabaseType;
databaseEdition?: DatabaseEdition;
}
export const DDLInstructions: React.FC<DDLInstructionsProps> = ({
databaseType,
}) => {
return (
<>
{DDLInstructionsMap[databaseType].map((instruction, index) => (
<DDLInstructionStep
key={index}
index={index + 1}
text={instruction.text}
code={instruction.code}
example={instruction.example}
/>
))}
</>
);
};

View File

@@ -0,0 +1,147 @@
import React, { useEffect, useMemo, useState } from 'react';
import { DatabaseType } from '@/lib/domain/database-type';
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
import { SSMSInfo } from './ssms-info/ssms-info';
import { useTranslation } from 'react-i18next';
import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
import type { DatabaseClient } from '@/lib/domain/database-clients';
import { minimizeQuery } from '@/lib/data/import-metadata/utils';
import {
databaseClientToLabelMap,
databaseTypeToClientsMap,
databaseEditionToClientsMap,
} from '@/lib/domain/database-clients';
import type { ImportMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts';
export interface SmartQueryInstructionsProps {
databaseType: DatabaseType;
databaseEdition?: DatabaseEdition;
showSSMSInfoDialog: boolean;
setShowSSMSInfoDialog: (show: boolean) => void;
}
export const SmartQueryInstructions: React.FC<SmartQueryInstructionsProps> = ({
databaseType,
databaseEdition,
showSSMSInfoDialog,
setShowSSMSInfoDialog,
}) => {
const databaseClients = useMemo(
() => [
...databaseTypeToClientsMap[databaseType],
...(databaseEdition
? databaseEditionToClientsMap[databaseEdition]
: []),
],
[databaseType, databaseEdition]
);
const [databaseClient, setDatabaseClient] = useState<
DatabaseClient | undefined
>();
const { t } = useTranslation();
const [importMetadataScripts, setImportMetadataScripts] =
useState<ImportMetadataScripts | null>(null);
const code = useMemo(
() =>
(databaseClients.length > 0
? importMetadataScripts?.[databaseType]?.({
databaseEdition,
databaseClient,
})
: importMetadataScripts?.[databaseType]?.({
databaseEdition,
})) ?? '',
[
databaseType,
databaseEdition,
databaseClients,
importMetadataScripts,
databaseClient,
]
);
useEffect(() => {
const loadScripts = async () => {
const { importMetadataScripts } = await import(
'@/lib/data/import-metadata/scripts/scripts'
);
setImportMetadataScripts(importMetadataScripts);
};
loadScripts();
}, []);
return (
<>
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1 text-sm text-primary">
<div>
<span className="font-medium">1.</span>{' '}
{t('new_diagram_dialog.import_database.step_1')}
</div>
{databaseType === DatabaseType.SQL_SERVER && (
<SSMSInfo
open={showSSMSInfoDialog}
setOpen={setShowSSMSInfoDialog}
/>
)}
</div>
{databaseClients.length > 0 ? (
<Tabs
value={!databaseClient ? 'dbclient' : databaseClient}
onValueChange={(value) => {
setDatabaseClient(
value === 'dbclient'
? undefined
: (value as DatabaseClient)
);
}}
>
<div className="flex flex-1">
<TabsList className="h-8 justify-start rounded-none rounded-t-sm ">
<TabsTrigger
value="dbclient"
className="h-6 w-20"
>
DB Client
</TabsTrigger>
{databaseClients?.map((client) => (
<TabsTrigger
key={client}
value={client}
className="h-6 !w-20"
>
{databaseClientToLabelMap[client]}
</TabsTrigger>
)) ?? []}
</TabsList>
</div>
<CodeSnippet
className="h-40 w-full md:h-[200px]"
loading={!importMetadataScripts}
code={minimizeQuery(code)}
codeToCopy={code}
language={databaseClient ? 'shell' : 'sql'}
/>
</Tabs>
) : (
<CodeSnippet
className="h-40 w-full flex-auto md:h-[200px]"
loading={!importMetadataScripts}
code={minimizeQuery(code)}
codeToCopy={code}
language="sql"
/>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm text-primary">
<span className="font-medium">2.</span>{' '}
{t('new_diagram_dialog.import_database.step_2')}
</p>
</div>
</>
);
};

View File

@@ -17,6 +17,7 @@ import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
import { ImportDatabase } from '../common/import-database/import-database';
import { useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { sqlImportToDiagram } from '@/lib/data/sql-import';
export interface CreateDiagramDialogProps extends BaseDialogProps {}
@@ -25,6 +26,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
}) => {
const { diagramId } = useChartDB();
const { t } = useTranslation();
const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query');
const [databaseType, setDatabaseType] = useState<DatabaseType>(
DatabaseType.GENERIC
);
@@ -41,6 +43,11 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
const [diagramNumber, setDiagramNumber] = useState<number>(1);
const navigate = useNavigate();
useEffect(() => {
setDatabaseEdition(undefined);
setImportMethod('query');
}, [databaseType]);
useEffect(() => {
const fetchDiagrams = async () => {
const diagrams = await listDiagrams();
@@ -54,29 +61,41 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
setDatabaseType(DatabaseType.GENERIC);
setDatabaseEdition(undefined);
setScriptResult('');
setImportMethod('query');
}, [dialog.open]);
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
const importNewDiagram = useCallback(async () => {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
let diagram: Diagram | undefined;
const diagram = await loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
diagramNumber,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
});
if (importMethod === 'ddl') {
diagram = await sqlImportToDiagram({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
});
} else {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
diagram = await loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
diagramNumber,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
});
}
await addDiagram({ diagram });
await updateConfig({ defaultDiagramId: diagram.id });
await updateConfig({ config: { defaultDiagramId: diagram.id } });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
}, [
importMethod,
databaseType,
addDiagram,
databaseEdition,
@@ -101,7 +120,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
};
await addDiagram({ diagram });
await updateConfig({ defaultDiagramId: diagram.id });
await updateConfig({ config: { defaultDiagramId: diagram.id } });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
setTimeout(
@@ -133,7 +152,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
}}
>
<DialogContent
className="flex max-h-screen w-[90vw] max-w-[90vw] flex-col overflow-y-auto md:overflow-visible lg:max-w-[60vw] xl:lg:max-w-lg xl:min-w-[45vw]"
className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
showClose={hasExistingDiagram}
>
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
@@ -159,6 +178,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
}
setScriptResult={setScriptResult}
title={t('new_diagram_dialog.import_database.title')}
importMethod={importMethod}
setImportMethod={setImportMethod}
/>
)}
</DialogContent>

View File

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

View File

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

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

View File

@@ -87,7 +87,12 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
};
if (targetDatabaseType === DatabaseType.GENERIC) {
return Promise.resolve(exportBaseSQL(filteredDiagram));
return Promise.resolve(
exportBaseSQL({
diagram: filteredDiagram,
targetDatabaseType,
})
);
} else {
return exportSQL(filteredDiagram, targetDatabaseType, {
stream: true,
@@ -135,7 +140,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
components={[
<a
key={0}
href="mailto:chartdb.io@gmail.com"
href="mailto:support@chartdb.io"
target="_blank"
className="text-pink-600 hover:underline"
rel="noreferrer"

View File

@@ -6,6 +6,7 @@ import { ImportDatabase } from '../common/import-database/import-database';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
import { loadDatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
import type { Diagram } from '@/lib/domain/diagram';
import { loadFromDatabaseMetadata } from '@/lib/domain/diagram';
import { useChartDB } from '@/hooks/use-chartdb';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
@@ -13,6 +14,7 @@ import { Trans, useTranslation } from 'react-i18next';
import { useReactFlow } from '@xyflow/react';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useAlert } from '@/context/alert-context/alert-context';
import { sqlImportToDiagram } from '@/lib/data/sql-import';
export interface ImportDatabaseDialogProps extends BaseDialogProps {
databaseType: DatabaseType;
@@ -22,6 +24,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
dialog,
databaseType,
}) => {
const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query');
const { closeImportDatabaseDialog } = useDialog();
const { showAlert } = useAlert();
const {
@@ -43,6 +46,10 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
DatabaseEdition | undefined
>();
useEffect(() => {
setDatabaseEdition(undefined);
}, [databaseType]);
useEffect(() => {
if (!dialog.open) return;
setDatabaseEdition(undefined);
@@ -50,17 +57,27 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
}, [dialog.open]);
const importDatabase = useCallback(async () => {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
let diagram: Diagram | undefined;
const diagram = await loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
});
if (importMethod === 'ddl') {
diagram = await sqlImportToDiagram({
sqlContent: scriptResult,
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
});
} else {
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
diagram = await loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
databaseEdition:
databaseEdition?.trim().length === 0
? undefined
: databaseEdition,
});
}
const tableIdsToRemove = tables
.filter((table) =>
@@ -304,6 +321,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
closeImportDatabaseDialog();
}, [
importMethod,
databaseEdition,
currentDatabaseType,
updateDatabaseType,
@@ -333,7 +351,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
}}
>
<DialogContent
className="flex max-h-screen w-[90vw] flex-col overflow-y-auto md:overflow-visible xl:min-w-[45vw]"
className="flex max-h-screen w-full flex-col md:max-w-[900px]"
showClose
>
<ImportDatabase
@@ -345,6 +363,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
setScriptResult={setScriptResult}
keepDialogAfterImport
title={t('import_database_dialog.title', { diagramName })}
importMethod={importMethod}
setImportMethod={setImportMethod}
/>
</DialogContent>
</Dialog>

View File

@@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next';
import { Editor } from '@/components/code-snippet/code-snippet';
import { useTheme } from '@/hooks/use-theme';
import { AlertCircle } from 'lucide-react';
import { importDBMLToDiagram } from '@/lib/dbml-import';
import { importDBMLToDiagram, sanitizeDBML } from '@/lib/dbml-import';
import { useChartDB } from '@/hooks/use-chartdb';
import { Parser } from '@dbml/core';
import { useCanvas } from '@/hooks/use-canvas';
@@ -189,8 +189,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
if (!content.trim()) return;
try {
const sanitizedContent = sanitizeDBML(content);
const parser = new Parser();
parser.parse(content, 'dbml');
parser.parse(sanitizedContent, 'dbml');
} catch (e) {
const parsedError = parseDBMLError(e);
if (parsedError) {
@@ -241,7 +242,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
if (!dbmlContent.trim() || errorMessage) return;
try {
const importedDiagram = await importDBMLToDiagram(dbmlContent);
// Sanitize DBML content before importing
const sanitizedContent = sanitizeDBML(dbmlContent);
const importedDiagram = await importDBMLToDiagram(sanitizedContent);
const tableIdsToRemove = tables
.filter((table) =>
importedDiagram.tables?.some(
@@ -330,7 +333,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
}}
>
<DialogContent
className="flex h-[80vh] max-h-screen flex-col"
className="flex h-[80vh] max-h-screen w-full flex-col md:max-w-[900px]"
showClose
>
<DialogHeader>

View File

@@ -28,10 +28,13 @@ 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();
@@ -62,7 +65,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
const openDiagram = useCallback(
(diagramId: string) => {
if (diagramId) {
updateConfig({ defaultDiagramId: diagramId });
updateConfig({ config: { defaultDiagramId: diagramId } });
navigate(`/diagrams/${diagramId}`);
}
},
@@ -122,14 +125,14 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
<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>
@@ -226,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

@@ -30,6 +30,14 @@
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--subtitle: 215.3 19.3% 34.5%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
@@ -58,6 +66,14 @@
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--subtitle: 212.7 26.8% 83.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}

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;
}

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,
};
};

23
src/hooks/use-mobile.tsx Normal file
View File

@@ -0,0 +1,23 @@
import * as React from 'react';
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined
);
React.useEffect(() => {
const mql = window.matchMedia(
`(max-width: ${MOBILE_BREAKPOINT - 1}px)`
);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);
return !!isMobile;
}

View File

@@ -34,16 +34,15 @@ 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: 'مساعدة',
visit_website: 'ChartDB قم بزيارة',
join_discord: 'Discord انضم إلينا على',
schedule_a_call: '!تحدث معنا',
docs_website: 'الوثائق',
join_discord: 'انضم إلينا على Discord',
},
},
@@ -150,6 +149,8 @@ export const ar: LanguageTranslation = {
comments: 'تعليقات',
no_comments: 'لا يوجد تعليقات',
delete_field: 'حذف الحقل',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'خصائص الفهرس',
@@ -209,6 +210,54 @@ export const ar: LanguageTranslation = {
description: 'إنشاء اعتماد للبدء',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -235,7 +284,7 @@ export const ar: LanguageTranslation = {
title: 'إسترد قاعدة بياناتك',
database_edition: ':إصدار قاعدة البيانات',
step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك',
step_2: ':إلصق نتيجة البرنامج النصي هنا',
step_2: ':إلصق نتيجة البرنامج النصي هنا',
script_results_placeholder: '...نتيجة البرنامج النصي هنا',
ssms_instructions: {
button_text: 'SSMS تعليمات',
@@ -329,6 +378,12 @@ export const ar: LanguageTranslation = {
scale_4x: '4x',
cancel: 'إلغاء',
export: 'تصدير',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -361,7 +416,7 @@ export const ar: LanguageTranslation = {
error: {
title: 'حدث خطأ أثناء التصدير',
description:
'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
'support@chartdb.io حدث خطأ ما. هل تحتاج إلى مساعدة؟',
},
},
import_diagram_dialog: {
@@ -372,7 +427,7 @@ export const ar: LanguageTranslation = {
error: {
title: 'حدث خطأ أثناء الاستيراد',
description:
'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
'support@chartdb.io و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
},
},
import_dbml_dialog: {
@@ -399,6 +454,8 @@ export const ar: LanguageTranslation = {
canvas_context_menu: {
new_table: 'جدول جديد',
new_relationship: 'علاقة جديدة',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -35,16 +35,15 @@ export const bn: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
share: {
share: 'শেয়ার করুন',
backup: {
backup: 'ব্যাকআপ',
export_diagram: 'ডায়াগ্রাম রপ্তানি করুন',
import_diagram: 'ডায়াগ্রাম আমদানি করুন',
restore_diagram: 'ডায়াগ্রাম পুনরুদ্ধার করুন',
},
help: {
help: 'সাহায্য',
visit_website: 'ChartDB ওয়েবসাইটে যান',
docs_website: 'ডকুমেন্টেশন',
join_discord: 'আমাদের Discord-এ যোগ দিন',
schedule_a_call: 'আমাদের সাথে কথা বলুন!',
},
},
@@ -151,6 +150,8 @@ export const bn: LanguageTranslation = {
comments: 'মন্তব্য',
no_comments: 'কোনো মন্তব্য নেই',
delete_field: 'ফিল্ড মুছুন',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ইনডেক্স কর্ম',
@@ -210,6 +211,53 @@ export const bn: LanguageTranslation = {
description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -236,7 +284,7 @@ export const bn: LanguageTranslation = {
title: 'আপনার ডাটাবেস আমদানি করুন',
database_edition: 'ডাটাবেস সংস্করণ:',
step_1: 'আপনার ডাটাবেসে এই স্ক্রিপ্ট চালান:',
step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন:',
step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন',
script_results_placeholder: 'স্ক্রিপ্টের ফলাফল এখানে...',
ssms_instructions: {
button_text: 'SSMS নির্দেশনা',
@@ -330,6 +378,12 @@ export const bn: LanguageTranslation = {
scale_4x: '4x',
cancel: 'বাতিল করুন',
export: 'রপ্তানি করুন',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -364,7 +418,7 @@ export const bn: LanguageTranslation = {
error: {
title: 'চিত্র রপ্তানিতে ত্রুটি',
description:
'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
},
},
@@ -376,7 +430,7 @@ export const bn: LanguageTranslation = {
error: {
title: 'চিত্র আমদানিতে ত্রুটি',
description:
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
},
},
// TODO: Translate
@@ -403,6 +457,8 @@ export const bn: LanguageTranslation = {
canvas_context_menu: {
new_table: 'নতুন টেবিল',
new_relationship: 'নতুন সম্পর্ক',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -35,16 +35,15 @@ 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',
visit_website: 'ChartDB Webseite',
docs_website: 'Dokumentation',
join_discord: 'Auf Discord beitreten',
schedule_a_call: 'Gespräch vereinbaren',
},
},
@@ -152,6 +151,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',
@@ -212,6 +213,53 @@ export const de: LanguageTranslation = {
description: 'Erstellen Sie eine Ansicht, um zu beginnen',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -238,7 +286,7 @@ export const de: LanguageTranslation = {
title: 'Datenbank importieren',
database_edition: 'Datenbank Edition:',
step_1: 'Führen Sie dieses Skript in Ihrer Datenbank aus:',
step_2: 'Fügen Sie das Skriptergebnis hier ein:',
step_2: 'Fügen Sie das Skriptergebnis hier ein',
script_results_placeholder: 'Skriptergebnisse hier...',
ssms_instructions: {
button_text: 'SSMS Anweisungen',
@@ -333,6 +381,12 @@ export const de: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Abbrechen',
export: 'Exportieren',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -367,7 +421,7 @@ export const de: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -379,7 +433,7 @@ export const de: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -406,6 +460,8 @@ export const de: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Neue Tabelle',
new_relationship: 'Neue Beziehung',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -33,16 +33,15 @@ 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',
visit_website: 'Visit ChartDB',
docs_website: 'Docs',
join_discord: 'Join us on Discord',
schedule_a_call: 'Talk with us!',
},
},
@@ -144,6 +143,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',
@@ -206,6 +206,52 @@ export const en = {
description: 'Create a view to get started',
},
},
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -232,7 +278,7 @@ export const en = {
title: 'Import your Database',
database_edition: 'Database Edition:',
step_1: 'Run this script in your database:',
step_2: 'Paste the script result here:',
step_2: 'Paste the script result into this modal →',
script_results_placeholder: 'Script results here...',
ssms_instructions: {
button_text: 'SSMS Instructions',
@@ -326,6 +372,11 @@ export const en = {
scale_4x: '4x',
cancel: 'Cancel',
export: 'Export',
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -359,7 +410,7 @@ export const en = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -371,7 +422,7 @@ export const en = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
@@ -398,6 +449,7 @@ export const en = {
canvas_context_menu: {
new_table: 'New Table',
new_relationship: 'New Relationship',
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -34,17 +34,15 @@ 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',
visit_website: 'Visitar ChartDB',
docs_website: 'Documentación',
join_discord: 'Únete a nosotros en Discord',
schedule_a_call: '¡Habla con nosotros!',
},
},
@@ -142,6 +140,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',
@@ -201,6 +201,53 @@ export const es: LanguageTranslation = {
description: 'Crea una vista para comenzar',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -227,7 +274,7 @@ export const es: LanguageTranslation = {
title: 'Importa tu Base de Datos',
database_edition: 'Edición de Base de Datos:',
step_1: 'Ejecuta este script en tu base de datos:',
step_2: 'Pega el resultado del script aquí:',
step_2: 'Pega el resultado del script aquí',
script_results_placeholder: 'Resultados del script aquí...',
ssms_instructions: {
button_text: 'Instrucciones SSMS',
@@ -323,6 +370,12 @@ export const es: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Cancelar',
export: 'Exportar',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -366,7 +419,7 @@ export const es: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -378,7 +431,7 @@ export const es: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -405,6 +458,8 @@ export const es: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nueva Tabla',
new_relationship: 'Nueva Relación',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -33,16 +33,15 @@ export const fr: LanguageTranslation = {
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',
visit_website: 'Visitez ChartDB',
docs_website: 'Documentation',
join_discord: 'Rejoignez-nous sur Discord',
schedule_a_call: 'Parlez avec nous !',
},
},
@@ -139,6 +138,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",
@@ -198,6 +199,53 @@ export const fr: LanguageTranslation = {
description: 'Créez une vue pour commencer',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -224,7 +272,7 @@ export const fr: LanguageTranslation = {
title: 'Importer votre Base de Données',
database_edition: 'Édition de la Base de Données :',
step_1: 'Exécutez ce script dans votre base de données :',
step_2: 'Collez le résultat du script ici :',
step_2: 'Collez le résultat du script ici ',
script_results_placeholder: 'Résultats du script ici...',
ssms_instructions: {
button_text: 'Instructions SSMS',
@@ -285,6 +333,12 @@ export const fr: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Annuler',
export: 'Exporter',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
multiple_schemas_alert: {
@@ -362,7 +416,7 @@ export const fr: LanguageTranslation = {
error: {
title: "Erreur lors de l'exportation du diagramme",
description:
"Une erreur s'est produite. Besoin d'aide ? chartdb.io@gmail.com",
"Une erreur s'est produite. Besoin d'aide ? support@chartdb.io",
},
},
import_diagram_dialog: {
@@ -373,7 +427,7 @@ export const fr: LanguageTranslation = {
error: {
title: "Erreur lors de l'exportation du diagramme",
description:
"Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? chartdb.io@gmail.com",
"Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? support@chartdb.io",
},
},
import_dbml_dialog: {
@@ -401,6 +455,8 @@ export const fr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nouvelle Table',
new_relationship: 'Nouvelle Relation',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -35,16 +35,15 @@ export const gu: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
share: {
share: 'શેર કરો',
backup: {
backup: 'બેકઅપ',
export_diagram: 'ડાયાગ્રામ નિકાસ કરો',
import_diagram: 'ડાયાગ્રામ આયાત કરો',
restore_diagram: 'ડાયાગ્રામ પુનઃસ્થાપિત કરો',
},
help: {
help: 'મદદ',
visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
docs_website: 'દસ્તાવેજીકરણ',
join_discord: 'અમારા Discordમાં જોડાઓ',
schedule_a_call: 'અમારી સાથે વાત કરો!',
},
},
@@ -152,6 +151,8 @@ export const gu: LanguageTranslation = {
comments: 'ટિપ્પણીઓ',
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
delete_field: 'ફીલ્ડ કાઢી નાખો',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ઇન્ડેક્સ લક્ષણો',
@@ -211,6 +212,53 @@ export const gu: LanguageTranslation = {
description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -236,7 +284,7 @@ export const gu: LanguageTranslation = {
title: 'તમારું ડેટાબેસ આયાત કરો',
database_edition: 'ડેટાબેસ આવૃત્તિ:',
step_1: 'તમારા ડેટાબેસમાં આ સ્ક્રિપ્ટ ચલાવો:',
step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો:',
step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો',
script_results_placeholder: 'સ્ક્રિપ્ટના પરિણામ અહીં...',
ssms_instructions: {
button_text: 'SSMS સૂચનાઓ',
@@ -330,6 +378,12 @@ export const gu: LanguageTranslation = {
scale_4x: '4x',
cancel: 'રદ કરો',
export: 'નિકાસ કરો',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -364,7 +418,7 @@ export const gu: LanguageTranslation = {
error: {
title: 'ડાયાગ્રામ નિકાસમાં ભૂલ',
description:
'કશુક તો ખોટું થયું. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
'કશુક તો ખોટું થયું. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.',
},
},
@@ -376,7 +430,7 @@ export const gu: LanguageTranslation = {
error: {
title: 'ડાયાગ્રામ આયાતમાં ભૂલ',
description:
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.',
},
},
// TODO: Translate
@@ -403,6 +457,8 @@ export const gu: LanguageTranslation = {
canvas_context_menu: {
new_table: 'નવું ટેબલ',
new_relationship: 'નવો સંબંધ',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -34,17 +34,15 @@ 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: 'मदद',
visit_website: 'ChartDB वेबसाइट पर जाएँ',
docs_website: 'દસ્તાવેજીકરણ',
join_discord: 'हमसे Discord पर जुड़ें',
schedule_a_call: 'हमसे बात करें!',
},
},
@@ -152,6 +150,8 @@ export const hi: LanguageTranslation = {
comments: 'टिप्पणियाँ',
no_comments: 'कोई टिप्पणी नहीं',
delete_field: 'फ़ील्ड हटाएँ',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'सूचकांक विशेषताएँ',
@@ -212,6 +212,53 @@ export const hi: LanguageTranslation = {
description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -238,7 +285,7 @@ export const hi: LanguageTranslation = {
title: 'अपना डेटाबेस आयात करें',
database_edition: 'डेटाबेस संस्करण:',
step_1: 'अपने डेटाबेस में यह स्क्रिप्ट चलाएँ:',
step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें:',
step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें',
script_results_placeholder: 'स्क्रिप्ट के परिणाम यहाँ...',
ssms_instructions: {
button_text: 'SSMS निर्देश',
@@ -334,6 +381,12 @@ export const hi: LanguageTranslation = {
scale_4x: '4x',
cancel: 'रद्द करें',
export: 'निर्यात करें',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -368,7 +421,7 @@ export const hi: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -380,7 +433,7 @@ export const hi: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -407,6 +460,8 @@ export const hi: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नई तालिका',
new_relationship: 'नया संबंध',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -34,16 +34,15 @@ 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',
visit_website: 'Kunjungi ChartDB',
docs_website: 'Dokumentasi',
join_discord: 'Bergabunglah di Discord kami',
schedule_a_call: 'Berbicara dengan kami!',
},
},
@@ -150,6 +149,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',
@@ -209,6 +210,53 @@ export const id_ID: LanguageTranslation = {
description: 'Buat tampilan untuk memulai',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -235,7 +283,7 @@ export const id_ID: LanguageTranslation = {
title: 'Impor Database Anda',
database_edition: 'Edisi Database:',
step_1: 'Jalankan skrip ini di database Anda:',
step_2: 'Tempel hasil skrip di sini:',
step_2: 'Tempel hasil skrip di sini',
script_results_placeholder: 'Hasil skrip di sini...',
ssms_instructions: {
button_text: 'Instruksi SSMS',
@@ -328,6 +376,12 @@ export const id_ID: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Batal',
export: 'Ekspor',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -362,7 +416,7 @@ export const id_ID: LanguageTranslation = {
error: {
title: 'Error ekspor diagram',
description:
'Sesuatu yang salah. Butuh bantuan? chartdb.io@gmail.com',
'Sesuatu yang salah. Butuh bantuan? support@chartdb.io',
},
},
@@ -374,7 +428,7 @@ export const id_ID: LanguageTranslation = {
error: {
title: 'Error impor diagram',
description:
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com',
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? support@chartdb.io',
},
},
// TODO: Translate
@@ -402,6 +456,8 @@ export const id_ID: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Tabel Baru',
new_relationship: 'Hubungan Baru',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -36,16 +36,15 @@ 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: 'ヘルプ',
visit_website: 'ChartDBにアクセス',
docs_website: 'ドキュメント',
join_discord: 'Discordに参加',
schedule_a_call: '話しかけてください!',
},
},
@@ -154,6 +153,8 @@ export const ja: LanguageTranslation = {
comments: 'コメント',
no_comments: 'コメントがありません',
delete_field: 'フィールドを削除',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'インデックス属性',
@@ -215,6 +216,53 @@ export const ja: LanguageTranslation = {
description: 'Create a view to get started',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -241,7 +289,7 @@ export const ja: LanguageTranslation = {
title: 'データベースをインポート',
database_edition: 'データベースエディション:',
step_1: 'このスクリプトをデータベースで実行してください:',
step_2: 'ここにスクリプトの結果を貼り付けてください:',
step_2: 'ここにスクリプトの結果を貼り付けてください',
script_results_placeholder: 'ここにスクリプトの結果...',
ssms_instructions: {
button_text: 'SSMSの手順',
@@ -337,6 +385,12 @@ export const ja: LanguageTranslation = {
scale_4x: '4x',
cancel: 'キャンセル',
export: 'エクスポート',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -371,7 +425,7 @@ export const ja: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -383,7 +437,7 @@ export const ja: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -410,6 +464,8 @@ export const ja: LanguageTranslation = {
canvas_context_menu: {
new_table: '新しいテーブル',
new_relationship: '新しいリレーションシップ',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -34,16 +34,15 @@ 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: '도움말',
visit_website: 'ChartDB 사이트 방문',
docs_website: '선적 서류 비치',
join_discord: 'Discord 가입',
schedule_a_call: 'Talk with us!',
},
},
@@ -150,6 +149,8 @@ export const ko_KR: LanguageTranslation = {
comments: '주석',
no_comments: '주석 없음',
delete_field: '필드 삭제',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '인덱스 속성',
@@ -209,6 +210,53 @@ export const ko_KR: LanguageTranslation = {
description: '뷰 테이블을 만들어 시작하세요.',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -235,7 +283,7 @@ export const ko_KR: LanguageTranslation = {
title: '당신의 데이터베이스를 가져오세요',
database_edition: '데이터베이스 세부 종류:',
step_1: '데이터베이스에서 아래의 SQL을 실행해주세요:',
step_2: '이곳에 결과를 붙여넣어주세요:',
step_2: '이곳에 결과를 붙여넣어주세요',
script_results_placeholder: '이곳에 스크립트 결과를 입력...',
ssms_instructions: {
button_text: 'SSMS을 사용하시는 경우',
@@ -328,6 +376,12 @@ export const ko_KR: LanguageTranslation = {
scale_4x: '4x',
cancel: '취소',
export: '내보내기',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -361,7 +415,7 @@ export const ko_KR: LanguageTranslation = {
error: {
title: '다이어그램 내보내기 오류',
description:
'무언가 문제가 발생하였습니다. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
'무언가 문제가 발생하였습니다. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.',
},
},
import_diagram_dialog: {
@@ -372,7 +426,7 @@ export const ko_KR: LanguageTranslation = {
error: {
title: '다이어그램 가져오기 오류',
description:
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.',
},
},
// TODO: Translate
@@ -399,6 +453,8 @@ export const ko_KR: LanguageTranslation = {
canvas_context_menu: {
new_table: '새 테이블',
new_relationship: '새 연관관계',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -34,17 +34,16 @@ 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: 'मदत',
visit_website: 'ChartDB ला भेट द्या',
docs_website: 'दस्तऐवजीकरण',
join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
schedule_a_call: 'आमच्याशी बोला!',
},
},
@@ -153,6 +152,8 @@ export const mr: LanguageTranslation = {
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'इंडेक्स गुणधर्म',
@@ -214,6 +215,53 @@ export const mr: LanguageTranslation = {
description: 'सुरू करण्यासाठी एक दृश्य तयार करा',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -240,7 +288,7 @@ export const mr: LanguageTranslation = {
title: 'तुमचा डेटाबेस आयात करा',
database_edition: 'डेटाबेस संस्करण:',
step_1: 'तुमच्या डेटाबेसमध्ये हा स्क्रिप्ट चालवा:',
step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा:',
step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा',
script_results_placeholder: 'स्क्रिप्ट परिणाम येथे...',
ssms_instructions: {
button_text: 'SSMS सूचना',
@@ -336,6 +384,12 @@ export const mr: LanguageTranslation = {
scale_4x: '4x',
cancel: 'रद्द करा',
export: 'निर्यात करा',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -371,7 +425,7 @@ export const mr: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -384,7 +438,7 @@ export const mr: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -412,6 +466,8 @@ export const mr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नवीन टेबल',
new_relationship: 'नवीन रिलेशनशिप',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -34,16 +34,16 @@ 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: 'मद्दत',
visit_website: 'वेबसाइटमा जानुहोस्',
docs_website: 'कागजात',
join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
schedule_a_call: 'कल अनुसूची गर्नुहोस्',
},
},
@@ -150,6 +150,8 @@ export const ne: LanguageTranslation = {
comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्',
delete_field: 'क्षेत्र हटाउनुहोस्',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'सूचक विशेषताहरू',
@@ -210,6 +212,53 @@ export const ne: LanguageTranslation = {
'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -237,7 +286,7 @@ export const ne: LanguageTranslation = {
title: 'तपाईंको डाटाबेस आयात गर्नुहोस्',
database_edition: 'डाटाबेस संस्करण:',
step_1: 'तपाईंको डाटाबेसमा यो स्क्रिप्ट चलाउनुहोस्:',
step_2: 'यो स्क्रिप्ट परिणाम यहाँ पेस्ट गर्नुहोस्:',
step_2: 'यो स्क्रिप्ट परिणाम यहाँ पेस्ट गर्नुहोस्',
script_results_placeholder: 'स्क्रिप्ट परिणाम यहाँ...',
ssms_instructions: {
button_text: 'SSMS निर्देशन',
@@ -332,6 +381,12 @@ export const ne: LanguageTranslation = {
scale_4x: '४x',
cancel: 'रद्द गर्नुहोस्',
export: 'निर्यात गर्नुहोस्',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -365,7 +420,7 @@ export const ne: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -377,7 +432,7 @@ export const ne: LanguageTranslation = {
error: {
title: 'डायाग्राम आयात गर्दा समस्या आयो',
description:
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? support@chartdb.io मा सम्पर्क गर्नुहोस्',
},
},
// TODO: Translate
@@ -405,6 +460,8 @@ export const ne: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नयाँ तालिका',
new_relationship: 'नयाँ सम्बन्ध',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -35,16 +35,15 @@ 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',
visit_website: 'Visitar ChartDB',
docs_website: 'Documentação',
join_discord: 'Junte-se a nós no Discord',
schedule_a_call: 'Fale Conosco!',
},
},
@@ -151,6 +150,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',
@@ -210,6 +211,53 @@ export const pt_BR: LanguageTranslation = {
description: 'Crie uma visualização para começar',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -236,7 +284,7 @@ export const pt_BR: LanguageTranslation = {
title: 'Importe seu Banco de Dados',
database_edition: 'Edição do Banco de Dados:',
step_1: 'Execute este script no seu banco de dados:',
step_2: 'Cole o resultado do script aqui:',
step_2: 'Cole o resultado do script aqui',
script_results_placeholder: 'Resultados do script aqui...',
ssms_instructions: {
button_text: 'Instruções do SSMS',
@@ -331,6 +379,12 @@ export const pt_BR: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Cancelar',
export: 'Exportar',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -365,7 +419,7 @@ export const pt_BR: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -377,7 +431,7 @@ export const pt_BR: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -404,6 +458,8 @@ export const pt_BR: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nova Tabela',
new_relationship: 'Novo Relacionamento',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -24,26 +24,24 @@ export const ru: LanguageTranslation = {
view: 'Вид',
show_sidebar: 'Показать боковую панель',
hide_sidebar: 'Скрыть боковую панель',
hide_cardinality: 'Скрыть множественность связи',
show_cardinality: 'Показать множественность связи',
hide_cardinality: 'Скрыть виды связи',
show_cardinality: 'Показать виды связи',
zoom_on_scroll: 'Увеличение при прокрутке',
theme: 'Тема',
show_dependencies: 'Показать зависимости',
hide_dependencies: 'Скрыть зависимости',
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
show_minimap: 'Показать мини-карту',
hide_minimap: 'Скрыть мини-карту',
},
share: {
share: 'Поделиться',
export_diagram: 'Экспорт кода диаграммы',
import_diagram: 'Импорт кода диаграммы',
backup: {
backup: 'Бэкап',
export_diagram: 'Экспорт диаграммы',
restore_diagram: 'Восстановить диаграмму',
},
help: {
help: 'Помощь',
visit_website: 'Перейти на сайт ChartDB',
docs_website: 'Документация',
join_discord: 'Присоединиться к сообществу в Discord',
schedule_a_call: 'Поговорите с нами!',
},
},
@@ -123,17 +121,17 @@ export const ru: LanguageTranslation = {
add_table: 'Добавить таблицу',
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',
clear: 'Очистить фильтр',
no_results:
'Таблицы не найдены, соответствующие вашему фильтру.',
show_list: 'Переключиться на список таблиц',
show_dbml: 'Переключиться на редактор DBML',
table: {
fields: 'Поля',
nullable: 'Может содержать NULL?',
primary_key: 'Первичный ключ,',
nullable: 'Может быть NULL?',
primary_key: 'Первичный ключ',
indexes: 'Индексы',
comments: 'Комментарии',
no_comments: 'Нет комментария',
@@ -149,6 +147,7 @@ export const ru: LanguageTranslation = {
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
character_length: 'Макс. длина',
},
index_actions: {
title: 'Атрибуты индекса',
@@ -161,7 +160,7 @@ export const ru: LanguageTranslation = {
change_schema: 'Изменить схему',
add_field: 'Добавить поле',
add_index: 'Добавить индекс',
duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Создать копию',
delete_table: 'Удалить таблицу',
},
},
@@ -178,7 +177,7 @@ export const ru: LanguageTranslation = {
relationship: {
primary: 'Основная таблица',
foreign: 'Справочная таблица',
cardinality: 'Тип множественности связи',
cardinality: 'Тип множественной связи',
delete_relationship: 'Удалить',
relationship_actions: {
title: 'Действия',
@@ -208,6 +207,54 @@ export const ru: LanguageTranslation = {
description: 'Создайте представление, чтобы начать',
},
},
areas_section: {
areas: 'Области',
add_area: 'Добавить область',
filter: 'Фильтр',
clear: 'Очистить фильтр',
no_results:
'Области не найдены, соответствующие вашему фильтру.',
area: {
area_actions: {
title: 'Действия',
edit_name: 'Изменить название',
delete_area: 'Удалить область',
},
},
empty_state: {
title: 'Нет областей',
description: 'Создайте область, чтобы начать',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -234,7 +281,7 @@ export const ru: LanguageTranslation = {
title: 'Импортируйте свою базу данных',
database_edition: 'Версия базы данных:',
step_1: 'Запустите этот скрипт в своей базе данных:',
step_2: 'Вставьте вывод скрипта сюда:',
step_2: 'Вставьте вывод скрипта сюда',
script_results_placeholder: 'Вывод скрипта здесь...',
ssms_instructions: {
button_text: 'SSMS Инструкции',
@@ -329,6 +376,12 @@ export const ru: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Отменить',
export: 'Экспортировать',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -362,7 +415,7 @@ export const ru: LanguageTranslation = {
error: {
title: 'Ошибка экспортирования диаграммы',
description:
'Что-то пошло не так. Если вам нужна помощь, напишите нам: chartdb.io@gmail.com',
'Что-то пошло не так. Если вам нужна помощь, напишите нам: support@chartdb.io',
},
},
import_diagram_dialog: {
@@ -373,21 +426,22 @@ export const ru: LanguageTranslation = {
error: {
title: 'Ошибка при импорте диаграммы',
description:
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: support@chartdb.io',
},
},
// 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',
example_title: 'Импорт DBML',
title: 'Импортировать DBML',
description: 'Импортировать схему базы данных из DBML формата.',
import: 'Импортировать',
cancel: 'Отмена',
skip_and_empty: 'Продолжить с пустой диаграммой',
show_example: 'Использовать эту схему',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
title: 'Ошибка',
description:
'Ошибка парсинга DBML. Пожалуйста проверьте синтаксис.',
},
},
relationship_type: {
@@ -400,13 +454,14 @@ export const ru: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Создать таблицу',
new_relationship: 'Создать отношение',
new_area: 'Новая область',
},
table_node_context_menu: {
edit_table: 'Изменить таблицу',
duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Создать копию',
delete_table: 'Удалить таблицу',
add_relationship: 'Add Relationship', // TODO: Translate
add_relationship: 'Добавить связь',
},
copy_to_clipboard: 'Скопировать в буфер обмена',

View File

@@ -35,16 +35,15 @@ 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: 'సహాయం',
visit_website: 'ChartDB సందర్శించండి',
docs_website: 'డాక్యుమెంటేషన్',
join_discord: 'డిస్కార్డ్‌లో మా నుంచి చేరండి',
schedule_a_call: 'మాతో మాట్లాడండి!',
},
},
@@ -151,6 +150,8 @@ export const te: LanguageTranslation = {
comments: 'వ్యాఖ్యలు',
no_comments: 'వ్యాఖ్యలు లేవు',
delete_field: 'ఫీల్డ్ తొలగించు',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ఇండెక్స్ గుణాలు',
@@ -211,6 +212,53 @@ export const te: LanguageTranslation = {
description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -237,7 +285,7 @@ export const te: LanguageTranslation = {
title: 'మీ డేటాబేస్‌ను దిగుమతి చేసుకోండి',
database_edition: 'డేటాబేస్ ఎడిషన్:',
step_1: 'ఈ స్క్రిప్ట్ను మీ డేటాబేస్‌లో అమలు చేయండి:',
step_2: 'స్క్రిప్ట్ ఫలితాన్ని ఇక్కడ పేస్ట్ చేయండి:',
step_2: 'స్క్రిప్ట్ ఫలితాన్ని ఇక్కడ పేస్ట్ చేయండి',
script_results_placeholder: 'స్క్రిప్ట్ ఫలితాలు ఇక్కడ...',
ssms_instructions: {
button_text: 'SSMS సూచనల్ని చూపించు',
@@ -332,6 +380,12 @@ export const te: LanguageTranslation = {
scale_4x: '4x',
cancel: 'రద్దు',
export: 'ఎగుమతి',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -367,7 +421,7 @@ export const te: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -380,7 +434,7 @@ export const te: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -408,6 +462,8 @@ export const te: LanguageTranslation = {
canvas_context_menu: {
new_table: 'కొత్త పట్టిక',
new_relationship: 'కొత్త సంబంధం',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -35,16 +35,15 @@ 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',
visit_website: "ChartDB'yi Ziyaret Et",
docs_website: 'Belgeleme',
join_discord: "Discord'a Katıl",
schedule_a_call: 'Bize Ulaş!',
},
},
@@ -150,6 +149,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',
@@ -210,6 +211,53 @@ export const tr: LanguageTranslation = {
description: 'Başlamak için bir görünüm oluşturun',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
zoom_in: 'Yakınlaştır',
@@ -233,7 +281,7 @@ export const tr: LanguageTranslation = {
title: 'Veritabanını İçe Aktar',
database_edition: 'Veritabanı Sürümü:',
step_1: 'Bu komut dosyasını veritabanınızda çalıştırın:',
step_2: 'Komut dosyası sonucunu buraya yapıştırın:',
step_2: 'Komut dosyası sonucunu buraya yapıştırın',
script_results_placeholder: 'Komut dosyası sonuçları burada...',
ssms_instructions: {
button_text: 'SSMS Talimatları',
@@ -324,6 +372,12 @@ export const tr: LanguageTranslation = {
scale_4x: '4x',
cancel: 'İptal',
export: 'Dışa Aktar',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
title: 'Şema Seç',
@@ -355,7 +409,7 @@ export const tr: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -367,7 +421,7 @@ export const tr: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -393,6 +447,8 @@ export const tr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Yeni Tablo',
new_relationship: 'Yeni İlişki',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
edit_table: 'Tabloyu Düzenle',

View File

@@ -33,16 +33,15 @@ export const uk: LanguageTranslation = {
show_minimap: 'Показати мінімапу',
hide_minimap: 'Приховати мінімапу',
},
share: {
share: 'Поширити',
backup: {
backup: 'Резервне копіювання',
export_diagram: 'Експорт діаграми',
import_diagram: 'Імпорт діаграми',
restore_diagram: 'Відновити діаграму',
},
help: {
help: 'Довідка',
visit_website: 'Сайт ChartDB',
docs_website: 'Документація',
join_discord: 'Приєднуйтесь до нас в Діскорд',
schedule_a_call: 'Забронювати зустріч!',
},
},
@@ -149,6 +148,8 @@ export const uk: LanguageTranslation = {
comments: 'Коментарі',
no_comments: 'Немає коментарів',
delete_field: 'Видалити поле',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Атрибути індексу',
@@ -208,6 +209,53 @@ export const uk: LanguageTranslation = {
description: 'Створіть подання, щоб почати',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -234,7 +282,7 @@ export const uk: LanguageTranslation = {
title: 'Імпортуйте вашу базу даних',
database_edition: 'Варіант бази даних:',
step_1: 'Запустіть цей сценарій у своїй базі даних:',
step_2: 'Вставте сюди результат сценарію:',
step_2: 'Вставте сюди результат сценарію',
script_results_placeholder: 'Результати сценарію має бути тут…',
ssms_instructions: {
button_text: 'SSMS Інструкції',
@@ -329,6 +377,12 @@ export const uk: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Скасувати',
export: 'Експортувати',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -361,7 +415,7 @@ export const uk: LanguageTranslation = {
error: {
title: 'Помилка експорут діаграми',
description:
'Щось пішло не так. Потрібна допомога? chartdb.io@gmail.com',
'Щось пішло не так. Потрібна допомога? support@chartdb.io',
},
},
import_diagram_dialog: {
@@ -372,7 +426,7 @@ export const uk: LanguageTranslation = {
error: {
title: 'Помилка імпорту діаграми',
description:
'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? chartdb.io@gmail.com',
'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? support@chartdb.io',
},
},
// TODO: Translate
@@ -399,6 +453,8 @@ export const uk: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Нова таблиця',
new_relationship: 'Новий звʼязок',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -34,16 +34,15 @@ 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',
visit_website: 'Truy cập ChartDB',
docs_website: 'Tài liệu',
join_discord: 'Tham gia Discord',
schedule_a_call: 'Trò chuyện cùng chúng tôi!',
},
},
@@ -150,6 +149,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',
@@ -209,6 +210,53 @@ export const vi: LanguageTranslation = {
description: 'Tạo bảng xem phụ thuộc để bắt đầu',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -235,7 +283,7 @@ export const vi: LanguageTranslation = {
title: 'Nhập cơ sở dữ liệu của bạn',
database_edition: 'Loại:',
step_1: 'Chạy lệnh này trong cơ sở dữ liệu của bạn:',
step_2: 'Dán kết quả vào đây:',
step_2: 'Dán kết quả vào đây',
script_results_placeholder: 'Kết quả...',
ssms_instructions: {
button_text: 'Hướng dẫn SSMS',
@@ -328,6 +376,12 @@ export const vi: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Hủy',
export: 'Xuất',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -361,7 +415,7 @@ export const vi: LanguageTranslation = {
error: {
title: 'Lỗi khi xuất sơ đồ',
description:
'Có gì đó không ổn. Cần trợ giúp? chartdb.io@gmail.com',
'Có gì đó không ổn. Cần trợ giúp? support@chartdb.io',
},
},
@@ -373,7 +427,7 @@ export const vi: LanguageTranslation = {
error: {
title: 'Lỗi khi nhập sơ đồ',
description:
'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',
'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? support@chartdb.io',
},
},
// TODO: Translate
@@ -400,6 +454,8 @@ export const vi: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Tạo bảng mới',
new_relationship: 'Tạo quan hệ mới',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -34,16 +34,15 @@ 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: '帮助',
visit_website: '访问 ChartDB',
docs_website: '文档',
join_discord: '在 Discord 上加入我们',
schedule_a_call: '和我们交流!',
},
},
@@ -147,6 +146,8 @@ export const zh_CN: LanguageTranslation = {
comments: '注释',
no_comments: '空',
delete_field: '删除字段',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '索引属性',
@@ -206,6 +207,53 @@ export const zh_CN: LanguageTranslation = {
description: '创建视图以开始',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -231,7 +279,7 @@ export const zh_CN: LanguageTranslation = {
title: '导入您的数据库',
database_edition: '数据库类型:',
step_1: '在您的数据库中执行以下脚本:',
step_2: '将结果粘贴于此',
step_2: '将结果粘贴于此',
script_results_placeholder: '结果...',
ssms_instructions: {
button_text: 'SSMS 说明',
@@ -325,6 +373,12 @@ export const zh_CN: LanguageTranslation = {
scale_4x: '4x',
cancel: '取消',
export: '导出',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -357,7 +411,7 @@ export const zh_CN: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -369,7 +423,7 @@ export const zh_CN: LanguageTranslation = {
error: {
title: '导入关系图时出错',
description:
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 support@chartdb.io',
},
},
// TODO: Translate
@@ -396,6 +450,8 @@ export const zh_CN: LanguageTranslation = {
canvas_context_menu: {
new_table: '新建表',
new_relationship: '新建关系',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -34,16 +34,15 @@ 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: '幫助',
visit_website: '訪問 ChartDB 網站',
docs_website: '文件',
join_discord: '加入 Discord',
schedule_a_call: '與我們聯絡!',
},
},
@@ -147,6 +146,8 @@ export const zh_TW: LanguageTranslation = {
comments: '註解',
no_comments: '無註解',
delete_field: '刪除欄位',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '索引屬性',
@@ -206,6 +207,53 @@ export const zh_TW: LanguageTranslation = {
description: '請建立檢視以開始',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
// TODO: Translate
custom_types_section: {
custom_types: 'Custom Types',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No custom types found matching your filter.',
empty_state: {
title: 'No custom types',
description:
'Custom types will appear here when they are available in your database',
},
custom_type: {
kind: 'Kind',
enum_values: 'Enum Values',
composite_fields: 'Fields',
no_fields: 'No fields defined',
field_name_placeholder: 'Field name',
field_type_placeholder: 'Select type',
add_field: 'Add Field',
custom_type_actions: {
title: 'Actions',
delete_custom_type: 'Delete',
},
delete_custom_type: 'Delete Type',
},
},
},
toolbar: {
@@ -231,7 +279,7 @@ export const zh_TW: LanguageTranslation = {
title: '匯入資料庫',
database_edition: '資料庫版本:',
step_1: '請在資料庫中執行以下腳本:',
step_2: '將腳本結果貼到此處:',
step_2: '將腳本結果貼到此處',
script_results_placeholder: '在此處貼上腳本結果...',
ssms_instructions: {
button_text: 'SSMS 操作步驟',
@@ -324,6 +372,12 @@ export const zh_TW: LanguageTranslation = {
scale_4x: '4x',
cancel: '取消',
export: '匯出',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -356,7 +410,7 @@ export const zh_TW: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -368,7 +422,7 @@ export const zh_TW: LanguageTranslation = {
error: {
title: '匯入圖表時發生錯誤',
description:
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 support@chartdb.io',
},
},
// TODO: Translate
@@ -395,6 +449,8 @@ export const zh_TW: LanguageTranslation = {
canvas_context_menu: {
new_table: '新建表格',
new_relationship: '新建關聯',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -1,3 +1,4 @@
import type { Area } from './domain/area';
import type { DBDependency } from './domain/db-dependency';
import type { DBField } from './domain/db-field';
import type { DBIndex } from './domain/db-index';
@@ -43,6 +44,10 @@ const generateIdsMapFromDiagram = (
idsMap.set(dependency.id, generateId());
});
diagram.areas?.forEach((area) => {
idsMap.set(area.id, generateId());
});
return idsMap;
};
@@ -119,7 +124,7 @@ export const cloneDiagram = (
} = {
generateId: defaultGenerateId,
}
): Diagram => {
): { diagram: Diagram; idsMap: Map<string, string> } => {
const { generateId } = options;
const diagramId = generateId();
@@ -193,13 +198,36 @@ export const cloneDiagram = (
(dependency): dependency is DBDependency => dependency !== null
) ?? [];
const areas: Area[] =
diagram.areas
?.map((area) => {
const id = getNewId(area.id);
if (!id) {
return null;
}
return {
...area,
id,
} satisfies Area;
})
.filter((area): area is Area => area !== null) ?? [];
return {
...diagram,
id: diagramId,
dependencies,
relationships,
tables,
createdAt: new Date(),
updatedAt: new Date(),
diagram: {
...diagram,
id: diagramId,
dependencies,
relationships,
tables,
areas,
createdAt: diagram.createdAt
? new Date(diagram.createdAt)
: new Date(),
updatedAt: diagram.updatedAt
? new Date(diagram.updatedAt)
: new Date(),
},
idsMap,
};
};

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

@@ -7,18 +7,24 @@ import { mysqlDataTypes } from './mysql-data-types';
import { postgresDataTypes } from './postgres-data-types';
import { sqlServerDataTypes } from './sql-server-data-types';
import { sqliteDataTypes } from './sqlite-data-types';
import { oracleDataTypes } from './oracle-data-types';
export interface DataType {
id: string;
name: string;
}
export interface DataTypeData extends DataType {
hasCharMaxLength?: boolean;
usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common
}
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,
@@ -27,8 +33,51 @@ export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
[DatabaseType.SQLITE]: sqliteDataTypes,
[DatabaseType.CLICKHOUSE]: clickhouseDataTypes,
[DatabaseType.COCKROACHDB]: postgresDataTypes,
[DatabaseType.ORACLE]: oracleDataTypes,
} as const;
export const sortDataTypes = (dataTypes: DataTypeData[]): DataTypeData[] => {
const types = [...dataTypes];
return types.sort((a, b) => {
// First sort by usage level (lower numbers first)
if ((a.usageLevel || 3) < (b.usageLevel || 3)) return -1;
if ((a.usageLevel || 3) > (b.usageLevel || 3)) return 1;
// Then sort alphabetically by name
return a.name.localeCompare(b.name);
});
};
export const sortedDataTypeMap: Record<DatabaseType, readonly DataTypeData[]> =
{
[DatabaseType.GENERIC]: sortDataTypes([
...dataTypeMap[DatabaseType.GENERIC],
]),
[DatabaseType.POSTGRESQL]: sortDataTypes([
...dataTypeMap[DatabaseType.POSTGRESQL],
]),
[DatabaseType.MYSQL]: sortDataTypes([
...dataTypeMap[DatabaseType.MYSQL],
]),
[DatabaseType.SQL_SERVER]: sortDataTypes([
...dataTypeMap[DatabaseType.SQL_SERVER],
]),
[DatabaseType.MARIADB]: sortDataTypes([
...dataTypeMap[DatabaseType.MARIADB],
]),
[DatabaseType.SQLITE]: sortDataTypes([
...dataTypeMap[DatabaseType.SQLITE],
]),
[DatabaseType.CLICKHOUSE]: sortDataTypes([
...dataTypeMap[DatabaseType.CLICKHOUSE],
]),
[DatabaseType.COCKROACHDB]: sortDataTypes([
...dataTypeMap[DatabaseType.COCKROACHDB],
]),
[DatabaseType.ORACLE]: sortDataTypes([
...dataTypeMap[DatabaseType.ORACLE],
]),
} as const;
const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
[DatabaseType.POSTGRESQL]: {
serial: ['integer'],
@@ -44,6 +93,7 @@ const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
[DatabaseType.SQLITE]: {},
[DatabaseType.CLICKHOUSE]: {},
[DatabaseType.COCKROACHDB]: {},
[DatabaseType.ORACLE]: {},
[DatabaseType.GENERIC]: {},
};
@@ -64,3 +114,21 @@ export function areFieldTypesCompatible(
}
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,27 +1,32 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const genericDataTypes: readonly DataType[] = [
export const genericDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
// Less common types
{ 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: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
{ name: 'decimal', id: 'decimal' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'double', id: 'double' },
{ name: 'enum', id: 'enum' },
{ name: 'float', id: 'float' },
{ name: 'int', id: 'int' },
{ name: 'json', id: 'json' },
{ name: 'numeric', id: 'numeric' },
{ name: 'real', id: 'real' },
{ name: 'set', id: 'set' },
{ name: 'smallint', id: 'smallint' },
{ name: 'text', id: 'text' },
{ 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 },
] as const;

View File

@@ -1,44 +1,44 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const mariadbDataTypes: readonly DataType[] = [
// Numeric Types
export const mariadbDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'bigint', id: 'bigint', usageLevel: 1 },
{ name: 'decimal', id: 'decimal', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
// Less common types
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
{ name: 'mediumint', id: 'mediumint' },
{ name: 'int', id: 'int' },
{ name: 'bigint', id: 'bigint' },
{ name: 'decimal', id: 'decimal' },
{ name: 'numeric', id: 'numeric' },
{ name: 'float', id: 'float' },
{ name: 'double', id: 'double' },
{ name: 'bit', id: 'bit' },
{ name: 'bool', id: 'bool' },
{ name: 'boolean', id: 'boolean' },
// Date and Time Types
{ name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'time', id: 'time' },
{ 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: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },
{ name: 'longblob', id: 'longblob' },
{ name: 'tinytext', id: 'tinytext' },
{ name: 'text', id: 'text' },
{ name: 'mediumtext', id: 'mediumtext' },
{ name: 'longtext', id: 'longtext' },
{ name: 'enum', id: 'enum' },
{ name: 'set', id: 'set' },
// Spatial Types
{ name: 'geometry', id: 'geometry' },
{ name: 'point', id: 'point' },
{ name: 'linestring', id: 'linestring' },
@@ -47,8 +47,4 @@ export const mariadbDataTypes: readonly DataType[] = [
{ name: 'multilinestring', id: 'multilinestring' },
{ name: 'multipolygon', id: 'multipolygon' },
{ name: 'geometrycollection', id: 'geometrycollection' },
// JSON Type
{ name: 'json', id: 'json' },
{ name: 'uuid', id: 'uuid' },
] as const;

View File

@@ -1,44 +1,41 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const mysqlDataTypes: readonly DataType[] = [
// Numeric Types
export const mysqlDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
// Less common types
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
{ name: 'mediumint', id: 'mediumint' },
{ name: 'int', id: 'int' },
{ name: 'bigint', id: 'bigint' },
{ name: 'decimal', id: 'decimal' },
{ name: 'numeric', id: 'numeric' },
{ name: 'float', id: 'float' },
{ name: 'double', id: 'double' },
{ name: 'bit', id: 'bit' },
{ name: 'bool', id: 'bool' },
{ name: 'boolean', id: 'boolean' },
// Date and Time Types
{ name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'time', id: 'time' },
{ name: 'year', id: 'year' },
// String Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'tinytext', id: 'tinytext' },
{ name: 'mediumtext', id: 'mediumtext' },
{ name: 'longtext', id: 'longtext' },
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },
{ name: 'longblob', id: 'longblob' },
{ name: 'tinytext', id: 'tinytext' },
{ name: 'text', id: 'text' },
{ name: 'mediumtext', id: 'mediumtext' },
{ name: 'longtext', id: 'longtext' },
{ name: 'enum', id: 'enum' },
{ name: 'set', id: 'set' },
// Spatial Types
{ name: 'time', id: 'time' },
{ name: 'year', id: 'year' },
{ name: 'geometry', id: 'geometry' },
{ name: 'point', id: 'point' },
{ name: 'linestring', id: 'linestring' },
@@ -47,7 +44,4 @@ export const mysqlDataTypes: readonly DataType[] = [
{ name: 'multilinestring', id: 'multilinestring' },
{ name: 'multipolygon', id: 'multipolygon' },
{ name: 'geometrycollection', id: 'geometrycollection' },
// JSON Type
{ name: 'json', id: 'json' },
] as const;

View File

@@ -0,0 +1,57 @@
import type { DataTypeData } from './data-types';
export const oracleDataTypes: readonly DataTypeData[] = [
// Character types
{ name: 'VARCHAR2', id: 'varchar2', usageLevel: 1, hasCharMaxLength: true },
{
name: 'NVARCHAR2',
id: 'nvarchar2',
usageLevel: 1,
hasCharMaxLength: true,
},
{ name: 'CHAR', id: 'char', usageLevel: 2, hasCharMaxLength: true },
{ name: 'NCHAR', id: 'nchar', usageLevel: 2, hasCharMaxLength: true },
{ name: 'CLOB', id: 'clob', usageLevel: 2 },
{ name: 'NCLOB', id: 'nclob', usageLevel: 2 },
// Numeric types
{ name: 'NUMBER', id: 'number', usageLevel: 1 },
{ name: 'FLOAT', id: 'float', usageLevel: 2 },
{ name: 'BINARY_FLOAT', id: 'binary_float', usageLevel: 2 },
{ name: 'BINARY_DOUBLE', id: 'binary_double', usageLevel: 2 },
// Date/Time types
{ name: 'DATE', id: 'date', usageLevel: 1 },
{ name: 'TIMESTAMP', id: 'timestamp', usageLevel: 1 },
{
name: 'TIMESTAMP WITH TIME ZONE',
id: 'timestamp_with_time_zone',
usageLevel: 2,
},
{
name: 'TIMESTAMP WITH LOCAL TIME ZONE',
id: 'timestamp_with_local_time_zone',
usageLevel: 2,
},
{
name: 'INTERVAL YEAR TO MONTH',
id: 'interval_year_to_month',
usageLevel: 2,
},
{
name: 'INTERVAL DAY TO SECOND',
id: 'interval_day_to_second',
usageLevel: 2,
},
// Large Object types
{ name: 'BLOB', id: 'blob', usageLevel: 2 },
{ name: 'BFILE', id: 'bfile', usageLevel: 2 },
// Other types
{ name: 'RAW', id: 'raw', usageLevel: 2, hasCharMaxLength: true },
{ name: 'LONG RAW', id: 'long_raw', usageLevel: 2 },
{ name: 'ROWID', id: 'rowid', usageLevel: 2 },
{ name: 'UROWID', id: 'urowid', usageLevel: 2 },
{ name: 'XMLType', id: 'xmltype', usageLevel: 2 },
] as const;

View File

@@ -1,45 +1,48 @@
import type { DataType } from './data-types';
import type { DataTypeData } from './data-types';
export const postgresDataTypes: readonly DataType[] = [
// Numeric Types
{ name: 'smallint', id: 'smallint' },
{ name: 'integer', id: 'integer' },
{ name: 'bigint', id: 'bigint' },
{ name: 'decimal', id: 'decimal' },
export const postgresDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'integer', id: 'integer', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'serial', id: 'serial', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 },
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
{
name: 'timestamp with time zone',
id: 'timestamp_with_time_zone',
usageLevel: 2,
},
// Less common types
{ name: 'numeric', id: 'numeric' },
{ name: 'real', id: 'real' },
{ name: 'double precision', id: 'double_precision' },
{ name: 'smallserial', id: 'smallserial' },
{ name: 'serial', id: 'serial' },
{ name: 'bigserial', id: 'bigserial' },
{ name: 'money', id: 'money' },
// Character Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'character varying', id: 'character_varying' },
{ name: 'text', id: 'text' },
// Binary Data Types
{ name: 'bytea', id: 'bytea' },
// Date/Time Types
{ name: 'date', id: 'date' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'timestamp with time zone', id: 'timestamp_with_time_zone' },
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
{ name: 'smallint', id: 'smallint' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{
name: 'character varying',
id: 'character_varying',
hasCharMaxLength: true,
},
{ name: 'time', id: 'time' },
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
{ name: 'time with time zone', id: 'time_with_time_zone' },
{ name: 'time without time zone', id: 'time_without_time_zone' },
{ name: 'interval', id: 'interval' },
// Boolean Type
{ name: 'boolean', id: 'boolean' },
// Enumerated Types
{ name: 'bytea', id: 'bytea' },
{ name: 'enum', id: 'enum' },
// Geometric Types
{ name: 'point', id: 'point' },
{ name: 'line', id: 'line' },
{ name: 'lseg', id: 'lseg' },
@@ -47,43 +50,22 @@ export const postgresDataTypes: readonly DataType[] = [
{ name: 'path', id: 'path' },
{ name: 'polygon', id: 'polygon' },
{ name: 'circle', id: 'circle' },
// Network Address Types
{ name: 'cidr', id: 'cidr' },
{ name: 'inet', id: 'inet' },
{ name: 'macaddr', id: 'macaddr' },
{ name: 'macaddr8', id: 'macaddr8' },
// Bit String Types
{ name: 'bit', id: 'bit' },
{ name: 'bit varying', id: 'bit_varying' },
// Text Search Types
{ name: 'tsvector', id: 'tsvector' },
{ name: 'tsquery', id: 'tsquery' },
// UUID Type
{ name: 'uuid', id: 'uuid' },
// XML Type
{ name: 'xml', id: 'xml' },
// JSON Types
{ name: 'json', id: 'json' },
{ name: 'jsonb', id: 'jsonb' },
// Array Types
{ name: 'array', id: 'array' },
// Range Types
{ name: 'int4range', id: 'int4range' },
{ name: 'int8range', id: 'int8range' },
{ name: 'numrange', id: 'numrange' },
{ name: 'tsrange', id: 'tsrange' },
{ name: 'tstzrange', id: 'tstzrange' },
{ name: 'daterange', id: 'daterange' },
// Object Identifier Types
{ name: 'oid', id: 'oid' },
{ name: 'regproc', id: 'regproc' },
{ name: 'regprocedure', id: 'regprocedure' },
@@ -95,7 +77,5 @@ export const postgresDataTypes: readonly DataType[] = [
{ name: 'regnamespace', id: 'regnamespace' },
{ name: 'regconfig', id: 'regconfig' },
{ name: 'regdictionary', id: 'regdictionary' },
// User Defined types
{ name: 'user-defined', id: 'user-defined' },
] as const;

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